@llblab/pi-actors 0.22.5 → 0.24.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.
Files changed (180) hide show
  1. package/AGENTS.md +125 -59
  2. package/BACKLOG.md +88 -403
  3. package/CHANGELOG.md +39 -0
  4. package/README.md +14 -1
  5. package/dist/fixtures/protocol/actor-message-branch.json +13 -0
  6. package/dist/fixtures/protocol/artifact-manifest.json +9 -0
  7. package/dist/fixtures/protocol/mailbox-contract.json +15 -0
  8. package/dist/fixtures/protocol/recipe-summary.json +16 -0
  9. package/dist/fixtures/protocol/room-message.json +11 -0
  10. package/dist/fixtures/protocol/room-roster.json +11 -0
  11. package/dist/fixtures/protocol/run-inbox-message.json +9 -0
  12. package/dist/fixtures/protocol/run-outbox-event.json +9 -0
  13. package/dist/fixtures/protocol/run-state.json +10 -0
  14. package/dist/index.js +18 -2
  15. package/dist/lib/actor-inspector-tui.d.ts +4 -0
  16. package/dist/lib/actor-inspector-tui.js +58 -44
  17. package/dist/lib/actor-rooms.d.ts +16 -0
  18. package/dist/lib/actor-rooms.js +123 -28
  19. package/dist/lib/actor-worker.d.ts +13 -0
  20. package/dist/lib/actor-worker.js +86 -0
  21. package/dist/lib/async-runner.d.ts +5 -0
  22. package/dist/lib/async-runner.js +134 -0
  23. package/dist/lib/async-runs.d.ts +34 -2
  24. package/dist/lib/async-runs.js +224 -50
  25. package/dist/lib/build-dist.d.ts +5 -0
  26. package/dist/lib/build-dist.js +24 -0
  27. package/dist/lib/command-templates.js +5 -5
  28. package/dist/lib/conformance.d.ts +12 -0
  29. package/dist/lib/conformance.js +28 -0
  30. package/dist/lib/coordinator.d.ts +5 -0
  31. package/dist/lib/coordinator.js +557 -0
  32. package/dist/lib/limits.d.ts +11 -0
  33. package/dist/lib/limits.js +11 -0
  34. package/dist/lib/locker.d.ts +5 -0
  35. package/dist/lib/locker.js +310 -0
  36. package/dist/lib/mailbox-loop.d.ts +41 -0
  37. package/dist/lib/mailbox-loop.js +62 -0
  38. package/dist/lib/observability.d.ts +2 -2
  39. package/dist/lib/observability.js +57 -57
  40. package/dist/lib/output.js +4 -5
  41. package/dist/lib/prompts.d.ts +1 -1
  42. package/dist/lib/prompts.js +1 -1
  43. package/dist/lib/recipe-discovery.js +1 -0
  44. package/dist/lib/recipe-references.d.ts +11 -2
  45. package/dist/lib/recipe-references.js +29 -4
  46. package/dist/lib/recipe-usage.d.ts +2 -1
  47. package/dist/lib/recipe-usage.js +15 -3
  48. package/dist/lib/recipe-utils.d.ts +5 -0
  49. package/dist/lib/recipe-utils.js +385 -0
  50. package/dist/lib/runtime-notifier.js +3 -7
  51. package/dist/lib/runtime.js +12 -2
  52. package/dist/lib/state-readers.d.ts +21 -0
  53. package/dist/lib/state-readers.js +74 -0
  54. package/dist/lib/tools.js +198 -39
  55. package/dist/lib/validate-recipe.d.ts +6 -0
  56. package/dist/lib/validate-recipe.js +104 -0
  57. package/dist/recipes/actor-worker.json +35 -0
  58. package/dist/recipes/coordinator-locker.json +45 -0
  59. package/dist/recipes/lens-swarm.json +66 -0
  60. package/dist/recipes/locker.json +45 -0
  61. package/dist/recipes/music-player.json +38 -0
  62. package/dist/recipes/pipeline-architect-coordinator.json +95 -0
  63. package/dist/recipes/pipeline-artifact-bundle.json +100 -0
  64. package/dist/recipes/pipeline-artifact-report.json +58 -0
  65. package/dist/recipes/pipeline-artifact-write.json +72 -0
  66. package/dist/recipes/pipeline-async-run-ops.json +70 -0
  67. package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
  68. package/dist/recipes/pipeline-development-tasking.json +81 -0
  69. package/dist/recipes/pipeline-docs-maintenance.json +80 -0
  70. package/dist/recipes/pipeline-media-library.json +59 -0
  71. package/dist/recipes/pipeline-quorum-review.json +79 -0
  72. package/dist/recipes/pipeline-release-readiness.json +110 -0
  73. package/dist/recipes/pipeline-release-summary.json +88 -0
  74. package/dist/recipes/pipeline-repo-health.json +89 -0
  75. package/dist/recipes/pipeline-research-synthesis.json +94 -0
  76. package/dist/recipes/pipeline-review-readiness.json +54 -0
  77. package/dist/recipes/pipeline-room-swarm.json +50 -0
  78. package/dist/recipes/subagent-artifact.json +32 -0
  79. package/dist/recipes/subagent-checkpoint.json +33 -0
  80. package/dist/recipes/subagent-conflict-report.json +32 -0
  81. package/dist/recipes/subagent-contradiction-map.json +33 -0
  82. package/dist/recipes/subagent-critic.json +35 -0
  83. package/dist/recipes/subagent-evidence-map.json +33 -0
  84. package/dist/recipes/subagent-followup.json +33 -0
  85. package/dist/recipes/subagent-judge.json +33 -0
  86. package/dist/recipes/subagent-merge.json +33 -0
  87. package/dist/recipes/subagent-message.json +34 -0
  88. package/dist/recipes/subagent-normalize.json +31 -0
  89. package/dist/recipes/subagent-plan.json +33 -0
  90. package/dist/recipes/subagent-prompt.json +28 -0
  91. package/dist/recipes/subagent-quorum.json +43 -0
  92. package/dist/recipes/subagent-review-coordinator.json +114 -0
  93. package/dist/recipes/subagent-review.json +37 -0
  94. package/dist/recipes/subagent-task-card.json +35 -0
  95. package/dist/recipes/subagent-tools.json +27 -0
  96. package/dist/recipes/subagent-verify.json +34 -0
  97. package/dist/recipes/subagents-prompts.json +51 -0
  98. package/dist/recipes/utility-actor-message.json +23 -0
  99. package/dist/recipes/utility-artifact-manifest.json +16 -0
  100. package/dist/recipes/utility-artifact-write.json +16 -0
  101. package/dist/recipes/utility-changelog-head.json +11 -0
  102. package/dist/recipes/utility-changelog-section.json +13 -0
  103. package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
  104. package/dist/recipes/utility-git-log.json +11 -0
  105. package/dist/recipes/utility-git-status.json +9 -0
  106. package/dist/recipes/utility-jsonl-tail.json +10 -0
  107. package/dist/recipes/utility-markdown-index.json +14 -0
  108. package/dist/recipes/utility-package-summary.json +11 -0
  109. package/dist/recipes/utility-playlist-build.json +17 -0
  110. package/dist/recipes/utility-playlist-scan.json +11 -0
  111. package/dist/recipes/utility-run-ops-snapshot.json +17 -0
  112. package/dist/recipes/utility-run-state-files.json +13 -0
  113. package/dist/recipes/utility-run-summary.json +11 -0
  114. package/dist/recipes/utility-skill-summary.json +13 -0
  115. package/dist/recipes/utility-validate-recipe.json +13 -0
  116. package/dist/recipes/utility-validation-wrapper.json +13 -0
  117. package/dist/scripts/actor-worker.mjs +31 -0
  118. package/dist/scripts/async-runner.mjs +31 -0
  119. package/dist/scripts/build-dist.mjs +25 -0
  120. package/dist/scripts/conformance.mjs +33 -0
  121. package/dist/scripts/coordinator.mjs +31 -0
  122. package/dist/scripts/locker.mjs +33 -0
  123. package/dist/scripts/music-player.mjs +964 -0
  124. package/dist/scripts/recipe-utils.mjs +31 -0
  125. package/dist/scripts/validate-recipe.mjs +34 -0
  126. package/dist/skills/actors/SKILL.md +375 -0
  127. package/dist/skills/swarm/SKILL.md +467 -0
  128. package/dist/skills/swarm/references/development-swarm.md +596 -0
  129. package/docs/actor-messages.md +2 -2
  130. package/docs/async-runs.md +13 -1
  131. package/docs/template-recipes.md +2 -2
  132. package/docs/tool-registry.md +0 -1
  133. package/fixtures/protocol/actor-message-branch.json +13 -0
  134. package/fixtures/protocol/artifact-manifest.json +9 -0
  135. package/fixtures/protocol/mailbox-contract.json +15 -0
  136. package/fixtures/protocol/recipe-summary.json +16 -0
  137. package/fixtures/protocol/room-message.json +11 -0
  138. package/fixtures/protocol/room-roster.json +11 -0
  139. package/fixtures/protocol/run-inbox-message.json +9 -0
  140. package/fixtures/protocol/run-outbox-event.json +9 -0
  141. package/fixtures/protocol/run-state.json +10 -0
  142. package/index.ts +21 -0
  143. package/lib/actor-inspector-tui.ts +138 -59
  144. package/lib/actor-rooms.ts +241 -60
  145. package/lib/actor-worker.ts +118 -0
  146. package/lib/async-runner.ts +173 -0
  147. package/lib/async-runs.ts +302 -53
  148. package/lib/build-dist.ts +30 -0
  149. package/lib/command-templates.ts +5 -5
  150. package/lib/conformance.ts +46 -0
  151. package/lib/coordinator.ts +649 -0
  152. package/lib/limits.ts +12 -0
  153. package/lib/locker.ts +340 -0
  154. package/lib/mailbox-loop.ts +148 -0
  155. package/lib/observability.ts +34 -23
  156. package/lib/output.ts +4 -6
  157. package/lib/prompts.ts +1 -1
  158. package/lib/recipe-discovery.ts +1 -0
  159. package/lib/recipe-references.ts +57 -6
  160. package/lib/recipe-usage.ts +31 -4
  161. package/lib/recipe-utils.ts +486 -0
  162. package/lib/runtime-notifier.ts +4 -6
  163. package/lib/runtime.ts +31 -7
  164. package/lib/state-readers.ts +93 -0
  165. package/lib/tools.ts +297 -58
  166. package/lib/validate-recipe.ts +110 -0
  167. package/package.json +11 -2
  168. package/recipes/actor-worker.json +35 -0
  169. package/recipes/pipeline-quorum-review.json +12 -7
  170. package/scripts/actor-worker.mjs +31 -0
  171. package/scripts/async-runner.mjs +11 -195
  172. package/scripts/build-dist.mjs +25 -0
  173. package/scripts/conformance.mjs +33 -0
  174. package/scripts/coordinator.mjs +19 -616
  175. package/scripts/locker.mjs +23 -322
  176. package/scripts/music-player.mjs +21 -2
  177. package/scripts/recipe-utils.mjs +20 -467
  178. package/scripts/validate-recipe.mjs +23 -113
  179. package/skills/actors/SKILL.md +7 -4
  180. package/skills/swarm/SKILL.md +3 -1
