@nylorun/runtime 0.22.0-beta → 0.24.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.
Files changed (273) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +95 -30
  3. package/dist/adapters/execution/restate.d.ts +15 -3
  4. package/dist/adapters/execution/restate.js +78 -84
  5. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  6. package/dist/adapters/replication/pgoutput.js +6 -3
  7. package/dist/adapters/sandbox/local.d.ts +9 -1
  8. package/dist/adapters/sandbox/local.js +16 -4
  9. package/dist/adapters/sandbox/virtual.js +4 -1
  10. package/dist/adapters/streams/s2.js +1 -13
  11. package/dist/api/a2a/endpoint.js +5 -1
  12. package/dist/api/a2a/routes.d.ts +2 -0
  13. package/dist/api/a2a/routes.js +2 -2
  14. package/dist/api/a2a/tasks.js +2 -1
  15. package/dist/api/ag-ui/endpoint.js +4 -2
  16. package/dist/api/ag-ui/history.js +4 -1
  17. package/dist/api/ag-ui/routes.d.ts +4 -1
  18. package/dist/api/ag-ui/routes.js +11 -10
  19. package/dist/api/ag-ui/translate.js +3 -2
  20. package/dist/api/components.d.ts +4953 -1204
  21. package/dist/api/components.js +25 -16
  22. package/dist/api/gate/keys.js +7 -1
  23. package/dist/api/gate/routes.d.ts +6 -0
  24. package/dist/api/gate/routes.js +39 -3
  25. package/dist/api/http/app.js +4 -4
  26. package/dist/api/http/define.d.ts +6 -0
  27. package/dist/api/http/define.js +13 -1
  28. package/dist/api/http/respond.js +1 -1
  29. package/dist/api/http/routes/artifacts.js +35 -8
  30. package/dist/api/http/routes/coverage.d.ts +9 -0
  31. package/dist/api/http/routes/coverage.js +34 -0
  32. package/dist/api/http/routes/environments.d.ts +11 -0
  33. package/dist/api/http/routes/environments.js +283 -0
  34. package/dist/api/http/routes/mcp.d.ts +1 -1
  35. package/dist/api/http/routes/mcp.js +2 -2
  36. package/dist/api/http/routes/me.js +1 -1
  37. package/dist/api/http/routes/reads.d.ts +45 -9
  38. package/dist/api/http/routes/reads.js +54 -5
  39. package/dist/api/http/routes/sessions.js +11 -6
  40. package/dist/api/http/routes/tenant.js +32 -10
  41. package/dist/api/openapi.js +31 -26
  42. package/dist/artifacts/disk.d.ts +59 -0
  43. package/dist/artifacts/disk.js +563 -0
  44. package/dist/artifacts/export.d.ts +1 -1
  45. package/dist/artifacts/export.js +2 -3
  46. package/dist/artifacts/folders.d.ts +6 -3
  47. package/dist/artifacts/folders.js +17 -12
  48. package/dist/artifacts/service.d.ts +2 -1
  49. package/dist/artifacts/service.js +6 -1
  50. package/dist/artifacts/workspace.d.ts +1 -1
  51. package/dist/artifacts/workspace.js +1 -1
  52. package/dist/core/flow-host.d.ts +3 -22
  53. package/dist/core/flow-host.js +11 -37
  54. package/dist/core/provider.d.ts +3 -2
  55. package/dist/core/provider.js +5 -37
  56. package/dist/core/sandbox-routes.d.ts +2 -21
  57. package/dist/core/sandbox-routes.js +10 -43
  58. package/dist/execution/types.d.ts +11 -9
  59. package/dist/execution/types.js +5 -7
  60. package/dist/gates/contract.d.ts +3 -24
  61. package/dist/gates/contract.js +1 -36
  62. package/dist/gates/egress-credentials.d.ts +9 -0
  63. package/dist/gates/egress-credentials.js +11 -4
  64. package/dist/gates/egress.d.ts +28 -1
  65. package/dist/gates/egress.js +115 -10
  66. package/dist/gates/fixture-model.d.ts +28 -0
  67. package/dist/gates/fixture-model.js +92 -0
  68. package/dist/gates/handler.js +4 -4
  69. package/dist/gates/http-tool.d.ts +6 -2
  70. package/dist/gates/http-tool.js +3 -5
  71. package/dist/gates/in-process.d.ts +5 -0
  72. package/dist/gates/in-process.js +12 -1
  73. package/dist/gates/journal.d.ts +30 -0
  74. package/dist/gates/journal.js +52 -0
  75. package/dist/gates/mcp-handler.d.ts +4 -0
  76. package/dist/gates/mcp-handler.js +5 -1
  77. package/dist/gates/meter.d.ts +10 -2
  78. package/dist/gates/meter.js +20 -5
  79. package/dist/gates/policy.d.ts +49 -0
  80. package/dist/gates/policy.js +134 -0
  81. package/dist/gates/tenant-vaults.d.ts +7 -7
  82. package/dist/gates/tenant-vaults.js +5 -5
  83. package/dist/gates/tool-calls.d.ts +10 -5
  84. package/dist/gates/tool-calls.js +28 -10
  85. package/dist/gates/tool-client.d.ts +8 -0
  86. package/dist/gates/tool-client.js +27 -11
  87. package/dist/gates/tool-contract.d.ts +10 -68
  88. package/dist/gates/tool-contract.js +1 -85
  89. package/dist/gates/tool-gate.d.ts +1 -1
  90. package/dist/harness/calls.d.ts +12 -5
  91. package/dist/harness/calls.js +21 -12
  92. package/dist/harness/executors.js +12 -9
  93. package/dist/harness/main.js +18 -5
  94. package/dist/harness/pod.d.ts +34 -0
  95. package/dist/harness/pod.js +41 -0
  96. package/dist/harness/service.d.ts +0 -1
  97. package/dist/harness/service.js +13 -5
  98. package/dist/harness/setup.d.ts +27 -0
  99. package/dist/harness/setup.js +150 -0
  100. package/dist/harness/ws-client.js +3 -2
  101. package/dist/harness/ws-port.d.ts +1 -5
  102. package/dist/harness/ws-port.js +1 -5
  103. package/dist/harness-api/in-process.js +3 -3
  104. package/dist/harness-api/record.d.ts +15 -1
  105. package/dist/harness-api/record.js +174 -44
  106. package/dist/harness-api/redact.d.ts +3 -0
  107. package/dist/harness-api/redact.js +45 -0
  108. package/dist/harness-api/server.d.ts +8 -0
  109. package/dist/harness-api/server.js +93 -13
  110. package/dist/harness-api/start.d.ts +10 -3
  111. package/dist/harness-api/start.js +40 -10
  112. package/dist/harness-api/tool-journal.d.ts +96 -0
  113. package/dist/harness-api/tool-journal.js +102 -0
  114. package/dist/harness-api/tool-status.d.ts +13 -0
  115. package/dist/harness-api/tool-status.js +26 -0
  116. package/dist/harness-api/workspace.d.ts +6 -3
  117. package/dist/harness-api/workspace.js +27 -11
  118. package/dist/harness-api/ws-server.d.ts +11 -3
  119. package/dist/harness-api/ws-server.js +94 -15
  120. package/dist/host/execution.d.ts +8 -6
  121. package/dist/host/gates.d.ts +2 -1
  122. package/dist/host/gates.js +10 -2
  123. package/dist/host/main.js +64 -21
  124. package/dist/host/operate.js +67 -5
  125. package/dist/host/stack-config.d.ts +70 -0
  126. package/dist/host/stack-config.js +67 -1
  127. package/dist/infra/execution.d.ts +18 -1
  128. package/dist/infra/execution.js +28 -4
  129. package/dist/infra/index.d.ts +1 -1
  130. package/dist/keys/client.js +4 -1
  131. package/dist/keys/keys.d.ts +5 -5
  132. package/dist/keys/keys.js +5 -7
  133. package/dist/management-openapi.json +999 -2211
  134. package/dist/mcp/connect.d.ts +25 -20
  135. package/dist/mcp/connect.js +29 -30
  136. package/dist/mcp/pool.d.ts +2 -2
  137. package/dist/mcp/pool.js +1 -1
  138. package/dist/mcp/preview.d.ts +3 -3
  139. package/dist/mcp/preview.js +10 -20
  140. package/dist/mcp/snapshot.d.ts +5 -11
  141. package/dist/mcp/snapshot.js +6 -7
  142. package/dist/model/pi-model.js +8 -0
  143. package/dist/openapi.json +6955 -2526
  144. package/dist/reads/types.d.ts +16 -3
  145. package/dist/record/append.d.ts +3 -1
  146. package/dist/record/append.js +1 -0
  147. package/dist/record/envelope.d.ts +5 -1
  148. package/dist/record/envelope.js +7 -3
  149. package/dist/record/environment.d.ts +25 -0
  150. package/dist/record/environment.js +44 -0
  151. package/dist/record/index.d.ts +1 -1
  152. package/dist/record/index.js +1 -1
  153. package/dist/retry.d.ts +21 -0
  154. package/dist/retry.js +34 -0
  155. package/dist/sandbox/disk-fs.d.ts +95 -0
  156. package/dist/sandbox/disk-fs.js +206 -0
  157. package/dist/sandbox/join.d.ts +17 -18
  158. package/dist/sandbox/join.js +46 -20
  159. package/dist/sandbox/manager.d.ts +16 -8
  160. package/dist/sandbox/manager.js +33 -17
  161. package/dist/sandbox/placement.d.ts +13 -12
  162. package/dist/sandbox/placement.js +29 -12
  163. package/dist/sandbox/pods/client.d.ts +6 -1
  164. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  165. package/dist/sandbox/pods/lifecycle.js +14 -26
  166. package/dist/sandbox/pods/reconcile.js +21 -15
  167. package/dist/sandbox/pods/spec.d.ts +15 -13
  168. package/dist/sandbox/pods/spec.js +16 -20
  169. package/dist/sandbox/records.d.ts +2 -2
  170. package/dist/sandbox/records.js +3 -3
  171. package/dist/sandbox/resolve.d.ts +35 -27
  172. package/dist/sandbox/resolve.js +66 -41
  173. package/dist/sandbox/session-sandbox.js +1 -1
  174. package/dist/sandbox/setup.d.ts +61 -0
  175. package/dist/sandbox/setup.js +123 -0
  176. package/dist/sandbox/share.d.ts +11 -21
  177. package/dist/sandbox/share.js +12 -41
  178. package/dist/sandbox/tenant-config.d.ts +12 -14
  179. package/dist/sandbox/tenant-config.js +12 -17
  180. package/dist/sandbox/types.d.ts +6 -0
  181. package/dist/store/postgres/control.js +8 -1
  182. package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
  183. package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -0
  184. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  185. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  186. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  187. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  188. package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
  189. package/dist/store/postgres/reads.js +117 -17
  190. package/dist/store/postgres/record-writer.d.ts +4 -4
  191. package/dist/store/postgres/record-writer.js +9 -9
  192. package/dist/store/postgres/record.d.ts +7 -0
  193. package/dist/store/postgres/record.js +23 -2
  194. package/dist/store/postgres/schema.d.ts +1213 -779
  195. package/dist/store/postgres/schema.js +136 -76
  196. package/dist/store/postgres/store.js +283 -117
  197. package/dist/store/types.d.ts +206 -88
  198. package/dist/streams/relay/core.js +1 -13
  199. package/dist/tenant/advance.d.ts +7 -6
  200. package/dist/tenant/advance.js +121 -49
  201. package/dist/tenant/artifact-tool.js +3 -4
  202. package/dist/tenant/commands.d.ts +15 -2
  203. package/dist/tenant/commands.js +94 -14
  204. package/dist/tenant/context.d.ts +67 -33
  205. package/dist/tenant/context.js +4 -15
  206. package/dist/tenant/control.d.ts +8 -1
  207. package/dist/tenant/control.js +2 -0
  208. package/dist/tenant/effects.d.ts +1 -1
  209. package/dist/tenant/effects.js +37 -20
  210. package/dist/tenant/environments.d.ts +167 -0
  211. package/dist/tenant/environments.js +823 -0
  212. package/dist/tenant/harness-tokens.d.ts +21 -0
  213. package/dist/tenant/harness-tokens.js +61 -0
  214. package/dist/tenant/host-token.d.ts +1 -1
  215. package/dist/tenant/host-token.js +2 -4
  216. package/dist/tenant/identity-file.d.ts +4 -4
  217. package/dist/tenant/identity-file.js +17 -8
  218. package/dist/tenant/issuers.js +3 -3
  219. package/dist/tenant/principals.js +1 -1
  220. package/dist/tenant/reset.js +3 -3
  221. package/dist/tenant/runtime.d.ts +6 -1
  222. package/dist/tenant/runtime.js +30 -4
  223. package/dist/tenant/scheduler.d.ts +7 -0
  224. package/dist/tenant/scheduler.js +38 -7
  225. package/dist/tenant/session-streams.d.ts +0 -1
  226. package/dist/tenant/session-streams.js +1 -13
  227. package/dist/tenant/session.d.ts +3 -3
  228. package/dist/tenant/session.js +0 -14
  229. package/dist/tenant/sessions.d.ts +13 -13
  230. package/dist/tenant/sessions.js +61 -138
  231. package/dist/tenant/signing-keys.d.ts +5 -1
  232. package/dist/tenant/status.js +3 -3
  233. package/dist/tenant/streams.js +14 -29
  234. package/dist/tenant/sweep.d.ts +39 -12
  235. package/dist/tenant/sweep.js +168 -63
  236. package/dist/tenant/transcript.d.ts +210 -20
  237. package/dist/tenant/transcript.js +62 -11
  238. package/dist/tenant/turn-timing.d.ts +43 -0
  239. package/dist/tenant/turn-timing.js +39 -0
  240. package/dist/tenant/types.d.ts +2 -0
  241. package/dist/traces/ids.d.ts +42 -0
  242. package/dist/traces/ids.js +85 -0
  243. package/dist/traces/key.d.ts +6 -0
  244. package/dist/traces/key.js +37 -0
  245. package/dist/traces/map.d.ts +113 -0
  246. package/dist/traces/map.js +317 -0
  247. package/dist/traces/options.d.ts +7 -0
  248. package/dist/traces/options.js +1 -0
  249. package/dist/traces/otlp.d.ts +42 -0
  250. package/dist/traces/otlp.js +84 -0
  251. package/dist/traces/projector.d.ts +58 -0
  252. package/dist/traces/projector.js +200 -0
  253. package/dist/traces/read.d.ts +23 -0
  254. package/dist/traces/read.js +16 -0
  255. package/dist/vault/coverage.d.ts +26 -0
  256. package/dist/vault/coverage.js +64 -0
  257. package/dist/vault/error.d.ts +11 -1
  258. package/dist/vault/error.js +5 -1
  259. package/dist/vault/service.d.ts +47 -48
  260. package/dist/vault/service.js +321 -382
  261. package/dist/vault/sources.d.ts +5 -16
  262. package/dist/vault/sources.js +6 -8
  263. package/dist/version.d.ts +1 -1
  264. package/dist/version.js +2 -2
  265. package/package.json +14 -13
  266. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  267. package/dist/api/http/routes/sandboxes.js +0 -153
  268. package/dist/api/http/routes/vaults.d.ts +0 -10
  269. package/dist/api/http/routes/vaults.js +0 -139
  270. package/dist/record/sandbox.d.ts +0 -25
  271. package/dist/record/sandbox.js +0 -43
  272. package/dist/tenant/sandboxes.d.ts +0 -122
  273. package/dist/tenant/sandboxes.js +0 -562
