mixdog 0.9.44 → 0.9.46

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 (117) hide show
  1. package/package.json +5 -2
  2. package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
  3. package/scripts/agent-model-liveness-test.mjs +618 -0
  4. package/scripts/agent-trace-io-test.mjs +68 -4
  5. package/scripts/bench-run.mjs +30 -3
  6. package/scripts/compact-pressure-test.mjs +187 -4
  7. package/scripts/compact-prior-context-flatten-test.mjs +252 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +8 -6
  9. package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
  10. package/scripts/context-mcp-metering-test.mjs +75 -0
  11. package/scripts/explore-prompt-policy-test.mjs +15 -16
  12. package/scripts/explore-timeout-cancel-test.mjs +345 -0
  13. package/scripts/headless-pristine-execution-test.mjs +614 -0
  14. package/scripts/internal-comms-smoke.mjs +15 -6
  15. package/scripts/live-worker-smoke.mjs +1 -1
  16. package/scripts/memory-core-input-test.mjs +137 -0
  17. package/scripts/memory-rule-contract-test.mjs +5 -5
  18. package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
  19. package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
  20. package/scripts/parent-abort-link-test.mjs +22 -0
  21. package/scripts/provider-toolcall-test.mjs +22 -0
  22. package/scripts/reactive-compact-persist-smoke.mjs +8 -4
  23. package/scripts/session-bench.mjs +3 -70
  24. package/scripts/task-bench.mjs +0 -2
  25. package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
  26. package/scripts/tool-smoke.mjs +21 -21
  27. package/scripts/tool-tui-presentation-test.mjs +68 -0
  28. package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
  29. package/src/app.mjs +28 -103
  30. package/src/cli.mjs +17 -13
  31. package/src/headless-command.mjs +139 -0
  32. package/src/headless-role.mjs +121 -10
  33. package/src/help.mjs +4 -1
  34. package/src/rules/agent/00-common.md +3 -3
  35. package/src/rules/agent/00-core.md +8 -9
  36. package/src/rules/agent/20-skip-protocol.md +2 -3
  37. package/src/rules/agent/30-explorer.md +50 -56
  38. package/src/rules/agent/40-cycle1-agent.md +10 -12
  39. package/src/rules/agent/41-cycle2-agent.md +12 -9
  40. package/src/rules/agent/42-cycle3-agent.md +4 -6
  41. package/src/rules/lead/01-general.md +5 -6
  42. package/src/rules/lead/02-channels.md +1 -1
  43. package/src/rules/lead/lead-brief.md +14 -17
  44. package/src/rules/lead/lead-tool.md +3 -3
  45. package/src/rules/shared/01-tool.md +41 -43
  46. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
  48. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
  49. package/src/runtime/agent/orchestrator/config.mjs +96 -30
  50. package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
  51. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  52. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
  53. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
  54. package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
  55. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
  56. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
  58. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
  60. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
  61. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
  64. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
  65. package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
  66. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
  67. package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
  69. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
  71. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
  74. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
  75. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
  76. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
  77. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
  78. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
  80. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
  81. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  82. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
  84. package/src/runtime/memory/index.mjs +0 -1
  85. package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
  86. package/src/runtime/memory/lib/http-router.mjs +0 -193
  87. package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
  88. package/src/runtime/memory/tool-defs.mjs +5 -6
  89. package/src/runtime/shared/config.mjs +11 -34
  90. package/src/runtime/shared/pristine-execution-contract.json +75 -0
  91. package/src/runtime/shared/pristine-execution.mjs +356 -0
  92. package/src/runtime/shared/provider-api-key.mjs +43 -0
  93. package/src/runtime/shared/provider-auth-binding.mjs +21 -0
  94. package/src/runtime/shared/update-checker.mjs +40 -10
  95. package/src/session-runtime/context-status.mjs +61 -13
  96. package/src/session-runtime/mcp-glue.mjs +29 -2
  97. package/src/session-runtime/plugin-mcp.mjs +7 -0
  98. package/src/session-runtime/resource-api.mjs +38 -5
  99. package/src/session-runtime/runtime-core.mjs +5 -1
  100. package/src/session-runtime/session-turn-api.mjs +14 -2
  101. package/src/session-runtime/settings-api.mjs +5 -0
  102. package/src/session-runtime/tool-catalog.mjs +13 -2
  103. package/src/session-runtime/tool-defs.mjs +1 -3
  104. package/src/standalone/agent-task-status.mjs +50 -11
  105. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  106. package/src/standalone/explore-tool.mjs +257 -49
  107. package/src/standalone/seeds.mjs +1 -0
  108. package/src/tui/App.jsx +26 -16
  109. package/src/tui/app/use-transcript-scroll.mjs +4 -3
  110. package/src/tui/app/use-transcript-window.mjs +12 -21
  111. package/src/tui/components/ContextPanel.jsx +19 -25
  112. package/src/tui/dist/index.mjs +89 -78
  113. package/src/tui/engine/agent-envelope.mjs +16 -5
  114. package/src/tui/engine/labels.mjs +1 -1
  115. package/src/workflows/default/WORKFLOW.md +21 -51
  116. package/src/workflows/solo/WORKFLOW.md +12 -17
  117. package/src/workflows/solo-review/WORKFLOW.md +0 -47
