@fitchmultz/pi-subagents 0.52.7 → 0.52.9

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 (80) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/agent/launch.js +1 -1
  3. package/dist/agent/observation-view.js +15 -7
  4. package/dist/agent/observe-card.js +2 -2
  5. package/dist/agent/observe-client.js +1 -1
  6. package/dist/agent/observe.js +4 -1
  7. package/dist/agent/process.js +5 -3
  8. package/dist/agent/runner.js +9 -4
  9. package/dist/agent/saved.js +16 -6
  10. package/dist/agent/view.js +5 -2
  11. package/dist/build-info.js +1 -1
  12. package/dist/pi-intercom/bridge-routes.js +1 -1
  13. package/dist/pi-intercom/bridge.js +4 -3
  14. package/dist/pi-intercom/broker/broker.js +1 -1
  15. package/dist/pi-intercom/broker/client-last.d.ts +1 -1
  16. package/dist/pi-intercom/broker/client.d.ts +9 -9
  17. package/dist/pi-intercom/broker/framing.js +6 -0
  18. package/dist/pi-intercom/broker/last.d.ts +5 -1
  19. package/dist/pi-intercom/broker/last.js +33 -15
  20. package/dist/pi-intercom/broker/parked.js +1 -1
  21. package/dist/pi-intercom/outbound.js +1 -0
  22. package/dist/pi-intercom/reply-tracker.js +11 -5
  23. package/dist/pi-intercom/session-targets.d.ts +2 -2
  24. package/dist/pi-intercom/session-targets.js +3 -2
  25. package/dist/pi-intercom/tool-registration.js +1 -0
  26. package/dist/pi-intercom/types.d.ts +37 -37
  27. package/dist/pi-intercom/types.js +9 -7
  28. package/dist/shared/output-page.js +1 -1
  29. package/package.json +5 -4
  30. package/scripts/archive-retired-history.mjs +3 -1
  31. package/src/agent/index.ts +3 -3
  32. package/src/agent/journal-tail.ts +2 -2
  33. package/src/agent/launch.ts +1 -1
  34. package/src/agent/notifications.ts +3 -3
  35. package/src/agent/observation-state.ts +2 -2
  36. package/src/agent/observation-view.ts +20 -7
  37. package/src/agent/observation.ts +4 -4
  38. package/src/agent/observe-card.ts +7 -3
  39. package/src/agent/observe-chrome.ts +2 -2
  40. package/src/agent/observe-client.ts +5 -5
  41. package/src/agent/observe-reading.ts +1 -1
  42. package/src/agent/observe-screen.ts +4 -4
  43. package/src/agent/observe-transcript.ts +4 -4
  44. package/src/agent/observe.ts +13 -8
  45. package/src/agent/parking.ts +1 -1
  46. package/src/agent/process.ts +6 -4
  47. package/src/agent/runner.ts +15 -7
  48. package/src/agent/saved.ts +17 -7
  49. package/src/agent/usage.ts +1 -1
  50. package/src/agent/view.ts +18 -9
  51. package/src/agent/wake.ts +2 -2
  52. package/src/pi-intercom/bridge-config.ts +2 -2
  53. package/src/pi-intercom/bridge-mailbox.ts +1 -1
  54. package/src/pi-intercom/bridge-presence.ts +1 -1
  55. package/src/pi-intercom/bridge-routes.ts +5 -5
  56. package/src/pi-intercom/bridge-session.ts +5 -5
  57. package/src/pi-intercom/bridge-wake.ts +2 -2
  58. package/src/pi-intercom/bridge.ts +4 -3
  59. package/src/pi-intercom/broker/broker.ts +1 -1
  60. package/src/pi-intercom/broker/client-last.ts +1 -1
  61. package/src/pi-intercom/broker/client-park.ts +1 -1
  62. package/src/pi-intercom/broker/client.ts +9 -9
  63. package/src/pi-intercom/broker/framing.ts +6 -0
  64. package/src/pi-intercom/broker/last.ts +62 -18
  65. package/src/pi-intercom/broker/parked.ts +8 -5
  66. package/src/pi-intercom/config.ts +1 -1
  67. package/src/pi-intercom/outbound.ts +1 -0
  68. package/src/pi-intercom/reply-tracker.ts +13 -6
  69. package/src/pi-intercom/reply-wait.ts +3 -3
  70. package/src/pi-intercom/runtime-types.ts +1 -1
  71. package/src/pi-intercom/session-targets.ts +5 -4
  72. package/src/pi-intercom/tool-registration.ts +1 -0
  73. package/src/pi-intercom/tool-results.ts +1 -1
  74. package/src/pi-intercom/types.ts +48 -43
  75. package/src/shared/journal-frames.ts +1 -1
  76. package/src/shared/json-projection.ts +2 -2
  77. package/src/shared/native-journal.ts +2 -2
  78. package/src/shared/output-page.ts +3 -3
  79. package/src/shared/post-exit-stdio-guard.ts +4 -4
  80. package/tsconfig.json +8 -1
