@toddzheng024/dscode-bundle 0.7.26 → 0.7.28

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 (99) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +10 -10
  3. package/bootstrap.mjs +1 -1
  4. package/cordis.patch.yml +418 -45
  5. package/package.json +279 -245
  6. package/plugins/auto-review/index.mjs +8 -5
  7. package/plugins/code-review/index.mjs +1 -1
  8. package/plugins/computer-use/index.mjs +37 -0
  9. package/plugins/cordis-config/plain.mjs +31 -0
  10. package/plugins/exec/cli.mjs +1 -1
  11. package/plugins/grok/index.mjs +28 -24
  12. package/plugins/memory/index.mjs +1 -1
  13. package/plugins/message-source/kind.mjs +20 -0
  14. package/plugins/openrouter/adapter.mjs +29 -13
  15. package/plugins/openrouter/index.mjs +31 -28
  16. package/plugins/session-bridge/communication.mjs +6 -3
  17. package/plugins/session-bridge/server.mjs +3 -2
  18. package/plugins/session-bridge/tasks.mjs +3 -1
  19. package/plugins/session-cards/index.mjs +1 -1
  20. package/plugins/time-marks/index.mjs +3 -2
  21. package/plugins/time-marks/marks.mjs +6 -5
  22. package/plugins/triggers/config.mjs +1 -1
  23. package/plugins/triggers/tools.mjs +1 -1
  24. package/plugins/tui-tools/doctor.mjs +1 -1
  25. package/plugins/tui-tools/index.mjs +16 -4
  26. package/plugins/ultra/policy.mjs +16 -0
  27. package/vendor/bash/index.js +455 -186
  28. package/vendor/bash/types/background.d.ts +38 -7
  29. package/vendor/bash/types/index.d.ts +20 -4
  30. package/vendor/bash/types/render.d.ts +28 -9
  31. package/vendor/command-goal/index.js +3 -1
  32. package/vendor/deepseek/index.js +1378 -1246
  33. package/vendor/deepseek/types/adapter.d.ts +16 -155
  34. package/vendor/deepseek/types/config.d.ts +127 -0
  35. package/vendor/deepseek/types/defaults.d.ts +24 -0
  36. package/vendor/deepseek/types/file-store.d.ts +5 -3
  37. package/vendor/deepseek/types/files-api.d.ts +12 -9
  38. package/vendor/deepseek/types/image-tokens.d.ts +24 -9
  39. package/vendor/deepseek/types/images.d.ts +35 -0
  40. package/vendor/deepseek/types/index.d.ts +10 -94
  41. package/vendor/deepseek/types/messages-api.d.ts +10 -0
  42. package/vendor/deepseek/types/model-info.d.ts +16 -0
  43. package/vendor/deepseek/types/models.d.ts +4 -0
  44. package/vendor/deepseek/types/replay.d.ts +27 -0
  45. package/vendor/deepseek/types/request-extensions.d.ts +16 -0
  46. package/vendor/deepseek/types/request-files.d.ts +49 -0
  47. package/vendor/deepseek/types/request-pricing.d.ts +32 -20
  48. package/vendor/deepseek/types/serialize.d.ts +17 -82
  49. package/vendor/deepseek/types/sse.d.ts +6 -22
  50. package/vendor/deepseek/types/translate.d.ts +13 -36
  51. package/vendor/deepseek/types/transport.d.ts +15 -0
  52. package/vendor/deepseek/types/types.d.ts +106 -167
  53. package/vendor/deepseek/types/wire-types.d.ts +62 -0
  54. package/vendor/persistent/index.js +9 -3
  55. package/vendor/subagent/index.js +11 -8
  56. package/vendor/subagent/model-selection-settings.js +12 -38
  57. package/vendor/subagent/types/index.d.ts +4 -3
  58. package/vendor/subagent/types/model-selection-settings.d.ts +12 -10
  59. package/vendor/subagent-core/index.js +401 -264
  60. package/vendor/subagent-core/typert.host.js +103 -113
  61. package/vendor/subagent-core/typert.remote-client.d.ts +1 -3
  62. package/vendor/subagent-core/typert.remote-client.js +23 -69
  63. package/vendor/subagent-core/types/archive-admission.d.ts +16 -0
  64. package/vendor/subagent-core/types/archive-admission.js +151 -0
  65. package/vendor/subagent-core/types/assistant-output.d.ts +2 -2
  66. package/vendor/subagent-core/types/catalog.d.ts +17 -10
  67. package/vendor/subagent-core/types/catalog.js +12 -6
  68. package/vendor/subagent-core/types/child-agent.d.ts +8 -5
  69. package/vendor/subagent-core/types/child-agent.js +11 -5
  70. package/vendor/subagent-core/types/continuation-activation.d.ts +17 -1
  71. package/vendor/subagent-core/types/continuation-activation.js +41 -4
  72. package/vendor/subagent-core/types/continuation-messages.d.ts +1 -1
  73. package/vendor/subagent-core/types/continuation-messages.js +7 -3
  74. package/vendor/subagent-core/types/continuation.d.ts +1 -1
  75. package/vendor/subagent-core/types/continuation.js +2 -2
  76. package/vendor/subagent-core/types/control-types.d.ts +29 -36
  77. package/vendor/subagent-core/types/control-types.js +2 -3
  78. package/vendor/subagent-core/types/control.d.ts +2 -28
  79. package/vendor/subagent-core/types/control.js +3 -40
  80. package/vendor/subagent-core/types/index.d.ts +45 -41
  81. package/vendor/subagent-core/types/index.js +46 -82
  82. package/vendor/subagent-core/types/internal.d.ts +2 -2
  83. package/vendor/subagent-core/types/internal.js +2 -2
  84. package/vendor/subagent-core/types/lifecycle.d.ts +1 -1
  85. package/vendor/subagent-core/types/lifecycle.js +5 -3
  86. package/vendor/subagent-core/types/list-children.d.ts +15 -32
  87. package/vendor/subagent-core/types/list-children.js +45 -49
  88. package/vendor/subagent-core/types/out-of-process.d.ts +1 -1
  89. package/vendor/subagent-core/types/projection-types.d.ts +9 -1
  90. package/vendor/subagent-core/types/projection.d.ts +5 -0
  91. package/vendor/subagent-core/types/projection.js +10 -4
  92. package/vendor/subagent-core/types/run-settlement.js +1 -1
  93. package/vendor/subagent-core/types/types.d.ts +2 -2
  94. package/vendor/terminal/index.js +94 -30
  95. package/vendor/tui/lib/index.mjs +2 -1
  96. package/vendor/tui/lib/kernel-panels.mjs +1 -1
  97. package/vendor/tui/lib/render/projection.mjs +50 -36
  98. package/presets/dscode/agent.cordis.yml +0 -321
  99. package/presets/dscode/preset.yml +0 -3
