@ixo/oracle-runtime-workers 0.13.0 → 0.15.0
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/README.md +32 -10
- package/package.json +13 -8
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +267 -0
- package/src/channels/contract.ts +137 -0
- package/src/channels/turns.ts +270 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +170 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +1883 -264
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +504 -108
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +529 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* run one attempt, the latest checkpoint of a session, and what to do when
|
|
10
10
|
* a run ends (deliver a task result, close a Matrix card).
|
|
11
11
|
*/
|
|
12
|
+
import type { ReplyPlan } from '../delivery/types';
|
|
12
13
|
import type { Logger } from '../plugin-api/types';
|
|
13
14
|
import {
|
|
14
15
|
framesOfSegments,
|
|
@@ -19,7 +20,10 @@ import {
|
|
|
19
20
|
import {
|
|
20
21
|
attemptSeqBase,
|
|
21
22
|
decideRecovery,
|
|
23
|
+
patchRunRecord,
|
|
24
|
+
runHasCheckpointed,
|
|
22
25
|
type RunDurabilityConfig,
|
|
26
|
+
type RunPatch,
|
|
23
27
|
type RunRecord,
|
|
24
28
|
type RunStore,
|
|
25
29
|
} from './run-store';
|
|
@@ -29,11 +33,28 @@ export interface RunOutcome {
|
|
|
29
33
|
status: 'finished' | 'aborted' | 'interrupted' | 'failed';
|
|
30
34
|
/** The reply (the last assistant message, or the streamed text so far). */
|
|
31
35
|
text: string;
|
|
36
|
+
/** A finished chat-surface run: the reply as its surface delivers it. */
|
|
37
|
+
plan?: ReplyPlan;
|
|
32
38
|
messageId?: string;
|
|
33
39
|
toolCalls?: Array<{ name: string; status: 'done' | 'error' }>;
|
|
34
40
|
error?: unknown;
|
|
41
|
+
/** The run's usage snapshot (JSON), stored with the terminal status. */
|
|
42
|
+
usage?: string;
|
|
35
43
|
}
|
|
36
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Thrown by `runAttempt` before anything executed, when the attempt cannot
|
|
47
|
+
* start for a transient reason outside the run (an authority it must consult
|
|
48
|
+
* is unreachable). The coordinator schedules the same attempt again with the
|
|
49
|
+
* recovery backoff instead of failing the run; after the recovery cap the
|
|
50
|
+
* run fails with this error.
|
|
51
|
+
*/
|
|
52
|
+
export class RunAttemptDeferred extends Error {
|
|
53
|
+
override readonly name = 'RunAttemptDeferred';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type AttemptSource = 'begin' | 'dequeue' | 'recovery';
|
|
57
|
+
|
|
37
58
|
export interface LiveRun {
|
|
38
59
|
readonly runId: string;
|
|
39
60
|
readonly sessionId: string;
|
|
@@ -48,6 +69,13 @@ export interface LiveRun {
|
|
|
48
69
|
resolve: (outcome: RunOutcome) => void;
|
|
49
70
|
/** An attempt is executing right now. */
|
|
50
71
|
attemptInFlight: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* What started the current attempt: `begin` (the message that was just
|
|
74
|
+
* admitted, started at once), `dequeue` (it waited behind another run of
|
|
75
|
+
* its session) or `recovery` (a retry after a reset or a deferral).
|
|
76
|
+
* Unset on a run built outside the coordinator.
|
|
77
|
+
*/
|
|
78
|
+
attemptSource?: AttemptSource;
|
|
51
79
|
}
|
|
52
80
|
|
|
53
81
|
/** The slice of `RunStore` the coordinator drives (tests supply an in-memory one). */
|
|
@@ -59,7 +87,7 @@ export type RunCoordinatorStore = Pick<
|
|
|
59
87
|
| 'listActive'
|
|
60
88
|
| 'readSegments'
|
|
61
89
|
| 'appendSegment'
|
|
62
|
-
| '
|
|
90
|
+
| 'close'
|
|
63
91
|
>;
|
|
64
92
|
|
|
65
93
|
export interface RunCoordinatorHost {
|
|
@@ -80,13 +108,15 @@ export interface RunCoordinatorHost {
|
|
|
80
108
|
checkpointIdOf: (sessionId: string) => Promise<string | null>;
|
|
81
109
|
/** After a run reached a terminal state and its row was closed. */
|
|
82
110
|
onRunEnded?: (record: RunRecord, outcome: RunOutcome) => Promise<void>;
|
|
111
|
+
/** How long a superseded or aborted attempt gets to wind down (default 5 s). */
|
|
112
|
+
supersedeGraceMs?: number;
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
export interface BeginRunInput {
|
|
86
116
|
runId: string;
|
|
87
117
|
sessionId: string;
|
|
88
118
|
requestId: string;
|
|
89
|
-
client: 'portal' | 'matrix';
|
|
119
|
+
client: 'portal' | 'matrix' | 'channel';
|
|
90
120
|
/** JSON the host needs to rebuild the attempt (see `StoredRunRequest`). */
|
|
91
121
|
request: string;
|
|
92
122
|
multitask: 'interrupt' | 'enqueue';
|
|
@@ -96,6 +126,43 @@ export interface BeginRunInput {
|
|
|
96
126
|
/** How long a superseded run gets to wind down before the next one starts. */
|
|
97
127
|
const SUPERSEDE_GRACE_MS = 5_000;
|
|
98
128
|
|
|
129
|
+
/** Shown when a run ends failed without its attempt having said why. */
|
|
130
|
+
const RUN_FAILED_MESSAGE =
|
|
131
|
+
'Something went wrong while answering. Please try again.';
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A `begin` that has not opened its run yet. A later `interrupt` message of
|
|
135
|
+
* the same session (or `abortAllForSession`) supersedes it: it then records
|
|
136
|
+
* its run as aborted and never starts an attempt.
|
|
137
|
+
*/
|
|
138
|
+
interface PendingBegin {
|
|
139
|
+
superseded: boolean;
|
|
140
|
+
/** Resolves when superseded, so a wait on the previous run is cut short. */
|
|
141
|
+
readonly woken: Promise<void>;
|
|
142
|
+
wake: () => void;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Wait for `promise`, at most `ms`, and never leave the timer behind (a
|
|
147
|
+
* pending timer keeps a Durable Object resident).
|
|
148
|
+
*/
|
|
149
|
+
async function withinGrace(
|
|
150
|
+
ms: number,
|
|
151
|
+
...promises: Array<Promise<unknown>>
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
154
|
+
try {
|
|
155
|
+
await Promise.race([
|
|
156
|
+
...promises,
|
|
157
|
+
new Promise<void>((resolve) => {
|
|
158
|
+
timer = setTimeout(resolve, ms);
|
|
159
|
+
}),
|
|
160
|
+
]);
|
|
161
|
+
} finally {
|
|
162
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
99
166
|
export interface JoinResult {
|
|
100
167
|
record: RunRecord;
|
|
101
168
|
/** Frames after the cursor, from the segments and the live tail. */
|
|
@@ -107,12 +174,27 @@ export interface JoinResult {
|
|
|
107
174
|
export class RunCoordinator {
|
|
108
175
|
private readonly live = new Map<string, LiveRun>();
|
|
109
176
|
|
|
177
|
+
/** Per session: the tail of the chain `begin` calls run on, one at a time. */
|
|
178
|
+
private readonly sessionTurns = new Map<string, Promise<void>>();
|
|
179
|
+
|
|
180
|
+
/** Per session: the `begin` calls that have not opened their run yet. */
|
|
181
|
+
private readonly pendingBegins = new Map<string, Set<PendingBegin>>();
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Runs queued only because the run their message superseded outlived the
|
|
185
|
+
* grace: the message is the session's current one, so Stop ends it too.
|
|
186
|
+
*/
|
|
187
|
+
private readonly queuedBehindSuperseded = new WeakSet<LiveRun>();
|
|
188
|
+
|
|
110
189
|
private keepAliveArmedUntil = 0;
|
|
111
190
|
|
|
112
191
|
private readonly now: () => number;
|
|
113
192
|
|
|
193
|
+
private readonly graceMs: number;
|
|
194
|
+
|
|
114
195
|
constructor(private readonly host: RunCoordinatorHost) {
|
|
115
196
|
this.now = host.now ?? (() => Date.now());
|
|
197
|
+
this.graceMs = host.supersedeGraceMs ?? SUPERSEDE_GRACE_MS;
|
|
116
198
|
}
|
|
117
199
|
|
|
118
200
|
// ── lookups ─────────────────────────────────────────────────────────────
|
|
@@ -185,56 +267,154 @@ export class RunCoordinator {
|
|
|
185
267
|
* Record a run and start it, or queue it behind the session's active run
|
|
186
268
|
* under the `enqueue` rule. Under `interrupt` (the default) the active run
|
|
187
269
|
* of the session is aborted first — and any run queued behind it.
|
|
270
|
+
*
|
|
271
|
+
* The `begin` calls of one session take turns, so each decides on the
|
|
272
|
+
* session as the previous one left it, and at most one attempt executes
|
|
273
|
+
* per session. A message superseded by a later `interrupt` message while
|
|
274
|
+
* it waited for its turn (or for the run it supersedes to wind down) is
|
|
275
|
+
* recorded and closed as aborted without an attempt; its `live` comes
|
|
276
|
+
* back with the buffer already closed on its `done` frame. A run that
|
|
277
|
+
* outlives the supersede grace keeps executing; the new message is then
|
|
278
|
+
* queued behind it instead of running beside it.
|
|
188
279
|
*/
|
|
189
280
|
async begin(
|
|
190
281
|
input: BeginRunInput,
|
|
191
282
|
): Promise<{ live: LiveRun; queued: boolean }> {
|
|
192
|
-
const
|
|
283
|
+
const { sessionId } = input;
|
|
284
|
+
if (input.multitask === 'interrupt') this.supersedePending(sessionId);
|
|
285
|
+
let wake!: () => void;
|
|
286
|
+
const woken = new Promise<void>((resolve) => {
|
|
287
|
+
wake = resolve;
|
|
288
|
+
});
|
|
289
|
+
const ticket: PendingBegin = { superseded: false, woken, wake };
|
|
290
|
+
let pending = this.pendingBegins.get(sessionId);
|
|
291
|
+
if (!pending) {
|
|
292
|
+
pending = new Set();
|
|
293
|
+
this.pendingBegins.set(sessionId, pending);
|
|
294
|
+
}
|
|
295
|
+
pending.add(ticket);
|
|
296
|
+
try {
|
|
297
|
+
return await this.inSessionTurn(sessionId, () =>
|
|
298
|
+
this.beginInTurn(input, ticket),
|
|
299
|
+
);
|
|
300
|
+
} finally {
|
|
301
|
+
pending.delete(ticket);
|
|
302
|
+
if (pending.size === 0 && this.pendingBegins.get(sessionId) === pending)
|
|
303
|
+
this.pendingBegins.delete(sessionId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Run `fn` after every earlier `inSessionTurn` call of the session settled. */
|
|
308
|
+
private inSessionTurn<T>(
|
|
309
|
+
sessionId: string,
|
|
310
|
+
fn: () => Promise<T>,
|
|
311
|
+
): Promise<T> {
|
|
312
|
+
const previous = this.sessionTurns.get(sessionId) ?? Promise.resolve();
|
|
313
|
+
const result = previous.then(fn);
|
|
314
|
+
const tail = result.then(
|
|
315
|
+
() => undefined,
|
|
316
|
+
() => undefined,
|
|
317
|
+
);
|
|
318
|
+
this.sessionTurns.set(sessionId, tail);
|
|
319
|
+
void tail.then(() => {
|
|
320
|
+
if (this.sessionTurns.get(sessionId) === tail)
|
|
321
|
+
this.sessionTurns.delete(sessionId);
|
|
322
|
+
});
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Supersede the session's waiting `begin` calls; whether there were any. */
|
|
327
|
+
private supersedePending(sessionId: string): boolean {
|
|
328
|
+
let any = false;
|
|
329
|
+
for (const ticket of this.pendingBegins.get(sessionId) ?? []) {
|
|
330
|
+
ticket.superseded = true;
|
|
331
|
+
ticket.wake();
|
|
332
|
+
any = true;
|
|
333
|
+
}
|
|
334
|
+
return any;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private async beginInTurn(
|
|
338
|
+
input: BeginRunInput,
|
|
339
|
+
ticket: PendingBegin,
|
|
340
|
+
): Promise<{ live: LiveRun; queued: boolean }> {
|
|
341
|
+
const { sessionId } = input;
|
|
193
342
|
let queued = false;
|
|
343
|
+
let behindSuperseded = false;
|
|
344
|
+
const active = ticket.superseded
|
|
345
|
+
? undefined
|
|
346
|
+
: this.activeForSession(sessionId);
|
|
194
347
|
if (active) {
|
|
195
348
|
if (input.multitask === 'enqueue') {
|
|
196
349
|
queued = true;
|
|
197
350
|
} else {
|
|
198
351
|
this.host.log.log(
|
|
199
|
-
`[runs] ${
|
|
352
|
+
`[runs] ${sessionId}: new message supersedes ${active.runId} (${active.record.status})`,
|
|
200
353
|
);
|
|
201
354
|
for (const run of [...this.live.values()])
|
|
202
|
-
if (
|
|
203
|
-
run.sessionId === input.sessionId &&
|
|
204
|
-
run.record.status === 'queued'
|
|
205
|
-
)
|
|
355
|
+
if (run.sessionId === sessionId && run.record.status === 'queued')
|
|
206
356
|
await this.cancelQueued(run, 'superseded');
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
357
|
+
const running = this.activeForSession(sessionId);
|
|
358
|
+
if (running) {
|
|
359
|
+
this.abortRun(running, 'superseded');
|
|
360
|
+
await withinGrace(this.graceMs, running.done, ticket.woken);
|
|
361
|
+
}
|
|
362
|
+
// Still winding down past the grace: wait behind it.
|
|
363
|
+
if (!ticket.superseded && this.activeForSession(sessionId)) {
|
|
364
|
+
queued = true;
|
|
365
|
+
behindSuperseded = true;
|
|
213
366
|
}
|
|
214
367
|
}
|
|
215
368
|
}
|
|
216
|
-
const checkpointId = await this.host.checkpointIdOf(
|
|
217
|
-
await this.host.store.create({
|
|
369
|
+
const checkpointId = await this.host.checkpointIdOf(sessionId);
|
|
370
|
+
const record = await this.host.store.create({
|
|
218
371
|
runId: input.runId,
|
|
219
|
-
sessionId
|
|
372
|
+
sessionId,
|
|
220
373
|
requestId: input.requestId,
|
|
221
374
|
client: input.client,
|
|
222
|
-
status: queued ? 'queued' : 'running',
|
|
375
|
+
status: queued || ticket.superseded ? 'queued' : 'running',
|
|
223
376
|
request: input.request,
|
|
224
377
|
checkpointId,
|
|
225
378
|
taskRunId: input.taskRunId ?? null,
|
|
226
379
|
instanceId: this.host.instanceId,
|
|
227
380
|
});
|
|
228
|
-
|
|
381
|
+
// No await from this check to the attempt's start: a supersede that
|
|
382
|
+
// lands later finds the run open and aborts it like any other.
|
|
383
|
+
if (ticket.superseded) {
|
|
384
|
+
const live = this.open({ ...record, status: 'queued' });
|
|
385
|
+
await this.cancelQueued(live, 'superseded');
|
|
386
|
+
return { live, queued: false };
|
|
387
|
+
}
|
|
229
388
|
const live = this.open(record);
|
|
230
|
-
if (!queued)
|
|
231
|
-
|
|
389
|
+
if (!queued) {
|
|
390
|
+
void this.startAttempt(live, false, 'begin');
|
|
391
|
+
return { live, queued };
|
|
392
|
+
}
|
|
393
|
+
if (behindSuperseded) this.queuedBehindSuperseded.add(live);
|
|
394
|
+
// The run ahead may have ended during the awaits above, before this
|
|
395
|
+
// one was open to be dequeued.
|
|
396
|
+
await this.startNextQueued(sessionId);
|
|
397
|
+
return { live, queued: live.record.status === 'queued' };
|
|
232
398
|
}
|
|
233
399
|
|
|
234
|
-
/**
|
|
400
|
+
/**
|
|
401
|
+
* Stop the session's current message (`POST /messages/abort`): its active
|
|
402
|
+
* run, and a message still waiting in `begin` or queued behind the run it
|
|
403
|
+
* superseded. Runs enqueued behind the active one still start after it.
|
|
404
|
+
*/
|
|
235
405
|
abortSession(sessionId: string): boolean {
|
|
406
|
+
let stopped = this.supersedePending(sessionId);
|
|
407
|
+
for (const run of [...this.live.values()])
|
|
408
|
+
if (
|
|
409
|
+
run.sessionId === sessionId &&
|
|
410
|
+
run.record.status === 'queued' &&
|
|
411
|
+
this.queuedBehindSuperseded.has(run)
|
|
412
|
+
) {
|
|
413
|
+
void this.cancelQueued(run, 'aborted');
|
|
414
|
+
stopped = true;
|
|
415
|
+
}
|
|
236
416
|
const active = this.activeForSession(sessionId);
|
|
237
|
-
if (!active) return
|
|
417
|
+
if (!active) return stopped;
|
|
238
418
|
if (active.record.status === 'queued') {
|
|
239
419
|
void this.cancelQueued(active, 'aborted');
|
|
240
420
|
return true;
|
|
@@ -243,6 +423,31 @@ export class RunCoordinator {
|
|
|
243
423
|
return true;
|
|
244
424
|
}
|
|
245
425
|
|
|
426
|
+
/**
|
|
427
|
+
* End every run of the session: the executing attempt is aborted, queued
|
|
428
|
+
* runs and runs waiting for a recovery attempt are closed, and messages
|
|
429
|
+
* still waiting in `begin` are closed without an attempt — each ends
|
|
430
|
+
* `aborted`, with its `done` frame. Resolves once the executing attempt
|
|
431
|
+
* ended, or after the supersede grace. Safe when nothing runs.
|
|
432
|
+
*/
|
|
433
|
+
async abortAllForSession(sessionId: string): Promise<void> {
|
|
434
|
+
this.supersedePending(sessionId);
|
|
435
|
+
const runs = [...this.live.values()].filter(
|
|
436
|
+
(run) => run.sessionId === sessionId,
|
|
437
|
+
);
|
|
438
|
+
if (runs.length === 0) return;
|
|
439
|
+
this.host.log.log(
|
|
440
|
+
`[runs] ${sessionId}: aborting all ${runs.length} run(s)`,
|
|
441
|
+
);
|
|
442
|
+
// Queued runs first, so an attempt that ends below dequeues none.
|
|
443
|
+
for (const run of runs)
|
|
444
|
+
if (run.record.status === 'queued')
|
|
445
|
+
void this.cancelQueued(run, 'aborted');
|
|
446
|
+
for (const run of runs)
|
|
447
|
+
if (run.record.status !== 'queued') this.abortRun(run, 'aborted');
|
|
448
|
+
await withinGrace(this.graceMs, Promise.all(runs.map((run) => run.done)));
|
|
449
|
+
}
|
|
450
|
+
|
|
246
451
|
private abortRun(live: LiveRun, reason: 'aborted' | 'superseded'): void {
|
|
247
452
|
live.abort.abort(new Error(`run aborted (${reason})`));
|
|
248
453
|
if (!live.attemptInFlight) {
|
|
@@ -260,7 +465,6 @@ export class RunCoordinator {
|
|
|
260
465
|
reason: 'aborted' | 'superseded',
|
|
261
466
|
): Promise<void> {
|
|
262
467
|
this.host.log.log(`[runs] ${live.runId} ${reason} while queued`);
|
|
263
|
-
live.buffer.push('done', { runId: live.runId, aborted: true });
|
|
264
468
|
await this.finalize(live, { status: 'aborted', text: '' });
|
|
265
469
|
}
|
|
266
470
|
|
|
@@ -276,17 +480,31 @@ export class RunCoordinator {
|
|
|
276
480
|
const record =
|
|
277
481
|
this.live.get(runId)?.record ?? (await this.host.store.get(runId));
|
|
278
482
|
if (!record) return undefined;
|
|
279
|
-
const
|
|
483
|
+
const before = this.live.get(runId);
|
|
280
484
|
const segments = await this.host.store.readSegments(runId, after);
|
|
281
485
|
const replay = framesOfSegments(segments, after);
|
|
282
486
|
const lastReplayed =
|
|
283
487
|
replay.length > 0 ? replay[replay.length - 1]!.seq : after;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
488
|
+
const live = this.live.get(runId);
|
|
489
|
+
if (!live) {
|
|
490
|
+
if (!before) return { record, replay };
|
|
491
|
+
// The run ended during the read: answer as for an ended run, from its
|
|
492
|
+
// closed row (the caller's trailer carries the final status and text),
|
|
493
|
+
// with the frames after the cursor that the read or the buffer still
|
|
494
|
+
// held, less the `done` the trailer replaces.
|
|
495
|
+
await before.done;
|
|
496
|
+
replay.push(...before.buffer.tailAfter(lastReplayed));
|
|
497
|
+
return {
|
|
498
|
+
record: before.record,
|
|
499
|
+
replay: replay.filter((frame) => frame.event !== 'done'),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// The tail includes the frames whose segment write is still pending
|
|
503
|
+
// (the read above may have been queued before it). No await between
|
|
504
|
+
// reading the tail and subscribing (the caller subscribes synchronously
|
|
505
|
+
// on the returned buffer), so no frame slips between the two.
|
|
288
506
|
replay.push(...live.buffer.tailAfter(lastReplayed));
|
|
289
|
-
return { record, replay, buffer: live.buffer };
|
|
507
|
+
return { record: live.record, replay, buffer: live.buffer };
|
|
290
508
|
}
|
|
291
509
|
|
|
292
510
|
// ── attempts ────────────────────────────────────────────────────────────
|
|
@@ -307,6 +525,7 @@ export class RunCoordinator {
|
|
|
307
525
|
done,
|
|
308
526
|
resolve,
|
|
309
527
|
attemptInFlight: false,
|
|
528
|
+
attemptSource: 'begin',
|
|
310
529
|
};
|
|
311
530
|
this.live.set(record.runId, live);
|
|
312
531
|
return live;
|
|
@@ -326,40 +545,129 @@ export class RunCoordinator {
|
|
|
326
545
|
});
|
|
327
546
|
}
|
|
328
547
|
|
|
329
|
-
private async startAttempt(
|
|
548
|
+
private async startAttempt(
|
|
549
|
+
live: LiveRun,
|
|
550
|
+
resumed: boolean,
|
|
551
|
+
source: AttemptSource,
|
|
552
|
+
): Promise<void> {
|
|
330
553
|
live.attemptInFlight = true;
|
|
554
|
+
live.attemptSource = source;
|
|
331
555
|
this.touchKeepAlive();
|
|
332
556
|
let outcome: RunOutcome;
|
|
333
557
|
try {
|
|
334
558
|
outcome = await this.host.runAttempt(live, resumed);
|
|
335
559
|
} catch (error) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
560
|
+
if (error instanceof RunAttemptDeferred) {
|
|
561
|
+
const deferred = await this.defer(live, error).catch(
|
|
562
|
+
(storeError: unknown) => {
|
|
563
|
+
this.host.log.error(
|
|
564
|
+
`[runs] ${live.runId}: could not reschedule the attempt: ${storeError instanceof Error ? storeError.message : String(storeError)}`,
|
|
565
|
+
);
|
|
566
|
+
return false;
|
|
567
|
+
},
|
|
568
|
+
);
|
|
569
|
+
if (deferred) return;
|
|
570
|
+
} else {
|
|
571
|
+
this.host.log.error(
|
|
572
|
+
`[runs] ${live.runId}: attempt crashed: ${error instanceof Error ? error.message : String(error)}`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
outcome = live.abort.signal.aborted
|
|
576
|
+
? { status: 'aborted', text: live.continuation ?? '' }
|
|
577
|
+
: { status: 'failed', text: live.continuation ?? '', error };
|
|
344
578
|
}
|
|
345
579
|
live.attemptInFlight = false;
|
|
346
580
|
await this.finalize(live, outcome);
|
|
347
581
|
}
|
|
348
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Put a deferred attempt back on the recovery schedule. The attempt did
|
|
585
|
+
* not touch the checkpoint, so every deferral counts against the recovery
|
|
586
|
+
* cap; `false` when the cap is spent (or the run was aborted meanwhile)
|
|
587
|
+
* and the caller closes the run instead. The attempt stays in flight
|
|
588
|
+
* until the run is rescheduled, so an abort meanwhile only signals.
|
|
589
|
+
*/
|
|
590
|
+
private async defer(
|
|
591
|
+
live: LiveRun,
|
|
592
|
+
error: RunAttemptDeferred,
|
|
593
|
+
): Promise<boolean> {
|
|
594
|
+
if (live.abort.signal.aborted) return false;
|
|
595
|
+
const decision = decideRecovery(
|
|
596
|
+
live.record,
|
|
597
|
+
live.record.checkpointId,
|
|
598
|
+
this.now(),
|
|
599
|
+
this.host.config,
|
|
600
|
+
);
|
|
601
|
+
if (decision.action === 'interrupt') {
|
|
602
|
+
this.host.log.warn(
|
|
603
|
+
`[runs] ${live.runId}: deferred ${decision.attempts} time(s) (${error.message}); failing the run`,
|
|
604
|
+
);
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
await this.write(live, {
|
|
608
|
+
status: 'recovering',
|
|
609
|
+
attempts: decision.attempts,
|
|
610
|
+
nextAttemptAt: decision.at,
|
|
611
|
+
});
|
|
612
|
+
if (live.abort.signal.aborted) return false;
|
|
613
|
+
live.attemptInFlight = false;
|
|
614
|
+
this.host.log.warn(
|
|
615
|
+
`[runs] ${live.runId}: attempt deferred (${error.message}); retry ${decision.attempts} in ${Math.round((decision.at - this.now()) / 1000)} s`,
|
|
616
|
+
);
|
|
617
|
+
this.host.requestAlarm(decision.at);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* The frames a run's subscribers need to finish, when its attempt did not
|
|
623
|
+
* push them (it crashed, was never started, or was closed while waiting):
|
|
624
|
+
* an `error` frame for a failure, then `done`.
|
|
625
|
+
*/
|
|
626
|
+
private pushClosingFrames(live: LiveRun, outcome: RunOutcome): void {
|
|
627
|
+
if (live.buffer.isClosed || live.buffer.hasDone) return;
|
|
628
|
+
if (outcome.status === 'failed')
|
|
629
|
+
live.buffer.push('error', {
|
|
630
|
+
error: RUN_FAILED_MESSAGE,
|
|
631
|
+
kind: 'unknown',
|
|
632
|
+
source: 'platform',
|
|
633
|
+
retryable: true,
|
|
634
|
+
sessionId: live.sessionId,
|
|
635
|
+
requestId: live.requestId,
|
|
636
|
+
runId: live.runId,
|
|
637
|
+
timestamp: new Date(this.now()).toISOString(),
|
|
638
|
+
});
|
|
639
|
+
live.buffer.push('done', {
|
|
640
|
+
runId: live.runId,
|
|
641
|
+
...(outcome.messageId ? { messageId: outcome.messageId } : {}),
|
|
642
|
+
...(outcome.status === 'finished' ? {} : { [outcome.status]: true }),
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Write `patch` to the run's row and keep `live.record` in step with it. */
|
|
647
|
+
private async write(live: LiveRun, patch: RunPatch): Promise<void> {
|
|
648
|
+
const updatedAt = await this.host.store.update(live.runId, patch);
|
|
649
|
+
live.record = patchRunRecord(live.record, patch, updatedAt);
|
|
650
|
+
}
|
|
651
|
+
|
|
349
652
|
private async finalize(live: LiveRun, outcome: RunOutcome): Promise<void> {
|
|
350
|
-
if (
|
|
653
|
+
if (this.live.get(live.runId) !== live) return;
|
|
351
654
|
this.live.delete(live.runId);
|
|
352
655
|
const { runId } = live;
|
|
353
656
|
try {
|
|
657
|
+
this.pushClosingFrames(live, outcome);
|
|
354
658
|
await live.buffer.close();
|
|
355
659
|
const partialText =
|
|
356
660
|
outcome.status === 'finished'
|
|
357
|
-
?
|
|
661
|
+
? live.record.client === 'channel'
|
|
662
|
+
? outcome.text
|
|
663
|
+
: null
|
|
358
664
|
: outcome.text ||
|
|
359
665
|
partialTextOf([
|
|
360
666
|
...framesOfSegments(await this.host.store.readSegments(runId)),
|
|
361
667
|
]);
|
|
362
|
-
|
|
668
|
+
// One transaction: the terminal row, and the cutover (the reply, or
|
|
669
|
+
// the partial text, is in the row and the transcript now).
|
|
670
|
+
const patch: RunPatch = {
|
|
363
671
|
status: outcome.status,
|
|
364
672
|
lastSeq: live.buffer.lastSeq,
|
|
365
673
|
messageId: outcome.messageId ?? null,
|
|
@@ -371,10 +679,10 @@ export class RunCoordinator {
|
|
|
371
679
|
? outcome.error.message
|
|
372
680
|
: String(outcome.error),
|
|
373
681
|
nextAttemptAt: null,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
await this.host.store.
|
|
377
|
-
live.record = (
|
|
682
|
+
...(outcome.usage !== undefined ? { usage: outcome.usage } : {}),
|
|
683
|
+
};
|
|
684
|
+
const updatedAt = await this.host.store.close(runId, patch);
|
|
685
|
+
live.record = patchRunRecord(live.record, patch, updatedAt);
|
|
378
686
|
this.host.log.log(
|
|
379
687
|
`[runs] ${runId} ${outcome.status} (${live.record.client}, session ${live.sessionId}, ${live.buffer.lastSeq} frames)`,
|
|
380
688
|
);
|
|
@@ -399,19 +707,46 @@ export class RunCoordinator {
|
|
|
399
707
|
// queue waiting: the session's turns stay strictly ordered.
|
|
400
708
|
const active = this.activeForSession(sessionId);
|
|
401
709
|
if (active && active.record.status !== 'queued') return;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
710
|
+
let run: LiveRun | undefined;
|
|
711
|
+
for (const candidate of this.live.values())
|
|
712
|
+
if (
|
|
713
|
+
candidate.sessionId === sessionId &&
|
|
714
|
+
candidate.record.status === 'queued'
|
|
715
|
+
) {
|
|
716
|
+
run = candidate;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
if (!run) return;
|
|
720
|
+
// Taken before the awaits below: the session is busy from here on, and
|
|
721
|
+
// an abort meanwhile only signals (the run is closed here).
|
|
722
|
+
run.attemptInFlight = true;
|
|
723
|
+
run.record = { ...run.record, status: 'running' };
|
|
724
|
+
try {
|
|
725
|
+
// The run's input is written from this checkpoint on: its start.
|
|
726
|
+
const checkpointId = await this.host.checkpointIdOf(sessionId);
|
|
727
|
+
await this.write(run, {
|
|
406
728
|
status: 'running',
|
|
407
729
|
instanceId: this.host.instanceId,
|
|
408
|
-
checkpointId
|
|
730
|
+
checkpointId,
|
|
731
|
+
startCheckpointId: checkpointId,
|
|
409
732
|
});
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
733
|
+
} catch (error) {
|
|
734
|
+
run.attemptInFlight = false;
|
|
735
|
+
await this.finalize(
|
|
736
|
+
run,
|
|
737
|
+
run.abort.signal.aborted
|
|
738
|
+
? { status: 'aborted', text: '' }
|
|
739
|
+
: { status: 'failed', text: '', error },
|
|
740
|
+
);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (run.abort.signal.aborted) {
|
|
744
|
+
run.attemptInFlight = false;
|
|
745
|
+
await this.finalize(run, { status: 'aborted', text: '' });
|
|
413
746
|
return;
|
|
414
747
|
}
|
|
748
|
+
this.host.log.log(`[runs] ${run.runId}: dequeued for ${sessionId}`);
|
|
749
|
+
void this.startAttempt(run, false, 'dequeue');
|
|
415
750
|
}
|
|
416
751
|
|
|
417
752
|
// ── recovery ────────────────────────────────────────────────────────────
|
|
@@ -454,8 +789,13 @@ export class RunCoordinator {
|
|
|
454
789
|
this.host.log.warn(
|
|
455
790
|
`[runs] ${record.runId}: ${decision.attempts} recovery attempt(s) without progress; closing as interrupted`,
|
|
456
791
|
);
|
|
457
|
-
await this.host.store.update(record.runId, {
|
|
458
|
-
|
|
792
|
+
const updatedAt = await this.host.store.update(record.runId, {
|
|
793
|
+
generation,
|
|
794
|
+
});
|
|
795
|
+
const live = this.open(
|
|
796
|
+
patchRunRecord(record, { generation }, updatedAt),
|
|
797
|
+
startSeq,
|
|
798
|
+
);
|
|
459
799
|
live.continuation = partialTextOf(frames) || null;
|
|
460
800
|
live.buffer.push('error', {
|
|
461
801
|
error:
|
|
@@ -474,17 +814,24 @@ export class RunCoordinator {
|
|
|
474
814
|
});
|
|
475
815
|
continue;
|
|
476
816
|
}
|
|
477
|
-
|
|
817
|
+
const patch: RunPatch = {
|
|
478
818
|
status: 'recovering',
|
|
479
819
|
attempts: decision.attempts,
|
|
480
820
|
generation,
|
|
481
821
|
nextAttemptAt: decision.at,
|
|
482
822
|
checkpointId: current,
|
|
483
823
|
instanceId: this.host.instanceId,
|
|
484
|
-
}
|
|
485
|
-
const
|
|
486
|
-
const live = this.open(
|
|
487
|
-
|
|
824
|
+
};
|
|
825
|
+
const updatedAt = await this.host.store.update(record.runId, patch);
|
|
826
|
+
const live = this.open(
|
|
827
|
+
patchRunRecord(record, patch, updatedAt),
|
|
828
|
+
startSeq,
|
|
829
|
+
);
|
|
830
|
+
// A run the graph never checkpointed is retried fresh (resumeDue):
|
|
831
|
+
// there is no reply to continue from.
|
|
832
|
+
live.continuation = runHasCheckpointed(record, current)
|
|
833
|
+
? partialTextOf(frames) || null
|
|
834
|
+
: null;
|
|
488
835
|
this.host.log.log(
|
|
489
836
|
`[runs] ${record.runId}: attempt ${decision.attempts} in ${Math.round((decision.at - now) / 1000)} s (${frames.length} frames restored)`,
|
|
490
837
|
);
|
|
@@ -504,27 +851,55 @@ export class RunCoordinator {
|
|
|
504
851
|
if (run.record.status !== 'recovering' || run.attemptInFlight) continue;
|
|
505
852
|
if (run.record.nextAttemptAt !== null && run.record.nextAttemptAt > now)
|
|
506
853
|
continue;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
854
|
+
// Taken before the awaits below: an abort meanwhile only signals, and
|
|
855
|
+
// the run is closed here instead of being attempted.
|
|
856
|
+
run.attemptInFlight = true;
|
|
857
|
+
let resumed: boolean;
|
|
858
|
+
try {
|
|
859
|
+
await this.write(run, { status: 'running', nextAttemptAt: null });
|
|
860
|
+
// Resume only when the graph persisted something for this run;
|
|
861
|
+
// otherwise it never saw the input and the attempt runs fresh with
|
|
862
|
+
// it. Derived from the stored start checkpoint, so it holds across a
|
|
863
|
+
// restart (recoverOrphans) as well as for a deferral in this instance.
|
|
864
|
+
resumed = runHasCheckpointed(
|
|
865
|
+
run.record,
|
|
866
|
+
await this.host.checkpointIdOf(run.sessionId),
|
|
867
|
+
);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
run.attemptInFlight = false;
|
|
870
|
+
if (!run.abort.signal.aborted) throw error;
|
|
871
|
+
await this.finalize(run, {
|
|
872
|
+
status: 'aborted',
|
|
873
|
+
text: run.continuation ?? '',
|
|
874
|
+
});
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (run.abort.signal.aborted) {
|
|
878
|
+
run.attemptInFlight = false;
|
|
879
|
+
await this.finalize(run, {
|
|
880
|
+
status: 'aborted',
|
|
881
|
+
text: run.continuation ?? '',
|
|
882
|
+
});
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (!resumed) run.continuation = null;
|
|
512
886
|
// `partialLength` is the reply text this attempt continues from (all
|
|
513
887
|
// packed `message` frames). A client that received more than that
|
|
514
888
|
// before the reset truncates to it, so the continuation never
|
|
515
889
|
// duplicates text the runtime lost with the unpacked tail.
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
890
|
+
if (resumed)
|
|
891
|
+
run.buffer.push('run', {
|
|
892
|
+
runId: run.runId,
|
|
893
|
+
sessionId: run.sessionId,
|
|
894
|
+
requestId: run.requestId,
|
|
895
|
+
resumed: true,
|
|
896
|
+
attempt: run.record.attempts,
|
|
897
|
+
partialLength: run.continuation?.length ?? 0,
|
|
898
|
+
});
|
|
524
899
|
this.host.log.log(
|
|
525
|
-
`[runs] ${run.runId}: resuming (attempt ${run.record.attempts})`,
|
|
900
|
+
`[runs] ${run.runId}: ${resumed ? 'resuming' : 'retrying'} (attempt ${run.record.attempts})`,
|
|
526
901
|
);
|
|
527
|
-
void this.startAttempt(run,
|
|
902
|
+
void this.startAttempt(run, resumed, 'recovery');
|
|
528
903
|
}
|
|
529
904
|
}
|
|
530
905
|
|