@@ -6,23 +6,23 @@ export type WakeResult = {
6
6
  };
7
7
  export interface SessionInfo {
8
8
  readonly id: string;
9
- readonly name?: string;
9
+ readonly name?: string | undefined;
10
10
  readonly cwd: string;
11
11
  readonly model: string;
12
12
  /** Opaque same-repository/worktree identity used for ambient peer awareness. */
13
- readonly projectId?: string;
14
- readonly rootSessionId?: string;
15
- readonly parentSessionId?: string;
16
- readonly incarnation?: string;
17
- readonly guardedDelivery?: 1;
18
- readonly applicationGuard?: string;
19
- readonly status?: string;
20
- readonly lastSeen?: number;
21
- readonly lastIntercomActivity?: number;
22
- readonly pendingAsks?: number;
23
- readonly acceptsAsks?: boolean;
24
- readonly subscriptions?: readonly TopicSubscription[];
25
- readonly topics?: readonly TopicUpdate[];
13
+ readonly projectId?: string | undefined;
14
+ readonly rootSessionId?: string | undefined;
15
+ readonly parentSessionId?: string | undefined;
16
+ readonly incarnation?: string | undefined;
17
+ readonly guardedDelivery?: 1 | undefined;
18
+ readonly applicationGuard?: string | undefined;
19
+ readonly status?: string | undefined;
20
+ readonly lastSeen?: number | undefined;
21
+ readonly lastIntercomActivity?: number | undefined;
22
+ readonly pendingAsks?: number | undefined;
23
+ readonly acceptsAsks?: boolean | undefined;
24
+ readonly subscriptions?: readonly TopicSubscription[] | undefined;
25
+ readonly topics?: readonly TopicUpdate[] | undefined;
26
26
  }
