@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.
@@ -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 readCheckpointSpec(deps.kv, { endpoint: deps.endpoint, token });
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":"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"}
1
+ {"version":3,"file":"resolve-checkpoint.js","sourceRoot":"","sources":["../src/resolve-checkpoint.ts"],"names":[],"mappings":"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"}
@@ -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,EAiCf,KAAK,UAAU,EAOhB,MAAM,gBAAgB,CAAC;AAmiBxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAMnE;AAoND;2EAC2E;AAC3E,wBAAsB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCjE"}
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"}
@@ -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, readRunRevocation, revokeRunAdmission, chatSubject, DEV_OWNER, readRunProgram, readRunRecord, renderLifecycleBlocked, replayRunJournal, resolveService, runDriverCaller, RUN_LAUNCH_DEADLINE_MS, standaloneConnectOpts, unansweredRail, unansweredRequest, walkKvEntries, } from "@cotal-ai/core";
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 { journalStepRow } from "./run-host.js";
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
- // Run record keys are `run.<endpoint>.<runId>.<spec|status>` in the records bucket; the scan is
317
- // over the spec half, which every run has exactly once. A consumer-free walk: the records bucket
318
- // is an authority stream whose consumer surface is an exact audited list (SPEC 13.9).
319
- const seen = new Set();
320
- const rows = [];
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
- if (replay.records.length === 0) {
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
- function printJournal(runId, rows) {
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("run answer: --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");
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
- console.log(`run ${view.runId} on ${view.endpoint}: ${st === undefined ? "(no status)" : `${st.state}, holder ${st.holder}, epoch ${st.epoch}`}`);
752
- printJournal(view.runId, view.journal);
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` rides.
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("run answer: <runId> <stepKey> are required");
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 or an answer names
798
- // the run its replay durable is pinned to. An answer finds its pause on this credential and then
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
  }