mixdog 1.3.2 → 1.3.4

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 (182) hide show
  1. package/package.json +8 -8
  2. package/scripts/verify-runtime-dependency-versions.mjs +158 -0
  3. package/src/agents/reviewer/AGENT.md +5 -7
  4. package/src/agents/reviewer/agent.json +1 -1
  5. package/src/defaults/shipped-history.json +4 -0
  6. package/src/defaults/skills/browser-use/SKILL.md +6 -3
  7. package/src/defaults/skills/local-provider/SKILL.md +33 -6
  8. package/src/rules/lead/LEAD.md +5 -8
  9. package/src/rules/shared/45-destructive.md +5 -1
  10. package/src/rules/shared/60-verification.md +6 -4
  11. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
  12. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
  13. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
  16. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  17. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  18. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
  19. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
  21. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
  22. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
  23. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
  24. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
  25. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
  26. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
  27. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
  28. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
  29. package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
  30. package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
  31. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
  32. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
  33. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
  34. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
  37. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
  38. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
  39. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
  41. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
  42. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
  43. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
  45. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
  46. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
  47. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
  50. package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
  51. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
  52. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
  53. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
  54. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
  56. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
  57. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
  58. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
  59. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
  60. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
  61. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
  63. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
  64. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
  65. package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
  66. package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
  68. package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
  69. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
  70. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
  71. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
  72. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
  73. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
  74. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
  75. package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
  76. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
  77. package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
  79. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
  81. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
  82. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
  84. package/src/runtime/attachments/legacy-office.mjs +110 -0
  85. package/src/runtime/attachments/prompt-restore.mjs +70 -0
  86. package/src/runtime/attachments/store.mjs +60 -3
  87. package/src/runtime/effort-judge/judge-client.mjs +61 -14
  88. package/src/runtime/effort-judge/model-install.mjs +34 -2
  89. package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
  90. package/src/runtime/office/portable/portable-soffice.mjs +132 -30
  91. package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
  92. package/src/runtime/shared/agent-avatar.mjs +3 -2
  93. package/src/runtime/shared/agent-room-host.d.mts +31 -0
  94. package/src/runtime/shared/agent-room-host.mjs +58 -0
  95. package/src/runtime/shared/bot-hops.mjs +16 -11
  96. package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
  97. package/src/runtime/shared/redact-directories.mjs +18 -0
  98. package/src/runtime/shared/remove-directory.mjs +28 -0
  99. package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
  100. package/src/runtime/shared/session-sidebar-host.mjs +16 -3
  101. package/src/runtime/shared/tool-execution-contract.mjs +4 -0
  102. package/src/runtime/shared/tool-work-units.mjs +12 -9
  103. package/src/runtime/shared/turn-snapshot.mjs +233 -122
  104. package/src/runtime/shared/user-data-guard.mjs +51 -19
  105. package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
  106. package/src/runtime/shared/worktrees/contract.mjs +11 -3
  107. package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
  108. package/src/runtime/shared/worktrees/manager.mjs +419 -52
  109. package/src/runtime/shared/worktrees/registry.mjs +3 -1
  110. package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
  111. package/src/runtime/tidy/process.mjs +41 -8
  112. package/src/runtime/tidy/resolve.mjs +10 -7
  113. package/src/session-runtime/agent-home-delivery.mjs +73 -91
  114. package/src/session-runtime/agent-profile-changes.mjs +16 -0
  115. package/src/session-runtime/agent-room-delivery.mjs +2 -63
  116. package/src/session-runtime/agent-task-sessions.mjs +194 -64
  117. package/src/session-runtime/agent-tool-routing.mjs +49 -4
  118. package/src/session-runtime/boot/apis.mjs +3 -0
  119. package/src/session-runtime/boot/facade.mjs +5 -6
  120. package/src/session-runtime/boot/tools.mjs +23 -1
  121. package/src/session-runtime/held-requests.mjs +20 -25
  122. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
  123. package/src/session-runtime/model-recency.mjs +1 -0
  124. package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
  125. package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
  126. package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
  127. package/src/session-runtime/runtime-review-api.mjs +4 -1
  128. package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
  129. package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
  130. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +44 -20
  131. package/src/session-runtime/services/agent-tool/render.mjs +3 -1
  132. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
  133. package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
  134. package/src/session-runtime/services/provider-admin.mjs +12 -2
  135. package/src/session-runtime/services/worktree-access.mjs +27 -0
  136. package/src/session-runtime/session-hooks.mjs +9 -17
  137. package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
  138. package/src/session-runtime/setup-tool/executor.mjs +4 -0
  139. package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
  140. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
  141. package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
  142. package/src/session-runtime/tool-policy-refresh.mjs +1 -0
  143. package/src/session-runtime/tool-surface.mjs +9 -5
  144. package/src/session-runtime/turn/session-ops.mjs +8 -0
  145. package/src/session-runtime/turn/turn-run.mjs +5 -1
  146. package/src/session-runtime/usage-stats-api.mjs +1 -1
  147. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
  148. package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
  149. package/src/session-runtime/workspace/transition-steps.mjs +1 -18
  150. package/src/session-runtime/workspace/transitions.mjs +279 -67
  151. package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
  152. package/src/standalone/daemon-lag-profiler.mjs +3 -1
  153. package/src/standalone/daemon-session-control.mjs +140 -23
  154. package/src/standalone/daemon-telemetry.mjs +9 -0
  155. package/src/standalone/daemon.mjs +5 -0
  156. package/src/standalone/local-session-runtime.mjs +11 -2
  157. package/src/standalone/session-protocol.mjs +1 -0
  158. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
  159. package/src/standalone/session-service/agent-route-follow.mjs +69 -0
  160. package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
  161. package/src/standalone/session-service.mjs +10 -0
  162. package/src/standalone/session-tool-output.mjs +1 -0
  163. package/src/standalone/staged-import.mjs +47 -0
  164. package/src/tui/App.jsx +2 -0
  165. package/src/tui/app/message-selector.mjs +15 -2
  166. package/src/tui/app/model-options.mjs +16 -2
  167. package/src/tui/app/model-picker.mjs +2 -1
  168. package/src/tui/app/route-pickers.mjs +3 -1
  169. package/src/tui/dist/index.mjs +403 -210
  170. package/src/tui/paste-attachments.mjs +0 -1
  171. package/src/tui/paste-text-policy.mjs +4 -5
  172. package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
  173. package/src/tui/session/notice-surface.mjs +3 -0
  174. package/src/tui/session/session-api/intake/abort.mjs +33 -17
  175. package/src/tui/session/session-api/intake/rewind.mjs +39 -4
  176. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  177. package/src/tui/session/tool-approval.mjs +11 -48
  178. package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
  179. package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
  180. package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
  181. package/src/session-runtime/room-member-gate.mjs +0 -61
  182. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
