@ckanner/flywheel 0.32.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +5 -0
  4. package/THIRD_PARTY_NOTICES.md +42 -0
  5. package/bin/runtime.js +25 -31
  6. package/cordis.patch.yml +5 -0
  7. package/dist/bridge.mjs +421 -0
  8. package/dist/claude-code-pro-bridge.mjs +82 -0
  9. package/dist/claude-code-pro-worker.mjs +5 -0
  10. package/dist/client.js +4078 -0
  11. package/dist/codex-pro-bridge.mjs +75 -0
  12. package/dist/codex-pro-worker.mjs +5 -0
  13. package/dist/console-read.mjs +51 -0
  14. package/dist/control.mjs +218 -0
  15. package/dist/index.mjs +185 -0
  16. package/dist/installation.mjs +56 -0
  17. package/dist/machine-worker.mjs +241 -0
  18. package/dist/policy.mjs +91 -0
  19. package/dist/prewarm.mjs +136 -0
  20. package/dist/protocol.mjs +260 -0
  21. package/dist/task-association.mjs +127 -0
  22. package/dist/worker.mjs +52 -0
  23. package/dist/workspaces.mjs +42 -0
  24. package/docs/README.md +1 -1
  25. package/docs/README.zh-CN.md +1 -1
  26. package/docs/design.md +7 -4
  27. package/docs/en/configuration.md +1 -1
  28. package/docs/en/contributing.md +5 -6
  29. package/docs/en/flow-console.md +1 -1
  30. package/docs/en/getting-started.md +9 -2
  31. package/docs/en/releasing.md +132 -174
  32. package/docs/validation/2026-11-13-release-0.32.1.md +83 -0
  33. package/docs/validation/2026-11-14-single-package.md +60 -0
  34. package/docs/validation/README.md +2 -0
  35. package/docs/zh-CN/configuration.md +1 -1
  36. package/docs/zh-CN/contributing.md +4 -5
  37. package/docs/zh-CN/flow-console.md +1 -1
  38. package/docs/zh-CN/getting-started.md +7 -3
  39. package/docs/zh-CN/releasing.md +97 -81
  40. package/native/darwin-amd64/flywheel +0 -0
  41. package/native/darwin-arm64/flywheel +0 -0
  42. package/native/linux-amd64/flywheel +0 -0
  43. package/native/linux-arm64/flywheel +0 -0
  44. package/native/manifest.json +22 -0
  45. package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +403 -0
  46. package/node_modules/dsh-subagent-codex-pro/LICENSE +21 -0
  47. package/node_modules/dsh-subagent-codex-pro/README.md +557 -0
  48. package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +30 -0
  49. package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +355 -0
  50. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +65 -0
  51. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +144 -0
  52. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +98 -0
  53. package/node_modules/dsh-subagent-codex-pro/docs/machine.md +194 -0
  54. package/node_modules/dsh-subagent-codex-pro/docs/orchestration.zh.md +86 -0
  55. package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js +68 -0
  56. package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js.map +1 -0
  57. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +15 -0
  58. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -0
  59. package/node_modules/dsh-subagent-codex-pro/lib/index.js +830 -0
  60. package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -0
  61. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +35 -0
  62. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -0
  63. package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js +229 -0
  64. package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js.map +1 -0
  65. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +101 -0
  66. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -0
  67. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +122 -0
  68. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -0
  69. package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js +167 -0
  70. package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js.map +1 -0
  71. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +141 -0
  72. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -0
  73. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +128 -0
  74. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -0
  75. package/node_modules/dsh-subagent-codex-pro/lib/machine.js +104 -0
  76. package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -0
  77. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +156 -0
  78. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -0
  79. package/node_modules/dsh-subagent-codex-pro/lib/run.js +842 -0
  80. package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -0
  81. package/node_modules/dsh-subagent-codex-pro/lib/structured.js +329 -0
  82. package/node_modules/dsh-subagent-codex-pro/lib/structured.js.map +1 -0
  83. package/node_modules/dsh-subagent-codex-pro/lib/tool.js +295 -0
  84. package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -0
  85. package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts +88 -0
  86. package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts.map +1 -0
  87. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +179 -0
  88. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -0
  89. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +266 -0
  90. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -0
  91. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +34 -0
  92. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -0
  93. package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts +113 -0
  94. package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts.map +1 -0
  95. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts +3 -0
  96. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts.map +1 -0
  97. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts +4 -0
  98. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -0
  99. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts +53 -0
  100. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts.map +1 -0
  101. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts +8 -0
  102. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -0
  103. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts +2 -0
  104. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts.map +1 -0
  105. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +70 -0
  106. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -0
  107. package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts +22 -0
  108. package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts.map +1 -0
  109. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +386 -0
  110. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -0
  111. package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts +35 -0
  112. package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts.map +1 -0
  113. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +107 -0
  114. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -0
  115. package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts +131 -0
  116. package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts.map +1 -0
  117. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +321 -0
  118. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -0
  119. package/node_modules/dsh-subagent-codex-pro/lib/usage.js +356 -0
  120. package/node_modules/dsh-subagent-codex-pro/lib/usage.js.map +1 -0
  121. package/node_modules/dsh-subagent-codex-pro/lib/wire.js +986 -0
  122. package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -0
  123. package/node_modules/dsh-subagent-codex-pro/package.json +76 -0
  124. package/npm/install.js +59 -6
  125. package/package.json +74 -12
  126. package/plugins/dsh/LICENSE +21 -0
  127. package/plugins/dsh/PROTOCOL.md +607 -0
  128. package/plugins/dsh/README.en.md +4 -0
  129. package/plugins/dsh/README.md +342 -0
  130. package/plugins/dsh/README.zh-CN.md +279 -0
  131. package/plugins/dsh/THIRD_PARTY_NOTICES.md +42 -0
  132. package/plugins/dsh/vendor/README.md +17 -0
  133. package/plugins/dsh/vendor/codex-pro.json +16 -0
