pi-crew 0.10.2 → 0.10.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.
Files changed (124) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +249 -0
  3. package/README.md +5 -1
  4. package/dist/index.mjs +10844 -7250
  5. package/docs/architecture.md +4 -4
  6. package/docs/commands-reference.md +3 -0
  7. package/docs/publishing.md +15 -3
  8. package/install.mjs +90 -39
  9. package/package.json +9 -3
  10. package/schema.json +11 -0
  11. package/scripts/README.md +4 -3
  12. package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +7 -2
  13. package/skills/real-test-pi-crew/SKILL.md +428 -82
  14. package/src/config/config-merge.ts +11 -1
  15. package/src/config/config-validation.ts +40 -1
  16. package/src/config/config.ts +28 -6
  17. package/src/config/defaults.ts +35 -10
  18. package/src/config/env-vars.ts +27 -2
  19. package/src/config/migration-validator.ts +113 -0
  20. package/src/config/types.ts +36 -0
  21. package/src/extension/cross-extension-rpc.ts +3 -7
  22. package/src/extension/register.ts +13 -0
  23. package/src/extension/registration/lifecycle-handlers.ts +40 -9
  24. package/src/extension/registration/observability.ts +3 -7
  25. package/src/extension/registration/subagent-tools.ts +3 -7
  26. package/src/extension/registration/team-tool.ts +56 -12
  27. package/src/extension/registration/ui.ts +3 -8
  28. package/src/extension/registration/viewers.ts +3 -10
  29. package/src/extension/team-manager-command.ts +3 -7
  30. package/src/extension/team-tool/api/agent-control.ts +17 -10
  31. package/src/extension/team-tool/api/heartbeat.ts +4 -3
  32. package/src/extension/team-tool/api/mailbox.ts +33 -20
  33. package/src/extension/team-tool/api/plan-approval.ts +5 -5
  34. package/src/extension/team-tool/api/task-claims.ts +8 -7
  35. package/src/extension/team-tool/cancel.ts +6 -0
  36. package/src/extension/team-tool/doctor.ts +364 -7
  37. package/src/extension/team-tool/handle-settings.ts +23 -1
  38. package/src/extension/team-tool/inspect.ts +10 -2
  39. package/src/extension/team-tool/run.ts +3 -7
  40. package/src/extension/team-tool/status.ts +12 -0
  41. package/src/extension/team-tool.ts +41 -16
  42. package/src/hooks/registry.ts +62 -56
  43. package/src/prompt/inbox-poll.ts +90 -0
  44. package/src/prompt/message-tool.ts +166 -0
  45. package/src/prompt/prompt-runtime.ts +201 -18
  46. package/src/prompt/scratchpad-lifecycle.ts +3 -3
  47. package/src/prompt/surface-worker.ts +720 -0
  48. package/src/prompt/worker-events-channel.ts +49 -3
  49. package/src/runtime/async-runner.ts +29 -1
  50. package/src/runtime/background-runner.ts +43 -42
  51. package/src/runtime/broker/broker-issuer.ts +27 -2
  52. package/src/runtime/broker/crew-broker-tokens.ts +56 -4
  53. package/src/runtime/broker/crew-broker.ts +334 -443
  54. package/src/runtime/broker/delegate/delegate-event.ts +37 -0
  55. package/src/runtime/broker/mailbox-observer/mailbox-fanout.ts +59 -0
  56. package/src/runtime/broker/protocol/connection-state.ts +103 -0
  57. package/src/runtime/broker/protocol/events-replay.ts +68 -0
  58. package/src/runtime/broker/protocol/manifest-loader.ts +20 -0
  59. package/src/runtime/broker/protocol/msg-inbox.ts +69 -0
  60. package/src/runtime/broker/protocol/request-parsers.ts +175 -0
  61. package/src/runtime/broker/protocol/wait-auth.ts +46 -0
  62. package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
  63. package/src/runtime/child-pi/child-pi-streams.ts +9 -1
  64. package/src/runtime/child-pi/child-pi.ts +368 -5
  65. package/src/runtime/crew-agent-records.ts +13 -1
  66. package/src/runtime/dispatch-batch.ts +12 -1
  67. package/src/runtime/event-log-tail-source.ts +374 -0
  68. package/src/runtime/finalize-run.ts +19 -7
  69. package/src/runtime/foreground-control.ts +19 -6
  70. package/src/runtime/goal-workflow/dynamic-workflow-context.ts +6 -0
  71. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -0
  72. package/src/runtime/goal-workflow/goal-loop-runner.ts +29 -27
  73. package/src/runtime/goal-workflow/goal-state-store.ts +3 -0
  74. package/src/runtime/heartbeat/heartbeat-watcher.ts +3 -3
  75. package/src/runtime/live-session/live-agent-manager.ts +34 -1
  76. package/src/runtime/live-session/live-control-realtime.ts +10 -0
  77. package/src/runtime/live-session/live-session-runtime.ts +47 -27
  78. package/src/runtime/manifest-cache.ts +128 -17
  79. package/src/runtime/model/pi-args.ts +59 -65
  80. package/src/runtime/output/sidechain-output.ts +61 -6
  81. package/src/runtime/plan-replan.ts +3 -0
  82. package/src/runtime/process/proc-stat.ts +46 -0
  83. package/src/runtime/process/zombie-scanner.ts +32 -19
  84. package/src/runtime/spawn-policy.ts +27 -41
  85. package/src/runtime/stale-reconciler.ts +28 -3
  86. package/src/runtime/supervisor-contact.ts +3 -0
  87. package/src/runtime/surface/degrade.ts +776 -0
  88. package/src/runtime/surface/herdr-provider.ts +546 -0
  89. package/src/runtime/surface/launch-script.ts +172 -0
  90. package/src/runtime/surface/resolve-surface.ts +274 -0
  91. package/src/runtime/surface/surface-provider.ts +129 -0
  92. package/src/runtime/surface/surface-spawn.ts +475 -0
  93. package/src/runtime/surface/tmux-provider.ts +400 -0
  94. package/src/runtime/task-runner/child-executor.ts +80 -0
  95. package/src/runtime/task-runner/post-execution.ts +57 -2
  96. package/src/runtime/task-runner/prompt-builder.ts +1 -0
  97. package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
  98. package/src/runtime/task-runner/state-helpers.ts +54 -30
  99. package/src/runtime/task-runner.ts +4 -2
  100. package/src/runtime/team-runner.ts +104 -3
  101. package/src/schema/config-schema.ts +24 -0
  102. package/src/state/atomic-write.ts +219 -40
  103. package/src/state/coordination/locks.ts +7 -5
  104. package/src/state/coordination/mailbox.ts +56 -10
  105. package/src/state/event-log/cursor.ts +413 -23
  106. package/src/state/event-log/event-log.ts +120 -113
  107. package/src/state/event-log/sequence-cache.ts +21 -3
  108. package/src/state/stores/ownership-map.ts +5 -4
  109. package/src/state/stores/plan-store.ts +12 -0
  110. package/src/state/stores/state-store.ts +103 -6
  111. package/src/state/types.ts +51 -0
  112. package/src/ui/inline-panel/agent-pane.ts +3 -0
  113. package/src/ui/powerbar-publisher.ts +3 -7
  114. package/src/ui/render-diff.ts +16 -8
  115. package/src/ui/run-action-dispatcher.ts +7 -10
  116. package/src/ui/run-dashboard.ts +87 -42
  117. package/src/ui/run-event-bus.ts +10 -1
  118. package/src/ui/run-snapshot-cache.ts +83 -35
  119. package/src/ui/settings-overlay.ts +4 -1
  120. package/src/ui/transcript-cache.ts +101 -13
  121. package/src/ui/transcript-viewer.ts +92 -24
  122. package/src/ui/widget/index.ts +32 -8
  123. package/src/utils/visual.ts +43 -0
  124. package/src/worktree/worktree-manager.ts +65 -4