27
27
  export interface TopicSubscription {
28
28
  readonly topic: string;
@@ -52,10 +52,10 @@ export interface DeliveryGuard extends ContextTarget {
52
52
  export interface PublicationReplyHint {
53
53
  readonly tool: string;
54
54
  readonly commandId: string;
55
- readonly commandFingerprint?: string;
55
+ readonly commandFingerprint?: string | undefined;
56
56
  }
57
57
  export type SendOptions = Omit<Message, "id" | "timestamp" | "content"> & Message["content"] & {
58
- readonly messageId?: string;
58
+ readonly messageId?: string | undefined;
59
59
  };
60
60
  export declare function isContextTarget(value: unknown): value is ContextTarget;
61
61
  export declare function isDeliveryGuard(value: unknown): value is DeliveryGuard;
@@ -69,20 +69,20 @@ export declare function isHumanMessageOrigin(value: unknown): value is HumanMess
69
69
  export interface Message {
70
70
  readonly id: string;
71
71
  readonly timestamp: number;
72
- readonly replyTo?: string;
73
- readonly expectsReply?: boolean;
72
+ readonly replyTo?: string | undefined;
73
+ readonly expectsReply?: boolean | undefined;
74
74
  /** Recipients verify parent ownership before elevating human origin. */
75
- readonly human?: HumanMessageOrigin;
76
- readonly topic?: TopicUpdate;
77
- readonly delivery?: MessageDelivery;
78
- readonly queueMode?: QueueMode;
79
- readonly threadId?: string;
80
- readonly passive?: boolean;
81
- readonly guard?: DeliveryGuard;
82
- readonly replyHint?: PublicationReplyHint;
75
+ readonly human?: HumanMessageOrigin | undefined;
76
+ readonly topic?: TopicUpdate | undefined;
77
+ readonly delivery?: MessageDelivery | undefined;
78
+ readonly queueMode?: QueueMode | undefined;
79
+ readonly threadId?: string | undefined;
80
+ readonly passive?: boolean | undefined;
81
+ readonly guard?: DeliveryGuard | undefined;
82
+ readonly replyHint?: PublicationReplyHint | undefined;
83
83
  readonly content: {
84
84
  readonly text: string;
85
- readonly attachments?: readonly Attachment[];
85
+ readonly attachments?: readonly Attachment[] | undefined;
86
86
  };
87
87
  }
88
88
  export interface Attachment {
@@ -119,17 +119,17 @@ export interface SessionSnapshot {
119
119
  })[];
120
120
  }
121
121
  type CompactTopicMessage = Omit<Message, "topic"> & {
122
- readonly topic?: Omit<TopicUpdate, "text"> & {
123
- readonly text?: string;
124
- };
122
+ readonly topic?: (Omit<TopicUpdate, "text"> & {
123
+ readonly text?: string | undefined;
124
+ }) | undefined;
125
125
  };
126
126
  /** Topic text and the standard coalescing key need only one copy on the wire. */
127
127
  export declare function compactTopicMessage(message: Message): CompactTopicMessage;
128
128
  export declare function normalizeMessage(value: unknown): Message | null;
129
129
  export interface LastMessageQuery {
130
- readonly entryId?: string;
131
- readonly offset?: number;
132
- readonly limit?: number;
130
+ readonly entryId?: string | undefined;
131
+ readonly offset?: number | undefined;
132
+ readonly limit?: number | undefined;
133
133
  }
134
134
  export declare const LAST_MESSAGE_PAGE_LIMIT = 16384;
135
135
  export declare function isLastMessageQuery(value: unknown): value is LastMessageQuery;
@@ -152,13 +152,13 @@ export interface NativeReceipt extends ReceiptQuery {
152
152
  readonly version: 1;
153
153
  readonly sender: {
154
154
  readonly peerId: string;
155
- readonly rootSessionId?: string;
156
- readonly incarnation?: string;
155
+ readonly rootSessionId?: string | undefined;
156
+ readonly incarnation?: string | undefined;
157
157
  };
158
158
  readonly textSha256: string;
159
159
  readonly delivery: MessageDelivery;
160
- readonly guard?: DeliveryGuard;
161
- readonly replyHint?: PublicationReplyHint;
160
+ readonly guard?: DeliveryGuard | undefined;
161
+ readonly replyHint?: PublicationReplyHint | undefined;
162
162
  readonly checkpointEntryId: string;
163
163
  readonly receivedAt: number;
164
164
  /** Historical observations, not a promise that an input is still queued. */
@@ -207,16 +207,18 @@ export function compactTopicMessage(message) {
207
207
  if (!message.topic) {
208
208
  return message;
209
209
  }
210
+ const { threadId, ...rest } = message;
211
+ const dropThreadId = message.queueMode === "replace" && threadId === `topic:${message.topic.topic}`;
210
212
  return {
211
- ...message,
212
- topic: message.topic.text === message.content.text
213
- ? { ...message.topic, text: undefined }
214
- : message.topic,
215
- threadId: message.queueMode === "replace" && message.threadId === `topic:${message.topic.topic}`
216
- ? undefined
217
- : message.threadId,
213
+ ...rest,
214
+ topic: message.topic.text === message.content.text ? withoutTopicText(message.topic) : message.topic,
215
+ ...(dropThreadId || threadId === undefined ? {} : { threadId }),
218
216
  };
219
217
  }
218
+ function withoutTopicText(topic) {
219
+ const { text: _duplicate, ...rest } = topic;
220
+ return rest;
221
+ }
220
222
  export function normalizeMessage(value) {
221
223
  if (isMessage(value)) {
222
224
  return value;
@@ -34,7 +34,7 @@ function utf8Bounds(bytes, count, more) {
34
34
  last--;
35
35
  }
36
36
  const lead = bytes[last];
37
- if (last + width(lead) > end) {
37
+ if (lead === undefined || last + width(lead) > end) {
38
38
  end = last;
39
39
  }
40
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitchmultz/pi-subagents",
3
- "version": "0.52.7",
3
+ "version": "0.52.9",
4
4
  "description": "Pi extension for subagent orchestration and local session coordination",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -63,8 +63,9 @@
63
63
  "smoke:package": "node scripts/package-smoke.mjs",
64
64
  "smoke:install": "node scripts/local-install-smoke.mjs",
65
65
  "check:size": "node scripts/measure-size.mjs",
66
- "lint": "oxlint --type-aware .",
66
+ "lint": "oxlint --type-aware --deny-warnings .",
67
67
  "lint:fix": "oxlint --type-aware --fix .",
68
+ "lint:agent": "oxlint --type-aware --deny-warnings --format=agent .",
68
69
  "format": "oxfmt --write .",
69
70
  "format:check": "oxfmt --check .",
70
71
  "ci": "npm run lint && npm run format:check && npm run typecheck && npm run check:size && npm run smoke:package && npm run smoke:install && npm run test:all",
@@ -109,8 +110,8 @@
109
110
  "@earendil-works/pi-tui": "1.4.0",
110
111
  "@types/node": "24.19.2",
111
112
  "@types/sinon": "22.0.2",
112
- "oxfmt": "0.74.0",
113
- "oxlint": "1.89.0",
113
+ "oxfmt": "0.75.0",
114
+ "oxlint": "1.90.0",
114
115
  "oxlint-tsgolint": "7.0.2005",
115
116
  "sinon": "22.1.0",
116
117
  "type-fest": "5.10.0",
@@ -418,7 +418,9 @@ function main() {
418
418
  fs.readFileSync("/proc/self/status", "utf8"),
419
419
  );
420
420
  assert.ok(capability !== null, "E_PROCESS_VISIBILITY");
421
- assert.ok((BigInt(`0x${capability[1]}`) & (1n << 19n)) !== 0n, "E_PROCESS_VISIBILITY");
421
+ const capEff = capability[1];
422
+ assert.ok(capEff !== undefined, "E_PROCESS_VISIBILITY");
423
+ assert.ok((BigInt(`0x${capEff}`) & (1n << 19n)) !== 0n, "E_PROCESS_VISIBILITY");
422
424
  }
423
425
  assert.match(values["owner-uid"] ?? "", /^(?:0|[1-9][0-9]*)$/, "E_OWNER_UID");
424
426
  const uid = Number(values["owner-uid"]);
@@ -82,17 +82,17 @@ function legacyInput(): string[] {
82
82
  }
83
83
  }
84
84
  class AgentRuntime {
85
- private ctx?: ExtensionContext;
85
+ private ctx?: ExtensionContext | undefined;
86
86
  private readonly reporter = new BackgroundReporter(() => this.ctx);
87
87
  // A managed launcher owns children across worker replacement; otherwise this process does.
88
- private host?: ManagedHost;
88
+ private host?: ManagedHost | undefined;
89
89
  private accepted = false;
90
90
  private base = "";
91
91
  private owner = "";
92
92
  private generation = "";
93
93
  // Polling remains only while change hints are unavailable or a reconciliation failed.
94
94
  private timer?: NodeJS.Timeout;
95
- private woken?: NodeJS.Immediate;
95
+ private woken?: NodeJS.Immediate | undefined;
96
96
  // Change hints for the owner directory, legacy results and every awaited canonical file.
97
97
  private readonly watches: CompletionWatches;
98
98
  private awaited: ReadonlySet<string> = new Set();
@@ -37,7 +37,7 @@ function contentIndex(keys: readonly (string | number)[]): number | undefined {
37
37
  }
38
38
  class TextPage {
39
39
  private chars = 0;
40
- private carry?: { index: number; text: string };
40
+ private carry?: { index: number; text: string } | undefined;
41
41
  private readonly text = new Map<number, string>();
42
42
  private readonly start: number;
43
43
  constructor(page: number) {
@@ -147,7 +147,7 @@ export class ConversationJournal {
147
147
  async page(
148
148
  entry: NativeRecord,
149
149
  page = 0,
150
- options: { signal?: AbortSignal; hideThinking?: boolean } = {},
150
+ options: { signal?: AbortSignal | undefined; hideThinking?: boolean } = {},
151
151
  ): Promise<ConversationPage> {
152
152
  const text = new TextPage(page);
153
153
  const value = await this.native.project(
@@ -126,7 +126,7 @@ export async function prepareLaunch(options: {
126
126
  model: selection.model.id,
127
127
  thinking: selection.thinking,
128
128
  trusted,
129
- name: input.name,
129
+ ...(input.name !== undefined ? { name: input.name } : {}),
130
130
  createdAt: Date.now(),
131
131
  };
132
132
  // Retain even unpublished native journals on failure, including full fork evidence.
@@ -114,12 +114,12 @@ interface PendingJournal {
114
114
  interface Loaded {
115
115
  readonly launch: Launch;
116
116
  readonly result: Result | undefined;
117
- readonly journal?: PendingJournal;
117
+ readonly journal?: PendingJournal | undefined;
118
118
  }
119
119
  interface Settled {
120
120
  readonly launch: string;
121
121
  readonly result: string;
122
- readonly pending?: Loaded;
122
+ readonly pending?: Loaded | undefined;
123
123
  readonly endedAt: number;
124
124
  readonly createdAt: number;
125
125
  }
@@ -127,7 +127,7 @@ interface Completed {
127
127
  readonly id: string;
128
128
  readonly endedAt: number;
129
129
  readonly createdAt: number;
130
- readonly loaded?: Loaded;
130
+ readonly loaded?: Loaded | undefined;
131
131
  }
132
132
  // Validated settled runs by file stamp, so polling does not reparse unchanged history.
133
133
  const settledRuns = new Map<string, Map<string, Settled>>();
@@ -24,7 +24,7 @@ const WRITING = { text: "writing", thinking: "thinking", toolCall: "calling" } a
24
24
  /** A live in-flight assistant message: a transient native reference, never a receipt. */
25
25
  export interface LiveMessage {
26
26
  readonly key: number;
27
- message: AssistantMessage;
27
+ message: AssistantMessage | undefined;
28
28
  ended: boolean;
29
29
  }
30
30
  /** A native message found in the session tree: `[seq, entry id, live message key, tool call id]`. */
@@ -77,7 +77,7 @@ export class LiveTool {
77
77
  export class LiveState {
78
78
  version = 0;
79
79
  activityVersion = 0;
80
- message?: LiveMessage;
80
+ message?: LiveMessage | undefined;
81
81
  readonly tools = new Map<string, LiveTool>();
82
82
  readonly settlements: Settlement[] = [];
83
83
  settled = 0;
@@ -206,7 +206,7 @@ export class Viewer {
206
206
  }
207
207
  private message(frame: Frame, state: LiveState): string | undefined {
208
208
  const live = state.message;
209
- if (live === undefined) {
209
+ if (live === undefined || live.message === undefined) {
210
210
  return undefined;
211
211
  }
212
212
  const reset = this.key !== live.key;
@@ -222,18 +222,31 @@ export class Viewer {
222
222
  this.ended = live.ended;
223
223
  const limit = Math.min(content.length, BLOCKS);
224
224
  this.blocks.length = Math.min(this.blocks.length, limit);
225
- for (let index = 0; index < limit && !frame.full; index++) {
226
- const item = this.cursor(index, content[index]).next(index, frame, state);
227
- if (item !== undefined) {
228
- frame.blocks.push(item);
229
- }
230
- }
225
+ this.appendBlocks(frame, state, content, limit);
231
226
  if (!changed && frame.blocks.length === 0) {
232
227
  return undefined;
233
228
  }
234
229
  // An ended message stays live until its session entry is found and handed over.
235
230
  return `{"key":${live.key},"reset":${reset},"ended":${live.ended},"count":${content.length},"blocks":[${frame.blocks.join(",")}]}`;
236
231
  }
232
+ /** Refill the frame from live content blocks, reusing cursors by block identity. */
233
+ private appendBlocks(
234
+ frame: Frame,
235
+ state: LiveState,
236
+ content: readonly Block[],
237
+ limit: number,
238
+ ): void {
239
+ for (let index = 0; index < limit && !frame.full; index++) {
240
+ const block = content[index];
241
+ if (block === undefined) {
242
+ continue;
243
+ }
244
+ const item = this.cursor(index, block).next(index, frame, state);
245
+ if (item !== undefined) {
246
+ frame.blocks.push(item);
247
+ }
248
+ }
249
+ }
237
250
  private cursor(index: number, block: Block): BlockCursor {
238
251
  const existing = this.blocks.at(index);
239
252
  if (existing?.ref === block) {
@@ -62,11 +62,11 @@ export class ActorObservation {
62
62
  private readonly state: LiveState;
63
63
  private readonly viewers = new Set<Viewer>();
64
64
  private readonly greeting = new Set<net.Socket>();
65
- private server?: net.Server;
66
- private socketPath?: string;
65
+ private server?: net.Server | undefined;
66
+ private socketPath?: string | undefined;
67
67
  private inode?: number;
68
- private unsubscribe?: () => void;
69
- private timer?: NodeJS.Timeout;
68
+ private unsubscribe?: (() => void) | undefined;
69
+ private timer?: NodeJS.Timeout | undefined;
70
70
  private closed = false;
71
71
  /** The actor's run is in progress, including native work before and after the agent loop. */
72
72
  private running = false;
@@ -125,7 +125,7 @@ export class Card implements Component {
125
125
  ready = false;
126
126
  page = 0;
127
127
  pages = 1;
128
- private cache?: { width: number; lines: string[] };
128
+ private cache?: { width: number; lines: string[] } | undefined;
129
129
  private arguments?: unknown;
130
130
  private phase = "";
131
131
  constructor(id: string) {
@@ -168,7 +168,9 @@ export class Card implements Component {
168
168
  this.tool.updateResult({
169
169
  content: blocks(message.content)
170
170
  .filter((part) => part.type === "text")
171
- .map((part) => ({ type: "text", text: part.text })),
171
+ .map((part) =>
172
+ part.text === undefined ? { type: "text" } : { type: "text", text: part.text },
173
+ ),
172
174
  isError: message.isError === true,
173
175
  });
174
176
  this.tool.setExpanded(this.expanded);
@@ -221,7 +223,9 @@ export class Card implements Component {
221
223
  {
222
224
  content: blocks(result.content)
223
225
  .filter((part) => part.type === "text")
224
- .map((part) => ({ type: "text", text: part.text })),
226
+ .map((part) =>
227
+ part.text === undefined ? { type: "text" } : { type: "text", text: part.text },
228
+ ),
225
229
  isError,
226
230
  },
227
231
  partial,
@@ -19,7 +19,7 @@ export interface ObserverRow {
19
19
  status: string;
20
20
  live: boolean;
21
21
  createdAt: number;
22
- activity?: string;
22
+ activity?: string | undefined;
23
23
  }
24
24
 
25
25
  export class Details extends ScrollView {
@@ -149,7 +149,7 @@ export function appendHeader(
149
149
  model: string;
150
150
  thinking: string;
151
151
  activity: string;
152
- details?: Details;
152
+ details?: Details | undefined;
153
153
  toggle: () => void;
154
154
  },
155
155
  ): void {
@@ -6,7 +6,7 @@ import { agentObservationSocketPath } from "../pi-intercom/broker/paths.ts";
6
6
  import { createMessageReader, writeFrame } from "../pi-intercom/broker/framing.ts";
7
7
  import type { ObservationIdentity } from "./observation.ts";
8
8
  import type { Launch } from "./contracts.ts";
9
- export type Watch = { launch: Launch; identity?: ObservationIdentity };
9
+ export type Watch = { launch: Launch; identity?: ObservationIdentity | undefined };
10
10
  import { Assert, Type } from "./typebox.ts";
11
11
 
12
12
  const activitySchema = Type.Object({
@@ -86,8 +86,8 @@ export interface ViewedTool {
86
86
  }
87
87
 
88
88
  export class ObservationChannel {
89
- current?: ObservationClient;
90
- private pending?: ObservationClient;
89
+ current?: ObservationClient | undefined;
90
+ private pending?: ObservationClient | undefined;
91
91
  private readonly changed: () => void;
92
92
  constructor(changed: () => void) {
93
93
  this.changed = changed;
@@ -134,7 +134,7 @@ export class ObservationClient {
134
134
  closed = false;
135
135
  revision = 0;
136
136
  leaf?: string | null;
137
- message?: { key: number; ended: boolean; blocks: ViewedBlock[] };
137
+ message?: { key: number; ended: boolean; blocks: ViewedBlock[] } | undefined;
138
138
  readonly tools = new Map<string, ViewedTool>();
139
139
  private readonly handovers: Array<Static<typeof settlementSchema>> = [];
140
140
  private seq = -1;
@@ -290,7 +290,7 @@ export class ObservationClient {
290
290
  }
291
291
  /** Retire live state only after its exact journal entries have been indexed. */
292
292
  settle(has: (entry: string) => boolean): boolean {
293
- while (this.handovers.length > 0 && has(this.handovers[0][1])) {
293
+ while (this.handovers.length > 0 && has(this.handovers[0]?.[1] ?? "")) {
294
294
  const item = this.handovers.shift();
295
295
  if (item) {
296
296
  this.retire(item[2], item[3]);
@@ -10,7 +10,7 @@ export class Reading {
10
10
  following = true;
11
11
  pending = false;
12
12
  width = 0;
13
- destination?: EntryAnchor;
13
+ destination?: EntryAnchor | undefined;
14
14
  private readonly changed: (kind: "history" | "content") => void;
15
15
  constructor(
16
16
  watch: Watch,
@@ -45,7 +45,7 @@ interface ScreenOptions {
45
45
  export class ObserverScreen extends VStack {
46
46
  private selected: string;
47
47
  private readonly list = new ChildList((id) => this.open(id));
48
- private reading?: Reading;
48
+ private reading?: Reading | undefined;
49
49
  private readonly connection = new ObservationChannel(() => this.schedule());
50
50
  private get client(): ObservationClient | undefined {
51
51
  return this.connection.current;
@@ -53,10 +53,10 @@ export class ObserverScreen extends VStack {
53
53
  private observed = { revision: -1, status: "", signature: "" };
54
54
  private readonly saved = new Map<string, Reading>();
55
55
  private focus: "list" | "conversation" = "list";
56
- private details?: Details;
56
+ private details?: Details | undefined;
57
57
  private failure = "";
58
- private timer?: NodeJS.Timeout;
59
- private indexer?: NodeJS.Immediate;
58
+ private timer?: NodeJS.Timeout | undefined;
59
+ private indexer?: NodeJS.Immediate | undefined;
60
60
  private lastFlush = -Infinity;
61
61
  private closed = false;
62
62
  private frame?: LayoutFrame;
@@ -37,11 +37,11 @@ export class ObserverTranscript extends Container {
37
37
  private readonly cards = new Map<string, Card>();
38
38
  private readonly pages = new Map<string, PageRequest>();
39
39
  private readonly expanded = new Set<string>();
40
- private before?: string;
40
+ private before?: string | undefined;
41
41
  private start = 0;
42
42
  private stop = 0;
43
43
  private entries: NativeRecord[] = [];
44
- private selected?: string;
44
+ private selected?: string | undefined;
45
45
  height = 0;
46
46
  private mounted: Card[] = [];
47
47
  waiting = false;
@@ -50,8 +50,8 @@ export class ObserverTranscript extends Container {
50
50
  private readonly changed: (event: TranscriptChange) => void;
51
51
  private readonly loaded: () => void;
52
52
  private paused = false;
53
- private hydration?: Promise<void>;
54
- private active?: Hydration;
53
+ private hydration?: Promise<void> | undefined;
54
+ private active?: Hydration | undefined;
55
55
  private readonly jobs = new Map<string, Hydration>();
56
56
  private readonly pending = new Map<string, PageRequest>();
57
57
  version = 0;
@@ -24,20 +24,20 @@ type Commit = (live: ReadonlyMap<string, boolean>) => void;
24
24
  type WidgetSnapshot = { status: string; rows: readonly ObserverRow[]; more: number; error: string };
25
25
 
26
26
  class Observer {
27
- private ctx?: ExtensionContext;
27
+ private ctx?: ExtensionContext | undefined;
28
28
  private owner = "";
29
29
  private base = "";
30
- private timer?: NodeJS.Timeout;
31
- private scanning?: Promise<void>;
30
+ private timer?: NodeJS.Timeout | undefined;
31
+ private scanning?: Promise<void> | undefined;
32
32
  private rescan = false;
33
- private close?: () => void;
34
- private refresh?: () => void;
33
+ private close?: (() => void) | undefined;
34
+ private refresh?: (() => void) | undefined;
35
35
  private readonly rows = new Map<string, ObserverRow>();
36
36
  private readonly peers = new Map<string, { token: string; client: ObservationClient }>();
37
- private watcher?: fs.FSWatcher;
37
+ private watcher?: fs.FSWatcher | undefined;
38
38
  private widgetVisible = false;
39
39
  private widgetSignature = "";
40
- private widgetRender?: (snapshot: WidgetSnapshot) => void;
40
+ private widgetRender?: ((snapshot: WidgetSnapshot) => void) | undefined;
41
41
  private readonly launches = new Map<string, Launch>();
42
42
  private readonly finished = new Set<string>();
43
43
  private pending: string[] = [];
@@ -209,7 +209,12 @@ class Observer {
209
209
  if (this.ctx !== ctx) {
210
210
  return;
211
211
  }
212
- const live = new Map(claims.map(({ root }, index) => [root, verdicts[index]]));
212
+ const live = new Map(
213
+ claims.flatMap(({ root }, index) => {
214
+ const verdict = verdicts[index];
215
+ return verdict === undefined ? [] : ([[root, verdict]] as const);
216
+ }),
217
+ );
213
218
  for (const commit of commits) {
214
219
  commit(live);
215
220
  }
@@ -162,7 +162,7 @@ export class Parking {
162
162
  private restored?: string;
163
163
  private since = 0;
164
164
  private leaf?: string | null;
165
- private shell?: string;
165
+ private shell?: string | undefined;
166
166
  private readonly base: string;
167
167
  private readonly launch: Launch;
168
168
  private readonly token: string;
@@ -76,7 +76,7 @@ export async function observeAlive(identities: readonly ClaimedProcess[]): Promi
76
76
  * may adopt the child meanwhile.
77
77
  */
78
78
  export class ParentCheck {
79
- private current?: object;
79
+ private current?: object | undefined;
80
80
  /**
81
81
  * Reads the attachment synchronously, so an unreadable one still fails the caller at once.
82
82
  * Missing or released attachments detach immediately; a check in flight makes this a no-op.
@@ -97,7 +97,7 @@ export class ParentCheck {
97
97
  const [live] = await observeAlive([attachment]);
98
98
  if (
99
99
  this.current === check &&
100
- !live &&
100
+ live !== true &&
101
101
  read(file, attachmentSchema)?.generation === attachment.generation
102
102
  ) {
103
103
  detached();
@@ -123,10 +123,11 @@ function replaced(identity: ClaimedProcess): boolean {
123
123
  }
124
124
  export function ownIdentity(pid = process.pid): Omit<ProcessIdentity, "token"> {
125
125
  // An ordinary main or managed launcher need not be a group leader. Ambiguous live PIDs fail closed.
126
+ const clock = readProcessClock();
126
127
  return {
127
128
  pid,
128
129
  identity: readChildProcessIdentity(pid) ?? "unverified",
129
- clock: readProcessClock(),
130
+ ...(clock !== undefined ? { clock } : {}),
130
131
  };
131
132
  }
132
133
  export function actor(base: string, launch: Launch): OwnedProcess | undefined {
@@ -218,11 +219,12 @@ async function spawnActor(
218
219
  trySignalChildTree(child, "SIGKILL");
219
220
  });
220
221
  child.unref();
222
+ const clock = readProcessClock();
221
223
  write(path.join(dir, "actor.lock", "process.json"), {
222
224
  pid: child.pid,
223
225
  identity: readChildProcessIdentity(child.pid) ?? "unverified",
224
226
  token,
225
- clock: readProcessClock(),
227
+ ...(clock !== undefined ? { clock } : {}),
226
228
  });
227
229
  }
228
230
  async function awaitStopping(
@@ -43,8 +43,16 @@ function completionStatus(
43
43
  }
44
44
  return "completed";
45
45
  }
46
+ /** The recorded failure: the launch error, the model's error, or the missing terminal message. */
47
+ const failureReason = (
48
+ error: string | undefined,
49
+ message: AssistantMessage | undefined,
50
+ ): string | undefined =>
51
+ error ??
52
+ message?.errorMessage ??
53
+ (message ? undefined : "Native run produced no terminal assistant message.");
46
54
  class Actor {
47
- private active?: Promise<void>;
55
+ private active?: Promise<void> | undefined;
48
56
  private stopping = false;
49
57
  private stopDeadline?: NodeJS.Timeout;
50
58
  private nextParentCheck = 0;
@@ -54,7 +62,7 @@ class Actor {
54
62
  private readonly wake = new Wake();
55
63
  // A deferred run waits for an idle session, which has no command-file event.
56
64
  private retry = false;
57
- private current?: Launch;
65
+ private current?: Launch | undefined;
58
66
  private readonly errors: string[] = [];
59
67
  private readonly started = new Set<string>();
60
68
  // Actor-local validation metadata only; never retain command prompts or receipt authority.
@@ -96,17 +104,17 @@ class Actor {
96
104
  const message = terminal?.message;
97
105
  const accounting = this.usage(launch, baseline, terminal?.id);
98
106
  const output = this.output(message);
107
+ const failure = failureReason(error, message);
99
108
  const result: Result = {
100
109
  id: launch.id,
101
110
  owner: launch.owner,
102
111
  status: completionStatus(this.stopping, message, error),
103
112
  endedAt: Date.now(),
104
- terminalEntryId: accounting.accountingError === undefined ? terminal?.id : undefined,
113
+ ...(accounting.accountingError === undefined && terminal?.id !== undefined
114
+ ? { terminalEntryId: terminal.id }
115
+ : {}),
105
116
  ...accounting,
106
- error:
107
- error ??
108
- message?.errorMessage ??
109
- (message ? undefined : "Native run produced no terminal assistant message."),
117
+ ...(failure !== undefined ? { error: failure } : {}),
110
118
  };
111
119
  fs.writeFileSync(path.join(runDirectory(this.base, launch.id), "output.txt"), output, {
112
120
  mode: 0o600,