mixdog 0.9.166 → 0.9.167

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 (221) hide show
  1. package/package.json +1 -1
  2. package/scripts/audit-usage-ledger.mjs +86 -0
  3. package/scripts/generate-voice-runtime-manifest.mjs +22 -17
  4. package/scripts/memory-chunk-quality.mjs +128 -0
  5. package/scripts/memory-layer2-check.mjs +133 -0
  6. package/scripts/model-catalog-audit.mjs +6 -0
  7. package/scripts/native-tool-download.mjs +14 -0
  8. package/scripts/prepare-native-assets.mjs +2 -7
  9. package/scripts/tool-failures.mjs +7 -2
  10. package/scripts/verify-release-assets.mjs +48 -90
  11. package/src/defaults/skills/browser-use/SKILL.md +33 -18
  12. package/src/defaults/skills/computer-use/SKILL.md +16 -8
  13. package/src/lib/keychain-cjs.cjs +0 -1
  14. package/src/lib/rules-builder.cjs +4 -1
  15. package/src/rules/agent/40-cycle1-agent.md +21 -13
  16. package/src/rules/lead/01-general.md +8 -3
  17. package/src/rules/shared/10-tool-workflow.md +20 -11
  18. package/src/rules/shared/30-exploration.md +11 -4
  19. package/src/rules/shared/40-editing.md +2 -3
  20. package/src/rules/shared/50-execution.md +6 -3
  21. package/src/rules/shared/60-verification.md +3 -2
  22. package/src/rules/shared/70-delivery.md +0 -4
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +51 -45
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +3 -4
  25. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -7
  26. package/src/runtime/agent/orchestrator/agent-trace.mjs +9 -0
  27. package/src/runtime/agent/orchestrator/config.mjs +9 -4
  28. package/src/runtime/agent/orchestrator/context/collect.mjs +25 -1055
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +272 -0
  30. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +296 -0
  31. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +523 -0
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +7 -2
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +13 -14
  34. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +214 -0
  35. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +220 -116
  36. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +74 -44
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +191 -29
  38. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +13 -12
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +1 -9
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +5 -2
  41. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +48 -25
  42. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +28 -2
  43. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -1
  44. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -8
  45. package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +32 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +21 -6
  47. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +61 -13
  48. package/src/runtime/agent/orchestrator/providers/model-catalog-projection.mjs +6 -1
  49. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +123 -118
  50. package/src/runtime/agent/orchestrator/providers/model-pricing-rates.mjs +54 -0
  51. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +5 -2
  52. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +34 -3
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-endpoints.mjs +21 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +154 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +322 -0
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +243 -702
  58. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +2 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-startup-prewarm.mjs +221 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +19 -0
  63. package/src/runtime/agent/orchestrator/providers/provider-model-identities.mjs +40 -0
  64. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +120 -710
  65. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +36 -15
  66. package/src/runtime/agent/orchestrator/session/cache/util.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +17 -4
  68. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +36 -31
  69. package/src/runtime/agent/orchestrator/session/compact.mjs +4 -5
  70. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +6 -5
  71. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -6
  72. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +121 -0
  73. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +2 -3
  74. package/src/runtime/agent/orchestrator/session/loop/diagnostics.mjs +74 -0
  75. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +44 -17
  76. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +266 -0
  77. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +111 -0
  78. package/src/runtime/agent/orchestrator/session/loop/request-projection.mjs +99 -0
  79. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +91 -0
  80. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +36 -0
  81. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +3 -3
  82. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  83. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -11
  84. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +3 -5
  85. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -3
  86. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +39 -0
  87. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +3 -2
  88. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +21 -3
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +62 -10
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +17 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +61 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +13 -9
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +12 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +3 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +8 -3
  99. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +69 -6
  100. package/src/runtime/agent/orchestrator/tools/builtin.mjs +14 -17
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  102. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  104. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  105. package/src/runtime/bridge-discovery.mjs +8 -0
  106. package/src/runtime/browser-bridge/action-schema.mjs +37 -43
  107. package/src/runtime/browser-bridge/client.mjs +111 -99
  108. package/src/runtime/browser-bridge/input-fields.mjs +187 -0
  109. package/src/runtime/browser-bridge/tool-defs.mjs +12 -227
  110. package/src/runtime/computer-bridge/action-schema.mjs +2 -46
  111. package/src/runtime/computer-bridge/client.mjs +69 -77
  112. package/src/runtime/computer-bridge/core-actions.mjs +7 -5
  113. package/src/runtime/computer-bridge/error-recovery.mjs +41 -37
  114. package/src/runtime/computer-bridge/tool-defs.mjs +1 -1
  115. package/src/runtime/github/commands.mjs +4 -1
  116. package/src/runtime/github/contract.mjs +1 -1
  117. package/src/runtime/github/tool.mjs +2 -2
  118. package/src/runtime/media/adapters/antigravity-image.mjs +72 -0
  119. package/src/runtime/media/adapters/gemini-image.mjs +20 -11
  120. package/src/runtime/media/auth.mjs +12 -0
  121. package/src/runtime/media/catalog.mjs +30 -10
  122. package/src/runtime/media/jobs.mjs +4 -0
  123. package/src/runtime/media/lanes.mjs +40 -31
  124. package/src/runtime/memory/index.mjs +2 -9
  125. package/src/runtime/memory/lib/compact-handoff.mjs +26 -18
  126. package/src/runtime/memory/lib/memory-chunk-audit.mjs +41 -0
  127. package/src/runtime/memory/lib/memory-chunk-quality.mjs +388 -0
  128. package/src/runtime/memory/lib/memory-cycle1.mjs +95 -394
  129. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +29 -4
  130. package/src/runtime/memory/lib/memory-recall-store.mjs +8 -12
  131. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -11
  132. package/src/runtime/memory/lib/memory.mjs +2 -0
  133. package/src/runtime/memory/lib/query-handlers.mjs +98 -144
  134. package/src/runtime/memory/lib/query-ranking.mjs +23 -9
  135. package/src/runtime/memory/lib/recall-format.mjs +37 -27
  136. package/src/runtime/memory/lib/session-ingest-runtime.mjs +14 -8
  137. package/src/runtime/memory/lib/session-ingest.mjs +45 -84
  138. package/src/runtime/memory/lib/transcript-ingest.mjs +19 -21
  139. package/src/runtime/memory/tool-defs.mjs +2 -2
  140. package/src/runtime/office/authoring/pptx-author-session.mjs +3 -7
  141. package/src/runtime/office/core/office-actions-batch.mjs +2 -8
  142. package/src/runtime/office/core/office-actions-open.mjs +124 -0
  143. package/src/runtime/office/core/office-actions-read.mjs +127 -0
  144. package/src/runtime/office/core/office-core.mjs +80 -1
  145. package/src/runtime/office/core/office-sessions.mjs +116 -138
  146. package/src/runtime/office/index.mjs +18 -271
  147. package/src/runtime/office/pdf/pdf-analysis.mjs +2 -2
  148. package/src/runtime/office/pdf/pdf-limits.mjs +2 -0
  149. package/src/runtime/office/tool-defs.mjs +7 -6
  150. package/src/runtime/shared/agent-route-config.mjs +8 -8
  151. package/src/runtime/shared/child-spawn-gate.mjs +10 -13
  152. package/src/runtime/shared/config.mjs +2 -7
  153. package/src/runtime/shared/env.mjs +3 -3
  154. package/src/runtime/shared/err-text.mjs +1 -1
  155. package/src/runtime/shared/extension-scopes.mjs +3 -2
  156. package/src/runtime/shared/heap-cap.mjs +10 -6
  157. package/src/runtime/shared/llm/cost.mjs +35 -10
  158. package/src/runtime/shared/llm/usage-accounting.mjs +11 -1
  159. package/src/runtime/shared/llm/usage-context.mjs +6 -0
  160. package/src/runtime/shared/llm/usage-ledger-import.mjs +5 -0
  161. package/src/runtime/shared/llm/usage-ledger-repair.mjs +109 -0
  162. package/src/runtime/shared/llm/usage-ledger.mjs +59 -20
  163. package/src/runtime/shared/llm/usage-pricing-refresh.mjs +26 -0
  164. package/src/runtime/shared/llm/usage-rollup.mjs +17 -15
  165. package/src/runtime/shared/native-asset.mjs +3 -4
  166. package/src/runtime/shared/object.mjs +4 -0
  167. package/src/runtime/shared/pg-schema-db.mjs +21 -0
  168. package/src/runtime/shared/plugin-metadata.mjs +3 -1
  169. package/src/runtime/shared/resource-admission.mjs +1 -6
  170. package/src/runtime/shared/schedules-db.mjs +8 -24
  171. package/src/runtime/shared/schema-value-error.d.mts +2 -0
  172. package/src/runtime/shared/schema-value-error.mjs +68 -0
  173. package/src/runtime/shared/singleton-owner.mjs +11 -24
  174. package/src/runtime/shared/staged-update.mjs +1 -10
  175. package/src/runtime/shared/tool-card-model.mjs +8 -5
  176. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  177. package/src/runtime/shared/tool-surface.mjs +1 -5
  178. package/src/runtime/shared/user-data-guard.mjs +1 -4
  179. package/src/runtime/shared/webhooks-db.mjs +8 -24
  180. package/src/runtime/web-search/index.mjs +32 -8
  181. package/src/runtime/web-search/tool-defs.mjs +4 -4
  182. package/src/session-runtime/config-lifecycle.mjs +34 -0
  183. package/src/session-runtime/context-status.mjs +19 -0
  184. package/src/session-runtime/cwd-plugins.mjs +12 -29
  185. package/src/session-runtime/goal-tasks.mjs +2 -2
  186. package/src/session-runtime/goal-tool-defs.mjs +3 -2
  187. package/src/session-runtime/internal-tool-executor.mjs +7 -0
  188. package/src/session-runtime/lifecycle-api.mjs +5 -11
  189. package/src/session-runtime/notification-bus.mjs +41 -0
  190. package/src/session-runtime/prewarm.mjs +22 -0
  191. package/src/session-runtime/runtime-bootstrap.mjs +90 -0
  192. package/src/session-runtime/runtime-core.mjs +88 -202
  193. package/src/session-runtime/runtime-feature-gates.mjs +37 -0
  194. package/src/session-runtime/session-lifecycle.mjs +5 -5
  195. package/src/session-runtime/session-turn-api.mjs +1 -12
  196. package/src/session-runtime/setup-tool/executor.mjs +13 -5
  197. package/src/session-runtime/setup-tool/tool-defs.mjs +52 -46
  198. package/src/session-runtime/tool-defs.mjs +73 -4
  199. package/src/session-runtime/usage-stats-api.mjs +4 -1
  200. package/src/session-runtime/warmup-schedulers.mjs +8 -2
  201. package/src/session-runtime/workflow.mjs +14 -0
  202. package/src/standalone/channel-session-router.mjs +3 -1
  203. package/src/standalone/channel-worker.mjs +18 -9
  204. package/src/standalone/daemon-crash-capture.mjs +534 -0
  205. package/src/standalone/daemon-telemetry.mjs +175 -0
  206. package/src/standalone/daemon.mjs +31 -21
  207. package/src/standalone/session-client.mjs +31 -23
  208. package/src/standalone/usage-dashboard-model.mjs +2 -0
  209. package/src/standalone/usage-stats-hours.mjs +18 -11
  210. package/src/standalone/usage-stats-model.mjs +39 -28
  211. package/src/standalone/usage-stats-period.mjs +43 -45
  212. package/src/tui/app/use-transcript-window.mjs +1 -1
  213. package/src/tui/components/StatusLine.jsx +13 -42
  214. package/src/tui/dist/index.mjs +256 -239
  215. package/src/tui/session/session-api-ext.mjs +44 -41
  216. package/src/tui/session/session-api.mjs +4 -0
  217. package/src/tui/session/tool-card-results.mjs +25 -78
  218. package/src/tui/session/tool-result-status.mjs +94 -1
  219. package/src/tui/session/turn-aggregate-cards.mjs +17 -54
  220. package/src/ui/statusline-format.mjs +6 -26
  221. package/src/workflows/default/WORKFLOW.md +5 -0
