@gajae-code/coding-agent 0.18.0 → 0.18.5
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/CHANGELOG.md +95 -0
- package/dist/types/cli/stats-cli.d.ts +0 -5
- package/dist/types/config/autorouting-tier-map.d.ts +29 -2
- package/dist/types/config/model-registry.d.ts +16 -1
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/exa/mcp-client.d.ts +1 -5
- package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +1 -0
- package/dist/types/hashline/parser.d.ts +28 -5
- package/dist/types/modes/acp/acp-agent.d.ts +6 -1
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
- package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
- package/dist/types/sdk/broker/runtime.d.ts +20 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
- package/dist/types/sdk/bus/index.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/client/client.d.ts +5 -1
- package/dist/types/sdk/host/session-runtime.d.ts +7 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/task/index.d.ts +0 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/descriptors.d.ts +5 -0
- package/dist/types/tools/eval-backends.d.ts +37 -0
- package/dist/types/tools/eval.d.ts +1 -6
- package/dist/types/tools/index.d.ts +1 -36
- package/dist/types/tools/session-descriptions.d.ts +22 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/cli/doctor/managed-link.ts +4 -4
- package/src/cli/doctor/repairs.ts +9 -1
- package/src/cli/stats-cli.ts +4 -36
- package/src/cli-main.ts +4 -8
- package/src/cli.ts +4 -8
- package/src/commands/commit.ts +2 -2
- package/src/commands/sdk.ts +1 -2
- package/src/config/autorouting-tier-map.ts +20 -2
- package/src/config/model-preset-registry.ts +17 -2
- package/src/config/model-profile-activation.ts +3 -1
- package/src/config/model-profiles.ts +11 -3
- package/src/config/model-registry.ts +45 -17
- package/src/config/settings-schema.ts +8 -2
- package/src/exa/mcp-client.ts +41 -8
- package/src/exec/bash-shell-guardian.ts +214 -48
- package/src/gjc-runtime/session-state-sidecar.ts +88 -58
- package/src/hashline/execute.ts +51 -3
- package/src/hashline/parser.ts +63 -16
- package/src/internal-urls/docs-index.generated.ts +25 -25
- package/src/modes/acp/acp-agent.ts +136 -20
- package/src/modes/controllers/ssh-command-controller.ts +3 -1
- package/src/modes/interactive-mode.ts +10 -0
- package/src/modes/utils/context-usage.ts +2 -6
- package/src/prompts/tools/task.md +8 -27
- package/src/sdk/acp/adapter.ts +38 -3
- package/src/sdk/broker/doctor-restart.ts +38 -3
- package/src/sdk/broker/lifecycle.ts +408 -218
- package/src/sdk/broker/recovery-backoff.ts +135 -0
- package/src/sdk/broker/runtime.ts +83 -0
- package/src/sdk/broker/startup-budget.ts +6 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +308 -22
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/client/client.ts +53 -35
- package/src/sdk/host/session-runtime.ts +99 -3
- package/src/sdk/prompt-watchdog.ts +17 -4
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/session/agent-session.ts +79 -17
- package/src/session/internal/managed-session-storage.ts +23 -0
- package/src/task/index.ts +21 -66
- package/src/tools/bash.ts +39 -0
- package/src/tools/descriptors.ts +27 -1
- package/src/tools/eval-backends.ts +95 -0
- package/src/tools/eval.ts +3 -15
- package/src/tools/index.ts +9 -88
- package/src/tools/search.ts +2 -7
- package/src/tools/session-descriptions.ts +135 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/dist/types/cli/grep-cli.d.ts +0 -17
- package/dist/types/cli/shell-cli.d.ts +0 -8
- package/dist/types/commands/grep.d.ts +0 -42
- package/dist/types/commands/shell.d.ts +0 -21
- package/src/cli/grep-cli.ts +0 -160
- package/src/cli/shell-cli.ts +0 -176
- package/src/commands/grep.ts +0 -48
- package/src/commands/shell.ts +0 -29
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded exponential backoff tracker for broker recovery failures per agent dir.
|
|
3
|
+
* Tracks repeated recovery failures and implements exponential backoff with a cap
|
|
4
|
+
* to prevent repeated spawns from destabilizing the system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Configuration for recovery backoff behavior. */
|
|
8
|
+
export interface RecoveryBackoffConfig {
|
|
9
|
+
/** Initial backoff delay in milliseconds (default: 1000ms). */
|
|
10
|
+
initialDelayMs: number;
|
|
11
|
+
/** Maximum backoff delay in milliseconds (default: 30000ms = 30s). */
|
|
12
|
+
maxDelayMs: number;
|
|
13
|
+
/** Backoff multiplier applied each retry (default: 2). */
|
|
14
|
+
multiplier: number;
|
|
15
|
+
/** Maximum number of recovery attempts per session (default: 5). */
|
|
16
|
+
maxAttempts: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** State for a single agent dir's recovery backoff. */
|
|
20
|
+
export interface RecoveryBackoffState {
|
|
21
|
+
/** Timestamp of the last recovery failure. */
|
|
22
|
+
lastFailureAt: number;
|
|
23
|
+
/** Current backoff delay in milliseconds. */
|
|
24
|
+
currentDelayMs: number;
|
|
25
|
+
/** Number of consecutive failures. */
|
|
26
|
+
failureCount: number;
|
|
27
|
+
/** Whether recovery has been permanently stopped due to max attempts. */
|
|
28
|
+
stopped: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Session broker recovery ticks every 30 s (`SESSION_BROKER_RECOVERY_INTERVAL_MS`), so the
|
|
33
|
+
* first retry waits two ticks and the delay doubles up to 30 minutes: a broker that keeps
|
|
34
|
+
* failing costs about two spawn attempts per hour at steady state instead of one per tick.
|
|
35
|
+
* Recovery never stops for good on failures alone -- a broker outage must still heal once
|
|
36
|
+
* the broker comes back. Only a replaced runtime image ends recovery (session-runtime.ts).
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULTS: RecoveryBackoffConfig = {
|
|
39
|
+
initialDelayMs: 60_000,
|
|
40
|
+
maxDelayMs: 30 * 60_000,
|
|
41
|
+
multiplier: 2,
|
|
42
|
+
maxAttempts: Number.POSITIVE_INFINITY,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Manages per-agent-dir recovery backoff state. */
|
|
46
|
+
export class RecoveryBackoffTracker {
|
|
47
|
+
#state = new Map<string, RecoveryBackoffState>();
|
|
48
|
+
#config: RecoveryBackoffConfig;
|
|
49
|
+
#clock: { now(): number } = { now: Date.now };
|
|
50
|
+
|
|
51
|
+
constructor(config: Partial<RecoveryBackoffConfig> = {}) {
|
|
52
|
+
this.#config = { ...DEFAULTS, ...config };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Test hook: inject a custom clock. */
|
|
56
|
+
setClockForTest(clock: { now(): number }): void {
|
|
57
|
+
this.#clock = clock;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if recovery should proceed now, considering backoff.
|
|
62
|
+
* Returns true if sufficient time has elapsed since the last failure.
|
|
63
|
+
*/
|
|
64
|
+
canAttemptRecovery(agentDir: string): boolean {
|
|
65
|
+
const state = this.#state.get(agentDir);
|
|
66
|
+
if (!state) return true;
|
|
67
|
+
if (state.stopped) return false;
|
|
68
|
+
|
|
69
|
+
const now = this.#clock.now();
|
|
70
|
+
return now >= state.lastFailureAt + state.currentDelayMs;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Record a recovery failure and update backoff state.
|
|
75
|
+
* Returns true if recovery should continue to be attempted, false if max attempts reached.
|
|
76
|
+
*/
|
|
77
|
+
recordFailure(agentDir: string): boolean {
|
|
78
|
+
let state = this.#state.get(agentDir);
|
|
79
|
+
if (!state) {
|
|
80
|
+
state = {
|
|
81
|
+
lastFailureAt: this.#clock.now(),
|
|
82
|
+
currentDelayMs: this.#config.initialDelayMs,
|
|
83
|
+
failureCount: 1,
|
|
84
|
+
stopped: false,
|
|
85
|
+
};
|
|
86
|
+
this.#state.set(agentDir, state);
|
|
87
|
+
} else {
|
|
88
|
+
state.lastFailureAt = this.#clock.now();
|
|
89
|
+
state.failureCount += 1;
|
|
90
|
+
|
|
91
|
+
// Apply exponential backoff
|
|
92
|
+
state.currentDelayMs = Math.min(state.currentDelayMs * this.#config.multiplier, this.#config.maxDelayMs);
|
|
93
|
+
|
|
94
|
+
// Stop if we've hit max attempts
|
|
95
|
+
if (state.failureCount >= this.#config.maxAttempts) {
|
|
96
|
+
state.stopped = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return !state.stopped;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Record a successful recovery and reset backoff state.
|
|
105
|
+
*/
|
|
106
|
+
recordSuccess(agentDir: string): void {
|
|
107
|
+
this.#state.delete(agentDir);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the current backoff state for an agent dir (for testing/diagnostics).
|
|
112
|
+
*/
|
|
113
|
+
getState(agentDir: string): RecoveryBackoffState | undefined {
|
|
114
|
+
return this.#state.get(agentDir);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get milliseconds until the next recovery attempt can proceed.
|
|
119
|
+
*/
|
|
120
|
+
getBackoffWaitMs(agentDir: string): number {
|
|
121
|
+
const state = this.#state.get(agentDir);
|
|
122
|
+
if (!state) return 0;
|
|
123
|
+
|
|
124
|
+
const now = this.#clock.now();
|
|
125
|
+
const nextAttemptAt = state.lastFailureAt + state.currentDelayMs;
|
|
126
|
+
return Math.max(0, nextAttemptAt - now);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reset all backoff state (for testing).
|
|
131
|
+
*/
|
|
132
|
+
resetForTest(): void {
|
|
133
|
+
this.#state.clear();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -271,6 +271,15 @@ export function sdkInternalRuntimeImage(): string | undefined {
|
|
|
271
271
|
return publishedRuntimeImage({});
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/** Identity of a runtime image: enough to detect replacement at the same path. */
|
|
275
|
+
export interface SdkInternalRuntimeImageIdentity {
|
|
276
|
+
path: string;
|
|
277
|
+
dev: number;
|
|
278
|
+
ino: number;
|
|
279
|
+
mtimeMs: number;
|
|
280
|
+
size: number;
|
|
281
|
+
}
|
|
282
|
+
|
|
274
283
|
/** A stat that has not answered by here is inconclusive, never proof of absence. */
|
|
275
284
|
const RUNTIME_IMAGE_PROBE_TIMEOUT_MS = 1_000;
|
|
276
285
|
|
|
@@ -308,6 +317,80 @@ export async function isSdkInternalRuntimeImagePresent(file: string): Promise<bo
|
|
|
308
317
|
}
|
|
309
318
|
}
|
|
310
319
|
|
|
320
|
+
const RUNTIME_IMAGE_PROBE_TIMED_OUT = Symbol("runtime-image-probe-timed-out");
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* `fsp.stat` bounded by the runtime-image probe timeout. A metadata request on a
|
|
324
|
+
* stalled mount can never settle; the timeout sentinel lets callers treat that as
|
|
325
|
+
* inconclusive instead of pending forever.
|
|
326
|
+
*/
|
|
327
|
+
async function statRuntimeImageBounded(file: string): Promise<fs.Stats | typeof RUNTIME_IMAGE_PROBE_TIMED_OUT> {
|
|
328
|
+
const timedOut = Promise.withResolvers<typeof RUNTIME_IMAGE_PROBE_TIMED_OUT>();
|
|
329
|
+
const timer: NodeJS.Timeout = setTimeout(
|
|
330
|
+
() => timedOut.resolve(RUNTIME_IMAGE_PROBE_TIMED_OUT),
|
|
331
|
+
RUNTIME_IMAGE_PROBE_TIMEOUT_MS,
|
|
332
|
+
);
|
|
333
|
+
try {
|
|
334
|
+
return await Promise.race([fsp.stat(file), timedOut.promise]);
|
|
335
|
+
} finally {
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Capture the runtime image identity (dev, ino, mtimeMs, size) at startup.
|
|
342
|
+
* Used to detect if the binary at the same path has been replaced.
|
|
343
|
+
*/
|
|
344
|
+
export async function captureRuntimeImageIdentity(file: string): Promise<SdkInternalRuntimeImageIdentity | undefined> {
|
|
345
|
+
try {
|
|
346
|
+
const resolved = path.resolve(file);
|
|
347
|
+
const stats = await statRuntimeImageBounded(resolved);
|
|
348
|
+
// A timed-out probe leaves the identity unknown, like any other failure.
|
|
349
|
+
if (stats === RUNTIME_IMAGE_PROBE_TIMED_OUT || !stats.isFile()) return undefined;
|
|
350
|
+
return {
|
|
351
|
+
path: resolved,
|
|
352
|
+
dev: stats.dev,
|
|
353
|
+
ino: stats.ino,
|
|
354
|
+
mtimeMs: stats.mtimeMs,
|
|
355
|
+
size: stats.size,
|
|
356
|
+
};
|
|
357
|
+
} catch {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Check if the runtime image has been replaced since startup.
|
|
364
|
+
* Returns `true` if the path is gone, its dev/ino differ from the startup identity,
|
|
365
|
+
* or its size or modification time changed (an in-place rewrite keeps the inode).
|
|
366
|
+
* Returns `false` (inconclusive) if the file exists and matches, or on any error.
|
|
367
|
+
*/
|
|
368
|
+
export async function isSdkInternalRuntimeImageReplaced(
|
|
369
|
+
startupIdentity: SdkInternalRuntimeImageIdentity | undefined,
|
|
370
|
+
): Promise<boolean> {
|
|
371
|
+
if (startupIdentity === undefined) return false;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const stats = await statRuntimeImageBounded(startupIdentity.path);
|
|
375
|
+
// A timed-out probe is inconclusive, not proof of replacement.
|
|
376
|
+
if (stats === RUNTIME_IMAGE_PROBE_TIMED_OUT) return false;
|
|
377
|
+
if (!stats.isFile()) return false; // Not a file, but not proven gone
|
|
378
|
+
// dev/ino catch a replacement at the same path; size and mtime catch an
|
|
379
|
+
// in-place rewrite that keeps the inode (mtime also covers same-size bytes).
|
|
380
|
+
return (
|
|
381
|
+
stats.dev !== startupIdentity.dev ||
|
|
382
|
+
stats.ino !== startupIdentity.ino ||
|
|
383
|
+
stats.size !== startupIdentity.size ||
|
|
384
|
+
stats.mtimeMs !== startupIdentity.mtimeMs
|
|
385
|
+
);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
// Only ENOENT/ENOTDIR prove absence
|
|
388
|
+
if (isProvenRuntimeImageAbsence(error)) return true;
|
|
389
|
+
// Inconclusive on other errors
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
311
394
|
/** Resolve the production descriptor from the statically imported marker and current Bun runtime evidence. */
|
|
312
395
|
export function resolveSdkInternalSpawnCommand(action: SdkInternalAction): SdkInternalSpawnCommand {
|
|
313
396
|
return resolveSdkInternalSpawnCommandWithEvidence(action, {});
|
|
@@ -11,6 +11,9 @@ export const MAX_PREPARATION_TIMEOUT_MS = 120_000;
|
|
|
11
11
|
/** Bounded workspace install window; independent of child semantic readiness. */
|
|
12
12
|
export const DEFAULT_DEPENDENCY_PREPARATION_TIMEOUT_MS = 30_000;
|
|
13
13
|
|
|
14
|
+
/** Non-worktree broker pre-spawn cap, also limited by the admission window remaining since receipt. */
|
|
15
|
+
export const DEFAULT_BROKER_PRESPAWN_PREPARATION_TIMEOUT_MS = 30_000;
|
|
16
|
+
|
|
14
17
|
export function isValidReadinessTimeoutMs(value: unknown): value is number {
|
|
15
18
|
return (
|
|
16
19
|
typeof value === "number" &&
|
|
@@ -98,7 +101,9 @@ export function lifecycleStartupBudgetMs(requestedReadinessTimeoutMs: number): n
|
|
|
98
101
|
*
|
|
99
102
|
* Worktree launches add independent preparation budgets so git add / install cannot
|
|
100
103
|
* cut the caller while the broker still owns the request. One-arg
|
|
101
|
-
* {@link lifecycleStartupBudgetMs} stays queue+readiness for no-worktree callers
|
|
104
|
+
* {@link lifecycleStartupBudgetMs} stays queue+readiness for no-worktree callers:
|
|
105
|
+
* broker-derived pre-spawn work uses only the unspent admission window from receipt,
|
|
106
|
+
* and the fresh readiness window ends within that unchanged caller budget.
|
|
102
107
|
*/
|
|
103
108
|
export function lifecycleRequestTimeoutMs(operation: string, input: Record<string, unknown>): number | undefined {
|
|
104
109
|
const deadlineFields = [
|
|
@@ -141,8 +141,8 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
141
141
|
* crashes, recovery, and attachment retirement cannot replay ambiguous work.
|
|
142
142
|
*/
|
|
143
143
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
144
|
-
discord:
|
|
145
|
-
slack:
|
|
144
|
+
discord: 82,
|
|
145
|
+
slack: 89,
|
|
146
146
|
};
|
|
147
147
|
|
|
148
148
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -116,6 +116,7 @@ import { type AbortScope, type ControlSurface, dispatchControl, TypedControlErro
|
|
|
116
116
|
import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "../host/control/runtime-gate";
|
|
117
117
|
import { isAutoroutingInactive, markAutoroutingInactive } from "../host/internal-autorouting-state";
|
|
118
118
|
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "../host/query";
|
|
119
|
+
import { RESPONSE_CEILING_BYTES } from "../host/query/handlers";
|
|
119
120
|
import type { SdkFrame } from "../host/types";
|
|
120
121
|
import {
|
|
121
122
|
parseSyntheticModelId,
|
|
@@ -137,6 +138,7 @@ import {
|
|
|
137
138
|
assistantFailureCode,
|
|
138
139
|
failedPromptOutcome,
|
|
139
140
|
formatPromptFailureForLocalLog,
|
|
141
|
+
isSafePromptFailureCode,
|
|
140
142
|
PROMPT_FAILURE_MESSAGE_DEADLINE,
|
|
141
143
|
providerDiagnosticField,
|
|
142
144
|
publishedPromptFailure,
|
|
@@ -4246,6 +4248,16 @@ export function shouldAwaitNotificationStartup(event: {
|
|
|
4246
4248
|
return event.type !== "session_switch" || event.transition?.origin !== INTERACTIVE_SELECTOR_RESUME_ORIGIN;
|
|
4247
4249
|
}
|
|
4248
4250
|
|
|
4251
|
+
export function boundedCorrelatedAgentEndFrame(frame: Record<string, unknown>): Record<string, unknown> {
|
|
4252
|
+
const finalText = typeof frame.finalText === "string" ? frame.finalText : "";
|
|
4253
|
+
if (finalText.length <= RESPONSE_CEILING_BYTES / 8) return frame;
|
|
4254
|
+
return {
|
|
4255
|
+
...frame,
|
|
4256
|
+
finalText: `${finalText.slice(0, RESPONSE_CEILING_BYTES / 8)}[truncated]`,
|
|
4257
|
+
finalTextTruncated: true,
|
|
4258
|
+
};
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4249
4261
|
export function createNotificationsExtension(
|
|
4250
4262
|
api: ExtensionAPI,
|
|
4251
4263
|
options: {
|
|
@@ -5080,6 +5092,8 @@ export function createNotificationsExtension(
|
|
|
5080
5092
|
acknowledged: boolean;
|
|
5081
5093
|
connectionId: string;
|
|
5082
5094
|
abandoned: boolean;
|
|
5095
|
+
terminalRetry?: Promise<void>;
|
|
5096
|
+
backlogDropLogged?: boolean;
|
|
5083
5097
|
failed: boolean;
|
|
5084
5098
|
terminal: boolean;
|
|
5085
5099
|
retainCorrelation: boolean;
|
|
@@ -5331,6 +5345,289 @@ export function createNotificationsExtension(
|
|
|
5331
5345
|
submission.abandoned = true;
|
|
5332
5346
|
submission.bufferedFrames.length = 0;
|
|
5333
5347
|
};
|
|
5348
|
+
const isCorrelatedDeltaFrame = (frame: Record<string, unknown>): boolean => {
|
|
5349
|
+
if (frame.type !== "event" || frame.kind !== "message_update") return false;
|
|
5350
|
+
const payload = frame.payload as {
|
|
5351
|
+
event?: { assistantMessageEvent?: { type?: string } };
|
|
5352
|
+
};
|
|
5353
|
+
const type = payload.event?.assistantMessageEvent?.type;
|
|
5354
|
+
return type === "text_delta" || type === "thinking_delta";
|
|
5355
|
+
};
|
|
5356
|
+
const boundedCorrelatedFrames = (frame: Record<string, unknown>): Record<string, unknown>[] | undefined => {
|
|
5357
|
+
if (frame.type === "event" && frame.kind === "message_update") {
|
|
5358
|
+
const payload = frame.payload as {
|
|
5359
|
+
event?: {
|
|
5360
|
+
message?: { role?: string };
|
|
5361
|
+
assistantMessageEvent?: { type?: string; delta?: string };
|
|
5362
|
+
};
|
|
5363
|
+
};
|
|
5364
|
+
const update = payload.event?.assistantMessageEvent;
|
|
5365
|
+
if (update?.type !== "text_delta" && update?.type !== "thinking_delta") return undefined;
|
|
5366
|
+
// ACP consumes deltas, so retain the delta while discarding the replaceable
|
|
5367
|
+
// accumulated message snapshot. Split giant deltas without changing content.
|
|
5368
|
+
const delta = update.delta ?? "";
|
|
5369
|
+
const makeFrame = (chunk: string): Record<string, unknown> => ({
|
|
5370
|
+
...frame,
|
|
5371
|
+
payload: {
|
|
5372
|
+
event_type: "message_update",
|
|
5373
|
+
event: {
|
|
5374
|
+
type: "message_update",
|
|
5375
|
+
message: { role: payload.event?.message?.role ?? "assistant", content: [] },
|
|
5376
|
+
assistantMessageEvent: { type: update.type, delta: chunk },
|
|
5377
|
+
},
|
|
5378
|
+
},
|
|
5379
|
+
});
|
|
5380
|
+
const frames: Record<string, unknown>[] = [];
|
|
5381
|
+
let offset = 0;
|
|
5382
|
+
while (offset < delta.length || (offset === 0 && delta.length === 0)) {
|
|
5383
|
+
let low = 1;
|
|
5384
|
+
let high = delta.length - offset + 1;
|
|
5385
|
+
let bestEnd = 0;
|
|
5386
|
+
while (low < high) {
|
|
5387
|
+
const candidateLength = Math.floor((low + high) / 2);
|
|
5388
|
+
let candidateEnd = offset + candidateLength;
|
|
5389
|
+
if (
|
|
5390
|
+
candidateEnd < delta.length &&
|
|
5391
|
+
delta.charCodeAt(candidateEnd) >= 0xdc00 &&
|
|
5392
|
+
delta.charCodeAt(candidateEnd) <= 0xdfff
|
|
5393
|
+
)
|
|
5394
|
+
candidateEnd += 1;
|
|
5395
|
+
const candidate = JSON.stringify(makeFrame(delta.slice(offset, candidateEnd)));
|
|
5396
|
+
if (Buffer.byteLength(candidate) <= RESPONSE_CEILING_BYTES) {
|
|
5397
|
+
bestEnd = candidateEnd;
|
|
5398
|
+
low = candidateLength + 1;
|
|
5399
|
+
} else {
|
|
5400
|
+
high = candidateLength;
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
if (bestEnd === 0) return undefined;
|
|
5404
|
+
frames.push(makeFrame(delta.slice(offset, bestEnd)));
|
|
5405
|
+
offset = bestEnd;
|
|
5406
|
+
if (delta.length === 0) break;
|
|
5407
|
+
}
|
|
5408
|
+
return frames;
|
|
5409
|
+
}
|
|
5410
|
+
if (frame.type === "event" && frame.kind === "message_end") {
|
|
5411
|
+
const payload = frame.payload as {
|
|
5412
|
+
event?: { message?: { role?: string; content?: Array<{ type?: string; text?: string }> } };
|
|
5413
|
+
};
|
|
5414
|
+
const message = payload.event?.message;
|
|
5415
|
+
const textBlocks =
|
|
5416
|
+
message?.content
|
|
5417
|
+
?.filter(
|
|
5418
|
+
(block): block is { type: "text"; text: string } =>
|
|
5419
|
+
block.type === "text" && typeof block.text === "string",
|
|
5420
|
+
)
|
|
5421
|
+
.map(block => block.text) ?? [];
|
|
5422
|
+
const makeMessageEndFrame = (
|
|
5423
|
+
content: Array<{ type: "text"; text: string }>,
|
|
5424
|
+
textTruncated?: true,
|
|
5425
|
+
omittedTextBlocks?: number,
|
|
5426
|
+
): Record<string, unknown> => ({
|
|
5427
|
+
...frame,
|
|
5428
|
+
payload: {
|
|
5429
|
+
event_type: "message_end",
|
|
5430
|
+
event: {
|
|
5431
|
+
type: "message_end",
|
|
5432
|
+
message: {
|
|
5433
|
+
role: message?.role ?? "assistant",
|
|
5434
|
+
content,
|
|
5435
|
+
...(textTruncated ? { textTruncated } : {}),
|
|
5436
|
+
...(omittedTextBlocks ? { omittedTextBlocks } : {}),
|
|
5437
|
+
},
|
|
5438
|
+
},
|
|
5439
|
+
},
|
|
5440
|
+
});
|
|
5441
|
+
const fits = (candidate: Record<string, unknown>): boolean =>
|
|
5442
|
+
Buffer.byteLength(JSON.stringify(candidate)) <= RESPONSE_CEILING_BYTES;
|
|
5443
|
+
const content: Array<{ type: "text"; text: string }> = [];
|
|
5444
|
+
let textTruncated = false;
|
|
5445
|
+
let omittedTextBlocks = 0;
|
|
5446
|
+
for (const [index, textBlock] of textBlocks.entries()) {
|
|
5447
|
+
const wholeBlock = [...content, { type: "text" as const, text: textBlock }];
|
|
5448
|
+
if (fits(makeMessageEndFrame(wholeBlock))) {
|
|
5449
|
+
content.push({ type: "text", text: textBlock });
|
|
5450
|
+
continue;
|
|
5451
|
+
}
|
|
5452
|
+
textTruncated = true;
|
|
5453
|
+
const remainingTextBlocks = textBlocks.length - index;
|
|
5454
|
+
let low = 1;
|
|
5455
|
+
let high = textBlock.length + 1;
|
|
5456
|
+
let bestEnd = 0;
|
|
5457
|
+
while (low < high) {
|
|
5458
|
+
const candidateLength = Math.floor((low + high) / 2);
|
|
5459
|
+
let candidateEnd = candidateLength;
|
|
5460
|
+
if (
|
|
5461
|
+
candidateEnd < textBlock.length &&
|
|
5462
|
+
textBlock.charCodeAt(candidateEnd) >= 0xdc00 &&
|
|
5463
|
+
textBlock.charCodeAt(candidateEnd) <= 0xdfff
|
|
5464
|
+
)
|
|
5465
|
+
candidateEnd += 1;
|
|
5466
|
+
const candidateContent = [
|
|
5467
|
+
...content,
|
|
5468
|
+
...(candidateEnd > 0 ? [{ type: "text" as const, text: textBlock.slice(0, candidateEnd) }] : []),
|
|
5469
|
+
];
|
|
5470
|
+
const candidateOmittedTextBlocks = remainingTextBlocks - (candidateEnd > 0 ? 1 : 0);
|
|
5471
|
+
if (
|
|
5472
|
+
fits(
|
|
5473
|
+
makeMessageEndFrame(
|
|
5474
|
+
candidateContent,
|
|
5475
|
+
true,
|
|
5476
|
+
candidateOmittedTextBlocks > 0 ? candidateOmittedTextBlocks : undefined,
|
|
5477
|
+
),
|
|
5478
|
+
)
|
|
5479
|
+
) {
|
|
5480
|
+
bestEnd = candidateEnd;
|
|
5481
|
+
low = candidateLength + 1;
|
|
5482
|
+
} else high = candidateLength;
|
|
5483
|
+
}
|
|
5484
|
+
if (bestEnd > 0) content.push({ type: "text", text: textBlock.slice(0, bestEnd) });
|
|
5485
|
+
omittedTextBlocks = remainingTextBlocks - (bestEnd > 0 ? 1 : 0);
|
|
5486
|
+
break;
|
|
5487
|
+
}
|
|
5488
|
+
return [
|
|
5489
|
+
makeMessageEndFrame(
|
|
5490
|
+
content,
|
|
5491
|
+
textTruncated ? true : undefined,
|
|
5492
|
+
omittedTextBlocks > 0 ? omittedTextBlocks : undefined,
|
|
5493
|
+
),
|
|
5494
|
+
];
|
|
5495
|
+
}
|
|
5496
|
+
if (frame.type === "agent_end") {
|
|
5497
|
+
return [boundedCorrelatedAgentEndFrame(frame)];
|
|
5498
|
+
}
|
|
5499
|
+
if (frame.type === "agent_failed") {
|
|
5500
|
+
const outcome = frame.outcome as Extract<SdkPromptTerminalOutcome, { kind: "failed" }> | undefined;
|
|
5501
|
+
return [
|
|
5502
|
+
{
|
|
5503
|
+
...frame,
|
|
5504
|
+
error: { code: outcome?.code ?? "prompt_failed", message: "Prompt failure details were truncated." },
|
|
5505
|
+
...(outcome ? { outcome: { ...outcome, message: "Prompt failure details were truncated." } } : {}),
|
|
5506
|
+
},
|
|
5507
|
+
];
|
|
5508
|
+
}
|
|
5509
|
+
return undefined;
|
|
5510
|
+
};
|
|
5511
|
+
const deliverCorrelatedFrame = (submission: PromptSubmission, frame: Record<string, unknown>) => {
|
|
5512
|
+
if (submission.abandoned) return;
|
|
5513
|
+
const activeRuntime = runtime;
|
|
5514
|
+
if (!activeRuntime) return;
|
|
5515
|
+
const fail = (cause: string, frameBytes: number, failedJson = JSON.stringify(frame)) => {
|
|
5516
|
+
// A non-terminal frame only reports progress from a still-running
|
|
5517
|
+
// prompt. Abandon its delivery on failure instead of publishing a
|
|
5518
|
+
// synthetic terminal that could race the real run and make ACP
|
|
5519
|
+
// settle the prompt while provider/tool execution continues.
|
|
5520
|
+
if (frame.type !== "agent_end" && frame.type !== "agent_failed") {
|
|
5521
|
+
if (cause === "writer_backlog_full") {
|
|
5522
|
+
if (!submission.backlogDropLogged) {
|
|
5523
|
+
submission.backlogDropLogged = true;
|
|
5524
|
+
logger.warn(
|
|
5525
|
+
`sdk: correlated non-terminal delivery dropped: cause=${cause} frameBytes=${frameBytes}`,
|
|
5526
|
+
);
|
|
5527
|
+
}
|
|
5528
|
+
return;
|
|
5529
|
+
}
|
|
5530
|
+
logger.warn(`sdk: correlated non-terminal delivery abandoned: cause=${cause} frameBytes=${frameBytes}`);
|
|
5531
|
+
abandonPrompt(submission);
|
|
5532
|
+
return;
|
|
5533
|
+
}
|
|
5534
|
+
if (cause === "writer_backlog_full") {
|
|
5535
|
+
const correlation = { commandId: String(frame.commandId), turnId: String(frame.turnId) };
|
|
5536
|
+
const key = promptSubmissionKey(correlation);
|
|
5537
|
+
submission.terminalRetry = (async () => {
|
|
5538
|
+
const deadline = Date.now() + 3_000;
|
|
5539
|
+
while (Date.now() < deadline) {
|
|
5540
|
+
await Bun.sleep(50);
|
|
5541
|
+
try {
|
|
5542
|
+
activeRuntime.server.sendTo(submission.connectionId, failedJson);
|
|
5543
|
+
return;
|
|
5544
|
+
} catch (retryError) {
|
|
5545
|
+
if (!String(retryError).includes("cause=writer_backlog_full")) break;
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
terminalDeliveryFailure(cause, frameBytes);
|
|
5549
|
+
})().finally(() => {
|
|
5550
|
+
submission.terminalRetry = undefined;
|
|
5551
|
+
if (submission.terminal && promptSubmissions.get(key) === submission) {
|
|
5552
|
+
if (!submission.abandoned) submission.phase = "delivered";
|
|
5553
|
+
finalizePrompt(key, correlation);
|
|
5554
|
+
}
|
|
5555
|
+
});
|
|
5556
|
+
return;
|
|
5557
|
+
}
|
|
5558
|
+
terminalDeliveryFailure(cause, frameBytes);
|
|
5559
|
+
};
|
|
5560
|
+
const terminalDeliveryFailure = (cause: string, frameBytes: number) => {
|
|
5561
|
+
const safeCause = isSafePromptFailureCode(cause) ? cause : "unknown";
|
|
5562
|
+
const correlation = { commandId: String(frame.commandId), turnId: String(frame.turnId) };
|
|
5563
|
+
const outcome = failedPromptOutcome({
|
|
5564
|
+
code: "prompt_failed",
|
|
5565
|
+
provenance: "agent_failed",
|
|
5566
|
+
providerCode: safeCause,
|
|
5567
|
+
evidence: {},
|
|
5568
|
+
});
|
|
5569
|
+
try {
|
|
5570
|
+
activeRuntime.server.sendTo(
|
|
5571
|
+
submission.connectionId,
|
|
5572
|
+
JSON.stringify({
|
|
5573
|
+
type: "agent_failed",
|
|
5574
|
+
sessionId: activeRuntime.id,
|
|
5575
|
+
...correlation,
|
|
5576
|
+
error: {
|
|
5577
|
+
code: "delivery_failed",
|
|
5578
|
+
message: "Prompt frame delivery failed.",
|
|
5579
|
+
cause: safeCause,
|
|
5580
|
+
frameBytes,
|
|
5581
|
+
},
|
|
5582
|
+
outcome,
|
|
5583
|
+
}),
|
|
5584
|
+
);
|
|
5585
|
+
} catch (error) {
|
|
5586
|
+
logger.warn(`sdk: correlated delivery failure terminal failed: ${String(error)}`);
|
|
5587
|
+
}
|
|
5588
|
+
abandonPrompt(submission);
|
|
5589
|
+
};
|
|
5590
|
+
const original = JSON.stringify(frame);
|
|
5591
|
+
const originalBytes = Buffer.byteLength(original);
|
|
5592
|
+
let framesToSend = [frame];
|
|
5593
|
+
if (originalBytes > RESPONSE_CEILING_BYTES) {
|
|
5594
|
+
const bounded = boundedCorrelatedFrames(frame);
|
|
5595
|
+
if (!bounded) {
|
|
5596
|
+
if (frame.type === "event" && frame.kind === "message_update" && !isCorrelatedDeltaFrame(frame)) return;
|
|
5597
|
+
fail("oversized_frame", originalBytes);
|
|
5598
|
+
return;
|
|
5599
|
+
}
|
|
5600
|
+
framesToSend = bounded;
|
|
5601
|
+
}
|
|
5602
|
+
for (const frameToSend of framesToSend) {
|
|
5603
|
+
const json = JSON.stringify(frameToSend);
|
|
5604
|
+
if (Buffer.byteLength(json) > RESPONSE_CEILING_BYTES) {
|
|
5605
|
+
fail("oversized_frame", originalBytes);
|
|
5606
|
+
return;
|
|
5607
|
+
}
|
|
5608
|
+
try {
|
|
5609
|
+
activeRuntime.server.sendTo(submission.connectionId, json);
|
|
5610
|
+
} catch (error) {
|
|
5611
|
+
const detail = String(error);
|
|
5612
|
+
if (detail.includes("cause=oversized_frame") && json === original) {
|
|
5613
|
+
const bounded = boundedCorrelatedFrames(frame);
|
|
5614
|
+
if (bounded) {
|
|
5615
|
+
try {
|
|
5616
|
+
for (const retryFrame of bounded)
|
|
5617
|
+
activeRuntime.server.sendTo(submission.connectionId, JSON.stringify(retryFrame));
|
|
5618
|
+
return;
|
|
5619
|
+
} catch (retryError) {
|
|
5620
|
+
logger.warn(`sdk: bounded correlated delivery failed: ${String(retryError)}`);
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5624
|
+
logger.warn(`sdk: correlated delivery failed: ${detail}`);
|
|
5625
|
+
const cause = /cause=([a-z_]+)/.exec(detail)?.[1] ?? "unknown";
|
|
5626
|
+
fail(cause, originalBytes, json);
|
|
5627
|
+
return;
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
};
|
|
5334
5631
|
const emitPromptLifecycle = (
|
|
5335
5632
|
correlation: { commandId: string; turnId: string } | undefined,
|
|
5336
5633
|
frame: PromptLifecycleFrame,
|
|
@@ -5353,13 +5650,9 @@ export function createNotificationsExtension(
|
|
|
5353
5650
|
submission.bufferedFrames.push(frame);
|
|
5354
5651
|
return;
|
|
5355
5652
|
}
|
|
5356
|
-
|
|
5357
|
-
runtime.server.sendTo(submission.connectionId, JSON.stringify(frame));
|
|
5358
|
-
} catch (error) {
|
|
5359
|
-
logger.warn(`sdk: correlated lifecycle delivery failed: ${String(error)}`);
|
|
5360
|
-
abandonPrompt(submission);
|
|
5361
|
-
}
|
|
5653
|
+
deliverCorrelatedFrame(submission, frame);
|
|
5362
5654
|
if (submission.terminal) {
|
|
5655
|
+
if (submission.terminalRetry) return;
|
|
5363
5656
|
submission.phase = "delivered";
|
|
5364
5657
|
finalizePrompt(key, correlation);
|
|
5365
5658
|
}
|
|
@@ -5497,12 +5790,7 @@ export function createNotificationsExtension(
|
|
|
5497
5790
|
submission.bufferedFrames.push(frame);
|
|
5498
5791
|
return;
|
|
5499
5792
|
}
|
|
5500
|
-
|
|
5501
|
-
runtime.server.sendTo(submission.connectionId, JSON.stringify(frame));
|
|
5502
|
-
} catch (error) {
|
|
5503
|
-
logger.warn(`sdk: correlated agent event delivery failed: ${String(error)}`);
|
|
5504
|
-
abandonPrompt(submission);
|
|
5505
|
-
}
|
|
5793
|
+
deliverCorrelatedFrame(submission, frame);
|
|
5506
5794
|
};
|
|
5507
5795
|
/**
|
|
5508
5796
|
* Status of a registered deadline expiry attempt after an awaited
|
|
@@ -5666,15 +5954,13 @@ export function createNotificationsExtension(
|
|
|
5666
5954
|
};
|
|
5667
5955
|
const flushPromptLifecycle = (key: string, submission: PromptSubmission) => {
|
|
5668
5956
|
for (const frame of submission.bufferedFrames.splice(0)) {
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
} catch (error) {
|
|
5672
|
-
logger.warn(`sdk: buffered correlated lifecycle delivery failed: ${String(error)}`);
|
|
5673
|
-
abandonPrompt(submission);
|
|
5674
|
-
break;
|
|
5675
|
-
}
|
|
5957
|
+
deliverCorrelatedFrame(submission, frame);
|
|
5958
|
+
if (submission.abandoned) break;
|
|
5676
5959
|
}
|
|
5677
5960
|
if (submission.terminal) {
|
|
5961
|
+
if (submission.terminalRetry) {
|
|
5962
|
+
return;
|
|
5963
|
+
}
|
|
5678
5964
|
submission.phase = "delivered";
|
|
5679
5965
|
const [commandId, turnId] = key.split(":", 2);
|
|
5680
5966
|
if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
|
|
@@ -9636,10 +9922,10 @@ export function createNotificationsExtension(
|
|
|
9636
9922
|
else if (finalAssistant?.errorKind === "provider_safety_stop")
|
|
9637
9923
|
outcome = { kind: "stopped", reason: "refusal", provenance: "agent" };
|
|
9638
9924
|
// A missing/normal final assistant with a non-failing loop stop reason is a
|
|
9639
|
-
// normal turn end; only explicit error/aborted assistants or
|
|
9640
|
-
// loop stop reasons are failures. Text is never parsed.
|
|
9925
|
+
// normal turn end; only explicit error/aborted assistants or other
|
|
9926
|
+
// non-completed loop stop reasons are failures. Text is never parsed.
|
|
9641
9927
|
else if (
|
|
9642
|
-
(event.stopReason === undefined || event.stopReason === "completed") &&
|
|
9928
|
+
(event.stopReason === undefined || event.stopReason === "completed" || event.stopReason === "paused") &&
|
|
9643
9929
|
finalAssistant?.stopReason !== "error" &&
|
|
9644
9930
|
finalAssistant?.stopReason !== "aborted"
|
|
9645
9931
|
)
|
|
@@ -297,7 +297,7 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
|
|
|
297
297
|
* Generation 193 gates streamed content on negotiated observer capabilities,
|
|
298
298
|
* so existing owners are replaced before exposing the new observer path.
|
|
299
299
|
*/
|
|
300
|
-
export const DAEMON_GENERATION =
|
|
300
|
+
export const DAEMON_GENERATION = 199;
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
303
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
|