@fitlab-ai/agent-infra 0.11.9 → 0.11.11

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 (219) hide show
  1. package/dist/bin/internal-cli.js +0 -6
  2. package/dist/lib/agent-clients/adapter.js +20 -0
  3. package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
  4. package/dist/lib/agent-clients/adapters/codex.js +4 -0
  5. package/dist/lib/git/workflow.js +1 -1
  6. package/dist/lib/internal/cli-route-inventory.js +0 -3
  7. package/dist/lib/internal/manual-validation.js +2 -2
  8. package/dist/lib/internal/platform-pr.js +4 -5
  9. package/dist/lib/internal/sandbox-control.js +30 -5
  10. package/dist/lib/internal/task-artifact.js +1 -1
  11. package/dist/lib/internal/task-create.js +15 -1
  12. package/dist/lib/internal/task-finalization.js +14 -25
  13. package/dist/lib/internal/task-operation-registry.js +6 -141
  14. package/dist/lib/internal/task-platform-recovery.js +9 -4
  15. package/dist/lib/platform/completion-backfill.js +2 -1
  16. package/dist/lib/platform/context.js +4 -2
  17. package/dist/lib/platform/github-client.js +20 -17
  18. package/dist/lib/platform/github-provider.js +42 -22
  19. package/dist/lib/platform/github-release-notes.js +4 -2
  20. package/dist/lib/platform/in-label-sync.js +2 -1
  21. package/dist/lib/platform/issue-comments.js +13 -9
  22. package/dist/lib/platform/operation-coordinator.js +1 -1
  23. package/dist/lib/platform/pr-review.js +2 -1
  24. package/dist/lib/platform/pr-summary.js +45 -8
  25. package/dist/lib/platform/provider-bridge.js +4 -5
  26. package/dist/lib/platform/provider-contract.d.ts +3 -6
  27. package/dist/lib/platform/provider-contract.js +3 -15
  28. package/dist/lib/platform/provider-validation.js +9 -4
  29. package/dist/lib/platform/pull-requests.js +83 -16
  30. package/dist/lib/platform/verification-sync.js +17 -14
  31. package/dist/lib/sandbox/commands/create.js +18 -2
  32. package/dist/lib/sandbox/commands/enter.js +47 -33
  33. package/dist/lib/sandbox/control/client.js +99 -17
  34. package/dist/lib/sandbox/control/executor.js +12 -14
  35. package/dist/lib/sandbox/control/host-recovery.js +218 -0
  36. package/dist/lib/sandbox/control/protocol.js +2 -3
  37. package/dist/lib/sandbox/control/server.js +42 -2
  38. package/dist/lib/sandbox/environment.js +150 -0
  39. package/dist/lib/sandbox/recovery.js +8 -0
  40. package/dist/lib/sandbox/removal.js +0 -1
  41. package/dist/lib/sandbox/workspace-identity.js +0 -14
  42. package/dist/lib/sandbox/workspace-view.js +33 -6
  43. package/dist/lib/task/artifact-command.js +18 -3
  44. package/dist/lib/task/artifact-lifecycle.js +121 -63
  45. package/dist/lib/task/artifact-selection.js +1 -1
  46. package/dist/lib/task/capabilities.js +59 -22
  47. package/dist/lib/task/commands/status.js +0 -27
  48. package/dist/lib/task/commit-identity.js +22 -7
  49. package/dist/lib/task/commit-intent.js +67 -26
  50. package/dist/lib/task/commit-operation.js +19 -8
  51. package/dist/lib/task/completion-facts.js +32 -10
  52. package/dist/lib/task/control-authority.js +2 -6
  53. package/dist/lib/task/current-contract.js +1 -5
  54. package/dist/lib/task/events.js +9 -275
  55. package/dist/lib/task/execution-inputs.js +14 -0
  56. package/dist/lib/task/finalization-envelope.js +4 -0
  57. package/dist/lib/task/finalization.js +43 -86
  58. package/dist/lib/task/frontmatter.js +25 -2
  59. package/dist/lib/task/ledger-intents.js +6 -8
  60. package/dist/lib/task/orchestration.js +11 -10
  61. package/dist/lib/task/platform-operation-journal.js +29 -10
  62. package/dist/lib/task/platform-operation-recovery.js +109 -35
  63. package/dist/lib/task/resolve-ref.js +8 -3
  64. package/dist/lib/task/review-fingerprint.js +4 -26
  65. package/dist/lib/task/verification-engine.js +95 -6
  66. package/dist/lib/task/write.js +0 -11
  67. package/dist/lifecycle-build-manifest.json +3 -3
  68. package/lib/agent-clients/adapter.ts +22 -0
  69. package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
  70. package/lib/agent-clients/adapters/codex.ts +4 -0
  71. package/lib/git/workflow.ts +1 -1
  72. package/lib/internal/cli-route-inventory.ts +0 -2
  73. package/lib/internal/manual-validation.ts +2 -2
  74. package/lib/internal/platform-pr.ts +3 -4
  75. package/lib/internal/sandbox-control.ts +39 -6
  76. package/lib/internal/task-artifact.ts +1 -1
  77. package/lib/internal/task-create.ts +15 -2
  78. package/lib/internal/task-finalization.ts +16 -26
  79. package/lib/internal/task-operation-registry.ts +6 -153
  80. package/lib/internal/task-platform-recovery.ts +9 -4
  81. package/lib/platform/completion-backfill.ts +2 -1
  82. package/lib/platform/context.ts +8 -4
  83. package/lib/platform/github-client.ts +24 -20
  84. package/lib/platform/github-provider.ts +47 -26
  85. package/lib/platform/github-release-notes.ts +5 -3
  86. package/lib/platform/in-label-sync.ts +2 -1
  87. package/lib/platform/issue-comments.ts +14 -9
  88. package/lib/platform/operation-coordinator.ts +1 -1
  89. package/lib/platform/pr-review.ts +2 -1
  90. package/lib/platform/pr-summary.ts +43 -7
  91. package/lib/platform/provider-bridge.ts +10 -9
  92. package/lib/platform/provider-contract.ts +6 -21
  93. package/lib/platform/provider-loader.ts +7 -6
  94. package/lib/platform/provider-validation.ts +30 -8
  95. package/lib/platform/pull-requests.ts +78 -15
  96. package/lib/platform/repository-metadata.ts +3 -2
  97. package/lib/platform/security-alerts.ts +1 -1
  98. package/lib/platform/types.ts +2 -0
  99. package/lib/platform/verification-sync.ts +18 -14
  100. package/lib/sandbox/commands/create.ts +24 -2
  101. package/lib/sandbox/commands/enter.ts +42 -39
  102. package/lib/sandbox/control/client.ts +107 -19
  103. package/lib/sandbox/control/executor.ts +13 -15
  104. package/lib/sandbox/control/host-recovery.ts +195 -0
  105. package/lib/sandbox/control/protocol.ts +4 -5
  106. package/lib/sandbox/control/server.ts +42 -1
  107. package/lib/sandbox/environment.ts +162 -0
  108. package/lib/sandbox/recovery.ts +10 -0
  109. package/lib/sandbox/removal.ts +2 -10
  110. package/lib/sandbox/tool-types.ts +1 -0
  111. package/lib/sandbox/workspace-identity.ts +0 -26
  112. package/lib/sandbox/workspace-view.ts +33 -8
  113. package/lib/task/artifact-command.ts +19 -3
  114. package/lib/task/artifact-lifecycle.ts +102 -53
  115. package/lib/task/artifact-selection.ts +1 -1
  116. package/lib/task/capabilities.ts +52 -26
  117. package/lib/task/commands/status.ts +0 -31
  118. package/lib/task/commit-identity.ts +24 -7
  119. package/lib/task/commit-intent.ts +58 -24
  120. package/lib/task/commit-operation.ts +17 -8
  121. package/lib/task/completion-facts.ts +31 -9
  122. package/lib/task/control-authority.ts +1 -8
  123. package/lib/task/current-contract.ts +1 -6
  124. package/lib/task/events.ts +9 -248
  125. package/lib/task/execution-inputs.ts +25 -0
  126. package/lib/task/finalization-envelope.ts +21 -0
  127. package/lib/task/finalization.ts +40 -82
  128. package/lib/task/frontmatter.ts +26 -2
  129. package/lib/task/ledger-intents.ts +6 -8
  130. package/lib/task/orchestration.ts +11 -17
  131. package/lib/task/platform-operation-journal.ts +47 -13
  132. package/lib/task/platform-operation-recovery.ts +102 -37
  133. package/lib/task/resolve-ref.ts +8 -3
  134. package/lib/task/review-fingerprint.ts +4 -26
  135. package/lib/task/verification-engine.ts +103 -9
  136. package/lib/task/write.ts +0 -19
  137. package/package.json +1 -1
  138. package/templates/.agents/rules/README.en.md +1 -1
  139. package/templates/.agents/rules/README.zh-CN.md +1 -1
  140. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  141. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  142. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  143. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  144. package/templates/.agents/rules/issue-sync.en.md +1 -1
  145. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  146. package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
  147. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
  148. package/templates/.agents/rules/review-handshake.en.md +3 -0
  149. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
  150. package/templates/.agents/rules/task-management.en.md +1 -1
  151. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  152. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  153. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  155. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  157. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  158. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
  159. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
  160. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
  161. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
  162. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  163. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +27 -10
  164. package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
  165. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
  166. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  167. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
  168. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
  169. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
  170. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
  171. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  172. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  173. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  174. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  175. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  176. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  177. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  178. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  179. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  180. package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
  181. package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
  182. package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
  183. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
  184. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  189. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  190. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  191. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  192. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  193. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  194. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
  195. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
  196. package/templates/.agents/skills/test/SKILL.en.md +4 -1
  197. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
  198. package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
  199. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
  200. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -2
  201. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -2
  202. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
  203. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
  204. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  205. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  206. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  207. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  208. package/templates/.agents/templates/task.zh-CN.md +0 -12
  209. package/templates/.github/workflows/pr-label.yml +1 -2
  210. package/dist/lib/internal/task-invalidation.js +0 -63
  211. package/dist/lib/task/finalization-handoff.js +0 -93
  212. package/dist/lib/task/intermediate-cleanup.js +0 -327
  213. package/dist/lib/task/invalidation-command.js +0 -88
  214. package/dist/lib/task/invalidation.js +0 -242
  215. package/lib/internal/task-invalidation.ts +0 -41
  216. package/lib/task/finalization-handoff.ts +0 -105
  217. package/lib/task/intermediate-cleanup.ts +0 -393
  218. package/lib/task/invalidation-command.ts +0 -88
  219. package/lib/task/invalidation.ts +0 -313