@@ -17,7 +17,7 @@ import { join } from 'node:path';
17
17
  import { readJsonState, readJsonStateSync, updateJsonAtomic } from '../atomic-file.mjs';
18
18
  import { isPlainObject } from '../object.mjs';
19
19
  import { resolvePluginData } from '../plugin-paths.mjs';
20
- import { normalizeWorktreeEntry, WORKTREE_STORE_DIR } from './contract.mjs';
20
+ import { normalizeWorktreeEntry, WORKTREE_SNAPSHOT_KIND, WORKTREE_STORE_DIR } from './contract.mjs';
21
21
 
22
22
  const REGISTRY_VERSION = 1;
23
23
 
@@ -106,5 +106,7 @@ export function entryView(record) {
106
106
  mergeTarget: record.mergeTarget ?? null,
107
107
  warnings: Array.isArray(record.warnings) ? [...record.warnings] : [],
108
108
  disposable: record.disposable === true,
109
+ // The session whose turn made a snapshot (it is removed when that turn settles).
110
+ ...(record.kind === WORKTREE_SNAPSHOT_KIND ? { holderSessionId: record.holderSessionId ?? null } : {}),
109
111
  };
110
112
  }
@@ -0,0 +1,39 @@
1
+ // Agent tool → session runtime channel for managed-worktree operations. The
2
+ // `git` tool (a builtin with no workspace authority of its own) reaches the
3
+ // runtime that hosts the calling Lead session through the host that runtime
4
+ // registers here; a caller no host claims (a delegated worker, a bare tool
5
+ // run) is no Lead and gets no worktree authority.
6
+ //
7
+ // Dependency-free, and shared through a global symbol like the sidebar host,
8
+ // so every bundle copy of this module sees one registry.
9
+
10
+ const HOSTS_KEY = Symbol.for('mixdog.worktreeToolHosts.v1');
11
+
12
+ function hosts() {
13
+ globalThis[HOSTS_KEY] ??= [];
14
+ return globalThis[HOSTS_KEY];
15
+ }
16
+
17
+ /**
18
+ * Register a runtime's host: `sessionId()` is the Lead session it hosts now
19
+ * (null before one exists); `run(request, context)` performs one operation
20
+ * (session-runtime/workspace/worktree-tool.mjs).
21
+ * @param {{ sessionId: () => string|null, run: (request: object, context: object) => Promise<object> }} host
22
+ * @returns {() => void} Unregister.
23
+ */
24
+ export function registerWorktreeToolHost(host) {
25
+ const entry = { sessionId: host.sessionId, run: host.run };
26
+ hosts().push(entry);
27
+ return () => {
28
+ const list = hosts();
29
+ const index = list.indexOf(entry);
30
+ if (index >= 0) list.splice(index, 1);
31
+ };
32
+ }
33
+
34
+ /** The host of Lead session `sessionId` (the most recently registered), or null. */
35
+ export function worktreeToolHost(sessionId) {
36
+ const id = String(sessionId || '').trim();
37
+ if (!id) return null;
38
+ return hosts().findLast((entry) => entry.sessionId() === id) ?? null;
39
+ }
@@ -4,7 +4,7 @@
4
4
  // binaries and file paths are untrusted input, so no value ever reaches a shell
