@gajae-code/coding-agent 0.18.1 → 0.18.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/CHANGELOG.md +161 -0
- package/dist/types/cli/stats-cli.d.ts +0 -5
- package/dist/types/cli-commands.d.ts +10 -0
- package/dist/types/cli-ordinary.d.ts +13 -0
- package/dist/types/cli.d.ts +12 -3
- package/dist/types/commands/contribution-prep.d.ts +0 -3
- package/dist/types/commands/sdk.d.ts +3 -0
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +29 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -0
- package/dist/types/config/model-registry.d.ts +16 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -3
- package/dist/types/edit/streaming.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/exec/bash-shell-supervisor.d.ts +12 -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/broker/discovery.d.ts +8 -0
- package/dist/types/sdk/broker/ensure.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
- 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 +21 -1
- 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/bus/telegram-daemon.d.ts +1 -0
- package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
- package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +7 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +13 -0
- package/dist/types/session/agent-session.d.ts +5 -1
- package/dist/types/tools/ask-contract.d.ts +75 -254
- package/dist/types/tools/bash.d.ts +1 -0
- package/package.json +7 -7
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/src/cli/public-command-entry.ts +5 -0
- package/src/cli/ssh-cli.ts +6 -3
- package/src/cli/stats-cli.ts +4 -36
- package/src/cli/web-search-cli.ts +9 -3
- package/src/cli-commands.ts +14 -0
- package/src/cli-main.ts +4 -8
- package/src/cli-ordinary.ts +197 -0
- package/src/cli.ts +25 -186
- package/src/commands/commit.ts +2 -2
- package/src/commands/contribution-prep.ts +1 -3
- package/src/commands/sdk.ts +60 -2
- package/src/commands/update.ts +15 -1
- package/src/config/autorouting-tier-map.ts +20 -2
- package/src/config/file-lock.ts +91 -2
- package/src/config/model-preset-registry.ts +17 -2
- package/src/config/model-profile-activation.ts +73 -5
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +29 -29
- package/src/config/model-registry.ts +45 -17
- package/src/config/model-resolver.ts +9 -1
- package/src/coordinator-mcp/event-webhook.ts +19 -14
- package/src/coordinator-mcp/policy.ts +14 -4
- package/src/coordinator-mcp/server.ts +142 -7
- package/src/coordinator-mcp/session-reaper.ts +9 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/edit/modes/replace.ts +6 -6
- package/src/edit/streaming.ts +15 -15
- package/src/exa/mcp-client.ts +41 -8
- package/src/exec/bash-shell-guardian.ts +184 -34
- package/src/exec/bash-shell-supervisor.ts +60 -3
- package/src/hashline/execute.ts +51 -3
- package/src/hashline/parser.ts +63 -16
- package/src/internal-urls/docs-index.generated.ts +29 -28
- package/src/memories/index.ts +8 -2
- package/src/modes/acp/acp-agent.ts +688 -371
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/controllers/ssh-command-controller.ts +3 -1
- package/src/prompts/tools/ask.md +2 -0
- package/src/sdk/broker/broker.ts +46 -0
- package/src/sdk/broker/daemon-entry.ts +67 -6
- package/src/sdk/broker/discovery.ts +8 -0
- package/src/sdk/broker/ensure.ts +156 -20
- package/src/sdk/broker/lifecycle-ledger.ts +110 -12
- package/src/sdk/broker/lifecycle.ts +108 -34
- package/src/sdk/broker/recovery-backoff.ts +135 -0
- package/src/sdk/broker/runtime.ts +88 -1
- package/src/sdk/broker/startup-budget.ts +6 -1
- package/src/sdk/broker/transport.ts +5 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +305 -19
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +15 -12
- package/src/sdk/bus/telegram-daemon.ts +34 -1
- package/src/sdk/cli/diagnostics-cli.ts +247 -0
- package/src/sdk/diagnostics/observe-broker.ts +654 -0
- package/src/sdk/host/control/dispatch.ts +52 -4
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +40 -2
- package/src/sdk/host/session-runtime.ts +112 -8
- package/src/sdk/index.ts +1 -0
- package/src/sdk/lifecycle-session.ts +1 -0
- package/src/sdk/prompt-watchdog.ts +17 -4
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/router/session-router.ts +7 -1
- package/src/sdk/session.ts +37 -8
- package/src/session/agent-session.ts +128 -41
- package/src/session/blob-store.ts +78 -33
- package/src/session/internal/managed-session-storage.ts +119 -3
- package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
- package/src/setup/hermes-setup.ts +4 -1
- package/src/slash-commands/builtin-registry.ts +8 -5
- package/src/tools/ask-contract.test.ts +76 -1
- package/src/tools/ask-contract.ts +57 -6
- package/src/tools/ask.ts +16 -7
- package/src/tools/bash.ts +77 -29
- package/src/tools/descriptors.test.ts +29 -0
- package/src/tools/tool-catalog.generated.ts +7 -4
- package/src/web/search/index.ts +2 -2
- 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
|
@@ -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 = [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { logger } from "@gajae-code/utils";
|
|
2
3
|
import type { ServerWebSocket } from "bun";
|
|
3
4
|
import type { Broker } from "./broker";
|
|
4
5
|
|
|
@@ -7,6 +8,8 @@ const PROTOCOL_VERSION = 3;
|
|
|
7
8
|
const MAX_BROKER_JSON_FRAME_BYTES = 4 * 1024 * 1024;
|
|
8
9
|
|
|
9
10
|
const BROKER_OPERATIONS = new Set([
|
|
11
|
+
// Observation-only published diagnostics; it starts, ensures and recovers nothing.
|
|
12
|
+
"broker.diagnostics",
|
|
10
13
|
"session.list",
|
|
11
14
|
"session.get_endpoint",
|
|
12
15
|
"session.create",
|
|
@@ -160,7 +163,8 @@ export class BrokerTransport {
|
|
|
160
163
|
try {
|
|
161
164
|
const result = await this.#broker.handleRequest(frame.operation, frame.input, frame.idempotencyKey);
|
|
162
165
|
send(socket, { type: "broker_response", id: frame.id, ...result });
|
|
163
|
-
} catch {
|
|
166
|
+
} catch (error) {
|
|
167
|
+
logger.warn(`sdk broker request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
164
168
|
sendError(socket, frame.id, "unavailable", "broker request failed");
|
|
165
169
|
}
|
|
166
170
|
}
|
|
@@ -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 });
|
|
@@ -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 = 200;
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
303
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
|
|
@@ -53,6 +53,16 @@ const nodeFs: TelegramDaemonFs = {
|
|
|
53
53
|
const DEFAULT_GRACEFUL_TIMEOUT_MS = 8_000;
|
|
54
54
|
const DEFAULT_KILL_TIMEOUT_MS = 3_000;
|
|
55
55
|
const DEFAULT_WAIT_STEP_MS = 25;
|
|
56
|
+
// Windows process handles cannot deliver SIGTERM cooperatively. The request
|
|
57
|
+
// file is therefore the sole notification channel, so a captured owner gets
|
|
58
|
+
// enough time to finish its long poll, network slack, and shutdown joins before
|
|
59
|
+
// an explicit force request may escalate. Short explicit timeouts are clamped
|
|
60
|
+
// to this floor only for hard-termination authorities; POSIX keeps 8 seconds.
|
|
61
|
+
const HARD_TERMINATION_LONG_POLL_MS = 25_000;
|
|
62
|
+
const HARD_TERMINATION_NETWORK_SLACK_MS = 5_000;
|
|
63
|
+
const HARD_TERMINATION_SHUTDOWN_JOINS_MS = 10_000;
|
|
64
|
+
const HARD_TERMINATION_GRACE_MS =
|
|
65
|
+
HARD_TERMINATION_LONG_POLL_MS + HARD_TERMINATION_NETWORK_SLACK_MS + HARD_TERMINATION_SHUTDOWN_JOINS_MS;
|
|
56
66
|
|
|
57
67
|
export interface TelegramDaemonControlRequest {
|
|
58
68
|
version: 1;
|
|
@@ -641,17 +651,6 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
641
651
|
const signalResult = attestedLegacyOwner
|
|
642
652
|
? await this.#signalAttestedLegacyOwner(attestedLegacyOwner, fp, chatId, "SIGTERM")
|
|
643
653
|
: await this.signalCapturedOwner(modernCapturedOwner!, fp, chatId, "SIGTERM");
|
|
644
|
-
if (signalResult === "hard_termination") {
|
|
645
|
-
await this.clearOwnRequest(requestId);
|
|
646
|
-
return this.result(
|
|
647
|
-
action,
|
|
648
|
-
false,
|
|
649
|
-
"telegram daemon has hard process authority; refusing cooperative SIGTERM",
|
|
650
|
-
before,
|
|
651
|
-
await this.status(),
|
|
652
|
-
warnings,
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
654
|
if (signalResult === "ownership_changed") {
|
|
656
655
|
await this.clearOwnRequest(requestId);
|
|
657
656
|
return this.result(
|
|
@@ -667,7 +666,11 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
667
666
|
// death through the normal path — waitForPidDeath returns immediately for a
|
|
668
667
|
// dead pid, so stop succeeds and reload proceeds to spawn the replacement.
|
|
669
668
|
|
|
670
|
-
|
|
669
|
+
const cooperativeGraceMs =
|
|
670
|
+
signalResult === "hard_termination"
|
|
671
|
+
? Math.max(gracefulTimeoutMs, HARD_TERMINATION_GRACE_MS)
|
|
672
|
+
: gracefulTimeoutMs;
|
|
673
|
+
let dead = await this.waitForPidDeath(oldPid, cooperativeGraceMs);
|
|
671
674
|
if (!dead) {
|
|
672
675
|
// Old pid still alive after the cooperative SIGTERM. Inspect current ownership.
|
|
673
676
|
const currentSnapshot = await readOwnerFreshnessSnapshot({ settings: this.settings, fs: this.fsImpl });
|
|
@@ -362,6 +362,7 @@ const BOT_API_RETRY_ATTEMPTS = 3;
|
|
|
362
362
|
// Backoff after a failed getUpdates long-poll so a persistent outage does not
|
|
363
363
|
// busy-loop the daemon.
|
|
364
364
|
const POLL_BACKOFF_MS = 1_000;
|
|
365
|
+
const CONTROL_STOP_WATCH_INTERVAL_MS = 750;
|
|
365
366
|
const AUTOMATIC_RELOAD_COOLDOWN_MS = 10 * 60 * 1_000;
|
|
366
367
|
// Default freshness-poll window a cooldown contender waits for a sibling's
|
|
367
368
|
// replacement daemon to publish a fresh ready owner before reloading itself.
|
|
@@ -4567,6 +4568,7 @@ export class TelegramNotificationDaemon {
|
|
|
4567
4568
|
private stopRequested = false;
|
|
4568
4569
|
/** First observed stop cause wins, including an explicit stop. */
|
|
4569
4570
|
#stopCause: TelegramDaemonStopCause | undefined;
|
|
4571
|
+
#controlStopCheck: Promise<boolean> | undefined;
|
|
4570
4572
|
/** Signal cause resolution is asynchronous because control requests are file-backed. */
|
|
4571
4573
|
#stopCauseResolution: Promise<void> | undefined;
|
|
4572
4574
|
|
|
@@ -12875,6 +12877,9 @@ export class TelegramNotificationDaemon {
|
|
|
12875
12877
|
throw new Error("validation forum destination must be the exact supergroup forum returned by getChat");
|
|
12876
12878
|
}
|
|
12877
12879
|
let ownershipProved = false;
|
|
12880
|
+
let controlWatchTimer: Timer | NodeJS.Timeout | undefined;
|
|
12881
|
+
let controlWatchActive = true;
|
|
12882
|
+
const clearTimeoutImpl = this.opts.clearTimeoutImpl ?? clearTimeout;
|
|
12878
12883
|
try {
|
|
12879
12884
|
const renewed = await renewDaemonHeartbeat({
|
|
12880
12885
|
settings: this.opts.settings,
|
|
@@ -12935,6 +12940,19 @@ export class TelegramNotificationDaemon {
|
|
|
12935
12940
|
}
|
|
12936
12941
|
await this.#attachmentRouter.start();
|
|
12937
12942
|
let idleSince = this.runtime.now();
|
|
12943
|
+
const setTimeoutImpl = this.opts.setTimeoutImpl ?? setTimeout;
|
|
12944
|
+
const watchControl = () => {
|
|
12945
|
+
if (!controlWatchActive || !this.running || !this.opts.control) return;
|
|
12946
|
+
controlWatchTimer = setTimeoutImpl(() => {
|
|
12947
|
+
controlWatchTimer = undefined;
|
|
12948
|
+
void this.controlStopRequested()
|
|
12949
|
+
.catch(() => false)
|
|
12950
|
+
.finally(() => {
|
|
12951
|
+
if (controlWatchActive && this.running) watchControl();
|
|
12952
|
+
});
|
|
12953
|
+
}, CONTROL_STOP_WATCH_INTERVAL_MS);
|
|
12954
|
+
};
|
|
12955
|
+
if (this.opts.control) watchControl();
|
|
12938
12956
|
while (this.running) {
|
|
12939
12957
|
if (await this.controlStopRequested()) break;
|
|
12940
12958
|
// A thrown renewal (e.g. a transient Windows EPERM/EACCES/EBUSY while
|
|
@@ -13003,6 +13021,8 @@ export class TelegramNotificationDaemon {
|
|
|
13003
13021
|
this.requestStop("unexpected_exception");
|
|
13004
13022
|
throw error;
|
|
13005
13023
|
} finally {
|
|
13024
|
+
controlWatchActive = false;
|
|
13025
|
+
if (controlWatchTimer !== undefined) clearTimeoutImpl(controlWatchTimer);
|
|
13006
13026
|
this.requestStop();
|
|
13007
13027
|
await this.#stopCauseResolution;
|
|
13008
13028
|
this.running = false;
|
|
@@ -13089,8 +13109,21 @@ export class TelegramNotificationDaemon {
|
|
|
13089
13109
|
private async controlStopRequested(): Promise<boolean> {
|
|
13090
13110
|
if (this.runtime.stopRequested) return true;
|
|
13091
13111
|
if (!this.opts.control) return false;
|
|
13112
|
+
if (this.#controlStopCheck) return this.#controlStopCheck;
|
|
13113
|
+
const check = this.checkControlStopRequested();
|
|
13114
|
+
this.#controlStopCheck = check;
|
|
13115
|
+
try {
|
|
13116
|
+
return await check;
|
|
13117
|
+
} finally {
|
|
13118
|
+
if (this.#controlStopCheck === check) this.#controlStopCheck = undefined;
|
|
13119
|
+
}
|
|
13120
|
+
}
|
|
13121
|
+
|
|
13122
|
+
private async checkControlStopRequested(): Promise<boolean> {
|
|
13092
13123
|
try {
|
|
13093
|
-
const
|
|
13124
|
+
const control = this.opts.control;
|
|
13125
|
+
if (!control) return false;
|
|
13126
|
+
const requested = await control.shouldStop(this.opts.ownerId);
|
|
13094
13127
|
if (!requested) return false;
|
|
13095
13128
|
this.#resolveControlStopCause("stop");
|
|
13096
13129
|
this.requestStop();
|