@@ -40,9 +40,13 @@
40
40
  * already recorded; an `ownership.lost` leaves the session to the advance that owns it.
41
41
  */
42
42
  import { runDurable, runFlowDurable, } from "@nylorun/harness/run";
43
+ import { redactTokens } from "../harness-api/redact.js";
43
44
  import { aggregateWaits, cancelSiblingWork, isWorkflowManifest, wakeLinkedWorkflow, } from "../core/flow-host.js";
44
45
  import { isOwnershipLost, ownedTx } from "../store/ownership.js";
45
- import { isGateToolEffect, recoversModelCalls, recoversToolCalls } from "./effects.js";
46
+ import { endAdvance } from "./scheduler.js";
47
+ import { harnessOf } from "./context.js";
48
+ import { HARNESS_ID } from "../sandbox/placement.js";
49
+ import { recoversModelCalls } from "./effects.js";
46
50
  import { slimModelEffects } from "./slim.js";
47
51
  import { applyUpdates, leanState, transcriptShadow, withTranscript, } from "./history.js";
48
52
  import { command } from "./commands.js";
@@ -50,7 +54,8 @@ import { exportOutputs } from "../artifacts/export.js";
50
54
  import { dropRunGrant, grantRun } from "./run-grants.js";
51
55
  import { startHeartbeat } from "../harness-api/renew.js";
