@gajae-code/coding-agent 0.12.19 → 0.12.20
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 +20 -9
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +66 -0
- package/dist/types/config/settings-schema.d.ts +34 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +97 -1
- package/dist/types/modes/acp/acp-agent.d.ts +33 -1
- package/dist/types/modes/components/bash-execution.d.ts +7 -0
- package/dist/types/modes/components/eval-execution.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +10 -1
- package/dist/types/sdk/acp/adapter.d.ts +9 -17
- package/dist/types/sdk/acp/index.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +93 -1
- package/dist/types/sdk/broker/ensure.d.ts +17 -0
- package/dist/types/sdk/broker/index.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +10 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +81 -2
- package/dist/types/sdk/broker/startup-budget.d.ts +44 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +11 -1
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +20 -9
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -6
- package/dist/types/sdk/bus/slack-daemon.d.ts +2 -2
- package/dist/types/sdk/host/query/revision-store.d.ts +5 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/lifecycle-session.d.ts +13 -2
- package/dist/types/sdk/prompt-failure.d.ts +13 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +82 -0
- package/dist/types/sdk/session-reconnect.d.ts +20 -0
- package/dist/types/sdk/startup-capability.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +32 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +40 -2
- package/dist/types/session/internal/session-memory-sidecar.d.ts +881 -0
- package/dist/types/session/session-manager.d.ts +301 -27
- package/dist/types/session/session-storage.d.ts +170 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tui/output-block.d.ts +6 -0
- package/package.json +8 -8
- package/src/cli/args.ts +75 -34
- package/src/commands/acp.ts +7 -1
- package/src/commands/gc.ts +31 -2
- package/src/commands/sdk.ts +141 -7
- package/src/config/settings-schema.ts +43 -0
- package/src/coordinator-mcp/server.ts +6 -1
- package/src/export/html/index.ts +83 -39
- package/src/gjc-runtime/gc-runtime.ts +1283 -4
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/local-protocol.ts +14 -0
- package/src/main.ts +40 -9
- package/src/modes/acp/acp-agent.ts +564 -37
- package/src/modes/acp/terminal-auth.ts +2 -2
- package/src/modes/components/bash-execution.ts +14 -0
- package/src/modes/components/eval-execution.ts +14 -0
- package/src/modes/controllers/command-controller.ts +63 -8
- package/src/modes/utils/ui-helpers.ts +81 -11
- package/src/sdk/acp/adapter.ts +46 -45
- package/src/sdk/acp/index.ts +1 -1
- package/src/sdk/broker/broker.ts +392 -11
- package/src/sdk/broker/ensure.ts +131 -61
- package/src/sdk/broker/index.ts +1 -0
- package/src/sdk/broker/lifecycle-ledger.ts +59 -5
- package/src/sdk/broker/lifecycle.ts +278 -63
- package/src/sdk/broker/startup-budget.ts +103 -0
- package/src/sdk/bus/chat-daemon-control.ts +9 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +565 -29
- package/src/sdk/bus/conversation-store.ts +1 -1
- package/src/sdk/bus/discord-daemon.ts +2 -1
- package/src/sdk/bus/discord-live-provider.ts +34 -6
- package/src/sdk/bus/index.ts +118 -20
- package/src/sdk/bus/kind-aware-reconciliation.ts +2 -1
- package/src/sdk/bus/prompt-reconciliation.ts +4 -9
- package/src/sdk/bus/slack-daemon.ts +91 -18
- package/src/sdk/cli/session-cli.ts +8 -1
- package/src/sdk/host/query/handlers.ts +1 -1
- package/src/sdk/host/query/revision-store.ts +48 -12
- package/src/sdk/host/session-runtime.test.ts +50 -0
- package/src/sdk/host/session-runtime.ts +12 -2
- package/src/sdk/lifecycle-session.ts +26 -3
- package/src/sdk/mcp/server.ts +6 -1
- package/src/sdk/prompt-failure.ts +42 -0
- package/src/sdk/prompt-watchdog.ts +162 -0
- package/src/sdk/session-reconnect.ts +43 -0
- package/src/sdk/startup-capability.ts +13 -4
- package/src/session/agent-session.ts +525 -222
- package/src/session/blob-store.ts +89 -0
- package/src/session/internal/managed-session-scope.ts +36 -26
- package/src/session/internal/managed-session-storage.ts +303 -32
- package/src/session/internal/session-memory-sidecar.ts +2160 -0
- package/src/session/session-manager.ts +7027 -394
- package/src/session/session-storage.ts +1134 -11
- package/src/task/executor.ts +17 -4
- package/src/tools/bash.ts +109 -7
- package/src/tools/find.ts +55 -26
- package/src/tools/todo-contract.ts +40 -8
- package/src/tui/output-block.ts +11 -1
|
@@ -349,7 +349,7 @@ export class ConversationStore<T extends ConversationRecord> {
|
|
|
349
349
|
try {
|
|
350
350
|
await this.#fs.writeFile(temporary, `${JSON.stringify(document)}\n`, { mode: 0o600 });
|
|
351
351
|
await this.#fs.chmod(temporary, 0o600);
|
|
352
|
-
const handle = await this.#fs.open(temporary, "r");
|
|
352
|
+
const handle = await this.#fs.open(temporary, "r+");
|
|
353
353
|
try {
|
|
354
354
|
await handle.sync();
|
|
355
355
|
} finally {
|
|
@@ -107,6 +107,7 @@ export interface DiscordNotificationInput {
|
|
|
107
107
|
threadName?: string;
|
|
108
108
|
actionId?: string;
|
|
109
109
|
options?: string[];
|
|
110
|
+
publicationId?: string;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
type DiscordInboundEffectPayload =
|
|
@@ -326,7 +327,7 @@ export class DiscordNotificationDaemon {
|
|
|
326
327
|
// original post and preserves the original component route.
|
|
327
328
|
const effectId = pendingActionId
|
|
328
329
|
? authoritative.pendingActionEffectId!
|
|
329
|
-
: `notification:${authoritative.threadId}:${randomUUID()}`;
|
|
330
|
+
: `notification:${authoritative.threadId}:${input.publicationId ?? randomUUID()}`;
|
|
330
331
|
await this.#postEffect(effectId, authoritative, input.content, components, pendingActionId !== undefined);
|
|
331
332
|
|
|
332
333
|
return authoritative;
|
|
@@ -13,6 +13,7 @@ const GATEWAY_INTENTS = 1 + 512 + 32_768;
|
|
|
13
13
|
const MAX_RATE_LIMIT_RETRIES = 2;
|
|
14
14
|
const NONCE_PREFIX = "<!-- gjc-thread-nonce:";
|
|
15
15
|
const INVALID_SESSION_RECONNECT_DELAY_MS = 1_000;
|
|
16
|
+
const TERMINAL_GATEWAY_CLOSE_CODES: ReadonlySet<number> = new Set([4_004, 4_010, 4_011, 4_012, 4_013, 4_014]);
|
|
16
17
|
|
|
17
18
|
const NONCE_SUFFIX = " -->";
|
|
18
19
|
|
|
@@ -329,15 +330,35 @@ export class DiscordLiveProvider implements DiscordProvider, DiscordDiagnosticPr
|
|
|
329
330
|
socket.addEventListener("message", event => {
|
|
330
331
|
void this.#handleGateway(socket, event).catch(error => this.#handleGatewayFailure(socket, error));
|
|
331
332
|
});
|
|
332
|
-
socket.addEventListener("close",
|
|
333
|
+
socket.addEventListener("close", event => {
|
|
334
|
+
const code = typeof (event as CloseEvent).code === "number" ? (event as CloseEvent).code : 0;
|
|
335
|
+
if (TERMINAL_GATEWAY_CLOSE_CODES.has(code)) {
|
|
336
|
+
this.#gatewayError = new Error(`Discord gateway rejected the connection (close code ${code})`);
|
|
337
|
+
this.#heartbeat?.cancel();
|
|
338
|
+
this.#heartbeat = undefined;
|
|
339
|
+
this.#gatewayReady = false;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.#scheduleReconnect(socket);
|
|
343
|
+
});
|
|
333
344
|
socket.addEventListener("error", () => {
|
|
334
345
|
if (socket.readyState !== 3) socket.close();
|
|
335
346
|
});
|
|
336
347
|
}
|
|
337
348
|
|
|
338
349
|
async #handleGateway(socket: DiscordGatewaySocket, event: Event): Promise<void> {
|
|
339
|
-
const
|
|
340
|
-
|
|
350
|
+
const raw = (event as MessageEvent).data;
|
|
351
|
+
const data =
|
|
352
|
+
typeof raw === "string"
|
|
353
|
+
? raw
|
|
354
|
+
: raw instanceof ArrayBuffer
|
|
355
|
+
? Buffer.from(raw).toString("utf8")
|
|
356
|
+
: ArrayBuffer.isView(raw)
|
|
357
|
+
? Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString("utf8")
|
|
358
|
+
: raw instanceof Blob
|
|
359
|
+
? await raw.text()
|
|
360
|
+
: undefined;
|
|
361
|
+
if (data === undefined) return;
|
|
341
362
|
let frame: JsonRecord;
|
|
342
363
|
try {
|
|
343
364
|
frame = JSON.parse(data) as JsonRecord;
|
|
@@ -387,7 +408,7 @@ export class DiscordLiveProvider implements DiscordProvider, DiscordDiagnosticPr
|
|
|
387
408
|
this.#resumeGatewayUrl = this.#string(payload, "resume_gateway_url");
|
|
388
409
|
}
|
|
389
410
|
if (frame.t === "READY" || frame.t === "RESUMED") this.#gatewayReady = true;
|
|
390
|
-
const inbound = this.#inbound(frame.t, payload);
|
|
411
|
+
const inbound = await this.#inbound(frame.t, payload);
|
|
391
412
|
if (inbound && !inbound.bot && inbound.authorId !== this.#botUserId) await this.#onEvent?.(inbound);
|
|
392
413
|
}
|
|
393
414
|
#handleGatewayFailure(socket: DiscordGatewaySocket, error: unknown): void {
|
|
@@ -526,14 +547,21 @@ export class DiscordLiveProvider implements DiscordProvider, DiscordDiagnosticPr
|
|
|
526
547
|
this.#string(this.#record(data.channel), "parent_id")
|
|
527
548
|
);
|
|
528
549
|
}
|
|
529
|
-
#inbound(type: unknown, data: JsonRecord): DiscordInboundEvent | undefined {
|
|
550
|
+
async #inbound(type: unknown, data: JsonRecord): Promise<DiscordInboundEvent | undefined> {
|
|
530
551
|
if (type === "MESSAGE_CREATE") {
|
|
531
552
|
const id = this.#string(data, "id");
|
|
532
553
|
const guildId = this.#string(data, "guild_id");
|
|
533
554
|
const threadId = this.#string(data, "channel_id");
|
|
534
555
|
const author = this.#record(data.author);
|
|
535
556
|
const authorId = this.#string(author, "id");
|
|
536
|
-
|
|
557
|
+
let parentId = this.#parentId(data);
|
|
558
|
+
if (!parentId && threadId) {
|
|
559
|
+
try {
|
|
560
|
+
parentId = this.#string(this.#record(await this.#request(`/channels/${threadId}`)), "parent_id");
|
|
561
|
+
} catch {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
537
565
|
if (!id || !guildId || !threadId || !parentId || !authorId) return undefined;
|
|
538
566
|
return {
|
|
539
567
|
id,
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
} from "../../tools/ask-answer-registry";
|
|
81
81
|
import { acpFinalTextFromMessage } from "../acp/final-text";
|
|
82
82
|
import { ensureBroker } from "../broker/ensure";
|
|
83
|
+
import { publishSessionHostRuntimeEvidence, type SessionHostRuntimePublication } from "../broker/lifecycle";
|
|
83
84
|
import { SessionIndex } from "../broker/session-index";
|
|
84
85
|
import { createSdkSurfaceFactory, type SessionSdkHost, SessionSdkSessionRuntime, shouldHostSdk } from "../host";
|
|
85
86
|
import { type ControlSurface, dispatchControl } from "../host/control";
|
|
@@ -92,6 +93,7 @@ import {
|
|
|
92
93
|
syntheticModelInputError,
|
|
93
94
|
syntheticNamespaceCollision,
|
|
94
95
|
} from "../model-profile-model";
|
|
96
|
+
import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
|
|
95
97
|
import { PROMPT_CLIENT_REF_MAX_LENGTH, type SdkPromptTerminalOutcome } from "../prompt-status";
|
|
96
98
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
97
99
|
import {
|
|
@@ -127,7 +129,7 @@ import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMess
|
|
|
127
129
|
import { createKindAwareReconciliation } from "./kind-aware-reconciliation";
|
|
128
130
|
import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
|
|
129
131
|
import { proposedTelegramIdentity } from "./notification-orchestration";
|
|
130
|
-
import { createPromptReconciliation
|
|
132
|
+
import { createPromptReconciliation } from "./prompt-reconciliation";
|
|
131
133
|
import { createReconciliationStore } from "./reconciliation-store";
|
|
132
134
|
import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
|
|
133
135
|
import type { SlackConversation } from "./slack-conversation";
|
|
@@ -153,6 +155,40 @@ export {
|
|
|
153
155
|
|
|
154
156
|
const PROMPT_SETTLEMENT_DIAGNOSTIC_ENTRY_LIMIT = 8;
|
|
155
157
|
const PROMPT_SETTLEMENT_DIAGNOSTIC_MAX_AGE_MS = 86_400_000;
|
|
158
|
+
/**
|
|
159
|
+
* Upper bound on the failure reason copied into the local operator log. Mirrors
|
|
160
|
+
* the 512-char bound documented for reconciliation failure messages so a runaway
|
|
161
|
+
* provider error cannot flood the log file.
|
|
162
|
+
*/
|
|
163
|
+
const PROMPT_TERMINAL_FAILURE_REASON_LOG_MAX = 512;
|
|
164
|
+
type PromptTerminalDiagnostic = {
|
|
165
|
+
reason?: unknown;
|
|
166
|
+
loopStopReason?: string;
|
|
167
|
+
assistantStopReason?: string;
|
|
168
|
+
errorKind?: string;
|
|
169
|
+
intentionalCancellation?: boolean;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
type PromptTerminalExtra = {
|
|
173
|
+
finalText?: string;
|
|
174
|
+
error?: { code: string; message: string };
|
|
175
|
+
diagnostic?: PromptTerminalDiagnostic;
|
|
176
|
+
diagnosticAlreadyLogged?: boolean;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
function formatPromptTerminalFailureReason(reason: unknown): string {
|
|
180
|
+
let rawReason: string;
|
|
181
|
+
if (reason instanceof Error) rawReason = reason.message;
|
|
182
|
+
else if (typeof reason === "string") rawReason = reason;
|
|
183
|
+
else if (reason === undefined || reason === null) return "unreported";
|
|
184
|
+
else
|
|
185
|
+
try {
|
|
186
|
+
rawReason = String(reason);
|
|
187
|
+
} catch {
|
|
188
|
+
return "unreported";
|
|
189
|
+
}
|
|
190
|
+
return rawReason ? rawReason.slice(0, PROMPT_TERMINAL_FAILURE_REASON_LOG_MAX) : "unreported";
|
|
191
|
+
}
|
|
156
192
|
|
|
157
193
|
export function formatPromptSettlementDiagnostic(
|
|
158
194
|
proof: Extract<RunSettlementProof, { status: "unfenced" }>,
|
|
@@ -1170,6 +1206,8 @@ interface SessionRuntime {
|
|
|
1170
1206
|
/** Terminal cleanup proof retained across retries; each owner is released at most once after proof. */
|
|
1171
1207
|
hostStopped: boolean;
|
|
1172
1208
|
serverStopped: boolean;
|
|
1209
|
+
/** This runtime's own host-liveness publication; only its teardown may retract it. */
|
|
1210
|
+
evidencePublication?: SessionHostRuntimePublication;
|
|
1173
1211
|
brokerRegistrationReleased: boolean;
|
|
1174
1212
|
/** Managed Telegram root registration released during terminal teardown. */
|
|
1175
1213
|
notificationRootRegistration?: { settings: Settings; cwd: string; registrationToken: string };
|
|
@@ -1192,11 +1230,8 @@ interface SessionRuntime {
|
|
|
1192
1230
|
terminalizePrompt: (
|
|
1193
1231
|
correlation: { commandId: string; turnId: string },
|
|
1194
1232
|
outcome: SdkPromptTerminalOutcome,
|
|
1195
|
-
extra?:
|
|
1233
|
+
extra?: PromptTerminalExtra,
|
|
1196
1234
|
) => Promise<void>;
|
|
1197
|
-
/** Records a correlated prompt terminal boundary after agent unwind. */
|
|
1198
|
-
/** Atomically claims a correlated prompt terminal boundary after agent unwind. */
|
|
1199
|
-
recordPromptTerminal: (correlation: { commandId: string; turnId: string } | undefined) => boolean;
|
|
1200
1235
|
/** Transitions the authoritative reconciliation record at lifecycle ingress; terminal outcomes settle once. */
|
|
1201
1236
|
notePromptReconciliation: (
|
|
1202
1237
|
correlation: { commandId: string; turnId: string } | undefined,
|
|
@@ -3749,6 +3784,13 @@ export function createNotificationsExtension(
|
|
|
3749
3784
|
await rt.server.stopAndWait();
|
|
3750
3785
|
serverStopped = true;
|
|
3751
3786
|
rt.serverStopped = true;
|
|
3787
|
+
// This runtime no longer serves an SDK endpoint, so it withdraws its
|
|
3788
|
+
// own evidence — and only its own. A predecessor's deferred teardown
|
|
3789
|
+
// runs while an identity successor is already serving, and clearing
|
|
3790
|
+
// that live runtime's reader would manufacture "no evidence" for a
|
|
3791
|
+
// host whose clients are still attached.
|
|
3792
|
+
rt.evidencePublication?.retract();
|
|
3793
|
+
rt.evidencePublication = undefined;
|
|
3752
3794
|
} catch (e) {
|
|
3753
3795
|
ownerReleaseFailures.push(e);
|
|
3754
3796
|
logger.warn(`notifications: stop failed: ${String(e)}`);
|
|
@@ -4047,7 +4089,6 @@ export function createNotificationsExtension(
|
|
|
4047
4089
|
connectionId: string;
|
|
4048
4090
|
abandoned: boolean;
|
|
4049
4091
|
failed: boolean;
|
|
4050
|
-
error: unknown;
|
|
4051
4092
|
terminal: boolean;
|
|
4052
4093
|
retainCorrelation: boolean;
|
|
4053
4094
|
/** Fatal/uncertain closure: transport-level only, never a semantic terminal. */
|
|
@@ -4246,7 +4287,6 @@ export function createNotificationsExtension(
|
|
|
4246
4287
|
connectionId: requesterConnectionId,
|
|
4247
4288
|
abandoned: false,
|
|
4248
4289
|
failed: false,
|
|
4249
|
-
error: undefined,
|
|
4250
4290
|
terminal: false,
|
|
4251
4291
|
retainCorrelation: trackReconciliation,
|
|
4252
4292
|
createdAt: Date.now(),
|
|
@@ -4268,6 +4308,7 @@ export function createNotificationsExtension(
|
|
|
4268
4308
|
provenance: "deadline",
|
|
4269
4309
|
},
|
|
4270
4310
|
{ fence: true },
|
|
4311
|
+
{ diagnostic: { reason: "Prompt deadline exceeded." } },
|
|
4271
4312
|
);
|
|
4272
4313
|
}, submission.deadlineMs);
|
|
4273
4314
|
};
|
|
@@ -4354,7 +4395,7 @@ export function createNotificationsExtension(
|
|
|
4354
4395
|
// run and prove settlement. A natural `agent_end`/`agent_failed` already unwound,
|
|
4355
4396
|
// so aborting there would cancel the next turn instead of fencing this one.
|
|
4356
4397
|
options: { fence?: boolean } = {},
|
|
4357
|
-
extra?:
|
|
4398
|
+
extra?: PromptTerminalExtra,
|
|
4358
4399
|
) => {
|
|
4359
4400
|
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
4360
4401
|
if (!submission || submission.terminal || submission.phase !== "active") return;
|
|
@@ -4426,6 +4467,24 @@ export function createNotificationsExtension(
|
|
|
4426
4467
|
}
|
|
4427
4468
|
if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
4428
4469
|
if (!recordPromptTerminal(correlation) || !runtime) return;
|
|
4470
|
+
if (
|
|
4471
|
+
winner.kind === "failed" &&
|
|
4472
|
+
extra?.diagnostic?.intentionalCancellation !== true &&
|
|
4473
|
+
extra?.diagnosticAlreadyLogged !== true
|
|
4474
|
+
) {
|
|
4475
|
+
const diagnostic = extra?.diagnostic;
|
|
4476
|
+
logger.error("sdk_prompt_terminal_failed", {
|
|
4477
|
+
sessionId: id,
|
|
4478
|
+
commandId: correlation.commandId,
|
|
4479
|
+
turnId: correlation.turnId,
|
|
4480
|
+
code: winner.code,
|
|
4481
|
+
provenance: winner.provenance,
|
|
4482
|
+
...(diagnostic?.loopStopReason ? { loopStopReason: diagnostic.loopStopReason } : {}),
|
|
4483
|
+
...(diagnostic?.assistantStopReason ? { assistantStopReason: diagnostic.assistantStopReason } : {}),
|
|
4484
|
+
...(diagnostic?.errorKind ? { errorKind: diagnostic.errorKind } : {}),
|
|
4485
|
+
reason: formatPromptTerminalFailureReason(diagnostic?.reason),
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4429
4488
|
if (winner.kind === "failed") {
|
|
4430
4489
|
emitPromptLifecycle(correlation, {
|
|
4431
4490
|
type: "agent_failed",
|
|
@@ -4445,26 +4504,41 @@ export function createNotificationsExtension(
|
|
|
4445
4504
|
}
|
|
4446
4505
|
};
|
|
4447
4506
|
const emitPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
4507
|
+
logger.error("SDK prompt submission failed", {
|
|
4508
|
+
commandId: correlation.commandId,
|
|
4509
|
+
turnId: correlation.turnId,
|
|
4510
|
+
error: formatPromptFailureForLocalLog(error),
|
|
4511
|
+
});
|
|
4448
4512
|
const sanitized = sanitizePromptFailure(error);
|
|
4513
|
+
const outcome: SdkPromptTerminalOutcome = {
|
|
4514
|
+
kind: "failed",
|
|
4515
|
+
code: "prompt_failed",
|
|
4516
|
+
message: sanitized.message,
|
|
4517
|
+
provenance: "agent_failed",
|
|
4518
|
+
};
|
|
4519
|
+
// This rejection bypasses `agent_end`, so record its local-only reason at
|
|
4520
|
+
// the accepted submission failure boundary before terminalization begins.
|
|
4521
|
+
logger.error("sdk_prompt_terminal_failed", {
|
|
4522
|
+
sessionId: id,
|
|
4523
|
+
commandId: correlation.commandId,
|
|
4524
|
+
turnId: correlation.turnId,
|
|
4525
|
+
code: outcome.code,
|
|
4526
|
+
provenance: outcome.provenance,
|
|
4527
|
+
reason: formatPromptTerminalFailureReason(error),
|
|
4528
|
+
});
|
|
4449
4529
|
void terminalizePrompt(
|
|
4450
4530
|
correlation,
|
|
4451
|
-
|
|
4452
|
-
kind: "failed",
|
|
4453
|
-
code: "prompt_failed",
|
|
4454
|
-
message: sanitized.message,
|
|
4455
|
-
provenance: "agent_failed",
|
|
4456
|
-
},
|
|
4531
|
+
outcome,
|
|
4457
4532
|
{},
|
|
4458
|
-
// The normalized outcome
|
|
4459
|
-
//
|
|
4460
|
-
{ error: sanitized },
|
|
4533
|
+
// The normalized outcome and wire error retain the fixed safe token.
|
|
4534
|
+
// Mark the diagnostic as recorded so terminalization does not duplicate it.
|
|
4535
|
+
{ error: sanitized, diagnostic: { reason: error }, diagnosticAlreadyLogged: true },
|
|
4461
4536
|
);
|
|
4462
4537
|
};
|
|
4463
4538
|
const recordPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
4464
4539
|
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
4465
4540
|
if (!submission) return;
|
|
4466
4541
|
submission.failed = true;
|
|
4467
|
-
submission.error = error;
|
|
4468
4542
|
removePendingPromptCorrelation(correlation);
|
|
4469
4543
|
if (
|
|
4470
4544
|
runtime?.activePromptCorrelation?.commandId === correlation.commandId &&
|
|
@@ -4894,7 +4968,6 @@ export function createNotificationsExtension(
|
|
|
4894
4968
|
bindPromptExecutionHandle,
|
|
4895
4969
|
peekPromptPendingOutcome: correlation => kindReconciliation.peekPendingOutcome(correlation),
|
|
4896
4970
|
terminalizePrompt: (correlation, outcome, extra) => terminalizePrompt(correlation, outcome, {}, extra),
|
|
4897
|
-
recordPromptTerminal,
|
|
4898
4971
|
notePromptReconciliation: (correlation, frame) => {
|
|
4899
4972
|
void kindReconciliation.noteTransition("prompt", correlation, frame);
|
|
4900
4973
|
},
|
|
@@ -5503,6 +5576,14 @@ export function createNotificationsExtension(
|
|
|
5503
5576
|
};
|
|
5504
5577
|
host.emitEvent({ kind: identityHeader.type, payload: identityHeader });
|
|
5505
5578
|
const endpoint = await sdkRuntime.startTransport();
|
|
5579
|
+
// The native server owns the only authoritative view of this host's live
|
|
5580
|
+
// SDK client sockets; publish it so a detached session host can bound its
|
|
5581
|
+
// own lifetime without probing the OS (#4010). The handle is this
|
|
5582
|
+
// runtime's alone, so only this runtime's teardown can retract it.
|
|
5583
|
+
initializedRuntime.evidencePublication = publishSessionHostRuntimeEvidence({
|
|
5584
|
+
attachedClients: () => server.clientCount(),
|
|
5585
|
+
workInFlight: () => initializedRuntime.busy || initializedRuntime.pendingPromptCorrelations.length > 0,
|
|
5586
|
+
});
|
|
5506
5587
|
ephemeralTurns.configureAuthority({
|
|
5507
5588
|
sessionId: id,
|
|
5508
5589
|
endpointDigest: endpointAuthorityDigest(endpoint.url, token),
|
|
@@ -6283,7 +6364,7 @@ export function createNotificationsExtension(
|
|
|
6283
6364
|
message =>
|
|
6284
6365
|
(message as { stopReason?: unknown }).stopReason === "error" ||
|
|
6285
6366
|
(message as { stopReason?: unknown }).stopReason === "aborted",
|
|
6286
|
-
) as { stopReason?: "error" | "aborted"; errorMessage?: unknown } | undefined;
|
|
6367
|
+
) as { stopReason?: "error" | "aborted"; errorMessage?: unknown; errorKind?: unknown } | undefined;
|
|
6287
6368
|
const legacyCode =
|
|
6288
6369
|
event.stopReason === "cancelled"
|
|
6289
6370
|
? "cancelled"
|
|
@@ -6292,8 +6373,25 @@ export function createNotificationsExtension(
|
|
|
6292
6373
|
: terminalAssistant?.stopReason === "aborted"
|
|
6293
6374
|
? "aborted"
|
|
6294
6375
|
: undefined;
|
|
6376
|
+
// The SDK wire outcome and ACP error envelope use a fixed safe token by
|
|
6377
|
+
// contract (see `sanitizePromptFailure`). Assistant failure messages may
|
|
6378
|
+
// still remain in the local session transcript; terminalization copies only
|
|
6379
|
+
// a bounded reason into the local operator log.
|
|
6295
6380
|
void rt.terminalizePrompt(correlation, outcome, {
|
|
6296
6381
|
...(finalText ? { finalText } : {}),
|
|
6382
|
+
...(outcome.kind === "failed"
|
|
6383
|
+
? {
|
|
6384
|
+
diagnostic: {
|
|
6385
|
+
reason: terminalAssistant?.errorMessage,
|
|
6386
|
+
loopStopReason: event.stopReason ?? "none",
|
|
6387
|
+
assistantStopReason: terminalAssistant?.stopReason ?? "none",
|
|
6388
|
+
...(event.stopReason === "cancelled" ? { intentionalCancellation: true } : {}),
|
|
6389
|
+
...(typeof terminalAssistant?.errorKind === "string"
|
|
6390
|
+
? { errorKind: terminalAssistant.errorKind }
|
|
6391
|
+
: {}),
|
|
6392
|
+
},
|
|
6393
|
+
}
|
|
6394
|
+
: {}),
|
|
6297
6395
|
...(outcome.kind === "failed" && legacyCode
|
|
6298
6396
|
? {
|
|
6299
6397
|
// Only the legacy discriminator is preserved; provider text is never
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Kind-aware invocation reconciliation (prompt | skill) with optional durable store.
|
|
3
3
|
* Preserves Q26 admit/first-terminal/capacity/TTL semantics; indexes and caps are per-kind.
|
|
4
4
|
*/
|
|
5
|
+
|
|
6
|
+
import { sanitizePromptFailure } from "../prompt-failure";
|
|
5
7
|
import type { PromptReconciliationStatus, SdkPromptTerminalOutcome, TurnPromptReconciliation } from "../prompt-status";
|
|
6
8
|
import {
|
|
7
9
|
PROMPT_RECONCILIATION_ACTIVE_CAPACITY,
|
|
8
10
|
PROMPT_RECONCILIATION_TERMINAL_CAPACITY,
|
|
9
11
|
PROMPT_RECONCILIATION_TERMINAL_TTL_MS,
|
|
10
12
|
type PromptCorrelation,
|
|
11
|
-
sanitizePromptFailure,
|
|
12
13
|
} from "./prompt-reconciliation";
|
|
13
14
|
import type { DurableReconciliationRecord, ReconciliationKind, ReconciliationStore } from "./reconciliation-store";
|
|
14
15
|
|
|
@@ -19,10 +19,13 @@
|
|
|
19
19
|
* - Terminal transitions settle once: the first terminal outcome wins.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { PROMPT_FAILURE_CODE_MAX, sanitizePromptFailure } from "../prompt-failure";
|
|
23
|
+
|
|
24
|
+
export { PROMPT_FAILURE_CODE_MAX, sanitizePromptFailure };
|
|
25
|
+
|
|
22
26
|
export const PROMPT_RECONCILIATION_ACTIVE_CAPACITY = 128;
|
|
23
27
|
export const PROMPT_RECONCILIATION_TERMINAL_CAPACITY = 256;
|
|
24
28
|
export const PROMPT_RECONCILIATION_TERMINAL_TTL_MS = 15 * 60_000;
|
|
25
|
-
export const PROMPT_FAILURE_CODE_MAX = 64;
|
|
26
29
|
|
|
27
30
|
export type PromptReconciliationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed";
|
|
28
31
|
|
|
@@ -94,14 +97,6 @@ export interface PromptReconciliation {
|
|
|
94
97
|
activeCount(): number;
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
/** Safe-token code capped at 64; arbitrary failure text is never retained. */
|
|
98
|
-
export function sanitizePromptFailure(error: unknown): { code: string; message: string } {
|
|
99
|
-
const candidate = error as { code?: unknown } | undefined;
|
|
100
|
-
const rawCode = typeof candidate?.code === "string" ? candidate.code : "";
|
|
101
|
-
const code = rawCode.length <= PROMPT_FAILURE_CODE_MAX && /^[A-Za-z0-9._-]+$/.test(rawCode) ? rawCode : "internal";
|
|
102
|
-
return { code, message: "Prompt submission failed." };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
100
|
export function createPromptReconciliation(options: { now?: () => number } = {}): PromptReconciliation {
|
|
106
101
|
const now = options.now ?? Date.now;
|
|
107
102
|
const records = new Map<string, PromptReconciliationRecord>();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { SdkClientError } from "../client/client";
|
|
3
3
|
import { readSdkSessionEndpoint, type SdkSessionEndpoint } from "../client/discovery";
|
|
4
4
|
import type { ChatDeliveryError } from "./chat-daemon-runtime";
|
|
@@ -35,7 +35,12 @@ class SlackReconciledAbsentEffectError extends Error {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
type ChatEffect,
|
|
40
|
+
ChatEffectJournal,
|
|
41
|
+
type ChatEffectLease,
|
|
42
|
+
MAX_TERMINAL_CHAT_EFFECTS,
|
|
43
|
+
} from "./chat-effect-journal";
|
|
39
44
|
import { SlackProviderError } from "./slack-live-provider";
|
|
40
45
|
import type {
|
|
41
46
|
SlackMessageSearchResult,
|
|
@@ -44,6 +49,11 @@ import type {
|
|
|
44
49
|
SlackSocketEnvelope,
|
|
45
50
|
} from "./slack-provider";
|
|
46
51
|
|
|
52
|
+
function slackPublicationClientMsgId(publicationId: string): string {
|
|
53
|
+
const digest = createHash("sha256").update(publicationId).digest("hex");
|
|
54
|
+
return `${digest.slice(0, 8)}-${digest.slice(8, 12)}-${digest.slice(12, 16)}-${digest.slice(16, 20)}-${digest.slice(20, 32)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
// Durable filesystem publication leases must outlast one event-loop and persistence turn.
|
|
48
58
|
const MIN_PUBLICATION_LEASE_MS = 100;
|
|
49
59
|
|
|
@@ -440,7 +450,12 @@ export class SlackNotificationDaemon {
|
|
|
440
450
|
throw new SlackThreadBindingError("root_not_found", "The Slack root was not found in the configured channel.");
|
|
441
451
|
}
|
|
442
452
|
|
|
443
|
-
async #postRoot(
|
|
453
|
+
async #postRoot(
|
|
454
|
+
sessionId: string,
|
|
455
|
+
body: string,
|
|
456
|
+
endpointGeneration?: number,
|
|
457
|
+
requestedClientMsgId?: string,
|
|
458
|
+
): Promise<SlackRootPublication> {
|
|
444
459
|
const endpoint = await this.#resolveEndpoint(sessionId);
|
|
445
460
|
if (!endpoint || (endpointGeneration !== undefined && endpoint.generation !== endpointGeneration))
|
|
446
461
|
throw new SlackEndpointBindingError("Slack root publication requires the current session endpoint.");
|
|
@@ -473,7 +488,7 @@ export class SlackNotificationDaemon {
|
|
|
473
488
|
teamId: this.options.teamId,
|
|
474
489
|
channelId: this.options.channelId,
|
|
475
490
|
sessionId,
|
|
476
|
-
clientMsgId: this.#randomId(),
|
|
491
|
+
clientMsgId: requestedClientMsgId ?? this.#randomId(),
|
|
477
492
|
rootPublicationOwner: this.#publicationOwnerId,
|
|
478
493
|
rootPublicationLeaseExpiresAt: now + this.#publicationLeaseMs,
|
|
479
494
|
rootPublicationFence: (current?.rootPublicationFence ?? 0) + 1,
|
|
@@ -570,27 +585,38 @@ export class SlackNotificationDaemon {
|
|
|
570
585
|
body: string,
|
|
571
586
|
actionId?: string,
|
|
572
587
|
endpointGeneration?: number,
|
|
588
|
+
publicationId?: string,
|
|
573
589
|
): Promise<SlackConversation> {
|
|
574
590
|
const endpoint = await this.#resolveEndpoint(sessionId);
|
|
575
591
|
if (!endpoint || (endpointGeneration !== undefined && endpoint.generation !== endpointGeneration))
|
|
576
592
|
throw new SlackEndpointBindingError("Slack notification requires the current session endpoint.");
|
|
577
593
|
const generation = endpoint.generation;
|
|
594
|
+
const rootPublication =
|
|
595
|
+
publicationId === undefined
|
|
596
|
+
? undefined
|
|
597
|
+
: await this.#publicationAttempt(
|
|
598
|
+
`root:${sessionId}:${publicationId}`,
|
|
599
|
+
clientMsgId => `root:${sessionId}:${clientMsgId}`,
|
|
600
|
+
);
|
|
578
601
|
const existing = await this.findSession(sessionId, false);
|
|
579
602
|
const usedExistingRoot =
|
|
580
603
|
existing?.record.state === "active" &&
|
|
581
604
|
!!existing.record.rootTs &&
|
|
582
605
|
existing.record.endpointGeneration === generation;
|
|
583
606
|
let conversation: SlackConversation;
|
|
584
|
-
let bodyWasUsedAsRoot =
|
|
607
|
+
let bodyWasUsedAsRoot =
|
|
608
|
+
usedExistingRoot &&
|
|
609
|
+
rootPublication !== undefined &&
|
|
610
|
+
existing.record.clientMsgId === rootPublication.clientMsgId;
|
|
585
611
|
if (usedExistingRoot) {
|
|
586
612
|
conversation = existing.record;
|
|
587
613
|
} else {
|
|
588
614
|
const publication =
|
|
589
615
|
existing?.record.state === "active"
|
|
590
|
-
? await this.#resumeWithRootPublication(sessionId, body, generation)
|
|
591
|
-
: await this.#postRoot(sessionId, body, generation);
|
|
616
|
+
? await this.#resumeWithRootPublication(sessionId, body, generation, rootPublication?.clientMsgId)
|
|
617
|
+
: await this.#postRoot(sessionId, body, generation, rootPublication?.clientMsgId);
|
|
592
618
|
conversation = publication.conversation;
|
|
593
|
-
bodyWasUsedAsRoot
|
|
619
|
+
bodyWasUsedAsRoot ||= publication.created;
|
|
594
620
|
}
|
|
595
621
|
if (!conversation.rootTs) return conversation;
|
|
596
622
|
const key = usedExistingRoot && existing ? existing.key : this.#intentKey(sessionId);
|
|
@@ -607,11 +633,21 @@ export class SlackNotificationDaemon {
|
|
|
607
633
|
return active;
|
|
608
634
|
}
|
|
609
635
|
if (!actionId) {
|
|
610
|
-
|
|
636
|
+
const publication =
|
|
637
|
+
publicationId === undefined
|
|
638
|
+
? {
|
|
639
|
+
effectId: `notification:${sessionId}:${this.#randomId()}`,
|
|
640
|
+
clientMsgId: this.#randomId(),
|
|
641
|
+
}
|
|
642
|
+
: await this.#publicationAttempt(
|
|
643
|
+
`notification:${sessionId}:${publicationId}`,
|
|
644
|
+
clientMsgId => `notification:${sessionId}:${clientMsgId}`,
|
|
645
|
+
);
|
|
646
|
+
await this.#postDurable(publication.effectId, sessionId, conversationGeneration, {
|
|
611
647
|
channel: conversation.channelId,
|
|
612
648
|
threadTs: conversation.rootTs,
|
|
613
649
|
text: body,
|
|
614
|
-
clientMsgId:
|
|
650
|
+
clientMsgId: publication.clientMsgId,
|
|
615
651
|
});
|
|
616
652
|
return conversation;
|
|
617
653
|
}
|
|
@@ -759,14 +795,28 @@ export class SlackNotificationDaemon {
|
|
|
759
795
|
return true;
|
|
760
796
|
}
|
|
761
797
|
|
|
762
|
-
async resume(
|
|
763
|
-
|
|
798
|
+
async resume(
|
|
799
|
+
sessionId: string,
|
|
800
|
+
body: string,
|
|
801
|
+
endpointGeneration?: number,
|
|
802
|
+
publicationId?: string,
|
|
803
|
+
): Promise<SlackConversation> {
|
|
804
|
+
const rootPublication =
|
|
805
|
+
publicationId === undefined
|
|
806
|
+
? undefined
|
|
807
|
+
: await this.#publicationAttempt(
|
|
808
|
+
`root:${sessionId}:${publicationId}`,
|
|
809
|
+
clientMsgId => `root:${sessionId}:${clientMsgId}`,
|
|
810
|
+
);
|
|
811
|
+
return (await this.#resumeWithRootPublication(sessionId, body, endpointGeneration, rootPublication?.clientMsgId))
|
|
812
|
+
.conversation;
|
|
764
813
|
}
|
|
765
814
|
|
|
766
815
|
async #resumeWithRootPublication(
|
|
767
816
|
sessionId: string,
|
|
768
817
|
body: string,
|
|
769
818
|
endpointGeneration?: number,
|
|
819
|
+
requestedClientMsgId?: string,
|
|
770
820
|
): Promise<SlackRootPublication> {
|
|
771
821
|
const inFlight = this.#rollovers.get(sessionId);
|
|
772
822
|
if (inFlight) {
|
|
@@ -776,9 +826,9 @@ export class SlackNotificationDaemon {
|
|
|
776
826
|
return { conversation: publication.conversation, created: false };
|
|
777
827
|
if (endpointGeneration < activeGeneration)
|
|
778
828
|
throw new SlackEndpointBindingError("Slack root belongs to a newer endpoint generation.");
|
|
779
|
-
return await this.#resumeWithRootPublication(sessionId, body, endpointGeneration);
|
|
829
|
+
return await this.#resumeWithRootPublication(sessionId, body, endpointGeneration, requestedClientMsgId);
|
|
780
830
|
}
|
|
781
|
-
const rollover = this.#resumeRoot(sessionId, body, endpointGeneration);
|
|
831
|
+
const rollover = this.#resumeRoot(sessionId, body, endpointGeneration, requestedClientMsgId);
|
|
782
832
|
this.#rollovers.set(sessionId, rollover);
|
|
783
833
|
try {
|
|
784
834
|
return await rollover;
|
|
@@ -787,7 +837,12 @@ export class SlackNotificationDaemon {
|
|
|
787
837
|
}
|
|
788
838
|
}
|
|
789
839
|
|
|
790
|
-
async #resumeRoot(
|
|
840
|
+
async #resumeRoot(
|
|
841
|
+
sessionId: string,
|
|
842
|
+
body: string,
|
|
843
|
+
endpointGeneration?: number,
|
|
844
|
+
requestedClientMsgId?: string,
|
|
845
|
+
): Promise<SlackRootPublication> {
|
|
791
846
|
const endpoint = await this.#resolveEndpoint(sessionId);
|
|
792
847
|
if (!endpoint || (endpointGeneration !== undefined && endpoint.generation !== endpointGeneration))
|
|
793
848
|
throw new SlackEndpointBindingError("Slack root rollover requires the current session endpoint.");
|
|
@@ -802,7 +857,7 @@ export class SlackNotificationDaemon {
|
|
|
802
857
|
previous = { key: previous.key, record: reconciled };
|
|
803
858
|
waitedForRollover = true;
|
|
804
859
|
}
|
|
805
|
-
if (!previous) return await this.#postRoot(sessionId, body, generation);
|
|
860
|
+
if (!previous) return await this.#postRoot(sessionId, body, generation, requestedClientMsgId);
|
|
806
861
|
if (previous.record.state !== "active") {
|
|
807
862
|
if (
|
|
808
863
|
previous.record.rootPublicationOwner !== this.#publicationOwnerId &&
|
|
@@ -812,7 +867,7 @@ export class SlackNotificationDaemon {
|
|
|
812
867
|
await Bun.sleep(10);
|
|
813
868
|
continue;
|
|
814
869
|
}
|
|
815
|
-
return await this.#postRoot(sessionId, body, generation);
|
|
870
|
+
return await this.#postRoot(sessionId, body, generation, requestedClientMsgId);
|
|
816
871
|
}
|
|
817
872
|
const previousGeneration = this.#requireEndpointGeneration(previous.record);
|
|
818
873
|
if (previousGeneration > generation)
|
|
@@ -873,7 +928,7 @@ export class SlackNotificationDaemon {
|
|
|
873
928
|
resumed.rootPublicationOwner === this.#publicationOwnerId &&
|
|
874
929
|
resumed.rootPublicationFence === fence
|
|
875
930
|
)
|
|
876
|
-
return await this.#postRoot(sessionId, body, generation);
|
|
931
|
+
return await this.#postRoot(sessionId, body, generation, requestedClientMsgId);
|
|
877
932
|
waitedForRollover = true;
|
|
878
933
|
await Bun.sleep(10);
|
|
879
934
|
}
|
|
@@ -1820,6 +1875,24 @@ export class SlackNotificationDaemon {
|
|
|
1820
1875
|
);
|
|
1821
1876
|
}
|
|
1822
1877
|
|
|
1878
|
+
/**
|
|
1879
|
+
* Reuse one provider identity while its outcome is pending or uncertain. Only a
|
|
1880
|
+
* terminal definite refusal advances to a fresh attempt identity, so safe retries
|
|
1881
|
+
* remain possible without turning an ambiguous acknowledgement into a duplicate.
|
|
1882
|
+
*/
|
|
1883
|
+
async #publicationAttempt(
|
|
1884
|
+
baseId: string,
|
|
1885
|
+
effectIdForClientMsgId: (clientMsgId: string) => string,
|
|
1886
|
+
): Promise<{ effectId: string; clientMsgId: string }> {
|
|
1887
|
+
for (let attempt = 1; attempt <= MAX_TERMINAL_CHAT_EFFECTS + 1; attempt++) {
|
|
1888
|
+
const occurrenceId = attempt === 1 ? baseId : `${baseId}:retry:${attempt}`;
|
|
1889
|
+
const clientMsgId = slackPublicationClientMsgId(occurrenceId);
|
|
1890
|
+
const effectId = effectIdForClientMsgId(clientMsgId);
|
|
1891
|
+
const existing = await this.#journal.read(effectId);
|
|
1892
|
+
if (existing?.state !== "terminal" || existing.receipt?.status === "posted") return { effectId, clientMsgId };
|
|
1893
|
+
}
|
|
1894
|
+
throw new Error("Slack publication exhausted its retained definite-failure identities");
|
|
1895
|
+
}
|
|
1823
1896
|
async #providerEffectPayload(
|
|
1824
1897
|
id: string,
|
|
1825
1898
|
sessionId: string,
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { getAgentDir } from "@gajae-code/utils";
|
|
4
4
|
import { ensureBroker } from "../broker/ensure";
|
|
5
|
+
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
5
6
|
import {
|
|
6
7
|
listSdkSessionEndpoints,
|
|
7
8
|
readSdkBrokerDiscovery,
|
|
@@ -242,7 +243,13 @@ export async function runSdkSessionCli(
|
|
|
242
243
|
throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for lifecycle operations.", 2);
|
|
243
244
|
const client = await connectBroker(agentDir);
|
|
244
245
|
try {
|
|
245
|
-
|
|
246
|
+
const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
|
|
247
|
+
writeOutput(
|
|
248
|
+
await client.global(operation, input, {
|
|
249
|
+
idempotencyKey,
|
|
250
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
246
253
|
} finally {
|
|
247
254
|
await client.close();
|
|
248
255
|
}
|