@dna113p/machines 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import type { StateValue } from "xstate";
2
2
  import type { AgentUpdate } from "./index.ts";
3
+ import { type RunGraph, type RunTrailStep } from "./run-graph.ts";
3
4
  /** One published run, stored as `<id>.json` in the run-status directory. */
4
5
  export interface RunStatusRecord {
5
6
  /** Version 1 records carry only `human.prompt`. */
@@ -30,6 +31,12 @@ export interface RunStatusRecord {
30
31
  /** Names the inbox responses the run took lately, so that each sender can tell; never a response. */
31
32
  readonly deliveries?: readonly string[];
32
33
  readonly error?: string;
34
+ /** The finished run's summary, at most 2000 characters including any ellipsis. */
35
+ readonly note?: string;
36
+ /** The Machine's states and transitions, when it is small enough to draw. */
37
+ readonly graph?: RunGraph;
38
+ /** The states the run entered, oldest first: the latest `trailLimit` of them. */
39
+ readonly trail?: readonly RunTrailStep[];
33
40
  readonly startedAt: string;
34
41
  readonly updatedAt: string;
35
42
  }
@@ -63,6 +70,7 @@ export interface RunStatusRun {
63
70
  /** Replaces `MACHINES_RUN_LABEL`, and is shortened as it is. */
64
71
  readonly label?: string;
65
72
  readonly answeredBy?: "host";
73
+ readonly graph?: RunGraph;
66
74
  }
67
75
  /** A present key replaces its field; a present key holding `undefined` clears it. */
68
76
  export interface RunStatusChange {
@@ -77,6 +85,7 @@ export interface RunStatusChange {
77
85
  }
78
86
  export type RunStatusOutcome = {
79
87
  readonly state?: StateValue;
88
+ readonly note?: string;
80
89
  } | {
81
90
  readonly error: string;
82
91
  };
@@ -89,6 +98,8 @@ export interface RunStatusPublisher {
89
98
  update(change: RunStatusChange): void;
90
99
  finish(outcome: RunStatusOutcome): void;
91
100
  }
101
+ /** How many entered states a record keeps: enough for a long run's repairs, small enough to rewrite at every step. */
102
+ export declare const trailLimit = 200;
92
103
  /** How long a record goes on naming a response its run took: longer than a sender waits to hear of it. */
93
104
  export declare const deliveryRetentionMilliseconds = 60000;
94
105
  /** How long a session's watch list is kept after its last change: sessions end without saying so. */
@@ -115,8 +126,9 @@ export declare function runDeliveryPath(directory: string, id: string, delivery:
115
126
  /**
116
127
  * Publishes one run for hosts that can only read files, such as a status line,
117
128
  * in `runStatusDirectory`. Publication is presentation: it never throws, and it
118
- * records neither Machine input nor output. A run whose first record cannot be
119
- * written, as in a read-only home, is not published at all.
129
+ * records no Machine input and only the supplied summary of its output.
130
+ * A run whose first record cannot be written, as in a read-only home, is not
131
+ * published at all.
120
132
  */
121
133
  export declare function createRunStatusPublisher(run: RunStatusRun, environment?: NodeJS.ProcessEnv): RunStatusPublisher;
122
134
  /**
@@ -129,7 +141,9 @@ export declare function runStatusObserver(update: (change: RunStatusChange) => v
129
141
  };
130
142
  /**
131
143
  * The session a process belongs to: the owner of the runs it starts, and the
132
- * session whose watch list `machine watch` changes.
144
+ * session whose watch list `machine watch` changes. `MACHINES_RUN_OWNER` is how
145
+ * any host names it; the others are what Claude Code, Codex, and Pi give the
146
+ * commands they start.
133
147
  */
134
148
  export declare function runSessionId(environment?: NodeJS.ProcessEnv): string | undefined;
135
149
  /** Returns the valid records in a directory. Unreadable or malformed files are skipped. */
@@ -137,3 +151,5 @@ export declare function readRunStatuses(directory: string): RunStatusRecord[];
137
151
  /** The id becomes a file name, so it must not be able to leave the directory. */
138
152
  export declare function isFileSafeId(id: string): boolean;
139
153
  export declare function isProcessAlive(pid: number): boolean;
154
+ /** Only an object's non-empty summary is published from a Machine's output. */
155
+ export declare function runOutputSummary(output: unknown): string | undefined;
@@ -2,7 +2,10 @@ import { randomUUID } from "node:crypto";
2
2
  import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { basename, isAbsolute, join, resolve } from "node:path";
4
4
  import { machinesUserHome } from "./discovery.js";
5
+ import { statePaths } from "./run-graph.js";
5
6
  const staleAfterMilliseconds = 60 * 60 * 1_000;
7
+ /** How many entered states a record keeps: enough for a long run's repairs, small enough to rewrite at every step. */
8
+ export const trailLimit = 200;
6
9
  /** How long a record goes on naming a response its run took: longer than a sender waits to hear of it. */
7
10
  export const deliveryRetentionMilliseconds = 60_000;
8
11
  /** How long a session's watch list is kept after its last change: sessions end without saying so. */
@@ -59,8 +62,9 @@ export function runDeliveryPath(directory, id, delivery) {
59
62
  /**
60
63
  * Publishes one run for hosts that can only read files, such as a status line,
61
64
  * in `runStatusDirectory`. Publication is presentation: it never throws, and it
62
- * records neither Machine input nor output. A run whose first record cannot be
63
- * written, as in a read-only home, is not published at all.
65
+ * records no Machine input and only the supplied summary of its output.
66
+ * A run whose first record cannot be written, as in a read-only home, is not
67
+ * published at all.
64
68
  */
65
69
  export function createRunStatusPublisher(run, environment = process.env) {
66
70
  const directory = runStatusDirectory(environment);
@@ -79,6 +83,14 @@ export function createRunStatusPublisher(run, environment = process.env) {
79
83
  let human;
80
84
  let deliveries = [];
81
85
  let error;
86
+ let note;
87
+ let trail = [];
88
+ const enter = (entered, at) => {
89
+ const path = entered === undefined ? "" : statePaths(entered).join(", ");
90
+ if (path === "" || trail.at(-1)?.state === path)
91
+ return;
92
+ trail = [...trail, { state: path, at }].slice(-trailLimit);
93
+ };
82
94
  const publish = () => {
83
95
  const record = {
84
96
  schemaVersion: 2,
@@ -99,6 +111,9 @@ export function createRunStatusPublisher(run, environment = process.env) {
99
111
  ...(run.answeredBy === undefined ? {} : { answeredBy: run.answeredBy }),
100
112
  ...(deliveries.length === 0 ? {} : { deliveries: deliveries.map((delivery) => delivery.id) }),
101
113
  ...(error === undefined ? {} : { error }),
114
+ ...(note === undefined ? {} : { note }),
115
+ ...(run.graph === undefined ? {} : { graph: run.graph }),
116
+ ...(trail.length === 0 ? {} : { trail }),
102
117
  startedAt,
103
118
  updatedAt: new Date().toISOString(),
104
119
  };
@@ -153,8 +168,10 @@ export function createRunStatusPublisher(run, environment = process.env) {
153
168
  // Repeated notifications for an unchanged run do not need another write.
154
169
  if (JSON.stringify(next) === JSON.stringify({ status, state, agent, limit, human, deliveries }))
155
170
  return;
156
- if (next.state !== state)
171
+ if (next.state !== state) {
157
172
  stateSince = new Date().toISOString();
173
+ enter(next.state, stateSince);
174
+ }
158
175
  ({ status, state, agent, limit, human, deliveries } = next);
159
176
  publish();
160
177
  },
@@ -168,6 +185,8 @@ export function createRunStatusPublisher(run, environment = process.env) {
168
185
  else {
169
186
  status = "completed";
170
187
  state = formatState(outcome.state) ?? state;
188
+ note = formatNote(outcome.note);
189
+ enter(state, new Date().toISOString());
171
190
  }
172
191
  stateSince = undefined;
173
192
  agent = undefined;
@@ -228,10 +247,16 @@ export function runStatusObserver(update) {
228
247
  }
229
248
  /**
230
249
  * The session a process belongs to: the owner of the runs it starts, and the
231
- * session whose watch list `machine watch` changes.
250
+ * session whose watch list `machine watch` changes. `MACHINES_RUN_OWNER` is how
251
+ * any host names it; the others are what Claude Code, Codex, and Pi give the
252
+ * commands they start.
232
253
  */
233
254
  export function runSessionId(environment = process.env) {
234
- return environment.MACHINES_RUN_OWNER || environment.CLAUDE_CODE_SESSION_ID || undefined;
255
+ return environment.MACHINES_RUN_OWNER
256
+ || environment.CLAUDE_CODE_SESSION_ID
257
+ || environment.CODEX_THREAD_ID
258
+ || environment.PI_SESSION_ID
259
+ || undefined;
235
260
  }
236
261
  /** Returns the valid records in a directory. Unreadable or malformed files are skipped. */
237
262
  export function readRunStatuses(directory) {
@@ -347,9 +372,23 @@ function isRunStatusRecord(value) {
347
372
  && (record.answeredBy === undefined || record.answeredBy === "host")
348
373
  && isOptionalStrings(record.deliveries)
349
374
  && isOptionalString(record.error)
375
+ && isOptionalString(record.note)
376
+ && (record.graph === undefined || isGraph(record.graph))
377
+ && (record.trail === undefined || (Array.isArray(record.trail) && record.trail.every((step) => step !== null && typeof step === "object" && typeof step.state === "string" && isTimestamp(step.at))))
350
378
  && isTimestamp(record.startedAt)
351
379
  && isTimestamp(record.updatedAt);
352
380
  }
381
+ function isGraph(value) {
382
+ if (value === null || typeof value !== "object")
383
+ return false;
384
+ const graph = value;
385
+ return isOptionalString(graph.initial)
386
+ && Array.isArray(graph.states) && graph.states.every((state) => state !== null && typeof state === "object"
387
+ && typeof state.id === "string" && typeof state.kind === "string"
388
+ && isOptionalString(state.parent) && isOptionalString(state.role))
389
+ && Array.isArray(graph.edges) && graph.edges.every((edge) => edge !== null && typeof edge === "object"
390
+ && typeof edge.from === "string" && typeof edge.to === "string" && typeof edge.event === "string");
391
+ }
353
392
  function isOptionalString(value) {
354
393
  return value === undefined || typeof value === "string";
355
394
  }
@@ -378,6 +417,26 @@ function formatState(state) {
378
417
  return undefined;
379
418
  return typeof state === "string" ? state : JSON.stringify(state);
380
419
  }
420
+ /** Only an object's non-empty summary is published from a Machine's output. */
421
+ export function runOutputSummary(output) {
422
+ if (output === null || typeof output !== "object" || Array.isArray(output))
423
+ return undefined;
424
+ const summary = output.summary;
425
+ return typeof summary === "string" && summary.trim() !== "" ? summary : undefined;
426
+ }
427
+ function formatNote(note) {
428
+ const text = note?.trim();
429
+ if (!text)
430
+ return undefined;
431
+ if (text.length <= 2000)
432
+ return text;
433
+ // Reserve one character for the ellipsis, keeping the last whole word that fits.
434
+ let end = 1999;
435
+ while (end > 0 && !/\s/u.test(text[end]))
436
+ end -= 1;
437
+ // A single word longer than the limit still has to fit in the record.
438
+ return `${text.slice(0, end || 1999).trimEnd()}…`;
439
+ }
381
440
  /** One short line: a label is a caption, not a place for task text. */
382
441
  function formatLabel(label) {
383
442
  const line = label?.split(/\r?\n/u).map((part) => part.trim()).find((part) => part !== "");
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The page `machine ui` serves: one self-contained document, with no outside
3
+ * scripts, styles, or fonts, so it works offline and loads nothing but its own
4
+ * data. The token lets only this page answer a run.
5
+ */
6
+ export declare function runUiPage(token: string): string;