52
56
  import { buildTurnStart, startSegment } from "../harness-api/start.js";
53
- import { touchPodSandbox } from "./sandboxes.js";
57
+ import { touchEnvironment } from "./environments.js";
58
+ import { endTurn, pauseTurn } from "./turn-timing.js";
54
59
  import { AdvanceAbort, AdvanceDeadlineError, abortKind, } from "./worker.js";
55
60
  /**
56
61
  * A segment stopped by a `shutdown` or `ownership.lost` abort before it settled. Never
@@ -169,10 +174,7 @@ export async function advance(ctx, id, signal) {
169
174
  heartbeat.stop();
170
175
  dropRunGrant(ctx, lease);
171
176
  signal.removeEventListener("abort", forward);
172
- if (ctx.work.running.get(id) === controller) {
173
- ctx.work.running.delete(id);
174
- ctx.work.runningTurns.delete(id);
175
- }
177
+ endAdvance(ctx.work, id, controller);
176
178
  // Best effort: a release that fails leaves a lease that simply expires.
177
179
  if (release)
178
180
  await ctx.store
@@ -181,28 +183,27 @@ export async function advance(ctx, id, signal) {
181
183
  sessionId: id,
182
184
  message: error instanceof Error ? error.message : String(error),
183
185
  }));
184
- // A pod sandbox's idle time counts from the end of each segment (D34).
185
- if (placed.pod !== undefined)
186
- await touchPodSandbox(ctx, placed.pod);
186
+ // The Environment was used: `deleteAfter`, and a pod's idle time (D34), count from the end
187
+ // of each segment.
188
+ if (placed.environment !== undefined)
189
+ await touchEnvironment(ctx, placed.environment);
187
190
  }
188
191
  return result;
189
192
  }
190
- /** Why a pod sandbox cannot run a segment, or undefined when it can. */
193
+ /** Why a pod Environment cannot run a segment, or undefined when it can. */
191
194
  function podUnusable(ctx, segment) {
192
195
  const pod = segment.pod;
193
196
  if (!pod)
194
197
  return undefined;
195
198
  if (!ctx.pods)
196
- return `Sandbox ${pod.id} is a pod sandbox, and this Runtime has no sandbox pods`;
199
+ return `Environment ${pod.id} is a pod Environment, and this Runtime has no sandbox pods`;
197
200
  const state = pod.pod;
198
201
  if (state.desired === "deleted")
199
- return `Sandbox ${pod.id} was deleted`;
202
+ return `Environment ${pod.id} was deleted`;
200
203
  if (state.observed === "lost")
201
- return `Sandbox ${pod.id} was lost: ${state.reason ?? "its volume is gone"}`;
202
- if (state.observed === "expired")
203
- return `Sandbox ${pod.id} expired`;
204
+ return `Environment ${pod.id} was lost: ${state.reason ?? "its volume is gone"}`;
204
205
  if (state.observed === "failed")
205
- return `Sandbox ${pod.id} failed to start: ${state.reason ?? "its pod was not ready"}`;
206
+ return `Environment ${pod.id} failed to start: ${state.reason ?? "its pod was not ready"}`;
206
207
  return undefined;
207
208
  }
