@astrosheep/keiyaku 4.5.24 → 4.5.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/build/src/akuma/akuma-handle.d.ts +10 -0
  2. package/build/src/akuma/akuma-handle.js +18 -18
  3. package/build/src/akuma/akuma-instance.d.ts +5 -10
  4. package/build/src/akuma/akuma-instance.js +55 -40
  5. package/build/src/akuma/akuma-observe.d.ts +40 -7
  6. package/build/src/akuma/akuma-observe.js +51 -16
  7. package/build/src/akuma/akuma-product.d.ts +1 -2
  8. package/build/src/akuma/akuma-product.js +8 -5
  9. package/build/src/akuma/akuma.d.ts +2 -0
  10. package/build/src/akuma/body.d.ts +1 -1
  11. package/build/src/akuma/body.js +48 -30
  12. package/build/src/akuma/call-request.d.ts +0 -1
  13. package/build/src/akuma/call-request.js +1 -2
  14. package/build/src/akuma/fleet-execution.d.ts +3 -1
  15. package/build/src/akuma/fleet-execution.js +30 -71
  16. package/build/src/akuma/fleet-observation.d.ts +13 -2
  17. package/build/src/akuma/fleet-observation.js +2 -1
  18. package/build/src/akuma/fleet-request.js +3 -2
  19. package/build/src/akuma/heart/timeline.d.ts +1 -1
  20. package/build/src/akuma/heart/timeline.js +5 -5
  21. package/build/src/akuma/projection-read.js +37 -7
  22. package/build/src/akuma/projection.d.ts +1 -0
  23. package/build/src/akuma/projection.js +24 -9
  24. package/build/src/akuma/providers/pi/index.d.ts +2 -1
  25. package/build/src/akuma/providers/pi/index.js +2 -1
  26. package/build/src/akuma/publication.js +7 -4
  27. package/build/src/akuma-body.js +1 -0
  28. package/build/src/cli/commands/akuma-invoke.d.ts +1 -0
  29. package/build/src/cli/commands/akuma-invoke.js +16 -3
  30. package/build/src/cli/invoke.js +1 -0
  31. package/build/src/cli/render/akuma-activity.d.ts +13 -5
  32. package/build/src/cli/render/akuma-activity.js +118 -112
  33. package/build/src/cli/render/akuma-tool.js +5 -3
  34. package/build/src/library/address.d.ts +1 -1
  35. package/build/src/library/address.js +10 -8
  36. package/build/src/library/akuma-creation.d.ts +3 -0
  37. package/build/src/library/akuma-creation.js +111 -46
  38. package/build/src/library/composition.d.ts +3 -1
  39. package/build/src/library/fleet.d.ts +7 -1
  40. package/build/src/library/fleet.js +35 -9
  41. package/build/src/library/keiyaku.d.ts +3 -1
  42. package/build/src/runtime/proc/windows-launch.exe +0 -0
  43. package/package.json +3 -3
@@ -185,8 +185,11 @@ async function settleLaunch(allocated, owned, failure) {
185
185
  throw new Error("Akuma birth settled without a seal");
186
186
  return "sealed";
187
187
  }
