mixdog 1.0.8 → 1.0.10

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 (124) hide show
  1. package/package.json +2 -1
  2. package/scripts/lib/run-node-tests.mjs +13 -0
  3. package/scripts/session-efficiency-diag.mjs +283 -0
  4. package/src/defaults/skills/computer-use/SKILL.md +3 -3
  5. package/src/defaults/skills/local-provider/SKILL.md +2 -1
  6. package/src/defaults/skills/setup/SKILL.md +4 -4
  7. package/src/defaults/skills/setup/references/actions.md +1 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +2 -3
  9. package/src/headless-exec.mjs +1 -0
  10. package/src/rules/shared/70-delivery.md +2 -2
  11. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +8 -0
  12. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
  13. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
  16. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
  17. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
  18. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
  19. package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
  20. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
  21. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
  22. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +163 -0
  23. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
  24. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
  26. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +23 -4
  27. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
  28. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
  29. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +3 -2
  31. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1 -28
  33. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
  35. package/src/runtime/agent/orchestrator/providers/provider-base-url.mjs +24 -0
  36. package/src/runtime/agent/orchestrator/providers/registry.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
  38. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
  39. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -2
  40. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
  41. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
  42. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
  44. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
  45. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
  46. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
  47. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
  48. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
  49. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +22 -1
  50. package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
  51. package/src/runtime/computer-bridge/action-schema.mjs +3 -3
  52. package/src/runtime/computer-bridge/actions.mjs +4 -0
  53. package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
  54. package/src/runtime/computer-bridge/client.mjs +10 -183
  55. package/src/runtime/computer-bridge/core-actions.mjs +2 -1
  56. package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
  57. package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
  58. package/src/runtime/local-provider/asset-installer.mjs +20 -10
  59. package/src/runtime/local-provider/catalog.mjs +4 -7
  60. package/src/runtime/local-provider/data/manifest.json +14 -1
  61. package/src/runtime/local-provider/hardware.mjs +36 -5
  62. package/src/runtime/local-provider/resumable-installations.mjs +2 -2
  63. package/src/runtime/local-provider/server.mjs +1 -1
  64. package/src/runtime/memory/index.mjs +2 -4
  65. package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
  66. package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
  67. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
  68. package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
  69. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
  70. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
  71. package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
  72. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
  73. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  74. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
  75. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
  76. package/src/runtime/memory/lib/memory.mjs +79 -32
  77. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
  78. package/src/runtime/office/com/com-adapter.mjs +3 -0
  79. package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
  80. package/src/runtime/office/pdf/pdf-render.mjs +2 -6
  81. package/src/runtime/office/pdf/pdf-security.mjs +8 -2
  82. package/src/runtime/office/portable/font-provisioner.mjs +28 -0
  83. package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
  84. package/src/runtime/office/portable/portable-soffice.mjs +44 -7
  85. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
  86. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
  87. package/src/runtime/shared/llm/default-context-window.mjs +42 -0
  88. package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
  89. package/src/runtime/shared/llm/model-catalog.mjs +2 -0
  90. package/src/runtime/shared/model-alias.mjs +5 -0
  91. package/src/runtime/shared/plugin-paths.mjs +9 -2
  92. package/src/runtime/shared/worker-exec-argv.mjs +25 -0
  93. package/src/runtime/web-search/lib/formatter.mjs +43 -18
  94. package/src/session-runtime/automation-agents.mjs +88 -0
  95. package/src/session-runtime/automation-workflow.mjs +19 -6
  96. package/src/session-runtime/boot/apis.mjs +6 -1
  97. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
  98. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
  99. package/src/session-runtime/model-recency.mjs +10 -2
  100. package/src/session-runtime/model-route/set-route.mjs +7 -1
  101. package/src/session-runtime/provider-auth/login.mjs +26 -1
  102. package/src/session-runtime/quick-model-rows/provider-rows.mjs +17 -2
  103. package/src/session-runtime/resource-api.mjs +8 -2
  104. package/src/session-runtime/resource-plugins-api.mjs +14 -2
  105. package/src/session-runtime/route-state.mjs +6 -2
  106. package/src/session-runtime/schedule-session-run.mjs +7 -1
  107. package/src/session-runtime/services/agent-tool/render.mjs +0 -5
  108. package/src/session-runtime/services/provider-admin.mjs +201 -3
  109. package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
  110. package/src/session-runtime/setup-tool/executor.mjs +7 -13
  111. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
  112. package/src/session-runtime/tool-policy-refresh.mjs +0 -2
  113. package/src/session-runtime/webhook-session-run.mjs +10 -11
  114. package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
  115. package/src/standalone/daemon-agent-control.mjs +8 -1
  116. package/src/standalone/daemon.mjs +4 -0
  117. package/src/standalone/session-protocol.mjs +4 -0
  118. package/src/tui/app/model-options.mjs +10 -1
  119. package/src/tui/dist/index.mjs +12 -3
  120. package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
  121. package/src/tui/session/live-share.mjs +16 -1
  122. package/src/tui/session/notification-plan.mjs +4 -0
  123. package/src/tui/session/session-api/integrations.mjs +12 -0
  124. package/src/session-runtime/bridge-first-use-gate.mjs +0 -72