@@ -0,0 +1,68 @@
1
+ // JSON Schema counts Unicode code points, not JavaScript's UTF-16 code units.
2
+ export function schemaStringLength(value) {
3
+ let length = 0;
4
+ for (const _ of value) length++;
5
+ return length;
6
+ }
7
+
8
+ // Shared schema checks; tools retain their action-specific target relationships.
9
+ export function schemaValueError(value, schema, path) {
10
+ if (schema.enum && !schema.enum.includes(value)) {
11
+ return `${path} must be one of: ${schema.enum.join(', ')}`;
12
+ }
13
+ if (schema.type === 'string') {
14
+ if (typeof value !== 'string') return `${path} must be a string`;
15
+ const length = schemaStringLength(value);
16
+ if (schema.minLength !== undefined && length < schema.minLength) {
17
+ return `${path} requires at least ${schema.minLength} characters`;
18
+ }
19
+ if (schema.maxLength !== undefined && length > schema.maxLength) {
20
+ return `${path} accepts at most ${schema.maxLength} characters`;
21
+ }
22
+ if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) {
23
+ return `${path} must match the required format`;
24
+ }
25
+ }
26
+ if (schema.type === 'boolean' && typeof value !== 'boolean') return `${path} must be a boolean`;
27
+ if (schema.type === 'number' && (typeof value !== 'number' || !Number.isFinite(value))) {
28
+ return `${path} must be a finite number`;
29
+ }
30
+ if (schema.type === 'integer' && !Number.isInteger(value)) return `${path} must be an integer`;
31
+ if (schema.type === 'array') {
32
+ if (!Array.isArray(value)) return `${path} must be an array`;
33
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
34
+ return `${path} requires at least ${schema.minItems} items`;
35
+ }
36
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
37
+ return `${path} accepts at most ${schema.maxItems} items`;
38
+ }
39
+ if (schema.uniqueItems && new Set(value.map((item) => JSON.stringify(item))).size !== value.length) {
40
+ return `${path} must contain unique items`;
41
+ }
42
+ for (let index = 0; index < value.length; index += 1) {
43
+ const error = schemaValueError(value[index], schema.items || {}, `${path}[${index}]`);
44
+ if (error) return error;
45
+ }
46
+ }
47
+ if (schema.type === 'object') {
48
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return `${path} must be an object`;
49
+ const properties = schema.properties || {};
50
+ const missing = (schema.required || []).filter((name) => !Object.hasOwn(value, name));
51
+ if (missing.length) return `${path} requires field(s): ${missing.join(', ')}`;
52
+ if (schema.additionalProperties === false) {
53
+ const extras = Object.keys(value).filter((name) => !Object.hasOwn(properties, name));
54
+ if (extras.length) return `${path} does not accept field(s): ${extras.join(', ')}`;
55
+ }
56
+ for (const [name, item] of Object.entries(value)) {
57
+ const field = Object.hasOwn(properties, name) ? properties[name]
58
+ : (typeof schema.additionalProperties === 'object' ? schema.additionalProperties : {});
59
+ const error = schemaValueError(item, field, `${path}.${name}`);
60
+ if (error) return error;
61
+ }
62
+ }
63
+ if (typeof value === 'number') {
64
+ if (schema.minimum !== undefined && value < schema.minimum) return `${path} must be at least ${schema.minimum}`;
65
+ if (schema.maximum !== undefined && value > schema.maximum) return `${path} must be at most ${schema.maximum}`;
66
+ }
67
+ return null;
68
+ }
@@ -1,15 +1,8 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
+ import { readJsonSafe } from './json-file.mjs';
3
4
  import { isPidAlive } from './pid-liveness.mjs';
