@gajae-code/coding-agent 0.12.5 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +4 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- 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-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- 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 +4 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -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/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +5 -2
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +178 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -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/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- 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-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- 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/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- 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 +32 -6
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +105 -5
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +19 -21
- package/src/session/session-storage.ts +146 -24
- package/src/session/streaming-output.ts +198 -40
- package/src/task/index.ts +45 -0
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +4 -0
- package/src/task/render.ts +30 -0
- package/src/task/types.ts +34 -0
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -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,
|
|
@@ -342,7 +349,12 @@ import {
|
|
|
342
349
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
343
350
|
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
344
351
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
345
|
-
import {
|
|
352
|
+
import {
|
|
353
|
+
createArtifactFailurePreview,
|
|
354
|
+
type OutputMeta,
|
|
355
|
+
outputMeta,
|
|
356
|
+
wrapToolWithMetaNotice,
|
|
357
|
+
} from "../tools/output-meta";
|
|
346
358
|
import { normalizeLocalScheme, resolveReadPath, resolveToCwd } from "../tools/path-utils";
|
|
347
359
|
import { registerResourceGcSession } from "../tools/resource-gc";
|
|
348
360
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
@@ -564,7 +576,11 @@ export interface AgentSessionConfig {
|
|
|
564
576
|
/** Tool-session factory context used to lazily attach workflow-gate-only tools. */
|
|
565
577
|
workflowGateToolSession?: ToolSession;
|
|
566
578
|
/** Current session pre-LLM message transform pipeline */
|
|
567
|
-
transformContext?: (
|
|
579
|
+
transformContext?: (
|
|
580
|
+
messages: AgentMessage[],
|
|
581
|
+
signal?: AbortSignal,
|
|
582
|
+
scope?: AttemptScopeRef,
|
|
583
|
+
) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
568
584
|
/** Provider payload hook used by the active session request path */
|
|
569
585
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
570
586
|
/** Provider response hook used by the active session request path */
|
|
@@ -869,14 +885,17 @@ type RetryErrorClassification =
|
|
|
869
885
|
|
|
870
886
|
const BARE_DEFAULT_WATCHDOG_ERROR =
|
|
871
887
|
/^(?:[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)$/;
|
|
888
|
+
const PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = "Provider stream timed out while waiting for the first event";
|
|
889
|
+
const WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = `Error: ${PROVIDER_FIRST_EVENT_TIMEOUT_ERROR}`;
|
|
890
|
+
const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream timed out while waiting for first event";
|
|
872
891
|
const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
|
|
873
892
|
const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
874
893
|
"anthropic-messages": new Set([
|
|
875
|
-
|
|
894
|
+
PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
|
|
876
895
|
"Anthropic stream timed out while waiting for the first event",
|
|
877
896
|
]),
|
|
878
897
|
"openai-completions": new Set([
|
|
879
|
-
|
|
898
|
+
PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
|
|
880
899
|
"OpenAI completions stream timed out while waiting for the first event",
|
|
881
900
|
]),
|
|
882
901
|
} as const;
|
|
@@ -884,11 +903,11 @@ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
|
884
903
|
const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
|
|
885
904
|
const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
886
905
|
"openai-responses": new Set([
|
|
887
|
-
|
|
906
|
+
PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
|
|
888
907
|
"OpenAI responses stream timed out while waiting for the first event",
|
|
889
908
|
]),
|
|
890
909
|
"openai-completions": new Set([
|
|
891
|
-
|
|
910
|
+
PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
|
|
892
911
|
"OpenAI completions stream timed out while waiting for the first event",
|
|
893
912
|
]),
|
|
894
913
|
} as const;
|
|
@@ -906,6 +925,25 @@ function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boole
|
|
|
906
925
|
(facts.headers !== undefined && Object.keys(facts.headers).length > 0)
|
|
907
926
|
);
|
|
908
927
|
}
|
|
928
|
+
function terminalProviderFirstEventTimeoutIdentity(errorMessage: string): string {
|
|
929
|
+
return errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR
|
|
930
|
+
? PROVIDER_FIRST_EVENT_TIMEOUT_ERROR
|
|
931
|
+
: errorMessage;
|
|
932
|
+
}
|
|
933
|
+
function isMessageOnlyFirstEventTimeout(message: AssistantMessage): boolean {
|
|
934
|
+
if (hasBareDefaultRetryDisqualifyingFacts(message)) return false;
|
|
935
|
+
return (
|
|
936
|
+
message.errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR ||
|
|
937
|
+
message.errorMessage === PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function isBareDefaultWrappedFirstEventTimeout(message: AssistantMessage): boolean {
|
|
942
|
+
return (
|
|
943
|
+
message.errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR &&
|
|
944
|
+
!hasBareDefaultRetryDisqualifyingFacts(message)
|
|
945
|
+
);
|
|
946
|
+
}
|
|
909
947
|
function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
|
|
910
948
|
return (
|
|
911
949
|
message.api === "openai-codex-responses" &&
|
|
@@ -1024,6 +1062,102 @@ function boundAgentBashArtifactSaveDiagnostic(error: unknown): string {
|
|
|
1024
1062
|
return truncateHeadBytes(normalized, AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES).text;
|
|
1025
1063
|
}
|
|
1026
1064
|
|
|
1065
|
+
const PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS = 500;
|
|
1066
|
+
const PRE_ADMISSION_ARTIFACT_MAX_PENDING_SAVES = 2;
|
|
1067
|
+
|
|
1068
|
+
type PreAdmissionArtifactSaveOutcome =
|
|
1069
|
+
| { kind: "saved"; saved: Awaited<ReturnType<SessionManager["saveArtifactReceipt"]>> }
|
|
1070
|
+
| { kind: "failed"; error: unknown };
|
|
1071
|
+
|
|
1072
|
+
interface PreAdmissionArtifactSaveAdmission {
|
|
1073
|
+
readonly pending: Set<Promise<PreAdmissionArtifactSaveOutcome>>;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Pre-admission spills are owned by their concrete SessionManager. A stalled
|
|
1078
|
+
* save remains admitted until its promise settles, so timeout does not turn
|
|
1079
|
+
* each subsequent tool result into another retained full payload.
|
|
1080
|
+
*/
|
|
1081
|
+
const preAdmissionArtifactSaveAdmissions = new WeakMap<SessionManager, PreAdmissionArtifactSaveAdmission>();
|
|
1082
|
+
|
|
1083
|
+
function preAdmissionArtifactSaveAdmissionFor(sessionManager: SessionManager): PreAdmissionArtifactSaveAdmission {
|
|
1084
|
+
let admission = preAdmissionArtifactSaveAdmissions.get(sessionManager);
|
|
1085
|
+
if (!admission) {
|
|
1086
|
+
admission = { pending: new Set() };
|
|
1087
|
+
preAdmissionArtifactSaveAdmissions.set(sessionManager, admission);
|
|
1088
|
+
}
|
|
1089
|
+
return admission;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function beginPreAdmissionArtifactSave(
|
|
1093
|
+
sessionManager: SessionManager,
|
|
1094
|
+
content: string,
|
|
1095
|
+
toolType: string,
|
|
1096
|
+
): { kind: "capacity" } | { kind: "started"; outcome: Promise<PreAdmissionArtifactSaveOutcome> } {
|
|
1097
|
+
const admission = preAdmissionArtifactSaveAdmissionFor(sessionManager);
|
|
1098
|
+
if (admission.pending.size >= PRE_ADMISSION_ARTIFACT_MAX_PENDING_SAVES) return { kind: "capacity" };
|
|
1099
|
+
|
|
1100
|
+
let outcome: Promise<PreAdmissionArtifactSaveOutcome>;
|
|
1101
|
+
try {
|
|
1102
|
+
outcome = sessionManager.saveArtifactReceipt(content, toolType).then(
|
|
1103
|
+
saved => ({ kind: "saved" as const, saved }),
|
|
1104
|
+
error => ({ kind: "failed" as const, error }),
|
|
1105
|
+
);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
return { kind: "started", outcome: Promise.resolve({ kind: "failed", error }) };
|
|
1108
|
+
}
|
|
1109
|
+
admission.pending.add(outcome);
|
|
1110
|
+
void outcome.then(
|
|
1111
|
+
() => admission.pending.delete(outcome),
|
|
1112
|
+
() => admission.pending.delete(outcome),
|
|
1113
|
+
);
|
|
1114
|
+
return { kind: "started", outcome };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function replaceToolResultWithArtifactFailurePreview(
|
|
1118
|
+
message: ToolResultMessage,
|
|
1119
|
+
fullText: string,
|
|
1120
|
+
maxInlineBytes: number | undefined,
|
|
1121
|
+
diagnostic: string,
|
|
1122
|
+
): void {
|
|
1123
|
+
const preview = createArtifactFailurePreview(fullText, maxInlineBytes, diagnostic);
|
|
1124
|
+
const existingDetails =
|
|
1125
|
+
message.details && typeof message.details === "object" ? (message.details as Record<string, unknown>) : {};
|
|
1126
|
+
const existingMeta =
|
|
1127
|
+
existingDetails.meta && typeof existingDetails.meta === "object" ? (existingDetails.meta as OutputMeta) : {};
|
|
1128
|
+
const existingTruncation = existingMeta.truncation;
|
|
1129
|
+
const totalLines = fullText.length > 0 ? fullText.split("\n").length : 0;
|
|
1130
|
+
const outputLines = preview.text.length > 0 ? preview.text.split("\n").length : 0;
|
|
1131
|
+
const outputBytes = Buffer.byteLength(preview.text, "utf-8");
|
|
1132
|
+
const truncation: NonNullable<OutputMeta["truncation"]> = {
|
|
1133
|
+
...(existingTruncation ?? {}),
|
|
1134
|
+
direction: "tail",
|
|
1135
|
+
truncatedBy: "bytes",
|
|
1136
|
+
noticeOwner: undefined,
|
|
1137
|
+
totalLines,
|
|
1138
|
+
totalBytes: Buffer.byteLength(fullText, "utf-8"),
|
|
1139
|
+
outputLines,
|
|
1140
|
+
outputBytes,
|
|
1141
|
+
maxBytes: maxInlineBytes,
|
|
1142
|
+
shownRange: undefined,
|
|
1143
|
+
headRange: undefined,
|
|
1144
|
+
tailRange: undefined,
|
|
1145
|
+
nextOffset: undefined,
|
|
1146
|
+
artifactId: undefined,
|
|
1147
|
+
artifactVerified: false,
|
|
1148
|
+
sourceCaptureIncomplete: true,
|
|
1149
|
+
artifactFailureDiagnostic: diagnostic,
|
|
1150
|
+
};
|
|
1151
|
+
message.details = {
|
|
1152
|
+
...existingDetails,
|
|
1153
|
+
meta: { ...existingMeta, truncation },
|
|
1154
|
+
};
|
|
1155
|
+
message.content = [
|
|
1156
|
+
...message.content.filter((block): block is ImageContent => block.type === "image"),
|
|
1157
|
+
{ type: "text", text: preview.text },
|
|
1158
|
+
];
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1027
1161
|
function summarizeAgentBashArtifactSave(
|
|
1028
1162
|
artifactId: string,
|
|
1029
1163
|
originalText: string,
|
|
@@ -1629,6 +1763,49 @@ export class StreamingEditFileCache {
|
|
|
1629
1763
|
}
|
|
1630
1764
|
}
|
|
1631
1765
|
type SessionAdmissionKind = "prompt" | "selection";
|
|
1766
|
+
class SessionRunCancellationDomainBridge implements RunCancellationDomainBridge {
|
|
1767
|
+
#domains = new Map<string, { domain: RunCancellationDomain; controller: AbortController }>();
|
|
1768
|
+
#released = new Set<string>();
|
|
1769
|
+
#quarantined = new Set<string>();
|
|
1770
|
+
|
|
1771
|
+
open(resourceRunId: string) {
|
|
1772
|
+
if (this.#quarantined.has(resourceRunId)) return { ok: false as const, reason: "quarantined" as const };
|
|
1773
|
+
const existing = this.#domains.get(resourceRunId);
|
|
1774
|
+
if (existing) return { ok: true as const, domain: existing.domain, created: false };
|
|
1775
|
+
if (this.#released.has(resourceRunId)) return { ok: false as const, reason: "duplicate_identity" as const };
|
|
1776
|
+
const controller = new AbortController();
|
|
1777
|
+
const domain: RunCancellationDomain = { resourceRunId, signal: controller.signal };
|
|
1778
|
+
this.#domains.set(resourceRunId, { domain, controller });
|
|
1779
|
+
return { ok: true as const, domain, created: true };
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
lookup(resourceRunId: string): RunCancellationDomain | undefined {
|
|
1783
|
+
return this.#domains.get(resourceRunId)?.domain;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
abort(resourceRunId: string, reason?: unknown) {
|
|
1787
|
+
const record = this.#domains.get(resourceRunId);
|
|
1788
|
+
if (!record)
|
|
1789
|
+
return {
|
|
1790
|
+
ok: false as const,
|
|
1791
|
+
reason: this.#quarantined.has(resourceRunId) ? ("quarantined" as const) : ("unknown_run" as const),
|
|
1792
|
+
};
|
|
1793
|
+
const newlyAborted = !record.controller.signal.aborted;
|
|
1794
|
+
if (newlyAborted) record.controller.abort(reason);
|
|
1795
|
+
return { ok: true as const, newlyAborted };
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
release(resourceRunId: string, disposition: "settled" | "quarantined"): void {
|
|
1799
|
+
const record = this.#domains.get(resourceRunId);
|
|
1800
|
+
if (!record) return;
|
|
1801
|
+
if (disposition === "quarantined") {
|
|
1802
|
+
this.#quarantined.add(resourceRunId);
|
|
1803
|
+
if (!record.controller.signal.aborted) record.controller.abort();
|
|
1804
|
+
}
|
|
1805
|
+
this.#domains.delete(resourceRunId);
|
|
1806
|
+
this.#released.add(resourceRunId);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1632
1809
|
|
|
1633
1810
|
type SessionAdmissionEntry = {
|
|
1634
1811
|
kind: SessionAdmissionKind;
|
|
@@ -1888,6 +2065,15 @@ export class AgentSession {
|
|
|
1888
2065
|
#newSessionTransition: Promise<boolean> | undefined;
|
|
1889
2066
|
// Extension system
|
|
1890
2067
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
2068
|
+
#attemptAuthority!: AttemptScopeAuthority;
|
|
2069
|
+
#attemptRecordStore!: AttemptRecordStore;
|
|
2070
|
+
#activeLogicalRunId: AttemptRunHandle["logicalRunId"] | undefined;
|
|
2071
|
+
#acceptRunHandle(handle: AttemptRunHandle | undefined): void {
|
|
2072
|
+
// Integration doubles can accept a run without minting a handle; keep the
|
|
2073
|
+
// previously recorded run id rather than throwing inside the callback.
|
|
2074
|
+
if (!handle) return;
|
|
2075
|
+
this.#activeLogicalRunId = handle.logicalRunId;
|
|
2076
|
+
}
|
|
1891
2077
|
|
|
1892
2078
|
#turnIndex = 0;
|
|
1893
2079
|
#workerIntegrationScheduler: WorkerIntegrationRequestScheduler;
|
|
@@ -1917,7 +2103,11 @@ export class AgentSession {
|
|
|
1917
2103
|
// Tool registry and prompt builder for extensions
|
|
1918
2104
|
#toolRegistry: Map<string, AgentTool>;
|
|
1919
2105
|
#workflowGateToolSession: ToolSession | undefined;
|
|
1920
|
-
#transformContext: (
|
|
2106
|
+
#transformContext: (
|
|
2107
|
+
messages: AgentMessage[],
|
|
2108
|
+
signal?: AbortSignal,
|
|
2109
|
+
scope?: AttemptScopeRef,
|
|
2110
|
+
) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
1921
2111
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
1922
2112
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
1923
2113
|
#onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
|
|
@@ -2009,6 +2199,10 @@ export class AgentSession {
|
|
|
2009
2199
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
2010
2200
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
2011
2201
|
#postPromptTasksAbortController = new AbortController();
|
|
2202
|
+
#runCancellationDomains = new SessionRunCancellationDomainBridge();
|
|
2203
|
+
#postPromptLeases = new Map<string, RunResourceProducerLease>();
|
|
2204
|
+
#runResourceLeaseContext = new AsyncLocalStorage<RunResourceProducerLease>();
|
|
2205
|
+
#agentSessionClaimKey = {};
|
|
2012
2206
|
|
|
2013
2207
|
#streamingEditAbortTriggered = false;
|
|
2014
2208
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
@@ -2051,6 +2245,7 @@ export class AgentSession {
|
|
|
2051
2245
|
// the successor or proves it cannot. Holds prevent a false idle event while
|
|
2052
2246
|
// preserving the predecessor for cancellation and preflight failures.
|
|
2053
2247
|
#pendingAgentEndContinuationHolds = new Map<symbol, AgentSessionEvent>();
|
|
2248
|
+
#deferredAgentEndLeases = new WeakMap<AgentSessionEvent, RunResourceProducerLease>();
|
|
2054
2249
|
#sessionSettlementPromise: Promise<void> | undefined;
|
|
2055
2250
|
#sessionSettlementResolve: (() => void) | undefined;
|
|
2056
2251
|
#agentEndPublicationInFlight = 0;
|
|
@@ -2334,6 +2529,17 @@ export class AgentSession {
|
|
|
2334
2529
|
this.#restoreDeferredAgentEndAfterContinuationFailure(pending);
|
|
2335
2530
|
}
|
|
2336
2531
|
|
|
2532
|
+
#releaseDeferredAgentEndLease(pending: AgentSessionEvent | undefined): void {
|
|
2533
|
+
if (!pending) return;
|
|
2534
|
+
const lease = this.#deferredAgentEndLeases.get(pending);
|
|
2535
|
+
if (!lease) return;
|
|
2536
|
+
this.#deferredAgentEndLeases.delete(pending);
|
|
2537
|
+
if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
|
|
2538
|
+
this.#postPromptLeases.delete(lease.resourceRunId);
|
|
2539
|
+
}
|
|
2540
|
+
lease.closeDiscovery();
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2337
2543
|
#isSessionSettlementPending(): boolean {
|
|
2338
2544
|
return (
|
|
2339
2545
|
this.#promptInFlightCount > 0 ||
|
|
@@ -2397,13 +2603,26 @@ export class AgentSession {
|
|
|
2397
2603
|
return;
|
|
2398
2604
|
}
|
|
2399
2605
|
this.#pendingAgentEndEmit = undefined;
|
|
2606
|
+
const lease = this.#deferredAgentEndLeases.get(pending);
|
|
2607
|
+
if (lease) this.#deferredAgentEndLeases.delete(pending);
|
|
2400
2608
|
this.#agentEndPublicationInFlight++;
|
|
2401
|
-
this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending);
|
|
2609
|
+
this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease);
|
|
2402
2610
|
void this.#agentEndPublicationPromise;
|
|
2403
2611
|
}
|
|
2404
2612
|
|
|
2405
|
-
async #publishDeferredAgentEnd(
|
|
2406
|
-
|
|
2613
|
+
async #publishDeferredAgentEnd(
|
|
2614
|
+
pending: AgentSessionEvent,
|
|
2615
|
+
lease: RunResourceProducerLease | undefined,
|
|
2616
|
+
): Promise<void> {
|
|
2617
|
+
let extensionDelivery: Promise<void> | undefined;
|
|
2618
|
+
const releaseLease = () => {
|
|
2619
|
+
if (!lease) return;
|
|
2620
|
+
if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
|
|
2621
|
+
this.#postPromptLeases.delete(lease.resourceRunId);
|
|
2622
|
+
}
|
|
2623
|
+
lease.closeDiscovery();
|
|
2624
|
+
};
|
|
2625
|
+
const publish = async () => {
|
|
2407
2626
|
// Worker integration is first-party lifecycle persistence, not an extension
|
|
2408
2627
|
// hook. Make it durable before publishing the terminal boundary while user
|
|
2409
2628
|
// extension delivery remains asynchronous.
|
|
@@ -2411,10 +2630,21 @@ export class AgentSession {
|
|
|
2411
2630
|
// Persist before notifying synchronous subscribers: a subscriber may start a
|
|
2412
2631
|
// successor prompt from agent_end, whose running state must serialize after
|
|
2413
2632
|
// this terminal boundary rather than be overwritten by it.
|
|
2414
|
-
this.#persistRuntimeStateInBackground(pending);
|
|
2633
|
+
void this.#persistRuntimeStateInBackground(pending);
|
|
2415
2634
|
this.#emit(pending);
|
|
2416
|
-
|
|
2635
|
+
extensionDelivery = this.#queueExtensionEvent(
|
|
2636
|
+
pending,
|
|
2637
|
+
undefined,
|
|
2638
|
+
true,
|
|
2639
|
+
(pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope,
|
|
2640
|
+
);
|
|
2641
|
+
};
|
|
2642
|
+
try {
|
|
2643
|
+
if (lease) await this.#runResourceLeaseContext.run(lease, publish);
|
|
2644
|
+
else await publish();
|
|
2417
2645
|
} finally {
|
|
2646
|
+
if (extensionDelivery) void extensionDelivery.then(releaseLease, releaseLease);
|
|
2647
|
+
else releaseLease();
|
|
2418
2648
|
this.#agentEndPublicationInFlight = Math.max(0, this.#agentEndPublicationInFlight - 1);
|
|
2419
2649
|
this.#resolveSessionSettlement();
|
|
2420
2650
|
}
|
|
@@ -2422,6 +2652,7 @@ export class AgentSession {
|
|
|
2422
2652
|
|
|
2423
2653
|
constructor(config: AgentSessionConfig) {
|
|
2424
2654
|
this.agent = config.agent;
|
|
2655
|
+
this.agent.bindRunCancellationDomainBridge(this.#runCancellationDomains, this.#agentSessionClaimKey);
|
|
2425
2656
|
this.sessionManager = config.sessionManager;
|
|
2426
2657
|
this.settings = config.settings;
|
|
2427
2658
|
this.#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(
|
|
@@ -2471,6 +2702,11 @@ export class AgentSession {
|
|
|
2471
2702
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
2472
2703
|
this.#slashCommands = config.slashCommands ?? [];
|
|
2473
2704
|
this.#extensionRunner = config.extensionRunner;
|
|
2705
|
+
this.#attemptAuthority = this.agent.getAttemptScopeAuthority();
|
|
2706
|
+
this.#attemptRecordStore = new AttemptRecordStore(this.#attemptAuthority);
|
|
2707
|
+
if (this.#extensionRunner && typeof this.#extensionRunner.setAttemptRecordStore === "function") {
|
|
2708
|
+
this.#extensionRunner.setAttemptRecordStore(this.#attemptRecordStore);
|
|
2709
|
+
}
|
|
2474
2710
|
this.#skills = config.skills ?? [];
|
|
2475
2711
|
this.#skillWarnings = config.skillWarnings ?? [];
|
|
2476
2712
|
this.#customCommands = config.customCommands ?? [];
|
|
@@ -2483,7 +2719,10 @@ export class AgentSession {
|
|
|
2483
2719
|
this.#workflowGateToolSession = config.workflowGateToolSession;
|
|
2484
2720
|
this.#requestedToolNames = config.requestedToolNames;
|
|
2485
2721
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
2486
|
-
|
|
2722
|
+
const configuredOnPayload = config.onPayload;
|
|
2723
|
+
this.#onPayload = configuredOnPayload
|
|
2724
|
+
? (payload, model, scope) => configuredOnPayload(payload, model, scope)
|
|
2725
|
+
: undefined;
|
|
2487
2726
|
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
2488
2727
|
// Avoid wrapping in an `async` closure when no user callback is configured: the
|
|
2489
2728
|
// outer await on `#onResponse` (provider-response.ts) tolerates a sync void return,
|
|
@@ -2491,20 +2730,20 @@ export class AgentSession {
|
|
|
2491
2730
|
// shows up as ~3.5% self time in streaming profiles.
|
|
2492
2731
|
const configuredOnResponse = config.onResponse;
|
|
2493
2732
|
this.#onResponse = configuredOnResponse
|
|
2494
|
-
? async (response, model) => {
|
|
2733
|
+
? async (response, model, scope) => {
|
|
2495
2734
|
this.rawSseDebugBuffer.recordResponse(response, model);
|
|
2496
|
-
await configuredOnResponse(response, model);
|
|
2735
|
+
await configuredOnResponse(response, model, scope);
|
|
2497
2736
|
}
|
|
2498
|
-
: (response, model) => {
|
|
2737
|
+
: (response, model, _scope) => {
|
|
2499
2738
|
this.rawSseDebugBuffer.recordResponse(response, model);
|
|
2500
2739
|
};
|
|
2501
2740
|
const configuredOnSseEvent = config.onSseEvent;
|
|
2502
2741
|
this.#onSseEvent = configuredOnSseEvent
|
|
2503
|
-
? (event, model) => {
|
|
2742
|
+
? (event, model, scope) => {
|
|
2504
2743
|
this.rawSseDebugBuffer.recordEvent(event, model);
|
|
2505
|
-
configuredOnSseEvent(event, model);
|
|
2744
|
+
configuredOnSseEvent(event, model, scope);
|
|
2506
2745
|
}
|
|
2507
|
-
: (event, model) => {
|
|
2746
|
+
: (event, model, _scope) => {
|
|
2508
2747
|
this.rawSseDebugBuffer.recordEvent(event, model);
|
|
2509
2748
|
};
|
|
2510
2749
|
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
@@ -3336,6 +3575,7 @@ export class AgentSession {
|
|
|
3336
3575
|
event: AgentSessionEvent,
|
|
3337
3576
|
turnGeneration?: number,
|
|
3338
3577
|
workerIntegrationSettled = false,
|
|
3578
|
+
scope?: AttemptScopeRef,
|
|
3339
3579
|
): Promise<void> {
|
|
3340
3580
|
// Streaming events observed after turn_end belong to no live extension turn.
|
|
3341
3581
|
// Events already queued before that boundary must drain in FIFO order, unless
|
|
@@ -3351,7 +3591,7 @@ export class AgentSession {
|
|
|
3351
3591
|
turnGeneration === undefined || turnGeneration === this.#extensionTurnGeneration;
|
|
3352
3592
|
const emit = async () => {
|
|
3353
3593
|
if (!belongsToCurrentTurn()) return;
|
|
3354
|
-
await this.#emitExtensionEvent(event, belongsToCurrentTurn, workerIntegrationSettled);
|
|
3594
|
+
await this.#emitExtensionEvent(event, belongsToCurrentTurn, workerIntegrationSettled, scope);
|
|
3355
3595
|
};
|
|
3356
3596
|
const queued = this.#queuedExtensionEvents.then(emit, emit);
|
|
3357
3597
|
this.#queuedExtensionEvents = queued.catch(() => {});
|
|
@@ -3364,38 +3604,103 @@ export class AgentSession {
|
|
|
3364
3604
|
}
|
|
3365
3605
|
|
|
3366
3606
|
#trackAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
3367
|
-
const
|
|
3607
|
+
const terminalOwner = event.type === "agent_end" ? getAgentTerminalOwnerContext(event) : undefined;
|
|
3608
|
+
const maintenanceCheckpoint =
|
|
3609
|
+
event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted";
|
|
3610
|
+
const terminalClaim =
|
|
3611
|
+
maintenanceCheckpoint || !terminalOwner
|
|
3612
|
+
? undefined
|
|
3613
|
+
: this.agent.resourceLedger.claimProducer(
|
|
3614
|
+
terminalOwner.resourceRunId,
|
|
3615
|
+
terminalOwner.domain,
|
|
3616
|
+
this.#agentSessionClaimKey,
|
|
3617
|
+
);
|
|
3618
|
+
// A maintenance checkpoint is not a terminal publication. Capture the current
|
|
3619
|
+
// logical handle synchronously, then reserve its producer before this listener
|
|
3620
|
+
// creates any async work. True terminals must never fall back to this mutable
|
|
3621
|
+
// session state: their owner context is the only authority.
|
|
3622
|
+
const activePromptHandle = maintenanceCheckpoint
|
|
3623
|
+
? this.activePromptHandle
|
|
3624
|
+
: event.type === "agent_end"
|
|
3625
|
+
? terminalOwner?.resourceRunId
|
|
3626
|
+
: this.activePromptHandle;
|
|
3627
|
+
const domain = activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle) : undefined;
|
|
3628
|
+
const eventReservation = terminalClaim?.ok
|
|
3629
|
+
? terminalClaim
|
|
3630
|
+
: activePromptHandle && domain
|
|
3631
|
+
? this.agent.resourceLedger.reserveProducer(
|
|
3632
|
+
activePromptHandle,
|
|
3633
|
+
domain,
|
|
3634
|
+
"post_prompt",
|
|
3635
|
+
"agent-session-event",
|
|
3636
|
+
)
|
|
3637
|
+
: undefined;
|
|
3638
|
+
const eventLease = eventReservation?.ok ? eventReservation.lease : undefined;
|
|
3368
3639
|
const agentEndHandled = event.type === "agent_end" ? Promise.withResolvers<void>() : undefined;
|
|
3369
3640
|
if (agentEndHandled) this.#agentEndHandlingPromise = agentEndHandled.promise;
|
|
3370
3641
|
this.#agentEventHandlersInFlight++;
|
|
3371
3642
|
const handler = (async (): Promise<void> => {
|
|
3372
3643
|
try {
|
|
3373
|
-
|
|
3644
|
+
// A terminal that carries owner context must win the exact producer claim;
|
|
3645
|
+
// a superseded run's late agent_end never falls back to a successor's
|
|
3646
|
+
// handle. Externally emitted terminals carry no owner context and own no
|
|
3647
|
+
// run resources, so they keep the ownerless handling path unless a
|
|
3648
|
+
// prompt-owned run is actually active.
|
|
3649
|
+
if (event.type === "agent_end" && !maintenanceCheckpoint) {
|
|
3650
|
+
if (terminalOwner ? !terminalClaim?.ok : this.activePromptHandle !== undefined) return;
|
|
3651
|
+
}
|
|
3652
|
+
// A prompt-owned event with a captured handle must have obtained an exact
|
|
3653
|
+
// producer lease before its handler was created. Failed ownership never
|
|
3654
|
+
// schedules prompt work or falls back to a successor's handle.
|
|
3655
|
+
if (activePromptHandle && !eventLease) return;
|
|
3656
|
+
if (eventLease && event.type === "agent_end")
|
|
3657
|
+
this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
|
|
3658
|
+
if (eventLease) {
|
|
3659
|
+
await this.#runResourceLeaseContext.run(eventLease, () =>
|
|
3660
|
+
this.#handleAgentEvent(event, activePromptHandle),
|
|
3661
|
+
);
|
|
3662
|
+
} else {
|
|
3663
|
+
await this.#handleAgentEvent(event, activePromptHandle);
|
|
3664
|
+
}
|
|
3374
3665
|
} catch (error) {
|
|
3375
3666
|
logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
|
|
3376
3667
|
} finally {
|
|
3668
|
+
if (eventLease) {
|
|
3669
|
+
const pendingAgentEnd =
|
|
3670
|
+
event.type === "agent_end" && !maintenanceCheckpoint && this.#pendingAgentEndEmit === event
|
|
3671
|
+
? this.#pendingAgentEndEmit
|
|
3672
|
+
: undefined;
|
|
3673
|
+
if (pendingAgentEnd) {
|
|
3674
|
+
this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
|
|
3675
|
+
} else {
|
|
3676
|
+
if (event.type === "agent_end" && this.#postPromptLeases.get(eventLease.resourceRunId) === eventLease)
|
|
3677
|
+
this.#postPromptLeases.delete(eventLease.resourceRunId);
|
|
3678
|
+
eventLease.closeDiscovery();
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3377
3681
|
this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
|
|
3378
3682
|
this.#flushPendingAgentEnd();
|
|
3379
3683
|
agentEndHandled?.resolve();
|
|
3380
3684
|
}
|
|
3381
3685
|
})();
|
|
3382
|
-
if (
|
|
3383
|
-
this.agent.resourceLedger.track(activePromptHandle, "post_prompt", "agent-session-event", handler);
|
|
3384
|
-
}
|
|
3686
|
+
if (eventLease) eventLease.track("post_prompt", "agent-session-event", handler);
|
|
3385
3687
|
return handler;
|
|
3386
3688
|
};
|
|
3387
3689
|
|
|
3388
|
-
#persistRuntimeStateInBackground(event: AgentSessionEvent): void {
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3690
|
+
async #persistRuntimeStateInBackground(event: AgentSessionEvent): Promise<void> {
|
|
3691
|
+
try {
|
|
3692
|
+
await persistCoordinatorRuntimeStateFromEvent(event, {
|
|
3693
|
+
sessionId: this.sessionId,
|
|
3694
|
+
cwd: this.sessionManager.getCwd(),
|
|
3695
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
3696
|
+
});
|
|
3697
|
+
} catch {
|
|
3394
3698
|
logger.warn("Failed to persist coordinator runtime state", { event: event.type });
|
|
3395
|
-
}
|
|
3699
|
+
}
|
|
3396
3700
|
}
|
|
3397
3701
|
|
|
3398
3702
|
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
3703
|
+
const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
|
|
3399
3704
|
if (event.type === "turn_start") {
|
|
3400
3705
|
this.#extensionTurnGeneration++;
|
|
3401
3706
|
this.#closedExtensionTurnGeneration = undefined;
|
|
@@ -3420,7 +3725,7 @@ export class AgentSession {
|
|
|
3420
3725
|
this.#emit(event);
|
|
3421
3726
|
if (this.#hasStreamingExtensionHandlers()) {
|
|
3422
3727
|
__agentSessionPerfCounters.messageUpdateExtensionQueues += 1;
|
|
3423
|
-
void this.#queueExtensionEvent(event, this.#extensionTurnGeneration);
|
|
3728
|
+
void this.#queueExtensionEvent(event, this.#extensionTurnGeneration, false, attemptScope);
|
|
3424
3729
|
}
|
|
3425
3730
|
return;
|
|
3426
3731
|
}
|
|
@@ -3449,7 +3754,7 @@ export class AgentSession {
|
|
|
3449
3754
|
if (event.type === "agent_end") {
|
|
3450
3755
|
// Start the durable terminal write before synchronous subscribers can
|
|
3451
3756
|
// re-enter prompt(), so a successor's running transition serializes after it.
|
|
3452
|
-
|
|
3757
|
+
await persistRuntimeState();
|
|
3453
3758
|
this.#emit(event);
|
|
3454
3759
|
await this.#emitExtensionEvent(event);
|
|
3455
3760
|
return;
|
|
@@ -3477,39 +3782,101 @@ export class AgentSession {
|
|
|
3477
3782
|
const textParts = message.content.flatMap(block => (block.type === "text" ? [block.text] : []));
|
|
3478
3783
|
if (textParts.length === 0) return;
|
|
3479
3784
|
const fullText = textParts.join("\n");
|
|
3785
|
+
const configuredInlineKiB = this.settings.get("tools.maxInlineResultBytes");
|
|
3786
|
+
const maxInlineBytes =
|
|
3787
|
+
configuredInlineKiB > 0 ? Math.max(1024, Math.floor(configuredInlineKiB * 1024)) : undefined;
|
|
3788
|
+
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
3789
|
+
const exceedsInlineCap = maxInlineBytes !== undefined && totalBytes > maxInlineBytes;
|
|
3480
3790
|
const contextWindow = this.model?.contextWindow;
|
|
3481
3791
|
const thresholdTokens = Math.min(
|
|
3482
3792
|
8_000,
|
|
3483
3793
|
contextWindow && contextWindow > 0 ? Math.floor(contextWindow * 0.05) : 8_000,
|
|
3484
3794
|
);
|
|
3485
|
-
|
|
3795
|
+
// The inline byte cap is an independent admission bound and must be checked
|
|
3796
|
+
// before token heuristics can opt this result out of spilling.
|
|
3797
|
+
if (!exceedsInlineCap && (thresholdTokens <= 0 || estimateTextTokensHeuristic(fullText) <= thresholdTokens))
|
|
3798
|
+
return;
|
|
3486
3799
|
|
|
3800
|
+
const saveAdmission = beginPreAdmissionArtifactSave(this.sessionManager, fullText, "tool-result");
|
|
3801
|
+
if (saveAdmission.kind === "capacity") {
|
|
3802
|
+
replaceToolResultWithArtifactFailurePreview(
|
|
3803
|
+
message,
|
|
3804
|
+
fullText,
|
|
3805
|
+
maxInlineBytes,
|
|
3806
|
+
"artifact save capacity exhausted; the previous save is still settling",
|
|
3807
|
+
);
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3487
3810
|
try {
|
|
3488
|
-
const
|
|
3489
|
-
|
|
3811
|
+
const saveOutcome = await Promise.race([
|
|
3812
|
+
saveAdmission.outcome,
|
|
3813
|
+
Bun.sleep(PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS).then(() => ({ kind: "timeout" as const })),
|
|
3814
|
+
]);
|
|
3815
|
+
if (saveOutcome.kind !== "saved" || !saveOutcome.saved) {
|
|
3816
|
+
const diagnostic =
|
|
3817
|
+
saveOutcome.kind === "timeout"
|
|
3818
|
+
? `did not settle within ${PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS}ms`
|
|
3819
|
+
: saveOutcome.kind === "failed"
|
|
3820
|
+
? boundAgentBashArtifactSaveDiagnostic(saveOutcome.error)
|
|
3821
|
+
: "artifact storage is unavailable";
|
|
3822
|
+
replaceToolResultWithArtifactFailurePreview(message, fullText, maxInlineBytes, diagnostic);
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
const saved = saveOutcome.saved;
|
|
3826
|
+
const existingDetails = message.details;
|
|
3827
|
+
const detailRecord =
|
|
3828
|
+
existingDetails && typeof existingDetails === "object" ? (existingDetails as Record<string, unknown>) : {};
|
|
3829
|
+
const existingMeta =
|
|
3830
|
+
detailRecord.meta && typeof detailRecord.meta === "object" ? (detailRecord.meta as OutputMeta) : {};
|
|
3831
|
+
const existingTruncation = existingMeta.truncation;
|
|
3832
|
+
const priorLoss = existingTruncation !== undefined;
|
|
3490
3833
|
const digest = crypto.createHash("sha256").update(fullText).digest("hex");
|
|
3491
|
-
const
|
|
3834
|
+
const sourceCaptureIncomplete = priorLoss || !saved.complete || undefined;
|
|
3835
|
+
const preview = createPreAdmissionArtifactSpillPreview(fullText, saved.id, digest, {
|
|
3836
|
+
maxInlineBytes,
|
|
3837
|
+
fullOutput: saved.complete && !priorLoss,
|
|
3838
|
+
});
|
|
3492
3839
|
const spillMeta = outputMeta()
|
|
3493
3840
|
.truncationFromText(preview, {
|
|
3494
3841
|
direction: "middle",
|
|
3495
3842
|
totalLines: fullText.split("\n").length,
|
|
3496
|
-
totalBytes
|
|
3497
|
-
artifactId,
|
|
3843
|
+
totalBytes,
|
|
3844
|
+
artifactId: saved.id,
|
|
3845
|
+
artifactVerified: true,
|
|
3846
|
+
sourceCaptureIncomplete,
|
|
3847
|
+
...(maxInlineBytes !== undefined ? { maxBytes: maxInlineBytes } : {}),
|
|
3498
3848
|
})
|
|
3499
3849
|
.get();
|
|
3500
|
-
const
|
|
3501
|
-
const
|
|
3502
|
-
|
|
3503
|
-
const
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3850
|
+
const spillTruncation = spillMeta?.truncation;
|
|
3851
|
+
const artifactTruncatedBytes =
|
|
3852
|
+
Math.max(existingTruncation?.artifactTruncatedBytes ?? 0, saved.omittedBytes ?? 0) || undefined;
|
|
3853
|
+
const mergedTruncation = spillTruncation
|
|
3854
|
+
? {
|
|
3855
|
+
...existingTruncation,
|
|
3856
|
+
...spillTruncation,
|
|
3857
|
+
artifactId: saved.id,
|
|
3858
|
+
artifactVerified: true,
|
|
3859
|
+
artifactTruncatedBytes,
|
|
3860
|
+
sourceTruncatedBytes: existingTruncation?.sourceTruncatedBytes,
|
|
3861
|
+
sourceCaptureIncomplete,
|
|
3862
|
+
columnDroppedBytes: existingTruncation?.columnDroppedBytes,
|
|
3863
|
+
shownRange: sourceCaptureIncomplete ? undefined : spillTruncation.shownRange,
|
|
3864
|
+
headRange: sourceCaptureIncomplete ? undefined : spillTruncation.headRange,
|
|
3865
|
+
tailRange: sourceCaptureIncomplete ? undefined : spillTruncation.tailRange,
|
|
3866
|
+
nextOffset: sourceCaptureIncomplete ? undefined : spillTruncation.nextOffset,
|
|
3867
|
+
}
|
|
3868
|
+
: existingTruncation;
|
|
3869
|
+
message.details = {
|
|
3870
|
+
...detailRecord,
|
|
3871
|
+
meta: { ...existingMeta, ...spillMeta, truncation: mergedTruncation },
|
|
3872
|
+
};
|
|
3508
3873
|
message.content = [
|
|
3509
3874
|
...message.content.filter((block): block is ImageContent => block.type === "image"),
|
|
3510
3875
|
{ type: "text", text: preview },
|
|
3511
3876
|
];
|
|
3512
3877
|
} catch (error) {
|
|
3878
|
+
const diagnostic = boundAgentBashArtifactSaveDiagnostic(error);
|
|
3879
|
+
replaceToolResultWithArtifactFailurePreview(message, fullText, maxInlineBytes, diagnostic);
|
|
3513
3880
|
logger.warn("Failed to spill oversized tool result before context admission", {
|
|
3514
3881
|
toolName: message.toolName,
|
|
3515
3882
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -3534,6 +3901,11 @@ export class AgentSession {
|
|
|
3534
3901
|
|
|
3535
3902
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
3536
3903
|
#handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
|
|
3904
|
+
const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
|
|
3905
|
+
if ((event.type === "agent_start" || event.type === "turn_start") && attemptScope) {
|
|
3906
|
+
this.#attemptRecordStore.register(attemptScope);
|
|
3907
|
+
this.#attemptRecordStore.establishClean(attemptScope);
|
|
3908
|
+
}
|
|
3537
3909
|
if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
|
|
3538
3910
|
if (
|
|
3539
3911
|
event.type === "tool_execution_start" ||
|
|
@@ -4048,6 +4420,7 @@ export class AgentSession {
|
|
|
4048
4420
|
generation: maintenanceGeneration,
|
|
4049
4421
|
skipCompactionCheck: true,
|
|
4050
4422
|
resourceRunId: activePromptHandle,
|
|
4423
|
+
maintenanceContinuation: true,
|
|
4051
4424
|
});
|
|
4052
4425
|
}
|
|
4053
4426
|
return;
|
|
@@ -4128,10 +4501,11 @@ export class AgentSession {
|
|
|
4128
4501
|
}
|
|
4129
4502
|
this.#resolveRetry();
|
|
4130
4503
|
|
|
4131
|
-
const compactionTask = this.#
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4504
|
+
const compactionTask = this.#schedulePostPromptTask(
|
|
4505
|
+
async () => {
|
|
4506
|
+
await this.#checkCompaction(msg, true, undefined, activePromptHandle);
|
|
4507
|
+
},
|
|
4508
|
+
{ resourceRunId: activePromptHandle },
|
|
4135
4509
|
);
|
|
4136
4510
|
await compactionTask;
|
|
4137
4511
|
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
@@ -4196,29 +4570,52 @@ export class AgentSession {
|
|
|
4196
4570
|
this.#postPromptTasksPromise = undefined;
|
|
4197
4571
|
}
|
|
4198
4572
|
|
|
4199
|
-
#trackPostPromptTask(task: Promise<void>,
|
|
4573
|
+
#trackPostPromptTask(task: Promise<void>, lease?: RunResourceProducerLease, leaseTask: Promise<void> = task): void {
|
|
4200
4574
|
this.#postPromptTasks.add(task);
|
|
4201
4575
|
this.#ensurePostPromptTasksPromise();
|
|
4202
|
-
if (
|
|
4203
|
-
|
|
4576
|
+
if (lease) {
|
|
4577
|
+
lease.track("post_prompt", "agent-session", leaseTask);
|
|
4578
|
+
void leaseTask.catch(() => {}).finally(() => lease.closeDiscovery());
|
|
4204
4579
|
}
|
|
4205
4580
|
void task
|
|
4206
4581
|
.catch(() => {})
|
|
4207
4582
|
.finally(() => {
|
|
4208
4583
|
this.#postPromptTasks.delete(task);
|
|
4209
|
-
if (this.#postPromptTasks.size === 0)
|
|
4210
|
-
this.#resolvePostPromptTasks();
|
|
4211
|
-
}
|
|
4584
|
+
if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
|
|
4212
4585
|
});
|
|
4213
4586
|
}
|
|
4214
4587
|
|
|
4215
4588
|
#schedulePostPromptTask(
|
|
4216
4589
|
task: (signal: AbortSignal) => Promise<void>,
|
|
4217
|
-
options?: {
|
|
4590
|
+
options?: {
|
|
4591
|
+
delayMs?: number;
|
|
4592
|
+
generation?: number;
|
|
4593
|
+
onSkip?: () => void;
|
|
4594
|
+
resourceRunId?: string;
|
|
4595
|
+
leaseTask?: Promise<void>;
|
|
4596
|
+
},
|
|
4218
4597
|
): Promise<void> {
|
|
4219
4598
|
const delayMs = options?.delayMs ?? 0;
|
|
4220
|
-
const
|
|
4221
|
-
const
|
|
4599
|
+
const resourceRunId = options?.resourceRunId;
|
|
4600
|
+
const contextualLease = this.#runResourceLeaseContext.getStore();
|
|
4601
|
+
const parentLease =
|
|
4602
|
+
resourceRunId && contextualLease?.resourceRunId === resourceRunId
|
|
4603
|
+
? contextualLease
|
|
4604
|
+
: resourceRunId
|
|
4605
|
+
? this.#postPromptLeases.get(resourceRunId)
|
|
4606
|
+
: undefined;
|
|
4607
|
+
const domain = resourceRunId ? this.#runCancellationDomains.lookup(resourceRunId) : undefined;
|
|
4608
|
+
const reservation = parentLease
|
|
4609
|
+
? parentLease.fork(parentLease.domain, "post_prompt", "agent-session")
|
|
4610
|
+
: resourceRunId && domain
|
|
4611
|
+
? this.agent.resourceLedger.reserveProducer(resourceRunId, domain, "post_prompt", "agent-session")
|
|
4612
|
+
: undefined;
|
|
4613
|
+
if (resourceRunId && !reservation?.ok) {
|
|
4614
|
+
options?.onSkip?.();
|
|
4615
|
+
return Promise.resolve();
|
|
4616
|
+
}
|
|
4617
|
+
const signal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
|
|
4618
|
+
const runScheduled = async () => {
|
|
4222
4619
|
if (delayMs > 0) {
|
|
4223
4620
|
try {
|
|
4224
4621
|
await scheduler.wait(delayMs, { signal });
|
|
@@ -4236,8 +4633,11 @@ export class AgentSession {
|
|
|
4236
4633
|
return;
|
|
4237
4634
|
}
|
|
4238
4635
|
await task(signal);
|
|
4239
|
-
}
|
|
4240
|
-
|
|
4636
|
+
};
|
|
4637
|
+
const scheduled = reservation?.ok
|
|
4638
|
+
? this.#runResourceLeaseContext.run(reservation.lease, runScheduled)
|
|
4639
|
+
: runScheduled();
|
|
4640
|
+
this.#trackPostPromptTask(scheduled, reservation?.ok ? reservation.lease : undefined, options?.leaseTask);
|
|
4241
4641
|
return scheduled;
|
|
4242
4642
|
}
|
|
4243
4643
|
|
|
@@ -4256,6 +4656,7 @@ export class AgentSession {
|
|
|
4256
4656
|
rescheduleOnBusy?: boolean;
|
|
4257
4657
|
onError?: (error: unknown) => void;
|
|
4258
4658
|
resourceRunId?: string;
|
|
4659
|
+
maintenanceContinuation?: boolean;
|
|
4259
4660
|
}): Promise<void> {
|
|
4260
4661
|
const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
|
|
4261
4662
|
? this.#reserveDeferredAgentEndForContinuation()
|
|
@@ -4277,12 +4678,13 @@ export class AgentSession {
|
|
|
4277
4678
|
options?.onError?.(error);
|
|
4278
4679
|
};
|
|
4279
4680
|
const scheduledGeneration = options?.generation;
|
|
4280
|
-
const
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4681
|
+
const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> => {
|
|
4682
|
+
const leaseSettlement = Promise.withResolvers<void>();
|
|
4683
|
+
const settleLease = () => leaseSettlement.resolve();
|
|
4684
|
+
return this.#schedulePostPromptTask(
|
|
4685
|
+
async scheduledSignal => {
|
|
4284
4686
|
const canContinue = (): boolean => {
|
|
4285
|
-
if (
|
|
4687
|
+
if (scheduledSignal.aborted || this.#isDisposed) {
|
|
4286
4688
|
skip("aborted_signal");
|
|
4287
4689
|
return false;
|
|
4288
4690
|
}
|
|
@@ -4300,13 +4702,16 @@ export class AgentSession {
|
|
|
4300
4702
|
}
|
|
4301
4703
|
return true;
|
|
4302
4704
|
};
|
|
4303
|
-
if (!canContinue())
|
|
4705
|
+
if (!canContinue()) {
|
|
4706
|
+
settleLease();
|
|
4707
|
+
return;
|
|
4708
|
+
}
|
|
4304
4709
|
try {
|
|
4305
4710
|
if (!options?.skipCompactionCheck) {
|
|
4306
4711
|
await this.#checkEstimatedContextBeforePrompt();
|
|
4307
4712
|
if (!canContinue()) return;
|
|
4308
4713
|
}
|
|
4309
|
-
if (
|
|
4714
|
+
if (scheduledSignal.aborted) {
|
|
4310
4715
|
skip("aborted_signal");
|
|
4311
4716
|
return;
|
|
4312
4717
|
}
|
|
@@ -4326,23 +4731,40 @@ export class AgentSession {
|
|
|
4326
4731
|
return;
|
|
4327
4732
|
}
|
|
4328
4733
|
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
4734
|
+
let predecessorAccepted = false;
|
|
4735
|
+
const releasePredecessor = () => {
|
|
4736
|
+
if (predecessorAccepted) return;
|
|
4737
|
+
predecessorAccepted = true;
|
|
4738
|
+
this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
|
|
4739
|
+
};
|
|
4329
4740
|
const startsQueuedSuccessor =
|
|
4330
4741
|
this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
|
|
4331
4742
|
try {
|
|
4332
4743
|
await this.agent.continue({
|
|
4333
4744
|
...this.#managedFallbackPromptOptions(),
|
|
4745
|
+
maintenanceContinuation: options?.maintenanceContinuation,
|
|
4334
4746
|
// Reset only after continue() has claimed the queued turn. Skipped or stale
|
|
4335
4747
|
// continuations retain predecessor accounting, and resetAttemptBudget keeps
|
|
4336
4748
|
// the sticky fallback cursor unchanged.
|
|
4337
|
-
onRunAccepted:
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4749
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
4750
|
+
this.#acceptRunHandle(handle);
|
|
4751
|
+
settleLease();
|
|
4752
|
+
releasePredecessor();
|
|
4753
|
+
if (startsQueuedSuccessor) {
|
|
4754
|
+
this.#defaultFallbackChain().resetAttemptBudget();
|
|
4755
|
+
this.#overflowMaintenanceAttempts = 0;
|
|
4756
|
+
}
|
|
4757
|
+
},
|
|
4343
4758
|
});
|
|
4759
|
+
releasePredecessor();
|
|
4344
4760
|
} catch (error) {
|
|
4345
|
-
if (
|
|
4761
|
+
if (
|
|
4762
|
+
options?.rescheduleOnBusy &&
|
|
4763
|
+
this.#isAgentBusyError(error) &&
|
|
4764
|
+
!predecessorAccepted &&
|
|
4765
|
+
!terminalized &&
|
|
4766
|
+
canContinue()
|
|
4767
|
+
) {
|
|
4346
4768
|
this.#retainDeferredAgentEndAfterContinuationBusy(predecessorAgentEndHold, predecessorAgentEnd);
|
|
4347
4769
|
logger.debug("agent.continue busy after scheduling; rescheduling", {
|
|
4348
4770
|
error: error.message,
|
|
@@ -4350,19 +4772,27 @@ export class AgentSession {
|
|
|
4350
4772
|
void scheduleAttempt(100);
|
|
4351
4773
|
return;
|
|
4352
4774
|
}
|
|
4353
|
-
|
|
4775
|
+
if (!predecessorAccepted)
|
|
4776
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
4354
4777
|
throw error;
|
|
4355
4778
|
}
|
|
4356
4779
|
} catch (error) {
|
|
4357
4780
|
fail(error);
|
|
4781
|
+
} finally {
|
|
4782
|
+
settleLease();
|
|
4358
4783
|
}
|
|
4359
4784
|
},
|
|
4360
4785
|
{
|
|
4361
4786
|
delayMs,
|
|
4362
|
-
onSkip: () =>
|
|
4787
|
+
onSkip: () => {
|
|
4788
|
+
settleLease();
|
|
4789
|
+
skip("aborted_signal");
|
|
4790
|
+
},
|
|
4363
4791
|
resourceRunId: options?.resourceRunId,
|
|
4792
|
+
leaseTask: leaseSettlement.promise,
|
|
4364
4793
|
},
|
|
4365
4794
|
);
|
|
4795
|
+
};
|
|
4366
4796
|
return scheduleAttempt();
|
|
4367
4797
|
}
|
|
4368
4798
|
|
|
@@ -4433,7 +4863,7 @@ export class AgentSession {
|
|
|
4433
4863
|
|
|
4434
4864
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
4435
4865
|
if (compactionSettings.autoContinue !== false) {
|
|
4436
|
-
this.#scheduleAutoContinuePrompt(generation, false);
|
|
4866
|
+
this.#scheduleAutoContinuePrompt(generation, false, resourceRunId);
|
|
4437
4867
|
return true;
|
|
4438
4868
|
}
|
|
4439
4869
|
|
|
@@ -4441,11 +4871,13 @@ export class AgentSession {
|
|
|
4441
4871
|
return false;
|
|
4442
4872
|
}
|
|
4443
4873
|
|
|
4444
|
-
#scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true): void {
|
|
4874
|
+
#scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true, resourceRunId?: string): void {
|
|
4445
4875
|
const predecessorAgentEndHold = this.#reserveDeferredAgentEndForContinuation();
|
|
4446
4876
|
const scheduledGeneration = generation;
|
|
4447
|
-
const
|
|
4448
|
-
|
|
4877
|
+
const continuationAuthorized = async (
|
|
4878
|
+
signal: AbortSignal,
|
|
4879
|
+
hasPendingNextTurnMessages = false,
|
|
4880
|
+
): Promise<boolean> => {
|
|
4449
4881
|
const snapshot = await this.#compactionStateSnapshot();
|
|
4450
4882
|
if (signal.aborted) {
|
|
4451
4883
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
@@ -4467,61 +4899,68 @@ export class AgentSession {
|
|
|
4467
4899
|
if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
|
|
4468
4900
|
return authorized;
|
|
4469
4901
|
};
|
|
4470
|
-
const
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4902
|
+
const scheduleAttempt = (delayMs = 0) => {
|
|
4903
|
+
let rescheduled = false;
|
|
4904
|
+
void this.#schedulePostPromptTask(
|
|
4905
|
+
async signal => {
|
|
4906
|
+
try {
|
|
4907
|
+
await Promise.resolve();
|
|
4908
|
+
if (signal.aborted) {
|
|
4909
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4910
|
+
return;
|
|
4911
|
+
}
|
|
4912
|
+
if (this.#promptGeneration !== scheduledGeneration) {
|
|
4913
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
4914
|
+
return;
|
|
4915
|
+
}
|
|
4916
|
+
if (!(await continuationAuthorized(signal))) return;
|
|
4917
|
+
await this.#promptWithMessage(
|
|
4918
|
+
{
|
|
4919
|
+
role: "developer",
|
|
4920
|
+
content: [{ type: "text", text: autoContinuePrompt }],
|
|
4921
|
+
attribution: "agent",
|
|
4922
|
+
timestamp: Date.now(),
|
|
4923
|
+
},
|
|
4924
|
+
autoContinuePrompt,
|
|
4925
|
+
{
|
|
4926
|
+
skipPostPromptRecoveryWait: true,
|
|
4927
|
+
skipCompactionCheck: true,
|
|
4928
|
+
predecessorAgentEndHold,
|
|
4929
|
+
onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
|
|
4930
|
+
continuationAuthorized(signal, hasPendingNextTurnMessages),
|
|
4931
|
+
},
|
|
4932
|
+
);
|
|
4933
|
+
} catch (error) {
|
|
4934
|
+
if (signal.aborted) {
|
|
4935
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4936
|
+
return;
|
|
4937
|
+
}
|
|
4938
|
+
if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
|
|
4939
|
+
logger.debug("Auto-compaction continuation busy; rescheduling", {
|
|
4940
|
+
source: "auto_continue_prompt",
|
|
4941
|
+
reason: "queue_drained",
|
|
4942
|
+
error: error.message,
|
|
4943
|
+
});
|
|
4944
|
+
rescheduled = true;
|
|
4945
|
+
scheduleAttempt(100);
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
4949
|
+
} finally {
|
|
4950
|
+
if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4951
|
+
}
|
|
4477
4952
|
},
|
|
4478
|
-
autoContinuePrompt,
|
|
4479
4953
|
{
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4954
|
+
delayMs,
|
|
4955
|
+
generation: scheduledGeneration,
|
|
4956
|
+
resourceRunId,
|
|
4957
|
+
onSkip: () => {
|
|
4958
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4959
|
+
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4960
|
+
},
|
|
4484
4961
|
},
|
|
4485
4962
|
);
|
|
4486
4963
|
};
|
|
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
4964
|
scheduleAttempt();
|
|
4526
4965
|
}
|
|
4527
4966
|
|
|
@@ -5266,163 +5705,200 @@ export class AgentSession {
|
|
|
5266
5705
|
event: AgentSessionEvent,
|
|
5267
5706
|
continueWhile?: () => boolean,
|
|
5268
5707
|
workerIntegrationSettled = false,
|
|
5708
|
+
scope?: AttemptScopeRef,
|
|
5269
5709
|
): Promise<void> {
|
|
5270
5710
|
if (event.type === "agent_end" && !workerIntegrationSettled) {
|
|
5271
5711
|
await this.#flushWorkerIntegrationForAgentEnd();
|
|
5272
5712
|
}
|
|
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",
|
|
5713
|
+
const deliveryScope = scope ?? (event as AgentSessionEvent & { scope?: AttemptScopeRef }).scope;
|
|
5714
|
+
const isTerminalAgentEnd =
|
|
5715
|
+
event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
|
|
5716
|
+
const finishAttempt = () => {
|
|
5717
|
+
if (!isTerminalAgentEnd) return;
|
|
5718
|
+
if (deliveryScope) this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
|
|
5719
|
+
this.#activeLogicalRunId = undefined;
|
|
5720
|
+
};
|
|
5721
|
+
if (!this.#extensionRunner) {
|
|
5722
|
+
finishAttempt();
|
|
5723
|
+
return;
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
try {
|
|
5727
|
+
if (event.type === "agent_start") {
|
|
5728
|
+
this.#turnIndex = 0;
|
|
5729
|
+
await this.#extensionRunner.emit({ type: "agent_start" }, undefined, deliveryScope);
|
|
5730
|
+
} else if (event.type === "agent_end") {
|
|
5731
|
+
await this.#extensionRunner.emit(
|
|
5732
|
+
{
|
|
5733
|
+
type: "agent_end",
|
|
5734
|
+
messages: event.messages,
|
|
5735
|
+
stopReason: event.stopReason,
|
|
5736
|
+
},
|
|
5737
|
+
undefined,
|
|
5738
|
+
deliveryScope,
|
|
5739
|
+
);
|
|
5740
|
+
} else if (event.type === "turn_start") {
|
|
5741
|
+
const hookEvent: TurnStartEvent = {
|
|
5742
|
+
type: "turn_start",
|
|
5743
|
+
turnIndex: this.#turnIndex,
|
|
5744
|
+
timestamp: Date.now(),
|
|
5745
|
+
};
|
|
5746
|
+
await this.#extensionRunner.emit(hookEvent, undefined, deliveryScope);
|
|
5747
|
+
} else if (event.type === "turn_end") {
|
|
5748
|
+
const hookEvent: TurnEndEvent = {
|
|
5749
|
+
type: "turn_end",
|
|
5750
|
+
turnIndex: this.#turnIndex,
|
|
5316
5751
|
message: event.message,
|
|
5317
|
-
|
|
5752
|
+
toolResults: event.toolResults,
|
|
5318
5753
|
};
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
} else if (event.
|
|
5322
|
-
const
|
|
5323
|
-
type: "
|
|
5754
|
+
await this.#extensionRunner.emit(hookEvent, undefined, deliveryScope);
|
|
5755
|
+
this.#turnIndex++;
|
|
5756
|
+
} else if (event.type === "message_start") {
|
|
5757
|
+
const extensionEvent: MessageStartEvent = {
|
|
5758
|
+
type: "message_start",
|
|
5324
5759
|
message: event.message,
|
|
5325
|
-
contentIndex: event.assistantMessageEvent.contentIndex,
|
|
5326
|
-
delta: event.assistantMessageEvent.delta,
|
|
5327
5760
|
};
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
type: "reasoning_summary_end",
|
|
5761
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5762
|
+
} else if (event.type === "message_update") {
|
|
5763
|
+
const extensionEvent: MessageUpdateEvent = {
|
|
5764
|
+
type: "message_update",
|
|
5333
5765
|
message: event.message,
|
|
5334
|
-
|
|
5335
|
-
content: event.assistantMessageEvent.content,
|
|
5766
|
+
assistantMessageEvent: event.assistantMessageEvent,
|
|
5336
5767
|
};
|
|
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
|
-
|
|
5768
|
+
await this.#extensionRunner.emit(extensionEvent, continueWhile, deliveryScope);
|
|
5769
|
+
if (continueWhile && !continueWhile()) return;
|
|
5770
|
+
if (event.assistantMessageEvent.type === "reasoning_summary_start") {
|
|
5771
|
+
const reasoningEvent: ReasoningSummaryStartEvent = {
|
|
5772
|
+
type: "reasoning_summary_start",
|
|
5773
|
+
message: event.message,
|
|
5774
|
+
contentIndex: event.assistantMessageEvent.contentIndex,
|
|
5775
|
+
};
|
|
5776
|
+
if (this.#extensionRunner.hasHandlers("reasoning_summary_start")) this.#markRetryReplayUnsafe();
|
|
5777
|
+
await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
|
|
5778
|
+
} else if (event.assistantMessageEvent.type === "reasoning_summary_delta") {
|
|
5779
|
+
const reasoningEvent: ReasoningSummaryDeltaEvent = {
|
|
5780
|
+
type: "reasoning_summary_delta",
|
|
5781
|
+
message: event.message,
|
|
5782
|
+
contentIndex: event.assistantMessageEvent.contentIndex,
|
|
5783
|
+
delta: event.assistantMessageEvent.delta,
|
|
5784
|
+
};
|
|
5785
|
+
if (this.#extensionRunner.hasHandlers("reasoning_summary_delta")) this.#markRetryReplayUnsafe();
|
|
5786
|
+
await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
|
|
5787
|
+
} else if (event.assistantMessageEvent.type === "reasoning_summary_end") {
|
|
5788
|
+
const reasoningEvent: ReasoningSummaryEndEvent = {
|
|
5789
|
+
type: "reasoning_summary_end",
|
|
5790
|
+
message: event.message,
|
|
5791
|
+
contentIndex: event.assistantMessageEvent.contentIndex,
|
|
5792
|
+
content: event.assistantMessageEvent.content,
|
|
5793
|
+
};
|
|
5794
|
+
if (this.#extensionRunner.hasHandlers("reasoning_summary_end")) this.#markRetryReplayUnsafe();
|
|
5795
|
+
await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
|
|
5796
|
+
}
|
|
5797
|
+
} else if (event.type === "message_end") {
|
|
5798
|
+
const extensionEvent: MessageEndEvent = {
|
|
5799
|
+
type: "message_end",
|
|
5800
|
+
message: event.message,
|
|
5801
|
+
};
|
|
5802
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5803
|
+
} else if (event.type === "tool_execution_start") {
|
|
5804
|
+
const extensionEvent: ToolExecutionStartEvent = {
|
|
5805
|
+
type: "tool_execution_start",
|
|
5806
|
+
toolCallId: event.toolCallId,
|
|
5807
|
+
toolName: event.toolName,
|
|
5808
|
+
args: event.args,
|
|
5809
|
+
intent: event.intent,
|
|
5810
|
+
};
|
|
5811
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5812
|
+
} else if (event.type === "tool_execution_update") {
|
|
5813
|
+
const extensionEvent: ToolExecutionUpdateEvent = {
|
|
5814
|
+
type: "tool_execution_update",
|
|
5815
|
+
toolCallId: event.toolCallId,
|
|
5816
|
+
toolName: event.toolName,
|
|
5817
|
+
args: event.args,
|
|
5818
|
+
partialResult: event.partialResult,
|
|
5819
|
+
};
|
|
5820
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5821
|
+
} else if (event.type === "tool_execution_end") {
|
|
5822
|
+
const extensionEvent: ToolExecutionEndEvent = {
|
|
5823
|
+
type: "tool_execution_end",
|
|
5824
|
+
toolCallId: event.toolCallId,
|
|
5825
|
+
toolName: event.toolName,
|
|
5826
|
+
result: event.result,
|
|
5827
|
+
isError: event.isError ?? false,
|
|
5828
|
+
};
|
|
5829
|
+
await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
|
|
5830
|
+
} else if (event.type === "auto_compaction_start") {
|
|
5831
|
+
await this.#extensionRunner.emit(
|
|
5832
|
+
{ type: "auto_compaction_start", reason: event.reason, action: event.action },
|
|
5833
|
+
undefined,
|
|
5834
|
+
deliveryScope,
|
|
5835
|
+
);
|
|
5836
|
+
} else if (event.type === "auto_compaction_end") {
|
|
5837
|
+
await this.#extensionRunner.emit(
|
|
5838
|
+
{
|
|
5839
|
+
type: "auto_compaction_end",
|
|
5840
|
+
action: event.action,
|
|
5841
|
+
result: event.result,
|
|
5842
|
+
aborted: event.aborted,
|
|
5843
|
+
willRetry: event.willRetry,
|
|
5844
|
+
errorMessage: event.errorMessage,
|
|
5845
|
+
skipped: event.skipped,
|
|
5846
|
+
continuationSkipReason: event.continuationSkipReason,
|
|
5847
|
+
},
|
|
5848
|
+
undefined,
|
|
5849
|
+
deliveryScope,
|
|
5850
|
+
);
|
|
5851
|
+
} else if (event.type === "auto_retry_start") {
|
|
5852
|
+
if (this.#extensionRunner.hasHandlers("auto_retry_start")) this.#markRetryReplayUnsafe();
|
|
5853
|
+
await this.#extensionRunner.emit(
|
|
5854
|
+
{
|
|
5855
|
+
type: "auto_retry_start",
|
|
5856
|
+
attempt: event.attempt,
|
|
5857
|
+
maxAttempts: event.maxAttempts,
|
|
5858
|
+
delayMs: event.delayMs,
|
|
5859
|
+
errorMessage: event.errorMessage,
|
|
5860
|
+
unbounded: event.unbounded,
|
|
5861
|
+
},
|
|
5862
|
+
undefined,
|
|
5863
|
+
deliveryScope,
|
|
5864
|
+
);
|
|
5865
|
+
} else if (event.type === "auto_retry_end") {
|
|
5866
|
+
await this.#extensionRunner.emit(
|
|
5867
|
+
{
|
|
5868
|
+
type: "auto_retry_end",
|
|
5869
|
+
success: event.success,
|
|
5870
|
+
attempt: event.attempt,
|
|
5871
|
+
finalError: event.finalError,
|
|
5872
|
+
},
|
|
5873
|
+
undefined,
|
|
5874
|
+
deliveryScope,
|
|
5875
|
+
);
|
|
5876
|
+
} else if (event.type === "ttsr_triggered") {
|
|
5877
|
+
await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules }, undefined, deliveryScope);
|
|
5878
|
+
} else if (event.type === "todo_reminder") {
|
|
5879
|
+
await this.#extensionRunner.emit(
|
|
5880
|
+
{
|
|
5881
|
+
type: "todo_reminder",
|
|
5882
|
+
todos: event.todos,
|
|
5883
|
+
attempt: event.attempt,
|
|
5884
|
+
maxAttempts: event.maxAttempts,
|
|
5885
|
+
},
|
|
5886
|
+
undefined,
|
|
5887
|
+
deliveryScope,
|
|
5888
|
+
);
|
|
5889
|
+
} else if (event.type === "goal_updated") {
|
|
5890
|
+
try {
|
|
5891
|
+
await this.#extensionRunner.emit(
|
|
5892
|
+
{ type: "goal_updated", goal: event.goal, state: event.state },
|
|
5893
|
+
undefined,
|
|
5894
|
+
deliveryScope,
|
|
5895
|
+
);
|
|
5896
|
+
} catch (error) {
|
|
5897
|
+
logger.warn("Goal updated extension hook failed", { error: String(error) });
|
|
5898
|
+
}
|
|
5425
5899
|
}
|
|
5900
|
+
} finally {
|
|
5901
|
+
finishAttempt();
|
|
5426
5902
|
}
|
|
5427
5903
|
}
|
|
5428
5904
|
|
|
@@ -5545,7 +6021,15 @@ export class AgentSession {
|
|
|
5545
6021
|
),
|
|
5546
6022
|
Bun.sleep(2_000).then(() => false),
|
|
5547
6023
|
]);
|
|
5548
|
-
if (!disposeIdleSettled)
|
|
6024
|
+
if (!disposeIdleSettled) {
|
|
6025
|
+
try {
|
|
6026
|
+
this.agent.forceAbort("Session disposed", this.#activeLogicalRunId);
|
|
6027
|
+
} catch {
|
|
6028
|
+
// AttemptScope handle may not be registered for sessions
|
|
6029
|
+
// that don't participate in the facility (e.g. test mocks).
|
|
6030
|
+
this.agent.forceAbort("Session disposed");
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
5549
6033
|
await admissionClosed;
|
|
5550
6034
|
await this.#agentEndPublicationPromise;
|
|
5551
6035
|
await this.#queuedExtensionEvents;
|
|
@@ -6929,7 +7413,8 @@ export class AgentSession {
|
|
|
6929
7413
|
this.#assertNoHandoffTransition();
|
|
6930
7414
|
await this.agent.continue({
|
|
6931
7415
|
...this.#managedFallbackPromptOptions(),
|
|
6932
|
-
onRunAccepted: () => {
|
|
7416
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
7417
|
+
this.#acceptRunHandle(handle);
|
|
6933
7418
|
if (hindsightRecall) hindsightState?.markRecallSnippetInjected(hindsightRecall);
|
|
6934
7419
|
},
|
|
6935
7420
|
});
|
|
@@ -6956,20 +7441,32 @@ export class AgentSession {
|
|
|
6956
7441
|
}
|
|
6957
7442
|
|
|
6958
7443
|
/** Convert session messages using the same pre-LLM pipeline as the active session. */
|
|
6959
|
-
async convertMessagesToLlm(
|
|
6960
|
-
|
|
7444
|
+
async convertMessagesToLlm(
|
|
7445
|
+
messages: AgentMessage[],
|
|
7446
|
+
signal?: AbortSignal,
|
|
7447
|
+
scope?: AttemptScope,
|
|
7448
|
+
): Promise<Message[]> {
|
|
7449
|
+
const transformedMessages =
|
|
7450
|
+
scope === undefined
|
|
7451
|
+
? await this.#transformContext(messages, signal)
|
|
7452
|
+
: await this.#transformContext(messages, signal, scope);
|
|
6961
7453
|
return await this.#convertToLlm(transformedMessages);
|
|
6962
7454
|
}
|
|
6963
7455
|
|
|
6964
7456
|
/** Apply session-level stream hooks to a direct side request. */
|
|
6965
|
-
prepareSimpleStreamOptions(
|
|
7457
|
+
prepareSimpleStreamOptions(
|
|
7458
|
+
options: SimpleStreamOptions,
|
|
7459
|
+
provider = "anthropic",
|
|
7460
|
+
scope?: AttemptScope,
|
|
7461
|
+
): SimpleStreamOptions {
|
|
6966
7462
|
const sessionOnPayload = this.#onPayload;
|
|
6967
7463
|
const sessionOnResponse = this.#onResponse;
|
|
6968
7464
|
const sessionMetadata = this.agent.metadataForProvider(provider);
|
|
6969
7465
|
const sessionOnSseEvent = this.#onSseEvent;
|
|
6970
|
-
if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent) return options;
|
|
7466
|
+
if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent && !scope) return options;
|
|
6971
7467
|
|
|
6972
7468
|
const preparedOptions: SimpleStreamOptions = { ...options };
|
|
7469
|
+
if (scope) preparedOptions.attemptScope = scope;
|
|
6973
7470
|
|
|
6974
7471
|
// Stamp session metadata (e.g. user_id={session_id}) onto direct-call requests so
|
|
6975
7472
|
// they share the same session bucket as Agent.prompt-routed requests on Anthropic
|
|
@@ -6983,10 +7480,10 @@ export class AgentSession {
|
|
|
6983
7480
|
preparedOptions.onPayload = sessionOnPayload;
|
|
6984
7481
|
} else {
|
|
6985
7482
|
const requestOnPayload = options.onPayload;
|
|
6986
|
-
preparedOptions.onPayload = async (payload, model) => {
|
|
6987
|
-
const sessionPayload = await sessionOnPayload(payload, model);
|
|
7483
|
+
preparedOptions.onPayload = async (payload, model, callbackScope) => {
|
|
7484
|
+
const sessionPayload = await sessionOnPayload(payload, model, callbackScope);
|
|
6988
7485
|
const sessionResolvedPayload = sessionPayload ?? payload;
|
|
6989
|
-
const requestPayload = await requestOnPayload(sessionResolvedPayload, model);
|
|
7486
|
+
const requestPayload = await requestOnPayload(sessionResolvedPayload, model, callbackScope);
|
|
6990
7487
|
return requestPayload ?? sessionResolvedPayload;
|
|
6991
7488
|
};
|
|
6992
7489
|
}
|
|
@@ -6997,9 +7494,9 @@ export class AgentSession {
|
|
|
6997
7494
|
preparedOptions.onResponse = sessionOnResponse;
|
|
6998
7495
|
} else {
|
|
6999
7496
|
const requestOnResponse = options.onResponse;
|
|
7000
|
-
preparedOptions.onResponse = async (response, model) => {
|
|
7001
|
-
await sessionOnResponse(response, model);
|
|
7002
|
-
await requestOnResponse(response, model);
|
|
7497
|
+
preparedOptions.onResponse = async (response, model, callbackScope) => {
|
|
7498
|
+
await sessionOnResponse(response, model, callbackScope);
|
|
7499
|
+
await requestOnResponse(response, model, callbackScope);
|
|
7003
7500
|
};
|
|
7004
7501
|
}
|
|
7005
7502
|
}
|
|
@@ -7009,9 +7506,9 @@ export class AgentSession {
|
|
|
7009
7506
|
preparedOptions.onSseEvent = sessionOnSseEvent;
|
|
7010
7507
|
} else {
|
|
7011
7508
|
const requestOnSseEvent = options.onSseEvent;
|
|
7012
|
-
preparedOptions.onSseEvent = (event, model) => {
|
|
7013
|
-
sessionOnSseEvent(event, model);
|
|
7014
|
-
requestOnSseEvent(event, model);
|
|
7509
|
+
preparedOptions.onSseEvent = (event, model, callbackScope) => {
|
|
7510
|
+
sessionOnSseEvent(event, model, callbackScope);
|
|
7511
|
+
requestOnSseEvent(event, model, callbackScope);
|
|
7015
7512
|
};
|
|
7016
7513
|
}
|
|
7017
7514
|
}
|
|
@@ -8058,7 +8555,7 @@ export class AgentSession {
|
|
|
8058
8555
|
skipPostPromptRecoveryWait?: boolean;
|
|
8059
8556
|
predecessorAgentEndHold?: symbol;
|
|
8060
8557
|
admissionLease?: SessionAdmissionLease;
|
|
8061
|
-
onRunAccepted?: () => void;
|
|
8558
|
+
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
8062
8559
|
onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
|
|
8063
8560
|
resetRetryReplaySafety?: boolean;
|
|
8064
8561
|
},
|
|
@@ -8284,8 +8781,9 @@ export class AgentSession {
|
|
|
8284
8781
|
const agentPromptOptions = {
|
|
8285
8782
|
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
8286
8783
|
...this.#managedFallbackPromptOptions(),
|
|
8287
|
-
onRunAccepted: () => {
|
|
8288
|
-
|
|
8784
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
8785
|
+
this.#acceptRunHandle(handle);
|
|
8786
|
+
options?.onRunAccepted?.(handle);
|
|
8289
8787
|
options?.admissionLease?.release();
|
|
8290
8788
|
if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
|
|
8291
8789
|
},
|
|
@@ -9408,7 +9906,14 @@ export class AgentSession {
|
|
|
9408
9906
|
]);
|
|
9409
9907
|
if (outcome.kind === "timeout") {
|
|
9410
9908
|
this.#abandonPostPromptTasks();
|
|
9411
|
-
this.agent.
|
|
9909
|
+
const forceAbortLogicalRunId = this.agent.currentManagedLogicalRunId ?? this.#activeLogicalRunId;
|
|
9910
|
+
try {
|
|
9911
|
+
if (forceAbortLogicalRunId !== undefined)
|
|
9912
|
+
this.agent.forceAbort("Abort cleanup timed out", forceAbortLogicalRunId);
|
|
9913
|
+
else this.agent.forceAbort("Abort cleanup timed out");
|
|
9914
|
+
} catch {
|
|
9915
|
+
this.agent.forceAbort("Abort cleanup timed out");
|
|
9916
|
+
}
|
|
9412
9917
|
this.emitNotice(
|
|
9413
9918
|
"warning",
|
|
9414
9919
|
"Abort cleanup timed out; forced session recovery. The previous provider stream or tool may still be unwinding in the background.",
|
|
@@ -9465,20 +9970,16 @@ export class AgentSession {
|
|
|
9465
9970
|
* Abort a specific active prompt and prove whether its tracked resources settled.
|
|
9466
9971
|
*/
|
|
9467
9972
|
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);
|
|
9973
|
+
const aborted = this.#runCancellationDomains.abort(handle);
|
|
9974
|
+
if (!aborted.ok) {
|
|
9975
|
+
if (aborted.reason === "quarantined") {
|
|
9976
|
+
return await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: 0 });
|
|
9977
|
+
}
|
|
9978
|
+
return { status: "unfenced", reason: "unknown_run", pending: [] };
|
|
9481
9979
|
}
|
|
9980
|
+
if (handle === this.agent.activeResourceRunId) this.agent.abort();
|
|
9981
|
+
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
9982
|
+
if (proof.status === "unfenced") this.agent.resourceLedger.quarantine(handle);
|
|
9482
9983
|
return proof;
|
|
9483
9984
|
}
|
|
9484
9985
|
|
|
@@ -11870,6 +12371,7 @@ export class AgentSession {
|
|
|
11870
12371
|
skipAbortedCheck = true,
|
|
11871
12372
|
onTerminalOverflowNoop?: () => void,
|
|
11872
12373
|
resourceRunId?: string,
|
|
12374
|
+
ownershipSignal?: AbortSignal,
|
|
11873
12375
|
): Promise<boolean> {
|
|
11874
12376
|
// Safety stops are terminal and must not trigger context maintenance.
|
|
11875
12377
|
if (
|
|
@@ -11913,7 +12415,8 @@ export class AgentSession {
|
|
|
11913
12415
|
}
|
|
11914
12416
|
|
|
11915
12417
|
// Try context promotion first - switch to a larger model and retry without compacting
|
|
11916
|
-
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
12418
|
+
const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
|
|
12419
|
+
if (ownershipSignal?.aborted) return false;
|
|
11917
12420
|
if (promoted) {
|
|
11918
12421
|
// Retry on the promoted (larger) model without compacting
|
|
11919
12422
|
this.#scheduleAgentContinue({
|
|
@@ -11938,6 +12441,7 @@ export class AgentSession {
|
|
|
11938
12441
|
}
|
|
11939
12442
|
},
|
|
11940
12443
|
resourceRunId,
|
|
12444
|
+
signal: ownershipSignal,
|
|
11941
12445
|
});
|
|
11942
12446
|
return "continuationScheduled" in status && status.continuationScheduled === true;
|
|
11943
12447
|
}
|
|
@@ -11972,14 +12476,16 @@ export class AgentSession {
|
|
|
11972
12476
|
autoCompactionOutputReserveTokens,
|
|
11973
12477
|
)
|
|
11974
12478
|
) {
|
|
11975
|
-
const pruneResult = await this.#pruneToolOutputs(
|
|
12479
|
+
const pruneResult = await this.#pruneToolOutputs(ownershipSignal, true);
|
|
12480
|
+
if (ownershipSignal?.aborted) return false;
|
|
11976
12481
|
if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
11977
12482
|
}
|
|
11978
12483
|
if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
11979
12484
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
11980
|
-
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
12485
|
+
const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
|
|
12486
|
+
if (ownershipSignal?.aborted) return false;
|
|
11981
12487
|
if (!promoted) {
|
|
11982
|
-
await this.#runAutoCompaction("threshold", false);
|
|
12488
|
+
await this.#runAutoCompaction("threshold", false, false, { resourceRunId, signal: ownershipSignal });
|
|
11983
12489
|
}
|
|
11984
12490
|
}
|
|
11985
12491
|
return true;
|
|
@@ -13358,7 +13864,7 @@ export class AgentSession {
|
|
|
13358
13864
|
});
|
|
13359
13865
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
13360
13866
|
if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
13361
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
13867
|
+
this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
|
|
13362
13868
|
}
|
|
13363
13869
|
|
|
13364
13870
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
@@ -13422,7 +13928,7 @@ export class AgentSession {
|
|
|
13422
13928
|
}
|
|
13423
13929
|
const overflowContinuationScheduled =
|
|
13424
13930
|
!overflowNoopWouldReplay && willRetry && !continuationSkipReason
|
|
13425
|
-
? await this.#scheduleOverflowRetryContinuation(generation)
|
|
13931
|
+
? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
|
|
13426
13932
|
: false;
|
|
13427
13933
|
await this.#emitSessionEvent({
|
|
13428
13934
|
type: "auto_compaction_end",
|
|
@@ -13445,6 +13951,7 @@ export class AgentSession {
|
|
|
13445
13951
|
rescheduleOnBusy: true,
|
|
13446
13952
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
13447
13953
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
13954
|
+
resourceRunId: options?.resourceRunId,
|
|
13448
13955
|
});
|
|
13449
13956
|
return { kind: "skipped", continuationScheduled: true };
|
|
13450
13957
|
}
|
|
@@ -13465,9 +13972,10 @@ export class AgentSession {
|
|
|
13465
13972
|
rescheduleOnBusy: true,
|
|
13466
13973
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
13467
13974
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
13975
|
+
resourceRunId: options?.resourceRunId,
|
|
13468
13976
|
});
|
|
13469
13977
|
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
13470
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
13978
|
+
this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
|
|
13471
13979
|
}
|
|
13472
13980
|
return { kind: "skipped" };
|
|
13473
13981
|
}
|
|
@@ -13694,7 +14202,9 @@ export class AgentSession {
|
|
|
13694
14202
|
this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
|
|
13695
14203
|
}
|
|
13696
14204
|
const overflowContinuationScheduled =
|
|
13697
|
-
willRetry && !continuationSkipReason
|
|
14205
|
+
willRetry && !continuationSkipReason
|
|
14206
|
+
? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
|
|
14207
|
+
: false;
|
|
13698
14208
|
await this.#emitSessionEvent({
|
|
13699
14209
|
type: "auto_compaction_end",
|
|
13700
14210
|
action,
|
|
@@ -13721,9 +14231,10 @@ export class AgentSession {
|
|
|
13721
14231
|
rescheduleOnBusy: true,
|
|
13722
14232
|
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
13723
14233
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
14234
|
+
resourceRunId: options?.resourceRunId,
|
|
13724
14235
|
});
|
|
13725
14236
|
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
13726
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
14237
|
+
this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
|
|
13727
14238
|
}
|
|
13728
14239
|
return { kind: "compacted" };
|
|
13729
14240
|
} catch (error) {
|
|
@@ -13788,7 +14299,7 @@ export class AgentSession {
|
|
|
13788
14299
|
|
|
13789
14300
|
#isKimiCodeFirstEventTimeout(message: AssistantMessage): boolean {
|
|
13790
14301
|
if (message.stopReason !== "error" || message.provider !== "kimi-code") return false;
|
|
13791
|
-
const errorMessage = message.errorMessage ?? "";
|
|
14302
|
+
const errorMessage = terminalProviderFirstEventTimeoutIdentity(message.errorMessage ?? "");
|
|
13792
14303
|
if (message.api === "anthropic-messages") {
|
|
13793
14304
|
return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["anthropic-messages"].has(errorMessage);
|
|
13794
14305
|
}
|
|
@@ -13815,7 +14326,8 @@ export class AgentSession {
|
|
|
13815
14326
|
#isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
|
|
13816
14327
|
if (message.stopReason !== "error" || message.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
|
|
13817
14328
|
const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(message.api);
|
|
13818
|
-
|
|
14329
|
+
const errorMessage = terminalProviderFirstEventTimeoutIdentity(message.errorMessage ?? "");
|
|
14330
|
+
return timeoutMessages?.has(errorMessage) ?? false;
|
|
13819
14331
|
}
|
|
13820
14332
|
|
|
13821
14333
|
#isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
|
|
@@ -14003,6 +14515,7 @@ export class AgentSession {
|
|
|
14003
14515
|
if (this.#isTerminalProviderFirstEventTimeout(message)) {
|
|
14004
14516
|
return "terminal";
|
|
14005
14517
|
}
|
|
14518
|
+
if (isMessageOnlyFirstEventTimeout(message)) return "first_event_timeout";
|
|
14006
14519
|
// Legacy providers that have not yet stamped the typed timeout fact retain
|
|
14007
14520
|
// their existing bounded ollama-cloud behavior.
|
|
14008
14521
|
if (this.#isFirstEventTimeoutErrorMessage(err) && this.#shouldFailClosedOnFirstEventTimeout(message)) {
|
|
@@ -14173,20 +14686,37 @@ export class AgentSession {
|
|
|
14173
14686
|
return {
|
|
14174
14687
|
type: "maintenance",
|
|
14175
14688
|
continuation: async ownership => {
|
|
14176
|
-
if (!ownership.isCurrent()) return;
|
|
14177
|
-
|
|
14178
|
-
const
|
|
14179
|
-
|
|
14180
|
-
|
|
14689
|
+
if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
14690
|
+
const resourceRunId = String(ownership.logicalRunId);
|
|
14691
|
+
const previousLease = this.#postPromptLeases.get(resourceRunId);
|
|
14692
|
+
this.#postPromptLeases.set(resourceRunId, ownership.lease);
|
|
14693
|
+
try {
|
|
14694
|
+
let terminalized = false;
|
|
14695
|
+
const successorScheduled = await this.#checkCompaction(
|
|
14696
|
+
outcome.message,
|
|
14697
|
+
true,
|
|
14698
|
+
() => {
|
|
14699
|
+
terminalized = true;
|
|
14700
|
+
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
14701
|
+
stopReason: "error",
|
|
14702
|
+
messages: [outcome.message],
|
|
14703
|
+
});
|
|
14704
|
+
},
|
|
14705
|
+
resourceRunId,
|
|
14706
|
+
ownership.lease.signal,
|
|
14707
|
+
);
|
|
14708
|
+
if (terminalized || successorScheduled || !ownership.isCurrent() || ownership.lease.signal.aborted)
|
|
14709
|
+
return;
|
|
14710
|
+
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
14181
14711
|
stopReason: "error",
|
|
14182
14712
|
messages: [outcome.message],
|
|
14183
14713
|
});
|
|
14184
|
-
}
|
|
14185
|
-
|
|
14186
|
-
|
|
14187
|
-
|
|
14188
|
-
|
|
14189
|
-
}
|
|
14714
|
+
} finally {
|
|
14715
|
+
if (this.#postPromptLeases.get(resourceRunId) === ownership.lease) {
|
|
14716
|
+
if (previousLease) this.#postPromptLeases.set(resourceRunId, previousLease);
|
|
14717
|
+
else this.#postPromptLeases.delete(resourceRunId);
|
|
14718
|
+
}
|
|
14719
|
+
}
|
|
14190
14720
|
},
|
|
14191
14721
|
};
|
|
14192
14722
|
}
|
|
@@ -14378,17 +14908,20 @@ export class AgentSession {
|
|
|
14378
14908
|
// retry.enabled=false always surfaces immediately, matching the explicit
|
|
14379
14909
|
// user opt-out.
|
|
14380
14910
|
if (!managedFallback && !retrySettings.enabled) return false;
|
|
14381
|
-
const classification =
|
|
14382
|
-
|
|
14911
|
+
const classification = this.#classifyErrorForRetry(message);
|
|
14912
|
+
const firstEventTimeout = classification === "first_event_timeout";
|
|
14913
|
+
if (!managedFallback && firstEventTimeout) {
|
|
14383
14914
|
this.#firstEventTimeoutRetryStartedAt ??= Date.now();
|
|
14384
14915
|
}
|
|
14385
14916
|
// First-event timeouts are replay-safe only before any model progress,
|
|
14386
14917
|
// abort, or extension/provider lifecycle participation.
|
|
14387
|
-
if (
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14918
|
+
if (firstEventTimeout && (!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))) {
|
|
14919
|
+
return managedOutcome
|
|
14920
|
+
? {
|
|
14921
|
+
type: "terminal",
|
|
14922
|
+
terminal: { stopReason: "error", messages: [message] },
|
|
14923
|
+
}
|
|
14924
|
+
: false;
|
|
14392
14925
|
}
|
|
14393
14926
|
const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
14394
14927
|
if (!trigger) {
|
|
@@ -14415,12 +14948,14 @@ export class AgentSession {
|
|
|
14415
14948
|
// capacity-overload event.
|
|
14416
14949
|
if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
|
|
14417
14950
|
const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
|
|
14951
|
+
const bareDefaultWrappedFirstEventTimeout = isBareDefaultWrappedFirstEventTimeout(message);
|
|
14418
14952
|
// Content-free Codex overload is replay-safe by the same reasoning as
|
|
14419
14953
|
// credential rotation: no partial output means no observable state.
|
|
14420
14954
|
const canReplayCodexOverload = bareDefaultCodexOverload;
|
|
14421
14955
|
if (
|
|
14422
14956
|
(!canReplayCodexOverload &&
|
|
14423
14957
|
!this.#isTypedFirstEventTimeout(message) &&
|
|
14958
|
+
!bareDefaultWrappedFirstEventTimeout &&
|
|
14424
14959
|
(hasBareDefaultRetryDisqualifyingFacts(message) ||
|
|
14425
14960
|
(classification !== "transient" && classification !== "first_event_timeout") ||
|
|
14426
14961
|
!BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
|
|
@@ -14429,7 +14964,7 @@ export class AgentSession {
|
|
|
14429
14964
|
return false;
|
|
14430
14965
|
}
|
|
14431
14966
|
}
|
|
14432
|
-
const legacyUnbounded = classification === "transient";
|
|
14967
|
+
const legacyUnbounded = !managedFallback && classification === "transient";
|
|
14433
14968
|
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
14434
14969
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
14435
14970
|
let outcome = managedFallback
|
|
@@ -14496,7 +15031,7 @@ export class AgentSession {
|
|
|
14496
15031
|
: this.#fallbackExhaustionError(controller);
|
|
14497
15032
|
this.emitNotice("error", errorMessage, "fallback");
|
|
14498
15033
|
if (managedOutcome && ownership) {
|
|
14499
|
-
this.agent.requestRunTerminal(ownership.logicalRunId, {
|
|
15034
|
+
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
14500
15035
|
stopReason: "exhausted",
|
|
14501
15036
|
messages: [this.#managedFallbackExhaustionMessage(message, errorMessage)],
|
|
14502
15037
|
});
|
|
@@ -14522,12 +15057,11 @@ export class AgentSession {
|
|
|
14522
15057
|
await this.#emitSessionEvent({
|
|
14523
15058
|
type: "auto_retry_start",
|
|
14524
15059
|
attempt: this.#retryAttempt,
|
|
14525
|
-
maxAttempts:
|
|
14526
|
-
|
|
15060
|
+
maxAttempts: managedFallback
|
|
15061
|
+
? controller.maxAttempts
|
|
15062
|
+
: firstEventTimeout
|
|
14527
15063
|
? retrySettings.maxRetries + 1
|
|
14528
|
-
:
|
|
14529
|
-
? controller.maxAttempts
|
|
14530
|
-
: retrySettings.maxRetries,
|
|
15064
|
+
: retrySettings.maxRetries,
|
|
14531
15065
|
delayMs,
|
|
14532
15066
|
errorMessage,
|
|
14533
15067
|
unbounded: managedFallback ? false : legacyUnbounded,
|
|
@@ -14548,8 +15082,13 @@ export class AgentSession {
|
|
|
14548
15082
|
}
|
|
14549
15083
|
this.agent.replaceMessages(messages.slice(0, end));
|
|
14550
15084
|
}
|
|
15085
|
+
const retrySignal = ownership
|
|
15086
|
+
? AbortSignal.any([retryAbortController.signal, ownership.lease.signal])
|
|
15087
|
+
: retryAbortController.signal;
|
|
15088
|
+
const ownershipCancelled = () =>
|
|
15089
|
+
Boolean(ownership && (!ownership.isCurrent() || ownership.lease.signal.aborted));
|
|
14551
15090
|
try {
|
|
14552
|
-
await scheduler.wait(delayMs, { signal:
|
|
15091
|
+
await scheduler.wait(delayMs, { signal: retrySignal });
|
|
14553
15092
|
} catch {
|
|
14554
15093
|
if (this.#retryAbortController !== retryAbortController) return;
|
|
14555
15094
|
this.#retryAbortController = undefined;
|
|
@@ -14568,7 +15107,10 @@ export class AgentSession {
|
|
|
14568
15107
|
return;
|
|
14569
15108
|
}
|
|
14570
15109
|
}
|
|
14571
|
-
if (
|
|
15110
|
+
if (
|
|
15111
|
+
(retryAbortController.signal.aborted || ownershipCancelled()) &&
|
|
15112
|
+
!(this.#retryNowRequested && !ownershipCancelled())
|
|
15113
|
+
) {
|
|
14572
15114
|
if (this.#retryAbortController !== retryAbortController) return;
|
|
14573
15115
|
this.#retryAbortController = undefined;
|
|
14574
15116
|
const attempt = this.#retryAttempt;
|
|
@@ -14588,7 +15130,7 @@ export class AgentSession {
|
|
|
14588
15130
|
if (managedOutcome) {
|
|
14589
15131
|
try {
|
|
14590
15132
|
await this.#checkEstimatedContextBeforePrompt();
|
|
14591
|
-
if (
|
|
15133
|
+
if (ownershipCancelled()) {
|
|
14592
15134
|
const attempt = this.#retryAttempt;
|
|
14593
15135
|
this.#retryAttempt = 0;
|
|
14594
15136
|
await this.#emitSessionEvent({
|
|
@@ -14619,10 +15161,13 @@ export class AgentSession {
|
|
|
14619
15161
|
}
|
|
14620
15162
|
}
|
|
14621
15163
|
|
|
15164
|
+
const resourceRunId = this.#runResourceLeaseContext.getStore()?.resourceRunId;
|
|
14622
15165
|
this.#scheduleAgentContinue({
|
|
14623
15166
|
delayMs: 1,
|
|
14624
15167
|
generation,
|
|
14625
15168
|
allowDuringCancelAndSubmit: true,
|
|
15169
|
+
suppressPredecessorAgentEnd: resourceRunId !== undefined,
|
|
15170
|
+
resourceRunId,
|
|
14626
15171
|
onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
|
|
14627
15172
|
onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
|
|
14628
15173
|
});
|
|
@@ -14677,7 +15222,11 @@ export class AgentSession {
|
|
|
14677
15222
|
|
|
14678
15223
|
async #promptAgentWithIdleRetry(
|
|
14679
15224
|
messages: AgentMessage[],
|
|
14680
|
-
options?: {
|
|
15225
|
+
options?: {
|
|
15226
|
+
toolChoice?: ToolChoice;
|
|
15227
|
+
fallbackManaged?: boolean;
|
|
15228
|
+
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
15229
|
+
},
|
|
14681
15230
|
predecessorAgentEndHold?: symbol,
|
|
14682
15231
|
): Promise<void> {
|
|
14683
15232
|
const deadline = Date.now() + 30_000;
|
|
@@ -14690,6 +15239,7 @@ export class AgentSession {
|
|
|
14690
15239
|
continuationHold = undefined;
|
|
14691
15240
|
try {
|
|
14692
15241
|
await this.agent.prompt(messages, options);
|
|
15242
|
+
this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
|
|
14693
15243
|
return;
|
|
14694
15244
|
} catch (error) {
|
|
14695
15245
|
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
@@ -15441,19 +15991,30 @@ export class AgentSession {
|
|
|
15441
15991
|
!rosterClaim || this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
15442
15992
|
const prependMessagesValid = () => rosterClaimIsCurrent() && args.prependMessagesValid?.() !== false;
|
|
15443
15993
|
const rosterMessage = !callerOwnsRosterClaim && rosterClaimIsCurrent() ? rosterClaim?.message : undefined;
|
|
15994
|
+
let sideAttempt: { scope: AttemptScope; dispose: () => void } | undefined;
|
|
15444
15995
|
try {
|
|
15445
15996
|
const model = this.model;
|
|
15446
15997
|
if (!model) throw new Error("No active model on session");
|
|
15447
15998
|
const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, this.sessionId), args.signal);
|
|
15448
15999
|
if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
16000
|
+
sideAttempt = this.agent.mintSideAttemptScope();
|
|
16001
|
+
const sideScope = sideAttempt.scope;
|
|
16002
|
+
this.#attemptRecordStore.register(sideScope);
|
|
16003
|
+
this.#attemptRecordStore.establishClean(sideScope);
|
|
15449
16004
|
const prependMessages = prependMessagesValid()
|
|
15450
16005
|
? [...(rosterMessage ? [rosterMessage] : []), ...(args.prependMessages ?? [])]
|
|
15451
16006
|
: undefined;
|
|
15452
16007
|
let snapshot = this.#buildEphemeralSnapshot(args.promptText, prependMessages);
|
|
15453
|
-
let llmMessages = await awaitEphemeralAbort(
|
|
16008
|
+
let llmMessages = await awaitEphemeralAbort(
|
|
16009
|
+
this.convertMessagesToLlm(snapshot, args.signal, sideScope),
|
|
16010
|
+
args.signal,
|
|
16011
|
+
);
|
|
15454
16012
|
if (prependMessages && !prependMessagesValid()) {
|
|
15455
16013
|
snapshot = this.#buildEphemeralSnapshot(args.promptText);
|
|
15456
|
-
llmMessages = await awaitEphemeralAbort(
|
|
16014
|
+
llmMessages = await awaitEphemeralAbort(
|
|
16015
|
+
this.convertMessagesToLlm(snapshot, args.signal, sideScope),
|
|
16016
|
+
args.signal,
|
|
16017
|
+
);
|
|
15457
16018
|
}
|
|
15458
16019
|
const context: Context = { systemPrompt: this.systemPrompt, messages: llmMessages, tools: [] };
|
|
15459
16020
|
const ephemeralSessionId = crypto.randomUUID();
|
|
@@ -15474,6 +16035,7 @@ export class AgentSession {
|
|
|
15474
16035
|
toolChoice: "none",
|
|
15475
16036
|
},
|
|
15476
16037
|
model.provider,
|
|
16038
|
+
sideScope,
|
|
15477
16039
|
);
|
|
15478
16040
|
args.signal?.throwIfAborted();
|
|
15479
16041
|
let replyText = "";
|
|
@@ -15501,6 +16063,10 @@ export class AgentSession {
|
|
|
15501
16063
|
}
|
|
15502
16064
|
return { replyText: replyText.trim(), assistantMessage };
|
|
15503
16065
|
} finally {
|
|
16066
|
+
if (sideAttempt) {
|
|
16067
|
+
sideAttempt.dispose();
|
|
16068
|
+
this.#attemptRecordStore.retire(sideAttempt.scope);
|
|
16069
|
+
}
|
|
15504
16070
|
if (!callerOwnsRosterClaim && rosterClaim) this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
15505
16071
|
}
|
|
15506
16072
|
}
|
|
@@ -15514,8 +16080,15 @@ export class AgentSession {
|
|
|
15514
16080
|
}
|
|
15515
16081
|
const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, credentialSessionId), args.signal);
|
|
15516
16082
|
if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
16083
|
+
const sideAttempt = this.agent.mintSideAttemptScope();
|
|
16084
|
+
this.#attemptRecordStore.register(sideAttempt.scope);
|
|
16085
|
+
this.#attemptRecordStore.establishClean(sideAttempt.scope);
|
|
15517
16086
|
const scope = args.turn.scope;
|
|
15518
|
-
if (!scope)
|
|
16087
|
+
if (!scope) {
|
|
16088
|
+
sideAttempt.dispose();
|
|
16089
|
+
this.#attemptRecordStore.retire(sideAttempt.scope);
|
|
16090
|
+
throw new Error("The /btw conversation scope was scrubbed.");
|
|
16091
|
+
}
|
|
15519
16092
|
const messages = scope.messages.map(message => ({
|
|
15520
16093
|
role: message.role,
|
|
15521
16094
|
content: [{ type: "text" as const, text: message.text }],
|
|
@@ -15548,6 +16121,7 @@ export class AgentSession {
|
|
|
15548
16121
|
requestMaxRetries: 0,
|
|
15549
16122
|
streamMaxRetries: 0,
|
|
15550
16123
|
streamFirstEventTimeoutMs: 0,
|
|
16124
|
+
attemptScope: sideAttempt.scope,
|
|
15551
16125
|
};
|
|
15552
16126
|
const iterator = streamSimple(scope.model, context, options)[Symbol.asyncIterator]();
|
|
15553
16127
|
let replyText = "";
|
|
@@ -15607,6 +16181,8 @@ export class AgentSession {
|
|
|
15607
16181
|
if (idleTimer) clearTimeout(idleTimer);
|
|
15608
16182
|
clearTimeout(totalTimer);
|
|
15609
16183
|
void iterator.return?.().catch(() => undefined);
|
|
16184
|
+
sideAttempt.dispose();
|
|
16185
|
+
this.#attemptRecordStore.retire(sideAttempt.scope);
|
|
15610
16186
|
}
|
|
15611
16187
|
}
|
|
15612
16188
|
|