@cjhyy/code-shell-core 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/arena/arena.d.ts +2 -0
  2. package/dist/arena/arena.js +30 -1
  3. package/dist/arena/phases/adjudication.d.ts +2 -1
  4. package/dist/arena/phases/adjudication.js +2 -1
  5. package/dist/arena/phases/build-consensus.d.ts +2 -1
  6. package/dist/arena/phases/build-consensus.js +3 -1
  7. package/dist/arena/phases/cross-review.d.ts +3 -1
  8. package/dist/arena/phases/cross-review.js +8 -2
  9. package/dist/arena/phases/debate-rounds.d.ts +2 -1
  10. package/dist/arena/phases/debate-rounds.js +4 -2
  11. package/dist/arena/phases/participant-research.d.ts +2 -1
  12. package/dist/arena/phases/participant-research.js +3 -1
  13. package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
  14. package/dist/arena/phases/planning-detail-expansion.js +2 -1
  15. package/dist/arena/planner.d.ts +2 -1
  16. package/dist/arena/planner.js +2 -1
  17. package/dist/arena/types.d.ts +5 -1
  18. package/dist/cli/agent-server-tcp.js +13 -2
  19. package/dist/context/manager.d.ts +3 -3
  20. package/dist/context/manager.js +6 -6
  21. package/dist/context/token-counter.js +5 -3
  22. package/dist/context/tool-result-storage.d.ts +6 -0
  23. package/dist/context/tool-result-storage.js +25 -4
  24. package/dist/credentials/access.d.ts +10 -1
  25. package/dist/credentials/access.js +76 -2
  26. package/dist/credentials/index.d.ts +3 -2
  27. package/dist/credentials/index.js +2 -1
  28. package/dist/credentials/oauth.d.ts +6 -1
  29. package/dist/credentials/oauth.js +66 -1
  30. package/dist/credentials/store.d.ts +1 -1
  31. package/dist/credentials/store.js +16 -7
  32. package/dist/credentials/types.d.ts +38 -1
  33. package/dist/credentials/types.js +16 -1
  34. package/dist/engine/engine.d.ts +61 -32
  35. package/dist/engine/engine.js +289 -55
  36. package/dist/engine/goal.d.ts +5 -3
  37. package/dist/engine/session-title.d.ts +2 -1
  38. package/dist/engine/session-title.js +4 -1
  39. package/dist/engine/turn-loop.d.ts +18 -0
  40. package/dist/engine/turn-loop.js +202 -27
  41. package/dist/engine/types.d.ts +6 -2
  42. package/dist/git/worktree/crud.d.ts +3 -0
  43. package/dist/git/worktree/crud.js +32 -3
  44. package/dist/git/worktree/git-exec.d.ts +2 -2
  45. package/dist/git/worktree/git-exec.js +47 -11
  46. package/dist/git/worktree/query.d.ts +8 -7
  47. package/dist/git/worktree/query.js +27 -20
  48. package/dist/hooks/events.d.ts +3 -0
  49. package/dist/hooks/events.js +0 -3
  50. package/dist/hooks/goal-stop-hook.d.ts +14 -4
  51. package/dist/hooks/goal-stop-hook.js +626 -71
  52. package/dist/hooks/registry.js +3 -0
  53. package/dist/hooks/shell-runner.d.ts +12 -1
  54. package/dist/hooks/shell-runner.js +160 -9
  55. package/dist/index.d.ts +7 -6
  56. package/dist/index.js +6 -5
  57. package/dist/llm/client-base.js +12 -10
  58. package/dist/llm/types.d.ts +12 -5
  59. package/dist/plugins/pluginCommandHook.d.ts +4 -4
  60. package/dist/plugins/pluginCommandHook.js +111 -13
  61. package/dist/protocol/chat-session-manager.d.ts +13 -2
  62. package/dist/protocol/chat-session-manager.js +90 -18
  63. package/dist/protocol/chat-session.d.ts +12 -0
  64. package/dist/protocol/chat-session.js +30 -5
  65. package/dist/protocol/client.d.ts +5 -2
  66. package/dist/protocol/client.js +22 -1
  67. package/dist/protocol/server.d.ts +25 -11
  68. package/dist/protocol/server.js +290 -72
  69. package/dist/protocol/types.d.ts +34 -2
  70. package/dist/protocol/types.js +2 -0
  71. package/dist/services/dream-consolidation.d.ts +3 -0
  72. package/dist/services/dream-consolidation.js +4 -1
  73. package/dist/services/index.d.ts +1 -1
  74. package/dist/services/index.js +1 -1
  75. package/dist/services/oauth.d.ts +34 -10
  76. package/dist/services/oauth.js +233 -98
  77. package/dist/session/session-manager.d.ts +35 -6
  78. package/dist/session/session-manager.js +362 -26
  79. package/dist/session/transcript.d.ts +30 -1
  80. package/dist/session/transcript.js +119 -4
  81. package/dist/tool-system/builtin/agent.js +5 -1
  82. package/dist/tool-system/builtin/arena.js +1 -0
  83. package/dist/tool-system/builtin/background-jobs.d.ts +8 -5
  84. package/dist/tool-system/builtin/background-jobs.js +15 -11
  85. package/dist/tool-system/builtin/background-work.d.ts +6 -1
  86. package/dist/tool-system/builtin/background-work.js +5 -1
  87. package/dist/tool-system/builtin/bash.d.ts +3 -5
  88. package/dist/tool-system/builtin/bash.js +10 -5
  89. package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
  90. package/dist/tool-system/builtin/edit.d.ts +2 -1
  91. package/dist/tool-system/builtin/edit.js +12 -4
  92. package/dist/tool-system/builtin/generate-video.d.ts +4 -0
  93. package/dist/tool-system/builtin/generate-video.js +138 -21
  94. package/dist/tool-system/builtin/glob.d.ts +2 -1
  95. package/dist/tool-system/builtin/glob.js +28 -3
  96. package/dist/tool-system/builtin/grep.d.ts +1 -0
  97. package/dist/tool-system/builtin/grep.js +82 -17
  98. package/dist/tool-system/builtin/index.d.ts +25 -11
  99. package/dist/tool-system/builtin/index.js +45 -2
  100. package/dist/tool-system/builtin/video-providers.d.ts +12 -15
  101. package/dist/tool-system/builtin/video-providers.js +1 -0
  102. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  103. package/dist/tool-system/builtin/web-fetch.js +44 -3
  104. package/dist/tool-system/builtin/worktree.js +25 -7
  105. package/dist/tool-system/builtin/write.d.ts +2 -1
  106. package/dist/tool-system/builtin/write.js +14 -4
  107. package/dist/tool-system/context.d.ts +17 -5
  108. package/dist/tool-system/executor.js +24 -8
  109. package/dist/tool-system/mcp-manager.d.ts +6 -0
  110. package/dist/tool-system/mcp-manager.js +55 -5
  111. package/dist/tool-system/path-policy.d.ts +19 -0
  112. package/dist/tool-system/path-policy.js +62 -1
  113. package/dist/tool-system/permission.d.ts +43 -3
  114. package/dist/tool-system/permission.js +383 -30
  115. package/dist/tool-system/registry.d.ts +3 -2
  116. package/dist/tool-system/registry.js +52 -34
  117. package/dist/types.d.ts +15 -0
  118. package/package.json +1 -1
