@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.
- package/dist/arena/arena.d.ts +2 -0
- package/dist/arena/arena.js +30 -1
- package/dist/arena/phases/adjudication.d.ts +2 -1
- package/dist/arena/phases/adjudication.js +2 -1
- package/dist/arena/phases/build-consensus.d.ts +2 -1
- package/dist/arena/phases/build-consensus.js +3 -1
- package/dist/arena/phases/cross-review.d.ts +3 -1
- package/dist/arena/phases/cross-review.js +8 -2
- package/dist/arena/phases/debate-rounds.d.ts +2 -1
- package/dist/arena/phases/debate-rounds.js +4 -2
- package/dist/arena/phases/participant-research.d.ts +2 -1
- package/dist/arena/phases/participant-research.js +3 -1
- package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
- package/dist/arena/phases/planning-detail-expansion.js +2 -1
- package/dist/arena/planner.d.ts +2 -1
- package/dist/arena/planner.js +2 -1
- package/dist/arena/types.d.ts +5 -1
- package/dist/cli/agent-server-tcp.js +13 -2
- package/dist/context/manager.d.ts +3 -3
- package/dist/context/manager.js +6 -6
- package/dist/context/token-counter.js +5 -3
- package/dist/context/tool-result-storage.d.ts +6 -0
- package/dist/context/tool-result-storage.js +25 -4
- package/dist/credentials/access.d.ts +10 -1
- package/dist/credentials/access.js +76 -2
- package/dist/credentials/index.d.ts +3 -2
- package/dist/credentials/index.js +2 -1
- package/dist/credentials/oauth.d.ts +6 -1
- package/dist/credentials/oauth.js +66 -1
- package/dist/credentials/store.d.ts +1 -1
- package/dist/credentials/store.js +16 -7
- package/dist/credentials/types.d.ts +38 -1
- package/dist/credentials/types.js +16 -1
- package/dist/engine/engine.d.ts +61 -32
- package/dist/engine/engine.js +289 -55
- package/dist/engine/goal.d.ts +5 -3
- package/dist/engine/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/turn-loop.d.ts +18 -0
- package/dist/engine/turn-loop.js +202 -27
- package/dist/engine/types.d.ts +6 -2
- package/dist/git/worktree/crud.d.ts +3 -0
- package/dist/git/worktree/crud.js +32 -3
- package/dist/git/worktree/git-exec.d.ts +2 -2
- package/dist/git/worktree/git-exec.js +47 -11
- package/dist/git/worktree/query.d.ts +8 -7
- package/dist/git/worktree/query.js +27 -20
- package/dist/hooks/events.d.ts +3 -0
- package/dist/hooks/events.js +0 -3
- package/dist/hooks/goal-stop-hook.d.ts +14 -4
- package/dist/hooks/goal-stop-hook.js +626 -71
- package/dist/hooks/registry.js +3 -0
- package/dist/hooks/shell-runner.d.ts +12 -1
- package/dist/hooks/shell-runner.js +160 -9
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/llm/client-base.js +12 -10
- package/dist/llm/types.d.ts +12 -5
- package/dist/plugins/pluginCommandHook.d.ts +4 -4
- package/dist/plugins/pluginCommandHook.js +111 -13
- package/dist/protocol/chat-session-manager.d.ts +13 -2
- package/dist/protocol/chat-session-manager.js +90 -18
- package/dist/protocol/chat-session.d.ts +12 -0
- package/dist/protocol/chat-session.js +30 -5
- package/dist/protocol/client.d.ts +5 -2
- package/dist/protocol/client.js +22 -1
- package/dist/protocol/server.d.ts +25 -11
- package/dist/protocol/server.js +290 -72
- package/dist/protocol/types.d.ts +34 -2
- package/dist/protocol/types.js +2 -0
- package/dist/services/dream-consolidation.d.ts +3 -0
- package/dist/services/dream-consolidation.js +4 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/oauth.d.ts +34 -10
- package/dist/services/oauth.js +233 -98
- package/dist/session/session-manager.d.ts +35 -6
- package/dist/session/session-manager.js +362 -26
- package/dist/session/transcript.d.ts +30 -1
- package/dist/session/transcript.js +119 -4
- package/dist/tool-system/builtin/agent.js +5 -1
- package/dist/tool-system/builtin/arena.js +1 -0
- package/dist/tool-system/builtin/background-jobs.d.ts +8 -5
- package/dist/tool-system/builtin/background-jobs.js +15 -11
- package/dist/tool-system/builtin/background-work.d.ts +6 -1
- package/dist/tool-system/builtin/background-work.js +5 -1
- package/dist/tool-system/builtin/bash.d.ts +3 -5
- package/dist/tool-system/builtin/bash.js +10 -5
- package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +12 -4
- package/dist/tool-system/builtin/generate-video.d.ts +4 -0
- package/dist/tool-system/builtin/generate-video.js +138 -21
- package/dist/tool-system/builtin/glob.d.ts +2 -1
- package/dist/tool-system/builtin/glob.js +28 -3
- package/dist/tool-system/builtin/grep.d.ts +1 -0
- package/dist/tool-system/builtin/grep.js +82 -17
- package/dist/tool-system/builtin/index.d.ts +25 -11
- package/dist/tool-system/builtin/index.js +45 -2
- package/dist/tool-system/builtin/video-providers.d.ts +12 -15
- package/dist/tool-system/builtin/video-providers.js +1 -0
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/web-fetch.js +44 -3
- package/dist/tool-system/builtin/worktree.js +25 -7
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +14 -4
- package/dist/tool-system/context.d.ts +17 -5
- package/dist/tool-system/executor.js +24 -8
- package/dist/tool-system/mcp-manager.d.ts +6 -0
- package/dist/tool-system/mcp-manager.js +55 -5
- package/dist/tool-system/path-policy.d.ts +19 -0
- package/dist/tool-system/path-policy.js +62 -1
- package/dist/tool-system/permission.d.ts +43 -3
- package/dist/tool-system/permission.js +383 -30
- package/dist/tool-system/registry.d.ts +3 -2
- package/dist/tool-system/registry.js +52 -34
- package/dist/types.d.ts +15 -0
- 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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
205
|
-
|
|
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).
|
package/dist/protocol/client.js
CHANGED
|
@@ -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
|
|
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/
|
|
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
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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
|
}
|