@gajae-code/coding-agent 0.12.19 → 0.12.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -9
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +66 -0
- package/dist/types/config/settings-schema.d.ts +34 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +97 -1
- package/dist/types/modes/acp/acp-agent.d.ts +33 -1
- package/dist/types/modes/components/bash-execution.d.ts +7 -0
- package/dist/types/modes/components/eval-execution.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +10 -1
- package/dist/types/sdk/acp/adapter.d.ts +9 -17
- package/dist/types/sdk/acp/index.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +93 -1
- package/dist/types/sdk/broker/ensure.d.ts +17 -0
- package/dist/types/sdk/broker/index.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +10 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +81 -2
- package/dist/types/sdk/broker/startup-budget.d.ts +44 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +11 -1
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +20 -9
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -6
- package/dist/types/sdk/bus/slack-daemon.d.ts +2 -2
- package/dist/types/sdk/host/query/revision-store.d.ts +5 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/lifecycle-session.d.ts +13 -2
- package/dist/types/sdk/prompt-failure.d.ts +13 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +82 -0
- package/dist/types/sdk/session-reconnect.d.ts +20 -0
- package/dist/types/sdk/startup-capability.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +32 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +40 -2
- package/dist/types/session/internal/session-memory-sidecar.d.ts +881 -0
- package/dist/types/session/session-manager.d.ts +301 -27
- package/dist/types/session/session-storage.d.ts +170 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tui/output-block.d.ts +6 -0
- package/package.json +8 -8
- package/src/cli/args.ts +75 -34
- package/src/commands/acp.ts +7 -1
- package/src/commands/gc.ts +31 -2
- package/src/commands/sdk.ts +141 -7
- package/src/config/settings-schema.ts +43 -0
- package/src/coordinator-mcp/server.ts +6 -1
- package/src/export/html/index.ts +83 -39
- package/src/gjc-runtime/gc-runtime.ts +1283 -4
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/local-protocol.ts +14 -0
- package/src/main.ts +40 -9
- package/src/modes/acp/acp-agent.ts +564 -37
- package/src/modes/acp/terminal-auth.ts +2 -2
- package/src/modes/components/bash-execution.ts +14 -0
- package/src/modes/components/eval-execution.ts +14 -0
- package/src/modes/controllers/command-controller.ts +63 -8
- package/src/modes/utils/ui-helpers.ts +81 -11
- package/src/sdk/acp/adapter.ts +46 -45
- package/src/sdk/acp/index.ts +1 -1
- package/src/sdk/broker/broker.ts +392 -11
- package/src/sdk/broker/ensure.ts +131 -61
- package/src/sdk/broker/index.ts +1 -0
- package/src/sdk/broker/lifecycle-ledger.ts +59 -5
- package/src/sdk/broker/lifecycle.ts +278 -63
- package/src/sdk/broker/startup-budget.ts +103 -0
- package/src/sdk/bus/chat-daemon-control.ts +9 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +565 -29
- package/src/sdk/bus/conversation-store.ts +1 -1
- package/src/sdk/bus/discord-daemon.ts +2 -1
- package/src/sdk/bus/discord-live-provider.ts +34 -6
- package/src/sdk/bus/index.ts +118 -20
- package/src/sdk/bus/kind-aware-reconciliation.ts +2 -1
- package/src/sdk/bus/prompt-reconciliation.ts +4 -9
- package/src/sdk/bus/slack-daemon.ts +91 -18
- package/src/sdk/cli/session-cli.ts +8 -1
- package/src/sdk/host/query/handlers.ts +1 -1
- package/src/sdk/host/query/revision-store.ts +48 -12
- package/src/sdk/host/session-runtime.test.ts +50 -0
- package/src/sdk/host/session-runtime.ts +12 -2
- package/src/sdk/lifecycle-session.ts +26 -3
- package/src/sdk/mcp/server.ts +6 -1
- package/src/sdk/prompt-failure.ts +42 -0
- package/src/sdk/prompt-watchdog.ts +162 -0
- package/src/sdk/session-reconnect.ts +43 -0
- package/src/sdk/startup-capability.ts +13 -4
- package/src/session/agent-session.ts +525 -222
- package/src/session/blob-store.ts +89 -0
- package/src/session/internal/managed-session-scope.ts +36 -26
- package/src/session/internal/managed-session-storage.ts +303 -32
- package/src/session/internal/session-memory-sidecar.ts +2160 -0
- package/src/session/session-manager.ts +7027 -394
- package/src/session/session-storage.ts +1134 -11
- package/src/task/executor.ts +17 -4
- package/src/tools/bash.ts +109 -7
- package/src/tools/find.ts +55 -26
- package/src/tools/todo-contract.ts +40 -8
- package/src/tui/output-block.ts +11 -1
|
@@ -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[]>;
|
|
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,58 @@ 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
|
+
if (promptWait === "publication") {
|
|
2633
|
+
await this.#agentEndPublicationPromise;
|
|
2634
|
+
} else if (promptWait === "full") {
|
|
2635
|
+
await this.#agentEndHandlingPromise;
|
|
2636
|
+
await this.#waitForPostPromptRecovery();
|
|
2637
|
+
await this.#agentEndPublicationPromise;
|
|
2638
|
+
}
|
|
2639
|
+
await this.#waitForSessionSettlement();
|
|
2640
|
+
if (flushError) throw flushError;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
#flushPendingPromptMessages(): void {
|
|
2644
|
+
const errors: unknown[] = [];
|
|
2645
|
+
try {
|
|
2646
|
+
this.#flushPendingBashMessages();
|
|
2647
|
+
} catch (error) {
|
|
2648
|
+
errors.push(error);
|
|
2649
|
+
}
|
|
2650
|
+
try {
|
|
2651
|
+
this.#flushPendingPythonMessages();
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
errors.push(error);
|
|
2654
|
+
}
|
|
2655
|
+
try {
|
|
2592
2656
|
this.#flushPendingBackgroundExchanges();
|
|
2593
|
-
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
errors.push(error);
|
|
2594
2659
|
}
|
|
2660
|
+
if (errors.length === 1) throw errors[0];
|
|
2661
|
+
if (errors.length > 1) throw new AggregateError(errors, "Multiple deferred prompt messages failed to flush");
|
|
2595
2662
|
}
|
|
2596
2663
|
|
|
2597
2664
|
#refreshTeamWorkerHeartbeat(): void {
|
|
@@ -2670,6 +2737,12 @@ export class AgentSession {
|
|
|
2670
2737
|
this.agent.bindRunCancellationDomainBridge(this.#runCancellationDomains, this.#agentSessionClaimKey);
|
|
2671
2738
|
this.sessionManager = config.sessionManager;
|
|
2672
2739
|
this.settings = config.settings;
|
|
2740
|
+
this.sessionManager.setSessionMemoryMode(this.settings.get("sessionMemory.mode"));
|
|
2741
|
+
this.#unregisterSessionMemorySettings = this.settings.onChanged(settingPath => {
|
|
2742
|
+
if (settingPath === "sessionMemory.mode") {
|
|
2743
|
+
this.sessionManager.setSessionMemoryMode(this.settings.get("sessionMemory.mode"));
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2673
2746
|
this.memoryBackend = config.memoryBackend ?? createMemoryBackendService(this.settings);
|
|
2674
2747
|
this.#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(
|
|
2675
2748
|
config.workerIntegrationRequest ??
|
|
@@ -6256,6 +6329,8 @@ export class AgentSession {
|
|
|
6256
6329
|
// No-op when this session opened no tabs. Failure is logged, not thrown.
|
|
6257
6330
|
this.#unregisterResourceGc?.();
|
|
6258
6331
|
this.#unregisterResourceGc = undefined;
|
|
6332
|
+
this.#unregisterSessionMemorySettings?.();
|
|
6333
|
+
this.#unregisterSessionMemorySettings = undefined;
|
|
6259
6334
|
if (ownerTerminalContextFromEnvironment() === null) this.#unregisterRuntimeStateFinalizer?.();
|
|
6260
6335
|
this.#unregisterRuntimeStateFinalizer = undefined;
|
|
6261
6336
|
await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
|
|
@@ -6320,6 +6395,8 @@ export class AgentSession {
|
|
|
6320
6395
|
const kernelOwnerId = this.#evalKernelOwnerId;
|
|
6321
6396
|
this.#unregisterResourceGc?.();
|
|
6322
6397
|
this.#unregisterResourceGc = undefined;
|
|
6398
|
+
this.#unregisterSessionMemorySettings?.();
|
|
6399
|
+
this.#unregisterSessionMemorySettings = undefined;
|
|
6323
6400
|
this.#unregisterTeamWorkerAsyncJobChange?.();
|
|
6324
6401
|
this.#unregisterTeamWorkerAsyncJobChange = undefined;
|
|
6325
6402
|
this.#teamWorkerHeartbeat?.dispose();
|
|
@@ -7604,8 +7681,7 @@ export class AgentSession {
|
|
|
7604
7681
|
await this.#waitForPostPromptRecovery();
|
|
7605
7682
|
} finally {
|
|
7606
7683
|
this.#removeEphemeralCustomMessages();
|
|
7607
|
-
this.#
|
|
7608
|
-
await this.#waitForSessionSettlement();
|
|
7684
|
+
await this.#settleEndedInFlight();
|
|
7609
7685
|
}
|
|
7610
7686
|
}
|
|
7611
7687
|
|
|
@@ -8799,10 +8875,8 @@ export class AgentSession {
|
|
|
8799
8875
|
this.#overflowMaintenanceAttempts = 0;
|
|
8800
8876
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
8801
8877
|
}
|
|
8802
|
-
//
|
|
8803
|
-
this.#
|
|
8804
|
-
this.#flushPendingPythonMessages();
|
|
8805
|
-
this.#flushPendingBackgroundExchanges();
|
|
8878
|
+
// Retry anything whose previous post-turn persistence failed before the new prompt.
|
|
8879
|
+
this.#flushPendingPromptMessages();
|
|
8806
8880
|
|
|
8807
8881
|
// Reset todo reminder count on new user prompt
|
|
8808
8882
|
this.#todoReminderCount = 0;
|
|
@@ -8837,160 +8911,198 @@ export class AgentSession {
|
|
|
8837
8911
|
]);
|
|
8838
8912
|
}
|
|
8839
8913
|
|
|
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
|
|
8914
|
+
// R3.2: one-time Phase A products are captured inside the closure and
|
|
8915
|
+
// executed once after admission acceptance; Phase B reassembles the
|
|
8916
|
+
// attempt messages after every idle wait / compact retry. Pending
|
|
8917
|
+
// next-turn messages are captured WITHOUT draining — the drain moves to
|
|
8918
|
+
// the run-accepted wrapper (R3.3) so AgentBusy/idle/compact-before-
|
|
8919
|
+
// acceptance never consume them.
|
|
8882
8920
|
pendingNextTurnMessageCount = this.#pendingNextTurnMessages.length;
|
|
8883
8921
|
hasPendingNextTurnMessages = pendingNextTurnMessageCount > 0;
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
const
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8922
|
+
const promptAttribution: "user" | "agent" | undefined =
|
|
8923
|
+
"attribution" in message ? message.attribution : undefined;
|
|
8924
|
+
let phaseACompleted = false;
|
|
8925
|
+
let fileMentionMessages: AgentMessage[] = [];
|
|
8926
|
+
let beforeAgentStartResultMessages: BeforeAgentStartInternalMessage[] = [];
|
|
8927
|
+
const contributedMessages: BeforeAgentStartInternalMessage[] = [];
|
|
8928
|
+
let recallMarked = false;
|
|
8929
|
+
let planReferenceMessage: CustomMessage | null = null;
|
|
8930
|
+
// Phase B attempt cache. AgentBusy/idle and forced-compaction retries clear
|
|
8931
|
+
// this cache so live plan/goal/volatile/MCP overlays are rebuilt. One-shot
|
|
8932
|
+
// Phase A products (including the plan reference) remain stable.
|
|
8933
|
+
let attemptMessages: AgentMessage[] | undefined;
|
|
8934
|
+
|
|
8935
|
+
const buildPreSubmit = async (): Promise<AgentMessage[]> => {
|
|
8936
|
+
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
8937
|
+
if (!phaseACompleted) {
|
|
8938
|
+
phaseACompleted = true;
|
|
8939
|
+
// Phase A (one-time side-effectful products; runs once).
|
|
8940
|
+
const fileMentions = extractFileMentions(expandedText);
|
|
8941
|
+
if (fileMentions.length > 0) {
|
|
8942
|
+
const cwd = this.sessionManager.getCwd();
|
|
8943
|
+
// Collect resolved paths already shown (read or mentioned) in the
|
|
8944
|
+
// recent window so a repeat @mention emits a compact note instead
|
|
8945
|
+
// of the full body.
|
|
8946
|
+
const RECENT_MENTION_WINDOW = 40;
|
|
8947
|
+
const recentlyShownPaths = new Set<string>();
|
|
8948
|
+
for (const entry of this.sessionManager.getBranch().slice(-RECENT_MENTION_WINDOW)) {
|
|
8949
|
+
if (entry.type !== "message") continue;
|
|
8950
|
+
const msg = entry.message;
|
|
8951
|
+
if (msg.role === "fileMention") {
|
|
8952
|
+
for (const file of msg.files) {
|
|
8953
|
+
if (!file.duplicate && !file.pruned) recentlyShownPaths.add(resolveReadPath(file.path, cwd));
|
|
8954
|
+
}
|
|
8955
|
+
} else if (msg.role === "toolResult") {
|
|
8956
|
+
const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
|
|
8957
|
+
if (typeof resolved === "string" && resolved)
|
|
8958
|
+
recentlyShownPaths.add(resolveReadPath(resolved, cwd));
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
fileMentionMessages = await generateFileMentionMessages(fileMentions, cwd, {
|
|
8962
|
+
autoResizeImages: this.settings.get("images.autoResize"),
|
|
8963
|
+
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
8964
|
+
maxInlineBytes: this.settings.get("tools.fileMentionInlineBytes") * 1024,
|
|
8965
|
+
recentlyShownPaths,
|
|
8966
|
+
});
|
|
8967
|
+
}
|
|
8968
|
+
const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
|
|
8969
|
+
hindsightRecall = this.getHindsightSessionState()?.getRecallSnippetForInjection();
|
|
8970
|
+
planReferenceMessage = await this.#buildPlanReferenceMessage();
|
|
8971
|
+
// Emit before_agent_start extension event. Race hook completion with
|
|
8972
|
+
// prompt cancellation so a wedged hook cannot retain SDK prompt authority.
|
|
8973
|
+
if (this.#extensionRunner?.hasHandlers("before_agent_start")) this.#markRetryReplayUnsafe();
|
|
8974
|
+
if (this.#extensionRunner) {
|
|
8975
|
+
const result = await this.#awaitPromptPreflight(
|
|
8976
|
+
generation,
|
|
8977
|
+
preflightSignal,
|
|
8978
|
+
this.#extensionRunner.emitBeforeAgentStart(
|
|
8979
|
+
expandedText,
|
|
8980
|
+
options?.images,
|
|
8981
|
+
beforeAgentStartSystemPrompt,
|
|
8982
|
+
),
|
|
8983
|
+
);
|
|
8984
|
+
if (result?.messages) beforeAgentStartResultMessages = [...result.messages];
|
|
8985
|
+
if (result?.systemPrompt !== undefined) {
|
|
8986
|
+
this.agent.setSystemPrompt(result.systemPrompt);
|
|
8987
|
+
} else {
|
|
8988
|
+
this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
|
|
8989
|
+
}
|
|
8990
|
+
} else {
|
|
8991
|
+
this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
|
|
8992
|
+
}
|
|
8993
|
+
// Invoke first-party internal before-agent-start contributors. These
|
|
8994
|
+
// run alongside the extension runner (not via user-loaded hooks) and
|
|
8995
|
+
// append through the same custom-message attribution path. Errors are nonfatal.
|
|
8996
|
+
if (this.#beforeAgentStartContributors.length > 0) {
|
|
8997
|
+
for (const contributor of this.#beforeAgentStartContributors) {
|
|
8998
|
+
try {
|
|
8999
|
+
const msg = await this.#awaitPromptPreflight(
|
|
9000
|
+
generation,
|
|
9001
|
+
preflightSignal,
|
|
9002
|
+
contributor({
|
|
9003
|
+
prompt: expandedText,
|
|
9004
|
+
images: options?.images,
|
|
9005
|
+
sessionId: this.sessionId,
|
|
9006
|
+
}),
|
|
9007
|
+
);
|
|
9008
|
+
if (msg) contributedMessages.push(msg);
|
|
9009
|
+
} catch (err) {
|
|
9010
|
+
if (this.#isPromptPreflightCancelled(generation, preflightSignal))
|
|
9011
|
+
throw promptPreflightCancelledError();
|
|
9012
|
+
logger.debug("before_agent_start contributor failed", { error: String(err) });
|
|
9013
|
+
}
|
|
8902
9014
|
}
|
|
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
9015
|
}
|
|
9016
|
+
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
8907
9017
|
}
|
|
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
9018
|
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
9019
|
+
// Phase B (attempt-dependent; re-runs after every idle wait / compact retry).
|
|
9020
|
+
// R3.2 overflow preflight: materialize the session context synchronously so
|
|
9021
|
+
// an over-budget graph throws SessionContextTooLargeError before any prompt.
|
|
9022
|
+
this.sessionManager.buildSessionContext();
|
|
9023
|
+
if (attemptMessages) {
|
|
9024
|
+
// Idle/AgentBusy retry: reuse the previously assembled attempt
|
|
9025
|
+
// messages; a superseded roster claim releases and the prompt
|
|
9026
|
+
// proceeds without it, matching the pre-seam reuse behavior.
|
|
9027
|
+
if (rosterClaim && !this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)) {
|
|
9028
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
9029
|
+
}
|
|
9030
|
+
return attemptMessages;
|
|
9031
|
+
}
|
|
9032
|
+
const messages: AgentMessage[] = [];
|
|
9033
|
+
const currentPlanReferenceMessage = planReferenceMessage;
|
|
9034
|
+
if (currentPlanReferenceMessage) {
|
|
9035
|
+
messages.push(currentPlanReferenceMessage);
|
|
9036
|
+
}
|
|
9037
|
+
const planModeMessage = await this.#buildAutomaticPlanModeMessage();
|
|
9038
|
+
if (planModeMessage) {
|
|
9039
|
+
messages.push(planModeMessage);
|
|
9040
|
+
}
|
|
9041
|
+
const goalModeMessage = this.#buildAutomaticGoalModeMessage();
|
|
9042
|
+
if (goalModeMessage) {
|
|
9043
|
+
messages.push(goalModeMessage);
|
|
9044
|
+
}
|
|
9045
|
+
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
9046
|
+
messages.push(volatileProjectContextMessage);
|
|
9047
|
+
const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
|
|
9048
|
+
if (untrustedMcpServerInstructionsMessage) messages.push(untrustedMcpServerInstructionsMessage);
|
|
9049
|
+
|
|
9050
|
+
// Roster: one Phase A claim, revalidated and reused on every attempt;
|
|
9051
|
+
// a superseded claim releases and the prompt proceeds without it.
|
|
9052
|
+
if (rosterClaim && this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)) {
|
|
9053
|
+
messages.push(rosterClaim.message);
|
|
9054
|
+
} else if (rosterClaim) {
|
|
9055
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
9056
|
+
}
|
|
9057
|
+
if (options?.prependMessages) {
|
|
9058
|
+
messages.push(...options.prependMessages);
|
|
9059
|
+
}
|
|
8935
9060
|
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
if (this.#extensionRunner?.hasHandlers("before_agent_start")) this.#markRetryReplayUnsafe();
|
|
9061
|
+
const promptIndex = messages.length;
|
|
9062
|
+
messages.push(message);
|
|
8939
9063
|
|
|
8940
|
-
|
|
8941
|
-
const
|
|
8942
|
-
|
|
8943
|
-
preflightSignal,
|
|
8944
|
-
this.#extensionRunner.emitBeforeAgentStart(expandedText, options?.images, beforeAgentStartSystemPrompt),
|
|
8945
|
-
);
|
|
8946
|
-
if (result?.messages) {
|
|
8947
|
-
this.#appendBeforeAgentStartCustomMessages(messages, result.messages, promptAttribution, message.role);
|
|
9064
|
+
// Re-present captured pending next-turn messages (never drained here).
|
|
9065
|
+
for (const msg of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
|
|
9066
|
+
messages.push(msg);
|
|
8948
9067
|
}
|
|
8949
|
-
|
|
8950
|
-
if (
|
|
8951
|
-
this.
|
|
8952
|
-
|
|
8953
|
-
|
|
9068
|
+
messages.push(...fileMentionMessages);
|
|
9069
|
+
if (hindsightRecall) {
|
|
9070
|
+
// Recall is provider-only context for this request. It must precede
|
|
9071
|
+
// the actual prompt but never become part of durable session history.
|
|
9072
|
+
messages.splice(promptIndex, 0, {
|
|
9073
|
+
role: "custom",
|
|
9074
|
+
customType: "hindsight-recall",
|
|
9075
|
+
content: hindsightRecall,
|
|
9076
|
+
display: false,
|
|
9077
|
+
attribution: "agent",
|
|
9078
|
+
timestamp: Date.now(),
|
|
9079
|
+
});
|
|
8954
9080
|
}
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
if (this.#beforeAgentStartContributors.length > 0) {
|
|
8963
|
-
const contributed: BeforeAgentStartInternalMessage[] = [];
|
|
8964
|
-
for (const contributor of this.#beforeAgentStartContributors) {
|
|
8965
|
-
try {
|
|
8966
|
-
const msg = await this.#awaitPromptPreflight(
|
|
8967
|
-
generation,
|
|
8968
|
-
preflightSignal,
|
|
8969
|
-
contributor({
|
|
8970
|
-
prompt: expandedText,
|
|
8971
|
-
images: options?.images,
|
|
8972
|
-
sessionId: this.sessionId,
|
|
8973
|
-
}),
|
|
8974
|
-
);
|
|
8975
|
-
if (msg) contributed.push(msg);
|
|
8976
|
-
} catch (err) {
|
|
8977
|
-
if (this.#isPromptPreflightCancelled(generation, preflightSignal))
|
|
8978
|
-
throw promptPreflightCancelledError();
|
|
8979
|
-
logger.debug("before_agent_start contributor failed", { error: String(err) });
|
|
8980
|
-
}
|
|
9081
|
+
if (beforeAgentStartResultMessages.length > 0) {
|
|
9082
|
+
this.#appendBeforeAgentStartCustomMessages(
|
|
9083
|
+
messages,
|
|
9084
|
+
beforeAgentStartResultMessages,
|
|
9085
|
+
promptAttribution,
|
|
9086
|
+
message.role,
|
|
9087
|
+
);
|
|
8981
9088
|
}
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
9089
|
+
if (contributedMessages.length > 0) {
|
|
9090
|
+
this.#appendBeforeAgentStartCustomMessages(
|
|
9091
|
+
messages,
|
|
9092
|
+
contributedMessages,
|
|
9093
|
+
promptAttribution,
|
|
9094
|
+
message.role,
|
|
9095
|
+
);
|
|
9096
|
+
}
|
|
9097
|
+
attemptMessages = messages;
|
|
9098
|
+
return messages;
|
|
9099
|
+
};
|
|
9100
|
+
const preSubmit: PreSubmitBuilder = {
|
|
9101
|
+
build: buildPreSubmit,
|
|
9102
|
+
reset: () => {
|
|
9103
|
+
attemptMessages = undefined;
|
|
9104
|
+
},
|
|
9105
|
+
};
|
|
8994
9106
|
|
|
8995
9107
|
const agentPromptOptions = {
|
|
8996
9108
|
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
@@ -8999,21 +9111,31 @@ export class AgentSession {
|
|
|
8999
9111
|
this.#acceptRunHandle(handle);
|
|
9000
9112
|
options?.onRunAccepted?.(handle);
|
|
9001
9113
|
options?.admissionLease?.release();
|
|
9002
|
-
|
|
9114
|
+
// R3.3: the accepted-run wrapper is the exact acceptance boundary —
|
|
9115
|
+
// pending next-turn drain and the exactly-once recall mark live here.
|
|
9116
|
+
if (hindsightRecall && !recallMarked) {
|
|
9117
|
+
recallMarked = true;
|
|
9118
|
+
this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
|
|
9119
|
+
}
|
|
9120
|
+
if (pendingNextTurnMessageCount > 0) {
|
|
9121
|
+
this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
|
|
9122
|
+
}
|
|
9123
|
+
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
9003
9124
|
},
|
|
9004
9125
|
};
|
|
9005
|
-
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
9006
|
-
else options?.onPreflightAccepted?.();
|
|
9007
9126
|
if (options?.onFinalPreflight && !(await options.onFinalPreflight({ hasPendingNextTurnMessages }))) {
|
|
9008
9127
|
this.#resetInjectedContextSignatures();
|
|
9009
9128
|
return;
|
|
9010
9129
|
}
|
|
9011
|
-
this.#
|
|
9012
|
-
|
|
9013
|
-
this.#
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9130
|
+
await this.#promptAgentWithIdleRetry(preSubmit, agentPromptOptions, predecessorAgentEndHold, {
|
|
9131
|
+
signal: preflightSignal,
|
|
9132
|
+
resourceRunId: this.#runResourceLeaseContext.getStore()?.resourceRunId,
|
|
9133
|
+
onPreflightAccepted: () => {
|
|
9134
|
+
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
9135
|
+
if (options?.onPreflightAcceptCommit) return options.onPreflightAcceptCommit();
|
|
9136
|
+
options?.onPreflightAccepted?.();
|
|
9137
|
+
},
|
|
9138
|
+
});
|
|
9017
9139
|
const terminalAssistant = this.#findLastAssistantMessage();
|
|
9018
9140
|
if (
|
|
9019
9141
|
rosterClaim &&
|
|
@@ -9048,14 +9170,7 @@ export class AgentSession {
|
|
|
9048
9170
|
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
9049
9171
|
}
|
|
9050
9172
|
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
|
-
}
|
|
9173
|
+
await this.#settleEndedInFlight(options?.skipPostPromptRecoveryWait ? "publication" : "full");
|
|
9059
9174
|
}
|
|
9060
9175
|
}
|
|
9061
9176
|
|
|
@@ -10712,19 +10827,56 @@ export class AgentSession {
|
|
|
10712
10827
|
// Flush current session to ensure all entries are written
|
|
10713
10828
|
await this.sessionManager.flush();
|
|
10714
10829
|
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10830
|
+
const boundedColdForkEligible =
|
|
10831
|
+
this.sessionManager.getSessionMemoryStats().coldRetirementActive &&
|
|
10832
|
+
!this.sessionManager.isManagedDestination() &&
|
|
10833
|
+
previousSessionFile !== undefined;
|
|
10834
|
+
if (boundedColdForkEligible) {
|
|
10835
|
+
const previousManager = this.sessionManager;
|
|
10836
|
+
const forkedManager = await SessionManager.forkFrom(
|
|
10837
|
+
previousSessionFile,
|
|
10838
|
+
previousManager.getCwd(),
|
|
10839
|
+
SessionManager.explicitDestination(previousManager.getSessionDir()),
|
|
10840
|
+
undefined,
|
|
10841
|
+
"copy-retain",
|
|
10842
|
+
this.settings.get("sessionMemory.mode"),
|
|
10843
|
+
);
|
|
10844
|
+
try {
|
|
10845
|
+
await initializeLocalRoot({
|
|
10846
|
+
getArtifactsDir: () => forkedManager.getArtifactsDir(),
|
|
10847
|
+
isManagedDestination: () => false,
|
|
10848
|
+
getManagedLegacyLocalMigrationSource: () => null,
|
|
10849
|
+
getSessionId: () => forkedManager.getSessionId(),
|
|
10850
|
+
});
|
|
10851
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
10852
|
+
} catch (error) {
|
|
10853
|
+
const forkedFile = forkedManager.getSessionFile();
|
|
10854
|
+
await forkedManager.close();
|
|
10855
|
+
if (forkedFile) await previousManager.discardUncommittedSession(forkedFile);
|
|
10856
|
+
throw error;
|
|
10857
|
+
}
|
|
10858
|
+
this.sessionManager = forkedManager;
|
|
10859
|
+
try {
|
|
10860
|
+
await previousManager.close();
|
|
10861
|
+
} catch (error) {
|
|
10862
|
+
logger.warn("Previous session close failed after bounded fork adoption", {
|
|
10863
|
+
error: error instanceof Error ? error.message : String(error),
|
|
10864
|
+
});
|
|
10865
|
+
}
|
|
10725
10866
|
await this.#runToolSessionTransitionCleanups();
|
|
10726
|
-
}
|
|
10727
|
-
|
|
10867
|
+
} else {
|
|
10868
|
+
// Prepare the copied successor and complete local:// readiness while all
|
|
10869
|
+
// public manager getters remain bound to the predecessor.
|
|
10870
|
+
const prepared = await this.sessionManager.prepareFork();
|
|
10871
|
+
if (!prepared) return false;
|
|
10872
|
+
try {
|
|
10873
|
+
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10874
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
10875
|
+
this.sessionManager.commitPreparedNewSession(prepared);
|
|
10876
|
+
await this.#runToolSessionTransitionCleanups();
|
|
10877
|
+
} catch (error) {
|
|
10878
|
+
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
10879
|
+
}
|
|
10728
10880
|
}
|
|
10729
10881
|
this.#syncAgentSessionId();
|
|
10730
10882
|
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
@@ -13363,13 +13515,15 @@ export class AgentSession {
|
|
|
13363
13515
|
}
|
|
13364
13516
|
}
|
|
13365
13517
|
}
|
|
13518
|
+
const sessionMemory = this.sessionManager.getSessionMemoryStats();
|
|
13366
13519
|
return {
|
|
13367
13520
|
heapUsedBytes: process.memoryUsage().heapUsed,
|
|
13368
13521
|
providerBytes,
|
|
13369
13522
|
messageCount: this.state.messages.length,
|
|
13370
13523
|
imageBytes,
|
|
13371
13524
|
sessionResidentImageBytes: this.sessionManager.getResidentImageBytes(),
|
|
13372
|
-
materializedResidentBytes:
|
|
13525
|
+
materializedResidentBytes:
|
|
13526
|
+
this.#streamingEditFileCache.totalBytes + sessionMemory.hotRegionBytes + sessionMemory.metaDescriptorBytes,
|
|
13373
13527
|
tuiChatChildren: retainedMemory.tuiChatChildren ?? 0,
|
|
13374
13528
|
tuiCachedRenderBytes: retainedMemory.tuiCachedRenderBytes ?? 0,
|
|
13375
13529
|
};
|
|
@@ -13777,11 +13931,6 @@ export class AgentSession {
|
|
|
13777
13931
|
`(Reminder ${this.#todoReminderCount}/${remindersMax})\n` +
|
|
13778
13932
|
`</system-reminder>`;
|
|
13779
13933
|
|
|
13780
|
-
logger.debug("Todo completion: sending reminder", {
|
|
13781
|
-
incomplete: incomplete.length,
|
|
13782
|
-
attempt: this.#todoReminderCount,
|
|
13783
|
-
});
|
|
13784
|
-
|
|
13785
13934
|
// Emit event for UI to render notification
|
|
13786
13935
|
await this.#emitSessionEvent({
|
|
13787
13936
|
type: "todo_reminder",
|
|
@@ -13790,7 +13939,27 @@ export class AgentSession {
|
|
|
13790
13939
|
maxAttempts: remindersMax,
|
|
13791
13940
|
});
|
|
13792
13941
|
|
|
13793
|
-
//
|
|
13942
|
+
// Consumers that cannot represent a server-initiated turn (ACP v1 clients, SDK
|
|
13943
|
+
// hosts) keep reporting the prompt as running until the terminal `agent_end` is
|
|
13944
|
+
// published. Injecting here would park that terminal behind a continuation hold
|
|
13945
|
+
// (`#scheduleAgentContinue` -> `#reserveDeferredAgentEndForContinuation`), so a
|
|
13946
|
+
// turn that already delivered its final answer would never settle — and there is
|
|
13947
|
+
// no interactive consumer to act on the nudge. Those hosts get the advisory
|
|
13948
|
+
// event above and nothing else.
|
|
13949
|
+
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
13950
|
+
logger.debug("Todo completion: advisory reminder only", {
|
|
13951
|
+
incomplete: incomplete.length,
|
|
13952
|
+
attempt: this.#todoReminderCount,
|
|
13953
|
+
});
|
|
13954
|
+
return;
|
|
13955
|
+
}
|
|
13956
|
+
|
|
13957
|
+
logger.debug("Todo completion: sending reminder", {
|
|
13958
|
+
incomplete: incomplete.length,
|
|
13959
|
+
attempt: this.#todoReminderCount,
|
|
13960
|
+
});
|
|
13961
|
+
|
|
13962
|
+
// Inject reminder and continue conversation
|
|
13794
13963
|
this.agent.appendMessage({
|
|
13795
13964
|
role: "developer",
|
|
13796
13965
|
content: [{ type: "text", text: reminder }],
|
|
@@ -15077,6 +15246,9 @@ export class AgentSession {
|
|
|
15077
15246
|
)
|
|
15078
15247
|
);
|
|
15079
15248
|
}
|
|
15249
|
+
#isIdleStreamStallErrorMessage(errorMessage: string): boolean {
|
|
15250
|
+
return /stream stalled while waiting for the next event/i.test(errorMessage);
|
|
15251
|
+
}
|
|
15080
15252
|
|
|
15081
15253
|
#isFirstEventTimeoutErrorMessage(errorMessage: string): boolean {
|
|
15082
15254
|
// First-event timeout: the stream watchdog aborted because no event
|
|
@@ -15133,8 +15305,8 @@ export class AgentSession {
|
|
|
15133
15305
|
/**
|
|
15134
15306
|
* Ordered retry classification: typed safety stop (surface) -> legacy safety stop
|
|
15135
15307
|
* (surface) -> overflow (compaction) -> terminal (surface) -> usage_limit
|
|
15136
|
-
* (rotation) -> first_event_timeout (bounded retry) -> transient (unbounded retry
|
|
15137
|
-
* unknown (bounded retry).
|
|
15308
|
+
* (rotation) -> first_event_timeout (bounded retry) -> transient (unbounded retry,
|
|
15309
|
+
* except canonical idle-stream stalls bounded downstream) -> unknown (bounded retry).
|
|
15138
15310
|
*/
|
|
15139
15311
|
#classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
|
|
15140
15312
|
if (message.stopReason !== "error") return "none";
|
|
@@ -15692,7 +15864,10 @@ export class AgentSession {
|
|
|
15692
15864
|
return false;
|
|
15693
15865
|
}
|
|
15694
15866
|
}
|
|
15695
|
-
const legacyUnbounded =
|
|
15867
|
+
const legacyUnbounded =
|
|
15868
|
+
!managedFallback &&
|
|
15869
|
+
classification === "transient" &&
|
|
15870
|
+
!this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
|
|
15696
15871
|
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
15697
15872
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
15698
15873
|
let outcome = managedFallback
|
|
@@ -15958,16 +16133,25 @@ export class AgentSession {
|
|
|
15958
16133
|
}
|
|
15959
16134
|
|
|
15960
16135
|
async #promptAgentWithIdleRetry(
|
|
15961
|
-
|
|
16136
|
+
preSubmit: PreSubmitBuilder,
|
|
15962
16137
|
options?: {
|
|
15963
16138
|
toolChoice?: ToolChoice;
|
|
15964
16139
|
fallbackManaged?: boolean;
|
|
15965
16140
|
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
15966
16141
|
},
|
|
15967
16142
|
predecessorAgentEndHold?: symbol,
|
|
16143
|
+
seam?: {
|
|
16144
|
+
signal?: AbortSignal;
|
|
16145
|
+
resourceRunId?: string;
|
|
16146
|
+
onPreflightAccepted?: () => void | Promise<void>;
|
|
16147
|
+
},
|
|
15968
16148
|
): Promise<void> {
|
|
15969
16149
|
const deadline = Date.now() + 30_000;
|
|
15970
16150
|
let continuationHold = predecessorAgentEndHold;
|
|
16151
|
+
// R3.2 helper-local compact-once flag: fresh per prompt; the inline retry
|
|
16152
|
+
// re-runs Phase B via preSubmit after the forced compaction.
|
|
16153
|
+
let overflowRetried = false;
|
|
16154
|
+
let preflightAccepted = false;
|
|
15971
16155
|
for (;;) {
|
|
15972
16156
|
try {
|
|
15973
16157
|
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(
|
|
@@ -15975,11 +16159,46 @@ export class AgentSession {
|
|
|
15975
16159
|
);
|
|
15976
16160
|
continuationHold = undefined;
|
|
15977
16161
|
try {
|
|
16162
|
+
const messages = await preSubmit.build();
|
|
16163
|
+
if (!preflightAccepted) {
|
|
16164
|
+
await seam?.onPreflightAccepted?.();
|
|
16165
|
+
preflightAccepted = true;
|
|
16166
|
+
}
|
|
15978
16167
|
await this.agent.prompt(messages, options);
|
|
15979
16168
|
this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
|
|
15980
16169
|
return;
|
|
15981
16170
|
} catch (error) {
|
|
15982
16171
|
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
16172
|
+
if (
|
|
16173
|
+
error instanceof SessionContextTooLargeError &&
|
|
16174
|
+
!overflowRetried &&
|
|
16175
|
+
this.settings.get("sessionMemory.contextOverflowRecovery")
|
|
16176
|
+
) {
|
|
16177
|
+
// D7: exactly one forced, no-continuation compaction
|
|
16178
|
+
// (`willRetry:false`, `continueAfterMaintenance:false`), then
|
|
16179
|
+
// exactly one inline retry. Any other terminal status rethrows
|
|
16180
|
+
// the ORIGINAL typed error with measurements.
|
|
16181
|
+
overflowRetried = true;
|
|
16182
|
+
let compacted = false;
|
|
16183
|
+
try {
|
|
16184
|
+
const status = await this.#runAutoCompaction("overflow", false, false, {
|
|
16185
|
+
force: true,
|
|
16186
|
+
continueAfterMaintenance: false,
|
|
16187
|
+
signal: seam?.signal,
|
|
16188
|
+
resourceRunId: seam?.resourceRunId,
|
|
16189
|
+
});
|
|
16190
|
+
compacted = status.kind === "compacted";
|
|
16191
|
+
} catch {
|
|
16192
|
+
compacted = false;
|
|
16193
|
+
}
|
|
16194
|
+
if (compacted) {
|
|
16195
|
+
// Post-compaction Phase B must rebuild overlays (the
|
|
16196
|
+
// injected-context signatures were reset by
|
|
16197
|
+
// #applyCompactionPostAppend), so drop the cached attempt.
|
|
16198
|
+
preSubmit.reset();
|
|
16199
|
+
continue;
|
|
16200
|
+
}
|
|
16201
|
+
}
|
|
15983
16202
|
throw error;
|
|
15984
16203
|
}
|
|
15985
16204
|
} catch (err) {
|
|
@@ -15990,6 +16209,7 @@ export class AgentSession {
|
|
|
15990
16209
|
throw new Error("Timed out waiting for prior agent run to finish before prompting.");
|
|
15991
16210
|
}
|
|
15992
16211
|
await this.agent.waitForIdle();
|
|
16212
|
+
preSubmit.reset();
|
|
15993
16213
|
}
|
|
15994
16214
|
}
|
|
15995
16215
|
}
|
|
@@ -16086,11 +16306,13 @@ export class AgentSession {
|
|
|
16086
16306
|
* @param command The bash command to execute
|
|
16087
16307
|
* @param onChunk Optional streaming callback for output
|
|
16088
16308
|
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
|
|
16309
|
+
* @param options.onPersisted Called once the execution's message is in session state
|
|
16310
|
+
* (immediately when idle, at the post-turn flush while streaming)
|
|
16089
16311
|
*/
|
|
16090
16312
|
async executeBash(
|
|
16091
16313
|
command: string,
|
|
16092
16314
|
onChunk?: (chunk: string) => void,
|
|
16093
|
-
options?: { excludeFromContext?: boolean },
|
|
16315
|
+
options?: { excludeFromContext?: boolean; onPersisted?: () => void },
|
|
16094
16316
|
): Promise<BashResult> {
|
|
16095
16317
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
16096
16318
|
this.#markRetryReplayUnsafe();
|
|
@@ -16145,7 +16367,11 @@ export class AgentSession {
|
|
|
16145
16367
|
* Record a bash execution result in session history.
|
|
16146
16368
|
* Used by executeBash and by extensions that handle bash execution themselves.
|
|
16147
16369
|
*/
|
|
16148
|
-
recordBashResult(
|
|
16370
|
+
recordBashResult(
|
|
16371
|
+
command: string,
|
|
16372
|
+
result: BashResult,
|
|
16373
|
+
options?: { excludeFromContext?: boolean; onPersisted?: () => void },
|
|
16374
|
+
): void {
|
|
16149
16375
|
const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
|
|
16150
16376
|
const bashMessage: BashExecutionMessage = {
|
|
16151
16377
|
role: "bashExecution",
|
|
@@ -16162,13 +16388,18 @@ export class AgentSession {
|
|
|
16162
16388
|
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
16163
16389
|
if (this.isStreaming) {
|
|
16164
16390
|
// Queue for later - will be flushed on agent_end
|
|
16165
|
-
this.#pendingBashMessages.push(
|
|
16391
|
+
this.#pendingBashMessages.push({
|
|
16392
|
+
message: bashMessage,
|
|
16393
|
+
onPersisted: options?.onPersisted,
|
|
16394
|
+
appendedToAgent: false,
|
|
16395
|
+
});
|
|
16166
16396
|
} else {
|
|
16167
16397
|
// Add to agent state immediately
|
|
16168
16398
|
this.agent.appendMessage(bashMessage);
|
|
16169
16399
|
|
|
16170
16400
|
// Save to session
|
|
16171
16401
|
this.sessionManager.appendMessage(bashMessage);
|
|
16402
|
+
options?.onPersisted?.();
|
|
16172
16403
|
}
|
|
16173
16404
|
}
|
|
16174
16405
|
|
|
@@ -16196,17 +16427,7 @@ export class AgentSession {
|
|
|
16196
16427
|
* Called after agent turn completes to maintain proper message ordering.
|
|
16197
16428
|
*/
|
|
16198
16429
|
#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 = [];
|
|
16430
|
+
this.#flushPendingExecutionMessages(this.#pendingBashMessages, "bash");
|
|
16210
16431
|
}
|
|
16211
16432
|
|
|
16212
16433
|
// =========================================================================
|
|
@@ -16219,11 +16440,13 @@ export class AgentSession {
|
|
|
16219
16440
|
* @param code The Python code to execute
|
|
16220
16441
|
* @param onChunk Optional streaming callback for output
|
|
16221
16442
|
* @param options.excludeFromContext If true, execution won't be sent to LLM ($$ prefix)
|
|
16443
|
+
* @param options.onPersisted Called once the execution's message is in session state
|
|
16444
|
+
* (immediately when idle, at the post-turn flush while streaming)
|
|
16222
16445
|
*/
|
|
16223
16446
|
async executePython(
|
|
16224
16447
|
code: string,
|
|
16225
16448
|
onChunk?: (chunk: string) => void,
|
|
16226
|
-
options?: { excludeFromContext?: boolean },
|
|
16449
|
+
options?: { excludeFromContext?: boolean; onPersisted?: () => void },
|
|
16227
16450
|
): Promise<PythonResult> {
|
|
16228
16451
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
16229
16452
|
this.#markRetryReplayUnsafe();
|
|
@@ -16291,7 +16514,11 @@ export class AgentSession {
|
|
|
16291
16514
|
/**
|
|
16292
16515
|
* Record a Python execution result in session history.
|
|
16293
16516
|
*/
|
|
16294
|
-
recordPythonResult(
|
|
16517
|
+
recordPythonResult(
|
|
16518
|
+
code: string,
|
|
16519
|
+
result: PythonResult,
|
|
16520
|
+
options?: { excludeFromContext?: boolean; onPersisted?: () => void },
|
|
16521
|
+
): void {
|
|
16295
16522
|
const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
|
|
16296
16523
|
const pythonMessage: PythonExecutionMessage = {
|
|
16297
16524
|
role: "pythonExecution",
|
|
@@ -16307,10 +16534,15 @@ export class AgentSession {
|
|
|
16307
16534
|
|
|
16308
16535
|
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
16309
16536
|
if (this.isStreaming) {
|
|
16310
|
-
this.#pendingPythonMessages.push(
|
|
16537
|
+
this.#pendingPythonMessages.push({
|
|
16538
|
+
message: pythonMessage,
|
|
16539
|
+
onPersisted: options?.onPersisted,
|
|
16540
|
+
appendedToAgent: false,
|
|
16541
|
+
});
|
|
16311
16542
|
} else {
|
|
16312
16543
|
this.agent.appendMessage(pythonMessage);
|
|
16313
16544
|
this.sessionManager.appendMessage(pythonMessage);
|
|
16545
|
+
options?.onPersisted?.();
|
|
16314
16546
|
}
|
|
16315
16547
|
}
|
|
16316
16548
|
|
|
@@ -16369,14 +16601,84 @@ export class AgentSession {
|
|
|
16369
16601
|
* Flush pending Python messages to agent state and session.
|
|
16370
16602
|
*/
|
|
16371
16603
|
#flushPendingPythonMessages(): void {
|
|
16372
|
-
|
|
16604
|
+
this.#flushPendingExecutionMessages(this.#pendingPythonMessages, "python");
|
|
16605
|
+
}
|
|
16373
16606
|
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16607
|
+
#flushPendingExecutionMessages<T extends BashExecutionMessage | PythonExecutionMessage>(
|
|
16608
|
+
pendingMessages: Array<{ message: T; onPersisted?: () => void; appendedToAgent: boolean }>,
|
|
16609
|
+
kind: "bash" | "python",
|
|
16610
|
+
): void {
|
|
16611
|
+
if (pendingMessages.length === 0) return;
|
|
16612
|
+
|
|
16613
|
+
const total = pendingMessages.length;
|
|
16614
|
+
const remaining: typeof pendingMessages = [];
|
|
16615
|
+
const errors: unknown[] = [];
|
|
16616
|
+
const persisted: string[] = [];
|
|
16617
|
+
let callbackFailureCount = 0;
|
|
16618
|
+
let persistenceBlocked = false;
|
|
16619
|
+
let agentAppendBlocked = false;
|
|
16620
|
+
for (const pending of pendingMessages) {
|
|
16621
|
+
if (!pending.appendedToAgent) {
|
|
16622
|
+
if (agentAppendBlocked) {
|
|
16623
|
+
remaining.push(pending);
|
|
16624
|
+
continue;
|
|
16625
|
+
}
|
|
16626
|
+
try {
|
|
16627
|
+
this.agent.appendMessage(pending.message);
|
|
16628
|
+
pending.appendedToAgent = true;
|
|
16629
|
+
} catch (error) {
|
|
16630
|
+
agentAppendBlocked = true;
|
|
16631
|
+
persistenceBlocked = true;
|
|
16632
|
+
remaining.push(pending);
|
|
16633
|
+
errors.push(error);
|
|
16634
|
+
continue;
|
|
16635
|
+
}
|
|
16636
|
+
}
|
|
16637
|
+
|
|
16638
|
+
if (persistenceBlocked) {
|
|
16639
|
+
remaining.push(pending);
|
|
16640
|
+
continue;
|
|
16641
|
+
}
|
|
16642
|
+
|
|
16643
|
+
try {
|
|
16644
|
+
this.sessionManager.appendMessage(pending.message);
|
|
16645
|
+
} catch (error) {
|
|
16646
|
+
// Session entries form a leaf-linked transcript. Once one append fails,
|
|
16647
|
+
// later entries must remain queued behind it or a retry would append the
|
|
16648
|
+
// failed entry after messages that originally followed it.
|
|
16649
|
+
persistenceBlocked = true;
|
|
16650
|
+
remaining.push(pending);
|
|
16651
|
+
errors.push(error);
|
|
16652
|
+
continue;
|
|
16653
|
+
}
|
|
16654
|
+
|
|
16655
|
+
persisted.push("command" in pending.message ? pending.message.command : pending.message.code);
|
|
16656
|
+
try {
|
|
16657
|
+
pending.onPersisted?.();
|
|
16658
|
+
} catch (error) {
|
|
16659
|
+
callbackFailureCount++;
|
|
16660
|
+
errors.push(error);
|
|
16661
|
+
}
|
|
16377
16662
|
}
|
|
16378
16663
|
|
|
16379
|
-
|
|
16664
|
+
pendingMessages.splice(0, pendingMessages.length, ...remaining);
|
|
16665
|
+
if (errors.length === 0) return;
|
|
16666
|
+
|
|
16667
|
+
const persistedCount = total - remaining.length;
|
|
16668
|
+
const persistenceFailureCount = remaining.length;
|
|
16669
|
+
const persistenceSummary =
|
|
16670
|
+
persistenceFailureCount > 0
|
|
16671
|
+
? `Failed to persist ${persistenceFailureCount} of ${total} deferred ${kind} execution message${total === 1 ? "" : "s"}; ${persistedCount} persisted, failed messages remain pending for retry`
|
|
16672
|
+
: `Persisted all ${total} deferred ${kind} execution message${total === 1 ? "" : "s"}`;
|
|
16673
|
+
const callbackSummary =
|
|
16674
|
+
callbackFailureCount > 0
|
|
16675
|
+
? `; ${callbackFailureCount} onPersisted callback${callbackFailureCount === 1 ? "" : "s"} failed`
|
|
16676
|
+
: "";
|
|
16677
|
+
const pending = remaining.map(item => ("command" in item.message ? item.message.command : item.message.code));
|
|
16678
|
+
throw new AggregateError(
|
|
16679
|
+
errors,
|
|
16680
|
+
`${persistenceSummary}${callbackSummary}; persisted: ${JSON.stringify(persisted)}; pending: ${JSON.stringify(pending)}`,
|
|
16681
|
+
);
|
|
16380
16682
|
}
|
|
16381
16683
|
|
|
16382
16684
|
// =========================================================================
|
|
@@ -17816,6 +18118,7 @@ export class AgentSession {
|
|
|
17816
18118
|
cost: totalCost,
|
|
17817
18119
|
...(hasCompleteCostBreakdown ? { costBreakdown: totalCostBreakdown } : {}),
|
|
17818
18120
|
premiumRequests: totalPremiumRequests,
|
|
18121
|
+
sessionMemory: this.sessionManager.getSessionMemoryStats(),
|
|
17819
18122
|
};
|
|
17820
18123
|
}
|
|
17821
18124
|
|