@nylorun/runtime 0.10.0-beta → 0.12.0-beta
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 +86 -0
- package/README.md +53 -51
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/core/executors.d.ts +4 -1
- package/dist/core/executors.js +20 -1
- package/dist/core/flow-host.d.ts +93 -132
- package/dist/core/flow-host.js +282 -547
- package/dist/core/sandbox-routes.d.ts +13 -4
- package/dist/core/sandbox-routes.js +14 -12
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +2 -1
- package/dist/host/config-for.js +3 -21
- package/dist/host/config.d.ts +0 -8
- package/dist/host/create-host.d.ts +36 -1
- package/dist/host/create-host.js +51 -24
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +4 -0
- package/dist/host/http.js +6 -0
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +2 -0
- package/dist/host/main.js +106 -47
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +1 -1
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +5 -5
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +1 -1
- package/dist/sandbox/select.js +4 -5
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +645 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/002_session_owner.d.ts +6 -0
- package/dist/store/postgres/migrations/002_session_owner.js +13 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +76 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +797 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +214 -0
- package/dist/store/types.d.ts +442 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +32 -0
- package/dist/tenant/auth.js +169 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +393 -0
- package/dist/tenant/context.d.ts +147 -0
- package/dist/tenant/context.js +48 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +564 -0
- package/dist/tenant/envelope.d.ts +0 -3
- package/dist/tenant/envelope.js +2 -34
- package/dist/tenant/ephemeral.d.ts +15 -4
- package/dist/tenant/ephemeral.js +106 -38
- package/dist/tenant/http.d.ts +38 -0
- package/dist/tenant/http.js +53 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +29 -8
- package/dist/tenant/module.js +165 -158
- package/dist/tenant/paths.d.ts +12 -8
- package/dist/tenant/paths.js +38 -12
- package/dist/tenant/principals.d.ts +12 -13
- package/dist/tenant/principals.js +50 -38
- package/dist/tenant/quarantine-error.d.ts +1 -1
- package/dist/tenant/quarantine-error.js +2 -8
- package/dist/tenant/quarantine.d.ts +0 -8
- package/dist/tenant/quarantine.js +8 -30
- package/dist/tenant/reset.d.ts +7 -6
- package/dist/tenant/reset.js +7 -51
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +137 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +151 -0
- package/dist/tenant/runtime.d.ts +59 -58
- package/dist/tenant/runtime.js +243 -2201
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +40 -0
- package/dist/tenant/sessions.js +163 -0
- package/dist/tenant/status.d.ts +13 -4
- package/dist/tenant/status.js +76 -41
- package/dist/tenant/store-memory.d.ts +9 -12
- package/dist/tenant/store-memory.js +79 -68
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/transcript.d.ts +104 -0
- package/dist/tenant/transcript.js +58 -0
- package/dist/tenant/types.d.ts +101 -15
- package/dist/tenant/types.js +23 -1
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/service.d.ts +44 -31
- package/dist/vault/service.js +454 -360
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -14
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/store.d.ts +0 -32
- package/dist/core/store.js +0 -143
- package/dist/launcher/commands.d.ts +0 -24
- package/dist/launcher/commands.js +0 -260
- package/dist/launcher/ensure-config.d.ts +0 -16
- package/dist/launcher/ensure-config.js +0 -79
- package/dist/launcher/errors.d.ts +0 -12
- package/dist/launcher/errors.js +0 -22
- package/dist/launcher/host-config.d.ts +0 -17
- package/dist/launcher/host-config.js +0 -64
- package/dist/launcher/host-state.d.ts +0 -17
- package/dist/launcher/host-state.js +0 -61
- package/dist/launcher/lifecycle.d.ts +0 -66
- package/dist/launcher/lifecycle.js +0 -407
- package/dist/launcher/locks.d.ts +0 -26
- package/dist/launcher/locks.js +0 -117
- package/dist/launcher/logs.d.ts +0 -18
- package/dist/launcher/logs.js +0 -117
- package/dist/launcher/main.d.ts +0 -2
- package/dist/launcher/main.js +0 -66
- package/dist/launcher/output.d.ts +0 -11
- package/dist/launcher/output.js +0 -68
- package/dist/launcher/paths.d.ts +0 -20
- package/dist/launcher/paths.js +0 -36
- package/dist/launcher/probe.d.ts +0 -23
- package/dist/launcher/probe.js +0 -72
- package/dist/launcher/protocol.d.ts +0 -62
- package/dist/launcher/protocol.js +0 -1
- package/dist/launcher/schema-guard.d.ts +0 -6
- package/dist/launcher/schema-guard.js +0 -36
- package/dist/launcher/spawn.d.ts +0 -22
- package/dist/launcher/spawn.js +0 -85
- package/dist/launcher/status.d.ts +0 -6
- package/dist/launcher/status.js +0 -65
- package/dist/tenant/migration.d.ts +0 -16
- package/dist/tenant/migration.js +0 -116
- package/dist/tenant/schema.d.ts +0 -23
- package/dist/tenant/schema.js +0 -151
- package/dist/tenant/store-fs.d.ts +0 -16
- package/dist/tenant/store-fs.js +0 -205
package/dist/core/flow-host.js
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow host: the Session Store side of workflow sessions (architecture §10–12).
|
|
3
|
+
*
|
|
4
|
+
* Every function that touches state takes the caller's transaction `t: Tx` and
|
|
5
|
+
* is async. Nothing here publishes, notifies or schedules directly (seam rule
|
|
6
|
+
* 1): events go through `t.event(...)`, which the store publishes after
|
|
7
|
+
* commit, and wakes go through `t.afterCommit(() => schedule(id, wake))`. Functions
|
|
8
|
+
* that rewrite a session-scoped document lock that session first with
|
|
9
|
+
* `t.lockSession` (a no-op when the caller already holds it).
|
|
10
|
+
*
|
|
11
|
+
* Exported signatures:
|
|
12
|
+
*
|
|
13
|
+
* - `deriveSessionId(workflowSessionId, path, ...parts): string`
|
|
14
|
+
* - `deriveAgentEffectSessionId(workflowSessionId, path, request): string`
|
|
15
|
+
* - `isWorkflowManifest(manifest): manifest is WorkflowManifest`
|
|
16
|
+
* - `isFlowToolEffect(request): boolean`, `isFlowEffect(request): boolean`
|
|
17
|
+
* - `pathDepth(path): number`
|
|
18
|
+
* - `countActiveFlowWork(t, workflowSessionId, turnId): Promise<number>`
|
|
19
|
+
* - `commandKey(sessionId, idempotencyKey): string`
|
|
20
|
+
* - `linkedMessageKey(request): string`
|
|
21
|
+
* - `linkedTurnOf(t, agentSessionId, request): Promise<string | undefined>`
|
|
22
|
+
* - `linkedTurnEnd(t, effect, agent): Promise<LinkedTurnEnd | undefined>`
|
|
23
|
+
* - `wakeLinkedWorkflow({ t, agentSessionId, turnId, output?, failed?, cancelled?, error?, schedule }): Promise<void>`
|
|
24
|
+
* - `pendingAgentEffects(t): Promise<FlowEffect[]>`
|
|
25
|
+
* - `reconcilePendingAgentEffect({ t, effectId, schedule }): Promise<boolean>`
|
|
26
|
+
* - `claimedFnVerifyActions(t): Promise<Action[]>`
|
|
27
|
+
* - `reofferFnVerifyClaim(t, actionId): Promise<boolean>`
|
|
28
|
+
* - `planCancelCascade({ t, workflowSessionId, turnId }): Promise<CascadeCancelPlan>`
|
|
29
|
+
* - `cancelSiblingWork({ t, workflowSessionId, turnId, siblingPaths?, cancelEffectIds? }): Promise<CancelSiblingResult>`
|
|
30
|
+
* - `fenceWorkflowActions({ t, workflowSessionId, turnId }): Promise<{ cancelled; uncertain }>`
|
|
31
|
+
* - `aggregateWaits({ t, workflowSessionId }): Promise<FlowWait[]>`
|
|
32
|
+
* - `findInteractionOwner({ t, workflowSessionId, interactionId }): Promise<{ sessionId; path } | undefined>`
|
|
33
|
+
* - `foreignInteractionConflict({ t, workflowSessionId, interactionId }): Promise<{ status: 409; message; ownerSessionId } | undefined>`
|
|
34
|
+
* - `wakeForQueuedEffects({ t, workflowSessionId, turnId, limits, schedule }): Promise<boolean>`
|
|
35
|
+
*
|
|
36
|
+
* `schedule: (sessionId, wake) => void | Promise<void>` runs after commit, never inside `t`;
|
|
37
|
+
* `wake` carries the reason (`linked`, `flow`) and, where the cause has one, a dedupe key.
|
|
38
|
+
*
|
|
39
|
+
* An `agent` effect settles only from the linked turn it started. A linked agent session is
|
|
40
|
+
* reused by every iteration of a Loop (its id derives from the workflow and the path), so its
|
|
41
|
+
* status and `lastOutput` may belong to an earlier iteration's turn until this effect's message
|
|
42
|
+
* commits. The effect is bound to its turn through the message's idempotency key
|
|
43
|
+
* (`linkedMessageKey`): the `commands` document of that key names the turn it opened.
|
|
44
|
+
*
|
|
45
|
+
* The Tenant sweep (`tenant/sweep.ts`) calls the reconcile and re-offer functions, one
|
|
46
|
+
* transaction per effect or Action.
|
|
47
|
+
*/
|
|
1
48
|
import { createHash } from "node:crypto";
|
|
2
49
|
import { mayDispatchMore } from "./limits.js";
|
|
3
50
|
/** Deterministic agent session id: derive(workflowSessionId, path, …parts). */
|
|
@@ -41,54 +88,19 @@ export function isFlowEffect(request) {
|
|
|
41
88
|
request.kind === "verify" ||
|
|
42
89
|
isFlowToolEffect(request));
|
|
43
90
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*/
|
|
48
|
-
export function resolveFlowEffect(input) {
|
|
49
|
-
const { request, store, session } = input;
|
|
50
|
-
if (!isFlowEffect(request))
|
|
51
|
-
throw new Error(`Not a flow effect: ${request.kind}`);
|
|
52
|
-
const existing = store.get("effects", request.effectId);
|
|
53
|
-
if (existing) {
|
|
54
|
-
if (existing.status === "completed")
|
|
55
|
-
return { status: "completed", outcome: existing.outcome };
|
|
56
|
-
if (existing.status === "queued") {
|
|
57
|
-
if (input.limits &&
|
|
58
|
-
!mayDispatchMore(countActiveFlowWork(store, session.id, request.turnId), input.limits))
|
|
59
|
-
return { status: "pending" };
|
|
60
|
-
return dispatchQueuedEffect(input, existing);
|
|
61
|
-
}
|
|
62
|
-
if (request.kind === "agent")
|
|
63
|
-
return settleAgentEffect(input, existing) ?? { status: "pending" };
|
|
64
|
-
return {
|
|
65
|
-
status: existing.status === "uncertain" || existing.status === "invoking"
|
|
66
|
-
? "uncertain"
|
|
67
|
-
: "pending",
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
if (input.limits &&
|
|
71
|
-
!mayDispatchMore(countActiveFlowWork(store, session.id, request.turnId), input.limits)) {
|
|
72
|
-
store.put("effects", request.effectId, {
|
|
73
|
-
request,
|
|
74
|
-
status: "queued",
|
|
75
|
-
});
|
|
76
|
-
return { status: "pending" };
|
|
77
|
-
}
|
|
78
|
-
if (request.kind === "agent")
|
|
79
|
-
return startAgentEffect(input);
|
|
80
|
-
if (isFlowToolEffect(request))
|
|
81
|
-
return startToolNode(input);
|
|
82
|
-
return startFnOrVerify(input);
|
|
91
|
+
const OPEN_ACTION = ["pending", "claimed"];
|
|
92
|
+
function scheduleAfterCommit(t, schedule, id, wake) {
|
|
93
|
+
t.afterCommit(() => schedule(id, wake));
|
|
83
94
|
}
|
|
84
95
|
/** Active agent turns + pending/claimed actions for one workflow turn. */
|
|
85
|
-
export function countActiveFlowWork(
|
|
96
|
+
export async function countActiveFlowWork(t, workflowSessionId, turnId) {
|
|
97
|
+
const effects = await t.effectsForTurn(workflowSessionId, turnId);
|
|
98
|
+
const openActions = new Set((await t.actionsForSession(workflowSessionId, {
|
|
99
|
+
turnId,
|
|
100
|
+
statuses: OPEN_ACTION,
|
|
101
|
+
})).map((action) => action.actionId));
|
|
86
102
|
let n = 0;
|
|
87
|
-
for (const effect of
|
|
88
|
-
if (effect.request?.sessionId !== workflowSessionId)
|
|
89
|
-
continue;
|
|
90
|
-
if (effect.request?.turnId !== turnId)
|
|
91
|
-
continue;
|
|
103
|
+
for (const effect of effects) {
|
|
92
104
|
if (effect.status === "queued")
|
|
93
105
|
continue;
|
|
94
106
|
if (effect.status === "completed" || effect.status === "cancelled")
|
|
@@ -97,228 +109,77 @@ export function countActiveFlowWork(store, workflowSessionId, turnId) {
|
|
|
97
109
|
n += 1;
|
|
98
110
|
continue;
|
|
99
111
|
}
|
|
100
|
-
if (effect.request?.kind === "fn" ||
|
|
112
|
+
if ((effect.request?.kind === "fn" ||
|
|
101
113
|
effect.request?.kind === "verify" ||
|
|
102
|
-
isFlowToolEffect(effect.request))
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
(action.status === "pending" || action.status === "claimed"))
|
|
106
|
-
n += 1;
|
|
107
|
-
}
|
|
114
|
+
isFlowToolEffect(effect.request)) &&
|
|
115
|
+
openActions.has(effect.request.effectId))
|
|
116
|
+
n += 1;
|
|
108
117
|
}
|
|
109
118
|
return n;
|
|
110
119
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
...existing,
|
|
115
|
-
status: "pending",
|
|
116
|
-
});
|
|
117
|
-
if (request.kind === "agent")
|
|
118
|
-
return startAgentEffect(input);
|
|
119
|
-
if (isFlowToolEffect(request))
|
|
120
|
-
return startToolNode(input);
|
|
121
|
-
return startFnOrVerify(input);
|
|
120
|
+
/** The `commands` document key of a session command's idempotency key (`tenant/commands.ts`). */
|
|
121
|
+
export function commandKey(sessionId, idempotencyKey) {
|
|
122
|
+
return JSON.stringify([sessionId, idempotencyKey]);
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
request
|
|
127
|
-
|
|
128
|
-
});
|
|
129
|
-
const kind = request.kind;
|
|
130
|
-
const action = {
|
|
131
|
-
actionId: request.effectId,
|
|
132
|
-
sessionId: request.sessionId,
|
|
133
|
-
turnId: request.turnId,
|
|
134
|
-
agentId: request.agentId,
|
|
135
|
-
manifestHash: request.manifestHash,
|
|
136
|
-
implementationVersion: session.implementationVersion,
|
|
137
|
-
input: request.input,
|
|
138
|
-
context: request.context,
|
|
139
|
-
status: "pending",
|
|
140
|
-
generation: 0,
|
|
141
|
-
claimId: null,
|
|
142
|
-
leaseExpiresAt: null,
|
|
143
|
-
kind,
|
|
144
|
-
path: request.path,
|
|
145
|
-
key: request.key,
|
|
146
|
-
};
|
|
147
|
-
store.put("actions", action.actionId, action);
|
|
148
|
-
const event = store.event(session.id, session.activeTurnId, "action.pending", {
|
|
149
|
-
actionId: action.actionId,
|
|
150
|
-
kind: action.kind,
|
|
151
|
-
path: action.path,
|
|
152
|
-
key: action.key,
|
|
153
|
-
input: action.input,
|
|
154
|
-
});
|
|
155
|
-
input.publish(event);
|
|
156
|
-
input.notify();
|
|
157
|
-
return { status: "pending" };
|
|
124
|
+
/** The idempotency key of the message a flow `agent` effect sends its linked session. */
|
|
125
|
+
export function linkedMessageKey(request) {
|
|
126
|
+
const path = request.input?.path ??
|
|
127
|
+
request.path;
|
|
128
|
+
return `${request.turnId}:${path}:${request.iterations ?? "-"}`;
|
|
158
129
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
actionId: request.effectId,
|
|
167
|
-
sessionId: request.sessionId,
|
|
168
|
-
turnId: request.turnId,
|
|
169
|
-
agentId: request.agentId,
|
|
170
|
-
manifestHash: request.manifestHash,
|
|
171
|
-
implementationVersion: session.implementationVersion,
|
|
172
|
-
input: request.input,
|
|
173
|
-
context: request.context,
|
|
174
|
-
status: "pending",
|
|
175
|
-
generation: 0,
|
|
176
|
-
claimId: null,
|
|
177
|
-
leaseExpiresAt: null,
|
|
178
|
-
kind: "tool",
|
|
179
|
-
path: request.path,
|
|
180
|
-
key: request.key,
|
|
181
|
-
};
|
|
182
|
-
store.put("actions", action.actionId, action);
|
|
183
|
-
emitNodeStarted(input, {
|
|
184
|
-
path: request.path,
|
|
185
|
-
kind: "tool",
|
|
186
|
-
key: request.key,
|
|
187
|
-
iterations: request.iterations,
|
|
188
|
-
});
|
|
189
|
-
const event = store.event(session.id, session.activeTurnId, "action.pending", {
|
|
190
|
-
actionId: action.actionId,
|
|
191
|
-
kind: action.kind,
|
|
192
|
-
path: action.path,
|
|
193
|
-
key: action.key,
|
|
194
|
-
input: action.input,
|
|
195
|
-
});
|
|
196
|
-
input.publish(event);
|
|
197
|
-
input.notify();
|
|
198
|
-
return { status: "pending" };
|
|
199
|
-
}
|
|
200
|
-
function startAgentEffect(input) {
|
|
201
|
-
const { request, store, session } = input;
|
|
202
|
-
const body = request.input;
|
|
203
|
-
const path = body.path ?? request.path;
|
|
204
|
-
const agentSessionId = deriveAgentEffectSessionId(session.id, path, request);
|
|
205
|
-
const iterations = request.iterations ?? "-";
|
|
206
|
-
const n = Number(request.context.n ?? iterations.split(".")[0] ?? 1);
|
|
207
|
-
store.put("effects", request.effectId, {
|
|
208
|
-
request,
|
|
209
|
-
status: "pending",
|
|
210
|
-
agentSessionId,
|
|
211
|
-
});
|
|
212
|
-
store.put("links", agentSessionId, {
|
|
213
|
-
workflowSessionId: session.id,
|
|
214
|
-
path,
|
|
215
|
-
effectId: request.effectId,
|
|
216
|
-
turnId: request.turnId,
|
|
217
|
-
});
|
|
218
|
-
const existingAgent = input.readSession(agentSessionId);
|
|
219
|
-
if (!existingAgent) {
|
|
220
|
-
input.putLinkedSession({
|
|
221
|
-
id: agentSessionId,
|
|
222
|
-
agentId: body.agentId,
|
|
223
|
-
ownerUserId: session.ownerUserId,
|
|
224
|
-
vaultIds: session.vaultIds,
|
|
225
|
-
credentialSelections: session.credentialSelections,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
const idempotencyKey = `${request.turnId}:${path}:${iterations}`;
|
|
229
|
-
const messageInput = body.input;
|
|
230
|
-
const accepted = input.sendMessage({
|
|
231
|
-
sessionId: agentSessionId,
|
|
232
|
-
...(typeof messageInput === "string"
|
|
233
|
-
? { content: messageInput }
|
|
234
|
-
: { data: messageInput }),
|
|
235
|
-
idempotencyKey,
|
|
236
|
-
requestId: `flow-${request.effectId}`,
|
|
237
|
-
});
|
|
238
|
-
const iterationEvent = store.event(session.id, session.activeTurnId, "loop.iteration", {
|
|
239
|
-
path: String(request.context.loopPath ?? path.split("/")[0]),
|
|
240
|
-
n,
|
|
241
|
-
sessionId: agentSessionId,
|
|
242
|
-
turnId: accepted.turnId ?? undefined,
|
|
243
|
-
});
|
|
244
|
-
input.publish(iterationEvent);
|
|
245
|
-
const nodeAgent = store.event(session.id, session.activeTurnId, "node.agent", {
|
|
246
|
-
path,
|
|
247
|
-
iterations,
|
|
248
|
-
sessionId: agentSessionId,
|
|
249
|
-
turnId: accepted.turnId,
|
|
250
|
-
});
|
|
251
|
-
input.publish(nodeAgent);
|
|
252
|
-
return (settleAgentEffect(input, store.get("effects", request.effectId)) ?? {
|
|
253
|
-
status: "pending",
|
|
254
|
-
});
|
|
130
|
+
/**
|
|
131
|
+
* The linked turn an `agent` effect started: the turn its message opened, or undefined until
|
|
132
|
+
* that message commits.
|
|
133
|
+
*/
|
|
134
|
+
export async function linkedTurnOf(t, agentSessionId, request) {
|
|
135
|
+
const accepted = await t.get("commands", commandKey(agentSessionId, linkedMessageKey(request)));
|
|
136
|
+
return accepted?.response?.turnId ?? undefined;
|
|
255
137
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
138
|
+
/**
|
|
139
|
+
* How the linked turn an `agent` effect started ended, or undefined while it has not: before
|
|
140
|
+
* its message commits, the linked session's status and `lastOutput` describe an earlier turn.
|
|
141
|
+
* `agent` is the linked session, read in the caller's transaction.
|
|
142
|
+
*/
|
|
143
|
+
export async function linkedTurnEnd(t, effect, agent) {
|
|
144
|
+
const id = effect.agentSessionId;
|
|
145
|
+
if (!agent || typeof id !== "string" || agent.activeTurnId !== null)
|
|
263
146
|
return undefined;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (agent.status === "failed") {
|
|
276
|
-
const outcome = {
|
|
277
|
-
value: {
|
|
278
|
-
kind: "failed",
|
|
279
|
-
code: "agent.failed",
|
|
280
|
-
message: agent.error ?? "Agent turn failed",
|
|
281
|
-
},
|
|
282
|
-
};
|
|
283
|
-
input.store.put("effects", input.request.effectId, {
|
|
284
|
-
request: input.request,
|
|
285
|
-
status: "completed",
|
|
286
|
-
outcome,
|
|
287
|
-
agentSessionId,
|
|
288
|
-
});
|
|
289
|
-
return { status: "completed", outcome };
|
|
290
|
-
}
|
|
291
|
-
if (agent.status === "cancelled") {
|
|
292
|
-
const outcome = {
|
|
293
|
-
value: {
|
|
294
|
-
kind: "failed",
|
|
295
|
-
code: "agent.cancelled",
|
|
296
|
-
message: agent.error ?? "Agent turn was cancelled",
|
|
297
|
-
},
|
|
147
|
+
const turnId = await linkedTurnOf(t, id, effect.request);
|
|
148
|
+
// Sessions settled before `lastTurnId` was recorded: the turn ended if none is active.
|
|
149
|
+
if (!turnId || (agent.lastTurnId ?? turnId) !== turnId)
|
|
150
|
+
return undefined;
|
|
151
|
+
if (agent.status === "completed")
|
|
152
|
+
return { turnId, status: "completed", output: agent.lastOutput ?? null };
|
|
153
|
+
if (agent.status === "failed" || agent.status === "cancelled")
|
|
154
|
+
return {
|
|
155
|
+
turnId,
|
|
156
|
+
status: agent.status,
|
|
157
|
+
...(agent.error !== undefined ? { error: agent.error } : {}),
|
|
298
158
|
};
|
|
299
|
-
input.store.put("effects", input.request.effectId, {
|
|
300
|
-
request: input.request,
|
|
301
|
-
status: "completed",
|
|
302
|
-
outcome,
|
|
303
|
-
agentSessionId,
|
|
304
|
-
});
|
|
305
|
-
return { status: "completed", outcome };
|
|
306
|
-
}
|
|
307
159
|
return undefined;
|
|
308
160
|
}
|
|
309
|
-
/**
|
|
310
|
-
|
|
311
|
-
|
|
161
|
+
/**
|
|
162
|
+
* After a linked agent turn ends, settle the `agent` effect that started it and wake the
|
|
163
|
+
* workflow. `turnId` is the linked session's turn that ended; an effect bound to another turn
|
|
164
|
+
* (a later iteration whose message has not committed yet) is left alone.
|
|
165
|
+
*/
|
|
166
|
+
export async function wakeLinkedWorkflow(input) {
|
|
167
|
+
const { t } = input;
|
|
168
|
+
const link = await t.get("links", input.agentSessionId);
|
|
312
169
|
if (!link)
|
|
313
170
|
return;
|
|
314
|
-
const
|
|
315
|
-
if (!effect || effect.status === "completed")
|
|
316
|
-
return;
|
|
317
|
-
const workflow = input.store.get("sessions", link.workflowSessionId);
|
|
171
|
+
const workflow = await t.lockSession(link.workflowSessionId);
|
|
318
172
|
if (!workflow)
|
|
319
173
|
return;
|
|
174
|
+
const effect = await t.get("effects", link.effectId);
|
|
175
|
+
if (!effect || effect.status === "completed")
|
|
176
|
+
return;
|
|
320
177
|
if (workflow.activeTurnId !== link.turnId)
|
|
321
178
|
return;
|
|
179
|
+
if (!input.turnId ||
|
|
180
|
+
(await linkedTurnOf(t, input.agentSessionId, effect.request)) !==
|
|
181
|
+
input.turnId)
|
|
182
|
+
return;
|
|
322
183
|
const outcome = input.cancelled
|
|
323
184
|
? {
|
|
324
185
|
value: {
|
|
@@ -336,191 +197,87 @@ export function wakeLinkedWorkflow(input) {
|
|
|
336
197
|
},
|
|
337
198
|
}
|
|
338
199
|
: { value: input.output ?? null };
|
|
339
|
-
|
|
200
|
+
await t.put("effects", link.effectId, {
|
|
340
201
|
...effect,
|
|
341
202
|
status: "completed",
|
|
342
203
|
outcome,
|
|
343
204
|
});
|
|
344
205
|
workflow.status = "runnable";
|
|
345
|
-
|
|
346
|
-
input.schedule
|
|
206
|
+
await t.put("sessions", workflow.id, workflow);
|
|
207
|
+
scheduleAfterCommit(t, input.schedule, workflow.id, {
|
|
208
|
+
reason: "linked",
|
|
209
|
+
dedupeKey: `linked:${link.turnId}:${link.effectId}`,
|
|
210
|
+
});
|
|
347
211
|
}
|
|
348
|
-
/**
|
|
349
|
-
export function
|
|
350
|
-
|
|
351
|
-
if (effect.request?.kind !== "agent" || effect.status !== "pending")
|
|
352
|
-
continue;
|
|
353
|
-
const id = typeof effect.agentSessionId === "string"
|
|
354
|
-
? effect.agentSessionId
|
|
355
|
-
: undefined;
|
|
356
|
-
if (!id)
|
|
357
|
-
continue;
|
|
358
|
-
const agent = input.store.get("sessions", id);
|
|
359
|
-
const workflow = input.store.get("sessions", effect.request.sessionId);
|
|
360
|
-
if (workflow && workflow.status === "waiting") {
|
|
361
|
-
workflow.status = "runnable";
|
|
362
|
-
input.store.put("sessions", workflow.id, workflow);
|
|
363
|
-
input.schedule(workflow.id);
|
|
364
|
-
}
|
|
365
|
-
if (!agent)
|
|
366
|
-
continue;
|
|
367
|
-
if (agent.status === "completed") {
|
|
368
|
-
wakeLinkedWorkflow({
|
|
369
|
-
agentSessionId: id,
|
|
370
|
-
store: input.store,
|
|
371
|
-
output: agent.lastOutput ?? null,
|
|
372
|
-
schedule: input.schedule,
|
|
373
|
-
publish: input.publish,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
else if (agent.status === "failed") {
|
|
377
|
-
wakeLinkedWorkflow({
|
|
378
|
-
agentSessionId: id,
|
|
379
|
-
store: input.store,
|
|
380
|
-
failed: true,
|
|
381
|
-
error: agent.error,
|
|
382
|
-
schedule: input.schedule,
|
|
383
|
-
publish: input.publish,
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
else if (agent.status === "cancelled") {
|
|
387
|
-
wakeLinkedWorkflow({
|
|
388
|
-
agentSessionId: id,
|
|
389
|
-
store: input.store,
|
|
390
|
-
cancelled: true,
|
|
391
|
-
error: agent.error,
|
|
392
|
-
schedule: input.schedule,
|
|
393
|
-
publish: input.publish,
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
else if (agent.status === "running" || agent.status === "runnable") {
|
|
397
|
-
input.schedule(id);
|
|
398
|
-
}
|
|
399
|
-
else if (agent.status === "paused") {
|
|
400
|
-
// Linked pause surfaces on the workflow waits list; keep waiting.
|
|
401
|
-
if (workflow && workflow.status !== "paused") {
|
|
402
|
-
const waits = aggregateWaits({
|
|
403
|
-
store: input.store,
|
|
404
|
-
workflowSessionId: workflow.id,
|
|
405
|
-
});
|
|
406
|
-
if (waits.length > 0) {
|
|
407
|
-
workflow.status = "paused";
|
|
408
|
-
workflow.waits = waits;
|
|
409
|
-
input.store.put("sessions", workflow.id, workflow);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
/** Re-offer orphaned fn/verify claims after a process restart (SD-P7 / WF-C9). */
|
|
416
|
-
export function reofferOrphanedFnVerifyClaims(store) {
|
|
417
|
-
let n = 0;
|
|
418
|
-
for (const action of store.all("actions")) {
|
|
419
|
-
if (action.status === "claimed" &&
|
|
420
|
-
(action.kind === "fn" || action.kind === "verify")) {
|
|
421
|
-
action.status = "pending";
|
|
422
|
-
action.claimId = null;
|
|
423
|
-
action.leaseExpiresAt = null;
|
|
424
|
-
store.put("actions", action.actionId, action);
|
|
425
|
-
const s = store.get("sessions", action.sessionId);
|
|
426
|
-
if (s && (s.status === "waiting" || s.status === "running")) {
|
|
427
|
-
s.status = "runnable";
|
|
428
|
-
store.put("sessions", s.id, s);
|
|
429
|
-
}
|
|
430
|
-
n += 1;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
return n;
|
|
212
|
+
/** Pending workflow `agent` effects with a linked session, for the Tenant sweep. */
|
|
213
|
+
export async function pendingAgentEffects(t) {
|
|
214
|
+
return (await t.effectsWithStatus(["pending"], { kinds: ["agent"] })).filter((effect) => typeof effect.agentSessionId === "string");
|
|
434
215
|
}
|
|
435
216
|
/**
|
|
436
|
-
*
|
|
437
|
-
*
|
|
217
|
+
* Tenant sweep: settle one pending `agent` effect whose linked turn already finished (the
|
|
218
|
+
* linked settle normally does this in its own transaction). Runs in its own transaction and
|
|
219
|
+
* locks the linked agent (child) session before the workflow (parent), per the lock order in
|
|
220
|
+
* `store/types.ts`. Only the linked turn the effect started settles it (`linkedTurnEnd`): the
|
|
221
|
+
* linked session may still show an earlier iteration's settled turn while this effect's message
|
|
222
|
+
* is not committed. A linked session still `running` or `runnable` is left to the sweep's
|
|
223
|
+
* orphan re-wake; a paused one surfaces on the workflow when the workflow settles. Returns
|
|
224
|
+
* true when it changed something.
|
|
438
225
|
*/
|
|
439
|
-
export function
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
return
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return "skip";
|
|
465
|
-
}
|
|
466
|
-
/** Emit loop.verified / loop.decided after fn/verify actions complete. */
|
|
467
|
-
export function emitLoopActionEvents(input) {
|
|
468
|
-
const { action, store, session } = input;
|
|
469
|
-
if (action.kind !== "fn" && action.kind !== "verify")
|
|
470
|
-
return;
|
|
471
|
-
const n = Number(action.context?.n ?? 1);
|
|
472
|
-
const path = String(action.context?.loopPath ?? action.path ?? "");
|
|
473
|
-
if (action.kind === "verify") {
|
|
474
|
-
const verdict = input.value;
|
|
475
|
-
if (verdict?.kind === "failed")
|
|
476
|
-
return;
|
|
477
|
-
const event = store.event(session.id, session.activeTurnId, "loop.verified", {
|
|
478
|
-
path,
|
|
479
|
-
n,
|
|
480
|
-
pass: Boolean(verdict?.pass),
|
|
481
|
-
...(verdict?.feedback !== undefined
|
|
482
|
-
? { feedback: verdict.feedback }
|
|
483
|
-
: {}),
|
|
484
|
-
...(verdict?.data !== undefined ? { data: verdict.data } : {}),
|
|
485
|
-
});
|
|
486
|
-
input.publish(event);
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
if (action.context?.role !== "decide")
|
|
490
|
-
return;
|
|
491
|
-
const decision = input.value;
|
|
492
|
-
const event = store.event(session.id, session.activeTurnId, "loop.decided", {
|
|
493
|
-
path,
|
|
494
|
-
n,
|
|
495
|
-
next: "output" in decision && !("input" in decision) ? "output" : "input",
|
|
496
|
-
patched: Boolean(decision.agent),
|
|
226
|
+
export async function reconcilePendingAgentEffect(input) {
|
|
227
|
+
const { t } = input;
|
|
228
|
+
const found = await t.get("effects", input.effectId);
|
|
229
|
+
const id = found?.agentSessionId;
|
|
230
|
+
if (!found || found.request?.kind !== "agent" || typeof id !== "string")
|
|
231
|
+
return false;
|
|
232
|
+
const agent = await t.lockSession(id);
|
|
233
|
+
const workflow = await t.lockSession(found.request.sessionId);
|
|
234
|
+
// Read again under both locks.
|
|
235
|
+
const effect = await t.get("effects", input.effectId);
|
|
236
|
+
if (!effect || effect.status !== "pending" || !agent || !workflow)
|
|
237
|
+
return false;
|
|
238
|
+
const end = await linkedTurnEnd(t, effect, agent);
|
|
239
|
+
if (!end)
|
|
240
|
+
return false;
|
|
241
|
+
await wakeLinkedWorkflow({
|
|
242
|
+
t,
|
|
243
|
+
agentSessionId: id,
|
|
244
|
+
turnId: end.turnId,
|
|
245
|
+
...(end.status === "completed"
|
|
246
|
+
? { output: end.output }
|
|
247
|
+
: end.status === "failed"
|
|
248
|
+
? { failed: true, error: end.error }
|
|
249
|
+
: { cancelled: true, error: end.error }),
|
|
250
|
+
schedule: input.schedule,
|
|
497
251
|
});
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
export function emitNodeStarted(input, payload) {
|
|
501
|
-
const event = input.store.event(input.session.id, input.session.activeTurnId, "node.started", payload);
|
|
502
|
-
input.publish(event);
|
|
503
|
-
return event;
|
|
504
|
-
}
|
|
505
|
-
export function emitNodeCompleted(input, payload) {
|
|
506
|
-
const event = input.store.event(input.session.id, input.session.activeTurnId, "node.completed", payload);
|
|
507
|
-
input.publish(event);
|
|
508
|
-
return event;
|
|
509
|
-
}
|
|
510
|
-
export function emitNodeFailed(input, payload) {
|
|
511
|
-
const event = input.store.event(input.session.id, input.session.activeTurnId, "node.failed", payload);
|
|
512
|
-
input.publish(event);
|
|
513
|
-
return event;
|
|
252
|
+
return true;
|
|
514
253
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return event;
|
|
254
|
+
/** Claimed `fn`/`verify` Actions, for the re-offer after a Tenant opens. */
|
|
255
|
+
export async function claimedFnVerifyActions(t) {
|
|
256
|
+
return t.actionsWithStatus(["claimed"], { kinds: ["fn", "verify"] });
|
|
519
257
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Re-offer one claimed `fn`/`verify` Action (SD-P7 / WF-C9): they are pure or repeat-safe, so
|
|
260
|
+
* a claim left by an executor of an earlier process is offered again at once rather than
|
|
261
|
+
* after its lease. The next claim bumps the generation, which fences a late result. Locks
|
|
262
|
+
* only the Action's session. Returns true when it re-offered.
|
|
263
|
+
*/
|
|
264
|
+
export async function reofferFnVerifyClaim(t, actionId) {
|
|
265
|
+
const found = await t.get("actions", actionId);
|
|
266
|
+
if (!found)
|
|
267
|
+
return false;
|
|
268
|
+
await t.lockSession(found.sessionId);
|
|
269
|
+
// Read again under the session lock.
|
|
270
|
+
const action = await t.get("actions", actionId);
|
|
271
|
+
if (!action ||
|
|
272
|
+
action.status !== "claimed" ||
|
|
273
|
+
(action.kind !== "fn" && action.kind !== "verify"))
|
|
274
|
+
return false;
|
|
275
|
+
action.status = "pending";
|
|
276
|
+
action.claimId = null;
|
|
277
|
+
action.leaseExpiresAt = null;
|
|
278
|
+
await t.put("actions", action.actionId, action);
|
|
279
|
+
t.signalWork();
|
|
280
|
+
return true;
|
|
524
281
|
}
|
|
525
282
|
/** Path depth for deepest-first cancel ordering. */
|
|
526
283
|
export function pathDepth(path) {
|
|
@@ -532,23 +289,17 @@ export function pathDepth(path) {
|
|
|
532
289
|
* Plan a cancel cascade for a workflow session: linked agents deepest first,
|
|
533
290
|
* then pending → cancelled and claimed → uncertain actions (SD-P11 / WF-R53).
|
|
534
291
|
*/
|
|
535
|
-
export function planCancelCascade(input) {
|
|
536
|
-
|
|
537
|
-
const agentEntries =
|
|
538
|
-
for (const sess of input.store.all("sessions")) {
|
|
539
|
-
const link = input.store.get("links", sess.id);
|
|
540
|
-
if (!link || link.workflowSessionId !== input.workflowSessionId)
|
|
541
|
-
continue;
|
|
542
|
-
agentEntries.push({ sessionId: sess.id, path: link.path });
|
|
543
|
-
}
|
|
292
|
+
export async function planCancelCascade(input) {
|
|
293
|
+
const { t } = input;
|
|
294
|
+
const agentEntries = (await t.linkedSessions(input.workflowSessionId)).map((linked) => ({ sessionId: linked.agentSessionId, path: linked.link.path }));
|
|
544
295
|
agentEntries.sort((a, b) => pathDepth(b.path) - pathDepth(a.path));
|
|
545
296
|
const pendingActionIds = [];
|
|
546
297
|
const claimedActionIds = [];
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
298
|
+
const actions = await t.actionsForSession(input.workflowSessionId, {
|
|
299
|
+
...(input.turnId !== null ? { turnId: input.turnId } : {}),
|
|
300
|
+
statuses: OPEN_ACTION,
|
|
301
|
+
});
|
|
302
|
+
for (const action of actions) {
|
|
552
303
|
if (action.status === "pending")
|
|
553
304
|
pendingActionIds.push(action.actionId);
|
|
554
305
|
else if (action.status === "claimed")
|
|
@@ -567,13 +318,15 @@ export function planCancelCascade(input) {
|
|
|
567
318
|
* When `cancelEffectIds` is provided, those effects are marked cancelled and their
|
|
568
319
|
* paths are included in the sibling path set.
|
|
569
320
|
*/
|
|
570
|
-
export function cancelSiblingWork(input) {
|
|
321
|
+
export async function cancelSiblingWork(input) {
|
|
322
|
+
const { t } = input;
|
|
323
|
+
await t.lockSession(input.workflowSessionId);
|
|
571
324
|
const cancelledActions = [];
|
|
572
325
|
const uncertainActions = [];
|
|
573
326
|
const agentSessionIds = [];
|
|
574
327
|
const siblingPaths = new Set(input.siblingPaths ?? []);
|
|
575
328
|
for (const effectId of input.cancelEffectIds ?? []) {
|
|
576
|
-
const effect =
|
|
329
|
+
const effect = await t.get("effects", effectId);
|
|
577
330
|
if (!effect)
|
|
578
331
|
continue;
|
|
579
332
|
const path = typeof effect.request?.path === "string"
|
|
@@ -585,7 +338,7 @@ export function cancelSiblingWork(input) {
|
|
|
585
338
|
effect.status === "queued" ||
|
|
586
339
|
effect.status === "uncertain") {
|
|
587
340
|
effect.status = "cancelled";
|
|
588
|
-
|
|
341
|
+
await t.put("effects", effectId, effect);
|
|
589
342
|
}
|
|
590
343
|
}
|
|
591
344
|
const matchesSibling = (path) => {
|
|
@@ -593,66 +346,56 @@ export function cancelSiblingWork(input) {
|
|
|
593
346
|
return false;
|
|
594
347
|
return [...siblingPaths].some((sib) => path === sib || path.startsWith(`${sib}/`));
|
|
595
348
|
};
|
|
596
|
-
for (const
|
|
597
|
-
const link = input.store.get("links", sess.id);
|
|
598
|
-
if (!link || link.workflowSessionId !== input.workflowSessionId)
|
|
599
|
-
continue;
|
|
349
|
+
for (const { agentSessionId, link, session } of await t.linkedSessions(input.workflowSessionId)) {
|
|
600
350
|
if (!matchesSibling(link.path))
|
|
601
351
|
continue;
|
|
602
|
-
if (
|
|
603
|
-
["running", "runnable", "paused", "waiting"].includes(
|
|
604
|
-
agentSessionIds.push(
|
|
352
|
+
if (session.activeTurnId &&
|
|
353
|
+
["running", "runnable", "paused", "waiting"].includes(session.status))
|
|
354
|
+
agentSessionIds.push(agentSessionId);
|
|
605
355
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
356
|
+
const actions = await t.actionsForSession(input.workflowSessionId, {
|
|
357
|
+
turnId: input.turnId,
|
|
358
|
+
statuses: OPEN_ACTION,
|
|
359
|
+
});
|
|
360
|
+
for (const action of actions) {
|
|
611
361
|
if (!matchesSibling(action.path))
|
|
612
362
|
continue;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
cancelledActions.push(action.actionId);
|
|
363
|
+
const next = action.status === "claimed" ? "uncertain" : "cancelled";
|
|
364
|
+
action.status = next;
|
|
365
|
+
await t.put("actions", action.actionId, action);
|
|
366
|
+
const effect = await t.get("effects", action.actionId);
|
|
367
|
+
if (effect) {
|
|
368
|
+
effect.status = next;
|
|
369
|
+
await t.put("effects", action.actionId, effect);
|
|
622
370
|
}
|
|
623
|
-
|
|
624
|
-
action.
|
|
625
|
-
|
|
626
|
-
const effect = input.store.get("effects", action.actionId);
|
|
627
|
-
if (effect) {
|
|
628
|
-
effect.status = "uncertain";
|
|
629
|
-
input.store.put("effects", action.actionId, effect);
|
|
630
|
-
}
|
|
371
|
+
if (next === "cancelled")
|
|
372
|
+
cancelledActions.push(action.actionId);
|
|
373
|
+
else
|
|
631
374
|
uncertainActions.push(action.actionId);
|
|
632
|
-
}
|
|
633
375
|
}
|
|
634
376
|
return { cancelledActions, uncertainActions, agentSessionIds };
|
|
635
377
|
}
|
|
636
378
|
/**
|
|
637
379
|
* Apply cancel fencing to workflow-session actions (pending→cancelled, claimed→uncertain).
|
|
638
380
|
*/
|
|
639
|
-
export function fenceWorkflowActions(input) {
|
|
381
|
+
export async function fenceWorkflowActions(input) {
|
|
382
|
+
const { t } = input;
|
|
383
|
+
await t.lockSession(input.workflowSessionId);
|
|
384
|
+
const turn = input.turnId !== null ? { turnId: input.turnId } : {};
|
|
640
385
|
const cancelled = [];
|
|
641
386
|
const uncertain = [];
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (!["pending", "claimed"].includes(action.status))
|
|
648
|
-
continue;
|
|
387
|
+
const actions = await t.actionsForSession(input.workflowSessionId, {
|
|
388
|
+
...turn,
|
|
389
|
+
statuses: OPEN_ACTION,
|
|
390
|
+
});
|
|
391
|
+
for (const action of actions) {
|
|
649
392
|
const next = action.status === "claimed" ? "uncertain" : "cancelled";
|
|
650
393
|
action.status = next;
|
|
651
|
-
|
|
652
|
-
const effect =
|
|
394
|
+
await t.put("actions", action.actionId, action);
|
|
395
|
+
const effect = await t.get("effects", action.actionId);
|
|
653
396
|
if (effect) {
|
|
654
397
|
effect.status = next;
|
|
655
|
-
|
|
398
|
+
await t.put("effects", action.actionId, effect);
|
|
656
399
|
}
|
|
657
400
|
if (next === "cancelled")
|
|
658
401
|
cancelled.push(action.actionId);
|
|
@@ -660,85 +403,77 @@ export function fenceWorkflowActions(input) {
|
|
|
660
403
|
uncertain.push(action.actionId);
|
|
661
404
|
}
|
|
662
405
|
// Drop queued effects that never started.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (effect.status === "queued") {
|
|
669
|
-
effect.status = "cancelled";
|
|
670
|
-
input.store.put("effects", effect.request.effectId, effect);
|
|
671
|
-
cancelled.push(effect.request.effectId);
|
|
672
|
-
}
|
|
406
|
+
const queued = await t.effectsForSession(input.workflowSessionId, { ...turn, statuses: ["queued"] });
|
|
407
|
+
for (const effect of queued) {
|
|
408
|
+
effect.status = "cancelled";
|
|
409
|
+
await t.put("effects", effect.request.effectId, effect);
|
|
410
|
+
cancelled.push(effect.request.effectId);
|
|
673
411
|
}
|
|
674
412
|
return { cancelled, uncertain };
|
|
675
413
|
}
|
|
414
|
+
function waitsFromSession(sessionId, path, raw) {
|
|
415
|
+
if (!Array.isArray(raw))
|
|
416
|
+
return [];
|
|
417
|
+
const waits = [];
|
|
418
|
+
for (const call of raw) {
|
|
419
|
+
const interaction = call.interaction ?? call;
|
|
420
|
+
const interactionId = String(interaction?.id ?? call.interactionId ?? "");
|
|
421
|
+
if (!interactionId)
|
|
422
|
+
continue;
|
|
423
|
+
waits.push({
|
|
424
|
+
sessionId,
|
|
425
|
+
path,
|
|
426
|
+
interactionId,
|
|
427
|
+
kind: String(interaction?.kind ?? call.kind ?? "approval"),
|
|
428
|
+
...(call.invocationId !== undefined
|
|
429
|
+
? { invocationId: String(call.invocationId) }
|
|
430
|
+
: {}),
|
|
431
|
+
...(call.wait !== undefined ? { wait: call.wait } : {}),
|
|
432
|
+
...(call.status !== undefined ? { status: String(call.status) } : {}),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return waits;
|
|
436
|
+
}
|
|
676
437
|
/**
|
|
677
438
|
* Aggregate human waits across the workflow session and linked agent sessions (WF-R51).
|
|
678
439
|
*/
|
|
679
|
-
export function aggregateWaits(input) {
|
|
680
|
-
const
|
|
681
|
-
const workflow =
|
|
440
|
+
export async function aggregateWaits(input) {
|
|
441
|
+
const { t } = input;
|
|
442
|
+
const workflow = await t.get("sessions", input.workflowSessionId);
|
|
682
443
|
if (!workflow)
|
|
683
|
-
return
|
|
684
|
-
const
|
|
685
|
-
if (!Array.isArray(raw))
|
|
686
|
-
return;
|
|
687
|
-
for (const call of raw) {
|
|
688
|
-
const interaction = call.interaction ?? call;
|
|
689
|
-
const interactionId = String(interaction?.id ?? call.interactionId ?? "");
|
|
690
|
-
if (!interactionId)
|
|
691
|
-
continue;
|
|
692
|
-
waits.push({
|
|
693
|
-
sessionId: session.id,
|
|
694
|
-
path,
|
|
695
|
-
interactionId,
|
|
696
|
-
kind: String(interaction?.kind ?? call.kind ?? "approval"),
|
|
697
|
-
...(call.invocationId !== undefined
|
|
698
|
-
? { invocationId: String(call.invocationId) }
|
|
699
|
-
: {}),
|
|
700
|
-
...(call.wait !== undefined ? { wait: call.wait } : {}),
|
|
701
|
-
...(call.status !== undefined ? { status: String(call.status) } : {}),
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
};
|
|
444
|
+
return [];
|
|
445
|
+
const waits = [];
|
|
705
446
|
// Workflow-owned interactions (tool-node / verify approvals) live on the workflow session.
|
|
706
447
|
if (Array.isArray(workflow.waits))
|
|
707
|
-
|
|
448
|
+
waits.push(...waitsFromSession(workflow.id, "", workflow.waits));
|
|
708
449
|
else if (workflow.waits &&
|
|
709
450
|
typeof workflow.waits === "object" &&
|
|
710
451
|
Array.isArray(workflow.waits.interactions))
|
|
711
|
-
|
|
712
|
-
for (const
|
|
713
|
-
|
|
714
|
-
if (!link || link.workflowSessionId !== input.workflowSessionId)
|
|
715
|
-
continue;
|
|
716
|
-
if (sess.status !== "paused")
|
|
452
|
+
waits.push(...waitsFromSession(workflow.id, "", workflow.waits.interactions));
|
|
453
|
+
for (const { agentSessionId, link, session } of await t.linkedSessions(input.workflowSessionId)) {
|
|
454
|
+
if (session.status !== "paused")
|
|
717
455
|
continue;
|
|
718
|
-
|
|
456
|
+
waits.push(...waitsFromSession(agentSessionId, link.path, session.waits));
|
|
719
457
|
}
|
|
720
458
|
return waits;
|
|
721
459
|
}
|
|
722
460
|
/**
|
|
723
461
|
* Look up which session owns an interaction id. Used for 409 foreign approvals (WF-R52).
|
|
724
462
|
*/
|
|
725
|
-
export function findInteractionOwner(input) {
|
|
726
|
-
const waits = aggregateWaits(input);
|
|
463
|
+
export async function findInteractionOwner(input) {
|
|
464
|
+
const waits = await aggregateWaits(input);
|
|
727
465
|
const hit = waits.find((w) => w.interactionId === input.interactionId);
|
|
728
466
|
if (hit)
|
|
729
467
|
return { sessionId: hit.sessionId, path: hit.path };
|
|
730
|
-
// Also scan linked
|
|
731
|
-
for (const
|
|
732
|
-
const
|
|
733
|
-
if (!link || link.workflowSessionId !== input.workflowSessionId)
|
|
734
|
-
continue;
|
|
735
|
-
const raw = sess.waits;
|
|
468
|
+
// Also scan linked sessions' waits that aggregateWaits skipped (not paused).
|
|
469
|
+
for (const { agentSessionId, link, session } of await input.t.linkedSessions(input.workflowSessionId)) {
|
|
470
|
+
const raw = session.waits;
|
|
736
471
|
if (!Array.isArray(raw))
|
|
737
472
|
continue;
|
|
738
473
|
for (const call of raw) {
|
|
739
474
|
const id = String(call.interaction?.id ?? call.interactionId ?? "");
|
|
740
475
|
if (id === input.interactionId)
|
|
741
|
-
return { sessionId:
|
|
476
|
+
return { sessionId: agentSessionId, path: link.path };
|
|
742
477
|
}
|
|
743
478
|
}
|
|
744
479
|
return undefined;
|
|
@@ -747,8 +482,8 @@ export function findInteractionOwner(input) {
|
|
|
747
482
|
* If approve/respond targets an interaction owned by a linked agent session,
|
|
748
483
|
* return a 409 message naming that session (WF-R52 / LOOP-A4).
|
|
749
484
|
*/
|
|
750
|
-
export function foreignInteractionConflict(input) {
|
|
751
|
-
const owner = findInteractionOwner(input);
|
|
485
|
+
export async function foreignInteractionConflict(input) {
|
|
486
|
+
const owner = await findInteractionOwner(input);
|
|
752
487
|
if (!owner)
|
|
753
488
|
return undefined;
|
|
754
489
|
if (owner.sessionId === input.workflowSessionId)
|
|
@@ -761,23 +496,23 @@ export function foreignInteractionConflict(input) {
|
|
|
761
496
|
}
|
|
762
497
|
/**
|
|
763
498
|
* When concurrency slots free, re-enter the workflow so queued effects can start
|
|
764
|
-
* (status stays `queued` until
|
|
499
|
+
* (status stays `queued` until the effect resolver dispatches them). Returns
|
|
500
|
+
* true when a wake was scheduled.
|
|
765
501
|
*/
|
|
766
|
-
export function wakeForQueuedEffects(input) {
|
|
767
|
-
|
|
502
|
+
export async function wakeForQueuedEffects(input) {
|
|
503
|
+
const { t } = input;
|
|
504
|
+
if (!mayDispatchMore(await countActiveFlowWork(t, input.workflowSessionId, input.turnId), input.limits))
|
|
768
505
|
return false;
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
.some((e) => e.status === "queued" &&
|
|
772
|
-
e.request?.sessionId === input.workflowSessionId &&
|
|
773
|
-
e.request?.turnId === input.turnId);
|
|
774
|
-
if (!hasQueued)
|
|
506
|
+
const queued = await t.effectsForTurn(input.workflowSessionId, input.turnId, ["queued"]);
|
|
507
|
+
if (queued.length === 0)
|
|
775
508
|
return false;
|
|
776
|
-
const workflow =
|
|
509
|
+
const workflow = await t.lockSession(input.workflowSessionId);
|
|
777
510
|
if (workflow && workflow.status === "waiting") {
|
|
778
511
|
workflow.status = "runnable";
|
|
779
|
-
|
|
512
|
+
await t.put("sessions", workflow.id, workflow);
|
|
780
513
|
}
|
|
781
|
-
input.schedule
|
|
514
|
+
scheduleAfterCommit(t, input.schedule, input.workflowSessionId, {
|
|
515
|
+
reason: "flow",
|
|
516
|
+
});
|
|
782
517
|
return true;
|
|
783
518
|
}
|