@llblab/pi-actors 0.22.2 → 0.22.4

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.
@@ -24,7 +24,9 @@ function assertToolSafeRepeatConfig(config, argTypes, defaults) {
24
24
  const trimmed = node.repeat.trim();
25
25
  if (!/^\d+$/.test(trimmed)) {
26
26
  const match = trimmed.match(/^\{?([A-Za-z_][A-Za-z0-9_-]*)\.length\}?$/);
27
- if (!match || (argTypes[match[1]]?.kind !== "array" && !Array.isArray(defaults[match[1]])))
27
+ if (!match ||
28
+ (argTypes[match[1]]?.kind !== "array" &&
29
+ !Array.isArray(defaults[match[1]])))
28
30
  throw new Error("Command template repeat must be a positive integer or {array.length} with an array argument/default.");
29
31
  }
30
32
  }
@@ -39,11 +41,16 @@ function listRecipeFiles(root) {
39
41
  (entry.name.endsWith(".json") || entry.name.endsWith(".md")) &&
40
42
  entry.name !== "legacy-tool-registry-migration-report.json")
41
43
  .map((entry) => join(root, entry.name))
42
- .sort((a, b) => a.replace(/\.md$/, ".json").localeCompare(b.replace(/\.md$/, ".json")) || (a.endsWith(".json") ? -1 : 1));
44
+ .sort((a, b) => a
45
+ .replace(/\.md$/, ".json")
46
+ .localeCompare(b.replace(/\.md$/, ".json")) ||
47
+ (a.endsWith(".json") ? -1 : 1));
43
48
  }
