mixdog 1.1.0 → 1.1.2

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 (174) hide show
  1. package/README.md +13 -6
  2. package/package.json +1 -2
  3. package/scripts/latest-guard.mjs +63 -0
  4. package/scripts/lib/run-node-tests.mjs +103 -27
  5. package/scripts/lib/test-failure-summary.mjs +7 -2
  6. package/scripts/merge-mac-update-feed.mjs +107 -0
  7. package/scripts/mobile-release.mjs +210 -0
  8. package/scripts/post-release-check.mjs +353 -0
  9. package/scripts/publish-asset-release.mjs +43 -0
  10. package/scripts/release-guard.mjs +63 -0
  11. package/scripts/release-paths.mjs +39 -2
  12. package/scripts/test.mjs +29 -7
  13. package/src/defaults/shipped-history.json +2 -0
  14. package/src/defaults/skills/computer-use/SKILL.md +1 -1
  15. package/src/defaults/skills/setup/references/actions.md +1 -0
  16. package/src/headless-role.mjs +2 -2
  17. package/src/lib/keychain-cjs.cjs +582 -17
  18. package/src/lib/rules-builder.cjs +0 -23
  19. package/src/rules/lead/LEAD.md +2 -2
  20. package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
  21. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  22. package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
  23. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
  24. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
  25. package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
  27. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
  28. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
  29. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
  30. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
  31. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
  32. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
  34. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
  35. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
  36. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
  40. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
  41. package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
  42. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
  43. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
  44. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +1 -5
  45. package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
  46. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
  47. package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
  49. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
  50. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
  51. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
  52. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
  53. package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
  54. package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
  55. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
  56. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
  57. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
  59. package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
  61. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
  65. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  66. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
  67. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
  68. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
  69. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
  70. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
  71. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
  72. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
  74. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
  77. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
  78. package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
  81. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
  83. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -91
  84. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
  85. package/src/runtime/channels/index.mjs +1 -5
  86. package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
  87. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
  88. package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
  89. package/src/runtime/channels/lib/worker-main.mjs +4 -34
  90. package/src/runtime/computer-bridge/client.mjs +25 -1
  91. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
  92. package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
  93. package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
  94. package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
  95. package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
  96. package/src/runtime/effort-judge/judge-input.mjs +5 -2
  97. package/src/runtime/effort-judge/model-install.mjs +9 -3
  98. package/src/runtime/effort-judge/model-manifest.json +7 -7
  99. package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
  100. package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
  101. package/src/runtime/shared/config.mjs +50 -2
  102. package/src/runtime/shared/developer-options.mjs +7 -0
  103. package/src/runtime/shared/instruction-files-mode.mjs +9 -0
  104. package/src/runtime/shared/llm/model-catalog.mjs +2 -1
  105. package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
  106. package/src/runtime/shared/pristine-execution-contract.json +9 -0
  107. package/src/runtime/shared/pristine-execution.mjs +1 -1
  108. package/src/runtime/shared/provider-accounts.mjs +2 -0
  109. package/src/runtime/shared/resource-admission.mjs +10 -3
  110. package/src/runtime/shared/schedule-model-ref.mjs +11 -0
  111. package/src/runtime/shared/semver.d.mts +1 -0
  112. package/src/runtime/shared/semver.mjs +57 -0
  113. package/src/runtime/shared/session-approval-wait.mjs +35 -0
  114. package/src/runtime/shared/shell-display.mjs +4 -2
  115. package/src/runtime/shared/tool-surface.mjs +2 -2
  116. package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
  117. package/src/runtime/shared/update-checker.mjs +2 -31
  118. package/src/session-runtime/boot/apis.mjs +5 -0
  119. package/src/session-runtime/boot/begin.mjs +0 -2
  120. package/src/session-runtime/boot/tools.mjs +5 -4
  121. package/src/session-runtime/model-recency.mjs +3 -0
  122. package/src/session-runtime/provider-auth/login.mjs +10 -10
  123. package/src/session-runtime/provider-auth/setup.mjs +22 -2
  124. package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
  125. package/src/session-runtime/provider-readiness.mjs +4 -0
  126. package/src/session-runtime/resource-mcp-api.mjs +7 -3
  127. package/src/session-runtime/runtime-bootstrap.mjs +0 -4
  128. package/src/session-runtime/runtime-facade.mjs +5 -0
  129. package/src/session-runtime/runtime-paths.mjs +0 -1
  130. package/src/session-runtime/schedule-session-run.mjs +3 -1
  131. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
  132. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
  133. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
  134. package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
  135. package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
  136. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
  137. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
  138. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
  139. package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
  140. package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
  141. package/src/session-runtime/services/channel-admin.mjs +4 -2
  142. package/src/session-runtime/services/channel-worker.mjs +1 -1
  143. package/src/session-runtime/services/provider-admin.mjs +80 -17
  144. package/src/session-runtime/services/seeds.mjs +7 -10
  145. package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
  146. package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
  147. package/src/session-runtime/settings-profile-api.mjs +27 -2
  148. package/src/session-runtime/setup-tool/executor.mjs +5 -0
  149. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  150. package/src/session-runtime/starter-guide-state.mjs +62 -0
  151. package/src/session-runtime/tool-policy-refresh.mjs +3 -0
  152. package/src/session-runtime/webhook-session-run.mjs +3 -1
  153. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
  154. package/src/standalone/channel-binding.mjs +1 -2
  155. package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
  156. package/src/standalone/daemon-channels-loader.mjs +2 -3
  157. package/src/standalone/local-session-runtime.mjs +2 -78
  158. package/src/standalone/session-protocol.mjs +2 -0
  159. package/src/tui/app/model-options.mjs +13 -1
  160. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
  161. package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
  162. package/src/tui/dist/index.mjs +84 -34
  163. package/src/tui/session/context-state.mjs +2 -0
  164. package/src/tui/session/live-share/owner.mjs +17 -0
  165. package/src/tui/session/oauth-flows.mjs +2 -0
  166. package/src/tui/session/session-api/extensions.mjs +12 -8
  167. package/src/tui/session/session-api/integrations.mjs +9 -8
  168. package/src/tui/session/session-api/routes.mjs +2 -2
  169. package/src/tui/session/session-api/settings.mjs +1 -0
  170. package/src/tui/session/session-api/shared.mjs +10 -6
  171. package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
  172. package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
  173. package/src/tui/spinner-meta.mjs +26 -10
  174. package/src/runtime/channels/tool-defs.mjs +0 -17
