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
@@ -113,10 +113,14 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
113
113
  },
114
114
  async saveMcpServer(input = {}) {
115
115
  const servers = mcpServersOf(getConfig());
116
- const originalName = clean(input.originalName) || clean(input.name);
117
- if (originalName && !Object.hasOwn(servers, originalName)) {
118
- throw new Error(`MCP server not configured: ${originalName}`);
116
+ // An explicit originalName must exist; a bare name edits a matching
117
+ // server and otherwise creates a new one.
118
+ const explicitOriginal = clean(input.originalName);
119
+ if (explicitOriginal && !Object.hasOwn(servers, explicitOriginal)) {
120
+ throw new Error(`MCP server not configured: ${explicitOriginal}`);
119
121
  }
122
+ const bareName = clean(input.name);
123
+ const originalName = explicitOriginal || (Object.hasOwn(servers, bareName) ? bareName : '');
120
124
  const existing = originalName ? servers[originalName] : {};
121
125
  const normalizedInput = normalizeMcpServerInput(mergedServerInput(input, existing, originalName));
122
126
  const name = originalName && clean(input.name) === originalName ? originalName : normalizedInput.name;
@@ -10,7 +10,6 @@ import {
10
10
  RUNTIME,
11
11
  WEB_SEARCH_TOOL_DEFS,
12
12
  MEMORY_TOOL_DEFS,
13
- CHANNEL_TOOL_DEFS,
14
13
  CODE_GRAPH_TOOL_DEFS,
15
14
  STATUSLINE_SESSION_ROUTES,
16
15
  STANDALONE_ROOT,
@@ -49,7 +48,6 @@ export async function loadRuntimeModules() {
49
48
  statusRoutes,
50
49
  webSearchToolDefs,
51
50
  memoryToolDefs,
52
- channelToolDefs,
53
51
  codeGraphToolDefs,
54
52
  ] = await Promise.all([
55
53
  profiledImport('config', `${RUNTIME}/config.mjs`),
@@ -62,7 +60,6 @@ export async function loadRuntimeModules() {
62
60
  profiledImport('status-routes', STATUSLINE_SESSION_ROUTES, { optional: true }),
63
61
  profiledImport('web-search-tool-defs', WEB_SEARCH_TOOL_DEFS, { optional: true }),
64
62
  profiledImport('memory-tool-defs', MEMORY_TOOL_DEFS, { optional: true }),
65
- profiledImport('channel-tool-defs', CHANNEL_TOOL_DEFS, { optional: true }),
66
63
  profiledImport('code-graph-tool-defs', CODE_GRAPH_TOOL_DEFS, { optional: true }),
67
64
  ]);
68
65
  bootProfile('imports:ready', { ms: (performance.now() - startedAt).toFixed(1) });
@@ -88,7 +85,6 @@ export async function loadRuntimeModules() {
88
85
  statusRoutes,
89
86
  webSearchToolDefs,
90
87
  memoryToolDefs,
91
- channelToolDefs,
92
88
  codeGraphToolDefs,
93
89
  };
94
90
  }
@@ -1,3 +1,4 @@
1
+ import { currentTurnEffort } from '../runtime/agent/orchestrator/session/turn-effort.mjs';
1
2
  import { SESSION_ID_PATTERN } from '../runtime/agent/orchestrator/runtime-core/session-id.mjs';
2
3
  import { SessionAddressError, bindSessionAddress } from './session-address.mjs';
3
4
 
@@ -79,6 +80,10 @@ export function createRuntimeFacade({
79
80
  get modelParameters() {
80
81
  return state.route.modelParameters || {};
81
82
  },
83
+ // The effort the runtime resolved for the turn in flight (Auto judge), or null.
84
+ get turnEffort() {
85
+ return currentTurnEffort(state.session);
86
+ },
82
87
  get effortOptions() {
83
88
  return state.route.effortOptions || [];
84
89
  },
@@ -6,7 +6,6 @@ export const RUNTIME = '../runtime/agent/orchestrator';
6
6
  export const WEB_SEARCH_RUNTIME = '../runtime/web-search/index.mjs';
7
7
  export const WEB_SEARCH_TOOL_DEFS = '../runtime/web-search/tool-defs.mjs';
8
8
  export const MEMORY_TOOL_DEFS = '../runtime/memory/tool-defs.mjs';
9
- export const CHANNEL_TOOL_DEFS = '../runtime/channels/tool-defs.mjs';
10
9
  export const CODE_GRAPH_TOOL_DEFS = '../runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs';
11
10
  export const CODE_GRAPH_RUNTIME = '../runtime/agent/orchestrator/tools/code-graph.mjs';
12
11
  export const STATUSLINE_SESSION_ROUTES = '../vendor/statusline/src/gateway/session-routes.mjs';
@@ -10,6 +10,7 @@ import { loadConfig } from '../runtime/agent/orchestrator/config.mjs';
10
10
  import { createSession } from '../runtime/agent/orchestrator/session/manager/session-lifecycle.mjs';
11
11
  import { askSession } from '../runtime/agent/orchestrator/session/manager/ask-session.mjs';
12
12
  import { modelRouteFields, parseScheduleModelRef } from '../runtime/shared/schedule-model-ref.mjs';
13
+ import { migrateCodexRoute } from '../runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs';
13
14
  import { automationWorkflowOpts } from './automation-workflow.mjs';
14
15
  import { runAutomationTurns } from './automation-agents.mjs';
15
16
  import { automationPromptContent } from '../runtime/shared/automation-attachments.mjs';
@@ -17,7 +18,8 @@ import { automationPromptContent } from '../runtime/shared/automation-attachment
17
18
  /** Resolve schedule.model into a {provider,model,effort?,fast?} route. */
18
19
  function scheduleRouteFromModelRef(modelRef, config = null) {
19
20
  const ref = parseScheduleModelRef(modelRef);
20
- if (ref && typeof ref === 'object') return ref;
21
+ // A model the provider catalog superseded runs on its successor.
22
+ if (ref && typeof ref === 'object') return migrateCodexRoute(ref);
21
23
  const cfg = config || loadConfig({ secrets: false });
22
24
  const preset = cfg.presets?.find((p) => p.id === ref || p.name === ref);
23
25
  if (!preset) {
@@ -10,6 +10,11 @@
10
10
  // the mutator: an async job replays those effects onto the fresh on-disk
11
11
  // document under the cross-process lock, so concurrent writers of other keys
12
12
  // are merged, never overwritten. Jobs run strictly one at a time in FIFO order.
13
+ //
14
+ // write(mutator, { rerun: true }) is the conditional-update form: the mutator
15
+ // itself is queued and re-run against the locked fresh document, in order with
16
+ // the recorded effects, so a field-level update re-checks its condition against
17
+ // what is really on disk instead of replacing a whole value it once saw.
13
18
  import { updateJsonAtomic, updateJsonAtomicSync } from '../../../runtime/shared/atomic-file.mjs';
14
19
  import { registerJsonProjection } from '../../../runtime/shared/pending-json-projection.mjs';
15
20
  import { sleep } from '../../../runtime/shared/sleep.mjs';
@@ -85,21 +90,40 @@ function compactOps(batch) {
85
90
  });
86
91
  }
87
92
 
88
- function replayOps(ops, args) {
93
+ function replayOps(ops, args, mergeSet) {
89
94
  args.forEach((arg, index) => {
90
95
  const op = ops[index];
91
96
  if (!op) return;
92
97
  if (arg instanceof Map) {
93
98
  for (const key of op.del) arg.delete(key);
94
- for (const [key, json] of op.set) arg.set(key, JSON.parse(json));
99
+ for (const [key, json] of op.set) arg.set(key, mergeSet(index, arg.get(key), JSON.parse(json)));
95
100
  } else if (arg instanceof Set) {
96
101
  for (const value of op.add) arg.add(value);
97
102
  }
98
103
  });
99
104
  }
100
105
 
101
- function replayBatch(batch, args) {
102
- replayOps(compactOps(batch), args);
106
+ /** Entries are recorded op sets or re-run mutators; runs of op sets between
107
+ * re-run mutators compact, and every entry keeps its place in the order. */
108
+ function replayBatch(batch, args, mergeSet) {
109
+ let run = [];
110
+ const replayRun = () => {
111
+ if (run.length) replayOps(compactOps(run), args, mergeSet);
112
+ run = [];
113
+ };
114
+ for (const entry of batch) {
115
+ if (typeof entry !== 'function') {
116
+ run.push(entry);
117
+ continue;
118
+ }
119
+ replayRun();
120
+ entry(...args);
121
+ }
122
+ replayRun();
123
+ }
124
+
125
+ function reportFailure(file, what, error) {
126
+ process.stderr.write(`[index-write] ${file}: ${what}: ${error?.message || error}\n`);
103
127
  }
104
128
 
105
129
  /**
@@ -109,8 +133,17 @@ function replayBatch(batch, args) {
109
133
  * builds the next document from a raw one; `mutator` receives the keyed collections
110
134
  * @param {() => any} options.readDoc fresh raw document from disk (sync read)
111
135
  * @param {() => void} [options.onPersisted] the file changed on disk
136
+ * @param {(index: number, prev: any, next: any) => any} [options.mergeSet]
137
+ * value a recorded set leaves in Map argument `index`, given the locked
138
+ * document's current value there; default: the recorded value
112
139
  */
113
- export function createIndexWriteQueue({ file, rewrite, readDoc, onPersisted = () => {} }) {
140
+ export function createIndexWriteQueue({
141
+ file,
142
+ rewrite,
143
+ readDoc,
144
+ onPersisted = () => {},
145
+ mergeSet = (_index, _prev, next) => next,
146
+ }) {
114
147
  let projection = null; // raw document including unpersisted ops, else null
115
148
  const self = { flush, flushSync: () => flushSync() };
116
149
  if (file) registerJsonProjection(file, () => projection);
@@ -134,10 +167,15 @@ export function createIndexWriteQueue({ file, rewrite, readDoc, onPersisted = ()
134
167
  async function persist(batch) {
135
168
  for (let attempt = 0; ; attempt += 1) {
136
169
  try {
137
- await updateJsonAtomic(file, (cur) => rewrite(cur, (...args) => replayBatch(batch, args)), { lock: true });
170
+ await updateJsonAtomic(file, (cur) => rewrite(cur, (...args) => replayBatch(batch, args, mergeSet)), {
171
+ lock: true,
172
+ });
138
173
  return;
139
- } catch {
140
- if (attempt >= RETRY_DELAYS_MS.length) return; // dropped, like the sync writer's swallowed failure
174
+ } catch (error) {
175
+ if (attempt >= RETRY_DELAYS_MS.length) {
176
+ reportFailure(file, `dropped ${batch.length} unpersisted write(s)`, error);
177
+ return;
178
+ }
141
179
  await sleep(RETRY_DELAYS_MS[attempt]);
142
180
  }
143
181
  }
@@ -181,11 +219,12 @@ export function createIndexWriteQueue({ file, rewrite, readDoc, onPersisted = ()
181
219
  clearTimer();
182
220
  pending = [];
183
221
  try {
184
- updateJsonAtomicSync(file, (cur) => rewrite(cur, (...args) => replayBatch(ops, args)), {
222
+ updateJsonAtomicSync(file, (cur) => rewrite(cur, (...args) => replayBatch(ops, args, mergeSet)), {
185
223
  lock: true,
186
224
  timeoutMs: EXIT_LOCK_TIMEOUT_MS,
187
225
  });
188
- } catch {
226
+ } catch (error) {
227
+ reportFailure(file, `synchronous flush of ${ops.length} write(s) failed; kept queued`, error);
189
228
  pending = ops;
190
229
  return false;
191
230
  }
@@ -197,18 +236,20 @@ export function createIndexWriteQueue({ file, rewrite, readDoc, onPersisted = ()
197
236
  }
198
237
 
199
238
  return {
200
- /** Run `mutator` now against the projection; persist its effect later.
239
+ /** Run `mutator` now against the projection; persist its effect later
240
+ * (`rerun`: persist by re-running it on the locked document instead).
201
241
  * Returns the projected document, or null (no file / mutator threw). */
202
- write(mutator) {
242
+ write(mutator, { rerun = false } = {}) {
203
243
  if (!file || typeof mutator !== 'function') return null;
204
244
  try {
205
245
  let ops = null;
206
246
  const next = rewrite(projection ?? readDoc(), (...args) => {
207
- ops = recordOps(args, () => mutator(...args));
247
+ if (rerun) mutator(...args);
248
+ else ops = recordOps(args, () => mutator(...args));
208
249
  });
209
250
  projection = next;
210
251
  version += 1;
211
- pending.push(ops);
252
+ pending.push(rerun ? mutator : ops);
212
253
  unsettled.add(self);
213
254
  schedule();
214
255
  return next;
@@ -19,12 +19,15 @@ export function isActiveWorkerRow(row = {}) {
19
19
  return !isTerminalWorkerStatus(effective);
20
20
  }
21
21
 
22
- function agentRow({ tag, session, runtime, progress, now }) {
22
+ function agentRow({ tag, session, runtime, progress, now, indexRow = null }) {
23
23
  const status = session.closed === true ? 'closed' : session.status || 'idle';
24
24
  const stage =
25
25
  session.stage ||
26
26
  (status === 'idle' || status === 'error' || status === 'closed' ? status : runtime?.stage || status);
27
- return {
27
+ // Additive: the current turn's start (worker index, stamped at dispatch) so
28
+ // surfaces time a continued worker from its turn, not from createdAt.
29
+ const turnStartedAt = indexRow?.turnStartedAt || session.turnStartedAt || null;
30
+ const row = {
28
31
  tag,
29
32
  sessionId: session.id,
30
33
  agent: session.agent || null,
@@ -33,8 +36,14 @@ function agentRow({ tag, session, runtime, progress, now }) {
33
36
  preset: session.presetName || null,
34
37
  effort: session.effort || null,
35
38
  fast: session.fast === true,
39
+ // Additive: route model parameters (e.g. Daybreak Blue) when the route has any.
40
+ ...(session.modelParameters && Object.keys(session.modelParameters).length
41
+ ? { modelParameters: { ...session.modelParameters } }
42
+ : {}),
36
43
  status,
37
44
  stage,
45
+ // Additive: present only while the worker's turn waits on a user approval.
46
+ ...(runtime?.awaitingUser ? { awaitingUser: runtime.awaitingUser } : {}),
38
47
  ...progress,
39
48
  createdAt: session.createdAt || null,
40
49
  updatedAt: session.updatedAt || null,
@@ -51,16 +60,19 @@ function agentRow({ tag, session, runtime, progress, now }) {
51
60
  : Math.max(0, Number(session.messageCount || 0)),
52
61
  tools: Array.isArray(session.tools) ? session.tools.length : Math.max(0, Number(session.toolCount || 0)),
53
62
  };
63
+ if (turnStartedAt && isActiveWorkerRow(row)) row.turnStartedAt = turnStartedAt;
64
+ return row;
54
65
  }
55
66
 
56
67
  export function createAgentList({ mgr, refreshTagsFromSessions, agentSessionEntries, progress }) {
57
68
  return function list({ scanSessions = false, context = {}, includeTerminal = false } = {}) {
58
69
  refreshTagsFromSessions({ scanSessions, context });
59
70
  const now = Date.now();
60
- const rows = agentSessionEntries({ scanSessions, context }).map(({ tag, session }) =>
71
+ const rows = agentSessionEntries({ scanSessions, context }).map(({ tag, session, indexRow }) =>
61
72
  agentRow({
62
73
  tag,
63
74
  session,
75
+ indexRow,
64
76
  runtime: mgr.getSessionRuntime?.(session.id),
65
77
  progress: progress.sessionProgressExtras(session.id, session.agent || null, now),
66
78
  now,
@@ -91,6 +91,8 @@ export function createSessionProgress({ mgr }) {
91
91
  return {
92
92
  workerStatus: status,
93
93
  stage: progress.worker_stage || runtime?.stage || status,
94
+ // Additive: present only while the worker's turn waits on a user approval.
95
+ ...(runtime?.awaitingUser ? { awaitingUser: runtime.awaitingUser } : {}),
94
96
  clientHostPid: session.clientHostPid || null,
95
97
  lastStreamDeltaAt: runtime?.lastStreamDeltaAt ? new Date(runtime.lastStreamDeltaAt).toISOString() : null,
96
98
  ...progress,
@@ -0,0 +1,97 @@
1
+ // worker-index/approval-wait.mjs
2
+ // Persists a worker's approval wait (runtime begin/end transitions) into the
3
+ // worker index, so processes that read only the stored index — the desktop
4
+ // Agents pane — see it. One process can hold several worker-index instances
5
+ // over the same file (every runtime's agent tool in the daemon); they share one
6
+ // row store per file, and exactly one writer per file answers each transition,
7
+ // so a wait costs one index write on begin and one on end.
8
+ //
9
+ // `awaitingUser` is a field of its own on the row: only conditional field
10
+ // updates (applyAwaitingUser, re-run on the locked document) set or clear it,
11
+ // fenced by session id and turn. Whole-row writes keep whatever the current
12
+ // row holds and drop it once the row settles (settleAwaitingUser).
13
+ import {
14
+ currentSessionApprovalWaits,
15
+ onSessionApprovalWaitChange,
16
+ } from '../../../../runtime/shared/session-approval-wait.mjs';
17
+ import { stampMs } from '../helpers.mjs';
18
+ import { isTerminalWorkerStatus } from '../worker-rows.mjs';
19
+
20
+ const writersByFile = new Map(); // file -> writer functions, first answers
21
+ let unsubscribe = null;
22
+
23
+ function withAwaitingUser(row, value) {
24
+ if (value ? row.awaitingUser === value : !('awaitingUser' in row)) return row;
25
+ const { awaitingUser: _prior, ...rest } = row;
26
+ return value ? { ...rest, awaitingUser: value } : rest;
27
+ }
28
+
29
+ /** `row` as a whole-row write may leave it: the flag `current` (the value the
30
+ * row holds before the write) survives unless the row settled. Returns `row`
31
+ * itself when nothing changes. */
32
+ export function settleAwaitingUser(row, current) {
33
+ return withAwaitingUser(row, current === 'approval' && !isTerminalWorkerStatus(row.status) ? 'approval' : null);
34
+ }
35
+
36
+ /** The flag `fence` wants on `row`, or undefined when the update is not for
37
+ * it: another session, or a turn that started after the transition's turn. */
38
+ function fencedAwaitingUser(row, { sessionId, awaitingUser, turnAt }) {
39
+ if (row.sessionId !== sessionId) return undefined;
40
+ if (turnAt > 0 && stampMs(row.turnStartedAt) > turnAt) return undefined;
41
+ return awaitingUser === 'approval' && !isTerminalWorkerStatus(row.status) ? 'approval' : null;
42
+ }
43
+
44
+ /** Whether applyAwaitingUser(fence) would change any of `rows`. */
45
+ export function awaitingUserWouldChange(rows, fence) {
46
+ return rows.some((row) => {
47
+ const wanted = fencedAwaitingUser(row, fence);
48
+ return wanted !== undefined && withAwaitingUser(row, wanted) !== row;
49
+ });
50
+ }
51
+
52
+ /** Field update: only `awaitingUser` of the fenced rows, nothing else. */
53
+ export function applyAwaitingUser(byKey, fence) {
54
+ for (const [key, row] of byKey) {
55
+ const wanted = fencedAwaitingUser(row, fence);
56
+ if (wanted === undefined) continue;
57
+ const next = withAwaitingUser(row, wanted);
58
+ if (next !== row) byKey.set(key, next);
59
+ }
60
+ }
61
+
62
+ /** Field update: drop every flag `isStale(row)` holds for. */
63
+ export function clearAwaitingUser(byKey, isStale) {
64
+ for (const [key, row] of byKey) {
65
+ if (row.awaitingUser && isStale(row)) byKey.set(key, withAwaitingUser(row, null));
66
+ }
67
+ }
68
+
69
+ function persistTransition(sessionId, awaitingUser, meta) {
70
+ for (const writers of writersByFile.values()) writers[0]?.(sessionId, awaitingUser, meta);
71
+ }
72
+
73
+ /** Register `writer(sessionId, awaitingUser, { turnAt })` for `file`; returns
74
+ * the release. The first writer of a file also receives the waits already
75
+ * open, so a wait that began before any index existed is not lost. */
76
+ export function registerApprovalWaitWriter(file, writer) {
77
+ if (!file || typeof writer !== 'function') return () => {};
78
+ const writers = writersByFile.get(file) || [];
79
+ writers.push(writer);
80
+ writersByFile.set(file, writers);
81
+ unsubscribe ??= onSessionApprovalWaitChange(persistTransition);
82
+ if (writers.length === 1) {
83
+ for (const { sessionId, ...meta } of currentSessionApprovalWaits()) writer(sessionId, 'approval', meta);
84
+ }
85
+ let released = false;
86
+ return () => {
87
+ if (released) return;
88
+ released = true;
89
+ const remaining = (writersByFile.get(file) || []).filter((entry) => entry !== writer);
90
+ if (remaining.length) writersByFile.set(file, remaining);
91
+ else writersByFile.delete(file);
92
+ if (writersByFile.size === 0 && unsubscribe) {
93
+ unsubscribe();
94
+ unsubscribe = null;
95
+ }
96
+ };
97
+ }
@@ -1,7 +1,8 @@
1
1
  // worker-index/mutation-batch.mjs
2
2
  // Spawn-path writes are batched onto one immediate so a parallel fanout pays a
3
- // single locked rewrite instead of one per worker.
4
- export function createMutationBatch(write) {
3
+ // single locked rewrite instead of one per worker. `afterFlush` runs once the
4
+ // batch's rows are in the store.
5
+ export function createMutationBatch(write, afterFlush = () => {}) {
5
6
  const pending = [];
6
7
  let timer = null;
7
8
 
@@ -21,6 +22,7 @@ export function createMutationBatch(write) {
21
22
  }
22
23
  }
23
24
  });
25
+ afterFlush();
24
26
  }
25
27
 
26
28
  function queue(mutator) {
@@ -14,6 +14,10 @@ const WORKER_POOL_FRESH_MS = 2 * 60 * 1000;
14
14
  export const isActiveWorkerRow = (row) =>
15
15
  ACTIVE_WORKER_STATUS.test(clean(row?.status)) || ACTIVE_WORKER_STATUS.test(clean(row?.stage));
16
16
 
17
+ /** A persisted approval wait whose runtime is gone: the in-memory wait died
18
+ * with that process (daemon restart), so the flag must not stick. */
19
+ export const isStaleApprovalWait = (row) => Boolean(row?.awaitingUser) && !runtimeAlive(row.runtimePid);
20
+
17
21
  export function createRowLiveness({ dataDir, cfgMod }) {
18
22
  function heartbeatFresh(sessionId, now) {
19
23
  const id = clean(sessionId);
@@ -47,11 +51,13 @@ export function createRowLiveness({ dataDir, cfgMod }) {
47
51
  }
48
52
 
49
53
  /** The row settled to idle. `touch` restamps it as finished now (this
50
- * process is giving up its own work); otherwise existing stamps win. */
54
+ * process is giving up its own work); otherwise existing stamps win. An
55
+ * idle worker waits on nobody, so a persisted approval wait drops too. */
51
56
  function idleRow(row, now, touch = false) {
52
57
  const stamp = new Date(now).toISOString();
58
+ const { awaitingUser: _awaitingUser, ...settled } = row;
53
59
  return {
54
- ...row,
60
+ ...settled,
55
61
  status: 'idle',
56
62
  stage: 'idle',
57
63
  turnStartedAt: null,
@@ -55,6 +55,9 @@ export function normalizeWorkerRows(value) {
55
55
  toolPermission: clean(row.toolPermission) || null,
56
56
  messages: positiveInt(row.messages) || 0,
57
57
  tools: positiveInt(row.tools) || 0,
58
+ // Additive and transient: present only while the worker's turn waits on
59
+ // a user approval (worker-index setAwaitingUser).
60
+ ...(row.awaitingUser === 'approval' ? { awaitingUser: 'approval' } : {}),
58
61
  }))
59
62
  .filter((row) => !isLeadPoolAgent(row.agent) && keepWorkerRow(row));
60
63
  }
@@ -1,12 +1,36 @@
1
1
  // worker-index/row-store.mjs
2
2
  // The on-disk worker index file: an mtime-keyed parse cache in front of it and
3
- // the single in-process writer queue that republishes rows + tombstones.
3
+ // the single in-process writer queue that republishes rows + tombstones. Every
4
+ // worker index of a process over one file shares one store (acquireWorkerRowStore),
5
+ // so all of them serialize through the same projection and queue.
4
6
  import { readFileSync, statSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
5
8
 
6
9
  import { createIndexWriteQueue } from '../index-write-queue.mjs';
7
10
  import { normalizeTagTombstones, tagTombstoneKey, workerRowKey } from '../worker-rows.mjs';
11
+ import { settleAwaitingUser } from './approval-wait.mjs';
8
12
  import { keepWorkerRow, normalizeWorkerRows } from './row-shape.mjs';
9
13
 
14
+ const ROWS_ARG = 0; // the rows Map's position in the rewrite mutator's arguments
15
+ const sharedStores = new Map(); // canonical file -> { store, refs }
16
+
17
+ const canonicalFile = (file) => (process.platform === 'win32' ? resolve(file).toLowerCase() : resolve(file));
18
+
19
+ /** A whole-row write never decides `awaitingUser`: every row it changed keeps
20
+ * the flag the row held before (never a stale copy's), dropped once settled. */
21
+ function keepingAwaitingUser(mutator) {
22
+ return (byKey, ...rest) => {
23
+ const before = new Map(byKey);
24
+ mutator(byKey, ...rest);
25
+ for (const [key, row] of byKey) {
26
+ const prior = before.get(key);
27
+ if (row === prior) continue;
28
+ const settled = settleAwaitingUser(row, prior?.awaitingUser);
29
+ if (settled !== row) byKey.set(key, settled);
30
+ }
31
+ };
32
+ }
33
+
10
34
  /** One rewrite: the current document as keyed maps, the caller's mutator over
11
35
  * them, then the v2 document to publish. */
12
36
  function rewriteIndexDocument(cur, mutator) {
@@ -61,6 +85,9 @@ export function createWorkerRowStore(file) {
61
85
  onPersisted: () => {
62
86
  dirty = true;
63
87
  },
88
+ // The same rule against the locked document: a replayed row keeps the
89
+ // flag that is on disk now, not the one the projection held.
90
+ mergeSet: (index, prev, next) => (index === ROWS_ARG ? settleAwaitingUser(next, prev?.awaitingUser) : next),
64
91
  });
65
92
 
66
93
  function projectedView() {
@@ -115,13 +142,17 @@ export function createWorkerRowStore(file) {
115
142
  // Single writer path: the mutator runs now over keyed maps against the
116
143
  // in-process projection (see index-write-queue.mjs); its effect is
117
144
  // republished as rows + tombstones by one async locked writer.
118
- const write = (mutator) => queue.write(mutator);
145
+ const write = (mutator) => queue.write(typeof mutator === 'function' ? keepingAwaitingUser(mutator) : mutator);
119
146
 
120
147
  return {
121
148
  readAll,
122
149
  readSnapshot,
123
150
  readTombstones,
124
151
  write,
152
+ /** Conditional field update: `mutator` runs now on the projection and again
153
+ * on the locked document when persisted, so its condition is re-checked
154
+ * against what is on disk. The one writer of `awaitingUser`. */
155
+ update: (mutator) => queue.write(mutator, { rerun: true }),
125
156
  /** Resolves once every write so far is on disk. */
126
157
  flush: queue.flush,
127
158
  /** Exit path: persist what is still queued, synchronously. */
@@ -131,3 +162,28 @@ export function createWorkerRowStore(file) {
131
162
  },
132
163
  };
133
164
  }
165
+
166
+ /** The process's one store for `file` (a fresh no-op store for null), and its
167
+ * idempotent release; the store is dropped once its last holder releases. */
168
+ export function acquireWorkerRowStore(file) {
169
+ if (!file) return { store: createWorkerRowStore(null), release: () => {} };
170
+ const key = canonicalFile(file);
171
+ let entry = sharedStores.get(key);
172
+ if (!entry) {
173
+ entry = { store: createWorkerRowStore(file), refs: 0 };
174
+ sharedStores.set(key, entry);
175
+ }
176
+ entry.refs += 1;
177
+ // A new holder re-reads the file, as a store of its own would.
178
+ entry.store.invalidate();
179
+ let released = false;
180
+ return {
181
+ store: entry.store,
182
+ release: () => {
183
+ if (released) return;
184
+ released = true;
185
+ entry.refs -= 1;
186
+ if (entry.refs === 0 && sharedStores.get(key) === entry) sharedStores.delete(key);
187
+ },
188
+ };
189
+ }