188
- async function awaitAsleepBirth(paths) {
189
- const leash = await acquireLeash(paths, { deadline: performance.now() + BIRTH_TIMEOUT_MS });
188
+ async function awaitAsleepBirth(paths, signal) {
189
+ const leash = await acquireLeash(paths, {
190
+ deadline: performance.now() + BIRTH_TIMEOUT_MS,
191
+ ...(signal === undefined ? {} : { signal }),
192
+ });
190
193
  if (leash === null)
191
194
  throw new Error("Forked Akuma did not finish its birth body");
192
195
  try {
@@ -241,7 +244,7 @@ export async function launchAkuma(input) {
241
244
  if (soul.id !== allocated.id)
242
245
  throw new Error("Akuma birth returned a different identity");
243
246
  if (input.awaitAsleep === true)
244
- await awaitAsleepBirth(allocated.paths);
247
+ await awaitAsleepBirth(allocated.paths, input.signal);
245
248
  owned?.release();
246
249
  return allocated;
247
250
  }
@@ -256,7 +259,7 @@ export async function launchAkuma(input) {
256
259
  }
257
260
  if (outcome === "born" && input.signal?.aborted && error === input.signal.reason) {
258
261
  if (input.awaitAsleep === true)
259
- await awaitAsleepBirth(allocated.paths);
262
+ await awaitAsleepBirth(allocated.paths, input.signal);
260
263
  return allocated;
261
264
  }
262
265
  throw error;
@@ -93,6 +93,7 @@ function fleetRequestPort(world) {
93
93
  schema: Schema.json(JSON.parse(input.schemaJson), (value) => value),
94
94
  ...(input.interrupt === undefined ? {} : { interrupt: input.interrupt }),
95
95
  ...(input.initiator === undefined ? {} : { initiator: input.initiator }),
96
+ signal: input.signal,
96
97
  }),
97
98
  kill: async (input) => {
98
99
  const result = await executeKillAkuma({ path: world, ids: input.targets, signal: input.signal });
@@ -99,6 +99,7 @@ type InvokeInput = Readonly<{
99
99
  environment: NodeJS.ProcessEnv;
100
100
  readStdin(): Promise<string>;
101
101
  execution?: ExecutionContext;
102
+ signal?: AbortSignal;
102
103
  }>;
103
104
  export declare function invokeAkuma(command: InvokedAkumaCommand, input: InvokeInput): Promise<AkumaInvocationResult>;
104
105
  export declare function invokeAkumaStatus(path: WorldRoot, akuma: string, alias?: string, repo?: Repo): Promise<AkumaInvocationResult>;
@@ -13,6 +13,9 @@ import { executionChannel } from "../../akuma/requests.js";
13
13
  import { requestForwardedFleetTellAnswer } from "../../akuma/fleet-request.js";
14
14
  /** The window a call observes for when the caller gives no `--wait` duration. */
15
15
  const CALL_TIMEOUT_MS = 300_000;
16
+ function callSignalOption(signal) {
17
+ return signal === undefined ? {} : { signal };
18
+ }
16
19
  function integrationFailure(error) {
17
20
  return {
18
21
  kind: error instanceof AuthorityCorruptionError ? "authority-corruption" : "infrastructure",
@@ -39,20 +42,23 @@ function writeProgress(body) {
39
42
  async function observeCallUntilComplete(command, input, born) {
40
43
  const stream = callObservationStream(resultContext(), callObservationHead(born));
41
44
  try {
42
- const status = await observeAkumaStatus(input.path, born.akuma, {
45
+ const observed = await observeAkumaStatus(input.path, born.akuma, {
43
46
  timeoutMs: command.timeoutMs ?? CALL_TIMEOUT_MS,
47
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
44
48
  observe: (observed) => {
45
49
  const lines = stream.observe(observed);
46
50
  if (lines.length > 0)
47
51
  writeProgress(lines.join("\n"));
48
52
  },
49
53
  });
50
- const conclusion = stream.conclude({ kind: "observed", status });
54
+ const conclusion = stream.conclude({ kind: "observed", reason: observed.reason, status: observed.status });
51
55
  if (conclusion.length > 0)
52
56
  writeProgress(conclusion);
53
- return { kind: "observed", status };
57
+ return { kind: "observed", reason: observed.reason, status: observed.status };
54
58
  }
55
59
  catch (error) {
60
+ if (input.signal?.aborted)
61
+ throw error;
56
62
  const failure = integrationFailure(error);
57
63
  const conclusion = stream.conclude({ kind: "failed", failure });
58
64
  if (conclusion.length > 0)
@@ -111,6 +117,7 @@ async function invokeWait(command, input) {
111
117
  ...(input.repo === undefined ? {} : { repo: input.repo }),
112
118
  ...(command.completion === undefined ? {} : { completion: command.completion }),
113
119
  ...(command.timeoutMs === undefined ? {} : { timeoutMs: command.timeoutMs }),
120
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
114
121
  }, input.execution ?? localExecutionContext(), observer);
115
122
  const frozen = {
116
123
  startedAt,
@@ -177,11 +184,13 @@ async function invokeTell(command, input) {
177
184
  schema,
178
185
  interrupt: command.interrupt,
179
186
  ...initiator,
187
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
180
188
  })
181
189
  : await Akuma.select(addressed.path, addressed.id).tell(body, {
182
190
  schema,
183
191
  ...initiator,
184
192
  ...(command.interrupt ? { interrupt: true } : {}),
193
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
185
194
  });
186
195
  const alias = inputAlias(command.akuma);
187
196
  return {
@@ -201,6 +210,7 @@ async function invokeTell(command, input) {
201
210
  akuma: command.akuma,
202
211
  body,
203
212
  ...(input.repo === undefined ? {} : { repo: input.repo }),
213
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
204
214
  });
205
215
  const alias = inputAlias(command.akuma);
206
216
  return {
@@ -218,6 +228,7 @@ async function invokeTell(command, input) {
218
228
  akuma: command.akuma,
219
229
  body,
220
230
  ...(input.repo === undefined ? {} : { repo: input.repo }),
231
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
221
232
  }, input.execution ?? localExecutionContext());
222
233
  const alias = inputAlias(command.akuma);
223
234
  return { kind: "akuma", action: "tell", mode: "ordinary", result, body, ...(alias === undefined ? {} : { alias }) };
@@ -269,6 +280,7 @@ async function invokeKill(command, input) {
269
280
  path: input.path,
270
281
  akuma: command.akuma,
271
282
  ...(input.repo === undefined ? {} : { repo: input.repo }),
283
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
272
284
  }, input.execution ?? localExecutionContext()),
273
285
  ...(alias === undefined ? {} : { alias }),
274
286
  };
@@ -291,6 +303,7 @@ export async function invokeAkuma(command, input) {
291
303
  ...(command.alias === undefined ? {} : { alias: command.alias }),
292
304
  ...(command.allowed === undefined ? {} : { allowed: command.allowed }),
293
305
  ...(schema === undefined ? {} : { schema }),
306
+ ...callSignalOption(input.signal),
294
307
  };
295
308
  const observing = schema === undefined && command.mode === "wait" && command.output === "text";
296
309
  const born = await caller.call(observing
@@ -140,6 +140,7 @@ async function invokeAkumaFromEdge(parsed, input) {
140
140
  environment: edge.environment,
141
141
  readStdin: edge.readStdin,
142
142
  execution: input.execution,
143
+ ...(edge.signal === undefined ? {} : { signal: edge.signal }),
143
144
  });
144
145
  }
145
146
  catch (error) {
@@ -1,4 +1,4 @@
1
- import type { AkumaStatus, KillEvidence } from "../../akuma/akuma.js";
1
+ import type { ActivityRow, AkumaStatus, KillEvidence } from "../../akuma/akuma.js";
2
2
  import type { CallObservation } from "../../library/akuma-creation.js";
3
3
  import type { AkumaObservation, AkumaObservationStage, CreatedTaskObservation, DispatchAssociation } from "../../index.js";
4
4
  import type { AkumaInvocationResult } from "../commands/akuma-invoke.js";
@@ -13,6 +13,10 @@ export declare const DEFAULT_CONTEXT: TextRenderContext;
13
13
  export declare function frameRule(headLines: readonly string[]): string;
14
14
  type FleetTimeline = AkumaObservation["status"]["timeline"];
15
15
  type RenderedSnapshot = FleetTimeline;
16
+ type RenderedActivity = Readonly<{
17
+ snapshot: RenderedSnapshot;
18
+ rows: readonly ActivityRow[];
19
+ }>;
16
20
  export declare function associatedIdentity(id: string, alias?: string, _contract?: DispatchAssociation): string;
17
21
  export declare function snapshotHeading(id: string, alias: string | undefined, contract: DispatchAssociation | undefined): readonly string[];
18
22
  /**
@@ -34,9 +38,9 @@ export declare function snapshotActivityLines(snapshot: RenderedSnapshot, contex
34
38
  latest?: boolean;
35
39
  }>): readonly string[];
36
40
  /** One command's append-only activity view, with a baseline and a final tail flush. */
37
- export type ActivityStream = ((snapshot: RenderedSnapshot) => readonly string[]) & Readonly<{
38
- /** Seed the sequence cursor without spending the command's live evidence budget. */
39
- seed: (snapshot: RenderedSnapshot) => readonly string[];
41
+ export type ActivityStream = ((activity: RenderedActivity) => readonly string[]) & Readonly<{
42
+ /** Establish a wait baseline without spending the command's live tool budget. */
43
+ seed: (activity: RenderedActivity) => readonly string[];
40
44
  /** Emit the deferred tail exactly once before the command's conclusion. */
41
45
  flush: () => readonly string[];
42
46
  }>;
@@ -50,6 +54,7 @@ export type ActivityStream = ((snapshot: RenderedSnapshot) => readonly string[])
50
54
  export declare function activityStream(context: TextRenderContext, layout?: RowLayout): ActivityStream;
51
55
  /** What a wait conclusion renders over: its observed and unobserved members. */
52
56
  export type WaitConclusionResult = Readonly<{
57
+ reason: "completed" | "deadline";
53
58
  observations: readonly AkumaObservation[];
54
59
  unobserved: readonly Readonly<{
55
60
  id: string;
@@ -93,7 +98,10 @@ export type ObservedCallHead = Readonly<{
93
98
  facts: readonly string[];
94
99
  }>;
95
100
  export type CallObservationStream = Readonly<{
96
- observe: (status: AkumaStatus) => readonly string[];
101
+ observe: (observation: Readonly<{
102
+ status: AkumaStatus;
103
+ rows: readonly ActivityRow[];
104
+ }>) => readonly string[];
97
105
  conclude: (observation: CallObservation) => string;
98
106
  opened: () => boolean;
99
107
  }>;
@@ -1,4 +1,5 @@
1
1
  import { parseAkumaStatus } from "../../akuma/akuma.js";
2
+ import { defaultWaitComplete } from "../../akuma/akuma-observe.js";
2
3
  import { toolRepr } from "./akuma-tool.js";
3
4
  import { displayColumns, renderBoundedTextBlock, safeText, takeDisplayColumns, truncateMiddleDisplayText, } from "./terminal.js";
4
5
  export const DEFAULT_CONTEXT = { columns: 80, color: false };
@@ -68,7 +69,7 @@ function label(row) {
68
69
  if (row.kind === "tell")
69
70
  return row.state === "told" ? "told" : "tell";
70
71
  if (row.kind === "outcome")
71
- return row.outcome.kind === "answered" ? "say" : "error";
72
+ return row.outcome.kind === "answered" ? "answer" : "error";
72
73
  if (row.kind === "turn")
73
74
  return "call";
74
75
  if (row.kind !== "tool")
@@ -91,18 +92,6 @@ function mark(row) {
91
92
  }
92
93
  return "│";
93
94
  }
94
- /** Strip Markdown decoration from preview prose without rewriting the words. */
95
- function undecorated(text) {
96
- return (text
97
- .replace(/^ {0,3}#{1,6}[ \t]+/gmu, "")
98
- .replace(/^ {0,3}(?:[-*+]|\d+\.)[ \t]+/gmu, "")
99
- .replace(/\*\*([^*\n]+?)\*\*/gu, "$1")
100
- // A single pair of asterisks is emphasis only when it does not sit inside a word, path, or glob.
101
- .replace(/(?<![\w*/\\])\*([^\s*/\\](?:[^*/\\\n]*[^\s*/\\])?)\*(?![\w*/\\])/gu, "$1")
102
- .replace(/`([^`\n]+?)`/gu, "$1")
103
- .replace(/\s+/gu, " ")
104
- .trim());
105
- }
106
95
  function rowText(row) {
107
96
  if (row.kind === "said" ||
108
97
  row.kind === "thought" ||
@@ -110,13 +99,13 @@ function rowText(row) {
110
99
  row.kind === "call" ||
111
100
  row.kind === "tell") {
112
101
  return {
113
- text: undecorated(row.text),
102
+ text: row.text,
114
103
  lines: row.kind === "said" || row.kind === "thought" ? 2 : row.kind === "tell" || row.kind === "call" ? 1 : 2,
115
104
  };
116
105
  }
117
106
  if (row.kind === "outcome")
118
107
  return row.outcome.kind === "answered"
119
- ? { text: undecorated(row.outcome.answer), lines: 3 }
108
+ ? { text: row.outcome.answer, lines: 3 }
120
109
  : { text: row.outcome.diagnostic, lines: 2 };
121
110
  if (row.kind === "turn")
122
111
  return { text: "", lines: 1 };
@@ -235,37 +224,21 @@ function orderedSnapshotEntries(snapshot) {
235
224
  entries.push({ kind: "row", row: snapshot.outcome });
236
225
  return entries;
237
226
  }
238
- /**
239
- * The current open turn starts from either its initial commission or a Tell
240
- * delivered when the Body launched it. A live Tell belongs to an already
241
- * pursuing turn and is not its boundary.
242
- */
243
- function currentTurnBoundary(snapshot) {
244
- if (snapshot.kind !== "open")
227
+ function currentTurnBoundary(activity) {
228
+ const snapshot = activity.snapshot;
229
+ if (snapshot.kind !== "open" || snapshot.openingSequence === undefined)
245
230
  return undefined;
246
- const rows = snapshot.entries.flatMap((entry) => (entry.kind === "row" ? [entry.row] : []));
247
- const wake = rows.findLast((row) => row.kind === "tell" &&
248
- row.state === "told" &&
249
- row.deliveries.some((delivery) => delivery.route === "launch" && delivery.turnSequence === snapshot.turn.turnSequence));
250
- if (wake !== undefined)
251
- return { row: wake, turnSequence: snapshot.turn.turnSequence };
252
- const call = rows.find((row) => row.kind === "call" && row.turnSequence === snapshot.turn.turnSequence);
253
- return call === undefined ? undefined : { row: call, turnSequence: snapshot.turn.turnSequence };
254
- }
255
- /** Move the visible current-turn boundary ahead of activity without duplicating it. */
256
- function boundaryFirstSnapshotEntries(snapshot) {
257
- const entries = orderedSnapshotEntries(snapshot);
258
- const boundary = currentTurnBoundary(snapshot);
259
- if (boundary === undefined)
260
- return entries;
261
- return [
262
- { kind: "row", row: boundary.row },
263
- ...entries.filter((entry) => entry.kind !== "row" || entry.row !== boundary.row),
264
- ];
231
+ const row = activity.rows.find((candidate) => candidate.sequence === snapshot.openingSequence);
232
+ if (row === undefined)
233
+ return undefined;
234
+ // The projector selects a typed opening: its call or delivered launch Tell.
235
+ if (row.kind !== "call" && !(row.kind === "tell" && row.state === "told"))
236
+ return undefined;
237
+ return { row, turnSequence: snapshot.turn.turnSequence };
265
238
  }
266
239
  /** Open status hides internal thought narration without selecting a second activity window. */
267
240
  function visibleOpenSnapshotEntries(snapshot) {
268
- return boundaryFirstSnapshotEntries(snapshot).filter((entry) => entry.kind !== "row" || entry.row.kind !== "thought");
241
+ return snapshot.entries.filter((entry) => entry.kind !== "row" || entry.row.kind !== "thought");
269
242
  }
270
243
  /** Adjacent omitted spans are one continuous unknown portion of the retained timeline. */
271
244
  function coalesceAdjacentGaps(entries) {
@@ -294,25 +267,45 @@ export function snapshotActivityLines(snapshot, context, selection = {}) {
294
267
  const latest = entries.filter((entry) => entry.kind === "row").at(-1);
295
268
  return latest === undefined ? [] : groupedEntries([latest], context);
296
269
  }
297
- /**
298
- * One status' timeline without its conclusion: the newest open entry is still
299
- * moving, and an idle outcome is the command's final result rather than a
300
- * settled row.
301
- */
302
- function settledTimeline(snapshot) {
303
- switch (snapshot.kind) {
304
- case "unborn":
305
- return snapshot;
306
- case "open":
307
- return { ...snapshot, entries: snapshot.entries.slice(0, -1) };
308
- case "idle": {
309
- const { outcome: _outcome, ...rest } = snapshot;
310
- return { ...rest, entries: snapshot.entries };
270
+ function isSettledStreamRow(row) {
271
+ if (row.kind === "turn" || row.kind === "outcome")
272
+ return false;
273
+ if (row.kind === "tell")
274
+ return row.state === "told";
275
+ return row.kind !== "tool" || (row.state !== "active" && row.state !== "unsettled");
276
+ }
277
+ /** A pending Tell or active tool retains its sequence when it later becomes eligible. */
278
+ function isMutableStreamRow(row) {
279
+ return (row.kind === "tell" && row.state === "pending") || (row.kind === "tool" && row.state === "active");
280
+ }
281
+ function settledRows(activity) {
282
+ return activity.rows.filter(isSettledStreamRow);
283
+ }
284
+ function isProtectedStreamRow(row) {
285
+ return row.kind === "said" || (row.kind === "tool" && row.call.kind === "fileChange");
286
+ }
287
+ function isBoundedStreamTool(row) {
288
+ return row.kind === "tool" && !isProtectedStreamRow(row);
289
+ }
290
+ function rememberMutableRows(state, activity) {
291
+ for (const row of activity.rows)
292
+ if (isMutableStreamRow(row))
293
+ state.mutableSequences.add(row.sequence);
294
+ }
295
+ /** A seeded wait counts its skipped settled companion evidence after its typed opening. */
296
+ function baselineOmissionCount(activity, boundary) {
297
+ let afterBoundary = false;
298
+ let count = 0;
299
+ for (const row of activity.rows) {
300
+ if (!afterBoundary) {
301
+ if (row.sequence === boundary.row.sequence)
302
+ afterBoundary = true;
303
+ continue;
311
304
  }
305
+ if (isSettledStreamRow(row))
306
+ count += 1;
312
307
  }
313
- }
314
- function settledRows(snapshot) {
315
- return orderedSnapshotEntries(settledTimeline(snapshot)).flatMap((entry) => entry.kind === "row" ? [entry.row] : []);
308
+ return count;
316
309
  }
317
310
  function renderStreamRow(state, row, lines, context, layout) {
318
311
  const at = clock(row.at);
@@ -333,7 +326,7 @@ function coalesceDeferredGaps(state) {
333
326
  }, []);
334
327
  }
335
328
  function omitOldestDeferredTool(state) {
336
- const index = state.deferred.findIndex((entry) => entry.kind === "row" && entry.row.kind === "tool");
329
+ const index = state.deferred.findIndex((entry) => entry.kind === "row" && isBoundedStreamTool(entry.row));
337
330
  if (index === -1)
338
331
  throw new Error("activity tail lost its pending tool");
339
332
  state.deferred[index] = { kind: "gap", count: 1 };
@@ -343,7 +336,7 @@ function omitOldestDeferredTool(state) {
343
336
  function flushSafeActivityPrefix(state, lines, context, layout) {
344
337
  for (;;) {
345
338
  const first = state.deferred[0];
346
- if (first === undefined || (first.kind === "row" && first.row.kind === "tool"))
339
+ if (first === undefined || (first.kind === "row" && isBoundedStreamTool(first.row)))
347
340
  return;
348
341
  if (first.kind === "row") {
349
342
  state.deferred.shift();
@@ -351,44 +344,55 @@ function flushSafeActivityPrefix(state, lines, context, layout) {
351
344
  continue;
352
345
  }
353
346
  const next = state.deferred[1];
354
- if (next === undefined || (next.kind === "row" && next.row.kind === "tool"))
347
+ if (next === undefined || (next.kind === "row" && isBoundedStreamTool(next.row)))
355
348
  return;
356
349
  state.deferred.shift();
357
350
  lines.push(layout.marker(first.count));
358
351
  }
359
352
  }
360
- function renderCurrentTurnBoundary(state, snapshot, lines, context, layout) {
361
- const boundary = currentTurnBoundary(snapshot);
353
+ function renderCurrentTurnBoundary(state, activity, lines, context, layout) {
354
+ const boundary = currentTurnBoundary(activity);
362
355
  if (boundary !== undefined && !state.renderedBoundaries.has(boundary.turnSequence)) {
363
- renderStreamRow(state, boundary.row, lines, context, layout);
364
356
  state.renderedBoundaries.add(boundary.turnSequence);
357
+ if (boundary.row.kind !== "thought")
358
+ renderStreamRow(state, boundary.row, lines, context, layout);
365
359
  }
366
360
  return boundary;
367
361
  }
368
- function observeActivitySnapshot(state, snapshot, context, layout) {
362
+ function observeActivitySnapshot(state, activity, context, layout) {
369
363
  const lines = [];
370
- const boundary = renderCurrentTurnBoundary(state, snapshot, lines, context, layout);
371
- const rows = settledRows(snapshot)
372
- .filter((row) => row !== boundary?.row)
373
- .filter((row) => state.newestSequence === undefined || row.sequence > state.newestSequence)
374
- // Thoughts remain retained activity, but are ineligible for default live progress.
375
- .filter((row) => row.kind !== "thought");
376
- if (rows.length === 0)
364
+ const boundary = currentTurnBoundary(activity);
365
+ rememberMutableRows(state, activity);
366
+ // A row newly selected as this Turn's typed opening must still respect the
367
+ // settled cursor, even if its earlier pending form was remembered as mutable.
368
+ if (boundary !== undefined)
369
+ state.mutableSequences.delete(boundary.row.sequence);
370
+ const observedRows = settledRows(activity)
371
+ .filter((row) => boundary === undefined ||
372
+ !state.renderedBoundaries.has(boundary.turnSequence) ||
373
+ row.sequence !== boundary.row.sequence)
374
+ .filter((row) => state.mutableSequences.has(row.sequence) ||
375
+ state.newestSettledSequence === undefined ||
376
+ row.sequence > state.newestSettledSequence);
377
+ if (observedRows.length === 0)
377
378
  return lines;
378
- state.newestSequence = rows.reduce((newest, row) => Math.max(newest, row.sequence), state.newestSequence ?? rows[0].sequence);
379
+ for (const row of observedRows)
380
+ state.mutableSequences.delete(row.sequence);
381
+ state.newestSettledSequence = observedRows.reduce((newest, row) => Math.max(newest, row.sequence), state.newestSettledSequence ?? observedRows[0].sequence);
382
+ const rows = observedRows.filter((row) => row.kind !== "thought");
379
383
  for (const row of rows) {
380
- if (row.kind === "tool" && state.openingTools < OPENING_TOOL_BUDGET) {
384
+ if (isBoundedStreamTool(row) && state.openingTools < OPENING_TOOL_BUDGET) {
381
385
  state.openingTools += 1;
382
386
  renderStreamRow(state, row, lines, context, layout);
383
387
  continue;
384
388
  }
385
- if (row.kind !== "tool" && state.deferred.length === 0) {
389
+ if (!isBoundedStreamTool(row) && state.deferred.length === 0) {
386
390
  renderStreamRow(state, row, lines, context, layout);
387
391
  continue;
388
392
  }
389
393
  state.deferred.push({ kind: "row", row });
390
- if (row.kind === "tool") {
391
- const pendingTools = state.deferred.filter((entry) => entry.kind === "row" && entry.row.kind === "tool").length;
394
+ if (isBoundedStreamTool(row)) {
395
+ const pendingTools = state.deferred.filter((entry) => entry.kind === "row" && isBoundedStreamTool(entry.row)).length;
392
396
  if (pendingTools > RECENT_TOOL_BUDGET)
393
397
  omitOldestDeferredTool(state);
394
398
  }
@@ -416,31 +420,45 @@ function flushActivityTail(state, context, layout) {
416
420
  */
417
421
  export function activityStream(context, layout = plainLayout()) {
418
422
  const state = {
419
- newestSequence: undefined,
423
+ newestSettledSequence: undefined,
424
+ mutableSequences: new Set(),
420
425
  previousClock: undefined,
421
426
  renderedBoundaries: new Set(),
422
427
  openingTools: 0,
423
428
  deferred: [],
424
429
  };
425
- const seed = (snapshot) => {
430
+ const seed = (activity) => {
426
431
  const lines = [];
427
- renderCurrentTurnBoundary(state, snapshot, lines, context, layout);
428
- const rows = settledRows(snapshot);
432
+ const boundary = currentTurnBoundary(activity);
433
+ const unseenBoundary = boundary !== undefined && !state.renderedBoundaries.has(boundary.turnSequence);
434
+ if (unseenBoundary) {
435
+ renderCurrentTurnBoundary(state, activity, lines, context, layout);
436
+ const omitted = baselineOmissionCount(activity, boundary);
437
+ if (omitted > 0)
438
+ lines.push(layout.marker(omitted));
439
+ }
440
+ rememberMutableRows(state, activity);
441
+ const rows = settledRows(activity);
429
442
  if (rows.length > 0)
430
- state.newestSequence = rows.reduce((newest, row) => Math.max(newest, row.sequence), rows[0].sequence);
443
+ state.newestSettledSequence = rows.reduce((newest, row) => Math.max(newest, row.sequence), rows[0].sequence);
431
444
  return lines;
432
445
  };
433
- const observe = (snapshot) => observeActivitySnapshot(state, snapshot, context, layout);
446
+ const observe = (activity) => observeActivitySnapshot(state, activity, context, layout);
434
447
  const flush = () => flushActivityTail(state, context, layout);
435
448
  return Object.assign(observe, { seed, flush });
436
449
  }
437
450
  function conclusionMarkVerb(status, answered) {
438
- if (status.life === "running")
439
- return { mark: "●", verb: "still running" };
451
+ if (failedOutcomeDiagnostic(status) !== undefined)
452
+ return { mark: "!", verb: "failed" };
440
453
  if (answered)
441
454
  return { mark: "✓", verb: "answered" };
455
+ if (status.life === "running")
456
+ return { mark: "●", verb: "still running" };
457
+ if (!defaultWaitComplete(status)) {
458
+ return { mark: "⧗", verb: "pending tell" };
459
+ }
442
460
  if (status.life === "asleep")
443
- return { mark: "!", verb: "failed" };
461
+ return { mark: "✓", verb: "completed" };
444
462
  if (status.life === "killed")
445
463
  return { mark: "×", verb: "killed" };
446
464
  if (status.life === "hung")
@@ -525,10 +543,10 @@ function observeWaitRound(state, round, context, now) {
525
543
  lines.push(...snapshotHeading(sole.status.id, sole.alias, sole.contract));
526
544
  }
527
545
  }
528
- for (const { status } of round) {
546
+ for (const { status, rows } of round) {
529
547
  const known = state.streams.get(status.id);
530
548
  if (known !== undefined) {
531
- lines.push(...known(status.timeline));
549
+ lines.push(...known({ snapshot: status.timeline, rows }));
532
550
  }
533
551
  else {
534
552
  // Only a plural wait attributes its rows; a single-target stream keeps the plain row grammar.
@@ -536,10 +554,10 @@ function observeWaitRound(state, round, context, now) {
536
554
  ? sourceLayout(state.sources.get(status.id) ?? status.id, () => state.sourceWidth)
537
555
  : plainLayout());
538
556
  state.streams.set(status.id, stream);
539
- // A wait starts at the current settled frontier: its backlog is neither evidence nor budget.
540
- lines.push(...stream.seed(status.timeline));
557
+ // Seed marks skipped retained evidence without spending this command's live tool budget.
558
+ lines.push(...stream.seed({ snapshot: status.timeline, rows }));
541
559
  }
542
- if (!state.settledAt.has(status.id) && waitComplete(status))
560
+ if (!state.settledAt.has(status.id) && defaultWaitComplete(status))
543
561
  state.settledAt.set(status.id, settleMoment(status) ?? now());
544
562
  }
545
563
  return lines;
@@ -574,13 +592,6 @@ function orderedWaitIds(state, result) {
574
592
  }
575
593
  function concludeWaitStream(state, result, startedAt, now) {
576
594
  const tail = [];
577
- // The returned observation can be newer than the final callback. Advance every known stream
578
- // before flushing it, while still flushing members that became unobserved at the end.
579
- for (const observation of result.observations) {
580
- const stream = state.streams.get(observation.status.id);
581
- if (stream !== undefined)
582
- tail.push(...stream(observation.status.timeline));
583
- }
584
595
  for (const stream of state.streams.values())
585
596
  tail.push(...stream.flush());
586
597
  const end = now();
@@ -598,7 +609,7 @@ function concludeWaitStream(state, result, startedAt, now) {
598
609
  if (observation === undefined)
599
610
  return [];
600
611
  const status = observation.status;
601
- const complete = waitComplete(status);
612
+ const complete = defaultWaitComplete(status);
602
613
  const at = complete ? (state.settledAt.get(id) ?? end) : end;
603
614
  const { mark, verb } = conclusionMarkVerb(status, statusAnswer(observation) !== undefined);
604
615
  const target = multi ? ` ${padToDisplay(state.sources.get(id) ?? id, state.sourceWidth)}` : "";
@@ -659,17 +670,15 @@ export function callObservationStream(context, head, options = {}) {
659
670
  opened = true;
660
671
  lines.push(...snapshotHeading(head.id, head.alias, head.contract), ...head.facts);
661
672
  };
662
- const observe = (status) => {
673
+ const observe = (observation) => {
663
674
  const lines = [];
664
675
  open(lines);
665
- lines.push(...stream(status.timeline));
676
+ lines.push(...stream({ snapshot: observation.status.timeline, rows: observation.rows }));
666
677
  return lines;
667
678
  };
668
679
  const conclude = (observation) => {
669
680
  const lines = [];
670
681
  open(lines);
671
- if (observation.kind === "observed")
672
- lines.push(...stream(observation.status.timeline));
673
682
  lines.push(...stream.flush());
674
683
  if (observation.kind === "failed") {
675
684
  lines.push(`! error ${safeText(observation.failure.diagnostic)}`);
@@ -680,7 +689,7 @@ export function callObservationStream(context, head, options = {}) {
680
689
  const end = now();
681
690
  const status = observation.status;
682
691
  const answered = statusAnswer({ status }) !== undefined;
683
- const complete = waitComplete(status);
692
+ const complete = defaultWaitComplete(status);
684
693
  const at = complete ? (settleMoment(status) ?? end) : end;
685
694
  const { mark, verb } = conclusionMarkVerb(status, answered);
686
695
  lines.push(`${clockFromMs(at)} ${mark} ${verb}${conclusionClause(at, startedAt, complete, end)}`);
@@ -798,7 +807,7 @@ export function mutationObservationStageText(id, observation, context, options =
798
807
  return mutationSnapshotText(observation, context, options);
799
808
  }
800
809
  export function statusAnswer(view) {
801
- if (!waitComplete(view.status))
810
+ if (!defaultWaitComplete(view.status))
802
811
  return undefined;
803
812
  if (view.status.life !== "asleep")
804
813
  return undefined;
@@ -809,10 +818,6 @@ export function statusAnswer(view) {
809
818
  return undefined;
810
819
  return timeline.outcome.outcome.answer;
811
820
  }
812
- function waitComplete(status) {
813
- return (status.life !== "running" &&
814
- !status.timeline.entries.some((entry) => entry.kind === "row" && entry.row.kind === "tell" && entry.row.state === "pending"));
815
- }
816
821
  function answerCallFailed(result) {
817
822
  return result.dispatch.kind === "failed" || result.alias.kind === "failed" || result.readonly?.enforcement === "none";
818
823
  }
@@ -835,7 +840,8 @@ export function akumaRawAnswer(result) {
835
840
  const single = total === 1 ? result.result.observations[0] : undefined;
836
841
  return single === undefined ? "" : (statusAnswer(single) ?? "");
837
842
  }
838
- if (result.result.observations.length === 1)
843
+ const total = result.result.observations.length + result.result.unobserved.length;
844
+ if (total === 1)
839
845
  return statusAnswer(result.result.observations[0]);
840
846
  }
841
847
  if (result.action === "history" && result.mode === "exact" && result.historyResult.kind === "exact") {
@@ -906,7 +912,7 @@ export function waitText(result, context) {
906
912
  if (observation === undefined)
907
913
  return [];
908
914
  const status = observation.status;
909
- const complete = waitComplete(status);
915
+ const complete = defaultWaitComplete(status);
910
916
  const at = complete ? (settleMoment(status) ?? end) : end;
911
917
  const { mark, verb } = conclusionMarkVerb(status, statusAnswer(observation) !== undefined);
912
918
  return [
@@ -52,15 +52,17 @@ function searchText(call) {
52
52
  ...(call.glob === undefined ? [] : [oneLine(call.glob)]),
53
53
  ].join(" · ");
54
54
  }
55
- function fileChange(call) {
55
+ function fileChange(call, state) {
56
56
  const first = call.changes[0];
57
57
  if (first === undefined)
58
58
  return { label: "edit", text: "files" };
59
59
  const label = call.changes.length === 1 ? (first.op === "add" ? "write" : first.op === "delete" ? "delete" : "edit") : "edit";
60
60
  const subject = call.changes.length === 1 ? oneLine(first.path) : `${call.changes.length} files · ${oneLine(first.path)} ...`;
61
61
  const complete = call.changes.every((change) => change.diffstat !== undefined);
62
- if (!complete)
62
+ if (!complete && (state === "active" || state === "unsettled"))
63
63
  return { label, text: subject };
64
+ if (!complete)
65
+ return { label, text: `${subject} — +? -?` };
64
66
  const totals = call.changes.reduce((sum, change) => ({
65
67
  added: sum.added + change.diffstat.added,
66
68
  removed: sum.removed + change.diffstat.removed,
@@ -85,7 +87,7 @@ export function toolRepr(row) {
85
87
  core = { label: searchLabel(row.call.scope), text: searchText(row.call) };
86
88
  break;
87
89
  case "fileChange":
88
- core = fileChange(row.call);
90
+ core = fileChange(row.call, row.state);
89
91
  break;
90
92
  case "other":
91
93
  core = { label: "use", text: oneLine(row.call.display || row.name) };