@@ -3,6 +3,7 @@ import {
3
3
  buildJobNotFoundMessage,
4
4
  peekShellJob,
5
5
  killShellJob,
6
+ otherSessionBackgroundShellJobCount,
6
7
  cancelBackgroundShellJobWatch,
7
8
  clearShellJobNotifyCtx,
8
9
  shellJobPublicTaskResult,
@@ -23,6 +24,7 @@ import {
23
24
  } from '../../../../shared/background-tasks.mjs';
24
25
  import { recordDeliveredCompletion } from '../../session/manager/delivered-completions.mjs';
25
26
  import { listShellJobRecords, readShellJobRecord } from './lib/shell-job-records.mjs';
27
+ import { resourceAdmission } from '../../../../shared/resource-admission.mjs';
26
28
  import { readShellTaskOutput } from './lib/shell-task-output.mjs';
27
29
 
28
30
  // Ceiling for confirming that a cancelled task's process really exited. The
@@ -88,18 +90,42 @@ function resolveTaskWaitTimeoutMs(value) {
88
90
  return Math.min(TASK_WAIT_TIMEOUT_MAX_MS, Math.max(TASK_WAIT_TIMEOUT_MIN_MS, Math.floor(raw)));
89
91
  }
90
92
 
93
+ // Read-only counts of this runtime's shell work owned by OTHER sessions.
94
+ // Admission is runtime-wide while task list/cancel are session-scoped, so
95
+ // without this those holders are invisible. Counts only: another session's
96
+ // command text is never shown.
97
+ function otherSessionShellHolders(options) {
98
+ const context =
99
+ options?.context && typeof options.context === 'object' ? { ...options.context, ...options } : options;
100
+ const mine = new Set(
101
+ [context?.callerSessionId, context?.routingSessionId, context?.sessionId].filter(Boolean).map(String)
102
+ );
103
+ if (mine.size === 0) return '';
104
+ const leases = (resourceAdmission.snapshot().activeLeases || []).filter(
105
+ (lease) => lease.kind === 'shell' && !mine.has(String(lease.ownerKey || ''))
106
+ ).length;
107
+ const jobs = otherSessionBackgroundShellJobCount(mine);
108
+ if (!leases && !jobs) return '';
109
+ return `[other sessions in this runtime] ${leases} foreground shell lease(s), ${jobs} background shell job(s) — read-only; only their owner sessions can cancel them.`;
110
+ }
111
+
91
112
  async function listTasks(options) {
92
113
  const rendered = renderBackgroundTaskList({ context: options });
93
114
  const liveIds = new Set([...rendered.matchAll(/^-\s+(\S+)/gm)].map((match) => match[1]));
94
115
  const recovered = (await listShellJobRecords()).filter(
95
116
  (record) => recoveredTaskMatches(record, options) && !liveIds.has(record.jobId)
96
117
  );
97
- if (recovered.length === 0) return rendered;
98
- const rows = recovered.map(
99
- (record) =>
100
- `- ${record.jobId} shell ${record.terminal ? record.status : 'state-unavailable'} recovered=true command=${JSON.stringify(record.command || '')}`
101
- );
102
- return `${rendered}\n[recovered shell tasks]\n${rows.join('\n')}`;
118
+ const sections = [rendered];
119
+ if (recovered.length > 0) {
120
+ const rows = recovered.map(
121
+ (record) =>
122
+ `- ${record.jobId} shell ${record.terminal ? record.status : 'state-unavailable'} recovered=true command=${JSON.stringify(record.command || '')}`
123
+ );
124
+ sections.push(`[recovered shell tasks]\n${rows.join('\n')}`);
125
+ }
126
+ const others = otherSessionShellHolders(options);
127
+ if (others) sections.push(others);
128
+ return sections.join('\n');
103
129
  }
104
130
 
105
131
  // Blocks until the shell task settles, the timeout elapses, or the turn/user
@@ -1,6 +1,12 @@
1
1
  import { createIoTrace } from './io-trace.mjs';
2
2
  import { classifyResultKind } from '../session/result-classification.mjs';
3
- import { coerceShapeFlex, normalizeInputPath, normalizeOutputPath, resolveAgainstCwd } from './builtin/path-utils.mjs';
3
+ import {
4
+ coerceShapeFlex,
5
+ normalizeInputPath,
6
+ normalizeOutputPath,
7
+ resolveAgainstCwd,
8
+ splitJoinedPathScopes,
9
+ } from './builtin/path-utils.mjs';
4
10
  import {
5
11
  SMART_BASH_HEAD_LINES,
6
12
  SMART_BASH_MAX_BYTES,
@@ -387,7 +393,8 @@ export async function executeBuiltinTool(name, args, cwd, options = {}) {
387
393
  readOffsetBase: args && Object.hasOwn(args, 'file_path') ? 1 : (options.readOffsetBase ?? 0),
388
394
  };
389
395
  }
390
- let argError;
396
+ let argError = splitJoinedPathScopes(toolName, args, cwd);
397
+ if (argError) return argError;
391
398
  if (toolName === 'grep') {
392
399
  const prepared = prepareGrepArgs(args);
393
400
  argError = prepared.error;
@@ -133,7 +133,9 @@ function collectGraphParseWarnings(graph, options) {
133
133
  // instead of erroring "file not found in graph: (missing file)".
134
134
  function _resolveRequestedMode(args) {
135
135
  let mode = String(args?.mode || '').trim();
136
- if (!mode) throw new Error('code_graph: "mode" is required');
136
+ if (!mode) {
137
+ throw new Error(`code_graph: "mode" is required, one of ${[...Object.keys(CODE_GRAPH_MODES), 'search'].join('|')}`);
138
+ }
137
139
  if (mode === 'search') mode = 'symbol_search';
138
140
  const nameOnlySymbols =
139
141
  mode === 'symbols' &&
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * promote-job.mjs — hand the still-running foreground child to the shell-jobs
3
- * registry: the bounded promotion attempt, the lease hand-over that follows
4
- * a successful one, and the kill fallback that follows a failed one.
3
+ * registry: the bounded promotion attempt, the lease release that follows a
4
+ * successful one, and the kill fallback that follows a failed one.
5
5
  */
6
- import { attachShellJobResourceLease, promoteForegroundShellJob } from '../../builtin/shell-jobs.mjs';
6
+ import { promoteForegroundShellJob } from '../../builtin/shell-jobs.mjs';
7
7
  import { treeKillForceSettle } from '../shell-run-state.mjs';
8
8
 
9
9
  export async function promoteWithRetry(
@@ -74,17 +74,14 @@ export function killAfterFailedPromotion(run, reason, promotionFailure) {
74
74
  );
75
75
  }
76
76
 
77
- export async function handOverLease(run, jobId) {
78
- const promotedLease = run.resourceLease;
77
+ /**
78
+ * The promoted job leaves foreground admission: its lease is RELEASED, which
79
+ * also restores the parent's high-load share. A long-lived server must never
80
+ * hold a foreground slot for its whole life; background jobs are not counted
81
+ * against any cap (session/runtime close stops them via shutdownShellJobs).
82
+ */
83
+ export async function handOverLease(run) {
84
+ const lease = run.resourceLease;
79
85
  run.resourceLease = null;
80
- if (!promotedLease) return;
81
- try {
82
- await promotedLease.detachDependency?.();
83
- attachShellJobResourceLease(jobId, promotedLease);
84
- } catch (error) {
85
- try {
86
- await promotedLease.release();
87
- } catch {}
88
- throw error;
89
- }
86
+ await lease?.release();
90
87
  }
@@ -11,7 +11,7 @@
11
11
  // exclusive with settle() via the autoBackgrounded flag set synchronously at
12
12
  // the top before any await.
13
13
  //
14
- // shell-run-background/promote-job.mjs — registry promotion, lease hand-over, kill fallback
14
+ // shell-run-background/promote-job.mjs — registry promotion, lease release, kill fallback
15
15
  // shell-run-background/promotion-results.mjs — the ExecResult of each promotion outcome
16
16
  import { clearForegroundRecord, clearForegroundTimers, elapsedSinceStart } from './shell-run-state.mjs';
17
17
  import { readCapturedOutput } from './shell-run-settle.mjs';
@@ -108,7 +108,7 @@ export function createAutoBackground({
108
108
  }
109
109
  const jobId = job.jobId;
110
110
  run.autoBackgroundJobId = jobId;
111
- await handOverLease(run, jobId);
111
+ await handOverLease(run);
112
112
  // Snapshot the partial output captured so far for the immediate result.
113
113
  const { stdout, stderr } = await readCapturedOutput(taskOutput);
114
114
  const capture = { stdout, stderr, spill: { ...promotedSpillPaths(taskOutput), stdoutPath } };
@@ -20,7 +20,7 @@ import { ShellTextDecoder, TaskOutput, treeKill } from '../shell-exec-output.mjs
20
20
  const KILL_CONFIRM_INTERVAL_MS = 5000;
21
21
  const KILL_CONFIRM_ATTEMPTS = 3;
22
22
 
23
- export function createShellRun({ abortSignal, resolve }) {
23
+ export function createShellRun({ abortSignal, resolve, jobId = null }) {
24
24
  const taskId = `shell_${randomUUID().slice(0, 8)}`;
25
25
  let resultResolved = false;
26
26
  return {
@@ -80,7 +80,9 @@ export function createShellRun({ abortSignal, resolve }) {
80
80
  // reached a shell — not lease/preflight entry, and not a warm standby's
81
81
  // process-creation time.
82
82
  commandStartedAtMs: 0,
83
- foregroundRecordId: `job_${Date.now()}_${randomUUID().slice(0, 6)}`,
83
+ // Also the task_id the command keeps once promoted. A queued command
84
+ // passes its queued task's id so one id names it before and after.
85
+ foregroundRecordId: jobId || `job_${Date.now()}_${randomUUID().slice(0, 6)}`,
84
86
  foregroundRecordTimer: null,
85
87
  foregroundRecordPublished: false,
86
88
  };
@@ -1,22 +1,13 @@
1
1
  // Async one-shot shell runner.
2
2
  //
3
- // Replaces the legacy spawnSync path in builtin.mjs shell execution. The
4
- // improvements over spawnSync are:
5
- // - native process-tree termination on timeout / abort.
6
- // - automatic spill to $PLUGIN_DATA/shell-output/<taskId>.* once the
7
- // in-memory buffers exceed SHELL_OUTPUT_INLINE_CAP bytes. The caller
8
- // receives an outputFilePath marker the model can FileRead later
9
- // instead of losing the tail past the inline cap.
10
- // - external AbortSignal hookup so a session-scoped abort (ESC, new
11
- // prompt) cancels in-flight bash work without orphaning the child.
12
- //
13
3
  // One run is an explicit state record (lib/shell-run-state.mjs) driven
14
4
  // through three phases: spawn (lib/shell-run-spawn.mjs), settle
15
5
  // (lib/shell-run-settle.mjs) and the auto-background promotion
16
6
  // (lib/shell-run-background.mjs). This module owns admission, preflight and
17
7
  // the wiring between those phases.
18
- import { startBackgroundTask } from '../../../shared/background-tasks.mjs';
8
+ import { getBackgroundTask, startBackgroundTask } from '../../../shared/background-tasks.mjs';
19
9
  import { resourceAdmission } from '../../../shared/resource-admission.mjs';
10
+ import { buildShellCompletion, killShellJob, peekShellJob, subscribeShellJobSettled } from './builtin/shell-jobs.mjs';
20
11
  import { ExecResult, treeKill } from './shell-exec-output.mjs';
21
12
  import {
22
13
  clearForegroundTimers,
@@ -50,25 +41,49 @@ const _envAdmissionWait = Math.floor(Number(process.env.MIXDOG_SHELL_ADMISSION_W
50
41
  const SHELL_ADMISSION_WAIT_MS =
51
42
  Number.isFinite(_envAdmissionWait) && _envAdmissionWait >= 0 ? _envAdmissionWait : 30_000;
52
43
 
53
- function _admissionSaturationError(admission, waitMs) {
44
+ // Foreground leases are labeled `[task_id: X] command` at spawn (X is the id
45
+ // the command keeps once promoted, or its queued task's id).
46
+ const LEASE_TASK_ID = /^\[task_id: ([^\]\s]+)\]/;
47
+
48
+ // Only the caller's OWN holders are described (oldest first, with task_id and
49
+ // an exact `task cancel` call when cancellable). Other sessions' holders are
50
+ // reported as a count with ages: their command text is never shown here.
51
+ function _admissionSaturationError(admission, waitMs, ownerKey = null) {
52
+ const own = String(ownerKey || '');
54
53
  let detail = '';
55
54
  try {
56
55
  const snap = admission.snapshot();
57
- const held = (snap.activeLeases || [])
56
+ const holders = (snap.activeLeases || [])
58
57
  .filter((lease) => lease.kind === 'shell')
59
- .map((lease) => `[${Math.round(lease.ageMs / 1000)}s] ${String(lease.label || '(unlabeled)')}`)
60
- .join(' | ');
58
+ .sort((a, b) => b.ageMs - a.ageMs);
59
+ const rows = [];
60
+ const cancels = [];
61
+ const otherAges = [];
62
+ for (const lease of holders) {
63
+ const age = `${Math.round(lease.ageMs / 1000)}s`;
64
+ if (!own || lease.ownerKey !== own) {
65
+ otherAges.push(age);
66
+ continue;
67
+ }
68
+ const label = String(lease.label || '(unlabeled)');
69
+ const taskId = LEASE_TASK_ID.exec(label)?.[1] || null;
70
+ const cancellable = taskId && getBackgroundTask(taskId);
71
+ if (cancellable) cancels.push(`task cancel task_id=${taskId}`);
72
+ rows.push(`[${age}] ${label}${cancellable ? '' : ' (foreground call)'}`);
73
+ }
61
74
  detail =
62
- ` ${snap.active.shell}/${snap.limits.maxShells} shell leases active` +
63
- (held ? ` (${held})` : '') +
64
- `, ${snap.queued} queued.`;
75
+ ` ${snap.active.shell}/${snap.limits.maxShells} shell leases active, ${snap.queued} queued.` +
76
+ (rows.length ? ` Your holders, oldest first: ${rows.join(' | ')}.` : '') +
77
+ (cancels.length ? ` Free your own slots: ${cancels.join('; ')}.` : '') +
78
+ (otherAges.length
79
+ ? ` ${otherAges.length} holder(s) belong to other sessions of this runtime (ages ${otherAges.join(', ')}); only their owners can cancel them.`
80
+ : '');
65
81
  } catch {
66
82
  /* diagnostics must not mask the timeout */
67
83
  }
68
84
  const error = new Error(
69
85
  `shell admission wait exceeded ${waitMs}ms —${detail} ` +
70
- 'Long-held leases usually mean stuck background shell process trees: ' +
71
- 'check task list, cancel stale tasks, kill lingering child processes, or restart the CLI.'
86
+ 'A lease returns when its call ends or is promoted to the background; background jobs hold none.'
72
87
  );
73
88
  error.code = 'ERESOURCEPRESSURE';
74
89
  error.admissionSaturated = true;
@@ -168,7 +183,7 @@ async function acquireShellLeaseBounded(
168
183
  const deadlineAt = Date.now() + waitMs;
169
184
  const deadline = setTimeout(() => {
170
185
  try {
171
- ctl.abort(_admissionSaturationError(admission, waitMs));
186
+ ctl.abort(_admissionSaturationError(admission, waitMs, ownerKey));
172
187
  } catch {}
173
188
  }, waitMs);
174
189
  if (deadline.unref) deadline.unref();
@@ -223,7 +238,7 @@ async function admitAndSpawn(run, params, { admissionWaitMs, onSaturated }) {
223
238
  run.resourceLease = await acquireShellLeaseBounded(admission, {
224
239
  abortSignal,
225
240
  waitMs: admissionWaitMs,
226
- label: String(command || '').slice(0, 120),
241
+ label: `[task_id: ${run.foregroundRecordId}] ${String(command || '').slice(0, 120)}`,
227
242
  ownerKey: ownerSessionId,
228
243
  });
229
244
  const policyErr = await _execPolicyBlockMessage(command);
@@ -252,36 +267,62 @@ async function admitAndSpawn(run, params, { admissionWaitMs, onSaturated }) {
252
267
  }
253
268
  }
254
269
 
270
+ // A queued command that outlived its foreground window was promoted under the
271
+ // queued task's id; the task settles with that job.
272
+ async function settleQueuedPromotion(jobId) {
273
+ await new Promise((resolve) => {
274
+ if (!subscribeShellJobSettled(jobId, resolve)) resolve();
275
+ });
276
+ const completion = buildShellCompletion(jobId, peekShellJob(jobId));
277
+ if (completion.taskStatus !== 'completed') {
278
+ throw new Error(completion.error || `shell task ${completion.taskStatus}`);
279
+ }
280
+ return completion.result;
281
+ }
282
+
255
283
  // A saturated shell lane must not fail the call: the command becomes a tracked
256
- // background task that waits (no admission ceiling) for a lease, runs, and
257
- // completes like any other task. The call returns its task_id immediately.
284
+ // background task that waits (no admission ceiling) for a lease and then runs
285
+ // as a normal foreground-then-promote command under the task's id, so a
286
+ // long-lived one releases its lease at promotion like any other. The call
287
+ // returns its task_id immediately.
258
288
  function queueSaturatedAsTask(run, saturation, spec) {
259
289
  const queuedAbort = new AbortController();
290
+ const taskId = run.foregroundRecordId;
260
291
  let task;
261
292
  try {
262
293
  task = startBackgroundTask({
263
- taskId: run.taskId,
294
+ taskId,
264
295
  surface: 'shell',
265
296
  operation: 'shell',
266
- label: String(spec.command || '').replace(/\s+/g, ' ').slice(0, 120),
297
+ label: String(spec.command || '')
298
+ .replace(/\s+/g, ' ')
299
+ .slice(0, 120),
267
300
  context: { callerSessionId: spec.ownerSessionId, clientHostPid: spec.clientHostPid },
268
301
  input: { command: spec.command, cwd: spec.cwd },
269
- meta: { task_id: run.taskId, stdout: null, stderr: null, cwd: spec.cwd, timeoutMs: spec.timeoutMs },
302
+ meta: { task_id: taskId, stdout: null, stderr: null, cwd: spec.cwd, timeoutMs: spec.backgroundDeadlineMs || 0 },
270
303
  resultType: 'shell_task_result',
271
- cancel: () => queuedAbort.abort(),
304
+ cancel: () => {
305
+ queuedAbort.abort();
306
+ try {
307
+ killShellJob(taskId);
308
+ } catch {}
309
+ },
272
310
  run: async () => {
273
311
  const result = await execShellCommand({
274
312
  ...spec,
313
+ jobId: taskId,
275
314
  abortSignal: queuedAbort.signal,
276
- autoBackgroundMs: 0,
277
- backgroundOnTimeout: false,
278
315
  admissionWaitMs: 0,
316
+ // The call already returned: nobody renders live progress.
317
+ onProgress: null,
318
+ onOutputTail: null,
279
319
  });
320
+ if (result.backgrounded && result.jobId) return settleQueuedPromotion(result.jobId);
280
321
  const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
281
322
  if (result.exitCode !== 0) {
282
323
  throw new Error(`exit code ${result.exitCode ?? result.signal ?? 'unknown'}${output ? `\n${output}` : ''}`);
283
324
  }
284
- return { task_id: run.taskId, status: 'completed', exit_code: 0, output };
325
+ return { task_id: taskId, status: 'completed', exit_code: 0, output };
285
326
  },
286
327
  });
287
328
  } catch {
@@ -474,72 +515,64 @@ function armDeadlines(run, { timeoutMs, autoBackgroundMs, backgroundOnTimeout, a
474
515
 
475
516
  // Run one command to a terminal ExecResult. Spawn-time EPERM retries (Windows
476
517
  // Defender scanning the child image) live in lib/shell-spawn-retry.mjs.
477
- export function execShellCommand({
478
- shell,
479
- shellArg,
480
- shellArgs,
481
- command,
482
- env,
483
- cwd,
484
- timeoutMs,
485
- abortSignal,
486
- autoBackgroundMs,
487
- onProgress,
488
- onOutputTail,
489
- clientHostPid,
490
- ownerSessionId,
491
- backgroundOnTimeout,
492
- promotedTimeoutMs = 0,
493
- backgroundDeadlineMs = 0,
494
- admissionWaitMs = SHELL_ADMISSION_WAIT_MS,
495
- admission = resourceAdmission,
496
- directArgv = null,
497
- // What the shell PARSES, when that must differ from what the caller shows.
498
- // `command` stays the display/record text (job labels, telemetry, shell-job
499
- // rows); `execScript` is the argv payload. They diverge only when a caller
500
- // hands the script to the shell out-of-band — see the POSIX env+eval path in
501
- // bash-tool.mjs, which keeps the command body out of the child's argv.
502
- execScript = null,
503
- }) {
518
+ export function execShellCommand(options) {
519
+ const {
520
+ shell,
521
+ shellArg,
522
+ shellArgs,
523
+ command,
524
+ env,
525
+ cwd,
526
+ timeoutMs,
527
+ abortSignal,
528
+ autoBackgroundMs,
529
+ onProgress,
530
+ onOutputTail,
531
+ clientHostPid,
532
+ ownerSessionId,
533
+ backgroundOnTimeout,
534
+ promotedTimeoutMs = 0,
535
+ backgroundDeadlineMs = 0,
536
+ // Fixed task_id for the run (a queued command reuses its task's id).
537
+ jobId = null,
538
+ admissionWaitMs = SHELL_ADMISSION_WAIT_MS,
539
+ admission = resourceAdmission,
540
+ directArgv = null,
541
+ // What the shell PARSES, when that must differ from what the caller shows.
542
+ // `command` stays the display/record text (job labels, telemetry, shell-job
543
+ // rows); `execScript` is the argv payload. They diverge only when a caller
544
+ // hands the script to the shell out-of-band — see the POSIX env+eval path in
545
+ // bash-tool.mjs, which keeps the command body out of the child's argv.
546
+ execScript = null,
547
+ } = options;
504
548
  return new Promise(async (resolve) => {
505
- const run = createShellRun({ abortSignal, resolve });
549
+ const run = createShellRun({ abortSignal, resolve, jobId });
506
550
  // An async executor's throw is NOT routed to this Promise — it becomes an
507
551
  // unhandled rejection of the executor's own promise while the awaiting
508
552
  // caller hangs forever. Every path below therefore has to settle the run
509
553
  // itself, including the unexpected one.
510
554
  try {
511
- const spawned = await admitAndSpawn(run, {
512
- admission,
513
- shell,
514
- shellArg,
515
- shellArgs,
516
- command,
517
- env,
518
- cwd,
519
- abortSignal,
520
- directArgv,
521
- execScript,
522
- ownerSessionId,
523
- clientHostPid,
524
- }, {
525
- admissionWaitMs,
526
- onSaturated: (err) =>
527
- backgroundOnTimeout &&
528
- queueSaturatedAsTask(run, err, {
529
- shell,
530
- shellArg,
531
- shellArgs,
532
- command,
533
- env,
534
- cwd,
535
- timeoutMs: backgroundDeadlineMs,
536
- clientHostPid,
537
- ownerSessionId,
538
- admission,
539
- directArgv,
540
- execScript,
541
- }),
542
- });
555
+ const spawned = await admitAndSpawn(
556
+ run,
557
+ {
558
+ admission,
559
+ shell,
560
+ shellArg,
561
+ shellArgs,
562
+ command,
563
+ env,
564
+ cwd,
565
+ abortSignal,
566
+ directArgv,
567
+ execScript,
568
+ ownerSessionId,
569
+ clientHostPid,
570
+ },
571
+ {
572
+ admissionWaitMs,
573
+ onSaturated: (err) => backgroundOnTimeout && queueSaturatedAsTask(run, err, options),
574
+ }
575
+ );
543
576
  if (!spawned) return;
544
577
 
545
578
  // Pre-aborted signal: kill immediately if the abort already fired
@@ -83,23 +83,36 @@ function classifyExecPolicy(command) {
83
83
  const decodedPowerShell = decodePowerShellEncodedCommand(text);
84
84
  if (decodedPowerShell) executableTargets.push(stripQuotedAndHeredoc(decodedPowerShell));
85
85
  if (executableTargets.some((target) => isBlockedCommand(target))) {
86
- return { decision: 'deny', reason: 'destructive or system-destabilising pattern (hard block)' };
86
+ return {
87
+ decision: 'deny',
88
+ reason: 'destructive or system-destabilising pattern (hard block)',
89
+ rule: 'hard-block',
90
+ };
87
91
  }
88
92
  // Hard-deny only real executable syntax. The policy scanner also sees user
89
93
  // search strings / regex literals (e.g. `-match 'powershell|bash|grep'`), so
90
94
  // high-risk pipe-to-shell / IEX / RunAs patterns must ignore quoted spans.
91
95
  const executableText = stripQuotedAndHeredoc(text);
92
96
  for (const pat of EXEC_POLICY_DENY_PATTERNS) {
93
- if (pat.test(executableText)) {
97
+ const hit = pat.exec(executableText);
98
+ if (hit) {
94
99
  return {
95
100
  decision: 'deny',
96
101
  reason: 'high-risk shell invocation (pipe-to-shell, elevated launcher, or remote-exec pattern)',
102
+ rule: `high-risk-pattern-${EXEC_POLICY_DENY_PATTERNS.indexOf(pat) + 1}`,
103
+ match: hit[0].trim().slice(0, 80),
104
+ alternative: 'save the script to a file, inspect it, then run it directly',
97
105
  };
98
106
  }
99
107
  }
100
108
  for (const name of _commandNamesAtPositions(executableText)) {
101
109
  if (EXEC_POLICY_DENY_COMMANDS.has(name)) {
102
- return { decision: 'deny', reason: `command "${name}" is not permitted` };
110
+ return {
111
+ decision: 'deny',
112
+ reason: `command "${name}" is not permitted`,
113
+ rule: 'denied-command',
114
+ match: name,
115
+ };
103
116
  }
104
117
  }
105
118
  // Destructive-but-legitimate commands are NOT judged here. The caller
@@ -131,7 +144,9 @@ export function evaluateExecPolicyFromTargets(targets) {
131
144
  export function formatExecPolicyBlockMessage(policyResult) {
132
145
  const r = policyResult || { decision: 'allow' };
133
146
  if (r.decision === 'deny') {
134
- return `Error: command blocked by exec policy${r.reason ? ` — ${r.reason}` : ''}`;
147
+ const rule = r.rule ? ` [rule ${r.rule}${r.match ? `: "${r.match}"` : ''}]` : '';
148
+ const alt = r.alternative ? `; instead: ${r.alternative}` : '';
149
+ return `Error: command blocked by exec policy${r.reason ? ` — ${r.reason}` : ''}${rule}${alt}`;
135
150
  }
136
151
  return null;
137
152
  }
@@ -1,13 +1,9 @@
1
1
  // Thin facade for the channels worker runtime. The implementation was split
2
2
  // out into ./lib/worker-main.mjs plus focused lib/ modules; this file preserves
3
- // the exact public API and boot side-effects by re-exporting from worker-main.
3
+ // the current worker API and boot side-effects by re-exporting from worker-main.
4
4
  // Kept as the channels worker entrypoint the daemon loads.
5
5
  export {
6
- TOOL_DEFS,
7
6
  handleToolCall,
8
- handleToolCallWithBridgeRetry,
9
- init,
10
- instructions,
11
7
  isChannelBridgeActive,
12
8
  isChannelsDegraded,
13
9
  start,
@@ -114,8 +114,7 @@ function getControlResponsePath(instanceId) {
114
114
  function getStopFlagPath(instanceId) {
115
115
  return join(RUNTIME_ROOT, `stop-${sanitize(instanceId)}.flag`);
116
116
  }
117
- // Pure metadata-advert read: ownership is the OS seat lock, so a leftover
118
- // advert is never evicted here. A transient read during an atomic rename may
117
+ // Pure metadata-advert read. A transient read during an atomic rename may
119
118
  // yield empty/partial content: retry once after 50 ms before reporting the
120
119
  // advert as unreadable.
121
120
  function readActiveInstance() {
@@ -141,8 +140,6 @@ function buildActiveInstanceState(instanceId, meta) {
141
140
  supervisor_started_at: Date.now(),
142
141
  server_started_at: Date.now(),
143
142
  updatedAt: Date.now(),
144
- turnEndFile: getTurnEndPath(instanceId),
145
- statusFile: getStatusPath(instanceId),
146
143
  ...metaFields(meta),
147
144
  };
148
145
  }
@@ -193,26 +190,14 @@ function refreshActiveInstance(instanceId, meta, options) {
193
190
  (curRaw) => {
194
191
  const prevForPreserve = curRaw;
195
192
  const prev = activeInstanceStaleReason(curRaw) ? null : curRaw;
196
- // CAS guard (opt-in via options.onlyIfOwned): heartbeat/refresh ticks
197
- // check ownership OUTSIDE this lock, so a newer session can claim the
198
- // seat between that check and this locked update. Without this re-read,
199
- // the tick would overwrite the newer owner's instanceId (TOCTOU
200
- // re-steal -> ownership ping-pong / double provider connections).
201
- // Returning undefined aborts the update with no write. Explicit claims
202
- // (boot, claimBridgeOwnership) omit the option and stay last-wins.
203
- if (options?.onlyIfOwned && (!prev?.instanceId || prev.instanceId !== instanceId)) {
204
- return undefined;
205
- }
206
- // CAS guard (opt-in via options.onlyIfVacant): auto-start claim-if-vacant.
207
- // Abort the write when a live (non-stale) owner OTHER than us already holds
208
- // the seat — an auto-start must never steal from a live owner. A stale/dead
209
- // prior owner leaves prev=null above, so it does NOT block the claim; an
210
- // absent seat (prev=null) is claimable too.
211
- if (options?.onlyIfVacant && prev?.instanceId && prev.instanceId !== instanceId) {
212
- return undefined;
213
- }
214
193
  // Drop stale fields (pid/startedAt) written by older server versions.
215
- const { pid: _legacyPid, startedAt: _legacyStartedAt, ...prevRest } = prev ?? {};
194
+ const {
195
+ pid: _legacyPid,
196
+ startedAt: _legacyStartedAt,
197
+ turnEndFile: _turnEndFile,
198
+ statusFile: _statusFile,
199
+ ...prevRest
200
+ } = prev ?? {};
216
201
  const identity = buildRuntimeIdentity();
217
202
  // server_started_at tracks the CURRENT server_pid's start time so the
218
203
  // dev-sync barrier can verify the CHILD's freshness (the supervisor's
@@ -82,12 +82,6 @@ function createToolDispatch({ isChannelsDegraded, lifecycle }) {
82
82
  };
83
83
  break;
84
84
  }
85
- case 'rebind_current_transcript': {
86
- // Transcript forwarding is retired with channel messaging: accept
87
- // and no-op so legacy lead-side pushes never surface an error.
88
- result = { content: [{ type: 'text', text: 'transcript rebind retired' }] };
89
- break;
90
- }
91
85
  // memory — handled by memory-service.mjs MCP
92
86
  default:
93
87
  result = {
@@ -105,13 +99,7 @@ function createToolDispatch({ isChannelsDegraded, lifecycle }) {
105
99
  return result;
106
100
  }
107
101
 
108
- // IPC/HTTP wrapper: with channel messaging retired there is no transcript
109
- // pre-flush or tool-log relay — plain dispatch.
110
- async function handleToolCallWithBridgeRetry(toolName, args, signal) {
111
- return handleToolCall(toolName, args, signal);
112
- }
113
-
114
- return { handleToolCall, handleToolCallWithBridgeRetry };
102
+ return { handleToolCall };
115
103
  }
116
104
 
117
105
  export { createToolDispatch };
@@ -14,7 +14,7 @@ export function runWorkerIpc({
14
14
  getProvider: _getProvider,
15
15
  getConfig: _getConfig,
16
16
  handleMemoryCallResponse,
17
- handleToolCallWithBridgeRetry,
17
+ handleToolCall,
18
18
  bootProfile,
19
19
  }) {
20
20
  const sendToParent = (message) =>
@@ -94,7 +94,7 @@ export function runWorkerIpc({
94
94
  _inFlightChannelCalls.set(msg.callId, ac);
95
95
  let result;
96
96
  try {
97
- result = await handleToolCallWithBridgeRetry(msg.name, msg.args || {}, ac.signal);
97
+ result = await handleToolCall(msg.name, msg.args || {}, ac.signal);
98
98
  } finally {
99
99
  _inFlightChannelCalls.delete(msg.callId);
100
100
  }