4
-
5
- function readJson(path) {
6
- try {
7
- if (!existsSync(path)) return null;
8
- return JSON.parse(readFileSync(path, 'utf8'));
9
- } catch {
10
- return null;
11
- }
12
- }
5
+ import { sleepSync } from './sleep.mjs';
13
6
 
14
7
  function writeJsonAtomic(path, value) {
15
8
  mkdirSync(dirname(path), { recursive: true });
@@ -18,12 +11,6 @@ function writeJsonAtomic(path, value) {
18
11
  renameSync(tmp, path);
19
12
  }
20
13
 
21
- function waitSync(ms) {
22
- try {
23
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
24
- } catch {}
25
- }
26
-
27
14
  function acquireClaimLock(path) {
28
15
  const lockPath = `${path}.claim.lock`;
29
16
  for (let i = 0; i < 20; i++) {
@@ -39,14 +26,14 @@ function acquireClaimLock(path) {
39
26
  continue;
40
27
  }
41
28
  } catch {}
42
- waitSync(25);
29
+ sleepSync(25);
43
30
  }
44
31
  }
45
32
  return null;
46
33
  }
47
34
 
48
35
  export function readSingletonOwner(path) {
49
- const owner = readJson(path);
36
+ const owner = readJsonSafe(path);
50
37
  if (!owner || typeof owner !== 'object') return { owner: null, alive: false };
51
38
  return { owner, alive: isPidAlive(owner.pid) };
52
39
  }