@@ -18,17 +18,13 @@ import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
18
18
  // Team tool handler — lazy-loaded because team-tool.ts imports many modules
19
19
  import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
20
20
 
21
- let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
22
21
  async function handleTeamTool(
23
22
  params: Parameters<typeof HandleTeamToolFn>[0],
24
23
  ctx: Parameters<typeof HandleTeamToolFn>[1],
25
24
  ): Promise<ReturnType<typeof HandleTeamToolFn>> {
26
- if (!_cachedHandleTeamTool) {
27
- // LAZY: team-tool.ts imports many modules — defer until first use.
28
- const mod = await import("../team-tool.ts");
29
- _cachedHandleTeamTool = mod.handleTeamTool;
30
- }
31
- return _cachedHandleTeamTool(params, ctx);
25
+ // LAZY: team-tool.ts imports many modules — defer until first use.
26
+ const mod = await import("../team-tool.ts");
27
+ return mod.handleTeamTool(params, ctx);
32
28
  }
33
29
 
34
30
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
@@ -147,6 +143,52 @@ export function detectUnrecognizedParams(schema: TObject, params: unknown): stri
147
143
  ].join("\n");
148
144
  }
149
145
 
146
+ // PERF (2026-08-24): cheap read actions (status/summary/events/get/list) re-rendered
147
+ // the widget + powerbar after every call; the event bus and render tick already
148
+ // reflect run-state changes. Refresh only on actions that mutate run state
149
+ // outside the watched files. Names mirror the five domain dispatcher tables in
150
+ // src/schema/team-tool-schema.ts (RUN/STATUS/CONTROL/MANAGE/AUTOMATE). Actions
151
+ // whose effect depends on sub-params (api: approve-plan/agent-control/mailbox;
152
+ // plans: approve/reject; anchor/auto-summarize/auto_boomerang: set/clear/toggle;
153
+ // config/autonomy: show vs write) are included — a redundant refresh on their
154
+ // read sub-actions is cheap, a missed refresh after a write leaves stale UI.
155
+ const MUTATING_ACTIONS = new Set([
156
+ // run domain
157
+ "run",
158
+ "parallel",
159
+ "orchestrate",
160
+ "resume",
161
+ "retry",
162
+ "steer",
163
+ "goal",
164
+ "plans",
165
+ // control domain
166
+ "cancel",
167
+ "invalidate",
168
+ "respond",
169
+ "cleanup",
170
+ "prune",
171
+ "forget",
172
+ // manage domain
173
+ "create",
174
+ "update",
175
+ "delete",
176
+ "init",
177
+ "config",
178
+ "autonomy",
179
+ "settings",
180
+ "workflow-create",
181
+ "workflow-save",
182
+ "workflow-delete",
183
+ "import",
184
+ // automate domain
185
+ "schedule",
186
+ "anchor",
187
+ "auto-summarize",
188
+ "auto_boomerang",
189
+ "api",
190
+ ]);
191
+
150
192
  export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