5
5
  // parser.
6
6
  import { spawn } from 'node:child_process';
7
- import { existsSync, statSync } from 'node:fs';
7
+ import { existsSync, readdirSync, statSync } from 'node:fs';
8
8
  import { delimiter, extname, isAbsolute, join } from 'node:path';
9
9
 
10
10
  const DEFAULT_PROCESS_TIMEOUT_MS = 90_000;
@@ -36,19 +36,52 @@ function isExecutableFile(candidate) {
36
36
  }
37
37
  }
38
38
 
39
- /** First match for `name` on PATH, or null. Pure filesystem lookup, no spawn. */
40
- export function which(name, { env = process.env } = {}) {
39
+ const pathDirs = (env) =>
40
+ String(env.PATH || env.Path || '')
41
+ .split(delimiter)
42
+ .map((value) => value.trim())
43
+ .filter(Boolean);
44
+
45
+ const nameKey = (value) => (process.platform === 'win32' ? value.toLowerCase() : value);
46
+
47
+ /**
48
+ * One PATH's directory listings, read once per directory on first use, for a
49
+ * batch of `which` lookups. Resolving the whole engine catalog probed every
50
+ * PATH dir × PATHEXT extension × engine name with its own statSync (≈17k
51
+ * synchronous stats on a 65-entry PATH); with an index a name that is on no
52
+ * directory costs no filesystem call at all.
53
+ */
54
+ export function createPathIndex(env = process.env) {
55
+ const listings = new Map(); // dir -> Set of entry names, or null (unlistable)
56
+ return {
57
+ dirs: pathDirs(env),
58
+ has(dir, candidate) {
59
+ if (!listings.has(dir)) {
60
+ let names = null;
61
+ try {
62
+ names = new Set(readdirSync(dir).map(nameKey));
63
+ } catch {
64
+ // Unlistable (no list permission, gone): probe it per candidate.
65
+ }
66
+ listings.set(dir, names);
67
+ }
68
+ const names = listings.get(dir);
69
+ return names ? names.has(nameKey(candidate)) : true;
70
+ },
71
+ };
72
+ }
73
+
74
+ /** First match for `name` on PATH, or null. Pure filesystem lookup, no spawn.
75
+ * `index` (createPathIndex over the same env) answers from directory listings. */
76
+ export function which(name, { env = process.env, index = null } = {}) {
41
77
  const bin = String(name || '').trim();
42
78
  if (!bin) return null;
43
79
  if (isAbsolute(bin) || bin.includes('/') || bin.includes('\\')) {
44
80
  return existsSync(bin) ? bin : null;
45
81
  }
46
- const dirs = String(env.PATH || env.Path || '')
47
- .split(delimiter)
48
- .map((value) => value.trim())
49
- .filter(Boolean);
50
- for (const dir of dirs) {
82
+ for (const dir of index ? index.dirs : pathDirs(env)) {
51
83
  for (const candidate of executableCandidates(bin)) {
84
+ if (index && !index.has(dir, candidate)) continue;
52
85
  const full = join(dir, candidate);
53
86
  if (isExecutableFile(full)) return full;
54
87
  }
@@ -7,7 +7,7 @@
7
7
  import { existsSync, readFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import { ENGINE_CATALOG, ENGINE_IDS, engineEntry, enginesForLanguages } from './engines.mjs';
10
- import { mapLimit, runProcess, which } from './process.mjs';
10
+ import { createPathIndex, mapLimit, runProcess, which } from './process.mjs';
11
11
  import { managedEngineBinary, manifestAsset, readEnginesManifest } from './install.mjs';
12
12
 
13
13
  const DOWNLOAD_POLICIES = Object.freeze(['ask', 'auto', 'never']);
@@ -113,9 +113,9 @@ export function projectConfigFileFor(cwd, entry) {
113
113
  return null;
114
114
  }
115
115
 
116
- function pathBinary(entry, env) {
116
+ function pathBinary(entry, env, index) {
117
117
  for (const name of [entry.bin, ...(entry.altBins || [])]) {
118
- const found = which(name, { env });
118
+ const found = which(name, { env, index });
119
119
  if (found) return { path: found, bin: name };
120
120
  }
121
121
  return null;
@@ -135,7 +135,7 @@ function installableFlag(entry, manifest) {
135
135
  return entry.managed && manifestAsset(manifest, entry.id) ? { installable: true } : {};
136
136
  }
137
137
 
138
- function resolveOne({ cwd, entry, config, env, manifest, pluginData }) {
138
+ function resolveOne({ cwd, entry, config, env, manifest, pluginData, pathIndex }) {
139
139
  const base = {
140
140
  id: entry.id,
141
141
  kind: entry.kind,
@@ -159,7 +159,7 @@ function resolveOne({ cwd, entry, config, env, manifest, pluginData }) {
159
159
  return { ...base, source: 'missing', path: '', missing: true };
160
160
  }
161
161
  if (entry.managedModule) {
162
- const host = pathBinary(entry, env);
162
+ const host = pathBinary(entry, env, pathIndex);
163
163
  if (!host) {
164
164
  return {
165
165
  ...base,
@@ -191,7 +191,7 @@ function resolveOne({ cwd, entry, config, env, manifest, pluginData }) {
191
191
  ...installableFlag(entry, manifest),
192
192
  };
193
193
  }
194
- const onPath = pathBinary(entry, env);
194
+ const onPath = pathBinary(entry, env, pathIndex);
195
195
  if (onPath) return { ...base, source: 'path', path: onPath.path, ...commandFor(entry, onPath.path) };
196
196
  if (entry.managed && pluginData) {
197
197
  const managed = managedEngineBinary({ id: entry.id, manifest, pluginData });
@@ -282,9 +282,12 @@ export async function resolveEngines({
282
282
  else if (languages.length > 0) requested = enginesForLanguages(languages);
283
283
  const unknown = engineIds.filter((id) => !ENGINE_IDS.includes(id));
284
284
 
285
+ // One listing per PATH dir for the whole batch: this runs synchronously on
286
+ // the host loop (the desktop's Code Tidy status read at startup).
287
+ const pathIndex = createPathIndex(env);
285
288
  const resolved = requested.map((id) => {
286
289
  const entry = ENGINE_CATALOG[id];
287
- const result = resolveOne({ cwd, entry, config, env, manifest: loadedManifest, pluginData });
290
+ const result = resolveOne({ cwd, entry, config, env, manifest: loadedManifest, pluginData, pathIndex });
288
291
  const configFile = projectConfigFileFor(cwd, entry);
289
292
  if (configFile) result.configFile = configFile;
290
293
  if (entry.requiresConfig && !configFile && result.source !== 'project-config') {
@@ -19,11 +19,10 @@
19
19
  * once; a cold thread picks it up from its stored record.
20
20
  *
21
21
  * A group-room member's task reports (enqueueTask `from` its member session)
22
- * go to the room instead: each line lands in the member's own session (never
23
- * waking it: the room host drives its turns), and a blocked or ended turn
24
- * also lands in the room's log as that member's speech row
25
- * (agent-room-delivery.mjs appendTaskReports), where the room's next exchange
26
- * sees it. Both ride this same durable queue, keyed by those sessions.
22
+ * land in that member session alone, never as rows in the room's log. An
23
+ * ended turn is owed a wake like a home's, but the relay turn runs in the
24
+ * room: the desktop room host (agent-room-host.mjs `relay`) gives the member
25
+ * one turn there to relay the result, with the same batches and retries.
27
26
  */
28
27
  import { randomUUID } from 'node:crypto';
29
28
  import { readFileSync } from 'node:fs';
@@ -42,8 +41,9 @@ import {
42
41
  isRoomThreadSessionId,
43
42
  roomIdOfSession,
44
43
  roomMemberOfSession,
45
- roomSessionId,
46
44
  } from '../runtime/agent/orchestrator/session/agent-room.mjs';
45
+ import { agentRoomHost } from '../runtime/shared/agent-room-host.mjs';
46
+ import { sendDepth } from '../runtime/shared/bot-hops.mjs';
47
47
  import {
48
48
  TASK_LOOP_CAP,
49
49
  agentTaskMessage,
@@ -147,8 +147,6 @@ export const ROUND_TTL_MS = 30 * 24 * 60 * 60 * 1000;
147
147
  // Per home: round-trip targets counted (least recently counted drop first),
148
148
  // reporting turns watched at once, and posted results owed a wake.
149
149
  export const QUEUE_CAPS = Object.freeze({ roundTargets: 100, watches: 200, wakeRunIds: 100 });
150
- // A task report's output kept for its room row (newlines kept).
151
- const ROOM_REPORT_TEXT_LIMIT = 4000;
152
150
  // Waits before the 2nd, 3rd and 4th try of a relay (wake) turn that did not finish.
153
151
  export const WAKE_RETRY_DELAYS_MS = Object.freeze([30_000, 120_000, 600_000]);
154
152
  // Refused wake submissions of one batch, a minute apart, before it is given up.
@@ -356,7 +354,9 @@ export function createRoutineResultQueue(
356
354
  const state = wakeState(doc.wakes, homeId);
357
355
  const index = state.batches.findIndex((batch) => batch.id === batchId);
358
356
  if (index < 0) return undefined;
359
- next = { ...state.batches[index], id: `awake_${randomUUID()}`, tries, notBefore };
357
+ // The new try's turn is its own: it follows no earlier turn.
358
+ const { follows: _follows, ...batch } = state.batches[index];
359
+ next = { ...batch, id: `awake_${randomUUID()}`, tries, notBefore };
360
360
  const batches = state.batches.map((batch, i) => (i === index ? next : batch));
361
361
  return withWakeState(doc, homeId, { ...state, batches });
362
362
  });
@@ -422,11 +422,14 @@ export function createRoutineResultQueue(
422
422
  else {
423
423
  // A task turn's end counts one round trip, once per run id, per home
424
424
  // and target: the user's next message in that home starts over
425
- // (resetRounds), and so does a count idle past ROUND_TTL_MS.
425
+ // (resetRounds), and so does a count idle past ROUND_TTL_MS. Only a
426
+ // send made in a bot-descended turn counts: the user's or a routine's
427
+ // own delegations never use the budget up.
426
428
  const own = freshRounds(doc.rounds[homeId]);
427
- const round = (own[item.taskSessionId]?.n || 0) + 1;
429
+ const counts = sendDepth(item) > 0;
430
+ const round = counts ? (own[item.taskSessionId]?.n || 0) + 1 : 0;
428
431
  next = appended(doc, homeId, [{ ...item, round }]);
429
- if (next) {
432
+ if (next && counts) {
430
433
  const counted = { ...own, [item.taskSessionId]: { n: round, at: now() } };
431
434
  next = { ...next, rounds: { ...doc.rounds, [homeId]: counted } };
432
435
  }
@@ -460,6 +463,16 @@ export function createRoutineResultQueue(
460
463
  async watches() {
461
464
  return Object.values((await read()).watches);
462
465
  },
466
+ /** The watch of `runId` follows the turn submitted as `submissionId`: a
467
+ * room member's one turn answering several sends, or a fresh turn
468
+ * replacing one cut short (recovery reads that turn's end). */
469
+ async followWatch(runId, submissionId) {
470
+ await update((doc) => {
471
+ const watch = doc.watches[runId];
472
+ if (!watch || watch.follows === submissionId) return undefined;
473
+ return { ...doc, watches: { ...doc.watches, [runId]: { ...watch, follows: submissionId } } };
474
+ });
475
+ },
463
476
  /** Drop the watch of `runId` (its turn was never taken). */
464
477
  async unwatch(runId) {
465
478
  await update((doc) => {
@@ -596,36 +609,6 @@ function taskReportItem({ runId, taskSessionId, title, status, text = '', target
596
609
  };
597
610
  }
598
611
 
599
- /** The room-log item of task report `item` by member `agentId` (`text`: the
600
- * report's whole output, newlines kept). */
601
- function roomReportItem(item, agentId, text) {
602
- return {
603
- runId: item.runId,
604
- kind: 'room-report',
605
- speaker: agentId,
606
- taskSessionId: item.taskSessionId,
607
- title: item.title,
608
- status: item.status,
609
- text: String(text ?? '')
610
- .trim()
611
- .slice(0, ROOM_REPORT_TEXT_LIMIT),
612
- ...(item.target ? { target: item.target } : {}),
613
- ...(item.files ? { files: item.files } : {}),
614
- at: item.at,
615
- };
616
- }
617
-
618
- /** Queue a room member's task report: the member's own session (`from`) gets
619
- * the line without a wake; a blocked or ended turn (`toRoom`) is the
620
- * member's row in the room log too. The watch drops with the last write, so
621
- * a restart in between re-queues both (deduplicated). */
622
- async function queueRoomTaskReport(queue, { from, roomLogId, toRoom }, item, agentId, text, unwatch) {
623
- await queue.add(from, { ...item, roundTrip: false }, { unwatch: toRoom ? '' : unwatch });
624
- if (toRoom) {
625
- await queue.add(roomLogId, roomReportItem(item, agentId, text), { unwatch });
626
- }
627
- }
628
-
629
612
  /** Append to `session.messages` the omission notice and the queued `items`
630
613
  * not posted before. `fresh`: the rows newly posted; `toWake`: the run ids
631
614
  * owed a wake (every dequeued task result, posted now or before). */
@@ -659,6 +642,7 @@ function createHomeRelay({
659
642
  queue,
660
643
  saveSession,
661
644
  live,
645
+ roomRelay,
662
646
  busy,
663
647
  later,
664
648
  drain,
@@ -672,14 +656,17 @@ function createHomeRelay({
672
656
  // 'done' or 'failed' once it settled (until the batch is retired).
673
657
  const relays = new Map();
674
658
 
675
- /** How batch `batchId`'s relay turn went: '' when no turn took it, else
659
+ /** How `batch`'s relay turn went: '' when no turn took it, else
676
660
  * 'running', 'done' or 'failed'. A turn an earlier process took decides by
677
- * its recorded end; one with none was cut off by the restart. */
678
- function relayOf(batchId, messages, taken) {
679
- const local = relays.get(batchId);
661
+ * its recorded end; one with none was cut off by the restart. A room
662
+ * member's batch may be answered by a turn under another submission id
663
+ * (`follows`, recorded before that turn was submitted). */
664
+ function relayOf(batch, messages, taken) {
665
+ const local = relays.get(batch.id);
680
666
  if (local) return local;
681
- if (!taken.has(batchId)) return '';
682
- const outcome = submissionOutcome(messages, batchId);
667
+ const turnId = batch.follows || batch.id;
668
+ if (!taken.has(turnId)) return '';
669
+ const outcome = submissionOutcome(messages, turnId);
683
670
  return outcome && outcome !== 'open' && outcome.status === 'completed' ? 'done' : 'failed';
684
671
  }
685
672
 
@@ -726,7 +713,7 @@ function createHomeRelay({
726
713
  for (;;) {
727
714
  batch = (await queue.wakes(homeId)).batches[0] || (await queue.batchWake(homeId));
728
715
  if (!batch) return;
729
- const relay = relayOf(batch.id, session.messages, taken);
716
+ const relay = relayOf(batch, session.messages, taken);
730
717
  // Its settle drains again.
731
718
  if (relay === 'running') return;
732
719
  if (relay) {
@@ -744,15 +731,23 @@ function createHomeRelay({
744
731
  // Its rows are gone with a thread reset.
745
732
  await queue.woke(homeId, batch.id);
746
733
  }
734
+ // A room member relays in its room (the room host runs that turn), any
735
+ // other thread in a hidden turn of its own.
747
736
  const surface = live();
748
- if (typeof surface?.wake !== 'function') return later(homeId, busyRetryMs);
737
+ const wake = roomMemberOfSession(homeId) ? roomRelay : surface?.wake?.bind(surface);
738
+ if (typeof wake !== 'function') return later(homeId, busyRetryMs);
749
739
  const batchId = batch.id;
750
740
  let turn;
751
741
  relays.set(batchId, 'running');
752
742
  try {
753
- // The turn descends from the bot-to-bot sends it reports: it carries their highest hop.
754
- const hop = Math.max(0, ...woken.map((message) => Number(message?.meta?.routine?.hop) || 0));
755
- turn = await surface.wake(homeId, agentTaskWakePrompt(woken, { more }), { id: batchId, hop });
743
+ // The reports answer sends this home made itself: the wake turn resumes
744
+ // at the depth of the turns that sent them (bot-hops.mjs), 0 for work the
745
+ // user or a routine started, so only bot-descended chains keep counting.
746
+ const hop = Math.max(0, ...woken.map((message) => sendDepth(message?.meta?.routine)));
747
+ // A room relay may be answered under another turn's submission id: the
748
+ // batch records it before that turn is submitted.
749
+ const follow = (submissionId) => queue.markBatch(homeId, batchId, { follows: submissionId });
750
+ turn = await wake(homeId, agentTaskWakePrompt(woken, { more }), { id: batchId, hop, follow });
756
751
  if (turn === false) throw new Error('the wake turn was refused');
757
752
  } catch (error) {
758
753
  relays.delete(batchId);
@@ -812,9 +807,10 @@ export function createAgentHomeDelivery({
812
807
  // Refused or failing wake submissions of one batch (missingRetryMs apart)
813
808
  // before it is given up the same way.
814
809
  wakeRefusalLimit = WAKE_REFUSAL_LIMIT,
815
- // Post queued room task reports into a room log (agent-room-delivery.mjs).
816
- postRoomReports = async (roomLogId, items) =>
817
- (await import('./agent-room-delivery.mjs')).appendRoomTaskReports(roomLogId, items),
810
+ // A room member's relay turn (wake of a member session): its turn in the
811
+ // room, through the desktop room host (agent-room-host.mjs `relay`), with
812
+ // the live surface's `wake` contract.
813
+ roomRelay = (sessionId, prompt, options) => agentRoomHost().relay(sessionId, prompt, options),
818
814
  log = (line) => process.stderr.write(`mixdog routines: ${line}\n`),
819
815
  }) {
820
816
  const busy = (sessionId) => {
@@ -834,6 +830,7 @@ export function createAgentHomeDelivery({
834
830
  queue,
835
831
  saveSession,
836
832
  live,
833
+ roomRelay,
837
834
  busy,
838
835
  later,
839
836
  drain,
@@ -874,6 +871,13 @@ export function createAgentHomeDelivery({
874
871
  return true;
875
872
  }
876
873
 
874
+ /** A room log's queued rows: task reports go to member sessions now, so what
875
+ * an earlier version left under the log's id is dropped, never posted. */
876
+ async function dropRoomLogQueue(logId, items, omitted) {
877
+ log(`${items.length} legacy task report(s) queued for the room log ${logId} are dropped`);
878
+ await dropQueued(queue, logId, items, omitted);
879
+ }
880
+
877
881
  /** A missing home of an agent that can never have one (no bot): its queued
878
882
  * rows are dropped with a log line, never retried. */
879
883
  async function dropHomeless(homeId, items, omitted) {
@@ -884,34 +888,13 @@ export function createAgentHomeDelivery({
884
888
  return true;
885
889
  }
886
890
 
887
- /** Post every queued task report of the room log `roomLogId` as member rows. */
888
- async function drainRoomLog(roomLogId) {
889
- const items = await queue.items(roomLogId);
890
- const omitted = await queue.omitted(roomLogId);
891
- if (!items.length && !omitted) return;
892
- const session = loadSession(roomLogId);
893
- if (!session || session.closed === true) {
894
- if (await dropGoneRoomThread(roomLogId, items, omitted)) return;
895
- log(`${items.length} task report(s) wait for the room log ${roomLogId}`);
896
- return later(roomLogId, missingRetryMs);
897
- }
898
- // A room log has no notice row: the dropped count is only logged.
899
- if (omitted) log(`${omitted.count} older task report(s) for ${roomLogId} were dropped: its queue was full`);
900
- if (items.length) await postRoomReports(roomLogId, items);
901
- await queue.remove(
902
- roomLogId,
903
- items.map((item) => item.runId),
904
- omitted
905
- );
906
- }
907
-
908
891
  /** Post every queued result of `homeId` if its thread is idle; else retry later. */
909
892
  async function drainOnce(homeId) {
910
- if (roomIdOfSession(homeId)) return drainRoomLog(homeId);
911
893
  const items = await queue.items(homeId);
912
894
  const omitted = await queue.omitted(homeId);
913
895
  const owed = owesWake(await queue.wakes(homeId));
914
896
  if (!items.length && !omitted && !owed) return;
897
+ if (roomIdOfSession(homeId)) return dropRoomLogQueue(homeId, items, omitted);
915
898
  const agentId = agentIdOfHomeSession(homeId);
916
899
  if (agentId && agentId !== MIXY_AGENT_ID && agentPresence(agentId) === 'deleted') {
917
900
  // The owner is gone (its home archived): Mixy, the default owner, gets them.
@@ -1014,23 +997,16 @@ export function createAgentHomeDelivery({
1014
997
  * create) for its home thread, then start draining it. `status` 'started'
1015
998
  * and 'blocked' only post a line; a turn's end also wakes the agent. */
1016
999
  async function enqueueTask(agentId, report) {
1017
- const { runId, taskSessionId, status, text = '', watchId = '', from = '' } = report;
1000
+ const { runId, taskSessionId, watchId = '', from = '' } = report;
1018
1001
  const homeId = agentHomeSessionId(agentId) || agentHomeSessionId(MIXY_AGENT_ID);
1019
1002
  if (!runId || !taskSessionId) throw new Error('task report: runId and taskSessionId required');
1020
1003
  const item = taskReportItem(report);
1021
1004
  const unwatch = watchId ? String(watchId) : '';
1022
- const member = roomMemberOfSession(from);
1023
- if (member?.agentId === agentId) {
1024
- const roomLogId = roomSessionId(member.roomId);
1025
- const toRoom = status !== 'started';
1026
- await queueRoomTaskReport(queue, { from, roomLogId, toRoom }, item, agentId, text, unwatch);
1027
- void drain(from);
1028
- if (toRoom) void drain(roomLogId);
1029
- return toRoom ? roomLogId : from;
1030
- }
1031
- await queue.add(homeId, item, { unwatch });
1032
- void drain(homeId);
1033
- return homeId;
1005
+ // A room member's own work reports to its member session, never its home.
1006
+ const target = roomMemberOfSession(from)?.agentId === agentId ? from : homeId;
1007
+ await queue.add(target, item, { unwatch });
1008
+ void drain(target);
1009
+ return target;
1034
1010
  }
1035
1011
 
1036
1012
  /** Show `run`'s live-only row (routineRunNotice: `running`, or a result whose
@@ -1083,6 +1059,7 @@ export function createAgentHomeDelivery({
1083
1059
  recover,
1084
1060
  watchTask,
1085
1061
  unwatchTask: (runId) => queue.unwatch(runId),
1062
+ followTask: (runId, submissionId) => queue.followWatch(runId, submissionId),
1086
1063
  taskWatches: () => queue.watches(),
1087
1064
  resetRounds: (homeId) => queue.resetRounds(homeId),
1088
1065
  };
@@ -1166,6 +1143,11 @@ export async function unwatchAgentTask(runId) {
1166
1143
  return (await processDelivery()).unwatchTask(runId);
1167
1144
  }
1168
1145
 
1146
+ /** One reporting turn's watch follows the turn submitted as `submissionId`. */
1147
+ export async function followAgentTask(runId, submissionId) {
1148
+ return (await processDelivery()).followTask(runId, submissionId);
1149
+ }
1150
+
1169
1151
  /** A user's own message in `homeId`: its round trips with every target restart. */
1170
1152
  export async function resetAgentTaskRounds(homeId) {
1171
1153
  return (await processDelivery()).resetRounds(homeId);
@@ -14,6 +14,22 @@ export function subscribeAgentProfileChanges(listener) {
14
14
  return { unsubscribe: () => listeners.delete(listener) };
15
15
  }
16
16
 
17
+ // A bot's saved model route: the hosts of its chats (the session service)
18
+ // move them onto it from their next turn.
19
+ const routeListeners = new Set();
20
+
21
+ export function subscribeAgentRouteChanges(listener) {
22
+ routeListeners.add(listener);
23
+ return { unsubscribe: () => routeListeners.delete(listener) };
24
+ }
25
+
26
+ /** Settles once every host has taken the route; one failing host never fails the save. */
27
+ export async function publishAgentRouteChange(agentId, route) {
28
+ await Promise.allSettled(
29
+ [...routeListeners].map((listener) => Promise.resolve().then(() => listener(agentId, route)))
30
+ );
31
+ }
32
+
17
33
  /** Settles once every runtime has applied the edit; one failing runtime never fails the save. */
18
34
  export async function publishAgentProfileChange() {
19
35
  invalidateRoleInstructionsCache();
@@ -6,15 +6,8 @@
6
6
  * message id, so a retried post adds nothing. A user row's files are taken in
7
7
  * here exactly as a chat prompt's are at submit (session-turns.mjs): the bytes
8
8
  * go to the attachment store once, and the row keeps their references.
9
- *
10
- * A task session a member opened (session create, agent-task-sessions.mjs)
11
- * reports here too: each blocked or ended turn becomes that member's speech
12
- * row (appendTaskReports), queued durably by agent-home-delivery.mjs. Posts
13
- * into one log run one at a time, so the host's rows and the reports never
14
- * overwrite each other.
9
+ * Posts into one log run one at a time, so no two posts overwrite each other.
15
10
  */
16
- import { MIXY_AGENT_ID } from '../runtime/agent/orchestrator/session/agent-home.mjs';
17
- import { mixyDisplayName } from '../runtime/agent/orchestrator/session/mixy-name.mjs';
18
11
  import {
19
12
  normalizeRoomRow,
20
13
  roomIdOfSession,
@@ -97,42 +90,6 @@ function storedFileFields(files) {
97
90
  };
98
91
  }
99
92
 
100
- const inline = (value) =>
101
- String(value ?? '')
102
- .replace(/[`*\r\n]+/g, ' ')
103
- .trim();
104
-
105
- /** The room row of one task report `item` (agent-home-delivery.mjs
106
- * enqueueTask): the member's speech naming the session, then its output
107
- * and files. The row id derives from the run id, so a replay adds nothing. */
108
- export function taskReportRow(item, name = '') {
109
- const what = item.target === 'session' ? 'Session' : 'Task session';
110
- const head = `${what} **${inline(item.title) || item.taskSessionId}** (\`${inline(item.taskSessionId)}\`)`;
111
- const text =
112
- item.status === 'blocked'
113
- ? `${head} is waiting for the user's answer or approval in that session.`
114
- : [`${head} ${item.status}.`, String(item.text ?? '').trim()].filter(Boolean).join('\n\n');
115
- return {
116
- id: `report_${String(item.runId).replace(/[^A-Za-z0-9_-]/g, '_')}`.slice(0, 120),
117
- kind: 'speech',
118
- speaker: item.speaker,
119
- ...(name ? { name } : {}),
120
- at: Number.isFinite(item.at) ? item.at : Date.now(),
121
- text,
122
- ...(item.status === 'blocked' ? {} : storedFileFields(item.files)),
123
- };
124
- }
125
-
126
- /** The name `agentId` last spoke under in a room log; Mixy's display name, else ''. */
127
- function speakerName(session, agentId) {
128
- const messages = Array.isArray(session?.messages) ? session.messages : [];
129
- for (let index = messages.length - 1; index >= 0; index -= 1) {
130
- const room = messages[index]?.meta?.room;
131
- if (room?.speaker === agentId && room.name) return room.name;
132
- }
133
- return agentId === MIXY_AGENT_ID ? mixyDisplayName() : '';
134
- }
135
-
136
93
  export function createAgentRoomDelivery({ loadSession, saveSession, live = agentHomeLiveSurface, log = () => {} }) {
137
94
  const tails = new Map(); // log session → its last post
138
95
  /** Run `post` after every earlier post into `sessionId`. */
@@ -193,20 +150,7 @@ export function createAgentRoomDelivery({ loadSession, saveSession, live = agent
193
150
  return commit(sessionId, () => prepared);
194
151
  });
195
152
  }
196
-
197
- /** Post task reports (agent-home-delivery.mjs room items) into the log of
198
- * `sessionId`, each as its member's speech row. */
199
- async function appendTaskReports(sessionId, items) {
200
- if (!roomIdOfSession(sessionId)) throw new TypeError('not a group-room session');
201
- return serialized(sessionId, () =>
202
- commit(sessionId, (session) =>
203
- (Array.isArray(items) ? items : []).map((item) =>
204
- normalizeRoomRow(taskReportRow(item, speakerName(session, item.speaker)))
205
- )
206
- )
207
- );
208
- }
209
- return { append, appendTaskReports };
153
+ return { append };
210
154
  }
211
155
 
212
156
  let processDelivery = null;
@@ -233,8 +177,3 @@ function roomDelivery() {
233
177
  export async function appendRoomRows(sessionId, rows) {
234
178
  return (await roomDelivery()).append(sessionId, rows);
235
179
  }
236
-
237
- /** Post queued task reports into the room log `sessionId` (agent-home-delivery.mjs). */
238
- export async function appendRoomTaskReports(sessionId, items) {
239
- return (await roomDelivery()).appendTaskReports(sessionId, items);
240
- }