@@ -15,16 +15,24 @@ export interface ChatSessionManagerOptions {
15
15
  maxSessions?: number;
16
16
  idleTtlMs?: number;
17
17
  }
18
+ export declare const CLOSED_CHAT_SESSION_TOMBSTONE_LIMIT = 4096;
18
19
  export declare class ChatSessionManager {
19
20
  private readonly sessions;
21
+ private readonly closingSessions;
22
+ private readonly closedSessions;
23
+ private readonly sessionGeneration;
20
24
  readonly runtime: EngineRuntime;
21
25
  private readonly factory;
22
26
  private readonly maxSessions;
23
27
  private readonly idleTtlMs;
24
28
  private sweeper;
25
29
  constructor(opts: ChatSessionManagerOptions);
26
- getOrCreate(sessionId: string, slice: EngineConfigSlice): ChatSession;
30
+ getOrCreate(sessionId: string, slice: EngineConfigSlice): Promise<ChatSession>;
31
+ private getOrCreateNow;
27
32
  get(sessionId: string): ChatSession | undefined;
33
+ isClosing(sessionId: string): boolean;
34
+ isClosed(sessionId: string): boolean;
35
+ isUnavailable(sessionId: string): boolean;
28
36
  sessionExistsOnDisk(sessionId: string, slice: EngineConfigSlice): boolean;
29
37
  /**
30
38
  * Iterate every live session once. Public iterator so callers (e.g. the
@@ -33,7 +41,9 @@ export declare class ChatSessionManager {
33
41
  * the values so a callback that closes a session can't perturb the walk.
34
42
  */
35
43
  forEachSession(fn: (s: ChatSession) => void): void;
36
- close(sessionId: string): void;
44
+ close(sessionId: string): Promise<void>;
45
+ private closeSession;
46
+ private rememberClosedSession;
37
47
  closeAll(): void;
38
48
  /**
39
49
  * Await-able shutdown. Cancels every session and reaps every background
@@ -46,4 +56,5 @@ export declare class ChatSessionManager {
46
56
  startIdleSweeper(intervalMs?: number): void;
47
57
  stopIdleSweeper(): void;
48
58
  private unregisterMcpOwner;
59
+ private engineSessionManager;
49
60
  }
@@ -7,8 +7,12 @@ import { clearInjectCredentialSessionAllow } from "../credentials/inject-credent
7
7
  import { logger } from "../logging/logger.js";
8
8
  import { clearSessionPathApprovals, openSessionPathApprovals } from "../tool-system/path-policy.js";
9
9
  import { clearInteractiveApprovalSession, openInteractiveApprovalSession, } from "../tool-system/permission.js";
10
+ export const CLOSED_CHAT_SESSION_TOMBSTONE_LIMIT = 4096;
10
11
  export class ChatSessionManager {
11
12
  sessions = new Map();
13
+ closingSessions = new Map();
14
+ closedSessions = new Set();
15
+ sessionGeneration = new Map();
12
16
  runtime;
13
17
  factory;
14
18
  maxSessions;
@@ -20,22 +24,21 @@ export class ChatSessionManager {
20
24
  this.maxSessions = opts.maxSessions ?? 16;
21
25
  this.idleTtlMs = opts.idleTtlMs ?? 30 * 60 * 1000;
22
26
  }
23
- getOrCreate(sessionId, slice) {
27
+ async getOrCreate(sessionId, slice) {
28
+ const closing = this.closingSessions.get(sessionId);
29
+ if (closing) {
30
+ await closing;
31
+ }
32
+ return this.getOrCreateNow(sessionId, slice);
33
+ }
34
+ getOrCreateNow(sessionId, slice) {
24
35
  openSessionPathApprovals(sessionId);
25
36
  openInteractiveApprovalSession(sessionId);
26
37
  const existing = this.sessions.get(sessionId);
27
38
  if (existing) {
28
39
  existing.lastActivityAt = Date.now();
29
- // Re-apply the per-send permission mode so a pill change on an
30
- // already-running session takes effect on the NEXT turn. Without this the
31
- // engine kept whatever mode it was first created with — changing the pill
32
- // on a resumed session was silently ignored at enforcement time (#11
33
- // secondary: "档位 doesn't take effect after first send"). setPermissionMode
34
- // reconfigures the live permission backend, so it's safe between turns.
35
- if (slice.permissionMode &&
36
- typeof existing.engine.getPermissionMode === "function" &&
37
- existing.engine.getPermissionMode() !== slice.permissionMode) {
38
- existing.engine.setPermissionMode(slice.permissionMode);
40
+ if (slice.permissionMode && existing.engine.getPermissionMode?.() !== slice.permissionMode) {
41
+ existing.engine.setPermissionMode?.(slice.permissionMode);
39
42
  }
40
43
  return existing;
41
44
  }
@@ -46,12 +49,27 @@ export class ChatSessionManager {
46
49
  }
47
50
  const engine = this.factory(slice);
48
51
  const session = new ChatSession({ id: sessionId, engine });
52
+ const sessionManager = this.engineSessionManager(engine);
53
+ const generation = sessionManager?.registerSessionGeneration(sessionId) ?? 1;
54
+ this.sessionGeneration.set(sessionId, generation);
49
55
  this.sessions.set(sessionId, session);
56
+ // A direct user run is an explicit resume/open and may clear the tombstone.
57
+ // Background wakeups must check isUnavailable() before reaching this path.
58
+ this.closedSessions.delete(sessionId);
50
59
  return session;
51
60
  }
52
61
  get(sessionId) {
53
62
  return this.sessions.get(sessionId);
54
63
  }
64
+ isClosing(sessionId) {
65
+ return this.closingSessions.has(sessionId);
66
+ }
67
+ isClosed(sessionId) {
68
+ return this.closedSessions.has(sessionId);
69
+ }
70
+ isUnavailable(sessionId) {
71
+ return this.isClosing(sessionId) || this.isClosed(sessionId);
72
+ }
55
73
  sessionExistsOnDisk(sessionId, slice) {
56
74
  const existing = this.sessions.get(sessionId);
57
75
  if (existing)
@@ -70,16 +88,62 @@ export class ChatSessionManager {
70
88
  fn(s);
71
89
  }
72
90
  close(sessionId) {
91
+ return this.closeSession(sessionId, true);
92
+ }
93
+ closeSession(sessionId, markClosed) {
94
+ const alreadyClosing = this.closingSessions.get(sessionId);
95
+ if (alreadyClosing)
96
+ return alreadyClosing;
73
97
  const s = this.sessions.get(sessionId);
74
- if (!s)
75
- return;
98
+ if (!s) {
99
+ if (markClosed)
100
+ this.rememberClosedSession(sessionId);
101
+ return Promise.resolve();
102
+ }
103
+ const sessionManager = this.engineSessionManager(s.engine);
104
+ const generation = this.sessionGeneration.get(sessionId) ?? 0;
105
+ const invalidated = sessionManager?.incrementSessionGeneration(sessionId) ?? generation + 1;
106
+ this.sessionGeneration.set(sessionId, invalidated);
76
107
  s.cancel();
77
108
  clearSessionPathApprovals(sessionId);
78
109
  clearInteractiveApprovalSession(sessionId);
79
110
  clearCredentialSessionAllow(sessionId);
80
111
  clearInjectCredentialSessionAllow(sessionId);
81
- this.unregisterMcpOwner(s);
82
- this.sessions.delete(sessionId);
112
+ const finishClose = () => {
113
+ this.unregisterMcpOwner(s);
114
+ if (this.sessions.get(sessionId) === s)
115
+ this.sessions.delete(sessionId);
116
+ if (markClosed)
117
+ this.rememberClosedSession(sessionId);
118
+ else
119
+ this.closedSessions.delete(sessionId);
120
+ this.sessionGeneration.delete(sessionId);
121
+ };
122
+ if (!s.isBusy()) {
123
+ finishClose();
124
+ return Promise.resolve();
125
+ }
126
+ const closing = (async () => {
127
+ try {
128
+ await s.settled;
129
+ finishClose();
130
+ }
131
+ finally {
132
+ this.closingSessions.delete(sessionId);
133
+ }
134
+ })();
135
+ this.closingSessions.set(sessionId, closing);
136
+ return closing;
137
+ }
138
+ rememberClosedSession(sessionId) {
139
+ this.closedSessions.delete(sessionId);
140
+ this.closedSessions.add(sessionId);
141
+ while (this.closedSessions.size > CLOSED_CHAT_SESSION_TOMBSTONE_LIMIT) {
142
+ const oldest = this.closedSessions.values().next().value;
143
+ if (oldest === undefined)
144
+ break;
145
+ this.closedSessions.delete(oldest);
146
+ }
83
147
  }
84
148
  closeAll() {
85
149
  // Fire-and-forget variant for callers that can't await (e.g. AgentServer
@@ -95,8 +159,7 @@ export class ChatSessionManager {
95
159
  * immediately afterward (the TUI REPL) doesn't orphan detached dev servers.
96
160
  */
97
161
  async closeAllAsync() {
98
- for (const id of [...this.sessions.keys()])
99
- this.close(id);
162
+ await Promise.all([...this.sessions.keys()].map((id) => this.close(id)));
100
163
  // App/worker shutdown — reap every background shell so a detached
101
164
  // `npm run dev` doesn't outlive the process as an orphan holding a port
102
165
  // (design §6 / §难点1). NOTE: deliberately NOT in close()/sweepIdle() —
@@ -120,7 +183,7 @@ export class ChatSessionManager {
120
183
  continue;
121
184
  if (backgroundJobRegistry.hasRunningForSession(id))
122
185
  continue;
123
- this.close(id);
186
+ void this.closeSession(id, false);
124
187
  }
125
188
  }
126
189
  startIdleSweeper(intervalMs = 60_000) {
@@ -146,4 +209,13 @@ export class ChatSessionManager {
146
209
  });
147
210
  });