@@ -1,33 +1,129 @@
1
1
  // dscode-named-shell-v1
2
2
  import z from "@deepseek-ai/schemastery";
3
- import { isAbsolute, resolve } from "node:path";
3
+ import { isAbsolute, sep } from "node:path";
4
4
  import { TOOL_ABORTED, defineTool } from "@deepseek-ai/dsh-tools";
5
5
  import { HarnessError } from "@deepseek-ai/dsh-llm";
6
- import { ESCALATION_TARGETS, approveEscalation, canonicalPath, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from "@deepseek-ai/dsh-sandbox";
6
+ import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from "@deepseek-ai/dsh-sandbox";
7
7
  import { DSH_ENV_PREFIX, parseExitStatus } from "@deepseek-ai/dsh-shell";
8
8
  //#region lib/types/background.js
9
9
  /**
10
- * Generic-task adaptation for background bash process handles.
10
+ * Generic-job adaptation for bash process handles: the terminal outcome the
11
+ * registry records, the pull sources it pumps, and the ring read a foreground
12
+ * call renders when it stops waiting.
11
13
  *
12
14
  * @module @deepseek-ai/dsh-tool-bash/background
13
15
  */
14
16
  /**
15
- * Map a settled background process onto the generic task-outcome vocabulary:
17
+ * Sandbox facts worth the terminal detail: a runner that never ran the
18
+ * command, or a denial (with the escalation hint this composition offers).
19
+ * @param sandbox - settled sandbox facts, when this was a confined process.
20
+ * @param escalationModes - escalation targets advertised by this composition.
21
+ * @returns the markers to append, oldest first.
22
+ */
23
+ function sandboxNotes(sandbox, escalationModes) {
24
+ if (sandbox?.runnerFailed) return [`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`];
25
+ if (sandbox?.denied) {
26
+ const notes = [sandboxDenialMarker(sandbox.mode)];
27
+ if (escalationModes.length > 0) notes.push(escalationHintMarker("command"));
28
+ return notes;
29
+ }
30
+ return [];
31
+ }
32
+ /**
33
+ * Map a settled background process onto the generic job-outcome vocabulary:
16
34
  * `killed` stays `killed` (detail: the signal when one is known), everything
17
35
  * else is `completed` with the exit code as detail. A nonzero command exit is
18
- * reported, not failed, exactly like the foreground rendering.
36
+ * reported, not failed, exactly like the foreground rendering. Sandbox facts
37
+ * join the detail, since a job's terminal reason is the one line every
38
+ * reader — the model's status line, the roster row — shows.
19
39
  * @param proc - the settled process handle.
40
+ * @param escalationModes - escalation targets advertised by this composition.
20
41
  * @returns the outcome for the `ctx.jobs` registration.
21
42
  */
22
- function processOutcome(proc) {
23
- if (proc.status === "killed") return {
43
+ function processOutcome(proc, escalationModes = []) {
44
+ const base = proc.status === "killed" ? {
24
45
  status: "killed",
25
46
  detail: proc.signal !== null ? `signal: ${proc.signal}` : "killed before exit"
26
- };
27
- return {
47
+ } : {
28
48
  status: "completed",
29
49
  detail: `exit code: ${proc.exitCode ?? 0}`
30
50
  };
51
+ const notes = sandboxNotes(proc.sandbox, escalationModes);
52
+ return notes.length === 0 ? base : {
53
+ ...base,
54
+ detail: `${base.detail}; ${notes.join(" ")}`
55
+ };
56
+ }
57
+ /**
58
+ * The process's non-consuming stream readers as registry pull sources. They
59
+ * bind lazily because the process is spawned inside the starter, after the
60
+ * registry admitted the job; a read before the spawn yields nothing, and the
61
+ * pump keeps the model's consuming cursor untouched. A rejected spawn's
62
+ * stderr reader carries the provider's `subprocess failed before reporting an
63
+ * outcome: …` note.
64
+ * @param proc - the started process's observed streams, once the starter has spawned it.
65
+ * @returns one source per stream, stdout first.
66
+ */
67
+ function processSources(proc) {
68
+ const source = (channel) => ({
69
+ channel,
70
+ read: (fromByte) => {
71
+ const live = proc();
72
+ return live === void 0 ? {
73
+ text: "",
74
+ nextOffset: fromByte,
75
+ lossy: false
76
+ } : live.observed[channel].readFrom(fromByte);
77
+ }
78
+ });
79
+ return [source("stdout"), source("stderr")];
80
+ }
81
+ /**
82
+ * The ring chunks of one consuming registry read as the shell tools render a
83
+ * process read: stdout chunks in order, then every stderr chunk in one
84
+ * `[stderr]` section, so the output a foreground call hands over when it
85
+ * stops waiting reads exactly like the `job_output` reads that follow it.
86
+ * @param chunks - the chunks since the model cursor, in offset order.
87
+ * @returns the delta text, possibly empty.
88
+ */
89
+ function ringDelta(chunks) {
90
+ const out = chunks.filter((chunk) => chunk.channel !== "stderr").map((chunk) => chunk.text).join("");
91
+ const err = chunks.filter((chunk) => chunk.channel === "stderr").map((chunk) => chunk.text).join("");
92
+ const separator = out.length > 0 && !out.endsWith("\n") ? "\n" : "";
93
+ return out + (err.length > 0 ? `${separator}[stderr]\n${err}` : "");
94
+ }
95
+ /**
96
+ * Adapt asynchronous shell preparation after job admission without exposing a partial process.
97
+ * @param start - starts the process with job-owned cancellation.
98
+ * @param outcome - projects the settled process into the job outcome.
99
+ * @returns synchronous job hooks whose completion includes preparation and process settlement.
100
+ */
101
+ function processJob(start, outcome) {
102
+ const controller = new AbortController();
103
+ let process;
104
+ return {
105
+ cancel: (reason) => {
106
+ if (controller.signal.aborted) return;
107
+ controller.abort(reason);
108
+ process?.kill();
109
+ },
110
+ done: (async () => {
111
+ try {
112
+ process = await start(controller.signal);
113
+ try {
114
+ if (controller.signal.aborted) process.kill();
115
+ } finally {
116
+ await process.done;
117
+ }
118
+ return outcome(process);
119
+ } catch (error) {
120
+ return {
121
+ status: controller.signal.aborted && process === void 0 ? "killed" : "failed",
122
+ detail: error instanceof Error ? error.message : String(error)
123
+ };
124
+ }
125
+ })()
126
+ };
31
127
  }
32
128
  //#endregion
33
129
  //#region lib/types/render.js
@@ -50,7 +146,7 @@ function streamText(output) {
50
146
  * @param escalationModes - the escalation targets this composition advertises;
51
147
  * non-empty adds the same-turn escalation hint after a denial marker
52
148
  * (default `[]`: no hint).
53
- * @returns the model-facing text: output body (or `(no output)`), then any timeout/signal/exit markers, each on its own line.
149
+ * @returns the model-facing text: output body (or `(no output)`), then any timeout/stopped/signal/exit markers, each on its own line.
54
150
  */
55
151
  function renderResult(result, escalationModes = []) {
56
152
  const out = streamText(result.stdout);
@@ -67,6 +163,7 @@ function renderResult(result, escalationModes = []) {
67
163
  if (escalationModes.length > 0) markers.push(escalationHintMarker("command"));
68
164
  }
69
165
  if (result.timedOut) markers.push(`[timed out after ${result.timeoutMs}ms]`);
166
+ if (result.stopped !== void 0) markers.push(`[stopped: ${result.stopped}]`);
70
167
  if (result.signal !== null) markers.push(`[killed by signal: ${result.signal}]`);
71
168
  else if (result.exitCode !== 0) markers.push(`[exit code: ${result.exitCode}]`);
72
169
  if (markers.length === 0) return body;
@@ -74,35 +171,50 @@ function renderResult(result, escalationModes = []) {
74
171
  return body + markers.join("\n");
75
172
  }
76
173
  /**
77
- * Shape one background-process read into the `job_output` delta the model
78
- * sees: the incremental delta, plus the lossy-read notice (with full-stream
79
- * spill paths) when in-memory truncation dropped unread bytes. Empty-delta
80
- * rendering (`(no new output)`) is the generic job controller's job.
81
- * @param read - one incremental read from the process handle.
174
+ * Shape a foreground call that stopped waiting into the text the model sees:
175
+ * the output captured so far (one consuming registry read taken at that
176
+ * point, so `job_output` continues exactly after it), then the still-running
177
+ * marker and the job hand-off guidance.
178
+ * @param promoted - the promoted result value: the job id, the wait that
179
+ * expired, and the output so far.
180
+ * @returns the model-facing text for a promoted call.
181
+ */
182
+ function renderPromoted(promoted) {
183
+ return `${promoted.output.length > 0 ? promoted.output.endsWith("\n") ? promoted.output : `${promoted.output}\n` : ""}[still running after ${promoted.timeoutMs}ms; moved to background job ${promoted.jobId}]\nThe command keeps running in the background. You will be notified when it finishes; read newer output with job_output, stop it with job_kill.`;
184
+ }
185
+ /**
186
+ * Shape the one consuming registry read a foreground call embeds in its
187
+ * result when it stops waiting: the output produced so far, plus the
188
+ * dropped-output notice (naming the job's spill files) when the model cursor
189
+ * fell behind the ring, and the sandbox notices. Later `job_output` reads
190
+ * render the same ring through the job tools.
191
+ * @param delta - the read's chunks as rendered text.
192
+ * @param lossy - whether bytes before the delta were evicted unread.
193
+ * @param spillPaths - the complete-stream files the job currently advertises.
82
194
  * @param sandbox - settled sandbox facts, when this was a confined process.
83
195
  * @param escalationModes - escalation targets advertised by this composition.
84
196
  * @returns the delta text with any loss or sandbox notice appended.
85
197
  */
86
- function renderProcessRead(read, sandbox, escalationModes = []) {
198
+ function renderJobRead(delta, lossy, spillPaths, sandbox, escalationModes = []) {
87
199
  const notices = [];
88
- if (read.lossy) {
89
- const paths = [read.stdoutSpillPath, read.stderrSpillPath].filter((path) => path !== void 0);
90
- notices.push(`[some output was dropped from memory; full output: ${paths.length > 0 ? paths.join(", ") : "(unavailable)"}]`);
91
- }
200
+ if (lossy) notices.push(`[some output was dropped from memory; full output: ${spillPaths.length > 0 ? spillPaths.join(", ") : "(unavailable)"}]`);
92
201
  if (sandbox?.runnerFailed) notices.push(`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`);
93
202
  else if (sandbox?.denied) {
94
203
  notices.push(sandboxDenialMarker(sandbox.mode));
95
204
  if (escalationModes.length > 0) notices.push(escalationHintMarker("command"));
96
205
  }
97
- if (notices.length === 0) return read.delta;
98
- return `${read.delta}${read.delta.length > 0 && !read.delta.endsWith("\n") ? "\n" : ""}${notices.join("\n")}`;
206
+ if (notices.length === 0) return delta;
207
+ return `${delta}${delta.length > 0 && !delta.endsWith("\n") ? "\n" : ""}${notices.join("\n")}`;
99
208
  }
100
209
  //#endregion
101
210
  //#region lib/types/index.js
102
211
  /**
103
- * Model-facing Consumer of the `ctx.shell` capability seam. Background calls
104
- * register process handles with `ctx.jobs`; their work uses job cancellation
105
- * rather than the tool-call signal after an id is returned.
212
+ * Model-facing Consumer of the `ctx.shell` capability seam. While a job
213
+ * registry is composed, every call registers its process with `ctx.jobs` as
214
+ * it starts: `run_in_background` returns the id at once, and a foreground call
215
+ * waits on its job until the command finishes or the wait times out, at which
216
+ * point it returns the same id. Without a registry the tool is foreground-only
217
+ * and the executor's deadline kills the command.
106
218
  *
107
219
  * TODO(permissions): deployment policy belongs in `tools/pre-execute` and
108
220
  * sandboxing executors; see docs/architecture.md § Where new behavior goes.
@@ -116,15 +228,21 @@ const inject = [
116
228
  "shellEnv"
117
229
  ];
118
230
  /** Runtime configuration schema for the bash tool plugin. */
119
- const Config = z.object({ toolName: z.string().default("bash"), enableRunInBackground: z.boolean().default(true) });
120
- function validateBashArgs(args) {
231
+ const Config = z.object({
232
+ toolName: z.string().default("bash"),
233
+ enableRunInBackground: z.boolean().default(true),
234
+ promoteOnTimeout: z.boolean().default(true)
235
+ });
236
+ function validateBashArgs(args, effectiveMode) {
121
237
  if (args.command.trim().length === 0) throw new Error("invalid command: expected a non-empty string");
122
238
  if (args.description.trim().length === 0) throw new Error("invalid description: expected a non-empty string");
123
239
  if (args.timeoutMs !== void 0 && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) throw new Error(`invalid timeoutMs: expected a positive number, got ${JSON.stringify(args.timeoutMs)}`);
124
- validateEscalationArgs(args.sandbox_permissions, args.justification);
240
+ if (args.sandbox_permissions !== void 0 && args.sandbox_permissions === effectiveMode) return;
241
+ const justification = args.sandbox_permissions === void 0 && args.justification?.trim() === "" ? void 0 : args.justification;
242
+ validateEscalationArgs(args.sandbox_permissions, justification);
125
243
  }
126
- function bashDescription(backgroundEnabled, escalationModes) {
127
- const background = backgroundEnabled ? "Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`." : "Background execution is not available; long-running commands must finish within the timeout.";
244
+ function bashDescription(backgroundEnabled, escalationModes, promoteOnTimeout) {
245
+ const background = backgroundEnabled ? "Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`." + (promoteOnTimeout ? " A foreground command that reaches its timeout is not killed: it moves to the background the same way, returning its job id and the output so far." : "") : "Background execution is not available; long-running commands must finish within the timeout.";
128
246
  const base = `Execute a bash command (\`bash -c\`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass \`workdir\` instead of using \`cd\`. Non-zero exits are reported as \`[exit code: N]\`. Current harness environment facts are exposed through managed \`$${DSH_ENV_PREFIX}*\` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as \`[sandbox: file access denied under <mode> mode]\` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. ` + background;
129
247
  if (escalationModes.length === 0) return base;
130
248
  return base + " Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.";
@@ -155,7 +273,9 @@ function presentBashResult(args, result) {
155
273
  const block = result.content.length === 1 ? result.content[0] : void 0;
156
274
  if (block === void 0 || block.type !== "text") return void 0;
157
275
  const raw = block.text;
158
- if (typeof args === "object" && args !== null && args.run_in_background === true || result.isError) return {
276
+ const isBackground = typeof args === "object" && args !== null && args.run_in_background === true;
277
+ const isPromoted = result.value?.kind === "promoted";
278
+ if (isBackground || isPromoted || result.isError) return {
159
279
  card: "generic",
160
280
  content: [{
161
281
  type: "text",
@@ -177,9 +297,9 @@ function presentBashResult(args, result) {
177
297
  */
178
298
  function resolveWorkdir(modelWorkdir, exec, policyWorkspaceRoot) {
179
299
  const headerCwd = exec.agent?.session.header.cwd;
180
- const sessionCwd = policyWorkspaceRoot ?? (headerCwd === void 0 ? void 0 : canonicalPath(headerCwd));
300
+ const sessionCwd = policyWorkspaceRoot ?? headerCwd;
181
301
  if (modelWorkdir === void 0) return sessionCwd;
182
- if (sessionCwd !== void 0 && !isAbsolute(modelWorkdir)) return resolve(sessionCwd, modelWorkdir);
302
+ if (sessionCwd !== void 0 && !isAbsolute(modelWorkdir)) return `${sessionCwd}${sep}${modelWorkdir}`;
183
303
  return modelWorkdir;
184
304
  }
185
305
  /** Detach the executor DTO from readonly Service Definition types into plain JSON data. */
@@ -205,6 +325,12 @@ function canonicalBashResult(result) {
205
325
  } } : {}
206
326
  };
207
327
  }
328
+ /** The structured abort the foreground paths throw when the caller cancels the call. */
329
+ function toolAborted() {
330
+ const error = new HarnessError("tool call aborted", TOOL_ABORTED);
331
+ error.name = "AbortError";
332
+ return error;
333
+ }
208
334
  /** Canonical background-handle properties shared by the bash output union. */
209
335
  const BACKGROUND_OUTPUT_PROPERTIES = {
210
336
  kind: {
@@ -219,6 +345,7 @@ const BACKGROUND_OUTPUT_PROPERTIES = {
219
345
  };
220
346
  function apply(ctx, config = {}) {
221
347
  const backgroundEnabled = config.enableRunInBackground ?? true;
348
+ const promoteOnTimeout = (config.promoteOnTimeout ?? true) && backgroundEnabled;
222
349
  const defaultMode = ctx.shell.sandboxMode;
223
350
  const escalationModes = defaultMode === void 0 ? [] : ESCALATION_TARGETS;
224
351
  const sandboxPolicy = defaultMode === void 0 ? void 0 : ctx.get("sandboxPolicy");
@@ -257,193 +384,335 @@ function apply(ctx, config = {}) {
257
384
  order: ctx.systemPrompt.getSectionOrder("TOOL_BASH"),
258
385
  text: `Check the [exit code: N] marker on every ${config.toolName} result; investigate failures before moving on.`
259
386
  });
260
- ctx.tools.register(defineTool({
261
- name: config.toolName,
262
- description: bashDescription(backgroundEnabled, escalationModes),
263
- parameters: {
264
- command: {
265
- type: "string",
266
- required: true,
267
- description: "The bash command to execute."
268
- },
269
- description: {
270
- type: "string",
271
- required: true,
272
- description: "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
273
- },
274
- timeoutMs: {
275
- type: "number",
276
- description: "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
277
- },
278
- workdir: {
279
- type: "string",
280
- description: "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
281
- },
282
- ...backgroundEnabled ? { run_in_background: {
283
- type: "boolean",
284
- description: "Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."
285
- } } : {},
286
- ...escalationModes.length > 0 ? {
287
- sandbox_permissions: {
387
+ /**
388
+ * One registration of the `bash` tool. With a registry, every call
389
+ * registers its process as a job at its start; without one the tool is
390
+ * foreground-only and the executor's deadline kills the command.
391
+ */
392
+ const bashTool = (jobs) => {
393
+ const background = jobs !== void 0;
394
+ const promote = background && promoteOnTimeout;
395
+ /** Register the command as a job; the process spawns inside the starter, after admission. */
396
+ const startJob = (registry, args, exec, spec) => {
397
+ let proc;
398
+ let stopped;
399
+ return {
400
+ id: registry.start({
401
+ kind: "bash",
402
+ label: args.command,
403
+ ...exec.agent ? { owner: exec.agent.id } : {},
404
+ output: processSources(() => proc),
405
+ run: () => {
406
+ const hooks = processJob(async (signal) => {
407
+ proc = await ctx.shell.execute({
408
+ ...spec,
409
+ signal
410
+ });
411
+ return proc;
412
+ }, (started) => processOutcome(started, escalationModes));
413
+ return {
414
+ done: hooks.done,
415
+ cancel: (reason) => {
416
+ stopped = reason;
417
+ hooks.cancel(reason);
418
+ }
419
+ };
420
+ }
421
+ }),
422
+ process: () => proc,
423
+ stopped: () => stopped
424
+ };
425
+ };
426
+ /** Wait on a registered foreground command until it settles or the timeout passes. */
427
+ const waitOnJob = async (registry, attached, exec, spec) => {
428
+ const owner = exec.agent?.id;
429
+ const timeoutMs = spec.timeoutMs;
430
+ /**
431
+ * Stop the job on this call's own account and stay on it until it
432
+ * settles, so the settlement is `awaited` and no completion notice
433
+ * follows a result this call already carries; the record then leaves
434
+ * with the call, as the model never saw the id.
435
+ */
436
+ const stop = async (reason) => {
437
+ registry.kill(attached.id, owner, reason);
438
+ const settled = await registry.wait(attached.id, timeoutMs, owner);
439
+ if (settled.status !== "running" && settled.status !== "stopping") registry.remove(attached.id, owner);
440
+ return settled;
441
+ };
442
+ let view;
443
+ try {
444
+ view = await registry.wait(attached.id, timeoutMs, owner, exec.signal);
445
+ } catch {
446
+ await stop("tool call aborted");
447
+ throw toolAborted();
448
+ }
449
+ if ((view.status === "running" || view.status === "stopping") && attached.process() === void 0) {
450
+ await stop("timed out during preparation");
451
+ return {
452
+ kind: "foreground",
453
+ exitCode: null,
454
+ signal: null,
455
+ timedOut: true,
456
+ aborted: false,
457
+ timeoutMs,
458
+ stdout: {
459
+ text: "",
460
+ truncated: false
461
+ },
462
+ stderr: {
463
+ text: "",
464
+ truncated: false
465
+ },
466
+ ...spec.sandboxPolicy !== void 0 ? { sandbox: {
467
+ mode: spec.sandboxPolicy.mode,
468
+ denied: false
469
+ } } : {}
470
+ };
471
+ }
472
+ if (view.status === "running" || view.status === "stopping") {
473
+ const read = registry.read(attached.id, owner);
474
+ return {
475
+ kind: "promoted",
476
+ jobId: attached.id,
477
+ timeoutMs,
478
+ output: renderJobRead(ringDelta(read.chunks), read.lossy, read.job.output.spillPaths ?? [], attached.process()?.sandbox, escalationModes)
479
+ };
480
+ }
481
+ registry.remove(attached.id, owner);
482
+ const process = attached.process();
483
+ if (process === void 0) throw new Error(view.detail);
484
+ const result = await process.result();
485
+ const stopped = attached.stopped();
486
+ return {
487
+ kind: "foreground",
488
+ ...canonicalBashResult(result),
489
+ ...stopped !== void 0 ? { stopped } : {}
490
+ };
491
+ };
492
+ return defineTool({
493
+ name: config.toolName,
494
+ description: bashDescription(background, escalationModes, promote),
495
+ parameters: {
496
+ command: {
288
497
  type: "string",
289
- enum: [...escalationModes],
290
- description: "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval."
498
+ required: true,
499
+ description: "The bash command to execute."
291
500
  },
292
- justification: {
501
+ description: {
293
502
  type: "string",
294
- description: "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
295
- }
296
- } : {}
297
- },
298
- output: {
299
- schema: { oneOf: [{
300
- type: "object",
301
- additionalProperties: false,
302
- properties: BACKGROUND_OUTPUT_PROPERTIES
303
- }, {
304
- type: "object",
305
- additionalProperties: false,
306
- properties: {
307
- kind: {
503
+ required: true,
504
+ description: "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
505
+ },
506
+ timeoutMs: {
507
+ type: "number",
508
+ description: promote ? "Timeout in milliseconds. The executor applies its configured default and cap; on expiry the command moves to the background as a job instead of being killed." : "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
509
+ },
510
+ workdir: {
511
+ type: "string",
512
+ description: "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
513
+ },
514
+ ...background ? { run_in_background: {
515
+ type: "boolean",
516
+ description: "Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."
517
+ } } : {},
518
+ ...escalationModes.length > 0 ? {
519
+ sandbox_permissions: {
308
520
  type: "string",
309
- required: true,
310
- const: "foreground"
311
- },
312
- exitCode: {
313
- required: true,
314
- oneOf: [{ type: "integer" }, { type: "null" }]
521
+ enum: [...escalationModes],
522
+ description: "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval."
315
523
  },
316
- signal: {
317
- required: true,
318
- oneOf: [{ type: "string" }, { type: "null" }]
319
- },
320
- timedOut: {
321
- type: "boolean",
322
- required: true
323
- },
324
- aborted: {
325
- type: "boolean",
326
- required: true
327
- },
328
- timeoutMs: {
329
- type: "number",
330
- required: true
524
+ justification: {
525
+ type: "string",
526
+ description: "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
527
+ }
528
+ } : {}
529
+ },
530
+ output: {
531
+ schema: { oneOf: [
532
+ {
533
+ type: "object",
534
+ additionalProperties: false,
535
+ properties: BACKGROUND_OUTPUT_PROPERTIES
331
536
  },
332
- stdout: {
537
+ {
333
538
  type: "object",
334
539
  additionalProperties: false,
335
- required: true,
336
540
  properties: {
337
- text: {
541
+ kind: {
542
+ type: "string",
543
+ required: true,
544
+ const: "promoted"
545
+ },
546
+ jobId: {
338
547
  type: "string",
339
548
  required: true
340
549
  },
341
- truncated: {
342
- type: "boolean",
550
+ timeoutMs: {
551
+ type: "number",
343
552
  required: true
344
553
  },
345
- spillPath: { type: "string" }
554
+ output: {
555
+ type: "string",
556
+ required: true
557
+ }
346
558
  }
347
559
  },
348
- stderr: {
560
+ {
349
561
  type: "object",
350
562
  additionalProperties: false,
351
- required: true,
352
563
  properties: {
353
- text: {
564
+ kind: {
354
565
  type: "string",
355
- required: true
566
+ required: true,
567
+ const: "foreground"
568
+ },
569
+ exitCode: {
570
+ required: true,
571
+ oneOf: [{ type: "integer" }, { type: "null" }]
572
+ },
573
+ signal: {
574
+ required: true,
575
+ oneOf: [{ type: "string" }, { type: "null" }]
356
576
  },
357
- truncated: {
577
+ timedOut: {
358
578
  type: "boolean",
359
579
  required: true
360
580
  },
361
- spillPath: { type: "string" }
362
- }
363
- },
364
- sandbox: {
365
- type: "object",
366
- additionalProperties: false,
367
- properties: {
368
- mode: {
369
- type: "string",
581
+ aborted: {
582
+ type: "boolean",
370
583
  required: true
371
584
  },
372
- denied: {
373
- type: "boolean",
585
+ stopped: { type: "string" },
586
+ timeoutMs: {
587
+ type: "number",
374
588
  required: true
375
589
  },
376
- enforcement: { type: "string" },
377
- runnerFailed: { type: "boolean" }
590
+ stdout: {
591
+ type: "object",
592
+ additionalProperties: false,
593
+ required: true,
594
+ properties: {
595
+ text: {
596
+ type: "string",
597
+ required: true
598
+ },
599
+ truncated: {
600
+ type: "boolean",
601
+ required: true
602
+ },
603
+ spillPath: { type: "string" }
604
+ }
605
+ },
606
+ stderr: {
607
+ type: "object",
608
+ additionalProperties: false,
609
+ required: true,
610
+ properties: {
611
+ text: {
612
+ type: "string",
613
+ required: true
614
+ },
615
+ truncated: {
616
+ type: "boolean",
617
+ required: true
618
+ },
619
+ spillPath: { type: "string" }
620
+ }
621
+ },
622
+ sandbox: {
623
+ type: "object",
624
+ additionalProperties: false,
625
+ properties: {
626
+ mode: {
627
+ type: "string",
628
+ required: true
629
+ },
630
+ denied: {
631
+ type: "boolean",
632
+ required: true
633
+ },
634
+ enforcement: { type: "string" },
635
+ runnerFailed: { type: "boolean" }
636
+ }
637
+ }
378
638
  }
379
639
  }
640
+ ] },
641
+ render: (_args, value) => [{
642
+ type: "text",
643
+ text: value.kind === "background" ? `started background job ${value.jobId}` : value.kind === "promoted" ? renderPromoted(value) : renderResult(value, escalationModes)
644
+ }]
645
+ },
646
+ async execute(args, exec) {
647
+ const standingPolicy = resolveSandboxPolicy(exec);
648
+ validateBashArgs(args, standingPolicy?.mode);
649
+ const approvedMode = args.sandbox_permissions !== void 0 && args.justification !== void 0 ? await approveBashEscalation(args.sandbox_permissions, args.justification, exec, standingPolicy) : void 0;
650
+ const policy = approvedMode === void 0 ? standingPolicy : {
651
+ ...standingPolicy,
652
+ mode: approvedMode
653
+ };
654
+ const workdir = resolveWorkdir(args.workdir, exec, standingPolicy?.workspaceRoot);
655
+ const dshEnv = ctx.shellEnv.collect(exec);
656
+ const request = {
657
+ command: args.command,
658
+ ...workdir !== void 0 ? { workdir } : {},
659
+ ...args.timeoutMs !== void 0 ? { timeoutMs: args.timeoutMs } : {},
660
+ dshEnv,
661
+ ...policy !== void 0 ? { sandboxPolicy: policy } : {}
662
+ };
663
+ if (args.run_in_background === true) {
664
+ if (!backgroundEnabled) throw new Error("run_in_background is disabled for this deployment (enableRunInBackground: false)");
665
+ if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
666
+ if (exec.signal.aborted) throw toolAborted();
667
+ return {
668
+ kind: "background",
669
+ jobId: startJob(jobs, args, exec, ctx.shell.resolve({
670
+ ...request,
671
+ onExpiry: "none"
672
+ })).id
673
+ };
380
674
  }
381
- }] },
382
- render: (_args, value) => [{
383
- type: "text",
384
- text: value.kind === "background" ? `started background job ${value.jobId}` : renderResult(value, escalationModes)
385
- }]
386
- },
387
- async execute(args, exec) {
388
- validateBashArgs(args);
389
- const standingPolicy = resolveSandboxPolicy(exec);
390
- const approvedMode = args.sandbox_permissions !== void 0 && args.justification !== void 0 ? await approveBashEscalation(args.sandbox_permissions, args.justification, exec, standingPolicy) : void 0;
391
- const policy = approvedMode === void 0 ? standingPolicy : {
392
- ...standingPolicy,
393
- mode: approvedMode
394
- };
395
- const workdir = resolveWorkdir(args.workdir, exec, standingPolicy?.workspaceRoot);
396
- const dshEnv = ctx.shellEnv.collect(exec);
397
- const request = {
398
- command: args.command,
399
- ...workdir !== void 0 ? { workdir } : {},
400
- ...args.timeoutMs !== void 0 ? { timeoutMs: args.timeoutMs } : {},
401
- dshEnv,
402
- ...policy !== void 0 ? { sandboxPolicy: policy } : {}
403
- };
404
- if (args.run_in_background === true) {
405
- if (!backgroundEnabled) throw new Error("run_in_background is disabled for this deployment (enableRunInBackground: false)");
406
- const jobs = ctx.get("jobs");
407
- if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
408
- if (exec.signal.aborted) {
409
- const error = new HarnessError("tool call aborted", TOOL_ABORTED);
410
- error.name = "AbortError";
411
- throw error;
675
+ if (jobs !== void 0 && promote) {
676
+ const spec = ctx.shell.resolve({
677
+ ...request,
678
+ onExpiry: "none"
679
+ });
680
+ let attached;
681
+ try {
682
+ attached = startJob(jobs, args, exec, spec);
683
+ } catch (error) {
684
+ ctx.logger.warn(`bash: job registration refused, running in the foreground with the timeout kill instead: ${String(error)}`);
685
+ }
686
+ if (attached !== void 0) return waitOnJob(jobs, attached, exec, spec);
412
687
  }
688
+ const result = await (await ctx.shell.execute(ctx.shell.resolve({
689
+ ...request,
690
+ signal: exec.signal
691
+ }))).result();
692
+ if (result.aborted) throw toolAborted();
413
693
  return {
414
- kind: "background",
415
- jobId: jobs.start({
416
- kind: "bash",
417
- label: args.command,
418
- ...exec.agent ? { owner: exec.agent } : {},
419
- run: () => {
420
- const proc = ctx.shell.start(ctx.shell.resolve(request));
421
- return {
422
- cancel: () => void proc.kill(),
423
- done: proc.done.then(() => processOutcome(proc)),
424
- readOutput: () => renderProcessRead(proc.readOutput(), proc.sandbox, escalationModes)
425
- };
426
- }
427
- })
694
+ kind: "foreground",
695
+ ...canonicalBashResult(result)
428
696
  };
429
- }
430
- const result = await ctx.shell.run(ctx.shell.resolve({
431
- ...request,
432
- signal: exec.signal
433
- }));
434
- if (result.aborted) {
435
- const error = new HarnessError("tool call aborted", TOOL_ABORTED);
436
- error.name = "AbortError";
437
- throw error;
438
- }
439
- return {
440
- kind: "foreground",
441
- ...canonicalBashResult(result)
442
- };
443
- },
444
- presentCall: presentBashCall,
445
- presentResult: presentBashResult
446
- }));
697
+ },
698
+ presentCall: presentBashCall,
699
+ presentResult: presentBashResult
700
+ });
701
+ };
702
+ if (!backgroundEnabled) {
703
+ ctx.tools.register(bashTool(void 0));
704
+ return;
705
+ }
706
+ let foregroundOnly = ctx.get("jobs") === void 0 ? ctx.tools.register(bashTool(void 0)) : void 0;
707
+ ctx.inject(["jobs"], (jobCtx) => {
708
+ foregroundOnly?.();
709
+ foregroundOnly = void 0;
710
+ const unregister = ctx.tools.register(bashTool(jobCtx.jobs));
711
+ jobCtx.effect(() => () => {
712
+ unregister();
713
+ if (ctx.fiber.state === 2) foregroundOnly = ctx.tools.register(bashTool(void 0));
714
+ });
715
+ });
447
716
  }
448
717
  //#endregion
449
718
  export { Config, apply, inject, name };