@llblab/pi-actors 0.42.3 → 0.43.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 (252) hide show
  1. package/AGENTS.md +121 -175
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +113 -276
  4. package/dist/fixtures/protocol/control-endpoint.json +6 -0
  5. package/dist/fixtures/protocol/control-record.json +9 -0
  6. package/dist/fixtures/protocol/recipe-summary.json +4 -12
  7. package/dist/fixtures/protocol/trace-event.json +9 -0
  8. package/dist/lib/async-runs.d.ts +14 -38
  9. package/dist/lib/async-runs.js +158 -108
  10. package/dist/lib/control.d.ts +12 -0
  11. package/dist/lib/control.js +84 -0
  12. package/dist/lib/execution-sessions.d.ts +17 -0
  13. package/dist/lib/execution-sessions.js +85 -0
  14. package/dist/lib/file-state.d.ts +1 -0
  15. package/dist/lib/file-state.js +17 -5
  16. package/dist/lib/inspector-actions.d.ts +2 -2
  17. package/dist/lib/inspector-actions.js +2 -2
  18. package/dist/lib/inspector-command.js +3 -3
  19. package/dist/lib/inspector-overlay.d.ts +52 -70
  20. package/dist/lib/inspector-overlay.js +532 -905
  21. package/dist/lib/inspector.d.ts +3 -71
  22. package/dist/lib/inspector.js +19 -665
  23. package/dist/lib/limits.d.ts +4 -2
  24. package/dist/lib/limits.js +4 -2
  25. package/dist/lib/observability.d.ts +16 -16
  26. package/dist/lib/observability.js +43 -82
  27. package/dist/lib/prompts.d.ts +1 -1
  28. package/dist/lib/prompts.js +2 -2
  29. package/dist/lib/recipe-control.d.ts +7 -0
  30. package/dist/lib/recipe-control.js +39 -0
  31. package/dist/lib/recipes-discovery.js +2 -0
  32. package/dist/lib/recipes-references.d.ts +1 -14
  33. package/dist/lib/recipes-references.js +6 -21
  34. package/dist/lib/review-projection.js +1 -5
  35. package/dist/lib/run-ui-runtime.js +2 -2
  36. package/dist/lib/runs-control-delivery.d.ts +21 -0
  37. package/dist/lib/runs-control-delivery.js +127 -0
  38. package/dist/lib/runs-controls.d.ts +35 -0
  39. package/dist/lib/runs-controls.js +144 -0
  40. package/dist/lib/runs-retention.d.ts +7 -0
  41. package/dist/lib/runs-retention.js +27 -3
  42. package/dist/lib/runs-start.js +4 -2
  43. package/dist/lib/runs-status.js +11 -6
  44. package/dist/lib/runs-trace.d.ts +24 -0
  45. package/dist/lib/runs-trace.js +98 -0
  46. package/dist/lib/runtime-notifier.d.ts +1 -1
  47. package/dist/lib/runtime-notifier.js +1 -1
  48. package/dist/lib/tools-inspect.d.ts +3 -3
  49. package/dist/lib/tools-inspect.js +203 -708
  50. package/dist/lib/tools-local.js +2 -10
  51. package/dist/lib/tools-message.d.ts +7 -7
  52. package/dist/lib/tools-message.js +95 -396
  53. package/dist/lib/tools-response.d.ts +1 -4
  54. package/dist/lib/tools-response.js +5 -39
  55. package/dist/lib/tools-spawn.js +16 -28
  56. package/dist/lib/tools.js +1 -2
  57. package/dist/lib/trace-projection.d.ts +22 -0
  58. package/dist/lib/trace-projection.js +165 -0
  59. package/dist/recipes/draft-review.json +0 -10
  60. package/dist/recipes/lens-swarm.json +0 -14
  61. package/dist/recipes/music-player.json +10 -19
  62. package/dist/recipes/pipeline-architect-coordinator.json +0 -11
  63. package/dist/recipes/pipeline-artifact-bundle.json +1 -22
  64. package/dist/recipes/pipeline-artifact-report.json +1 -18
  65. package/dist/recipes/pipeline-artifact-write.json +1 -18
  66. package/dist/recipes/pipeline-async-run-ops.json +0 -12
  67. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
  68. package/dist/recipes/pipeline-development-tasking.json +0 -12
  69. package/dist/recipes/pipeline-docs-maintenance.json +0 -12
  70. package/dist/recipes/pipeline-media-library.json +0 -12
  71. package/dist/recipes/pipeline-quorum-review.json +0 -12
  72. package/dist/recipes/pipeline-release-readiness.json +0 -12
  73. package/dist/recipes/pipeline-release-summary.json +0 -12
  74. package/dist/recipes/pipeline-repo-health.json +0 -12
  75. package/dist/recipes/pipeline-research-synthesis.json +0 -11
  76. package/dist/recipes/pipeline-review-readiness.json +0 -12
  77. package/dist/recipes/resource-locker.json +27 -0
  78. package/dist/recipes/subagent-artifact.json +0 -9
  79. package/dist/recipes/subagent-checkpoint.json +0 -10
  80. package/dist/recipes/subagent-conflict-report.json +0 -11
  81. package/dist/recipes/subagent-contradiction-map.json +0 -11
  82. package/dist/recipes/subagent-critic.json +0 -11
  83. package/dist/recipes/subagent-evidence-map.json +0 -11
  84. package/dist/recipes/subagent-followup.json +0 -10
  85. package/dist/recipes/subagent-judge.json +0 -11
  86. package/dist/recipes/subagent-merge.json +0 -11
  87. package/dist/recipes/subagent-normalize.json +0 -11
  88. package/dist/recipes/subagent-plan.json +0 -11
  89. package/dist/recipes/subagent-preflight.json +0 -11
  90. package/dist/recipes/subagent-prompt.json +0 -10
  91. package/dist/recipes/subagent-quorum.json +0 -10
  92. package/dist/recipes/subagent-review-coordinator.json +0 -14
  93. package/dist/recipes/subagent-review.json +0 -11
  94. package/dist/recipes/subagent-task-card.json +0 -11
  95. package/dist/recipes/subagent-tools.json +0 -10
  96. package/dist/recipes/subagent-verify.json +0 -11
  97. package/dist/recipes/subagents-prompts.json +0 -10
  98. package/dist/recipes/tool-review.json +0 -10
  99. package/dist/scripts/async-runner.mjs +25 -25
  100. package/dist/scripts/conformance.mjs +4 -2
  101. package/dist/scripts/locker.mjs +200 -66
  102. package/dist/scripts/music-player.mjs +159 -150
  103. package/dist/scripts/recipe-utils.mjs +6 -96
  104. package/dist/scripts/release-gates.mjs +60 -0
  105. package/dist/scripts/validate-recipe.mjs +3 -53
  106. package/dist/skills/actors/SKILL.md +53 -266
  107. package/dist/skills/swarm/SKILL.md +11 -33
  108. package/docs/0.43-baseline.md +44 -0
  109. package/docs/README.md +3 -3
  110. package/docs/actor-inspector.md +26 -64
  111. package/docs/actors-deep-reference.md +92 -50
  112. package/docs/async-runs.md +81 -328
  113. package/docs/command-templates.md +2 -2
  114. package/docs/component-recipes.md +30 -133
  115. package/docs/recipe-library.md +57 -182
  116. package/docs/task-first-recipes.md +10 -12
  117. package/docs/template-recipes.md +76 -289
  118. package/docs/tool-registry.md +41 -161
  119. package/fixtures/protocol/control-endpoint.json +6 -0
  120. package/fixtures/protocol/control-record.json +9 -0
  121. package/fixtures/protocol/recipe-summary.json +4 -12
  122. package/fixtures/protocol/trace-event.json +9 -0
  123. package/lib/async-runs.ts +202 -201
  124. package/lib/control.ts +102 -0
  125. package/lib/execution-sessions.ts +111 -0
  126. package/lib/file-state.ts +17 -4
  127. package/lib/inspector-actions.ts +2 -2
  128. package/lib/inspector-command.ts +3 -3
  129. package/lib/inspector-overlay.ts +577 -1121
  130. package/lib/inspector.ts +46 -979
  131. package/lib/limits.ts +4 -2
  132. package/lib/observability.ts +60 -101
  133. package/lib/prompts.ts +2 -2
  134. package/lib/recipe-control.ts +45 -0
  135. package/lib/recipes-discovery.ts +2 -0
  136. package/lib/recipes-references.ts +9 -45
  137. package/lib/review-projection.ts +1 -5
  138. package/lib/run-ui-runtime.ts +2 -2
  139. package/lib/runs-control-delivery.ts +181 -0
  140. package/lib/runs-controls.ts +204 -0
  141. package/lib/runs-retention.ts +38 -3
  142. package/lib/runs-start.ts +4 -2
  143. package/lib/runs-status.ts +11 -6
  144. package/lib/runs-trace.ts +132 -0
  145. package/lib/runtime-notifier.ts +1 -1
  146. package/lib/tools-inspect.ts +240 -901
  147. package/lib/tools-local.ts +2 -12
  148. package/lib/tools-message.ts +112 -519
  149. package/lib/tools-response.ts +5 -52
  150. package/lib/tools-spawn.ts +16 -32
  151. package/lib/tools.ts +1 -2
  152. package/lib/trace-projection.ts +221 -0
  153. package/package.json +2 -1
  154. package/recipes/draft-review.json +0 -10
  155. package/recipes/lens-swarm.json +0 -14
  156. package/recipes/music-player.json +10 -19
  157. package/recipes/pipeline-architect-coordinator.json +0 -11
  158. package/recipes/pipeline-artifact-bundle.json +1 -22
  159. package/recipes/pipeline-artifact-report.json +1 -18
  160. package/recipes/pipeline-artifact-write.json +1 -18
  161. package/recipes/pipeline-async-run-ops.json +0 -12
  162. package/recipes/pipeline-checkpoint-continuation.json +0 -14
  163. package/recipes/pipeline-development-tasking.json +0 -12
  164. package/recipes/pipeline-docs-maintenance.json +0 -12
  165. package/recipes/pipeline-media-library.json +0 -12
  166. package/recipes/pipeline-quorum-review.json +0 -12
  167. package/recipes/pipeline-release-readiness.json +0 -12
  168. package/recipes/pipeline-release-summary.json +0 -12
  169. package/recipes/pipeline-repo-health.json +0 -12
  170. package/recipes/pipeline-research-synthesis.json +0 -11
  171. package/recipes/pipeline-review-readiness.json +0 -12
  172. package/recipes/resource-locker.json +27 -0
  173. package/recipes/subagent-artifact.json +0 -9
  174. package/recipes/subagent-checkpoint.json +0 -10
  175. package/recipes/subagent-conflict-report.json +0 -11
  176. package/recipes/subagent-contradiction-map.json +0 -11
  177. package/recipes/subagent-critic.json +0 -11
  178. package/recipes/subagent-evidence-map.json +0 -11
  179. package/recipes/subagent-followup.json +0 -10
  180. package/recipes/subagent-judge.json +0 -11
  181. package/recipes/subagent-merge.json +0 -11
  182. package/recipes/subagent-normalize.json +0 -11
  183. package/recipes/subagent-plan.json +0 -11
  184. package/recipes/subagent-preflight.json +0 -11
  185. package/recipes/subagent-prompt.json +0 -10
  186. package/recipes/subagent-quorum.json +0 -10
  187. package/recipes/subagent-review-coordinator.json +0 -14
  188. package/recipes/subagent-review.json +0 -11
  189. package/recipes/subagent-task-card.json +0 -11
  190. package/recipes/subagent-tools.json +0 -10
  191. package/recipes/subagent-verify.json +0 -11
  192. package/recipes/subagents-prompts.json +0 -10
  193. package/recipes/tool-review.json +0 -10
  194. package/scripts/async-runner.mjs +25 -25
  195. package/scripts/conformance.mjs +4 -2
  196. package/scripts/locker.mjs +200 -66
  197. package/scripts/music-player.mjs +159 -150
  198. package/scripts/recipe-utils.mjs +6 -96
  199. package/scripts/release-gates.mjs +60 -0
  200. package/scripts/validate-recipe.mjs +3 -53
  201. package/skills/actors/SKILL.md +53 -266
  202. package/skills/swarm/SKILL.md +11 -33
  203. package/dist/fixtures/protocol/actor-message-branch.json +0 -13
  204. package/dist/fixtures/protocol/mailbox-contract.json +0 -15
  205. package/dist/fixtures/protocol/room-message.json +0 -11
  206. package/dist/fixtures/protocol/room-roster.json +0 -11
  207. package/dist/fixtures/protocol/run-inbox-message.json +0 -9
  208. package/dist/fixtures/protocol/run-outbox-event.json +0 -9
  209. package/dist/lib/mailbox-loop.d.ts +0 -41
  210. package/dist/lib/mailbox-loop.js +0 -60
  211. package/dist/lib/messages.d.ts +0 -25
  212. package/dist/lib/messages.js +0 -122
  213. package/dist/lib/rooms.d.ts +0 -104
  214. package/dist/lib/rooms.js +0 -647
  215. package/dist/lib/runs-mailbox.d.ts +0 -25
  216. package/dist/lib/runs-mailbox.js +0 -146
  217. package/dist/lib/runs-messages.d.ts +0 -15
  218. package/dist/lib/runs-messages.js +0 -179
  219. package/dist/lib/runs-outbox.d.ts +0 -41
  220. package/dist/lib/runs-outbox.js +0 -87
  221. package/dist/lib/tools-mailbox.d.ts +0 -8
  222. package/dist/lib/tools-mailbox.js +0 -48
  223. package/dist/recipes/actor-worker.json +0 -39
  224. package/dist/recipes/coordinator-locker.json +0 -45
  225. package/dist/recipes/locker.json +0 -45
  226. package/dist/recipes/pipeline-room-swarm.json +0 -50
  227. package/dist/recipes/subagent-message.json +0 -32
  228. package/dist/recipes/utility-actor-message.json +0 -23
  229. package/dist/scripts/actor-worker.mjs +0 -214
  230. package/dist/scripts/coordinator.mjs +0 -799
  231. package/docs/actor-messages.md +0 -225
  232. package/fixtures/protocol/actor-message-branch.json +0 -13
  233. package/fixtures/protocol/mailbox-contract.json +0 -15
  234. package/fixtures/protocol/room-message.json +0 -11
  235. package/fixtures/protocol/room-roster.json +0 -11
  236. package/fixtures/protocol/run-inbox-message.json +0 -9
  237. package/fixtures/protocol/run-outbox-event.json +0 -9
  238. package/lib/mailbox-loop.ts +0 -144
  239. package/lib/messages.ts +0 -151
  240. package/lib/rooms.ts +0 -939
  241. package/lib/runs-mailbox.ts +0 -208
  242. package/lib/runs-messages.ts +0 -252
  243. package/lib/runs-outbox.ts +0 -144
  244. package/lib/tools-mailbox.ts +0 -56
  245. package/recipes/actor-worker.json +0 -39
  246. package/recipes/coordinator-locker.json +0 -45
  247. package/recipes/locker.json +0 -45
  248. package/recipes/pipeline-room-swarm.json +0 -50
  249. package/recipes/subagent-message.json +0 -32
  250. package/recipes/utility-actor-message.json +0 -23
  251. package/scripts/actor-worker.mjs +0 -214
  252. package/scripts/coordinator.mjs +0 -799