@@ -1,5 +1,4 @@
1
1
  import fs from 'node:fs';
2
- import { hasTaskBoundMarker } from '../../internal/task-operation-registry.js';
3
2
  import path from 'node:path';
4
3
  import { randomUUID } from 'node:crypto';
5
4
  import { SANDBOX_CONTROL_ADMISSION_WINDOW_MS, SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
@@ -8,7 +7,11 @@ import { readSandboxControlPayload, readSandboxControlStatus } from './state.js'
8
7
  import { accessSandboxTaskView, taskViewFromStatus } from './task-view.js';
9
8
  import { readSandboxControlIdentitySentinel } from './identity-sentinel.js';
10
9
  import { configuredShortIdLength, resolveShortIdReadOnly } from '../../task/short-id.js';
10
+ import { hasTaskBoundMarker, isSandbox } from '../environment.js';
11
+ import { loadConfig } from '../config.js';
12
+ import { recoverSandboxControlFromHost } from './host-recovery.js';
11
13
  const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
14
+ const SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS = 5 * 60_000;
12
15
  export class SandboxControlClientError extends Error {
13
16
  detail;
14
17
  accepted;
@@ -79,8 +82,10 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
79
82
  }
80
83
  }
81
84
  function taskViewEffectForRequest(request) {
82
- if (request.family === 'task-lifecycle' || request.family === 'task-finalization')
85
+ if (request.family === 'task-lifecycle')
83
86
  return 'progress';
87
+ if (request.family === 'task-finalization')
88
+ return 'recovery';
84
89
  return null;
85
90
  }
