@llblab/pi-actors 0.42.2 → 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 (254) hide show
  1. package/AGENTS.md +121 -175
  2. package/CHANGELOG.md +16 -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 +17 -17
  26. package/dist/lib/observability.js +45 -84
  27. package/dist/lib/pi.d.ts +1 -1
  28. package/dist/lib/prompts.d.ts +1 -1
  29. package/dist/lib/prompts.js +2 -2
  30. package/dist/lib/recipe-control.d.ts +7 -0
  31. package/dist/lib/recipe-control.js +39 -0
  32. package/dist/lib/recipes-discovery.js +2 -0
  33. package/dist/lib/recipes-references.d.ts +1 -14
  34. package/dist/lib/recipes-references.js +6 -21
  35. package/dist/lib/review-projection.js +1 -5
  36. package/dist/lib/run-ui-runtime.js +2 -2
  37. package/dist/lib/runs-control-delivery.d.ts +21 -0
  38. package/dist/lib/runs-control-delivery.js +127 -0
  39. package/dist/lib/runs-controls.d.ts +35 -0
  40. package/dist/lib/runs-controls.js +144 -0
  41. package/dist/lib/runs-retention.d.ts +7 -0
  42. package/dist/lib/runs-retention.js +27 -3
  43. package/dist/lib/runs-start.js +4 -2
  44. package/dist/lib/runs-status.js +11 -6
  45. package/dist/lib/runs-trace.d.ts +24 -0
  46. package/dist/lib/runs-trace.js +98 -0
  47. package/dist/lib/runtime-notifier.d.ts +1 -1
  48. package/dist/lib/runtime-notifier.js +1 -1
  49. package/dist/lib/tools-inspect.d.ts +3 -3
  50. package/dist/lib/tools-inspect.js +203 -708
  51. package/dist/lib/tools-local.js +2 -10
  52. package/dist/lib/tools-message.d.ts +7 -7
  53. package/dist/lib/tools-message.js +95 -396
  54. package/dist/lib/tools-response.d.ts +1 -4
  55. package/dist/lib/tools-response.js +5 -39
  56. package/dist/lib/tools-spawn.js +16 -28
  57. package/dist/lib/tools.js +1 -2
  58. package/dist/lib/trace-projection.d.ts +22 -0
  59. package/dist/lib/trace-projection.js +165 -0
  60. package/dist/recipes/draft-review.json +0 -10
  61. package/dist/recipes/lens-swarm.json +0 -14
  62. package/dist/recipes/music-player.json +10 -19
  63. package/dist/recipes/pipeline-architect-coordinator.json +0 -11
  64. package/dist/recipes/pipeline-artifact-bundle.json +1 -22
  65. package/dist/recipes/pipeline-artifact-report.json +1 -18
  66. package/dist/recipes/pipeline-artifact-write.json +1 -18
  67. package/dist/recipes/pipeline-async-run-ops.json +0 -12
  68. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
  69. package/dist/recipes/pipeline-development-tasking.json +0 -12
  70. package/dist/recipes/pipeline-docs-maintenance.json +0 -12
  71. package/dist/recipes/pipeline-media-library.json +0 -12
  72. package/dist/recipes/pipeline-quorum-review.json +0 -12
  73. package/dist/recipes/pipeline-release-readiness.json +0 -12
  74. package/dist/recipes/pipeline-release-summary.json +0 -12
  75. package/dist/recipes/pipeline-repo-health.json +0 -12
  76. package/dist/recipes/pipeline-research-synthesis.json +0 -11
  77. package/dist/recipes/pipeline-review-readiness.json +0 -12
  78. package/dist/recipes/resource-locker.json +27 -0
  79. package/dist/recipes/subagent-artifact.json +0 -9
  80. package/dist/recipes/subagent-checkpoint.json +0 -10
  81. package/dist/recipes/subagent-conflict-report.json +0 -11
  82. package/dist/recipes/subagent-contradiction-map.json +0 -11
  83. package/dist/recipes/subagent-critic.json +0 -11
  84. package/dist/recipes/subagent-evidence-map.json +0 -11
  85. package/dist/recipes/subagent-followup.json +0 -10
  86. package/dist/recipes/subagent-judge.json +0 -11
  87. package/dist/recipes/subagent-merge.json +0 -11
  88. package/dist/recipes/subagent-normalize.json +0 -11
  89. package/dist/recipes/subagent-plan.json +0 -11
  90. package/dist/recipes/subagent-preflight.json +0 -11
  91. package/dist/recipes/subagent-prompt.json +0 -10
  92. package/dist/recipes/subagent-quorum.json +0 -10
  93. package/dist/recipes/subagent-review-coordinator.json +0 -14
  94. package/dist/recipes/subagent-review.json +0 -11
  95. package/dist/recipes/subagent-task-card.json +0 -11
  96. package/dist/recipes/subagent-tools.json +0 -10
  97. package/dist/recipes/subagent-verify.json +0 -11
  98. package/dist/recipes/subagents-prompts.json +0 -10
  99. package/dist/recipes/tool-review.json +0 -10
  100. package/dist/scripts/async-runner.mjs +25 -25
  101. package/dist/scripts/conformance.mjs +4 -2
  102. package/dist/scripts/locker.mjs +200 -66
  103. package/dist/scripts/music-player.mjs +159 -150
  104. package/dist/scripts/recipe-utils.mjs +6 -96
  105. package/dist/scripts/release-gates.mjs +60 -0
  106. package/dist/scripts/validate-recipe.mjs +3 -53
  107. package/dist/skills/actors/SKILL.md +53 -266
  108. package/dist/skills/swarm/SKILL.md +11 -33
  109. package/docs/0.43-baseline.md +44 -0
  110. package/docs/README.md +3 -3
  111. package/docs/actor-inspector.md +26 -64
  112. package/docs/actors-deep-reference.md +92 -50
  113. package/docs/async-runs.md +81 -328
  114. package/docs/command-templates.md +2 -2
  115. package/docs/component-recipes.md +30 -133
  116. package/docs/recipe-library.md +57 -182
  117. package/docs/task-first-recipes.md +10 -12
  118. package/docs/template-recipes.md +76 -289
  119. package/docs/tool-registry.md +41 -161
  120. package/fixtures/protocol/control-endpoint.json +6 -0
  121. package/fixtures/protocol/control-record.json +9 -0
  122. package/fixtures/protocol/recipe-summary.json +4 -12
  123. package/fixtures/protocol/trace-event.json +9 -0
  124. package/lib/async-runs.ts +202 -201
  125. package/lib/control.ts +102 -0
  126. package/lib/execution-sessions.ts +111 -0
  127. package/lib/file-state.ts +17 -4
  128. package/lib/inspector-actions.ts +2 -2
  129. package/lib/inspector-command.ts +3 -3
  130. package/lib/inspector-overlay.ts +577 -1121
  131. package/lib/inspector.ts +46 -979
  132. package/lib/limits.ts +4 -2
  133. package/lib/observability.ts +63 -104
  134. package/lib/pi.ts +1 -1
  135. package/lib/prompts.ts +2 -2
  136. package/lib/recipe-control.ts +45 -0
  137. package/lib/recipes-discovery.ts +2 -0
  138. package/lib/recipes-references.ts +9 -45
  139. package/lib/review-projection.ts +1 -5
  140. package/lib/run-ui-runtime.ts +2 -2
  141. package/lib/runs-control-delivery.ts +181 -0
  142. package/lib/runs-controls.ts +204 -0
  143. package/lib/runs-retention.ts +38 -3
  144. package/lib/runs-start.ts +4 -2
  145. package/lib/runs-status.ts +11 -6
  146. package/lib/runs-trace.ts +132 -0
  147. package/lib/runtime-notifier.ts +1 -1
  148. package/lib/tools-inspect.ts +240 -901
  149. package/lib/tools-local.ts +2 -12
  150. package/lib/tools-message.ts +112 -519
  151. package/lib/tools-response.ts +5 -52
  152. package/lib/tools-spawn.ts +16 -32
  153. package/lib/tools.ts +1 -2
  154. package/lib/trace-projection.ts +221 -0
  155. package/package.json +2 -1
  156. package/recipes/draft-review.json +0 -10
  157. package/recipes/lens-swarm.json +0 -14
  158. package/recipes/music-player.json +10 -19
  159. package/recipes/pipeline-architect-coordinator.json +0 -11
  160. package/recipes/pipeline-artifact-bundle.json +1 -22
  161. package/recipes/pipeline-artifact-report.json +1 -18
  162. package/recipes/pipeline-artifact-write.json +1 -18
  163. package/recipes/pipeline-async-run-ops.json +0 -12
  164. package/recipes/pipeline-checkpoint-continuation.json +0 -14
  165. package/recipes/pipeline-development-tasking.json +0 -12
  166. package/recipes/pipeline-docs-maintenance.json +0 -12
  167. package/recipes/pipeline-media-library.json +0 -12
  168. package/recipes/pipeline-quorum-review.json +0 -12
  169. package/recipes/pipeline-release-readiness.json +0 -12
  170. package/recipes/pipeline-release-summary.json +0 -12
  171. package/recipes/pipeline-repo-health.json +0 -12
  172. package/recipes/pipeline-research-synthesis.json +0 -11
  173. package/recipes/pipeline-review-readiness.json +0 -12
  174. package/recipes/resource-locker.json +27 -0
  175. package/recipes/subagent-artifact.json +0 -9
  176. package/recipes/subagent-checkpoint.json +0 -10
  177. package/recipes/subagent-conflict-report.json +0 -11
  178. package/recipes/subagent-contradiction-map.json +0 -11
  179. package/recipes/subagent-critic.json +0 -11
  180. package/recipes/subagent-evidence-map.json +0 -11
  181. package/recipes/subagent-followup.json +0 -10
  182. package/recipes/subagent-judge.json +0 -11
  183. package/recipes/subagent-merge.json +0 -11
  184. package/recipes/subagent-normalize.json +0 -11
  185. package/recipes/subagent-plan.json +0 -11
  186. package/recipes/subagent-preflight.json +0 -11
  187. package/recipes/subagent-prompt.json +0 -10
  188. package/recipes/subagent-quorum.json +0 -10
  189. package/recipes/subagent-review-coordinator.json +0 -14
  190. package/recipes/subagent-review.json +0 -11
  191. package/recipes/subagent-task-card.json +0 -11
  192. package/recipes/subagent-tools.json +0 -10
  193. package/recipes/subagent-verify.json +0 -11
  194. package/recipes/subagents-prompts.json +0 -10
  195. package/recipes/tool-review.json +0 -10
  196. package/scripts/async-runner.mjs +25 -25
  197. package/scripts/conformance.mjs +4 -2
  198. package/scripts/locker.mjs +200 -66
  199. package/scripts/music-player.mjs +159 -150
  200. package/scripts/recipe-utils.mjs +6 -96
  201. package/scripts/release-gates.mjs +60 -0
  202. package/scripts/validate-recipe.mjs +3 -53
  203. package/skills/actors/SKILL.md +53 -266
  204. package/skills/swarm/SKILL.md +11 -33
  205. package/dist/fixtures/protocol/actor-message-branch.json +0 -13
  206. package/dist/fixtures/protocol/mailbox-contract.json +0 -15
  207. package/dist/fixtures/protocol/room-message.json +0 -11
  208. package/dist/fixtures/protocol/room-roster.json +0 -11
  209. package/dist/fixtures/protocol/run-inbox-message.json +0 -9
  210. package/dist/fixtures/protocol/run-outbox-event.json +0 -9
  211. package/dist/lib/mailbox-loop.d.ts +0 -41
  212. package/dist/lib/mailbox-loop.js +0 -60
  213. package/dist/lib/messages.d.ts +0 -25
  214. package/dist/lib/messages.js +0 -122
  215. package/dist/lib/rooms.d.ts +0 -104
  216. package/dist/lib/rooms.js +0 -647
  217. package/dist/lib/runs-mailbox.d.ts +0 -25
  218. package/dist/lib/runs-mailbox.js +0 -146
  219. package/dist/lib/runs-messages.d.ts +0 -15
  220. package/dist/lib/runs-messages.js +0 -179
  221. package/dist/lib/runs-outbox.d.ts +0 -41
  222. package/dist/lib/runs-outbox.js +0 -87
  223. package/dist/lib/tools-mailbox.d.ts +0 -8
  224. package/dist/lib/tools-mailbox.js +0 -48
  225. package/dist/recipes/actor-worker.json +0 -39
  226. package/dist/recipes/coordinator-locker.json +0 -45
  227. package/dist/recipes/locker.json +0 -45
  228. package/dist/recipes/pipeline-room-swarm.json +0 -50
  229. package/dist/recipes/subagent-message.json +0 -32
  230. package/dist/recipes/utility-actor-message.json +0 -23
  231. package/dist/scripts/actor-worker.mjs +0 -214
  232. package/dist/scripts/coordinator.mjs +0 -799
  233. package/docs/actor-messages.md +0 -225
  234. package/fixtures/protocol/actor-message-branch.json +0 -13
  235. package/fixtures/protocol/mailbox-contract.json +0 -15
  236. package/fixtures/protocol/room-message.json +0 -11
  237. package/fixtures/protocol/room-roster.json +0 -11
  238. package/fixtures/protocol/run-inbox-message.json +0 -9
  239. package/fixtures/protocol/run-outbox-event.json +0 -9
  240. package/lib/mailbox-loop.ts +0 -144
  241. package/lib/messages.ts +0 -151
  242. package/lib/rooms.ts +0 -939
  243. package/lib/runs-mailbox.ts +0 -208
  244. package/lib/runs-messages.ts +0 -252
  245. package/lib/runs-outbox.ts +0 -144
  246. package/lib/tools-mailbox.ts +0 -56
  247. package/recipes/actor-worker.json +0 -39
  248. package/recipes/coordinator-locker.json +0 -45
  249. package/recipes/locker.json +0 -45
  250. package/recipes/pipeline-room-swarm.json +0 -50
  251. package/recipes/subagent-message.json +0 -32
  252. package/recipes/utility-actor-message.json +0 -23
  253. package/scripts/actor-worker.mjs +0 -214
  254. 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>;