package/lib/rooms.ts DELETED
@@ -1,939 +0,0 @@
1
- /**
2
- * Actor room persistence.
3
- * Zones: room timelines, rosters, branch inboxes, cross-branch discovery state
4
- * Owns file-backed room and branch-mailbox state; routing policy stays in tools/runtime adapters.
5
- */
6
-
7
- import * as fs from "node:fs";
8
- import { randomUUID } from "node:crypto";
9
- import * as path from "node:path";
10
-
11
- import type { ActorMessage } from "./messages.ts";
12
- import * as Limits from "./limits.ts";
13
- import { notifyRuntimeWake } from "./runtime-notifier.ts";
14
- import {
15
- formatStateReadDiagnostics,
16
- readJsonFileResilient,
17
- readJsonlFileResilient,
18
- } from "./state-readers.ts";
19
-
20
- const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
21
- const STATE_LOCK_TIMEOUT_MS = 5000;
22
- const DEFAULT_ROOM_MAX_MESSAGES = 10000;
23
- const DEFAULT_BRANCH_INBOX_TERMINAL_RETAINED = 2000;
24
- const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = 250;
25
-
26
- export interface RoomMember {
27
- address: string;
28
- caps?: unknown;
29
- claim?: unknown;
30
- display?: unknown;
31
- joined_at: string;
32
- last_seen: string;
33
- parent?: unknown;
34
- role?: unknown;
35
- status?: string;
36
- }
37
-
38
- export interface RoomTimelineEntry extends ActorMessage {
39
- received_at: string;
40
- }
41
-
42
- export interface RoomAppendResult {
43
- room: string;
44
- message_count: number;
45
- roster_count: number;
46
- sent: true;
47
- }
48
-
49
- export interface RoomMessagePreview {
50
- body_preview?: string;
51
- from?: string;
52
- summary?: string;
53
- timestamp: string;
54
- to: string;
55
- type: string;
56
- }
57
-
58
- export interface RoomCompactionInfo {
59
- compacted_at: string;
60
- dropped_count: number;
61
- first_kept_at?: string;
62
- last_kept_at?: string;
63
- max_messages: number;
64
- }
65
-
66
- export interface RoomStatus {
67
- room: string;
68
- message_count: number;
69
- roster_count: number;
70
- compaction?: RoomCompactionInfo;
71
- diagnostics?: string[];
72
- diagnostics_count?: number;
73
- last_message_at?: string;
74
- last_message_from?: string;
75
- last_message_summary?: string;
76
- last_message_type?: string;
77
- }
78
-
79
- export interface RoomContact {
80
- address: string;
81
- caps?: unknown;
82
- claim?: unknown;
83
- parent?: unknown;
84
- role?: unknown;
85
- status?: string;
86
- }
87
-
88
- export interface ActorCommunicationSnapshot {
89
- contacts?: RoomContact[];
90
- parent?: string;
91
- root: string;
92
- self: string;
93
- rooms: Array<{
94
- address: string;
95
- members?: RoomMember[];
96
- name: string;
97
- }>;
98
- updated_at: string;
99
- }
100
-
101
- function roomDir(stateDir: string, room: string): string {
102
- return path.join(stateDir, "rooms", room);
103
- }
104
-
105
- function messagesFile(stateDir: string, room: string): string {
106
- return path.join(roomDir(stateDir, room), "messages.jsonl");
107
- }
108
-
109
- function rosterFile(stateDir: string, room: string): string {
110
- return path.join(roomDir(stateDir, room), "roster.json");
111
- }
112
-
113
- function snapshotFile(stateDir: string): string {
114
- return path.join(stateDir, "communication.json");
115
- }
116
-
117
- function branchSnapshotFile(stateDir: string, branch: string): string {
118
- return path.join(stateDir, "branches", branch, "communication.json");
119
- }
120
-
121
- function branchInboxFile(stateDir: string, branch: string): string {
122
- return path.join(stateDir, "branches", branch, "inbox.jsonl");
123
- }
124
-
125
- function branchIdFromAddress(
126
- address: string | undefined,
127
- run: string,
128
- ): string | undefined {
129
- if (!address) return undefined;
130
- const match = new RegExp(`^branch:${run}/(.+)$`).exec(address);
131
- return match?.[1];
132
- }
133
-
134
- function ensureRoomDir(stateDir: string, room: string): void {
135
- fs.mkdirSync(roomDir(stateDir, room), { recursive: true });
136
- }
137
-
138
- function sleepSync(ms: number): void {
139
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
140
- }
141
-
142
- function acquireStateLock(
143
- parentDir: string,
144
- name: string,
145
- label: string,
146
- ): () => void {
147
- fs.mkdirSync(parentDir, { recursive: true });
148
- const lockDir = path.join(parentDir, name);
149
- const started = Date.now();
150
- while (true) {
151
- try {
152
- fs.mkdirSync(lockDir);
153
- fs.writeFileSync(
154
- path.join(lockDir, "owner.json"),
155
- `${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`,
156
- );
157
- return () => fs.rmSync(lockDir, { recursive: true, force: true });
158
- } catch (error) {
159
- try {
160
- const stat = fs.statSync(lockDir);
161
- if (Date.now() - stat.mtimeMs > STATE_LOCK_MAX_AGE_MS) {
162
- fs.rmSync(lockDir, { recursive: true, force: true });
163
- continue;
164
- }
165
- } catch {
166
- continue;
167
- }
168
- if (Date.now() - started > STATE_LOCK_TIMEOUT_MS) {
169
- throw new Error(`${label} lock timed out.`, { cause: error });
170
- }
171
- sleepSync(10);
172
- }
173
- }
174
- }
175
-
176
- function acquireRoomLock(stateDir: string, room: string): () => void {
177
- return acquireStateLock(
178
- roomDir(stateDir, room),
179
- ".append.lock",
180
- `Room append ${room}`,
181
- );
182
- }
183
-
184
- function acquireBranchInboxLock(stateDir: string, branch: string): () => void {
185
- return acquireStateLock(
186
- path.dirname(branchInboxFile(stateDir, branch)),
187
- ".inbox.lock",
188
- `Branch inbox ${branch}`,
189
- );
190
- }
191
-
192
- function asRecord(value: unknown): Record<string, unknown> {
193
- return value && typeof value === "object" && !Array.isArray(value)
194
- ? (value as Record<string, unknown>)
195
- : {};
196
- }
197
-
198
- function runFromRoomAddress(address: string): string | undefined {
199
- const match = /^room:([^/]+)(?:\/main)?$/.exec(address);
200
- return match?.[1];
201
- }
202
-
203
- function readJsonFile<T>(file: string, fallback: T): T {
204
- return readJsonFileResilient<T>(file, fallback).value;
205
- }
206
-
207
- function writeJsonFile(file: string, value: unknown): void {
208
- fs.mkdirSync(path.dirname(file), { recursive: true });
209
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
210
- }
211
-
212
- function notifyActorWake(
213
- stateDir: string,
214
- actor: string,
215
- reason: string,
216
- metadata: Record<string, unknown> = {},
217
- ): void {
218
- try {
219
- notifyRuntimeWake(stateDir, { actor, metadata, reason });
220
- } catch {
221
- // Runtime wakes are advisory; durable room/inbox state remains canonical.
222
- }
223
- }
224
-
225
- function positiveEnvInt(name: string, fallback: number): number {
226
- const value = Number(process.env[name] ?? fallback);
227
- return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
228
- }
229
-
230
- function roomMaxMessages(): number {
231
- return positiveEnvInt(
232
- "PI_ACTORS_ROOM_MAX_MESSAGES",
233
- DEFAULT_ROOM_MAX_MESSAGES,
234
- );
235
- }
236
-
237
- function snapshotMinIntervalMs(): number {
238
- return positiveEnvInt(
239
- "PI_ACTORS_COMMUNICATION_SNAPSHOT_MIN_MS",
240
- DEFAULT_SNAPSHOT_MIN_INTERVAL_MS,
241
- );
242
- }
243
-
244
- function rosterMinIntervalMs(): number {
245
- return positiveEnvInt(
246
- "PI_ACTORS_ROOM_ROSTER_MIN_MS",
247
- DEFAULT_SNAPSHOT_MIN_INTERVAL_MS,
248
- );
249
- }
250
-
251
- function compactRoomMessages(stateDir: string, room: string): void {
252
- const maxMessages = roomMaxMessages();
253
- const file = messagesFile(stateDir, room);
254
- const lines = readJsonlTailLines(file, maxMessages + 1);
255
- if (lines.length <= maxMessages) return;
256
- const kept = lines.slice(-maxMessages);
257
- fs.writeFileSync(file, `${kept.join("\n")}\n`);
258
- const keptMessages = kept.flatMap((line) => {
259
- try {
260
- return [JSON.parse(line) as RoomTimelineEntry];
261
- } catch {
262
- return [];
263
- }
264
- });
265
- writeJsonFile(path.join(roomDir(stateDir, room), "compaction.json"), {
266
- compacted_at: new Date().toISOString(),
267
- dropped_count: lines.length - kept.length,
268
- ...(keptMessages[0]?.received_at
269
- ? { first_kept_at: keptMessages[0].received_at }
270
- : {}),
271
- ...(keptMessages.at(-1)?.received_at
272
- ? { last_kept_at: keptMessages.at(-1)?.received_at }
273
- : {}),
274
- max_messages: maxMessages,
275
- });
276
- }
277
-
278
- function readJsonlLineCount(file: string): number {
279
- const stat = fs.statSync(file);
280
- if (stat.size === 0) return 0;
281
- const fd = fs.openSync(file, "r");
282
- try {
283
- const chunkSize = 64 * 1024;
284
- const chunk = Buffer.allocUnsafe(chunkSize);
285
- let position = 0;
286
- let count = 0;
287
- let lastByte: number | undefined;
288
- while (position < stat.size) {
289
- const bytesRead = fs.readSync(
290
- fd,
291
- chunk,
292
- 0,
293
- Math.min(chunkSize, stat.size - position),
294
- position,
295
- );
296
- if (bytesRead <= 0) break;
297
- position += bytesRead;
298
- for (let index = 0; index < bytesRead; index += 1) {
299
- if (chunk[index] === 10) count += 1;
300
- }
301
- lastByte = chunk[bytesRead - 1];
302
- }
303
- return lastByte === 10 ? count : count + 1;
304
- } finally {
305
- fs.closeSync(fd);
306
- }
307
- }
308
-
309
- function readRoomMessageCount(stateDir: string, room: string): number {
310
- try {
311
- return readJsonlLineCount(messagesFile(stateDir, room));
312
- } catch (error) {
313
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0;
314
- throw error;
315
- }
316
- }
317
-
318
- function readJsonlTailLines(file: string, limit: number): string[] {
319
- const lineLimit = Math.max(1, limit);
320
- const stat = fs.statSync(file);
321
- if (stat.size === 0) return [];
322
- const fd = fs.openSync(file, "r");
323
- try {
324
- const chunkSize = 64 * 1024;
325
- const chunks: Buffer[] = [];
326
- let position = stat.size;
327
- let newlines = 0;
328
- while (position > 0 && newlines <= lineLimit) {
329
- const size = Math.min(chunkSize, position);
330
- position -= size;
331
- const chunk = Buffer.allocUnsafe(size);
332
- fs.readSync(fd, chunk, 0, size, position);
333
- chunks.unshift(chunk);
334
- for (let index = size - 1; index >= 0; index -= 1) {
335
- if (chunk[index] === 10) newlines += 1;
336
- }
337
- }
338
- return Buffer.concat(chunks)
339
- .toString("utf8")
340
- .split("\n")
341
- .filter(Boolean)
342
- .slice(-lineLimit);
343
- } finally {
344
- fs.closeSync(fd);
345
- }
346
- }
347
-
348
- export function readRoomRoster(
349
- stateDir: string,
350
- room: string,
351
- ): Record<string, RoomMember> {
352
- return readJsonFile<Record<string, RoomMember>>(
353
- rosterFile(stateDir, room),
354
- {},
355
- );
356
- }
357
-
358
- function writeRoomRoster(
359
- stateDir: string,
360
- room: string,
361
- roster: Record<string, RoomMember>,
362
- ): void {
363
- ensureRoomDir(stateDir, room);
364
- writeJsonFile(rosterFile(stateDir, room), roster);
365
- }
366
-
367
- function shouldDebounceFile(file: string, minIntervalMs: number): boolean {
368
- try {
369
- return Date.now() - fs.statSync(file).mtimeMs < minIntervalMs;
370
- } catch (error) {
371
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
372
- throw error;
373
- }
374
- }
375
-
376
- function shouldDebounceSnapshot(file: string): boolean {
377
- return shouldDebounceFile(file, snapshotMinIntervalMs());
378
- }
379
-
380
- function comparableRosterMember(member: RoomMember | undefined): string {
381
- if (!member) return "";
382
- const { last_seen: _lastSeen, ...semantic } = member;
383
- return JSON.stringify(semantic);
384
- }
385
-
386
- function shouldWriteRoomRosterMember(
387
- stateDir: string,
388
- room: string,
389
- before: RoomMember | undefined,
390
- after: RoomMember,
391
- ): boolean {
392
- if (!before) return true;
393
- if (comparableRosterMember(before) !== comparableRosterMember(after)) {
394
- return true;
395
- }
396
- return !shouldDebounceFile(rosterFile(stateDir, room), rosterMinIntervalMs());
397
- }
398
-
399
- function updateRosterForMessage(
400
- stateDir: string,
401
- room: string,
402
- message: ActorMessage,
403
- receivedAt: string,
404
- ): Record<string, RoomMember> {
405
- const roster = readRoomRoster(stateDir, room);
406
- if (!message.from) return roster;
407
- const body = asRecord(message.body);
408
- const current = roster[message.from];
409
- const next =
410
- message.type === "actor.leave"
411
- ? {
412
- address: message.from,
413
- joined_at: current?.joined_at ?? receivedAt,
414
- last_seen: receivedAt,
415
- ...(current?.caps !== undefined ? { caps: current.caps } : {}),
416
- ...(current?.claim !== undefined ? { claim: current.claim } : {}),
417
- ...(current?.display !== undefined
418
- ? { display: current.display }
419
- : {}),
420
- ...(current?.parent !== undefined ? { parent: current.parent } : {}),
421
- ...(current?.role !== undefined
422
- ? { role: current.role }
423
- : { role: "actor" }),
424
- status: String(body.status ?? "left"),
425
- }
426
- : {
427
- address: message.from,
428
- joined_at: current?.joined_at ?? receivedAt,
429
- last_seen: receivedAt,
430
- ...(body.caps !== undefined
431
- ? { caps: body.caps }
432
- : current?.caps !== undefined
433
- ? { caps: current.caps }
434
- : {}),
435
- ...(body.claim !== undefined
436
- ? { claim: body.claim }
437
- : current?.claim !== undefined
438
- ? { claim: current.claim }
439
- : {}),
440
- ...(body.display !== undefined
441
- ? { display: body.display }
442
- : current?.display !== undefined
443
- ? { display: current.display }
444
- : {}),
445
- ...(body.parent !== undefined
446
- ? { parent: body.parent }
447
- : current?.parent !== undefined
448
- ? { parent: current.parent }
449
- : {}),
450
- ...(body.role !== undefined
451
- ? { role: body.role }
452
- : current?.role !== undefined
453
- ? { role: current.role }
454
- : { role: "actor" }),
455
- status: String(body.status ?? current?.status ?? "present"),
456
- };
457
- roster[message.from] = next;
458
- if (shouldWriteRoomRosterMember(stateDir, room, current, next)) {
459
- writeRoomRoster(stateDir, room, roster);
460
- }
461
- return roster;
462
- }
463
-
464
- export interface BranchInboxRecord extends ActorMessage {
465
- claimed_at?: string;
466
- claimed_by?: string;
467
- error?: string;
468
- failed_at?: string;
469
- handled_at?: string;
470
- id?: string;
471
- queued_at?: string;
472
- status?: string;
473
- }
474
-
475
- export interface BranchInboxReadResult {
476
- corrupted: number;
477
- messages: BranchInboxRecord[];
478
- }
479
-
480
- function readBranchInboxFile(
481
- stateDir: string,
482
- run: string,
483
- address: string,
484
- limit = 40,
485
- ): BranchInboxReadResult {
486
- const branch = branchIdFromAddress(address, run);
487
- if (!branch)
488
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
489
- try {
490
- const lines = readJsonlTailLines(branchInboxFile(stateDir, branch), limit);
491
- const messages: BranchInboxRecord[] = [];
492
- let corrupted = 0;
493
- for (const line of lines) {
494
- try {
495
- messages.push(JSON.parse(line));
496
- } catch {
497
- corrupted += 1;
498
- }
499
- }
500
- return { corrupted, messages };
501
- } catch (error) {
502
- if ((error as NodeJS.ErrnoException).code === "ENOENT")
503
- return { corrupted: 0, messages: [] };
504
- throw error;
505
- }
506
- }
507
-
508
- function readAllBranchInboxLines(
509
- stateDir: string,
510
- run: string,
511
- address: string,
512
- ): Array<{ raw: string } | { message: BranchInboxRecord }> {
513
- const branch = branchIdFromAddress(address, run);
514
- if (!branch)
515
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
516
- try {
517
- const content = fs.readFileSync(branchInboxFile(stateDir, branch), "utf8");
518
- return content
519
- .split("\n")
520
- .filter((line) => line.length > 0)
521
- .map((line) => {
522
- try {
523
- return { message: JSON.parse(line) as BranchInboxRecord };
524
- } catch {
525
- return { raw: line };
526
- }
527
- });
528
- } catch (error) {
529
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
530
- throw error;
531
- }
532
- }
533
-
534
- export function readBranchInboxMessages(
535
- stateDir: string,
536
- run: string,
537
- address: string,
538
- limit = 40,
539
- ): BranchInboxRecord[] {
540
- return readBranchInboxFile(stateDir, run, address, limit).messages;
541
- }
542
-
543
- export function readBranchInboxDiagnostics(
544
- stateDir: string,
545
- run: string,
546
- address: string,
547
- limit = 40,
548
- ): BranchInboxReadResult {
549
- return readBranchInboxFile(stateDir, run, address, limit);
550
- }
551
-
552
- export function getBranchInboxTerminalRetainLimit(): number {
553
- const value = Number(
554
- process.env.PI_ACTORS_BRANCH_INBOX_TERMINAL_RETAINED ?? "",
555
- );
556
- return Number.isInteger(value) && value >= 0
557
- ? value
558
- : DEFAULT_BRANCH_INBOX_TERMINAL_RETAINED;
559
- }
560
-
561
- function compactBranchInboxMessages<T extends { status?: string }>(
562
- messages: T[],
563
- ): T[] {
564
- const retainTerminal = getBranchInboxTerminalRetainLimit();
565
- const active = messages.filter(
566
- (message) => message.status !== "handled" && message.status !== "failed",
567
- );
568
- const terminal = messages.filter(
569
- (message) => message.status === "handled" || message.status === "failed",
570
- );
571
- return [...terminal.slice(-retainTerminal), ...active];
572
- }
573
-
574
- export function appendBranchInboxMessage(
575
- stateDir: string,
576
- run: string,
577
- address: string,
578
- message: ActorMessage,
579
- ): void {
580
- const branch = branchIdFromAddress(address, run);
581
- if (!branch)
582
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
583
- const releaseLock = acquireBranchInboxLock(stateDir, branch);
584
- try {
585
- fs.writeFileSync(
586
- branchInboxFile(stateDir, branch),
587
- `${JSON.stringify({ ...message, id: randomUUID(), queued_at: new Date().toISOString(), status: "queued" })}\n`,
588
- { flag: "a" },
589
- );
590
- notifyActorWake(stateDir, address, "branch.message", {
591
- ...(message.from ? { from: message.from } : {}),
592
- type: message.type,
593
- });
594
- } finally {
595
- releaseLock();
596
- }
597
- }
598
-
599
- export function claimBranchInboxMessage(
600
- stateDir: string,
601
- run: string,
602
- address: string,
603
- owner = "runtime",
604
- statuses: string[] = ["queued"],
605
- ): BranchInboxRecord | undefined {
606
- const branch = branchIdFromAddress(address, run);
607
- if (!branch)
608
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
609
- const releaseLock = acquireBranchInboxLock(stateDir, branch);
610
- try {
611
- const file = branchInboxFile(stateDir, branch);
612
- const records = readAllBranchInboxLines(stateDir, run, address);
613
- let claimed: BranchInboxRecord | undefined;
614
- const now = new Date().toISOString();
615
- const updated = records.map((record) => {
616
- if (claimed || !("message" in record)) return record;
617
- const status = record.message.status ?? "queued";
618
- if (!statuses.includes(status)) return record;
619
- claimed = {
620
- ...record.message,
621
- claimed_at: now,
622
- claimed_by: owner,
623
- status: "claimed",
624
- };
625
- return { message: claimed };
626
- });
627
- if (!claimed) return undefined;
628
- fs.writeFileSync(
629
- file,
630
- `${updated.map((record) => ("raw" in record ? record.raw : JSON.stringify(record.message))).join("\n")}\n`,
631
- );
632
- notifyActorWake(stateDir, address, "branch.inbox.claim", {
633
- id: claimed.id,
634
- owner,
635
- });
636
- return claimed;
637
- } finally {
638
- releaseLock();
639
- }
640
- }
641
-
642
- export function updateBranchInboxMessageStatus(
643
- stateDir: string,
644
- run: string,
645
- address: string,
646
- id: string,
647
- status: "claimed" | "handled" | "failed",
648
- metadata: Record<string, unknown> = {},
649
- ): boolean {
650
- const branch = branchIdFromAddress(address, run);
651
- if (!branch)
652
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
653
- const releaseLock = acquireBranchInboxLock(stateDir, branch);
654
- try {
655
- const file = branchInboxFile(stateDir, branch);
656
- const records = readAllBranchInboxLines(stateDir, run, address);
657
- let changed = false;
658
- const timestampKey = `${status}_at`;
659
- const updated = records.map((record) => {
660
- if (!("message" in record) || record.message.id !== id) return record;
661
- changed = true;
662
- return {
663
- message: {
664
- ...record.message,
665
- ...metadata,
666
- [timestampKey]: new Date().toISOString(),
667
- status,
668
- },
669
- };
670
- });
671
- if (!changed) return false;
672
- const validMessages = updated.flatMap((record) =>
673
- "message" in record ? [record.message] : [],
674
- );
675
- const compactedIds = new Set(
676
- compactBranchInboxMessages(validMessages).map((message) => message.id),
677
- );
678
- const lines = updated.flatMap((record) => {
679
- if ("raw" in record) return [record.raw];
680
- if (record.message.id && !compactedIds.has(record.message.id)) return [];
681
- return [JSON.stringify(record.message)];
682
- });
683
- fs.writeFileSync(file, lines.length ? `${lines.join("\n")}\n` : "");
684
- notifyActorWake(stateDir, address, "branch.inbox.status", { id, status });
685
- return true;
686
- } finally {
687
- releaseLock();
688
- }
689
- }
690
-
691
- export function appendRoomMessage(
692
- stateDir: string,
693
- room: string,
694
- message: ActorMessage,
695
- ): RoomAppendResult {
696
- const releaseLock = acquireRoomLock(stateDir, room);
697
- try {
698
- const receivedAt = new Date().toISOString();
699
- const entry: RoomTimelineEntry = { ...message, received_at: receivedAt };
700
- fs.appendFileSync(
701
- messagesFile(stateDir, room),
702
- `${JSON.stringify(entry)}\n`,
703
- );
704
- compactRoomMessages(stateDir, room);
705
- const roster = updateRosterForMessage(stateDir, room, message, receivedAt);
706
- const run = runFromRoomAddress(message.to);
707
- if (run) {
708
- writeCommunicationSnapshot(stateDir, run);
709
- notifyActorWake(stateDir, message.to, "room.message", {
710
- ...(message.from ? { from: message.from } : {}),
711
- type: message.type,
712
- });
713
- if (message.from && branchIdFromAddress(message.from, run)) {
714
- writeBranchCommunicationSnapshotDebounced(stateDir, run, message.from);
715
- }
716
- }
717
- return {
718
- message_count: readRoomMessageCount(stateDir, room),
719
- room,
720
- roster_count: Object.keys(roster).length,
721
- sent: true,
722
- };
723
- } finally {
724
- releaseLock();
725
- }
726
- }
727
-
728
- export function readRoomMessages(
729
- stateDir: string,
730
- room: string,
731
- limit = 40,
732
- ): RoomTimelineEntry[] {
733
- try {
734
- const lines = readJsonlTailLines(messagesFile(stateDir, room), limit);
735
- return lines.flatMap((line) => {
736
- try {
737
- return [JSON.parse(line) as RoomTimelineEntry];
738
- } catch {
739
- return [];
740
- }
741
- });
742
- } catch (error) {
743
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
744
- throw error;
745
- }
746
- }
747
-
748
- function previewValue(
749
- value: unknown,
750
- maxLength = Limits.ROOM_MESSAGE_PREVIEW_CHARS,
751
- ): string | undefined {
752
- if (value === undefined) return undefined;
753
- const text = typeof value === "string" ? value : JSON.stringify(value);
754
- const compact = text.replaceAll(/\s+/g, " ").trim();
755
- if (!compact) return undefined;
756
- return compact.length > maxLength
757
- ? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
758
- : compact;
759
- }
760
-
761
- export function readRoomMessagePreviews(
762
- stateDir: string,
763
- room: string,
764
- limit = 40,
765
- ): RoomMessagePreview[] {
766
- return readRoomMessages(stateDir, room, limit).map((message) => ({
767
- ...(previewValue(message.body)
768
- ? { body_preview: previewValue(message.body) }
769
- : {}),
770
- ...(message.from ? { from: message.from } : {}),
771
- ...(message.summary ? { summary: message.summary } : {}),
772
- timestamp: message.received_at,
773
- to: message.to,
774
- type: message.type,
775
- }));
776
- }
777
-
778
- export function getRoomStatus(stateDir: string, room: string): RoomStatus {
779
- const messageCount = readRoomMessageCount(stateDir, room);
780
- const [last] = readRoomMessages(stateDir, room, 1);
781
- const compactionRead = readJsonFileResilient<RoomCompactionInfo | undefined>(
782
- path.join(roomDir(stateDir, room), "compaction.json"),
783
- undefined,
784
- );
785
- const rosterRead = readJsonFileResilient<Record<string, RoomMember>>(
786
- rosterFile(stateDir, room),
787
- {},
788
- );
789
- const messageRead = readJsonlFileResilient<RoomTimelineEntry>(
790
- messagesFile(stateDir, room),
791
- );
792
- const diagnostics = [
793
- ...messageRead.diagnostics,
794
- ...rosterRead.diagnostics,
795
- ...compactionRead.diagnostics,
796
- ];
797
- return {
798
- ...(last
799
- ? {
800
- last_message_at: last.received_at,
801
- ...(last.from ? { last_message_from: last.from } : {}),
802
- ...(last.summary ? { last_message_summary: last.summary } : {}),
803
- last_message_type: last.type,
804
- }
805
- : {}),
806
- ...(compactionRead.value ? { compaction: compactionRead.value } : {}),
807
- ...(diagnostics.length
808
- ? {
809
- diagnostics: formatStateReadDiagnostics(diagnostics),
810
- diagnostics_count: diagnostics.length,
811
- }
812
- : {}),
813
- message_count: messageCount,
814
- room,
815
- roster_count: Object.keys(rosterRead.value).length,
816
- };
817
- }
818
-
819
- export function ensureRoomMember(
820
- stateDir: string,
821
- run: string,
822
- room: string,
823
- address: string,
824
- body: Record<string, unknown>,
825
- summary: string,
826
- ): RoomAppendResult {
827
- const roster = readRoomRoster(stateDir, room);
828
- if (roster[address]) {
829
- return {
830
- message_count: readRoomMessageCount(stateDir, room),
831
- room,
832
- roster_count: Object.keys(roster).length,
833
- sent: true,
834
- };
835
- }
836
- return appendRoomMessage(stateDir, room, {
837
- body,
838
- from: address,
839
- summary,
840
- to: `room:${run}`,
841
- type: "actor.join",
842
- });
843
- }
844
-
845
- export function ensureDefaultRoom(
846
- stateDir: string,
847
- run: string,
848
- ): RoomAppendResult {
849
- return ensureRoomMember(
850
- stateDir,
851
- run,
852
- "main",
853
- `run:${run}`,
854
- { role: "run", status: "present" },
855
- "Run joined default room",
856
- );
857
- }
858
-
859
- export function readCommunicationSnapshot(
860
- stateDir: string,
861
- ): ActorCommunicationSnapshot | undefined {
862
- return readJsonFileResilient<ActorCommunicationSnapshot | undefined>(
863
- snapshotFile(stateDir),
864
- undefined,
865
- ).value;
866
- }
867
-
868
- export function readRoomContacts(
869
- stateDir: string,
870
- room: string,
871
- self?: string,
872
- ): RoomContact[] {
873
- return Object.values(readRoomRoster(stateDir, room))
874
- .filter((member) => member.address !== self)
875
- .map((member) => ({
876
- address: member.address,
877
- ...(member.caps !== undefined ? { caps: member.caps } : {}),
878
- ...(member.claim !== undefined ? { claim: member.claim } : {}),
879
- ...(member.parent !== undefined ? { parent: member.parent } : {}),
880
- ...(member.role !== undefined ? { role: member.role } : {}),
881
- ...(member.status !== undefined ? { status: member.status } : {}),
882
- }));
883
- }
884
-
885
- function buildCommunicationSnapshot(
886
- stateDir: string,
887
- run: string,
888
- self: string,
889
- ): ActorCommunicationSnapshot {
890
- const mainRoster = Object.values(readRoomRoster(stateDir, "main"));
891
- const contacts = readRoomContacts(stateDir, "main", self);
892
- return {
893
- ...(contacts.length > 0 ? { contacts } : {}),
894
- ...(self !== `run:${run}` ? { parent: `run:${run}` } : {}),
895
- root: `run:${run}`,
896
- self,
897
- rooms: [
898
- {
899
- address: `room:${run}`,
900
- ...(mainRoster.length > 0 ? { members: mainRoster } : {}),
901
- name: "main",
902
- },
903
- ],
904
- updated_at: new Date().toISOString(),
905
- };
906
- }
907
-
908
- export function writeCommunicationSnapshot(
909
- stateDir: string,
910
- run: string,
911
- ): ActorCommunicationSnapshot {
912
- const snapshot = buildCommunicationSnapshot(stateDir, run, `run:${run}`);
913
- writeJsonFile(snapshotFile(stateDir), snapshot);
914
- return snapshot;
915
- }
916
-
917
- export function writeBranchCommunicationSnapshot(
918
- stateDir: string,
919
- run: string,
920
- self: string,
921
- ): ActorCommunicationSnapshot {
922
- const branch = branchIdFromAddress(self, run);
923
- if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${self}`);
924
- const snapshot = buildCommunicationSnapshot(stateDir, run, self);
925
- writeJsonFile(branchSnapshotFile(stateDir, branch), snapshot);
926
- return snapshot;
927
- }
928
-
929
- function writeBranchCommunicationSnapshotDebounced(
930
- stateDir: string,
931
- run: string,
932
- self: string,
933
- ): ActorCommunicationSnapshot | undefined {
934
- const branch = branchIdFromAddress(self, run);
935
- if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${self}`);
936
- if (shouldDebounceSnapshot(branchSnapshotFile(stateDir, branch)))
937
- return undefined;
938
- return writeBranchCommunicationSnapshot(stateDir, run, self);
939
- }