@stratusagent/gateway 0.14.8 → 0.14.9

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.
Files changed (40) hide show
  1. package/dist/agent-source.d.ts +10 -0
  2. package/dist/agent-source.d.ts.map +1 -0
  3. package/dist/agent-source.js +7 -0
  4. package/dist/agent-source.js.map +1 -0
  5. package/dist/approvals.d.ts +38 -0
  6. package/dist/approvals.d.ts.map +1 -0
  7. package/dist/approvals.js +330 -0
  8. package/dist/approvals.js.map +1 -0
  9. package/dist/credential-requests.d.ts +24 -0
  10. package/dist/credential-requests.d.ts.map +1 -0
  11. package/dist/credential-requests.js +452 -0
  12. package/dist/credential-requests.js.map +1 -0
  13. package/dist/dispatch.d.ts +34 -0
  14. package/dist/dispatch.d.ts.map +1 -0
  15. package/dist/dispatch.js +461 -0
  16. package/dist/dispatch.js.map +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +368 -2443
  19. package/dist/index.js.map +1 -1
  20. package/dist/merge-batch.d.ts +60 -0
  21. package/dist/merge-batch.d.ts.map +1 -0
  22. package/dist/merge-batch.js +59 -0
  23. package/dist/merge-batch.js.map +1 -0
  24. package/dist/recovery.d.ts +49 -0
  25. package/dist/recovery.d.ts.map +1 -0
  26. package/dist/recovery.js +487 -0
  27. package/dist/recovery.js.map +1 -0
  28. package/dist/steering.d.ts +24 -0
  29. package/dist/steering.d.ts.map +1 -0
  30. package/dist/steering.js +108 -0
  31. package/dist/steering.js.map +1 -0
  32. package/dist/sub-agents.d.ts +20 -0
  33. package/dist/sub-agents.d.ts.map +1 -0
  34. package/dist/sub-agents.js +263 -0
  35. package/dist/sub-agents.js.map +1 -0
  36. package/dist/turn-dispatch.d.ts +34 -0
  37. package/dist/turn-dispatch.d.ts.map +1 -0
  38. package/dist/turn-dispatch.js +179 -0
  39. package/dist/turn-dispatch.js.map +1 -0
  40. package/package.json +7 -7
