@morty-build/cli 0.4.3 → 0.4.7

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 (91) hide show
  1. package/dist/{chunk-DVSX6ALB.js → chunk-J56NGK6J.js} +763 -780
  2. package/dist/{chunk-DVSX6ALB.js.map → chunk-J56NGK6J.js.map} +4 -4
  3. package/dist/{chunk-OEX63ZJX.js → chunk-TLYWGG4Q.js} +2 -2
  4. package/dist/{chunk-OEX63ZJX.js.map → chunk-TLYWGG4Q.js.map} +1 -1
  5. package/dist/{cli-3TKBBY7N.js → cli-MJKKZEI4.js} +3 -3
  6. package/dist/cli.js +2 -2
  7. package/dist/engine/generated/acp-session-store-migrations.d.ts +6 -0
  8. package/dist/engine/generated/acp-session-store-migrations.d.ts.map +1 -1
  9. package/dist/engine/generated/acp-session-store-migrations.js +9 -1
  10. package/dist/engine/generated/acp-session-store-migrations.js.map +1 -1
  11. package/dist/engine/runtime/command/registry/prompt.d.ts.map +1 -1
  12. package/dist/engine/runtime/command/registry/prompt.js +16 -3
  13. package/dist/engine/runtime/command/registry/prompt.js.map +1 -1
  14. package/dist/engine/schema/{command-cancellation/cancelled-command.d.ts → acp-session-store/command-outcome.d.ts} +10 -7
  15. package/dist/engine/schema/acp-session-store/command-outcome.d.ts.map +1 -0
  16. package/dist/engine/schema/acp-session-store/command-outcome.js +10 -0
  17. package/dist/engine/schema/acp-session-store/command-outcome.js.map +1 -0
  18. package/dist/engine/schema/acp-session-store/{execution-receipt.d.ts → command.d.ts} +20 -32
  19. package/dist/engine/schema/acp-session-store/command.d.ts.map +1 -0
  20. package/dist/engine/schema/acp-session-store/command.js +24 -0
  21. package/dist/engine/schema/acp-session-store/command.js.map +1 -0
  22. package/dist/engine/schema/acp-session-store/index.d.ts +2 -2
  23. package/dist/engine/schema/acp-session-store/index.d.ts.map +1 -1
  24. package/dist/engine/schema/acp-session-store/index.js +2 -2
  25. package/dist/engine/schema/acp-session-store/index.js.map +1 -1
  26. package/dist/engine/session/api/session-api.d.ts +0 -2
  27. package/dist/engine/session/api/session-api.d.ts.map +1 -1
  28. package/dist/engine/session/api/session-api.js.map +1 -1
  29. package/dist/engine/session/api/session-layer.d.ts.map +1 -1
  30. package/dist/engine/session/api/session-layer.js +3 -7
  31. package/dist/engine/session/api/session-layer.js.map +1 -1
  32. package/dist/engine/session/prompt/execute-prompt.d.ts +4 -9
  33. package/dist/engine/session/prompt/execute-prompt.d.ts.map +1 -1
  34. package/dist/engine/session/prompt/execute-prompt.js +9 -19
  35. package/dist/engine/session/prompt/execute-prompt.js.map +1 -1
  36. package/dist/engine/session/prompt/prompt-lifecycle.d.ts +2 -1
  37. package/dist/engine/session/prompt/prompt-lifecycle.d.ts.map +1 -1
  38. package/dist/engine/session/prompt/prompt-lifecycle.js +9 -6
  39. package/dist/engine/session/prompt/prompt-lifecycle.js.map +1 -1
  40. package/dist/engine/session/turn/active-turn.d.ts +17 -2
  41. package/dist/engine/session/turn/active-turn.d.ts.map +1 -1
  42. package/dist/engine/session/turn/active-turn.js +23 -15
  43. package/dist/engine/session/turn/active-turn.js.map +1 -1
  44. package/dist/engine/session/turn/execution-receipts.d.ts +28 -21
  45. package/dist/engine/session/turn/execution-receipts.d.ts.map +1 -1
  46. package/dist/engine/session/turn/execution-receipts.js +173 -259
  47. package/dist/engine/session/turn/execution-receipts.js.map +1 -1
  48. package/dist/engine/session/turn/session-execution.d.ts +7 -10
  49. package/dist/engine/session/turn/session-execution.d.ts.map +1 -1
  50. package/dist/engine/session/turn/session-execution.js +13 -19
  51. package/dist/engine/session/turn/session-execution.js.map +1 -1
  52. package/dist/engine/session/turn/session-turns.d.ts +2 -3
  53. package/dist/engine/session/turn/session-turns.d.ts.map +1 -1
  54. package/dist/engine/session/turn/session-turns.js.map +1 -1
  55. package/dist/engine/storage/session/command-ledger-table.d.ts +68 -0
  56. package/dist/engine/storage/session/command-ledger-table.d.ts.map +1 -0
  57. package/dist/engine/storage/session/command-ledger-table.js +172 -0
  58. package/dist/engine/storage/session/command-ledger-table.js.map +1 -0
  59. package/dist/engine/storage/sqlite/local-databases.d.ts +0 -3
  60. package/dist/engine/storage/sqlite/local-databases.d.ts.map +1 -1
  61. package/dist/engine/storage/sqlite/local-databases.js +0 -4
  62. package/dist/engine/storage/sqlite/local-databases.js.map +1 -1
  63. package/dist/engine.js +2 -2
  64. package/package.json +1 -1
  65. package/dist/engine/generated/command-cancellation-migrations.d.ts +0 -5
  66. package/dist/engine/generated/command-cancellation-migrations.d.ts.map +0 -1
  67. package/dist/engine/generated/command-cancellation-migrations.js +0 -9
  68. package/dist/engine/generated/command-cancellation-migrations.js.map +0 -1
  69. package/dist/engine/schema/acp-session-store/execution-identity.d.ts +0 -57
  70. package/dist/engine/schema/acp-session-store/execution-identity.d.ts.map +0 -1
  71. package/dist/engine/schema/acp-session-store/execution-identity.js +0 -8
  72. package/dist/engine/schema/acp-session-store/execution-identity.js.map +0 -1
  73. package/dist/engine/schema/acp-session-store/execution-receipt.d.ts.map +0 -1
  74. package/dist/engine/schema/acp-session-store/execution-receipt.js +0 -12
  75. package/dist/engine/schema/acp-session-store/execution-receipt.js.map +0 -1
  76. package/dist/engine/schema/command-cancellation/cancelled-command.d.ts.map +0 -1
  77. package/dist/engine/schema/command-cancellation/cancelled-command.js +0 -7
  78. package/dist/engine/schema/command-cancellation/cancelled-command.js.map +0 -1
  79. package/dist/engine/schema/command-cancellation/index.d.ts +0 -2
  80. package/dist/engine/schema/command-cancellation/index.d.ts.map +0 -1
  81. package/dist/engine/schema/command-cancellation/index.js +0 -2
  82. package/dist/engine/schema/command-cancellation/index.js.map +0 -1
  83. package/dist/engine/storage/session/command-cancellation-table.d.ts +0 -17
  84. package/dist/engine/storage/session/command-cancellation-table.d.ts.map +0 -1
  85. package/dist/engine/storage/session/command-cancellation-table.js +0 -28
  86. package/dist/engine/storage/session/command-cancellation-table.js.map +0 -1
  87. package/dist/engine/storage/session/execution-receipt-table.d.ts +0 -28
  88. package/dist/engine/storage/session/execution-receipt-table.d.ts.map +0 -1
  89. package/dist/engine/storage/session/execution-receipt-table.js +0 -87
  90. package/dist/engine/storage/session/execution-receipt-table.js.map +0 -1
  91. /package/dist/{cli-3TKBBY7N.js.map → cli-MJKKZEI4.js.map} +0 -0
