@cjhyy/code-shell-core 0.9.5 → 0.9.6
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/credentials/access.d.ts +9 -1
- package/dist/credentials/access.js +15 -4
- package/dist/credentials/store.d.ts +11 -0
- package/dist/credentials/store.js +44 -9
- package/dist/credentials/types.d.ts +4 -0
- package/dist/credentials/types.js +4 -0
- package/dist/credentials/use-credential-tool.js +12 -2
- package/dist/engine/engine-workspace-authority.js +10 -3
- package/dist/engine/engine.d.ts +3 -0
- package/dist/engine/engine.js +38 -6
- package/dist/engine/prompt-cache-diagnostics.js +10 -1
- package/dist/engine/run-goal.js +7 -3
- package/dist/engine/run-types.d.ts +16 -0
- package/dist/engine/run-workspace.js +5 -21
- package/dist/engine/turn-loop.d.ts +4 -4
- package/dist/engine/turn-loop.js +29 -9
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/links/index.d.ts +1 -0
- package/dist/links/index.js +1 -0
- package/dist/links/link-action-tool.d.ts +2 -1
- package/dist/links/link-action-tool.js +80 -44
- package/dist/links/status.d.ts +53 -0
- package/dist/links/status.js +175 -0
- package/dist/llm/prompt-cache.d.ts +35 -3
- package/dist/llm/prompt-cache.js +63 -3
- package/dist/llm/providers/openai.d.ts +3 -0
- package/dist/llm/providers/openai.js +57 -14
- package/dist/protocol/background-result-wakeup.d.ts +8 -1
- package/dist/protocol/background-result-wakeup.js +76 -38
- package/dist/protocol/chat-session-manager.d.ts +7 -1
- package/dist/protocol/chat-session-manager.js +44 -8
- package/dist/protocol/chat-session.d.ts +2 -0
- package/dist/protocol/chat-session.js +4 -1
- package/dist/protocol/server.d.ts +3 -0
- package/dist/protocol/server.js +148 -43
- package/dist/protocol/session-message-result.d.ts +12 -0
- package/dist/protocol/session-message-result.js +42 -0
- package/dist/protocol/session-message-workspace.d.ts +21 -0
- package/dist/protocol/session-message-workspace.js +57 -0
- package/dist/protocol/types.d.ts +2 -0
- package/dist/session/session-message.d.ts +17 -2
- package/dist/tool-system/browser-bridge.d.ts +18 -0
- package/dist/tool-system/builtin/agent.js +23 -9
- package/dist/tool-system/builtin/browser-tools.js +18 -1
- package/dist/tool-system/builtin/index.js +3 -3
- package/dist/tool-system/builtin/send-message-to-session.js +19 -3
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@ import { capabilitiesFor } from "../capabilities/index.js";
|
|
|
16
16
|
import { clampMaxTokens } from "../clamp-max-tokens.js";
|
|
17
17
|
import { resolveApiKey, resolveHeaders } from "../provider-auth.js";
|
|
18
18
|
import { stripVisionFromHistory } from "../strip-vision.js";
|
|
19
|
-
import { resolvePromptCachePolicy, uniquePromptCacheBreakpointIndexes, } from "../prompt-cache.js";
|
|
19
|
+
import { createPromptCacheKey, PromptCacheHistory, resolvePromptCachePolicy, uniquePromptCacheBreakpointIndexes, } from "../prompt-cache.js";
|
|
20
20
|
import { STREAM_WATCHDOG_CONFIG, StreamIdleTimeoutError } from "../stream-watchdog.js";
|
|
21
21
|
/**
|
|
22
22
|
* Extract prompt-cache counts from an OpenAI-compatible usage object.
|
|
@@ -25,8 +25,8 @@ import { STREAM_WATCHDOG_CONFIG, StreamIdleTimeoutError } from "../stream-watchd
|
|
|
25
25
|
* top-level field). Both OpenAI and OpenRouter report this.
|
|
26
26
|
* - Cache WRITES (first-time prefix ingestion) are reported by OpenRouter as
|
|
27
27
|
* `prompt_tokens_details.cache_write_tokens` (verified live 2026-07-02).
|
|
28
|
-
* OpenAI
|
|
29
|
-
* map it to `cacheCreationTokens` so the UI can show "writing cache" on the
|
|
28
|
+
* Earlier OpenAI models omit it; GPT-5.6+ reports separately billed writes.
|
|
29
|
+
* We map it to `cacheCreationTokens` so the UI can show "writing cache" on the
|
|
30
30
|
* first turn, not just hits on later turns.
|
|
31
31
|
*
|
|
32
32
|
* Returns a spreadable partial so callers omit each key entirely when the API
|
|
@@ -163,6 +163,9 @@ export async function runStreamWithWatchdog(stream, opts = {}) {
|
|
|
163
163
|
return text;
|
|
164
164
|
}
|
|
165
165
|
const MISSING_TOOL_RESULT_WIRE_TEXT = "Error: Tool execution did not complete before the conversation resumed.";
|
|
166
|
+
// Clients are recreated between Engine runs. Keep only bounded, expiring
|
|
167
|
+
// boundary hashes across those recreations; never retain conversation text.
|
|
168
|
+
const sharedPromptCacheHistory = new PromptCacheHistory();
|
|
166
169
|
/**
|
|
167
170
|
* OpenAI requires each assistant tool_calls batch to be followed immediately
|
|
168
171
|
* by exactly one role:tool message per id. Normalize at the provider boundary
|
|
@@ -210,6 +213,7 @@ function normalizeOpenAIToolMessagePairs(messages) {
|
|
|
210
213
|
}
|
|
211
214
|
export class OpenAIClient extends LLMClientBase {
|
|
212
215
|
_client = null;
|
|
216
|
+
promptCacheHistory;
|
|
213
217
|
dangerouslyAllowBrowser;
|
|
214
218
|
// Sticky override: once the endpoint tells us `max_tokens` is rejected for
|
|
215
219
|
// this model, switch to `max_completion_tokens` for the lifetime of the
|
|
@@ -228,6 +232,7 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
228
232
|
_disablePromptCacheKey = false;
|
|
229
233
|
constructor(config, defaults, runtimeOptions = {}) {
|
|
230
234
|
super(config, defaults);
|
|
235
|
+
this.promptCacheHistory = runtimeOptions.promptCacheHistory ?? sharedPromptCacheHistory;
|
|
231
236
|
this.dangerouslyAllowBrowser = runtimeOptions.dangerouslyAllowBrowser === true;
|
|
232
237
|
}
|
|
233
238
|
initClient() {
|
|
@@ -305,8 +310,35 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
305
310
|
// Per-call reasoning wins; otherwise fall back to provider default
|
|
306
311
|
// (settings.providers[].reasoning, threaded through LLMConfig).
|
|
307
312
|
const reasoning = options.reasoning ?? this.config.reasoning;
|
|
308
|
-
const messages = this.buildMessages(options.systemPrompt, options.messages, reasoning, options.promptCache);
|
|
313
|
+
const { messages, stablePrefixEndIndex } = this.buildMessages(options.systemPrompt, options.messages, reasoning, options.promptCache);
|
|
309
314
|
const tools = options.tools?.length ? this.convertTools(options.tools) : undefined;
|
|
315
|
+
const cachePolicy = this.promptCachePolicy(options.promptCache);
|
|
316
|
+
let cachePlan;
|
|
317
|
+
if (cachePolicy.strategy === "openai-hybrid" && options.promptCache?.scopeId) {
|
|
318
|
+
let latestEligibleIndex = -1;
|
|
319
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
320
|
+
const message = messages[index];
|
|
321
|
+
if ((message.role === "user" || message.role === "tool") &&
|
|
322
|
+
(typeof message.content === "string"
|
|
323
|
+
? message.content.length > 0
|
|
324
|
+
: Array.isArray(message.content) && message.content.length > 0)) {
|
|
325
|
+
latestEligibleIndex = index;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Hash the effective request settings, including tools and passthrough
|
|
330
|
+
// schemas. Transport streaming does not alter the rendered prefix.
|
|
331
|
+
const { messages: _cacheMessages, ...requestIdentity } = this.buildRequestBody(options, messages, tools, reasoning, false);
|
|
332
|
+
cachePlan = this.promptCacheHistory.prepare({
|
|
333
|
+
scopeKey: createPromptCacheKey(options.promptCache.scopeId, JSON.stringify(this.getPromptCacheScopeIdentity())),
|
|
334
|
+
messages,
|
|
335
|
+
latestBoundaryIndex: latestEligibleIndex,
|
|
336
|
+
requestIdentity,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (options.promptCache || cachePolicy.strategy === "anthropic-explicit") {
|
|
340
|
+
this.applyPromptCacheBreakpoints(messages, cachePolicy, stablePrefixEndIndex, cachePlan?.readBoundaryIndex);
|
|
341
|
+
}
|
|
310
342
|
const span = logger.span("llm.request", {
|
|
311
343
|
cat: "llm",
|
|
312
344
|
provider: this.provider,
|
|
@@ -314,12 +346,15 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
314
346
|
stream: !!(options.stream && options.onChunk),
|
|
315
347
|
messageCount: messages.length,
|
|
316
348
|
toolCount: tools?.length ?? 0,
|
|
317
|
-
cacheStrategy:
|
|
349
|
+
cacheStrategy: cachePolicy.strategy,
|
|
318
350
|
});
|
|
319
351
|
try {
|
|
320
352
|
const response = options.stream && options.onChunk
|
|
321
353
|
? await this.streamMessage(options, messages, tools, reasoning, requestSignal)
|
|
322
354
|
: await this.nonStreamMessage(options, messages, tools, reasoning, requestSignal);
|
|
355
|
+
if (cachePlan && !requestSignal?.aborted && (response.usage?.promptTokens ?? 0) > 0) {
|
|
356
|
+
this.promptCacheHistory.commit(cachePlan);
|
|
357
|
+
}
|
|
323
358
|
span.end({
|
|
324
359
|
stopReason: response.stopReason,
|
|
325
360
|
promptTokens: response.usage?.promptTokens,
|
|
@@ -965,20 +1000,21 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
965
1000
|
const stablePrefixEndIndex = stablePrefixEndMessage
|
|
966
1001
|
? normalized.indexOf(stablePrefixEndMessage)
|
|
967
1002
|
: undefined;
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1003
|
+
return {
|
|
1004
|
+
messages: normalized,
|
|
1005
|
+
stablePrefixEndIndex: stablePrefixEndIndex !== undefined && stablePrefixEndIndex >= 0
|
|
971
1006
|
? stablePrefixEndIndex
|
|
972
|
-
: undefined
|
|
973
|
-
}
|
|
974
|
-
return normalized;
|
|
1007
|
+
: undefined,
|
|
1008
|
+
};
|
|
975
1009
|
}
|
|
976
1010
|
/**
|
|
977
1011
|
* Translate semantic prefix boundaries to the active wire format. Both
|
|
978
1012
|
* formats annotate content blocks without reordering messages.
|
|
979
1013
|
*/
|
|
980
|
-
applyPromptCacheBreakpoints(messages, policy, stablePrefixEndIndex) {
|
|
981
|
-
if (policy.strategy !== "anthropic-explicit" &&
|
|
1014
|
+
applyPromptCacheBreakpoints(messages, policy, stablePrefixEndIndex, previousBoundaryIndex) {
|
|
1015
|
+
if (policy.strategy !== "anthropic-explicit" &&
|
|
1016
|
+
policy.strategy !== "openai-explicit" &&
|
|
1017
|
+
policy.strategy !== "openai-hybrid") {
|
|
982
1018
|
return;
|
|
983
1019
|
}
|
|
984
1020
|
const markedMessages = new Set();
|
|
@@ -1014,7 +1050,14 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
1014
1050
|
const requested = uniquePromptCacheBreakpointIndexes([
|
|
1015
1051
|
policy.breakpoints.includes("system") ? 0 : undefined,
|
|
1016
1052
|
policy.breakpoints.includes("stable-history") ? stablePrefixEndIndex : undefined,
|
|
1017
|
-
|
|
1053
|
+
// Hybrid uses at most three explicit boundaries (system, stable, prior
|
|
1054
|
+
// successful tail), leaving the fourth write slot for the implicit tail.
|
|
1055
|
+
// Retaining the previous boundary lets this request read the last write.
|
|
1056
|
+
policy.strategy === "openai-hybrid"
|
|
1057
|
+
? previousBoundaryIndex
|
|
1058
|
+
: policy.breakpoints.includes("rolling-history")
|
|
1059
|
+
? messages.length - 1
|
|
1060
|
+
: undefined,
|
|
1018
1061
|
]);
|
|
1019
1062
|
for (const index of requested)
|
|
1020
1063
|
mark(index);
|
|
@@ -3,6 +3,11 @@ import { type NotificationQueue } from "../tool-system/builtin/agent-notificatio
|
|
|
3
3
|
import type { ApprovalRouter } from "../tool-system/permission.js";
|
|
4
4
|
import type { ChatSession } from "./chat-session.js";
|
|
5
5
|
import type { ChatSessionManager } from "./chat-session-manager.js";
|
|
6
|
+
import type { WorkspaceContext } from "../workspace/workspace-context.js";
|
|
7
|
+
interface BackgroundRunWorkspace {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
workspaceContext?: WorkspaceContext;
|
|
10
|
+
}
|
|
6
11
|
interface BackgroundResultWakeOptions {
|
|
7
12
|
sessionId: string;
|
|
8
13
|
manager: ChatSessionManager | null;
|
|
@@ -10,11 +15,13 @@ interface BackgroundResultWakeOptions {
|
|
|
10
15
|
approvalRouter: ApprovalRouter;
|
|
11
16
|
onStream(event: StreamEvent): void;
|
|
12
17
|
notificationMailbox?: NotificationQueue;
|
|
18
|
+
/** Reconstruct fresh host authority for a cold, project-bound Session. */
|
|
19
|
+
resolveWorkspace?(session: ChatSession): Promise<BackgroundRunWorkspace>;
|
|
13
20
|
}
|
|
14
21
|
/**
|
|
15
22
|
* Drain pending background results into exactly one synthetic continuation.
|
|
16
23
|
* Busy sessions are awaited so a completion cannot fall into the gap between
|
|
17
24
|
* the notification bus callback and the interactive run-boundary re-check.
|
|
18
25
|
*/
|
|
19
|
-
export declare function wakeSessionForBackgroundResults({ sessionId, manager, rehydrate, approvalRouter, onStream, notificationMailbox, }: BackgroundResultWakeOptions): Promise<boolean>;
|
|
26
|
+
export declare function wakeSessionForBackgroundResults({ sessionId, manager, rehydrate, approvalRouter, onStream, notificationMailbox, resolveWorkspace, }: BackgroundResultWakeOptions): Promise<boolean>;
|
|
20
27
|
export {};
|
|
@@ -5,7 +5,7 @@ import { buildNotificationMessage, notificationQueue, } from "../tool-system/bui
|
|
|
5
5
|
* Busy sessions are awaited so a completion cannot fall into the gap between
|
|
6
6
|
* the notification bus callback and the interactive run-boundary re-check.
|
|
7
7
|
*/
|
|
8
|
-
export async function wakeSessionForBackgroundResults({ sessionId, manager, rehydrate, approvalRouter, onStream, notificationMailbox = notificationQueue, }) {
|
|
8
|
+
export async function wakeSessionForBackgroundResults({ sessionId, manager, rehydrate, approvalRouter, onStream, notificationMailbox = notificationQueue, resolveWorkspace, }) {
|
|
9
9
|
if (!manager) {
|
|
10
10
|
logger.debug("bg_wakeup.skipped", { sessionId, reason: "no_chat_manager" });
|
|
11
11
|
return false;
|
|
@@ -14,54 +14,92 @@ export async function wakeSessionForBackgroundResults({ sessionId, manager, rehy
|
|
|
14
14
|
logger.debug("bg_wakeup.skipped", { sessionId, reason: "session_unavailable" });
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
|
+
if (notificationMailbox.getSnapshot(sessionId).length === 0) {
|
|
18
|
+
logger.debug("bg_wakeup.skipped", { sessionId, reason: "no_pending_results" });
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
17
21
|
let session = manager.get(sessionId) ?? (await rehydrate(sessionId));
|
|
18
22
|
if (!session) {
|
|
19
23
|
logger.debug("bg_wakeup.skipped", { sessionId, reason: "session_missing" });
|
|
20
24
|
return false;
|
|
21
25
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
logger.debug("bg_wakeup.
|
|
26
|
+
let runWorkspace;
|
|
27
|
+
for (;;) {
|
|
28
|
+
if (manager.isUnavailable(sessionId) || manager.get(sessionId) !== session) {
|
|
29
|
+
logger.debug("bg_wakeup.skipped", { sessionId, reason: "session_owner_changed" });
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
while (session.isBusy()) {
|
|
33
|
+
logger.debug("bg_wakeup.waiting_for_idle", {
|
|
30
34
|
sessionId,
|
|
31
|
-
|
|
35
|
+
pendingCount: notificationMailbox.getSnapshot(sessionId).length,
|
|
32
36
|
});
|
|
37
|
+
await session.settled;
|
|
38
|
+
if (manager.isUnavailable(sessionId)) {
|
|
39
|
+
logger.debug("bg_wakeup.skipped", {
|
|
40
|
+
sessionId,
|
|
41
|
+
reason: "session_became_unavailable",
|
|
42
|
+
});
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const current = manager.get(sessionId);
|
|
46
|
+
if (!current) {
|
|
47
|
+
logger.debug("bg_wakeup.skipped", { sessionId, reason: "session_evicted_after_settle" });
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
session = current;
|
|
51
|
+
}
|
|
52
|
+
// Headless/automation runs are one-shot and have no continuation consumer.
|
|
53
|
+
if (session.engine.isHeadless()) {
|
|
54
|
+
logger.debug("bg_wakeup.skipped", { sessionId, reason: "headless" });
|
|
33
55
|
return false;
|
|
34
56
|
}
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
logger.debug("bg_wakeup.skipped", { sessionId, reason: "
|
|
57
|
+
// A user Stop must win over a later background completion.
|
|
58
|
+
if (session.wasCancelledSinceLastTurn()) {
|
|
59
|
+
logger.debug("bg_wakeup.skipped", { sessionId, reason: "cancelled_since_last_turn" });
|
|
38
60
|
return false;
|
|
39
61
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
62
|
+
if (notificationMailbox.getSnapshot(sessionId).length === 0) {
|
|
63
|
+
logger.debug("bg_wakeup.skipped", { sessionId, reason: "no_pending_results" });
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const resolvedEngine = session.engine;
|
|
67
|
+
const settledBeforeResolution = session.settled;
|
|
68
|
+
try {
|
|
69
|
+
if (resolveWorkspace) {
|
|
70
|
+
runWorkspace = await resolveWorkspace(session);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const resolver = session.engine.resolveSessionRunWorkspace;
|
|
74
|
+
runWorkspace = resolver?.call(session.engine, sessionId) ?? {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Resolve before draining so an unavailable authoritative worktree context
|
|
79
|
+
// leaves the completion recoverable by a later user run.
|
|
80
|
+
logger.warn("bg_wakeup.workspace_unavailable", {
|
|
81
|
+
sessionId,
|
|
82
|
+
error: error instanceof Error ? error.message : String(error),
|
|
83
|
+
});
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
// Host resolution yields to other turns, Stop, close and root migration.
|
|
87
|
+
// Validate ownership before consuming anything, and never carry a context
|
|
88
|
+
// across an intervening run boundary even when that run already finished.
|
|
89
|
+
if (manager.isUnavailable(sessionId) || manager.get(sessionId) !== session) {
|
|
90
|
+
logger.debug("bg_wakeup.skipped", { sessionId, reason: "session_owner_changed" });
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (session.wasCancelledSinceLastTurn()) {
|
|
94
|
+
logger.debug("bg_wakeup.skipped", { sessionId, reason: "cancelled_since_last_turn" });
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (session.isBusy() ||
|
|
98
|
+
session.engine !== resolvedEngine ||
|
|
99
|
+
session.settled !== settledBeforeResolution) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
65
103
|
}
|
|
66
104
|
const pending = notificationMailbox.drainAll(sessionId);
|
|
67
105
|
if (pending.length === 0) {
|
|
@@ -36,6 +36,12 @@ export interface ChatSessionManagerOptions {
|
|
|
36
36
|
*/
|
|
37
37
|
dataRoot?: string;
|
|
38
38
|
}
|
|
39
|
+
export interface GetOrCreateSessionOptions {
|
|
40
|
+
/** Only explicit user opens may clear a closed Session's tombstone. */
|
|
41
|
+
allowReopen?: boolean;
|
|
42
|
+
/** Cancel lifecycle waits without creating or reopening the Session. */
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
}
|
|
39
45
|
export interface LiveChatSessionSnapshot {
|
|
40
46
|
generation: number;
|
|
41
47
|
/** Identity scope of the manager that produced this snapshot. */
|
|
@@ -95,7 +101,7 @@ export declare class ChatSessionManager {
|
|
|
95
101
|
* identity; each call here builds a fresh instance.
|
|
96
102
|
*/
|
|
97
103
|
forIdentity(identity: string): ChatSessionManager;
|
|
98
|
-
getOrCreate(sessionId: string, slice: EngineConfigSlice): Promise<ChatSession>;
|
|
104
|
+
getOrCreate(sessionId: string, slice: EngineConfigSlice, options?: GetOrCreateSessionOptions): Promise<ChatSession>;
|
|
99
105
|
/**
|
|
100
106
|
* Prove whether this worker currently owns a resident Engine, or fence the
|
|
101
107
|
* Session so Main can perform one durable migration without a re-resume race.
|
|
@@ -28,6 +28,27 @@ function assertSafeIdentity(identity) {
|
|
|
28
28
|
throw new Error(`invalid identity: unexpected characters: ${identity}`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
function waitForSessionTransition(transition, signal) {
|
|
32
|
+
if (!signal)
|
|
33
|
+
return transition;
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let settled = false;
|
|
37
|
+
const finish = (failed = false, error) => {
|
|
38
|
+
if (settled)
|
|
39
|
+
return;
|
|
40
|
+
settled = true;
|
|
41
|
+
signal.removeEventListener("abort", onAbort);
|
|
42
|
+
if (failed)
|
|
43
|
+
reject(error);
|
|
44
|
+
else
|
|
45
|
+
resolve();
|
|
46
|
+
};
|
|
47
|
+
const onAbort = () => finish(true, signal.reason);
|
|
48
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
49
|
+
transition.then(() => finish(), (error) => finish(true, error));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
31
52
|
export const CLOSED_CHAT_SESSION_TOMBSTONE_LIMIT = 4096;
|
|
32
53
|
export class ChatSessionManager {
|
|
33
54
|
sessions = new Map();
|
|
@@ -80,29 +101,37 @@ export class ChatSessionManager {
|
|
|
80
101
|
engineFactory: (slice) => baseFactory({ ...slice, sessionStorageDir }),
|
|
81
102
|
});
|
|
82
103
|
}
|
|
83
|
-
async getOrCreate(sessionId, slice) {
|
|
104
|
+
async getOrCreate(sessionId, slice, options = {}) {
|
|
105
|
+
const assertAccess = () => {
|
|
106
|
+
options.signal?.throwIfAborted();
|
|
107
|
+
if (options.allowReopen === false && this.isUnavailable(sessionId)) {
|
|
108
|
+
throw new Error(`target Session is closing or closed: ${sessionId}`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
84
111
|
// A non-resident migration claim is a short ownership handoff to Main.
|
|
85
112
|
// Wait rather than fail the user's run: once Main atomically commits (or
|
|
86
113
|
// aborts) and releases the token, the Engine is created from current disk.
|
|
87
114
|
for (;;) {
|
|
115
|
+
assertAccess();
|
|
88
116
|
const residentMigration = this.residentMigrations.get(sessionId);
|
|
89
117
|
if (residentMigration) {
|
|
90
|
-
await residentMigration.released;
|
|
118
|
+
await waitForSessionTransition(residentMigration.released, options.signal);
|
|
91
119
|
continue;
|
|
92
120
|
}
|
|
93
121
|
const claim = this.migrationClaims.get(sessionId);
|
|
94
122
|
if (claim) {
|
|
95
|
-
await claim.released;
|
|
123
|
+
await waitForSessionTransition(claim.released, options.signal);
|
|
96
124
|
continue;
|
|
97
125
|
}
|
|
98
126
|
const closing = this.closingSessions.get(sessionId);
|
|
99
127
|
if (closing) {
|
|
100
|
-
await closing;
|
|
128
|
+
await waitForSessionTransition(closing, options.signal);
|
|
101
129
|
continue;
|
|
102
130
|
}
|
|
103
131
|
// No await separates the final claim check from getOrCreateNow. On this
|
|
104
132
|
// process's event loop, either this creates the resident owner first or
|
|
105
133
|
// beginSessionMigration installs the fence first; both cannot win.
|
|
134
|
+
assertAccess();
|
|
106
135
|
return this.getOrCreateNow(sessionId, slice);
|
|
107
136
|
}
|
|
108
137
|
}
|
|
@@ -327,13 +356,20 @@ export class ChatSessionManager {
|
|
|
327
356
|
return this.closeSession(sessionId, true);
|
|
328
357
|
}
|
|
329
358
|
closeSession(sessionId, markClosed) {
|
|
330
|
-
const migration = this.residentMigrations.get(sessionId);
|
|
331
|
-
if (migration) {
|
|
332
|
-
return migration.released.then(() => this.closeSession(sessionId, markClosed));
|
|
333
|
-
}
|
|
334
359
|
const alreadyClosing = this.closingSessions.get(sessionId);
|
|
335
360
|
if (alreadyClosing)
|
|
336
361
|
return alreadyClosing;
|
|
362
|
+
const migration = this.residentMigrations.get(sessionId);
|
|
363
|
+
if (migration) {
|
|
364
|
+
// Publish closing intent before waiting so internal senders cannot
|
|
365
|
+
// acquire the migrated owner ahead of this deferred close operation.
|
|
366
|
+
const closing = migration.released.then(() => {
|
|
367
|
+
this.closingSessions.delete(sessionId);
|
|
368
|
+
return this.closeSession(sessionId, markClosed);
|
|
369
|
+
});
|
|
370
|
+
this.closingSessions.set(sessionId, closing);
|
|
371
|
+
return closing;
|
|
372
|
+
}
|
|
337
373
|
const s = this.sessions.get(sessionId);
|
|
338
374
|
if (!s) {
|
|
339
375
|
if (sessionId.startsWith("qchat-")) {
|
|
@@ -28,6 +28,8 @@ export interface TurnOpts {
|
|
|
28
28
|
/** Goal mode for this turn — forwarded to engine.run (loop-until-done).
|
|
29
29
|
* String objective or full GoalConfig (objective + optional budgets). */
|
|
30
30
|
goal?: string | import("../goal/lifecycle.js").GoalConfig;
|
|
31
|
+
/** Disable all Goal resolution for this standalone turn. */
|
|
32
|
+
disableGoal?: boolean;
|
|
31
33
|
/** Marks this turn as a synthetic system-reminder injection (background-job
|
|
32
34
|
* completion notification) rather than the user's own input — persisted so
|
|
33
35
|
* the disk reader skips it as a user bubble on replay. See Engine.run. */
|
|
@@ -138,7 +138,9 @@ export class ChatSession {
|
|
|
138
138
|
// Drain queued turns as cancelled
|
|
139
139
|
const drained = this.queue.splice(0);
|
|
140
140
|
for (const t of drained) {
|
|
141
|
-
t.reject(new Error("cancelled: session aborted before turn ran")
|
|
141
|
+
t.reject(Object.assign(new Error("cancelled: session aborted before turn ran"), {
|
|
142
|
+
name: "AbortError",
|
|
143
|
+
}));
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
isBusy() {
|
|
@@ -306,6 +308,7 @@ export class ChatSession {
|
|
|
306
308
|
signal: this.controller.signal,
|
|
307
309
|
onStream,
|
|
308
310
|
goal: next.opts.goal,
|
|
311
|
+
disableGoal: next.opts.disableGoal,
|
|
309
312
|
injected: next.opts.injected,
|
|
310
313
|
clientMessageId: next.opts.clientMessageId,
|
|
311
314
|
archiveBeforeCurrentTurn: next.opts.archiveBeforeCurrentTurn,
|
|
@@ -371,6 +371,9 @@ export declare class AgentServer {
|
|
|
371
371
|
private makePanelBridge;
|
|
372
372
|
private requestPanelActionForSession;
|
|
373
373
|
private requestWorkspaceSwitchForSession;
|
|
374
|
+
/** The same host authority resolves both dispatched turns and their reply wakeups. */
|
|
375
|
+
private resolveHostSessionWorkspace;
|
|
376
|
+
private requestWorkspaceActionForSession;
|
|
374
377
|
/**
|
|
375
378
|
* Ask the client to answer a question from the agent (legacy single-engine
|
|
376
379
|
* path). This intentionally has no wall-clock timeout; Stop/cancel drains
|