44
49
  function getRecipeConfigDiagnostics(file, config) {
45
- if (!config)
46
- return [`Invalid recipe: ${file}`];
50
+ if (!config) {
51
+ const reason = RecipeReferences.diagnoseRawRecipeConfigFailure(file);
52
+ return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
53
+ }
47
54
  const commandTemplateConfig = typeof config.template === "object" && config.template !== null
48
55
  ? config.template
49
56
  : config;
@@ -94,7 +101,14 @@ function filesForSource(source) {
94
101
  const defaultTool = source.defaultTool === true;
95
102
  const mutableUsage = source.mutableUsage === true;
96
103
  if (source.file)
97
- return [{ root: source.root ?? source.file, file: source.file, defaultTool, mutableUsage }];
104
+ return [
105
+ {
106
+ root: source.root ?? source.file,
107
+ file: source.file,
108
+ defaultTool,
109
+ mutableUsage,
110
+ },
111
+ ];
98
112
  return source.root
99
113
  ? listRecipeFiles(source.root).map((file) => ({
100
114
  root: source.root,
@@ -139,7 +153,9 @@ export function discoverRecipeSources(sources) {
139
153
  const diagnostics = getRecipeRootDiagnostics(sources);
140
154
  for (const [id, bucket] of byId) {
141
155
  bucket.sort((a, b) => a.priority - b.priority ||
142
- a.path.replace(/\.md$/, ".json").localeCompare(b.path.replace(/\.md$/, ".json")) ||
156
+ a.path
157
+ .replace(/\.md$/, ".json")
158
+ .localeCompare(b.path.replace(/\.md$/, ".json")) ||
143
159
  (a.path.endsWith(".json") ? -1 : 1));
144
160
  const winner = bucket[0];
145
161
  winner.active = true;
@@ -209,7 +225,13 @@ function cleanupRecommendation(entry) {
209
225
  id: entry.id,
210
226
  path: entry.path,
211
227
  reason: "user recipe is a component, not an active tool",
212
- actions: ["keep component", "move into tool root", "merge", "delete", "archive"],
228
+ actions: [
229
+ "keep component",
230
+ "move into tool root",
231
+ "merge",
232
+ "delete",
233
+ "archive",
234
+ ],
213
235
  };
214
236
  }
215
237
  if (entry.shadows.length > 0) {
@@ -241,6 +263,66 @@ export function createRecipeIntegrityManifest(result) {
241
263
  })
242
264
  .sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path));
243
265
  }
266
+ function diagnosticSeverity(message) {
267
+ if (/invalid|failed to load|not found|cyclic|exceeds|must define|repeat must/i.test(message)) {
268
+ return "error";
269
+ }
270
+ if (/world-writable|group-writable|invokes bash|eval|destructive|unsafe/i.test(message)) {
271
+ return "warning";
272
+ }
273
+ return "info";
274
+ }
275
+ function diagnosticSuggestedAction(message) {
276
+ if (/must define template/i.test(message))
277
+ return "add a template field or remove the recipe";
278
+ if (/JSON|Expected|parse/i.test(message))
279
+ return "fix recipe syntax or archive the file";
280
+ if (/Markdown recipe/i.test(message))
281
+ return "fix frontmatter and add a fenced template or recipe block";
282
+ if (/cyclic/i.test(message))
283
+ return "break the import cycle";
284
+ if (/exceeds.*size/i.test(message))
285
+ return "split large prompt or data into separate files";
286
+ if (/repeat must/i.test(message))
287
+ return "use a positive repeat count or an array-typed repeat source";
288
+ if (/world-writable|group-writable/i.test(message))
289
+ return "tighten recipe root permissions";
290
+ if (/invokes bash/i.test(message))
291
+ return "audit the trusted shell boundary or move details to recipe doctor";
292
+ if (/shadows/i.test(message))
293
+ return "confirm the active override or rename one recipe";
294
+ if (/disabled/i.test(message))
295
+ return "keep disabled intentionally or delete/archive the file";
296
+ return "inspect the recipe and fix or archive it if unexpected";
297
+ }
298
+ function diagnosticDetails(result) {
299
+ const details = [];
300
+ const seen = new Set();
301
+ const push = (message, entry) => {
302
+ const key = `${entry?.path ?? "root"}\n${message}`;
303
+ if (seen.has(key))
304
+ return;
305
+ seen.add(key);
306
+ details.push({
307
+ ...(entry ? { id: entry.id, path: entry.path } : {}),
308
+ action: diagnosticSuggestedAction(message),
309
+ message,
310
+ severity: diagnosticSeverity(message),
311
+ });
312
+ };
313
+ for (const message of result.diagnostics)
314
+ push(message);
315
+ for (const entry of result.entries) {
316
+ for (const message of entry.diagnostics)
317
+ push(message, entry);
318
+ }
319
+ return details.sort((a, b) => {
320
+ const rank = { error: 0, warning: 1, info: 2 };
321
+ return (rank[a.severity] -
322
+ rank[b.severity] ||
323
+ String(a.message).localeCompare(String(b.message)));
324
+ });
325
+ }
244
326
  function recommendationForEntry(entry, activePath) {
245
327
  const recommendation = cleanupRecommendation(entry);
246
328
  if (!recommendation)
@@ -257,9 +339,11 @@ export function summarizeDiscovery(result) {
257
339
  const recommendations = result.entries
258
340
  .map((entry) => recommendationForEntry(entry, result.active.get(entry.id)?.path))
259
341
  .filter((entry) => Boolean(entry))
260
- .sort((a, b) => String(a.id).localeCompare(String(b.id)) || String(a.path).localeCompare(String(b.path)));
342
+ .sort((a, b) => String(a.id).localeCompare(String(b.id)) ||
343
+ String(a.path).localeCompare(String(b.path)));
261
344
  return {
262
- active: [...result.active.values()].map((entry) => ({
345
+ active: [...result.active.values()]
346
+ .map((entry) => ({
263
347
  id: entry.id,
264
348
  path: entry.path,
265
349
  description: entry.config?.description,
@@ -267,15 +351,26 @@ export function summarizeDiscovery(result) {
267
351
  disabled: entry.disabled,
268
352
  invalid: entry.invalid,
269
353
  shadows: entry.shadows,
270
- ...(recipeUsage(entry.config) ? { usage: recipeUsage(entry.config) } : {}),
271
- })).sort((a, b) => a.id.localeCompare(b.id)),
354
+ ...(recipeUsage(entry.config)
355
+ ? { usage: recipeUsage(entry.config) }
356
+ : {}),
357
+ }))
358
+ .sort((a, b) => a.id.localeCompare(b.id)),
272
359
  shadowed: result.entries
273
360
  .filter((entry) => entry.shadowed)
274
- .map((entry) => ({ id: entry.id, path: entry.path, shadowedBy: result.active.get(entry.id)?.path }))
361
+ .map((entry) => ({
362
+ id: entry.id,
363
+ path: entry.path,
364
+ shadowedBy: result.active.get(entry.id)?.path,
365
+ }))
275
366
  .sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path)),
276
367
  invalid: result.entries
277
368
  .filter((entry) => entry.invalid)
278
- .map((entry) => ({ id: entry.id, path: entry.path, diagnostics: entry.diagnostics }))
369
+ .map((entry) => ({
370
+ id: entry.id,
371
+ path: entry.path,
372
+ diagnostics: entry.diagnostics,
373
+ }))
279
374
  .sort((a, b) => a.id.localeCompare(b.id)),
280
375
  disabled: result.entries
281
376
  .filter((entry) => entry.disabled)
@@ -283,6 +378,7 @@ export function summarizeDiscovery(result) {
283
378
  .sort((a, b) => a.id.localeCompare(b.id)),
284
379
  recommendations,
285
380
  diagnostics: result.diagnostics,
381
+ diagnostic_details: diagnosticDetails(result),
286
382
  integrity_manifest: createRecipeIntegrityManifest(result),
287
383
  };
288
384
  }
@@ -297,7 +393,10 @@ export function toRegisteredTool(entry) {
297
393
  ? {
298
394
  ...argTemplate,
299
395
  ...(cfg.args !== undefined ? { args: cfg.args } : {}),
300
- defaults: { ...(argTemplate.defaults ?? {}), ...(cfg.defaults ?? {}) },
396
+ defaults: {
397
+ ...(argTemplate.defaults ?? {}),
398
+ ...(cfg.defaults ?? {}),
399
+ },
301
400
  }
302
401
  : { args: cfg.args, defaults: cfg.defaults ?? {}, template: argTemplate };
303
402
  const explicitArgTypes = Object.fromEntries((cfg.args ?? []).map((arg) => {
@@ -312,13 +411,19 @@ export function toRegisteredTool(entry) {
312
411
  template,
313
412
  recipe: cfg,
314
413
  args: Schema.getToolArgNames(argTemplateConfig),
315
- defaults: Object.fromEntries(Object.entries(cfg.defaults ?? {}).map(([key, value]) => [key, String(value)])),
414
+ defaults: Object.fromEntries(Object.entries(cfg.defaults ?? {}).map(([key, value]) => [
415
+ key,
416
+ String(value),
417
+ ])),
316
418
  ...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
317
419
  ...(entry.mutableUsage ? { sourcePath: entry.path } : {}),
318
420
  ...(cfg.args ? { storedArgs: cfg.args } : {}),
319
421
  ...(cfg.defaults
320
422
  ? {
321
- storedDefaults: Object.fromEntries(Object.entries(cfg.defaults).map(([key, value]) => [key, String(value)])),
423
+ storedDefaults: Object.fromEntries(Object.entries(cfg.defaults).map(([key, value]) => [
424
+ key,
425
+ String(value),
426
+ ])),
322
427
  }
323
428
  : {}),
324
429
  };
@@ -57,6 +57,7 @@ export interface ReadResolvedRecipeConfigOptions {
57
57
  }
58
58
  export declare function resolveRecipePath(value: string, recipeRoot?: string): string;
59
59
  export declare function getRecipePath(value: unknown, recipeRoot?: string): string | undefined;
60
+ export declare function diagnoseRawRecipeConfigFailure(path: string): string | undefined;
60
61
  export declare function readRawRecipeConfig(path: string): Record<string, unknown> | undefined;
61
62
  export declare function getRecipeIdFromPath(file: string): string;
62
63
  export declare function readResolvedRecipeConfig(file: string, stack?: string[], options?: ReadResolvedRecipeConfigOptions): TemplateRecipeConfig | undefined;
@@ -29,7 +29,10 @@ export function resolveRecipePath(value, recipeRoot = Paths.getRecipeRoot()) {
29
29
  }
30
30
  function isBareRecipeName(value) {
31
31
  const trimmed = value.trim();
32
- return Boolean(trimmed) && !trimmed.includes("/") && !trimmed.startsWith("~") && !trimmed.includes("{");
32
+ return (Boolean(trimmed) &&
33
+ !trimmed.includes("/") &&
34
+ !trimmed.startsWith("~") &&
35
+ !trimmed.includes("{"));
33
36
  }
34
37
  function recipeNameFiles(value) {
35
38
  const trimmed = value.trim();
@@ -260,6 +263,42 @@ function parseMarkdownRecipeConfig(content) {
260
263
  }
261
264
  return { ...frontmatter, template: text };
262
265
  }
266
+ export function diagnoseRawRecipeConfigFailure(path) {
267
+ if (!existsSync(path))
268
+ return "file does not exist";
269
+ const size = statSync(path).size;
270
+ if (size > MAX_RECIPE_FILE_BYTES) {
271
+ return `file exceeds size limit ${MAX_RECIPE_FILE_BYTES} bytes`;
272
+ }
273
+ try {
274
+ const content = readFileSync(path, "utf8");
275
+ if (path.endsWith(".md")) {
276
+ const lines = content.split(/\r?\n/);
277
+ if (lines[0]?.trim() !== "---") {
278
+ return "Markdown recipe must start with frontmatter";
279
+ }
280
+ const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
281
+ if (end === -1)
282
+ return "Markdown recipe frontmatter is not closed";
283
+ const parsed = parseMarkdownRecipeConfig(content);
284
+ if (!parsed)
285
+ return "Markdown recipe has no executable recipe/template fence";
286
+ if (!Object.hasOwn(parsed, "template"))
287
+ return "recipe must define template";
288
+ return undefined;
289
+ }
290
+ const raw = JSON.parse(content);
291
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
292
+ return "JSON recipe must be an object";
293
+ }
294
+ if (!Object.hasOwn(raw, "template"))
295
+ return "recipe must define template";
296
+ return undefined;
297
+ }
298
+ catch (error) {
299
+ return error instanceof Error ? error.message : String(error);
300
+ }
301
+ }
263
302
  export function readRawRecipeConfig(path) {
264
303
  if (!existsSync(path))
265
304
  return undefined;
@@ -82,7 +82,11 @@ function getInputTemplate(value) {
82
82
  throw new Error(Output.formatToolText("Tool template sequence is empty."));
83
83
  return value;
84
84
  }
85
- throw new Error(Output.formatToolText("Tool template must be a string or sequence."));
85
+ if (value && typeof value === "object") {
86
+ CommandTemplates.expandCommandTemplateConfigs(value);
87
+ return value;
88
+ }
89
+ throw new Error(Output.formatToolText("Tool template must be a string, object, or sequence."));
86
90
  }
87
91
  function buildConfig(name, input, existing) {
88
92
  const explicitArgs = input.args === undefined
package/dist/lib/tools.js CHANGED
@@ -361,6 +361,7 @@ export function createRegisterToolDefinition(deps) {
361
361
  state_dir: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir),
362
362
  template: unionSchema([
363
363
  stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
364
+ looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
364
365
  arraySchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateArray),
365
366
  nullSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateNull),
366
367
  ]),
@@ -390,6 +391,16 @@ function messageBodyToToolParams(message) {
390
391
  return {};
391
392
  return { input: message.body };
392
393
  }
394
+ function formatToolActorFailure(tool, message, params, error) {
395
+ const original = error instanceof Error ? error.message : String(error);
396
+ const paramsPreview = compactPreview(params, 240) ?? "{}";
397
+ return Object.assign(new Error(`tool actor ${tool} failed for message type ${message.type}: ${original}; params=${paramsPreview}`), {
398
+ message_type: message.type,
399
+ original_error: original,
400
+ params_preview: paramsPreview,
401
+ tool,
402
+ });
403
+ }
393
404
  function runIdFromActorAddress(address) {
394
405
  if (!address)
395
406
  return undefined;
@@ -672,15 +683,16 @@ export function createInspectToolDefinition(deps = {}) {
672
683
  if (view !== "mailbox")
673
684
  throw new Error("inspect branch:<run>/<branch> supports view=mailbox.");
674
685
  const status = assertRunAccessibleToContext(runId, ctx);
675
- const messages = ActorRooms.readBranchInboxMessages(String(status.state_dir ?? ""), runId, target, Number(input.lines || 40));
686
+ const branchInbox = ActorRooms.readBranchInboxDiagnostics(String(status.state_dir ?? ""), runId, target, Number(input.lines || 40));
687
+ const messages = branchInbox.messages;
676
688
  return {
677
689
  content: [
678
690
  {
679
691
  type: "text",
680
- text: maybeJsonText(messages, input.verbose === true, compactBranchInbox(messages.map((message) => ({ ...message })))),
692
+ text: maybeJsonText(input.verbose === true ? branchInbox : messages, input.verbose === true, `${compactBranchInbox(messages.map((message) => ({ ...message })))}${branchInbox.corrupted > 0 ? `\ncorrupted=${branchInbox.corrupted}` : ""}`),
681
693
  },
682
694
  ],
683
- details: { messages },
695
+ details: { corrupted: branchInbox.corrupted, messages },
684
696
  };
685
697
  }
686
698
  switch (view) {
@@ -806,9 +818,7 @@ export function createActorMessageToolDefinition(deps = {}) {
806
818
  const runId = address.value;
807
819
  if (message.from)
808
820
  assertMessageSenderBelongsToRun(message, runId, `branch:${runId}/<branch>`);
809
- const status = message.from
810
- ? assertRunExistsForActorMessage(runId)
811
- : assertRunAccessibleToContext(runId, ctx);
821
+ const status = assertRunAccessibleToContext(runId, ctx);
812
822
  const stateDir = String(status.state_dir ?? "");
813
823
  if (stateDir && address.branch) {
814
824
  const ensureBranchMember = (actorAddress) => {
@@ -834,7 +844,7 @@ export function createActorMessageToolDefinition(deps = {}) {
834
844
  else if (address.kind === "room" && address.value && address.room) {
835
845
  const runId = address.value;
836
846
  assertMessageSenderBelongsToRun(message, runId, `room:${runId}`);
837
- const status = assertRunExistsForActorMessage(runId);
847
+ const status = assertRunAccessibleToContext(runId, ctx);
838
848
  const stateDir = String(status.state_dir ?? "");
839
849
  if (!stateDir)
840
850
  throw new Error(`${message.to} has no run state directory.`);
@@ -853,7 +863,14 @@ export function createActorMessageToolDefinition(deps = {}) {
853
863
  if (!tool || typeof tool.execute !== "function") {
854
864
  throw new Error(`tool actor not found or not executable: ${address.value}`);
855
865
  }
856
- const toolResult = await tool.execute(`message:${message.type}`, messageBodyToToolParams(message), _signal, _onUpdate, ctx);
866
+ const toolParams = messageBodyToToolParams(message);
867
+ let toolResult;
868
+ try {
869
+ toolResult = await tool.execute(`message:${message.type}`, toolParams, _signal, _onUpdate, ctx);
870
+ }
871
+ catch (error) {
872
+ throw formatToolActorFailure(address.value, message, toolParams, error);
873
+ }
857
874
  result = {
858
875
  invoked: true,
859
876
  sent: true,
package/index.ts CHANGED
@@ -174,7 +174,7 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
174
174
  observedRuns,
175
175
  observedRunEventLines,
176
176
  summary,
177
- transitions.map((transition) => transition.run),
177
+ transitions.map((transition) => transition.stateDir ?? transition.run),
178
178
  );
179
179
  for (const event of outboxEvents) {
180
180
  if (!Observability.shouldNotifyRunOutboxEvent(event)) continue;
@@ -383,24 +383,86 @@ function updateRosterForMessage(
383
383
  return roster;
384
384
  }
385
385
 
386
- export function readBranchInboxMessages(
386
+ export interface BranchInboxRecord extends ActorMessage {
387
+ id?: string;
388
+ queued_at?: string;
389
+ status?: string;
390
+ }
391
+
392
+ export interface BranchInboxReadResult {
393
+ corrupted: number;
394
+ messages: BranchInboxRecord[];
395
+ }
396
+
397
+ function readBranchInboxFile(
387
398
  stateDir: string,
388
399
  run: string,
389
400
  address: string,
390
401
  limit = 40,
391
- ): Array<ActorMessage & { id?: string; queued_at?: string; status?: string }> {
402
+ ): BranchInboxReadResult {
392
403
  const branch = branchIdFromAddress(address, run);
393
404
  if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
394
405
  try {
395
- return readJsonlTailLines(branchInboxFile(stateDir, branch), limit).map(
396
- (line) => JSON.parse(line),
397
- );
406
+ const lines = readJsonlTailLines(branchInboxFile(stateDir, branch), limit);
407
+ const messages: BranchInboxRecord[] = [];
408
+ let corrupted = 0;
409
+ for (const line of lines) {
410
+ try {
411
+ messages.push(JSON.parse(line));
412
+ } catch {
413
+ corrupted += 1;
414
+ }
415
+ }
416
+ return { corrupted, messages };
417
+ } catch (error) {
418
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { corrupted: 0, messages: [] };
419
+ throw error;
420
+ }
421
+ }
422
+
423
+ function readAllBranchInboxLines(
424
+ stateDir: string,
425
+ run: string,
426
+ address: string,
427
+ ): Array<{ raw: string } | { message: BranchInboxRecord }> {
428
+ const branch = branchIdFromAddress(address, run);
429
+ if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
430
+ try {
431
+ const content = fs.readFileSync(branchInboxFile(stateDir, branch), "utf8");
432
+ return content
433
+ .split("\n")
434
+ .filter((line) => line.length > 0)
435
+ .map((line) => {
436
+ try {
437
+ return { message: JSON.parse(line) as BranchInboxRecord };
438
+ } catch {
439
+ return { raw: line };
440
+ }
441
+ });
398
442
  } catch (error) {
399
443
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
400
444
  throw error;
401
445
  }
402
446
  }
403
447
 
448
+ export function readBranchInboxMessages(
449
+ stateDir: string,
450
+ run: string,
451
+ address: string,
452
+ limit = 40,
453
+ ): BranchInboxRecord[] {
454
+ return readBranchInboxFile(stateDir, run, address, limit).messages;
455
+ }
456
+
457
+ export function readBranchInboxDiagnostics(
458
+ stateDir: string,
459
+ run: string,
460
+ address: string,
461
+ limit = 40,
462
+ ): BranchInboxReadResult {
463
+ return readBranchInboxFile(stateDir, run, address, limit);
464
+ }
465
+
404
466
  export function getBranchInboxTerminalRetainLimit(): number {
405
467
  const value = Number(process.env.PI_ACTORS_BRANCH_INBOX_TERMINAL_RETAINED ?? "");
406
468
  return Number.isInteger(value) && value >= 0
@@ -458,17 +520,23 @@ export function updateBranchInboxMessageStatus(
458
520
  const releaseLock = acquireBranchInboxLock(stateDir, branch);
459
521
  try {
460
522
  const file = branchInboxFile(stateDir, branch);
461
- const messages = readBranchInboxMessages(stateDir, run, address, Number.MAX_SAFE_INTEGER);
523
+ const records = readAllBranchInboxLines(stateDir, run, address);
462
524
  let changed = false;
463
525
  const timestampKey = `${status}_at`;
464
- const updated = messages.map((message) => {
465
- if (message.id !== id) return message;
526
+ const updated = records.map((record) => {
527
+ if (!("message" in record) || record.message.id !== id) return record;
466
528
  changed = true;
467
- return { ...message, ...metadata, [timestampKey]: new Date().toISOString(), status };
529
+ return { message: { ...record.message, ...metadata, [timestampKey]: new Date().toISOString(), status } };
468
530
  });
469
531
  if (!changed) return false;
470
- const compacted = compactBranchInboxMessages(updated);
471
- fs.writeFileSync(file, `${compacted.map((message) => JSON.stringify(message)).join("\n")}\n`);
532
+ const validMessages = updated.flatMap((record) => "message" in record ? [record.message] : []);
533
+ const compactedIds = new Set(compactBranchInboxMessages(validMessages).map((message) => message.id));
534
+ const lines = updated.flatMap((record) => {
535
+ if ("raw" in record) return [record.raw];
536
+ if (record.message.id && !compactedIds.has(record.message.id)) return [];
537
+ return [JSON.stringify(record.message)];
538
+ });
539
+ fs.writeFileSync(file, lines.length ? `${lines.join("\n")}\n` : "");
472
540
  notifyActorWake(stateDir, address, "branch.inbox.status", { id, status });
473
541
  return true;
474
542
  } finally {