@gajae-code/coding-agent 0.12.19 → 0.12.21
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 +27 -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 +302 -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/tools/todo-contract.d.ts +10 -1
- 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 +541 -226
- 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 +7057 -399
- 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/descriptors.test.ts +51 -0
- package/src/tools/find.ts +55 -26
- package/src/tools/todo-contract.ts +82 -13
- package/src/tools/todo-write.ts +2 -5
- package/src/tui/output-block.ts +11 -1
|
@@ -431,8 +431,8 @@ import type {
|
|
|
431
431
|
RecoveryHydrationPromotionFence,
|
|
432
432
|
SessionContext,
|
|
433
433
|
SessionEntry,
|
|
434
|
-
SessionManager,
|
|
435
434
|
SessionManagerCloseOutcome,
|
|
435
|
+
SessionMemoryStats,
|
|
436
436
|
} from "./session-manager";
|
|
437
437
|
|
|
438
438
|
import {
|
|
@@ -441,6 +441,8 @@ import {
|
|
|
441
441
|
getSessionMessageEntryId,
|
|
442
442
|
getSessionMessageObservationId,
|
|
443
443
|
SessionAppendPersistenceError,
|
|
444
|
+
SessionContextTooLargeError,
|
|
445
|
+
SessionManager,
|
|
444
446
|
transferSessionMessageIdentity,
|
|
445
447
|
} from "./session-manager";
|
|
446
448
|
import { getEntriesForInternalRead, getSessionContextForInternalRead } from "./session-manager-internal";
|
|
@@ -744,6 +746,17 @@ type AutoCompactionTerminalStatus =
|
|
|
744
746
|
| { kind: "skipped"; continuationScheduled?: boolean }
|
|
745
747
|
| { kind: "failed" };
|
|
746
748
|
|
|
749
|
+
/**
|
|
750
|
+
* R3.2 pre-submit seam: `build` assembles the attempt messages (Phase A once,
|
|
751
|
+
* Phase B per attempt) and may throw the typed overflow error; `reset` drops the
|
|
752
|
+
* cached Phase B attempt messages so the next `build` rebuilds them (used after a
|
|
753
|
+
* forced compaction retry).
|
|
754
|
+
*/
|
|
755
|
+
type PreSubmitBuilder = {
|
|
756
|
+
build: () => Promise<AgentMessage[] | null>;
|
|
757
|
+
reset: () => void;
|
|
758
|
+
};
|
|
759
|
+
|
|
747
760
|
/** Options for AgentSession.prompt() */
|
|
748
761
|
export interface PromptOptions {
|
|
749
762
|
/** Whether to expand file-based prompt templates (default: true) */
|
|
@@ -769,8 +782,8 @@ export interface PromptOptions {
|
|
|
769
782
|
*/
|
|
770
783
|
onPreflightAccepted?: () => void;
|
|
771
784
|
/**
|
|
772
|
-
* Awaitable durable-accept fence. Called after preflight and before
|
|
773
|
-
*
|
|
785
|
+
* Awaitable durable-accept fence. Called after preflight and immediately before
|
|
786
|
+
* agent execution begins. SDK bus installs a closure that fsyncs acceptance.
|
|
774
787
|
*/
|
|
775
788
|
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
776
789
|
/** Skill-only: prepared metadata before the durable fence (path/lineCount/cleanedArgs). */
|
|
@@ -904,6 +917,7 @@ export interface SessionStats {
|
|
|
904
917
|
premiumRequests: number;
|
|
905
918
|
cost: number;
|
|
906
919
|
costBreakdown?: Usage["cost"];
|
|
920
|
+
sessionMemory: SessionMemoryStats;
|
|
907
921
|
}
|
|
908
922
|
|
|
909
923
|
/** Internal marker for hook messages queued through the agent loop */
|
|
@@ -1798,7 +1812,7 @@ function deobfuscateSessionContext(context: SessionContext, obfuscator: SecretOb
|
|
|
1798
1812
|
|
|
1799
1813
|
export class AgentSession {
|
|
1800
1814
|
readonly agent: Agent;
|
|
1801
|
-
|
|
1815
|
+
sessionManager: SessionManager;
|
|
1802
1816
|
readonly settings: Settings;
|
|
1803
1817
|
readonly memoryBackend: LazyService<MemoryBackend>;
|
|
1804
1818
|
readonly notificationSessionController: NotificationSessionController | undefined;
|
|
@@ -2015,7 +2029,11 @@ export class AgentSession {
|
|
|
2015
2029
|
|
|
2016
2030
|
// Bash execution state
|
|
2017
2031
|
#bashAbortControllers = new Set<AbortController>();
|
|
2018
|
-
#pendingBashMessages:
|
|
2032
|
+
#pendingBashMessages: Array<{
|
|
2033
|
+
message: BashExecutionMessage;
|
|
2034
|
+
onPersisted?: () => void;
|
|
2035
|
+
appendedToAgent: boolean;
|
|
2036
|
+
}> = [];
|
|
2019
2037
|
#foregroundBashBackgroundRequestHandler: (() => void) | undefined;
|
|
2020
2038
|
|
|
2021
2039
|
// Python execution state
|
|
@@ -2024,6 +2042,7 @@ export class AgentSession {
|
|
|
2024
2042
|
/** Idempotent unregister handle for this session's resource-GC registration. */
|
|
2025
2043
|
#unregisterResourceGc?: () => void;
|
|
2026
2044
|
#unregisterRuntimeStateFinalizer?: () => void;
|
|
2045
|
+
#unregisterSessionMemorySettings?: () => void;
|
|
2027
2046
|
/**
|
|
2028
2047
|
* AsyncJobManager owned by this session (top-level only). Subagents leave
|
|
2029
2048
|
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
@@ -2031,7 +2050,11 @@ export class AgentSession {
|
|
|
2031
2050
|
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
2032
2051
|
readonly #ownedMcpManager: MCPManager | undefined;
|
|
2033
2052
|
#startupTurnBarrier: Promise<void> | undefined;
|
|
2034
|
-
#pendingPythonMessages:
|
|
2053
|
+
#pendingPythonMessages: Array<{
|
|
2054
|
+
message: PythonExecutionMessage;
|
|
2055
|
+
onPersisted?: () => void;
|
|
2056
|
+
appendedToAgent: boolean;
|
|
2057
|
+
}> = [];
|
|
2035
2058
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
2036
2059
|
#evalExecutionDisposing = false;
|
|
2037
2060
|
|
|
@@ -2584,14 +2607,62 @@ export class AgentSession {
|
|
|
2584
2607
|
}
|
|
2585
2608
|
}
|
|
2586
2609
|
|
|
2587
|
-
#endInFlight():
|
|
2610
|
+
#endInFlight(): unknown {
|
|
2588
2611
|
this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
|
|
2589
|
-
if (this.#promptInFlightCount
|
|
2590
|
-
|
|
2591
|
-
|
|
2612
|
+
if (this.#promptInFlightCount !== 0) return undefined;
|
|
2613
|
+
|
|
2614
|
+
this.#releasePowerAssertion();
|
|
2615
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
2616
|
+
let flushError: unknown;
|
|
2617
|
+
try {
|
|
2618
|
+
// The turn is over, so nothing can split a tool_use/tool_result pair any
|
|
2619
|
+
// more: a `!`/`$` block that finished mid-stream must own its place in
|
|
2620
|
+
// agent state and the session now, not at the next prompt. Until it does,
|
|
2621
|
+
// the TUI shows output the transcript lacks and `onPersisted` stays unfired,
|
|
2622
|
+
// so a rebuild in that gap drops the only rendering of the execution.
|
|
2623
|
+
this.#flushPendingPromptMessages();
|
|
2624
|
+
} catch (error) {
|
|
2625
|
+
flushError = error;
|
|
2626
|
+
}
|
|
2627
|
+
this.#flushPendingAgentEnd();
|
|
2628
|
+
return flushError;
|
|
2629
|
+
}
|
|
2630
|
+
async #settleEndedInFlight(promptWait?: "publication" | "full"): Promise<void> {
|
|
2631
|
+
const flushError = this.#endInFlight();
|
|
2632
|
+
const predecessorPromptStillInFlight = this.#promptInFlightCount > 0;
|
|
2633
|
+
if (promptWait === "publication") {
|
|
2634
|
+
await this.#agentEndPublicationPromise;
|
|
2635
|
+
} else if (promptWait === "full") {
|
|
2636
|
+
await this.#agentEndHandlingPromise;
|
|
2637
|
+
await this.#waitForPostPromptRecovery();
|
|
2638
|
+
await this.#agentEndPublicationPromise;
|
|
2639
|
+
}
|
|
2640
|
+
// A post-prompt continuation runs before its predecessor prompt returns. Its
|
|
2641
|
+
// publication must settle, but session-wide settlement can still be owned by
|
|
2642
|
+
// that predecessor; waiting here would make each prompt wait on the other.
|
|
2643
|
+
if (!predecessorPromptStillInFlight) await this.#waitForSessionSettlement();
|
|
2644
|
+
if (flushError) throw flushError;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
#flushPendingPromptMessages(): void {
|
|
2648
|
+
const errors: unknown[] = [];
|
|
2649
|
+
try {
|
|
2650
|
+
this.#flushPendingBashMessages();
|
|
2651
|
+
} catch (error) {
|
|
2652
|
+
errors.push(error);
|
|
2653
|
+
}
|
|
2654
|
+
try {
|
|
2655
|
+
this.#flushPendingPythonMessages();
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
errors.push(error);
|
|
2658
|
+
}
|
|
2659
|
+
try {
|
|
2592
2660
|
this.#flushPendingBackgroundExchanges();
|
|
2593
|
-
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
errors.push(error);
|
|
2594
2663
|
}
|
|
2664
|
+
if (errors.length === 1) throw errors[0];
|
|
2665
|
+
if (errors.length > 1) throw new AggregateError(errors, "Multiple deferred prompt messages failed to flush");
|
|
2595
2666
|
}
|
|
2596
2667
|
|
|
2597
2668
|
#refreshTeamWorkerHeartbeat(): void {
|
|
@@ -2670,6 +2741,12 @@ export class AgentSession {
|
|
|
2670
2741
|
this.agent.bindRunCancellationDomainBridge(this.#runCancellationDomains, this.#agentSessionClaimKey);
|
|
2671
2742
|
this.sessionManager = config.sessionManager;
|
|
2672
2743
|
this.settings = config.settings;
|
|
2744
|
+
this.sessionManager.setSessionMemoryMode(this.settings.get("sessionMemory.mode"));
|
|
2745
|
+
this.#unregisterSessionMemorySettings = this.settings.onChanged(settingPath => {
|
|
2746
|
+
if (settingPath === "sessionMemory.mode") {
|
|
2747
|
+
this.sessionManager.setSessionMemoryMode(this.settings.get("sessionMemory.mode"));
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2673
2750
|
this.memoryBackend = config.memoryBackend ?? createMemoryBackendService(this.settings);
|
|
2674
2751
|
this.#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(
|
|
2675
2752
|
config.workerIntegrationRequest ??
|
|
@@ -6256,6 +6333,8 @@ export class AgentSession {
|
|
|
6256
6333
|
// No-op when this session opened no tabs. Failure is logged, not thrown.
|
|
6257
6334
|
this.#unregisterResourceGc?.();
|
|
6258
6335
|
this.#unregisterResourceGc = undefined;
|
|
6336
|
+
this.#unregisterSessionMemorySettings?.();
|
|
6337
|
+
this.#unregisterSessionMemorySettings = undefined;
|
|
6259
6338
|
if (ownerTerminalContextFromEnvironment() === null) this.#unregisterRuntimeStateFinalizer?.();
|
|
6260
6339
|
this.#unregisterRuntimeStateFinalizer = undefined;
|
|
6261
6340
|
await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
|
|
@@ -6320,6 +6399,8 @@ export class AgentSession {
|
|
|
6320
6399
|
const kernelOwnerId = this.#evalKernelOwnerId;
|
|
6321
6400
|
this.#unregisterResourceGc?.();
|
|
6322
6401
|
this.#unregisterResourceGc = undefined;
|
|
6402
|
+
this.#unregisterSessionMemorySettings?.();
|
|
6403
|
+
this.#unregisterSessionMemorySettings = undefined;
|
|
6323
6404
|
this.#unregisterTeamWorkerAsyncJobChange?.();
|
|
6324
6405
|
this.#unregisterTeamWorkerAsyncJobChange = undefined;
|
|
6325
6406
|
this.#teamWorkerHeartbeat?.dispose();
|
|
@@ -7604,8 +7685,7 @@ export class AgentSession {
|
|
|
7604
7685
|
await this.#waitForPostPromptRecovery();
|
|
7605
7686
|
} finally {
|
|
7606
7687
|
this.#removeEphemeralCustomMessages();
|
|
7607
|
-
this.#
|
|
7608
|
-
await this.#waitForSessionSettlement();
|
|
7688
|
+
await this.#settleEndedInFlight();
|
|
7609
7689
|
}
|
|
7610
7690
|
}
|
|
7611
7691
|
|
|
@@ -8799,10 +8879,8 @@ export class AgentSession {
|
|
|
8799
8879
|
this.#overflowMaintenanceAttempts = 0;
|
|
8800
8880
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
8801
8881
|
}
|
|
8802
|
-
//
|
|
8803
|
-
this.#
|
|
8804
|
-
this.#flushPendingPythonMessages();
|
|
8805
|
-
this.#flushPendingBackgroundExchanges();
|
|
8882
|
+
// Retry anything whose previous post-turn persistence failed before the new prompt.
|
|
8883
|
+
this.#flushPendingPromptMessages();
|
|
8806
8884
|
|
|
8807
8885
|
// Reset todo reminder count on new user prompt
|
|
8808
8886
|
this.#todoReminderCount = 0;
|
|
@@ -8837,160 +8915,206 @@ export class AgentSession {
|
|
|
8837
8915
|
]);
|
|
8838
8916
|
}
|
|
8839
8917
|
|
|
8840
|
-
//
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
const planModeMessage = await this.#buildAutomaticPlanModeMessage();
|
|
8847
|
-
if (planModeMessage) {
|
|
8848
|
-
messages.push(planModeMessage);
|
|
8849
|
-
}
|
|
8850
|
-
const goalModeMessage = this.#buildAutomaticGoalModeMessage();
|
|
8851
|
-
if (goalModeMessage) {
|
|
8852
|
-
messages.push(goalModeMessage);
|
|
8853
|
-
}
|
|
8854
|
-
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
8855
|
-
messages.push(volatileProjectContextMessage);
|
|
8856
|
-
const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
|
|
8857
|
-
if (untrustedMcpServerInstructionsMessage) messages.push(untrustedMcpServerInstructionsMessage);
|
|
8858
|
-
|
|
8859
|
-
if (rosterClaim && this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)) {
|
|
8860
|
-
messages.push(rosterClaim.message);
|
|
8861
|
-
} else if (rosterClaim) {
|
|
8862
|
-
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
8863
|
-
}
|
|
8864
|
-
if (options?.prependMessages) {
|
|
8865
|
-
messages.push(...options.prependMessages);
|
|
8866
|
-
}
|
|
8867
|
-
|
|
8868
|
-
messages.push(message);
|
|
8869
|
-
|
|
8870
|
-
// Early bail-out: a generation change or cancellation during setup must
|
|
8871
|
-
// terminate preflight rather than retaining SDK prompt authority.
|
|
8872
|
-
if (this.#isPromptPreflightCancelled(generation, preflightSignal)) {
|
|
8873
|
-
this.#resetInjectedContextSignatures();
|
|
8874
|
-
// A newer abort/prompt cycle superseded this preflight. Callers awaiting
|
|
8875
|
-
// acceptance (onPreflightAccepted) must be told it never ran; direct
|
|
8876
|
-
// callers (e.g. prompt() aborted during a TTSR wait) resolve gracefully.
|
|
8877
|
-
if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
|
|
8878
|
-
return;
|
|
8879
|
-
}
|
|
8880
|
-
|
|
8881
|
-
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
8918
|
+
// R3.2: one-time Phase A products are captured inside the closure and
|
|
8919
|
+
// executed once after admission acceptance; Phase B reassembles the
|
|
8920
|
+
// attempt messages after every idle wait / compact retry. Pending
|
|
8921
|
+
// next-turn messages are captured WITHOUT draining — the drain moves to
|
|
8922
|
+
// the run-accepted wrapper (R3.3) so AgentBusy/idle/compact-before-
|
|
8923
|
+
// acceptance never consume them.
|
|
8882
8924
|
pendingNextTurnMessageCount = this.#pendingNextTurnMessages.length;
|
|
8883
8925
|
hasPendingNextTurnMessages = pendingNextTurnMessageCount > 0;
|
|
8884
|
-
for (const msg of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
|
|
8885
|
-
messages.push(msg);
|
|
8886
|
-
}
|
|
8887
|
-
|
|
8888
|
-
// Auto-read @filepath mentions
|
|
8889
|
-
const fileMentions = extractFileMentions(expandedText);
|
|
8890
|
-
if (fileMentions.length > 0) {
|
|
8891
|
-
const cwd = this.sessionManager.getCwd();
|
|
8892
|
-
// Collect resolved paths already shown (read or mentioned) in the recent
|
|
8893
|
-
// window so a repeat @mention emits a compact note instead of the full body.
|
|
8894
|
-
const RECENT_MENTION_WINDOW = 40;
|
|
8895
|
-
const recentlyShownPaths = new Set<string>();
|
|
8896
|
-
for (const entry of this.sessionManager.getBranch().slice(-RECENT_MENTION_WINDOW)) {
|
|
8897
|
-
if (entry.type !== "message") continue;
|
|
8898
|
-
const msg = entry.message;
|
|
8899
|
-
if (msg.role === "fileMention") {
|
|
8900
|
-
for (const file of msg.files) {
|
|
8901
|
-
if (!file.duplicate && !file.pruned) recentlyShownPaths.add(resolveReadPath(file.path, cwd));
|
|
8902
|
-
}
|
|
8903
|
-
} else if (msg.role === "toolResult") {
|
|
8904
|
-
const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
|
|
8905
|
-
if (typeof resolved === "string" && resolved) recentlyShownPaths.add(resolveReadPath(resolved, cwd));
|
|
8906
|
-
}
|
|
8907
|
-
}
|
|
8908
|
-
const fileMentionMessages = await generateFileMentionMessages(fileMentions, cwd, {
|
|
8909
|
-
autoResizeImages: this.settings.get("images.autoResize"),
|
|
8910
|
-
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
8911
|
-
maxInlineBytes: this.settings.get("tools.fileMentionInlineBytes") * 1024,
|
|
8912
|
-
recentlyShownPaths,
|
|
8913
|
-
});
|
|
8914
|
-
messages.push(...fileMentionMessages);
|
|
8915
|
-
}
|
|
8916
|
-
|
|
8917
|
-
const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
|
|
8918
|
-
hindsightRecall = this.getHindsightSessionState()?.getRecallSnippetForInjection();
|
|
8919
|
-
if (hindsightRecall) {
|
|
8920
|
-
// Recall is provider-only context for this request. It must precede the
|
|
8921
|
-
// actual prompt but never become part of durable session history.
|
|
8922
|
-
const promptIndex = messages.lastIndexOf(message);
|
|
8923
|
-
messages.splice(promptIndex, 0, {
|
|
8924
|
-
role: "custom",
|
|
8925
|
-
customType: "hindsight-recall",
|
|
8926
|
-
content: hindsightRecall,
|
|
8927
|
-
display: false,
|
|
8928
|
-
attribution: "agent",
|
|
8929
|
-
timestamp: Date.now(),
|
|
8930
|
-
});
|
|
8931
|
-
}
|
|
8932
|
-
|
|
8933
8926
|
const promptAttribution: "user" | "agent" | undefined =
|
|
8934
8927
|
"attribution" in message ? message.attribution : undefined;
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
}
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
this.agent.setSystemPrompt(result.systemPrompt);
|
|
8952
|
-
} else {
|
|
8953
|
-
this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
|
|
8928
|
+
let phaseACompleted = false;
|
|
8929
|
+
let fileMentionMessages: AgentMessage[] = [];
|
|
8930
|
+
let beforeAgentStartResultMessages: BeforeAgentStartInternalMessage[] = [];
|
|
8931
|
+
const contributedMessages: BeforeAgentStartInternalMessage[] = [];
|
|
8932
|
+
let recallMarked = false;
|
|
8933
|
+
let planReferenceMessage: CustomMessage | null = null;
|
|
8934
|
+
// Phase B attempt cache. AgentBusy/idle and forced-compaction retries clear
|
|
8935
|
+
// this cache so live plan/goal/volatile/MCP overlays are rebuilt. One-shot
|
|
8936
|
+
// Phase A products (including the plan reference) remain stable.
|
|
8937
|
+
let attemptMessages: AgentMessage[] | undefined;
|
|
8938
|
+
|
|
8939
|
+
const buildPreSubmit = async (): Promise<AgentMessage[] | null> => {
|
|
8940
|
+
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
8941
|
+
if (options?.onFinalPreflight && !(await options.onFinalPreflight({ hasPendingNextTurnMessages }))) {
|
|
8942
|
+
this.#resetInjectedContextSignatures();
|
|
8943
|
+
return null;
|
|
8954
8944
|
}
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
const
|
|
8945
|
+
if (!phaseACompleted) {
|
|
8946
|
+
phaseACompleted = true;
|
|
8947
|
+
// Phase A (one-time side-effectful products; runs once).
|
|
8948
|
+
const fileMentions = extractFileMentions(expandedText);
|
|
8949
|
+
if (fileMentions.length > 0) {
|
|
8950
|
+
const cwd = this.sessionManager.getCwd();
|
|
8951
|
+
// Collect resolved paths already shown (read or mentioned) in the
|
|
8952
|
+
// recent window so a repeat @mention emits a compact note instead
|
|
8953
|
+
// of the full body.
|
|
8954
|
+
const RECENT_MENTION_WINDOW = 40;
|
|
8955
|
+
const recentlyShownPaths = new Set<string>();
|
|
8956
|
+
for (const entry of this.sessionManager.getBranch().slice(-RECENT_MENTION_WINDOW)) {
|
|
8957
|
+
if (entry.type !== "message") continue;
|
|
8958
|
+
const msg = entry.message;
|
|
8959
|
+
if (msg.role === "fileMention") {
|
|
8960
|
+
for (const file of msg.files) {
|
|
8961
|
+
if (!file.duplicate && !file.pruned) recentlyShownPaths.add(resolveReadPath(file.path, cwd));
|
|
8962
|
+
}
|
|
8963
|
+
} else if (msg.role === "toolResult") {
|
|
8964
|
+
const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
|
|
8965
|
+
if (typeof resolved === "string" && resolved)
|
|
8966
|
+
recentlyShownPaths.add(resolveReadPath(resolved, cwd));
|
|
8967
|
+
}
|
|
8968
|
+
}
|
|
8969
|
+
fileMentionMessages = await generateFileMentionMessages(fileMentions, cwd, {
|
|
8970
|
+
autoResizeImages: this.settings.get("images.autoResize"),
|
|
8971
|
+
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
8972
|
+
maxInlineBytes: this.settings.get("tools.fileMentionInlineBytes") * 1024,
|
|
8973
|
+
recentlyShownPaths,
|
|
8974
|
+
});
|
|
8975
|
+
}
|
|
8976
|
+
const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
|
|
8977
|
+
hindsightRecall = this.getHindsightSessionState()?.getRecallSnippetForInjection();
|
|
8978
|
+
planReferenceMessage = await this.#buildPlanReferenceMessage();
|
|
8979
|
+
// Emit before_agent_start extension event. Race hook completion with
|
|
8980
|
+
// prompt cancellation so a wedged hook cannot retain SDK prompt authority.
|
|
8981
|
+
if (this.#extensionRunner?.hasHandlers("before_agent_start")) this.#markRetryReplayUnsafe();
|
|
8982
|
+
if (this.#extensionRunner) {
|
|
8983
|
+
const result = await this.#awaitPromptPreflight(
|
|
8967
8984
|
generation,
|
|
8968
8985
|
preflightSignal,
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8986
|
+
this.#extensionRunner.emitBeforeAgentStart(
|
|
8987
|
+
expandedText,
|
|
8988
|
+
options?.images,
|
|
8989
|
+
beforeAgentStartSystemPrompt,
|
|
8990
|
+
),
|
|
8974
8991
|
);
|
|
8975
|
-
if (
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8992
|
+
if (result?.messages) beforeAgentStartResultMessages = [...result.messages];
|
|
8993
|
+
if (result?.systemPrompt !== undefined) {
|
|
8994
|
+
this.agent.setSystemPrompt(result.systemPrompt);
|
|
8995
|
+
} else {
|
|
8996
|
+
this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
|
|
8997
|
+
}
|
|
8998
|
+
} else {
|
|
8999
|
+
this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
|
|
9000
|
+
}
|
|
9001
|
+
// Invoke first-party internal before-agent-start contributors. These
|
|
9002
|
+
// run alongside the extension runner (not via user-loaded hooks) and
|
|
9003
|
+
// append through the same custom-message attribution path. Errors are nonfatal.
|
|
9004
|
+
if (this.#beforeAgentStartContributors.length > 0) {
|
|
9005
|
+
for (const contributor of this.#beforeAgentStartContributors) {
|
|
9006
|
+
try {
|
|
9007
|
+
const msg = await this.#awaitPromptPreflight(
|
|
9008
|
+
generation,
|
|
9009
|
+
preflightSignal,
|
|
9010
|
+
contributor({
|
|
9011
|
+
prompt: expandedText,
|
|
9012
|
+
images: options?.images,
|
|
9013
|
+
sessionId: this.sessionId,
|
|
9014
|
+
}),
|
|
9015
|
+
);
|
|
9016
|
+
if (msg) contributedMessages.push(msg);
|
|
9017
|
+
} catch (err) {
|
|
9018
|
+
if (this.#isPromptPreflightCancelled(generation, preflightSignal))
|
|
9019
|
+
throw promptPreflightCancelledError();
|
|
9020
|
+
logger.debug("before_agent_start contributor failed", { error: String(err) });
|
|
9021
|
+
}
|
|
9022
|
+
}
|
|
8980
9023
|
}
|
|
9024
|
+
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
8981
9025
|
}
|
|
8982
|
-
this.#appendBeforeAgentStartCustomMessages(messages, contributed, promptAttribution, message.role);
|
|
8983
|
-
}
|
|
8984
9026
|
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
this
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
9027
|
+
// Phase B (attempt-dependent; re-runs after every idle wait / compact retry).
|
|
9028
|
+
// R3.2 overflow preflight: materialize the session context synchronously so
|
|
9029
|
+
// an over-budget graph throws SessionContextTooLargeError before any prompt.
|
|
9030
|
+
this.sessionManager.buildSessionContext();
|
|
9031
|
+
if (attemptMessages) {
|
|
9032
|
+
// Idle/AgentBusy retry: reuse the previously assembled attempt
|
|
9033
|
+
// messages; a superseded roster claim releases and the prompt
|
|
9034
|
+
// proceeds without it, matching the pre-seam reuse behavior.
|
|
9035
|
+
if (rosterClaim && !this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)) {
|
|
9036
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
9037
|
+
}
|
|
9038
|
+
return attemptMessages;
|
|
9039
|
+
}
|
|
9040
|
+
const messages: AgentMessage[] = [];
|
|
9041
|
+
const currentPlanReferenceMessage = planReferenceMessage;
|
|
9042
|
+
if (currentPlanReferenceMessage) {
|
|
9043
|
+
messages.push(currentPlanReferenceMessage);
|
|
9044
|
+
}
|
|
9045
|
+
const planModeMessage = await this.#buildAutomaticPlanModeMessage();
|
|
9046
|
+
if (planModeMessage) {
|
|
9047
|
+
messages.push(planModeMessage);
|
|
9048
|
+
}
|
|
9049
|
+
const goalModeMessage = this.#buildAutomaticGoalModeMessage();
|
|
9050
|
+
if (goalModeMessage) {
|
|
9051
|
+
messages.push(goalModeMessage);
|
|
9052
|
+
}
|
|
9053
|
+
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
9054
|
+
messages.push(volatileProjectContextMessage);
|
|
9055
|
+
const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
|
|
9056
|
+
if (untrustedMcpServerInstructionsMessage) messages.push(untrustedMcpServerInstructionsMessage);
|
|
9057
|
+
|
|
9058
|
+
// Roster: one Phase A claim, revalidated and reused on every attempt;
|
|
9059
|
+
// a superseded claim releases and the prompt proceeds without it.
|
|
9060
|
+
if (rosterClaim && this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)) {
|
|
9061
|
+
messages.push(rosterClaim.message);
|
|
9062
|
+
} else if (rosterClaim) {
|
|
9063
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
9064
|
+
}
|
|
9065
|
+
if (options?.prependMessages) {
|
|
9066
|
+
messages.push(...options.prependMessages);
|
|
9067
|
+
}
|
|
9068
|
+
|
|
9069
|
+
const promptIndex = messages.length;
|
|
9070
|
+
messages.push(message);
|
|
9071
|
+
|
|
9072
|
+
// Re-present captured pending next-turn messages (never drained here).
|
|
9073
|
+
for (const msg of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
|
|
9074
|
+
messages.push(msg);
|
|
9075
|
+
}
|
|
9076
|
+
messages.push(...fileMentionMessages);
|
|
9077
|
+
if (hindsightRecall) {
|
|
9078
|
+
// Recall is provider-only context for this request. It must precede
|
|
9079
|
+
// the actual prompt but never become part of durable session history.
|
|
9080
|
+
messages.splice(promptIndex, 0, {
|
|
9081
|
+
role: "custom",
|
|
9082
|
+
customType: "hindsight-recall",
|
|
9083
|
+
content: hindsightRecall,
|
|
9084
|
+
display: false,
|
|
9085
|
+
attribution: "agent",
|
|
9086
|
+
timestamp: Date.now(),
|
|
9087
|
+
});
|
|
9088
|
+
}
|
|
9089
|
+
if (beforeAgentStartResultMessages.length > 0) {
|
|
9090
|
+
this.#appendBeforeAgentStartCustomMessages(
|
|
9091
|
+
messages,
|
|
9092
|
+
beforeAgentStartResultMessages,
|
|
9093
|
+
promptAttribution,
|
|
9094
|
+
message.role,
|
|
9095
|
+
);
|
|
9096
|
+
}
|
|
9097
|
+
if (contributedMessages.length > 0) {
|
|
9098
|
+
this.#appendBeforeAgentStartCustomMessages(
|
|
9099
|
+
messages,
|
|
9100
|
+
contributedMessages,
|
|
9101
|
+
promptAttribution,
|
|
9102
|
+
message.role,
|
|
9103
|
+
);
|
|
9104
|
+
}
|
|
9105
|
+
if (options?.onFinalPreflight && !(await options.onFinalPreflight({ hasPendingNextTurnMessages }))) {
|
|
9106
|
+
this.#resetInjectedContextSignatures();
|
|
9107
|
+
return null;
|
|
9108
|
+
}
|
|
9109
|
+
attemptMessages = messages;
|
|
9110
|
+
return messages;
|
|
9111
|
+
};
|
|
9112
|
+
const preSubmit: PreSubmitBuilder = {
|
|
9113
|
+
build: buildPreSubmit,
|
|
9114
|
+
reset: () => {
|
|
9115
|
+
attemptMessages = undefined;
|
|
9116
|
+
},
|
|
9117
|
+
};
|
|
8994
9118
|
|
|
8995
9119
|
const agentPromptOptions = {
|
|
8996
9120
|
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
@@ -8999,21 +9123,27 @@ export class AgentSession {
|
|
|
8999
9123
|
this.#acceptRunHandle(handle);
|
|
9000
9124
|
options?.onRunAccepted?.(handle);
|
|
9001
9125
|
options?.admissionLease?.release();
|
|
9002
|
-
|
|
9126
|
+
// R3.3: the accepted-run wrapper is the exact acceptance boundary —
|
|
9127
|
+
// pending next-turn drain and the exactly-once recall mark live here.
|
|
9128
|
+
if (hindsightRecall && !recallMarked) {
|
|
9129
|
+
recallMarked = true;
|
|
9130
|
+
this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
|
|
9131
|
+
}
|
|
9132
|
+
if (pendingNextTurnMessageCount > 0) {
|
|
9133
|
+
this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
|
|
9134
|
+
}
|
|
9135
|
+
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
9003
9136
|
},
|
|
9004
9137
|
};
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
}
|
|
9015
|
-
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
9016
|
-
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
|
|
9138
|
+
await this.#promptAgentWithIdleRetry(preSubmit, agentPromptOptions, predecessorAgentEndHold, {
|
|
9139
|
+
signal: preflightSignal,
|
|
9140
|
+
resourceRunId: this.#runResourceLeaseContext.getStore()?.resourceRunId,
|
|
9141
|
+
onPreflightAccepted: () => {
|
|
9142
|
+
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
9143
|
+
if (options?.onPreflightAcceptCommit) return options.onPreflightAcceptCommit();
|
|
9144
|
+
options?.onPreflightAccepted?.();
|
|
9145
|
+
},
|
|
9146
|
+
});
|
|
9017
9147
|
const terminalAssistant = this.#findLastAssistantMessage();
|
|
9018
9148
|
if (
|
|
9019
9149
|
rosterClaim &&
|
|
@@ -9048,14 +9178,7 @@ export class AgentSession {
|
|
|
9048
9178
|
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
9049
9179
|
}
|
|
9050
9180
|
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
9051
|
-
this.#
|
|
9052
|
-
if (options?.skipPostPromptRecoveryWait) {
|
|
9053
|
-
await this.#agentEndPublicationPromise;
|
|
9054
|
-
} else {
|
|
9055
|
-
await this.#agentEndHandlingPromise;
|
|
9056
|
-
await this.#waitForPostPromptRecovery();
|
|
9057
|
-
await this.#agentEndPublicationPromise;
|
|
9058
|
-
}
|
|
9181
|
+
await this.#settleEndedInFlight(options?.skipPostPromptRecoveryWait ? "publication" : "full");
|
|
9059
9182
|
}
|
|
9060
9183
|
}
|
|
9061
9184
|
|
|
@@ -10712,19 +10835,56 @@ export class AgentSession {
|
|
|
10712
10835
|
// Flush current session to ensure all entries are written
|
|
10713
10836
|
await this.sessionManager.flush();
|
|
10714
10837
|
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10838
|
+
const boundedColdForkEligible =
|
|
10839
|
+
this.sessionManager.getSessionMemoryStats().coldRetirementActive &&
|
|
10840
|
+
!this.sessionManager.isManagedDestination() &&
|
|
10841
|
+
previousSessionFile !== undefined;
|
|
10842
|
+
if (boundedColdForkEligible) {
|
|
10843
|
+
const previousManager = this.sessionManager;
|
|
10844
|
+
const forkedManager = await SessionManager.forkFrom(
|
|
10845
|
+
previousSessionFile,
|
|
10846
|
+
previousManager.getCwd(),
|
|
10847
|
+
SessionManager.explicitDestination(previousManager.getSessionDir()),
|
|
10848
|
+
undefined,
|
|
10849
|
+
"copy-retain",
|
|
10850
|
+
this.settings.get("sessionMemory.mode"),
|
|
10851
|
+
);
|
|
10852
|
+
try {
|
|
10853
|
+
await initializeLocalRoot({
|
|
10854
|
+
getArtifactsDir: () => forkedManager.getArtifactsDir(),
|
|
10855
|
+
isManagedDestination: () => false,
|
|
10856
|
+
getManagedLegacyLocalMigrationSource: () => null,
|
|
10857
|
+
getSessionId: () => forkedManager.getSessionId(),
|
|
10858
|
+
});
|
|
10859
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
10860
|
+
} catch (error) {
|
|
10861
|
+
const forkedFile = forkedManager.getSessionFile();
|
|
10862
|
+
await forkedManager.close();
|
|
10863
|
+
if (forkedFile) await previousManager.discardUncommittedSession(forkedFile);
|
|
10864
|
+
throw error;
|
|
10865
|
+
}
|
|
10866
|
+
this.sessionManager = forkedManager;
|
|
10867
|
+
try {
|
|
10868
|
+
await previousManager.close();
|
|
10869
|
+
} catch (error) {
|
|
10870
|
+
logger.warn("Previous session close failed after bounded fork adoption", {
|
|
10871
|
+
error: error instanceof Error ? error.message : String(error),
|
|
10872
|
+
});
|
|
10873
|
+
}
|
|
10725
10874
|
await this.#runToolSessionTransitionCleanups();
|
|
10726
|
-
}
|
|
10727
|
-
|
|
10875
|
+
} else {
|
|
10876
|
+
// Prepare the copied successor and complete local:// readiness while all
|
|
10877
|
+
// public manager getters remain bound to the predecessor.
|
|
10878
|
+
const prepared = await this.sessionManager.prepareFork();
|
|
10879
|
+
if (!prepared) return false;
|
|
10880
|
+
try {
|
|
10881
|
+
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10882
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
10883
|
+
this.sessionManager.commitPreparedNewSession(prepared);
|
|
10884
|
+
await this.#runToolSessionTransitionCleanups();
|
|
10885
|
+
} catch (error) {
|
|
10886
|
+
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
10887
|
+
}
|
|
10728
10888
|
}
|
|
10729
10889
|
this.#syncAgentSessionId();
|
|
10730
10890
|
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
@@ -13363,13 +13523,15 @@ export class AgentSession {
|
|
|
13363
13523
|
}
|
|
13364
13524
|
}
|
|
13365
13525
|
}
|
|
13526
|
+
const sessionMemory = this.sessionManager.getSessionMemoryStats();
|
|
13366
13527
|
return {
|
|
13367
13528
|
heapUsedBytes: process.memoryUsage().heapUsed,
|
|
13368
13529
|
providerBytes,
|
|
13369
13530
|
messageCount: this.state.messages.length,
|
|
13370
13531
|
imageBytes,
|
|
13371
13532
|
sessionResidentImageBytes: this.sessionManager.getResidentImageBytes(),
|
|
13372
|
-
materializedResidentBytes:
|
|
13533
|
+
materializedResidentBytes:
|
|
13534
|
+
this.#streamingEditFileCache.totalBytes + sessionMemory.hotRegionBytes + sessionMemory.metaDescriptorBytes,
|
|
13373
13535
|
tuiChatChildren: retainedMemory.tuiChatChildren ?? 0,
|
|
13374
13536
|
tuiCachedRenderBytes: retainedMemory.tuiCachedRenderBytes ?? 0,
|
|
13375
13537
|
};
|
|
@@ -13777,11 +13939,6 @@ export class AgentSession {
|
|
|
13777
13939
|
`(Reminder ${this.#todoReminderCount}/${remindersMax})\n` +
|
|
13778
13940
|
`</system-reminder>`;
|
|
13779
13941
|
|
|
13780
|
-
logger.debug("Todo completion: sending reminder", {
|
|
13781
|
-
incomplete: incomplete.length,
|
|
13782
|
-
attempt: this.#todoReminderCount,
|
|
13783
|
-
});
|
|
13784
|
-
|
|
13785
13942
|
// Emit event for UI to render notification
|
|
13786
13943
|
await this.#emitSessionEvent({
|
|
13787
13944
|
type: "todo_reminder",
|
|
@@ -13790,7 +13947,27 @@ export class AgentSession {
|
|
|
13790
13947
|
maxAttempts: remindersMax,
|
|
13791
13948
|
});
|
|
13792
13949
|
|
|
13793
|
-
//
|
|
13950
|
+
// Consumers that cannot represent a server-initiated turn (ACP v1 clients, SDK
|
|
13951
|
+
// hosts) keep reporting the prompt as running until the terminal `agent_end` is
|
|
13952
|
+
// published. Injecting here would park that terminal behind a continuation hold
|
|
13953
|
+
// (`#scheduleAgentContinue` -> `#reserveDeferredAgentEndForContinuation`), so a
|
|
13954
|
+
// turn that already delivered its final answer would never settle — and there is
|
|
13955
|
+
// no interactive consumer to act on the nudge. Those hosts get the advisory
|
|
13956
|
+
// event above and nothing else.
|
|
13957
|
+
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
13958
|
+
logger.debug("Todo completion: advisory reminder only", {
|
|
13959
|
+
incomplete: incomplete.length,
|
|
13960
|
+
attempt: this.#todoReminderCount,
|
|
13961
|
+
});
|
|
13962
|
+
return;
|
|
13963
|
+
}
|
|
13964
|
+
|
|
13965
|
+
logger.debug("Todo completion: sending reminder", {
|
|
13966
|
+
incomplete: incomplete.length,
|
|
13967
|
+
attempt: this.#todoReminderCount,
|
|
13968
|
+
});
|
|
13969
|
+
|
|
13970
|
+
// Inject reminder and continue conversation
|
|
13794
13971
|
this.agent.appendMessage({
|
|
13795
13972
|
role: "developer",
|
|
13796
13973
|
content: [{ type: "text", text: reminder }],
|
|
@@ -15077,6 +15254,9 @@ export class AgentSession {
|
|
|
15077
15254
|
)
|
|
15078
15255
|
);
|
|
15079
15256
|
}
|
|
15257
|
+
#isIdleStreamStallErrorMessage(errorMessage: string): boolean {
|
|
15258
|
+
return /stream stalled while waiting for the next event/i.test(errorMessage);
|
|
15259
|
+
}
|
|
15080
15260
|
|
|
15081
15261
|
#isFirstEventTimeoutErrorMessage(errorMessage: string): boolean {
|
|
15082
15262
|
// First-event timeout: the stream watchdog aborted because no event
|
|
@@ -15133,8 +15313,8 @@ export class AgentSession {
|
|
|
15133
15313
|
/**
|
|
15134
15314
|
* Ordered retry classification: typed safety stop (surface) -> legacy safety stop
|
|
15135
15315
|
* (surface) -> overflow (compaction) -> terminal (surface) -> usage_limit
|
|
15136
|
-
* (rotation) -> first_event_timeout (bounded retry) -> transient (unbounded retry
|
|
15137
|
-
* unknown (bounded retry).
|
|
15316
|
+
* (rotation) -> first_event_timeout (bounded retry) -> transient (unbounded retry,
|
|
15317
|
+
* except canonical idle-stream stalls bounded downstream) -> unknown (bounded retry).
|
|
15138
15318
|
*/
|
|
15139
15319
|
#classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
|
|
15140
15320
|
if (message.stopReason !== "error") return "none";
|
|
@@ -15692,7 +15872,10 @@ export class AgentSession {
|
|
|
15692
15872
|
return false;
|
|
15693
15873
|
}
|
|
15694
15874
|
}
|
|
15695
|
-
const legacyUnbounded =
|
|
15875
|
+
const legacyUnbounded =
|
|
15876
|
+
!managedFallback &&
|
|
15877
|
+
classification === "transient" &&
|
|
15878
|
+
!this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
|
|
15696
15879
|
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
15697
15880
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
15698
15881
|
let outcome = managedFallback
|
|
@@ -15958,16 +16141,25 @@ export class AgentSession {
|
|
|
15958
16141
|
}
|
|
15959
16142
|
|
|
15960
16143
|
async #promptAgentWithIdleRetry(
|
|
15961
|
-
|
|
16144
|
+
preSubmit: PreSubmitBuilder,
|
|
15962
16145
|
options?: {
|
|
15963
16146
|
toolChoice?: ToolChoice;
|
|
15964
16147
|
fallbackManaged?: boolean;
|
|
15965
16148
|
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
15966
16149
|
},
|
|
15967
16150
|
predecessorAgentEndHold?: symbol,
|
|
16151
|
+
seam?: {
|
|
16152
|
+
signal?: AbortSignal;
|
|
16153
|
+
resourceRunId?: string;
|
|
16154
|
+
onPreflightAccepted?: () => void | Promise<void>;
|
|
16155
|
+
},
|
|
15968
16156
|
): Promise<void> {
|
|
15969
16157
|
const deadline = Date.now() + 30_000;
|
|
15970
16158
|
let continuationHold = predecessorAgentEndHold;
|
|
16159
|
+
// R3.2 helper-local compact-once flag: fresh per prompt; the inline retry
|
|
16160
|
+
// re-runs Phase B via preSubmit after the forced compaction.
|
|
16161
|
+
let overflowRetried = false;
|
|
16162
|
+
let preflightAccepted = false;
|
|
15971
16163
|
for (;;) {
|
|
15972
16164
|
try {
|
|
15973
16165
|
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(
|
|
@@ -15975,11 +16167,50 @@ export class AgentSession {
|
|
|
15975
16167
|
);
|
|
15976
16168
|
continuationHold = undefined;
|
|
15977
16169
|
try {
|
|
16170
|
+
const messages = await preSubmit.build();
|
|
16171
|
+
if (messages === null) {
|
|
16172
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
16173
|
+
return;
|
|
16174
|
+
}
|
|
16175
|
+
if (!preflightAccepted) {
|
|
16176
|
+
await seam?.onPreflightAccepted?.();
|
|
16177
|
+
preflightAccepted = true;
|
|
16178
|
+
}
|
|
15978
16179
|
await this.agent.prompt(messages, options);
|
|
15979
16180
|
this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
|
|
15980
16181
|
return;
|
|
15981
16182
|
} catch (error) {
|
|
15982
16183
|
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
16184
|
+
if (
|
|
16185
|
+
error instanceof SessionContextTooLargeError &&
|
|
16186
|
+
!overflowRetried &&
|
|
16187
|
+
this.settings.get("sessionMemory.contextOverflowRecovery")
|
|
16188
|
+
) {
|
|
16189
|
+
// D7: exactly one forced, no-continuation compaction
|
|
16190
|
+
// (`willRetry:false`, `continueAfterMaintenance:false`), then
|
|
16191
|
+
// exactly one inline retry. Any other terminal status rethrows
|
|
16192
|
+
// the ORIGINAL typed error with measurements.
|
|
16193
|
+
overflowRetried = true;
|
|
16194
|
+
let compacted = false;
|
|
16195
|
+
try {
|
|
16196
|
+
const status = await this.#runAutoCompaction("overflow", false, false, {
|
|
16197
|
+
force: true,
|
|
16198
|
+
continueAfterMaintenance: false,
|
|
16199
|
+
signal: seam?.signal,
|
|
16200
|
+
resourceRunId: seam?.resourceRunId,
|
|
16201
|
+
});
|
|
16202
|
+
compacted = status.kind === "compacted";
|
|
16203
|
+
} catch {
|
|
16204
|
+
compacted = false;
|
|
16205
|
+
}
|
|
16206
|
+
if (compacted) {
|
|
16207
|
+
// Post-compaction Phase B must rebuild overlays (the
|
|
16208
|
+
// injected-context signatures were reset by
|
|
16209
|
+
// #applyCompactionPostAppend), so drop the cached attempt.
|
|
16210
|
+
preSubmit.reset();
|
|
16211
|
+
continue;
|
|
16212
|
+
}
|
|
16213
|
+
}
|
|
15983
16214
|
throw error;
|
|
15984
16215
|
}
|
|
15985
16216
|
} catch (err) {
|
|
@@ -15990,6 +16221,7 @@ export class AgentSession {
|
|
|
15990
16221
|
throw new Error("Timed out waiting for prior agent run to finish before prompting.");
|
|
15991
16222
|
}
|
|
15992
16223
|
await this.agent.waitForIdle();
|
|
16224
|
+
preSubmit.reset();
|
|
15993
16225
|
}
|
|
15994
16226
|
}
|
|
15995
16227
|
}
|
|
@@ -16086,11 +16318,13 @@ export class AgentSession {
|
|
|
16086
16318
|
* @param command The bash command to execute
|
|
16087
16319
|
* @param onChunk Optional streaming callback for output
|
|
16088
16320
|
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
|
|
16321
|
+
* @param options.onPersisted Called once the execution's message is in session state
|
|
16322
|
+
* (immediately when idle, at the post-turn flush while streaming)
|
|
16089
16323
|
*/
|
|
16090
16324
|
async executeBash(
|
|
16091
16325
|
command: string,
|
|
16092
16326
|
onChunk?: (chunk: string) => void,
|
|
16093
|
-
options?: { excludeFromContext?: boolean },
|
|
16327
|
+
options?: { excludeFromContext?: boolean; onPersisted?: () => void },
|
|
16094
16328
|
): Promise<BashResult> {
|
|
16095
16329
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
16096
16330
|
this.#markRetryReplayUnsafe();
|
|
@@ -16145,7 +16379,11 @@ export class AgentSession {
|
|
|
16145
16379
|
* Record a bash execution result in session history.
|
|
16146
16380
|
* Used by executeBash and by extensions that handle bash execution themselves.
|
|
16147
16381
|
*/
|
|
16148
|
-
recordBashResult(
|
|
16382
|
+
recordBashResult(
|
|
16383
|
+
command: string,
|
|
16384
|
+
result: BashResult,
|
|
16385
|
+
options?: { excludeFromContext?: boolean; onPersisted?: () => void },
|
|
16386
|
+
): void {
|
|
16149
16387
|
const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
|
|
16150
16388
|
const bashMessage: BashExecutionMessage = {
|
|
16151
16389
|
role: "bashExecution",
|
|
@@ -16162,13 +16400,18 @@ export class AgentSession {
|
|
|
16162
16400
|
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
16163
16401
|
if (this.isStreaming) {
|
|
16164
16402
|
// Queue for later - will be flushed on agent_end
|
|
16165
|
-
this.#pendingBashMessages.push(
|
|
16403
|
+
this.#pendingBashMessages.push({
|
|
16404
|
+
message: bashMessage,
|
|
16405
|
+
onPersisted: options?.onPersisted,
|
|
16406
|
+
appendedToAgent: false,
|
|
16407
|
+
});
|
|
16166
16408
|
} else {
|
|
16167
16409
|
// Add to agent state immediately
|
|
16168
16410
|
this.agent.appendMessage(bashMessage);
|
|
16169
16411
|
|
|
16170
16412
|
// Save to session
|
|
16171
16413
|
this.sessionManager.appendMessage(bashMessage);
|
|
16414
|
+
options?.onPersisted?.();
|
|
16172
16415
|
}
|
|
16173
16416
|
}
|
|
16174
16417
|
|
|
@@ -16196,17 +16439,7 @@ export class AgentSession {
|
|
|
16196
16439
|
* Called after agent turn completes to maintain proper message ordering.
|
|
16197
16440
|
*/
|
|
16198
16441
|
#flushPendingBashMessages(): void {
|
|
16199
|
-
|
|
16200
|
-
|
|
16201
|
-
for (const bashMessage of this.#pendingBashMessages) {
|
|
16202
|
-
// Add to agent state
|
|
16203
|
-
this.agent.appendMessage(bashMessage);
|
|
16204
|
-
|
|
16205
|
-
// Save to session
|
|
16206
|
-
this.sessionManager.appendMessage(bashMessage);
|
|
16207
|
-
}
|
|
16208
|
-
|
|
16209
|
-
this.#pendingBashMessages = [];
|
|
16442
|
+
this.#flushPendingExecutionMessages(this.#pendingBashMessages, "bash");
|
|
16210
16443
|
}
|
|
16211
16444
|
|
|
16212
16445
|
// =========================================================================
|
|
@@ -16219,11 +16452,13 @@ export class AgentSession {
|
|
|
16219
16452
|
* @param code The Python code to execute
|
|
16220
16453
|
* @param onChunk Optional streaming callback for output
|
|
16221
16454
|
* @param options.excludeFromContext If true, execution won't be sent to LLM ($$ prefix)
|
|
16455
|
+
* @param options.onPersisted Called once the execution's message is in session state
|
|
16456
|
+
* (immediately when idle, at the post-turn flush while streaming)
|
|
16222
16457
|
*/
|
|
16223
16458
|
async executePython(
|
|
16224
16459
|
code: string,
|
|
16225
16460
|
onChunk?: (chunk: string) => void,
|
|
16226
|
-
options?: { excludeFromContext?: boolean },
|
|
16461
|
+
options?: { excludeFromContext?: boolean; onPersisted?: () => void },
|
|
16227
16462
|
): Promise<PythonResult> {
|
|
16228
16463
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
16229
16464
|
this.#markRetryReplayUnsafe();
|
|
@@ -16291,7 +16526,11 @@ export class AgentSession {
|
|
|
16291
16526
|
/**
|
|
16292
16527
|
* Record a Python execution result in session history.
|
|
16293
16528
|
*/
|
|
16294
|
-
recordPythonResult(
|
|
16529
|
+
recordPythonResult(
|
|
16530
|
+
code: string,
|
|
16531
|
+
result: PythonResult,
|
|
16532
|
+
options?: { excludeFromContext?: boolean; onPersisted?: () => void },
|
|
16533
|
+
): void {
|
|
16295
16534
|
const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
|
|
16296
16535
|
const pythonMessage: PythonExecutionMessage = {
|
|
16297
16536
|
role: "pythonExecution",
|
|
@@ -16307,10 +16546,15 @@ export class AgentSession {
|
|
|
16307
16546
|
|
|
16308
16547
|
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
16309
16548
|
if (this.isStreaming) {
|
|
16310
|
-
this.#pendingPythonMessages.push(
|
|
16549
|
+
this.#pendingPythonMessages.push({
|
|
16550
|
+
message: pythonMessage,
|
|
16551
|
+
onPersisted: options?.onPersisted,
|
|
16552
|
+
appendedToAgent: false,
|
|
16553
|
+
});
|
|
16311
16554
|
} else {
|
|
16312
16555
|
this.agent.appendMessage(pythonMessage);
|
|
16313
16556
|
this.sessionManager.appendMessage(pythonMessage);
|
|
16557
|
+
options?.onPersisted?.();
|
|
16314
16558
|
}
|
|
16315
16559
|
}
|
|
16316
16560
|
|
|
@@ -16369,14 +16613,84 @@ export class AgentSession {
|
|
|
16369
16613
|
* Flush pending Python messages to agent state and session.
|
|
16370
16614
|
*/
|
|
16371
16615
|
#flushPendingPythonMessages(): void {
|
|
16372
|
-
|
|
16616
|
+
this.#flushPendingExecutionMessages(this.#pendingPythonMessages, "python");
|
|
16617
|
+
}
|
|
16373
16618
|
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16619
|
+
#flushPendingExecutionMessages<T extends BashExecutionMessage | PythonExecutionMessage>(
|
|
16620
|
+
pendingMessages: Array<{ message: T; onPersisted?: () => void; appendedToAgent: boolean }>,
|
|
16621
|
+
kind: "bash" | "python",
|
|
16622
|
+
): void {
|
|
16623
|
+
if (pendingMessages.length === 0) return;
|
|
16624
|
+
|
|
16625
|
+
const total = pendingMessages.length;
|
|
16626
|
+
const remaining: typeof pendingMessages = [];
|
|
16627
|
+
const errors: unknown[] = [];
|
|
16628
|
+
const persisted: string[] = [];
|
|
16629
|
+
let callbackFailureCount = 0;
|
|
16630
|
+
let persistenceBlocked = false;
|
|
16631
|
+
let agentAppendBlocked = false;
|
|
16632
|
+
for (const pending of pendingMessages) {
|
|
16633
|
+
if (!pending.appendedToAgent) {
|
|
16634
|
+
if (agentAppendBlocked) {
|
|
16635
|
+
remaining.push(pending);
|
|
16636
|
+
continue;
|
|
16637
|
+
}
|
|
16638
|
+
try {
|
|
16639
|
+
this.agent.appendMessage(pending.message);
|
|
16640
|
+
pending.appendedToAgent = true;
|
|
16641
|
+
} catch (error) {
|
|
16642
|
+
agentAppendBlocked = true;
|
|
16643
|
+
persistenceBlocked = true;
|
|
16644
|
+
remaining.push(pending);
|
|
16645
|
+
errors.push(error);
|
|
16646
|
+
continue;
|
|
16647
|
+
}
|
|
16648
|
+
}
|
|
16649
|
+
|
|
16650
|
+
if (persistenceBlocked) {
|
|
16651
|
+
remaining.push(pending);
|
|
16652
|
+
continue;
|
|
16653
|
+
}
|
|
16654
|
+
|
|
16655
|
+
try {
|
|
16656
|
+
this.sessionManager.appendMessage(pending.message);
|
|
16657
|
+
} catch (error) {
|
|
16658
|
+
// Session entries form a leaf-linked transcript. Once one append fails,
|
|
16659
|
+
// later entries must remain queued behind it or a retry would append the
|
|
16660
|
+
// failed entry after messages that originally followed it.
|
|
16661
|
+
persistenceBlocked = true;
|
|
16662
|
+
remaining.push(pending);
|
|
16663
|
+
errors.push(error);
|
|
16664
|
+
continue;
|
|
16665
|
+
}
|
|
16666
|
+
|
|
16667
|
+
persisted.push("command" in pending.message ? pending.message.command : pending.message.code);
|
|
16668
|
+
try {
|
|
16669
|
+
pending.onPersisted?.();
|
|
16670
|
+
} catch (error) {
|
|
16671
|
+
callbackFailureCount++;
|
|
16672
|
+
errors.push(error);
|
|
16673
|
+
}
|
|
16377
16674
|
}
|
|
16378
16675
|
|
|
16379
|
-
|
|
16676
|
+
pendingMessages.splice(0, pendingMessages.length, ...remaining);
|
|
16677
|
+
if (errors.length === 0) return;
|
|
16678
|
+
|
|
16679
|
+
const persistedCount = total - remaining.length;
|
|
16680
|
+
const persistenceFailureCount = remaining.length;
|
|
16681
|
+
const persistenceSummary =
|
|
16682
|
+
persistenceFailureCount > 0
|
|
16683
|
+
? `Failed to persist ${persistenceFailureCount} of ${total} deferred ${kind} execution message${total === 1 ? "" : "s"}; ${persistedCount} persisted, failed messages remain pending for retry`
|
|
16684
|
+
: `Persisted all ${total} deferred ${kind} execution message${total === 1 ? "" : "s"}`;
|
|
16685
|
+
const callbackSummary =
|
|
16686
|
+
callbackFailureCount > 0
|
|
16687
|
+
? `; ${callbackFailureCount} onPersisted callback${callbackFailureCount === 1 ? "" : "s"} failed`
|
|
16688
|
+
: "";
|
|
16689
|
+
const pending = remaining.map(item => ("command" in item.message ? item.message.command : item.message.code));
|
|
16690
|
+
throw new AggregateError(
|
|
16691
|
+
errors,
|
|
16692
|
+
`${persistenceSummary}${callbackSummary}; persisted: ${JSON.stringify(persisted)}; pending: ${JSON.stringify(pending)}`,
|
|
16693
|
+
);
|
|
16380
16694
|
}
|
|
16381
16695
|
|
|
16382
16696
|
// =========================================================================
|
|
@@ -17816,6 +18130,7 @@ export class AgentSession {
|
|
|
17816
18130
|
cost: totalCost,
|
|
17817
18131
|
...(hasCompleteCostBreakdown ? { costBreakdown: totalCostBreakdown } : {}),
|
|
17818
18132
|
premiumRequests: totalPremiumRequests,
|
|
18133
|
+
sessionMemory: this.sessionManager.getSessionMemoryStats(),
|
|
17819
18134
|
};
|
|
17820
18135
|
}
|
|
17821
18136
|
|