@stigmer/server 3.15.3-dev.20260916211208 → 3.16.1-dev.20260917224426

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 (170) hide show
  1. package/dist/authorization/lane-admission.d.ts +50 -0
  2. package/dist/authorization/lane-admission.d.ts.map +1 -0
  3. package/dist/authorization/lane-admission.js +13 -0
  4. package/dist/authorization/lane-admission.js.map +1 -0
  5. package/dist/authorization/schedule-fire-caller.d.ts +2 -2
  6. package/dist/authorization/schedule-fire-caller.d.ts.map +1 -1
  7. package/dist/authorization/schedule-fire-caller.js +8 -8
  8. package/dist/authorization/schedule-fire-caller.js.map +1 -1
  9. package/dist/boot/compose.d.ts +12 -0
  10. package/dist/boot/compose.d.ts.map +1 -1
  11. package/dist/boot/compose.js +68 -14
  12. package/dist/boot/compose.js.map +1 -1
  13. package/dist/boot/inprocess.d.ts.map +1 -1
  14. package/dist/boot/inprocess.js +6 -1
  15. package/dist/boot/inprocess.js.map +1 -1
  16. package/dist/domain/executioncontext/resolve-values-for-caller.d.ts +33 -14
  17. package/dist/domain/executioncontext/resolve-values-for-caller.d.ts.map +1 -1
  18. package/dist/domain/executioncontext/resolve-values-for-caller.js +32 -8
  19. package/dist/domain/executioncontext/resolve-values-for-caller.js.map +1 -1
  20. package/dist/domain/identityaccount/actor.d.ts +33 -18
  21. package/dist/domain/identityaccount/actor.d.ts.map +1 -1
  22. package/dist/domain/identityaccount/actor.js +20 -3
  23. package/dist/domain/identityaccount/actor.js.map +1 -1
  24. package/dist/domain/identityaccount/resolve.d.ts +19 -0
  25. package/dist/domain/identityaccount/resolve.d.ts.map +1 -1
  26. package/dist/domain/identityaccount/resolve.js +12 -0
  27. package/dist/domain/identityaccount/resolve.js.map +1 -1
  28. package/dist/domain/mcpserver/connect-execution-id.d.ts +5 -0
  29. package/dist/domain/mcpserver/connect-execution-id.d.ts.map +1 -0
  30. package/dist/domain/mcpserver/connect-execution-id.js +32 -0
  31. package/dist/domain/mcpserver/connect-execution-id.js.map +1 -0
  32. package/dist/domain/mcpserver/connect.d.ts +28 -2
  33. package/dist/domain/mcpserver/connect.d.ts.map +1 -1
  34. package/dist/domain/mcpserver/connect.js +27 -7
  35. package/dist/domain/mcpserver/connect.js.map +1 -1
  36. package/dist/domain/mcpserver/engine.d.ts +9 -3
  37. package/dist/domain/mcpserver/engine.d.ts.map +1 -1
  38. package/dist/domain/mcpserver/engine.js.map +1 -1
  39. package/dist/domain/mcpserver/start-connect.js +1 -1
  40. package/dist/domain/mcpserver/start-connect.js.map +1 -1
  41. package/dist/domain/platform/controller.d.ts +31 -11
  42. package/dist/domain/platform/controller.d.ts.map +1 -1
  43. package/dist/domain/platform/controller.js +2 -3
  44. package/dist/domain/platform/controller.js.map +1 -1
  45. package/dist/domain/plugin/materialize/agent.d.ts +10 -1
  46. package/dist/domain/plugin/materialize/agent.d.ts.map +1 -1
  47. package/dist/domain/plugin/materialize/agent.js +35 -1
  48. package/dist/domain/plugin/materialize/agent.js.map +1 -1
  49. package/dist/domain/plugin/materialize/plan.d.ts.map +1 -1
  50. package/dist/domain/plugin/materialize/plan.js +1 -1
  51. package/dist/domain/plugin/materialize/plan.js.map +1 -1
  52. package/dist/domain/plugin/overlay/sanitize.d.ts.map +1 -1
  53. package/dist/domain/plugin/overlay/sanitize.js +4 -2
  54. package/dist/domain/plugin/overlay/sanitize.js.map +1 -1
  55. package/dist/domain/project/reconcile.d.ts.map +1 -1
  56. package/dist/domain/project/reconcile.js +14 -10
  57. package/dist/domain/project/reconcile.js.map +1 -1
  58. package/dist/domain/workflowexecution/phases.d.ts +21 -0
  59. package/dist/domain/workflowexecution/phases.d.ts.map +1 -0
  60. package/dist/domain/workflowexecution/phases.js +31 -0
  61. package/dist/domain/workflowexecution/phases.js.map +1 -0
  62. package/dist/identity/oidc-verifier.js +8 -1
  63. package/dist/identity/oidc-verifier.js.map +1 -1
  64. package/dist/pipeline/apiresource-meta.d.ts +15 -0
  65. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  66. package/dist/pipeline/apiresource-meta.js +34 -0
  67. package/dist/pipeline/apiresource-meta.js.map +1 -1
  68. package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
  69. package/dist/pipeline/steps/guard-memory-capture.js +5 -3
  70. package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
  71. package/dist/runnerauth/bound-execution.d.ts +27 -0
  72. package/dist/runnerauth/bound-execution.d.ts.map +1 -0
  73. package/dist/runnerauth/bound-execution.js +137 -0
  74. package/dist/runnerauth/bound-execution.js.map +1 -0
  75. package/dist/runnerauth/built-in-runner-credential-provider.d.ts +28 -0
  76. package/dist/runnerauth/built-in-runner-credential-provider.d.ts.map +1 -0
  77. package/dist/runnerauth/built-in-runner-credential-provider.js +217 -0
  78. package/dist/runnerauth/built-in-runner-credential-provider.js.map +1 -0
  79. package/dist/runnerauth/constants.d.ts +63 -0
  80. package/dist/runnerauth/constants.d.ts.map +1 -0
  81. package/dist/runnerauth/constants.js +63 -0
  82. package/dist/runnerauth/constants.js.map +1 -0
  83. package/dist/runnerauth/dispatch-credential.d.ts +38 -0
  84. package/dist/runnerauth/dispatch-credential.d.ts.map +1 -0
  85. package/dist/runnerauth/dispatch-credential.js +17 -0
  86. package/dist/runnerauth/dispatch-credential.js.map +1 -0
  87. package/dist/runnerauth/runner-credential-provider.d.ts +49 -11
  88. package/dist/runnerauth/runner-credential-provider.d.ts.map +1 -1
  89. package/dist/runnerauth/runner-credential-provider.js +16 -4
  90. package/dist/runnerauth/runner-credential-provider.js.map +1 -1
  91. package/dist/runnerauth/runner-subject-verifier.d.ts +14 -0
  92. package/dist/runnerauth/runner-subject-verifier.d.ts.map +1 -0
  93. package/dist/runnerauth/runner-subject-verifier.js +115 -0
  94. package/dist/runnerauth/runner-subject-verifier.js.map +1 -0
  95. package/dist/runnerauth/runnerauth.d.ts +33 -4
  96. package/dist/runnerauth/runnerauth.d.ts.map +1 -1
  97. package/dist/runnerauth/runnerauth.js +131 -35
  98. package/dist/runnerauth/runnerauth.js.map +1 -1
  99. package/dist/temporal/agentexecution/engine-client.d.ts +13 -0
  100. package/dist/temporal/agentexecution/engine-client.d.ts.map +1 -1
  101. package/dist/temporal/agentexecution/engine-client.js +10 -2
  102. package/dist/temporal/agentexecution/engine-client.js.map +1 -1
  103. package/dist/temporal/agentexecution/workflow-input.d.ts +13 -0
  104. package/dist/temporal/agentexecution/workflow-input.d.ts.map +1 -1
  105. package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
  106. package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +35 -31
  107. package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
  108. package/dist/temporal/workflowexecution/engine-client.d.ts +24 -0
  109. package/dist/temporal/workflowexecution/engine-client.d.ts.map +1 -1
  110. package/dist/temporal/workflowexecution/engine-client.js +26 -19
  111. package/dist/temporal/workflowexecution/engine-client.js.map +1 -1
  112. package/dist/temporal/workflowexecution/workflow-input.d.ts +14 -0
  113. package/dist/temporal/workflowexecution/workflow-input.d.ts.map +1 -1
  114. package/package.json +5 -5
  115. package/src/authorization/__tests__/lane-admission.test.ts +115 -0
  116. package/src/authorization/lane-admission.ts +73 -0
  117. package/src/authorization/schedule-fire-caller.ts +10 -14
  118. package/src/boot/compose.ts +93 -22
  119. package/src/boot/inprocess.ts +7 -1
  120. package/src/domain/executioncontext/__tests__/executioncontext.test.ts +104 -0
  121. package/src/domain/executioncontext/__tests__/resolve-values-capability.test.ts +33 -4
  122. package/src/domain/executioncontext/resolve-values-for-caller.ts +79 -22
  123. package/src/domain/identityaccount/__tests__/account-for-stamp.test.ts +111 -0
  124. package/src/domain/identityaccount/actor.ts +61 -21
  125. package/src/domain/identityaccount/resolve.ts +31 -0
  126. package/src/domain/mcpserver/__tests__/connect.test.ts +17 -2
  127. package/src/domain/mcpserver/connect-execution-id.ts +34 -0
  128. package/src/domain/mcpserver/connect.ts +38 -3
  129. package/src/domain/mcpserver/engine.ts +9 -3
  130. package/src/domain/mcpserver/start-connect.ts +1 -1
  131. package/src/domain/platform/__tests__/platform.test.ts +47 -1
  132. package/src/domain/platform/controller.ts +33 -14
  133. package/src/domain/plugin/__tests__/materialize.test.ts +60 -1
  134. package/src/domain/plugin/__tests__/plugin.test.ts +122 -1
  135. package/src/domain/plugin/__tests__/sanitize.test.ts +2 -2
  136. package/src/domain/plugin/materialize/agent.ts +38 -0
  137. package/src/domain/plugin/materialize/plan.ts +7 -1
  138. package/src/domain/plugin/overlay/sanitize.ts +4 -2
  139. package/src/domain/project/__tests__/reconcile.test.ts +51 -5
  140. package/src/domain/project/reconcile.ts +20 -15
  141. package/src/domain/workflowexecution/__tests__/phases.test.ts +58 -0
  142. package/src/domain/workflowexecution/phases.ts +33 -0
  143. package/src/extensions/__tests__/extension-composition.test.ts +7 -1
  144. package/src/identity/__tests__/oidc-verifier.test.ts +22 -0
  145. package/src/identity/oidc-verifier.ts +8 -1
  146. package/src/pipeline/__tests__/kind-by-id-prefix.test.ts +75 -0
  147. package/src/pipeline/apiresource-meta.ts +41 -0
  148. package/src/pipeline/steps/__tests__/guard-memory-capture.test.ts +55 -35
  149. package/src/pipeline/steps/guard-memory-capture.ts +5 -3
  150. package/src/runnerauth/__tests__/bound-execution.test.ts +334 -0
  151. package/src/runnerauth/__tests__/built-in-runner-credential-provider.test.ts +516 -0
  152. package/src/runnerauth/__tests__/dispatch-credential.test.ts +94 -0
  153. package/src/runnerauth/__tests__/runner-credential-provider.test.ts +27 -0
  154. package/src/runnerauth/__tests__/runner-subject-composed.test.ts +497 -0
  155. package/src/runnerauth/__tests__/runner-subject-verifier.test.ts +541 -0
  156. package/src/runnerauth/bound-execution.ts +270 -0
  157. package/src/runnerauth/built-in-runner-credential-provider.ts +300 -0
  158. package/src/runnerauth/constants.ts +74 -0
  159. package/src/runnerauth/dispatch-credential.ts +63 -0
  160. package/src/runnerauth/runner-credential-provider.ts +60 -11
  161. package/src/runnerauth/runner-subject-verifier.ts +152 -0
  162. package/src/runnerauth/runnerauth.ts +148 -38
  163. package/src/temporal/agentexecution/__tests__/engine-client.test.ts +107 -38
  164. package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +105 -2
  165. package/src/temporal/agentexecution/engine-client.ts +27 -2
  166. package/src/temporal/agentexecution/workflow-input.ts +13 -0
  167. package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +90 -44
  168. package/src/temporal/workflowexecution/__tests__/engine-client.test.ts +139 -33
  169. package/src/temporal/workflowexecution/engine-client.ts +48 -21
  170. package/src/temporal/workflowexecution/workflow-input.ts +14 -0
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Pins the bound-execution reader (bound-execution.ts) in isolation, with
3
+ * the clock injected, so the liveness rule every lane shares is stated
4
+ * by enumeration here and the verifier's and the decrypt lane's tests can
5
+ * assert behavior rather than restate it:
6
+ *
7
+ * - the kind is read off the id alone: the two execution kinds by the
8
+ * contract's prefix table, the connect binding by its own predicate;
9
+ * anything else is `undefined` with no store read;
10
+ * - a connect binding resolves through the connect's ExecutionContext
11
+ * row (by `spec.executionId`), is live while that row exists, and is
12
+ * not a run (`bindsARun`) — the shape both no-`exp` lanes refuse;
13
+ * - live is "not terminal" for each kind's OWN terminal set (the agent
14
+ * side's TERMINATED and WAITING_FOR_APPROVAL, the workflow side's
15
+ * TERMINATED and PAUSED are the arms that differ from a naive set);
16
+ * - a terminal row is live within the grace of its `completed_at`, dead
17
+ * at the boundary and beyond, and dead at once with no timestamp or an
18
+ * unparsable one (fail closed);
19
+ * - a missing row is `undefined`; any other store failure propagates as
20
+ * the same error object.
21
+ */
22
+ import { create } from "@bufbuild/protobuf";
23
+ import type { DescMessage, MessageShape } from "@bufbuild/protobuf";
24
+ import { describe, expect, it, vi } from "vitest";
25
+
26
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
27
+ import { ExecutionPhase as AgentPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
28
+ import { ExecutionContextSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/api_pb";
29
+ import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
30
+ import { ExecutionPhase as WorkflowPhase } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
31
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
32
+
33
+ import { newConnectExecutionId } from "../../domain/mcpserver/connect-execution-id.js";
34
+ import { ResourceNotFoundError } from "../../store/interface.js";
35
+ import {
36
+ bindsARun,
37
+ boundExecutionKindOf,
38
+ loadBoundExecution,
39
+ } from "../bound-execution.js";
40
+ import type { BoundExecutionStore } from "../bound-execution.js";
41
+ import { RUN_CREDENTIAL_GRACE_AFTER_TERMINAL_MS } from "../constants.js";
42
+
43
+ const NOW = Date.parse("2026-09-16T12:00:00Z");
44
+ const iso = (offsetMs: number): string =>
45
+ new Date(NOW + offsetMs).toISOString();
46
+
47
+ /**
48
+ * A store of rows by id, plus the connect ECs by the execution id they
49
+ * were created for (`findByField` on `spec.executionId`). `reads` records
50
+ * every lookup key, so an arm can assert "no store read" and which read.
51
+ */
52
+ function storeOf(
53
+ rows: Record<string, unknown>,
54
+ executionContexts: Record<string, unknown> = {},
55
+ ): BoundExecutionStore & {
56
+ reads: string[];
57
+ } {
58
+ const reads: string[] = [];
59
+ return {
60
+ reads,
61
+ getResource<Desc extends DescMessage>(
62
+ kind: ApiResourceKind,
63
+ id: string,
64
+ _schema: Desc,
65
+ ): Promise<MessageShape<Desc>> {
66
+ reads.push(id);
67
+ const row = rows[id];
68
+ return row === undefined
69
+ ? Promise.reject(new ResourceNotFoundError(`${kind}/${id}`))
70
+ : Promise.resolve(row as MessageShape<Desc>);
71
+ },
72
+ findByField<Desc extends DescMessage>(
73
+ kind: ApiResourceKind,
74
+ fieldPath: string,
75
+ value: string,
76
+ _schema: Desc,
77
+ ): Promise<MessageShape<Desc>> {
78
+ reads.push(`${fieldPath}=${value}`);
79
+ const row =
80
+ kind === ApiResourceKind.execution_context &&
81
+ fieldPath === "spec.executionId"
82
+ ? executionContexts[value]
83
+ : undefined;
84
+ return row === undefined
85
+ ? Promise.reject(new ResourceNotFoundError(`${kind}/${value}`))
86
+ : Promise.resolve(row as MessageShape<Desc>);
87
+ },
88
+ };
89
+ }
90
+
91
+ function connectContext(executionId: string, createdBy: string, org = "acme") {
92
+ return create(ExecutionContextSchema, {
93
+ metadata: { id: "ectx_1", name: `exec-ctx-${executionId}`, org },
94
+ spec: { executionId },
95
+ status: { audit: { specAudit: { createdBy: { id: createdBy } } } },
96
+ });
97
+ }
98
+
99
+ function agentRun(
100
+ id: string,
101
+ phase: AgentPhase,
102
+ completedAt = "",
103
+ extra: { createdBy?: string; sessionId?: string; org?: string } = {},
104
+ ) {
105
+ return create(AgentExecutionSchema, {
106
+ metadata: { id, name: id, org: extra.org ?? "acme" },
107
+ spec: { sessionId: extra.sessionId ?? "" },
108
+ status: {
109
+ phase,
110
+ completedAt,
111
+ audit: { specAudit: { createdBy: { id: extra.createdBy ?? "" } } },
112
+ },
113
+ });
114
+ }
115
+
116
+ function workflowRun(id: string, phase: WorkflowPhase, completedAt = "") {
117
+ return create(WorkflowExecutionSchema, {
118
+ metadata: { id, name: id, org: "acme" },
119
+ status: { phase, completedAt },
120
+ });
121
+ }
122
+
123
+ describe("boundExecutionKindOf", () => {
124
+ it.each([
125
+ ["aex_1", "agent-execution"],
126
+ ["wex_1", "workflow-execution"],
127
+ [newConnectExecutionId("mcps_1"), "mcp-connect"],
128
+ ])("%s binds %s", (id, kind) => {
129
+ expect(boundExecutionKindOf(id)).toBe(kind);
130
+ });
131
+
132
+ it.each([["ses_1"], ["agt_1"], ["zzz_1"], [""], ["aex"], ["connect"]])(
133
+ "%s binds nothing",
134
+ (id) => {
135
+ expect(boundExecutionKindOf(id)).toBeUndefined();
136
+ },
137
+ );
138
+ });
139
+
140
+ describe("bindsARun — what a no-`exp` credential may name", () => {
141
+ it.each([
142
+ ["agent-execution", true],
143
+ ["workflow-execution", true],
144
+ ["mcp-connect", false],
145
+ ] as const)("%s → %s", (kind, expected) => {
146
+ expect(bindsARun(kind)).toBe(expected);
147
+ });
148
+ });
149
+
150
+ describe("loadBoundExecution", () => {
151
+ it("reads the row's facts the lane needs and nothing else", async () => {
152
+ const store = storeOf({
153
+ aex_1: agentRun("aex_1", AgentPhase.EXECUTION_IN_PROGRESS, "", {
154
+ createdBy: "ida_carol",
155
+ sessionId: "ses_carol",
156
+ org: "acme",
157
+ }),
158
+ });
159
+ expect(await loadBoundExecution(store, "aex_1", NOW)).toEqual({
160
+ kind: "agent-execution",
161
+ executionId: "aex_1",
162
+ org: "acme",
163
+ createdBy: "ida_carol",
164
+ sessionId: "ses_carol",
165
+ live: true,
166
+ });
167
+ });
168
+
169
+ it("an id that binds no kind is undefined with NO store read", async () => {
170
+ const store = storeOf({});
171
+ expect(await loadBoundExecution(store, "ses_1", NOW)).toBeUndefined();
172
+ expect(store.reads).toEqual([]);
173
+ });
174
+
175
+ describe("the connect binding — the ExecutionContext row by its execution id", () => {
176
+ const connectId = newConnectExecutionId("mcps_1");
177
+
178
+ it("resolves to the row's creator and org, no session, live while the row exists", async () => {
179
+ const store = storeOf(
180
+ {},
181
+ { [connectId]: connectContext(connectId, "ida_member", "acme") },
182
+ );
183
+ expect(await loadBoundExecution(store, connectId, NOW)).toEqual({
184
+ kind: "mcp-connect",
185
+ executionId: connectId,
186
+ org: "acme",
187
+ createdBy: "ida_member",
188
+ sessionId: "",
189
+ live: true,
190
+ });
191
+ // The read getByExecutionId already makes, and no primary-key read.
192
+ expect(store.reads).toEqual([`spec.executionId=${connectId}`]);
193
+ });
194
+
195
+ it("a connect whose row is gone (the connect settled) is undefined — the credential names no row", async () => {
196
+ expect(
197
+ await loadBoundExecution(storeOf({}), connectId, NOW),
198
+ ).toBeUndefined();
199
+ });
200
+
201
+ it("a store fault on the EC read propagates as the same error object", async () => {
202
+ const fault = new Error("connection reset");
203
+ const store: BoundExecutionStore = {
204
+ getResource: vi.fn(() => Promise.reject(new Error("unreachable"))),
205
+ findByField: vi.fn(() => Promise.reject(fault)),
206
+ };
207
+ await expect(loadBoundExecution(store, connectId, NOW)).rejects.toBe(
208
+ fault,
209
+ );
210
+ });
211
+ });
212
+
213
+ it("a missing row is undefined — the typed not-found, and only that, is absorbed", async () => {
214
+ expect(
215
+ await loadBoundExecution(storeOf({}), "aex_missing", NOW),
216
+ ).toBeUndefined();
217
+ });
218
+
219
+ it("any other store failure propagates as the same error object", async () => {
220
+ const fault = new Error("connection reset");
221
+ const store: BoundExecutionStore = {
222
+ getResource: vi.fn(() => Promise.reject(fault)),
223
+ findByField: vi.fn(() => Promise.reject(new Error("unreachable"))),
224
+ };
225
+ await expect(loadBoundExecution(store, "aex_1", NOW)).rejects.toBe(fault);
226
+ });
227
+
228
+ describe("liveness — the agent execution's own terminal set", () => {
229
+ it.each([
230
+ ["PENDING", AgentPhase.EXECUTION_PENDING],
231
+ ["IN_PROGRESS", AgentPhase.EXECUTION_IN_PROGRESS],
232
+ ["WAITING_FOR_APPROVAL", AgentPhase.EXECUTION_WAITING_FOR_APPROVAL],
233
+ ["PAUSED", AgentPhase.EXECUTION_PAUSED],
234
+ ])("%s is live", async (_name, phase) => {
235
+ const store = storeOf({ aex_1: agentRun("aex_1", phase) });
236
+ expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(true);
237
+ });
238
+
239
+ it.each([
240
+ ["COMPLETED", AgentPhase.EXECUTION_COMPLETED],
241
+ ["FAILED", AgentPhase.EXECUTION_FAILED],
242
+ ["CANCELLED", AgentPhase.EXECUTION_CANCELLED],
243
+ ["TERMINATED", AgentPhase.EXECUTION_TERMINATED],
244
+ ])(
245
+ "%s with no completed_at is dead at once — no grace without a timestamp",
246
+ async (_name, phase) => {
247
+ const store = storeOf({ aex_1: agentRun("aex_1", phase) });
248
+ expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(
249
+ false,
250
+ );
251
+ },
252
+ );
253
+
254
+ it("a row with no status has not started, which is live", async () => {
255
+ const store = storeOf({
256
+ aex_1: create(AgentExecutionSchema, { metadata: { id: "aex_1" } }),
257
+ });
258
+ expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(true);
259
+ });
260
+ });
261
+
262
+ describe("liveness — the workflow execution's own terminal set", () => {
263
+ it.each([
264
+ ["PENDING", WorkflowPhase.EXECUTION_PENDING],
265
+ ["IN_PROGRESS", WorkflowPhase.EXECUTION_IN_PROGRESS],
266
+ ["PAUSED", WorkflowPhase.EXECUTION_PAUSED],
267
+ ])("%s is live", async (_name, phase) => {
268
+ const store = storeOf({ wex_1: workflowRun("wex_1", phase) });
269
+ expect((await loadBoundExecution(store, "wex_1", NOW))?.live).toBe(true);
270
+ });
271
+
272
+ it.each([
273
+ ["COMPLETED", WorkflowPhase.EXECUTION_COMPLETED],
274
+ ["FAILED", WorkflowPhase.EXECUTION_FAILED],
275
+ ["CANCELLED", WorkflowPhase.EXECUTION_CANCELLED],
276
+ ["TERMINATED", WorkflowPhase.EXECUTION_TERMINATED],
277
+ ])("%s with no completed_at is dead at once", async (_name, phase) => {
278
+ const store = storeOf({ wex_1: workflowRun("wex_1", phase) });
279
+ expect((await loadBoundExecution(store, "wex_1", NOW))?.live).toBe(false);
280
+ });
281
+
282
+ it("a workflow execution has no session — the field is empty, never a guess", async () => {
283
+ const store = storeOf({
284
+ wex_1: workflowRun("wex_1", WorkflowPhase.EXECUTION_IN_PROGRESS),
285
+ });
286
+ expect((await loadBoundExecution(store, "wex_1", NOW))?.sessionId).toBe(
287
+ "",
288
+ );
289
+ });
290
+ });
291
+
292
+ describe("the grace after terminal, measured from completed_at", () => {
293
+ const G = RUN_CREDENTIAL_GRACE_AFTER_TERMINAL_MS;
294
+
295
+ it.each([
296
+ ["the moment it ended", 0, true],
297
+ ["one millisecond before the grace ends", -(G - 1), true],
298
+ ["exactly at the grace boundary", -G, false],
299
+ ["long after", -(G * 10), false],
300
+ ])("a run that ended %s → live=%s", async (_label, offset, live) => {
301
+ const store = storeOf({
302
+ aex_1: agentRun("aex_1", AgentPhase.EXECUTION_COMPLETED, iso(offset)),
303
+ });
304
+ expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(live);
305
+ });
306
+
307
+ it("a completed_at in the future (a skewed writer's clock) is inside the grace, not an error", async () => {
308
+ const store = storeOf({
309
+ aex_1: agentRun("aex_1", AgentPhase.EXECUTION_COMPLETED, iso(60_000)),
310
+ });
311
+ expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(true);
312
+ });
313
+
314
+ it("an unparsable completed_at gets no grace — fail closed", async () => {
315
+ const store = storeOf({
316
+ aex_1: agentRun(
317
+ "aex_1",
318
+ AgentPhase.EXECUTION_COMPLETED,
319
+ "yesterday-ish",
320
+ ),
321
+ });
322
+ expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(false);
323
+ });
324
+
325
+ it("the same rule applies to a workflow execution", async () => {
326
+ const store = storeOf({
327
+ wex_1: workflowRun("wex_1", WorkflowPhase.EXECUTION_FAILED, iso(-1000)),
328
+ wex_2: workflowRun("wex_2", WorkflowPhase.EXECUTION_FAILED, iso(-G)),
329
+ });
330
+ expect((await loadBoundExecution(store, "wex_1", NOW))?.live).toBe(true);
331
+ expect((await loadBoundExecution(store, "wex_2", NOW))?.live).toBe(false);
332
+ });
333
+ });
334
+ });