mixdog 0.9.87 → 0.9.89
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 +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
listCoreCandidates,
|
|
33
33
|
promoteCoreCandidate,
|
|
34
34
|
dismissCoreCandidate,
|
|
35
|
-
CORE_SUMMARY_MAX,
|
|
36
35
|
normalizeCoreInput,
|
|
37
36
|
} from './core-memory-store.mjs'
|
|
38
37
|
import { resolveProjectId, resolveProjectScope } from './project-id-resolver.mjs'
|
|
@@ -54,15 +53,26 @@ export function createMemoryActionHandlers({
|
|
|
54
53
|
startCycle1Run,
|
|
55
54
|
finalizeCycle2Run,
|
|
56
55
|
finalizeCycle3Run,
|
|
56
|
+
requestCycle3Review,
|
|
57
57
|
getSchedulerCycle1InFlight,
|
|
58
58
|
getCycle2CallLlm,
|
|
59
59
|
getCycle3CallLlm,
|
|
60
60
|
ingestTranscriptFile,
|
|
61
61
|
cwdFromTranscriptPath,
|
|
62
|
+
addCoreImpl = addCore,
|
|
62
63
|
editCoreImpl = editCore,
|
|
63
64
|
}) {
|
|
64
65
|
const DATA_DIR = dataDir
|
|
65
66
|
|
|
67
|
+
async function queueCoreReview(reason) {
|
|
68
|
+
if (typeof requestCycle3Review !== 'function') return
|
|
69
|
+
try {
|
|
70
|
+
await requestCycle3Review(reason)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
log?.(`[core-memory] follow-up review queue failed: ${err?.message || err}\n`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
// Whole-action backfill mutex. memory-cycle1's _cycle1InFlight only protects
|
|
67
77
|
// cycle1; ingest workers (memory-ops-policy.mjs) and cycle2 can still overlap
|
|
68
78
|
// if a second backfill kicks in (e.g. setup-server timeout + retry). Track the
|
|
@@ -634,6 +644,7 @@ export function createMemoryActionHandlers({
|
|
|
634
644
|
}
|
|
635
645
|
if (op === 'promote') {
|
|
636
646
|
const entry = await promoteCoreCandidate(coreDataDir, args.id, { ...args, scope })
|
|
647
|
+
await queueCoreReview('core-promote')
|
|
637
648
|
const mergeNote = entry.merged_with ? ` (merged into core id=${entry.merged_with}, sim=${entry.sim})` : ''
|
|
638
649
|
return { text: `core promoted candidate id=${args.id} → core id=${entry.id}${mergeNote}: ${entry.element}` }
|
|
639
650
|
}
|
|
@@ -646,14 +657,14 @@ export function createMemoryActionHandlers({
|
|
|
646
657
|
}
|
|
647
658
|
// Local trim helper — the manage-block trimOrNull at :1807 is scoped to
|
|
648
659
|
// that branch and unreachable from here.
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
// target entry's stored pool.
|
|
660
|
+
// An explicit project_id wins. Otherwise infer from the active cwd/session
|
|
661
|
+
// and fall back to COMMON. Cycle3 can later reclassify a mis-scoped row.
|
|
652
662
|
const hasProjectIdKey = Object.prototype.hasOwnProperty.call(args, 'project_id')
|
|
653
663
|
const projectIdText = typeof args.project_id === 'string' ? args.project_id.trim() : ''
|
|
654
664
|
const projectId = (() => {
|
|
665
|
+
if (!hasProjectIdKey || !projectIdText) return resolveProjectScope(args.cwd)
|
|
655
666
|
if (projectIdText.toLowerCase() === 'common') return null
|
|
656
|
-
return projectIdText
|
|
667
|
+
return projectIdText
|
|
657
668
|
})()
|
|
658
669
|
try {
|
|
659
670
|
if (op === 'add' || op === 'edit') {
|
|
@@ -669,17 +680,7 @@ export function createMemoryActionHandlers({
|
|
|
669
680
|
})
|
|
670
681
|
const errors = [...normalized.errors]
|
|
671
682
|
if (op === 'add') {
|
|
672
|
-
if (
|
|
673
|
-
const inferredProjectId = typeof args.cwd === 'string' && args.cwd
|
|
674
|
-
? resolveProjectId(args.cwd)
|
|
675
|
-
: null
|
|
676
|
-
errors.unshift(
|
|
677
|
-
`project_id required — pass "common" for COMMON pool, or project slug like "owner/repo" for scoped pool` +
|
|
678
|
-
(typeof inferredProjectId === 'string' && /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)?$/.test(inferredProjectId)
|
|
679
|
-
? ` (cwd suggests "${inferredProjectId}")`
|
|
680
|
-
: ''),
|
|
681
|
-
)
|
|
682
|
-
} else if (projectId === '*') {
|
|
683
|
+
if (projectId === '*') {
|
|
683
684
|
errors.unshift('project_id "*" only valid for op="list"')
|
|
684
685
|
}
|
|
685
686
|
}
|
|
@@ -721,11 +722,13 @@ export function createMemoryActionHandlers({
|
|
|
721
722
|
return { text: lines.join('\n') }
|
|
722
723
|
}
|
|
723
724
|
if (op === 'add') {
|
|
724
|
-
const entry = await
|
|
725
|
+
const entry = await addCoreImpl(coreDataDir, args, projectId)
|
|
726
|
+
await queueCoreReview('core-add')
|
|
725
727
|
return { text: `core added (id=${entry.id}): ${entry.element} — ${entry.summary.slice(0, 200)}` }
|
|
726
728
|
}
|
|
727
729
|
if (op === 'edit') {
|
|
728
730
|
const entry = await editCoreImpl(coreDataDir, args.id, args)
|
|
731
|
+
await queueCoreReview('core-edit')
|
|
729
732
|
return { text: `core edited (id=${entry.id}): ${entry.element} — ${entry.summary.slice(0, 200)}` }
|
|
730
733
|
}
|
|
731
734
|
if (op === 'delete') {
|
|
@@ -796,7 +799,7 @@ export function createMemoryActionHandlers({
|
|
|
796
799
|
for (const a of actions) {
|
|
797
800
|
if (a?.action === 'core') {
|
|
798
801
|
const cid = Number(a.entry_id)
|
|
799
|
-
const core = String(a.core_summary ?? '').replace(/\s+/g, ' ').trim()
|
|
802
|
+
const core = String(a.core_summary ?? '').replace(/\s+/g, ' ').trim()
|
|
800
803
|
if (Number.isFinite(cid) && core) coreSummaryById.set(cid, core)
|
|
801
804
|
} else {
|
|
802
805
|
primaryActions.push(a)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// memory-cycle2-gate.mjs — prompt/parse/validate + runUnifiedGate + cascade
|
|
8
8
|
import { flushEmbeddingDirty } from './memory-embed.mjs'
|
|
9
9
|
import { refreshHotActive } from './memory.mjs'
|
|
10
|
-
import { backfillCoreEmbeddings, nominateCoreCandidates
|
|
10
|
+
import { backfillCoreEmbeddings, nominateCoreCandidates } from './core-memory-store.mjs'
|
|
11
11
|
import { markCycleRequest, consumeCycleRequests, resolveCoalesceMaxDrains, scheduleCoalescedCycleRetry, makeCycleRequestSignature, resolveCoalesceMaxRetries } from './memory-cycle-requests.mjs'
|
|
12
12
|
import { __mixdogMemoryLog, throwIfAborted } from './memory-cycle2-shared.mjs'
|
|
13
13
|
import {
|
|
@@ -394,7 +394,7 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
|
|
|
394
394
|
throwIfAborted(signal)
|
|
395
395
|
if (a.action === 'core') {
|
|
396
396
|
const id = Number(a.entry_id)
|
|
397
|
-
const core = String(a.core_summary ?? '').replace(/\s+/g, ' ').trim()
|
|
397
|
+
const core = String(a.core_summary ?? '').replace(/\s+/g, ' ').trim()
|
|
398
398
|
if (Number.isFinite(id) && core) coreSummaryById.set(id, core)
|
|
399
399
|
} else {
|
|
400
400
|
primaryActions.push(a)
|
|
@@ -404,7 +404,7 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
|
|
|
404
404
|
const setCoreSummary = async (entryId, explicitSummary) => {
|
|
405
405
|
const id = Number(entryId)
|
|
406
406
|
if (!Number.isFinite(id)) return false
|
|
407
|
-
|
|
407
|
+
const core = String(explicitSummary ?? '').replace(/\s+/g, ' ').trim()
|
|
408
408
|
if (!core) return false
|
|
409
409
|
await db.query(`UPDATE entries SET core_summary = $1 WHERE id = $2 AND is_root = 1`, [core, id])
|
|
410
410
|
return true
|
|
@@ -25,7 +25,7 @@ import { join } from 'path'
|
|
|
25
25
|
import { fileURLToPath } from 'url'
|
|
26
26
|
import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
|
|
27
27
|
import { callAgentDispatch } from './agent-ipc.mjs'
|
|
28
|
-
import { listCore, editCore, deleteCore, archiveCore
|
|
28
|
+
import { listCore, editCore, deleteCore, archiveCore } from './core-memory-store.mjs'
|
|
29
29
|
import { reclassifyCore } from './core-memory-store.mjs'
|
|
30
30
|
import { loadCurrentRulesDigest } from './memory-cycle2.mjs'
|
|
31
31
|
import { embedText } from './embedding-provider.mjs'
|
|
@@ -136,8 +136,6 @@ async function isSafeConservativeUpdate(current, action) {
|
|
|
136
136
|
const newElement = normalizeComparable(action.element)
|
|
137
137
|
const newSummary = normalizeComparable(action.summary)
|
|
138
138
|
if (!newElement || !newSummary) return { ok: false, reason: 'empty rewrite' }
|
|
139
|
-
if (newSummary.length > CORE_SUMMARY_MAX) return { ok: false, reason: 'summary too long' }
|
|
140
|
-
|
|
141
139
|
const oldText = coreText(current)
|
|
142
140
|
const newText = `${action.element}\n${action.summary}`
|
|
143
141
|
const oldLen = normalizeComparable(oldText).length
|
|
@@ -10,6 +10,28 @@ import { recallReadQuery } from './memory-recall-read-query.mjs'
|
|
|
10
10
|
// uses entries directly; this guard remains for explicit useHotActive callers.
|
|
11
11
|
import { SEMANTIC_ONLY_MIN_SIM, memberTsInWindow, SEMANTIC_TOP_RANK_MAX, SEMANTIC_TOP_RANK_MIN_SIM, SEMANTIC_TOP_RANK_STRICT_SIM, SHORT_QUERY_TOKEN_MAX, SIM_FLOOR, W_DENSE, W_WINDOW_RECENCY, W_RARE, RARE_DF_MAX, queryTokensLower, rowDisplayText, windowRecencyFactor, buildExactTerms, countQueryTokens, hasFullQueryTextMatch, hasQueryTokenCoverage, exactTextBoost, _checkMvHotActivePopulated } from './recall-scoring.mjs';
|
|
12
12
|
|
|
13
|
+
// Bounded lexical scan window. The trgm/exact CTE legs run `ILIKE '%…%'`
|
|
14
|
+
// which no index accelerates, so their worst case grows linearly with the
|
|
15
|
+
// entries table — on a years-old memory DB every recall pays a full-table
|
|
16
|
+
// substring scan twice. Bound both legs to the newest N rows by id (bigserial
|
|
17
|
+
// ⇒ insertion order; MAX(id) resolves via the pk index, no sort). Dense
|
|
18
|
+
// (HNSW) and sparse (GIN FTS) legs still cover the WHOLE table through real
|
|
19
|
+
// indexes, so old memories remain reachable semantically/topically — only the
|
|
20
|
+
// exact-substring rescue narrows to recent rows. Default 200k: measured on a
|
|
21
|
+
// live DB, a 20k bound regressed recall quality (bench MRR 0.75 → 0.33 — the
|
|
22
|
+
// rescued rows sat beyond the newest 20k inserts), so the default only guards
|
|
23
|
+
// pathological table growth, not today's scale. MIXDOG_RECALL_LEXSCAN_ROWS
|
|
24
|
+
// overrides; 0 restores the unbounded scan.
|
|
25
|
+
const _envLexScan = Number(process.env.MIXDOG_RECALL_LEXSCAN_ROWS)
|
|
26
|
+
const RECALL_LEXSCAN_ROWS = Number.isFinite(_envLexScan) && _envLexScan >= 0
|
|
27
|
+
? Math.floor(_envLexScan)
|
|
28
|
+
: 200_000
|
|
29
|
+
function lexScanBound(alias = '') {
|
|
30
|
+
if (RECALL_LEXSCAN_ROWS <= 0) return ''
|
|
31
|
+
const col = alias ? `${alias}.id` : 'id'
|
|
32
|
+
return `AND ${col} >= (SELECT GREATEST(COALESCE(MAX(id), 0) - ${RECALL_LEXSCAN_ROWS}, 0) FROM entries)`
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
export async function searchRelevantHybrid(db, query, options = {}) {
|
|
14
36
|
const clean = String(query ?? '').trim()
|
|
15
37
|
if (!clean) return []
|
|
@@ -280,6 +302,7 @@ trgm AS (
|
|
|
280
302
|
OR coalesce(summary, '') ILIKE '%' || $3 || '%'
|
|
281
303
|
)
|
|
282
304
|
${trgmFilterClause}
|
|
305
|
+
${lexScanBound()}
|
|
283
306
|
ORDER BY ts DESC
|
|
284
307
|
LIMIT $4
|
|
285
308
|
),`
|
|
@@ -297,6 +320,7 @@ exact AS (
|
|
|
297
320
|
)
|
|
298
321
|
WHERE true
|
|
299
322
|
${exactFilterClause}
|
|
323
|
+
${lexScanBound('ee')}
|
|
300
324
|
GROUP BY ee.id, ee.ts
|
|
301
325
|
HAVING COUNT(*) >= ${minExactHits}
|
|
302
326
|
ORDER BY exact_hits DESC, ee.ts DESC
|
|
@@ -39,6 +39,7 @@ export function createQueryHandlers({
|
|
|
39
39
|
log,
|
|
40
40
|
resolveProjectScope,
|
|
41
41
|
embeddingWarmupCanStart,
|
|
42
|
+
ensureKoMorph,
|
|
42
43
|
getBootTimestamp,
|
|
43
44
|
getTraceDb,
|
|
44
45
|
}) {
|
|
@@ -123,9 +124,13 @@ export function createQueryHandlers({
|
|
|
123
124
|
// is fresh (within FRESHNESS_MS of now) — an older newest-unchunked-turn
|
|
124
125
|
// is completed history (cycle1 just hasn't gotten to it, or drain timed
|
|
125
126
|
// out) and must stay visible, not be silently hidden every browse.
|
|
127
|
+
// Never applies to a compaction digest: that browse runs over a freshly
|
|
128
|
+
// and fully ingested transcript, so every row (including the previous
|
|
129
|
+
// turn's FINAL assistant message) carries an ingest-time ts and would be
|
|
130
|
+
// misread as in-flight, silently dropping the newest turn from the digest.
|
|
126
131
|
const IN_FLIGHT_TURN_FRESHNESS_MS = 5 * 60 * 1000
|
|
127
132
|
let excludeSourceTurnId = null
|
|
128
|
-
if (terms.length === 0) {
|
|
133
|
+
if (!compactDigest && terms.length === 0) {
|
|
129
134
|
try {
|
|
130
135
|
const r = await db.query(
|
|
131
136
|
`SELECT source_turn t, MAX(ts) last_ts FROM entries
|
|
@@ -140,7 +145,7 @@ export function createQueryHandlers({
|
|
|
140
145
|
}
|
|
141
146
|
} catch {}
|
|
142
147
|
}
|
|
143
|
-
if (Number.isFinite(excludeSourceTurnId)) {
|
|
148
|
+
if (!compactDigest && Number.isFinite(excludeSourceTurnId)) {
|
|
144
149
|
params.push(excludeSourceTurnId)
|
|
145
150
|
where.push(`NOT (chunk_root IS NULL AND source_turn = $${params.length})`)
|
|
146
151
|
}
|
|
@@ -166,7 +171,7 @@ export function createQueryHandlers({
|
|
|
166
171
|
const fillLimit = Math.max(0, fetchLimit - rows.length)
|
|
167
172
|
const fillWhere = ['session_id = $1', 'id <> ALL($2::bigint[])', '(is_root = 1 OR chunk_root IS NULL OR chunk_root = id)']
|
|
168
173
|
const fillParams = [sessionId, [...seen]]
|
|
169
|
-
if (Number.isFinite(excludeSourceTurnId)) {
|
|
174
|
+
if (!compactDigest && Number.isFinite(excludeSourceTurnId)) {
|
|
170
175
|
fillParams.push(excludeSourceTurnId)
|
|
171
176
|
fillWhere.push(`NOT (chunk_root IS NULL AND source_turn = $${fillParams.length})`)
|
|
172
177
|
}
|
|
@@ -410,6 +415,14 @@ export function createQueryHandlers({
|
|
|
410
415
|
return { text: header + parts.join('\n\n') }
|
|
411
416
|
}
|
|
412
417
|
const query = String(args.query ?? '').trim()
|
|
418
|
+
// Kiwi is a large optional analyzer. A Korean query starts it lazily, but
|
|
419
|
+
// this request keeps the lightweight lexical path instead of waiting for
|
|
420
|
+
// WASM/model construction. Later queries use it once ready.
|
|
421
|
+
if (query && /[\uAC00-\uD7AF]/u.test(query)) {
|
|
422
|
+
void Promise.resolve(ensureKoMorph?.()).catch((err) => {
|
|
423
|
+
log(`[memory-service] lazy kiwi init failed; recall stays lexical: ${err?.message || err}\n`)
|
|
424
|
+
})
|
|
425
|
+
}
|
|
413
426
|
let period = String(args.period ?? '').trim() || undefined
|
|
414
427
|
// Period and sort are caller-supplied only. Lead is responsible for
|
|
415
428
|
// mapping vague time phrases / chronological intent into the period
|
|
@@ -500,31 +513,16 @@ export function createQueryHandlers({
|
|
|
500
513
|
if (isEmbeddingModelReady()) {
|
|
501
514
|
queryVector = await embedText(query, { priority: true })
|
|
502
515
|
} else if (embeddingWarmupCanStart()) {
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
// the
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
warmupEmbeddingProvider().then(() => embedText(query, { priority: true })),
|
|
514
|
-
new Promise((resolve) => { timer = setTimeout(() => resolve(null), COLD_RECALL_WARMUP_WAIT_MS) }),
|
|
515
|
-
])
|
|
516
|
-
} catch (err) {
|
|
517
|
-
log(`[memory-service] embedding warmup during cold recall failed: ${err?.message || err}\n`)
|
|
518
|
-
queryVector = null
|
|
519
|
-
} finally {
|
|
520
|
-
clearTimeout(timer)
|
|
521
|
-
}
|
|
522
|
-
if (!Array.isArray(queryVector)) {
|
|
523
|
-
const now = Date.now()
|
|
524
|
-
if (now - _embeddingColdRecallLogAt > 10_000) {
|
|
525
|
-
_embeddingColdRecallLogAt = now
|
|
526
|
-
log(`[recall] embedding still cold after ${COLD_RECALL_WARMUP_WAIT_MS}ms wait; returning lexical results while background warmup continues\n`)
|
|
527
|
-
}
|
|
516
|
+
// Never turn a cold model into an 8-second foreground lock. Start the
|
|
517
|
+
// isolated worker and return lexical results now; the next recall gets
|
|
518
|
+
// the dense leg when the model is ready.
|
|
519
|
+
void warmupEmbeddingProvider().catch((err) => {
|
|
520
|
+
log(`[memory-service] embedding warmup after cold recall failed: ${err?.message || err}\n`)
|
|
521
|
+
})
|
|
522
|
+
const now = Date.now()
|
|
523
|
+
if (now - _embeddingColdRecallLogAt > 10_000) {
|
|
524
|
+
_embeddingColdRecallLogAt = now
|
|
525
|
+
log('[recall] embedding model warming in background; returning lexical results\n')
|
|
528
526
|
}
|
|
529
527
|
} else {
|
|
530
528
|
const now = Date.now()
|
|
@@ -27,12 +27,12 @@ export const TOOL_DEFS = [
|
|
|
27
27
|
description: 'Required for action=core.',
|
|
28
28
|
},
|
|
29
29
|
id: { type: 'number', description: 'Exact memory id.' },
|
|
30
|
-
element: { type: 'string',
|
|
31
|
-
summary: { type: 'string',
|
|
30
|
+
element: { type: 'string', description: 'Memory key/title. Defaults to the first 40 chars of summary.' },
|
|
31
|
+
summary: { type: 'string', description: 'Durable memory content; the memory worker compacts it after storage.' },
|
|
32
32
|
status: { type: 'string', enum: ['pending','active','archived'], description: 'Lifecycle status.' },
|
|
33
33
|
limit: { type: 'number', description: 'Max rows/items.' },
|
|
34
34
|
confirm: { type: 'string', description: 'Exact confirmation phrase for destructive actions.' },
|
|
35
|
-
project_id: { type: 'string', description: '
|
|
35
|
+
project_id: { type: 'string', description: 'Optional core pool override: common or slug. Omitted values are inferred from cwd/session, then fall back to common.' },
|
|
36
36
|
},
|
|
37
37
|
anyOf: [
|
|
38
38
|
{ properties: { action: { const: 'status' } }, required: ['action'] },
|
|
@@ -30,7 +30,11 @@ const _execFileAsync = promisify(execFile);
|
|
|
30
30
|
const RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY', 'EEXIST']);
|
|
31
31
|
const LOCK_WAIT_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
|
|
32
32
|
const DEFAULT_BACKOFFS_MS = Object.freeze([25, 50, 100, 200, 400, 800, 1200, 1600]);
|
|
33
|
-
const
|
|
33
|
+
const _configuredLockTimeoutMs = Number(process.env.MIXDOG_LOCK_TIMEOUT_MS);
|
|
34
|
+
const DEFAULT_LOCK_TIMEOUT_MS = Number.isFinite(_configuredLockTimeoutMs)
|
|
35
|
+
&& _configuredLockTimeoutMs >= 0
|
|
36
|
+
? _configuredLockTimeoutMs
|
|
37
|
+
: 2000;
|
|
34
38
|
|
|
35
39
|
// Per-process owner identity. A bare pid is not a durable holder identity:
|
|
36
40
|
// after a holder crashes the OS can recycle its pid onto an unrelated live
|
|
@@ -8,6 +8,13 @@ function positiveInt(value) {
|
|
|
8
8
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const DEFAULT_MIN_FREE_MEMORY_MB = 1024;
|
|
12
|
+
|
|
13
|
+
export function childGuardianMemoryFloorMb(env = process.env) {
|
|
14
|
+
const n = Math.floor(Number(env.MIXDOG_MIN_FREE_MEMORY_MB));
|
|
15
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_MIN_FREE_MEMORY_MB;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export function childGuardianSpawnEnv(env = process.env) {
|
|
12
19
|
return {
|
|
13
20
|
PATH: env.PATH || '',
|
|
@@ -22,9 +29,20 @@ export function childGuardianSpawnEnv(env = process.env) {
|
|
|
22
29
|
};
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
function guardianScript({
|
|
32
|
+
function guardianScript({
|
|
33
|
+
parentPid,
|
|
34
|
+
childPid,
|
|
35
|
+
childGroupPid,
|
|
36
|
+
platform,
|
|
37
|
+
pollMs,
|
|
38
|
+
orphanGraceMs,
|
|
39
|
+
forceGraceMs,
|
|
40
|
+
minFreeMemoryMb,
|
|
41
|
+
receiptPath,
|
|
42
|
+
}) {
|
|
26
43
|
return `
|
|
27
44
|
const { spawnSync } = require('node:child_process');
|
|
45
|
+
const { freemem } = require('node:os');
|
|
28
46
|
const parentPid = ${JSON.stringify(parentPid)};
|
|
29
47
|
const childPid = ${JSON.stringify(childPid)};
|
|
30
48
|
const childGroupPid = ${JSON.stringify(childGroupPid || childPid)};
|
|
@@ -32,6 +50,23 @@ const platform = ${JSON.stringify(platform)};
|
|
|
32
50
|
const pollMs = ${JSON.stringify(pollMs)};
|
|
33
51
|
const orphanGraceMs = ${JSON.stringify(orphanGraceMs)};
|
|
34
52
|
const forceGraceMs = ${JSON.stringify(forceGraceMs)};
|
|
53
|
+
const minFreeMemoryBytes = ${JSON.stringify(minFreeMemoryMb * 1024 * 1024)};
|
|
54
|
+
const receiptPath = ${JSON.stringify(receiptPath || null)};
|
|
55
|
+
// Kill receipt: guardian kills are otherwise indistinguishable from a crashed
|
|
56
|
+
// wrapper ("process exited without reporting an exit code"). Written BEFORE
|
|
57
|
+
// the kill so the owner's next status refresh can attribute the death.
|
|
58
|
+
function writeReceipt(reason, extra) {
|
|
59
|
+
if (!receiptPath) return;
|
|
60
|
+
try {
|
|
61
|
+
require('node:fs').writeFileSync(receiptPath, JSON.stringify({
|
|
62
|
+
reason,
|
|
63
|
+
at: new Date().toISOString(),
|
|
64
|
+
guardianPid: process.pid,
|
|
65
|
+
childPid,
|
|
66
|
+
...extra,
|
|
67
|
+
}));
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
35
70
|
function alive(pid) {
|
|
36
71
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
37
72
|
try { process.kill(pid, 0); return true; }
|
|
@@ -57,6 +92,20 @@ let killing = false;
|
|
|
57
92
|
let orphanedAt = 0;
|
|
58
93
|
const timer = setInterval(() => {
|
|
59
94
|
if (!alive(childPid)) process.exit(0);
|
|
95
|
+
// Admission protects only NEW work. A running shell can still consume the
|
|
96
|
+
// host after it starts, so reclaim its entire tree before it starves the
|
|
97
|
+
// owning Mixdog process. Force is intentional: at the memory floor there is
|
|
98
|
+
// no safe grace window for a runaway allocator.
|
|
99
|
+
if (!killing && minFreeMemoryBytes > 0) {
|
|
100
|
+
let freeBytes = Number.POSITIVE_INFINITY;
|
|
101
|
+
try { freeBytes = Number(freemem()); } catch {}
|
|
102
|
+
if (Number.isFinite(freeBytes) && freeBytes < minFreeMemoryBytes) {
|
|
103
|
+
killing = true;
|
|
104
|
+
writeReceipt('host-memory-floor', { freeBytes, minFreeMemoryBytes });
|
|
105
|
+
killTarget(true);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
60
109
|
// Orphaned when the parent PID dies. Matches mainstream CLIs: no console
|
|
61
110
|
// probe — a hidden shell spawn (windowsHide) plus tree-kill on cleanup, so
|
|
62
111
|
// orphan detection is pure parent-liveness.
|
|
@@ -67,6 +116,7 @@ const timer = setInterval(() => {
|
|
|
67
116
|
if (!orphanedAt) orphanedAt = Date.now();
|
|
68
117
|
if (killing || Date.now() - orphanedAt < orphanGraceMs) return;
|
|
69
118
|
killing = true;
|
|
119
|
+
writeReceipt('parent-exit', {});
|
|
70
120
|
killTarget(false);
|
|
71
121
|
setTimeout(() => { if (alive(childPid)) killTarget(true); process.exit(0); }, forceGraceMs).unref?.();
|
|
72
122
|
}, pollMs);
|
|
@@ -82,10 +132,14 @@ export function startChildGuardian({
|
|
|
82
132
|
graceMs = 3000,
|
|
83
133
|
orphanGraceMs = graceMs,
|
|
84
134
|
forceGraceMs = graceMs,
|
|
135
|
+
protectHostMemory = false,
|
|
136
|
+
minFreeMemoryMb = protectHostMemory ? childGuardianMemoryFloorMb() : 0,
|
|
137
|
+
receiptPath = null,
|
|
85
138
|
} = {}) {
|
|
86
139
|
const parent = positiveInt(parentPid);
|
|
87
140
|
const child = positiveInt(childPid);
|
|
88
141
|
if (!parent || !child || parent === child) return null;
|
|
142
|
+
const memoryFloorMb = Math.max(0, Math.floor(Number(minFreeMemoryMb) || 0));
|
|
89
143
|
|
|
90
144
|
try {
|
|
91
145
|
const guardian = spawn(process.execPath, [
|
|
@@ -99,6 +153,8 @@ export function startChildGuardian({
|
|
|
99
153
|
pollMs: Math.max(100, Math.floor(Number(pollMs) || 750)),
|
|
100
154
|
orphanGraceMs: Math.max(100, Math.floor(Number(orphanGraceMs) || Number(graceMs) || 3000)),
|
|
101
155
|
forceGraceMs: Math.max(100, Math.floor(Number(forceGraceMs) || Number(graceMs) || 3000)),
|
|
156
|
+
minFreeMemoryMb: memoryFloorMb,
|
|
157
|
+
receiptPath: typeof receiptPath === 'string' && receiptPath ? receiptPath : null,
|
|
102
158
|
}),
|
|
103
159
|
], {
|
|
104
160
|
stdio: 'ignore',
|
|
@@ -106,7 +162,22 @@ export function startChildGuardian({
|
|
|
106
162
|
...detachedSpawnOpts,
|
|
107
163
|
});
|
|
108
164
|
guardian.unref?.();
|
|
109
|
-
|
|
165
|
+
let stopped = false;
|
|
166
|
+
return {
|
|
167
|
+
pid: guardian.pid || null,
|
|
168
|
+
label,
|
|
169
|
+
childPid: child,
|
|
170
|
+
stop() {
|
|
171
|
+
if (stopped) return false;
|
|
172
|
+
stopped = true;
|
|
173
|
+
try {
|
|
174
|
+
guardian.kill();
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
110
181
|
} catch {
|
|
111
182
|
return null;
|
|
112
183
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
|
-
const { getSecret } = require('../../lib/keychain-cjs.cjs');
|
|
4
|
+
const { getSecret, hasSecret } = require('../../lib/keychain-cjs.cjs');
|
|
5
5
|
|
|
6
6
|
// Lightweight API-key lookup shared by pristine startup and the general config
|
|
7
7
|
// loader. This module deliberately has no mixdog-config.json dependency: asking
|
|
@@ -23,12 +23,32 @@ function providerSecretEnv(provider) {
|
|
|
23
23
|
return `MIXDOG_AGENT_${String(provider || '').replace(/[.\s]+/g, '_').toUpperCase()}_APIKEY`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
function environmentApiKey(provider) {
|
|
27
27
|
const id = String(provider || '').trim();
|
|
28
28
|
const standardEnv = AGENT_PROVIDER_ENV[id];
|
|
29
29
|
if (standardEnv && process.env[standardEnv]) return process.env[standardEnv];
|
|
30
30
|
const mixdogEnv = providerSecretEnv(id);
|
|
31
31
|
if (process.env[mixdogEnv]) return process.env[mixdogEnv];
|
|
32
|
+
for (const alias of AGENT_PROVIDER_ENV_ALIASES[id] || []) {
|
|
33
|
+
if (process.env[alias]) return process.env[alias];
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Presence-only credential probe. On Windows this is a cheap DPAPI-file
|
|
39
|
+
* check; decrypting through PowerShell is reserved for an actual API call. */
|
|
40
|
+
export function hasAgentApiKey(provider) {
|
|
41
|
+
const id = String(provider || '').trim();
|
|
42
|
+
if (!id) return false;
|
|
43
|
+
if (environmentApiKey(id)) return true;
|
|
44
|
+
try { return hasSecret(`agent.${id}.apiKey`); }
|
|
45
|
+
catch { return false; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getAgentApiKey(provider) {
|
|
49
|
+
const id = String(provider || '').trim();
|
|
50
|
+
const environment = environmentApiKey(id);
|
|
51
|
+
if (environment) return environment;
|
|
32
52
|
try {
|
|
33
53
|
const stored = getSecret(`agent.${id}.apiKey`);
|
|
34
54
|
if (stored) return stored;
|
|
@@ -36,8 +56,5 @@ export function getAgentApiKey(provider) {
|
|
|
36
56
|
// A missing/locked keychain is equivalent to no stored key. Explicit
|
|
37
57
|
// environment aliases remain available as the final provider-local source.
|
|
38
58
|
}
|
|
39
|
-
for (const alias of AGENT_PROVIDER_ENV_ALIASES[id] || []) {
|
|
40
|
-
if (process.env[alias]) return process.env[alias];
|
|
41
|
-
}
|
|
42
59
|
return null;
|
|
43
60
|
}
|
|
@@ -204,6 +204,59 @@ export function isLikelyToolCompletionWrapper(text) {
|
|
|
204
204
|
return quotedCount / quotedLines.length >= 0.8;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
const INTERNAL_TRANSCRIPT_CONTEXT_RE =
|
|
208
|
+
/^<(?:system-reminder|task-notification|skill|memory-context|mcp-instructions|available-deferred-tools|event)\b/i;
|
|
209
|
+
const INTERNAL_TRANSCRIPT_SYNTHETIC_RE =
|
|
210
|
+
/^(?:\[mixdog-runtime\]|A previous model worked on this task and produced the compacted handoff summary below\b|Re-attached after compaction\b|Reference files:\s)/i;
|
|
211
|
+
const INTERNAL_TRANSCRIPT_ASYNC_HEAD_RE =
|
|
212
|
+
/^The async (?:agent task|shell task|\w+ execution) \S+ has (?:finished|completed)\b/i;
|
|
213
|
+
// Persisted USER cancellation control rows ("[Request interrupted by user]"
|
|
214
|
+
// and its tool-use variant) exist for the next model step, not for humans:
|
|
215
|
+
// the human already saw the cancel they typed. The live engine path already
|
|
216
|
+
// withholds them; history/lane snapshots deliver them as plain user rows,
|
|
217
|
+
// which flashed in unfocused panes until focus swapped the source (user
|
|
218
|
+
// report). Full-row match only — never hide real prose around them.
|
|
219
|
+
// "[Request interrupted by process restart]" is deliberately NOT matched: it
|
|
220
|
+
// is a user-visible crash-recovery marker showing where a force-killed turn
|
|
221
|
+
// stopped, and the restored transcript must keep that row
|
|
222
|
+
// (scripts/turn-checkpoint-crash-test.mjs).
|
|
223
|
+
const INTERNAL_TRANSCRIPT_INTERRUPT_RE =
|
|
224
|
+
/^\[request interrupted by user(?: for tool use)?\]$/i;
|
|
225
|
+
|
|
226
|
+
// One display-only policy for every transcript surface. Runtime control rows
|
|
227
|
+
// may be persisted as ordinary role:user messages so the next model step can
|
|
228
|
+
// consume them; they are never human-authored chat and must not be rendered.
|
|
229
|
+
// The flexible Result separator covers legacy rows and command bodies whose
|
|
230
|
+
// embedded newlines lost their quote prefix during persistence.
|
|
231
|
+
export function isInternalTranscriptDisplayText(text) {
|
|
232
|
+
const value = String(text ?? '').trim();
|
|
233
|
+
if (!value) return false;
|
|
234
|
+
if (INTERNAL_TRANSCRIPT_CONTEXT_RE.test(value)
|
|
235
|
+
|| INTERNAL_TRANSCRIPT_SYNTHETIC_RE.test(value)
|
|
236
|
+
|| INTERNAL_TRANSCRIPT_INTERRUPT_RE.test(value)
|
|
237
|
+
|| isInternalRuntimeNotificationText(value)
|
|
238
|
+
|| isModelVisibleToolCompletionWrapper(value)
|
|
239
|
+
|| isLikelyToolCompletionWrapper(value)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (INTERNAL_TRANSCRIPT_ASYNC_HEAD_RE.test(value) && !/\bResult:\s*(?:\r?\n|$)/i.test(value)) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
const resultSplit = /\r?\n(?:[ \t]*\r?\n)?Result:[ \t]*\r?\n/i.exec(value);
|
|
246
|
+
if (!resultSplit) return false;
|
|
247
|
+
const preamble = value.slice(0, resultSplit.index).trim();
|
|
248
|
+
const instructionLike = MODEL_VISIBLE_COMPLETION_INSTRUCTION_RE.test(preamble)
|
|
249
|
+
|| MODEL_VISIBLE_COMPLETION_REVIEW_RE.test(preamble)
|
|
250
|
+
|| MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE.test(preamble);
|
|
251
|
+
if (!instructionLike) return false;
|
|
252
|
+
const normalizedBody = value.slice(resultSplit.index + resultSplit[0].length)
|
|
253
|
+
.split(/\r?\n/)
|
|
254
|
+
.map((line) => line.replace(/^>\s?/, ''))
|
|
255
|
+
.join('\n')
|
|
256
|
+
.trim();
|
|
257
|
+
return isInternalRuntimeNotificationText(normalizedBody);
|
|
258
|
+
}
|
|
259
|
+
|
|
207
260
|
export function modelVisibleToolCompletionMessage(text, meta = {}) {
|
|
208
261
|
const message = String(text || '').trim();
|
|
209
262
|
if (!message) return '';
|
|
@@ -122,10 +122,11 @@ const AUTO_CLEAR_DEFAULT_IDLE_MS = 60 * 60 * 1000;
|
|
|
122
122
|
// Lead 11.75M vs 20.11M and agents 14.46M vs 45.04M input-token equivalents,
|
|
123
123
|
// because cold misses rewrite the accumulated tail. Gemini, xAI and Mistral
|
|
124
124
|
// caches run ~1h, Groq's implicit cache persists ~2h, OpenAI/DeepSeek
|
|
125
|
-
// key-prefix/implicit caches persist far longer (~24h), and OpenAI OAuth
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
125
|
+
// key-prefix/implicit caches persist far longer (~24h), and OpenAI OAuth
|
|
126
|
+
// (Codex backend) guarantees a 30-minute minimum prompt-cache TTL on
|
|
127
|
+
// GPT-5.6+ models (prompt_cache_options.ttl=30m, refreshed on use), so the
|
|
128
|
+
// sweep matches that window to avoid clearing a still-warm cache.
|
|
129
|
+
// Unknown/unrecognized providers fall back to the 'default' 1h entry.
|
|
129
130
|
export const AUTO_CLEAR_PROVIDER_IDLE_MS = Object.freeze({
|
|
130
131
|
'anthropic': 60 * 60 * 1000,
|
|
131
132
|
'anthropic-oauth': 60 * 60 * 1000,
|
|
@@ -134,7 +135,7 @@ export const AUTO_CLEAR_PROVIDER_IDLE_MS = Object.freeze({
|
|
|
134
135
|
'openai': 24 * 60 * 60 * 1000,
|
|
135
136
|
'deepseek': 24 * 60 * 60 * 1000,
|
|
136
137
|
'xai': 60 * 60 * 1000,
|
|
137
|
-
'openai-oauth':
|
|
138
|
+
'openai-oauth': 30 * 60 * 1000,
|
|
138
139
|
'mistral': 60 * 60 * 1000,
|
|
139
140
|
'default': AUTO_CLEAR_DEFAULT_IDLE_MS,
|
|
140
141
|
});
|
|
@@ -52,6 +52,7 @@ export function createLifecycleApi(deps) {
|
|
|
52
52
|
beginRoutePreparation, clearRoutePreparation,
|
|
53
53
|
pushTranscriptRebind,
|
|
54
54
|
notificationListeners, remoteStateListeners, desktopSession,
|
|
55
|
+
disposeSessionTitles,
|
|
55
56
|
} = deps;
|
|
56
57
|
const closeSurfaceSession = (session, reason, options) => {
|
|
57
58
|
if (!session?.id) return false;
|
|
@@ -111,6 +112,7 @@ export function createLifecycleApi(deps) {
|
|
|
111
112
|
model: s.model,
|
|
112
113
|
provider: s.provider,
|
|
113
114
|
messageCount,
|
|
115
|
+
title: cleanSessionPreview(s.title || '', 100),
|
|
114
116
|
preview,
|
|
115
117
|
// Working indicator: the .hb sidecar ALONE is the liveness signal. Its
|
|
116
118
|
// deletion at turn end IS the completion signal, so the persisted
|
|
@@ -166,6 +168,7 @@ export function createLifecycleApi(deps) {
|
|
|
166
168
|
// of reaping every session's jobs. CLI exit paths never set this.
|
|
167
169
|
const keepBackgroundWork = options?.keepBackgroundWork === true;
|
|
168
170
|
setCloseRequested(true);
|
|
171
|
+
try { disposeSessionTitles?.(); } catch {}
|
|
169
172
|
// Self-update now stages in the background and swaps on the next clean
|
|
170
173
|
// launch (see staged-update.mjs) — nothing installs at shutdown. On a
|
|
171
174
|
// real process exit we just drop this session's live-refcount pid file so
|
|
@@ -415,12 +418,21 @@ export function createLifecycleApi(deps) {
|
|
|
415
418
|
if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return null;
|
|
416
419
|
const current = getSession();
|
|
417
420
|
if (current?.id === sessionId) return current;
|
|
418
|
-
|
|
421
|
+
// Visible desktop panes reconcile dead-owner checkpoints during their
|
|
422
|
+
// startup peek instead of waiting for a later click/resume. Older test
|
|
423
|
+
// or embedded managers retain the plain read-only fallback.
|
|
424
|
+
const session = typeof mgr.recoverSessionAfterProcessRestart === 'function'
|
|
425
|
+
? mgr.recoverSessionAfterProcessRestart(sessionId)
|
|
426
|
+
: mgr.loadSession?.(sessionId);
|
|
419
427
|
// `closed` means the previous live owner released this durable session;
|
|
420
428
|
// it is still a valid historical transcript. Desktop pane peeks are
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
|
|
429
|
+
// ownership-neutral, so they may inspect it without resuming. Crash
|
|
430
|
+
// checkpoint reconciliation is the sole durable mutation here.
|
|
431
|
+
if (!session) return null;
|
|
432
|
+
return mgr.prepareSessionProjection?.(
|
|
433
|
+
session,
|
|
434
|
+
toolSpecForMode(getMode()),
|
|
435
|
+
) || session;
|
|
424
436
|
},
|
|
425
437
|
async resume(id) {
|
|
426
438
|
clearRoutePreparation?.();
|