@gajae-code/coding-agent 0.18.5 → 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 +101 -0
- 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/model-profile-activation.d.ts +19 -0
- 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/exec/bash-shell-supervisor.d.ts +12 -0
- 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/runtime.d.ts +1 -1
- 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/index.d.ts +1 -0
- 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 -1
- 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/web-search-cli.ts +9 -3
- package/src/cli-commands.ts +14 -0
- package/src/cli-ordinary.ts +197 -0
- package/src/cli.ts +25 -182
- package/src/commands/contribution-prep.ts +1 -3
- package/src/commands/sdk.ts +59 -0
- package/src/commands/update.ts +15 -1
- package/src/config/file-lock.ts +91 -2
- package/src/config/model-profile-activation.ts +70 -4
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +23 -31
- 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/exec/bash-shell-supervisor.ts +60 -3
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/memories/index.ts +8 -2
- package/src/modes/acp/acp-agent.ts +649 -367
- package/src/modes/components/tool-execution.ts +1 -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 +225 -341
- package/src/sdk/broker/runtime.ts +5 -1
- package/src/sdk/broker/transport.ts +5 -1
- 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 +13 -5
- package/src/sdk/index.ts +1 -0
- package/src/sdk/lifecycle-session.ts +1 -0
- 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 +114 -39
- package/src/session/blob-store.ts +78 -33
- package/src/session/internal/managed-session-storage.ts +96 -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 +38 -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
|
@@ -133,6 +133,7 @@ import {
|
|
|
133
133
|
classifyFallbackTrigger,
|
|
134
134
|
EMPTY_RESPONSE_PROVIDER_CODE,
|
|
135
135
|
type FallbackAttemptToken,
|
|
136
|
+
PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE,
|
|
136
137
|
type FallbackTriggerClass,
|
|
137
138
|
SERVER_OVERLOADED_PROVIDER_CODE,
|
|
138
139
|
STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
|
|
@@ -1210,7 +1211,10 @@ export interface TrackedSendUserMessageOptions extends SendUserMessageOptions {
|
|
|
1210
1211
|
trackSubmission: true;
|
|
1211
1212
|
}
|
|
1212
1213
|
|
|
1213
|
-
type SendUserMessageDispatchOptions = SendUserMessageOptions & {
|
|
1214
|
+
type SendUserMessageDispatchOptions = SendUserMessageOptions & {
|
|
1215
|
+
trackSubmission?: boolean;
|
|
1216
|
+
expectedSdkRunToken?: string;
|
|
1217
|
+
};
|
|
1214
1218
|
|
|
1215
1219
|
type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
|
|
1216
1220
|
type InternalCustomMessageOptions = Pick<
|
|
@@ -1419,8 +1423,11 @@ const BARE_DEFAULT_WATCHDOG_ERROR =
|
|
|
1419
1423
|
const PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = "Provider stream timed out while waiting for the first event";
|
|
1420
1424
|
const WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = `Error: ${PROVIDER_FIRST_EVENT_TIMEOUT_ERROR}`;
|
|
1421
1425
|
const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream timed out while waiting for first event";
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1426
|
+
const BARE_DEFAULT_CODEX_RETRYABLE_ERROR =
|
|
1427
|
+
/^Codex error event(?:: .*)? \(code=(?:server_is_overloaded|server_error|internal_error)(?:, [^)]+)*\)$/;
|
|
1428
|
+
const BARE_DEFAULT_CODEX_RETRYABLE_CODES = new Set(["server_is_overloaded", "server_error", "internal_error"]);
|
|
1429
|
+
const BARE_DEFAULT_OVERLOAD_CODES = new Set([SERVER_OVERLOADED_PROVIDER_CODE]);
|
|
1430
|
+
/** Anthropic's typed capacity-overload `error.type`, the only Anthropic overload code admitted below. */
|
|
1424
1431
|
const ANTHROPIC_OVERLOADED_ERROR_TYPE = "overloaded_error";
|
|
1425
1432
|
const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
1426
1433
|
"anthropic-messages": new Set([
|
|
@@ -1501,8 +1508,8 @@ function isBareDefaultMessageOnlyFirstEventTimeout(message: AssistantMessage): b
|
|
|
1501
1508
|
);
|
|
1502
1509
|
}
|
|
1503
1510
|
/**
|
|
1504
|
-
* True when the attempt's structured evidence is exactly
|
|
1505
|
-
*
|
|
1511
|
+
* True when the attempt's structured evidence is exactly an admitted provider
|
|
1512
|
+
* code and nothing else: no status, no retained retry header,
|
|
1506
1513
|
* no second typed code, no error kind, and none of the transport's other
|
|
1507
1514
|
* observations (request size, first-event timing, endpoint class, provider retry
|
|
1508
1515
|
* ceiling). The code arrives statuslessly (a Codex error event, an HTTP 200
|
|
@@ -1511,15 +1518,18 @@ function isBareDefaultMessageOnlyFirstEventTimeout(message: AssistantMessage): b
|
|
|
1511
1518
|
* attempt is no longer provably that bare capacity rejection. The code is
|
|
1512
1519
|
* compared case-sensitively, matching the parser and the transport gate.
|
|
1513
1520
|
*/
|
|
1514
|
-
function isExactTypedOverloadFacts(
|
|
1521
|
+
function isExactTypedOverloadFacts(
|
|
1522
|
+
message: AssistantMessage,
|
|
1523
|
+
acceptedProviderCodes: ReadonlySet<string> = BARE_DEFAULT_OVERLOAD_CODES,
|
|
1524
|
+
): boolean {
|
|
1515
1525
|
if (message.errorKind !== undefined || message.errorStatus !== undefined) return false;
|
|
1516
1526
|
const facts = message.transportFailure;
|
|
1517
1527
|
if (!facts) return false;
|
|
1518
1528
|
return (
|
|
1519
|
-
facts.providerCode
|
|
1529
|
+
acceptedProviderCodes.has(facts.providerCode ?? "") &&
|
|
1520
1530
|
facts.status === undefined &&
|
|
1521
1531
|
facts.anthropicErrorType === undefined &&
|
|
1522
|
-
(facts.openaiErrorCode === undefined || facts.openaiErrorCode ===
|
|
1532
|
+
(facts.openaiErrorCode === undefined || facts.openaiErrorCode === facts.providerCode) &&
|
|
1523
1533
|
(facts.headers === undefined || Object.keys(facts.headers).length === 0) &&
|
|
1524
1534
|
facts.requestBytes === undefined &&
|
|
1525
1535
|
facts.firstEventElapsedMs === undefined &&
|
|
@@ -1548,8 +1558,9 @@ function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined
|
|
|
1548
1558
|
function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
|
|
1549
1559
|
return (
|
|
1550
1560
|
message.api === "openai-codex-responses" &&
|
|
1551
|
-
|
|
1552
|
-
|
|
1561
|
+
(isExactTypedOverloadFacts(message, BARE_DEFAULT_CODEX_RETRYABLE_CODES) ||
|
|
1562
|
+
(BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "") &&
|
|
1563
|
+
!hasBareDefaultRetryDisqualifyingFacts(message))) &&
|
|
1553
1564
|
!assistantMessageHasVisibleOrToolContent(message)
|
|
1554
1565
|
);
|
|
1555
1566
|
}
|
|
@@ -5077,6 +5088,7 @@ export class AgentSession {
|
|
|
5077
5088
|
if (lease) this.#deferredAgentEndLeases.delete(pending);
|
|
5078
5089
|
this.#startAgentEndPublication(pending, lease);
|
|
5079
5090
|
}
|
|
5091
|
+
|
|
5080
5092
|
#startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
|
|
5081
5093
|
this.#agentEndPublicationInFlight++;
|
|
5082
5094
|
const pendingScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
|
|
@@ -6166,7 +6178,10 @@ export class AgentSession {
|
|
|
6166
6178
|
path.basename(sessionModeStatePath(this.sessionManager.getCwd(), currentSessionId, active.skill)),
|
|
6167
6179
|
);
|
|
6168
6180
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
6169
|
-
const parsed = JSON.parse(raw) as { state?: { intent_contract?: unknown } };
|
|
6181
|
+
const parsed = JSON.parse(raw) as { current_phase?: unknown; state?: { intent_contract?: unknown } };
|
|
6182
|
+
// A persisted final spec moves to handoff. Its next-workflow/approval
|
|
6183
|
+
// asks are not interview rounds and must not require recorder metadata.
|
|
6184
|
+
if (parsed.current_phase !== "interviewing") return undefined;
|
|
6170
6185
|
const intentContract = parsed.state?.intent_contract;
|
|
6171
6186
|
if (intentContract === undefined) return "topology";
|
|
6172
6187
|
assertDeepInterviewIntentManifest(intentContract);
|
|
@@ -6318,7 +6333,10 @@ export class AgentSession {
|
|
|
6318
6333
|
},
|
|
6319
6334
|
};
|
|
6320
6335
|
}
|
|
6321
|
-
const transformedMessages = await this.#transformContext(
|
|
6336
|
+
const transformedMessages = await this.#transformContext(
|
|
6337
|
+
this.#withoutEphemeralCustomMessages(this.messages),
|
|
6338
|
+
options.signal,
|
|
6339
|
+
);
|
|
6322
6340
|
const convertedMessages = await this.#convertToLlm(transformedMessages);
|
|
6323
6341
|
const providerMessages = this.model
|
|
6324
6342
|
? normalizeMessagesForProvider(convertedMessages, this.model)
|
|
@@ -12297,9 +12315,16 @@ export class AgentSession {
|
|
|
12297
12315
|
return this.agent.activeResourceRunId;
|
|
12298
12316
|
}
|
|
12299
12317
|
|
|
12300
|
-
/**
|
|
12318
|
+
/**
|
|
12319
|
+
* All messages including custom types like BashExecutionMessage. Request-scoped
|
|
12320
|
+
* ephemeral copies are retained only in `agent.state.messages` to keep the provider
|
|
12321
|
+
* cache prefix stable; they are never part of this public view.
|
|
12322
|
+
*/
|
|
12301
12323
|
get messages(): AgentMessage[] {
|
|
12302
|
-
|
|
12324
|
+
const messages = this.agent.state.messages;
|
|
12325
|
+
return messages.some(message => message.role === "custom" && this.#isEphemeralCustomMessageType(message.customType))
|
|
12326
|
+
? this.#withoutEphemeralCustomMessages(messages)
|
|
12327
|
+
: messages;
|
|
12303
12328
|
}
|
|
12304
12329
|
get transcriptPromptGeneration(): number {
|
|
12305
12330
|
return this.#promptGeneration;
|
|
@@ -12389,9 +12414,16 @@ export class AgentSession {
|
|
|
12389
12414
|
let continuationSdkRunToken: string | undefined;
|
|
12390
12415
|
try {
|
|
12391
12416
|
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
12392
|
-
this
|
|
12417
|
+
if (!this.#isLatestRetainedEphemeralCopy(volatileProjectContextMessage)) {
|
|
12418
|
+
this.agent.appendMessage(volatileProjectContextMessage);
|
|
12419
|
+
}
|
|
12393
12420
|
const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
|
|
12394
|
-
if (
|
|
12421
|
+
if (
|
|
12422
|
+
untrustedMcpServerInstructionsMessage &&
|
|
12423
|
+
!this.#isLatestRetainedEphemeralCopy(untrustedMcpServerInstructionsMessage)
|
|
12424
|
+
) {
|
|
12425
|
+
this.agent.appendMessage(untrustedMcpServerInstructionsMessage);
|
|
12426
|
+
}
|
|
12395
12427
|
const hindsightState = this.getHindsightSessionState();
|
|
12396
12428
|
await hindsightState?.maybeRecallOnAgentStart();
|
|
12397
12429
|
hindsightRecall = hindsightState?.getRecallSnippetForInjection();
|
|
@@ -12429,7 +12461,6 @@ export class AgentSession {
|
|
|
12429
12461
|
});
|
|
12430
12462
|
if (continuationSdkRunToken === undefined) await this.#waitForPostPromptRecovery();
|
|
12431
12463
|
} finally {
|
|
12432
|
-
this.#removeEphemeralCustomMessages();
|
|
12433
12464
|
await this.#settleEndedInFlight(
|
|
12434
12465
|
inFlightPrompt,
|
|
12435
12466
|
continuationSdkRunToken === undefined ? "full" : "publication",
|
|
@@ -13293,6 +13324,21 @@ export class AgentSession {
|
|
|
13293
13324
|
);
|
|
13294
13325
|
}
|
|
13295
13326
|
|
|
13327
|
+
/**
|
|
13328
|
+
* Sent ephemeral copies stay in live state so the provider prefix is stable. A new
|
|
13329
|
+
* copy is only worth appending when its provider-visible content changed; an
|
|
13330
|
+
* identical one would just repeat tokens that still count against the window.
|
|
13331
|
+
*/
|
|
13332
|
+
#isLatestRetainedEphemeralCopy(message: CustomMessage): boolean {
|
|
13333
|
+
const messages = this.agent.state.messages;
|
|
13334
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
13335
|
+
const candidate = messages[index];
|
|
13336
|
+
if (candidate?.role !== "custom" || candidate.customType !== message.customType) continue;
|
|
13337
|
+
return JSON.stringify(candidate.content) === JSON.stringify(message.content);
|
|
13338
|
+
}
|
|
13339
|
+
return false;
|
|
13340
|
+
}
|
|
13341
|
+
|
|
13296
13342
|
#removeEphemeralCustomMessages(): void {
|
|
13297
13343
|
const messages = this.agent.state.messages;
|
|
13298
13344
|
const withoutEphemeralMessages = this.#withoutEphemeralCustomMessages(messages);
|
|
@@ -13988,8 +14034,6 @@ export class AgentSession {
|
|
|
13988
14034
|
throw new Error(formatNoCredentialOnboardingError(this.model.provider));
|
|
13989
14035
|
}
|
|
13990
14036
|
|
|
13991
|
-
this.#removeEphemeralCustomMessages();
|
|
13992
|
-
|
|
13993
14037
|
// Check if we need to compact before sending (catches aborted responses)
|
|
13994
14038
|
const lastAssistant = this.#findLastAssistantMessage();
|
|
13995
14039
|
if (lastAssistant && !options?.skipCompactionCheck) {
|
|
@@ -14164,9 +14208,16 @@ export class AgentSession {
|
|
|
14164
14208
|
messages.push(goalModeMessage);
|
|
14165
14209
|
}
|
|
14166
14210
|
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
14167
|
-
|
|
14211
|
+
if (!this.#isLatestRetainedEphemeralCopy(volatileProjectContextMessage)) {
|
|
14212
|
+
messages.push(volatileProjectContextMessage);
|
|
14213
|
+
}
|
|
14168
14214
|
const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
|
|
14169
|
-
if (
|
|
14215
|
+
if (
|
|
14216
|
+
untrustedMcpServerInstructionsMessage &&
|
|
14217
|
+
!this.#isLatestRetainedEphemeralCopy(untrustedMcpServerInstructionsMessage)
|
|
14218
|
+
) {
|
|
14219
|
+
messages.push(untrustedMcpServerInstructionsMessage);
|
|
14220
|
+
}
|
|
14170
14221
|
|
|
14171
14222
|
// Roster: one Phase A claim, revalidated and reused on every attempt;
|
|
14172
14223
|
// a superseded claim releases and the prompt proceeds without it.
|
|
@@ -14328,7 +14379,6 @@ export class AgentSession {
|
|
|
14328
14379
|
return;
|
|
14329
14380
|
throw error;
|
|
14330
14381
|
} finally {
|
|
14331
|
-
this.#removeEphemeralCustomMessages();
|
|
14332
14382
|
if (rosterClaim) {
|
|
14333
14383
|
this.agent.replaceMessages(
|
|
14334
14384
|
this.agent.state.messages.filter(
|
|
@@ -14606,6 +14656,7 @@ export class AgentSession {
|
|
|
14606
14656
|
onQueuedAfterAdmission?: (message: AgentMessage, cancelQueued: () => boolean) => void;
|
|
14607
14657
|
external?: boolean;
|
|
14608
14658
|
sdkRunToken?: string;
|
|
14659
|
+
expectedSdkRunToken?: string;
|
|
14609
14660
|
forceOneAtATime?: boolean;
|
|
14610
14661
|
allowDuringSessionTransition?: boolean;
|
|
14611
14662
|
allowCancelAndSubmit?: boolean;
|
|
@@ -14622,11 +14673,18 @@ export class AgentSession {
|
|
|
14622
14673
|
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
14623
14674
|
// Enqueue-time admission: the Agent only accepts a steer into a live,
|
|
14624
14675
|
// non-aborted run. Anything else (idle, unwinding after agent_end, another
|
|
14625
|
-
// prompt still in preflight, aborting) has no run to steer
|
|
14626
|
-
//
|
|
14627
|
-
//
|
|
14676
|
+
// prompt still in preflight, aborting) has no run to steer. Ordinary steers
|
|
14677
|
+
// become a sequential follow-up owned by the next turn; SDK turn-bound
|
|
14678
|
+
// steers fail closed rather than being delivered to a successor run.
|
|
14679
|
+
if (
|
|
14680
|
+
options?.expectedSdkRunToken !== undefined &&
|
|
14681
|
+
this.#activeSdkRunToken !== options.expectedSdkRunToken
|
|
14682
|
+
)
|
|
14683
|
+
throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
|
|
14628
14684
|
const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
14629
14685
|
if (!admission.admitted) {
|
|
14686
|
+
if (options?.expectedSdkRunToken !== undefined)
|
|
14687
|
+
throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
|
|
14630
14688
|
const queuedFollowUp = await this.#queueFollowUpAfterReservation(message, displayText, {
|
|
14631
14689
|
forceOneAtATime: options?.forceOneAtATime,
|
|
14632
14690
|
claimsGenuineUserIntent: options?.claimsGenuineUserIntent,
|
|
@@ -15779,6 +15837,7 @@ export class AgentSession {
|
|
|
15779
15837
|
onPromoted: onQueuedPromoted,
|
|
15780
15838
|
external: true,
|
|
15781
15839
|
sdkRunToken: internalOptions?.sdkRunToken,
|
|
15840
|
+
expectedSdkRunToken: internalOptions?.expectedSdkRunToken,
|
|
15782
15841
|
forceOneAtATime: options?.queuePolicy === "sequential",
|
|
15783
15842
|
onQueuedAfterAdmission: onTrackedQueued,
|
|
15784
15843
|
allowDuringSessionTransition: this.#hasCommittedSuccessorTrackedAdmission(options),
|
|
@@ -16145,6 +16204,9 @@ export class AgentSession {
|
|
|
16145
16204
|
this.#cachedWorkspaceTree = undefined;
|
|
16146
16205
|
this.#cachedWorkspaceTreeAt = 0;
|
|
16147
16206
|
this.#pendingWorkspaceTreeRescope = true;
|
|
16207
|
+
// Sent copies name the abandoned root; the rescope already rebuilds the stable
|
|
16208
|
+
// prompt, so there is no cache prefix left to preserve by keeping them.
|
|
16209
|
+
this.#removeEphemeralCustomMessages();
|
|
16148
16210
|
}
|
|
16149
16211
|
|
|
16150
16212
|
/** Skill loading warnings captured by SDK */
|
|
@@ -20282,7 +20344,7 @@ export class AgentSession {
|
|
|
20282
20344
|
}
|
|
20283
20345
|
|
|
20284
20346
|
const handoffText = await generateHandoff(
|
|
20285
|
-
this.agent.state.messages,
|
|
20347
|
+
this.#withoutEphemeralCustomMessages(this.agent.state.messages),
|
|
20286
20348
|
model,
|
|
20287
20349
|
apiKey,
|
|
20288
20350
|
{
|
|
@@ -20516,7 +20578,7 @@ export class AgentSession {
|
|
|
20516
20578
|
sessionId: this.sessionId,
|
|
20517
20579
|
cwd: this.sessionManager.getCwd(),
|
|
20518
20580
|
sessionFile: this.sessionFile,
|
|
20519
|
-
messages: this.agent.state.messages,
|
|
20581
|
+
messages: this.#withoutEphemeralCustomMessages(this.agent.state.messages),
|
|
20520
20582
|
customInstructions: options.customInstructions,
|
|
20521
20583
|
},
|
|
20522
20584
|
options,
|
|
@@ -22699,6 +22761,7 @@ export class AgentSession {
|
|
|
22699
22761
|
willRetry && !continuationSkipReason
|
|
22700
22762
|
? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
|
|
22701
22763
|
: false;
|
|
22764
|
+
|
|
22702
22765
|
await this.#emitSessionEvent({
|
|
22703
22766
|
type: "auto_compaction_end",
|
|
22704
22767
|
action,
|
|
@@ -22707,6 +22770,7 @@ export class AgentSession {
|
|
|
22707
22770
|
willRetry: overflowContinuationScheduled,
|
|
22708
22771
|
continuationSkipReason,
|
|
22709
22772
|
});
|
|
22773
|
+
|
|
22710
22774
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
22711
22775
|
|
|
22712
22776
|
if (willRetry) {
|
|
@@ -22940,6 +23004,7 @@ export class AgentSession {
|
|
|
22940
23004
|
return false;
|
|
22941
23005
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
22942
23006
|
if (classifyContextOverflow(message, transportFailure, contextWindow)) return false;
|
|
23007
|
+
if (isBareDefaultCodexOverload(message) && this.settings.get("retry.enabled") !== false) return true;
|
|
22943
23008
|
const managedFallback = this.#defaultFallbackChain().chain.entries.length > 1;
|
|
22944
23009
|
// An account-specific model rejection that cannot rotate to another
|
|
22945
23010
|
// credential stays terminal only on the session's own retry path; managed
|
|
@@ -23082,6 +23147,9 @@ export class AgentSession {
|
|
|
23082
23147
|
// Without this the message carries no transport facts and would fall
|
|
23083
23148
|
// through to "unknown", which is admitted for bounded retry (#5627).
|
|
23084
23149
|
if (message.errorCode === REPETITION_GUARD_ERROR_CODE) return "terminal";
|
|
23150
|
+
// A 2xx body in the wrong protocol replays identically; its embedded body text
|
|
23151
|
+
// (e.g. "HTTP 503") must not be reparsed as a transient failure.
|
|
23152
|
+
if (message.errorCode === PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE) return "terminal";
|
|
23085
23153
|
if (message.errorKind === "local_snapshot_failure") return "local_snapshot";
|
|
23086
23154
|
if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
|
|
23087
23155
|
if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
|
|
@@ -24301,6 +24369,7 @@ export class AgentSession {
|
|
|
24301
24369
|
isBareDefaultCodexOverload(message) ||
|
|
24302
24370
|
isBareDefaultAnthropicOverload(message) ||
|
|
24303
24371
|
isBareDefaultOpenAIResponsesOverload(message);
|
|
24372
|
+
const canReplayCodexProviderOverload = isBareDefaultCodexOverload(message);
|
|
24304
24373
|
const reportedRetryMaxAttempts = transportFailure?.retryMaxAttempts;
|
|
24305
24374
|
if (reportedRetryMaxAttempts !== undefined) {
|
|
24306
24375
|
this.#providerRetryMaxAttempts = Math.min(
|
|
@@ -24340,19 +24409,24 @@ export class AgentSession {
|
|
|
24340
24409
|
}
|
|
24341
24410
|
: false;
|
|
24342
24411
|
}
|
|
24412
|
+
const fallbackTrigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
24343
24413
|
const trigger:
|
|
24344
24414
|
| { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
|
|
24345
|
-
| undefined =
|
|
24415
|
+
| undefined = canReplayCodexProviderOverload ? { class: "server" } : fallbackTrigger;
|
|
24346
24416
|
// OpenAI's typed statusless capacity-overload code (issue #5018) must not
|
|
24347
24417
|
// gain managed-chain retry/advance authority from its new facts. Before
|
|
24348
24418
|
// the code survived transport, this failure reached the session as an
|
|
24349
24419
|
// ordinary committed error and surfaced immediately, so mirror that
|
|
24350
|
-
// behavior with the existing exhaustion decision.
|
|
24420
|
+
// behavior with the existing exhaustion decision. However, when called from
|
|
24421
|
+
// the agent_end path (managedOutcome=false), we must return false to allow
|
|
24422
|
+
// proper session termination handling.
|
|
24351
24423
|
if (managedFallback && isStatuslessTypedOverloadFacts(transportFailure)) {
|
|
24352
|
-
return
|
|
24353
|
-
|
|
24354
|
-
|
|
24355
|
-
|
|
24424
|
+
return managedOutcome
|
|
24425
|
+
? this.#managedFallbackExhaustionDecision(
|
|
24426
|
+
message,
|
|
24427
|
+
message.errorMessage || "Model fallback attempt failed",
|
|
24428
|
+
)
|
|
24429
|
+
: false;
|
|
24356
24430
|
}
|
|
24357
24431
|
if (!trigger) {
|
|
24358
24432
|
return managedOutcome
|
|
@@ -26036,7 +26110,8 @@ export class AgentSession {
|
|
|
26036
26110
|
|
|
26037
26111
|
/** Build a background snapshot with in-flight assistant and optional context. */
|
|
26038
26112
|
#buildEphemeralSnapshot(promptText: string, prependMessages?: AgentMessage[]): AgentMessage[] {
|
|
26039
|
-
|
|
26113
|
+
// Side requests run in a fresh provider session with no prefix to preserve.
|
|
26114
|
+
const messages = this.#withoutEphemeralCustomMessages(this.messages);
|
|
26040
26115
|
const streaming = this.agent.state.streamMessage;
|
|
26041
26116
|
if (streaming && streaming.role === "assistant") {
|
|
26042
26117
|
const preservedBlocks: AssistantMessage["content"] = [];
|
|
@@ -27268,7 +27343,7 @@ export class AgentSession {
|
|
|
27268
27343
|
const branchEntries = this.sessionManager.getBranch();
|
|
27269
27344
|
const latestCompaction = getLatestCompactionEntry(branchEntries);
|
|
27270
27345
|
const boundaryTs = latestCompaction ? new Date(latestCompaction.timestamp).getTime() : 0;
|
|
27271
|
-
const anchor = this.#findAnchorableUsageIndex(this.messages, boundaryTs);
|
|
27346
|
+
const anchor = this.#findAnchorableUsageIndex(this.agent.state.messages, boundaryTs);
|
|
27272
27347
|
let value: ContextUsage;
|
|
27273
27348
|
if (latestCompaction && !anchor) {
|
|
27274
27349
|
value = { tokens: null, contextWindow, percent: null, source: "unknown" };
|
|
@@ -27291,7 +27366,7 @@ export class AgentSession {
|
|
|
27291
27366
|
}
|
|
27292
27367
|
|
|
27293
27368
|
#contextUsageCacheKey(model: Model, contextWindow: number): string {
|
|
27294
|
-
const messages = this.messages;
|
|
27369
|
+
const messages = this.agent.state.messages;
|
|
27295
27370
|
const lastMessage = messages[messages.length - 1];
|
|
27296
27371
|
// Entry and leaf revisions change whenever the active branch changes, avoiding getBranch() on warm reads.
|
|
27297
27372
|
const revision = this.sessionManager.revisionSnapshot();
|
|
@@ -27440,7 +27515,7 @@ export class AgentSession {
|
|
|
27440
27515
|
* raw promptTokens/estimatedTokens quotient). Clamped to [0.5, 2] downstream.
|
|
27441
27516
|
*/
|
|
27442
27517
|
#computeCompactionTokenCorrectionRatio(): number | undefined {
|
|
27443
|
-
const messages = this.messages;
|
|
27518
|
+
const messages = this.agent.state.messages;
|
|
27444
27519
|
let lastUsageIndex = -1;
|
|
27445
27520
|
let lastUsage: Usage | undefined;
|
|
27446
27521
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -27490,7 +27565,7 @@ export class AgentSession {
|
|
|
27490
27565
|
tokens: number;
|
|
27491
27566
|
anchored: boolean;
|
|
27492
27567
|
} {
|
|
27493
|
-
const messages = this.messages;
|
|
27568
|
+
const messages = this.agent.state.messages;
|
|
27494
27569
|
let anchor = knownAnchor;
|
|
27495
27570
|
if (boundaryTs === undefined) {
|
|
27496
27571
|
const latestCompaction = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
@@ -27666,7 +27741,7 @@ export class AgentSession {
|
|
|
27666
27741
|
*/
|
|
27667
27742
|
formatSessionAsText(): string {
|
|
27668
27743
|
return formatSessionDumpText({
|
|
27669
|
-
messages: this.messages,
|
|
27744
|
+
messages: this.#withoutEphemeralCustomMessages(this.messages),
|
|
27670
27745
|
systemPrompt: this.agent.state.systemPrompt,
|
|
27671
27746
|
model: this.agent.state.model,
|
|
27672
27747
|
thinkingLevel: this.#thinkingLevel,
|
|
@@ -4,6 +4,7 @@ import * as fsp from "node:fs/promises";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { exactUnlink } from "@gajae-code/natives";
|
|
6
6
|
import { isEnoent, logger, postmortem } from "@gajae-code/utils";
|
|
7
|
+
import { readLinuxProcStartTimeSync } from "../gjc-runtime/linux-proc";
|
|
7
8
|
|
|
8
9
|
const BLOB_PREFIX = "blob:sha256:";
|
|
9
10
|
const CANONICAL_BLOB_NAME = /^[0-9a-f]{64}$/;
|
|
@@ -46,10 +47,21 @@ const RESIDENT_CACHE_SWEEP_EXPECTED_IO_CODES = new Set([
|
|
|
46
47
|
]);
|
|
47
48
|
const ownedResidentCacheInstanceDirs = new Set<string>();
|
|
48
49
|
const activeResidentCacheRootSweeps = new Set<string>();
|
|
49
|
-
let
|
|
50
|
+
let ownResidentCacheProcessStart: ResidentCacheProcessStart | undefined;
|
|
50
51
|
|
|
51
|
-
/** Marks `startTimeMs` as an absolute instant parsed from a UTC-pinned `ps` render. */
|
|
52
|
-
const
|
|
52
|
+
/** Marks `startTimeMs` as an absolute instant parsed from a UTC-pinned `ps` render (non-Linux). */
|
|
53
|
+
const RESIDENT_CACHE_UTC_START_TIME_BASIS = "utc";
|
|
54
|
+
/**
|
|
55
|
+
* Prefix for a Linux start identity: `startTimeMs` holds `/proc/<pid>/stat` field 22
|
|
56
|
+
* (clock ticks since boot) and the suffix is the kernel `boot_id`. Ticks are immune
|
|
57
|
+
* to wall-clock adjustment; the boot id scopes them to one kernel boot.
|
|
58
|
+
*/
|
|
59
|
+
const RESIDENT_CACHE_LINUX_START_TIME_BASIS_PREFIX = "linux-proc-ticks:";
|
|
60
|
+
|
|
61
|
+
interface ResidentCacheProcessStart {
|
|
62
|
+
readonly startTimeMs: number | null;
|
|
63
|
+
readonly basis: string;
|
|
64
|
+
}
|
|
53
65
|
|
|
54
66
|
interface ResidentCacheOwnerToken {
|
|
55
67
|
readonly pid: number;
|
|
@@ -240,19 +252,47 @@ function assertResidentCacheDirectoryPathMatchesDescriptor(pathname: string, des
|
|
|
240
252
|
assertResidentCacheDirectoryDescriptor(pathname, current, descriptor, uid);
|
|
241
253
|
}
|
|
242
254
|
|
|
243
|
-
|
|
244
|
-
if (pid === process.pid && ownResidentCacheProcessStartTimeMs !== undefined) {
|
|
245
|
-
return ownResidentCacheProcessStartTimeMs;
|
|
246
|
-
}
|
|
255
|
+
let residentCacheLinuxBootId: string | null | undefined;
|
|
247
256
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
function readResidentCacheLinuxBootId(): string | null {
|
|
258
|
+
if (residentCacheLinuxBootId !== undefined) return residentCacheLinuxBootId;
|
|
259
|
+
let bootId: string | null = null;
|
|
260
|
+
try {
|
|
261
|
+
const text = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
262
|
+
if (/^[0-9a-f-]{36}$/.test(text)) bootId = text;
|
|
263
|
+
} catch {
|
|
264
|
+
// Without a boot id, tick counts cannot be scoped to one boot.
|
|
265
|
+
}
|
|
266
|
+
residentCacheLinuxBootId = bootId;
|
|
267
|
+
return bootId;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function residentCacheProcessStart(pid: number): ResidentCacheProcessStart {
|
|
271
|
+
if (pid === process.pid && ownResidentCacheProcessStart !== undefined) return ownResidentCacheProcessStart;
|
|
272
|
+
const start = process.platform === "linux" ? residentCacheLinuxProcessStart(pid) : residentCacheUtcProcessStart(pid);
|
|
273
|
+
if (pid === process.pid) ownResidentCacheProcessStart = start;
|
|
274
|
+
return start;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// `ps -o lstart` is NOT a stable identity on Linux: procps renders it as
|
|
278
|
+
// `btime + starttime_ticks`, and `btime` is re-derived from the current wall clock
|
|
279
|
+
// minus uptime. On hosts whose clock is continuously slewed (WSL2 is the common
|
|
280
|
+
// case) `btime` drifts, so the very same live process reports a start time that
|
|
281
|
+
// moves by seconds to minutes over a session. Any drift reads as PID reuse and the
|
|
282
|
+
// GC reaps a running session's cache. The raw tick count never moves.
|
|
283
|
+
function residentCacheLinuxProcessStart(pid: number): ResidentCacheProcessStart {
|
|
284
|
+
const bootId = readResidentCacheLinuxBootId();
|
|
285
|
+
if (bootId === null) return { startTimeMs: null, basis: RESIDENT_CACHE_LINUX_START_TIME_BASIS_PREFIX };
|
|
286
|
+
const basis = `${RESIDENT_CACHE_LINUX_START_TIME_BASIS_PREFIX}${bootId}`;
|
|
287
|
+
const ticks = readLinuxProcStartTimeSync(pid);
|
|
288
|
+
const startTimeMs = ticks === null ? Number.NaN : Number(ticks);
|
|
289
|
+
return { startTimeMs: Number.isSafeInteger(startTimeMs) ? startTimeMs : null, basis };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function residentCacheUtcProcessStart(pid: number): ResidentCacheProcessStart {
|
|
293
|
+
// Pin the locale AND the zone. `lstart` is a zoneless local wall clock rendered
|
|
294
|
+
// by `ps` from /etc/localtime, while `Date.parse` of a zoneless string binds it
|
|
295
|
+
// to the JS runtime's own zone. Pinning TZ on the child fixes the rendering
|
|
256
296
|
// side; the explicit GMT suffix fixes the parsing side.
|
|
257
297
|
let startTimeMs: number | null = null;
|
|
258
298
|
try {
|
|
@@ -269,8 +309,7 @@ function residentCacheProcessStartTimeMs(pid: number): number | null {
|
|
|
269
309
|
} catch {
|
|
270
310
|
// An unavailable process-start source cannot prove PID reuse.
|
|
271
311
|
}
|
|
272
|
-
|
|
273
|
-
return startTimeMs;
|
|
312
|
+
return { startTimeMs, basis: RESIDENT_CACHE_UTC_START_TIME_BASIS };
|
|
274
313
|
}
|
|
275
314
|
|
|
276
315
|
type ResidentCacheOwnerLiveness = "alive" | "dead" | "unknown";
|
|
@@ -376,10 +415,11 @@ function readResidentCacheOwnerSnapshot(instanceDir: string, uid: number): Resid
|
|
|
376
415
|
function writeResidentCacheOwnerToken(instanceDir: string, uid: number): void {
|
|
377
416
|
const nonce = path.basename(instanceDir).slice("i-".length);
|
|
378
417
|
if (!nonce) throw new ResidentCacheTrustError("instance_nonce_missing", instanceDir);
|
|
418
|
+
const ownStart = residentCacheProcessStart(process.pid);
|
|
379
419
|
const owner: ResidentCacheOwnerToken = {
|
|
380
420
|
pid: process.pid,
|
|
381
|
-
startTimeMs:
|
|
382
|
-
startTimeBasis:
|
|
421
|
+
startTimeMs: ownStart.startTimeMs,
|
|
422
|
+
startTimeBasis: ownStart.basis,
|
|
383
423
|
nonce,
|
|
384
424
|
createdAt: Date.now(),
|
|
385
425
|
};
|
|
@@ -579,26 +619,31 @@ function residentCacheSweepLimit(value: number | undefined, ceiling: number): nu
|
|
|
579
619
|
return Math.max(0, Math.min(Math.floor(value), ceiling));
|
|
580
620
|
}
|
|
581
621
|
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
622
|
+
function cachedResidentCacheProcessStart(
|
|
623
|
+
pid: number,
|
|
624
|
+
cache: Map<number, ResidentCacheProcessStart>,
|
|
625
|
+
): ResidentCacheProcessStart {
|
|
626
|
+
const cached = cache.get(pid);
|
|
627
|
+
if (cached !== undefined) return cached;
|
|
628
|
+
const start = residentCacheProcessStart(pid);
|
|
629
|
+
cache.set(pid, start);
|
|
630
|
+
return start;
|
|
587
631
|
}
|
|
588
632
|
|
|
589
633
|
function residentCacheOwnerIsStale(
|
|
590
634
|
owner: ResidentCacheOwnerToken,
|
|
591
|
-
startTimeCache: Map<number,
|
|
635
|
+
startTimeCache: Map<number, ResidentCacheProcessStart>,
|
|
592
636
|
): boolean {
|
|
593
637
|
const liveness = residentCacheOwnerLiveness(owner.pid);
|
|
594
638
|
if (liveness === "dead") return true;
|
|
595
639
|
if (liveness !== "alive" || owner.startTimeMs === null) return false;
|
|
596
|
-
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
640
|
+
const current = cachedResidentCacheProcessStart(owner.pid, startTimeCache);
|
|
641
|
+
// Start times are comparable only on the same basis (and, on Linux, the same
|
|
642
|
+
// boot). A token from an older writer (unlabelled zone-dependent wall clock, or
|
|
643
|
+
// the drifting `ps` render) or another basis leaves reuse unproven, so a live
|
|
644
|
+
// owner is never reaped on a basis mismatch.
|
|
645
|
+
if (owner.startTimeBasis !== current.basis) return false;
|
|
646
|
+
return current.startTimeMs !== null && current.startTimeMs !== owner.startTimeMs;
|
|
602
647
|
}
|
|
603
648
|
|
|
604
649
|
function reapResidentCacheInstanceDir(
|
|
@@ -681,7 +726,7 @@ export async function sweepResidentCacheRoot(root: string, options: ResidentCach
|
|
|
681
726
|
// cache state behind.
|
|
682
727
|
rootDescriptor = openVerifiedResidentCacheDirectory(root, uid);
|
|
683
728
|
assertResidentCacheDirectoryPathMatchesDescriptor(root, rootDescriptor, uid);
|
|
684
|
-
const startTimeCache = new Map<number,
|
|
729
|
+
const startTimeCache = new Map<number, ResidentCacheProcessStart>();
|
|
685
730
|
const entries = fs.opendirSync(root);
|
|
686
731
|
try {
|
|
687
732
|
let examined = 0;
|
|
@@ -746,7 +791,7 @@ function openVerifiedCacheInstanceDir(root: string, instanceName?: string): stri
|
|
|
746
791
|
const staleInstance = readResidentCacheOwnerSnapshot(candidate, uid);
|
|
747
792
|
if (
|
|
748
793
|
staleInstance === null ||
|
|
749
|
-
!residentCacheOwnerIsStale(staleInstance.owner, new Map<number,
|
|
794
|
+
!residentCacheOwnerIsStale(staleInstance.owner, new Map<number, ResidentCacheProcessStart>()) ||
|
|
750
795
|
!reapResidentCacheInstanceDir(root, rootDescriptor, candidate, staleInstance, uid)
|
|
751
796
|
) {
|
|
752
797
|
throw error;
|