@@ -1,86 +1,56 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { decodeKnownSessionEventPayload, encodeSessionCancelBeforeStartCommand, isPromptTerminal, promptCompletion, } from "@morty-build/client/protocol";
3
3
  import { z } from "zod";
4
- const MAX_TERMINAL_RECEIPTS = 1_024;
5
- // Keep the execution claim after settlement: the outcome alone cannot decide replay safety.
6
- function executionStarted(state) {
7
- switch (state.kind) {
8
- case "pending":
9
- case "settled-before-start":
10
- return false;
11
- case "started":
12
- case "settled-after-start":
13
- return true;
14
- default: {
15
- const unreachable = state;
16
- throw new Error("Unexpected execution state", { cause: unreachable });
17
- }
18
- }
4
+ const isStarted = (fate) => fate.kind === "started" || fate.kind === "settled-after-start";
5
+ const isUnsettled = (fate) => fate.kind === "pending" || fate.kind === "started";
6
+ const lifecyclePayload = (outcome) => {
7
+ const event = decodeKnownSessionEventPayload(JSON.parse(outcome));
8
+ return event?.type === "lifecycle-metadata" ? event.payload : undefined;
9
+ };
10
+ /** A restart failed the command before it started, so its identifier may run again. */
11
+ function restartedBeforeStart(outcome) {
12
+ const payload = lifecyclePayload(outcome);
13
+ return payload?.kind === "prompt.failed-before-start" && payload.code === "runtime_restarted";
19
14
  }
20
- function cancelledBeforeStart(state) {
21
- if (state.kind !== "settled-before-start") {
22
- return false;
15
+ /** How an unsettled command settles with `outcome`: a completion as cancelled before it started is a cancellation. */
16
+ function settledFate(fate, outcome) {
17
+ if (fate.kind === "started") {
18
+ return { kind: "settled-after-start", startedAt: fate.startedAt, outcome };
23
19
  }
24
- const outcome = decodeKnownSessionEventPayload(JSON.parse(state.outcome));
25
- return (outcome?.type === "lifecycle-metadata" &&
26
- outcome.payload.kind === "prompt.completed" &&
27
- promptCompletion(outcome.payload).kind === "cancelled");
20
+ const payload = lifecyclePayload(outcome);
21
+ return payload?.kind === "prompt.completed" && promptCompletion(payload).kind === "cancelled"
22
+ ? { kind: "cancelled", outcome }
23
+ : { kind: "settled-before-start", outcome };
28
24
  }
29
- /** A restart failed the command before it started, so its identifier may run again. */
30
- function restartedBeforeStart(state) {
31
- if (state.kind !== "settled-before-start") {
32
- return false;
25
+ function commandStanding(command, sessionId) {
26
+ if (command === undefined) {
27
+ return { kind: "unseen" };
33
28
  }
34
- const outcome = decodeKnownSessionEventPayload(JSON.parse(state.outcome));
35
- return (outcome?.type === "lifecycle-metadata" &&
36
- outcome.payload.kind === "prompt.failed-before-start" &&
37
- outcome.payload.code === "runtime_restarted");
38
- }
39
- function executionOutcome(state) {
40
- switch (state.kind) {
29
+ if (command.sessionId !== sessionId) {
30
+ return { kind: "foreign" };
31
+ }
32
+ const fate = command.fate;
33
+ switch (fate.kind) {
41
34
  case "pending":
42
35
  case "started":
43
- return null;
36
+ return { kind: "retained", command };
37
+ case "cancelled":
38
+ return fate.outcome === null ? { kind: "cancelled" } : { kind: "retained", command };
44
39
  case "settled-before-start":
40
+ return restartedBeforeStart(fate.outcome) ? { kind: "retryable" } : { kind: "retained", command };
45
41
  case "settled-after-start":
46
- return state.outcome;
47
- default: {
48
- const unreachable = state;
49
- throw new Error("Unexpected execution state", { cause: unreachable });
50
- }
51
- }
52
- }
53
- function settledExecution(state, outcome) {
54
- return { kind: executionStarted(state) ? "settled-after-start" : "settled-before-start", outcome };
55
- }
56
- function commandStanding({ receipt, identity, cancellation }, sessionId) {
57
- if (receipt !== undefined) {
58
- if (receipt.sessionId !== sessionId) {
59
- return { kind: "foreign" };
60
- }
61
- if (!executionStarted(receipt.state) &&
62
- !cancelledBeforeStart(receipt.state) &&
63
- cancellation?.sessionId === sessionId) {
64
- // The cancellation commits before its receipt, so a crash between the two leaves this pair.
65
- return { kind: "cancelled", receipt };
66
- }
67
- return restartedBeforeStart(receipt.state) ? { kind: "retryable", receipt } : { kind: "retained", receipt };
68
- }
69
- if (identity !== undefined) {
70
- return identity.sessionId === sessionId
71
- ? { kind: "expired", commandHash: identity.commandHash }
72
- : { kind: "foreign" };
42
+ return fate.outcome === null
43
+ ? { kind: "expired", commandHash: command.commandHash }
44
+ : { kind: "retained", command };
45
+ default:
46
+ return fate;
73
47
  }
74
- if (cancellation !== undefined) {
75
- return cancellation.sessionId === sessionId ? { kind: "cancelled", receipt: undefined } : { kind: "foreign" };
76
- }
77
- return { kind: "unseen" };
78
48
  }
79
49
  const cancelledBeforeStartOutcome = (commandId) => ({
80
50
  type: "lifecycle-metadata",
81
51
  payload: { kind: "prompt.completed", commandId, stopReason: "cancelled", execution: "not-started" },
82
52
  });
83
- /** A receipt's stored outcome: the event as recorded, with its recording time. */
53
+ /** A stored outcome: the event as recorded, with its recording time. */
84
54
  const storedOutcome = (stored) => {
85
55
  const parsed = JSON.parse(stored);
86
56
  const event = decodeKnownSessionEventPayload(parsed);
@@ -88,95 +58,74 @@ const storedOutcome = (stored) => {
88
58
  ? undefined
89
59
  : { event, recordedAt: z.object({ recordedAt: z.string() }).parse(parsed).recordedAt };
90
60
  };
91
- const unexpectedStanding = (standing) => {
92
- throw new Error("Unexpected command standing", { cause: standing });
93
- };
61
+ /**
62
+ * The Runtime's answers about the commands it admitted, read from the command ledger, which alone records what
63
+ * became of each command.
64
+ */
94
65
  export class ExecutionReceipts {
95
- #executions = new Map();
96
- #table;
97
- #cancellations;
98
- #closing = false;
66
+ #ledger;
99
67
  #onCount;
100
- constructor(table, cancellations, onCount = () => undefined) {
68
+ /**
69
+ * The terminal events this process recorded with the Agent's own failure text. That text can quote user input, so
70
+ * it never enters the ledger, which keeps the content-free form; clients of this process still read the original.
71
+ */
72
+ #agentOutcomes = new Map();
73
+ /**
74
+ * Settlements reached while this process shuts down. They are artifacts of stopping, not of the commands, so the
75
+ * ledger keeps the commands unsettled and the next process reports that the restart interrupted them.
76
+ */
77
+ #shutdownSettlements = new Map();
78
+ #closing = false;
79
+ constructor(ledger, onCount = () => undefined) {
80
+ this.#ledger = ledger;
101
81
  this.#onCount = onCount;
102
- this.#table = table;
103
- this.#cancellations = cancellations;
104
- const retained = [];
105
- for (const { commandId, started, outcome, updatedAt: _updatedAt, ...identity } of table.list()) {
106
- const pending = { kind: started ? "started" : "pending" };
107
- const state = outcome === null ? pending : settledExecution(pending, outcome);
108
- this.#executions.set(commandId, {
109
- ...identity,
110
- state,
111
- });
112
- if (cancelledBeforeStart(state)) {
113
- retained.push({ commandId, sessionId: identity.sessionId });
114
- }
115
- }
116
- cancellations.retain(retained);
117
82
  }
118
- /** Reads every ledger that can still change the answer and classifies the identifier for the Session. */
119
- #standing = (commandId, sessionId) => {
120
- const receipt = this.#executions.get(commandId);
121
- // A started receipt, or one settled as cancelled, is final; only the others consult the cancellation ledger.
122
- const final = receipt !== undefined && (executionStarted(receipt.state) || cancelledBeforeStart(receipt.state));
123
- return commandStanding({
124
- receipt,
125
- identity: receipt === undefined ? this.#table.identity(commandId) : undefined,
126
- cancellation: final ? undefined : this.#cancellations.read(commandId),
127
- }, sessionId);
83
+ #withShutdownSettlement = (command) => {
84
+ const fate = this.#shutdownSettlements.get(command.commandId);
85
+ return fate === undefined ? command : { ...command, fate };
128
86
  };
129
- #remember = (commandId, receipt, evicted) => {
130
- // Live failure detail may quote user input; only the durable receipt goes to the table.
131
- this.#executions.set(commandId, receipt);
132
- for (const evictedCommandId of evicted) {
133
- this.#executions.delete(evictedCommandId);
134
- }
135
- let activeCount = 0;
136
- for (const entry of this.#executions.values()) {
137
- if (executionOutcome(entry.state) === null) {
138
- activeCount += 1;
139
- }
140
- }
141
- this.#onCount(activeCount);
87
+ #read = (commandId) => {
88
+ const command = this.#ledger.read(commandId);
89
+ return command === undefined ? undefined : this.#withShutdownSettlement(command);
142
90
  };
143
- #save = (commandId, receipt, durableOutcome = executionOutcome(receipt.state)) => {
144
- const { state, ...identity } = receipt;
145
- if (!this.#closing && cancelledBeforeStart(state)) {
146
- // Commit the non-replayable fact first. A crash before saving/pruning the receipt must not reopen work.
147
- this.#cancellations.retain([{ commandId, sessionId: receipt.sessionId }]);
148
- // Cancellation is already authoritative even if the detailed receipt write fails.
149
- this.#remember(commandId, receipt, []);
91
+ #standing = (commandId, sessionId) => commandStanding(this.#read(commandId), sessionId);
92
+ /** The outcome clients read for a settled command: what this process recorded, else the ledger's form. */
93
+ #outcome = ({ commandId, fate }) => {
94
+ if (isUnsettled(fate) || fate.outcome === null) {
95
+ return undefined;
150
96
  }
151
- const evicted = this.#closing
152
- ? []
153
- : this.#table.save({
154
- ...identity,
155
- commandId,
156
- started: executionStarted(state),
157
- outcome: durableOutcome,
158
- updatedAt: new Date().toISOString(),
159
- }, MAX_TERMINAL_RECEIPTS);
160
- this.#remember(commandId, receipt, evicted);
97
+ return this.#agentOutcomes.get(commandId) ?? fate.outcome;
161
98
  };
162
- #nextAdmissionOrder = () => {
163
- let latest = 0;
164
- for (const receipt of this.#executions.values()) {
165
- latest = Math.max(latest, receipt.admissionOrder);
99
+ #publishCount = () => {
100
+ this.#onCount(this.#ledger.unsettled().filter((commandId) => !this.#shutdownSettlements.has(commandId)).length);
101
+ };
102
+ /** Settles an unsettled command. `agentOutcome` is the outcome with the Agent's text, when it differs. */
103
+ #settle = (commandId, fate, outcome, agentOutcome) => {
104
+ const settled = settledFate(fate, outcome);
105
+ if (agentOutcome !== undefined) {
106
+ this.#agentOutcomes.set(commandId, agentOutcome);
107
+ }
108
+ if (this.#closing) {
109
+ this.#shutdownSettlements.set(commandId, settled);
110
+ }
111
+ else {
112
+ for (const dropped of this.#ledger.settle(commandId, settled.kind, outcome)) {
113
+ this.#agentOutcomes.delete(dropped);
114
+ }
166
115
  }
167
- return latest + 1;
116
+ this.#publishCount();
168
117
  };
169
118
  beginShutdown = () => {
170
119
  this.#closing = true;
171
120
  };
172
121
  outcome = (commandId) => this.restatedOutcome(commandId)?.event;
173
- /** The outcome the command's receipt holds, with the time it was recorded. */
122
+ /** The outcome the ledger keeps for the command, with the time it was recorded. */
174
123
  restatedOutcome = (commandId) => {
175
- const receipt = this.#executions.get(commandId);
176
- const outcome = receipt === undefined ? null : executionOutcome(receipt.state);
177
- return outcome === null ? undefined : storedOutcome(outcome);
124
+ const command = this.#read(commandId);
125
+ const outcome = command === undefined ? undefined : this.#outcome(command);
126
+ return outcome === undefined ? undefined : storedOutcome(outcome);
178
127
  };
179
- /** The command's delivery as these receipts know it; another Session's command reads as unknown. */
128
+ /** The command's delivery as the ledger knows it; another Session's command reads as unknown. */
180
129
  delivery = (commandId, sessionId) => {
181
130
  const standing = this.#standing(commandId, sessionId);
182
131
  switch (standing.kind) {
@@ -192,9 +141,10 @@ export class ExecutionReceipts {
192
141
  case "retained":
193
142
  break;
194
143
  default:
195
- return unexpectedStanding(standing);
144
+ return standing;
196
145
  }
197
- if (executionOutcome(standing.receipt.state) === null) {
146
+ const command = this.#read(commandId);
147
+ if (command === undefined || isUnsettled(command.fate)) {
198
148
  return { state: "accepted" };
199
149
  }
200
150
  const outcome = this.outcome(commandId);
@@ -210,66 +160,54 @@ export class ExecutionReceipts {
210
160
  }
211
161
  };
212
162
  /**
213
- * How the Session's most recently admitted turn ended. Admission order survives updates and
214
- * restarts. Commands that never started (queued, or reserved for cancellation) are not turns,
215
- * unless they failed before starting. A steering command that completed as `steered` joined the
216
- * turn it steered, which reports for both; a steer not known to have joined (it failed, or a
217
- * restart interrupted it) keeps its own outcome. Undefined while the latest turn runs or none is
218
- * retained.
163
+ * How the Session's most recently admitted turn ended. Commands that never started (queued, or reserved for
164
+ * cancellation) are not turns, unless they failed before starting. A steering command that completed as `steered`
165
+ * joined the turn it steered, which reports for both; a steer not known to have joined (it failed, or a restart
166
+ * interrupted it) keeps its own outcome. Undefined while the latest turn runs or its outcome is no longer kept.
219
167
  */
220
168
  lastTurn = (sessionId) => {
221
169
  let last;
222
- let latestOrder = 0;
223
- for (const [commandId, receipt] of this.#executions) {
224
- if (receipt.sessionId !== sessionId) {
225
- continue;
226
- }
227
- const outcome = this.outcome(commandId);
170
+ for (const stored of this.#ledger.session(sessionId)) {
171
+ const command = this.#withShutdownSettlement(stored);
172
+ const outcome = this.outcome(command.commandId);
228
173
  const payload = outcome?.type === "lifecycle-metadata" ? outcome.payload : undefined;
229
174
  const failedBeforeStart = payload?.kind === "prompt.failed-before-start";
230
175
  const steered = payload?.kind === "prompt.completed" && promptCompletion(payload).kind === "steered";
231
- if ((executionStarted(receipt.state) || failedBeforeStart) &&
232
- !steered &&
233
- receipt.admissionOrder > latestOrder) {
234
- last = commandId;
235
- latestOrder = receipt.admissionOrder;
176
+ const kept = command.fate.kind === "started" || outcome !== undefined;
177
+ if (kept && (isStarted(command.fate) || failedBeforeStart) && !steered) {
178
+ last = { commandId: command.commandId, outcome };
236
179
  }
237
180
  }
238
- const outcome = last === undefined ? undefined : this.outcome(last);
181
+ const outcome = last?.outcome;
239
182
  if (last === undefined || outcome?.type !== "lifecycle-metadata" || !isPromptTerminal(outcome.payload)) {
240
183
  return undefined;
241
184
  }
242
185
  switch (outcome.payload.kind) {
243
186
  case "prompt.completed":
244
- return { commandId: last, state: "completed", stopReason: outcome.payload.stopReason };
187
+ return { commandId: last.commandId, state: "completed", stopReason: outcome.payload.stopReason };
245
188
  case "prompt.failed":
246
189
  case "prompt.failed-before-start":
247
190
  return {
248
191
  code: outcome.payload.code,
249
- commandId: last,
192
+ commandId: last.commandId,
250
193
  message: outcome.payload.message,
251
194
  state: "failed",
252
195
  };
253
196
  }
254
197
  };
255
- /** When the Session's running command started, while its receipt holds the time. */
198
+ /** When the Session's running command started. */
256
199
  startedAt = (commandId, sessionId) => {
257
- const receipt = this.#executions.get(commandId);
258
- return receipt?.sessionId === sessionId && receipt.state.kind === "started"
259
- ? receipt.state.startedAt
260
- : undefined;
200
+ const command = this.#read(commandId);
201
+ return command?.sessionId === sessionId && command.fate.kind === "started" ? command.fate.startedAt : undefined;
261
202
  };
262
203
  /**
263
204
  * The recorded terminal events of the Session's settled prompts whose Agent reported the user message they
264
- * recorded, while their receipts are retained.
205
+ * recorded, while the ledger keeps their outcomes.
265
206
  */
266
207
  promptOutcomes = (sessionId) => {
267
208
  const outcomes = [];
268
- for (const [commandId, receipt] of this.#executions) {
269
- if (receipt.sessionId !== sessionId) {
270
- continue;
271
- }
272
- const outcome = this.restatedOutcome(commandId);
209
+ for (const command of this.#ledger.session(sessionId)) {
210
+ const outcome = this.restatedOutcome(command.commandId);
273
211
  const event = outcome?.event;
274
212
  if (outcome?.recordedAt !== undefined &&
275
213
  event?.type === "lifecycle-metadata" &&
@@ -280,7 +218,7 @@ export class ExecutionReceipts {
280
218
  }
281
219
  return outcomes;
282
220
  };
283
- /** Started identities remain authoritative after their detailed receipt is evicted. */
221
+ /** A command that started stays started after its outcome is no longer kept. */
284
222
  hasStarted = (commandId, sessionId) => {
285
223
  const standing = this.#standing(commandId, sessionId);
286
224
  switch (standing.kind) {
@@ -292,9 +230,9 @@ export class ExecutionReceipts {
292
230
  case "expired":
293
231
  return true;
294
232
  case "retained":
295
- return executionStarted(standing.receipt.state);
233
+ return isStarted(standing.command.fate);
296
234
  default:
297
- return unexpectedStanding(standing);
235
+ return standing;
298
236
  }
299
237
  };
300
238
  /**
@@ -322,7 +260,7 @@ export class ExecutionReceipts {
322
260
  case "unseen":
323
261
  break;
324
262
  default:
325
- return unexpectedStanding(standing);
263
+ return standing;
326
264
  }
327
265
  const outcome = cancelledBeforeStartOutcome(commandId);
328
266
  // The identifier belongs to the cancellation, so a later prompt conflicts rather than executing.
@@ -333,22 +271,15 @@ export class ExecutionReceipts {
333
271
  sessionId,
334
272
  }))
335
273
  .digest("hex");
336
- this.#save(commandId, {
337
- admissionOrder: this.#nextAdmissionOrder(),
338
- attemptId: randomUUID(),
339
- commandHash,
340
- sessionId,
341
- state: {
342
- kind: "settled-before-start",
343
- outcome: JSON.stringify({ ...outcome, recordedAt: new Date().toISOString() }),
344
- },
345
- });
274
+ for (const dropped of this.#ledger.admitCancelled({ attemptId: randomUUID(), commandHash, commandId, sessionId }, JSON.stringify({ ...outcome, recordedAt: new Date().toISOString() }))) {
275
+ this.#agentOutcomes.delete(dropped);
276
+ }
346
277
  return { kind: "recorded", outcome };
347
278
  };
348
279
  /**
349
- * Receipts follow their command's recorded lifecycle. `prompt.started` is recorded where the command is handed
280
+ * The ledger follows each command's recorded lifecycle. `prompt.started` is recorded where the command is handed
350
281
  * to the Agent, so the started mark and the event clients see are one transition; a terminal event settles the
351
- * receipt. Nothing else marks a command started.
282
+ * command. Nothing else marks a command started.
352
283
  */
353
284
  record = (event) => {
354
285
  if (event.type !== "lifecycle-metadata") {
@@ -356,77 +287,65 @@ export class ExecutionReceipts {
356
287
  }
357
288
  const payload = event.payload;
358
289
  if (payload.kind === "prompt.started") {
359
- const receipt = this.#executions.get(payload.commandId);
360
290
  // A start reported after settlement does not reopen the execution.
361
- if (receipt?.state.kind === "pending") {
362
- this.#save(payload.commandId, {
363
- ...receipt,
364
- state: { kind: "started", startedAt: event.recordedAt },
365
- });
366
- }
291
+ this.#ledger.start(payload.commandId, event.recordedAt);
367
292
  return;
368
293
  }
369
294
  if (!isPromptTerminal(payload)) {
370
295
  return;
371
296
  }
372
- const receipt = this.#executions.get(payload.commandId);
373
- if (receipt === undefined) {
297
+ const fate = this.#read(payload.commandId)?.fate;
298
+ if (fate === undefined || !isUnsettled(fate)) {
374
299
  return;
375
300
  }
376
- const sanitized = payload.kind === "prompt.completed"
377
- ? payload
378
- : { ...payload, message: "The local Agent could not complete the request" };
379
301
  const outcome = { type: "lifecycle-metadata", payload, recordedAt: event.recordedAt };
380
- this.#save(payload.commandId, { ...receipt, state: settledExecution(receipt.state, JSON.stringify(outcome)) }, JSON.stringify({ ...outcome, payload: sanitized }));
302
+ if (payload.kind === "prompt.completed") {
303
+ this.#settle(payload.commandId, fate, JSON.stringify(outcome));
304
+ return;
305
+ }
306
+ const contentFree = { ...payload, message: "The local Agent could not complete the request" };
307
+ this.#settle(payload.commandId, fate, JSON.stringify({ ...outcome, payload: contentFree }), JSON.stringify(outcome));
381
308
  };
382
309
  recover = () => {
383
310
  const recovered = [];
384
- for (const [commandId, receipt] of [...this.#executions]) {
385
- const outcome = executionOutcome(receipt.state);
386
- // A cancellation committed without its receipt settles the receipt as that cancellation.
387
- const restoreCancellation = this.#standing(commandId, receipt.sessionId).kind === "cancelled";
388
- const event = restoreCancellation
389
- ? cancelledBeforeStartOutcome(commandId)
390
- : outcome === null
391
- ? {
392
- type: "lifecycle-metadata",
393
- payload: executionStarted(receipt.state)
394
- ? {
395
- kind: "prompt.failed",
396
- commandId,
397
- attemptId: receipt.attemptId,
398
- code: "runtime_restarted",
399
- message: "The local runtime restarted while this request was running",
400
- }
401
- : {
402
- kind: "prompt.failed-before-start",
403
- commandId,
404
- code: "runtime_restarted",
405
- message: "The local runtime restarted before this request started",
406
- },
407
- }
408
- : decodeKnownSessionEventPayload(JSON.parse(outcome));
409
- if (event === null) {
410
- continue;
411
- }
412
- if (!restoreCancellation && outcome !== null) {
311
+ for (const command of this.#ledger.all()) {
312
+ const fate = command.fate;
313
+ if (!isUnsettled(fate)) {
413
314
  // The previous process recorded this outcome; publishing it again restates that fact.
414
- const restated = storedOutcome(outcome);
315
+ const restated = fate.outcome === null ? undefined : storedOutcome(fate.outcome);
415
316
  if (restated !== undefined) {
416
- recovered.push({ kind: "restated", sessionId: receipt.sessionId, outcome: restated });
317
+ recovered.push({ kind: "restated", sessionId: command.sessionId, outcome: restated });
417
318
  }
418
319
  continue;
419
320
  }
321
+ const event = {
322
+ type: "lifecycle-metadata",
323
+ payload: fate.kind === "started"
324
+ ? {
325
+ kind: "prompt.failed",
326
+ commandId: command.commandId,
327
+ attemptId: command.attemptId,
328
+ code: "runtime_restarted",
329
+ message: "The local runtime restarted while this request was running",
330
+ }
331
+ : {
332
+ kind: "prompt.failed-before-start",
333
+ commandId: command.commandId,
334
+ code: "runtime_restarted",
335
+ message: "The local runtime restarted before this request started",
336
+ },
337
+ };
420
338
  const recordedAt = new Date().toISOString();
421
- recovered.push({ kind: "recorded", sessionId: receipt.sessionId, event, recordedAt });
422
- this.#save(commandId, {
423
- ...receipt,
424
- state: settledExecution(receipt.state, JSON.stringify({ ...event, recordedAt })),
425
- });
339
+ recovered.push({ kind: "recorded", sessionId: command.sessionId, event, recordedAt });
340
+ this.#settle(command.commandId, fate, JSON.stringify({ ...event, recordedAt }));
426
341
  }
427
342
  return recovered;
428
343
  };
429
- claimExecution = (commandId, sessionId, commandJson) => {
344
+ /**
345
+ * Claims a command's identifier for the Session. A known identifier is answered from the ledger; a new command is
346
+ * recorded only when `admits` accepts it in the same step, so a command the Session refuses leaves no record.
347
+ */
348
+ claimExecution = (commandId, sessionId, commandJson, admits) => {
430
349
  const commandHash = createHash("sha256").update(commandJson).digest("hex");
431
350
  const standing = this.#standing(commandId, sessionId);
432
351
  switch (standing.kind) {
@@ -434,49 +353,44 @@ export class ExecutionReceipts {
434
353
  case "cancelled":
435
354
  return { kind: "conflict" };
436
355
  case "retained":
437
- if (standing.receipt.commandHash !== commandHash) {
356
+ if (standing.command.commandHash !== commandHash) {
438
357
  return { kind: "conflict" };
439
358
  }
440
359
  return {
441
- attemptId: standing.receipt.attemptId,
360
+ attemptId: standing.command.attemptId,
442
361
  commandId,
443
362
  kind: "duplicate",
444
- state: executionOutcome(standing.receipt.state) === null ? "unsettled" : "settled",
363
+ state: isUnsettled(standing.command.fate) ? "unsettled" : "settled",
445
364
  };
446
365
  case "expired":
447
366
  return standing.commandHash === commandHash ? { kind: "expired" } : { kind: "conflict" };
448
367
  case "retryable":
449
- // Forget the restart failure, then claim as if it had never been received.
450
- this.#table.remove(commandId);
451
- this.#executions.delete(commandId);
452
- return this.claimExecution(commandId, sessionId, commandJson);
368
+ // A restart failed it before it started: the admission replaces that failure.
453
369
  case "unseen":
454
370
  break;
455
371
  default:
456
- return unexpectedStanding(standing);
372
+ return standing;
373
+ }
374
+ if (!admits()) {
375
+ return { kind: "refused" };
457
376
  }
458
377
  const attemptId = randomUUID();
459
- this.#save(commandId, {
460
- admissionOrder: this.#nextAdmissionOrder(),
461
- attemptId,
462
- commandHash,
463
- sessionId,
464
- state: { kind: "pending" },
465
- });
378
+ this.#ledger.admit({ attemptId, commandHash, commandId, sessionId });
379
+ this.#publishCount();
466
380
  return { attemptId, commandId, kind: "accepted" };
467
381
  };
468
382
  /** Only commands without a terminal result can still be cancelled. */
469
383
  findUnsettledAttemptId = (commandId, sessionId) => {
470
- const receipt = this.#executions.get(commandId);
471
- return receipt?.sessionId === sessionId && executionOutcome(receipt.state) === null ? receipt.attemptId : null;
384
+ const command = this.#read(commandId);
385
+ return command?.sessionId === sessionId && isUnsettled(command.fate) ? command.attemptId : null;
472
386
  };
473
387
  isTerminalExecution = (commandId, sessionId) => {
474
- const receipt = this.#executions.get(commandId);
475
- return receipt?.sessionId === sessionId && executionOutcome(receipt.state) !== null;
388
+ const command = this.#read(commandId);
389
+ return command?.sessionId === sessionId && !isUnsettled(command.fate);
476
390
  };
477
391
  close = () => {
478
- this.#executions.clear();
479
- this.#cancellations.close();
392
+ this.#agentOutcomes.clear();
393
+ this.#shutdownSettlements.clear();
480
394
  };
481
395
  }
482
396
  //# sourceMappingURL=execution-receipts.js.map