@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/dist/lib/rooms.js DELETED
@@ -1,647 +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
- import * as fs from "node:fs";
7
- import { randomUUID } from "node:crypto";
8
- import * as path from "node:path";
9
- import * as Limits from "./limits.js";
10
- import { notifyRuntimeWake } from "./runtime-notifier.js";
11
- import { formatStateReadDiagnostics, readJsonFileResilient, readJsonlFileResilient, } from "./state-readers.js";
12
- const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
13
- const STATE_LOCK_TIMEOUT_MS = 5000;
14
- const DEFAULT_ROOM_MAX_MESSAGES = 10000;
15
- const DEFAULT_BRANCH_INBOX_TERMINAL_RETAINED = 2000;
16
- const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = 250;
17
- function roomDir(stateDir, room) {
18
- return path.join(stateDir, "rooms", room);
19
- }
20
- function messagesFile(stateDir, room) {
21
- return path.join(roomDir(stateDir, room), "messages.jsonl");
22
- }
23
- function rosterFile(stateDir, room) {
24
- return path.join(roomDir(stateDir, room), "roster.json");
25
- }
26
- function snapshotFile(stateDir) {
27
- return path.join(stateDir, "communication.json");
28
- }
29
- function branchSnapshotFile(stateDir, branch) {
30
- return path.join(stateDir, "branches", branch, "communication.json");
31
- }
32
- function branchInboxFile(stateDir, branch) {
33
- return path.join(stateDir, "branches", branch, "inbox.jsonl");
34
- }
35
- function branchIdFromAddress(address, run) {
36
- if (!address)
37
- return undefined;
38
- const match = new RegExp(`^branch:${run}/(.+)$`).exec(address);
39
- return match?.[1];
40
- }
41
- function ensureRoomDir(stateDir, room) {
42
- fs.mkdirSync(roomDir(stateDir, room), { recursive: true });
43
- }
44
- function sleepSync(ms) {
45
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
46
- }
47
- function acquireStateLock(parentDir, name, label) {
48
- fs.mkdirSync(parentDir, { recursive: true });
49
- const lockDir = path.join(parentDir, name);
50
- const started = Date.now();
51
- while (true) {
52
- try {
53
- fs.mkdirSync(lockDir);
54
- fs.writeFileSync(path.join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`);
55
- return () => fs.rmSync(lockDir, { recursive: true, force: true });
56
- }
57
- catch (error) {
58
- try {
59
- const stat = fs.statSync(lockDir);
60
- if (Date.now() - stat.mtimeMs > STATE_LOCK_MAX_AGE_MS) {
61
- fs.rmSync(lockDir, { recursive: true, force: true });
62
- continue;
63
- }
64
- }
65
- catch {
66
- continue;
67
- }
68
- if (Date.now() - started > STATE_LOCK_TIMEOUT_MS) {
69
- throw new Error(`${label} lock timed out.`, { cause: error });
70
- }
71
- sleepSync(10);
72
- }
73
- }
74
- }
75
- function acquireRoomLock(stateDir, room) {
76
- return acquireStateLock(roomDir(stateDir, room), ".append.lock", `Room append ${room}`);
77
- }
78
- function acquireBranchInboxLock(stateDir, branch) {
79
- return acquireStateLock(path.dirname(branchInboxFile(stateDir, branch)), ".inbox.lock", `Branch inbox ${branch}`);
80
- }
81
- function asRecord(value) {
82
- return value && typeof value === "object" && !Array.isArray(value)
83
- ? value
84
- : {};
85
- }
86
- function runFromRoomAddress(address) {
87
- const match = /^room:([^/]+)(?:\/main)?$/.exec(address);
88
- return match?.[1];
89
- }
90
- function readJsonFile(file, fallback) {
91
- return readJsonFileResilient(file, fallback).value;
92
- }
93
- function writeJsonFile(file, value) {
94
- fs.mkdirSync(path.dirname(file), { recursive: true });
95
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
96
- }
97
- function notifyActorWake(stateDir, actor, reason, metadata = {}) {
98
- try {
99
- notifyRuntimeWake(stateDir, { actor, metadata, reason });
100
- }
101
- catch {
102
- // Runtime wakes are advisory; durable room/inbox state remains canonical.
103
- }
104
- }
105
- function positiveEnvInt(name, fallback) {
106
- const value = Number(process.env[name] ?? fallback);
107
- return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
108
- }
109
- function roomMaxMessages() {
110
- return positiveEnvInt("PI_ACTORS_ROOM_MAX_MESSAGES", DEFAULT_ROOM_MAX_MESSAGES);
111
- }
112
- function snapshotMinIntervalMs() {
113
- return positiveEnvInt("PI_ACTORS_COMMUNICATION_SNAPSHOT_MIN_MS", DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
114
- }
115
- function rosterMinIntervalMs() {
116
- return positiveEnvInt("PI_ACTORS_ROOM_ROSTER_MIN_MS", DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
117
- }
118
- function compactRoomMessages(stateDir, room) {
119
- const maxMessages = roomMaxMessages();
120
- const file = messagesFile(stateDir, room);
121
- const lines = readJsonlTailLines(file, maxMessages + 1);
122
- if (lines.length <= maxMessages)
123
- return;
124
- const kept = lines.slice(-maxMessages);
125
- fs.writeFileSync(file, `${kept.join("\n")}\n`);
126
- const keptMessages = kept.flatMap((line) => {
127
- try {
128
- return [JSON.parse(line)];
129
- }
130
- catch {
131
- return [];
132
- }
133
- });
134
- writeJsonFile(path.join(roomDir(stateDir, room), "compaction.json"), {
135
- compacted_at: new Date().toISOString(),
136
- dropped_count: lines.length - kept.length,
137
- ...(keptMessages[0]?.received_at
138
- ? { first_kept_at: keptMessages[0].received_at }
139
- : {}),
140
- ...(keptMessages.at(-1)?.received_at
141
- ? { last_kept_at: keptMessages.at(-1)?.received_at }
142
- : {}),
143
- max_messages: maxMessages,
144
- });
145
- }
146
- function readJsonlLineCount(file) {
147
- const stat = fs.statSync(file);
148
- if (stat.size === 0)
149
- return 0;
150
- const fd = fs.openSync(file, "r");
151
- try {
152
- const chunkSize = 64 * 1024;
153
- const chunk = Buffer.allocUnsafe(chunkSize);
154
- let position = 0;
155
- let count = 0;
156
- let lastByte;
157
- while (position < stat.size) {
158
- const bytesRead = fs.readSync(fd, chunk, 0, Math.min(chunkSize, stat.size - position), position);
159
- if (bytesRead <= 0)
160
- break;
161
- position += bytesRead;
162
- for (let index = 0; index < bytesRead; index += 1) {
163
- if (chunk[index] === 10)
164
- count += 1;
165
- }
166
- lastByte = chunk[bytesRead - 1];
167
- }
168
- return lastByte === 10 ? count : count + 1;
169
- }
170
- finally {
171
- fs.closeSync(fd);
172
- }
173
- }
174
- function readRoomMessageCount(stateDir, room) {
175
- try {
176
- return readJsonlLineCount(messagesFile(stateDir, room));
177
- }
178
- catch (error) {
179
- if (error.code === "ENOENT")
180
- return 0;
181
- throw error;
182
- }
183
- }
184
- function readJsonlTailLines(file, limit) {
185
- const lineLimit = Math.max(1, limit);
186
- const stat = fs.statSync(file);
187
- if (stat.size === 0)
188
- return [];
189
- const fd = fs.openSync(file, "r");
190
- try {
191
- const chunkSize = 64 * 1024;
192
- const chunks = [];
193
- let position = stat.size;
194
- let newlines = 0;
195
- while (position > 0 && newlines <= lineLimit) {
196
- const size = Math.min(chunkSize, position);
197
- position -= size;
198
- const chunk = Buffer.allocUnsafe(size);
199
- fs.readSync(fd, chunk, 0, size, position);
200
- chunks.unshift(chunk);
201
- for (let index = size - 1; index >= 0; index -= 1) {
202
- if (chunk[index] === 10)
203
- newlines += 1;
204
- }
205
- }
206
- return Buffer.concat(chunks)
207
- .toString("utf8")
208
- .split("\n")
209
- .filter(Boolean)
210
- .slice(-lineLimit);
211
- }
212
- finally {
213
- fs.closeSync(fd);
214
- }
215
- }
216
- export function readRoomRoster(stateDir, room) {
217
- return readJsonFile(rosterFile(stateDir, room), {});
218
- }
219
- function writeRoomRoster(stateDir, room, roster) {
220
- ensureRoomDir(stateDir, room);
221
- writeJsonFile(rosterFile(stateDir, room), roster);
222
- }
223
- function shouldDebounceFile(file, minIntervalMs) {
224
- try {
225
- return Date.now() - fs.statSync(file).mtimeMs < minIntervalMs;
226
- }
227
- catch (error) {
228
- if (error.code === "ENOENT")
229
- return false;
230
- throw error;
231
- }
232
- }
233
- function shouldDebounceSnapshot(file) {
234
- return shouldDebounceFile(file, snapshotMinIntervalMs());
235
- }
236
- function comparableRosterMember(member) {
237
- if (!member)
238
- return "";
239
- const { last_seen: _lastSeen, ...semantic } = member;
240
- return JSON.stringify(semantic);
241
- }
242
- function shouldWriteRoomRosterMember(stateDir, room, before, after) {
243
- if (!before)
244
- return true;
245
- if (comparableRosterMember(before) !== comparableRosterMember(after)) {
246
- return true;
247
- }
248
- return !shouldDebounceFile(rosterFile(stateDir, room), rosterMinIntervalMs());
249
- }
250
- function updateRosterForMessage(stateDir, room, message, receivedAt) {
251
- const roster = readRoomRoster(stateDir, room);
252
- if (!message.from)
253
- return roster;
254
- const body = asRecord(message.body);
255
- const current = roster[message.from];
256
- const next = message.type === "actor.leave"
257
- ? {
258
- address: message.from,
259
- joined_at: current?.joined_at ?? receivedAt,
260
- last_seen: receivedAt,
261
- ...(current?.caps !== undefined ? { caps: current.caps } : {}),
262
- ...(current?.claim !== undefined ? { claim: current.claim } : {}),
263
- ...(current?.display !== undefined
264
- ? { display: current.display }
265
- : {}),
266
- ...(current?.parent !== undefined ? { parent: current.parent } : {}),
267
- ...(current?.role !== undefined
268
- ? { role: current.role }
269
- : { role: "actor" }),
270
- status: String(body.status ?? "left"),
271
- }
272
- : {
273
- address: message.from,
274
- joined_at: current?.joined_at ?? receivedAt,
275
- last_seen: receivedAt,
276
- ...(body.caps !== undefined
277
- ? { caps: body.caps }
278
- : current?.caps !== undefined
279
- ? { caps: current.caps }
280
- : {}),
281
- ...(body.claim !== undefined
282
- ? { claim: body.claim }
283
- : current?.claim !== undefined
284
- ? { claim: current.claim }
285
- : {}),
286
- ...(body.display !== undefined
287
- ? { display: body.display }
288
- : current?.display !== undefined
289
- ? { display: current.display }
290
- : {}),
291
- ...(body.parent !== undefined
292
- ? { parent: body.parent }
293
- : current?.parent !== undefined
294
- ? { parent: current.parent }
295
- : {}),
296
- ...(body.role !== undefined
297
- ? { role: body.role }
298
- : current?.role !== undefined
299
- ? { role: current.role }
300
- : { role: "actor" }),
301
- status: String(body.status ?? current?.status ?? "present"),
302
- };
303
- roster[message.from] = next;
304
- if (shouldWriteRoomRosterMember(stateDir, room, current, next)) {
305
- writeRoomRoster(stateDir, room, roster);
306
- }
307
- return roster;
308
- }
309
- function readBranchInboxFile(stateDir, run, address, limit = 40) {
310
- const branch = branchIdFromAddress(address, run);
311
- if (!branch)
312
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
313
- try {
314
- const lines = readJsonlTailLines(branchInboxFile(stateDir, branch), limit);
315
- const messages = [];
316
- let corrupted = 0;
317
- for (const line of lines) {
318
- try {
319
- messages.push(JSON.parse(line));
320
- }
321
- catch {
322
- corrupted += 1;
323
- }
324
- }
325
- return { corrupted, messages };
326
- }
327
- catch (error) {
328
- if (error.code === "ENOENT")
329
- return { corrupted: 0, messages: [] };
330
- throw error;
331
- }
332
- }
333
- function readAllBranchInboxLines(stateDir, run, address) {
334
- const branch = branchIdFromAddress(address, run);
335
- if (!branch)
336
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
337
- try {
338
- const content = fs.readFileSync(branchInboxFile(stateDir, branch), "utf8");
339
- return content
340
- .split("\n")
341
- .filter((line) => line.length > 0)
342
- .map((line) => {
343
- try {
344
- return { message: JSON.parse(line) };
345
- }
346
- catch {
347
- return { raw: line };
348
- }
349
- });
350
- }
351
- catch (error) {
352
- if (error.code === "ENOENT")
353
- return [];
354
- throw error;
355
- }
356
- }
357
- export function readBranchInboxMessages(stateDir, run, address, limit = 40) {
358
- return readBranchInboxFile(stateDir, run, address, limit).messages;
359
- }
360
- export function readBranchInboxDiagnostics(stateDir, run, address, limit = 40) {
361
- return readBranchInboxFile(stateDir, run, address, limit);
362
- }
363
- export function getBranchInboxTerminalRetainLimit() {
364
- const value = Number(process.env.PI_ACTORS_BRANCH_INBOX_TERMINAL_RETAINED ?? "");
365
- return Number.isInteger(value) && value >= 0
366
- ? value
367
- : DEFAULT_BRANCH_INBOX_TERMINAL_RETAINED;
368
- }
369
- function compactBranchInboxMessages(messages) {
370
- const retainTerminal = getBranchInboxTerminalRetainLimit();
371
- const active = messages.filter((message) => message.status !== "handled" && message.status !== "failed");
372
- const terminal = messages.filter((message) => message.status === "handled" || message.status === "failed");
373
- return [...terminal.slice(-retainTerminal), ...active];
374
- }
375
- export function appendBranchInboxMessage(stateDir, run, address, message) {
376
- const branch = branchIdFromAddress(address, run);
377
- if (!branch)
378
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
379
- const releaseLock = acquireBranchInboxLock(stateDir, branch);
380
- try {
381
- fs.writeFileSync(branchInboxFile(stateDir, branch), `${JSON.stringify({ ...message, id: randomUUID(), queued_at: new Date().toISOString(), status: "queued" })}\n`, { flag: "a" });
382
- notifyActorWake(stateDir, address, "branch.message", {
383
- ...(message.from ? { from: message.from } : {}),
384
- type: message.type,
385
- });
386
- }
387
- finally {
388
- releaseLock();
389
- }
390
- }
391
- export function claimBranchInboxMessage(stateDir, run, address, owner = "runtime", statuses = ["queued"]) {
392
- const branch = branchIdFromAddress(address, run);
393
- if (!branch)
394
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
395
- const releaseLock = acquireBranchInboxLock(stateDir, branch);
396
- try {
397
- const file = branchInboxFile(stateDir, branch);
398
- const records = readAllBranchInboxLines(stateDir, run, address);
399
- let claimed;
400
- const now = new Date().toISOString();
401
- const updated = records.map((record) => {
402
- if (claimed || !("message" in record))
403
- return record;
404
- const status = record.message.status ?? "queued";
405
- if (!statuses.includes(status))
406
- return record;
407
- claimed = {
408
- ...record.message,
409
- claimed_at: now,
410
- claimed_by: owner,
411
- status: "claimed",
412
- };
413
- return { message: claimed };
414
- });
415
- if (!claimed)
416
- return undefined;
417
- fs.writeFileSync(file, `${updated.map((record) => ("raw" in record ? record.raw : JSON.stringify(record.message))).join("\n")}\n`);
418
- notifyActorWake(stateDir, address, "branch.inbox.claim", {
419
- id: claimed.id,
420
- owner,
421
- });
422
- return claimed;
423
- }
424
- finally {
425
- releaseLock();
426
- }
427
- }
428
- export function updateBranchInboxMessageStatus(stateDir, run, address, id, status, metadata = {}) {
429
- const branch = branchIdFromAddress(address, run);
430
- if (!branch)
431
- throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
432
- const releaseLock = acquireBranchInboxLock(stateDir, branch);
433
- try {
434
- const file = branchInboxFile(stateDir, branch);
435
- const records = readAllBranchInboxLines(stateDir, run, address);
436
- let changed = false;
437
- const timestampKey = `${status}_at`;
438
- const updated = records.map((record) => {
439
- if (!("message" in record) || record.message.id !== id)
440
- return record;
441
- changed = true;
442
- return {
443
- message: {
444
- ...record.message,
445
- ...metadata,
446
- [timestampKey]: new Date().toISOString(),
447
- status,
448
- },
449
- };
450
- });
451
- if (!changed)
452
- return false;
453
- const validMessages = updated.flatMap((record) => "message" in record ? [record.message] : []);
454
- const compactedIds = new Set(compactBranchInboxMessages(validMessages).map((message) => message.id));
455
- const lines = updated.flatMap((record) => {
456
- if ("raw" in record)
457
- return [record.raw];
458
- if (record.message.id && !compactedIds.has(record.message.id))
459
- return [];
460
- return [JSON.stringify(record.message)];
461
- });
462
- fs.writeFileSync(file, lines.length ? `${lines.join("\n")}\n` : "");
463
- notifyActorWake(stateDir, address, "branch.inbox.status", { id, status });
464
- return true;
465
- }
466
- finally {
467
- releaseLock();
468
- }
469
- }
470
- export function appendRoomMessage(stateDir, room, message) {
471
- const releaseLock = acquireRoomLock(stateDir, room);
472
- try {
473
- const receivedAt = new Date().toISOString();
474
- const entry = { ...message, received_at: receivedAt };
475
- fs.appendFileSync(messagesFile(stateDir, room), `${JSON.stringify(entry)}\n`);
476
- compactRoomMessages(stateDir, room);
477
- const roster = updateRosterForMessage(stateDir, room, message, receivedAt);
478
- const run = runFromRoomAddress(message.to);
479
- if (run) {
480
- writeCommunicationSnapshot(stateDir, run);
481
- notifyActorWake(stateDir, message.to, "room.message", {
482
- ...(message.from ? { from: message.from } : {}),
483
- type: message.type,
484
- });
485
- if (message.from && branchIdFromAddress(message.from, run)) {
486
- writeBranchCommunicationSnapshotDebounced(stateDir, run, message.from);
487
- }
488
- }
489
- return {
490
- message_count: readRoomMessageCount(stateDir, room),
491
- room,
492
- roster_count: Object.keys(roster).length,
493
- sent: true,
494
- };
495
- }
496
- finally {
497
- releaseLock();
498
- }
499
- }
500
- export function readRoomMessages(stateDir, room, limit = 40) {
501
- try {
502
- const lines = readJsonlTailLines(messagesFile(stateDir, room), limit);
503
- return lines.flatMap((line) => {
504
- try {
505
- return [JSON.parse(line)];
506
- }
507
- catch {
508
- return [];
509
- }
510
- });
511
- }
512
- catch (error) {
513
- if (error.code === "ENOENT")
514
- return [];
515
- throw error;
516
- }
517
- }
518
- function previewValue(value, maxLength = Limits.ROOM_MESSAGE_PREVIEW_CHARS) {
519
- if (value === undefined)
520
- return undefined;
521
- const text = typeof value === "string" ? value : JSON.stringify(value);
522
- const compact = text.replaceAll(/\s+/g, " ").trim();
523
- if (!compact)
524
- return undefined;
525
- return compact.length > maxLength
526
- ? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
527
- : compact;
528
- }
529
- export function readRoomMessagePreviews(stateDir, room, limit = 40) {
530
- return readRoomMessages(stateDir, room, limit).map((message) => ({
531
- ...(previewValue(message.body)
532
- ? { body_preview: previewValue(message.body) }
533
- : {}),
534
- ...(message.from ? { from: message.from } : {}),
535
- ...(message.summary ? { summary: message.summary } : {}),
536
- timestamp: message.received_at,
537
- to: message.to,
538
- type: message.type,
539
- }));
540
- }
541
- export function getRoomStatus(stateDir, room) {
542
- const messageCount = readRoomMessageCount(stateDir, room);
543
- const [last] = readRoomMessages(stateDir, room, 1);
544
- const compactionRead = readJsonFileResilient(path.join(roomDir(stateDir, room), "compaction.json"), undefined);
545
- const rosterRead = readJsonFileResilient(rosterFile(stateDir, room), {});
546
- const messageRead = readJsonlFileResilient(messagesFile(stateDir, room));
547
- const diagnostics = [
548
- ...messageRead.diagnostics,
549
- ...rosterRead.diagnostics,
550
- ...compactionRead.diagnostics,
551
- ];
552
- return {
553
- ...(last
554
- ? {
555
- last_message_at: last.received_at,
556
- ...(last.from ? { last_message_from: last.from } : {}),
557
- ...(last.summary ? { last_message_summary: last.summary } : {}),
558
- last_message_type: last.type,
559
- }
560
- : {}),
561
- ...(compactionRead.value ? { compaction: compactionRead.value } : {}),
562
- ...(diagnostics.length
563
- ? {
564
- diagnostics: formatStateReadDiagnostics(diagnostics),
565
- diagnostics_count: diagnostics.length,
566
- }
567
- : {}),
568
- message_count: messageCount,
569
- room,
570
- roster_count: Object.keys(rosterRead.value).length,
571
- };
572
- }
573
- export function ensureRoomMember(stateDir, run, room, address, body, summary) {
574
- const roster = readRoomRoster(stateDir, room);
575
- if (roster[address]) {
576
- return {
577
- message_count: readRoomMessageCount(stateDir, room),
578
- room,
579
- roster_count: Object.keys(roster).length,
580
- sent: true,
581
- };
582
- }
583
- return appendRoomMessage(stateDir, room, {
584
- body,
585
- from: address,
586
- summary,
587
- to: `room:${run}`,
588
- type: "actor.join",
589
- });
590
- }
591
- export function ensureDefaultRoom(stateDir, run) {
592
- return ensureRoomMember(stateDir, run, "main", `run:${run}`, { role: "run", status: "present" }, "Run joined default room");
593
- }
594
- export function readCommunicationSnapshot(stateDir) {
595
- return readJsonFileResilient(snapshotFile(stateDir), undefined).value;
596
- }
597
- export function readRoomContacts(stateDir, room, self) {
598
- return Object.values(readRoomRoster(stateDir, room))
599
- .filter((member) => member.address !== self)
600
- .map((member) => ({
601
- address: member.address,
602
- ...(member.caps !== undefined ? { caps: member.caps } : {}),
603
- ...(member.claim !== undefined ? { claim: member.claim } : {}),
604
- ...(member.parent !== undefined ? { parent: member.parent } : {}),
605
- ...(member.role !== undefined ? { role: member.role } : {}),
606
- ...(member.status !== undefined ? { status: member.status } : {}),
607
- }));
608
- }
609
- function buildCommunicationSnapshot(stateDir, run, self) {
610
- const mainRoster = Object.values(readRoomRoster(stateDir, "main"));
611
- const contacts = readRoomContacts(stateDir, "main", self);
612
- return {
613
- ...(contacts.length > 0 ? { contacts } : {}),
614
- ...(self !== `run:${run}` ? { parent: `run:${run}` } : {}),
615
- root: `run:${run}`,
616
- self,
617
- rooms: [
618
- {
619
- address: `room:${run}`,
620
- ...(mainRoster.length > 0 ? { members: mainRoster } : {}),
621
- name: "main",
622
- },
623
- ],
624
- updated_at: new Date().toISOString(),
625
- };
626
- }
627
- export function writeCommunicationSnapshot(stateDir, run) {
628
- const snapshot = buildCommunicationSnapshot(stateDir, run, `run:${run}`);
629
- writeJsonFile(snapshotFile(stateDir), snapshot);
630
- return snapshot;
631
- }
632
- export function writeBranchCommunicationSnapshot(stateDir, run, self) {
633
- const branch = branchIdFromAddress(self, run);
634
- if (!branch)
635
- throw new Error(`Expected branch:${run}/<branch>; got ${self}`);
636
- const snapshot = buildCommunicationSnapshot(stateDir, run, self);
637
- writeJsonFile(branchSnapshotFile(stateDir, branch), snapshot);
638
- return snapshot;
639
- }
640
- function writeBranchCommunicationSnapshotDebounced(stateDir, run, self) {
641
- const branch = branchIdFromAddress(self, run);
642
- if (!branch)
643
- throw new Error(`Expected branch:${run}/<branch>; got ${self}`);
644
- if (shouldDebounceSnapshot(branchSnapshotFile(stateDir, branch)))
645
- return undefined;
646
- return writeBranchCommunicationSnapshot(stateDir, run, self);
647
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Async run mailbox state.
3
- * Owns: durable run inbox records, claim locks, and handler status transitions.
4
- */
5
- export type RunInboxStatus = "queued" | "sent" | "claimed" | "handled" | "failed";
6
- export type RunInboxMessage = Record<string, unknown> & {
7
- id?: string;
8
- status?: RunInboxStatus | string;
9
- };
10
- export interface ProcessRunInboxResult {
11
- claimed: number;
12
- failed: number;
13
- handled: number;
14
- }
15
- export declare function runInboxFile(stateDir: string): string;
16
- export declare function parseRunInboxLine(line: string): RunInboxMessage | undefined;
17
- export declare function readRunInboxMessagesFromStateDir(stateDir: string): RunInboxMessage[];
18
- export declare function updateRunInboxMessageStatusInStateDir(stateDir: string, id: string, nextStatus: RunInboxStatus, metadata?: Record<string, unknown>): boolean;
19
- export declare function claimRunInboxMessageInStateDir(stateDir: string, owner?: string, statuses?: string[]): RunInboxMessage | undefined;
20
- export declare function appendRunInboxMessageInStateDir(stateDir: string, record: Record<string, unknown>): string;
21
- export declare function processRunInboxMessagesInStateDir(stateDir: string, handler: (message: RunInboxMessage) => Promise<void> | void, options?: {
22
- limit?: number;
23
- owner?: string;
24
- statuses?: string[];
25
- }): Promise<ProcessRunInboxResult>;