86
91
  function parseResponse(raw, id, accepted = false) {
@@ -95,7 +100,7 @@ function parseResponse(raw, id, accepted = false) {
95
100
  }
96
101
  return response;
97
102
  }
98
- function readPublishedResponse(raw, id, channelDir, accepted = false) {
103
+ function readPublishedResponse(raw, id, channelDir, accepted, generation) {
99
104
  const response = parseResponse(raw, id, accepted);
100
105
  if (response.outputState !== 'available')
101
106
  return response;
@@ -109,6 +114,7 @@ function readPublishedResponse(raw, id, channelDir, accepted = false) {
109
114
  }
110
115
  if (!response.payload || response.payload.version !== payload.version
111
116
  || response.payload.id !== payload.id || response.payload.generation !== payload.generation
117
+ || payload.generation !== generation
112
118
  || response.payload.stdoutBytes !== payload.stdoutBytes || response.payload.stderrBytes !== payload.stderrBytes
113
119
  || response.payload.stdoutSha256 !== payload.stdoutSha256 || response.payload.stderrSha256 !== payload.stderrSha256) {
114
120
  clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker payload reference is invalid', false, true, id);
@@ -213,7 +219,7 @@ function exchangeSandboxControl(request, params) {
213
219
  try {
214
220
  // The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
215
221
  accepted = true;
216
- response = readPublishedResponse(raw, request.id, channelDir, accepted);
222
+ response = readPublishedResponse(raw, request.id, channelDir, accepted, request.generation);
217
223
  malformedResponseRaw = null;
218
224
  }
219
225
  catch (error) {
@@ -261,7 +267,40 @@ export function recoverSandboxControl(requestId, params = {}) {
261
267
  if (!/^[a-f0-9-]{16,64}$/u.test(requestId)) {
262
268
  clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'request id is invalid', false, true);
263
269
  }
264
- const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
270
+ try {
271
+ if (!isSandbox()) {
272
+ const config = loadConfig();
273
+ const response = recoverSandboxControlFromHost(requestId, {
274
+ managedRoot: path.join(config.controlBase, config.project),
275
+ timeoutMs: params.timeoutMs
276
+ });
277
+ if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
278
+ clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'recovery evidence is unavailable; inspect domain state before retrying', false, true, requestId);
279
+ }
280
+ return response;
281
+ }
282
+ const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
283
+ const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
284
+ const identity = readSandboxControlIdentitySentinel(statusDir);
285
+ return recoverSandboxControlFromChannel(requestId, {
286
+ channelDir, generation: identity.generation, timeoutMs: params.timeoutMs
287
+ });
288
+ }
289
+ catch (error) {
290
+ if (error instanceof SandboxControlClientError)
291
+ throw error;
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'SANDBOX_CONTROL_RECOVERY_INVALID';
294
+ clientError(code, 'sandbox control recovery evidence is invalid or unavailable', false, true, requestId);
295
+ }
296
+ }
297
+ // Low-level channel reader for control integration tests and internal callers
298
+ // that already hold a manifest-bound channel path.
299
+ export function recoverSandboxControlFromChannel(requestId, params) {
300
+ if (!/^[a-f0-9-]{16,64}$/u.test(requestId)) {
301
+ clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'request id is invalid', false, true);
302
+ }
303
+ const { channelDir } = params;
265
304
  const responsePath = path.join(channelDir, 'responses', `${requestId}.json`);
266
305
  const deadline = Date.now() + (params.timeoutMs ?? 30_000);
267
306
  let accepted = false;
@@ -284,7 +323,7 @@ export function recoverSandboxControl(requestId, params = {}) {
284
323
  try {
285
324
  // The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
286
325
  accepted = true;
287
- response = readPublishedResponse(raw, requestId, channelDir, accepted);
326
+ response = readPublishedResponse(raw, requestId, channelDir, accepted, params.generation);
288
327
  malformedResponseRaw = null;
289
328
  }
290
329
  catch (error) {
@@ -298,15 +337,24 @@ export function recoverSandboxControl(requestId, params = {}) {
298
337
  else if (now - malformedResponseObservedAt >= SANDBOX_CONTROL_RESPONSE_SETTLE_MS) {
299
338
  clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted, requestId);
300
339
  }
301
- sleep(25);
340
+ const remainingMs = deadline - Date.now();
341
+ if (remainingMs > 0)
342
+ sleep(Math.min(25, Math.ceil(remainingMs)));
302
343
  continue;
303
344
  }
304
- if (response.phase === 'accepted')
345
+ if (response.phase === 'accepted') {
305
346
  accepted = true;
306
- else
347
+ }
348
+ else if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
349
+ accepted = true;
350
+ }
351
+ else {
307
352
  return response;
353
+ }
308
354
  }
309
- sleep(25);
355
+ const remainingMs = deadline - Date.now();
356
+ if (remainingMs > 0)
357
+ sleep(Math.min(25, Math.ceil(remainingMs)));
310
358
  }