208
209
  /**
@@ -231,7 +232,6 @@ async function takeOwnership(ctx, id) {
231
232
  if (taken.takeover)
232
233
  await takeOver(t, s, {
233
234
  recoversModelCalls: recoversModelCalls(ctx),
234
- recoversToolCalls: recoversToolCalls(ctx),
235
235
  });
236
236
  if (!s.checkpoint || !["running", "runnable"].includes(s.status)) {
237
237
  await t.releaseOwnership(id, lease.owner, lease.epoch);
@@ -245,10 +245,12 @@ async function takeOwnership(ctx, id) {
245
245
  /**
246
246
  * Takeover (§10.5 step 2, §11.4), inside the transaction that took ownership: the dead
247
247
  * owner's `invoking` effects become `uncertain` and are never invoked again, except model
248
- * calls when the gate recovers them (P1.2) and remote MCP and HTTP tool calls when the Tool
249
- * Gate does (F4.1 G3): those stay `invoking`, and the replay re-sends them. When one belongs
250
- * to the active turn, the session becomes `uncertain` too, with an `effect.uncertain` event.
251
- * Mutates and writes `s`. Returns the effect ids it marked.
248
+ * calls when the gate recovers them (P1.2) and every tool call (R5.3): those stay `invoking`,
249
+ * and the replay decides. It re-sends a model call, and an MCP or HTTP tool call with the same
250
+ * key; a sandbox or core-served tool call fails to the model as `tool.interrupted`
251
+ * (`harness-api/tool-journal.ts`). When an effect marked `uncertain` belongs to the active
252
+ * turn, the session becomes `uncertain` too, with an `effect.uncertain` event. Mutates and
253
+ * writes `s`. Returns the effect ids it marked.
252
254
  */
253
255
  export async function takeOver(t, s, options = {}) {
254
256
  const marked = [];
@@ -256,7 +258,7 @@ export async function takeOver(t, s, options = {}) {
256
258
  // Still running at the gate, or finished there: the replay re-sends it (P1.2).
257
259
  if (options.recoversModelCalls && effect.request.kind === "model")
258
260
  continue;
259
- if (options.recoversToolCalls && isGateToolEffect(s, effect.request))
261
+ if (effect.request.kind === "tool")
260
262
  continue;
261
263
  effect.status = "uncertain";
262
264
  effect.error ??= "The Worker running this effect stopped before its outcome was recorded";
@@ -281,13 +283,16 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
281
283
  let end;
282
284
  try {
283
285
  const segment = await startSegment(ctx, lease, { harness: true });
284
- // A pod sandbox's segment runs in its pod (F7.2), or fails when the pod cannot run it.
286
+ // A pod Environment's segment runs in its pod (F7.2), or fails when the pod cannot run it.
285
287
  const unusable = podUnusable(ctx, segment);
286
288
  if (unusable)
287
289
  throw new Error(unusable);
290
+ // A new volume being seeded from a disk version takes no turn until it holds it (R2d step 3).
291
+ if (segment.pod?.seeding)
292
+ throw new HarnessUnavailable();
288
293
  const pod = segment.pod?.id;
289
- if (pod !== undefined)
290
- placed.pod = pod;
294
+ if (started.environmentId !== undefined)
295
+ placed.environment = started.environmentId;
291
296
  end = await ctx.harness.offer({
292
297
  lease,
293
298
  start: buildTurnStart(ctx, segment),
@@ -295,6 +300,8 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
295
300
  transcript: segment.transcript,
296
301
  run,
297
302
  onTaken,
303
+ // Only a connection of the session's harness takes its runs (R4.1).
304
+ harness: harnessOf(segment.current),
298
305
  ...(pod === undefined ? {} : { pod, waitMs: POD_OFFER_WAIT_MS }),
299
306
  });
300
307
  if (end.kind === "unavailable") {
@@ -312,7 +319,8 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
312
319
  stopIfLeaving(signal);
313
320
  if (end.kind === "released")
314
321
  throw new SegmentStopped(end.reason === "ownership.lost" ? "ownership.lost" : "shutdown");
315
- const { output } = end;
322
+ // A harness's output never carries the Runtime's own tokens into the record (R4).
323
+ const output = redactTokens(end.output);
316
324
  if (output.thrown)
317
325
  throw thrownError(output.thrown);
318
326
  const result = resultOf(output, segment);
@@ -322,7 +330,9 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
322
330
  await settleFailure(ctx, lease, started, signal.reason);
323
331
  else {
324
332
  const recorded = result.status === "yielded" || result.status === "completed" || result.status === "paused";
325
- cursor = await settleTurn(ctx, lease, started, result, recorded ? output.transcript ?? [] : [], signal, segment.cursor);
333
+ cursor = await settleTurn(ctx, lease, started, result, recorded ? output.transcript ?? [] : [], signal, segment.cursor,
334
+ // The turn's end and its transcript are what the harness reported (R4.9).
335
+ { source: { kind: "harness", id: harnessOf(started) }, evidence: "claimed", runId: output.runId });
326
336
  }
327
337
  end.reply(cursor === undefined ? {} : { cursor });
328
338
  }
@@ -363,7 +373,8 @@ function resultOf(output, segment) {
363
373
  effectIds: output.effectIds ?? [],
364
374
  };
365
375
  let state = output.state;
366
- if (state !== undefined && transcriptShadow())
376
+ // Our engine's transcript shadow (tests); another harness's state is stored as sent (R4.2).
377
+ if (state !== undefined && transcriptShadow() && ours(current))
367
378
  state = withTranscript(state, applyUpdates(segment.transcript, output.transcript ?? []));
368
379
  const checkpoint = state === undefined || isWorkflowManifest(current.manifest)
369
380
  ? current.checkpoint
@@ -395,7 +406,6 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
395
406
  error: type === "turn.failed"
396
407
  ? String(payload.message ?? "")
397
408
  : undefined,
398
- schedule: ctx.wake,
399
409
  });
400
410
  else if (type === "turn.cancelled")
401
411
  await wakeLinkedWorkflow({
@@ -404,7 +414,6 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
404
414
  turnId,
405
415
  cancelled: true,
406
416
  error: "Agent turn was cancelled",
407
- schedule: ctx.wake,
408
417
  });
409
418
  }
