@morty-build/cli 0.4.1 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ME4HCJOA.js → chunk-DUNSEOXK.js} +2 -2
- package/dist/{chunk-ME4HCJOA.js.map → chunk-DUNSEOXK.js.map} +1 -1
- package/dist/{chunk-IXGJ5G6B.js → chunk-IOB4UFOK.js} +763 -780
- package/dist/{chunk-IXGJ5G6B.js.map → chunk-IOB4UFOK.js.map} +4 -4
- package/dist/{cli-TTSS5YD2.js → cli-SYW44S5V.js} +3 -3
- package/dist/cli.js +2 -2
- package/dist/engine/generated/acp-session-store-migrations.d.ts +6 -0
- package/dist/engine/generated/acp-session-store-migrations.d.ts.map +1 -1
- package/dist/engine/generated/acp-session-store-migrations.js +9 -1
- package/dist/engine/generated/acp-session-store-migrations.js.map +1 -1
- package/dist/engine/runtime/command/registry/prompt.d.ts.map +1 -1
- package/dist/engine/runtime/command/registry/prompt.js +16 -3
- package/dist/engine/runtime/command/registry/prompt.js.map +1 -1
- package/dist/engine/schema/{command-cancellation/cancelled-command.d.ts → acp-session-store/command-outcome.d.ts} +10 -7
- package/dist/engine/schema/acp-session-store/command-outcome.d.ts.map +1 -0
- package/dist/engine/schema/acp-session-store/command-outcome.js +10 -0
- package/dist/engine/schema/acp-session-store/command-outcome.js.map +1 -0
- package/dist/engine/schema/acp-session-store/{execution-receipt.d.ts → command.d.ts} +20 -32
- package/dist/engine/schema/acp-session-store/command.d.ts.map +1 -0
- package/dist/engine/schema/acp-session-store/command.js +24 -0
- package/dist/engine/schema/acp-session-store/command.js.map +1 -0
- package/dist/engine/schema/acp-session-store/index.d.ts +2 -2
- package/dist/engine/schema/acp-session-store/index.d.ts.map +1 -1
- package/dist/engine/schema/acp-session-store/index.js +2 -2
- package/dist/engine/schema/acp-session-store/index.js.map +1 -1
- package/dist/engine/session/api/session-api.d.ts +0 -2
- package/dist/engine/session/api/session-api.d.ts.map +1 -1
- package/dist/engine/session/api/session-api.js.map +1 -1
- package/dist/engine/session/api/session-layer.d.ts.map +1 -1
- package/dist/engine/session/api/session-layer.js +3 -7
- package/dist/engine/session/api/session-layer.js.map +1 -1
- package/dist/engine/session/prompt/execute-prompt.d.ts +4 -9
- package/dist/engine/session/prompt/execute-prompt.d.ts.map +1 -1
- package/dist/engine/session/prompt/execute-prompt.js +9 -19
- package/dist/engine/session/prompt/execute-prompt.js.map +1 -1
- package/dist/engine/session/prompt/prompt-lifecycle.d.ts +2 -1
- package/dist/engine/session/prompt/prompt-lifecycle.d.ts.map +1 -1
- package/dist/engine/session/prompt/prompt-lifecycle.js +9 -6
- package/dist/engine/session/prompt/prompt-lifecycle.js.map +1 -1
- package/dist/engine/session/turn/active-turn.d.ts +17 -2
- package/dist/engine/session/turn/active-turn.d.ts.map +1 -1
- package/dist/engine/session/turn/active-turn.js +23 -15
- package/dist/engine/session/turn/active-turn.js.map +1 -1
- package/dist/engine/session/turn/execution-receipts.d.ts +28 -21
- package/dist/engine/session/turn/execution-receipts.d.ts.map +1 -1
- package/dist/engine/session/turn/execution-receipts.js +173 -259
- package/dist/engine/session/turn/execution-receipts.js.map +1 -1
- package/dist/engine/session/turn/session-execution.d.ts +7 -10
- package/dist/engine/session/turn/session-execution.d.ts.map +1 -1
- package/dist/engine/session/turn/session-execution.js +13 -19
- package/dist/engine/session/turn/session-execution.js.map +1 -1
- package/dist/engine/session/turn/session-turns.d.ts +2 -3
- package/dist/engine/session/turn/session-turns.d.ts.map +1 -1
- package/dist/engine/session/turn/session-turns.js.map +1 -1
- package/dist/engine/storage/session/command-ledger-table.d.ts +68 -0
- package/dist/engine/storage/session/command-ledger-table.d.ts.map +1 -0
- package/dist/engine/storage/session/command-ledger-table.js +172 -0
- package/dist/engine/storage/session/command-ledger-table.js.map +1 -0
- package/dist/engine/storage/sqlite/local-databases.d.ts +0 -3
- package/dist/engine/storage/sqlite/local-databases.d.ts.map +1 -1
- package/dist/engine/storage/sqlite/local-databases.js +0 -4
- package/dist/engine/storage/sqlite/local-databases.js.map +1 -1
- package/dist/engine.js +2 -2
- package/package.json +1 -1
- package/dist/engine/generated/command-cancellation-migrations.d.ts +0 -5
- package/dist/engine/generated/command-cancellation-migrations.d.ts.map +0 -1
- package/dist/engine/generated/command-cancellation-migrations.js +0 -9
- package/dist/engine/generated/command-cancellation-migrations.js.map +0 -1
- package/dist/engine/schema/acp-session-store/execution-identity.d.ts +0 -57
- package/dist/engine/schema/acp-session-store/execution-identity.d.ts.map +0 -1
- package/dist/engine/schema/acp-session-store/execution-identity.js +0 -8
- package/dist/engine/schema/acp-session-store/execution-identity.js.map +0 -1
- package/dist/engine/schema/acp-session-store/execution-receipt.d.ts.map +0 -1
- package/dist/engine/schema/acp-session-store/execution-receipt.js +0 -12
- package/dist/engine/schema/acp-session-store/execution-receipt.js.map +0 -1
- package/dist/engine/schema/command-cancellation/cancelled-command.d.ts.map +0 -1
- package/dist/engine/schema/command-cancellation/cancelled-command.js +0 -7
- package/dist/engine/schema/command-cancellation/cancelled-command.js.map +0 -1
- package/dist/engine/schema/command-cancellation/index.d.ts +0 -2
- package/dist/engine/schema/command-cancellation/index.d.ts.map +0 -1
- package/dist/engine/schema/command-cancellation/index.js +0 -2
- package/dist/engine/schema/command-cancellation/index.js.map +0 -1
- package/dist/engine/storage/session/command-cancellation-table.d.ts +0 -17
- package/dist/engine/storage/session/command-cancellation-table.d.ts.map +0 -1
- package/dist/engine/storage/session/command-cancellation-table.js +0 -28
- package/dist/engine/storage/session/command-cancellation-table.js.map +0 -1
- package/dist/engine/storage/session/execution-receipt-table.d.ts +0 -28
- package/dist/engine/storage/session/execution-receipt-table.d.ts.map +0 -1
- package/dist/engine/storage/session/execution-receipt-table.js +0 -87
- package/dist/engine/storage/session/execution-receipt-table.js.map +0 -1
- /package/dist/{cli-TTSS5YD2.js.map → cli-SYW44S5V.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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return false;
|
|
25
|
+
function commandStanding(command, sessionId) {
|
|
26
|
+
if (command === undefined) {
|
|
27
|
+
return { kind: "unseen" };
|
|
33
28
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
#
|
|
96
|
-
#table;
|
|
97
|
-
#cancellations;
|
|
98
|
-
#closing = false;
|
|
66
|
+
#ledger;
|
|
99
67
|
#onCount;
|
|
100
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
#
|
|
130
|
-
|
|
131
|
-
this.#
|
|
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
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
|
122
|
+
/** The outcome the ledger keeps for the command, with the time it was recorded. */
|
|
174
123
|
restatedOutcome = (commandId) => {
|
|
175
|
-
const
|
|
176
|
-
const outcome =
|
|
177
|
-
return 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
|
|
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
|
|
144
|
+
return standing;
|
|
196
145
|
}
|
|
197
|
-
|
|
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.
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
|
198
|
+
/** When the Session's running command started. */
|
|
256
199
|
startedAt = (commandId, sessionId) => {
|
|
257
|
-
const
|
|
258
|
-
return
|
|
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
|
|
205
|
+
* recorded, while the ledger keeps their outcomes.
|
|
265
206
|
*/
|
|
266
207
|
promptOutcomes = (sessionId) => {
|
|
267
208
|
const outcomes = [];
|
|
268
|
-
for (const
|
|
269
|
-
|
|
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
|
-
/**
|
|
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
|
|
233
|
+
return isStarted(standing.command.fate);
|
|
296
234
|
default:
|
|
297
|
-
return
|
|
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
|
|
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.#
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
373
|
-
if (
|
|
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
|
-
|
|
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
|
|
385
|
-
const
|
|
386
|
-
|
|
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:
|
|
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:
|
|
422
|
-
this.#
|
|
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
|
-
|
|
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.
|
|
356
|
+
if (standing.command.commandHash !== commandHash) {
|
|
438
357
|
return { kind: "conflict" };
|
|
439
358
|
}
|
|
440
359
|
return {
|
|
441
|
-
attemptId: standing.
|
|
360
|
+
attemptId: standing.command.attemptId,
|
|
442
361
|
commandId,
|
|
443
362
|
kind: "duplicate",
|
|
444
|
-
state:
|
|
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
|
-
|
|
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
|
|
372
|
+
return standing;
|
|
373
|
+
}
|
|
374
|
+
if (!admits()) {
|
|
375
|
+
return { kind: "refused" };
|
|
457
376
|
}
|
|
458
377
|
const attemptId = randomUUID();
|
|
459
|
-
this.#
|
|
460
|
-
|
|
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
|
|
471
|
-
return
|
|
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
|
|
475
|
-
return
|
|
388
|
+
const command = this.#read(commandId);
|
|
389
|
+
return command?.sessionId === sessionId && !isUnsettled(command.fate);
|
|
476
390
|
};
|
|
477
391
|
close = () => {
|
|
478
|
-
this.#
|
|
479
|
-
this.#
|
|
392
|
+
this.#agentOutcomes.clear();
|
|
393
|
+
this.#shutdownSettlements.clear();
|
|
480
394
|
};
|
|
481
395
|
}
|
|
482
396
|
//# sourceMappingURL=execution-receipts.js.map
|