@gajae-code/coding-agent 0.12.5 → 0.12.7
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 +34 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +43 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +430 -46
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/modes/acp/acp-agent.ts +27 -13
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +30 -6
- package/src/sdk/bus/index.ts +23 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +752 -339
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +1 -0
- package/src/session/session-manager.ts +7 -0
- package/src/session/session-storage.ts +146 -24
- package/src/task/index.ts +230 -19
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +2 -0
- package/src/task/types.ts +10 -0
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
|
@@ -32,16 +32,21 @@ import {
|
|
|
32
32
|
type AgentTool,
|
|
33
33
|
assertImagePlaceholdersHavePayload,
|
|
34
34
|
canContinuePersistedHistory,
|
|
35
|
+
getAgentTerminalOwnerContext,
|
|
35
36
|
type ManagedAttemptContinuationOwnership,
|
|
36
37
|
type ManagedAttemptDecision,
|
|
37
38
|
type ManagedAttemptOutcome,
|
|
38
39
|
type MidRunMaintenanceOutcome,
|
|
40
|
+
type RunCancellationDomain,
|
|
41
|
+
type RunCancellationDomainBridge,
|
|
42
|
+
type RunResourceProducerLease,
|
|
39
43
|
type RunSettlementProof,
|
|
40
44
|
resolveTelemetry,
|
|
41
45
|
type StablePrefixSnapshot,
|
|
42
46
|
ThinkingLevel,
|
|
43
47
|
} from "@gajae-code/agent-core";
|
|
44
48
|
import { normalizeMessagesForProvider } from "@gajae-code/agent-core/agent-loop";
|
|
49
|
+
import type { AttemptRunHandle, AttemptScope, AttemptScopeAuthority } from "@gajae-code/agent-core/attempt-scope";
|
|
45
50
|
import {
|
|
46
51
|
AUTO_HANDOFF_THRESHOLD_FOCUS,
|
|
47
52
|
CompactionCancelledError,
|
|
@@ -73,6 +78,7 @@ import {
|
|
|
73
78
|
} from "@gajae-code/agent-core/compaction/pruning";
|
|
74
79
|
import type {
|
|
75
80
|
AssistantMessage,
|
|
81
|
+
AttemptScopeRef,
|
|
76
82
|
Context,
|
|
77
83
|
DeveloperMessage,
|
|
78
84
|
Effort,
|
|
@@ -109,6 +115,7 @@ import {
|
|
|
109
115
|
type FallbackTriggerClass,
|
|
110
116
|
STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
|
|
111
117
|
} from "@gajae-code/ai/utils/fallback-transport";
|
|
118
|
+
import { AttemptRecordStore } from "./attempt-record-store";
|
|
112
119
|
import {
|
|
113
120
|
BTW_MAX_ANSWER_UTF8_BYTES,
|
|
114
121
|
BTW_MAX_QUESTION_UTF8_BYTES,
|
|
@@ -564,7 +571,11 @@ export interface AgentSessionConfig {
|
|
|
564
571
|
/** Tool-session factory context used to lazily attach workflow-gate-only tools. */
|
|
565
572
|
workflowGateToolSession?: ToolSession;
|
|
566
573
|
/** Current session pre-LLM message transform pipeline */
|
|
567
|
-
transformContext?: (
|
|
574
|
+
transformContext?: (
|
|
575
|
+
messages: AgentMessage[],
|
|
576
|
+
signal?: AbortSignal,
|
|
577
|
+
scope?: AttemptScopeRef,
|
|
578
|
+
) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
568
579
|
/** Provider payload hook used by the active session request path */
|
|
569
580
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
570
581
|
/** Provider response hook used by the active session request path */
|
|
@@ -869,14 +880,17 @@ type RetryErrorClassification =
|
|
|
869
880
|
|
|
870
881
|
const BARE_DEFAULT_WATCHDOG_ERROR =
|
|
871
882
|
/^(?:[A-Za-z][A-Za-z0-9-]*(?: [A-Za-z][A-Za-z0-9-]*){0,3} )stream (?:timed out while waiting for the first event|stalled while waiting for the next event)$/;
|
|
883
|
+
const PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = "Provider stream timed out while waiting for the first event";
|
|
884
|
+
const WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = `Error: ${PROVIDER_FIRST_EVENT_TIMEOUT_ERROR}`;
|
|
885
|
+
const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream timed out while waiting for first event";
|
|
872
886
|
const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
|
|
873
887
|
const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
874
888
|
"anthropic-messages": new Set([
|
|
875
|
-
|
|
889
|
+
PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
|
|
876
890
|
"Anthropic stream timed out while waiting for the first event",
|
|
877
891
|
]),
|
|
878
892
|
"openai-completions": new Set([
|
|
879
|
-
|
|
893
|
+
PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
|
|
880
894
|
"OpenAI completions stream timed out while waiting for the first event",
|
|
881
895
|
]),
|
|
882
896
|
} as const;
|
|
@@ -884,11 +898,11 @@ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
|
884
898
|
const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
|
|
885
899
|
const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
886
900
|
"openai-responses": new Set([
|
|
887
|
-
|
|
901
|
+
PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
|
|
888
902
|
"OpenAI responses stream timed out while waiting for the first event",
|
|
889
903
|
]),
|
|
890
904
|
"openai-completions": new Set([
|
|
891
|
-
|
|
905
|
+
PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
|
|
892
906
|
"OpenAI completions stream timed out while waiting for the first event",
|
|
893
907
|
]),
|
|
894
908
|
} as const;
|
|
@@ -906,6 +920,25 @@ function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boole
|
|
|
906
920
|
(facts.headers !== undefined && Object.keys(facts.headers).length > 0)
|
|
907
921
|
);
|
|
908
922
|
}
|
|
923
|
+
function terminalProviderFirstEventTimeoutIdentity(errorMessage: string): string {
|
|
924
|
+
return errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR
|
|
925
|
+
? PROVIDER_FIRST_EVENT_TIMEOUT_ERROR
|
|
926
|
+
: errorMessage;
|
|
927
|
+
}
|
|
928
|
+
function isMessageOnlyFirstEventTimeout(message: AssistantMessage): boolean {
|
|
929
|
+
if (hasBareDefaultRetryDisqualifyingFacts(message)) return false;
|
|
930
|
+
return (
|
|
931
|
+
message.errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR ||
|
|
932
|
+
message.errorMessage === PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function isBareDefaultWrappedFirstEventTimeout(message: AssistantMessage): boolean {
|
|
937
|
+
return (
|
|
938
|
+
message.errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR &&
|
|
939
|
+
!hasBareDefaultRetryDisqualifyingFacts(message)
|
|
940
|
+
);
|
|
941
|
+
}
|
|
909
942
|
function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
|
|
910
943
|
return (
|
|
911
944
|
message.api === "openai-codex-responses" &&
|
|
@@ -1629,6 +1662,49 @@ export class StreamingEditFileCache {
|
|
|
1629
1662
|
}
|
|
1630
1663
|
}
|
|
1631
1664
|
type SessionAdmissionKind = "prompt" | "selection";
|
|
1665
|
+
class SessionRunCancellationDomainBridge implements RunCancellationDomainBridge {
|
|
1666
|
+
#domains = new Map<string, { domain: RunCancellationDomain; controller: AbortController }>();
|
|
1667
|
+
#released = new Set<string>();
|
|
1668
|
+
#quarantined = new Set<string>();
|
|
1669
|
+
|
|
1670
|
+
open(resourceRunId: string) {
|
|
1671
|
+
if (this.#quarantined.has(resourceRunId)) return { ok: false as const, reason: "quarantined" as const };
|
|
1672
|
+
const existing = this.#domains.get(resourceRunId);
|
|
1673
|
+
if (existing) return { ok: true as const, domain: existing.domain, created: false };
|
|
1674
|
+
if (this.#released.has(resourceRunId)) return { ok: false as const, reason: "duplicate_identity" as const };
|
|
1675
|
+
const controller = new AbortController();
|
|
1676
|
+
const domain: RunCancellationDomain = { resourceRunId, signal: controller.signal };
|
|
1677
|
+
this.#domains.set(resourceRunId, { domain, controller });
|
|
1678
|
+
return { ok: true as const, domain, created: true };
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
lookup(resourceRunId: string): RunCancellationDomain | undefined {
|
|
1682
|
+
return this.#domains.get(resourceRunId)?.domain;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
abort(resourceRunId: string, reason?: unknown) {
|
|
1686
|
+
const record = this.#domains.get(resourceRunId);
|
|
1687
|
+
if (!record)
|
|
1688
|
+
return {
|
|
1689
|
+
ok: false as const,
|
|
1690
|
+
reason: this.#quarantined.has(resourceRunId) ? ("quarantined" as const) : ("unknown_run" as const),
|
|
1691
|
+
};
|
|
1692
|
+
const newlyAborted = !record.controller.signal.aborted;
|
|
1693
|
+
if (newlyAborted) record.controller.abort(reason);
|
|
1694
|
+
return { ok: true as const, newlyAborted };
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
release(resourceRunId: string, disposition: "settled" | "quarantined"): void {
|
|
1698
|
+
const record = this.#domains.get(resourceRunId);
|
|
1699
|
+
if (!record) return;
|
|
1700
|
+
if (disposition === "quarantined") {
|
|
1701
|
+
this.#quarantined.add(resourceRunId);
|
|
1702
|
+
if (!record.controller.signal.aborted) record.controller.abort();
|
|
1703
|
+
}
|
|
1704
|
+
this.#domains.delete(resourceRunId);
|
|
1705
|
+
this.#released.add(resourceRunId);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1632
1708
|
|
|
1633
1709
|
type SessionAdmissionEntry = {
|
|
1634
1710
|
kind: SessionAdmissionKind;
|
|
@@ -1888,6 +1964,15 @@ export class AgentSession {
|
|
|
1888
1964
|
#newSessionTransition: Promise<boolean> | undefined;
|
|
1889
1965
|
// Extension system
|
|
1890
1966
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
1967
|
+
#attemptAuthority!: AttemptScopeAuthority;
|
|
1968
|
+
#attemptRecordStore!: AttemptRecordStore;
|
|
1969
|
+
#activeLogicalRunId: AttemptRunHandle["logicalRunId"] | undefined;
|
|
1970
|
+
#acceptRunHandle(handle: AttemptRunHandle | undefined): void {
|
|
1971
|
+
// Integration doubles can accept a run without minting a handle; keep the
|
|
1972
|
+
// previously recorded run id rather than throwing inside the callback.
|
|
1973
|
+
if (!handle) return;
|
|
1974
|
+
this.#activeLogicalRunId = handle.logicalRunId;
|
|
1975
|
+
}
|
|
1891
1976
|
|
|
1892
1977
|
#turnIndex = 0;
|
|
1893
1978
|
#workerIntegrationScheduler: WorkerIntegrationRequestScheduler;
|
|
@@ -1917,7 +2002,11 @@ export class AgentSession {
|
|
|
1917
2002
|
// Tool registry and prompt builder for extensions
|
|
1918
2003
|
#toolRegistry: Map<string, AgentTool>;
|
|
1919
2004
|
#workflowGateToolSession: ToolSession | undefined;
|
|
1920
|
-
#transformContext: (
|
|
2005
|
+
#transformContext: (
|
|
2006
|
+
messages: AgentMessage[],
|
|
2007
|
+
signal?: AbortSignal,
|
|
2008
|
+
scope?: AttemptScopeRef,
|
|
2009
|
+
) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
1921
2010
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
1922
2011
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
1923
2012
|
#onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
|
|
@@ -2009,6 +2098,10 @@ export class AgentSession {
|
|
|
2009
2098
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
2010
2099
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
2011
2100
|
#postPromptTasksAbortController = new AbortController();
|
|
2101
|
+
#runCancellationDomains = new SessionRunCancellationDomainBridge();
|
|
2102
|
+
#postPromptLeases = new Map<string, RunResourceProducerLease>();
|
|
2103
|
+
#runResourceLeaseContext = new AsyncLocalStorage<RunResourceProducerLease>();
|
|
2104
|
+
#agentSessionClaimKey = {};
|
|
2012
2105
|
|
|
2013
2106
|
#streamingEditAbortTriggered = false;
|
|
2014
2107
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
@@ -2051,6 +2144,7 @@ export class AgentSession {
|
|
|
2051
2144
|
// the successor or proves it cannot. Holds prevent a false idle event while
|
|
2052
2145
|
// preserving the predecessor for cancellation and preflight failures.
|
|
2053
2146
|
#pendingAgentEndContinuationHolds = new Map<symbol, AgentSessionEvent>();
|
|
2147
|
+
#deferredAgentEndLeases = new WeakMap<AgentSessionEvent, RunResourceProducerLease>();
|
|
2054
2148
|
#sessionSettlementPromise: Promise<void> | undefined;
|
|
2055
2149
|
#sessionSettlementResolve: (() => void) | undefined;
|
|
2056
2150
|
#agentEndPublicationInFlight = 0;
|
|
@@ -2334,6 +2428,17 @@ export class AgentSession {
|
|
|
2334
2428
|
this.#restoreDeferredAgentEndAfterContinuationFailure(pending);
|
|
2335
2429
|
}
|
|
2336
2430
|
|
|
2431
|
+
#releaseDeferredAgentEndLease(pending: AgentSessionEvent | undefined): void {
|
|
2432
|
+
if (!pending) return;
|
|
2433
|
+
const lease = this.#deferredAgentEndLeases.get(pending);
|
|
2434
|
+
if (!lease) return;
|
|
2435
|
+
this.#deferredAgentEndLeases.delete(pending);
|
|
2436
|
+
if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
|
|
2437
|
+
this.#postPromptLeases.delete(lease.resourceRunId);
|
|
2438
|
+
}
|
|
2439
|
+
lease.closeDiscovery();
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2337
2442
|
#isSessionSettlementPending(): boolean {
|
|
2338
2443
|
return (
|
|
2339
2444
|
this.#promptInFlightCount > 0 ||
|
|
@@ -2397,13 +2502,26 @@ export class AgentSession {
|
|
|
2397
2502
|
return;
|
|
2398
2503
|
}
|
|
2399
2504
|
this.#pendingAgentEndEmit = undefined;
|
|
2505
|
+
const lease = this.#deferredAgentEndLeases.get(pending);
|
|
2506
|
+
if (lease) this.#deferredAgentEndLeases.delete(pending);
|
|
2400
2507
|
this.#agentEndPublicationInFlight++;
|
|
2401
|
-
this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending);
|
|
2508
|
+
this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease);
|
|
2402
2509
|
void this.#agentEndPublicationPromise;
|
|
2403
2510
|
}
|
|
2404
2511
|
|
|
2405
|
-
async #publishDeferredAgentEnd(
|
|
2406
|
-
|
|
2512
|
+
async #publishDeferredAgentEnd(
|
|
2513
|
+
pending: AgentSessionEvent,
|
|
2514
|
+
lease: RunResourceProducerLease | undefined,
|
|
2515
|
+
): Promise<void> {
|
|
2516
|
+
let extensionDelivery: Promise<void> | undefined;
|
|
2517
|
+
const releaseLease = () => {
|
|
2518
|
+
if (!lease) return;
|
|
2519
|
+
if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
|
|
2520
|
+
this.#postPromptLeases.delete(lease.resourceRunId);
|
|
2521
|
+
}
|
|
2522
|
+
lease.closeDiscovery();
|
|
2523
|
+
};
|
|
2524
|
+
const publish = async () => {
|
|
2407
2525
|
// Worker integration is first-party lifecycle persistence, not an extension
|
|
2408
2526
|
// hook. Make it durable before publishing the terminal boundary while user
|
|
2409
2527
|
// extension delivery remains asynchronous.
|
|
@@ -2411,10 +2529,21 @@ export class AgentSession {
|
|
|
2411
2529
|
// Persist before notifying synchronous subscribers: a subscriber may start a
|
|
2412
2530
|
// successor prompt from agent_end, whose running state must serialize after
|
|
2413
2531
|
// this terminal boundary rather than be overwritten by it.
|
|
2414
|
-
this.#persistRuntimeStateInBackground(pending);
|
|
2532
|
+
void this.#persistRuntimeStateInBackground(pending);
|
|
2415
2533
|
this.#emit(pending);
|
|
2416
|
-
|
|
2534
|
+
extensionDelivery = this.#queueExtensionEvent(
|
|
2535
|
+
pending,
|
|
2536
|
+
undefined,
|
|
2537
|
+
true,
|
|
2538
|
+
(pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope,
|
|
2539
|
+
);
|
|
2540
|
+
};
|
|
2541
|
+
try {
|
|
2542
|
+
if (lease) await this.#runResourceLeaseContext.run(lease, publish);
|
|
2543
|
+
else await publish();
|
|
2417
2544
|
} finally {
|
|
2545
|
+
if (extensionDelivery) void extensionDelivery.then(releaseLease, releaseLease);
|
|
2546
|
+
else releaseLease();
|
|
2418
2547
|
this.#agentEndPublicationInFlight = Math.max(0, this.#agentEndPublicationInFlight - 1);
|
|
2419
2548
|
this.#resolveSessionSettlement();
|
|
2420
2549
|
}
|
|
@@ -2422,6 +2551,7 @@ export class AgentSession {
|
|
|
2422
2551
|
|
|
2423
2552
|
constructor(config: AgentSessionConfig) {
|
|
2424
2553
|
this.agent = config.agent;
|
|
2554
|
+
this.agent.bindRunCancellationDomainBridge(this.#runCancellationDomains, this.#agentSessionClaimKey);
|
|
2425
2555
|
this.sessionManager = config.sessionManager;
|
|
2426
2556
|
this.settings = config.settings;
|
|
2427
2557
|
this.#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(
|
|
@@ -2471,6 +2601,11 @@ export class AgentSession {
|
|
|
2471
2601
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
2472
2602
|
this.#slashCommands = config.slashCommands ?? [];
|
|
2473
2603
|
this.#extensionRunner = config.extensionRunner;
|
|
2604
|
+
this.#attemptAuthority = this.agent.getAttemptScopeAuthority();
|
|
2605
|
+
this.#attemptRecordStore = new AttemptRecordStore(this.#attemptAuthority);
|
|
2606
|
+
if (this.#extensionRunner && typeof this.#extensionRunner.setAttemptRecordStore === "function") {
|
|
2607
|
+
this.#extensionRunner.setAttemptRecordStore(this.#attemptRecordStore);
|
|
2608
|
+
}
|
|
2474
2609
|
this.#skills = config.skills ?? [];
|
|
2475
2610
|
this.#skillWarnings = config.skillWarnings ?? [];
|
|
2476
2611
|
this.#customCommands = config.customCommands ?? [];
|
|
@@ -2483,7 +2618,10 @@ export class AgentSession {
|
|
|
2483
2618
|
this.#workflowGateToolSession = config.workflowGateToolSession;
|
|
2484
2619
|
this.#requestedToolNames = config.requestedToolNames;
|
|
2485
2620
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
2486
|
-
|
|
2621
|
+
const configuredOnPayload = config.onPayload;
|
|
2622
|
+
this.#onPayload = configuredOnPayload
|
|
2623
|
+
? (payload, model, scope) => configuredOnPayload(payload, model, scope)
|
|
2624
|
+
: undefined;
|
|
2487
2625
|
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
2488
2626
|
// Avoid wrapping in an `async` closure when no user callback is configured: the
|
|
2489
2627
|
// outer await on `#onResponse` (provider-response.ts) tolerates a sync void return,
|
|
@@ -2491,20 +2629,20 @@ export class AgentSession {
|
|
|
2491
2629
|
// shows up as ~3.5% self time in streaming profiles.
|
|
2492
2630
|
const configuredOnResponse = config.onResponse;
|
|
2493
2631
|
this.#onResponse = configuredOnResponse
|
|
2494
|
-
? async (response, model) => {
|
|
2632
|
+
? async (response, model, scope) => {
|
|
2495
2633
|
this.rawSseDebugBuffer.recordResponse(response, model);
|
|
2496
|
-
await configuredOnResponse(response, model);
|
|
2634
|
+
await configuredOnResponse(response, model, scope);
|
|
2497
2635
|
}
|
|
2498
|
-
: (response, model) => {
|
|
2636
|
+
: (response, model, _scope) => {
|
|
2499
2637
|
this.rawSseDebugBuffer.recordResponse(response, model);
|
|
2500
2638
|
};
|
|
2501
2639
|
const configuredOnSseEvent = config.onSseEvent;
|
|
2502
2640
|
this.#onSseEvent = configuredOnSseEvent
|
|
2503
|
-
? (event, model) => {
|
|
2641
|
+
? (event, model, scope) => {
|
|
2504
2642
|
this.rawSseDebugBuffer.recordEvent(event, model);
|
|
2505
|
-
configuredOnSseEvent(event, model);
|
|
2643
|
+
configuredOnSseEvent(event, model, scope);
|
|
2506
2644
|
}
|
|
2507
|
-
: (event, model) => {
|
|
2645
|
+
: (event, model, _scope) => {
|
|
2508
2646
|
this.rawSseDebugBuffer.recordEvent(event, model);
|
|
2509
2647
|
};
|
|
2510
2648
|
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
@@ -3336,6 +3474,7 @@ export class AgentSession {
|
|
|
3336
3474
|
event: AgentSessionEvent,
|
|
3337
3475
|
turnGeneration?: number,
|
|
3338
3476
|
workerIntegrationSettled = false,
|
|
3477
|
+
scope?: AttemptScopeRef,
|
|
3339
3478
|
): Promise<void> {
|
|
3340
3479
|
// Streaming events observed after turn_end belong to no live extension turn.
|
|
3341
3480
|
// Events already queued before that boundary must drain in FIFO order, unless
|
|
@@ -3351,7 +3490,7 @@ export class AgentSession {
|
|
|
3351
3490
|
turnGeneration === undefined || turnGeneration === this.#extensionTurnGeneration;
|
|
3352
3491
|
const emit = async () => {
|
|
3353
3492
|
if (!belongsToCurrentTurn()) return;
|
|
3354
|
-
await this.#emitExtensionEvent(event, belongsToCurrentTurn, workerIntegrationSettled);
|
|
3493
|
+
await this.#emitExtensionEvent(event, belongsToCurrentTurn, workerIntegrationSettled, scope);
|
|
3355
3494
|
};
|
|
3356
3495
|
const queued = this.#queuedExtensionEvents.then(emit, emit);
|
|
3357
3496
|
this.#queuedExtensionEvents = queued.catch(() => {});
|
|
@@ -3364,38 +3503,103 @@ export class AgentSession {
|
|
|
3364
3503
|
}
|
|
3365
3504
|
|
|
3366
3505
|
#trackAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
3367
|
-
const
|
|
3506
|
+
const terminalOwner = event.type === "agent_end" ? getAgentTerminalOwnerContext(event) : undefined;
|
|
3507
|
+
const maintenanceCheckpoint =
|
|
3508
|
+
event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted";
|
|
3509
|
+
const terminalClaim =
|
|
3510
|
+
maintenanceCheckpoint || !terminalOwner
|
|
3511
|
+
? undefined
|
|
3512
|
+
: this.agent.resourceLedger.claimProducer(
|
|
3513
|
+
terminalOwner.resourceRunId,
|
|
3514
|
+
terminalOwner.domain,
|
|
3515
|
+
this.#agentSessionClaimKey,
|
|
3516
|
+
);
|
|
3517
|
+
// A maintenance checkpoint is not a terminal publication. Capture the current
|
|
3518
|
+
// logical handle synchronously, then reserve its producer before this listener
|
|
3519
|
+
// creates any async work. True terminals must never fall back to this mutable
|
|
3520
|
+
// session state: their owner context is the only authority.
|
|
3521
|
+
const activePromptHandle = maintenanceCheckpoint
|
|
3522
|
+
? this.activePromptHandle
|
|
3523
|
+
: event.type === "agent_end"
|
|
3524
|
+
? terminalOwner?.resourceRunId
|
|
3525
|
+
: this.activePromptHandle;
|
|
3526
|
+
const domain = activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle) : undefined;
|
|
3527
|
+
const eventReservation = terminalClaim?.ok
|
|
3528
|
+
? terminalClaim
|
|
3529
|
+
: activePromptHandle && domain
|
|
3530
|
+
? this.agent.resourceLedger.reserveProducer(
|
|
3531
|
+
activePromptHandle,
|
|
3532
|
+
domain,
|
|
3533
|
+
"post_prompt",
|
|
3534
|
+
"agent-session-event",
|
|
3535
|
+
)
|
|
3536
|
+
: undefined;
|
|
3537
|
+
const eventLease = eventReservation?.ok ? eventReservation.lease : undefined;
|
|
3368
3538
|
const agentEndHandled = event.type === "agent_end" ? Promise.withResolvers<void>() : undefined;
|
|
3369
3539
|
if (agentEndHandled) this.#agentEndHandlingPromise = agentEndHandled.promise;
|
|
3370
3540
|
this.#agentEventHandlersInFlight++;
|
|
3371
3541
|
const handler = (async (): Promise<void> => {
|
|
3372
3542
|
try {
|
|
3373
|
-
|
|
3543
|
+
// A terminal that carries owner context must win the exact producer claim;
|
|
3544
|
+
// a superseded run's late agent_end never falls back to a successor's
|
|
3545
|
+
// handle. Externally emitted terminals carry no owner context and own no
|
|
3546
|
+
// run resources, so they keep the ownerless handling path unless a
|
|
3547
|
+
// prompt-owned run is actually active.
|
|
3548
|
+
if (event.type === "agent_end" && !maintenanceCheckpoint) {
|
|
3549
|
+
if (terminalOwner ? !terminalClaim?.ok : this.activePromptHandle !== undefined) return;
|
|
3550
|
+
}
|
|
3551
|
+
// A prompt-owned event with a captured handle must have obtained an exact
|
|
3552
|
+
// producer lease before its handler was created. Failed ownership never
|
|
3553
|
+
// schedules prompt work or falls back to a successor's handle.
|
|
3554
|
+
if (activePromptHandle && !eventLease) return;
|
|
3555
|
+
if (eventLease && event.type === "agent_end")
|
|
3556
|
+
this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
|
|
3557
|
+
if (eventLease) {
|
|
3558
|
+
await this.#runResourceLeaseContext.run(eventLease, () =>
|
|
3559
|
+
this.#handleAgentEvent(event, activePromptHandle),
|
|
3560
|
+
);
|
|
3561
|
+
} else {
|
|
3562
|
+
await this.#handleAgentEvent(event, activePromptHandle);
|
|
3563
|
+
}
|
|
3374
3564
|
} catch (error) {
|
|
3375
3565
|
logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
|
|
3376
3566
|
} finally {
|
|
3567
|
+
if (eventLease) {
|
|
3568
|
+
const pendingAgentEnd =
|
|
3569
|
+
event.type === "agent_end" && !maintenanceCheckpoint && this.#pendingAgentEndEmit === event
|
|
3570
|
+
? this.#pendingAgentEndEmit
|
|
3571
|
+
: undefined;
|
|
3572
|
+
if (pendingAgentEnd) {
|
|
3573
|
+
this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
|
|
3574
|
+
} else {
|
|
3575
|
+
if (event.type === "agent_end" && this.#postPromptLeases.get(eventLease.resourceRunId) === eventLease)
|
|
3576
|
+
this.#postPromptLeases.delete(eventLease.resourceRunId);
|
|
3577
|
+
eventLease.closeDiscovery();
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3377
3580
|
this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
|
|
3378
3581
|
this.#flushPendingAgentEnd();
|
|
3379
3582
|
agentEndHandled?.resolve();
|
|
3380
3583
|
}
|
|
3381
3584
|
})();
|
|
3382
|
-
if (
|
|
3383
|
-
this.agent.resourceLedger.track(activePromptHandle, "post_prompt", "agent-session-event", handler);
|
|
3384
|
-
}
|
|
3585
|
+
if (eventLease) eventLease.track("post_prompt", "agent-session-event", handler);
|
|
3385
3586
|
return handler;
|
|
3386
3587
|
};
|
|
3387
3588
|
|
|
3388
|
-
#persistRuntimeStateInBackground(event: AgentSessionEvent): void {
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3589
|
+
async #persistRuntimeStateInBackground(event: AgentSessionEvent): Promise<void> {
|
|
3590
|
+
try {
|
|
3591
|
+
await persistCoordinatorRuntimeStateFromEvent(event, {
|
|
3592
|
+
sessionId: this.sessionId,
|
|
3593
|
+
cwd: this.sessionManager.getCwd(),
|
|
3594
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
3595
|
+
});
|
|
3596
|
+
} catch {
|
|
3394
3597
|
logger.warn("Failed to persist coordinator runtime state", { event: event.type });
|
|
3395
|
-
}
|
|
3598
|
+
}
|
|
3396
3599
|
}
|
|
3397
3600
|
|
|
3398
3601
|
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
3602
|
+
const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
|
|
3399
3603
|
if (event.type === "turn_start") {
|
|
3400
3604
|
this.#extensionTurnGeneration++;
|
|
3401
3605
|
this.#closedExtensionTurnGeneration = undefined;
|
|
@@ -3420,7 +3624,7 @@ export class AgentSession {
|
|
|
3420
3624
|
this.#emit(event);
|
|
3421
3625
|
if (this.#hasStreamingExtensionHandlers()) {
|
|
3422
3626
|
__agentSessionPerfCounters.messageUpdateExtensionQueues += 1;
|
|
3423
|
-
void this.#queueExtensionEvent(event, this.#extensionTurnGeneration);
|
|
3627
|
+
void this.#queueExtensionEvent(event, this.#extensionTurnGeneration, false, attemptScope);
|
|
3424
3628
|
}
|
|
3425
3629
|
return;
|
|
3426
3630
|
}
|
|
@@ -3449,7 +3653,7 @@ export class AgentSession {
|
|
|
3449
3653
|
if (event.type === "agent_end") {
|
|
3450
3654
|
// Start the durable terminal write before synchronous subscribers can
|
|
3451
3655
|
// re-enter prompt(), so a successor's running transition serializes after it.
|
|
3452
|
-
|
|
3656
|
+
await persistRuntimeState();
|
|
3453
3657
|
this.#emit(event);
|
|
3454
3658
|
await this.#emitExtensionEvent(event);
|
|
3455
3659
|
return;
|
|
@@ -3534,6 +3738,11 @@ export class AgentSession {
|
|
|
3534
3738
|
|
|
3535
3739
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
3536
3740
|
#handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
|
|
3741
|
+
const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
|
|
3742
|
+
if ((event.type === "agent_start" || event.type === "turn_start") && attemptScope) {
|
|
3743
|
+
this.#attemptRecordStore.register(attemptScope);
|
|
3744
|
+
this.#attemptRecordStore.establishClean(attemptScope);
|
|
3745
|
+
}
|
|
3537
3746
|
if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
|
|
3538
3747
|
if (
|
|
3539
3748
|
event.type === "tool_execution_start" ||
|
|
@@ -4048,6 +4257,7 @@ export class AgentSession {
|
|
|
4048
4257
|
generation: maintenanceGeneration,
|
|
4049
4258
|
skipCompactionCheck: true,
|
|
4050
4259
|
resourceRunId: activePromptHandle,
|
|
4260
|
+
maintenanceContinuation: true,
|
|
4051
4261
|
});
|
|
4052
4262
|
}
|
|
4053
4263
|
return;
|
|
@@ -4128,10 +4338,11 @@ export class AgentSession {
|
|
|
4128
4338
|
}
|
|
4129
4339
|
this.#resolveRetry();
|
|
4130
4340
|
|
|
4131
|
-
const compactionTask = this.#
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4341
|
+
const compactionTask = this.#schedulePostPromptTask(
|
|
4342
|
+
async () => {
|
|
4343
|
+
await this.#checkCompaction(msg, true, undefined, activePromptHandle);
|
|
4344
|
+
},
|
|
4345
|
+
{ resourceRunId: activePromptHandle },
|
|
4135
4346
|
);
|
|
4136
4347
|
await compactionTask;
|
|
4137
4348
|
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
@@ -4196,29 +4407,52 @@ export class AgentSession {
|
|
|
4196
4407
|
this.#postPromptTasksPromise = undefined;
|
|
4197
4408
|
}
|
|
4198
4409
|
|
|
4199
|
-
#trackPostPromptTask(task: Promise<void>,
|
|
4410
|
+
#trackPostPromptTask(task: Promise<void>, lease?: RunResourceProducerLease, leaseTask: Promise<void> = task): void {
|
|
4200
4411
|
this.#postPromptTasks.add(task);
|
|
4201
4412
|
this.#ensurePostPromptTasksPromise();
|
|
4202
|
-
if (
|
|
4203
|
-
|
|
4413
|
+
if (lease) {
|
|
4414
|
+
lease.track("post_prompt", "agent-session", leaseTask);
|
|
4415
|
+
void leaseTask.catch(() => {}).finally(() => lease.closeDiscovery());
|
|
4204
4416
|
}
|
|
4205
4417
|
void task
|
|
4206
4418
|
.catch(() => {})
|
|
4207
4419
|
.finally(() => {
|
|
4208
4420
|
this.#postPromptTasks.delete(task);
|
|
4209
|
-
if (this.#postPromptTasks.size === 0)
|
|
4210
|
-
this.#resolvePostPromptTasks();
|
|
4211
|
-
}
|
|
4421
|
+
if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
|
|
4212
4422
|
});
|
|
4213
4423
|
}
|
|
4214
4424
|
|
|
4215
4425
|
#schedulePostPromptTask(
|
|
4216
4426
|
task: (signal: AbortSignal) => Promise<void>,
|
|
4217
|
-
options?: {
|
|
4427
|
+
options?: {
|
|
4428
|
+
delayMs?: number;
|
|
4429
|
+
generation?: number;
|
|
4430
|
+
onSkip?: () => void;
|
|
4431
|
+
resourceRunId?: string;
|
|
4432
|
+
leaseTask?: Promise<void>;
|
|
4433
|
+
},
|
|
4218
4434
|
): Promise<void> {
|
|
4219
4435
|
const delayMs = options?.delayMs ?? 0;
|
|
4220
|
-
const
|
|
4221
|
-
const
|
|
4436
|
+
const resourceRunId = options?.resourceRunId;
|
|
4437
|
+
const contextualLease = this.#runResourceLeaseContext.getStore();
|
|
4438
|
+
const parentLease =
|
|
4439
|
+
resourceRunId && contextualLease?.resourceRunId === resourceRunId
|
|
4440
|
+
? contextualLease
|
|
4441
|
+
: resourceRunId
|
|
4442
|
+
? this.#postPromptLeases.get(resourceRunId)
|
|
4443
|
+
: undefined;
|
|
4444
|
+
const domain = resourceRunId ? this.#runCancellationDomains.lookup(resourceRunId) : undefined;
|
|
4445
|
+
const reservation = parentLease
|
|
4446
|
+
? parentLease.fork(parentLease.domain, "post_prompt", "agent-session")
|
|
4447
|
+
: resourceRunId && domain
|
|
4448
|
+
? this.agent.resourceLedger.reserveProducer(resourceRunId, domain, "post_prompt", "agent-session")
|
|
4449
|
+
: undefined;
|
|
4450
|
+
if (resourceRunId && !reservation?.ok) {
|
|
4451
|
+
options?.onSkip?.();
|
|
4452
|
+
return Promise.resolve();
|
|
4453
|
+
}
|
|
4454
|
+
const signal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
|
|
4455
|
+
const runScheduled = async () => {
|
|
4222
4456
|
if (delayMs > 0) {
|
|
4223
4457
|
try {
|
|
4224
4458
|
await scheduler.wait(delayMs, { signal });
|
|
@@ -4236,8 +4470,11 @@ export class AgentSession {
|
|
|
4236
4470
|
return;
|
|
4237
4471
|
}
|
|
4238
4472
|
await task(signal);
|
|
4239
|
-
}
|
|
4240
|
-
|
|
4473
|
+
};
|
|
4474
|
+
const scheduled = reservation?.ok
|
|
4475
|
+
? this.#runResourceLeaseContext.run(reservation.lease, runScheduled)
|
|
4476
|
+
: runScheduled();
|
|
4477
|
+
this.#trackPostPromptTask(scheduled, reservation?.ok ? reservation.lease : undefined, options?.leaseTask);
|
|
4241
4478
|
return scheduled;
|
|
4242
4479
|
}
|
|
4243
4480
|
|
|
@@ -4256,6 +4493,7 @@ export class AgentSession {
|
|
|
4256
4493
|
rescheduleOnBusy?: boolean;
|
|
4257
4494
|
onError?: (error: unknown) => void;
|
|
4258
4495
|
resourceRunId?: string;
|
|
4496
|
+
maintenanceContinuation?: boolean;
|
|
4259
4497
|
}): Promise<void> {
|
|
4260
4498
|
const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
|
|
4261
4499
|
? this.#reserveDeferredAgentEndForContinuation()
|
|
@@ -4277,12 +4515,13 @@ export class AgentSession {
|
|
|
4277
4515
|
options?.onError?.(error);
|
|
4278
4516
|
};
|
|
4279
4517
|
const scheduledGeneration = options?.generation;
|
|
4280
|
-
const
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4518
|
+
const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> => {
|
|
4519
|
+
const leaseSettlement = Promise.withResolvers<void>();
|
|
4520
|
+
const settleLease = () => leaseSettlement.resolve();
|
|
4521
|
+
return this.#schedulePostPromptTask(
|
|
4522
|
+
async scheduledSignal => {
|
|
4284
4523
|
const canContinue = (): boolean => {
|
|
4285
|
-
if (
|
|
4524
|
+
if (scheduledSignal.aborted || this.#isDisposed) {
|
|
4286
4525
|
skip("aborted_signal");
|
|
4287
4526
|
return false;
|
|
4288
4527
|
}
|
|
@@ -4300,13 +4539,16 @@ export class AgentSession {
|
|
|
4300
4539
|
}
|
|
4301
4540
|
return true;
|
|
4302
4541
|
};
|
|
4303
|
-
if (!canContinue())
|
|
4542
|
+
if (!canContinue()) {
|
|
4543
|
+
settleLease();
|
|
4544
|
+
return;
|
|
4545
|
+
}
|
|
4304
4546
|
try {
|
|
4305
4547
|
if (!options?.skipCompactionCheck) {
|
|
4306
4548
|
await this.#checkEstimatedContextBeforePrompt();
|
|
4307
4549
|
if (!canContinue()) return;
|
|
4308
4550
|
}
|
|
4309
|
-
if (
|
|
4551
|
+
if (scheduledSignal.aborted) {
|
|
4310
4552
|
skip("aborted_signal");
|
|
4311
4553
|
return;
|
|
4312
4554
|
}
|
|
@@ -4326,23 +4568,40 @@ export class AgentSession {
|
|
|
4326
4568
|
return;
|
|
4327
4569
|
}
|
|
4328
4570
|
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
4571
|
+
let predecessorAccepted = false;
|
|
4572
|
+
const releasePredecessor = () => {
|
|
4573
|
+
if (predecessorAccepted) return;
|
|
4574
|
+
predecessorAccepted = true;
|
|
4575
|
+
this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
|
|
4576
|
+
};
|
|
4329
4577
|
const startsQueuedSuccessor =
|
|
4330
4578
|
this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
|
|
4331
4579
|
try {
|
|
4332
4580
|
await this.agent.continue({
|
|
4333
4581
|
...this.#managedFallbackPromptOptions(),
|
|
4582
|
+
maintenanceContinuation: options?.maintenanceContinuation,
|
|
4334
4583
|
// Reset only after continue() has claimed the queued turn. Skipped or stale
|
|
4335
4584
|
// continuations retain predecessor accounting, and resetAttemptBudget keeps
|
|
4336
4585
|
// the sticky fallback cursor unchanged.
|
|
4337
|
-
onRunAccepted:
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4586
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
4587
|
+
this.#acceptRunHandle(handle);
|
|
4588
|
+
settleLease();
|
|
4589
|
+
releasePredecessor();
|
|
4590
|
+
if (startsQueuedSuccessor) {
|
|
4591
|
+
this.#defaultFallbackChain().resetAttemptBudget();
|
|
4592
|
+
this.#overflowMaintenanceAttempts = 0;
|
|
4593
|
+
}
|
|
4594
|
+
},
|
|
4343
4595
|
});
|
|
4596
|
+
releasePredecessor();
|
|
4344
4597
|
} catch (error) {
|
|
4345
|
-
if (
|
|
4598
|
+
if (
|
|
4599
|
+
options?.rescheduleOnBusy &&
|
|
4600
|
+
this.#isAgentBusyError(error) &&
|
|
4601
|
+
!predecessorAccepted &&
|
|
4602
|
+
!terminalized &&
|
|
4603
|
+
canContinue()
|
|
4604
|
+
) {
|
|
4346
4605
|
this.#retainDeferredAgentEndAfterContinuationBusy(predecessorAgentEndHold, predecessorAgentEnd);
|
|
4347
4606
|
logger.debug("agent.continue busy after scheduling; rescheduling", {
|
|
4348
4607
|
error: error.message,
|
|
@@ -4350,19 +4609,27 @@ export class AgentSession {
|
|
|
4350
4609
|
void scheduleAttempt(100);
|
|
4351
4610
|
return;
|
|
4352
4611
|
}
|
|
4353
|
-
|
|
4612
|
+
if (!predecessorAccepted)
|
|
4613
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
4354
4614
|
throw error;
|
|
4355
4615
|
}
|
|
4356
4616
|
} catch (error) {
|
|
4357
4617
|
fail(error);
|
|
4618
|
+
} finally {
|
|
4619
|
+
settleLease();
|
|
4358
4620
|
}
|
|
4359
4621
|
},
|
|
4360
4622
|
{
|
|
4361
4623
|
delayMs,
|
|
4362
|
-
onSkip: () =>
|
|
4624
|
+
onSkip: () => {
|
|
4625
|
+
settleLease();
|
|
4626
|
+
skip("aborted_signal");
|
|
4627
|
+
},
|
|
4363
4628
|
resourceRunId: options?.resourceRunId,
|
|
4629
|
+
leaseTask: leaseSettlement.promise,
|
|
4364
4630
|
},
|
|
4365
4631
|
);
|
|
4632
|
+
};
|
|
4366
4633
|
return scheduleAttempt();
|
|
4367
4634
|
}
|
|
4368
4635
|
|
|
@@ -4433,7 +4700,7 @@ export class AgentSession {
|
|
|
4433
4700
|
|
|
4434
4701
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
4435
4702
|
if (compactionSettings.autoContinue !== false) {
|
|
4436
|
-
this.#scheduleAutoContinuePrompt(generation, false);
|
|
4703
|
+
this.#scheduleAutoContinuePrompt(generation, false, resourceRunId);
|
|
4437
4704
|
return true;
|
|
4438
4705
|
}
|
|
4439
4706
|
|
|
@@ -4441,11 +4708,13 @@ export class AgentSession {
|
|
|
4441
4708
|
return false;
|
|
4442
4709
|
}
|
|
4443
4710
|
|
|
4444
|
-
#scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true): void {
|
|
4711
|
+
#scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true, resourceRunId?: string): void {
|
|
4445
4712
|
const predecessorAgentEndHold = this.#reserveDeferredAgentEndForContinuation();
|
|
4446
4713
|
const scheduledGeneration = generation;
|
|
4447
|
-
const
|
|
4448
|
-
|
|
4714
|
+
const continuationAuthorized = async (
|
|
4715
|
+
signal: AbortSignal,
|
|
4716
|
+
hasPendingNextTurnMessages = false,
|
|
4717
|
+
): Promise<boolean> => {
|
|
4449
4718
|
const snapshot = await this.#compactionStateSnapshot();
|
|
4450
4719
|
if (signal.aborted) {
|
|
4451
4720
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
@@ -4467,61 +4736,68 @@ export class AgentSession {
|
|
|
4467
4736
|
if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
|
|
4468
4737
|
return authorized;
|
|
4469
4738
|
};
|
|
4470
|
-
const
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4739
|
+
const scheduleAttempt = (delayMs = 0) => {
|
|
4740
|
+
let rescheduled = false;
|
|
4741
|
+
void this.#schedulePostPromptTask(
|
|
4742
|
+
async signal => {
|
|
4743
|
+
try {
|
|
4744
|
+
await Promise.resolve();
|
|
4745
|
+
if (signal.aborted) {
|
|
4746
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4747
|
+
return;
|
|
4748
|
+
}
|
|
4749
|
+
if (this.#promptGeneration !== scheduledGeneration) {
|
|
4750
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
if (!(await continuationAuthorized(signal))) return;
|
|
4754
|
+
await this.#promptWithMessage(
|
|
4755
|
+
{
|
|
4756
|
+
role: "developer",
|
|
4757
|
+
content: [{ type: "text", text: autoContinuePrompt }],
|
|
4758
|
+
attribution: "agent",
|
|
4759
|
+
timestamp: Date.now(),
|
|
4760
|
+
},
|
|
4761
|
+
autoContinuePrompt,
|
|
4762
|
+
{
|
|
4763
|
+
skipPostPromptRecoveryWait: true,
|
|
4764
|
+
skipCompactionCheck: true,
|
|
4765
|
+
predecessorAgentEndHold,
|
|
4766
|
+
onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
|
|
4767
|
+
continuationAuthorized(signal, hasPendingNextTurnMessages),
|
|
4768
|
+
},
|
|
4769
|
+
);
|
|
4770
|
+
} catch (error) {
|
|
4771
|
+
if (signal.aborted) {
|
|
4772
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4773
|
+
return;
|
|
4774
|
+
}
|
|
4775
|
+
if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
|
|
4776
|
+
logger.debug("Auto-compaction continuation busy; rescheduling", {
|
|
4777
|
+
source: "auto_continue_prompt",
|
|
4778
|
+
reason: "queue_drained",
|
|
4779
|
+
error: error.message,
|
|
4780
|
+
});
|
|
4781
|
+
rescheduled = true;
|
|
4782
|
+
scheduleAttempt(100);
|
|
4783
|
+
return;
|
|
4784
|
+
}
|
|
4785
|
+
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
4786
|
+
} finally {
|
|
4787
|
+
if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4788
|
+
}
|
|
4477
4789
|
},
|
|
4478
|
-
autoContinuePrompt,
|
|
4479
4790
|
{
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4791
|
+
delayMs,
|
|
4792
|
+
generation: scheduledGeneration,
|
|
4793
|
+
resourceRunId,
|
|
4794
|
+
onSkip: () => {
|
|
4795
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4796
|
+
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4797
|
+
},
|
|
4484
4798
|
},
|
|
4485
4799
|
);
|
|
4486
4800
|
};
|
|
4487
|
-
const scheduleAttempt = (delayMs = 0) => {
|
|
4488
|
-
const attempt = (async () => {
|
|
4489
|
-
let rescheduled = false;
|
|
4490
|
-
try {
|
|
4491
|
-
if (delayMs > 0) await scheduler.wait(delayMs, { signal });
|
|
4492
|
-
await Promise.resolve();
|
|
4493
|
-
if (signal.aborted) {
|
|
4494
|
-
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4495
|
-
return;
|
|
4496
|
-
}
|
|
4497
|
-
if (this.#promptGeneration !== scheduledGeneration) {
|
|
4498
|
-
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
4499
|
-
return;
|
|
4500
|
-
}
|
|
4501
|
-
if (!(await continuationAuthorized())) return;
|
|
4502
|
-
await continuePrompt();
|
|
4503
|
-
} catch (error) {
|
|
4504
|
-
if (signal.aborted) {
|
|
4505
|
-
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4506
|
-
return;
|
|
4507
|
-
}
|
|
4508
|
-
if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
|
|
4509
|
-
logger.debug("Auto-compaction continuation busy; rescheduling", {
|
|
4510
|
-
source: "auto_continue_prompt",
|
|
4511
|
-
reason: "queue_drained",
|
|
4512
|
-
error: error.message,
|
|
4513
|
-
});
|
|
4514
|
-
rescheduled = true;
|
|
4515
|
-
scheduleAttempt(100);
|
|
4516
|
-
return;
|
|
4517
|
-
}
|
|
4518
|
-
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
4519
|
-
} finally {
|
|
4520
|
-
if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4521
|
-
}
|
|
4522
|
-
})();
|
|
4523
|
-
this.#trackPostPromptTask(attempt);
|
|
4524
|
-
};
|
|
4525
4801
|
scheduleAttempt();
|
|
4526
4802
|
}
|
|
4527
4803
|
|
|
@@ -5266,163 +5542,200 @@ export class AgentSession {
|
|
|
5266
5542
|
event: AgentSessionEvent,
|
|
5267
5543
|
continueWhile?: () => boolean,
|
|
5268
5544
|
workerIntegrationSettled = false,
|
|
5545
|
+
scope?: AttemptScopeRef,
|
|
5269
5546
|
): Promise<void> {
|
|
5270
5547
|
if (event.type === "agent_end" && !workerIntegrationSettled) {
|
|
5271
5548
|
await this.#flushWorkerIntegrationForAgentEnd();
|
|
5272
5549
|
}
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
await this.#extensionRunner.emit(extensionEvent, continueWhile);
|
|
5312
|
-
if (continueWhile && !continueWhile()) return;
|
|
5313
|
-
if (event.assistantMessageEvent.type === "reasoning_summary_start") {
|
|
5314
|
-
const reasoningEvent: ReasoningSummaryStartEvent = {
|
|
5315
|
-
type: "reasoning_summary_start",
|
|
5550
|
+
const deliveryScope = scope ?? (event as AgentSessionEvent & { scope?: AttemptScopeRef }).scope;
|
|
5551
|
+
const isTerminalAgentEnd =
|
|
5552
|
+
event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
|
|
5553
|
+
const finishAttempt = () => {
|
|
5554
|
+
if (!isTerminalAgentEnd) return;
|
|
5555
|
+
if (deliveryScope) this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
|
|
5556
|
+
this.#activeLogicalRunId = undefined;
|
|
5557
|
+
};
|
|
5558
|
+
if (!this.#extensionRunner) {
|
|
5559
|
+
finishAttempt();
|
|
5560
|
+
return;
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
try {
|
|
5564
|
+
if (event.type === "agent_start") {
|
|
5565
|
+
this.#turnIndex = 0;
|
|
5566
|
+
await this.#extensionRunner.emit({ type: "agent_start" }, undefined, deliveryScope);
|
|
5567
|
+
} else if (event.type === "agent_end") {
|
|
5568
|
+
await this.#extensionRunner.emit(
|
|
5569
|
+
{
|
|
5570
|
+
type: "agent_end",
|
|
5571
|
+
messages: event.messages,
|
|
5572
|
+
stopReason: event.stopReason,
|
|
5573
|
+
},
|
|
5574
|
+
undefined,
|
|
5575
|
+
deliveryScope,
|
|
5576
|
+
);
|
|
5577
|
+
} else if (event.type === "turn_start") {
|
|
5578
|
+
const hookEvent: TurnStartEvent = {
|
|
5579
|
+
type: "turn_start",
|
|
5580
|
+
turnIndex: this.#turnIndex,
|
|
5581
|
+
timestamp: Date.now(),
|
|
5582
|
+
};
|
|
5583
|
+
await this.#extensionRunner.emit(hookEvent, undefined, deliveryScope);
|
|
5584
|
+
} else if (event.type === "turn_end") {
|
|
5585
|
+
const hookEvent: TurnEndEvent = {
|
|
5586
|
+
type: "turn_end",
|
|
5587
|
+
turnIndex: this.#turnIndex,
|
|
5316
5588
|
message: event.message,
|
|
5317
|
-
|
|
5589
|
+
toolResults: event.toolResults,
|
|
5318
5590
|
};
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
} else if (event.
|
|
5322
|
-
const
|
|
5323
|
-
type: "
|
|
5591
|
+
await this.#extensionRunner.emit(hookEvent, undefined, deliveryScope);
|
|
5592
|
+
this.#turnIndex++;
|
|
5593
|
+
} else if (event.type === "message_start") {
|
|
5594
|
+
const extensionEvent: MessageStartEvent = {
|
|
5595
|
+
type: "message_start",
|
|
5324
5596
|
message: event.message,
|
|
5325
|
-
contentIndex: event.assistantMessageEvent.contentIndex,
|
|
5326
|
-
delta: event.assistantMessageEvent.delta,
|
|
5327
5597
|
};
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
type: "reasoning_summary_end",
|
|
5598
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5599
|
+
} else if (event.type === "message_update") {
|
|
5600
|
+
const extensionEvent: MessageUpdateEvent = {
|
|
5601
|
+
type: "message_update",
|
|
5333
5602
|
message: event.message,
|
|
5334
|
-
|
|
5335
|
-
content: event.assistantMessageEvent.content,
|
|
5603
|
+
assistantMessageEvent: event.assistantMessageEvent,
|
|
5336
5604
|
};
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
})
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5605
|
+
await this.#extensionRunner.emit(extensionEvent, continueWhile, deliveryScope);
|
|
5606
|
+
if (continueWhile && !continueWhile()) return;
|
|
5607
|
+
if (event.assistantMessageEvent.type === "reasoning_summary_start") {
|
|
5608
|
+
const reasoningEvent: ReasoningSummaryStartEvent = {
|
|
5609
|
+
type: "reasoning_summary_start",
|
|
5610
|
+
message: event.message,
|
|
5611
|
+
contentIndex: event.assistantMessageEvent.contentIndex,
|
|
5612
|
+
};
|
|
5613
|
+
if (this.#extensionRunner.hasHandlers("reasoning_summary_start")) this.#markRetryReplayUnsafe();
|
|
5614
|
+
await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
|
|
5615
|
+
} else if (event.assistantMessageEvent.type === "reasoning_summary_delta") {
|
|
5616
|
+
const reasoningEvent: ReasoningSummaryDeltaEvent = {
|
|
5617
|
+
type: "reasoning_summary_delta",
|
|
5618
|
+
message: event.message,
|
|
5619
|
+
contentIndex: event.assistantMessageEvent.contentIndex,
|
|
5620
|
+
delta: event.assistantMessageEvent.delta,
|
|
5621
|
+
};
|
|
5622
|
+
if (this.#extensionRunner.hasHandlers("reasoning_summary_delta")) this.#markRetryReplayUnsafe();
|
|
5623
|
+
await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
|
|
5624
|
+
} else if (event.assistantMessageEvent.type === "reasoning_summary_end") {
|
|
5625
|
+
const reasoningEvent: ReasoningSummaryEndEvent = {
|
|
5626
|
+
type: "reasoning_summary_end",
|
|
5627
|
+
message: event.message,
|
|
5628
|
+
contentIndex: event.assistantMessageEvent.contentIndex,
|
|
5629
|
+
content: event.assistantMessageEvent.content,
|
|
5630
|
+
};
|
|
5631
|
+
if (this.#extensionRunner.hasHandlers("reasoning_summary_end")) this.#markRetryReplayUnsafe();
|
|
5632
|
+
await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
|
|
5633
|
+
}
|
|
5634
|
+
} else if (event.type === "message_end") {
|
|
5635
|
+
const extensionEvent: MessageEndEvent = {
|
|
5636
|
+
type: "message_end",
|
|
5637
|
+
message: event.message,
|
|
5638
|
+
};
|
|
5639
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5640
|
+
} else if (event.type === "tool_execution_start") {
|
|
5641
|
+
const extensionEvent: ToolExecutionStartEvent = {
|
|
5642
|
+
type: "tool_execution_start",
|
|
5643
|
+
toolCallId: event.toolCallId,
|
|
5644
|
+
toolName: event.toolName,
|
|
5645
|
+
args: event.args,
|
|
5646
|
+
intent: event.intent,
|
|
5647
|
+
};
|
|
5648
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5649
|
+
} else if (event.type === "tool_execution_update") {
|
|
5650
|
+
const extensionEvent: ToolExecutionUpdateEvent = {
|
|
5651
|
+
type: "tool_execution_update",
|
|
5652
|
+
toolCallId: event.toolCallId,
|
|
5653
|
+
toolName: event.toolName,
|
|
5654
|
+
args: event.args,
|
|
5655
|
+
partialResult: event.partialResult,
|
|
5656
|
+
};
|
|
5657
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5658
|
+
} else if (event.type === "tool_execution_end") {
|
|
5659
|
+
const extensionEvent: ToolExecutionEndEvent = {
|
|
5660
|
+
type: "tool_execution_end",
|
|
5661
|
+
toolCallId: event.toolCallId,
|
|
5662
|
+
toolName: event.toolName,
|
|
5663
|
+
result: event.result,
|
|
5664
|
+
isError: event.isError ?? false,
|
|
5665
|
+
};
|
|
5666
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5667
|
+
} else if (event.type === "auto_compaction_start") {
|
|
5668
|
+
await this.#extensionRunner.emit(
|
|
5669
|
+
{ type: "auto_compaction_start", reason: event.reason, action: event.action },
|
|
5670
|
+
undefined,
|
|
5671
|
+
deliveryScope,
|
|
5672
|
+
);
|
|
5673
|
+
} else if (event.type === "auto_compaction_end") {
|
|
5674
|
+
await this.#extensionRunner.emit(
|
|
5675
|
+
{
|
|
5676
|
+
type: "auto_compaction_end",
|
|
5677
|
+
action: event.action,
|
|
5678
|
+
result: event.result,
|
|
5679
|
+
aborted: event.aborted,
|
|
5680
|
+
willRetry: event.willRetry,
|
|
5681
|
+
errorMessage: event.errorMessage,
|
|
5682
|
+
skipped: event.skipped,
|
|
5683
|
+
continuationSkipReason: event.continuationSkipReason,
|
|
5684
|
+
},
|
|
5685
|
+
undefined,
|
|
5686
|
+
deliveryScope,
|
|
5687
|
+
);
|
|
5688
|
+
} else if (event.type === "auto_retry_start") {
|
|
5689
|
+
if (this.#extensionRunner.hasHandlers("auto_retry_start")) this.#markRetryReplayUnsafe();
|
|
5690
|
+
await this.#extensionRunner.emit(
|
|
5691
|
+
{
|
|
5692
|
+
type: "auto_retry_start",
|
|
5693
|
+
attempt: event.attempt,
|
|
5694
|
+
maxAttempts: event.maxAttempts,
|
|
5695
|
+
delayMs: event.delayMs,
|
|
5696
|
+
errorMessage: event.errorMessage,
|
|
5697
|
+
unbounded: event.unbounded,
|
|
5698
|
+
},
|
|
5699
|
+
undefined,
|
|
5700
|
+
deliveryScope,
|
|
5701
|
+
);
|
|
5702
|
+
} else if (event.type === "auto_retry_end") {
|
|
5703
|
+
await this.#extensionRunner.emit(
|
|
5704
|
+
{
|
|
5705
|
+
type: "auto_retry_end",
|
|
5706
|
+
success: event.success,
|
|
5707
|
+
attempt: event.attempt,
|
|
5708
|
+
finalError: event.finalError,
|
|
5709
|
+
},
|
|
5710
|
+
undefined,
|
|
5711
|
+
deliveryScope,
|
|
5712
|
+
);
|
|
5713
|
+
} else if (event.type === "ttsr_triggered") {
|
|
5714
|
+
await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules }, undefined, deliveryScope);
|
|
5715
|
+
} else if (event.type === "todo_reminder") {
|
|
5716
|
+
await this.#extensionRunner.emit(
|
|
5717
|
+
{
|
|
5718
|
+
type: "todo_reminder",
|
|
5719
|
+
todos: event.todos,
|
|
5720
|
+
attempt: event.attempt,
|
|
5721
|
+
maxAttempts: event.maxAttempts,
|
|
5722
|
+
},
|
|
5723
|
+
undefined,
|
|
5724
|
+
deliveryScope,
|
|
5725
|
+
);
|
|
5726
|
+
} else if (event.type === "goal_updated") {
|
|
5727
|
+
try {
|
|
5728
|
+
await this.#extensionRunner.emit(
|
|
5729
|
+
{ type: "goal_updated", goal: event.goal, state: event.state },
|
|
5730
|
+
undefined,
|
|
5731
|
+
deliveryScope,
|
|
5732
|
+
);
|
|
5733
|
+
} catch (error) {
|
|
5734
|
+
logger.warn("Goal updated extension hook failed", { error: String(error) });
|
|
5735
|
+
}
|
|
5425
5736
|
}
|
|
5737
|
+
} finally {
|
|
5738
|
+
finishAttempt();
|
|
5426
5739
|
}
|
|
5427
5740
|
}
|
|
5428
5741
|
|
|
@@ -5545,7 +5858,15 @@ export class AgentSession {
|
|
|
5545
5858
|
),
|
|
5546
5859
|
Bun.sleep(2_000).then(() => false),
|
|
5547
5860
|
]);
|
|
5548
|
-
if (!disposeIdleSettled)
|
|
5861
|
+
if (!disposeIdleSettled) {
|
|
5862
|
+
try {
|
|
5863
|
+
this.agent.forceAbort("Session disposed", this.#activeLogicalRunId);
|
|
5864
|
+
} catch {
|
|
5865
|
+
// AttemptScope handle may not be registered for sessions
|
|
5866
|
+
// that don't participate in the facility (e.g. test mocks).
|
|
5867
|
+
this.agent.forceAbort("Session disposed");
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5549
5870
|
await admissionClosed;
|
|
5550
5871
|
await this.#agentEndPublicationPromise;
|
|
5551
5872
|
await this.#queuedExtensionEvents;
|
|
@@ -6929,7 +7250,8 @@ export class AgentSession {
|
|
|
6929
7250
|
this.#assertNoHandoffTransition();
|
|
6930
7251
|
await this.agent.continue({
|
|
6931
7252
|
...this.#managedFallbackPromptOptions(),
|
|
6932
|
-
onRunAccepted: () => {
|
|
7253
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
7254
|
+
this.#acceptRunHandle(handle);
|
|
6933
7255
|
if (hindsightRecall) hindsightState?.markRecallSnippetInjected(hindsightRecall);
|
|
6934
7256
|
},
|
|
6935
7257
|
});
|
|
@@ -6956,20 +7278,32 @@ export class AgentSession {
|
|
|
6956
7278
|
}
|
|
6957
7279
|
|
|
6958
7280
|
/** Convert session messages using the same pre-LLM pipeline as the active session. */
|
|
6959
|
-
async convertMessagesToLlm(
|
|
6960
|
-
|
|
7281
|
+
async convertMessagesToLlm(
|
|
7282
|
+
messages: AgentMessage[],
|
|
7283
|
+
signal?: AbortSignal,
|
|
7284
|
+
scope?: AttemptScope,
|
|
7285
|
+
): Promise<Message[]> {
|
|
7286
|
+
const transformedMessages =
|
|
7287
|
+
scope === undefined
|
|
7288
|
+
? await this.#transformContext(messages, signal)
|
|
7289
|
+
: await this.#transformContext(messages, signal, scope);
|
|
6961
7290
|
return await this.#convertToLlm(transformedMessages);
|
|
6962
7291
|
}
|
|
6963
7292
|
|
|
6964
7293
|
/** Apply session-level stream hooks to a direct side request. */
|
|
6965
|
-
prepareSimpleStreamOptions(
|
|
7294
|
+
prepareSimpleStreamOptions(
|
|
7295
|
+
options: SimpleStreamOptions,
|
|
7296
|
+
provider = "anthropic",
|
|
7297
|
+
scope?: AttemptScope,
|
|
7298
|
+
): SimpleStreamOptions {
|
|
6966
7299
|
const sessionOnPayload = this.#onPayload;
|
|
6967
7300
|
const sessionOnResponse = this.#onResponse;
|
|
6968
7301
|
const sessionMetadata = this.agent.metadataForProvider(provider);
|
|
6969
7302
|
const sessionOnSseEvent = this.#onSseEvent;
|
|
6970
|
-
if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent) return options;
|
|
7303
|
+
if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent && !scope) return options;
|
|
6971
7304
|
|
|
6972
7305
|
const preparedOptions: SimpleStreamOptions = { ...options };
|
|
7306
|
+
if (scope) preparedOptions.attemptScope = scope;
|
|
6973
7307
|
|
|
6974
7308
|
// Stamp session metadata (e.g. user_id={session_id}) onto direct-call requests so
|
|
6975
7309
|
// they share the same session bucket as Agent.prompt-routed requests on Anthropic
|
|
@@ -6983,10 +7317,10 @@ export class AgentSession {
|
|
|
6983
7317
|
preparedOptions.onPayload = sessionOnPayload;
|
|
6984
7318
|
} else {
|
|
6985
7319
|
const requestOnPayload = options.onPayload;
|
|
6986
|
-
preparedOptions.onPayload = async (payload, model) => {
|
|
6987
|
-
const sessionPayload = await sessionOnPayload(payload, model);
|
|
7320
|
+
preparedOptions.onPayload = async (payload, model, callbackScope) => {
|
|
7321
|
+
const sessionPayload = await sessionOnPayload(payload, model, callbackScope);
|
|
6988
7322
|
const sessionResolvedPayload = sessionPayload ?? payload;
|
|
6989
|
-
const requestPayload = await requestOnPayload(sessionResolvedPayload, model);
|
|
7323
|
+
const requestPayload = await requestOnPayload(sessionResolvedPayload, model, callbackScope);
|
|
6990
7324
|
return requestPayload ?? sessionResolvedPayload;
|
|
6991
7325
|
};
|
|
6992
7326
|
}
|
|
@@ -6997,9 +7331,9 @@ export class AgentSession {
|
|
|
6997
7331
|
preparedOptions.onResponse = sessionOnResponse;
|
|
6998
7332
|
} else {
|
|
6999
7333
|
const requestOnResponse = options.onResponse;
|
|
7000
|
-
preparedOptions.onResponse = async (response, model) => {
|
|
7001
|
-
await sessionOnResponse(response, model);
|
|
7002
|
-
await requestOnResponse(response, model);
|
|
7334
|
+
preparedOptions.onResponse = async (response, model, callbackScope) => {
|
|
7335
|
+
await sessionOnResponse(response, model, callbackScope);
|
|
7336
|
+
await requestOnResponse(response, model, callbackScope);
|
|
7003
7337
|
};
|
|
7004
7338
|
}
|
|
7005
7339
|
}
|
|
@@ -7009,9 +7343,9 @@ export class AgentSession {
|
|
|
7009
7343
|
preparedOptions.onSseEvent = sessionOnSseEvent;
|
|
7010
7344
|
} else {
|
|
7011
7345
|
const requestOnSseEvent = options.onSseEvent;
|
|
7012
|
-
preparedOptions.onSseEvent = (event, model) => {
|
|
7013
|
-
sessionOnSseEvent(event, model);
|
|
7014
|
-
requestOnSseEvent(event, model);
|
|
7346
|
+
preparedOptions.onSseEvent = (event, model, callbackScope) => {
|
|
7347
|
+
sessionOnSseEvent(event, model, callbackScope);
|
|
7348
|
+
requestOnSseEvent(event, model, callbackScope);
|
|
7015
7349
|
};
|
|
7016
7350
|
}
|
|
7017
7351
|
}
|
|
@@ -8058,7 +8392,7 @@ export class AgentSession {
|
|
|
8058
8392
|
skipPostPromptRecoveryWait?: boolean;
|
|
8059
8393
|
predecessorAgentEndHold?: symbol;
|
|
8060
8394
|
admissionLease?: SessionAdmissionLease;
|
|
8061
|
-
onRunAccepted?: () => void;
|
|
8395
|
+
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
8062
8396
|
onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
|
|
8063
8397
|
resetRetryReplaySafety?: boolean;
|
|
8064
8398
|
},
|
|
@@ -8284,8 +8618,9 @@ export class AgentSession {
|
|
|
8284
8618
|
const agentPromptOptions = {
|
|
8285
8619
|
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
8286
8620
|
...this.#managedFallbackPromptOptions(),
|
|
8287
|
-
onRunAccepted: () => {
|
|
8288
|
-
|
|
8621
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
8622
|
+
this.#acceptRunHandle(handle);
|
|
8623
|
+
options?.onRunAccepted?.(handle);
|
|
8289
8624
|
options?.admissionLease?.release();
|
|
8290
8625
|
if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
|
|
8291
8626
|
},
|
|
@@ -9408,7 +9743,14 @@ export class AgentSession {
|
|
|
9408
9743
|
]);
|
|
9409
9744
|
if (outcome.kind === "timeout") {
|
|
9410
9745
|
this.#abandonPostPromptTasks();
|
|
9411
|
-
this.agent.
|
|
9746
|
+
const forceAbortLogicalRunId = this.agent.currentManagedLogicalRunId ?? this.#activeLogicalRunId;
|
|
9747
|
+
try {
|
|
9748
|
+
if (forceAbortLogicalRunId !== undefined)
|
|
9749
|
+
this.agent.forceAbort("Abort cleanup timed out", forceAbortLogicalRunId);
|
|
9750
|
+
else this.agent.forceAbort("Abort cleanup timed out");
|
|
9751
|
+
} catch {
|
|
9752
|
+
this.agent.forceAbort("Abort cleanup timed out");
|
|
9753
|
+
}
|
|
9412
9754
|
this.emitNotice(
|
|
9413
9755
|
"warning",
|
|
9414
9756
|
"Abort cleanup timed out; forced session recovery. The previous provider stream or tool may still be unwinding in the background.",
|
|
@@ -9465,20 +9807,16 @@ export class AgentSession {
|
|
|
9465
9807
|
* Abort a specific active prompt and prove whether its tracked resources settled.
|
|
9466
9808
|
*/
|
|
9467
9809
|
async abortPromptAndWait(handle: string, options: { graceMs: number }): Promise<RunSettlementProof> {
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
void this.abort({ timeoutMs: options.graceMs }).catch(() => {});
|
|
9475
|
-
|
|
9476
|
-
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
9477
|
-
if (proof.status === "unfenced") {
|
|
9478
|
-
// Detach the unsettled resources so they can never re-enter this run; their real
|
|
9479
|
-
// promises still release them inside the ledger.
|
|
9480
|
-
this.agent.resourceLedger.quarantine(handle);
|
|
9810
|
+
const aborted = this.#runCancellationDomains.abort(handle);
|
|
9811
|
+
if (!aborted.ok) {
|
|
9812
|
+
if (aborted.reason === "quarantined") {
|
|
9813
|
+
return await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: 0 });
|
|
9814
|
+
}
|
|
9815
|
+
return { status: "unfenced", reason: "unknown_run", pending: [] };
|
|
9481
9816
|
}
|
|
9817
|
+
if (handle === this.agent.activeResourceRunId) this.agent.abort();
|
|
9818
|
+
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
9819
|
+
if (proof.status === "unfenced") this.agent.resourceLedger.quarantine(handle);
|
|
9482
9820
|
return proof;
|
|
9483
9821
|
}
|
|
9484
9822
|
|
|
@@ -11870,6 +12208,7 @@ export class AgentSession {
|
|
|
11870
12208
|
skipAbortedCheck = true,
|
|
11871
12209
|
onTerminalOverflowNoop?: () => void,
|
|
11872
12210
|
resourceRunId?: string,
|
|
12211
|
+
ownershipSignal?: AbortSignal,
|
|
11873
12212
|
): Promise<boolean> {
|
|
11874
12213
|
// Safety stops are terminal and must not trigger context maintenance.
|
|
11875
12214
|
if (
|
|
@@ -11913,7 +12252,8 @@ export class AgentSession {
|
|
|
11913
12252
|
}
|
|
11914
12253
|
|
|
11915
12254
|
// Try context promotion first - switch to a larger model and retry without compacting
|
|
11916
|
-
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
12255
|
+
const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
|
|
12256
|
+
if (ownershipSignal?.aborted) return false;
|
|
11917
12257
|
if (promoted) {
|
|
11918
12258
|
// Retry on the promoted (larger) model without compacting
|
|
11919
12259
|
this.#scheduleAgentContinue({
|
|
@@ -11938,6 +12278,7 @@ export class AgentSession {
|
|
|
11938
12278
|
}
|
|
11939
12279
|
},
|
|
11940
12280
|
resourceRunId,
|
|
12281
|
+
signal: ownershipSignal,
|
|
11941
12282
|
});
|
|
11942
12283
|
return "continuationScheduled" in status && status.continuationScheduled === true;
|
|
11943
12284
|
}
|
|
@@ -11972,14 +12313,16 @@ export class AgentSession {
|
|
|
11972
12313
|
autoCompactionOutputReserveTokens,
|
|
11973
12314
|
)
|
|
11974
12315
|
) {
|
|
11975
|
-
const pruneResult = await this.#pruneToolOutputs(
|
|
12316
|
+
const pruneResult = await this.#pruneToolOutputs(ownershipSignal, true);
|
|
12317
|
+
if (ownershipSignal?.aborted) return false;
|
|
11976
12318
|
if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
11977
12319
|
}
|
|
11978
12320
|
if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
11979
12321
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
11980
|
-
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
12322
|
+
const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
|
|
12323
|
+
if (ownershipSignal?.aborted) return false;
|
|
11981
12324
|
if (!promoted) {
|
|
11982
|
-
await this.#runAutoCompaction("threshold", false);
|
|
12325
|
+
await this.#runAutoCompaction("threshold", false, false, { resourceRunId, signal: ownershipSignal });
|
|
11983
12326
|
}
|
|
11984
12327
|
}
|
|
11985
12328
|
return true;
|
|
@@ -13358,7 +13701,7 @@ export class AgentSession {
|
|
|
13358
13701
|
});
|
|
13359
13702
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
13360
13703
|
if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
13361
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
13704
|
+
this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
|
|
13362
13705
|
}
|
|
13363
13706
|
|
|
13364
13707
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
@@ -13422,7 +13765,7 @@ export class AgentSession {
|
|
|
13422
13765
|
}
|
|
13423
13766
|
const overflowContinuationScheduled =
|
|
13424
13767
|
!overflowNoopWouldReplay && willRetry && !continuationSkipReason
|
|
13425
|
-
? await this.#scheduleOverflowRetryContinuation(generation)
|
|
13768
|
+
? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
|
|
13426
13769
|
: false;
|
|
13427
13770
|
await this.#emitSessionEvent({
|
|
13428
13771
|
type: "auto_compaction_end",
|
|
@@ -13445,6 +13788,7 @@ export class AgentSession {
|
|
|
13445
13788
|
rescheduleOnBusy: true,
|
|
13446
13789
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
13447
13790
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
13791
|
+
resourceRunId: options?.resourceRunId,
|
|
13448
13792
|
});
|
|
13449
13793
|
return { kind: "skipped", continuationScheduled: true };
|
|
13450
13794
|
}
|
|
@@ -13465,9 +13809,10 @@ export class AgentSession {
|
|
|
13465
13809
|
rescheduleOnBusy: true,
|
|
13466
13810
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
13467
13811
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
13812
|
+
resourceRunId: options?.resourceRunId,
|
|
13468
13813
|
});
|
|
13469
13814
|
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
13470
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
13815
|
+
this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
|
|
13471
13816
|
}
|
|
13472
13817
|
return { kind: "skipped" };
|
|
13473
13818
|
}
|
|
@@ -13694,7 +14039,9 @@ export class AgentSession {
|
|
|
13694
14039
|
this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
|
|
13695
14040
|
}
|
|
13696
14041
|
const overflowContinuationScheduled =
|
|
13697
|
-
willRetry && !continuationSkipReason
|
|
14042
|
+
willRetry && !continuationSkipReason
|
|
14043
|
+
? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
|
|
14044
|
+
: false;
|
|
13698
14045
|
await this.#emitSessionEvent({
|
|
13699
14046
|
type: "auto_compaction_end",
|
|
13700
14047
|
action,
|
|
@@ -13721,9 +14068,10 @@ export class AgentSession {
|
|
|
13721
14068
|
rescheduleOnBusy: true,
|
|
13722
14069
|
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
13723
14070
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
14071
|
+
resourceRunId: options?.resourceRunId,
|
|
13724
14072
|
});
|
|
13725
14073
|
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
13726
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
14074
|
+
this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
|
|
13727
14075
|
}
|
|
13728
14076
|
return { kind: "compacted" };
|
|
13729
14077
|
} catch (error) {
|
|
@@ -13788,7 +14136,7 @@ export class AgentSession {
|
|
|
13788
14136
|
|
|
13789
14137
|
#isKimiCodeFirstEventTimeout(message: AssistantMessage): boolean {
|
|
13790
14138
|
if (message.stopReason !== "error" || message.provider !== "kimi-code") return false;
|
|
13791
|
-
const errorMessage = message.errorMessage ?? "";
|
|
14139
|
+
const errorMessage = terminalProviderFirstEventTimeoutIdentity(message.errorMessage ?? "");
|
|
13792
14140
|
if (message.api === "anthropic-messages") {
|
|
13793
14141
|
return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["anthropic-messages"].has(errorMessage);
|
|
13794
14142
|
}
|
|
@@ -13815,7 +14163,8 @@ export class AgentSession {
|
|
|
13815
14163
|
#isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
|
|
13816
14164
|
if (message.stopReason !== "error" || message.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
|
|
13817
14165
|
const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(message.api);
|
|
13818
|
-
|
|
14166
|
+
const errorMessage = terminalProviderFirstEventTimeoutIdentity(message.errorMessage ?? "");
|
|
14167
|
+
return timeoutMessages?.has(errorMessage) ?? false;
|
|
13819
14168
|
}
|
|
13820
14169
|
|
|
13821
14170
|
#isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
|
|
@@ -14003,6 +14352,7 @@ export class AgentSession {
|
|
|
14003
14352
|
if (this.#isTerminalProviderFirstEventTimeout(message)) {
|
|
14004
14353
|
return "terminal";
|
|
14005
14354
|
}
|
|
14355
|
+
if (isMessageOnlyFirstEventTimeout(message)) return "first_event_timeout";
|
|
14006
14356
|
// Legacy providers that have not yet stamped the typed timeout fact retain
|
|
14007
14357
|
// their existing bounded ollama-cloud behavior.
|
|
14008
14358
|
if (this.#isFirstEventTimeoutErrorMessage(err) && this.#shouldFailClosedOnFirstEventTimeout(message)) {
|
|
@@ -14173,20 +14523,37 @@ export class AgentSession {
|
|
|
14173
14523
|
return {
|
|
14174
14524
|
type: "maintenance",
|
|
14175
14525
|
continuation: async ownership => {
|
|
14176
|
-
if (!ownership.isCurrent()) return;
|
|
14177
|
-
|
|
14178
|
-
const
|
|
14179
|
-
|
|
14180
|
-
|
|
14526
|
+
if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
14527
|
+
const resourceRunId = String(ownership.logicalRunId);
|
|
14528
|
+
const previousLease = this.#postPromptLeases.get(resourceRunId);
|
|
14529
|
+
this.#postPromptLeases.set(resourceRunId, ownership.lease);
|
|
14530
|
+
try {
|
|
14531
|
+
let terminalized = false;
|
|
14532
|
+
const successorScheduled = await this.#checkCompaction(
|
|
14533
|
+
outcome.message,
|
|
14534
|
+
true,
|
|
14535
|
+
() => {
|
|
14536
|
+
terminalized = true;
|
|
14537
|
+
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
14538
|
+
stopReason: "error",
|
|
14539
|
+
messages: [outcome.message],
|
|
14540
|
+
});
|
|
14541
|
+
},
|
|
14542
|
+
resourceRunId,
|
|
14543
|
+
ownership.lease.signal,
|
|
14544
|
+
);
|
|
14545
|
+
if (terminalized || successorScheduled || !ownership.isCurrent() || ownership.lease.signal.aborted)
|
|
14546
|
+
return;
|
|
14547
|
+
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
14181
14548
|
stopReason: "error",
|
|
14182
14549
|
messages: [outcome.message],
|
|
14183
14550
|
});
|
|
14184
|
-
}
|
|
14185
|
-
|
|
14186
|
-
|
|
14187
|
-
|
|
14188
|
-
|
|
14189
|
-
}
|
|
14551
|
+
} finally {
|
|
14552
|
+
if (this.#postPromptLeases.get(resourceRunId) === ownership.lease) {
|
|
14553
|
+
if (previousLease) this.#postPromptLeases.set(resourceRunId, previousLease);
|
|
14554
|
+
else this.#postPromptLeases.delete(resourceRunId);
|
|
14555
|
+
}
|
|
14556
|
+
}
|
|
14190
14557
|
},
|
|
14191
14558
|
};
|
|
14192
14559
|
}
|
|
@@ -14378,17 +14745,20 @@ export class AgentSession {
|
|
|
14378
14745
|
// retry.enabled=false always surfaces immediately, matching the explicit
|
|
14379
14746
|
// user opt-out.
|
|
14380
14747
|
if (!managedFallback && !retrySettings.enabled) return false;
|
|
14381
|
-
const classification =
|
|
14382
|
-
|
|
14748
|
+
const classification = this.#classifyErrorForRetry(message);
|
|
14749
|
+
const firstEventTimeout = classification === "first_event_timeout";
|
|
14750
|
+
if (!managedFallback && firstEventTimeout) {
|
|
14383
14751
|
this.#firstEventTimeoutRetryStartedAt ??= Date.now();
|
|
14384
14752
|
}
|
|
14385
14753
|
// First-event timeouts are replay-safe only before any model progress,
|
|
14386
14754
|
// abort, or extension/provider lifecycle participation.
|
|
14387
|
-
if (
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14755
|
+
if (firstEventTimeout && (!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))) {
|
|
14756
|
+
return managedOutcome
|
|
14757
|
+
? {
|
|
14758
|
+
type: "terminal",
|
|
14759
|
+
terminal: { stopReason: "error", messages: [message] },
|
|
14760
|
+
}
|
|
14761
|
+
: false;
|
|
14392
14762
|
}
|
|
14393
14763
|
const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
14394
14764
|
if (!trigger) {
|
|
@@ -14415,12 +14785,14 @@ export class AgentSession {
|
|
|
14415
14785
|
// capacity-overload event.
|
|
14416
14786
|
if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
|
|
14417
14787
|
const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
|
|
14788
|
+
const bareDefaultWrappedFirstEventTimeout = isBareDefaultWrappedFirstEventTimeout(message);
|
|
14418
14789
|
// Content-free Codex overload is replay-safe by the same reasoning as
|
|
14419
14790
|
// credential rotation: no partial output means no observable state.
|
|
14420
14791
|
const canReplayCodexOverload = bareDefaultCodexOverload;
|
|
14421
14792
|
if (
|
|
14422
14793
|
(!canReplayCodexOverload &&
|
|
14423
14794
|
!this.#isTypedFirstEventTimeout(message) &&
|
|
14795
|
+
!bareDefaultWrappedFirstEventTimeout &&
|
|
14424
14796
|
(hasBareDefaultRetryDisqualifyingFacts(message) ||
|
|
14425
14797
|
(classification !== "transient" && classification !== "first_event_timeout") ||
|
|
14426
14798
|
!BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
|
|
@@ -14429,7 +14801,7 @@ export class AgentSession {
|
|
|
14429
14801
|
return false;
|
|
14430
14802
|
}
|
|
14431
14803
|
}
|
|
14432
|
-
const legacyUnbounded = classification === "transient";
|
|
14804
|
+
const legacyUnbounded = !managedFallback && classification === "transient";
|
|
14433
14805
|
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
14434
14806
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
14435
14807
|
let outcome = managedFallback
|
|
@@ -14496,7 +14868,7 @@ export class AgentSession {
|
|
|
14496
14868
|
: this.#fallbackExhaustionError(controller);
|
|
14497
14869
|
this.emitNotice("error", errorMessage, "fallback");
|
|
14498
14870
|
if (managedOutcome && ownership) {
|
|
14499
|
-
this.agent.requestRunTerminal(ownership.logicalRunId, {
|
|
14871
|
+
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
14500
14872
|
stopReason: "exhausted",
|
|
14501
14873
|
messages: [this.#managedFallbackExhaustionMessage(message, errorMessage)],
|
|
14502
14874
|
});
|
|
@@ -14522,12 +14894,11 @@ export class AgentSession {
|
|
|
14522
14894
|
await this.#emitSessionEvent({
|
|
14523
14895
|
type: "auto_retry_start",
|
|
14524
14896
|
attempt: this.#retryAttempt,
|
|
14525
|
-
maxAttempts:
|
|
14526
|
-
|
|
14897
|
+
maxAttempts: managedFallback
|
|
14898
|
+
? controller.maxAttempts
|
|
14899
|
+
: firstEventTimeout
|
|
14527
14900
|
? retrySettings.maxRetries + 1
|
|
14528
|
-
:
|
|
14529
|
-
? controller.maxAttempts
|
|
14530
|
-
: retrySettings.maxRetries,
|
|
14901
|
+
: retrySettings.maxRetries,
|
|
14531
14902
|
delayMs,
|
|
14532
14903
|
errorMessage,
|
|
14533
14904
|
unbounded: managedFallback ? false : legacyUnbounded,
|
|
@@ -14548,8 +14919,13 @@ export class AgentSession {
|
|
|
14548
14919
|
}
|
|
14549
14920
|
this.agent.replaceMessages(messages.slice(0, end));
|
|
14550
14921
|
}
|
|
14922
|
+
const retrySignal = ownership
|
|
14923
|
+
? AbortSignal.any([retryAbortController.signal, ownership.lease.signal])
|
|
14924
|
+
: retryAbortController.signal;
|
|
14925
|
+
const ownershipCancelled = () =>
|
|
14926
|
+
Boolean(ownership && (!ownership.isCurrent() || ownership.lease.signal.aborted));
|
|
14551
14927
|
try {
|
|
14552
|
-
await scheduler.wait(delayMs, { signal:
|
|
14928
|
+
await scheduler.wait(delayMs, { signal: retrySignal });
|
|
14553
14929
|
} catch {
|
|
14554
14930
|
if (this.#retryAbortController !== retryAbortController) return;
|
|
14555
14931
|
this.#retryAbortController = undefined;
|
|
@@ -14568,7 +14944,10 @@ export class AgentSession {
|
|
|
14568
14944
|
return;
|
|
14569
14945
|
}
|
|
14570
14946
|
}
|
|
14571
|
-
if (
|
|
14947
|
+
if (
|
|
14948
|
+
(retryAbortController.signal.aborted || ownershipCancelled()) &&
|
|
14949
|
+
!(this.#retryNowRequested && !ownershipCancelled())
|
|
14950
|
+
) {
|
|
14572
14951
|
if (this.#retryAbortController !== retryAbortController) return;
|
|
14573
14952
|
this.#retryAbortController = undefined;
|
|
14574
14953
|
const attempt = this.#retryAttempt;
|
|
@@ -14588,7 +14967,7 @@ export class AgentSession {
|
|
|
14588
14967
|
if (managedOutcome) {
|
|
14589
14968
|
try {
|
|
14590
14969
|
await this.#checkEstimatedContextBeforePrompt();
|
|
14591
|
-
if (
|
|
14970
|
+
if (ownershipCancelled()) {
|
|
14592
14971
|
const attempt = this.#retryAttempt;
|
|
14593
14972
|
this.#retryAttempt = 0;
|
|
14594
14973
|
await this.#emitSessionEvent({
|
|
@@ -14619,10 +14998,13 @@ export class AgentSession {
|
|
|
14619
14998
|
}
|
|
14620
14999
|
}
|
|
14621
15000
|
|
|
15001
|
+
const resourceRunId = this.#runResourceLeaseContext.getStore()?.resourceRunId;
|
|
14622
15002
|
this.#scheduleAgentContinue({
|
|
14623
15003
|
delayMs: 1,
|
|
14624
15004
|
generation,
|
|
14625
15005
|
allowDuringCancelAndSubmit: true,
|
|
15006
|
+
suppressPredecessorAgentEnd: resourceRunId !== undefined,
|
|
15007
|
+
resourceRunId,
|
|
14626
15008
|
onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
|
|
14627
15009
|
onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
|
|
14628
15010
|
});
|
|
@@ -14677,7 +15059,11 @@ export class AgentSession {
|
|
|
14677
15059
|
|
|
14678
15060
|
async #promptAgentWithIdleRetry(
|
|
14679
15061
|
messages: AgentMessage[],
|
|
14680
|
-
options?: {
|
|
15062
|
+
options?: {
|
|
15063
|
+
toolChoice?: ToolChoice;
|
|
15064
|
+
fallbackManaged?: boolean;
|
|
15065
|
+
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
15066
|
+
},
|
|
14681
15067
|
predecessorAgentEndHold?: symbol,
|
|
14682
15068
|
): Promise<void> {
|
|
14683
15069
|
const deadline = Date.now() + 30_000;
|
|
@@ -14690,6 +15076,7 @@ export class AgentSession {
|
|
|
14690
15076
|
continuationHold = undefined;
|
|
14691
15077
|
try {
|
|
14692
15078
|
await this.agent.prompt(messages, options);
|
|
15079
|
+
this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
|
|
14693
15080
|
return;
|
|
14694
15081
|
} catch (error) {
|
|
14695
15082
|
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
@@ -15441,19 +15828,30 @@ export class AgentSession {
|
|
|
15441
15828
|
!rosterClaim || this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
15442
15829
|
const prependMessagesValid = () => rosterClaimIsCurrent() && args.prependMessagesValid?.() !== false;
|
|
15443
15830
|
const rosterMessage = !callerOwnsRosterClaim && rosterClaimIsCurrent() ? rosterClaim?.message : undefined;
|
|
15831
|
+
let sideAttempt: { scope: AttemptScope; dispose: () => void } | undefined;
|
|
15444
15832
|
try {
|
|
15445
15833
|
const model = this.model;
|
|
15446
15834
|
if (!model) throw new Error("No active model on session");
|
|
15447
15835
|
const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, this.sessionId), args.signal);
|
|
15448
15836
|
if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
15837
|
+
sideAttempt = this.agent.mintSideAttemptScope();
|
|
15838
|
+
const sideScope = sideAttempt.scope;
|
|
15839
|
+
this.#attemptRecordStore.register(sideScope);
|
|
15840
|
+
this.#attemptRecordStore.establishClean(sideScope);
|
|
15449
15841
|
const prependMessages = prependMessagesValid()
|
|
15450
15842
|
? [...(rosterMessage ? [rosterMessage] : []), ...(args.prependMessages ?? [])]
|
|
15451
15843
|
: undefined;
|
|
15452
15844
|
let snapshot = this.#buildEphemeralSnapshot(args.promptText, prependMessages);
|
|
15453
|
-
let llmMessages = await awaitEphemeralAbort(
|
|
15845
|
+
let llmMessages = await awaitEphemeralAbort(
|
|
15846
|
+
this.convertMessagesToLlm(snapshot, args.signal, sideScope),
|
|
15847
|
+
args.signal,
|
|
15848
|
+
);
|
|
15454
15849
|
if (prependMessages && !prependMessagesValid()) {
|
|
15455
15850
|
snapshot = this.#buildEphemeralSnapshot(args.promptText);
|
|
15456
|
-
llmMessages = await awaitEphemeralAbort(
|
|
15851
|
+
llmMessages = await awaitEphemeralAbort(
|
|
15852
|
+
this.convertMessagesToLlm(snapshot, args.signal, sideScope),
|
|
15853
|
+
args.signal,
|
|
15854
|
+
);
|
|
15457
15855
|
}
|
|
15458
15856
|
const context: Context = { systemPrompt: this.systemPrompt, messages: llmMessages, tools: [] };
|
|
15459
15857
|
const ephemeralSessionId = crypto.randomUUID();
|
|
@@ -15474,6 +15872,7 @@ export class AgentSession {
|
|
|
15474
15872
|
toolChoice: "none",
|
|
15475
15873
|
},
|
|
15476
15874
|
model.provider,
|
|
15875
|
+
sideScope,
|
|
15477
15876
|
);
|
|
15478
15877
|
args.signal?.throwIfAborted();
|
|
15479
15878
|
let replyText = "";
|
|
@@ -15501,6 +15900,10 @@ export class AgentSession {
|
|
|
15501
15900
|
}
|
|
15502
15901
|
return { replyText: replyText.trim(), assistantMessage };
|
|
15503
15902
|
} finally {
|
|
15903
|
+
if (sideAttempt) {
|
|
15904
|
+
sideAttempt.dispose();
|
|
15905
|
+
this.#attemptRecordStore.retire(sideAttempt.scope);
|
|
15906
|
+
}
|
|
15504
15907
|
if (!callerOwnsRosterClaim && rosterClaim) this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
15505
15908
|
}
|
|
15506
15909
|
}
|
|
@@ -15514,8 +15917,15 @@ export class AgentSession {
|
|
|
15514
15917
|
}
|
|
15515
15918
|
const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, credentialSessionId), args.signal);
|
|
15516
15919
|
if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
15920
|
+
const sideAttempt = this.agent.mintSideAttemptScope();
|
|
15921
|
+
this.#attemptRecordStore.register(sideAttempt.scope);
|
|
15922
|
+
this.#attemptRecordStore.establishClean(sideAttempt.scope);
|
|
15517
15923
|
const scope = args.turn.scope;
|
|
15518
|
-
if (!scope)
|
|
15924
|
+
if (!scope) {
|
|
15925
|
+
sideAttempt.dispose();
|
|
15926
|
+
this.#attemptRecordStore.retire(sideAttempt.scope);
|
|
15927
|
+
throw new Error("The /btw conversation scope was scrubbed.");
|
|
15928
|
+
}
|
|
15519
15929
|
const messages = scope.messages.map(message => ({
|
|
15520
15930
|
role: message.role,
|
|
15521
15931
|
content: [{ type: "text" as const, text: message.text }],
|
|
@@ -15548,6 +15958,7 @@ export class AgentSession {
|
|
|
15548
15958
|
requestMaxRetries: 0,
|
|
15549
15959
|
streamMaxRetries: 0,
|
|
15550
15960
|
streamFirstEventTimeoutMs: 0,
|
|
15961
|
+
attemptScope: sideAttempt.scope,
|
|
15551
15962
|
};
|
|
15552
15963
|
const iterator = streamSimple(scope.model, context, options)[Symbol.asyncIterator]();
|
|
15553
15964
|
let replyText = "";
|
|
@@ -15607,6 +16018,8 @@ export class AgentSession {
|
|
|
15607
16018
|
if (idleTimer) clearTimeout(idleTimer);
|
|
15608
16019
|
clearTimeout(totalTimer);
|
|
15609
16020
|
void iterator.return?.().catch(() => undefined);
|
|
16021
|
+
sideAttempt.dispose();
|
|
16022
|
+
this.#attemptRecordStore.retire(sideAttempt.scope);
|
|
15610
16023
|
}
|
|
15611
16024
|
}
|
|
15612
16025
|
|