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.
- package/package.json +2 -1
- package/scripts/lib/run-node-tests.mjs +13 -0
- package/scripts/session-efficiency-diag.mjs +283 -0
- package/src/defaults/skills/computer-use/SKILL.md +3 -3
- package/src/defaults/skills/local-provider/SKILL.md +2 -1
- package/src/defaults/skills/setup/SKILL.md +4 -4
- package/src/defaults/skills/setup/references/actions.md +1 -3
- package/src/defaults/skills/setup/references/surfaces.md +2 -3
- package/src/headless-exec.mjs +1 -0
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
- package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +163 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +23 -4
- package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1 -28
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/provider-base-url.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +14 -4
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -2
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +22 -1
- package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
- package/src/runtime/computer-bridge/action-schema.mjs +3 -3
- package/src/runtime/computer-bridge/actions.mjs +4 -0
- package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
- package/src/runtime/computer-bridge/client.mjs +10 -183
- package/src/runtime/computer-bridge/core-actions.mjs +2 -1
- package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
- package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
- package/src/runtime/local-provider/asset-installer.mjs +20 -10
- package/src/runtime/local-provider/catalog.mjs +4 -7
- package/src/runtime/local-provider/data/manifest.json +14 -1
- package/src/runtime/local-provider/hardware.mjs +36 -5
- package/src/runtime/local-provider/resumable-installations.mjs +2 -2
- package/src/runtime/local-provider/server.mjs +1 -1
- package/src/runtime/memory/index.mjs +2 -4
- package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
- package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
- package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
- package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
- package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
- package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
- package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
- package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
- package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
- package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
- package/src/runtime/memory/lib/memory.mjs +79 -32
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
- package/src/runtime/office/com/com-adapter.mjs +3 -0
- package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
- package/src/runtime/office/pdf/pdf-render.mjs +2 -6
- package/src/runtime/office/pdf/pdf-security.mjs +8 -2
- package/src/runtime/office/portable/font-provisioner.mjs +28 -0
- package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
- package/src/runtime/office/portable/portable-soffice.mjs +44 -7
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
- package/src/runtime/shared/llm/default-context-window.mjs +42 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
- package/src/runtime/shared/llm/model-catalog.mjs +2 -0
- package/src/runtime/shared/model-alias.mjs +5 -0
- package/src/runtime/shared/plugin-paths.mjs +9 -2
- package/src/runtime/shared/worker-exec-argv.mjs +25 -0
- package/src/runtime/web-search/lib/formatter.mjs +43 -18
- package/src/session-runtime/automation-agents.mjs +88 -0
- package/src/session-runtime/automation-workflow.mjs +19 -6
- package/src/session-runtime/boot/apis.mjs +6 -1
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
- package/src/session-runtime/model-recency.mjs +10 -2
- package/src/session-runtime/model-route/set-route.mjs +7 -1
- package/src/session-runtime/provider-auth/login.mjs +26 -1
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +17 -2
- package/src/session-runtime/resource-api.mjs +8 -2
- package/src/session-runtime/resource-plugins-api.mjs +14 -2
- package/src/session-runtime/route-state.mjs +6 -2
- package/src/session-runtime/schedule-session-run.mjs +7 -1
- package/src/session-runtime/services/agent-tool/render.mjs +0 -5
- package/src/session-runtime/services/provider-admin.mjs +201 -3
- package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
- package/src/session-runtime/setup-tool/executor.mjs +7 -13
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
- package/src/session-runtime/tool-policy-refresh.mjs +0 -2
- package/src/session-runtime/webhook-session-run.mjs +10 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
- package/src/standalone/daemon-agent-control.mjs +8 -1
- package/src/standalone/daemon.mjs +4 -0
- package/src/standalone/session-protocol.mjs +4 -0
- package/src/tui/app/model-options.mjs +10 -1
- package/src/tui/dist/index.mjs +12 -3
- package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
- package/src/tui/session/live-share.mjs +16 -1
- package/src/tui/session/notification-plan.mjs +4 -0
- package/src/tui/session/session-api/integrations.mjs +12 -0
- 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
|
|
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 =
|
|
77
|
-
(
|
|
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 {
|
|
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 =
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
198
|
-
status = 'active', archived_at = NULL, updated_at = $
|
|
199
|
-
WHERE id = $
|
|
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,
|
|
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,
|
|
208
|
-
VALUES ($1, $2, $3, $4, $5
|
|
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,
|
|
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`
|
|
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
|
-
|
|
318
|
-
|
|
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 {
|
|
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
|
|
38
|
-
AND
|
|
39
|
-
|
|
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
|
-
|
|
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, {
|
|
85
|
-
const where = [
|
|
112
|
+
async function countSessionUnchunkedRows(db, { dueAt = null } = {}) {
|
|
113
|
+
const where = [cycle1UnchunkedSql()];
|
|
86
114
|
const params = [];
|
|
87
|
-
if (
|
|
88
|
-
params.push(
|
|
89
|
-
where.push('
|
|
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, {
|
|
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
|
|
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
|
|
140
|
-
AND
|
|
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
|
|
166
|
-
AND (
|
|
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)
|
|
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 (
|
|
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
|
-
|
|
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}
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
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) {
|