mixdog 0.8.0 → 0.8.1
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 +1 -1
- package/scripts/boot-smoke.mjs +4 -4
- package/scripts/session-context-bench.mjs +172 -0
- package/scripts/tool-smoke.mjs +7 -11
- package/src/mixdog-session-runtime.mjs +121 -12
- package/src/repl.mjs +8 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +3 -3
- package/src/runtime/agent/orchestrator/internal-roles.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +11 -62
- package/src/runtime/agent/orchestrator/session/manager.mjs +19 -7
- package/src/runtime/agent/orchestrator/session/store.mjs +230 -23
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/memory/index.mjs +116 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +10 -10
- package/src/runtime/memory/lib/memory-cycle2.mjs +2 -2
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/pg/adapter.mjs +14 -0
- package/src/runtime/shared/tool-surface.mjs +1 -4
- package/src/tui/App.jsx +10 -2
- package/src/tui/components/ContextPanel.jsx +76 -28
- package/src/tui/components/ToolExecution.jsx +26 -7
- package/src/tui/dist/index.mjs +106 -45
- package/src/ui/tool-card.mjs +1 -3
|
@@ -87,7 +87,7 @@ import { configureEmbedding, embedText, embedTexts, getEmbeddingDims, getEmbeddi
|
|
|
87
87
|
import { startLlmWorker, stopLlmWorker } from './lib/llm-worker-host.mjs'
|
|
88
88
|
import { runCycle1, runCycle2, runCycle3, runUnifiedGate, parseInterval, syncRootEmbedding, applySimpleStatus, applyUpdate, applyMerge, CYCLE2_ACTIVE_TARGET_CAP } from './lib/memory-cycle.mjs'
|
|
89
89
|
import { getInFlightCycle1 } from './lib/memory-cycle1.mjs'
|
|
90
|
-
import { claimAndMarkScheduledCycle, makeCycleRequestSignature, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
90
|
+
import { claimAndMarkScheduledCycle, makeCycleRequestSignature, resolveCoalesceMaxRetries, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
91
91
|
import { searchRelevantHybrid } from './lib/memory-recall-store.mjs'
|
|
92
92
|
import { fetchEntriesByIdsScoped } from './lib/memory-recall-id-patch.mjs'
|
|
93
93
|
import { retrieveEntries } from './lib/memory-retrievers.mjs'
|
|
@@ -1110,24 +1110,34 @@ async function enqueueScheduledCycle3(intervalMs, _reason = 'scheduled') {
|
|
|
1110
1110
|
}
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
|
-
function scheduleScheduledCycle1(config, signature) {
|
|
1113
|
+
function scheduleScheduledCycle1(config, signature, attempt = 0) {
|
|
1114
|
+
const maxRetries = resolveCoalesceMaxRetries(config, 3)
|
|
1115
|
+
if (attempt > maxRetries) {
|
|
1116
|
+
__mixdogMemoryLog('[cycle1] scheduled queue retry cap reached\n')
|
|
1117
|
+
return
|
|
1118
|
+
}
|
|
1114
1119
|
scheduleCoalescedCycleRetry(db, 'cycle1', async () => {
|
|
1115
1120
|
if (_cycle1InFlight) {
|
|
1116
|
-
scheduleScheduledCycle1(config, signature)
|
|
1121
|
+
scheduleScheduledCycle1(config, signature, attempt + 1)
|
|
1117
1122
|
return
|
|
1118
1123
|
}
|
|
1119
1124
|
const result = await _awaitCycle1Run(config, {
|
|
1120
1125
|
coalescedRetry: true,
|
|
1121
1126
|
onCoalescedSuccess: recordCycle1Result,
|
|
1122
1127
|
})
|
|
1123
|
-
if (result?.skippedInFlight) scheduleScheduledCycle1(config, signature)
|
|
1128
|
+
if (result?.skippedInFlight) scheduleScheduledCycle1(config, signature, attempt + 1)
|
|
1124
1129
|
}, config, signature)
|
|
1125
1130
|
}
|
|
1126
1131
|
|
|
1127
|
-
function scheduleScheduledCycle2(config, signature) {
|
|
1132
|
+
function scheduleScheduledCycle2(config, signature, attempt = 0) {
|
|
1133
|
+
const maxRetries = resolveCoalesceMaxRetries(config, 3)
|
|
1134
|
+
if (attempt > maxRetries) {
|
|
1135
|
+
__mixdogMemoryLog('[cycle2] scheduled queue retry cap reached\n')
|
|
1136
|
+
return
|
|
1137
|
+
}
|
|
1128
1138
|
scheduleCoalescedCycleRetry(db, 'cycle2', async () => {
|
|
1129
1139
|
if (_cycle2InFlight) {
|
|
1130
|
-
scheduleScheduledCycle2(config, signature)
|
|
1140
|
+
scheduleScheduledCycle2(config, signature, attempt + 1)
|
|
1131
1141
|
return
|
|
1132
1142
|
}
|
|
1133
1143
|
_cycle2InFlight = true
|
|
@@ -1137,7 +1147,7 @@ function scheduleScheduledCycle2(config, signature) {
|
|
|
1137
1147
|
onCoalescedSuccess: _finalizeCycle2Run,
|
|
1138
1148
|
}, DATA_DIR)
|
|
1139
1149
|
if (result?.skippedInFlight) {
|
|
1140
|
-
scheduleScheduledCycle2(config, signature)
|
|
1150
|
+
scheduleScheduledCycle2(config, signature, attempt + 1)
|
|
1141
1151
|
} else if (result?.coalescedRetryNoop) {
|
|
1142
1152
|
__mixdogMemoryLog('[cycle2] scheduled queue noop\n')
|
|
1143
1153
|
} else if (result?.ok === false) {
|
|
@@ -1151,11 +1161,16 @@ function scheduleScheduledCycle2(config, signature) {
|
|
|
1151
1161
|
}, config, signature)
|
|
1152
1162
|
}
|
|
1153
1163
|
|
|
1154
|
-
function scheduleScheduledCycle3(config, signature) {
|
|
1164
|
+
function scheduleScheduledCycle3(config, signature, attempt = 0) {
|
|
1155
1165
|
const retryConfig = config?.cycle3 || config
|
|
1166
|
+
const maxRetries = resolveCoalesceMaxRetries(retryConfig, 3)
|
|
1167
|
+
if (attempt > maxRetries) {
|
|
1168
|
+
__mixdogMemoryLog('[cycle3] scheduled queue retry cap reached\n')
|
|
1169
|
+
return
|
|
1170
|
+
}
|
|
1156
1171
|
scheduleCoalescedCycleRetry(db, 'cycle3', async () => {
|
|
1157
1172
|
if (_cycle3InFlight) {
|
|
1158
|
-
scheduleScheduledCycle3(config, signature)
|
|
1173
|
+
scheduleScheduledCycle3(config, signature, attempt + 1)
|
|
1159
1174
|
return
|
|
1160
1175
|
}
|
|
1161
1176
|
_cycle3InFlight = true
|
|
@@ -1165,7 +1180,7 @@ function scheduleScheduledCycle3(config, signature) {
|
|
|
1165
1180
|
onCoalescedSuccess: () => setCycleLastRun('cycle3', Date.now()),
|
|
1166
1181
|
})
|
|
1167
1182
|
if (result?.skippedInFlight) {
|
|
1168
|
-
scheduleScheduledCycle3(config, signature)
|
|
1183
|
+
scheduleScheduledCycle3(config, signature, attempt + 1)
|
|
1169
1184
|
} else if (result?.coalescedRetryNoop) {
|
|
1170
1185
|
__mixdogMemoryLog('[cycle3] scheduled queue noop\n')
|
|
1171
1186
|
}
|
|
@@ -1860,6 +1875,93 @@ function renderEntryLines(rows) {
|
|
|
1860
1875
|
return lines.join('\n')
|
|
1861
1876
|
}
|
|
1862
1877
|
|
|
1878
|
+
async function dumpSessionRootChunks(args = {}) {
|
|
1879
|
+
const sessionId = String(args.sessionId || args.session_id || '').trim()
|
|
1880
|
+
if (!sessionId) return { text: '(no current session)', rows: [], chunks: [], isError: true }
|
|
1881
|
+
const includeRaw = args.includeRaw !== false
|
|
1882
|
+
const limit = Math.max(1, Math.min(1000, Number(args.limit) || 1000))
|
|
1883
|
+
const rootRows = (await db.query(`
|
|
1884
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
1885
|
+
element, category, summary, status, score, last_seen_at, project_id
|
|
1886
|
+
FROM entries
|
|
1887
|
+
WHERE session_id = $1 AND is_root = 1
|
|
1888
|
+
ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
1889
|
+
LIMIT $2
|
|
1890
|
+
`, [sessionId, limit])).rows
|
|
1891
|
+
const roots = rootRows.map((r) => ({ ...r, members: [] }))
|
|
1892
|
+
const rootIds = roots.map((r) => Number(r.id)).filter((id) => Number.isFinite(id))
|
|
1893
|
+
const memberRows = rootIds.length > 0
|
|
1894
|
+
? (await db.query(`
|
|
1895
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
|
|
1896
|
+
FROM entries
|
|
1897
|
+
WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
|
|
1898
|
+
ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
1899
|
+
`, [rootIds])).rows
|
|
1900
|
+
: []
|
|
1901
|
+
const byRoot = new Map(roots.map((r) => [Number(r.id), r]))
|
|
1902
|
+
for (const m of memberRows) {
|
|
1903
|
+
const root = byRoot.get(Number(m.chunk_root))
|
|
1904
|
+
if (root) root.members.push(m)
|
|
1905
|
+
}
|
|
1906
|
+
let rawRows = []
|
|
1907
|
+
if (includeRaw) {
|
|
1908
|
+
rawRows = (await db.query(`
|
|
1909
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
|
|
1910
|
+
FROM entries
|
|
1911
|
+
WHERE session_id = $1
|
|
1912
|
+
AND is_root = 0
|
|
1913
|
+
AND (chunk_root IS NULL OR chunk_root = id)
|
|
1914
|
+
ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
1915
|
+
LIMIT $2
|
|
1916
|
+
`, [sessionId, limit])).rows
|
|
1917
|
+
}
|
|
1918
|
+
const chunks = []
|
|
1919
|
+
for (const root of roots) {
|
|
1920
|
+
const memberText = root.members
|
|
1921
|
+
.map((m) => `${m.role === 'assistant' ? 'assistant' : m.role === 'user' ? 'user' : m.role}: ${cleanMemoryText(String(m.content ?? ''))}`)
|
|
1922
|
+
.filter(Boolean)
|
|
1923
|
+
.join('\n')
|
|
1924
|
+
const summary = [root.element, root.summary].map((v) => String(v || '').trim()).filter(Boolean).join(' — ')
|
|
1925
|
+
chunks.push({
|
|
1926
|
+
id: Number(root.id),
|
|
1927
|
+
kind: 'root',
|
|
1928
|
+
ts: Number(root.ts) || 0,
|
|
1929
|
+
sourceTurn: root.source_turn ?? null,
|
|
1930
|
+
category: root.category || null,
|
|
1931
|
+
summary,
|
|
1932
|
+
text: memberText || cleanMemoryText(String(root.content ?? '')),
|
|
1933
|
+
members: root.members,
|
|
1934
|
+
})
|
|
1935
|
+
}
|
|
1936
|
+
for (const raw of rawRows) {
|
|
1937
|
+
chunks.push({
|
|
1938
|
+
id: Number(raw.id),
|
|
1939
|
+
kind: 'raw',
|
|
1940
|
+
ts: Number(raw.ts) || 0,
|
|
1941
|
+
sourceTurn: raw.source_turn ?? null,
|
|
1942
|
+
category: null,
|
|
1943
|
+
summary: '',
|
|
1944
|
+
text: `${raw.role === 'assistant' ? 'assistant' : raw.role === 'user' ? 'user' : raw.role}: ${cleanMemoryText(String(raw.content ?? ''))}`,
|
|
1945
|
+
members: [],
|
|
1946
|
+
})
|
|
1947
|
+
}
|
|
1948
|
+
chunks.sort((a, b) => {
|
|
1949
|
+
const at = Number.isFinite(Number(a.sourceTurn)) ? Number(a.sourceTurn) : 2147483647
|
|
1950
|
+
const bt = Number.isFinite(Number(b.sourceTurn)) ? Number(b.sourceTurn) : 2147483647
|
|
1951
|
+
return (at - bt) || ((a.ts || 0) - (b.ts || 0)) || ((a.id || 0) - (b.id || 0))
|
|
1952
|
+
})
|
|
1953
|
+
const text = chunks.length
|
|
1954
|
+
? chunks.map((chunk, idx) => {
|
|
1955
|
+
const label = chunk.kind === 'root'
|
|
1956
|
+
? `# chunk ${idx + 1} root=${chunk.id}${chunk.category ? ` category=${chunk.category}` : ''}`
|
|
1957
|
+
: `# raw ${idx + 1} id=${chunk.id}`
|
|
1958
|
+
const summary = chunk.summary ? `summary: ${chunk.summary}\n` : ''
|
|
1959
|
+
return `${label}\n${summary}${chunk.text}`.trim()
|
|
1960
|
+
}).join('\n\n')
|
|
1961
|
+
: '(no results)'
|
|
1962
|
+
return { text, rows: [...roots, ...rawRows], chunks }
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1863
1965
|
async function entryStats() {
|
|
1864
1966
|
return await db.transaction(async (tx) => {
|
|
1865
1967
|
const total = (await tx.query(`SELECT COUNT(*) c FROM entries`)).rows[0].c
|
|
@@ -2224,6 +2326,10 @@ async function handleMemoryAction(args, signal) {
|
|
|
2224
2326
|
return ingestSessionMessages(args)
|
|
2225
2327
|
}
|
|
2226
2328
|
|
|
2329
|
+
if (action === 'dump_session_roots') {
|
|
2330
|
+
return dumpSessionRootChunks(args)
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2227
2333
|
if (action === 'manage') {
|
|
2228
2334
|
const op = String(args.op ?? '').trim().toLowerCase()
|
|
2229
2335
|
if (!['add', 'edit', 'delete'].includes(op)) {
|
|
@@ -277,7 +277,7 @@ export async function runCycle1(db, config = {}, options = {}, dataDir = null) {
|
|
|
277
277
|
)
|
|
278
278
|
if (_runCycle1InFlight.has(db)) {
|
|
279
279
|
if (!coalescedRetry) await markCycleRequest(db, 'cycle1', 'in-flight', requestSignature)
|
|
280
|
-
scheduleRetry()
|
|
280
|
+
if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
|
|
281
281
|
logCycle1Throttled('in-flight', '[cycle1] skipped: already in flight for this db\n')
|
|
282
282
|
return {
|
|
283
283
|
processed: 0, chunks: 0, skipped: 0, sessions: 0,
|
|
@@ -301,7 +301,7 @@ export async function runCycle1(db, config = {}, options = {}, dataDir = null) {
|
|
|
301
301
|
if (!gotLock) {
|
|
302
302
|
client.release()
|
|
303
303
|
if (!coalescedRetry) await markCycleRequest(db, 'cycle1', 'advisory-lock', requestSignature)
|
|
304
|
-
scheduleRetry()
|
|
304
|
+
if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
|
|
305
305
|
logCycle1Throttled('advisory-lock', '[cycle1] skipped: advisory lock held by another worker\n')
|
|
306
306
|
return { processed: 0, chunks: 0, skipped: 0, sessions: 0, skippedInFlight: true, pendingRows: await countPendingRows(db) }
|
|
307
307
|
}
|
|
@@ -390,23 +390,23 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
|
|
|
390
390
|
const timeout = callerDeadlineMs > 0
|
|
391
391
|
? Math.min(baseTimeout, Math.max(5000, callerDeadlineMs - 1000))
|
|
392
392
|
: baseTimeout
|
|
393
|
-
// Select
|
|
394
|
-
// session.
|
|
395
|
-
//
|
|
393
|
+
// Select closest/recent sessions first, then fetch closest/recent rows per
|
|
394
|
+
// selected session. Memory fill is recency-first; session isolation below
|
|
395
|
+
// keeps unrelated episodes out of the same classifier prompt.
|
|
396
396
|
const fetchResult = await db.query(
|
|
397
397
|
`WITH selected_sessions AS (
|
|
398
|
-
SELECT session_id,
|
|
398
|
+
SELECT session_id, MAX(ts) AS latest_ts, MAX(id) AS latest_id
|
|
399
399
|
FROM entries
|
|
400
400
|
WHERE chunk_root IS NULL
|
|
401
401
|
AND NULLIF(btrim(session_id), '') IS NOT NULL
|
|
402
402
|
AND (reviewed_at IS NULL OR reviewed_at < $2)
|
|
403
403
|
GROUP BY session_id
|
|
404
|
-
ORDER BY
|
|
404
|
+
ORDER BY latest_ts DESC, latest_id DESC
|
|
405
405
|
LIMIT $1
|
|
406
406
|
), ranked AS (
|
|
407
407
|
SELECT e.id, e.ts, e.role, e.content, e.session_id, e.source_ref, e.project_id,
|
|
408
|
-
s.
|
|
409
|
-
ROW_NUMBER() OVER (PARTITION BY e.session_id ORDER BY e.ts
|
|
408
|
+
s.latest_ts, s.latest_id,
|
|
409
|
+
ROW_NUMBER() OVER (PARTITION BY e.session_id ORDER BY e.ts DESC, e.id DESC) AS rn
|
|
410
410
|
FROM entries e
|
|
411
411
|
JOIN selected_sessions s ON s.session_id = e.session_id
|
|
412
412
|
WHERE e.chunk_root IS NULL
|
|
@@ -415,7 +415,7 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
|
|
|
415
415
|
SELECT id, ts, role, content, session_id, source_ref, project_id
|
|
416
416
|
FROM ranked
|
|
417
417
|
WHERE rn <= $3
|
|
418
|
-
ORDER BY
|
|
418
|
+
ORDER BY latest_ts DESC, latest_id DESC, session_id, ts DESC, id DESC`,
|
|
419
419
|
[sessionCap, Date.now() - CYCLE1_OMITTED_COOLDOWN_MS, batchSize],
|
|
420
420
|
)
|
|
421
421
|
throwIfAborted(signal)
|
|
@@ -901,7 +901,7 @@ export async function runCycle2(db, config = {}, options = {}, dataDir = null) {
|
|
|
901
901
|
}
|
|
902
902
|
if (_runCycle2InFlight.has(db)) {
|
|
903
903
|
if (!coalescedRetry) await markCycleRequest(db, 'cycle2', 'in-flight', requestSignature)
|
|
904
|
-
scheduleRetry()
|
|
904
|
+
if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
|
|
905
905
|
__mixdogMemoryLog('[cycle2] skipped: already in flight for this db\n')
|
|
906
906
|
return { ok: true, ...partial, skippedInFlight: true }
|
|
907
907
|
}
|
|
@@ -921,7 +921,7 @@ export async function runCycle2(db, config = {}, options = {}, dataDir = null) {
|
|
|
921
921
|
if (!gotLock) {
|
|
922
922
|
client.release()
|
|
923
923
|
if (!coalescedRetry) await markCycleRequest(db, 'cycle2', 'advisory-lock', requestSignature)
|
|
924
|
-
scheduleRetry()
|
|
924
|
+
if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
|
|
925
925
|
__mixdogMemoryLog('[cycle2] skipped: advisory lock held by another worker\n')
|
|
926
926
|
return { ok: true, ...partial, skippedInFlight: true }
|
|
927
927
|
}
|
|
@@ -272,7 +272,7 @@ export async function runCycle3(db, config, dataDir, options = {}) {
|
|
|
272
272
|
}
|
|
273
273
|
if (_runCycle3InFlight.has(db)) {
|
|
274
274
|
if (!coalescedRetry) await markCycleRequest(db, 'cycle3', 'in-flight', requestSignature)
|
|
275
|
-
scheduleRetry()
|
|
275
|
+
if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
|
|
276
276
|
__mixdogMemoryLog('[cycle3] skipped: already in flight for this db\n')
|
|
277
277
|
return { ...partial, skippedInFlight: true }
|
|
278
278
|
}
|
|
@@ -291,7 +291,7 @@ export async function runCycle3(db, config, dataDir, options = {}) {
|
|
|
291
291
|
if (!gotLock) {
|
|
292
292
|
client.release()
|
|
293
293
|
if (!coalescedRetry) await markCycleRequest(db, 'cycle3', 'advisory-lock', requestSignature)
|
|
294
|
-
scheduleRetry()
|
|
294
|
+
if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
|
|
295
295
|
__mixdogMemoryLog('[cycle3] skipped: advisory lock held by another worker\n')
|
|
296
296
|
return { ...partial, skippedInFlight: true }
|
|
297
297
|
}
|
|
@@ -4,6 +4,18 @@ function __mixdogMemoryLog(...args) {
|
|
|
4
4
|
return __mixdogMemoryStderrWrite(...args);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function installPoolErrorHandler(pool, label) {
|
|
8
|
+
if (!pool || typeof pool.on !== 'function') return pool
|
|
9
|
+
pool.on('error', (err, client) => {
|
|
10
|
+
const code = err?.code || err?.errno || ''
|
|
11
|
+
const msg = err?.message || String(err || 'unknown error')
|
|
12
|
+
const pid = client?.processID ? ` pid=${client.processID}` : ''
|
|
13
|
+
const suffix = code ? ` ${code}` : ''
|
|
14
|
+
__mixdogMemoryLog(`[pg-adapter] ${label} idle client error${suffix}${pid}: ${msg}\n`)
|
|
15
|
+
})
|
|
16
|
+
return pool
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
// pg-adapter.mjs — PG connection manager for mixdog 0.4.0
|
|
8
20
|
// Single owner: supervisor-pg.ensurePgInstance(dataDir) starts PG.
|
|
9
21
|
// pg-adapter calls supervisor-pg — never pg-process directly.
|
|
@@ -235,6 +247,7 @@ export async function ensurePgInstance(dataDir, opts = {}) {
|
|
|
235
247
|
host, port, user: PG_USER, database: 'postgres',
|
|
236
248
|
password: '', max: 1, idleTimeoutMillis: 5_000,
|
|
237
249
|
})
|
|
250
|
+
installPoolErrorHandler(adminPool, 'admin-pool')
|
|
238
251
|
try {
|
|
239
252
|
// CREATE DATABASE cannot run inside a transaction, so guard the
|
|
240
253
|
// check-then-create with a session-level advisory lock held on a single
|
|
@@ -280,6 +293,7 @@ export async function ensurePgInstance(dataDir, opts = {}) {
|
|
|
280
293
|
password: '', max: 5, idleTimeoutMillis: 30_000,
|
|
281
294
|
connectionTimeoutMillis: 10_000,
|
|
282
295
|
})
|
|
296
|
+
installPoolErrorHandler(pgPool, `pool:${schema}`)
|
|
283
297
|
|
|
284
298
|
// 4. Bootstrap extensions + schemas once (idempotent).
|
|
285
299
|
await bootstrapInstance(pgPool, resolve(dataDir))
|
|
@@ -721,7 +721,7 @@ export function isMemorySurface(label) {
|
|
|
721
721
|
|
|
722
722
|
export const CATEGORY_ORDER = [
|
|
723
723
|
'Read', 'Search', 'Web Research', 'Memory', 'Explore',
|
|
724
|
-
'Patch', '
|
|
724
|
+
'Patch', 'Shell', 'Agent', 'Channel', 'Setup', 'Other',
|
|
725
725
|
];
|
|
726
726
|
|
|
727
727
|
const TOOL_CATEGORY = new Map([
|
|
@@ -751,8 +751,6 @@ const TOOL_CATEGORY = new Map([
|
|
|
751
751
|
['memory', 'Memory'],
|
|
752
752
|
['explore', 'Explore'],
|
|
753
753
|
['apply_patch', 'Patch'],
|
|
754
|
-
['write', 'Edit'],
|
|
755
|
-
['edit', 'Edit'],
|
|
756
754
|
['bash', 'Shell'],
|
|
757
755
|
['shell', 'Shell'],
|
|
758
756
|
['shell_command', 'Shell'],
|
|
@@ -804,7 +802,6 @@ const CATEGORY_COPY = new Map([
|
|
|
804
802
|
['Memory', { active: 'Checking', done: 'Checked', noun: 'memory item' }],
|
|
805
803
|
['Explore', { active: 'Exploring', done: 'Explored', noun: 'item' }],
|
|
806
804
|
['Patch', { active: 'Editing', done: 'Edited', noun: 'item' }],
|
|
807
|
-
['Edit', { active: 'Editing', done: 'Edited', noun: 'item' }],
|
|
808
805
|
['Shell', { active: 'Running', done: 'Ran', noun: 'command' }],
|
|
809
806
|
['Agent', { active: 'Calling', done: 'Called', noun: 'agent' }],
|
|
810
807
|
['Channel', { active: 'Sending', done: 'Sent', noun: 'message' }],
|
package/src/tui/App.jsx
CHANGED
|
@@ -2420,6 +2420,7 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
2420
2420
|
|
|
2421
2421
|
const openContextPicker = () => {
|
|
2422
2422
|
const tools = store.toolsStatus?.() || { activeCount: 0, count: 0, activeTools: [] };
|
|
2423
|
+
const mcp = store.mcpStatus?.() || { connectedCount: 0, configuredCount: 0, failedCount: 0 };
|
|
2423
2424
|
const skills = store.skillsStatus?.() || { count: 0 };
|
|
2424
2425
|
const plugins = store.pluginsStatus?.() || { count: 0 };
|
|
2425
2426
|
const context = store.contextStatus?.() || {};
|
|
@@ -2553,6 +2554,7 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
2553
2554
|
messages: {
|
|
2554
2555
|
tokens: messages.estimatedTokens,
|
|
2555
2556
|
count: messages.count,
|
|
2557
|
+
semantic: messages.semantic,
|
|
2556
2558
|
},
|
|
2557
2559
|
tools: {
|
|
2558
2560
|
schemaTokens: request.toolSchemaTokens,
|
|
@@ -2564,6 +2566,7 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
2564
2566
|
results: messages.toolResultCount,
|
|
2565
2567
|
},
|
|
2566
2568
|
request: {
|
|
2569
|
+
toolSchemaBreakdown: request.toolSchemaBreakdown,
|
|
2567
2570
|
overheadTokens: request.requestOverheadTokens,
|
|
2568
2571
|
reserveTokens: request.reserveTokens,
|
|
2569
2572
|
},
|
|
@@ -2580,6 +2583,11 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
2580
2583
|
skills: skills.count,
|
|
2581
2584
|
plugins: plugins.count,
|
|
2582
2585
|
},
|
|
2586
|
+
mcp: {
|
|
2587
|
+
connected: mcp.connectedCount,
|
|
2588
|
+
configured: mcp.configuredCount,
|
|
2589
|
+
failed: mcp.failedCount,
|
|
2590
|
+
},
|
|
2583
2591
|
},
|
|
2584
2592
|
rows: contextRows,
|
|
2585
2593
|
});
|
|
@@ -4538,7 +4546,7 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
4538
4546
|
return;
|
|
4539
4547
|
}
|
|
4540
4548
|
if (r.changed === false) {
|
|
4541
|
-
store.pushNotice('
|
|
4549
|
+
store.pushNotice('nothing to compact', 'warn');
|
|
4542
4550
|
return;
|
|
4543
4551
|
}
|
|
4544
4552
|
store.pushNotice('Compact done.', 'info');
|
|
@@ -5001,7 +5009,7 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
5001
5009
|
const desiredFloatingPanelRows = picker
|
|
5002
5010
|
? (picker.fillAvailable ? maxFloatingPanelRows : PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS)
|
|
5003
5011
|
: contextPanel
|
|
5004
|
-
? PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS
|
|
5012
|
+
? PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS + 3
|
|
5005
5013
|
: usagePanel
|
|
5006
5014
|
? PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS
|
|
5007
5015
|
: slashPaletteOpen
|
|
@@ -75,25 +75,57 @@ function metricValue(parts) {
|
|
|
75
75
|
return parts.filter(Boolean).join(' · ');
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
const
|
|
78
|
+
function DetailLine({ label, value, columns }) {
|
|
79
|
+
const innerWidth = Math.max(24, Math.floor(columns || 80) - 4);
|
|
80
|
+
const labelWidth = 10;
|
|
81
|
+
const valueWidth = Math.max(0, innerWidth - labelWidth - 2);
|
|
82
|
+
return (
|
|
83
|
+
<Box flexDirection="row" width="100%">
|
|
84
|
+
<Text color={theme.subtle}>{padCells(truncateText(label, labelWidth), labelWidth)}</Text>
|
|
85
|
+
<Text color={theme.inactive}> </Text>
|
|
86
|
+
<Text color={theme.text}>{truncateText(value, valueWidth)}</Text>
|
|
87
|
+
</Box>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function bucketTokens(map, names) {
|
|
92
|
+
return names.reduce((sum, name) => sum + finiteNumber(map?.[name]?.tokens), 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function bucketCount(map, names) {
|
|
96
|
+
return names.reduce((sum, name) => sum + finiteNumber(map?.[name]?.count), 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function semanticTokens(semantic, names) {
|
|
100
|
+
return names.reduce((sum, name) => sum + finiteNumber(semantic?.[name]?.tokens), 0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function CategoryItem({ label, tokens, total, width }) {
|
|
104
|
+
const labelWidth = Math.min(11, Math.max(7, Math.floor(width * 0.22)));
|
|
105
|
+
const pctWidth = 6;
|
|
106
|
+
const tokenWidth = 7;
|
|
107
|
+
const barWidth = Math.max(6, Math.min(20, width - labelWidth - pctWidth - tokenWidth - 4));
|
|
108
|
+
const pct = percent(tokens, total);
|
|
81
109
|
return (
|
|
82
110
|
<Box flexDirection="row" width={width}>
|
|
83
|
-
<Text color={theme.subtle}>{padCells(truncateText(label, labelWidth), labelWidth)}
|
|
84
|
-
<Text color={
|
|
111
|
+
<Text color={theme.subtle}>{padCells(truncateText(label, labelWidth), labelWidth)}</Text>
|
|
112
|
+
<Text color={theme.inactive}> </Text>
|
|
113
|
+
<Text color={usageColor(pct)}>{padCells(percentLabel(tokens, total), pctWidth)}</Text>
|
|
114
|
+
<ProgressBar value={tokens} total={total} width={barWidth} />
|
|
115
|
+
<Text color={theme.inactive}> </Text>
|
|
116
|
+
<Text color={theme.text}>{padCells(formatTokens(tokens), tokenWidth)}</Text>
|
|
85
117
|
</Box>
|
|
86
118
|
);
|
|
87
119
|
}
|
|
88
120
|
|
|
89
|
-
function
|
|
121
|
+
function CategoryGrid({ categories, columns, total }) {
|
|
90
122
|
const innerWidth = Math.max(24, Math.floor(columns || 80) - 4);
|
|
91
|
-
const twoColumns = innerWidth >=
|
|
123
|
+
const twoColumns = innerWidth >= 84;
|
|
92
124
|
if (!twoColumns) {
|
|
93
125
|
return (
|
|
94
126
|
<Box flexDirection="column" width="100%">
|
|
95
|
-
{
|
|
96
|
-
<
|
|
127
|
+
{categories.map((category) => (
|
|
128
|
+
<CategoryItem key={category.label} {...category} total={total} width={innerWidth} />
|
|
97
129
|
))}
|
|
98
130
|
</Box>
|
|
99
131
|
);
|
|
@@ -102,14 +134,14 @@ function MetricGrid({ cells, columns }) {
|
|
|
102
134
|
const leftWidth = Math.floor((innerWidth - gap) / 2);
|
|
103
135
|
const rightWidth = innerWidth - gap - leftWidth;
|
|
104
136
|
const pairs = [];
|
|
105
|
-
for (let i = 0; i <
|
|
137
|
+
for (let i = 0; i < categories.length; i += 2) pairs.push(categories.slice(i, i + 2));
|
|
106
138
|
return (
|
|
107
139
|
<Box flexDirection="column" width="100%">
|
|
108
140
|
{pairs.map((pair, index) => (
|
|
109
141
|
<Box key={index} flexDirection="row" width="100%">
|
|
110
|
-
<
|
|
142
|
+
<CategoryItem {...pair[0]} total={total} width={leftWidth} />
|
|
111
143
|
<Text>{' '.repeat(gap)}</Text>
|
|
112
|
-
{pair[1] ? <
|
|
144
|
+
{pair[1] ? <CategoryItem {...pair[1]} total={total} width={rightWidth} /> : null}
|
|
113
145
|
</Box>
|
|
114
146
|
))}
|
|
115
147
|
</Box>
|
|
@@ -127,6 +159,9 @@ function ContextUsageView({ detail, columns }) {
|
|
|
127
159
|
const lastApi = detail?.lastApi || {};
|
|
128
160
|
const cache = detail?.cache || {};
|
|
129
161
|
const extensions = detail?.extensions || {};
|
|
162
|
+
const mcp = detail?.mcp || {};
|
|
163
|
+
const semantic = messages.semantic || {};
|
|
164
|
+
const schema = request.toolSchemaBreakdown || {};
|
|
130
165
|
const usedTokens = finiteNumber(usage.usedTokens);
|
|
131
166
|
const windowTokens = finiteNumber(usage.windowTokens);
|
|
132
167
|
const freeTokens = windowTokens ? Math.max(0, windowTokens - usedTokens) : finiteNumber(usage.freeTokens);
|
|
@@ -134,25 +169,34 @@ function ContextUsageView({ detail, columns }) {
|
|
|
134
169
|
const summaryText = `${formatTokens(usedTokens)} / ${formatTokens(windowTokens)} · ${formatTokens(freeTokens)} free`;
|
|
135
170
|
const pctText = `${percentLabel(usedTokens, windowTokens)} used`;
|
|
136
171
|
const barWidth = Math.max(12, Math.min(34, innerWidth - stringWidth(summaryText) - stringWidth(pctText) - 5));
|
|
172
|
+
const builtInToolTokens = bucketTokens(schema, ['code', 'web', 'mutation', 'channels', 'setup', 'other']);
|
|
173
|
+
const builtInToolCount = bucketCount(schema, ['code', 'web', 'mutation', 'channels', 'setup', 'other']);
|
|
174
|
+
const projectTokens = semanticTokens(semantic, ['project', 'workspace', 'environment', 'other']);
|
|
137
175
|
const compactionLine = metricValue([
|
|
138
|
-
compaction.stage
|
|
176
|
+
compaction.stage && compaction.stage !== 'pending' ? compaction.stage : '',
|
|
139
177
|
compaction.state,
|
|
140
178
|
compaction.triggerTokens ? `trigger ${formatTokens(compaction.triggerTokens)}` : '',
|
|
141
|
-
compaction.boundaryTokens ? `boundary ${formatTokens(compaction.boundaryTokens)}` : '',
|
|
142
179
|
]);
|
|
143
|
-
const
|
|
144
|
-
usage.
|
|
145
|
-
usage.effective ? 'effective window' : '',
|
|
180
|
+
const sourceLine = metricValue([
|
|
181
|
+
usage.effective ? `effective ${formatTokens(windowTokens)}` : `window ${formatTokens(windowTokens)}`,
|
|
146
182
|
usage.rawWindowTokens && usage.rawWindowTokens !== usage.windowTokens ? `raw ${formatTokens(usage.rawWindowTokens)}` : '',
|
|
147
183
|
]);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
{ label: '
|
|
155
|
-
{ label: '
|
|
184
|
+
const apiLine = metricValue([
|
|
185
|
+
`last ctx ${formatTokens(lastApi.contextTokens)}`,
|
|
186
|
+
`in/out ${formatTokens(lastApi.inputTokens)}/${formatTokens(lastApi.outputTokens)}`,
|
|
187
|
+
`cache ${cache.hitRate || 'n/a'}`,
|
|
188
|
+
]);
|
|
189
|
+
const categories = [
|
|
190
|
+
{ label: 'Messages', tokens: semanticTokens(semantic, ['chat', 'assistant']), meta: '' },
|
|
191
|
+
{ label: 'Tools', tokens: builtInToolTokens, meta: `${tools.active || 0}/${tools.count || 0} active${builtInToolCount ? ` · ${builtInToolCount} defs` : ''}` },
|
|
192
|
+
{ label: 'MCP', tokens: bucketTokens(schema, ['mcp']), meta: `${mcp.connected || 0}/${mcp.configured || 0} servers` },
|
|
193
|
+
{ label: 'Skills', tokens: bucketTokens(schema, ['skills']), meta: `${extensions.skills || 0} skills` },
|
|
194
|
+
{ label: 'Memory', tokens: semanticTokens(semantic, ['memory']) + bucketTokens(schema, ['memory']), meta: 'core + recall tools' },
|
|
195
|
+
{ label: 'Project', tokens: projectTokens, meta: 'mixdog.md · workspace' },
|
|
196
|
+
{ label: 'Workflow', tokens: semanticTokens(semantic, ['workflow']) + bucketTokens(schema, ['agents']), meta: 'workflow · agents' },
|
|
197
|
+
{ label: 'System', tokens: semanticTokens(semantic, ['system']), meta: 'rules · role catalog' },
|
|
198
|
+
{ label: 'Overhead', tokens: finiteNumber(request.overheadTokens), meta: 'request frame' },
|
|
199
|
+
{ label: 'Tool I/O', tokens: semanticTokens(semantic, ['toolResults']), meta: `${toolIo.calls || 0} calls · ${toolIo.results || 0} results` },
|
|
156
200
|
];
|
|
157
201
|
|
|
158
202
|
return (
|
|
@@ -165,11 +209,15 @@ function ContextUsageView({ detail, columns }) {
|
|
|
165
209
|
<Text color={theme.text}>{truncateText(summaryText, Math.max(0, innerWidth - Math.min(10, innerWidth) - barWidth - 3))}</Text>
|
|
166
210
|
</Box>
|
|
167
211
|
<Box marginTop={1} flexDirection="column" width="100%">
|
|
168
|
-
<
|
|
169
|
-
<
|
|
212
|
+
<DetailLine label="Source" value={sourceLine} columns={columns} />
|
|
213
|
+
<DetailLine label="Compaction" value={compactionLine} columns={columns} />
|
|
214
|
+
<DetailLine label="API/cache" value={apiLine} columns={columns} />
|
|
170
215
|
</Box>
|
|
171
216
|
<Box marginTop={1} flexDirection="column" width="100%">
|
|
172
|
-
<
|
|
217
|
+
<Text color={theme.subtle}>Context mix</Text>
|
|
218
|
+
<Box marginTop={1} flexDirection="column" width="100%">
|
|
219
|
+
<CategoryGrid categories={categories} columns={columns} total={windowTokens} />
|
|
220
|
+
</Box>
|
|
173
221
|
</Box>
|
|
174
222
|
</Box>
|
|
175
223
|
);
|
|
@@ -38,6 +38,7 @@ export function summarizeArgs(name, args) {
|
|
|
38
38
|
|
|
39
39
|
export const MAX_RESULT_LINES = 8;
|
|
40
40
|
const TOOL_BLINK_MS = 500;
|
|
41
|
+
const TOOL_BLINK_LIMIT_MS = 3000;
|
|
41
42
|
const TOOL_PENDING_SHOW_DELAY_MS = 1000;
|
|
42
43
|
const TOOL_HINT_DONE_COLOR = theme.subtle;
|
|
43
44
|
const COUNT_TWEEN_MS = 700;
|
|
@@ -249,8 +250,6 @@ function statusCopy(normalizedName, label, count, doneCount, pending, isError) {
|
|
|
249
250
|
case 'view_image':
|
|
250
251
|
case 'read_mcp_resource':
|
|
251
252
|
return copy('Reading', 'Read', 'file');
|
|
252
|
-
case 'write':
|
|
253
|
-
case 'edit':
|
|
254
253
|
case 'apply_patch':
|
|
255
254
|
return copy('Editing', 'Edited', 'file');
|
|
256
255
|
case 'grep':
|
|
@@ -492,7 +491,7 @@ function progressDetail({ normalizedName, label, elapsed }) {
|
|
|
492
491
|
if (n === 'explore' || l === 'explore') return `Exploring${suffix}`;
|
|
493
492
|
if (n === 'grep' || n === 'glob' || n === 'list' || n === 'ls' || l === 'search') return `Searching${suffix}`;
|
|
494
493
|
if (n === 'read' || n === 'view_image' || n === 'read_mcp_resource' || l === 'read') return `Reading${suffix}`;
|
|
495
|
-
if (n === '
|
|
494
|
+
if (n === 'apply_patch' || l === 'update') return `Editing${suffix}`;
|
|
496
495
|
if (n === 'recall' || n === 'recall_memory' || n === 'search_memories' || l === 'memory') return `Checking Memory${suffix}`;
|
|
497
496
|
if (l === 'setup') return `Setting Up${suffix}`;
|
|
498
497
|
return `Working${suffix}`;
|
|
@@ -538,6 +537,7 @@ function toolStatusColor({ pending, groupCount, failedCount }) {
|
|
|
538
537
|
|
|
539
538
|
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded, globalExpanded = false, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, headerFinalized = true }) {
|
|
540
539
|
const [blinkOn, setBlinkOn] = useState(true);
|
|
540
|
+
const [blinkExpired, setBlinkExpired] = useState(false);
|
|
541
541
|
const [pendingDelayElapsed, setPendingDelayElapsed] = useState(false);
|
|
542
542
|
const groupCount = Math.max(1, Number(count || 1));
|
|
543
543
|
const doneCount = Math.max(0, Math.min(groupCount, Number(completedCount || (result == null ? 0 : groupCount))));
|
|
@@ -581,10 +581,29 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
581
581
|
}, [pending, startedAt]);
|
|
582
582
|
|
|
583
583
|
useEffect(() => {
|
|
584
|
-
if (!pending || !pendingDisplayReady)
|
|
584
|
+
if (!pending || !pendingDisplayReady || blinkExpired) {
|
|
585
|
+
setBlinkOn(true);
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
585
588
|
const timer = setInterval(() => setBlinkOn((on) => !on), TOOL_BLINK_MS);
|
|
586
589
|
return () => clearInterval(timer);
|
|
587
|
-
}, [pending, pendingDisplayReady]);
|
|
590
|
+
}, [pending, pendingDisplayReady, blinkExpired]);
|
|
591
|
+
|
|
592
|
+
useEffect(() => {
|
|
593
|
+
if (!pending || !pendingDisplayReady) {
|
|
594
|
+
setBlinkExpired(false);
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
const started = Number(startedAt || 0);
|
|
598
|
+
const remaining = TOOL_BLINK_LIMIT_MS - (started ? Math.max(0, Date.now() - started) : 0);
|
|
599
|
+
if (remaining <= 0) {
|
|
600
|
+
setBlinkExpired(true);
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
setBlinkExpired(false);
|
|
604
|
+
const timer = setTimeout(() => setBlinkExpired(true), remaining);
|
|
605
|
+
return () => clearTimeout(timer);
|
|
606
|
+
}, [pending, pendingDisplayReady, startedAt]);
|
|
588
607
|
|
|
589
608
|
if (pending && !pendingDisplayReady) return null;
|
|
590
609
|
|
|
@@ -605,7 +624,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
605
624
|
}
|
|
606
625
|
|
|
607
626
|
const dotColor = statusColor;
|
|
608
|
-
const dotText = pending && !blinkOn ? ' ' : TURN_MARKER;
|
|
627
|
+
const dotText = pending && !blinkExpired && !blinkOn ? ' ' : TURN_MARKER;
|
|
609
628
|
const gutter = 2;
|
|
610
629
|
const showHeaderExpandHint = hasRawResult;
|
|
611
630
|
const hintLabel = showHeaderExpandHint ? `ctrl+o ${expanded ? 'collapse' : 'expand'}` : '';
|
|
@@ -700,7 +719,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
700
719
|
const isAgentResponse = isAgentResult && hasAgentResponseResult(rt);
|
|
701
720
|
const isAgentMetadataResult = isAgentResult && !isAgentResponse;
|
|
702
721
|
const dotColor = statusColor;
|
|
703
|
-
const dotText = pending && !blinkOn ? ' ' : TURN_MARKER;
|
|
722
|
+
const dotText = pending && !blinkExpired && !blinkOn ? ' ' : TURN_MARKER;
|
|
704
723
|
let labelText;
|
|
705
724
|
if (isAgentResponse) labelText = agentResponseTitle(parsedArgs);
|
|
706
725
|
else if (isBackgroundResponse) labelText = backgroundTaskResultTitle(normalizedName, backgroundMeta || parsedArgs);
|