@@ -1,10 +1,37 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { cpus, totalmem } from 'node:os';
2
3
  import { promisify } from 'node:util';
3
4
 
4
5
  const execFileAsync = promisify(execFile);
5
6
  const GPU_FIELDS = 'index,uuid,name,memory.total,memory.free';
6
7
  const CACHE_MS = 5_000;
7
8
 
9
+ /** The manifest runtime this host runs: CUDA on Windows x64, Metal on Apple
10
+ * Silicon; '' where no llama.cpp build is pinned. */
11
+ export function localProviderPlatformKey(platform = process.platform, arch = process.arch) {
12
+ if (platform === 'win32' && arch === 'x64') return 'win32-x64-nvidia';
13
+ if (platform === 'darwin' && arch === 'arm64') return 'darwin-arm64-metal';
14
+ return '';
15
+ }
16
+
17
+ /** Apple Silicon has one GPU on unified memory. Metal caps what it wires for
18
+ * the GPU: the `iogpu.wired_limit_mb` override when set, else two thirds of
19
+ * RAM — exactly what llama.cpp's Metal backend reports on a 16 GB M4; larger
20
+ * Macs may allow more, which this leaves unused. Metal reports that whole
21
+ * working set as free, so free equals total here as it does in llama.cpp. */
22
+ export function appleSiliconGpu({ name, totalBytes, wiredLimitMb }) {
23
+ const memoryBytes = wiredLimitMb > 0 ? wiredLimitMb * 1024 ** 2 : Math.floor((totalBytes * 2) / 3);
24
+ return { index: 0, uuid: 'MTL0', vendor: 'Apple', name, memoryBytes, freeMemoryBytes: memoryBytes };
25
+ }
26
+
27
+ async function queryAppleSilicon() {
28
+ const limit = await execFileAsync('/usr/sbin/sysctl', ['-n', 'iogpu.wired_limit_mb'], {
29
+ encoding: 'utf8',
30
+ timeout: 5_000,
31
+ }).catch(() => ({ stdout: '0' })); // macOS before 14 has no such key
32
+ return { name: cpus()[0]?.model || 'Apple Silicon', totalBytes: totalmem(), wiredLimitMb: Number(limit.stdout) || 0 };
33
+ }
34
+
8
35
  export function parseNvidiaGpus(output) {
9
36
  return String(output || '')
10
37
  .split(/\r?\n/)
@@ -40,12 +67,14 @@ export function createHardwareProbe({
40
67
  maxBuffer: 64 * 1024,
41
68
  })
42
69
  ).stdout,
70
+ appleQueryFn = queryAppleSilicon,
43
71
  platform = process.platform,
44
72
  arch = process.arch,
45
73
  now = Date.now,
46
74
  cacheMs = CACHE_MS,