@@ -63,15 +50,15 @@ export function claimSingletonOwner(path, {
63
50
  ...meta,
64
51
  };
65
52
 
66
- const current = readJson(path);
53
+ const current = readJsonSafe(path);
67
54
  if (current?.pid && Number(current.pid) !== Number(pid) && isPidAlive(current.pid)) {
68
55
  return { owned: false, owner: current };
69
56
  }
70
57
 
71
58
  const lockPath = acquireClaimLock(path);
72
- if (!lockPath) return { owned: false, owner: readJson(path) };
59
+ if (!lockPath) return { owned: false, owner: readJsonSafe(path) };
73
60
  try {
74
- const lockedCurrent = readJson(path);
61
+ const lockedCurrent = readJsonSafe(path);
75
62
  if (lockedCurrent?.pid && Number(lockedCurrent.pid) !== Number(pid) && isPidAlive(lockedCurrent.pid)) {
76
63
  return { owned: false, owner: lockedCurrent };
77
64
  }
@@ -83,7 +70,7 @@ export function claimSingletonOwner(path, {
83
70
  }
84
71
 
85
72
  export function releaseSingletonOwner(path, pid = process.pid) {
86
- const current = readJson(path);
73
+ const current = readJsonSafe(path);
87
74
  if (!current?.pid || Number(current.pid) !== Number(pid)) return false;
88
75
  try {
89
76
  rmSync(path, { force: true });
@@ -101,9 +88,9 @@ export function releaseSingletonOwner(path, pid = process.pid) {
101
88
  // should fall back to the winner rather than overwrite it.
102
89
  export function handoffSingletonOwner(path, fromPid, toOwner = {}) {
103
90
  const lockPath = acquireClaimLock(path);
104
- if (!lockPath) return { owned: false, owner: readJson(path) };
91
+ if (!lockPath) return { owned: false, owner: readJsonSafe(path) };
105
92
  try {
106
- const current = readJson(path);
93
+ const current = readJsonSafe(path);
107
94
  if (current?.pid && Number(current.pid) !== Number(fromPid)) {
108
95
  return { owned: false, owner: current };
109
96
  }
@@ -38,6 +38,7 @@ import { spawn } from 'node:child_process';
38
38
  import { resolvePluginData } from './plugin-paths.mjs';
39
39
  // Same probe every discovery/lock reader uses; `process.pid` is trivially alive.
40
40
  import { isPidAlive as pidAlive } from './pid-liveness.mjs';
41
+ import { sleepSync } from './sleep.mjs';
41
42
  import { detachedSpawnOpts, hiddenSpawnOpts } from './spawn-flags.mjs';
42
43
  import { renameWithRetrySync } from './atomic-file.mjs';
43
44
  import {
@@ -69,16 +70,6 @@ function rmDir(dir) {
69
70
  try { rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
70
71
  }
71
72
 
72
- function sleepSync(ms) {
73
- const dur = Math.max(1, Number(ms) || 1);
74
- try {
75
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, dur);
76
- } catch {
77
- const end = Date.now() + dur;
78
- while (Date.now() < end) { /* spin fallback */ }
79
- }
80
- }
81
-
82
73
  // ── Live-session refcount (pid files) ─────────────────────────────────────
83
74
  // Each runtime registers a `<pid>.pid` file on boot and removes it on close.
84
75
  // The swap consults these to skip while another mixdog is running. Liveness is
@@ -21,6 +21,7 @@ import {
21
21
  summarizeToolResult,
22
22
  toolLoadingTargets,
23
23
  } from './tool-surface.mjs';
24
+ import { pluralize } from './tool-primitives.mjs';
24
25
  import { backgroundTaskFailureStatusLabel, isBackgroundErrorOnlyBody } from './err-text.mjs';
25
26
  import { normalizeToolTerminalStatus, toolResultTerminalStatus } from './tool-status.mjs';
26
27
  import { formatElapsed } from './time-format.mjs';
@@ -67,9 +68,7 @@ export function normalizeCountMap(value = {}) {
67
68
  return out;
68
69
  }
69
70
 
70
- export function plural(count, singular, pluralText = `${singular}s`) {
71
- return count === 1 ? singular : pluralText;
72
- }
71
+ export { pluralize as plural };
73
72
 
74
73
  export function shellResultStatus(value) {
75
74
  const match = String(value || '').match(/(?:^|\b)status:\s*(running|pending|queued|completed|failed|cancelled|canceled)\b/im);
@@ -188,7 +187,7 @@ export function shellDisplayStatus({ pending = false, failedCount = 0, exitFaile
188
187
 
189
188
  export function shellHeader(status, count = 1, verifying = false) {
190
189
  const n = Math.max(1, Number(count) || 1);
191
- const object = `${n} ${plural(n, 'command')}`;
190
+ const object = `${n} ${pluralize(n, 'command')}`;
192
191
  // A shell that follows an edit in the same batch IS the verification —
193
192
  // label it as such while healthy; failures keep the neutral Ran + detail.
194
193
  if (verifying && status === 'running') return `Verifying ${object}`;
@@ -205,8 +204,12 @@ export const SKILL_SURFACE_NAMES = new Set([
205
204
  'skill', 'skill_execute', 'skill_view', 'skills_list', 'use_skill',
206
205
  ]);
207
206
 
207
+ const BACKGROUND_TASK_TOOL_NAMES = new Set([
208
+ 'web_search', 'shell', 'bash', 'bash_session', 'shell_command', 'task',
209
+ ]);
210
+
208
211
  export function isBackgroundTaskTool(normalizedName) {
209
- return new Set(['web_search', 'shell', 'bash', 'bash_session', 'shell_command', 'task']).has(String(normalizedName || '').toLowerCase());
212
+ return BACKGROUND_TASK_TOOL_NAMES.has(String(normalizedName || '').toLowerCase());
210
213
  }
211
214
 
212
215
  const AGENT_DISPLAY_NAMES = new Map([
@@ -1,7 +1,7 @@
1
1
  import { clean } from './clean.mjs';
2
2
 
3
3
  export const TOOL_SYNC_EXECUTION_CONTRACT =
4
- 'Runs synchronously in this tool call.';
4
+ 'Returns final results in this call.';
5
5
 
6
6
  export const TOOL_ASYNC_EXECUTION_CONTRACT =
7
7
  'Runs sync inline by default; async returns a background task_id and delivers a completion notification.';
@@ -493,11 +493,7 @@ const TOOL_CATEGORY = new Map([
493
493
  export function classifyToolCategory(name, args = {}) {
494
494
  if (isExternalMcpToolName(name)) return 'MCP';
495
495
  const normalized = normalizeToolName(name);
496
- if (normalized === 'code_graph') {
497
- const mode = String(args.mode || args.action || '').toLowerCase();
498
- if (mode === 'prewarm' || mode === 'index' || mode === 'build' || mode === 'refresh') return 'Setup';
499
- return (mode === 'search' || mode === 'find_symbol' || mode === 'references' || mode === 'callers' || mode === 'callees') ? 'Search' : 'Read';
500
- }
496
+ if (normalized === 'code_graph') return codeGraphLabel(args);
501
497
  return TOOL_CATEGORY.get(normalized) || 'Other';
502
498
  }
503
499
 
@@ -18,6 +18,7 @@ import {
18
18
  import { dirname, join } from 'path';
19
19
  import { homedir } from 'os';
20
20
  import { createHash } from 'crypto';
21
+ import { isPlainObject } from './object.mjs';
21
22
 
22
23
  function mixdogConfigBaseDir() {
23
24
  return process.env.MIXDOG_CONFIG_DIR || join(homedir(), '.mixdog');
@@ -53,10 +54,6 @@ function initMarkerPath(dataDir) {
53
54
  return join(getBackupRoot(), `.initialized-${id}.json`);
54
55
  }
55
56
 
56
- function isPlainObject(value) {
57
- return !!value && typeof value === 'object' && !Array.isArray(value);
58
- }
59
-
60
57
  export function hasUserDataInitMarker(dataDir) {
61
58
  return existsSync(initMarkerPath(dataDir));
62
59
  }
@@ -15,16 +15,11 @@
15
15
  */
16
16
 
17
17
  import { ensurePgInstance, withSchemaBootstrapLock } from '../memory/lib/pg/adapter.mjs';
18
+ import { createPgSchemaDb } from './pg-schema-db.mjs';
18
19
  import { resolvePluginData } from './plugin-paths.mjs';
19
20
 
20
21
  const SCHEMA = 'webhooks';
21
22
 
22
- // ---------------------------------------------------------------------------
23
- // Lazy connection + one-shot idempotent DDL (keyed per resolved dataDir).
24
- // ---------------------------------------------------------------------------
25
-
26
- const _ready = new Map(); // dataDir → Promise<db>
27
-
28
23
  const DDL = `
29
24
  CREATE SCHEMA IF NOT EXISTS webhooks;
30
25
  CREATE TABLE IF NOT EXISTS webhooks.endpoints (
@@ -59,24 +54,13 @@ CREATE TABLE IF NOT EXISTS webhooks.deliveries (
59
54
  );
60
55
  `;
61
56
 
62
- async function getDb(dataDir = resolvePluginData()) {
63
- if (_ready.has(dataDir)) return _ready.get(dataDir);
64
- const p = (async () => {
65
- const { db, pool } = await ensurePgInstance(dataDir, { schema: SCHEMA });
66
- // Serialize the CREATE TABLE across concurrent first-boot processes on the
67
- // same cluster-global advisory lock the adapter uses for schema bootstrap,
68
- // so racing first calls can't run the DDL simultaneously.
69
- await withSchemaBootstrapLock(pool, () => db.exec(DDL));
70
- return db;
71
- })();
72
- _ready.set(dataDir, p);
73
- try {
74
- return await p;
75
- } catch (err) {
76
- _ready.delete(dataDir); // let the next call retry DDL after a transient failure
77
- throw err;
78
- }
79
- }
57
+ const getDb = createPgSchemaDb({
58
+ schema: SCHEMA,
59
+ ddl: DDL,
60
+ defaultDataDir: resolvePluginData,
61
+ ensurePg: ensurePgInstance,
62
+ withLock: withSchemaBootstrapLock,
63
+ });
80
64
 
81
65
  // ---------------------------------------------------------------------------
82
66
  // Row <-> def mapping
@@ -75,11 +75,8 @@ function normalizeWebSearchArgs(rawArgs) {
75
75
  }
76
76
  }
77
77
  if (Array.isArray(args.keywords)) {
78
- const keywords = args.keywords
78
+ args.keywords = args.keywords
79
79
  .map(value => typeof value === 'string' ? value.trim() : value)
80
- .filter(value => typeof value === 'string' ? value.length > 0 : Boolean(value))
81
- if (keywords.length > 0) args.keywords = keywords
82
- else delete args.keywords
83
80
  }
84
81
  return args
85
82
  }
@@ -203,16 +200,35 @@ function buildAgentWebSearchPrompt(args) {
203
200
  return lines.join('\n')
204
201
  }
205
202
 
203
+ // Native providers disagree on citation field names. These lists are the
204
+ // accepted shapes, not a guess: the first present string wins.
205
+ const NATIVE_SOURCE_URL_FIELDS = ['url', 'uri', 'href', 'source_url']
206
+ const NATIVE_SOURCE_TITLE_FIELDS = ['title', 'query', 'name']
207
+ const NATIVE_SOURCE_SNIPPET_FIELDS = ['snippet', 'text', 'description']
208
+
209
+ function firstSourceField(source, names, fallback = '') {
210
+ let raw = fallback
211
+ if (source && typeof source === 'object') {
212
+ for (const name of names) {
213
+ if (source[name]) {
214
+ raw = source[name]
215
+ break
216
+ }
217
+ }
218
+ }
219
+ return String(raw || '').trim()
220
+ }
221
+
206
222
  function sourceUrl(source) {
207
- return String(source?.url || source?.uri || source?.href || source?.source_url || '').trim()
223
+ return firstSourceField(source, NATIVE_SOURCE_URL_FIELDS)
208
224
  }
209
225
 
210
226
  function sourceTitle(source, fallbackUrl = '') {
211
- return String(source?.title || source?.query || source?.name || fallbackUrl || '(untitled)').trim()
227
+ return firstSourceField(source, NATIVE_SOURCE_TITLE_FIELDS, fallbackUrl || '(untitled)')
212
228
  }
213
229
 
214
230
  function sourceSnippet(source) {
215
- return String(source?.snippet || source?.text || source?.description || '').replace(/\s+/g, ' ').trim()
231
+ return firstSourceField(source, NATIVE_SOURCE_SNIPPET_FIELDS).replace(/\s+/g, ' ').trim()
216
232
  }
217
233
 
218
234
  function collectNativeWebSearchSources(result) {
@@ -457,16 +473,24 @@ async function handleToolCall(name, rawArgs, options = {}) {
457
473
  const keywords = [...new Set(args.keywords.map(kw => String(kw || '').trim()).filter(Boolean))]
458
474
  const sections = new Array(keywords.length)
459
475
  let cursor = 0
476
+ let failed = 0
460
477
  await Promise.all(Array.from({ length: Math.min(concurrency, keywords.length) }, async () => {
461
478
  while (cursor < keywords.length) {
462
479
  const index = cursor++
463
480
  const kw = keywords[index]
464
481
  const sub = await handleToolCall('web_search', { ...rawArgs, keywords: kw }, { signal, nativeWebSearch })
465
482
  const text = (sub.content || []).filter(p => p.type === 'text').map(p => p.text).join('\n')
483
+ if (sub.isError) failed++
466
484
  sections[index] = `### Query: ${kw}\n\n${text}`
467
485
  }
468
486
  }))
469
- return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] }
487
+ const summary = failed
488
+ ? `[web_search] ${failed}/${keywords.length} queries failed; successful results are retained.\n\n`
489
+ : ''
490
+ return {
491
+ content: [{ type: 'text', text: summary + sections.join('\n\n---\n\n') }],
492
+ ...(failed ? { isError: true } : {}),
493
+ }
470
494
  }
471
495
  try {
472
496
  const result = await _webSearchCore(args, { cacheState, nativeWebSearch, signal })
@@ -8,11 +8,11 @@ export const TOOL_DEFS = [
8
8
  {
9
9
  name: 'web_search',
10
10
  title: 'Mixdog Web Search',
11
- description: `Cached web/docs/current-info search. ${TOOL_SYNC_EXECUTION_CONTRACT}`,
11
+ description: `Cached web/docs/current-info search; batch queries sharing filters. ${TOOL_SYNC_EXECUTION_CONTRACT}`,
12
12
  inputSchema: {
13
13
  type: 'object',
14
14
  properties: {
15
- query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Required query or array for lossless fan-out.' },
15
+ query: { anyOf: [{ type: 'string', minLength: 1, pattern: '\\S' }, { type: 'array', minItems: 1, items: { type: 'string', minLength: 1, pattern: '\\S' } }], description: 'Non-empty query or array for lossless fan-out.' },
16
16
  site: { type: 'string', description: 'Site/domain filter.' },
17
17
  type: { type: 'string', enum: ['web', 'news', 'images'], description: 'Default web.' },
18
18
  maxResults: { type: 'integer', minimum: 1, maximum: 20, description: 'Result count; default 10; 1-20.' },
@@ -27,11 +27,11 @@ export const TOOL_DEFS = [
27
27
  {
28
28
  name: 'web_fetch',
29
29
  title: 'Mixdog Web Fetch',
30
- description: 'Fetch page/docs body from URL.',
30
+ description: 'Fetch page/docs body from URL; batch known URLs with shared slice options.',
31
31
  inputSchema: {
32
32
  type: 'object',
33
33
  properties: {
34
- url: { anyOf: [{ type: 'string', format: 'uri' }, { type: 'array', items: { type: 'string', format: 'uri' }, maxItems: 10 }], description: 'Public HTTP(S) URL or array of up to 10 URLs.' },
34
+ url: { anyOf: [{ type: 'string', format: 'uri' }, { type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1, maxItems: 10 }], description: 'Public HTTP(S) URL or array of up to 10 URLs.' },
35
35
  startIndex: { type: 'integer', minimum: 0, description: 'Character offset; default 0.' },
36
36
  maxLength: { type: 'integer', minimum: 0, description: 'Maximum characters; default 50000; 0 unlimited.' },
37
37
  },
@@ -18,8 +18,42 @@
18
18
  const CONFIG_SAVE_DEBOUNCE_MS = 150;
19
19
 
20
20
  import { withGrandfatheredBuiltins } from './builtin-features.mjs';
21
+ import { webSearchRouteOrDefault } from './workflow.mjs';
21
22
  import { createDebouncedWriter } from '../runtime/shared/debounced-writer.mjs';
22
23
 
24
+ /**
25
+ * Boot-time config/route state for one runtime. Caller overrides are applied
26
+ * ONLY when supplied, so an option left out keeps the persisted route value
27
+ * (`effort: undefined` must not erase a stored effort, and `fast` is a tri-state
28
+ * where only an explicit boolean overrides).
29
+ */
30
+ export function resolveInitialConfigState({
31
+ initialConfig,
32
+ loadConfig,
33
+ resolveRoute,
34
+ provider,
35
+ model,
36
+ effort,
37
+ fast,
38
+ modelParameters,
39
+ }) {
40
+ const config = withGrandfatheredBuiltins(
41
+ initialConfig && typeof initialConfig === 'object' ? initialConfig : loadConfig(),
42
+ );
43
+ return {
44
+ config,
45
+ route: {
46
+ ...resolveRoute(config, { provider, model }),
47
+ ...(effort === undefined ? {} : { effort: effort || null }),
48
+ ...(fast === true || fast === false ? { fast } : {}),
49
+ ...(modelParameters && typeof modelParameters === 'object'
50
+ ? { modelParameters: { ...modelParameters } }
51
+ : {}),
52
+ },
53
+ webSearchRoute: webSearchRouteOrDefault(config.webSearchRoute),
54
+ };
55
+ }
56
+
23
57
  export function createConfigLifecycle({
24
58
  // config mutable-state injection
25
59
  getConfig,
@@ -393,3 +393,22 @@ export function createContextStatus({
393
393
 
394
394
  return { contextStatus, invalidateContextStatusCache };
395
395
  }
396
+
397
+ // One-shot gauge for a session that is NOT the runtime's current session (a
398
+ // listed/foreign session). The route is read off the session itself and the
399
+ // calculator is thrown away with its cache, so a foreign read can never poison
400
+ // the live gauge's memoization.
401
+ export function contextStatusForSession(session, { getMode, fallbackCwd = '' } = {}) {
402
+ if (!session || typeof session !== 'object') return null;
403
+ const { contextStatus } = createContextStatus({
404
+ getSession: () => session,
405
+ getRoute: () => ({
406
+ provider: session.provider || '',
407
+ model: session.model || '',
408
+ contextWindow: session.contextWindow || null,
409
+ }),
410
+ getCurrentCwd: () => session.cwd || fallbackCwd,
411
+ getMode,
412
+ });
413
+ return contextStatus();
414
+ }
@@ -23,7 +23,6 @@ export function createCwdPlugins({
23
23
  isCodeGraphPrewarmLazy,
24
24
  isCodeGraphFirstTurnPrewarmDone,
25
25
  getCodeGraphPrewarmDelayMs,
26
- setSessionNeedsCwdRefresh,
27
26
  // callbacks / deps
28
27
  scheduleCodeGraphPrewarm,
29
28
  hooks,
@@ -99,11 +98,11 @@ export function createCwdPlugins({
99
98
  return next;
100
99
  }
101
100
 
102
- function applyResolvedCwd(nextCwd, {
103
- markRefresh = true,
104
- waitForMcpReset = false,
105
- persistProjectSelection = false,
106
- } = {}) {
101
+ // cwd changes NEVER recreate the session and never reload MCP or Skills
102
+ // (extension settings are global): a mid-conversation cwd switch must
103
+ // preserve the full message history. The live execution cwd is retargeted in
104
+ // place; the BP3 session snapshot remains the session-start environment.
105
+ function applyResolvedCwd(nextCwd, { persistProjectSelection = false } = {}) {
107
106
  const resolved = resolve(nextCwd);
108
107
  const stat = statSync(resolved);
109
108
  if (!stat.isDirectory()) throw new Error(`cwd: not a directory: ${resolved}`);
@@ -114,22 +113,18 @@ export function createCwdPlugins({
114
113
  if (session) session.cwd = currentCwd;
115
114
  updateCurrentCwdOverride?.(currentCwd);
116
115
  writeLastSessionCwd(currentCwd, session?.clientHostPid);
117
- if (persistProjectSelection && session) {
118
- const desktop = getDesktopSession?.() || session.desktopSession;
116
+ if (persistProjectSelection) {
117
+ const desktop = getDesktopSession?.() || session?.desktopSession;
119
118
  if (desktop && typeof desktop === 'object') {
120
119
  const nextDesktop = { classification: 'project', projectPath: currentCwd };
121
120
  setDesktopSession?.(nextDesktop);
122
- session.desktopSession = nextDesktop;
121
+ if (session) session.desktopSession = nextDesktop;
122
+ }
123
+ if (session) {
124
+ session.updatedAt = Date.now();
125
+ persistSession(session);
123
126
  }
124
- session.updatedAt = Date.now();
125
- persistSession(session);
126
127
  }
127
- // cwd changes NEVER recreate the session: a mid-conversation cwd switch must
128
- // preserve the full message history. We retarget the live execution cwd in
129
- // place; the BP3 session snapshot remains the session-start environment.
130
- // `markRefresh`/`changed` are kept only for signature compatibility with
131
- // existing callers.
132
- void markRefresh;
133
128
  // Lazy mode: before the first turn (e.g. the initial project-selection
134
129
  // cwd set), do NOT prewarm — that is exactly the post-first-frame freeze
135
130
  // we are avoiding. Once a turn has run, an in-session cwd switch DOES
@@ -140,8 +135,6 @@ export function createCwdPlugins({
140
135
  const delay = getCodeGraphPrewarmDelayMs();
141
136
  scheduleCodeGraphPrewarm(changed ? 0 : delay, changed ? 'cwd-change' : 'cwd');
142
137
  }
143
- // Extension settings are global; cwd changes never reload MCP or Skills.
144
- void waitForMcpReset;
145
138
  // CwdChanged: bridge an effective cwd switch to the standard hook bus.
146
139
  // No matcher event — payload is minimal { cwd }. Fire-and-forget.
147
140
  if (changed) {
@@ -150,15 +143,6 @@ export function createCwdPlugins({
150
143
  return currentCwd;
151
144
  }
152
145
 
153
- async function refreshSessionForCwdIfNeeded(reason = 'cwd-change') {
154
- // No-op: cwd changes are applied in place by applyResolvedCwd and never
155
- // tear down the session. Retained as a stable hook for ask()'s pre-turn
156
- // call so the surrounding turn flow is unchanged.
157
- void reason;
158
- setSessionNeedsCwdRefresh(false);
159
- return getSession();
160
- }
161
-
162
146
  function pluginsStatus() {
163
147
  const config = getConfig();
164
148
  const dataDir = cfgMod.getPluginData?.();
@@ -272,7 +256,6 @@ export function createCwdPlugins({
272
256
  return {
273
257
  resolveCwdPath,
274
258
  applyResolvedCwd,
275
- refreshSessionForCwdIfNeeded,
276
259
  pluginsStatus,
277
260
  formatCoreMemoryLines,
278
261
  loadCoreMemoryContext,
@@ -1,4 +1,4 @@
1
- import { GOAL_TASK_SETTLED, GOAL_TASK_STATUSES, MAX_GOAL_TASKS } from './goal-tool-defs.mjs';
1
+ import { GOAL_TASK_SETTLED, GOAL_TASK_STATUSES, MAX_GOAL_TASKS, MAX_GOAL_TASK_TEXT_LENGTH } from './goal-tool-defs.mjs';
2
2
  import { clean } from '../runtime/shared/clean.mjs';
3
3
 
4
4
  // Dropped rows remain in the record but are no longer requested work.
@@ -42,7 +42,7 @@ export function normalizeGoalTasks(input, previous = [], { strict = false } = {}
42
42
  if (!source || typeof source !== 'object') throw new Error(`goal task ${index + 1} is invalid`);
43
43
  const text = clean(source.text);
44
44
  if (!text) throw new Error('goal task text is required');
45
- if ([...text].length > 500) throw new Error('goal task exceeds 500 characters');
45
+ if ([...text].length > MAX_GOAL_TASK_TEXT_LENGTH) throw new Error(`goal task exceeds ${MAX_GOAL_TASK_TEXT_LENGTH} characters`);
46
46
  if (seenText.has(text)) throw new Error(`duplicate goal task: ${text}`);
47
47
  seenText.add(text);
48
48
  const id = clean(source.id) || clean(previousByText.get(text)?.id) || `task_${nextId++}`;
@@ -4,10 +4,11 @@ export const GOAL_TASK_STATUSES = Object.freeze([
4
4
  ]);
5
5
  export const GOAL_TASK_SETTLED = Object.freeze(['completed', 'dropped']);
6
6
  export const MAX_GOAL_TASKS = 20;
7
+ export const MAX_GOAL_TASK_TEXT_LENGTH = 500;
7
8
 
8
9
  const taskFields = {
9
10
  id: { type: 'string', description: 'Stable task id; omit only for new tasks.' },
10
- text: { type: 'string', description: 'Required work or verification outcome.' },
11
+ text: { type: 'string', minLength: 1, maxLength: MAX_GOAL_TASK_TEXT_LENGTH, description: 'Required work or verification outcome.' },
11
12
  status: {
12
13
  type: 'string',
13
14
  enum: GOAL_TASK_STATUSES,
@@ -47,7 +48,7 @@ export const GOAL_TOOL_DEFS = Object.freeze([{
47
48
  tasks: {
48
49
  type: 'array', minItems: 1, maxItems: MAX_GOAL_TASKS,
49
50
  items: { type: 'object', properties: taskFields, required: ['text', 'status', 'kind'], additionalProperties: false },
50
- description: 'create/set_tasks: full list; update_tasks/resume: new tasks to append, without ids.',
51
+ description: `create/set_tasks: full list; update_tasks/resume: new tasks, no ids. Max ${MAX_GOAL_TASKS} total, including completed/dropped.`,
51
52
  },
52
53
  updates: {
53
54
  type: 'array', minItems: 1, maxItems: MAX_GOAL_TASKS,
@@ -15,6 +15,7 @@ import { featureEnvOverride } from './config-helpers.mjs';
15
15
  import { renderToolSearch } from './tool-catalog.mjs';
16
16
  import { clean } from './session-text.mjs';
17
17
  import { STANDALONE_DATA_DIR } from './runtime-paths.mjs';
18
+ import { listProjects } from '../standalone/projects.mjs';
18
19
  import {
19
20
  dispatchWebSearchRuntimeTool,
20
21
  memoryToolArgsForCaller,
@@ -149,6 +150,12 @@ export function createInternalToolExecutor({
149
150
  if (name === 'cwd') {
150
151
  const action = clean(args?.action || (args?.path ? 'set' : 'get')).toLowerCase();
151
152
  let currentCwd = callerCwd;
153
+ if (action === 'list') {
154
+ return JSON.stringify({
155
+ cwd: currentCwd,
156
+ projects: listProjects().map((project) => ({ name: project.name, path: project.path })),
157
+ }, null, 2);
158
+ }
152
159
  if (action === 'set') {
153
160
  const rawPath = clean(args?.path);
154
161
  if (!rawPath) throw new Error('cwd: path is required for action=set');