@@ -0,0 +1,487 @@
1
+ /*
2
+ * Restart recovery: the turns the last process left parked on a person or
3
+ * still running, the sub-sessions it orphaned, and how a repeated
4
+ * idempotency key settles against an unfinished item or a rollover's
5
+ * archived transcript. Moved out of `createGateway` in index.ts as is: the
6
+ * statements are unchanged, inside a factory that takes what they closed
7
+ * over there, and `createGateway` calls it where they were. The edits:
8
+ * `stopping`, `abortingWith` and `orphanedAtStart` are `let`s reassigned
9
+ * later, so they are read through `isStopping()`, `getAbortingWith()` and
10
+ * `getOrphanedAtStart()`, and `onSessionChain`, declared further down, is
11
+ * passed late-bound.
12
+ */
13
+ import { completeAnsweredTurn, lastResponseIsAnswer, PENDING_APPROVAL_METADATA_KEY, readPendingApproval, recordTurnFailure, turnKeyOf, turnMessageOf, workItemState, } from '@stratusagent/core';
14
+ import { delegatingSessionIdOf, isDelegatedSession, outstandingDelegationFor, SCHEDULE_ID_METADATA_KEY, } from '@stratusagent/agents';
15
+ import { ABANDONED_TURN_ERROR, crossIdentityError, isSubAgentSession, ORPHANED_DELEGATION_ERROR, ROLLED_OVER_FROM_METADATA_KEY, ROLLOVER_REPEAT_WINDOW_MS, } from "./session-keys.js";
16
+ import { FALLBACK_ACTIVE_METADATA_KEY } from '@stratusagent/state';
17
+ export const createRecovery = ({ log, warn, bus, store, scheduler, effectiveApprovalTimeoutMs, refreshAgent, runtimeContextFor, runnerFor, withWatchdog, isStopping, trackTurn, untrackTurn, getAbortingWith, shutdownAbort, activeTurns, onTurnRuntime, getOrphanedAtStart, runtimeForAgent, watchdogStreamsFor, reachesHarness, onSessionChain, }) => {
18
+ /**
19
+ * Finishes the turns that were parked on a human when this daemon last
20
+ * went down.
21
+ *
22
+ * Run after the channels are up, deliberately: recovery re-asks, and a
23
+ * request emitted into a gateway with no live adapter would be declined
24
+ * for want of anywhere to put it — turning "your approval survived the
25
+ * restart" into "your approval was denied by the restart".
26
+ *
27
+ * Each session is recovered independently and failures are per-session:
28
+ * one unrecoverable turn must not stop the daemon from serving, or from
29
+ * recovering the others.
30
+ */
31
+ const recoverParkedTurns = async (orphaned) => {
32
+ if (!store.listIdsByStatus) {
33
+ return;
34
+ }
35
+ const parked = await store.listIdsByStatus('pending_approval');
36
+ if (parked.length === 0) {
37
+ return;
38
+ }
39
+ log(`recovering ${parked.length} turn(s) parked on approval`);
40
+ // Started together, not one after another. Each recovery re-asks and
41
+ // then waits on a human, so awaiting them in sequence would mean the
42
+ // second parked turn is not even *announced* until the first is
43
+ // answered — fifteen minutes later by default, and never at all with a
44
+ // zero timeout. They are independent turns; only same-session work is
45
+ // ordered, and onSessionChain already guarantees that.
46
+ await Promise.allSettled(parked.map((sessionId) => recoverOne(sessionId, orphaned.has(sessionId))));
47
+ };
48
+ /**
49
+ * The parked sub-sessions whose delegating turn the last process was
50
+ * still inside — read BEFORE the channels start, for the same reason the
51
+ * abandoned list is.
52
+ *
53
+ * The proof is the parent's transcript: only the runner writes an
54
+ * assistant `agent.delegate` call, and only a delegation in flight leaves
55
+ * one with no result behind it. That evidence does not survive contact
56
+ * with a channel, though — a message for the parent arriving once the
57
+ * adapters are up resumes it, and the resume closes the dangling call as
58
+ * interrupted. Read live from the sweep, the parent would then look as if
59
+ * it awaited nothing and the child would be re-asked after all, which is
60
+ * the bug this exists to stop. So it is read while nothing can dispatch,
61
+ * and the sweep judges from the snapshot; a child whose parent is missing
62
+ * or not inside such a call is left to ordinary recovery.
63
+ *
64
+ * Matched call by call, not parent by parent: the call that awaits a
65
+ * child names it (see `outstandingDelegationFor`), and a call can await
66
+ * only one. Two parked children claiming the same call — one of them a
67
+ * sub-session continued from outside under a version that kept its
68
+ * markers, parked again on something of its own — is an ambiguity this
69
+ * cannot settle, and both are left to ordinary recovery rather than one
70
+ * of them being failed for a delegation that was never its.
71
+ */
72
+ const listOrphanedDelegations = async () => {
73
+ const claimants = new Map();
74
+ if (!store.listIdsByStatus) {
75
+ return new Set();
76
+ }
77
+ for (const sessionId of await store.listIdsByStatus('pending_approval')) {
78
+ const parentId = delegatingSessionIdOf(sessionId);
79
+ if (parentId === undefined) {
80
+ continue;
81
+ }
82
+ const session = await store.get(sessionId);
83
+ if (!session || !isDelegatedSession(session)) {
84
+ continue;
85
+ }
86
+ const parent = await store.get(parentId);
87
+ const callId = parent ? outstandingDelegationFor(parent, session) : undefined;
88
+ if (callId !== undefined) {
89
+ const key = `${parentId}\u0000${callId}`;
90
+ claimants.set(key, [...(claimants.get(key) ?? []), sessionId]);
91
+ }
92
+ }
93
+ return new Set([...claimants.values()].filter((ids) => ids.length === 1).map(([id]) => id));
94
+ };
95
+ const recoverOne = (sessionId, orphaned) => onSessionChain(sessionId, () => recoverParked(sessionId, orphaned));
96
+ /**
97
+ * `recoverOne`'s work, for a caller already on the session's chain: a
98
+ * repeated idempotency key that reached the chain before the parked sweep
99
+ * did, and would otherwise wait on a turn nothing is running yet.
100
+ */
101
+ const recoverParked = async (sessionId, orphaned) => {
102
+ if (isStopping()) {
103
+ return;
104
+ }
105
+ try {
106
+ const session = await store.get(sessionId);
107
+ if (!session) {
108
+ return;
109
+ }
110
+ const record = readPendingApproval(session);
111
+ // A delegated sub-session is only ever awaited by its parent's
112
+ // `agent.delegate` call, and that parent was running when the last
113
+ // process died — it is in the abandoned sweep, not in this one.
114
+ // Nothing will read the reply, so the parked call is not re-asked:
115
+ // a person would be approving a command that runs for no one.
116
+ //
117
+ // Judged on the re-read, on the chain: the channels are up before
118
+ // this sweep starts, so a message for this id can have taken the
119
+ // chain first and moved the session on — and a turn that finished
120
+ // normally must not be rewritten as failed. The delegation metadata
121
+ // is durable, so it proves what the session is, not what state it
122
+ // is in; the status and the checkpoint say that.
123
+ //
124
+ // Whether it IS an orphan was decided before the channels started
125
+ // (`listOrphanedDelegations`), from the parent's transcript, which
126
+ // a message arriving since may already have rewritten.
127
+ // A sub-agent never parks (nobody is asked for it), and if one ever
128
+ // did, its parent's turn died with the last process: nothing would
129
+ // read its answer, and recovering it would run it as the full agent.
130
+ if (isSubAgentSession(session) && session.status === 'pending_approval') {
131
+ log(`${sessionId}: a sub-agent parked when the last stratusd stopped; failing it`);
132
+ await failParked(session, ABANDONED_TURN_ERROR);
133
+ return;
134
+ }
135
+ if (orphaned && isDelegatedSession(session) && session.status === 'pending_approval' && record) {
136
+ log(`${sessionId}: parked on ${record.call.toolName} for a delegating turn the last stratusd was still running; failing it`);
137
+ await failOrphanedDelegation(session);
138
+ return;
139
+ }
140
+ // A wait that outlived its window while the daemon was down is
141
+ // honoured, not restarted: the request really did go unanswered for
142
+ // the whole time it was configured to wait, and downtime is not a
143
+ // reason to extend a security decision. Measured from when the turn
144
+ // parked against this daemon's timeout — the transport's original
145
+ // deadline was chosen after the checkpoint was written and is gone
146
+ // with the process that chose it. Denying goes through the same
147
+ // recovery path, so the queue behind it still drains.
148
+ const parkedAt = record ? Date.parse(record.parkedAt) : Number.NaN;
149
+ const expired = effectiveApprovalTimeoutMs > 0
150
+ && Number.isFinite(parkedAt)
151
+ && Date.now() - parkedAt >= effectiveApprovalTimeoutMs;
152
+ if (expired) {
153
+ log(`${sessionId}: the approval for ${record?.call.toolName} outlived its window while the daemon was down; denying it`);
154
+ }
155
+ // Resolved the way a dispatch for this agent would resolve it, so a
156
+ // recovered turn finishes on the same provider, model, and
157
+ // credentials it was parked on.
158
+ const source = await refreshAgent(session.agent.id);
159
+ // And on the CURRENT definition, saved before recovery reads the
160
+ // session back. An allowlist is a permission boundary: a soul that
161
+ // dropped a tool while the daemon was down must not have that tool
162
+ // executed by the turn that outlived the change, and
163
+ // `allowedToolsFor` reads the definition frozen into the session
164
+ // ahead of the registry. Dispatch refreshes it the same way before
165
+ // resuming.
166
+ session.agent = source.definition;
167
+ await store.save(session);
168
+ const recoveredConfig = await runtimeForAgent(source);
169
+ const runner = runnerFor(recoveredConfig, source.definition.id);
170
+ // Tracked like a dispatched turn's controller: a recovered turn that
171
+ // runs on past its approval is a turn like any other, so a restart's
172
+ // window has to be able to cut it short with the same reason (or it
173
+ // runs unseen to the second window and the restart reports
174
+ // undrained), and a stop for the session has to find it. Only once
175
+ // the checkpoint has been read on this chain, right before the runner
176
+ // takes it: a stop counted then always ends in the turn's own
177
+ // outcome. A stop sent during the reads above finds nothing running
178
+ // and reaches the session as an ordinary message, queued behind it.
179
+ const controller = new AbortController();
180
+ if (record) {
181
+ trackTurn(sessionId, controller);
182
+ if (getAbortingWith() !== undefined) {
183
+ controller.abort(shutdownAbort());
184
+ }
185
+ }
186
+ try {
187
+ await onTurnRuntime(sessionId, { source, config: recoveredConfig }, () => runner.recoverPendingApproval(sessionId, {
188
+ denyPending: expired,
189
+ runtime: runtimeContextFor(source, recoveredConfig, session.metadata?.[FALLBACK_ACTIVE_METADATA_KEY] === true, session.metadata),
190
+ signal: controller.signal,
191
+ }));
192
+ }
193
+ finally {
194
+ untrackTurn(sessionId, controller);
195
+ // A recovered firing's row outlived the process that would have
196
+ // retired it, and the firing is over either way — a recovery that
197
+ // failed has durably failed its session, which is as finished as
198
+ // completing. The scheduler decides whether there is a row to
199
+ // retire, and only this firing's own.
200
+ const scheduleId = session.metadata?.[SCHEDULE_ID_METADATA_KEY];
201
+ if (typeof scheduleId === 'string') {
202
+ scheduler.retireSpentOneShot(scheduleId, sessionId);
203
+ }
204
+ }
205
+ }
206
+ catch (error) {
207
+ warn(`could not recover parked session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
208
+ // A keyed item is a promise to its caller, and one whose recovery
209
+ // cannot even start (its agent gone, its provider unresolvable) would
210
+ // otherwise stay parked for every later start and redelivery to try
211
+ // again. An unkeyed one keeps waiting for a sweep that can, as before.
212
+ const after = await store.get(sessionId).catch(() => undefined);
213
+ const key = after === undefined ? undefined : turnKeyOf(turnMessageOf(after));
214
+ if (after !== undefined && key !== undefined && after.status === 'pending_approval' && workItemState(after, key) === 'unfinished') {
215
+ await failParked(after, ABANDONED_TURN_ERROR).catch(() => { });
216
+ }
217
+ }
218
+ };
219
+ /**
220
+ * Closes an orphaned sub-session the way the abandoned sweep closes its
221
+ * parent: durably failed, with the checkpoint retired so no later sweep
222
+ * can find a call to re-enter, and announced where surfaces can hear it.
223
+ */
224
+ const failOrphanedDelegation = (session) => failParked(session, ORPHANED_DELEGATION_ERROR);
225
+ const failParked = async (session, error) => {
226
+ const metadata = { ...(session.metadata ?? {}) };
227
+ delete metadata[PENDING_APPROVAL_METADATA_KEY];
228
+ session.metadata = metadata;
229
+ recordTurnFailure(session, error);
230
+ await store.save(session);
231
+ await bus.emit({ type: 'session.updated', sessionId: session.id, status: 'failed' });
232
+ await bus.emit({ type: 'session.failed', sessionId: session.id, error });
233
+ };
234
+ /**
235
+ * Turns the last process was still running when it stopped being able to
236
+ * finish them.
237
+ *
238
+ * `pending_approval` is the one window a turn can be picked up from, and
239
+ * it has its own sweep above. Everything else a turn does is saved as
240
+ * `running`: the provider call, the executor, and — on a provider that
241
+ * hosts its own loop — the approval wait too, since a hosted call is
242
+ * deliberately not checkpointed (`executeHostedToolCall` passes
243
+ * `recoverable: false`, because the SDK's inner loop is not something a
244
+ * restart can rebuild). None of those can be re-entered where they
245
+ * stopped, and nothing else sweeps them, so the record would go on
246
+ * claiming the turn is running for as long as the session exists. A turn
247
+ * whose caller gave it an idempotency key is continued from its
248
+ * transcript instead, with its dangling calls closed as interrupted
249
+ * (`settleAbandonedTurn`); the rest are failed.
250
+ *
251
+ * Note what this is *not* for. A graceful stop denies everything parked
252
+ * and drains the turns those denials release, so an operator restarting
253
+ * the daemon normally has nothing here. These are the turns a crash, a
254
+ * SIGKILL, or a lost machine left behind.
255
+ */
256
+ const listAbandonedTurns = async () => {
257
+ if (!store.listIdsByStatus) {
258
+ return [];
259
+ }
260
+ return store.listIdsByStatus('running');
261
+ };
262
+ /**
263
+ * Settles each of them — continued if keyed, otherwise marked failed with
264
+ * a reason that says what happened.
265
+ *
266
+ * Read BEFORE the channels start and applied after, which is the only
267
+ * ordering that is both honest and safe. Reading first means the list is
268
+ * exactly what the last process left, with no chance of a turn this
269
+ * process started being in it. Applying after means the failure is
270
+ * emitted into a gateway whose surfaces are listening, rather than into
271
+ * one where nothing is.
272
+ *
273
+ * The gap between the two is real, though: an inbound message for one of
274
+ * these sessions can arrive as soon as the channels are up. What makes
275
+ * that safe is single-flight — every writer of a session goes through
276
+ * `onSessionChain`, so this runs either wholly before that turn (it
277
+ * fails, and `resume()` then clears `lastError` and starts fresh) or
278
+ * wholly after it (the status has moved off `running`, and the re-read
279
+ * below leaves it alone). There is no interleaving in which this
280
+ * overwrites a live turn.
281
+ *
282
+ * What that invariant does NOT cover is a second daemon on the same
283
+ * database, whose turns are running with no chain of ours in front of
284
+ * them. Two processes sharing a session store is already unsupported —
285
+ * the parked sweep above would re-ask that daemon's approvals for the
286
+ * same reason — and no check here would make it supported.
287
+ */
288
+ const failAbandonedTurns = async (abandoned) => {
289
+ if (abandoned.length === 0) {
290
+ return;
291
+ }
292
+ log(`settling ${abandoned.length} turn(s) left running by the last stratusd`);
293
+ await Promise.allSettled(abandoned.map((id) => failAbandonedTurn(id)));
294
+ };
295
+ const failAbandonedTurn = (id) => onSessionChain(id, async () => {
296
+ if (isStopping()) {
297
+ return;
298
+ }
299
+ try {
300
+ const session = await store.get(id);
301
+ // Re-read on the chain, because the snapshot is from before the
302
+ // channels came up: a message that arrived since has already taken
303
+ // this session somewhere else, and that turn owns it now.
304
+ if (!session || session.status !== 'running') {
305
+ return;
306
+ }
307
+ await settleAbandonedTurn(session);
308
+ }
309
+ catch (error) {
310
+ warn(`could not settle abandoned turn ${id}: ${error instanceof Error ? error.message : String(error)}`);
311
+ }
312
+ });
313
+ /**
314
+ * A turn left `running` with nothing running it, read on its chain.
315
+ *
316
+ * One whose message carries an idempotency key is a work item its caller
317
+ * was promised would finish, so it is continued from the transcript
318
+ * (`AgentRunner.continueTurn`) — once. A turn that already was, and is
319
+ * here again, took the last process down with it, and continuing it again
320
+ * would do the same on every start; it is failed like an unkeyed one,
321
+ * whose caller was never promised anything and is told to send again.
322
+ */
323
+ const settleAbandonedTurn = async (session) => {
324
+ const message = turnMessageOf(session);
325
+ if (message === undefined || turnKeyOf(message) === undefined) {
326
+ return failAbandoned(session);
327
+ }
328
+ // An answer already in the transcript is completed whatever came before
329
+ // — a second crash, a harness, an agent or provider gone since —
330
+ // because completing it needs none of them and runs nothing again;
331
+ // failing it would throw away a finished reply.
332
+ if (lastResponseIsAnswer(session)) {
333
+ log(`${session.id}: a keyed turn the last stratusd was running had saved its answer; completing it`);
334
+ return completeAnsweredTurn(session, store, bus);
335
+ }
336
+ if (message.continuedAfterCrash === true) {
337
+ return failAbandoned(session);
338
+ }
339
+ if (message.hostedLoop === true) {
340
+ log(`${session.id}: a keyed turn the last stratusd was running could reach a harness, which cannot be continued; failing it`);
341
+ return failAbandoned(session);
342
+ }
343
+ return continueAbandonedTurn(session);
344
+ };
345
+ const failAbandoned = async (session) => {
346
+ recordTurnFailure(session, ABANDONED_TURN_ERROR);
347
+ await store.save(session);
348
+ await bus.emit({ type: 'session.updated', sessionId: session.id, status: 'failed' });
349
+ await bus.emit({ type: 'session.failed', sessionId: session.id, error: ABANDONED_TURN_ERROR });
350
+ return session;
351
+ };
352
+ /**
353
+ * Continues an abandoned turn on the configuration a dispatch for its
354
+ * agent would resolve now, and under the same watchdog — it is a provider
355
+ * call like any turn's. The definition is refreshed and saved first, as
356
+ * `recoverOne` does and for the same reason: an allowlist is a permission
357
+ * boundary, and a tool dropped while the daemon was down must not run.
358
+ *
359
+ * Not on a harness (`runsOnHarness`) — neither one the turn could have
360
+ * run on (`Message.hostedLoop`, recorded when it was accepted) nor one it
361
+ * would continue on. A harness runs its own tool loop inside a single
362
+ * provider call and keeps its own conversation, so the prompt the dead
363
+ * turn sent may already have run tools there that the transcript never
364
+ * saw, and continuing would send it again. That turn is failed as an
365
+ * unkeyed one is — the same answer as before keys existed. (A turn whose
366
+ * answer is already saved never gets here: `settleAbandonedTurn`
367
+ * completes it first.)
368
+ */
369
+ const continueAbandonedTurn = async (session) => {
370
+ // Its parent's turn died with the last process, and continuing it here
371
+ // would run it as the full agent. (Sub-agent turns carry no key today,
372
+ // so this is a guard, not a path.)
373
+ if (isSubAgentSession(session)) {
374
+ return failAbandoned(session);
375
+ }
376
+ // Settled either way: a turn whose agent left the roster, or whose
377
+ // provider no longer resolves, must not stay `running` for every later
378
+ // start and every redelivery to find unfinished and trip over again.
379
+ let source;
380
+ let config;
381
+ try {
382
+ source = await refreshAgent(session.agent.id);
383
+ config = await runtimeForAgent(source);
384
+ }
385
+ catch (error) {
386
+ log(`${session.id}: a keyed turn the last stratusd was running cannot be continued (${error instanceof Error ? error.message : String(error)}); failing it`);
387
+ return failAbandoned(session);
388
+ }
389
+ const { switchedToFallback, effectiveStreams, fallbackStreams } = watchdogStreamsFor(session, config);
390
+ // The message's own record covers the runtimes the turn could have run
391
+ // on; this covers the ones it would continue on — a fallback included —
392
+ // which a changed config may make a harness.
393
+ if (reachesHarness(config, switchedToFallback)) {
394
+ log(`${session.id}: a keyed turn the last stratusd was running would continue on a harness; failing it`);
395
+ return failAbandoned(session);
396
+ }
397
+ log(`${session.id}: continuing a keyed turn the last stratusd was still running`);
398
+ session.agent = source.definition;
399
+ await store.save(session);
400
+ const runner = runnerFor(config, source.definition.id);
401
+ return onTurnRuntime(session.id, { source, config }, () => withWatchdog(session.id, undefined, effectiveStreams, fallbackStreams, async (signal) => await runner.continueTurn(session.id, {
402
+ runtime: runtimeContextFor(source, config, switchedToFallback, session.metadata),
403
+ signal,
404
+ }) ?? session));
405
+ };
406
+ /**
407
+ * A repeated idempotency key that reached the session's chain, so nothing
408
+ * else is running on the session. `undefined` means the key is new here
409
+ * and the dispatch runs as any other; otherwise this is the session to
410
+ * resolve with, and no new turn starts.
411
+ *
412
+ * An unfinished item here has no live turn — the live map would have
413
+ * answered first — so it is one the last process left, reached before
414
+ * the start-up sweep that would have settled it: settled now, the way
415
+ * that sweep would, which then finds it no longer needs anything.
416
+ */
417
+ const settleRepeat = async (sessionId, idempotencyKey, agentId, turnId, onRepeat) => {
418
+ const session = await store.get(sessionId);
419
+ if (session === undefined) {
420
+ return undefined;
421
+ }
422
+ // Before anything is returned: the turn a dispatch would run is refused
423
+ // for naming another agent, and the one it would repeat is no less that
424
+ // agent's — a repeat is not a way to read its transcript.
425
+ if (agentId !== undefined && session.agent.id !== agentId) {
426
+ throw crossIdentityError(sessionId, session.agent.id, agentId);
427
+ }
428
+ const state = workItemState(session, idempotencyKey);
429
+ if (state === undefined) {
430
+ const archived = await archivedRepeat(session, idempotencyKey);
431
+ if (archived !== undefined) {
432
+ onRepeat?.('finished');
433
+ }
434
+ return archived;
435
+ }
436
+ if (state === 'finished') {
437
+ onRepeat?.('finished');
438
+ return session;
439
+ }
440
+ // Run now, on this caller's behalf, so its events are this caller's
441
+ // turn — the adapter waiting on them has no other way to claim them.
442
+ if (turnId !== undefined) {
443
+ activeTurns.set(sessionId, turnId);
444
+ }
445
+ try {
446
+ return await settleUnfinished(session);
447
+ }
448
+ finally {
449
+ if (turnId !== undefined) {
450
+ activeTurns.delete(sessionId);
451
+ }
452
+ }
453
+ };
454
+ /**
455
+ * The archived transcript holding a key the live row does not, when the
456
+ * row is a rollover's fresh one made inside `ROLLOVER_REPEAT_WINDOW_MS`.
457
+ * Only the one rollover back: a second inside the window is a redelivery
458
+ * outliving two deliberate resets, and the chain is not walked for it.
459
+ * Always finished — a rollover refuses a session with a turn in flight.
460
+ */
461
+ const archivedRepeat = async (session, idempotencyKey) => {
462
+ const archivedAs = session.metadata?.[ROLLED_OVER_FROM_METADATA_KEY];
463
+ if (typeof archivedAs !== 'string' || Date.now() - Date.parse(session.createdAt) > ROLLOVER_REPEAT_WINDOW_MS) {
464
+ return undefined;
465
+ }
466
+ const archived = await store.get(archivedAs);
467
+ return archived !== undefined && workItemState(archived, idempotencyKey) !== undefined ? archived : undefined;
468
+ };
469
+ /** An unfinished item with no live turn, settled the way the start-up sweeps would. */
470
+ const settleUnfinished = async (session) => {
471
+ if (session.status === 'pending_approval') {
472
+ await recoverParked(session.id, getOrphanedAtStart().has(session.id));
473
+ return (await store.get(session.id)) ?? session;
474
+ }
475
+ return settleAbandonedTurn(session);
476
+ };
477
+ return {
478
+ recoverParkedTurns,
479
+ listOrphanedDelegations,
480
+ listAbandonedTurns,
481
+ failAbandonedTurns,
482
+ settleRepeat,
483
+ archivedRepeat,
484
+ settleUnfinished,
485
+ };
486
+ };
487
+ //# sourceMappingURL=recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.js","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { type Session, type SteeredMessage } from '@stratusagent/core';
2
+ import { type MergeBatch, type MergeJoiner } from './merge-batch.ts';
3
+ import type { ShardedSessionStore } from './sessions.ts';
4
+ import type { Recovery } from './recovery.ts';
5
+ /** What steering closes over in `createGateway`. */
6
+ export interface SteeringDeps extends Pick<Recovery, 'archivedRepeat'> {
7
+ log: (line: string) => void;
8
+ store: ShardedSessionStore;
9
+ releaseQueued: (sessionId: string) => void;
10
+ waitingDispatches: Map<string, number>;
11
+ isStopping: () => boolean;
12
+ liveWorkItems: Map<string, Map<string, {
13
+ turn: Promise<Session>;
14
+ agentId: string | undefined;
15
+ confirmed: boolean;
16
+ addressed: boolean;
17
+ }>>;
18
+ openBatches: Map<string, MergeBatch>;
19
+ }
20
+ export declare const createSteering: ({ log, store, releaseQueued, waitingDispatches, isStopping, liveWorkItems, openBatches, archivedRepeat, }: SteeringDeps) => {
21
+ takeSteered: (sessionId: string, agentId: string | undefined, into: MergeJoiner[], admit: (parts: readonly MergeJoiner[]) => boolean) => Promise<SteeredMessage | undefined>;
22
+ };
23
+ export type Steering = ReturnType<typeof createSteering>;
24
+ //# sourceMappingURL=steering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steering.d.ts","sourceRoot":"","sources":["../src/steering.ts"],"names":[],"mappings":"AASA,OAAO,EAKL,KAAK,OAAO,EACZ,KAAK,cAAc,EAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,UAAU,EAAoB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,oDAAoD;AACpD,MAAM,WAAW,YAAa,SAAQ,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpE,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,aAAa,EAAE,GAAG,CAChB,MAAM,EACN,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAC7G,CAAC;IACF,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACtC;AAED,eAAO,MAAM,cAAc,GAAI,2GAS5B,YAAY;6BAeA,MAAM,WACR,MAAM,GAAG,SAAS,QACrB,WAAW,EAAE,SAOZ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,KAAK,OAAO,KAChD,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;CA6EvC,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC"}
@@ -0,0 +1,108 @@
1
+ /*
2
+ * What a running turn takes in at its next tool step (card 948): the merged
3
+ * batch waiting behind it. Moved out of `createGateway` in index.ts as is:
4
+ * the statement is unchanged, inside a factory that takes what it closed
5
+ * over there, and `createGateway` calls it where it was. The one kind of
6
+ * edit: `stopping` is a `let` set at shutdown, so it is read through
7
+ * `isStopping()`.
8
+ */
9
+ import { abortErrorFor, leastTrusted, SENDER_TRUST_METADATA_KEY, senderTrustOf, workItemState, } from '@stratusagent/core';
10
+ import { MERGED_SEPARATOR } from "./merge-batch.js";
11
+ export const createSteering = ({ log, store, releaseQueued, waitingDispatches, isStopping, liveWorkItems, openBatches, archivedRepeat, }) => {
12
+ /**
13
+ * What a running turn takes in at its next tool step (card 948): the
14
+ * merged-turn batch waiting behind it, whole — leader and absorbed alike
15
+ * — when it is for the same agent. The dispatches it takes join `into`,
16
+ * which the running turn settles with its own outcome (`runSteerable`).
17
+ *
18
+ * A part cancelled while it waited is refused, as `runMerged` refuses it.
19
+ * A part whose key this session (or the archive it rolled over from)
20
+ * already holds is a repeat, and judging a repeat can mean settling a
21
+ * turn — never something to do from inside one — so such a batch is not
22
+ * taken: it waits and runs next, merged, where that is judged. Nor while
23
+ * the gateway is stopping, when the batch is what the drain refuses.
24
+ */
25
+ const takeSteered = async (sessionId, agentId, into,
26
+ /**
27
+ * Asked last, with nothing awaited after it: whether the running turn
28
+ * still takes these callers, joining them to its cancellation if so. A
29
+ * turn cancelled while the store was read takes nothing, and the batch
30
+ * waits and runs next, as it would have.
31
+ */
32
+ admit) => {
33
+ const batch = openBatches.get(sessionId);
34
+ if (isStopping() || batch === undefined || !batch.firstInLine || batch.agentId !== agentId) {
35
+ return undefined;
36
+ }
37
+ // Until every key in it is checked with nothing awaited since: a message
38
+ // can join while the store is read, and is checked on the next pass; a
39
+ // batch closed meanwhile is no longer this turn's to take.
40
+ const checked = new Set();
41
+ let session;
42
+ for (;;) {
43
+ if (isStopping() || openBatches.get(sessionId) !== batch) {
44
+ return undefined;
45
+ }
46
+ const unchecked = [batch.leader, ...batch.absorbed].flatMap((part) => (part.input.idempotencyKey !== undefined && !checked.has(part.input.idempotencyKey) ? [part.input.idempotencyKey] : []));
47
+ if (unchecked.length === 0) {
48
+ break;
49
+ }
50
+ session ??= await store.get(sessionId);
51
+ for (const key of unchecked) {
52
+ checked.add(key);
53
+ if (session !== undefined && (workItemState(session, key) !== undefined || await archivedRepeat(session, key) !== undefined)) {
54
+ return undefined;
55
+ }
56
+ }
57
+ }
58
+ const all = [batch.leader, ...batch.absorbed];
59
+ if (!admit(all.filter((part) => part.input.signal?.aborted !== true))) {
60
+ return undefined;
61
+ }
62
+ openBatches.delete(sessionId);
63
+ batch.steered = true;
64
+ // Its place on the chain runs nothing now, so it is not ahead of a batch
65
+ // opening behind it, which can then be steered at a later step.
66
+ releaseQueued(sessionId);
67
+ // Not waiting any more; the leader's place on the chain does not count it again.
68
+ const left = (waitingDispatches.get(sessionId) ?? all.length) - all.length;
69
+ if (left > 0) {
70
+ waitingDispatches.set(sessionId, left);
71
+ }
72
+ else {
73
+ waitingDispatches.delete(sessionId);
74
+ }
75
+ const runnable = [];
76
+ for (const part of all) {
77
+ if (part.input.signal?.aborted) {
78
+ part.reject(abortErrorFor(part.input.signal));
79
+ continue;
80
+ }
81
+ runnable.push(part);
82
+ }
83
+ if (runnable.length === 0) {
84
+ return undefined;
85
+ }
86
+ const keys = runnable.flatMap((part) => (part.input.idempotencyKey !== undefined ? [part.input.idempotencyKey] : []));
87
+ const items = liveWorkItems.get(sessionId);
88
+ for (const key of keys) {
89
+ const item = items?.get(key);
90
+ if (item !== undefined) {
91
+ item.confirmed = true;
92
+ }
93
+ }
94
+ into.push(...runnable);
95
+ const labelled = runnable.some((part) => part.input.metadata !== undefined && SENDER_TRUST_METADATA_KEY in part.input.metadata);
96
+ const lowest = leastTrusted(...runnable.map((part) => senderTrustOf(part.input.metadata)));
97
+ const images = runnable.flatMap((part) => part.input.images ?? []);
98
+ log(`${sessionId}: ${runnable.length} message(s) sent during the running turn reached it at its next step`);
99
+ return {
100
+ userMessage: runnable.map((part) => part.input.userMessage).join(MERGED_SEPARATOR),
101
+ ...(images.length > 0 ? { images } : {}),
102
+ ...(labelled ? { metadata: { [SENDER_TRUST_METADATA_KEY]: lowest } } : {}),
103
+ ...(keys.length > 0 ? { keys } : {}),
104
+ };
105
+ };
106
+ return { takeSteered };
107
+ };
108
+ //# sourceMappingURL=steering.js.map