@@ -410,55 +410,6 @@ function capToolOutput(result, options = {}) {
410
410
  return `${head}\n... [tool-output truncated: ${Math.round(bytes / 1024)} KB -> ${Math.round(cap / 1024)} KB cap (tool_output_token_limit)] ...\n${tail}`;
411
411
  }
412
412
 
413
- const _GREP_SWEEP_NUDGE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_ENABLE_GREP_SWEEP_NUDGE || ''));
414
- const _GREP_SWEEP_MIN_CALLS = 20;
415
- const _GREP_SWEEP_MIN_UNIQUE = 16;
416
- const _grepSweepState = new Map();
417
-
418
- function _grepSweepTerms(pattern) {
419
- const raw = Array.isArray(pattern) ? pattern.join('|') : String(pattern || '');
420
- return [...new Set(raw
421
- .split(/[|,\s()"'`[\]{}.*+?^$\\:-]+/g)
422
- .map((s) => s.trim().toLowerCase())
423
- .filter((s) => s.length >= 4))];
424
- }
425
-
426
- function _normalizeSweepPath(value) {
427
- return String(value || '.')
428
- .replace(/\\/g, '/')
429
- .replace(/^([A-Za-z]):/, (_, d) => d.toLowerCase() + ':')
430
- .replace(/\/+/g, '/')
431
- .replace(/\/$/, '') || '.';
432
- }
433
-
434
- function _grepSweepKey(args = {}) {
435
- const path = _normalizeSweepPath(args.path || '.');
436
- const glob = Array.isArray(args.glob) ? args.glob.join(',') : String(args.glob || '');
437
- const terms = _grepSweepTerms(args.pattern).slice(0, 3).join('|');
438
- return `${path}::${glob}::${terms}`;
439
- }
440
-
441
- function _maybeAppendGrepSweepNudge(toolName, args, result, scope) {
442
- if (!_GREP_SWEEP_NUDGE_ENABLED || toolName !== 'grep' || typeof result !== 'string') return result;
443
- const key = scope || 'global';
444
- const now = Date.now();
445
- let state = _grepSweepState.get(key);
446
- if (!state || now - state.lastTs > 10 * 60_000) {
447
- state = { count: 0, unique: new Set(), lastTs: 0, nudged: false };
448
- _grepSweepState.set(key, state);
449
- }
450
- state.count += 1;
451
- state.unique.add(_grepSweepKey(args));
452
- state.lastTs = now;
453
- if (_grepSweepState.size > 256) {
454
- const oldest = [..._grepSweepState.entries()].sort((a, b) => (a[1].lastTs || 0) - (b[1].lastTs || 0)).slice(0, 32);
455
- for (const [oldKey] of oldest) _grepSweepState.delete(oldKey);
456
- }
457
- if (state.nudged || state.count < _GREP_SWEEP_MIN_CALLS || state.unique.size < _GREP_SWEEP_MIN_UNIQUE) return result;
458
- state.nudged = true;
459
- return `${result}\n\n[grep_sweep_hint] Many distinct grep searches were used in this session. Stop rewording search terms; synthesize from existing anchors or switch tools once (code_graph/find/read known regions).`;
460
- }
461
-
462
413
  export async function executeBuiltinTool(name, args, cwd, options = {}) {
463
414
  if (options.abortSignal && !options.signal) {
464
415
  options = { ...options, signal: options.abortSignal };
@@ -521,8 +472,7 @@ export async function executeBuiltinTool(name, args, cwd, options = {}) {
521
472
  return formatUnknownBuiltinToolMessage(name, args);
522
473
  }
523
474
  })();
524
- const _withNudge = _maybeAppendGrepSweepNudge(toolName, args, _toolResult, readStateScope);
525
- return capToolOutput(_appendClampNotices(args, _withNudge), options);
475
+ return capToolOutput(_appendClampNotices(args, _toolResult), options);
526
476
  }
527
477
 
528
478
  // Surface arg-guard clamp notices (args._clampNotices, see pushClampNotice in
@@ -3,13 +3,13 @@ export const CODE_GRAPH_TOOL_DEFS = [
3
3
  name: 'code_graph',
4
4
  title: 'Code Graph',
5
5
  annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
6
- description: 'Repo-local code structure/flow (not web): symbols/references/calls/deps Public files[] must be verified paths; verified source files only. IDs→graph; literal/zero→grep. Exact identifiers: find_symbol/references/callers/callees; keywords: symbol_search/search Unsupported target arrays omitted, never silently mixed. Legacy file/symbol/language compatible, hidden. Batch symbols[]/files[] by mode',
6
+ description: 'Repo code structure/flow over verified source files only. IDs→graph; literal/zero→grep. Exact identifiers: find_symbol/references/callers/callees; keywords: symbol_search/search. Unsupported target arrays are omitted, never silently mixed. Batch symbols[]/files[] by mode.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
10
- mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: 'Repo-local: file modes={overview,imports,dependents,related,impact}; symbols with files→files[] file outline; symbol modes={find_symbol,symbol_search,search,references,callers,callees}; fileless symbols→symbol_search keywords.' },
11
- files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Verified source files; supported targets only.' },
12
- symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Exact identifiers: find_symbol/references/callers/callees; keywords: symbol_search/search; multiple exact symbols use one symbols[] call; one call.' },
10
+ mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: 'File modes={overview,imports,dependents,related,impact}; symbols with files→files[] file outline; symbol modes={find_symbol,symbol_search,search,references,callers,callees}; fileless symbols→symbol_search keywords.' },
11
+ files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Source file path(s); supported targets only.' },
12
+ symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Exact identifiers (find_symbol/references/callers/callees) or keywords (symbol_search/search); multiple exact symbols use one symbols[] call.' },
13
13
  body: { type: 'boolean', description: 'Include body.' },