410
419
  /**
@@ -412,8 +421,8 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
412
421
  * the turn has committed and while the advance still holds the lease. The harness hears back
413
422
  * after the export. The export never throws.
414
423
  */
415
- async function settleTurn(ctx, lease, s, result, updates, signal, cursor) {
416
- const kept = await settle(ctx, lease, s, result, updates, cursor);
424
+ async function settleTurn(ctx, lease, s, result, updates, signal, cursor, writer) {
425
+ const kept = await settle(ctx, lease, s, result, updates, cursor, writer);
417
426
  if (result.status === "completed")
418
427
  await exportOutputs(ctx, lease.sessionId, s.activeTurnId, signal);
419
428
  return kept;
@@ -423,7 +432,9 @@ async function settleTurn(ctx, lease, s, result, updates, signal, cursor) {
423
432
  * cursor of the transcript it recorded (`cursor` when it recorded no edit), or undefined when
424
433
  * the transcript was not kept.
425
434
  */
426
- async function settle(ctx, lease, s, result, updates = [], cursor) {
435
+ async function settle(ctx, lease, s, result, updates = [], cursor,
436
+ /** Who reported the result (R4.9): the run's harness. */
437
+ writer) {
427
438
  const id = lease.sessionId;
428
439
  let kept;
429
440
  const siblingCancelIds = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
@@ -434,11 +445,24 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
434
445
  // A result can arrive during concurrent action persistence. Preserve the runnable marker.
435
446
  const resumeRequested = current.status === "runnable";
436
447
  if (result.status === "waiting" || result.status === "uncertain") {
448
+ // Tool calls the Tool Gate held for approval (R5 step 2): the turn pauses for a person.
449
+ const held = resumeRequested ? [] : await heldCalls(t, result.effectIds);
450
+ if (held.length > 0) {
451
+ current.status = "paused";
452
+ pauseTurn(current);
453
+ current.waits = held;
454
+ const payload = { interactions: held };
455
+ await t.event(id, s.activeTurnId, "turn.paused", payload);
456
+ await t.put("sessions", id, current);
457
+ await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.paused", payload);
458
+ return siblings;
459
+ }
437
460
  const linkedWaits = isWorkflowManifest(current.manifest)
438
461
  ? await aggregateWaits({ t, workflowSessionId: id })
439
462
  : [];
440
463
  if (linkedWaits.length > 0 && !resumeRequested) {
441
464
  current.status = "paused";
465
+ pauseTurn(current);
442
466
  current.waits = linkedWaits;
443
467
  }
444
468
  else {
@@ -460,8 +484,8 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
460
484
  if (result.status === "yielded") {
461
485
  // The turn goes on in a new segment: same turn, next checkpoint, woken right away.
462
486
  const finished = result.checkpoint;
463
- kept = (await recordTranscript(t, id, s.activeTurnId, current, updates)) ?? cursor;
464
- current.state = leanState(result.result.state);
487
+ kept = (await recordTranscript(t, id, s.activeTurnId, current, updates, writer)) ?? cursor;
488
+ current.state = leanOf(current, result.result.state);
465
489
  current.checkpoint = {
466
490
  ...finished,
467
491
  segment: finished.segment + 1,
@@ -473,22 +497,25 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
473
497
  await slimModelEffects(t, id, s.activeTurnId);
474
498
  await t.put("sessions", id, current);
475
499
  const segment = finished.segment + 1;
476
- await t.wake(id, { reason: "rollover", dedupeKey: `rollover:${s.activeTurnId}:${segment}` }, ctx.wake);
500
+ await t.wake(id, {
501
+ reason: "rollover",
502
+ dedupeKey: `rollover:${s.activeTurnId}:${segment}`,
503
+ });
477
504
  return siblings;
478
505
  }
479
506
  const flow = isWorkflowManifest(current.manifest);
480
507
  if (!flow) {
481
508
  if (result.status !== "failed") {
482
- const last = await recordTranscript(t, id, s.activeTurnId, current, updates);
509
+ const last = await recordTranscript(t, id, s.activeTurnId, current, updates, writer);
483
510
  if (result.status !== "cancelled")
484
511
  kept = last ?? cursor;
485
512
  }
486
513
  current.state =
487
514
  result.status === "failed"
488
515
  ? current.turnStartState
489
- : leanState(result.result.state);
516
+ : leanOf(current, result.result.state);
490
517
  }
491
- current.checkpoint = leanCheckpoint(result.checkpoint);
518
+ current.checkpoint = leanCheckpoint(current, result.checkpoint);
492
519
  current.status = result.status;
493
520
  current.waits =
494
521
  result.status === "paused" ? result.result.pending : undefined;
@@ -509,7 +536,11 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
509
536
  });
510
537
  siblings.push(...cancelResult.agentSessionIds);
511
538
  }
512
- if (result.status !== "paused") {
539
+ // The turn's span (R7.1): a pause starts its wait; an end, its duration and totals.
540
+ const ended = result.status === "paused" ? undefined : await endTurn(t, current, s.activeTurnId);
541
+ if (result.status === "paused")
542
+ pauseTurn(current);
543
+ else {
513
544
  current.activeTurnId = null;
514
545
  if (s.activeTurnId)
515
546
  current.lastTurnId = s.activeTurnId;
@@ -517,7 +548,7 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
517
548
  }
518
549
  const type = `turn.${result.status}`;
519
550
  const payload = result.status === "completed"
520
- ? { output: result.result.output }
551
+ ? { output: result.result.output, ...ended }
521
552
  : result.status === "paused"
522
553
  ? {
523
554
  interactions: (result.result.pending ?? []).map((call) => ({
@@ -540,9 +571,10 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
540
571
  ? { path: result.result.error.path }
541
572
  : {}),
542
573
  },
574
+ ...ended,
543
575
  }
544
- : {};
545
- await t.event(id, s.activeTurnId, type, payload);
576
+ : { ...ended };
577
+ await t.event(id, s.activeTurnId, type, payload, writer);
546
578
  await t.put("sessions", id, current);
547
579
  await wakeWorkflowOf(ctx, t, current, s.activeTurnId, type, payload);
548
580
  return siblings;
@@ -565,6 +597,37 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
565
597
  }
566
598
  return kept;
567
599
  }
600
+ /**
601
+ * The approvals a waiting segment's held tool calls ask for (R5 step 2), as `turn.paused`
602
+ * interactions: each one's id is the call's effect id, which `approve` answers.
603
+ */
604
+ async function heldCalls(t, effectIds) {
605
+ const held = [];
606
+ for (const effectId of effectIds) {
607
+ const row = await t.get("effects", effectId);
608
+ if (row?.status !== "held")
609
+ continue;
610
+ const request = row.request;
611
+ const context = (request.context ?? {});
612
+ const toolName = request.toolName ?? request.key ?? effectId;
613
+ const reason = row.hold?.reason ?? "The Tenant's tool policy asks a person about this call";
614
+ held.push({
615
+ invocationId: typeof context.invocationId === "string" ? context.invocationId : effectId,
616
+ ...(typeof context.callId === "string" ? { callId: context.callId } : {}),
617
+ interaction: {
618
+ id: effectId,
619
+ kind: "approval",
620
+ prompt: `Approve ${toolName}?`,
621
+ reason,
622
+ ...(row.server === undefined ? {} : { server: row.server }),
623
+ input: request.input ?? null,
624
+ },
625
+ status: "interaction",
626
+ toolName,
627
+ });
628
+ }
629
+ return held;
630
+ }
568
631
  /** A segment threw: fail the turn, restore the turn-start state, wake a linked workflow. */
569
632
  async function settleFailure(ctx, lease, s, error) {
570
633
  const id = lease.sessionId;
@@ -575,7 +638,7 @@ async function settleFailure(ctx, lease, s, error) {
575
638
  current.status = "failed";
576
639
  current.state = current.turnStartState;
577
640
  current.error = error instanceof Error ? error.message : String(error);
578
- const payload = { message: current.error };
641
+ const payload = { message: current.error, ...(await endTurn(t, current, current.activeTurnId)) };
579
642
  await t.event(id, current.activeTurnId, "turn.failed", payload);
580
643
  await slimModelEffects(t, id, current.activeTurnId);
581
644
  if (current.activeTurnId)
@@ -585,21 +648,30 @@ async function settleFailure(ctx, lease, s, error) {
585
648
  await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.failed", payload);
586
649
  });
587
650
  }
588
- /** `checkpoint` with its engine state stored lean (`history.ts`). */
589
- function leanCheckpoint(checkpoint) {
651
+ /** Whether the session runs our harness, whose state core may store lean (R4.2). */
652
+ const ours = (session) => harnessOf(session) === HARNESS_ID;
653
+ /**
654
+ * `state` as the session row stores it: our engine's without its transcript (`history.ts`);
655
+ * another harness's as it sent it, never read (R4.2).
656
+ */
657
+ function leanOf(session, state) {
658
+ return ours(session) ? leanState(state) : state;
659
+ }
660
+ /** `checkpoint` with its engine state stored lean (`leanOf`). */
661
+ function leanCheckpoint(session, checkpoint) {
590
662
  const state = checkpoint?.state;
591
663
  if (!checkpoint || !state)
592
664
  return checkpoint;
593
- return { ...checkpoint, state: leanState(state) };
665
+ return { ...checkpoint, state: leanOf(session, state) };
594
666
  }
595
667
  /**
596
668
  * Appends a segment's transcript edits; a snapshot moves `history.snapshot`. Returns the seq of
597
669
  * the last edit, if any.
598
670
  */
599
- async function recordTranscript(t, id, turnId, s, updates) {
671
+ async function recordTranscript(t, id, turnId, s, updates, writer) {
600
672
  let last;
601
673
  for (const update of updates) {
602
- const event = await t.event(id, turnId, "transcript.updated", update);
674
+ const event = await t.event(id, turnId, "transcript.updated", update, writer);
603
675
  if (update.keep === 0)
604
676
  s.history = { from: s.history?.from ?? event.seq, snapshot: event.seq };
605
677
  last = event.seq;
@@ -27,7 +27,7 @@ import { uploadArtifact } from "../artifacts/service.js";
27
27
  import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
28
28
  import { sandboxCapabilityOf } from "../sandbox/manager.js";
29
29
  import { sandboxWorkspaceOf } from "../sandbox/share.js";
30
- import { sandboxLookup, sessionOf } from "./context.js";
30
+ import { sessionOf } from "./context.js";
31
31
  import { HttpError } from "./http.js";
32
32
  import { manifestFor } from "./session.js";
33
33
  import { jsonBytes, utf8Boundary } from "./tool-results.js";
@@ -47,13 +47,12 @@ export async function callSaveArtifact(ctx, request, signal) {
47
47
  const artifactId = typeof input.artifactId === "string" ? input.artifactId : undefined;
48
48
  const { s, workspace, limits, ours } = await ctx.store.tx(async (t) => {
49
49
  const s = await sessionOf(t, request.sessionId);
50
- const lookup = await sandboxLookup(t, s.id);
51
50
  // A new version only of an artifact of this session.
52
51
  const ours = artifactId === undefined ||
53
52
  (isArtifactId(artifactId) && (await t.artifact(artifactId))?.sessionId === s.id);
54
53
  return {
55
54
  s,
56
- workspace: sandboxWorkspaceOf(s, lookup),
55
+ workspace: sandboxWorkspaceOf(s),
57
56
  limits: await readArtifactLimits(t),
58
57
  ours,
59
58
  };
@@ -70,7 +69,7 @@ export async function callSaveArtifact(ctx, request, signal) {
70
69
  id: workspace.ownerId,
71
70
  activeTurnId: s.activeTurnId,
72
71
  manifest: s.manifest,
73
- ...(workspace.sandboxId === undefined ? {} : { sandboxId: workspace.sandboxId }),
72
+ ...(workspace.environmentId === undefined ? {} : { environmentId: workspace.environmentId }),
74
73
  }, capability, path, limits.fileBytes, signal);
75
74
  if (read.kind === "failed")
76
75
  return read;
@@ -1,3 +1,16 @@
1
- import type { SessionCommand } from "@nylorun/core/contracts";
1
+ import type { EventWriter, SessionCommand } from "@nylorun/core/contracts";
2
2
  import { type AuthScope, type TenantContext } from "./context.js";
3
- export declare function command(ctx: TenantContext, id: string, input: SessionCommand, scope: AuthScope): Promise<unknown>;
3
+ import type { TraceContext } from "../traces/ids.js";
4
+ export declare function command(ctx: TenantContext, id: string, input: SessionCommand, scope: AuthScope,
5
+ /**
6
+ * The W3C trace context the request carried (R7.4): a `message` records it on its
7
+ * `command.message`, and the turn joins the caller's trace. Not part of the command's identity.
8
+ */
9
+ options?: {
10
+ trace?: TraceContext;
11
+ }): Promise<unknown>;
12
+ /**
13
+ * Who sent a command (R4.9), from its credential and never its body: a client over the Runtime
14
+ * API (an application, or a person through it or a token), or core itself.
15
+ */
16
+ export declare function commandWriter(scope: AuthScope): EventWriter;
@@ -22,17 +22,24 @@ import { resolveMessageManifest } from "../core/turn-manifest.js";
22
22
  import { canonical } from "../store/canonical.js";
23
23
  import { lockedSession, } from "./context.js";
24
24
  import { fail } from "./http.js";
25
+ import { settleToolRow } from "../harness-api/tool-journal.js";
25
26
  import { accessOf } from "./auth.js";
26
- import { checkSandboxTurn } from "./sandboxes.js";
27
- import { rebaseSessionState, turnManifestOf, variantStore } from "./session.js";
27
+ import { checkEnvironmentTurn } from "./environments.js";
28
+ import { turnManifestOf, variantStore } from "./session.js";
28
29
  import { slimModelEffects } from "./slim.js";
29
30
  import { resolveMessageParts } from "../artifacts/parts.js";
31
+ import { endTurn, resumeTurn, startTurn } from "./turn-timing.js";
30
32
  /** A command's identity for idempotency: everything but the request id. */
31
33
  const semantic = (value) => {
32
34
  const { requestId: _, ...body } = value;
33
35
  return canonical(body);
34
36
  };
35
- export async function command(ctx, id, input, scope) {
37
+ export async function command(ctx, id, input, scope,
38
+ /**
39
+ * The W3C trace context the request carried (R7.4): a `message` records it on its
40
+ * `command.message`, and the turn joins the caller's trace. Not part of the command's identity.
41
+ */
42
+ options = {}) {
36
43
  const { store } = ctx;
37
44
  const cascadeCancelIds = [];
38
45
  const response = await store.tx(async (t) => {
@@ -53,6 +60,8 @@ export async function command(ctx, id, input, scope) {
53
60
  return existing.response;
54
61
  }
55
62
  let event;
63
+ // A tool call the Tool Gate held, which this `approve` answers (R5 step 2).
64
+ let held;
56
65
  // The turn a `cancel` ended, if one was active.
57
66
  let cancelledTurnId = null;
58
67
  if (command.type === "cancel") {
@@ -73,15 +82,24 @@ export async function command(ctx, id, input, scope) {
73
82
  turnId: cancelledTurnId,
74
83
  });
75
84
  if (cancelledTurnId !== null)
76
- for (const effect of await t.effectsForTurn(id, cancelledTurnId, ["invoking"])) {
85
+ for (const effect of await t.effectsForTurn(id, cancelledTurnId, ["invoking", "held"])) {
86
+ // A tool call ends `cancelled` with its `tool.completed` (R5.1); it may have run.
87
+ if (effect.request.kind === "tool") {
88
+ await settleToolRow(t, s, effect, {
89
+ kind: "failed",
90
+ code: "tool.cancelled",
91
+ message: "The turn was cancelled while this call ran; it may have run.",
92
+ }, { cancelled: true });
93
+ continue;
94
+ }
77
95
  effect.status = "uncertain";
78
96
  effect.error =
79
97
  "Turn cancelled before model outcome was durably recorded";
80
98
  await t.put("effects", effect.request.effectId, effect);
81
99
  }
82
- event = await t.event(id, cancelledTurnId, "turn.cancelled", {
83
- reason: command.reason,
84
- });
100
+ // The cancelled turn's span (R7.1): its duration, agent and totals.
101
+ const ended = cancelledTurnId !== null ? await endTurn(t, s, cancelledTurnId) : {};
102
+ event = await t.event(id, cancelledTurnId, "turn.cancelled", { reason: command.reason, ...ended }, commandWriter(scope));
85
103
  await slimModelEffects(t, id, cancelledTurnId);
86
104
  // The process running the advance aborts it on this signal (control.ts), sent at commit.
87
105
  await t.signal({
@@ -110,17 +128,25 @@ export async function command(ctx, id, input, scope) {
110
128
  }
111
129
  }
112
130
  else {
131
+ // A session opened before protocol 11 (migration 0020) keeps its history but takes no turn.
132
+ if (s.closedReason !== undefined)
133
+ fail(409, "This session was opened before protocol 11: open a new session that names an Environment.", { code: "session_closed" });
113
134
  if (command.type === "message") {
114
135
  if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
115
136
  fail(409, "Session has active or unresolved work");
116
137
  // Every turn start on a sandbox: it exists, the token's grants reach it, and no other
117
138
  // session's turn holds it.
118
- await checkSandboxTurn(t, s, scope, ctx);
139
+ await checkEnvironmentTurn(t, s, scope, ctx);
119
140
  s.turnStartState = s.state;
120
141
  // A new turn folds from the latest snapshot: a cancel or failure only reverts this turn.
121
142
  if (s.history?.snapshot !== undefined)
122
143
  s.history = { ...s.history, from: s.history.snapshot };
123
144
  s.activeTurnId = randomUUID();
145
+ startTurn(s);
146
+ if (options.trace)
147
+ s.turnTrace = options.trace;
148
+ else
149
+ delete s.turnTrace;
124
150
  // File parts name artifacts the caller may read; each is pinned to a version now.
125
151
  const parts = "parts" in command
126
152
  ? await resolveMessageParts(t, command.parts, id, accessOf(scope))
@@ -151,7 +177,6 @@ export async function command(ctx, id, input, scope) {
151
177
  fail(400, resolved.message);
152
178
  }
153
179
  else {
154
- rebaseSessionState(s, resolved.hash);
155
180
  s.checkpoint = createDurableCheckpoint({
156
181
  manifest: resolved.manifest,
157
182
  sessionId: id,
@@ -169,6 +194,25 @@ export async function command(ctx, id, input, scope) {
169
194
  }
170
195
  }
171
196
  }
197
+ else if (command.type === "approve" &&
198
+ (held = await heldCall(t, s, command.interactionId)) !== undefined) {
199
+ // A tool call the Tool Gate held for approval (R5 step 2). Approved: the replay sends it
200
+ // again and the gate runs it. Denied: it fails to the model as `tool.denied`, unsent.
201
+ if (s.status !== "paused")
202
+ fail(409, "Session is not awaiting a response");
203
+ if (command.approved) {
204
+ held.status = "invoking";
205
+ const by = principalOf(scope);
206
+ held.approval = { approved: true, at: new Date().toISOString(), ...(by ? { by } : {}) };
207
+ await t.put("effects", held.request.effectId, held);
208
+ }
209
+ else
210
+ await settleToolRow(t, s, held, {
211
+ kind: "failed",
212
+ code: "tool.denied",
213
+ message: "A person denied this call; it was not sent.",
214
+ });
215
+ }
172
216
  else if (isWorkflowManifest(s.manifest)) {
173
217
  // Approvals owned by linked agent sessions must be answered there (WF-R52).
174
218
  const conflict = await foreignInteractionConflict({
@@ -221,18 +265,21 @@ export async function command(ctx, id, input, scope) {
221
265
  segment: (s.checkpoint?.segment ?? 0) + 1,
222
266
  });
223
267
  }
268
+ // A paused turn goes on: its wait for a person is not its running time (R7.1).
269
+ resumeTurn(s);
224
270
  s.status = "runnable";
225
271
  s.waits = undefined;
226
- // A flow answers each interaction in the same segment: the interaction names the cause.
227
- const cause = command.type !== "message" && isWorkflowManifest(s.manifest)
272
+ // A flow answers each interaction in the same segment, as a held tool call's approval is
273
+ // answered (R5 step 2): the interaction names the cause.
274
+ const cause = command.type !== "message" && (isWorkflowManifest(s.manifest) || held !== undefined)
228
275
  ? command.interactionId
229
276
  : s.checkpoint.segment;
230
277
  const commandWake = {
231
278
  reason: command.type,
232
279
  dedupeKey: `${command.type}:${s.activeTurnId}:${cause}`,
233
280
  };
234
- await t.wake(id, commandWake, ctx.wake);
235
- event = await t.event(id, s.activeTurnId, `command.${command.type}`, command);
281
+ await t.wake(id, commandWake);
282
+ event = await t.event(id, s.activeTurnId, `command.${command.type}`, command.type === "message" && options.trace ? { ...command, trace: options.trace } : command, commandWriter(scope));
236
283
  }
237
284
  await t.put("sessions", id, s);
238
285
  // Direct cancel of a linked agent fails that node with agent.cancelled (WF-R54). The agent
@@ -244,7 +291,6 @@ export async function command(ctx, id, input, scope) {
244
291
  turnId: cancelledTurnId,
245
292
  cancelled: true,
246
293
  error: "Agent turn was cancelled",
247
- schedule: ctx.wake,
248
294
  });
249
295
  const response = {
250
296
  status: "accepted",
@@ -273,3 +319,37 @@ export async function command(ctx, id, input, scope) {
273
319
  }
274
320
  return response;
275
321
  }
322
+ /** Core's own commands (R4.9): the flow host's, and a drain's cancels. */
323
+ const CORE_PRINCIPALS = { "flow-host": "flow-host", drain: "core" };
324
+ /**
325
+ * Who sent a command (R4.9), from its credential and never its body: a client over the Runtime
326
+ * API (an application, or a person through it or a token), or core itself.
327
+ */
328
+ export function commandWriter(scope) {
329
+ if (scope.kind === "application" && CORE_PRINCIPALS[scope.principalId] !== undefined)
330
+ return {
331
+ source: { kind: "core", id: "runtime" },
332
+ principal: { kind: CORE_PRINCIPALS[scope.principalId], id: scope.principalId },
333
+ };
334
+ const principal = scope.kind === "subject" || scope.kind === "token"
335
+ ? { kind: "subject", id: scope.subject }
336
+ : { kind: "application", id: scope.kind === "anonymous" ? "anonymous" : scope.principalId };
337
+ return { source: { kind: "client", id: principal.id }, principal };
338
+ }
339
+ /** The held tool call of the session's active turn that `interactionId` names (R5 step 2). */
340
+ async function heldCall(t, s, interactionId) {
341
+ const row = await t.get("effects", interactionId);
342
+ if (row?.status !== "held")
343
+ return undefined;
344
+ if (row.request.sessionId !== s.id || row.request.turnId !== s.activeTurnId)
345
+ return undefined;
346
+ return row;
347
+ }
348
+ /** Who answered: the principal (its subject, when it acts for one), or the token's subject. */
349
+ function principalOf(scope) {
350
+ if (scope.kind === "token")
351
+ return `${scope.issuer}:${scope.subject}`;
352
+ if (scope.kind === "subject")
353
+ return `${scope.principalId}:${scope.subject}`;
354
+ return "principalId" in scope ? scope.principalId : undefined;
355
+ }