@@ -0,0 +1,986 @@
1
+ /**
2
+ * Codex app-server protocol adapter.
3
+ *
4
+ * A faithful port of `@deepseek-ai/dsh-subagent-codex`'s wire module, extended
5
+ * with the per-call choices this package exists for: `model` and the unattended
6
+ * permission policy on `thread/start`, per-turn `effort` on `turn/start`, and
7
+ * explicit model/cwd/permission overrides plus effective-configuration checks
8
+ * on `thread/resume` (0.165+). A thinking level is **not** a `thread/start`
9
+ * field in 0.165/0.166. The module also accounts for token usage and account
10
+ * rate limits, keeping them separate.
11
+ *
12
+ * Everything else is carried over as-is, because the surrounding behaviour is
13
+ * load-bearing and was tuned against a real app-server: the initialize
14
+ * handshake, thread/turn association (including notifications that arrive
15
+ * before `turn/start` returns), unattended approval decisions, terminal-answer
16
+ * selection by phase, and the failure taxonomy.
17
+ *
18
+ * Differences from the source, all mechanical:
19
+ * - `JsonRpcLineTransport` is this package's own `LineTransport`, so the
20
+ * transport method shape is `request(method, params, { signal })` rather than
21
+ * a positional signal.
22
+ * - Error text uses this package's prefix.
23
+ * - Model and permission choices are constructor state; `effort` travels through
24
+ * `runTurn`; resume overrides are per call.
25
+ *
26
+ * @module dsh-subagent-codex-pro/wire
27
+ */
28
+ import { isAbsolute, resolve } from 'node:path';
29
+ import { realpathSync } from 'node:fs';
30
+ import { TokenUsageTracker } from "./usage.js";
31
+ /** Prefix on every error this module raises. */
32
+ const P = 'codex-pro';
33
+ /** Explicit thread permissions; approval policy never implies a sandbox. */
34
+ const THREAD_PERMISSION_PARAMS = {
35
+ 'read-only': {
36
+ approvalPolicy: 'never',
37
+ sandbox: 'read-only',
38
+ },
39
+ 'workspace-write': {
40
+ approvalPolicy: 'never',
41
+ sandbox: 'workspace-write',
42
+ },
43
+ never: { approvalPolicy: 'never' },
44
+ 'approve-for-me': {
45
+ approvalPolicy: 'on-request',
46
+ approvalsReviewer: 'auto_review',
47
+ sandbox: 'workspace-write',
48
+ },
49
+ 'dangerously-bypass-approvals-and-sandbox': {
50
+ approvalPolicy: 'never',
51
+ sandbox: 'danger-full-access',
52
+ },
53
+ };
54
+ /** Assert a protocol value is a non-array object. */
55
+ function object(value, label) {
56
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
57
+ throw new Error(`${P}: app-server returned invalid ${label}`);
58
+ }
59
+ return value;
60
+ }
61
+ /** Assert a protocol value is a non-empty string. */
62
+ function string(value, label) {
63
+ if (typeof value !== 'string' || value.length === 0) {
64
+ throw new Error(`${P}: app-server returned invalid ${label}`);
65
+ }
66
+ return value;
67
+ }
68
+ /**
69
+ * The only decision an unattended provider can make from what the server offers.
70
+ *
71
+ * @param params - the approval request's params.
72
+ * @returns the decision to send back.
73
+ */
74
+ function unattendedDecision(params) {
75
+ const available = params.availableDecisions;
76
+ if (available === undefined || available === null)
77
+ return 'decline';
78
+ if (Array.isArray(available)) {
79
+ if (available.includes('cancel'))
80
+ return 'cancel';
81
+ if (available.includes('decline'))
82
+ return 'decline';
83
+ }
84
+ throw new Error(`${P}: app-server offered no unattended approval decision`);
85
+ }
86
+ /** A plausible HTTP status, or undefined. */
87
+ function numericHttpStatus(value) {
88
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 65_535 ? value : undefined;
89
+ }
90
+ /** Classify a structured `codexErrorInfo` object. */
91
+ function objectFailureInfo(value) {
92
+ const keys = Object.keys(value);
93
+ const category = keys[0];
94
+ if (keys.length !== 1 || category === undefined)
95
+ return { category: 'unknown' };
96
+ const detail = value[category];
97
+ if (detail === null || typeof detail !== 'object' || Array.isArray(detail))
98
+ return { category: 'unknown' };
99
+ const fields = detail;
100
+ switch (category) {
101
+ case 'httpConnectionFailed':
102
+ case 'responseStreamConnectionFailed':
103
+ case 'responseStreamDisconnected':
104
+ case 'responseTooManyFailedAttempts': {
105
+ const httpStatus = numericHttpStatus(fields.httpStatusCode);
106
+ return httpStatus === undefined ? { category: 'transport' } : { category: 'transport', httpStatus };
107
+ }
108
+ case 'activeTurnNotSteerable':
109
+ return { category: 'product-error' };
110
+ default:
111
+ return { category: 'unknown' };
112
+ }
113
+ }
114
+ /** Read the failure taxonomy out of a terminal turn. */
115
+ function failureInfo(turn) {
116
+ if (turn.status !== 'failed')
117
+ return { category: 'unknown' };
118
+ const error = turn.error;
119
+ if (error === null || typeof error !== 'object' || Array.isArray(error))
120
+ return { category: 'unknown' };
121
+ const info = error.codexErrorInfo;
122
+ if (typeof info === 'string') {
123
+ switch (info) {
124
+ case 'contextWindowExceeded':
125
+ return { category: 'limit', maxTokens: true };
126
+ case 'sessionBudgetExceeded':
127
+ case 'usageLimitExceeded':
128
+ return { category: 'limit' };
129
+ case 'serverOverloaded':
130
+ case 'internalServerError':
131
+ return { category: 'service' };
132
+ case 'cyberPolicy':
133
+ case 'misalignmentPolicyViolation':
134
+ case 'unauthorized':
135
+ return { category: 'access-policy' };
136
+ case 'badRequest':
137
+ case 'threadRollbackFailed':
138
+ case 'other':
139
+ return { category: 'product-error' };
140
+ case 'sandboxError':
141
+ return { category: 'access-policy', sandboxFailure: true };
142
+ default:
143
+ return { category: 'unknown' };
144
+ }
145
+ }
146
+ return info !== null && typeof info === 'object' && !Array.isArray(info)
147
+ ? objectFailureInfo(info)
148
+ : { category: 'unknown' };
149
+ }
150
+ /**
151
+ * One progress line for a completed non-message item.
152
+ *
153
+ * @param item - the completed item.
154
+ * @returns a short human-readable line.
155
+ */
156
+ function describeItem(item) {
157
+ const type = typeof item.type === 'string' ? item.type : 'item';
158
+ const status = typeof item.status === 'string' ? ` ${item.status}` : '';
159
+ const command = typeof item.command === 'string' ? `: ${item.command.slice(0, 120)}` : '';
160
+ const path = typeof item.path === 'string' ? `: ${item.path.slice(0, 120)}` : '';
161
+ return `${type}${status}${command}${path}`;
162
+ }
163
+ /** Normalise an unknown throwable. */
164
+ function thrown(value) {
165
+ return value instanceof Error ? value : new Error(String(value));
166
+ }
167
+ /** The failure produced when a signal aborts a request. */
168
+ function abortError(signal) {
169
+ return signal.reason instanceof Error
170
+ ? signal.reason
171
+ : new Error(`${P}: app-server request aborted: ${String(signal.reason)}`);
172
+ }
173
+ /**
174
+ * Race one pending operation against a cancellation signal.
175
+ *
176
+ * @param pending - the operation.
177
+ * @param signal - cancellation.
178
+ * @returns the operation's value.
179
+ */
180
+ export async function raceAbort(pending, signal) {
181
+ if (signal === undefined)
182
+ return pending;
183
+ if (signal.aborted) {
184
+ pending.catch(() => { });
185
+ throw abortError(signal);
186
+ }
187
+ let rejectAbort = () => { };
188
+ const aborted = new Promise((_resolve, reject) => { rejectAbort = reject; });
189
+ const onAbort = () => { rejectAbort(abortError(signal)); };
190
+ signal.addEventListener('abort', onAbort, { once: true });
191
+ try {
192
+ return await Promise.race([pending, aborted]);
193
+ }
194
+ finally {
195
+ signal.removeEventListener('abort', onAbort);
196
+ }
197
+ }
198
+ /** The originator the Codex desktop client lists. */
199
+ export const DEFAULT_ORIGINATOR = 'Codex Desktop';
200
+ /** The subset of a permission mode that thread requests actually send. */
201
+ function requestedPermission(permission) {
202
+ return {
203
+ approvalPolicy: permission.approvalPolicy,
204
+ ...'approvalsReviewer' in permission ? { approvalsReviewer: permission.approvalsReviewer } : {},
205
+ ...'sandbox' in permission ? { sandbox: permission.sandbox } : {},
206
+ };
207
+ }
208
+ /** Map the app-server's sandbox policy object back to the request's mode name. */
209
+ function sandboxModeOf(value) {
210
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
211
+ return undefined;
212
+ const type = value.type;
213
+ switch (type) {
214
+ case 'readOnly': return 'read-only';
215
+ case 'workspaceWrite': return 'workspace-write';
216
+ case 'dangerFullAccess': return 'danger-full-access';
217
+ case 'externalSandbox': return 'external-sandbox';
218
+ default: return undefined;
219
+ }
220
+ }
221
+ /** Read the effective configuration fields a start/resume response carries. */
222
+ function effectiveConfiguration(response) {
223
+ const text = (field) => typeof response[field] === 'string' && response[field] !== '' ? response[field] : undefined;
224
+ const model = text('model');
225
+ const reportedCwd = text('cwd');
226
+ const cwd = reportedCwd !== undefined && isAbsolute(reportedCwd) ? reportedCwd : undefined;
227
+ const approvalPolicy = text('approvalPolicy');
228
+ const approvalsReviewer = text('approvalsReviewer');
229
+ const sandbox = sandboxModeOf(response.sandbox);
230
+ const reasoningEffort = response.reasoningEffort;
231
+ return {
232
+ ...model === undefined ? {} : { model },
233
+ ...cwd === undefined ? {} : { cwd },
234
+ ...approvalPolicy === undefined ? {} : { approvalPolicy },
235
+ ...approvalsReviewer === undefined ? {} : { approvalsReviewer },
236
+ ...sandbox === undefined ? {} : { sandbox },
237
+ ...sandbox === undefined ? {} : { sandboxPolicy: structuredClone(response.sandbox) },
238
+ ...reasoningEffort === null || typeof reasoningEffort === 'string'
239
+ ? { reasoningEffort: reasoningEffort }
240
+ : {},
241
+ };
242
+ }
243
+ /** Codex may return a physical macOS path such as /private/tmp for /tmp. */
244
+ function sameDirectory(actual, requested) {
245
+ if (resolve(actual) === resolve(requested))
246
+ return true;
247
+ try {
248
+ return realpathSync(actual) === realpathSync(requested);
249
+ }
250
+ catch {
251
+ return false;
252
+ }
253
+ }
254
+ /**
255
+ * Compare requested facts with the effective ones and retain both.
256
+ *
257
+ * The generated 0.165/0.166 start/resume responses carry effective model, cwd,
258
+ * approval policy, reviewer and sandbox fields. A missing or malformed requested
259
+ * field is a failed capability check, not permission to proceed. Retain the
260
+ * facts before throwing, without echoing arbitrary protocol values in errors.
261
+ */
262
+ function appliedConfiguration(source, requested, response) {
263
+ const effective = effectiveConfiguration(response);
264
+ const missing = [];
265
+ const mismatches = [];
266
+ const equals = (field, actual, requestedValue) => field === 'cwd' ? sameDirectory(actual, requestedValue) : actual === requestedValue;
267
+ const compare = (field, requestedValue) => {
268
+ if (requestedValue === undefined)
269
+ return true;
270
+ const actual = effective[field];
271
+ if (actual === undefined) {
272
+ missing.push(field);
273
+ return false;
274
+ }
275
+ if (!equals(field, actual, requestedValue)) {
276
+ mismatches.push(field);
277
+ return false;
278
+ }
279
+ return true;
280
+ };
281
+ const verified = [
282
+ compare('model', requested.model),
283
+ compare('cwd', requested.cwd),
284
+ compare('approvalPolicy', requested.approvalPolicy),
285
+ compare('approvalsReviewer', requested.approvalsReviewer),
286
+ compare('sandbox', requested.sandbox),
287
+ ];
288
+ return {
289
+ source, requested, effective, matched: verified.every(Boolean), missing,
290
+ mismatches,
291
+ };
292
+ }
293
+ /** Refuse unproven configuration before any user task is sent. */
294
+ function requireAppliedConfiguration(configuration) {
295
+ if (configuration.matched)
296
+ return;
297
+ const detail = [
298
+ ...(configuration.missing.length === 0 ? [] : [`missing or invalid ${configuration.missing.join(', ')}`]),
299
+ ...(configuration.mismatches.length === 0 ? [] : [`mismatched ${configuration.mismatches.join(', ')}`]),
300
+ ].join('; ');
301
+ throw new Error(`${P}: ${configuration.source} did not apply the requested configuration: ${detail}`);
302
+ }
303
+ /**
304
+ * One app-server connection and its single ephemeral thread/turn.
305
+ *
306
+ * Deliberately exposes no generic request surface: supporting another product
307
+ * method must first become part of the provider contract.
308
+ */
309
+ export class CodexAppServerWire {
310
+ #permissionMode;
311
+ #threadMode;
312
+ #originator;
313
+ #model;
314
+ #transport;
315
+ #fatal = Promise.withResolvers();
316
+ #threadId;
317
+ #turnId;
318
+ #dispatch = 'not-dispatched';
319
+ #pendingTurnId;
320
+ #turnCompleted;
321
+ #earlyTurnNotifications = [];
322
+ /** Usage notifications that arrived before thread/start or thread/resume returned. */
323
+ #earlyUsageNotifications = [];
324
+ #earlyUsageOverflow = false;
325
+ #lastFinalAnswer;
326
+ /** Stable partial answer text that later assistant output or tool calls may follow. */
327
+ #lastPartialAnswer;
328
+ #lastUnphasedAnswer;
329
+ #diagnostic;
330
+ /** Images the run produced, in the order the app-server reported them. */
331
+ #images = [];
332
+ #failure;
333
+ #diagnosticOrder = 0;
334
+ #observationOrder = 0;
335
+ #pendingDiagnostic;
336
+ #inputEnded = false;
337
+ #terminalObserved = false;
338
+ #closed = false;
339
+ /** Token usage folds cumulative snapshots and replays into one run report. */
340
+ #usage = new TokenUsageTracker();
341
+ /** The last rate-limit snapshot, kept separately from token usage. */
342
+ #rateLimits;
343
+ #rateLimitObservations = 0;
344
+ /** Requested/effective facts of the one start or resume this wire performed. */
345
+ #configuration;
346
+ #turnConfiguration;
347
+ /** Narrates notable events, for a caller that reports progress. */
348
+ #onProgress;
349
+ constructor(input, output, transport, options) {
350
+ this.#permissionMode = options.permissionMode;
351
+ this.#model = options.model;
352
+ this.#threadMode = options.threadMode ?? 'ephemeral';
353
+ this.#originator = options.originator ?? DEFAULT_ORIGINATOR;
354
+ this.#transport = transport;
355
+ this.#fatal.promise.catch(() => { });
356
+ this.#transport.setHooks({
357
+ onRequest: (method, params) => this.#handleServerRequest(method, params),
358
+ onNotification: (method, params) => {
359
+ try {
360
+ this.#handleNotification(method, params);
361
+ }
362
+ catch (error) {
363
+ this.#fail(thrown(error));
364
+ }
365
+ },
366
+ });
367
+ input.on('error', this.#onInputError);
368
+ input.on('end', this.#onInputEnd);
369
+ output.on('error', this.#onOutputError);
370
+ }
371
+ /**
372
+ * Install a progress narrator.
373
+ *
374
+ * A delegation can run for minutes, so a caller that reports progress needs
375
+ * the child's intermediate activity rather than only its final answer. Never
376
+ * lets a reporter's failure reach the protocol loop.
377
+ *
378
+ * @param onProgress - called with one line per notable event.
379
+ */
380
+ setProgressReporter(onProgress) {
381
+ this.#onProgress = onProgress;
382
+ }
383
+ /** Report one progress line, swallowing a reporter's own failure. */
384
+ #progress(text) {
385
+ if (this.#onProgress === undefined)
386
+ return;
387
+ try {
388
+ this.#onProgress(text);
389
+ }
390
+ catch {
391
+ // A progress reporter must never break the run it observes.
392
+ }
393
+ }
394
+ /** Whether protocol output ended before a terminal turn notification. */
395
+ get endedBeforeTerminal() {
396
+ return this.#inputEnded && !this.#terminalObserved;
397
+ }
398
+ /**
399
+ * Perform the required initialize/initialized handshake.
400
+ *
401
+ * @param signal - cancellation for an unpublished start.
402
+ */
403
+ async initialize(signal) {
404
+ object(await this.#guarded(this.#transport.request('initialize', {
405
+ clientInfo: { name: this.#originator, title: 'DeepSeek Harness', version: '0.0.1' },
406
+ capabilities: { experimentalApi: false, requestAttestation: false },
407
+ }, { signal }), signal), 'initialize response');
408
+ this.#transport.notify('initialized');
409
+ await this.#guarded(this.#transport.flush(), signal);
410
+ }
411
+ /**
412
+ * Create the run's thread and retain its identity.
413
+ *
414
+ * `ephemeral` follows this wire's thread mode. The app-server reports the
415
+ * decision back, and the check below is what proves it honoured the request:
416
+ * a `persistent` thread that came back ephemeral would be a silent downgrade,
417
+ * and the caller is about to promise the user a thread they can resume.
418
+ *
419
+ * @param cwd - the parent session's workspace.
420
+ * @param signal - cancellation for an unpublished start.
421
+ */
422
+ async startThread(cwd, signal) {
423
+ const permission = THREAD_PERMISSION_PARAMS[this.#permissionMode];
424
+ const requested = {
425
+ cwd,
426
+ ...(this.#model === undefined ? {} : { model: this.#model }),
427
+ ...requestedPermission(permission),
428
+ };
429
+ const response = object(await this.#guarded(this.#transport.request('thread/start', {
430
+ cwd,
431
+ ephemeral: this.#threadMode === 'ephemeral',
432
+ ...this.#model === undefined ? {} : { model: this.#model },
433
+ ...permission,
434
+ }, { signal }), signal), 'thread/start response');
435
+ const thread = object(response.thread, 'thread/start thread');
436
+ const id = string(thread.id, 'thread/start thread id');
437
+ if (typeof thread.ephemeral !== 'boolean')
438
+ throw new Error(`${P}: app-server omitted explicit thread persistence evidence`);
439
+ const ephemeral = thread.ephemeral === true;
440
+ if (ephemeral !== (this.#threadMode === 'ephemeral')) {
441
+ throw new Error(`${P}: app-server created a ${ephemeral ? 'ephemeral' : 'persistent'} thread `
442
+ + `where ${this.#threadMode} was asked for`);
443
+ }
444
+ this.#threadId = id;
445
+ this.#usage.resetForNewThread(id);
446
+ this.#flushEarlyUsage();
447
+ this.#configuration = appliedConfiguration('thread/start', requested, response);
448
+ this.#progress(`thread ${id} (${this.#threadMode}) configuration matched=${this.#configuration.matched}`);
449
+ requireAppliedConfiguration(this.#configuration);
450
+ }
451
+ /**
452
+ * Continue an existing thread instead of creating one.
453
+ *
454
+ * Codex 0.165+ accepts configuration overrides on `thread/resume`. Parameters
455
+ * that are present replace the thread's historical values; absent parameters
456
+ * leave history alone. The response is compared with the requested facts so a
457
+ * caller never mistakes a requested model, cwd, or permission policy for an
458
+ * effective one.
459
+ *
460
+ * The app-server refuses a thread with no rollout yet, so a thread becomes
461
+ * resumable only after it has carried one turn.
462
+ *
463
+ * @param threadId - the thread to continue, as a previous run reported it.
464
+ * @param overrides - explicit model/cwd/permission facts to apply.
465
+ * @param signal - cancellation for an unpublished resume.
466
+ */
467
+ async resumeThread(threadId, overrides = {}, signal) {
468
+ const permission = overrides.permission === undefined
469
+ ? undefined
470
+ : THREAD_PERMISSION_PARAMS[overrides.permission];
471
+ const requested = {
472
+ ...(overrides.cwd === undefined ? {} : { cwd: overrides.cwd }),
473
+ ...(overrides.model === undefined ? {} : { model: overrides.model }),
474
+ ...(permission === undefined ? {} : requestedPermission(permission)),
475
+ };
476
+ const response = object(await this.#guarded(this.#transport.request('thread/resume', {
477
+ threadId,
478
+ ...(overrides.cwd === undefined ? {} : { cwd: overrides.cwd }),
479
+ ...(overrides.model === undefined ? {} : { model: overrides.model }),
480
+ ...(permission ?? {}),
481
+ }, { signal }), signal), 'thread/resume response');
482
+ // The reply carries the thread, but a thread already known by id is what the
483
+ // caller asked for; the response only has to prove the resume was accepted.
484
+ const resumed = response.thread;
485
+ if (resumed !== undefined) {
486
+ const id = string(object(resumed, 'thread/resume thread').id, 'thread/resume thread id');
487
+ if (id !== threadId)
488
+ throw new Error(`${P}: thread/resume answered for ${id}, not ${threadId}`);
489
+ }
490
+ this.#threadId = threadId;
491
+ this.#usage.resetForResume(threadId);
492
+ this.#flushEarlyUsage();
493
+ this.#configuration = appliedConfiguration('thread/resume', requested, response);
494
+ this.#progress(`thread ${threadId} (resumed) configuration matched=${this.#configuration.matched}`);
495
+ requireAppliedConfiguration(this.#configuration);
496
+ }
497
+ /**
498
+ * Give the thread a name.
499
+ *
500
+ * The name is what a person sees in the Codex application's thread list. It
501
+ * matters here because a thread this provider creates has to report itself as
502
+ * `Codex Desktop` to be listed at all, so the name is the only place left to
503
+ * say that the work came from DSH and what it was. Best effort: a thread that
504
+ * ran is worth more than a naming failure, so the caller is told rather than
505
+ * thrown at.
506
+ *
507
+ * @param name - the name to record.
508
+ * @param signal - cancellation for an unpublished request.
509
+ * @returns whether the app-server accepted it.
510
+ */
511
+ async setThreadName(name, signal) {
512
+ if (this.#threadId === undefined)
513
+ return false;
514
+ try {
515
+ await this.#guarded(this.#transport.request('thread/name/set', { threadId: this.#threadId, name }, { signal }), signal);
516
+ this.#progress(`thread ${this.#threadId} named ${JSON.stringify(name)}`);
517
+ return true;
518
+ }
519
+ catch (error) {
520
+ // Naming is best effort, but a deadline/caller abort is not: swallowing
521
+ // it would let a timed-out startup publish a run after its budget.
522
+ if (signal?.aborted === true)
523
+ throw error;
524
+ return false;
525
+ }
526
+ }
527
+ /** The images this run produced, in arrival order. */
528
+ get images() {
529
+ return this.#images;
530
+ }
531
+ /**
532
+ * Retain the images one completed item carries.
533
+ *
534
+ * `imageView` reports a path rather than bytes: the app-server expects the host
535
+ * that asked for the work to read the file itself, which is exactly what the
536
+ * caller can do. An MCP tool result is the other shape — a computer-use
537
+ * screenshot arrives as an image content block inside it.
538
+ *
539
+ * @param item - one completed item.
540
+ */
541
+ #recordImages(item) {
542
+ if (item.type === 'imageView' && typeof item.path === 'string' && item.path !== '') {
543
+ this.#images.push({ path: item.path });
544
+ return;
545
+ }
546
+ if (item.type !== 'mcpToolCall')
547
+ return;
548
+ const result = item.result;
549
+ if (typeof result !== 'object' || result === null)
550
+ return;
551
+ const content = result.content;
552
+ if (!Array.isArray(content))
553
+ return;
554
+ for (const block of content) {
555
+ if (typeof block !== 'object' || block === null)
556
+ continue;
557
+ const image = block;
558
+ if (image.type !== 'image')
559
+ continue;
560
+ if (typeof image.data === 'string' && image.data !== '') {
561
+ this.#images.push({
562
+ data: image.data,
563
+ ...(typeof image.mimeType === 'string' ? { mimeType: image.mimeType } : {}),
564
+ });
565
+ }
566
+ }
567
+ }
568
+ /** The thread this wire is working in, once it has one. */
569
+ get threadId() {
570
+ return this.#threadId;
571
+ }
572
+ /** Authoritative turn id, adopted only after the start response agrees. */
573
+ get turnId() {
574
+ return this.#turnId;
575
+ }
576
+ /** Conservative request boundary, not inferred from progress or a known PID. */
577
+ get dispatch() { return this.#dispatch; }
578
+ /** Token usage attributable to this one delegation; never a zero guess. */
579
+ get usageReport() {
580
+ return this.#usage.report();
581
+ }
582
+ /** Account rate limits, kept separate from token usage. */
583
+ get rateLimitsReport() {
584
+ return {
585
+ observations: this.#rateLimitObservations,
586
+ ...(this.#rateLimits === undefined ? {} : { latest: this.#rateLimits }),
587
+ };
588
+ }
589
+ /** The requested and effective configuration of the one start/resume. */
590
+ get configuration() {
591
+ return this.#configuration;
592
+ }
593
+ get turnConfiguration() {
594
+ return this.#turnConfiguration;
595
+ }
596
+ /**
597
+ * Submit the one text-only task and wait for this thread/turn's terminal
598
+ * notification.
599
+ *
600
+ * @param texts - already validated task text blocks.
601
+ * @param signal - local cancellation for the published run.
602
+ * @param effort - per-turn thinking level, if the caller chose one.
603
+ * @returns the selected output and its stop reason.
604
+ */
605
+ async runTurn(texts, signal, effort, images = [], outputSchema, lifecycle) {
606
+ const completion = Promise.withResolvers();
607
+ this.#turnCompleted = completion;
608
+ const threadId = this.#threadId;
609
+ if (threadId === undefined || this.#configuration?.matched !== true) {
610
+ throw new Error(`${P}: cannot start a turn without verified thread configuration`);
611
+ }
612
+ // These are programmatic persistence barriers, never model text or human
613
+ // approval. Mark possible BEFORE writing: a crash after that ACK must stay
614
+ // ambiguous unless a durable terminal receipt can be reconciled.
615
+ if (lifecycle !== undefined) {
616
+ await this.#guarded(lifecycle('dispatch-intent'), signal);
617
+ this.#dispatch = 'possible';
618
+ await this.#guarded(lifecycle('dispatch-possible'), signal);
619
+ }
620
+ this.#dispatch = 'possible';
621
+ this.#usage.startTurnRequest();
622
+ this.#turnConfiguration = {
623
+ ...(effort === undefined ? {} : { requestedEffort: effort }),
624
+ effectiveEffort: null, evidence: 'turn/start-request', accepted: false,
625
+ };
626
+ try {
627
+ const turn = object(object(await this.#guarded(this.#transport.request('turn/start', {
628
+ threadId,
629
+ // Images first, as `localImage`: the app-server enumerates its own
630
+ // input variants and rejects anything else, and a host path is the
631
+ // one form it takes without the bytes travelling at all.
632
+ input: [
633
+ ...images.map((path) => ({ type: 'localImage', path })),
634
+ ...texts.map((text) => ({ type: 'text', text, text_elements: [] })),
635
+ ],
636
+ // ── this package's addition ──────────────────────────────────────
637
+ // The per-turn thinking level. The official adapter never sends it.
638
+ ...effort === undefined ? {} : { effort },
639
+ // `outputSchema` belongs on the turn: the same field on thread/start
640
+ // is accepted and then silently ignored, measured — the answer came
641
+ // back as prose.
642
+ ...outputSchema === undefined ? {} : { outputSchema },
643
+ }, { signal }), signal), 'turn/start response').turn, 'turn/start turn');
644
+ this.#commitTurnId(string(turn.id, 'turn/start turn id'));
645
+ this.#usage.beginTurn(string(turn.id, 'turn/start turn id'));
646
+ this.#turnConfiguration = { ...this.#turnConfiguration, accepted: true };
647
+ this.#dispatch = 'accepted';
648
+ if (lifecycle !== undefined)
649
+ await this.#guarded(lifecycle('turn-accepted'), signal);
650
+ }
651
+ catch (error) {
652
+ this.#recordFailure({ stage: 'turn-start', category: 'unknown' });
653
+ throw error;
654
+ }
655
+ let completed;
656
+ let terminal;
657
+ try {
658
+ completed = await this.#guarded(completion.promise, signal);
659
+ terminal = object(completed.params.turn, 'turn/completed turn');
660
+ }
661
+ catch (error) {
662
+ this.#recordFailure({ stage: 'turn', category: 'unknown' });
663
+ throw error;
664
+ }
665
+ const status = terminal.status;
666
+ if (status !== 'completed') {
667
+ const parsed = failureInfo(terminal);
668
+ this.#recordFailure(parsed.httpStatus === undefined
669
+ ? { stage: 'turn', category: parsed.category }
670
+ : { stage: 'turn', category: parsed.category, httpStatus: parsed.httpStatus });
671
+ if (parsed.sandboxFailure === true) {
672
+ this.#recordDiagnostic('sandbox execution', 'failed', 'Codex reported a sandbox failure', completed.order);
673
+ }
674
+ if (parsed.maxTokens === true)
675
+ return { output: this.#collectOutput(), stopReason: 'max-tokens', images: this.images };
676
+ const detail = status === 'failed' ? `: ${parsed.category}` : '';
677
+ throw new Error(`${P}: Codex turn ended with status ${String(status)}${detail}`);
678
+ }
679
+ const output = this.#collectOutput();
680
+ if (output.length === 0) {
681
+ this.#recordFailure({ stage: 'turn', category: 'invalid-result' });
682
+ throw new Error(`${P}: Codex completed without a final answer`);
683
+ }
684
+ return { output, stopReason: 'completed', images: this.images };
685
+ }
686
+ /** The best non-commentary answer observed so far, preserving exact bytes. */
687
+ get partialOutput() {
688
+ return this.#collectOutput();
689
+ }
690
+ /**
691
+ * Best-effort remote cancellation. Local settlement and process teardown stay
692
+ * authoritative when the child no longer accepts protocol requests.
693
+ */
694
+ interrupt() {
695
+ if (this.#threadId === undefined || this.#turnId === undefined || this.#closed)
696
+ return;
697
+ this.#transport.request('turn/interrupt', { threadId: this.#threadId, turnId: this.#turnId }).catch(() => { });
698
+ }
699
+ /** The latest safe unattended permission fact observed for this run. */
700
+ collectDiagnostic() {
701
+ return this.#diagnostic;
702
+ }
703
+ /** The structured failure fact observed for this published turn. */
704
+ collectFailure() {
705
+ return this.#failure;
706
+ }
707
+ /** Detach listeners and fail outstanding requests. Idempotent. */
708
+ close() {
709
+ if (this.#closed)
710
+ return;
711
+ this.#closed = true;
712
+ this.#usage.close();
713
+ this.#transport.dispose(new Error(`${P}: app-server connection closed`));
714
+ }
715
+ /** The best non-commentary answer observed so far, preserving exact bytes. */
716
+ #collectOutput() {
717
+ // Latest final answer wins, then stable partial text (DSH 0.2.1's official
718
+ // adapter selects the same way), then an unphased message for compatibility.
719
+ const selected = this.#lastFinalAnswer ?? this.#lastPartialAnswer ?? this.#lastUnphasedAnswer;
720
+ return selected !== undefined && selected.trim().length > 0 ? [{ type: 'text', text: selected }] : [];
721
+ }
722
+ /** Race one operation against the connection's fatal failure. */
723
+ async #guarded(pending, signal) {
724
+ return raceAbort(Promise.race([this.#fatal.promise, pending]), signal);
725
+ }
726
+ /** Poison the connection: every guarded operation fails from here on. */
727
+ #fail(error) {
728
+ this.#fatal.reject(error);
729
+ }
730
+ #onInputError = (error) => { this.#fail(error); };
731
+ #onOutputError = (error) => { this.#fail(error); };
732
+ #onInputEnd = () => {
733
+ this.#inputEnded = true;
734
+ this.#fail(new Error(`${P}: app-server protocol stream closed`));
735
+ };
736
+ /** Record a turn id observed before `turn/start` returned. */
737
+ #observePendingTurnId(id) {
738
+ if (this.#turnCompleted === undefined)
739
+ throw new Error(`${P}: app-server referenced a turn before turn/start`);
740
+ if (this.#pendingTurnId !== undefined && this.#pendingTurnId !== id) {
741
+ throw new Error(`${P}: app-server referenced conflicting turns`);
742
+ }
743
+ this.#pendingTurnId = id;
744
+ }
745
+ /** Adopt the authoritative turn id and replay anything that arrived first. */
746
+ #commitTurnId(id) {
747
+ if (this.#pendingTurnId !== undefined && this.#pendingTurnId !== id) {
748
+ throw new Error(`${P}: turn/start response did not match the active turn`);
749
+ }
750
+ this.#turnId = id;
751
+ const pendingDiagnostic = this.#pendingDiagnostic;
752
+ this.#pendingDiagnostic = undefined;
753
+ if (pendingDiagnostic !== undefined) {
754
+ this.#recordDiagnostic(pendingDiagnostic.request, pendingDiagnostic.decision, pendingDiagnostic.reason, pendingDiagnostic.order);
755
+ }
756
+ const notifications = this.#earlyTurnNotifications.splice(0);
757
+ for (const notification of notifications)
758
+ this.#handleNotification(notification.method, notification.params, notification.order);
759
+ }
760
+ /**
761
+ * Validate a request's thread and turn association.
762
+ *
763
+ * @param params - the server request's params.
764
+ * @param nullableTurn - whether a null turn id is acceptable.
765
+ * @returns `true` when the matching turn is still provisional.
766
+ */
767
+ #validateRunIds(params, nullableTurn = false) {
768
+ if (params.threadId !== this.#threadId)
769
+ throw new Error(`${P}: app-server request referenced another thread`);
770
+ if (nullableTurn && params.turnId === null)
771
+ return false;
772
+ const id = string(params.turnId, 'server request turn id');
773
+ if (this.#turnId === undefined) {
774
+ this.#observePendingTurnId(id);
775
+ return true;
776
+ }
777
+ if (id !== this.#turnId)
778
+ throw new Error(`${P}: app-server request referenced another turn`);
779
+ return false;
780
+ }
781
+ /** Record a decision, deferring when its turn is not authoritative yet. */
782
+ #recordRequestDiagnostic(provisional, request, decision, reason) {
783
+ const order = this.#nextObservationOrder();
784
+ if (provisional) {
785
+ this.#pendingDiagnostic = { order, request, decision, reason };
786
+ return;
787
+ }
788
+ this.#recordDiagnostic(request, decision, reason, order);
789
+ }
790
+ /** Keep the newest diagnostic. */
791
+ #recordDiagnostic(request, decision, reason, order = this.#nextObservationOrder()) {
792
+ if (order < this.#diagnosticOrder)
793
+ return;
794
+ this.#diagnosticOrder = order;
795
+ this.#diagnostic = `Codex unattended decision (mode: ${this.#permissionMode}; request: ${request}; decision: ${decision}): ${reason}`;
796
+ }
797
+ #recordFailure(facts) {
798
+ this.#failure = facts;
799
+ }
800
+ #nextObservationOrder() {
801
+ this.#observationOrder += 1;
802
+ return this.#observationOrder;
803
+ }
804
+ /** Note an item Codex declined, which is a permission fact worth reporting. */
805
+ #recordDeclinedItem(item, order) {
806
+ if (item.type === 'commandExecution' && item.status === 'declined') {
807
+ this.#recordDiagnostic('command execution', 'declined', 'Codex declined the command under the selected permission mode', order);
808
+ return true;
809
+ }
810
+ if (item.type === 'fileChange' && item.status === 'declined') {
811
+ this.#recordDiagnostic('file change', 'declined', 'Codex declined the file change under the selected permission mode', order);
812
+ return true;
813
+ }
814
+ return false;
815
+ }
816
+ /** Answer every interactive request without a human. */
817
+ #handleServerRequest(method, params) {
818
+ try {
819
+ const fields = (params ?? {});
820
+ switch (method) {
821
+ case 'item/commandExecution/requestApproval': {
822
+ const provisional = this.#validateRunIds(fields);
823
+ const decision = unattendedDecision(fields);
824
+ this.#recordRequestDiagnostic(provisional, 'command approval', decision === 'cancel' ? 'cancelled' : 'declined', 'the provider does not grant interactive approval');
825
+ return Promise.resolve({ decision });
826
+ }
827
+ case 'item/fileChange/requestApproval': {
828
+ const provisional = this.#validateRunIds(fields);
829
+ const decision = unattendedDecision(fields);
830
+ this.#recordRequestDiagnostic(provisional, 'file approval', decision === 'cancel' ? 'cancelled' : 'declined', 'the provider does not grant interactive approval');
831
+ return Promise.resolve({ decision });
832
+ }
833
+ case 'item/permissions/requestApproval':
834
+ this.#recordRequestDiagnostic(this.#validateRunIds(fields), 'permission grant', 'denied', 'the provider grants no additional turn permissions');
835
+ return Promise.resolve({ permissions: {}, scope: 'turn' });
836
+ case 'item/tool/requestUserInput':
837
+ this.#recordRequestDiagnostic(this.#validateRunIds(fields), 'user input', 'empty response', 'the provider does not collect interactive answers');
838
+ return Promise.resolve({ answers: {} });
839
+ case 'mcpServer/elicitation/request':
840
+ this.#recordRequestDiagnostic(this.#validateRunIds(fields, true), 'MCP elicitation', 'declined', 'the provider does not collect interactive MCP input');
841
+ return Promise.resolve({ action: 'decline', content: null, _meta: null });
842
+ default:
843
+ throw new Error(`${P}: unsupported app-server request ${JSON.stringify(method)}`);
844
+ }
845
+ }
846
+ catch (error) {
847
+ const normalized = thrown(error);
848
+ this.#fail(normalized);
849
+ return Promise.reject(normalized);
850
+ }
851
+ }
852
+ /** Replay usage notifications that raced the start/resume response. */
853
+ #flushEarlyUsage() {
854
+ if (this.#threadId === undefined)
855
+ return;
856
+ if (this.#earlyUsageOverflow)
857
+ this.#usage.observeInvalid();
858
+ const pending = this.#earlyUsageNotifications.splice(0);
859
+ for (const notification of pending) {
860
+ if (typeof notification.threadId !== 'string' || notification.threadId === '') {
861
+ this.#usage.observeInvalid();
862
+ continue;
863
+ }
864
+ if (notification.threadId !== this.#threadId)
865
+ continue;
866
+ const tokenUsage = notification.tokenUsage;
867
+ if (typeof tokenUsage !== 'object' || tokenUsage === null || Array.isArray(tokenUsage)) {
868
+ this.#usage.observeInvalid();
869
+ continue;
870
+ }
871
+ this.#usage.observe(notification);
872
+ }
873
+ }
874
+ /** Route one notification, buffering anything that precedes the turn id. */
875
+ #handleNotification(method, params, order) {
876
+ const fields = (params ?? {});
877
+ if (method === 'thread/tokenUsage/updated') {
878
+ if (this.#threadId === undefined) {
879
+ if (this.#earlyUsageNotifications.length < 128) {
880
+ this.#earlyUsageNotifications.push({
881
+ threadId: fields.threadId,
882
+ turnId: fields.turnId,
883
+ tokenUsage: fields.tokenUsage,
884
+ });
885
+ }
886
+ else
887
+ this.#earlyUsageOverflow = true;
888
+ return;
889
+ }
890
+ if (typeof fields.threadId !== 'string' || fields.threadId === '') {
891
+ this.#usage.observeInvalid();
892
+ return;
893
+ }
894
+ if (fields.threadId !== this.#threadId)
895
+ return;
896
+ const tokenUsage = fields.tokenUsage;
897
+ if (typeof tokenUsage !== 'object' || tokenUsage === null || Array.isArray(tokenUsage)) {
898
+ this.#usage.observeInvalid();
899
+ return;
900
+ }
901
+ this.#usage.observe({
902
+ threadId: fields.threadId,
903
+ turnId: fields.turnId,
904
+ tokenUsage,
905
+ });
906
+ return;
907
+ }
908
+ if (method === 'account/rateLimits/updated') {
909
+ const rateLimits = fields.rateLimits;
910
+ if (typeof rateLimits !== 'object' || rateLimits === null || Array.isArray(rateLimits))
911
+ return;
912
+ this.#rateLimits = rateLimits;
913
+ this.#rateLimitObservations += 1;
914
+ return;
915
+ }
916
+ if (method === 'turn/started') {
917
+ if (string(fields.threadId, 'turn/started thread id') !== this.#threadId)
918
+ return;
919
+ const turn = object(fields.turn, 'turn/started turn');
920
+ if (this.#turnCompleted !== undefined && this.#turnId === undefined) {
921
+ this.#observePendingTurnId(string(turn.id, 'turn/started turn id'));
922
+ }
923
+ return;
924
+ }
925
+ if (method === 'item/completed') {
926
+ if (string(fields.threadId, 'item/completed thread id') !== this.#threadId)
927
+ return;
928
+ const id = string(fields.turnId, 'item/completed turn id');
929
+ if (this.#turnId === undefined) {
930
+ if (this.#turnCompleted !== undefined) {
931
+ this.#observePendingTurnId(id);
932
+ this.#earlyTurnNotifications.push({ method, params, order: this.#nextObservationOrder() });
933
+ }
934
+ return;
935
+ }
936
+ if (id !== this.#turnId)
937
+ return;
938
+ const item = object(fields.item, 'item/completed item');
939
+ if (this.#recordDeclinedItem(item, order))
940
+ return;
941
+ this.#recordImages(item);
942
+ if (item.type !== 'agentMessage') {
943
+ // Commands, file changes, and searches: the interesting part of a long run.
944
+ this.#progress(describeItem(item));
945
+ return;
946
+ }
947
+ const text = typeof item.text === 'string'
948
+ ? item.text
949
+ : (() => { throw new Error(`${P}: app-server returned an invalid agent message`); })();
950
+ if (item.phase === 'final_answer')
951
+ this.#lastFinalAnswer = text;
952
+ else if (item.phase === 'partial_answer')
953
+ this.#lastPartialAnswer = text;
954
+ else if (item.phase === null)
955
+ this.#lastUnphasedAnswer = text;
956
+ else if (item.phase === 'commentary')
957
+ this.#progress(text);
958
+ else {
959
+ throw new Error(`${P}: app-server returned an unknown agent message phase ${JSON.stringify(item.phase)}`);
960
+ }
961
+ return;
962
+ }
963
+ if (method !== 'turn/completed')
964
+ return;
965
+ if (string(fields.threadId, 'turn/completed thread id') !== this.#threadId)
966
+ return;
967
+ const turn = object(fields.turn, 'turn/completed turn');
968
+ const id = string(turn.id, 'turn/completed turn id');
969
+ const turnCompleted = this.#turnCompleted;
970
+ if (turnCompleted === undefined)
971
+ return;
972
+ if (this.#turnId === undefined) {
973
+ this.#observePendingTurnId(id);
974
+ this.#earlyTurnNotifications.push({ method, params, order: this.#nextObservationOrder() });
975
+ return;
976
+ }
977
+ if (id !== this.#turnId)
978
+ return;
979
+ this.#terminalObserved = true;
980
+ if (!['completed', 'interrupted', 'failed'].includes(String(turn.status))) {
981
+ throw new Error(`${P}: app-server returned invalid terminal turn status ${String(turn.status)}`);
982
+ }
983
+ turnCompleted.resolve({ params: params, order: order ?? this.#nextObservationOrder() });
984
+ }
985
+ }
986
+ //# sourceMappingURL=wire.js.map