@ckanner/flywheel 0.32.1 → 0.33.1

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 (134) hide show
  1. package/CHANGELOG.md +20 -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 +4140 -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 +8 -5
  27. package/docs/en/configuration.md +1 -1
  28. package/docs/en/contributing.md +5 -6
  29. package/docs/en/flow-console.md +2 -2
  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-release-0.33.0.md +49 -0
  34. package/docs/validation/2026-11-14-single-package.md +60 -0
  35. package/docs/validation/README.md +3 -0
  36. package/docs/zh-CN/configuration.md +1 -1
  37. package/docs/zh-CN/contributing.md +4 -5
  38. package/docs/zh-CN/flow-console.md +2 -2
  39. package/docs/zh-CN/getting-started.md +7 -3
  40. package/docs/zh-CN/releasing.md +97 -81
  41. package/native/darwin-amd64/flywheel +0 -0
  42. package/native/darwin-arm64/flywheel +0 -0
  43. package/native/linux-amd64/flywheel +0 -0
  44. package/native/linux-arm64/flywheel +0 -0
  45. package/native/manifest.json +22 -0
  46. package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +403 -0
  47. package/node_modules/dsh-subagent-codex-pro/LICENSE +21 -0
  48. package/node_modules/dsh-subagent-codex-pro/README.md +557 -0
  49. package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +30 -0
  50. package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +355 -0
  51. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +65 -0
  52. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +144 -0
  53. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +98 -0
  54. package/node_modules/dsh-subagent-codex-pro/docs/machine.md +194 -0
  55. package/node_modules/dsh-subagent-codex-pro/docs/orchestration.zh.md +86 -0
  56. package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js +68 -0
  57. package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js.map +1 -0
  58. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +15 -0
  59. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -0
  60. package/node_modules/dsh-subagent-codex-pro/lib/index.js +830 -0
  61. package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -0
  62. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +35 -0
  63. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -0
  64. package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js +229 -0
  65. package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js.map +1 -0
  66. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +101 -0
  67. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -0
  68. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +122 -0
  69. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -0
  70. package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js +167 -0
  71. package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js.map +1 -0
  72. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +141 -0
  73. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -0
  74. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +128 -0
  75. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -0
  76. package/node_modules/dsh-subagent-codex-pro/lib/machine.js +104 -0
  77. package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -0
  78. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +156 -0
  79. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -0
  80. package/node_modules/dsh-subagent-codex-pro/lib/run.js +842 -0
  81. package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -0
  82. package/node_modules/dsh-subagent-codex-pro/lib/structured.js +329 -0
  83. package/node_modules/dsh-subagent-codex-pro/lib/structured.js.map +1 -0
  84. package/node_modules/dsh-subagent-codex-pro/lib/tool.js +295 -0
  85. package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -0
  86. package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts +88 -0
  87. package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts.map +1 -0
  88. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +179 -0
  89. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -0
  90. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +266 -0
  91. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -0
  92. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +34 -0
  93. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -0
  94. package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts +113 -0
  95. package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts.map +1 -0
  96. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts +3 -0
  97. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts.map +1 -0
  98. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts +4 -0
  99. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -0
  100. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts +53 -0
  101. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts.map +1 -0
  102. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts +8 -0
  103. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -0
  104. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts +2 -0
  105. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts.map +1 -0
  106. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +70 -0
  107. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -0
  108. package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts +22 -0
  109. package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts.map +1 -0
  110. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +386 -0
  111. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -0
  112. package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts +35 -0
  113. package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts.map +1 -0
  114. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +107 -0
  115. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -0
  116. package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts +131 -0
  117. package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts.map +1 -0
  118. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +321 -0
  119. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -0
  120. package/node_modules/dsh-subagent-codex-pro/lib/usage.js +356 -0
  121. package/node_modules/dsh-subagent-codex-pro/lib/usage.js.map +1 -0
  122. package/node_modules/dsh-subagent-codex-pro/lib/wire.js +986 -0
  123. package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -0
  124. package/node_modules/dsh-subagent-codex-pro/package.json +76 -0
  125. package/npm/install.js +59 -6
  126. package/package.json +74 -12
  127. package/plugins/dsh/LICENSE +21 -0
  128. package/plugins/dsh/PROTOCOL.md +609 -0
  129. package/plugins/dsh/README.en.md +4 -0
  130. package/plugins/dsh/README.md +342 -0
  131. package/plugins/dsh/README.zh-CN.md +279 -0
  132. package/plugins/dsh/THIRD_PARTY_NOTICES.md +42 -0
  133. package/plugins/dsh/vendor/README.md +17 -0
  134. package/plugins/dsh/vendor/codex-pro.json +16 -0