148
211
  }
212
+ engineSessionManager(engine) {
213
+ const candidate = engine;
214
+ const manager = candidate.getSessionManager?.();
215
+ if (typeof manager?.registerSessionGeneration !== "function" ||
216
+ typeof manager.incrementSessionGeneration !== "function") {
217
+ return undefined;
218
+ }
219
+ return manager;
220
+ }
149
221
  }
@@ -1,7 +1,9 @@
1
1
  import type { Engine, EngineResult } from "../engine/engine.js";
2
2
  import type { ModelEntry } from "../llm/model-pool.js";
3
3
  import type { StreamEvent } from "../types.js";
4
+ import type { PermissionMode } from "../types.js";
4
5
  import type { InputAttachmentMeta } from "./types.js";
6
+ import type { ApprovalRouter } from "../tool-system/permission.js";
5
7
  export interface ChatSessionOptions {
6
8
  id: string;
7
9
  engine: Engine;
@@ -23,6 +25,12 @@ export interface TurnOpts {
23
25
  clientMessageId?: string;
24
26
  /** Structured input attachments for this turn. */
25
27
  attachments?: InputAttachmentMeta[];
28
+ /** Permission mode snapshot for this queued turn only. */
29
+ permissionMode?: PermissionMode;
30
+ /** Plan-mode snapshot for this queued turn only. */
31
+ planMode?: boolean;
32
+ /** Connection owner router for permission prompts in this turn. */
33
+ approvalRouter?: ApprovalRouter;
26
34
  }
27
35
  /**
28
36
  * One ChatSession per UI chat tab. Owns a single Engine, an AbortController
@@ -69,6 +77,8 @@ export declare class ChatSession {
69
77
  * (the user is engaging again) so normal wakeups resume working afterward.
70
78
  */
71
79
  private cancelledSinceLastTurn;
80
+ private settlePromise;
81
+ private resolveSettled;
72
82
  constructor(opts: ChatSessionOptions);
73
83
  enqueueTurn(task: string, opts: TurnOpts): Promise<EngineResult>;
74
84
  /**
@@ -88,6 +98,8 @@ export declare class ChatSession {
88
98
  */
89
99
  cancel(): void;
90
100
  isBusy(): boolean;
101
+ /** Resolves after the active engine.run() has completed its pump finally. */
102
+ get settled(): Promise<void>;
91
103
  /**
92
104
  * Extend the in-flight goal run's turn/budget ceilings (TODO 3.1). Delegates
93
105
  * to the engine, which forwards to the active TurnLoop. Returns null when no
@@ -44,6 +44,8 @@ export class ChatSession {
44
44
  * (the user is engaging again) so normal wakeups resume working afterward.
45
45
  */
46
46
  cancelledSinceLastTurn = false;
47
+ settlePromise = Promise.resolve();
48
+ resolveSettled = null;
47
49
  constructor(opts) {
48
50
  this.id = opts.id;
49
51
  this.engine = opts.engine;
@@ -94,6 +96,10 @@ export class ChatSession {
94
96
  isBusy() {
95
97
  return this.active !== null;
96
98
  }
99
+ /** Resolves after the active engine.run() has completed its pump finally. */
100
+ get settled() {
101
+ return this.settlePromise;
102
+ }
97
103
  /**
98
104
  * Extend the in-flight goal run's turn/budget ceilings (TODO 3.1). Delegates
99
105
  * to the engine, which forwards to the active TurnLoop. Returns null when no
@@ -158,6 +164,9 @@ export class ChatSession {
158
164
  if (!next)
159
165
  return;
160
166
  this.active = next;
167
+ this.settlePromise = new Promise((resolve) => {
168
+ this.resolveSettled = resolve;
169
+ });
161
170
  this.cancelledActive = false;
162
171
  this.controller = new AbortController();
163
172
  try {
@@ -171,6 +180,9 @@ export class ChatSession {
171
180
  injected: next.opts.injected,
172
181
  clientMessageId: next.opts.clientMessageId,
173
182
  attachments: next.opts.attachments,
183
+ permissionMode: next.opts.permissionMode,
184
+ planMode: next.opts.planMode,
185
+ approvalRouter: next.opts.approvalRouter,
174
186
  });
175
187
  this.lastActivityAt = Date.now();
176
188
  next.resolve(result);
@@ -195,19 +207,32 @@ export class ChatSession {
195
207
  }
196
208
  }
197
209
  finally {
210
+ // close() must never be held hostage by run-boundary cleanup. Resolve
211
+ // before model switching or any other operation that may throw.
212
+ this.resolveSettled?.();
213
+ this.resolveSettled = null;
198
214
  this.active = null;
199
215
  this.controller = null;
200
216
  this.cancelledActive = false;
201
217
  // Run boundary: apply any model switch that was deferred because it was
202
218
  // requested mid-run. Done here (not in pump) so it still applies when
203
219
  // no further turn is queued.
204
- if (this.pendingModel !== null) {
205
- this.applyModelSwitch(this.pendingModel);
220
+ try {
221
+ if (this.pendingModel !== null) {
222
+ this.applyModelSwitch(this.pendingModel);
223
+ }
224
+ }
225
+ catch {
226
+ // requestModelSwitch already returned when the switch was queued. A
227
+ // run-boundary persistence failure must not become an unhandled pump
228
+ // rejection or prevent the session from settling/continuing.
229
+ }
230
+ finally {
206
231
  this.pendingModel = null;
232
+ // Drain the next turn even if deferred model switching failed.
233
+ if (this.queue.length > 0)
234
+ void this.pump();
207
235
  }
208
- // Drain the next turn if one is waiting.
209
- if (this.queue.length > 0)
210
- void this.pump();
211
236
  }
212
237
  }
213
238
  }
@@ -11,9 +11,9 @@
11
11
  * const result = await client.run("fix the bug");
12
12
  */
13
13
  import type { Transport } from "./transport.js";
14
- import { type RunParams, type RunResult, type AgentStreamEventNotification, type ConfigureParams, type QueryParams, type QueryResult, type ApprovalRequestNotification, type ApprovalResolvedNotification } from "./types.js";
14
+ import { type RunParams, type RunResult, type ForkSessionParams, type ForkSessionResult, type AgentStreamEventNotification, type ConfigureParams, type QueryParams, type QueryResult, type ApprovalRequestNotification, type ApprovalResolvedNotification } from "./types.js";
15
15
  import type { ApprovalRequest, ApprovalResult, PermissionMode, BackgroundAgentCompletedEvent } from "../types.js";
16
- export type ApprovalRequestMeta = Pick<ApprovalRequestNotification, "sessionId">;
16
+ export type ApprovalRequestMeta = Pick<ApprovalRequestNotification, "connectionId" | "sessionId" | "generation">;
17
17
  export type ApprovalResolvedEvent = ApprovalResolvedNotification;
18
18
  export interface AgentClientEvents {
19
19
  /** Multi-session envelope: carries sessionId + event. */
@@ -34,6 +34,8 @@ export declare class AgentClient {
34
34
  private transport;
35
35
  private emitter;
36
36
  private pendingRequests;
37
+ /** Routing tuple captured from approval notifications and echoed on approve. */
38
+ private approvalRoutes;
37
39
  /**
38
40
  * Map from a user-supplied `BackgroundAgentCompletedHandler` to the
39
41
  * internal envelope-filtering wrapper actually registered on the
@@ -68,6 +70,7 @@ export declare class AgentClient {
68
70
  * Legacy form: cancel(reason?)
69
71
  */
70
72
  cancel(sessionId?: string, reason?: string): Promise<void>;
73
+ forkSession(params: ForkSessionParams): Promise<ForkSessionResult>;
71
74
  /**
72
75
  * Clear a session's persisted active goal (CC `/goal clear`). Returns whether
73
76
  * a goal was actually cleared (false = there was none).
@@ -17,6 +17,8 @@ export class AgentClient {
17
17
  transport;
18
18
  emitter = new EventEmitter();
19
19
  pendingRequests = new Map();
20
+ /** Routing tuple captured from approval notifications and echoed on approve. */
21
+ approvalRoutes = new Map();
20
22
  /**
21
23
  * Map from a user-supplied `BackgroundAgentCompletedHandler` to the
22
24
  * internal envelope-filtering wrapper actually registered on the
@@ -78,16 +80,24 @@ export class AgentClient {
78
80
  approve(...args) {
79
81
  if (args.length === 3) {
80
82
  const [sessionId, requestId, decision] = args;
83
+ const route = this.approvalRoutes.get(requestId);
81
84
  return this.request(Methods.Approve, {
85
+ ...route,
82
86
  sessionId,
83
87
  requestId,
84
88
  decision,
89
+ }).finally(() => {
90
+ this.approvalRoutes.delete(requestId);
85
91
  });
86
92
  }
87
93
  const [requestId, decision] = args;
94
+ const route = this.approvalRoutes.get(requestId);
88
95
  return this.request(Methods.Approve, {
96
+ ...route,
89
97
  requestId,
90
98
  decision,
99
+ }).finally(() => {
100
+ this.approvalRoutes.delete(requestId);
91
101
  });
92
102
  }
93
103
  /**
@@ -99,6 +109,9 @@ export class AgentClient {
99
109
  async cancel(sessionId, reason) {
100
110
  await this.request(Methods.Cancel, { sessionId, reason });
101
111
  }
112
+ async forkSession(params) {
113
+ return this.request(Methods.ForkSession, params);
114
+ }
102
115
  /**
103
116
  * Clear a session's persisted active goal (CC `/goal clear`). Returns whether
104
117
  * a goal was actually cleared (false = there was none).
@@ -286,7 +299,14 @@ export class AgentClient {
286
299
  const request = params.request;
287
300
  if (requestId && request) {
288
301
  const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
289
- const meta = sessionId === undefined ? {} : { sessionId };
302
+ const connectionId = typeof params.connectionId === "string" ? params.connectionId : undefined;
303
+ const generation = typeof params.generation === "number" ? params.generation : undefined;
304
+ const meta = {
305
+ ...(connectionId === undefined ? {} : { connectionId }),
306
+ ...(sessionId === undefined ? {} : { sessionId }),
307
+ ...(generation === undefined ? {} : { generation }),
308
+ };
309
+ this.approvalRoutes.set(requestId, meta);
290
310
  this.emitter.emit("approvalRequest", requestId, request, meta);
291
311
  }
292
312
  break;
@@ -294,6 +314,7 @@ export class AgentClient {
294
314
  case Methods.ApprovalResolved: {
295
315
  const requestId = params.requestId;
296
316
  if (requestId) {
317
+ this.approvalRoutes.delete(requestId);
297
318
  const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
298
319
  const event = sessionId === undefined ? { requestId } : { sessionId, requestId };
299
320
  this.emitter.emit("approvalResolved", event);
@@ -16,6 +16,7 @@
16
16
  import type { Transport } from "./transport.js";
17
17
  import type { Engine } from "../engine/engine.js";
18
18
  import type { ValidatedSettings } from "../settings/schema.js";
19
+ import { type ApprovalRouter } from "../tool-system/permission.js";
19
20
  import type { ChatSessionManager } from "./chat-session-manager.js";
20
21
  export interface AgentServerOptions {
21
22
  /**
@@ -56,6 +57,11 @@ export interface AgentServerOptions {
56
57
  * service.
57
58
  */
58
59
  workspaceBridge?: boolean;
60
+ /** Stable transport connection identity. Supplying it enables strict
61
+ * connection+session+generation+requestId approval response matching. */
62
+ connectionId?: string;
63
+ /** Shared owner router; injectable for hosts/tests, process singleton by default. */
64
+ approvalRouter?: ApprovalRouter;
59
65
  }
60
66
  export declare class AgentServer {
61
67
  private readonly chatManager;
@@ -67,10 +73,17 @@ export declare class AgentServer {
67
73
  /** Disk-only active-goal reader for agent/goalGet on a non-live session. */
68
74
  private readonly readActiveGoalFromDisk;
69
75
  private readonly workspaceBridgeEnabled;
76
+ private readonly connectionId;
77
+ private readonly strictApprovalRouting;
78
+ private readonly approvalRouter;
79
+ private approvalConnectionUnregister;
80
+ private disconnected;
81
+ private readonly pendingApprovalTargets;
70
82
  /**
71
83
  * Last per-session EngineConfigSlice supplied by agent/run. Kept even after
72
84
  * idle eviction so background-completion wakeups can rebuild the ChatSession
73
- * with the same cwd/permission/trust inputs before draining the queue.
85
+ * with the same cwd/trust inputs before draining the queue. Per-turn
86
+ * permission/plan overrides deliberately do not survive into wakeup turns.
74
87
  */
75
88
  private readonly lastSliceBySession;
76
89
  /** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
@@ -114,6 +127,7 @@ export declare class AgentServer {
114
127
  * itself outlives the server (it's a process-local singleton).
115
128
  */
116
129
  private bgAgentBusUnsubscribe;
130
+ private readonly wakeupsInFlight;
117
131
  constructor(options: AgentServerOptions);
118
132
  /**
119
133
  * Wake an IDLE chatManager session that has pending background-completion
@@ -133,23 +147,18 @@ export declare class AgentServer {
133
147
  * the first drains all currently-pending items; subsequent bus events for
134
148
  * the same session find it busy (or find an empty queue) and no-op.
135
149
  *
136
- * INVARIANT (the burst-merge correctness depends on it): the merge only
137
- * holds because (a) the bus fans out to subscribers SYNCHRONOUSLY, and (b)
138
- * `enqueueTurn` sets the session's `active` (→ isBusy()===true) SYNCHRONOUSLY
139
- * before its first await. So when bus events #2..N arrive — still on the same
140
- * synchronous fan-out as #1 — they already see the session as busy and no-op.
141
- * If either path is ever made async (e.g. an await is added inside
142
- * enqueueTurn before `active` is set, or the bus starts deferring delivery),
143
- * this degrades into N concurrent wakeups for N completions. Keep both
144
- * synchronous, or replace this comment's assumption with an explicit
145
- * "wakeup in flight" guard flag.
150
+ * `wakeupsInFlight` serializes the now-async rehydrate path, so a burst of
151
+ * completion events cannot create duplicate sessions or enqueue duplicate
152
+ * wakeup turns while getOrCreate waits for a closing generation to settle.
146
153
  */
147
154
  private maybeWakeIdleSession;
155
+ private wakeIdleSession;
148
156
  private rehydrateSessionForWake;
149
157
  private sliceForWakeRehydrate;
150
158
  private rememberSessionSlice;
151
159
  private wireInteractiveSession;
152
160
  private handleRequest;
161
+ private handleForkSession;
153
162
  private handleRun;
154
163
  private handleRunMulti;
155
164
  private handleRunLegacy;
@@ -207,6 +216,7 @@ export declare class AgentServer {
207
216
  * Ask the client to approve a tool operation (legacy single-engine path).
208
217
  */
209
218
  private requestApprovalFromClient;
219
+ private approvalRouteEnvelope;
210
220
  /**
211
221
  * Per-session AskUserQuestion for the chatManager path. Resolves via the
212
222
  * SESSION's pendingApprovals (the chatManager approve handler looks there,
@@ -249,6 +259,9 @@ export declare class AgentServer {
249
259
  */
250
260
  private anyEngine;
251
261
  close(): void;
262
+ /** Release only this transport's approval ownership. Safe on socket close
263
+ * even when the ChatSessionManager is shared by other TCP connections. */
264
+ disconnect(reason?: string): void;
252
265
  private clearApprovalTimer;
253
266
  /**
254
267
  * Resolve all of a chat session's pending approvals as cancelled and clear
@@ -258,5 +271,6 @@ export declare class AgentServer {
258
271
  * AskUserQuestion does not.
259
272
  */
260
273
  private cancelSessionApprovals;
274
+ private failClosedApprovalTargets;
261
275
  private clearAllApprovalTimers;
262
276
  }