@cotal-ai/runtime 0.57.0 → 0.59.0
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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/journal-store.d.ts +27 -2
- package/dist/journal-store.d.ts.map +1 -1
- package/dist/journal-store.js +42 -2
- package/dist/journal-store.js.map +1 -1
- package/dist/mesh-handler.d.ts +23 -19
- package/dist/mesh-handler.d.ts.map +1 -1
- package/dist/mesh-handler.js +120 -32
- package/dist/mesh-handler.js.map +1 -1
- package/dist/resolve-checkpoint.d.ts +51 -0
- package/dist/resolve-checkpoint.d.ts.map +1 -1
- package/dist/resolve-checkpoint.js +121 -3
- package/dist/resolve-checkpoint.js.map +1 -1
- package/dist/run-command.d.ts +1 -1
- package/dist/run-command.d.ts.map +1 -1
- package/dist/run-command.js +103 -116
- package/dist/run-command.js.map +1 -1
- package/dist/run-driver.d.ts +17 -0
- package/dist/run-driver.d.ts.map +1 -1
- package/dist/run-driver.js +54 -13
- package/dist/run-driver.js.map +1 -1
- package/dist/run-effect-host.d.ts +1 -0
- package/dist/run-effect-host.d.ts.map +1 -1
- package/dist/run-effect-host.js +1 -0
- package/dist/run-effect-host.js.map +1 -1
- package/dist/run-host.d.ts +11 -2
- package/dist/run-host.d.ts.map +1 -1
- package/dist/run-host.js +97 -12
- package/dist/run-host.js.map +1 -1
- package/dist/run-scope-authority.d.ts +16 -1
- package/dist/run-scope-authority.d.ts.map +1 -1
- package/dist/run-scope-authority.js +51 -10
- package/dist/run-scope-authority.js.map +1 -1
- package/package.json +5 -5
|
@@ -27,8 +27,13 @@
|
|
|
27
27
|
* "the checkpoint named `approve` in this run". The journal is what maps one to the other, and it is
|
|
28
28
|
* also what says whether that step is still open — which is the question a resolver most needs
|
|
29
29
|
* answered before it collects a human's decision.
|
|
30
|
+
*
|
|
31
|
+
* **A settled step is never re-answered, but it can be AMENDED.** The pause stays settled and the run
|
|
32
|
+
* keeps the answer it acted on; {@link amendAcceptedAnswer} files a later position beside that
|
|
33
|
+
* answer, naming it, so a participant who changes their mind records it on the step that holds the
|
|
34
|
+
* original rather than somewhere else.
|
|
30
35
|
*/
|
|
31
|
-
import { replayRunJournal, newTakeoverId, recordCheckpointAnswer, checkpointAnswerId, readCheckpointSpec, resumeCheckpoint, } from "@cotal-ai/core";
|
|
36
|
+
import { replayRunJournal, newTakeoverId, recordCheckpointAnswer, checkpointAnswerId, newAmendmentId, readCheckpointAnswer, readCheckpointSpec, readCheckpointStatus, resumeCheckpoint, } from "@cotal-ai/core";
|
|
32
37
|
import { journalEntryKeyString } from "@cotal-ai/lang";
|
|
33
38
|
/** No open checkpoint (or ask) answers to this address in this run. */
|
|
34
39
|
export class CheckpointNotOpen extends Error {
|
|
@@ -48,6 +53,24 @@ export class CheckpointNotOpen extends Error {
|
|
|
48
53
|
this.name = "CheckpointNotOpen";
|
|
49
54
|
}
|
|
50
55
|
}
|
|
56
|
+
/** No accepted answer at this address that an amendment could be filed beside. */
|
|
57
|
+
export class CheckpointNotAmendable extends Error {
|
|
58
|
+
runId;
|
|
59
|
+
stepKey;
|
|
60
|
+
why;
|
|
61
|
+
constructor(runId, stepKey, why) {
|
|
62
|
+
super(`run ${runId} has no accepted answer to amend at ${stepKey}: ${{
|
|
63
|
+
unknown: "no step is recorded under that key",
|
|
64
|
+
open: "that step is still open, so answer it instead",
|
|
65
|
+
"not-a-checkpoint": "that step is neither a checkpoint nor an ask",
|
|
66
|
+
unanswered: "that step settled without accepting an answer",
|
|
67
|
+
}[why]}`);
|
|
68
|
+
this.runId = runId;
|
|
69
|
+
this.stepKey = stepKey;
|
|
70
|
+
this.why = why;
|
|
71
|
+
this.name = "CheckpointNotAmendable";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
51
74
|
/**
|
|
52
75
|
* Answer a run's open checkpoint over ONE set of planes: {@link locateOpenCheckpoint} then
|
|
53
76
|
* {@link answerOpenCheckpoint}. A caller whose credential must be pinned to the pause before it may
|
|
@@ -78,13 +101,46 @@ export async function resolveCheckpoint(deps, req) {
|
|
|
78
101
|
export async function locateOpenCheckpoint(deps, req) {
|
|
79
102
|
const entries = await replayRunEntries(deps, req.runId, req.takeoverId);
|
|
80
103
|
const token = openCheckpointToken(entries, req.runId, req.stepKey);
|
|
81
|
-
const spec = await
|
|
104
|
+
const spec = await readSpecPastTheMintWindow(deps, token);
|
|
82
105
|
if (spec === undefined) {
|
|
83
|
-
throw new Error(`checkpoint "${token}" has a journal entry but no record on endpoint ${deps.endpoint}
|
|
106
|
+
throw new Error(`checkpoint "${token}" has a journal entry but no record on endpoint ${deps.endpoint} `
|
|
107
|
+
+ `after ${MINT_WINDOW_ATTEMPTS} reads over ${(MINT_WINDOW_ATTEMPTS - 1) * MINT_WINDOW_STEP_MS}ms; `
|
|
84
108
|
+ `refusing to guess a presenter — reconcile the store before answering`);
|
|
85
109
|
}
|
|
86
110
|
return { token, holder: spec.holder };
|
|
87
111
|
}
|
|
112
|
+
/** Reads long enough to tell a mint in flight from a torn store, and no longer.
|
|
113
|
+
*
|
|
114
|
+
* `checkpoint` in the mesh handler appends the step's PENDING journal entry before it mints the
|
|
115
|
+
* pause's record, deliberately: its own comment calls that the harmless direction, because a crash
|
|
116
|
+
* between the two leaves an entry saying what it was going to ask rather than a pause nothing
|
|
117
|
+
* records. The cost is a window in which the two halves this resolver needs are not both readable
|
|
118
|
+
* yet, and one read cannot tell that window from the store being torn — the SAME observation means
|
|
119
|
+
* either. What separates them is time: the window closes in a couple of round trips, and a torn
|
|
120
|
+
* store never closes.
|
|
121
|
+
*
|
|
122
|
+
* Bounded on purpose, both ways. Refusing on the first read turns a checkpoint that is about to be
|
|
123
|
+
* answerable into "reconcile the store", which is the wrong instruction and, at the operator
|
|
124
|
+
* surface, an answer a human has to give twice. Waiting without a bound would hang that human on a
|
|
125
|
+
* pause that will never be answerable. The window is a KV create plus its status write on a loaded
|
|
126
|
+
* host, so a low single-digit number of seconds covers it with room to spare.
|
|
127
|
+
*
|
|
128
|
+
* Only the record is re-read. The token is a fact about the journal at the moment it was replayed,
|
|
129
|
+
* and a replay binds its own consumer, so re-reading the journal per attempt would contend with
|
|
130
|
+
* the driver for a staleness this call already carries either way. */
|
|
131
|
+
const MINT_WINDOW_ATTEMPTS = 10;
|
|
132
|
+
const MINT_WINDOW_STEP_MS = 200;
|
|
133
|
+
async function readSpecPastTheMintWindow(deps, token) {
|
|
134
|
+
const ref = { endpoint: deps.endpoint, token };
|
|
135
|
+
for (let i = 0; i < MINT_WINDOW_ATTEMPTS; i += 1) {
|
|
136
|
+
const spec = await readCheckpointSpec(deps.kv, ref);
|
|
137
|
+
if (spec !== undefined)
|
|
138
|
+
return spec;
|
|
139
|
+
if (i + 1 < MINT_WINDOW_ATTEMPTS)
|
|
140
|
+
await new Promise((r) => setTimeout(r, MINT_WINDOW_STEP_MS));
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
88
144
|
/**
|
|
89
145
|
* The WRITE half of an answer: file the answer record for the located pause, then present its
|
|
90
146
|
* token as the arming holder. Every write is keyed by `open.token`, so a credential minted for this
|
|
@@ -115,6 +171,68 @@ export async function answerOpenCheckpoint(deps, req) {
|
|
|
115
171
|
});
|
|
116
172
|
return { token, answerId, settle };
|
|
117
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* The READ half of an amendment: replay the run's journal to the settled pause at `stepKey`, then
|
|
176
|
+
* read which answer its settlement accepted off the checkpoint's status record. Nothing is written.
|
|
177
|
+
* The status names the id for an `ask` as well as a checkpoint, where the journal's frozen result
|
|
178
|
+
* names it for a checkpoint only.
|
|
179
|
+
*/
|
|
180
|
+
export async function locateAcceptedAnswer(deps, req) {
|
|
181
|
+
const entries = await replayRunEntries(deps, req.runId, req.takeoverId);
|
|
182
|
+
const token = settledPauseToken(entries, req.runId, req.stepKey);
|
|
183
|
+
if (token === undefined)
|
|
184
|
+
throw new CheckpointNotAmendable(req.runId, req.stepKey, "unanswered");
|
|
185
|
+
const status = await readCheckpointStatus(deps.kv, { endpoint: deps.endpoint, token });
|
|
186
|
+
const answerId = status?.value.state === "resumed" ? status.value.settledAnswerId : undefined;
|
|
187
|
+
if (answerId === undefined)
|
|
188
|
+
throw new CheckpointNotAmendable(req.runId, req.stepKey, "unanswered");
|
|
189
|
+
const accepted = await readCheckpointAnswer(deps.kv, deps.endpoint, token, answerId);
|
|
190
|
+
if (accepted === undefined)
|
|
191
|
+
throw new Error(`checkpoint "${token}" settled naming the answer ${answerId}, which is not on record; reconcile the store before amending`);
|
|
192
|
+
return { token, answerId, by: accepted.by };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The WRITE half of an amendment: file a create-only answer record under the accepted answer's
|
|
196
|
+
* token that names it as `supersedes`, under a fresh id, so every call is its own filing. No token
|
|
197
|
+
* is presented, so the pause stays settled and the run never reads this record; the journal lists
|
|
198
|
+
* it under the step.
|
|
199
|
+
*/
|
|
200
|
+
export async function amendAcceptedAnswer(deps, req) {
|
|
201
|
+
const { token, answerId: supersedes } = req.accepted;
|
|
202
|
+
const answerId = newAmendmentId();
|
|
203
|
+
await recordCheckpointAnswer(deps.kv, deps.endpoint, {
|
|
204
|
+
v: 1,
|
|
205
|
+
token,
|
|
206
|
+
answerId,
|
|
207
|
+
...(req.value !== undefined ? { value: req.value } : {}),
|
|
208
|
+
...(req.artifact !== undefined ? { artifact: req.artifact } : {}),
|
|
209
|
+
by: req.by,
|
|
210
|
+
at: req.now,
|
|
211
|
+
supersedes,
|
|
212
|
+
});
|
|
213
|
+
return { token, answerId, supersedes };
|
|
214
|
+
}
|
|
215
|
+
/** The token a settled checkpoint (or ask) settled under, or a loud refusal naming why the step has
|
|
216
|
+
* none to amend. A checkpoint settles under its request id; an `ask` under its LAST attempt's
|
|
217
|
+
* token, which rides the step's pending entries as `askToken` (attempt 1 is the request id). */
|
|
218
|
+
export function settledPauseToken(entries, runId, stepKey) {
|
|
219
|
+
let entry;
|
|
220
|
+
let askToken;
|
|
221
|
+
for (const e of entries) {
|
|
222
|
+
if (journalEntryKeyString(e) !== stepKey)
|
|
223
|
+
continue;
|
|
224
|
+
entry = e;
|
|
225
|
+
if (typeof e.external?.askToken === "string")
|
|
226
|
+
askToken = e.external.askToken;
|
|
227
|
+
}
|
|
228
|
+
if (entry === undefined)
|
|
229
|
+
throw new CheckpointNotAmendable(runId, stepKey, "unknown");
|
|
230
|
+
if (entry.kind !== "checkpoint" && entry.kind !== "ask")
|
|
231
|
+
throw new CheckpointNotAmendable(runId, stepKey, "not-a-checkpoint");
|
|
232
|
+
if (entry.state === "pending")
|
|
233
|
+
throw new CheckpointNotAmendable(runId, stepKey, "open");
|
|
234
|
+
return entry.kind === "ask" ? askToken ?? entry.requestId : entry.requestId;
|
|
235
|
+
}
|
|
118
236
|
/** The token of the open checkpoint (or ask attempt) at this address, or a loud refusal naming
|
|
119
237
|
* which it is not. An `ask` parks on the checkpoint plane too — one pause per attempt — and the
|
|
120
238
|
* CURRENT attempt's token rides the entry's external state as `askToken` (attempt 1 is the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-checkpoint.js","sourceRoot":"","sources":["../src/resolve-checkpoint.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"resolve-checkpoint.js","sourceRoot":"","sources":["../src/resolve-checkpoint.ts"],"names":[],"mappings":"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"}
|
package/dist/run-command.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { EpEnvelopeError, type ParsedArgs } from "@cotal-ai/core";
|
|
|
20
20
|
* fixture with no manager at all, and an operator there would have been sent to check a version.
|
|
21
21
|
*/
|
|
22
22
|
export declare function unansweredManagerRefusal(e: EpEnvelopeError): string;
|
|
23
|
-
/** `cotal run <start|resume|ps|journal|answer>` — dispatch. The manager hosts by default;
|
|
23
|
+
/** `cotal run <start|resume|ps|journal|answer|amend>` — dispatch. The manager hosts by default;
|
|
24
24
|
* `--local` drives in this process over one connection per invocation. */
|
|
25
25
|
export declare function runWorkflow(args: ParsedArgs): Promise<void>;
|
|
26
26
|
//# sourceMappingURL=run-command.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../src/run-command.ts"],"names":[],"mappings":"AAuBA,OAAO,EAEL,eAAe,
|
|
1
|
+
{"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../src/run-command.ts"],"names":[],"mappings":"AAuBA,OAAO,EAEL,eAAe,EA8Bf,KAAK,UAAU,EAOhB,MAAM,gBAAgB,CAAC;AAsfxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAMnE;AA2PD;2EAC2E;AAC3E,wBAAsB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA2CjE"}
|
package/dist/run-command.js
CHANGED
|
@@ -20,17 +20,17 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
20
20
|
import { existsSync, readFileSync } from "node:fs";
|
|
21
21
|
import { jetstream, jetstreamManager } from "@nats-io/jetstream";
|
|
22
22
|
import { Kvm } from "@nats-io/kv";
|
|
23
|
-
import { BASELINE_LIFECYCLE_ENDPOINT, EpEnvelopeError, LANG_PROBLEM_DETAIL_KIND, dialerFor, invokeCommand, newTakeoverId, newIdentity, mintCreds, readAcceptedRow, openRecordsBucket, admissionBucket, createRunAdmission, readRunAdmission,
|
|
23
|
+
import { BASELINE_LIFECYCLE_ENDPOINT, EpEnvelopeError, LANG_PROBLEM_DETAIL_KIND, dialerFor, invokeCommand, newTakeoverId, newIdentity, mintCreds, readAcceptedRow, openRecordsBucket, admissionBucket, createRunAdmission, readRunAdmission, revokeRunAdmission, chatSubject, DEV_OWNER, readRunProgram, readRunRecord, renderLifecycleBlocked, replayRunJournal, resolveService, runDriverCaller, RUN_LAUNCH_DEADLINE_MS, standaloneConnectOpts, unansweredRail, unansweredRequest, } from "@cotal-ai/core";
|
|
24
24
|
import {} from "@cotal-ai/lang";
|
|
25
25
|
import { agentLifecycleSecretFilePaths, connectOrExit, controlCaller, endpointAuth, resolveControlTarget, resolveMeshTarget } from "@cotal-ai/workspace";
|
|
26
26
|
import { startRun, driveRun } from "./run-driver.js";
|
|
27
27
|
import { migrateRun } from "./migrate.js";
|
|
28
|
-
import {
|
|
28
|
+
import { cotalLangRunHost as runHost, journalRows } from "./run-host.js";
|
|
29
29
|
import { createRunEffectHost } from "./run-effect-host.js";
|
|
30
30
|
import { createRunScopeAuthority } from "./run-scope-authority.js";
|
|
31
31
|
import { createRunRecordHost, runRecordView } from "./run-record-host.js";
|
|
32
|
-
import { locateOpenCheckpoint, answerOpenCheckpoint } from "./resolve-checkpoint.js";
|
|
33
|
-
const USAGE = 'usage: cotal run <start --file <program> [--timeout <dur>] | resume <runId> [--local --file <program>] | ps [--endpoint <ep>] | journal <runId> [--endpoint <ep>] | answer <runId> <stepKey> [--value <json>] [--artifact <ref>] [--endpoint <ep>] [--local --by <who>] | revoke <runId> --local --by <who> --reason <text> [--endpoint <ep>] | migrate <runId> --local --file <program> [--endpoint <ep>]> [--local [--admit-read <channels> --admit-publish <channels>]] [--space <s>] [--server <url>] [--creds <path>]';
|
|
32
|
+
import { locateOpenCheckpoint, answerOpenCheckpoint, locateAcceptedAnswer, amendAcceptedAnswer } from "./resolve-checkpoint.js";
|
|
33
|
+
const USAGE = 'usage: cotal run <start --file <program> [--timeout <dur>] | resume <runId> [--local --file <program>] | ps [--endpoint <ep>] [--json] | journal <runId> [--endpoint <ep>] [--json] | answer <runId> <stepKey> [--value <json>] [--artifact <ref>] [--endpoint <ep>] [--local --by <who>] | amend <runId> <stepKey> [--value <json>] [--artifact <ref>] [--endpoint <ep>] [--local --by <who>] | revoke <runId> --local --by <who> --reason <text> [--endpoint <ep>] | migrate <runId> --local --file <program> [--endpoint <ep>]> [--local [--admit-read <channels> --admit-publish <channels>]] [--space <s>] [--server <url>] [--creds <path>]';
|
|
34
34
|
/** One raw connection to the resolved mesh under the verb's OWN profile, with the planes over it.
|
|
35
35
|
* A drive rides the run's `run-driver` credential (SPEC 14.6), minted for the one run and attempt
|
|
36
36
|
* it drives; a read or an answer rides a one-shot `run-operator` credential for that call. An
|
|
@@ -61,13 +61,13 @@ async function openPlanes(values, role, mint) {
|
|
|
61
61
|
},
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
/** Mint from the same resolved target as the driver, preserving its broker and TLS intent. */
|
|
65
64
|
/** A local drive mints its mediator and its admitter from the folder's signer; a lone `--creds`
|
|
66
65
|
* file can supply neither, and is refused by name before any other local check. */
|
|
67
66
|
function refuseSingleCredential(conn) {
|
|
68
67
|
if (conn.creds !== undefined && conn.auth === undefined)
|
|
69
68
|
throw new Error("run --local needs the space signer to mint a separate mediator; a single --creds file cannot supply both roles. From the project with the recorded static-auth mesh and signer, omit --creds and run cotal run start --local --space <space> --file <program>. If you only have caller credentials, ask the mesh operator to host the run.");
|
|
70
69
|
}
|
|
70
|
+
/** Mint from the same resolved target as the driver, preserving its broker and TLS intent. */
|
|
71
71
|
async function openMediator(driver, pin) {
|
|
72
72
|
const conn = driver.connection;
|
|
73
73
|
refuseSingleCredential(conn);
|
|
@@ -313,108 +313,20 @@ async function drive(values, planes, a) {
|
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
async function ps(values, planes) {
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
/** Why each revoked row reads `revoked`, printed under the table: the marker carries `by` and the
|
|
322
|
-
* reason, and the table has no column for either. */
|
|
323
|
-
const revocations = new Map();
|
|
324
|
-
/** Rows whose revocation could not be read at all, each with the reason and its record's state,
|
|
325
|
-
* printed to stderr after the table. */
|
|
326
|
-
const unchecked = [];
|
|
327
|
-
for (const e of await walkKvEntries(planes.kv, "run.*.*.spec")) {
|
|
328
|
-
const parts = e.key.split(".");
|
|
329
|
-
if (parts.length !== 4 || parts[3] !== "spec")
|
|
330
|
-
continue;
|
|
331
|
-
const endpoint = parts[1];
|
|
332
|
-
const runId = parts[2];
|
|
333
|
-
const dedupe = `${endpoint}/${runId}`;
|
|
334
|
-
if (seen.has(dedupe))
|
|
335
|
-
continue;
|
|
336
|
-
seen.add(dedupe);
|
|
337
|
-
if (values.endpoint !== undefined && endpoint !== values.endpoint)
|
|
338
|
-
continue;
|
|
339
|
-
const record = await readRunRecord(planes.kv, endpoint, runId);
|
|
340
|
-
if (record === undefined)
|
|
341
|
-
continue;
|
|
342
|
-
const st = record.status?.value;
|
|
343
|
-
const lineage = record.spec.value.forkedFrom;
|
|
344
|
-
// THE ADMISSION STORE, not the status record alone. A revocation is a create-only marker under
|
|
345
|
-
// its own key and nothing rewrites the record (SPEC 14.8, docs/workflows.md), while the record
|
|
346
|
-
// is written only by a driver. A driver that dies mid-run therefore leaves `running` behind
|
|
347
|
-
// with nothing left to write anything else, and this table listed that run as live for as long
|
|
348
|
-
// as it existed. `resume` already reads the marker and refuses; the table was the one surface
|
|
349
|
-
// that never learned.
|
|
350
|
-
//
|
|
351
|
-
// DISPLAY ONLY. A revoke does not make a run `failed` or `released`: nobody drove it there, the
|
|
352
|
-
// journal owns the facts, and fabricating a terminal state here would put a fact in front of an
|
|
353
|
-
// operator that no host ever recorded.
|
|
354
|
-
let state = st?.state ?? "(no status)";
|
|
355
|
-
try {
|
|
356
|
-
const revoked = await readRunRevocation(planes.jsm, planes.space, endpoint, runId);
|
|
357
|
-
if (revoked !== undefined) {
|
|
358
|
-
revocations.set(dedupe, revoked);
|
|
359
|
-
state = "revoked";
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
catch (e) {
|
|
363
|
-
// Absence of EVIDENCE, and the STATE column itself says so. A marker this call could not read
|
|
364
|
-
// (a store it could not reach, a marker version or shape it does not know) says nothing about
|
|
365
|
-
// whether the run was revoked. The record's own word is what a revoked run printed before
|
|
366
|
-
// this table read the marker, so the column says `unchecked` and the record's word goes to
|
|
367
|
-
// stderr with the reason, where `awk '{print $3}'` and `grep running` over stdout never see it.
|
|
368
|
-
// The row itself still prints: one unreadable marker does not hide the rest of the listing.
|
|
369
|
-
state = "unchecked";
|
|
370
|
-
unchecked.push(`${dedupe}: revocation marker could not be read (${e.message}); its record reads ${st?.state ?? "(no status)"}`);
|
|
371
|
-
}
|
|
372
|
-
rows.push([
|
|
373
|
-
runId,
|
|
374
|
-
endpoint,
|
|
375
|
-
state,
|
|
376
|
-
st?.holder ?? "-",
|
|
377
|
-
st === undefined ? "-" : String(st.journalHigh),
|
|
378
|
-
lineage === undefined ? "-" : `${lineage.run}@${lineage.step}`,
|
|
379
|
-
]);
|
|
380
|
-
}
|
|
381
|
-
if (rows.length === 0) {
|
|
382
|
-
console.log(`no workflow runs recorded in space ${planes.space}`);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
const header = ["RUN", "ENDPOINT", "STATE", "HOLDER", "JOURNAL", "FORKED-FROM"];
|
|
386
|
-
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
387
|
-
const line = (r) => r.map((cell, i) => cell.padEnd(widths[i])).join(" ");
|
|
388
|
-
console.log(line(header));
|
|
389
|
-
for (const r of rows)
|
|
390
|
-
console.log(line(r));
|
|
391
|
-
// WHO revoked it and WHY, under the table rather than in it. The marker carries both and the
|
|
392
|
-
// columns carry neither, and a state of `revoked` with no attribution anywhere sends the
|
|
393
|
-
// operator to a second command to learn what they are looking at.
|
|
394
|
-
for (const [key, r] of revocations)
|
|
395
|
-
console.log(`${key}: revoked by ${r.by} (${r.reason}); the status record is left as its driver last wrote it`);
|
|
396
|
-
// A listing with an unchecked row is incomplete, and the exit status says so after every row
|
|
397
|
-
// has been printed, as `ls` and `find` do when one entry cannot be read: the rows on stdout, the
|
|
398
|
-
// failed reads on stderr, and a non-zero status a caller can test without parsing the column.
|
|
399
|
-
for (const u of unchecked)
|
|
400
|
-
console.error(u);
|
|
401
|
-
if (unchecked.length > 0)
|
|
402
|
-
process.exitCode = 1;
|
|
316
|
+
// The same rows the manager answers `run-ps` with, read under this call's operator credential:
|
|
317
|
+
// the record's state beside its revocation marker (SPEC 14.8), so `--local` and hosted agree
|
|
318
|
+
// row for row, `--json` included.
|
|
319
|
+
const rows = await runHost.list(planes, values.endpoint !== undefined ? { endpoint: values.endpoint } : {});
|
|
320
|
+
printRuns(planes.space, rows, values.json === true);
|
|
403
321
|
}
|
|
404
|
-
async function journal(planes, runId, takeoverId) {
|
|
322
|
+
async function journal(values, planes, endpoint, runId, takeoverId) {
|
|
405
323
|
if (runId === undefined) {
|
|
406
324
|
console.error(USAGE);
|
|
407
325
|
process.exit(1);
|
|
408
326
|
}
|
|
409
327
|
// The replay durable is named by the takeover id this call's credential was minted for.
|
|
410
328
|
const replay = await replayRunJournal(planes.js, planes.jsm, planes.space, runId, takeoverId);
|
|
411
|
-
|
|
412
|
-
console.log(`run ${runId}: no journal records (never started, or retired)`);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
printJournal(runId, replay.records.map(({ record }) => record.kind === "activation"
|
|
416
|
-
? { n: record.n, kind: "activation", holder: record.holder, epoch: record.epoch, replayedTo: record.replayedTo }
|
|
417
|
-
: journalStepRow(record.n, record.entry)));
|
|
329
|
+
printJournal(runId, await journalRows(planes.kv, endpoint, replay.records), values.json === true);
|
|
418
330
|
}
|
|
419
331
|
/** `answer --local`: the pause is found under the READ credential this call was opened on, then
|
|
420
332
|
* the answer rides a second, one-shot credential pinned to that pause's token. */
|
|
@@ -445,6 +357,33 @@ async function answer(values, reader, runId, stepKey, takeoverId) {
|
|
|
445
357
|
await writer.close();
|
|
446
358
|
}
|
|
447
359
|
}
|
|
360
|
+
/** `amend --local`: the accepted answer is found under the READ credential this call was opened on,
|
|
361
|
+
* then the amendment rides a second, one-shot credential pinned to that pause's token. Nothing is
|
|
362
|
+
* presented: the step stays settled and the run keeps the answer it acted on. */
|
|
363
|
+
async function amend(values, reader, runId, stepKey, takeoverId) {
|
|
364
|
+
if (runId === undefined || stepKey === undefined || values.by === undefined) {
|
|
365
|
+
console.error(USAGE);
|
|
366
|
+
console.error("run amend: <runId> <stepKey> and --by <who> are required");
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
const endpoint = values.endpoint ?? "manager";
|
|
370
|
+
const parsedValue = parseAnswerValue(values);
|
|
371
|
+
const accepted = await locateAcceptedAnswer({ kv: reader.kv, js: reader.js, jsm: reader.jsm, space: reader.space, endpoint }, { runId, stepKey, takeoverId });
|
|
372
|
+
const writer = await openPlanes(values, "run-operator", { runOperator: { endpoint, takeoverId: newTakeoverId(), answers: { token: accepted.token } } });
|
|
373
|
+
try {
|
|
374
|
+
const result = await amendAcceptedAnswer({ kv: writer.kv, js: writer.js, jsm: writer.jsm, space: writer.space, endpoint }, {
|
|
375
|
+
accepted,
|
|
376
|
+
by: values.by,
|
|
377
|
+
...(values.value !== undefined ? { value: parsedValue } : {}),
|
|
378
|
+
...(values.artifact !== undefined ? { artifact: values.artifact } : {}),
|
|
379
|
+
now: Date.now(),
|
|
380
|
+
});
|
|
381
|
+
console.log(JSON.stringify(result, null, 2));
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
await writer.close();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
448
387
|
/** Parse `--value` as JSON, with the one hint every first-time user needs. */
|
|
449
388
|
function parseAnswerValue(values) {
|
|
450
389
|
if (values.value === undefined)
|
|
@@ -648,7 +587,14 @@ function renderLangProblem(d) {
|
|
|
648
587
|
const at = where ? `${where.file ?? "<program>"}:${where.line ?? "?"}:${where.column ?? "?"}` : "<program>";
|
|
649
588
|
return ` ${String(d.code ?? "L????")} ${String(d.title ?? "")} (${at})\n ${String(d.cause ?? "")}\n fix: ${String(d.fix ?? "")}`;
|
|
650
589
|
}
|
|
651
|
-
|
|
590
|
+
/** `--json` prints each row unchanged as one JSON object per line, as `cotal ps --json` does for
|
|
591
|
+
* seats: stdout carries rows only, so an empty journal prints nothing there. */
|
|
592
|
+
function printJournal(runId, rows, json) {
|
|
593
|
+
if (json) {
|
|
594
|
+
for (const r of rows)
|
|
595
|
+
console.log(JSON.stringify(r));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
652
598
|
if (rows.length === 0) {
|
|
653
599
|
console.log(`run ${runId}: no journal records (never started, or retired)`);
|
|
654
600
|
return;
|
|
@@ -671,17 +617,45 @@ function printJournal(runId, rows) {
|
|
|
671
617
|
];
|
|
672
618
|
console.log(` answered ${facts.join(" ")}`);
|
|
673
619
|
}
|
|
620
|
+
for (const m of r.amendments ?? []) {
|
|
621
|
+
const facts = [
|
|
622
|
+
...(Object.hasOwn(m, "value") ? [`value=${JSON.stringify(m.value)}`] : []),
|
|
623
|
+
`by=${JSON.stringify(m.by)}`,
|
|
624
|
+
...(m.artifact !== undefined ? [`artifact=${JSON.stringify(m.artifact)}`] : []),
|
|
625
|
+
`at=${JSON.stringify(m.at)}`,
|
|
626
|
+
`supersedes=${JSON.stringify(m.supersedes)}`,
|
|
627
|
+
`answerId=${JSON.stringify(m.answerId)}`,
|
|
628
|
+
];
|
|
629
|
+
console.log(` amended ${facts.join(" ")}`);
|
|
630
|
+
}
|
|
674
631
|
}
|
|
675
632
|
}
|
|
676
|
-
function printRuns(space, rows) {
|
|
633
|
+
function printRuns(space, rows, json) {
|
|
634
|
+
// A marker the host could not read fails the listing after every row is printed, under `--json`
|
|
635
|
+
// as in the table: its reason and the record's word on stderr, and exit 1.
|
|
636
|
+
const unchecked = rows.filter((r) => r.revocationUnreadable !== undefined);
|
|
637
|
+
const reportUnchecked = () => {
|
|
638
|
+
for (const r of unchecked)
|
|
639
|
+
console.error(`${r.endpoint}/${r.runId}: revocation marker could not be read (${r.revocationUnreadable}); its record reads ${r.state ?? "(no status)"}`);
|
|
640
|
+
if (unchecked.length > 0)
|
|
641
|
+
process.exitCode = 1;
|
|
642
|
+
};
|
|
643
|
+
if (json) {
|
|
644
|
+
for (const r of rows)
|
|
645
|
+
console.log(JSON.stringify(r));
|
|
646
|
+
reportUnchecked();
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
677
649
|
if (rows.length === 0) {
|
|
678
650
|
console.log(`no workflow runs recorded in space ${space}`);
|
|
679
651
|
return;
|
|
680
652
|
}
|
|
653
|
+
// The same reading as `run ps --local`: a marker wins over the record's state, and a marker the
|
|
654
|
+
// host could not read prints `unchecked` with the record's word on stderr and exit 1.
|
|
681
655
|
const table = rows.map((r) => [
|
|
682
656
|
r.runId,
|
|
683
657
|
r.endpoint,
|
|
684
|
-
r.state ?? "(no status)",
|
|
658
|
+
r.revoked !== undefined ? "revoked" : r.revocationUnreadable !== undefined ? "unchecked" : r.state ?? "(no status)",
|
|
685
659
|
r.holder ?? "-",
|
|
686
660
|
r.journalHigh === undefined ? "-" : String(r.journalHigh),
|
|
687
661
|
r.forkedFrom === undefined ? "-" : `${r.forkedFrom.run}@${r.forkedFrom.step}`,
|
|
@@ -692,6 +666,10 @@ function printRuns(space, rows) {
|
|
|
692
666
|
console.log(line(header));
|
|
693
667
|
for (const r of table)
|
|
694
668
|
console.log(line(r));
|
|
669
|
+
for (const r of rows)
|
|
670
|
+
if (r.revoked !== undefined)
|
|
671
|
+
console.log(`${r.endpoint}/${r.runId}: revoked by ${r.revoked.by} (${r.revoked.reason}); the status record is left as its driver last wrote it`);
|
|
672
|
+
reportUnchecked();
|
|
695
673
|
}
|
|
696
674
|
async function hosted(values, verb, a, b) {
|
|
697
675
|
const endpoint = values.endpoint !== undefined ? { endpoint: values.endpoint } : {};
|
|
@@ -701,8 +679,8 @@ async function hosted(values, verb, a, b) {
|
|
|
701
679
|
console.error(`run ${verb}: --endpoint is not taken on the hosted path; the manager records the run under its own endpoint. \`--local\` drives under a chosen endpoint from this process`);
|
|
702
680
|
process.exit(1);
|
|
703
681
|
}
|
|
704
|
-
if (verb === "answer" && values.by !== undefined) {
|
|
705
|
-
console.error(
|
|
682
|
+
if ((verb === "answer" || verb === "amend") && values.by !== undefined) {
|
|
683
|
+
console.error(`run ${verb}: --by is not taken on the hosted path; the manager records you as the answerer from your credential. \`--local --by <who>\` names the answerer when driving from this process`);
|
|
706
684
|
process.exit(1);
|
|
707
685
|
}
|
|
708
686
|
// The manager serves no `run-migrate` command, so there is nothing to route to: the check reads
|
|
@@ -738,7 +716,7 @@ async function hosted(values, verb, a, b) {
|
|
|
738
716
|
if (verb === "ps") {
|
|
739
717
|
const rows = await askHost(values, "run-ps", Object.keys(endpoint).length ? endpoint : undefined);
|
|
740
718
|
const t = values.space ?? "(the resolved mesh)";
|
|
741
|
-
printRuns(t, rows);
|
|
719
|
+
printRuns(t, rows, values.json === true);
|
|
742
720
|
return;
|
|
743
721
|
}
|
|
744
722
|
if (verb === "journal") {
|
|
@@ -748,14 +726,16 @@ async function hosted(values, verb, a, b) {
|
|
|
748
726
|
}
|
|
749
727
|
const view = await askHost(values, "run-status", { runId: a, ...endpoint });
|
|
750
728
|
const st = view.status;
|
|
751
|
-
|
|
752
|
-
|
|
729
|
+
// Under --json the run's header line is structure, not a row: it goes to stderr.
|
|
730
|
+
(values.json === true ? console.error : console.log)(`run ${view.runId} on ${view.endpoint}: ${st === undefined ? "(no status)" : `${st.state}, holder ${st.holder}, epoch ${st.epoch}`}`);
|
|
731
|
+
printJournal(view.runId, view.journal, values.json === true);
|
|
753
732
|
return;
|
|
754
733
|
}
|
|
755
|
-
// answer: the manager records the caller as the answerer (SPEC 14.5), so no `--by`
|
|
734
|
+
// answer and amend: the manager records the caller as the answerer (SPEC 14.5), so no `--by`
|
|
735
|
+
// rides. An amend is the same served command with `amend` set.
|
|
756
736
|
if (a === undefined || b === undefined) {
|
|
757
737
|
console.error(USAGE);
|
|
758
|
-
console.error(
|
|
738
|
+
console.error(`run ${verb}: <runId> <stepKey> are required`);
|
|
759
739
|
process.exit(1);
|
|
760
740
|
}
|
|
761
741
|
const value = parseAnswerValue(values);
|
|
@@ -765,18 +745,23 @@ async function hosted(values, verb, a, b) {
|
|
|
765
745
|
...endpoint,
|
|
766
746
|
...(values.value !== undefined ? { value } : {}),
|
|
767
747
|
...(values.artifact !== undefined ? { artifact: values.artifact } : {}),
|
|
748
|
+
...(verb === "amend" ? { amend: true } : {}),
|
|
768
749
|
});
|
|
769
750
|
console.log(JSON.stringify(result, null, 2));
|
|
770
751
|
}
|
|
771
|
-
/** `cotal run <start|resume|ps|journal|answer>` — dispatch. The manager hosts by default;
|
|
752
|
+
/** `cotal run <start|resume|ps|journal|answer|amend>` — dispatch. The manager hosts by default;
|
|
772
753
|
* `--local` drives in this process over one connection per invocation. */
|
|
773
754
|
export async function runWorkflow(args) {
|
|
774
755
|
const values = args.values;
|
|
775
756
|
const [verb, a, b] = args.positionals;
|
|
776
|
-
if (verb === undefined || !["start", "resume", "ps", "journal", "answer", "revoke", "migrate"].includes(verb)) {
|
|
757
|
+
if (verb === undefined || !["start", "resume", "ps", "journal", "answer", "amend", "revoke", "migrate"].includes(verb)) {
|
|
777
758
|
console.error(USAGE);
|
|
778
759
|
process.exit(1);
|
|
779
760
|
}
|
|
761
|
+
if (values.json === true && verb !== "ps" && verb !== "journal") {
|
|
762
|
+
console.error(`run ${verb}: --json is taken by \`ps\` and \`journal\` only`);
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
780
765
|
if (verb === "revoke") {
|
|
781
766
|
// Revocation is an operator act on the admission store, never a served command: the marker is
|
|
782
767
|
// written from the folder's trust material, so it is `--local` only.
|
|
@@ -794,9 +779,9 @@ export async function runWorkflow(args) {
|
|
|
794
779
|
return start(values);
|
|
795
780
|
if (verb === "resume")
|
|
796
781
|
return resume(values, a);
|
|
797
|
-
// The reads ride a one-shot operator READ credential for that call; a journal
|
|
798
|
-
// the run its replay durable is pinned to. An answer finds its pause on
|
|
799
|
-
// writes on a second one, minted for that pause alone (see `answer`).
|
|
782
|
+
// The reads ride a one-shot operator READ credential for that call; a journal, an answer or an
|
|
783
|
+
// amend names the run its replay durable is pinned to. An answer or an amend finds its pause on
|
|
784
|
+
// this credential and then writes on a second one, minted for that pause alone (see `answer`).
|
|
800
785
|
const endpoint = values.endpoint ?? "manager";
|
|
801
786
|
const takeoverId = newTakeoverId();
|
|
802
787
|
const planes = await openPlanes(values, "run-operator", {
|
|
@@ -806,9 +791,11 @@ export async function runWorkflow(args) {
|
|
|
806
791
|
if (verb === "ps")
|
|
807
792
|
await ps(values, planes);
|
|
808
793
|
else if (verb === "journal")
|
|
809
|
-
await journal(planes, a, takeoverId);
|
|
794
|
+
await journal(values, planes, endpoint, a, takeoverId);
|
|
810
795
|
else if (verb === "migrate")
|
|
811
796
|
await migrate(values, planes, a, takeoverId);
|
|
797
|
+
else if (verb === "amend")
|
|
798
|
+
await amend(values, planes, a, b, takeoverId);
|
|
812
799
|
else
|
|
813
800
|
await answer(values, planes, a, b, takeoverId);
|
|
814
801
|
}
|