@@ -0,0 +1,15 @@
1
+ {
2
+ "accepts": [
3
+ "task.assign",
4
+ {
5
+ "type": "control.stop",
6
+ "description": "Request graceful worker shutdown"
7
+ }
8
+ ],
9
+ "emits": [
10
+ "task.claim",
11
+ "task.result",
12
+ "awaiting_assignment",
13
+ "actor.leave"
14
+ ]
15
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "actor-worker",
3
+ "path": "recipes/actor-worker.json",
4
+ "location": "packaged",
5
+ "active": true,
6
+ "args": [
7
+ "run",
8
+ "branch",
9
+ "poll_ms",
10
+ "state_dir"
11
+ ],
12
+ "mailbox": {
13
+ "kind": "branch",
14
+ "accepts": ["task.assign", "control.stop"]
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "to": "room:demo",
3
+ "from": "branch:demo/worker",
4
+ "type": "task.result",
5
+ "summary": "Worker completed task",
6
+ "body": {
7
+ "id": "inbox-001",
8
+ "result": "ok"
9
+ },
10
+ "received_at": "2026-05-26T00:00:00.000Z"
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "room": "main",
3
+ "members": {
4
+ "branch:demo/worker": {
5
+ "display": "worker",
6
+ "role": "worker",
7
+ "status": "present",
8
+ "last_seen": "2026-05-26T00:00:00.000Z"
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "inbox-001",
3
+ "status": "queued",
4
+ "queued_at": "2026-05-26T00:00:00.000Z",
5
+ "to": "run:demo",
6
+ "from": "coordinator",
7
+ "type": "control.continue",
8
+ "body": "continue"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "event-001",
3
+ "type": "task.result",
4
+ "summary": "Worker completed task",
5
+ "body": {
6
+ "result": "ok"
7
+ },
8
+ "created_at": "2026-05-26T00:00:00.000Z"
9
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "run": "demo",
3
+ "state_dir": "{state_dir}",
4
+ "status": "running",
5
+ "template": "echo demo",
6
+ "values": {
7
+ "run_id": "demo",
8
+ "state_dir": "{state_dir}"
9
+ }
10
+ }
package/index.ts CHANGED
@@ -48,6 +48,7 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
48
48
  const runDirWatchers = new Map<string, FSWatcher>();
49
49
  const observedRuns = new Map<string, Observability.RunObservedStatus>();
50
50
  const observedRunEventLines = new Map<string, number>();
51
+ const observedRunOutboxEventIds = new Map<string, Set<string>>();
51
52
  const retirementAttempts = new Set<string>();
52
53
  let runStatusFrame = 0;
53
54
  let communicationWidgetVisible = false;
@@ -60,6 +61,7 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
60
61
  let actorInspectorUnreadOnly = false;
61
62
  let actorInspectorRoomLimitPerRun = 12;
62
63
  let selectedInspectorSequence: number | undefined;
64
+ const actorInspectorReadKeys = new Set<string>();
63
65
  let recipeWatcherFailureNotified = false;
64
66
  const getRunOwnerId = (ctx: ExtensionContext): string =>
65
67
  ctx.sessionManager.getSessionId();
@@ -108,6 +110,7 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
108
110
  branch: actorInspectorBranch,
109
111
  mention: actorInspectorMention,
110
112
  ownerId,
113
+ readKeys: actorInspectorReadKeys,
111
114
  roomLimitPerRun: actorInspectorRoomLimitPerRun,
112
115
  unreadOnly: actorInspectorUnreadOnly,
113
116
  },
@@ -150,6 +153,7 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
150
153
  const outboxEvents = Observability.detectRunOutboxEvents(
151
154
  observedRunEventLines,
152
155
  summary,
156
+ observedRunOutboxEventIds,
153
157
  );
154
158
  if (!notify) return;
155
159
  retireCandidateRuns(ctx, summary);
@@ -175,6 +179,7 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
175
179
  observedRunEventLines,
176
180
  summary,
177
181
  transitions.map((transition) => transition.stateDir ?? transition.run),
182
+ observedRunOutboxEventIds,
178
183
  );
179
184
  for (const event of outboxEvents) {
180
185
  if (!Observability.shouldNotifyRunOutboxEvent(event)) continue;
@@ -414,6 +419,22 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
414
419
  ctx.ui.notify("Usage: /actors-inspect <number>", "warning");
415
420
  return;
416
421
  }
422
+ const previews = ActorInspectorTui.readActorInspectorPreviews(
423
+ RUN_STATE_ROOT,
424
+ actorInspectorRows,
425
+ {
426
+ channels: actorInspectorChannels,
427
+ currentRunOnly: true,
428
+ branch: actorInspectorBranch,
429
+ mention: actorInspectorMention,
430
+ ownerId: getRunOwnerId(ctx),
431
+ readKeys: actorInspectorReadKeys,
432
+ roomLimitPerRun: actorInspectorRoomLimitPerRun,
433
+ unreadOnly: actorInspectorUnreadOnly,
434
+ },
435
+ );
436
+ const preview = previews.find((item) => item.sequence === sequence);
437
+ if (preview) actorInspectorReadKeys.add(ActorInspectorTui.inspectorPreviewReadKey(preview));
417
438
  selectedInspectorSequence = sequence;
418
439
  communicationWidgetVisible = true;
419
440
  updateRunUi(ctx);
@@ -9,16 +9,20 @@ import * as path from "node:path";
9
9
  import { visibleWidth } from "@earendil-works/pi-tui";
10
10
 
11
11
  import type { ActorMessage } from "./actor-messages.ts";
12
+ import * as Limits from "./limits.ts";
12
13
  import * as Paths from "./paths.ts";
14
+ import { readJsonFileResilient, readJsonlFileResilient } from "./state-readers.ts";
13
15
 
14
16
  export interface ActorInspectorPreview {
15
17
  body_preview?: string;
16
18
  branch?: string;
17
19
  channel: "broadcast" | "direct" | "room";
20
+ event_id?: string;
18
21
  from?: string;
19
22
  from_display?: string;
20
23
  inbox_status?: string;
21
24
  message_id?: string;
25
+ needs_response?: boolean;
22
26
  run: string;
23
27
  sequence?: number;
24
28
  summary?: string;
@@ -57,6 +61,7 @@ export interface ActorInspectorPreviewReadOptions {
57
61
  currentRunOnly?: boolean;
58
62
  channels?: ActorInspectorPreview["channel"][];
59
63
  mention?: string;
64
+ readKeys?: Iterable<string>;
60
65
  roomLimitPerRun?: number;
61
66
  unreadOnly?: boolean;
62
67
  }
@@ -68,25 +73,13 @@ function asRecord(value: unknown): Record<string, unknown> {
68
73
  }
69
74
 
70
75
  function readJsonLines(file: string): Record<string, unknown>[] {
71
- try {
72
- return fs
73
- .readFileSync(file, "utf8")
74
- .split("\n")
75
- .filter(Boolean)
76
- .flatMap((line) => {
77
- try {
78
- return [JSON.parse(line) as Record<string, unknown>];
79
- } catch {
80
- return [];
81
- }
82
- });
83
- } catch (error) {
84
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
85
- return [];
86
- }
76
+ return readJsonlFileResilient<Record<string, unknown>>(file).records;
87
77
  }
88
78
 
89
- function previewValue(value: unknown, maxLength = 320): string | undefined {
79
+ function previewValue(
80
+ value: unknown,
81
+ maxLength = Limits.INSPECTOR_BODY_PREVIEW_CHARS,
82
+ ): string | undefined {
90
83
  if (value === undefined) return undefined;
91
84
  const text = typeof value === "string" ? value : JSON.stringify(value);
92
85
  const compact = text.replaceAll(/\s+/g, " ").trim();
@@ -118,6 +111,11 @@ function previewFromMessage(
118
111
  const summary =
119
112
  typeof message.summary === "string" ? message.summary : undefined;
120
113
  const body = asRecord(message.body);
114
+ const metadata = asRecord(message.metadata);
115
+ const eventId =
116
+ typeof message.id === "string" && message.id.trim()
117
+ ? message.id.trim()
118
+ : undefined;
121
119
  const display = from
122
120
  ? typeof body.display === "string" && body.display.trim()
123
121
  ? body.display.trim()
@@ -128,8 +126,10 @@ function previewFromMessage(
128
126
  ? { body_preview: previewValue(message.body) }
129
127
  : {}),
130
128
  channel: channelFor({ to }),
129
+ ...(eventId ? { event_id: eventId } : {}),
131
130
  ...(from ? { from } : {}),
132
131
  ...(display ? { from_display: display } : {}),
132
+ ...(metadata.requires_response === true ? { needs_response: true } : {}),
133
133
  run,
134
134
  ...(summary ? { summary } : {}),
135
135
  timestamp,
@@ -142,21 +142,25 @@ function readRoomRosterRecords(
142
142
  stateDir: string,
143
143
  room: string,
144
144
  ): Record<string, Record<string, unknown>> {
145
- try {
146
- return JSON.parse(
147
- fs.readFileSync(path.join(stateDir, "rooms", room, "roster.json"), "utf8"),
148
- ) as Record<string, Record<string, unknown>>;
149
- } catch {
150
- return {};
151
- }
145
+ return readJsonFileResilient<Record<string, Record<string, unknown>>>(
146
+ path.join(stateDir, "rooms", room, "roster.json"),
147
+ {},
148
+ ).value;
152
149
  }
153
150
 
154
- function memberDisplay(_address: string, member: Record<string, unknown>): string | undefined {
155
- const display = typeof member.display === "string" ? member.display.trim() : "";
151
+ function memberDisplay(
152
+ _address: string,
153
+ member: Record<string, unknown>,
154
+ ): string | undefined {
155
+ const display =
156
+ typeof member.display === "string" ? member.display.trim() : "";
156
157
  return display || undefined;
157
158
  }
158
159
 
159
- function readRoomDisplayNames(stateDir: string, room: string): Record<string, string> {
160
+ function readRoomDisplayNames(
161
+ stateDir: string,
162
+ room: string,
163
+ ): Record<string, string> {
160
164
  const roster = readRoomRosterRecords(stateDir, room);
161
165
  return Object.fromEntries(
162
166
  Object.entries(roster).flatMap(([address, member]) => {
@@ -227,17 +231,28 @@ function readBranchInboxPreviews(
227
231
  const preview = previewFromMessage(
228
232
  run,
229
233
  message,
230
- String(message.queued_at ?? message.received_at ?? message.timestamp ?? ""),
234
+ String(
235
+ message.queued_at ??
236
+ message.received_at ??
237
+ message.timestamp ??
238
+ "",
239
+ ),
231
240
  );
232
241
  if (!preview) return undefined;
233
242
  return {
234
243
  ...preview,
235
244
  branch: entry.name,
236
- ...(typeof message.id === "string" ? { message_id: message.id } : {}),
237
- ...(typeof message.status === "string" ? { inbox_status: message.status } : {}),
245
+ ...(typeof message.id === "string"
246
+ ? { message_id: message.id }
247
+ : {}),
248
+ ...(typeof message.status === "string"
249
+ ? { inbox_status: message.status }
250
+ : {}),
238
251
  };
239
252
  })
240
- .filter((preview): preview is ActorInspectorPreview => Boolean(preview)),
253
+ .filter((preview): preview is ActorInspectorPreview =>
254
+ Boolean(preview),
255
+ ),
241
256
  );
242
257
  } catch (error) {
243
258
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
@@ -262,22 +277,40 @@ function readOutboxPreviews(
262
277
  }
263
278
 
264
279
  function getRunOwnerId(stateDir: string): string | undefined {
265
- try {
266
- const meta = JSON.parse(
267
- fs.readFileSync(path.join(stateDir, "run.json"), "utf8"),
268
- ) as Record<string, unknown>;
269
- return typeof meta.ownerId === "string" ? meta.ownerId : undefined;
270
- } catch {
271
- return undefined;
272
- }
280
+ const meta = readJsonFileResilient<Record<string, unknown> | undefined>(
281
+ path.join(stateDir, "run.json"),
282
+ undefined,
283
+ ).value;
284
+ return typeof meta?.ownerId === "string" ? meta.ownerId : undefined;
273
285
  }
274
286
 
275
287
  function matchesOwner(stateDir: string, ownerId: string | undefined): boolean {
276
288
  return ownerId === undefined || getRunOwnerId(stateDir) === ownerId;
277
289
  }
278
290
 
279
- function isUnreadPreview(preview: ActorInspectorPreview): boolean {
280
- return preview.inbox_status === "queued" || preview.inbox_status === undefined && preview.branch !== undefined;
291
+ export function inspectorPreviewReadKey(
292
+ preview: ActorInspectorPreview,
293
+ ): string {
294
+ return [
295
+ preview.channel,
296
+ preview.run,
297
+ preview.branch ?? "",
298
+ preview.event_id ?? preview.message_id ?? preview.timestamp,
299
+ preview.from ?? "",
300
+ preview.to,
301
+ preview.type,
302
+ ].join("|");
303
+ }
304
+
305
+ function isUnreadPreview(
306
+ preview: ActorInspectorPreview,
307
+ readKeys?: Set<string>,
308
+ ): boolean {
309
+ if (readKeys?.has(inspectorPreviewReadKey(preview))) return false;
310
+ return (
311
+ preview.inbox_status === "queued" ||
312
+ (preview.inbox_status === undefined && preview.branch !== undefined)
313
+ );
281
314
  }
282
315
 
283
316
  function matchesBranchFilter(
@@ -298,7 +331,8 @@ function matchesPreviewFilter(
298
331
  if (options.channels?.length && !options.channels.includes(preview.channel)) {
299
332
  return false;
300
333
  }
301
- if (options.unreadOnly && !isUnreadPreview(preview)) return false;
334
+ const readKeys = options.readKeys ? new Set(options.readKeys) : undefined;
335
+ if (options.unreadOnly && !isUnreadPreview(preview, readKeys)) return false;
302
336
  if (!matchesBranchFilter(preview, options.branch)) return false;
303
337
  const mention = options.mention?.trim().toLowerCase();
304
338
  if (!mention) return true;
@@ -421,6 +455,10 @@ function routeText(preview: ActorInspectorPreview): string {
421
455
  return `${actor} → ${actorName(preview.to)}`;
422
456
  }
423
457
 
458
+ function attentionMarker(preview: ActorInspectorPreview): string {
459
+ return preview.needs_response ? "! " : "";
460
+ }
461
+
424
462
  function style(
425
463
  styleFn: ((text: string) => string) | undefined,
426
464
  text: string,
@@ -435,7 +473,8 @@ function previewText(preview: ActorInspectorPreview): string {
435
473
  function propertyValue(value: unknown): string {
436
474
  if (value === undefined) return "";
437
475
  if (typeof value === "string") return value;
438
- if (typeof value === "number" || typeof value === "boolean") return String(value);
476
+ if (typeof value === "number" || typeof value === "boolean")
477
+ return String(value);
439
478
  return JSON.stringify(value);
440
479
  }
441
480
 
@@ -491,7 +530,7 @@ function renderCompactInspectorEntry(
491
530
  const contentWidth = Math.max(8, width - prefix.length - suffix.length);
492
531
  const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
493
532
  const sequencePrefix = `${sequence}${separator}`;
494
- const route = routeText(preview);
533
+ const route = `${attentionMarker(preview)}${routeText(preview)}`;
495
534
  const routePadding = " ".repeat(
496
535
  Math.max(0, routeWidth - displayWidth(route)),
497
536
  );
@@ -537,14 +576,19 @@ function renderInspectorEntry(
537
576
  const contentWidth = Math.max(8, width - prefix.length - suffix.length);
538
577
  const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
539
578
  const sequencePrefix = `${sequence}${separator}`;
540
- const route = routeText(preview);
579
+ const route = `${attentionMarker(preview)}${routeText(preview)}`;
541
580
  const type = preview.type;
542
581
  const summary = preview.summary?.trim() ?? "";
543
- const body = preview.body_preview?.trim() || (!summary ? previewText(preview) : "-");
582
+ const body =
583
+ preview.body_preview?.trim() || (!summary ? previewText(preview) : "-");
544
584
  const visibleRoute = boundedLine(route, routeWidth);
545
585
  const visibleType = boundedLine(type, typeWidth);
546
- const boundedRoutePadding = " ".repeat(Math.max(0, routeWidth - displayWidth(visibleRoute)));
547
- const boundedTypePadding = " ".repeat(Math.max(0, typeWidth - displayWidth(visibleType)));
586
+ const boundedRoutePadding = " ".repeat(
587
+ Math.max(0, routeWidth - displayWidth(visibleRoute)),
588
+ );
589
+ const boundedTypePadding = " ".repeat(
590
+ Math.max(0, typeWidth - displayWidth(visibleType)),
591
+ );
548
592
  const leadParts = [
549
593
  style(styles.muted, sequencePrefix),
550
594
  style(styles.target, visibleRoute),
@@ -557,11 +601,20 @@ function renderInspectorEntry(
557
601
  let lead = `${sequencePrefix}${visibleRoute}${boundedRoutePadding}${separator}${visibleType}${boundedTypePadding}${separator}`;
558
602
  if (summary) {
559
603
  const visibleSummary = boundedLine(summary, summaryWidth);
560
- const summaryPadding = " ".repeat(Math.max(0, summaryWidth - displayWidth(visibleSummary)));
604
+ const summaryPadding = " ".repeat(
605
+ Math.max(0, summaryWidth - displayWidth(visibleSummary)),
606
+ );
561
607
  lead += `${visibleSummary}${summaryPadding}${separator}`;
562
- leadParts.push(style(styles.preview, visibleSummary), summaryPadding, separator);
608
+ leadParts.push(
609
+ style(styles.preview, visibleSummary),
610
+ summaryPadding,
611
+ separator,
612
+ );
563
613
  }
564
- const visibleBody = boundedLine(body, Math.max(0, contentWidth - displayWidth(lead)));
614
+ const visibleBody = boundedLine(
615
+ body,
616
+ Math.max(0, contentWidth - displayWidth(lead)),
617
+ );
565
618
  const plain = `${lead}${visibleBody}`;
566
619
  const rendered = `${leadParts.join("")}${style(styles.preview, visibleBody)}`;
567
620
  const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}${suffix}`;
@@ -589,7 +642,10 @@ export function readActorInspectorRoster(
589
642
 
590
643
  function rosterRoleText(role: string | undefined): string | undefined {
591
644
  const roleLabel = role?.split(";")[0] ?? "";
592
- const cleaned = roleLabel.replaceAll(/\s*\([^)]*\)\s*$/g, "").trim().toLowerCase();
645
+ const cleaned = roleLabel
646
+ .replaceAll(/\s*\([^)]*\)\s*$/g, "")
647
+ .trim()
648
+ .toLowerCase();
593
649
  if (!cleaned || cleaned === "actor") return undefined;
594
650
  return cleaned.replaceAll(/\s+/g, "-");
595
651
  }
@@ -603,7 +659,12 @@ function rosterMemberText(member: ActorInspectorRosterMember): string {
603
659
 
604
660
  function isRosterMemberActive(member: ActorInspectorRosterMember): boolean {
605
661
  const status = member.status?.trim().toLowerCase();
606
- return !status || status === "present" || status === "active" || status === "running";
662
+ return (
663
+ !status ||
664
+ status === "present" ||
665
+ status === "active" ||
666
+ status === "running"
667
+ );
607
668
  }
608
669
 
609
670
  export function renderInspectorRosterLine(
@@ -633,12 +694,17 @@ export function renderInspectorRosterPanel(
633
694
  const flush = () => {
634
695
  const visible = boundedLine(plain, innerWidth);
635
696
  const line = visible === plain ? rendered : style(styles.muted, visible);
636
- lines.push(` ${line}${" ".repeat(Math.max(0, innerWidth - displayWidth(visible)))} `);
697
+ lines.push(
698
+ ` ${line}${" ".repeat(Math.max(0, innerWidth - displayWidth(visible)))} `,
699
+ );
637
700
  };
638
701
  for (const token of tokens) {
639
702
  const separator = plain === prefix ? "" : ", ";
640
703
  const nextPlain = `${plain}${separator}${token.text}`;
641
- const renderedToken = style(token.active ? styles.target : styles.muted, token.text);
704
+ const renderedToken = style(
705
+ token.active ? styles.target : styles.muted,
706
+ token.text,
707
+ );
642
708
  if (plain !== prefix && displayWidth(nextPlain) > innerWidth) {
643
709
  flush();
644
710
  plain = ` ${token.text}`;
@@ -664,12 +730,15 @@ export function renderInspectorItemView(
664
730
  const orderedKeys = [
665
731
  "channel",
666
732
  "run",
733
+ "event_id",
667
734
  "from",
668
735
  "from_display",
669
736
  "to",
670
737
  "type",
671
738
  "summary",
672
739
  "body_preview",
740
+ "message_id",
741
+ "needs_response",
673
742
  "timestamp",
674
743
  "stripe",
675
744
  ] as const;
@@ -678,7 +747,9 @@ export function renderInspectorItemView(
678
747
  .map((key) => [key, propertyValue(preview[key])] as const);
679
748
  const keyWidth = Math.max(1, ...entries.map(([key]) => displayWidth(key)));
680
749
  const sequenceText = String(preview.sequence ?? options.sequence);
681
- const sequencePadding = " ".repeat(Math.max(0, keyWidth - displayWidth(sequenceText)));
750
+ const sequencePadding = " ".repeat(
751
+ Math.max(0, keyWidth - displayWidth(sequenceText)),
752
+ );
682
753
  const headerSeparator = " ";
683
754
  const route = routeText(preview);
684
755
  const visibleRoute = boundedLine(
@@ -728,7 +799,9 @@ export function renderInspectorWidget(
728
799
  safeWidth - 1 - sequencePrefixWidth - fixedSeparatorsWidth,
729
800
  );
730
801
  const naturalRouteWidth = Math.max(
731
- ...visible.map(({ preview }) => displayWidth(routeText(preview))),
802
+ ...visible.map(({ preview }) =>
803
+ displayWidth(`${attentionMarker(preview)}${routeText(preview)}`),
804
+ ),
732
805
  );
733
806
  const naturalTypeWidth = Math.max(
734
807
  ...visible.map(({ preview }) => displayWidth(preview.type)),
@@ -741,13 +814,19 @@ export function renderInspectorWidget(
741
814
  naturalTypeWidth,
742
815
  Math.max(4, Math.floor(availableForColumns * 0.25)),
743
816
  );
744
- const messageWidth = Math.max(0, availableForColumns - routeWidth - typeWidth);
817
+ const messageWidth = Math.max(
818
+ 0,
819
+ availableForColumns - routeWidth - typeWidth,
820
+ );
745
821
  const summaryWidths = visible
746
822
  .map(({ preview }) => preview.summary?.trim())
747
823
  .filter((summary): summary is string => Boolean(summary))
748
824
  .map((summary) => displayWidth(summary));
749
825
  const summaryWidth = summaryWidths.length
750
- ? Math.min(Math.max(...summaryWidths), Math.max(1, Math.floor(messageWidth * 0.5)))
826
+ ? Math.min(
827
+ Math.max(...summaryWidths),
828
+ Math.max(1, Math.floor(messageWidth * 0.5)),
829
+ )
751
830
  : 0;
752
831
  for (const { preview, stripe } of visible) {
753
832
  lines.push(