@@ -0,0 +1,830 @@
1
+ /**
2
+ * A Codex delegation that can carry a model and a reasoning effort.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * `@deepseek-ai/dsh-subagent-codex` declares `NO_START_CAPABILITIES`, so its
7
+ * `tool-subagent` row cannot enable `modelSelectionSettings` — that requires a
8
+ * provider declaring `agentOptions`, which the Codex provider does not. The
9
+ * Codex protocol supports the choices this package exposes: `model` on
10
+ * `thread/start`, per-turn `effort` on `turn/start`, and model/cwd/permission
11
+ * overrides on `thread/resume` (0.165+). The official adapter sends none of
12
+ * them; a thinking level is not a `thread/start` field in 0.165/0.166.
13
+ *
14
+ * Enabling the flag on a Codex row of our own provider was tried and did not
15
+ * work: the row vanished with no tool and no error, while a control row — same
16
+ * provider, same row, flag off — registered fine. That defect belongs to the
17
+ * flag, not to the provider, and is reported upstream.
18
+ *
19
+ * ## What this does instead
20
+ *
21
+ * Two things, both under this package's control:
22
+ *
23
+ * 1. **Execution** is a port of the official app-server adapter (`wire.ts`,
24
+ * `run.ts`), with `model` on `thread/start`, `effort` on `turn/start`, and
25
+ * explicit model/cwd/permission overrides plus effective-config verification
26
+ * on `thread/resume`. That is the same protocol the official provider speaks,
27
+ * including its initialize handshake, ephemeral thread, unattended approval
28
+ * decisions, and terminal-answer selection.
29
+ * 2. **The tool** is registered on `agent/created` into that Agent's own context
30
+ * — the path `dsh-plugin-lcu` uses — so the harness's preset-row machinery
31
+ * stays out of the way. `presets` names the presets that get it.
32
+ *
33
+ * `model` and `reasoning_effort` appear while this plugin's independent
34
+ * modelSelection setting is enabled. Codex model ids are not DSH LLM routes.
35
+ *
36
+ * @module dsh-subagent-codex-pro
37
+ */
38
+ import { appendFileSync, readFileSync, statSync } from 'node:fs';
39
+ import { createRequire } from 'node:module';
40
+ import { homedir } from 'node:os';
41
+ import { join } from 'node:path';
42
+ import { settleJob } from "./jobs.js";
43
+ import { CodexRunFailure, DEFAULT_RUN_TIMEOUT_MS, chooseThread, delegateOnce, startCodexRun } from "./run.js";
44
+ import { DEFAULT_ORIGINATOR } from "./wire.js";
45
+ import { createCodexTool } from "./tool.js";
46
+ /** Cordis plugin name. */
47
+ export const name = 'codex-pro';
48
+ /**
49
+ * The services this plugin cannot work without.
50
+ *
51
+ * `subagents` is the provider registry. Everything else — the Agent registry,
52
+ * the preset registry, the host opt-in — is looked up with `ctx.get`, so a
53
+ * deployment missing one degrades instead of failing to load.
54
+ */
55
+ export const inject = ['subagents'];
56
+ /**
57
+ * Check a caller-named deadline.
58
+ *
59
+ * A duration, so it must be a whole number of milliseconds: negative and
60
+ * fractional values have no meaning, and silently rounding one would give a
61
+ * caller a deadline it did not ask for. `0` is allowed and means no deadline,
62
+ * which is what the plugin's own configuration uses to disable it.
63
+ *
64
+ * @param value - the deadline a call named.
65
+ * @returns the deadline, unchanged.
66
+ * @throws when it is not a whole non-negative number of milliseconds.
67
+ */
68
+ export function checkedTimeout(value) {
69
+ if (!Number.isFinite(value))
70
+ throw new Error('subagent_codex: timeout_ms must be a finite number');
71
+ if (!Number.isInteger(value))
72
+ throw new Error('subagent_codex: timeout_ms must be whole milliseconds');
73
+ if (value < 0)
74
+ throw new Error('subagent_codex: timeout_ms must not be negative');
75
+ return value;
76
+ }
77
+ /**
78
+ * Check a caller-named output schema's shape.
79
+ *
80
+ * The harness applies the same rule to a schema it passes a provider —
81
+ * `assertObjectJsonSchema` — so matching it means a schema this tool accepts is
82
+ * one the harness would too. Only the root is checked: it must be an object with
83
+ * `type: "object"`, because Codex constrains a whole answer and a bare array or
84
+ * scalar has nowhere to put the shape.
85
+ *
86
+ * @param value - the schema a call named.
87
+ * @returns the schema, unchanged.
88
+ * @throws when it is not an object-rooted JSON Schema.
89
+ */
90
+ export function checkedOutputSchema(value) {
91
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
92
+ throw new Error('subagent_codex: output_schema must be a JSON Schema object');
93
+ }
94
+ const schema = value;
95
+ if (schema.type !== 'object') {
96
+ throw new Error('subagent_codex: output_schema must have "type": "object" at its root');
97
+ }
98
+ return schema;
99
+ }
100
+ /**
101
+ * Check one caller-named image path.
102
+ *
103
+ * The path must be absolute and must name a file this plugin could hand the
104
+ * app-server. Whether it exists is checked where the run is prepared, next to
105
+ * the check for the working directory, because that is where a real path is.
106
+ *
107
+ * @param requested - the path a call named.
108
+ * @returns the path, unchanged.
109
+ * @throws when it is not an absolute path.
110
+ */
111
+ export function checkedImagePath(requested) {
112
+ if (requested.trim() === '')
113
+ throw new Error('subagent_codex: image paths must not be empty');
114
+ if (!requested.startsWith('/')) {
115
+ throw new Error(`subagent_codex: image paths must be absolute, got "${requested}"`);
116
+ }
117
+ return requested;
118
+ }
119
+ /**
120
+ * Check a caller-named working directory's shape.
121
+ *
122
+ * Only the shape is checked — absolute and non-empty — so the rule is testable
123
+ * without a filesystem. Whether the path exists and is a directory is checked
124
+ * where the run is prepared, because that is where a real path matters.
125
+ *
126
+ * @param requested - the path a call named.
127
+ * @returns the path, unchanged.
128
+ * @throws when it is not an absolute path.
129
+ */
130
+ export function checkedWorkdir(requested) {
131
+ if (requested.trim() === '')
132
+ throw new Error('subagent_codex: cwd must not be empty');
133
+ if (!requested.startsWith('/')) {
134
+ throw new Error(`subagent_codex: cwd must be an absolute path, got "${requested}"`);
135
+ }
136
+ return requested;
137
+ }
138
+ /**
139
+ * Whether a model route's declared modalities refuse images.
140
+ *
141
+ * This is the harness's own rule, and matching it exactly is the point: an image
142
+ * is refused only when the route **declares** input modalities and `image` is not
143
+ * among them. An undeclared route is left to the harness, which rejects an
144
+ * attachment for a model that cannot read it anyway.
145
+ *
146
+ * Being stricter cost a real screenshot. The default `deepseek-flash` entry
147
+ * declares `['text', 'image']`, and an earlier guard here failed closed whenever
148
+ * the route could not be resolved — which turned every image into a text note in
149
+ * a session that could have displayed it.
150
+ *
151
+ * @param info - the resolved model route, when it could be resolved.
152
+ * @returns whether the route says it cannot take images.
153
+ */
154
+ export function routeRefusesImages(info) {
155
+ const declared = info?.inputModalities;
156
+ return declared !== undefined && !declared.includes('image');
157
+ }
158
+ /** Media types this plugin hands the attachment store, by file extension. */
159
+ const IMAGE_MEDIA_TYPES = {
160
+ '.png': 'image/png',
161
+ '.jpg': 'image/jpeg',
162
+ '.jpeg': 'image/jpeg',
163
+ '.webp': 'image/webp',
164
+ '.gif': 'image/gif',
165
+ };
166
+ /** A cap on one image, so a stray large file cannot stall a delegation. */
167
+ const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
168
+ /**
169
+ * A line naming where each image is, appended to the text.
170
+ *
171
+ * A foreground call carries the pictures as blocks, but a background call's
172
+ * result is a string by contract — its reader sees this text and nothing else. So
173
+ * the paths go in the text as well: a screenshot a background delegation took is
174
+ * still a file on this host, and naming it is the difference between the work
175
+ * being usable and being lost.
176
+ *
177
+ * @param text - the delegation's answer.
178
+ * @param images - the images the run produced.
179
+ * @returns the answer with the image line appended.
180
+ */
181
+ export function withImageNote(text, images) {
182
+ if (images.length === 0)
183
+ return text;
184
+ const paths = images.map((image) => image.path).filter((path) => path !== undefined);
185
+ const label = images.length === 1 ? 'image' : 'images';
186
+ return paths.length === 0
187
+ ? `${text}\n\n[${String(images.length)} ${label}]`
188
+ : `${text}\n\n[${String(images.length)} ${label}: ${paths.join(', ')}]`;
189
+ }
190
+ /** Stable text for an image the model could not be handed. */
191
+ function imageNote(reason) {
192
+ return `[image not delivered: ${reason}]`;
193
+ }
194
+ /** The media type for a path, when its extension names one this store admits. */
195
+ function mediaTypeOf(path) {
196
+ const dot = path.lastIndexOf('.');
197
+ return dot < 0 ? undefined : IMAGE_MEDIA_TYPES[path.slice(dot).toLowerCase()];
198
+ }
199
+ /** This package's own version, read once for the diagnostic line. */
200
+ const PLUGIN_VERSION = (() => {
201
+ try {
202
+ const require_ = createRequire(import.meta.url);
203
+ const pkg = require_('../package.json');
204
+ return pkg.version ?? 'unknown';
205
+ }
206
+ catch {
207
+ return 'unknown';
208
+ }
209
+ })();
210
+ /** The effort values Codex accepts; used only for diagnostics. */
211
+ const KNOWN_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
212
+ /** The unattended default: a delegated task must run without a human present. */
213
+ const DEFAULT_PERMISSION_MODE = 'workspace-write';
214
+ /** Tool calls may narrow their configured policy, never expand it implicitly. */
215
+ export function resolvePermissionOverride(configured, requested) {
216
+ const permission = requested ?? configured;
217
+ if (permission !== configured && permission !== 'read-only'
218
+ && !(configured === 'dangerously-bypass-approvals-and-sandbox' && permission === 'workspace-write')) {
219
+ throw new Error('subagent_codex: permission override exceeds the configured policy; change plugin configuration explicitly');
220
+ }
221
+ return permission;
222
+ }
223
+ /**
224
+ * Append one line to `~/.dsh/codex-pro.log`.
225
+ *
226
+ * A plugin has no log surface a running session can read, and a tool that fails
227
+ * to register is otherwise silent. Never throws.
228
+ *
229
+ * @param message - the line to record.
230
+ */
231
+ function diag(message) {
232
+ try {
233
+ appendFileSync(join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'codex-pro.log'), `${new Date().toISOString()} ${message}\n`);
234
+ }
235
+ catch {
236
+ // Diagnostics must never affect what they describe.
237
+ }
238
+ }
239
+ /** Join the textual blocks of a delegated prompt. */
240
+ function promptText(prompt) {
241
+ return prompt
242
+ .filter((block) => block.type === 'text')
243
+ .map((block) => block.text)
244
+ .join('\n\n');
245
+ }
246
+ /**
247
+ * One delegation to Codex, exposed as a provider the harness can start.
248
+ *
249
+ * Exported so the provider adapter can be exercised against the same fake
250
+ * app-server as the tool path; the plugin's `apply` is its only production
251
+ * constructor.
252
+ */
253
+ export class CodexExecProvider {
254
+ name;
255
+ inheritsParentContext = false;
256
+ /**
257
+ * The declaration that unlocks the delegation tool's model surface.
258
+ *
259
+ * `agentOptions: true` is what `tool-subagent` checks before accepting either a
260
+ * configured `agentOptions` or `modelSelectionSettings: true` on its row.
261
+ */
262
+ capabilities = {
263
+ agentOptions: true,
264
+ agentPreset: false,
265
+ outputSchema: true,
266
+ depthLimit: false,
267
+ };
268
+ /**
269
+ * The resolved configuration.
270
+ *
271
+ * A plain field rather than a constructor parameter property: Node's
272
+ * type-stripping mode rejects that syntax outright, which made this module
273
+ * unimportable from a test and left its pure helpers untestable.
274
+ */
275
+ resolved;
276
+ constructor(resolved) {
277
+ this.resolved = resolved;
278
+ this.name = resolved.providerName;
279
+ }
280
+ /**
281
+ * Start one Codex run over the app-server protocol.
282
+ *
283
+ * @param request - the delegation the harness resolved.
284
+ * @returns a run handle; its `result` settles with the child's final message.
285
+ */
286
+ async start(request) {
287
+ const cwd = request.cwd ?? request.parent.session.header.cwd;
288
+ if (cwd === undefined) {
289
+ throw new Error('codex-pro: the parent session has no working directory to delegate from');
290
+ }
291
+ const options = request.agentOptions;
292
+ // Host AgentOptions are not Codex configuration. Unsupported fields must
293
+ // never disappear behind the coarse agentOptions capability flag.
294
+ if (options?.provider !== undefined || options?.maxTokens !== undefined) {
295
+ throw new Error('codex-pro: provider and maxTokens overrides are not supported by this Codex backend');
296
+ }
297
+ const model = options?.model ?? this.resolved.model;
298
+ const effort = options?.reasoningEffort ?? this.resolved.reasoningEffort;
299
+ const spec = {
300
+ cwd,
301
+ command: this.resolved.command,
302
+ permissionMode: this.resolved.permissionMode,
303
+ model,
304
+ reasoningEffort: effort,
305
+ effort,
306
+ runTimeoutMs: this.resolved.runTimeoutMs,
307
+ ...(request.outputSchema === undefined
308
+ ? {}
309
+ : { outputSchema: request.outputSchema }),
310
+ env: this.resolved.env,
311
+ onProgress: (text) => { diag(`provider progress: ${text}`); },
312
+ };
313
+ diag(`provider start: model=${model ?? '<default>'} effort=${effort ?? '<default>'} cwd=${cwd} via=app-server`);
314
+ let run;
315
+ try {
316
+ run = await startCodexRun(request.prompt, spec, request.signal);
317
+ }
318
+ catch (error) {
319
+ diag(`provider start FAILED: ${error instanceof Error ? error.message : String(error)}`);
320
+ throw error;
321
+ }
322
+ diag(`provider start: published run ${run.id}`);
323
+ return {
324
+ id: run.id,
325
+ localAgent: undefined,
326
+ result: run.result.then((outcome) => {
327
+ diag(`provider done: stop=${outcome.stopReason} thread=${outcome.threadId ?? '<none>'} `
328
+ + `usage=${outcome.usage.status}${outcome.usage.run === undefined ? '' : `(${String(outcome.usage.run.totalTokens)} tokens)`} `
329
+ + `cleanup=${outcome.cleanup.confirmed ? 'confirmed' : 'UNCONFIRMED'} `
330
+ + `rateLimits=${String(outcome.rateLimits.observations)} `
331
+ + `config=${outcome.configuration === undefined ? 'n/a' : String(outcome.configuration.matched)}`);
332
+ if (outcome.configuration !== undefined) {
333
+ diag(`provider config: requested=${JSON.stringify(outcome.configuration.requested)} `
334
+ + `effective=${JSON.stringify(outcome.configuration.effective)}`);
335
+ }
336
+ // DSH diagnostics are bounded metadata, never raw server messages,
337
+ // prompts, paths, environment values, or structured answer contents.
338
+ const failure = outcome.execution.failure;
339
+ const diagnostic = [
340
+ ...(outcome.stopReason === 'completed' ? [] : ['codex-pro: run ended as ' + outcome.stopReason]),
341
+ ...(outcome.cleanup.confirmed ? [] : ['process cleanup could not be confirmed']),
342
+ ...(outcome.schemaError === undefined ? [] : ['structured output did not satisfy the requested schema']),
343
+ ...(failure === null ? [] : [`failure: ${failure.stage}/${failure.category}`
344
+ + (failure.httpStatus === undefined ? '' : ` (HTTP ${String(failure.httpStatus)})`)]),
345
+ ...(outcome.approvalDiagnostic === undefined ? [] : [outcome.approvalDiagnostic]),
346
+ ].join('; ').slice(0, 4096);
347
+ return {
348
+ output: outcome.output,
349
+ // The harness's own vocabulary has no `timeout`; its `aborted` is what
350
+ // a cancelled run is, and the diagnostic carries the reason either way.
351
+ stopReason: outcome.stopReason === 'timeout' ? 'aborted' : outcome.stopReason,
352
+ ...(outcome.structured === undefined ? {} : { structured: outcome.structured }),
353
+ ...(diagnostic === '' ? {} : { diagnostic }),
354
+ };
355
+ }),
356
+ // Host dispose returns void on success, but rejects when teardown cannot
357
+ // be confirmed. The manager records that failure separately from output.
358
+ dispose: async () => {
359
+ const cleanup = await run.teardown();
360
+ if (!cleanup.confirmed)
361
+ throw new Error('codex-pro: process cleanup could not be confirmed');
362
+ },
363
+ };
364
+ }
365
+ }
366
+ /**
367
+ * Register the provider and the delegation tool.
368
+ *
369
+ * @param ctx - context carrying the subagent registry.
370
+ * @param config - provider name, defaults, and the presets to serve.
371
+ */
372
+ export function apply(ctx, config = {}) {
373
+ const resolved = {
374
+ providerName: config.providerName ?? name,
375
+ command: config.command ?? 'codex',
376
+ permissionMode: config.permissionMode ?? DEFAULT_PERMISSION_MODE,
377
+ modelSelection: config.modelSelection ?? true,
378
+ threadMode: config.threadMode ?? 'ephemeral',
379
+ originator: config.originator ?? DEFAULT_ORIGINATOR,
380
+ runTimeoutMs: config.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS,
381
+ ...(config.model === undefined ? {} : { model: config.model }),
382
+ ...(config.reasoningEffort === undefined ? {} : { reasoningEffort: config.reasoningEffort }),
383
+ ...(config.env === undefined ? {} : { env: config.env }),
384
+ };
385
+ const presets = config.presets ?? ['codexprobe'];
386
+ try {
387
+ ctx.subagents.registerProvider(new CodexExecProvider(resolved));
388
+ diag(`apply: v${PLUGIN_VERSION} provider "${resolved.providerName}" registered with agentOptions: true `
389
+ + `(efforts: ${KNOWN_EFFORTS.join('/')}, permission=${resolved.permissionMode}); `
390
+ + `threadMode=${resolved.threadMode} originator=${JSON.stringify(resolved.originator)} `
391
+ + `timeout=${String(resolved.runTimeoutMs)}ms; `
392
+ + `presets=${JSON.stringify(presets)}`);
393
+ }
394
+ catch (error) {
395
+ diag(`apply: provider registration FAILED: ${String(error)}`);
396
+ }
397
+ /**
398
+ * Whether one Agent's committed preset is one this plugin serves.
399
+ *
400
+ * The registry is authoritative; the session header is only the value the
401
+ * session was created with, which the picker replaces afterwards.
402
+ */
403
+ const presetAllows = (agent) => {
404
+ const registry = ctx.get('agentPresets');
405
+ let composed;
406
+ let composeError;
407
+ try {
408
+ composed = registry?.composedPreset?.(agent.ctx);
409
+ }
410
+ catch (error) {
411
+ composeError = error instanceof Error ? error.message : String(error);
412
+ }
413
+ const header = agent.session.header.agentPreset;
414
+ const preset = composed ?? header;
415
+ const allowed = preset !== undefined && presets.includes(preset);
416
+ diag(`decide ${String(agent.id)} composed=${String(composed)} header=${String(header)} `
417
+ + `err=${String(composeError)} -> ${allowed ? 'serve' : 'skip'}`);
418
+ return allowed;
419
+ };
420
+ /** Register the delegation tool for one Agent. */
421
+ const serve = (agent) => {
422
+ const cwd = agent.session.header.cwd;
423
+ if (cwd === undefined) {
424
+ diag(`serve ${String(agent.id)}: skipped, the session has no working directory`);
425
+ return;
426
+ }
427
+ const selection = resolved.modelSelection;
428
+ // A delegation takes a minute or more, so a background job is the default
429
+ // shape: it keeps the turn free. Without the jobs service the tool still
430
+ // works, it just runs in the foreground and says nothing about it.
431
+ const jobs = ctx.get('jobs');
432
+ /**
433
+ * Whether the calling Agent's model route refuses images.
434
+ *
435
+ * The rule is the harness's own, deliberately: an image is refused only when
436
+ * the route **declares** input modalities and `image` is not among them. An
437
+ * undeclared route is left to the harness, which is where an attachment for a
438
+ * model that cannot read it is rejected anyway.
439
+ *
440
+ * Being stricter here was a real mistake: the default `deepseek-flash` entry
441
+ * declares `['text', 'image']`, and a guard that failed closed on an
442
+ * unresolvable route silently turned every screenshot into a text note. So an
443
+ * unresolvable route now delivers the image and lets the harness decide.
444
+ *
445
+ * @returns whether this plugin should withhold images.
446
+ */
447
+ const shouldWithholdImages = async () => {
448
+ const llm = ctx.get('llm');
449
+ const header = agent.session.requestHeader?.();
450
+ const options = agent.options;
451
+ const provider = header?.config?.provider ?? options?.provider;
452
+ const model = header?.config?.model ?? options?.model;
453
+ if (llm === undefined || provider === undefined || model === undefined)
454
+ return false;
455
+ try {
456
+ return routeRefusesImages(await llm.resolveModelInfo(provider, model));
457
+ }
458
+ catch {
459
+ return false;
460
+ }
461
+ };
462
+ /**
463
+ * Turn one finished delegation into the blocks the model receives.
464
+ *
465
+ * A run's images are read here, because the app-server reports them as paths
466
+ * on this host rather than as bytes, and handed to the attachment store so
467
+ * the model gets an image instead of a description of one. Nothing here can
468
+ * fail the delegation: a store that is absent, a route that cannot take
469
+ * images, and a file that cannot be read all become a text note, because the
470
+ * answer the delegation produced matters more than the picture beside it.
471
+ *
472
+ * @param outcome - the finished run.
473
+ * @param mode - the thread mode the run used.
474
+ * @returns the text, the thread id, and any content blocks to deliver.
475
+ */
476
+ const deliverResult = async (outcome, mode, machineOutput) => {
477
+ const text = withThreadMarker(outcome, mode, machineOutput);
478
+ const images = outcome.images ?? [];
479
+ if (images.length === 0)
480
+ return { text, threadId: outcome.threadId };
481
+ const notes = [];
482
+ const notesFor = (reason) => { notes.push(imageNote(reason)); };
483
+ const store = ctx.get('attachments');
484
+ if (store === undefined) {
485
+ for (let i = 0; i < images.length; i += 1)
486
+ notesFor('no attachment store is mounted');
487
+ diag(`deliver: ${String(notes.length)} image(s) could not be attached (no store)`);
488
+ return { text: machineOutput ? text : `${text}\n\n${notes.join('\n')}`, threadId: outcome.threadId };
489
+ }
490
+ if (await shouldWithholdImages()) {
491
+ for (let i = 0; i < images.length; i += 1)
492
+ notesFor('this model route declares no image input');
493
+ diag(`deliver: ${String(notes.length)} image(s) withheld from this model route`);
494
+ return { text: machineOutput ? text : `${text}\n\n${notes.join('\n')}`, threadId: outcome.threadId };
495
+ }
496
+ const payloads = [];
497
+ for (const image of images) {
498
+ try {
499
+ if (image.data !== undefined) {
500
+ payloads.push({ data: Buffer.from(image.data, 'base64'), mediaType: image.mimeType ?? 'image/png' });
501
+ continue;
502
+ }
503
+ if (image.path === undefined) {
504
+ notesFor('the image had neither a path nor data');
505
+ continue;
506
+ }
507
+ const mediaType = mediaTypeOf(image.path);
508
+ if (mediaType === undefined) {
509
+ notesFor(`unsupported image type: ${image.path}`);
510
+ continue;
511
+ }
512
+ const stat = statSync(image.path);
513
+ if (stat.size > MAX_IMAGE_BYTES) {
514
+ notesFor(`larger than ${String(MAX_IMAGE_BYTES)} bytes: ${image.path}`);
515
+ continue;
516
+ }
517
+ payloads.push({ data: readFileSync(image.path), mediaType });
518
+ }
519
+ catch (error) {
520
+ notesFor(`could not read ${image.path ?? 'the image'}: ${String(error)}`.slice(0, 200));
521
+ }
522
+ }
523
+ if (payloads.length === 0) {
524
+ diag(`deliver: ${String(notes.length)} image(s) could not be read`);
525
+ return { text: machineOutput ? text : `${text}\n\n${notes.join('\n')}`, threadId: outcome.threadId };
526
+ }
527
+ try {
528
+ const refs = await store.saveImages(payloads);
529
+ const deliveredText = machineOutput ? text : withImageNote(text, images);
530
+ const blocks = [
531
+ { type: 'text', text: deliveredText },
532
+ ...refs.map((ref) => ({ type: 'image', attachment: ref })),
533
+ ];
534
+ diag(`deliver: ${String(refs.length)} image(s) attached to the delegation result`);
535
+ return { text: deliveredText, threadId: outcome.threadId, blocks };
536
+ }
537
+ catch (error) {
538
+ notesFor(`the attachment store refused them: ${String(error)}`.slice(0, 200));
539
+ diag(`deliver: attachment store refused ${String(payloads.length)} image(s)`);
540
+ return { text: machineOutput ? text : `${text}\n\n${notes.join('\n')}`, threadId: outcome.threadId };
541
+ }
542
+ };
543
+ /**
544
+ * The durable thread this Agent session is reusing, for `threadMode: session`.
545
+ *
546
+ * One Agent is one session, so this map is that session's memory of the
547
+ * thread it opened; it is deliberately per-Agent rather than global, because
548
+ * two sessions delegating at once must not continue each other's work.
549
+ */
550
+ const sessionThread = new Map();
551
+ /**
552
+ * Threads this Agent is delegating in right now.
553
+ *
554
+ * A thread carries one turn at a time, so the session's remembered thread
555
+ * cannot be handed to two calls at once. Tracking it here turns that into a
556
+ * decision before the call starts rather than a failure after it.
557
+ */
558
+ const inFlight = new Set();
559
+ /** Run one call over the app-server protocol. */
560
+ const runCall = async (call, signal, onProgress) => {
561
+ // A delegation may name its own working directory; the session's is the
562
+ // default. The path is a working directory, not a sandbox boundary: what a
563
+ // run may touch is the permission mode's decision, not this one's.
564
+ let runCwd = cwd;
565
+ if (call.cwd !== undefined) {
566
+ runCwd = checkedWorkdir(call.cwd);
567
+ let stat;
568
+ try {
569
+ stat = statSync(runCwd);
570
+ }
571
+ catch {
572
+ throw new Error(`subagent_codex: cwd does not exist: ${runCwd}`);
573
+ }
574
+ if (!stat.isDirectory())
575
+ throw new Error(`subagent_codex: cwd is not a directory: ${runCwd}`);
576
+ }
577
+ // Images are checked here, before anything is spawned: a path that does not
578
+ // exist should fail the call, not start a run that silently lacks the
579
+ // picture the task was about.
580
+ let runImages;
581
+ if (call.images !== undefined && call.images.length > 0) {
582
+ runImages = [];
583
+ for (const image of call.images) {
584
+ const path = checkedImagePath(image);
585
+ let stat;
586
+ try {
587
+ stat = statSync(path);
588
+ }
589
+ catch {
590
+ throw new Error(`subagent_codex: image does not exist: ${path}`);
591
+ }
592
+ if (!stat.isFile())
593
+ throw new Error(`subagent_codex: image is not a file: ${path}`);
594
+ runImages.push(path);
595
+ }
596
+ }
597
+ const runTimeout = call.timeoutMs === undefined ? undefined : checkedTimeout(call.timeoutMs);
598
+ const runSchema = call.outputSchema === undefined
599
+ ? undefined
600
+ : checkedOutputSchema(call.outputSchema);
601
+ const callPermission = resolvePermissionOverride(resolved.permissionMode, call.permissionMode);
602
+ const timeoutMs = runTimeout ?? resolved.runTimeoutMs;
603
+ // One absolute deadline for this call, shared by the auto-resume retry: a
604
+ // second attempt after a failed resume must not receive a fresh budget.
605
+ const deadlineAt = timeoutMs > 0 ? Date.now() + timeoutMs : undefined;
606
+ const machineOutput = runSchema !== undefined;
607
+ // An explicit thread id wins; then an explicit mode; then the session's
608
+ // remembered thread; then the configured default.
609
+ const configured = resolved.threadMode;
610
+ const mode = call.threadMode ?? (configured === 'ephemeral' ? 'ephemeral' : 'persistent');
611
+ const remembered = sessionThread.get(String(agent.id));
612
+ const explicit = call.threadId;
613
+ const choice = chooseThread({
614
+ explicit,
615
+ configured,
616
+ // A remembered thread is only reusable for a durable call, and only in
617
+ // the directory it was created in: a Codex thread belongs to its own
618
+ // working directory, so a call that names another one starts fresh.
619
+ remembered: mode === 'persistent' && call.cwd === undefined ? remembered : undefined,
620
+ inFlight,
621
+ });
622
+ const reuse = choice.reuse;
623
+ if (choice.skippedBusy !== undefined) {
624
+ diag(`run: session thread ${choice.skippedBusy} is busy with another delegation - `
625
+ + `this call gets a thread of its own`);
626
+ }
627
+ diag(`run: model=${call.model ?? '<default>'} effort=${call.reasoningEffort ?? '<default>'} `
628
+ + `thread=${mode}${reuse === undefined ? '' : ` resume=${reuse}`} `
629
+ + `cwd=${runCwd}${call.cwd === undefined ? '' : ' (call-named)'} `
630
+ + `images=${String(runImages?.length ?? 0)} `
631
+ + `schema=${runSchema === undefined ? 'none' : 'yes'} `
632
+ + `timeout=${String(runTimeout ?? resolved.runTimeoutMs)}ms`
633
+ + `${runTimeout === undefined ? '' : ' (call-named)'} `
634
+ + `prompt=${String(call.prompt.length)}B via=app-server`);
635
+ // Claim the thread for the whole call, so a concurrent delegation sees it
636
+ // as busy before it tries rather than failing when it does.
637
+ if (reuse !== undefined)
638
+ inFlight.add(reuse);
639
+ try {
640
+ const outcome = await delegateOnce({
641
+ prompt: call.prompt,
642
+ cwd: runCwd,
643
+ command: resolved.command,
644
+ permissionMode: callPermission,
645
+ model: call.model,
646
+ reasoningEffort: call.reasoningEffort,
647
+ threadMode: mode,
648
+ threadId: reuse,
649
+ originator: resolved.originator,
650
+ runTimeoutMs: timeoutMs,
651
+ ...(deadlineAt === undefined ? {} : { deadlineAt }),
652
+ images: runImages,
653
+ outputSchema: runSchema,
654
+ threadName: threadNameFor(call.label),
655
+ env: resolved.env,
656
+ signal,
657
+ onStart: (id) => { diag(`run: published ${id}`); },
658
+ onProgress,
659
+ });
660
+ if (mode === 'persistent' && outcome.threadId !== undefined) {
661
+ sessionThread.set(String(agent.id), outcome.threadId);
662
+ }
663
+ diag(`run done: text=${String(outcome.text.length)}B thread=${outcome.threadId ?? '<none>'} `
664
+ + `images=${String(outcome.images?.length ?? 0)} usage=${outcome.usage.status}`
665
+ + `${outcome.usage.run === undefined ? '' : `(${String(outcome.usage.run.totalTokens)} tokens)`} `
666
+ + `cleanup=${outcome.cleanup.confirmed ? 'confirmed' : 'UNCONFIRMED'} `
667
+ + `rateLimits=${String(outcome.rateLimits.observations)} `
668
+ + `config=${outcome.configuration === undefined ? 'n/a' : String(outcome.configuration.matched)}`);
669
+ if (outcome.configuration !== undefined) {
670
+ diag(`run config: requested=${JSON.stringify(outcome.configuration.requested)} `
671
+ + `effective=${JSON.stringify(outcome.configuration.effective)}`);
672
+ }
673
+ return await deliverResult(outcome, mode, machineOutput);
674
+ }
675
+ catch (error) {
676
+ // A session's remembered thread can be gone — the user removed it, or the
677
+ // Codex state moved. That is this plugin's own bookkeeping, so a fresh
678
+ // thread is the right repair; an id the caller passed explicitly is not
679
+ // repaired silently, because continuing it was the whole request.
680
+ // Only a resume that the app-server refused is repaired here. A turn that
681
+ // failed for its own reasons must not be retried: that would pay for the
682
+ // same work twice and hide the real failure behind a second attempt.
683
+ if (reuse !== undefined
684
+ && explicit === undefined
685
+ && mode === 'persistent'
686
+ && error instanceof CodexRunFailure
687
+ && error.facts.stage === 'thread-resume'
688
+ // A protocol-level refusal or configuration mismatch is what a fresh
689
+ // thread repairs. A timeout, caller abort, or process failure is not:
690
+ // retrying those would muddy the reason the delegation stopped.
691
+ && error.facts.category === 'protocol') {
692
+ if (deadlineAt !== undefined && deadlineAt <= Date.now()) {
693
+ diag(`run: remembered thread ${reuse} could not be resumed and the call has no budget left; not retrying`);
694
+ throw error;
695
+ }
696
+ diag(`run: remembered thread ${reuse} could not be resumed (${String(error)}) - starting a new one `
697
+ + `with ${deadlineAt === undefined ? 'no deadline' : `${String(deadlineAt - Date.now())}ms remaining`}`);
698
+ sessionThread.delete(String(agent.id));
699
+ const outcome = await delegateOnce({
700
+ prompt: call.prompt,
701
+ cwd: runCwd,
702
+ command: resolved.command,
703
+ permissionMode: callPermission,
704
+ model: call.model,
705
+ reasoningEffort: call.reasoningEffort,
706
+ threadMode: mode,
707
+ originator: resolved.originator,
708
+ runTimeoutMs: timeoutMs,
709
+ ...(deadlineAt === undefined ? {} : { deadlineAt }),
710
+ images: runImages,
711
+ outputSchema: runSchema,
712
+ threadName: threadNameFor(call.label),
713
+ env: resolved.env,
714
+ signal,
715
+ onStart: (id) => { diag(`run: published ${id}`); },
716
+ onProgress,
717
+ });
718
+ if (outcome.threadId !== undefined)
719
+ sessionThread.set(String(agent.id), outcome.threadId);
720
+ diag(`run done: text=${String(outcome.text.length)}B thread=${outcome.threadId ?? '<none>'} `
721
+ + `images=${String(outcome.images?.length ?? 0)} usage=${outcome.usage.status} `
722
+ + `cleanup=${outcome.cleanup.confirmed ? 'confirmed' : 'UNCONFIRMED'}`);
723
+ return await deliverResult(outcome, mode, machineOutput);
724
+ }
725
+ throw error;
726
+ }
727
+ finally {
728
+ if (reuse !== undefined)
729
+ inFlight.delete(reuse);
730
+ }
731
+ };
732
+ try {
733
+ agent.ctx.tools.register(createCodexTool({
734
+ cwd,
735
+ modelSelectionEnabled: selection,
736
+ backgroundEnabled: jobs !== undefined,
737
+ defaultThreadMode: resolved.threadMode,
738
+ permissionMode: resolved.permissionMode,
739
+ threadControlEnabled: true,
740
+ ...(resolved.model === undefined ? {} : { model: resolved.model }),
741
+ ...(resolved.reasoningEffort === undefined ? {} : { reasoningEffort: resolved.reasoningEffort }),
742
+ run: (call) => runCall(call, call.signal),
743
+ startBackground: (call) => {
744
+ if (jobs === undefined) {
745
+ throw new Error('subagent_codex: background runs need the jobs service (@deepseek-ai/dsh-jobs)');
746
+ }
747
+ const controller = new AbortController();
748
+ const id = jobs.start({
749
+ kind: 'subagent',
750
+ label: call.label,
751
+ owner: String(agent.id),
752
+ run: (handle) => {
753
+ handle.updateProgress('starting Codex');
754
+ return {
755
+ cancel: (reason) => {
756
+ controller.abort(new Error(reason ?? 'codex delegation killed'));
757
+ },
758
+ // The child's own activity becomes the job's output, so
759
+ // `job_output` shows how a long delegation is going rather than
760
+ // only what it finally said. `settleJob` turns the delegation
761
+ // into the outcome record the registry requires.
762
+ done: settleJob(runCall(call, controller.signal, (text) => { handle.append(text); }))
763
+ .finally(() => { handle.updateProgress(''); }),
764
+ };
765
+ },
766
+ });
767
+ diag(`serve: background job ${id} started for "${call.label}"`);
768
+ return id;
769
+ },
770
+ }));
771
+ diag(`serve ${String(agent.id)}: subagent_codex registered `
772
+ + `(modelSelection=${String(selection)}, background=${String(jobs !== undefined)}, `
773
+ + `model=${resolved.model ?? '<default>'}, via=app-server)`);
774
+ }
775
+ catch (error) {
776
+ diag(`serve ${String(agent.id)} FAILED: ${error instanceof Error ? `${error.name}: ${error.message}` : String(error)}`);
777
+ }
778
+ };
779
+ ctx.on('agent/created', ({ agent }) => {
780
+ if (!presetAllows(agent))
781
+ return;
782
+ serve(agent);
783
+ });
784
+ // A new task starts on the deployment default and the picker's mode is applied
785
+ // afterwards, so `agent/created` alone can see the wrong composition.
786
+ ctx.on('agent-preset/selected', (sessionId, presetId) => {
787
+ if (!presets.includes(presetId))
788
+ return;
789
+ const agents = ctx.get('agents');
790
+ const agent = agents?.get?.(String(sessionId));
791
+ if (agent === undefined) {
792
+ diag(`preset/selected ${String(sessionId)}: no live agent`);
793
+ return;
794
+ }
795
+ serve(agent);
796
+ });
797
+ }
798
+ /**
799
+ * Append the thread marker to a human-text delegation result.
800
+ *
801
+ * A machine-readable answer (one carrying `output_schema`) must stay valid JSON:
802
+ * appending a marker would make the whole result unparseable. Such a run keeps
803
+ * its thread id in the result envelope/diagnostic log instead of in the text.
804
+ *
805
+ * @param outcome - the finished run.
806
+ * @param mode - the thread mode the run used.
807
+ * @param machineOutput - whether the answer is schema-constrained JSON.
808
+ * @returns the text to report.
809
+ */
810
+ export function withThreadMarker(outcome, mode, machineOutput) {
811
+ if (machineOutput || mode === 'ephemeral' || outcome.threadId === undefined)
812
+ return outcome.text;
813
+ return `${outcome.text}\n\n[codex thread ${outcome.threadId} · persistent · pass it as thread_id to continue]`;
814
+ }
815
+ /**
816
+ * The name a persistent thread carries in the Codex application.
817
+ *
818
+ * A thread from this provider has to report itself as `Codex Desktop` to be
819
+ * listed there at all, so the name is the only place that can say the work came
820
+ * from DSH and what it was. The model's own short description is the best label
821
+ * available, and the prefix keeps them together in a list sorted by recency.
822
+ *
823
+ * @param label - the call's short description.
824
+ * @returns the name to record.
825
+ */
826
+ function threadNameFor(label) {
827
+ const trimmed = label.trim();
828
+ return trimmed === '' ? 'DSH · Codex delegation' : `DSH · ${trimmed}`;
829
+ }
830
+ //# sourceMappingURL=index.js.map