@fitlab-ai/agent-infra 0.9.5 → 0.9.7

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 (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -1,48 +1,88 @@
1
1
  import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../../task/create.ts';
2
+ import type { ProcessIdentity } from '../../server/process-state.ts';
2
3
 
3
4
  export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
5
+ export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
6
+ export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
7
+ export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
8
+ export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
4
9
  export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'] as const;
10
+ export type SandboxControlTimingPolicy = Readonly<{
11
+ controlTickMs: number;
12
+ parkedBindingInitialMs: number;
13
+ slowCheckMs: number;
14
+ containerHeartbeatMs: number;
15
+ quiesceDeadlineMs: number;
16
+ }>;
17
+ export const DEFAULT_SANDBOX_CONTROL_TIMING: SandboxControlTimingPolicy = Object.freeze({
18
+ controlTickMs: 250,
19
+ parkedBindingInitialMs: 1_000,
20
+ slowCheckMs: 5_000,
21
+ containerHeartbeatMs: 5_000,
22
+ quiesceDeadlineMs: 7_000
23
+ } as const);
5
24
 
6
25
  export type SandboxControlFamily = typeof SANDBOX_CONTROL_FAMILIES[number];
7
-
26
+ export type SandboxControlContainerIdentity = Readonly<{
27
+ id: string;
28
+ labels: Readonly<Record<string, string>>;
29
+ }>;
8
30
  export type SandboxControlManifest = Readonly<{
9
- version: 2;
10
- repoRoot: string;
11
- worktreeRoot: string;
12
- project: string;
13
- container: string;
14
- branch: string;
15
- mode: 'task-bound' | 'branch-only';
16
- taskId: string | null;
17
- token: string;
18
- channelDir: string;
31
+ version: 4; engine: string; repoRoot: string; worktreeRoot: string; project: string; container: string;
32
+ containerIdentity: SandboxControlContainerIdentity;
33
+ branch: string; mode: 'task-bound' | 'branch-only'; taskId: string | null; token: string;
34
+ generation: string; channelDir: string; publicStatusDir: string; processingDir: string;
19
35
  }>;
20
-
21
- export type SandboxTaskCommandRequest = Readonly<{
22
- version: 1;
23
- id: string;
36
+ export type SandboxControlBrokerOwner = ProcessIdentity & Readonly<{
37
+ version: 3;
38
+ brokerId: string;
24
39
  token: string;
25
- family: 'task-lifecycle' | 'task-orchestration';
26
- args: string[];
40
+ generation: string;
27
41
  }>;
28
-
29
- export type SandboxTaskCreateRequest = Readonly<{
30
- version: 1;
31
- id: string;
32
- token: string;
33
- family: 'task-create';
34
- candidate: TaskCreateCandidateV1;
42
+ type RequestBase = Readonly<{
43
+ version: 2; id: string; token: string; generation: string; issuedAt: number; expiresAt: number;
44
+ }>;
45
+ export type SandboxTaskCommandRequest = RequestBase & Readonly<{
46
+ family: 'task-lifecycle' | 'task-orchestration'; args: string[];
47
+ }>;
48
+ export type SandboxTaskCreateRequest = RequestBase & Readonly<{
49
+ family: 'task-create'; candidate: TaskCreateCandidateV1;
35
50
  }>;
36
-
37
51
  export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskCreateRequest;
38
-
52
+ export type SandboxControlError = Readonly<{ code: string; message: string; retryable: boolean }>;
39
53
  export type SandboxControlResponse = Readonly<{
40
- version: 1;
41
- id: string;
42
- exitCode: number;
43
- stdout: string;
44
- stderr: string;
54
+ version: 2; id: string; phase: 'accepted' | 'completed' | 'rejected'; exitCode: number | null;
55
+ stdout: string; stderr: string; error: SandboxControlError | null;
56
+ }>;
57
+ export type SandboxControlStatus = Readonly<{
58
+ version: 2; generation: string; broker: ProcessIdentity & { brokerId: string };
59
+ state: 'starting' | 'healthy' | 'busy' | 'parked'; reasonCode: string | null;
60
+ activeRequestId: string | null; updatedAt: number;
45
61
  }>;
62
+ export type SandboxControlLease = Readonly<{
63
+ version: 2; generation: string; nonce: string; owner: ProcessIdentity;
64
+ issuedAt: number; expiresAt: number; taskId: string | null; branch: string; reason: string;
65
+ }>;
66
+ export type SandboxControlExecution = Readonly<{
67
+ version: 2; generation: string; requestId: string; nonce: string;
68
+ child: ProcessIdentity & { processGroupId: number | null };
69
+ phase: 'prepared' | 'running' | 'terminating'; updatedAt: number;
70
+ }>;
71
+
72
+ export class SandboxControlProtocolError extends Error {
73
+ readonly code: string;
74
+ readonly retryable: boolean;
75
+ constructor(code: string, message: string, retryable = false) {
76
+ super(`${code}: ${message}`);
77
+ this.name = 'SandboxControlProtocolError';
78
+ this.code = code;
79
+ this.retryable = retryable;
80
+ }
81
+ }
82
+
83
+ function fail(code: string, message: string, retryable = false): never {
84
+ throw new SandboxControlProtocolError(code, message, retryable);
85
+ }
46
86
 
47
87
  export function isSandboxControlFamily(value: string): value is SandboxControlFamily {
48
88
  return SANDBOX_CONTROL_FAMILIES.includes(value as SandboxControlFamily);
@@ -50,61 +90,74 @@ export function isSandboxControlFamily(value: string): value is SandboxControlFa
50
90
 
51
91
  export function validateSandboxControlRequest(
52
92
  value: unknown,
53
- manifest: SandboxControlManifest
93
+ manifest: SandboxControlManifest,
94
+ options: { now?: number } = {}
54
95
  ): SandboxControlRequest {
55
96
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
56
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request must be an object');
97
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
57
98
  }
58
99
  const request = value as Record<string, unknown>;
59
100
  if (
60
- request.version !== 1
61
- || typeof request.id !== 'string'
62
- || !/^[a-f0-9-]{16,64}$/.test(request.id)
63
- || request.token !== manifest.token
64
- || typeof request.family !== 'string'
65
- || !isSandboxControlFamily(request.family)
66
- ) {
67
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
101
+ request.version !== 2 || typeof request.id !== 'string'
102
+ || !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
103
+ || typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
104
+ || !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt)
105
+ ) fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
106
+ if (request.generation !== manifest.generation) {
107
+ fail('SANDBOX_CONTROL_GENERATION_INVALID', 'request generation does not match the broker');
108
+ }
109
+ const issuedAt = request.issuedAt as number;
110
+ const expiresAt = request.expiresAt as number;
111
+ const now = options.now ?? Date.now();
112
+ if (expiresAt <= issuedAt || expiresAt - issuedAt > SANDBOX_CONTROL_ADMISSION_WINDOW_MS || issuedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
113
+ fail('SANDBOX_CONTROL_REQUEST_DEADLINE_INVALID', 'request admission deadline is invalid');
68
114
  }
115
+ if (now > expiresAt) fail('SANDBOX_CONTROL_REQUEST_EXPIRED', 'request admission deadline has passed', true);
69
116
  if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
70
- throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
117
+ fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
71
118
  }
72
119
  if (request.family === 'task-create') {
73
- const keys = Object.keys(request).sort().join(',');
74
- if (keys !== ['candidate', 'family', 'id', 'token', 'version'].sort().join(',')) {
75
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
120
+ const expected = ['candidate', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
121
+ if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
122
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
76
123
  }
77
124
  return { ...request, candidate: validateTaskCreateCandidate(request.candidate) } as SandboxTaskCreateRequest;
78
125
  }
79
- const keys = Object.keys(request).sort().join(',');
80
- if (
81
- keys !== ['args', 'family', 'id', 'token', 'version'].sort().join(',')
82
- || !Array.isArray(request.args)
83
- || !request.args.every((arg) => typeof arg === 'string')
84
- ) {
85
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
126
+ const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
127
+ if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
128
+ || !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
129
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
86
130
  }
87
131
  if (manifest.mode !== 'task-bound' || !manifest.taskId) {
88
- throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
132
+ fail(
133
+ 'SANDBOX_CONTROL_BRANCH_ONLY',
134
+ "branch-only sandboxes cannot coordinate tasks; return to the host and run 'ai sandbox start --recreate <task-ref-or-correct-branch>'"
135
+ );
89
136
  }
90
- if (
91
- request.family === 'task-orchestration'
92
- && request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))
93
- ) {
94
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: worktree binding is reserved for the control broker');
137
+ if (request.family === 'task-orchestration'
138
+ && request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
139
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'worktree binding is reserved for the control broker');
95
140
  }
96
141
  return request as SandboxTaskCommandRequest;
97
142
  }