151
193
  const tool: ToolDefinition = {
152
194
  name: "team",
@@ -229,11 +271,13 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
229
271
  timestamp: Date.now(),
230
272
  });
231
273
  }
232
- const config = loadConfig(toolCtx.cwd).config.ui;
233
- const cache = deps.getManifestCache(toolCtx.cwd);
234
- const snapshotCache = deps.getRunSnapshotCache?.(toolCtx.cwd);
235
- updateCrewWidget(toolCtx, deps.widgetState, config, cache, snapshotCache);
236
- updatePiCrewPowerbar(pi.events, toolCtx.cwd, config, cache, snapshotCache, toolCtx);
274
+ if (MUTATING_ACTIONS.has(resolved.action ?? "list")) {
275
+ const config = loadConfig(toolCtx.cwd).config.ui;
276
+ const cache = deps.getManifestCache(toolCtx.cwd);
277
+ const snapshotCache = deps.getRunSnapshotCache?.(toolCtx.cwd);
278
+ updateCrewWidget(toolCtx, deps.widgetState, config, cache, snapshotCache);
279
+ updatePiCrewPowerbar(pi.events, toolCtx.cwd, config, cache, snapshotCache, toolCtx);
280
+ }
237
281
  return output;
238
282
  } finally {
239
283
  signal?.removeEventListener("abort", abort);
@@ -27,15 +27,10 @@ import type { CrewWidgetState } from "../../ui/widget/index.ts";
27
27
  import { stopCrewWidget, updateCrewWidget } from "../../ui/widget/index.ts";
28
28
  import { logInternalError } from "../../utils/internal-error.ts";
29
29
 
30
- /** Cached live-run-sidebar constructor (lazy-loaded on first overlay open). */
31
- let _cachedLiveRunSidebar: typeof LiveRunSidebarType | undefined;
32
30
  async function importLiveRunSidebar(): Promise<typeof LiveRunSidebarType> {
33
- if (!_cachedLiveRunSidebar) {
34
- // LAZY: defer LiveRunSidebar import until the user opens a sidebar overlay.
35
- const mod = await import("../../ui/live-run-sidebar.ts");
36
- _cachedLiveRunSidebar = mod.LiveRunSidebar;
37
- }
38
- return _cachedLiveRunSidebar;
31
+ // LAZY: defer LiveRunSidebar import until the user opens a sidebar overlay.
32
+ const mod = await import("../../ui/live-run-sidebar.ts");
33
+ return mod.LiveRunSidebar;
39
34
  }
40
35
 
41
36
  /** Mutable state owned by register.ts. */
@@ -8,17 +8,10 @@ import { asCrewTheme } from "../../ui/theme-adapter.ts";
8
8
  // Lazy-loaded: DurableTranscriptViewer is 658ms — only needed for /crew transcript command
9
9
  import type { DurableTranscriptViewer as DurableTranscriptViewerType } from "../../ui/transcript-viewer.ts";
10
10
 
11
- let _cachedViewer: typeof DurableTranscriptViewerType | undefined;
12
- let _viewerPromise: Promise<typeof DurableTranscriptViewerType> | undefined;
13
11
  async function getViewer(): Promise<typeof DurableTranscriptViewerType> {
14
- if (_cachedViewer) return _cachedViewer;
15
- if (!_viewerPromise) {
16
- _viewerPromise = import("../../ui/transcript-viewer.ts").then((mod) => {
17
- _cachedViewer = mod.DurableTranscriptViewer;
18
- return mod.DurableTranscriptViewer;
19
- });
20
- }
21
- return _viewerPromise;
12
+ // LAZY: DurableTranscriptViewer is 658ms — only needed for /crew transcript.
13
+ const mod = await import("../../ui/transcript-viewer.ts");
14
+ return mod.DurableTranscriptViewer;
22
15
  }
23
16
 
24
17
  export async function selectAgentTask(
@@ -3,17 +3,13 @@ import { listRuns } from "./run-index.ts";
3
3
  // Lazy-loaded: team-tool.ts pulls in entire runtime chain.
4
4
  import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
5
5
 
6
- let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
7
6
  async function handleTeamTool(
8
7
  params: Parameters<typeof HandleTeamToolFn>[0],
9
8
  ctx: Parameters<typeof HandleTeamToolFn>[1],
10
9
  ): Promise<Awaited<ReturnType<typeof HandleTeamToolFn>>> {
11
- if (!_cachedHandleTeamTool) {
12
- // LAZY: team-tool.ts pulls in the entire runtime chain.
13
- const mod = await import("./team-tool.ts");
14
- _cachedHandleTeamTool = mod.handleTeamTool;
15
- }
16
- return _cachedHandleTeamTool(params, ctx);
10
+ // LAZY: team-tool.ts pulls in the entire runtime chain.
11
+ const mod = await import("./team-tool.ts");
12
+ return mod.handleTeamTool(params, ctx);
17
13
  }
18
14
 
19
15
  import { isToolError, textFromToolResult } from "./tool-result.ts";
@@ -22,7 +22,8 @@ import {
22
22
  appendSteeringMessage,
23
23
  appendSteeringMessageAsync,
24
24
  } from "../../../state/coordination/mailbox.ts";
25
- import { appendEvent } from "../../../state/event-log/event-log.ts";
25
+ import { appendEvent, appendEventBuffered } from "../../../state/event-log/event-log.ts";
26
+ import { logInternalError } from "../../../utils/internal-error.ts";
26
27
  import type { ApiOperationHandler } from "./handler-context.ts";
27
28
 
28
29
  export const handleNudgeAgent: ApiOperationHandler = (hctx) => {
@@ -54,13 +55,19 @@ export const handleNudgeAgent: ApiOperationHandler = (hctx) => {
54
55
  priority: "normal",
55
56
  data: { source: "nudge-agent" },
56
57
  });
57
- appendEvent(loaded.manifest.eventsPath, {
58
- type: "agent.nudged",
59
- runId: loaded.manifest.runId,
60
- taskId: agent.taskId,
61
- message: messageText,
62
- data: { agentId: agent.id, mailboxMessageId: message.id },
63
- });
58
+ // Read-your-writes (CI 2026-09-11, phase8 integration): the nudge caller
59
+ // reads events.jsonl synchronously right after dispatch — sync appendEvent.
60
+ try {
61
+ appendEvent(loaded.manifest.eventsPath, {
62
+ type: "agent.nudged",
63
+ runId: loaded.manifest.runId,
64
+ taskId: agent.taskId,
65
+ message: messageText,
66
+ data: { agentId: agent.id, mailboxMessageId: message.id },
67
+ });
68
+ } catch (e) {
69
+ logInternalError("api.agent-control.append", e, "type=agent.nudged");
70
+ }
64
71
  ctx.events?.emit?.("crew.mailbox.message", {
65
72
  runId: loaded.manifest.runId,
66
73
  id: message.id,
@@ -314,7 +321,7 @@ export const handleLiveAgentControl: ApiOperationHandler = async (hctx) => {
314
321
  : undefined;
315
322
  publishLiveControlRealtime(request);
316
323
  ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
317
- appendEvent(loaded.manifest.eventsPath, {
324
+ appendEventBuffered(loaded.manifest.eventsPath, {
318
325
  type: "agent.control.queued",
319
326
  runId: loaded.manifest.runId,
320
327
  taskId: agent.taskId,
@@ -324,7 +331,7 @@ export const handleLiveAgentControl: ApiOperationHandler = async (hctx) => {
324
331
  mailboxMessageId: mailboxMessage?.id,
325
332
  realtime: true,
326
333
  },
327
- });
334
+ }).catch((e) => logInternalError("api.agent-control.buffered", e, "type=agent.control.queued"));
328
335
  return result(JSON.stringify({ queued: true, request, mailboxMessage }, null, 2), {
329
336
  action: "api",
330
337
  status: "ok",
@@ -8,8 +8,9 @@
8
8
  import { touchWorkerHeartbeat } from "../../../runtime/heartbeat/worker-heartbeat.ts";
9
9
  import { isTerminalTaskStatus } from "../../../state/contracts.ts";
10
10
  import { withRunLockSync } from "../../../state/coordination/locks.ts";
11
- import { appendEvent } from "../../../state/event-log/event-log.ts";
11
+ import { appendEventBuffered } from "../../../state/event-log/event-log.ts";
12
12
  import { loadRunManifestById, saveRunTasks } from "../../../state/stores/state-store.ts";
13
+ import { logInternalError } from "../../../utils/internal-error.ts";
13
14
  import { RUN_NOT_FOUND_HINT } from "../run-not-found.ts";
14
15
  import type { ApiOperationHandler } from "./handler-context.ts";
15
16
 
@@ -79,12 +80,12 @@ export const handleWriteHeartbeat: ApiOperationHandler = (hctx) => {
79
80
  );
80
81
  const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? { ...item, heartbeat } : item));
81
82
  saveRunTasks(fresh.manifest, tasks);
82
- appendEvent(fresh.manifest.eventsPath, {
83
+ appendEventBuffered(fresh.manifest.eventsPath, {
83
84
  type: "worker.heartbeat",
84
85
  runId: fresh.manifest.runId,
85
86
  taskId: freshTask.id,
86
87
  data: { ...heartbeat },
87
- });
88
+ }).catch((e) => logInternalError("api.heartbeat.buffered", e, "type=worker.heartbeat"));
88
89
  return result(JSON.stringify(heartbeat, null, 2), {
89
90
  action: "api",
90
91
  status: "ok",
@@ -16,7 +16,8 @@ import {
16
16
  readMailboxMessage,
17
17
  validateMailbox,
18
18
  } from "../../../state/coordination/mailbox.ts";
19
- import { appendEvent } from "../../../state/event-log/event-log.ts";
19
+ import { appendEvent, appendEventBuffered } from "../../../state/event-log/event-log.ts";
20
+ import { logInternalError } from "../../../utils/internal-error.ts";
20
21
  import type { ApiOperationHandler } from "./handler-context.ts";
21
22
 
22
23
  export const handleReadMailbox: ApiOperationHandler = (hctx) => {
@@ -129,11 +130,11 @@ export const handleSendMessage: ApiOperationHandler = (hctx) => {
129
130
  // run-lock callback. Sync append (byte-identical to pre-extract
130
131
  // api.ts) so consumers reading eventsPath immediately after the
131
132
  // call see the event — async fire-and-forget would race.
132
- appendEvent(loaded.manifest.eventsPath, {
133
+ appendEventBuffered(loaded.manifest.eventsPath, {
133
134
  type: "mailbox.message",
134
135
  runId: loaded.manifest.runId,
135
136
  data: { id: message.id, direction, from, to },
136
- });
137
+ }).catch((e) => logInternalError("api.mailbox.buffered", e, "type=mailbox.message"));
137
138
  ctx.events?.emit?.("crew.mailbox.message", {
138
139
  runId: loaded.manifest.runId,
139
140
  id: message.id,
@@ -185,26 +186,38 @@ export const handleAckMessage: ApiOperationHandler = (hctx) => {
185
186
  return withRunLockSync(loaded.manifest, () => {
186
187
  const message = readMailboxMessage(loaded.manifest, messageId);
187
188
  const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
188
- appendEvent(loaded.manifest.eventsPath, {
189
- type: "mailbox.acknowledged",
190
- runId: loaded.manifest.runId,
191
- data: { messageId },
192
- });
193
- if (message?.data?.kind === "group_join" && typeof message.data.requestId === "string") {
189
+ // Read-your-writes (CI 2026-09-11, phase4 integration): the ack API
190
+ // returns and callers/tests read events.jsonl synchronously right after —
191
+ // buffered append flushes later (bufferMs window), so the events were
192
+ // missing at read. Sync appendEvent (base behavior, b6eba80f pattern).
193
+ try {
194
194
  appendEvent(loaded.manifest.eventsPath, {
195
- type: "agent.group_join.acknowledged",
195
+ type: "mailbox.acknowledged",
196
196
  runId: loaded.manifest.runId,
197
- message: "Group join delivery acknowledged via mailbox ack.",
198
- data: {
199
- requestId: message.data.requestId,
200
- messageId,
201
- batchId: message.data.batchId,
202
- partial: message.data.partial,
203
- acknowledgedAt: delivery.updatedAt,
204
- acknowledgedBy: "leader",
205
- },
206
- metadata: { provenance: "api" },
197
+ data: { messageId },
207
198
  });
199
+ } catch (e) {
200
+ logInternalError("api.mailbox.append", e, "type=mailbox.acknowledged");
201
+ }
202
+ if (message?.data?.kind === "group_join" && typeof message.data.requestId === "string") {
203
+ try {
204
+ appendEvent(loaded.manifest.eventsPath, {
205
+ type: "agent.group_join.acknowledged",
206
+ runId: loaded.manifest.runId,
207
+ message: "Group join delivery acknowledged via mailbox ack.",
208
+ data: {
209
+ requestId: message.data.requestId,
210
+ messageId,
211
+ batchId: message.data.batchId,
212
+ partial: message.data.partial,
213
+ acknowledgedAt: delivery.updatedAt,
214
+ acknowledgedBy: "leader",
215
+ },
216
+ metadata: { provenance: "api" },
217
+ });
218
+ } catch (e) {
219
+ logInternalError("api.mailbox.append", e, "type=agent.group_join.acknowledged");
220
+ }
208
221
  }
209
222
  ctx.events?.emit?.("crew.mailbox.acknowledged", {
210
223
  runId: loaded.manifest.runId,
@@ -8,7 +8,7 @@
8
8
  import { terminateLiveAgentsForRun } from "../../../runtime/live-session/live-agent-manager.ts";
9
9
  import { currentCrewRole, permissionForRole } from "../../../runtime/role-permission.ts";
10
10
  import { withRunLock } from "../../../state/coordination/locks.ts";
11
- import { appendEvent } from "../../../state/event-log/event-log.ts";
11
+ import { appendEvent, appendEventBuffered } from "../../../state/event-log/event-log.ts";
12
12
  import { getCurrentPlanRecord, setPlanApproval } from "../../../state/stores/plan-store.ts";
13
13
  import { loadRunManifestById, saveRunManifestAsync, saveRunTasks, updateRunStatus } from "../../../state/stores/state-store.ts";
14
14
  import { logInternalError } from "../../../utils/internal-error.ts";
@@ -68,13 +68,13 @@ export const handleApprovePlan: ApiOperationHandler = async (hctx) => {
68
68
  // plan id+version. Pre-v2 runs without a PlanRecord skip silently.
69
69
  const currentRecord = getCurrentPlanRecord(manifest);
70
70
  if (currentRecord) setPlanApproval(manifest, { status: "approved", planVersion: currentRecord.version, by: "api" });
71
- appendEvent(manifest.eventsPath, {
71
+ appendEventBuffered(manifest.eventsPath, {
72
72
  type: "plan.approved",
73
73
  runId: manifest.runId,
74
74
  taskId: approval.planTaskId,
75
75
  message: "Adaptive implementation plan approved; resume the run to execute mutating tasks.",
76
76
  metadata: { provenance: "api" },
77
- });
77
+ }).catch((e) => logInternalError("api.plan-approval.buffered", e, "type=plan.approved"));
78
78
  return result(JSON.stringify(manifest.planApproval, null, 2), {
79
79
  action: "api",
80
80
  status: "ok",
@@ -150,13 +150,13 @@ export const handleCancelPlan: ApiOperationHandler = async (hctx) => {
150
150
  const denyRecord = getCurrentPlanRecord(manifest);
151
151
  if (denyRecord) setPlanApproval(manifest, { status: "rejected", planVersion: denyRecord.version, by: "api" });
152
152
  saveRunTasks(manifest, tasks);
153
- appendEvent(manifest.eventsPath, {
153
+ appendEventBuffered(manifest.eventsPath, {
154
154
  type: "plan.cancelled",
155
155
  runId: manifest.runId,
156
156
  taskId: approval.planTaskId,
157
157
  message: "Adaptive implementation plan was cancelled.",
158
158
  metadata: { provenance: "api" },
159
- });
159
+ }).catch((e) => logInternalError("api.plan-approval.buffered", e, "type=plan.cancelled"));
160
160
  manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
161
161
  void terminateLiveAgentsForRun(manifest.runId, "cancelled", appendEvent, manifest.eventsPath).catch((error) =>
162
162
  logInternalError("team-tool.cancel-plan.terminate", error, `runId=${manifest.runId}`),
@@ -16,9 +16,10 @@
16
16
  import { canTransitionTaskStatus, isTeamTaskStatus } from "../../../state/contracts.ts";
17
17
  import { withRunLockSync } from "../../../state/coordination/locks.ts";
18
18
  import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../../state/coordination/task-claims.ts";
19
- import { appendEvent } from "../../../state/event-log/event-log.ts";
19
+ import { appendEventBuffered } from "../../../state/event-log/event-log.ts";
20
20
  import { loadRunManifestById, saveRunTasks } from "../../../state/stores/state-store.ts";
21
21
  import type { TeamTaskState } from "../../../state/types.ts";
22
+ import { logInternalError } from "../../../utils/internal-error.ts";
22
23
  import { RUN_NOT_FOUND_HINT } from "../run-not-found.ts";
23
24
  import type { ApiHandlerContext, ApiOperationHandler } from "./handler-context.ts";
24
25
 
@@ -92,7 +93,7 @@ export const handleClaimTask: ApiOperationHandler = (ctx) => {
92
93
  const updatedTask = claimTask(freshTask, owner);
93
94
  const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? updatedTask : item));
94
95
  saveRunTasks(fresh.manifest, tasks);
95
- appendEvent(fresh.manifest.eventsPath, {
96
+ appendEventBuffered(fresh.manifest.eventsPath, {
96
97
  type: "task.claimed",
97
98
  runId: fresh.manifest.runId,
98
99
  taskId: freshTask.id,
@@ -101,7 +102,7 @@ export const handleClaimTask: ApiOperationHandler = (ctx) => {
101
102
  token: "[REDACTED]",
102
103
  leasedUntil: updatedTask.claim?.leasedUntil,
103
104
  },
104
- });
105
+ }).catch((e) => logInternalError("api.task-claims.buffered", e, "type=task.claimed"));
105
106
  return result(JSON.stringify(updatedTask.claim, null, 2), {
106
107
  action: "api",
107
108
  status: "ok",
@@ -162,12 +163,12 @@ export const handleReleaseTaskClaim: ApiOperationHandler = (ctx) => {
162
163
  const updatedTask = releaseTaskClaim(freshTask, owner, token);
163
164
  const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? updatedTask : item));
164
165
  saveRunTasks(fresh.manifest, tasks);
165
- appendEvent(fresh.manifest.eventsPath, {
166
+ appendEventBuffered(fresh.manifest.eventsPath, {
166
167
  type: "task.claim_released",
167
168
  runId: fresh.manifest.runId,
168
169
  taskId: freshTask.id,
169
170
  data: { owner },
170
- });
171
+ }).catch((e) => logInternalError("api.task-claims.buffered", e, "type=task.claim_released"));
171
172
  return result(JSON.stringify(updatedTask, null, 2), {
172
173
  action: "api",
173
174
  status: "ok",
@@ -241,12 +242,12 @@ export const handleTransitionTaskStatus: ApiOperationHandler = (ctx) => {
241
242
  const updatedTask = transitionClaimedTaskStatus(freshTask, owner, token, to);
242
243
  const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? updatedTask : item));
243
244
  saveRunTasks(fresh.manifest, tasks);
244
- appendEvent(fresh.manifest.eventsPath, {
245
+ appendEventBuffered(fresh.manifest.eventsPath, {
245
246
  type: "task.status_transitioned",
246
247
  runId: fresh.manifest.runId,
247
248
  taskId: freshTask.id,
248
249
  data: { owner, status: to },
249
- });
250
+ }).catch((e) => logInternalError("api.task-claims.buffered", e, "type=task.status_transitioned"));
250
251
  return result(JSON.stringify(updatedTask, null, 2), {
251
252
  action: "api",
252
253
  status: "ok",
@@ -392,6 +392,12 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
392
392
  }
393
393
  ctx.abortForegroundRun?.(fresh.manifest.runId);
394
394
  for (const taskId of abortResult.abortedIds) {
395
+ // REVIEW FIX (2026-09-10): reverted M2b buffered conversion. Although
396
+ // task.cancelled is a TERMINAL type (buffered terminal path bypasses
397
+ // the 20ms buffer), that path still resolves via a microtask chain
398
+ // (flushPromise.then(appendEvent)) — NOT same-tick. Cancel tests and
399
+ // the immediately-following updateRunStatus reader need same-tick
400
+ // durability, so plain sync appendEvent (base behavior).
395
401
  appendEvent(fresh.manifest.eventsPath, {
396
402
  type: "task.cancelled",
397
403
  runId: fresh.manifest.runId,