Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### Fixed

- Session tree construction no longer freezes on sessions with duplicate entry IDs; `getTree()` builds one edge per unique node instead of re-linking the same node for every persisted row ([#1247](https://github.com/code-yeongyu/senpi/issues/1247)).

- Multi-session RPC hosts launched with `SENPI_RPC_CLIENT_CAPABILITIES` (for example `extension_events`) now apply those capabilities to connection-owned session bindings when the client never sends `set_client_info`. Previously the launch capabilities were advertised through `get_protocol_info` but dropped at binding creation, so undeclared clients received no `extension_event` frames (breaking downstream task/DAG/monitor liveness in omo-desktop-app). An explicit `set_client_info` declaration, including an empty capability list, still overrides the launch default.

- RPC `abort` acknowledgements are now sent immediately after the abort signal is dispatched instead of waiting for full session quiescence, preventing desktop stop requests from hitting their 10-second bounded-ack timeout under host load.
Expand Down
18 changes: 18 additions & 0 deletions packages/coding-agent/src/core/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# changes

## 2026-09-01 - Bound session tree construction with duplicate entry IDs

### What changed

- `packages/coding-agent/src/core/session-manager.ts`: `SessionManager.getTree()` now links each unique entry ID once, while preserving the existing final-entry-wins index semantics for repeated persisted rows.

### Why

- Duplicate entry IDs caused the same `SessionTreeNode` object to be appended repeatedly to a parent's children. Repeated duplicate edges along a branch multiplied the later traversal work and could freeze the interactive session tree before it rendered.

### Why an extension could not handle it

- Persisted session indexing and tree construction are core `SessionManager` responsibilities below the extension API. An extension cannot replace the synchronous tree returned to interactive, RPC, and other consumers.

### Expected merge conflict zones

- LOW: the edge-construction loop in `packages/coding-agent/src/core/session-manager.ts`.

## 2026-08-31 - Session activity contract for host occupancy decisions

### What changed
Expand Down
7 changes: 4 additions & 3 deletions packages/coding-agent/src/core/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1568,9 +1568,10 @@ export class SessionManager {
nodeMap.set(entry.id, { entry, children: [], label, labelTimestamp });
}

// Build tree
for (const entry of entries) {
const node = nodeMap.get(entry.id)!;
// Build one edge per unique ID. Persisted sessions can contain repeated rows,
// and nodeMap intentionally keeps the final entry for each ID.
for (const node of nodeMap.values()) {
const { entry } = node;
if (entry.parentId === null || entry.parentId === entry.id) {
roots.push(node);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { type CustomEntry, SessionManager } from "../../../src/core/session-manager.ts";

function customEntry(id: string, parentId: string | null, revision: number): CustomEntry {
return {
type: "custom",
customType: "duplicate-id-regression",
data: { revision },
id,
parentId,
timestamp: `2026-09-01T00:00:0${revision}.000Z`,
};
}

describe("issue #1247 duplicate session entry IDs", () => {
it("builds one tree node per entry ID when persisted rows repeat", () => {
// Given: a persisted-style chain where the same child ID appears twice.
const session = SessionManager.inMemory();
session.appendEntry(customEntry("root", null, 0));
session.appendEntry(customEntry("child", "root", 1));
session.appendEntry(customEntry("child", "root", 2));
session.appendEntry(customEntry("grandchild", "child", 3));

// When: the session tree is constructed.
const tree = session.getTree();

// Then: the last row for the duplicate ID wins without adding duplicate edges.
expect(tree).toHaveLength(1);
expect(tree[0]?.entry.id).toBe("root");
expect(tree[0]?.children.map((node) => node.entry.id)).toEqual(["child"]);
expect(tree[0]?.children[0]?.entry).toMatchObject({
id: "child",
data: { revision: 2 },
});
expect(tree[0]?.children[0]?.children.map((node) => node.entry.id)).toEqual(["grandchild"]);
});
});
Loading