98
143
 
99
- export function bindSandboxControlTask(
100
- request: SandboxControlRequest,
101
- taskId: string
102
- ): string[] {
103
- if (request.family === 'task-create') {
104
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: task-create requests do not bind a current task');
105
- }
106
- if (request.args.length === 0) {
107
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
108
- }
144
+ export function bindSandboxControlTask(request: SandboxControlRequest, taskId: string): string[] {
145
+ if (request.family === 'task-create') fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-create requests do not bind a current task');
146
+ if (request.args.length === 0) fail('SANDBOX_CONTROL_REQUEST_INVALID', 'command arguments are required');
109
147
  return [taskId, ...request.args.slice(1)];
110
148
  }
149
+
150
+ export function controlError(error: unknown): SandboxControlError {
151
+ if (error instanceof SandboxControlProtocolError) {
152
+ return { code: error.code, message: error.message, retryable: error.retryable };
153
+ }
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'SANDBOX_CONTROL_INTERNAL_ERROR';
156
+ const retryable = new Set([
157
+ 'SANDBOX_CONTROL_BUSY',
158
+ 'SANDBOX_CONTROL_HANDOFF_ACTIVE',
159
+ 'SANDBOX_CONTROL_REQUEST_EXPIRED',
160
+ 'SANDBOX_WORKTREE_BINDING_LOST'
161
+ ]).has(code);
162
+ return { code, message, retryable };
163
+ }