47
75
  } = {}) {
48
- const platformSupported = platform === 'win32' && arch === 'x64';
76
+ const platformSupported = Boolean(localProviderPlatformKey(platform, arch));
77
+ const apple = platform === 'darwin';
49
78
  let value = {
50
79
  platform,
51
80
  arch,
@@ -70,12 +99,14 @@ export function createHardwareProbe({
70
99
  return value;
71
100
  };
72
101
  pending = Promise.resolve()
73
- .then(queryFn)
102
+ .then(apple ? appleQueryFn : queryFn)
74
103
  .then(
75
104
  (output) => {
76
- const gpus = parseNvidiaGpus(output).sort(
77
- (a, b) => b.memoryBytes - a.memoryBytes || b.freeMemoryBytes - a.freeMemoryBytes || a.index - b.index
78
- );
105
+ const gpus = apple
106
+ ? [appleSiliconGpu(output)]
107
+ : parseNvidiaGpus(output).sort(
108
+ (a, b) => b.memoryBytes - a.memoryBytes || b.freeMemoryBytes - a.freeMemoryBytes || a.index - b.index
109
+ );
79
110
  return settle({
80
111
  gpu: gpus[0] || null,
81
112
  gpus,
@@ -1,5 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import { LOCAL_PROVIDER_MANIFEST, exactLocalProviderFile, localProviderRuntimeRoot } from './catalog.mjs';
2
+ import { exactLocalProviderFile, localProviderRuntimePlatformEntry, localProviderRuntimeRoot } from './catalog.mjs';
3
3
  import { partialAssetBytes } from './asset-storage.mjs';
4
4
 
5
5
  // Partial files are durable evidence after a process restart. They do not
@@ -27,7 +27,7 @@ export function resumableLocalInstallations(catalog, live, dataDir) {
27
27
  entries.push(pausedEntry('model', model.id, receivedBytes, model.sizeBytes));
28
28
  }
29
29
  if (!catalog.runtime.installed && !entries.some((entry) => entry.phase === 'runtime')) {
30
- const assets = LOCAL_PROVIDER_MANIFEST.runtime.platforms['win32-x64-nvidia'].assets;
30
+ const assets = localProviderRuntimePlatformEntry()?.assets || [];
31
31
  const receivedBytes = assets.reduce((sum, asset) => {
32
32
  const path = join(localProviderRuntimeRoot(dataDir), '.downloads', asset.name);
33
33
  return sum + (exactLocalProviderFile(path, asset.size) ? asset.size : partialAssetBytes(path, asset.size));
@@ -83,7 +83,7 @@ export async function ensureLocalProviderServer(modelId, { dataDir = resolvePlug
83
83
  const hardware = await detectLocalProviderHardware({ refresh: true });
84
84
  startupSignal?.throwIfAborted();
85
85
  const gpu = selectLocalProviderGpu(hardware, entry);
86
- return { gpu, env: { CUDA_VISIBLE_DEVICES: gpu.uuid } };
86
+ return { gpu, env: gpu.vendor === 'NVIDIA' ? { CUDA_VISIBLE_DEVICES: gpu.uuid } : {} };
87
87
  },
88
88
  onReady: async ({ baseURL, apiKey, loadTimeMs }, startupSignal) => {
89
89
  let props = {};
@@ -59,7 +59,6 @@ import {
59
59
  resolveCoalesceMaxRetries,
60
60
  scheduleCoalescedCycleRetry,
61
61
  } from './lib/memory-cycle-requests.mjs';
62
- import { backfillCoreEmbeddings } from './lib/core-memory-store.mjs';
63
62
  import { drainEmbeddingReindex } from './lib/embedding-reindex.mjs';
64
63
  import { refreshCoreMemoryFile } from './lib/core-memory-file.mjs';
65
64
  import { resolveProjectId, resolveProjectScope } from './lib/project-id-resolver.mjs';
@@ -420,14 +419,13 @@ function _startEmbeddingReindex() {
420
419
  _embeddingReindexController = controller;
421
420
  const promise = drainEmbeddingReindex({
422
421
  flushEntries: ({ signal }) => flushEmbeddingDirty(db, { signal }),
423
- backfillCore: ({ signal }) => backfillCoreEmbeddings(DATA_DIR, { signal }),
424
422
  signal: controller.signal,
425
423
  })
426
424
  .then((summary) => {
427
- if (summary.attempted > 0 || summary.coreFilled > 0) {
425
+ if (summary.attempted > 0) {
428
426
  __mixdogMemoryLog(
429
427
  `[memory-service] embedding reindex passes=${summary.passes} attempted=${summary.attempted} ` +
430
- `ok=${summary.succeeded} failed=${summary.failed} core=${summary.coreFilled}\n`
428
+ `ok=${summary.succeeded} failed=${summary.failed}\n`
431
429
  );
432
430
  }
433
431
  })
@@ -30,15 +30,10 @@ function normalizeSummary(value) {
30
30
  .trim();
31
31
  }
32
32
 
33
+ // The prompt carries the user's text only; the memory tool addresses entries
34
+ // through its own list results, not through injected ids.
33
35
  export function formatCuratedCoreMemoryLine(row) {
34
- const summary = normalizeSummary(row?.summary);
35
- if (!summary) return '';
36
- const id = Number(row?.id);
37
- const scope = row?.project_id ?? row?.projectId ?? 'common';
38
- const version = row?.index_revision ?? row?.indexRevision;
39
- return Number.isInteger(id) && id > 0 && version
40
- ? `[project=${scope} id=${id} index_revision=${version}] ${summary}`
41
- : summary;
36
+ return normalizeSummary(row?.summary);
42
37
  }
43
38
 
44
39
  function normalizeCuratedEntry(row) {
@@ -1,15 +1,14 @@
1
- import { __mixdogMemoryLog } from './memory-log.mjs';
2
1
 
3
2
  // User-curated core memory store — native PG-backed via core_entries table.
4
3
  // Per-project entries distinguished by project_id column (NULL = COMMON).
5
4
  // Explicit add/edit/delete operations affect only the requested entry.
6
5
  // Generated conversation history never enters this store automatically.
7
6
 
8
- import { getDatabase, embeddingToSql } from './memory.mjs';
7
+ // Entries are injected verbatim and recalled by term match, so no write here
8
+ // embeds: saving never waits on the embedding model.
9
+ import { getDatabase } from './memory.mjs';
9
10
  import { VALID_CATEGORY } from './memory-categories.mjs';
10
- import { cachedEmbedTextBatch } from './memory-embed.mjs';
11
11
  import { checkedConnect } from './pg/adapter.mjs';
12
- import { throwIfAborted } from './memory-cycle-shared.mjs';
13
12
  import { findCoreKeyRows } from './core-memory-uniqueness.mjs';
14
13
 
15
14
  const CORE_ELEMENT_DERIVE_LENGTH = 40;
@@ -75,46 +74,6 @@ function _getDb(dataDir) {
75
74
  return db;
76
75
  }
77
76
 
78
- async function _embedFor(db, element, summary) {
79
- const text = `${element}\n${summary || ''}`.trim();
80
- if (!text) return null;
81
- const [vec] = await cachedEmbedTextBatch(db, [text]);
82
- return Array.isArray(vec) ? vec : null;
83
- }
84
-
85
- // Lazy repair of NULL embeddings on existing rows. Runs once per boot or
86
- // whenever a NULL slips back in via direct SQL. SELECT WHERE embedding IS NULL
87
- // returns 0 rows on a fully-populated table, so this is a fast no-op.
88
- async function _backfillNullEmbeddings(db, options = {}) {
89
- const signal = options?.signal;
90
- throwIfAborted(signal);
91
- // Only refill live cores; legacy archived entries stay inactive.
92
- const r = await db.query(
93
- `SELECT id, element, summary FROM core_entries WHERE embedding IS NULL AND (status IS NULL OR status = 'active')`
94
- );
95
- if (r.rows.length === 0) return 0;
96
- let filled = 0;
97
- for (const row of r.rows) {
98
- throwIfAborted(signal);
99
- const vec = await _embedFor(db, row.element, row.summary);
100
- throwIfAborted(signal);
101
- if (!vec) continue;
102
- await db.query(`UPDATE core_entries SET embedding = $1::halfvec WHERE id = $2 AND embedding IS NULL`, [
103
- embeddingToSql(vec),
104
- row.id,
105
- ]);
106
- filled++;
107
- }
108
- if (filled > 0) {
109
- __mixdogMemoryLog(`[core-memory] backfilled ${filled} NULL embedding(s) on core_entries\n`);
110
- }
111
- return filled;
112
- }
113
-
114
- export async function backfillCoreEmbeddings(dataDir, options = {}) {
115
- const db = _getDb(dataDir);
116
- return await _backfillNullEmbeddings(db, options);
117
- }
118
77
 
119
78
  // NOTE: boot-time core id compaction was REMOVED. Resequencing core_entries.id
120
79
  // to 1..N rewrote primary keys that are handed out as stable `core:N`
@@ -177,8 +136,6 @@ async function _addCoreImpl(dataDir, input, projectId) {
177
136
  if (errors.length) throw new Error(errors.join('; '));
178
137
  const db = _getDb(dataDir);
179
138
  const now = Date.now();
180
- await _backfillNullEmbeddings(db);
181
- const embedding = await _embedFor(db, el, sm);
182
139
 
183
140
  const client = await checkedConnect(db._pool, 'memory');
184
141
  try {
@@ -194,20 +151,20 @@ async function _addCoreImpl(dataDir, input, projectId) {
194
151
  );
195
152
  }
196
153
  const revived = await client.query(
197
- `UPDATE core_entries SET summary = $1, category = $2, embedding = $3::halfvec,
198
- status = 'active', archived_at = NULL, updated_at = $4
199
- WHERE id = $5
154
+ `UPDATE core_entries SET summary = $1, category = $2,
155
+ status = 'active', archived_at = NULL, updated_at = $3
156
+ WHERE id = $4
200
157
  RETURNING id, element, summary, category, project_id, created_at, updated_at`,
201
- [sm, cat, embedding ? embeddingToSql(embedding) : null, now, collisions[0].id]
158
+ [sm, cat, now, collisions[0].id]
202
159
  );
203
160
  await client.query('COMMIT');
204
161
  return { ...revived.rows[0], revived_from_archived: true };
205
162
  }
206
163
  const r = await client.query(
207
- `INSERT INTO core_entries(element, summary, category, project_id, embedding, created_at, updated_at)
208
- VALUES ($1, $2, $3, $4, $5::halfvec, $6, $7)
164
+ `INSERT INTO core_entries(element, summary, category, project_id, created_at, updated_at)
165
+ VALUES ($1, $2, $3, $4, $5, $6)
209
166
  RETURNING id, element, summary, category, project_id, created_at, updated_at`,
210
- [el, sm, cat, projectId, embedding ? embeddingToSql(embedding) : null, now, now]
167
+ [el, sm, cat, projectId, now, now]
211
168
  );
212
169
  await client.query('COMMIT');
213
170
  return r.rows[0];
@@ -276,8 +233,6 @@ async function _editCoreImpl(dataDir, id, patch) {
276
233
  return { ...cur, element: newElement, summary: newSummary, category: newCategory, updated_at: now };
277
234
  }
278
235
 
279
- await _backfillNullEmbeddings(db);
280
- const embedding = await _embedFor(db, newElement, newSummary);
281
236
  const client = await checkedConnect(db._pool, 'memory');
282
237
  try {
283
238
  await client.query('BEGIN');
@@ -289,8 +244,7 @@ async function _editCoreImpl(dataDir, id, patch) {
289
244
  for (const poolKey of new Set(poolKeys)) {
290
245
  await client.query(`SELECT pg_advisory_xact_lock(hashtext($1))`, [poolKey]);
291
246
  }
292
- // Revalidate under the lock. `cur` (and the embedding derived from it) was
293
- // read before any locking, so without this check two concurrent edits both
247
+ // Revalidate under the lock. `cur` was read before any locking, so without this check two concurrent edits both
294
248
  // computed a patch from the same snapshot and the slower one silently
295
249
  // overwrote the newer content.
296
250
  const fresh = (await client.query(`SELECT * FROM core_entries WHERE id = $1 FOR UPDATE`, [numId])).rows[0];
@@ -313,10 +267,9 @@ async function _editCoreImpl(dataDir, id, patch) {
313
267
  }
314
268
  await client.query(
315
269
  `UPDATE core_entries
316
- SET element = $1, summary = $2, category = $3, project_id = $4,
317
- embedding = $5::halfvec, updated_at = $6
318
- WHERE id = $7`,
319
- [newElement, newSummary, newCategory, newProjectId, embedding ? embeddingToSql(embedding) : null, now, numId]
270
+ SET element = $1, summary = $2, category = $3, project_id = $4, updated_at = $5
271
+ WHERE id = $6`,
272
+ [newElement, newSummary, newCategory, newProjectId, now, numId]
320
273
  );
321
274
  await client.query('COMMIT');
322
275
  return {
@@ -1,4 +1,4 @@
1
- import { CYCLE1_OMITTED_COOLDOWN_MS } from '../cycle1/cycle1-plan.mjs';
1
+ import { cycle1RowDueSql, cycle1UnchunkedSql } from '../cycle1/cycle1-rows.mjs';
2
2
 
3
3
  const BACKLOG_WARN_PENDING = 500;
4
4
 
@@ -27,17 +27,13 @@ export function createBacklogProbe({ getDb, ledger, log, flushRawEmbeddings }) {
27
27
  async function probe(now) {
28
28
  const db = getDb();
29
29
  try {
30
- const unchunked = await countRows(
31
- db,
32
- `SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL AND NULLIF(btrim(session_id), '') IS NOT NULL`
33
- );
30
+ const unchunked = await countRows(db, `SELECT COUNT(*) c FROM entries WHERE ${cycle1UnchunkedSql()}`);
34
31
  const unchunkedEligible = await countRows(
35
32
  db,
36
33
  `SELECT COUNT(*) c FROM entries
37
- WHERE chunk_root IS NULL
38
- AND NULLIF(btrim(session_id), '') IS NOT NULL
39
- AND (reviewed_at IS NULL OR reviewed_at < $1)`,
40
- [now - CYCLE1_OMITTED_COOLDOWN_MS]
34
+ WHERE ${cycle1UnchunkedSql()}
35
+ AND ${cycle1RowDueSql('$1')}`,
36
+ [now]
41
37
  );
42
38
  ledger.setBacklog({ unchunked, unchunked_eligible: unchunkedEligible, at: now });
43
39
  if (unchunked > BACKLOG_WARN_PENDING) {
@@ -9,6 +9,13 @@ const CYCLE1_SESSION_CAP = 10;
9
9
  export const CYCLE1_PACKET_MAX_ROWS = 50;
10
10
  export const CYCLE1_MAX_PACKETS = 4;
11
11
  export const CYCLE1_OMITTED_COOLDOWN_MS = 60 * 60 * 1000;
12
+ // A row the classifier leaves raw again waits twice as long each time, up to
13
+ // this. Most such rows are one- or two-row leftovers too short for any summary
14
+ // to come out shorter than they are, so the same prompt yields the same miss:
15
+ // at a flat hourly retry they filled every packet slot of every run (runtime
16
+ // log 2026-10-04..05: 1768 of 1832 classifier calls committed nothing; one
17
+ // two-character row had been sent 278 times).
18
+ export const CYCLE1_OMITTED_COOLDOWN_MAX_MS = 30 * 24 * 60 * 60 * 1000;
12
19
  // A session is chunked only after it has been quiet this long, so one task's
13
20
  // request, work, result and correction reach the classifier together instead
14
21
  // of being split at every scheduler tick.
@@ -4,7 +4,31 @@
4
4
  // terminal / omitted sentinel updates.
5
5
  import { __mixdogMemoryLog } from '../memory-log.mjs';
6
6
  import { throwIfAborted } from '../memory-cycle-shared.mjs';
7
- import { CYCLE1_OMITTED_COOLDOWN_MS } from './cycle1-plan.mjs';
7
+ import { CYCLE1_OMITTED_COOLDOWN_MAX_MS, CYCLE1_OMITTED_COOLDOWN_MS } from './cycle1-plan.mjs';
8
+
9
+ const CYCLE1_OMITTED_MAX_DOUBLINGS = Math.ceil(Math.log2(CYCLE1_OMITTED_COOLDOWN_MAX_MS / CYCLE1_OMITTED_COOLDOWN_MS));
10
+
11
+ /** SQL for "this unchunked row may be sent now": never reviewed, or its
12
+ * cooldown has passed. The cooldown starts at CYCLE1_OMITTED_COOLDOWN_MS and
13
+ * doubles with each further miss counted in error_count, up to the maximum.
14
+ * `nowParam` is the placeholder holding the current time in ms. */
15
+ export function cycle1RowDueSql(nowParam, alias = '') {
16
+ const column = alias ? `${alias}.` : '';
17
+ const doublings = `LEAST(GREATEST(COALESCE(${column}error_count, 0) - 1, 0), ${CYCLE1_OMITTED_MAX_DOUBLINGS})`;
18
+ const cooldown = `LEAST(${CYCLE1_OMITTED_COOLDOWN_MAX_MS}::bigint, ${CYCLE1_OMITTED_COOLDOWN_MS}::bigint * (1::bigint << ${doublings}))`;
19
+ return `(${column}reviewed_at IS NULL OR ${column}reviewed_at < ${nowParam}::bigint - ${cooldown})`;
20
+ }
21
+
22
+ /** SQL for "an unchunked row that belongs to a session". `session_id IS NOT
23
+ * NULL` is implied by the trimmed check; stating it is what lets the planner
24
+ * use the partial index on pending rows. Without it every count scanned the
25
+ * whole table, and the backlog probe alone runs two a minute (EXPLAIN ANALYZE
26
+ * on a 162K-row store: parallel seq scan, 100-170 ms and 21K blocks read,
27
+ * against 32 ms through the index). */
28
+ export function cycle1UnchunkedSql(alias = '') {
29
+ const column = alias ? `${alias}.` : '';
30
+ return `${column}chunk_root IS NULL AND ${column}session_id IS NOT NULL AND NULLIF(btrim(${column}session_id), '') IS NOT NULL`;
31
+ }
8
32
 
9
33
  export function isStructurallyUnchunkableInput(row) {
10
34
  return !String(row?.content ?? '').trim();
@@ -41,19 +65,23 @@ export async function markTerminalRows(db, rowIds, label = 'terminal') {
41
65
  }
42
66
  }
43
67
 
44
- export async function markOmittedRows(db, rowIds) {
68
+ /** Defers the rows a window left raw. Only a miss the classifier produced
69
+ * counts toward the lengthening cooldown (cycle1RowDueSql); `transientRowIds`
70
+ * never got an answer — a provider error, a commit that lost a race — and
71
+ * come back at the base cooldown however long the outage lasts. */
72
+ export async function markOmittedRows(db, rowIds, { transientRowIds = [] } = {}) {
45
73
  const ids = positiveEntryIds(rowIds);
46
74
  if (ids.length === 0) return { attempted: 0, deferred: 0, marked: 0, failed: 0 };
47
75
  try {
48
76
  const result = await db.query(
49
77
  `UPDATE entries
50
78
  SET reviewed_at = $2,
51
- error_count = COALESCE(error_count, 0) + 1
79
+ error_count = COALESCE(error_count, 0) + CASE WHEN id = ANY($3::bigint[]) THEN 0 ELSE 1 END
52
80
  WHERE id = ANY($1::bigint[])
53
81
  AND chunk_root IS NULL
54
82
  AND is_root = 0
55
83
  RETURNING id`,
56
- [ids, Date.now()]
84
+ [ids, Date.now(), positiveEntryIds(transientRowIds)]
57
85
  );
58
86
  const rows = Array.isArray(result?.rows) ? result.rows : [];
59
87
  // A model failure is not permission to retire source data. Keep every
@@ -81,12 +109,12 @@ export function selectRootId(members) {
81
109
  return rootId;
82
110
  }
83
111
 
84
- async function countSessionUnchunkedRows(db, { reviewedBefore = null } = {}) {
85
- const where = ['chunk_root IS NULL', `NULLIF(btrim(session_id), '') IS NOT NULL`];
112
+ async function countSessionUnchunkedRows(db, { dueAt = null } = {}) {
113
+ const where = [cycle1UnchunkedSql()];
86
114
  const params = [];
87
- if (reviewedBefore != null) {
88
- params.push(reviewedBefore);
89
- where.push('(reviewed_at IS NULL OR reviewed_at < $1)');
115
+ if (dueAt != null) {
116
+ params.push(dueAt);
117
+ where.push(cycle1RowDueSql('$1'));
90
118
  }
91
119
  try {
92
120
  const result = await db.query(
@@ -102,7 +130,7 @@ async function countSessionUnchunkedRows(db, { reviewedBefore = null } = {}) {
102
130
  }
103
131
 
104
132
  export function countPendingRows(db) {
105
- return countSessionUnchunkedRows(db, { reviewedBefore: Date.now() - CYCLE1_OMITTED_COOLDOWN_MS });
133
+ return countSessionUnchunkedRows(db, { dueAt: Date.now() });
106
134
  }
107
135
 
108
136
  export function countRawUnchunkedRows(db) {
@@ -115,7 +143,7 @@ export function countRawUnchunkedRows(db) {
115
143
  * classifier prompt. Rows come back newest-first. */
116
144
  export async function fetchCycle1Rows(db, plan, now = Date.now()) {
117
145
  const sessionFilterSql = plan.onlySessionId ? 'AND session_id = $4' : '';
118
- const queryParams = [plan.sessionCap, now - CYCLE1_OMITTED_COOLDOWN_MS, plan.rowsPerSession];
146
+ const queryParams = [plan.sessionCap, now, plan.rowsPerSession];
119
147
  if (plan.onlySessionId) queryParams.push(plan.onlySessionId);
120
148
  queryParams.push(plan.backfillCap);
121
149
  const backfillParam = `$${queryParams.length}`;
@@ -136,9 +164,8 @@ export async function fetchCycle1Rows(db, plan, now = Date.now()) {
136
164
  `WITH eligible_sessions AS (
137
165
  SELECT session_id, MAX(ts) AS latest_ts, MAX(id) AS latest_id, MIN(ts) AS oldest_ts
138
166
  FROM entries
139
- WHERE chunk_root IS NULL
140
- AND NULLIF(btrim(session_id), '') IS NOT NULL
141
- AND (reviewed_at IS NULL OR reviewed_at < $2)
167
+ WHERE ${cycle1UnchunkedSql()}
168
+ AND ${cycle1RowDueSql('$2')}
142
169
  ${sessionFilterSql}
143
170
  GROUP BY session_id
144
171
  ), due_sessions AS (
@@ -162,8 +189,8 @@ export async function fetchCycle1Rows(db, plan, now = Date.now()) {
162
189
  ROW_NUMBER() OVER (PARTITION BY e.session_id ORDER BY e.ts DESC, e.id DESC) AS rn
163
190
  FROM entries e
164
191
  JOIN selected_sessions s ON s.session_id = e.session_id
165
- WHERE e.chunk_root IS NULL
166
- AND (e.reviewed_at IS NULL OR e.reviewed_at < $2)
192
+ WHERE ${cycle1UnchunkedSql('e')}
193
+ AND ${cycle1RowDueSql('$2', 'e')}
167
194
  )
168
195
  SELECT id, ts, role, content, session_id, source_ref, project_id
169
196
  FROM ranked
@@ -66,6 +66,11 @@ async function commitGeneratedChunks(db, generated, signal) {
66
66
  const invalidChunks = generated.invalidChunks;
67
67
  const invalidRowIds = new Set(invalidChunks.flatMap((chunk) => chunk.member_ids || []));
68
68
  const failedRowIds = generated.rawRowIds.filter((id) => invalidRowIds.has(id));
69
+ // Rows that never got an answer from the classifier, as opposed to rows it
70
+ // answered badly or left out: these do not lengthen their retry cooldown.
71
+ const transientRowIds = invalidChunks
72
+ .filter((chunk) => chunk.reason === 'llm_error')
73
+ .flatMap((chunk) => chunk.member_ids || []);
69
74
  const commitStartedAt = Date.now();
70
75
  for (const chunk of generated.chunks) {
71
76
  // A chunk commit is one DB transaction; do not split it with an abort
@@ -97,7 +102,10 @@ async function commitGeneratedChunks(db, generated, signal) {
97
102
  } catch (err) {
98
103
  __mixdogMemoryLog(`[cycle1] chunk commit failed (root=${rootId}): ${err.message}\n`);
99
104
  skippedChunks += 1;
100
- for (const mid of memberIds) failedRowIds.push(mid);
105
+ for (const mid of memberIds) {
106
+ failedRowIds.push(mid);
107
+ transientRowIds.push(mid);
108
+ }
101
109
  }
102
110
  }
103
111
  return {
@@ -107,6 +115,7 @@ async function commitGeneratedChunks(db, generated, signal) {
107
115
  skippedChunks,
108
116
  invalidChunks,
109
117
  failedRowIds,
118
+ transientRowIds,
110
119
  commitStartedAt,
111
120
  };
112
121
  }
@@ -160,7 +169,7 @@ export async function processCycle1Window({ db, rows: originalRows, windowIdx, p
160
169
 
161
170
  const rawRowIds = rows.map((r) => Number(r.id)).filter((id) => !commit.committedRowIds.has(id));
162
171
  const llmOmittedRowIds = rawRowIds.filter((id) => !commit.failedRowIds.includes(id));
163
- const omittedMark = await markOmittedRows(db, rawRowIds);
172
+ const omittedMark = await markOmittedRows(db, rawRowIds, { transientRowIds: commit.transientRowIds });
164
173
  const omittedRowIds = llmOmittedRowIds.concat(prefilteredRowIds);
165
174
 
166
175
  __mixdogMemoryLog(
@@ -10,6 +10,7 @@ import { fileURLToPath } from 'node:url';
10
10
  import { writeProfilePoint } from './model-profile.mjs';
11
11
  import { createCompactVectorCache } from './compact-vector-cache.mjs';
12
12
  import { EMBED_KEEP_WARM_MS } from './embedding-idle-lease.mjs';
13
+ import { workerExecArgv } from '../../shared/worker-exec-argv.mjs';
13
14
  import {
14
15
  getConfiguredEmbeddingModelId,
15
16
  getDefaultEmbeddingDtype,
@@ -47,11 +48,7 @@ const queryEmbeddingCache = createCompactVectorCache({
47
48
  const WORKER_PATH = join(fileURLToPath(import.meta.url), '..', 'embedding-worker.mjs');
48
49
 
49
50
  export function embeddingWorkerExecArgv(execArgv = process.execArgv) {
50
- return (Array.isArray(execArgv) ? execArgv : []).filter((arg) => {
51
- const value = String(arg);
52
- if (value.startsWith('--input-type')) return false;
53
- return !/^--(?:max-old-space-size|max-semi-space-size|initial-old-space-size)(?:=|$)/.test(value);
54
- });
51
+ return workerExecArgv(execArgv);
55
52
  }
56
53
 
57
54
  function embeddingCacheKey(text, inputType, dtype = _configuredDtype) {
@@ -20,7 +20,6 @@ function waitForNextPass(ms, signal) {
20
20
 
21
21
  export async function drainEmbeddingReindex({
22
22
  flushEntries,
23
- backfillCore = async () => 0,
24
23
  signal,
25
24
  pauseMs = DEFAULT_PASS_PAUSE_MS,
26
25
  wait = waitForNextPass,
@@ -45,8 +44,5 @@ export async function drainEmbeddingReindex({
45
44
  await wait(pauseMs, signal);
46
45
  }
47
46
 
48
- throwIfAborted(signal);
49
- const coreFilled = Number((await backfillCore({ signal })) ?? 0);
50
- throwIfAborted(signal);
51
- return { passes, attempted, succeeded, failed, coreFilled };
47
+ return { passes, attempted, succeeded, failed };
52
48
  }
@@ -61,7 +61,6 @@ export function createLifecycleRoutes({
61
61
  archived_roots: stats.archived_roots,
62
62
  unchunked_leaves: stats.unchunked_leaves,
63
63
  core_entries: stats.core_entries,
64
- core_embed_null: stats.core_embed_null,
65
64
  cycle_running: cycleScheduler.getCycleRunning(),
66
65
  cycle_health: cycleScheduler.getCycleHealth(),
67
66
  cycle_backlog: cycleScheduler.getCycleBacklogSnapshot(),
@@ -32,7 +32,7 @@ export function createMaintenanceActions({ getDb, entryStats, getCycleLastRun })
32
32
  `entries: total=${stats.total} roots=${stats.roots} cycle1_raw=${stats.unchunked_leaves} (unchunked leaves)`,
33
33
  `status: ${stats.byStatus.map((r) => `${r.status ?? '?'}:${r.c}`).join(', ') || 'empty'}`,
34
34
  `categories: ${stats.byCategory.map((r) => `${r.category ?? 'NULL'}:${r.c}`).join(', ') || 'empty'}`,
35
- `core_memory: user=${stats.core_entries} embed_null=${stats.core_embed_null}`,
35
+ `core_memory: user=${stats.core_entries}`,
36
36
  `embedding_index: ready dims=${dims}${dimsErr ? ` (meta_read_error: ${dimsErr})` : ''}`,
37
37
  `bootstrap: ${bootstrapComplete ? 'complete' : 'incomplete'}`,
38
38
  `last_cycle1: ${agoText(last.cycle1)}`,
@@ -1,14 +1,14 @@
1
1
  /**
2
- * core-and-meta.mjs — core_entries, the meta key/value table, the cycle
3
- * state view and the bootstrap stamps that record which embedding model and
4
- * dimension the schema was built for.
2
+ * core-and-meta.mjs — core_entries, the meta key/value table and the
3
+ * bootstrap stamps that record which embedding model and dimension the schema
4
+ * was built for.
5
5
  */
6
6
  import { ensureCoreKeyIndex } from '../core-memory-uniqueness.mjs';
7
7
 
8
8
  const UPSERT_META = `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
9
9
  ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`;
10
10
 
11
- export async function ensureCoreEntriesSchema(db, dimCount) {
11
+ export async function ensureCoreEntriesSchema(db) {
12
12
  await db.exec(`
13
13
  CREATE TABLE IF NOT EXISTS core_entries (
14
14
  id BIGSERIAL PRIMARY KEY,
@@ -16,16 +16,12 @@ export async function ensureCoreEntriesSchema(db, dimCount) {
16
16
  summary TEXT NOT NULL,
17
17
  category TEXT NOT NULL,
18
18
  project_id TEXT,
19
- embedding halfvec(${dimCount}),
20
19
  created_at BIGINT NOT NULL,
21
20
  updated_at BIGINT NOT NULL
22
21
  )
23
22
  `);
24
23
  await db.exec(`CREATE INDEX IF NOT EXISTS core_entries_project_idx ON core_entries(project_id)`);
25
24
  await ensureCoreKeyIndex(db);
26
- await db.exec(
27
- `CREATE INDEX IF NOT EXISTS core_entries_embedding_hnsw ON core_entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL`
28
- );
29
25
  }
30
26
 
31
27
  export async function ensureMetaSchema(db) {
@@ -35,20 +31,6 @@ export async function ensureMetaSchema(db) {
35
31
  value JSONB NOT NULL
36
32
  )
37
33
  `);
38
-
39
- // Operational view — used by /health and dashboards. One round-trip,
40
- // covers the metrics that previously needed 6+ COUNT queries.
41
- await db.exec(`
42
- CREATE OR REPLACE VIEW v_cycle_state AS
43
- SELECT
44
- COUNT(*) FILTER (WHERE is_root = 1) AS roots,
45
- COUNT(*) FILTER (WHERE is_root = 1 AND status = 'pending') AS pending,
46
- COUNT(*) FILTER (WHERE is_root = 1 AND status = 'active') AS active,
47
- COUNT(*) FILTER (WHERE is_root = 1 AND status = 'archived') AS archived,
48
- COUNT(*) FILTER (WHERE chunk_root IS NULL) AS unclassified,
49
- COUNT(*) AS total
50
- FROM entries
51
- `);
52
34
  }
53
35
 
54
36
  export async function stampBootstrapMeta(db, dimCount, embeddingIdentity) {