311
359
  clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'request did not produce a final result; inspect domain state before retrying', false, accepted, requestId);
312
360
  }
@@ -326,7 +374,7 @@ export function requestSandboxControl(params) {
326
374
  const auth = authority(params);
327
375
  const issuedAt = Date.now();
328
376
  const request = {
329
- version: 3, id: randomUUID(), ...auth, issuedAt,
377
+ version: 4, id: randomUUID(), ...auth, issuedAt,
330
378
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
331
379
  family: params.family, args: params.args,
332
380
  controllerProcess: null,
@@ -341,9 +389,8 @@ export function requestSandboxTaskFinalization(params) {
341
389
  const auth = authority(params);
342
390
  const issuedAt = Date.now();
343
391
  const id = randomUUID();
344
- const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
345
392
  const request = {
346
- version: 3,
393
+ version: 4,
347
394
  id,
348
395
  ...auth,
349
396
  issuedAt,
@@ -351,12 +398,47 @@ export function requestSandboxTaskFinalization(params) {
351
398
  family: 'task-finalization',
352
399
  operation: 'complete',
353
400
  agent,
354
- handoffSha256,
355
401
  args: [],
356
402
  controllerProcess: null,
357
403
  controllerProof: null
358
404
  };
359
- return exchangeSandboxControl(request, params);
405
+ let response;
406
+ try {
407
+ response = exchangeSandboxControl(request, params);
408
+ }
409
+ catch (error) {
410
+ if (!(error instanceof SandboxControlClientError)
411
+ || error.detail.code !== 'SANDBOX_CONTROL_RESULT_UNKNOWN'
412
+ || !error.accepted || !error.requestId)
413
+ throw error;
414
+ return recoverAcceptedTaskFinalization(error.requestId, params);
415
+ }
416
+ if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
417
+ return recoverAcceptedTaskFinalization(response.id, params);
418
+ }
419
+ return response;
420
+ }
421
+ export function recoverAcceptedTaskFinalization(requestId, params, recover = recoverSandboxControl) {
422
+ const deadline = Date.now() + (params.recoveryBudgetMs ?? SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS);
423
+ while (Date.now() < deadline) {
424
+ const timeoutMs = Math.min(30_000, deadline - Date.now());
425
+ if (timeoutMs <= 0)
426
+ break;
427
+ try {
428
+ const response = recover(requestId, { timeoutMs });
429
+ if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
430
+ continue;
431
+ return response;
432
+ }
433
+ catch (error) {
434
+ if (error instanceof SandboxControlClientError
435
+ && error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
436
+ && error.requestId === requestId)
437
+ continue;
438
+ throw error;
439
+ }
440
+ }
441
+ clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'accepted finalization request remained unknown after the automatic recovery budget; inspect the receipt and resume by request id', false, true, requestId);
360
442
  }
361
443
  function exactKeys(value, keys) {
362
444
  return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
@@ -456,7 +538,7 @@ function requestCodexController(params) {
456
538
  const auth = authority(params);
457
539
  const issuedAt = Date.now();
458
540
  const request = {
459
- version: 3,
541
+ version: 4,
460
542
  id: randomUUID(),
461
543
  ...auth,
462
544
  issuedAt,
@@ -503,7 +585,7 @@ export function requestSandboxTaskCreate(params) {
503
585
  const auth = authority(params);
504
586
  const issuedAt = Date.now();
505
587
  const request = {
506
- version: 3, id: randomUUID(), ...auth, issuedAt,
588
+ version: 4, id: randomUUID(), ...auth, issuedAt,
507
589
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
508
590
  family: 'task-create', candidate: params.candidate,
509
591
  controllerProcess: null,
@@ -2,10 +2,10 @@ import { spawn } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
- import { homedir } from 'node:os';
6
5
  import { getProcessStartTime } from '../../server/process-state.js';
7
6
  import { createTask, projectTaskCreateResult, taskCreateExitCode, taskCreateFailure } from '../../task/create-service.js';
8
7
  import { applyTaskFinalization } from '../../task/finalization.js';
8
+ import { serializeTaskFinalizationEnvelope } from '../../task/finalization-envelope.js';
9
9
  import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
10
10
  import { atomicWriteJson, executionPath, readActiveLease, terminateSandboxControlExecution } from './state.js';
11
11
  import { requestAuditFields, resultAuditFields, appendDiagnosticAudit, createSandboxControlAuditContext, appendCriticalAudit, writeSandboxControlTransition } from './audit.js';
@@ -14,10 +14,6 @@ import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, pa
14
14
  import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
15
15
  import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.js';
16
16
  import { closeCodexControllerRegistration, CodexControllerRegistrationError, openCodexControllerRegistration, resolveCodexControllerBinding } from './controller-registration.js';
17
- import { shareBranchDir } from '../constants.js';
18
- function handoffDirectory(manifest) {
19
- return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
20
- }
21
17
  function sameBrokerOwner(left, right) {
22
18
  return left.version === right.version
23
19
  && left.pid === right.pid
@@ -232,7 +228,7 @@ function finalizationResult(result) {
232
228
  async function executeRequestInner(manifest, manifestPath, request, options = {}) {
233
229
  if (request.family === 'task-create') {
234
230
  try {
235
- const result = await createTask(request.candidate, { repoRoot: manifest.repoRoot });
231
+ const result = await (options.createTask ?? createTask)(request.candidate, { repoRoot: manifest.repoRoot });
236
232
  return taskCreateExecutionResult(projectTaskCreateResult(result, {
237
233
  requestId: request.id,
238
234
  accepted: true,
@@ -303,7 +299,7 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
303
299
  const parsed = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
304
300
  if (parsed.family !== 'task-finalization')
305
301
  throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
306
- const operation = { ...parsed, request: { ...parsed.request, handoffSha256: request.handoffSha256 } };
302
+ const operation = parsed;
307
303
  const result = dispatchTaskControlOperation(createSandboxExecutorExecutionContext({
308
304
  repoRoot: manifest.repoRoot,
309
305
  worktreeRoot: manifest.worktreeRoot,
@@ -311,8 +307,7 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
311
307
  taskId: manifest.taskId,
312
308
  generation: manifest.generation,
313
309
  manifestPath,
314
- requestId: request.id,
315
- handoffDirectory: handoffDirectory(manifest)
310
+ requestId: request.id
316
311
  }), operation);
317
312
  return finalizationResult(await result);
318
313
  }
@@ -333,8 +328,7 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
333
328
  taskId: manifest.taskId,
334
329
  generation: manifest.generation,
335
330
  manifestPath,
336
- requestId: request.id,
337
- handoffDirectory: handoffDirectory(manifest)
331
+ requestId: request.id
338
332
  });
339
333
  const format = (value) => {
340
334
  const result = value;
@@ -456,10 +450,14 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
456
450
  result = request.family === 'task-finalization'
457
451
  ? {
458
452
  exitCode: 1,
459
- stdout: `${JSON.stringify({
460
- version: 1, status: 'failed', changed: false, accepted: true, result: null,
453
+ stdout: serializeTaskFinalizationEnvelope({
454
+ status: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 'unknown' : 'failed',
455
+ changed: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? null : false,
456
+ accepted: true,
457
+ requestId: request.id,
458
+ result: null,
461
459
  error: { code, message: detail, retryable: false }
462
- })}\n`,
460
+ }),
463
461
  stderr: ''
464
462
  }
465
463
  : request.family === 'task-create'
@@ -0,0 +1,218 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { readSandboxControlManifest } from './lifecycle.js';
5
+ import { identityDigest } from './identity-sentinel.js';
6
+ import { readSandboxControlPayload, readSandboxControlResultEvidence } from './state.js';
7
+ import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
8
+ const REQUEST_ID = /^[a-f0-9-]{16,64}$/u;
9
+ function unknown(requestId) {
10
+ return {
11
+ version: 2, id: requestId, phase: 'rejected', exitCode: null, stdout: '', stderr: '',
12
+ error: { code: 'SANDBOX_CONTROL_RESULT_UNKNOWN', message: 'SANDBOX_CONTROL_RESULT_UNKNOWN: recovery evidence is unavailable', retryable: false },
13
+ outputState: 'unavailable', payload: null
14
+ };
15
+ }
16
+ function manifestDigest(manifest) {
17
+ return identityDigest({
18
+ version: 1, mode: manifest.mode, taskId: manifest.taskId,
19
+ generation: manifest.generation, controlRootId: manifest.controlRootId
20
+ });
21
+ }
22
+ function managedManifestPaths(managedRoot) {
23
+ if (!fs.existsSync(managedRoot))
24
+ return [];
25
+ const rootStat = fs.lstatSync(managedRoot);
26
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
27
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
28
+ const result = [];
29
+ for (const container of fs.readdirSync(managedRoot, { withFileTypes: true })) {
30
+ if (!container.isDirectory() || container.isSymbolicLink())
31
+ continue;
32
+ const containerPath = path.join(managedRoot, container.name);
33
+ for (const identity of fs.readdirSync(containerPath, { withFileTypes: true })) {
34
+ if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name))
35
+ continue;
36
+ result.push(path.join(containerPath, identity.name, 'manifest.json'));
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ function assertDirectory(directory) {
42
+ const stat = fs.lstatSync(directory);
43
+ if (!stat.isDirectory() || stat.isSymbolicLink())
44
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
45
+ }
46
+ function readAudit(manifest) {
47
+ const root = path.dirname(path.resolve(manifest.publicStatusDir));
48
+ const records = [];
49
+ for (const file of [path.join(root, 'audit.ndjson.1'), path.join(root, 'audit.ndjson')]) {
50
+ if (!fs.existsSync(file))
51
+ continue;
52
+ const stat = fs.lstatSync(file);
53
+ if (!stat.isFile() || stat.isSymbolicLink())
54
+ throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
55
+ for (const line of fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
56
+ let value;
57
+ try {
58
+ value = JSON.parse(line);
59
+ }
60
+ catch {
61
+ throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
62
+ }
63
+ if (!value || typeof value !== 'object' || Array.isArray(value))
64
+ throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
65
+ records.push(value);
66
+ }
67
+ }
68
+ return records;
69
+ }
70
+ function auditProof(manifest, requestId) {
71
+ const digest = manifestDigest(manifest);
72
+ const related = readAudit(manifest).filter((record) => record.requestId === requestId);
73
+ for (const record of related) {
74
+ if (record.generation !== manifest.generation || record.identityDigest !== digest) {
75
+ throw new Error('SANDBOX_CONTROL_AUDIT_IDENTITY_MISMATCH');
76
+ }
77
+ }
78
+ if (!related.some((record) => record.event === 'accepted-authorized'
79
+ && record.version === 2 && record.phase === 'accepted-authorized'))
80
+ return [];
81
+ return related;
82
+ }
83
+ function publishedEvidence(records, generation) {
84
+ const publications = records.filter((record) => record.event === 'executor-result-published');
85
+ let selected = null;
86
+ let selectedSummary = null;
87
+ for (const record of publications) {
88
+ if (record.version !== 2 || record.requestGeneration !== generation
89
+ || !Number.isSafeInteger(record.exitCode)
90
+ || !Number.isSafeInteger(record.outputBytes) || !Number.isSafeInteger(record.errorBytes)
91
+ || typeof record.outputDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.outputDigest)
92
+ || typeof record.errorDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.errorDigest)) {
93
+ throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
94
+ }
95
+ const summary = JSON.stringify([
96
+ record.requestFamily ?? null, record.requestGeneration, record.exitCode,
97
+ record.outputBytes, record.errorBytes, record.outputDigest, record.errorDigest
98
+ ]);
99
+ if (selectedSummary !== null && summary !== selectedSummary) {
100
+ throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_CONFLICT');
101
+ }
102
+ selected ??= record;
103
+ selectedSummary ??= summary;
104
+ }
105
+ return selected;
106
+ }
107
+ function verifiedResponse(manifest, requestId, records) {
108
+ assertDirectory(manifest.channelDir);
109
+ const responsesDir = path.join(manifest.channelDir, 'responses');
110
+ assertDirectory(responsesDir);
111
+ const responsePath = path.join(responsesDir, `${requestId}.json`);
112
+ if (!fs.existsSync(responsePath))
113
+ return null;
114
+ const responseStat = fs.lstatSync(responsePath);
115
+ if (!responseStat.isFile() || responseStat.isSymbolicLink())
116
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
117
+ const raw = fs.readFileSync(responsePath, 'utf8');
118
+ let response;
119
+ try {
120
+ response = JSON.parse(raw);
121
+ }
122
+ catch {
123
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
124
+ }
125
+ if (!response || response.version !== 2 || response.id !== requestId)
126
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
127
+ if (response.phase === 'accepted')
128
+ return null;
129
+ if (!['completed', 'rejected'].includes(response.phase))
130
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
131
+ if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
132
+ return unknown(requestId);
133
+ const evidencePath = path.join(manifest.processingDir, requestId, 'result.json');
134
+ const published = publishedEvidence(records, manifest.generation);
135
+ if (!published)
136
+ return unknown(requestId);
137
+ let evidence = null;
138
+ if (fs.existsSync(evidencePath)) {
139
+ evidence = readSandboxControlResultEvidence(evidencePath);
140
+ if (evidence.id !== requestId || evidence.generation !== manifest.generation
141
+ || evidence.exitCode !== published.exitCode || evidence.stdoutBytes !== published.outputBytes
142
+ || evidence.stderrBytes !== published.errorBytes || evidence.stdoutSha256 !== published.outputDigest
143
+ || evidence.stderrSha256 !== published.errorDigest)
144
+ throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
145
+ }
146
+ if (response.phase !== 'completed' || response.exitCode !== published.exitCode || response.error !== null) {
147
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
148
+ }
149
+ if (response.outputState === undefined) {
150
+ const stdout = response.stdout;
151
+ const stderr = response.stderr;
152
+ if (response.payload !== undefined
153
+ || Buffer.byteLength(stdout, 'utf8') !== published.outputBytes
154
+ || Buffer.byteLength(stderr, 'utf8') !== published.errorBytes
155
+ || createHash('sha256').update(stdout, 'utf8').digest('hex') !== published.outputDigest
156
+ || createHash('sha256').update(stderr, 'utf8').digest('hex') !== published.errorDigest) {
157
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
158
+ }
159
+ return response;
160
+ }
161
+ if (response.outputState === 'available') {
162
+ const payload = readSandboxControlPayload(path.join(manifest.channelDir, 'responses', `${requestId}.payload.json`));
163
+ if (payload.id !== requestId || payload.generation !== manifest.generation
164
+ || payload.stdoutBytes !== published.outputBytes || payload.stderrBytes !== published.errorBytes
165
+ || payload.stdoutSha256 !== published.outputDigest || payload.stderrSha256 !== published.errorDigest
166
+ || response.payload?.id !== payload.id || response.payload?.generation !== payload.generation
167
+ || response.payload?.stdoutBytes !== payload.stdoutBytes || response.payload?.stderrBytes !== payload.stderrBytes
168
+ || response.payload?.stdoutSha256 !== payload.stdoutSha256 || response.payload?.stderrSha256 !== payload.stderrSha256) {
169
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
170
+ }
171
+ return { ...response, stdout: payload.stdout, stderr: payload.stderr };
172
+ }
173
+ const expectedStdout = published.requestFamily === 'task-create'
174
+ ? `${JSON.stringify(taskCreateOutputUnavailableResult(requestId))}\n`
175
+ : '';
176
+ if (response.outputState !== 'unavailable' || response.payload !== null
177
+ || response.stdout !== expectedStdout || response.stderr !== 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: broker restarted after executor completion\n'
178
+ && response.stderr !== 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: output payload was not retained\n') {
179
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
180
+ }
181
+ return response;
182
+ }
183
+ export function recoverSandboxControlFromHost(requestId, params) {
184
+ if (!REQUEST_ID.test(requestId))
185
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
186
+ const deadline = Date.now() + (params.timeoutMs ?? 30_000);
187
+ let candidates = [];
188
+ while (Date.now() < deadline) {
189
+ candidates = [];
190
+ for (const manifestPath of managedManifestPaths(params.managedRoot)) {
191
+ let manifest;
192
+ try {
193
+ manifest = readSandboxControlManifest(manifestPath);
194
+ }
195
+ catch {
196
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
197
+ }
198
+ if (manifest.project !== path.basename(path.resolve(params.managedRoot)) || !manifest.generation) {
199
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
200
+ }
201
+ const records = auditProof(manifest, requestId);
202
+ if (records.length)
203
+ candidates.push({ manifest, records });
204
+ }
205
+ if (candidates.length > 1)
206
+ throw new Error('SANDBOX_CONTROL_RECOVERY_AMBIGUOUS');
207
+ if (candidates.length === 1) {
208
+ const response = verifiedResponse(candidates[0].manifest, requestId, candidates[0].records);
209
+ if (response)
210
+ return response;
211
+ }
212
+ const remainingMs = deadline - Date.now();
213
+ if (remainingMs > 0)
214
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(25, remainingMs));
215
+ }
216
+ return unknown(requestId);
217
+ }
218
+ //# sourceMappingURL=host-recovery.js.map
@@ -111,7 +111,7 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
111
111
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
112
112
  }
113
113
  const request = value;
114
- if (request.version !== 3 || typeof request.id !== 'string'
114
+ if (request.version !== 4 || typeof request.id !== 'string'
115
115
  || !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
116
116
  || typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
117
117
  || !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
@@ -158,12 +158,11 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
158
158
  return request;
159
159
  }
160
160
  if (request.family === 'task-finalization') {
161
- const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'handoffSha256', 'id', 'issuedAt', 'operation', 'token', 'version'];
161
+ const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
162
162
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
163
163
  || request.operation !== 'complete'
164
164
  || !Array.isArray(request.args) || request.args.length !== 0
165
165
  || typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
166
- || typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
167
166
  || request.controllerProcess !== null || request.controllerProof !== null) {
168
167
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
169
168
  }
@@ -20,8 +20,35 @@ import { nextSandboxControlBackoff } from './timing.js';
20
20
  import { readTaskFinalizationReceipt } from '../../task/finalization.js';
21
21
  import { readCodexControllerRegistration } from './controller-registration.js';
22
22
  import { validateSandboxControlIdentity } from './identity-sentinel.js';
23
- import { mergeSandboxTaskView, taskViewForManifest } from './task-view.js';
23
+ import { mergeSandboxTaskView, taskViewAfterFinalization, taskViewForManifest } from './task-view.js';
24
24
  import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
25
+ async function taskViewForFinalizationResult(manifest, request, result) {
26
+ if (request.family !== 'task-finalization' || result?.exitCode !== 0 || !manifest.taskId)
27
+ return null;
28
+ let receipt = null;
29
+ for (let attempt = 0; attempt < 100 && !receipt; attempt += 1) {
30
+ try {
31
+ receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
32
+ }
33
+ catch {
34
+ // A missing or temporarily moving task directory is retried before publishing completion.
35
+ }
36
+ if (!receipt)
37
+ await new Promise((resolve) => setTimeout(resolve, 10));
38
+ }
39
+ const finalized = taskViewAfterFinalization({
40
+ taskId: manifest.taskId,
41
+ generation: manifest.generation,
42
+ requestId: request.id,
43
+ receipt
44
+ });
45
+ if (finalized.state === 'unknown')
46
+ return finalized;
47
+ const canonical = taskViewForManifest({ ...manifest, receipt });
48
+ return canonical.state === 'current' && canonical.observedSource === 'completed'
49
+ ? canonical
50
+ : finalized;
51
+ }
25
52
  function appendBrokerAudit(manifest, event, fields = {}) {
26
53
  appendDiagnosticAudit(manifest, event, { source: 'broker', ...fields });
27
54
  }
@@ -722,6 +749,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
722
749
  // A fresh control root has no previous task-view projection.
723
750
  }
724
751
  let lastState = '';
752
+ let lastTaskView = '';
725
753
  let lastStatusAt = 0;
726
754
  let nextBindingCheckAt = 0;
727
755
  let nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
@@ -838,6 +866,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
838
866
  settledExecution = active;
839
867
  active = null;
840
868
  }
869
+ const finalizationView = settledExecution
870
+ ? await taskViewForFinalizationResult(manifest, settledExecution.request, settledExecution.result)
871
+ : null;
872
+ if (finalizationView)
873
+ taskView = finalizationView;
841
874
  let reasonCode = null;
842
875
  try {
843
876
  if (!brokerOwns())
@@ -881,9 +914,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
881
914
  }
882
915
  const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
883
916
  const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
884
- if (brokerOwns() && (stateKey !== lastState || now - lastStatusAt >= timing.controlTickMs)) {
917
+ const taskViewKey = JSON.stringify(taskView);
918
+ if (brokerOwns() && (stateKey !== lastState || taskViewKey !== lastTaskView || now - lastStatusAt >= timing.controlTickMs)) {
885
919
  writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now, taskView);
886
920
  lastStatusAt = now;
921
+ lastTaskView = taskViewKey;
887
922
  }
888
923
  if (brokerOwns() && stateKey !== lastState) {
889
924
  appendBrokerAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
@@ -1156,6 +1191,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
1156
1191
  }
1157
1192
  }
1158
1193
  if (owned && active.result && active.resultEvidenceWritten) {
1194
+ const finalizationView = await taskViewForFinalizationResult(manifest, active.request, active.result);
1195
+ if (finalizationView) {
1196
+ taskView = finalizationView;
1197
+ writeSandboxControlStatus(manifest, broker, 'healthy', null, null, Date.now(), taskView);
1198
+ }
1159
1199
  if (publishExecutionResult(manifest, active.request, active.result, brokerOwns)) {
1160
1200
  if (brokerOwns()) {
1161
1201
  removeAcceptedResponse(manifest, active.request.id);