14
14
  limit: { type: 'number', minimum: 1, description: 'Max results.' },
15
15
  depth: { type: 'number', minimum: 1, maximum: 5, description: 'Caller depth.' },
@@ -363,7 +363,7 @@ async function apply_patch(args, cwd, options = {}) {
363
363
  throw new Error('apply_patch: "patch" is required (unified diff or V4A patch string)');
364
364
  }
365
365
  if (isCompactedPlaceholderPatch(patchStr)) {
366
- throw new Error('patch body is a compacted-history placeholder ([mixdog compacted …]), not real patch content. Re-read the target file span and write a fresh patch; never resubmit compacted output as new tool input.');
366
+ throw new Error('patch body is a compacted-history placeholder ([mixdog compacted …]), not real patch content and cannot be executed. Re-read the current target file contents now, then construct and submit a fresh full patch from those contents. Do not reuse the marker, omit the patch argument, or reconstruct the old patch from history.');
367
367
  }
368
368
  const patchByteLen = Buffer.byteLength(patchStr, 'utf8');
369
369
  if (patchByteLen > APPLY_PATCH_MAX_BYTES) {
@@ -0,0 +1,26 @@
1
+ {
2
+ "version": "0.7.19",
3
+ "_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
4
+ "assets": {
5
+ "darwin-arm64": {
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-darwin-arm64",
7
+ "sha256": "c808fcfb69d2817fd7678dd364d2e1dd68f6ef2d9807f618bdf110c03ca00ccb"
8
+ },
9
+ "darwin-x64": {
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-darwin-x64",
11
+ "sha256": "7ed0eeff84796559ca865fc93e7a2a94f2c3fa00126f919064bb8273656fcd85"
12
+ },
13
+ "linux-arm64": {
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-linux-arm64",
15
+ "sha256": "3cad9c9542f2d97732953a102ef72daf4c79be5361560698d5fd2c5f0c18eda7"
16
+ },
17
+ "linux-x64": {
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-linux-x64",
19
+ "sha256": "cb0e30346f437811dff19238e1854bd37d97a78f35b13e0b704ebeaa9499ae87"
20
+ },
21
+ "win32-x64": {
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-win32-x64.exe",
23
+ "sha256": "4bf61a99f63e446570d9d2a9c9ef38475d0d70c0a2b632a5f44d4fe9b684b968"
24
+ }
25
+ }
26
+ }
@@ -793,7 +793,6 @@ const _httpRouter = createHttpRouter({
793
793
  registerClient,
794
794
  deregisterClient,
795
795
  getDraining: () => _stopPromise != null,
796
- getCycle1CallLlm,
797
796
  getTraceDb: () => _traceDb,
798
797
  setTraceDb: (v) => { _traceDb = v },
799
798
  ingestTranscriptFile,
@@ -39,6 +39,29 @@ function trimOrNull(v) {
39
39
  return s === '' ? null : s
40
40
  }
41
41
 
42
+ export function normalizeCoreInput(input = {}, options = {}) {
43
+ const summary = trimOrNull(input.summary ?? input.content)
44
+ const element = trimOrNull(input.element) ?? (summary ? summary.slice(0, CORE_ELEMENT_MAX) : null)
45
+ const suppliedCategory = trimOrNull(input.category)
46
+ const category = (suppliedCategory ?? 'fact').toLowerCase()
47
+ const errors = []
48
+
49
+ if (options.requireElement && !element) errors.push('element required')
50
+ if (options.requireSummary && !summary) errors.push('summary required')
51
+ if (options.requireCategory && !suppliedCategory) errors.push('category required')
52
+ if (element && element.length > CORE_ELEMENT_MAX) {
53
+ errors.push(`element too long (${element.length}/${CORE_ELEMENT_MAX} chars, remove ${element.length - CORE_ELEMENT_MAX})`)
54
+ }
55
+ if (summary && summary.length > CORE_SUMMARY_MAX) {
56
+ errors.push(`summary too long (${summary.length}/${CORE_SUMMARY_MAX} chars, remove ${summary.length - CORE_SUMMARY_MAX})`)
57
+ }
58
+ if (suppliedCategory && !VALID_CAT.has(category)) {
59
+ errors.push(`invalid category "${category}". Valid: ${[...VALID_CAT].join(', ')}`)
60
+ }
61
+
62
+ return { element, summary, category, suppliedCategory, errors }
63
+ }
64
+
42
65
  function _getDb(dataDir) {
43
66
  if (!dataDir) throw new Error('core-memory: dataDir required')
44
67
  const db = getDatabase(dataDir)
@@ -206,21 +229,14 @@ export async function listCore(dataDir, projectId = null) {
206
229
  return r.rows
207
230
  }
208
231
 
209
- export async function addCore(dataDir, { element, summary, category }, projectId) {
232
+ export async function addCore(dataDir, input, projectId) {
210
233
  if (projectId === undefined) throw new Error('addCore: projectId required — pass null for COMMON pool, or slug string for scoped pool')
211
- const el = trimOrNull(element)
212
- const sm = trimOrNull(summary) ?? el
213
- if (!el || !sm) throw new Error('add requires element and summary')
214
- if (el.length > CORE_ELEMENT_MAX) {
215
- throw new Error(`core element too long (${el.length}/${CORE_ELEMENT_MAX} chars, remove ${el.length - CORE_ELEMENT_MAX}) — element is a short key/title, not content.`)
216
- }
217
- if (sm.length > CORE_SUMMARY_MAX) {
218
- throw new Error(`core summary too long (${sm.length}/${CORE_SUMMARY_MAX} chars, remove ${sm.length - CORE_SUMMARY_MAX}) — 1 fact in 1-2 sentences; move procedures/multi-step/code to recap or docs.`)
219
- }
220
- const cat = (trimOrNull(category) ?? 'fact').toLowerCase()
221
- if (!VALID_CAT.has(cat)) {
222
- throw new Error(`invalid category "${cat}". Valid: ${[...VALID_CAT].join(', ')}`)
223
- }
234
+ const { element: el, summary: sm, category: cat, errors } = normalizeCoreInput(input, {
235
+ requireElement: true,
236
+ requireSummary: true,
237
+ requireCategory: true,
238
+ })
239
+ if (errors.length) throw new Error(errors.join('; '))
224
240
  const db = _getDb(dataDir)
225
241
  const now = Date.now()
226
242
  await _backfillNullEmbeddings(db)
@@ -299,19 +315,23 @@ export async function editCore(dataDir, id, patch) {
299
315
  const db = _getDb(dataDir)
300
316
  const cur = (await db.query(`SELECT * FROM core_entries WHERE id = $1`, [numId])).rows[0]
301
317
  if (!cur) throw new Error(`no entry with id=${numId}`)
302
- const newElement = trimOrNull(patch.element) ?? cur.element
303
- const newSummary = trimOrNull(patch.summary) ?? cur.summary
304
- const newCategoryRaw = trimOrNull(patch.category)
305
- const newCategory = newCategoryRaw ? newCategoryRaw.toLowerCase() : cur.category
306
- if (!VALID_CAT.has(newCategory)) {
307
- throw new Error(`invalid category "${newCategory}". Valid: ${[...VALID_CAT].join(', ')}`)
308
- }
318
+ const incoming = normalizeCoreInput(patch)
319
+ const newElement = incoming.element ?? cur.element
320
+ const newSummary = incoming.summary ?? cur.summary
321
+ const newCategory = incoming.suppliedCategory ? incoming.category : cur.category
322
+ const { errors } = normalizeCoreInput({
323
+ element: newElement,
324
+ summary: newSummary,
325
+ category: newCategory,
326
+ }, {
327
+ requireElement: true,
328
+ requireSummary: true,
329
+ requireCategory: true,
330
+ })
331
+ if (errors.length) throw new Error(errors.join('; '))
309
332
  if (newElement === cur.element && newSummary === cur.summary && newCategory === cur.category) {
310
333
  throw new Error('no change')
311
334
  }
312
- if (newSummary && newSummary.length > CORE_SUMMARY_MAX) {
313
- throw new Error(`core summary too long (${newSummary.length}/${CORE_SUMMARY_MAX} chars, remove ${newSummary.length - CORE_SUMMARY_MAX}) — 1 fact in 1-2 sentences; move procedures/multi-step/code to recap or docs.`)
314
- }
315
335
  const now = Date.now()
316
336
  const textChanged = newElement !== cur.element || newSummary !== cur.summary
317
337
  if (!textChanged) {
@@ -45,7 +45,6 @@ export function createHttpRouter({
45
45
  registerClient,
46
46
  deregisterClient,
47
47
  getDraining,
48
- getCycle1CallLlm,
49
48
  getTraceDb,
50
49
  setTraceDb,
51
50
  ingestTranscriptFile,
@@ -425,198 +424,6 @@ export function createHttpRouter({
425
424
  return
426
425
  }
427
426
 
428
- // DEV-ONLY cycle1 chunking bench. Gated by env MIXDOG_DEV_BENCH=1 so
429
- // production is untouched (route returns 404 when unset). Mirrors cycle1's
430
- // exact fetch query + per-session windowing, then runs each window through
431
- // buildCycle1ChunkPrompt + callAgentDispatch + parseCycle1LineFormat. STRICT
432
- // read-only — no UPDATE, no transaction, no commit.
433
- if (req.method === 'POST' && req.url === '/dev/cycle1-bench') {
434
- const db = getDb()
435
- // Gate: env MIXDOG_DEV_BENCH=1 OR a runtime flag file, so it can be
436
- // toggled without restarting the host agent (env only reaches the worker
437
- // on a full CC restart, not via dev-sync full-restart).
438
- const _devBenchOn = process.env.MIXDOG_DEV_BENCH === '1'
439
- || (DATA_DIR && fs.existsSync(path.join(DATA_DIR, '.dev-bench-enabled')))
440
- if (!_devBenchOn) {
441
- sendJson(res, { error: 'not found' }, 404)
442
- return
443
- }
444
- if (!isLocalOrigin(req)) {
445
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
446
- return
447
- }
448
- try {
449
- const body = await readBody(req)
450
- const sets = Math.max(1, Number(body?.sets ?? 5))
451
- const repeat = Math.max(1, Number(body?.repeat ?? 1))
452
- // Optional variant matrix. Each variant: {name, rules}. rules=null → default prompt.
453
- const rawVariants = Array.isArray(body?.variants) ? body.variants : null
454
- const variants = rawVariants && rawVariants.length > 0
455
- ? rawVariants.map((v, i) => ({
456
- name: typeof v?.name === 'string' && v.name ? v.name : `variant-${i + 1}`,
457
- rules: Array.isArray(v?.rules) ? v.rules : null,
458
- }))
459
- : null
460
-
461
- // Lazy-load LLM + chunking helpers so production boot pays nothing.
462
- // Use the same in-process agent dispatch adapter as real cycle1 — the legacy
463
- // agent-ipc callAgentDispatch() path is dead in the detached standalone
464
- // memory daemon (no connected IPC), so the dev bench must mirror prod.
465
- const [{ buildCycle1ChunkPrompt, parseCycle1LineFormat }, { resolveMaintenancePreset }] = await Promise.all([
466
- import('./memory-cycle1.mjs'),
467
- import('../../shared/llm/index.mjs'),
468
- ])
469
- const benchCallLlm = getCycle1CallLlm()
470
-
471
- const CYCLE1_MIN_BATCH = 3
472
- const CYCLE1_SESSION_CAP = 10
473
- const BATCH_SIZE = 100
474
- const TIMEOUT_MS = 180_000
475
- const fetchLimit = CYCLE1_SESSION_CAP * BATCH_SIZE
476
-
477
- const fetchResult = await db.query(
478
- `SELECT id, ts, role, content, session_id, source_ref, project_id
479
- FROM entries
480
- WHERE chunk_root IS NULL AND session_id IS NOT NULL
481
- ORDER BY ts DESC, id DESC
482
- LIMIT $1`,
483
- [fetchLimit],
484
- )
485
- const rowsDesc = fetchResult.rows
486
-
487
- if (rowsDesc.length < CYCLE1_MIN_BATCH) {
488
- sendJson(res, {
489
- ok: true,
490
- sets, repeat,
491
- windowsAvailable: 0,
492
- note: `not enough pending rows (need >= ${CYCLE1_MIN_BATCH}, got ${rowsDesc.length})`,
493
- results: [],
494
- })
495
- return
496
- }
497
-
498
- // Partition by session_id — same as memory-cycle1.mjs _runCycle1Impl L207-233.
499
- const sessionMap = new Map()
500
- for (const row of rowsDesc.slice().reverse()) {
501
- const sid = row.session_id
502
- if (!sessionMap.has(sid)) sessionMap.set(sid, [])
503
- sessionMap.get(sid).push(row)
504
- }
505
- const windows = []
506
- for (const [sid, sessionRows] of sessionMap) {
507
- if (sessionRows.length < CYCLE1_MIN_BATCH) continue
508
- const windowCount = Math.max(1, Math.ceil(sessionRows.length / BATCH_SIZE))
509
- const baseSize = Math.floor(sessionRows.length / windowCount)
510
- const remainder = sessionRows.length % windowCount
511
- let _offset = 0
512
- for (let i = 0; i < windowCount; i++) {
513
- const size = baseSize + (i < remainder ? 1 : 0)
514
- windows.push({ sid, rows: sessionRows.slice(_offset, _offset + size) })
515
- _offset += size
516
- }
517
- }
518
- const chosen = windows.slice(0, sets)
519
-
520
- const preset = resolveMaintenancePreset('memory')
521
-
522
- function summariseChunks(chunks, totalEntries) {
523
- const usedIdx = new Set()
524
- for (const c of chunks) for (const i of (c._idxList || [])) usedIdx.add(i)
525
- const omitted = []
526
- for (let i = 1; i <= totalEntries; i++) if (!usedIdx.has(i)) omitted.push(i)
527
- return { covered: usedIdx.size, omitted }
528
- }
529
-
530
- // When variants are absent, fall back to a single implicit baseline so the
531
- // pre-variant call shape (single rows × repeat) keeps producing the same
532
- // {runs:[…]} payload the trigger already knows how to print.
533
- const variantList = variants ?? [{ name: 'baseline', rules: null }]
534
-
535
- async function runOnce(rows, customRules) {
536
- const userMessage = buildCycle1ChunkPrompt(rows, customRules)
537
- const t0 = Date.now()
538
- let raw, error
539
- try {
540
- raw = await benchCallLlm({
541
- preset,
542
- timeout: TIMEOUT_MS,
543
- }, userMessage)
544
- } catch (e) {
545
- error = e?.message ?? String(e)
546
- }
547
- const llmMs = Date.now() - t0
548
- if (error) return { ok: false, llmMs, error }
549
- const parsed = parseCycle1LineFormat(raw)
550
- const chunks = Array.isArray(parsed?.chunks) ? parsed.chunks : []
551
- const { covered, omitted } = summariseChunks(chunks, rows.length)
552
- const ratio = chunks.length > 0
553
- ? parseFloat((rows.length / chunks.length).toFixed(2))
554
- : null
555
- return {
556
- ok: true,
557
- llmMs,
558
- entries: rows.length,
559
- chunks: chunks.length,
560
- ratio,
561
- covered,
562
- omitted,
563
- chunkList: chunks.map(c => ({
564
- idx: c._idxList,
565
- element: c.element,
566
- category: c.category,
567
- summary: c.summary,
568
- })),
569
- }
570
- }
571
-
572
- const results = []
573
- for (let s = 0; s < chosen.length; s++) {
574
- const { sid, rows } = chosen[s]
575
- const sidShort = String(sid).slice(0, 8)
576
- if (variants) {
577
- // Variant mode: same rows, one run per variant per repeat.
578
- const variantResults = []
579
- for (const v of variantList) {
580
- const runs = []
581
- for (let r = 0; r < repeat; r++) {
582
- const run = await runOnce(rows, v.rules)
583
- runs.push({ repIdx: r + 1, ...run })
584
- }
585
- variantResults.push({ name: v.name, runs })
586
- }
587
- results.push({
588
- setIdx: s + 1,
589
- sessionIdShort: sidShort,
590
- entries: rows.length,
591
- variants: variantResults,
592
- })
593
- } else {
594
- // Legacy single-baseline payload shape.
595
- const runs = []
596
- for (let r = 0; r < repeat; r++) {
597
- const run = await runOnce(rows, null)
598
- runs.push({ repIdx: r + 1, ...run })
599
- }
600
- results.push({
601
- setIdx: s + 1,
602
- sessionIdShort: sidShort,
603
- entries: rows.length,
604
- runs,
605
- })
606
- }
607
- }
608
- sendJson(res, {
609
- ok: true,
610
- sets, repeat,
611
- windowsAvailable: windows.length,
612
- variants: variants ? variantList.map(v => v.name) : null,
613
- results,
614
- })
615
- } catch (e) {
616
- sendError(res, e?.message || String(e))
617
- }
618
- return
619
- }
620
427
 
621
428
  if (req.method === 'POST' && req.url === '/api/tool') {
622
429
  if (!isLocalOrigin(req)) {
@@ -33,8 +33,9 @@ import {
33
33
  promoteCoreCandidate,
34
34
  dismissCoreCandidate,
35
35
  CORE_SUMMARY_MAX,
36
+ normalizeCoreInput,
36
37
  } from './core-memory-store.mjs'
37
- import { resolveProjectScope } from './project-id-resolver.mjs'
38
+ import { resolveProjectId, resolveProjectScope } from './project-id-resolver.mjs'
38
39
  import { resolvePluginData } from '../../shared/plugin-paths.mjs'
39
40
  import { getMetaValue, isBootstrapComplete } from './memory.mjs'
40
41
 
@@ -57,6 +58,7 @@ export function createMemoryActionHandlers({
57
58
  getCycle3CallLlm,
58
59
  ingestTranscriptFile,
59
60
  cwdFromTranscriptPath,
61
+ editCoreImpl = editCore,
60
62
  }) {
61
63
  const DATA_DIR = dataDir
62
64
 
@@ -643,16 +645,43 @@ export function createMemoryActionHandlers({
643
645
  }
644
646
  // Local trim helper — the manage-block trimOrNull at :1807 is scoped to
645
647
  // that branch and unreachable from here.
646
- // Normalize project_id: 'common' (case-insensitive) or null null (COMMON pool); non-empty string → slug.
648
+ // Only the explicit string "common" selects the COMMON pool. Core add
649
+ // validates missing, null, and blank project_id below; edits use the
650
+ // target entry's stored pool.
647
651
  const hasProjectIdKey = Object.prototype.hasOwnProperty.call(args, 'project_id')
652
+ const projectIdText = typeof args.project_id === 'string' ? args.project_id.trim() : ''
648
653
  const projectId = (() => {
649
- if (!hasProjectIdKey || args.project_id == null) return null
650
- const s = String(args.project_id).trim()
651
- if (s === '' || s.toLowerCase() === 'common') return null
652
- if (s === '*') return '*'
653
- return s
654
+ if (projectIdText.toLowerCase() === 'common') return null
655
+ return projectIdText || null
654
656
  })()
655
657
  try {
658
+ if (op === 'add' || op === 'edit') {
659
+ const normalized = normalizeCoreInput(args, {
660
+ requireElement: true,
661
+ requireSummary: true,
662
+ requireCategory: true,
663
+ })
664
+ const errors = [...normalized.errors]
665
+ if (op === 'add') {
666
+ if (!hasProjectIdKey || !projectIdText) {
667
+ const inferredProjectId = typeof args.cwd === 'string' && args.cwd
668
+ ? resolveProjectId(args.cwd)
669
+ : null
670
+ errors.unshift(
671
+ `project_id required — pass "common" for COMMON pool, or project slug like "owner/repo" for scoped pool` +
672
+ (typeof inferredProjectId === 'string' && /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)?$/.test(inferredProjectId)
673
+ ? ` (cwd suggests "${inferredProjectId}")`
674
+ : ''),
675
+ )
676
+ } else if (projectId === '*') {
677
+ errors.unshift('project_id "*" only valid for op="list"')
678
+ }
679
+ }
680
+ if (errors.length) {
681
+ return { text: `core ${op}: ${errors.join('; ')}`, isError: true }
682
+ }
683
+ args = { ...args, element: normalized.element, summary: normalized.summary, category: normalized.category }
684
+ }
656
685
  if (projectId === '*' && op !== 'list') {
657
686
  return { text: `core ${op}: project_id "*" only valid for op="list"`, isError: true }
658
687
  }
@@ -681,14 +710,11 @@ export function createMemoryActionHandlers({
681
710
  return { text: lines.join('\n') }
682
711
  }
683
712
  if (op === 'add') {
684
- if (!hasProjectIdKey) {
685
- return { text: 'core add: project_id required — pass "common" for COMMON pool, or project slug like "owner/repo" for scoped pool', isError: true }
686
- }
687
713
  const entry = await addCore(coreDataDir, args, projectId)
688
714
  return { text: `core added (id=${entry.id}): [${entry.category}] ${entry.element} — ${entry.summary.slice(0, 200)}` }
689
715
  }
690
716
  if (op === 'edit') {
691
- const entry = await editCore(coreDataDir, args.id, args)
717
+ const entry = await editCoreImpl(coreDataDir, args.id, args)
692
718
  return { text: `core edited (id=${entry.id}): [${entry.category}] ${entry.element} — ${entry.summary.slice(0, 200)}` }
693
719
  }
694
720
  if (op === 'delete') {
@@ -8,20 +8,20 @@ export const TOOL_DEFS = [
8
8
  name: 'memory',
9
9
  title: 'Memory Cycle',
10
10
  annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
11
- description: 'Core-memory mutation and status. Use recall for retrieval. Persist with action:"core" op:"add" when: user corrects/confirms your approach, states a durable preference, or says "remember". Core add/edit requires project_id + category + element + summary. Only what code/git/docs cannot derive; include the why. Never transient task state. No prior recall dedup needed — the cycle dedups.',
11
+ description: 'Core-memory mutation and status; use recall for retrieval. Store durable rules/preferences/facts, one short clause each never transient task state. add requires project_id+category+summary; edit works by id alone.',
12
12
  inputSchema: {
13
13
  type: 'object',
14
14
  properties: {
15
15
  action: { type: 'string', enum: ['core','status'], description: 'Operation.' },
16
16
  op: { type: 'string', enum: ['add','edit','delete','list','candidates','promote','dismiss'], description: 'Mutation op. candidates/promote/dismiss drive core-memory proposal approval.' },
17
17
  id: { type: 'number', description: 'Exact memory id.' },
18
- element: { type: 'string', maxLength: 40, description: 'Memory key/title. Max 40 chars.' },
19
- summary: { type: 'string', maxLength: 100, description: 'Memory content: 1 fact, 1-2 sentences, max 100 chars.' },
18
+ element: { type: 'string', maxLength: 40, description: 'Memory key/title. Defaults to the first 40 chars of summary. Max 40 chars.' },
19
+ summary: { type: 'string', maxLength: 100, description: 'Memory content: one short fact, max 100 chars.' },
20
20
  category: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'], description: 'Category.' },
21
21
  status: { type: 'string', enum: ['pending','active','archived'], description: 'Lifecycle status.' },
22
22
  limit: { type: 'number', description: 'Max rows/items.' },
23
23
  confirm: { type: 'string', description: 'Exact confirmation phrase for destructive actions.' },
24
- project_id: { type: 'string', description: 'Core pool: common, slug, or *. Required for core add/edit; there is no default pool.' },
24
+ project_id: { type: 'string', description: 'Core pool: explicit common or slug. Required for core add only (edit uses the id\'s stored pool); there is no default pool.' },
25
25
  },
26
26
  additionalProperties: false,
27
27
  required: ['action'],
@@ -31,7 +31,7 @@ export const TOOL_DEFS = [
31
31
  name: 'recall',
32
32
  title: 'Recall',
33
33
  annotations: { title: 'Recall', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
34
- description: 'Retrieve stored memory/session history. Call when a task ties to prior work resumes, continuations, or references to earlier decisions/messages. Do not call as a reflexive pre-step, to verify a just-made decision, or before storing memory. Query is topic/semantic search, not regex. Patterns: period:"last" browses recent sessions; +query narrows topic; sessionId without query for current session; period:"3h"/"30m"/"24h" for recent; YYYY-MM-DD, date ranges, or HH:MM~HH:MM for time windows; id for exact follow-up.',
34
+ description: 'Retrieve stored memory/session history for prior-work context (resumes, earlier decisions); not a reflexive pre-step. Query is topic/semantic, not regex; period selects the time window; id for exact follow-up.',
35
35
  inputSchema: {
36
36
  type: 'object',
37
37
  properties: {
@@ -44,7 +44,6 @@ export const TOOL_DEFS = [
44
44
  category: { anyOf: [{ type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, { type: 'array', items: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, minItems: 1 }], description: 'Category filter.' },
45
45
  includeArchived: { type: 'boolean', description: 'Include archived entries.' },
46
46
  sessionId: { type: 'string', description: 'Scoped session id.' },
47
- session_id: { type: 'string', description: 'Alias for sessionId.' },
48
47
  includeMembers: { type: 'boolean', description: 'Include chunk members in output; does not widen the search pool.' },
49
48
  includeRaw: { type: 'boolean', description: 'Include unchunked raw/episode rows.' },
50
49
  sessionOnly: { type: 'boolean', description: 'Search this session only.' },
@@ -14,6 +14,17 @@ import {
14
14
  loadLatestMixdogConfigFromBackup,
15
15
  hasUserDataInitMarker,
16
16
  } from './user-data-guard.mjs'
17
+ import {
18
+ AGENT_PROVIDER_ENV,
19
+ AGENT_PROVIDER_ENV_ALIASES,
20
+ getAgentApiKey,
21
+ } from './provider-api-key.mjs'
22
+
23
+ export {
24
+ AGENT_PROVIDER_ENV,
25
+ AGENT_PROVIDER_ENV_ALIASES,
26
+ getAgentApiKey,
27
+ }
17
28
 
18
29
  const _require = createRequire(import.meta.url)
19
30
  const { getSecret: _getSecret, setSecret: _setSecret, deleteSecret: _deleteSecret, hasSecret: _hasSecret } = _require('../../lib/keychain-cjs.cjs')
@@ -453,40 +464,6 @@ export function getWebhookAuthtoken() {
453
464
  return _readSecret(SECRET_ACCOUNTS.webhookAuth)
454
465
  }
455
466
 
456
- // Standard provider env names take precedence so existing OPENAI_API_KEY-style
457
- // exports keep working, then MIXDOG_AGENT_<P>_APIKEY, then the OS keychain.
458
- // SSOT for agent API-key providers: setup-server.mjs and config-merge.mjs import
459
- // this so UI key-presence detection uses the exact same predicate the runtime
460
- // loads from. Add a provider here and every path picks it up.
461
- export const AGENT_PROVIDER_ENV = Object.freeze({
462
- openai: 'OPENAI_API_KEY', anthropic: 'ANTHROPIC_API_KEY', gemini: 'GEMINI_API_KEY',
463
- deepseek: 'DEEPSEEK_API_KEY', xai: 'XAI_API_KEY', 'opencode-go': 'OPENCODE_API_KEY',
464
- })
465
-
466
- // Last-resort env aliases honored AFTER the standard env / MIXDOG_AGENT_* /
467
- // keychain sources. GROK_API_KEY is the established xAI alias elsewhere in the
468
- // repo (search discovery, xai-api/grok-oauth backends), so honoring it here
469
- // keeps provider discovery and dispatch resolving the same credential.
470
- export const AGENT_PROVIDER_ENV_ALIASES = Object.freeze({
471
- xai: ['GROK_API_KEY'],
472
- })
473
-
474
- /**
475
- * Returns the API key for an agent provider.
476
- * Priority: <PROVIDER>_API_KEY env -> MIXDOG_AGENT_<PROVIDER>_APIKEY -> keychain('agent.<provider>.apiKey') -> alias env (e.g. GROK_API_KEY for xai) -> null.
477
- * Never reads mixdog-config.json — provider keys are keychain-only.
478
- */
479
- export function getAgentApiKey(provider) {
480
- const std = AGENT_PROVIDER_ENV[provider]
481
- if (std && process.env[std]) return process.env[std]
482
- const fromStore = _readSecret(SECRET_ACCOUNTS.agentApiKey(provider))
483
- if (fromStore) return fromStore
484
- for (const alias of AGENT_PROVIDER_ENV_ALIASES[provider] || []) {
485
- if (process.env[alias]) return process.env[alias]
486
- }
487
- return null
488
- }
489
-
490
467
  export function getOpenAIUsageSessionKey() {
491
468
  return process.env.OPENAI_USAGE_SESSION_KEY
492
469
  || process.env.OPENAI_DASHBOARD_SESSION_KEY