mixdog 0.9.55 → 0.9.59
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 +3 -1
- package/scripts/_smoke_wd.mjs +7 -0
- package/scripts/agent-terminal-reap-test.mjs +127 -2
- package/scripts/compact-file-reattach-test.mjs +88 -0
- package/scripts/compact-pressure-test.mjs +45 -21
- package/scripts/compact-recall-digest-test.mjs +57 -0
- package/scripts/compact-smoke.mjs +35 -39
- package/scripts/desktop-session-bridge-test.mjs +271 -9
- package/scripts/generate-oc-icons.mjs +11 -0
- package/scripts/live-share-test.mjs +148 -0
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/max-output-recovery-test.mjs +12 -1
- package/scripts/mouse-tracking-restore-smoke.mjs +107 -0
- package/scripts/provider-admission-scheduler-test.mjs +100 -0
- package/scripts/provider-contract-test.mjs +49 -0
- package/scripts/resource-admission-test.mjs +5 -2
- package/scripts/session-ingest-compaction-smoke.mjs +38 -0
- package/scripts/steering-drain-buckets-test.mjs +15 -0
- package/scripts/submit-commandbusy-race-test.mjs +54 -3
- package/scripts/tool-smoke.mjs +2 -3
- package/scripts/tui-ambiguous-width-test.mjs +113 -0
- package/scripts/tui-runtime-load-bench.mjs +5 -0
- package/scripts/tui-transcript-perf-test.mjs +167 -0
- package/src/app.mjs +23 -0
- package/src/cli.mjs +6 -0
- package/src/lib/keychain-cjs.cjs +70 -20
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +4 -2
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +18 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +111 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +61 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +13 -4
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +43 -2
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +112 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +203 -49
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -15
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +109 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -7
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +39 -39
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +108 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -11
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +132 -24
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +91 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +14 -33
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +169 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +474 -42
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +17 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +36 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +249 -11
- package/src/runtime/channels/data/voice-runtime-manifest.json +21 -5
- package/src/runtime/channels/lib/scheduler.mjs +8 -6
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +71 -8
- package/src/runtime/channels/lib/voice-transcription.mjs +2 -2
- package/src/runtime/channels/lib/whisper-language.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +7 -0
- package/src/runtime/memory/lib/embedding-worker.mjs +20 -0
- package/src/runtime/memory/lib/query-handlers.mjs +10 -4
- package/src/runtime/memory/lib/recall-format.mjs +43 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +90 -64
- package/src/runtime/shared/err-text.mjs +4 -1
- package/src/runtime/shared/resource-admission.mjs +11 -0
- package/src/runtime/shared/schedule-model-ref.mjs +28 -0
- package/src/runtime/shared/schedule-session-run.mjs +63 -0
- package/src/runtime/shared/schedules-db.mjs +8 -3
- package/src/runtime/shared/tool-result-summary.mjs +4 -1
- package/src/runtime/shared/tool-surface.mjs +7 -7
- package/src/runtime/shared/transcript-writer.mjs +17 -0
- package/src/session-runtime/channel-config-api.mjs +11 -0
- package/src/session-runtime/config-helpers.mjs +9 -6
- package/src/session-runtime/context-status.mjs +80 -3
- package/src/session-runtime/lifecycle-api.mjs +154 -40
- package/src/session-runtime/provider-auth-api.mjs +21 -2
- package/src/session-runtime/provider-usage.mjs +4 -1
- package/src/session-runtime/resource-api.mjs +12 -11
- package/src/session-runtime/runtime-core.mjs +45 -11
- package/src/session-runtime/session-text.mjs +56 -6
- package/src/session-runtime/session-turn-api.mjs +70 -2
- package/src/session-runtime/tool-catalog.mjs +2 -1
- package/src/session-runtime/workflow-agents-api.mjs +2 -2
- package/src/standalone/agent-tool/render.mjs +5 -1
- package/src/standalone/agent-tool.mjs +63 -3
- package/src/standalone/channel-admin.mjs +19 -3
- package/src/standalone/channel-daemon.mjs +40 -0
- package/src/tui/app/resume-picker.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +19 -0
- package/src/tui/app/transcript-window.mjs +45 -8
- package/src/tui/app/use-mouse-input.mjs +101 -31
- package/src/tui/app/use-transcript-window.mjs +53 -9
- package/src/tui/components/ToolExecution.jsx +3 -4
- package/src/tui/components/TranscriptItem.jsx +3 -0
- package/src/tui/dist/index.mjs +17391 -14920
- package/src/tui/engine/context-state.mjs +10 -1
- package/src/tui/engine/live-share.mjs +390 -0
- package/src/tui/engine/queue-helpers.mjs +23 -0
- package/src/tui/engine/session-api-ext.mjs +439 -40
- package/src/tui/engine/session-api.mjs +7 -1
- package/src/tui/engine/session-flow.mjs +21 -7
- package/src/tui/engine/tool-card-results.mjs +3 -1
- package/src/tui/engine/turn.mjs +57 -4
- package/src/tui/engine.mjs +211 -7
- package/src/tui/index.jsx +2 -0
- package/src/tui/lib/voice-setup.mjs +10 -4
- package/src/tui/paste-attachments.mjs +4 -1
- package/src/vendor/statusline/src/gateway/session-routes.mjs +24 -1
- package/src/workflows/default/WORKFLOW.md +3 -0
- package/vendor/ink/build/display-width.js +14 -0
- package/vendor/ink/build/output.js +24 -3
- package/vendor/ink/build/wrap-text.d.ts +2 -0
- package/vendor/ink/build/wrap-text.js +45 -4
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a schedule as a VISIBLE Mixdog session instead of a hidden agent
|
|
3
|
+
* dispatch. Shared by the engine run-now capability and the channels-worker
|
|
4
|
+
* scheduler so both paths leave the same trail: a resumable session with
|
|
5
|
+
* owner 'user' + sourceType 'schedule' (admitted by the lead-session list
|
|
6
|
+
* filter as its own type, so it shows in desktop Recent / TUI resume), the
|
|
7
|
+
* schedule's provider/model route, and the schedule's project cwd.
|
|
8
|
+
*/
|
|
9
|
+
import { loadConfig } from '../agent/orchestrator/config.mjs';
|
|
10
|
+
import { createSession } from '../agent/orchestrator/session/manager/session-lifecycle.mjs';
|
|
11
|
+
import { askSession } from '../agent/orchestrator/session/manager/ask-session.mjs';
|
|
12
|
+
import { parseScheduleModelRef } from './schedule-model-ref.mjs';
|
|
13
|
+
|
|
14
|
+
/** Resolve schedule.model into a {provider,model,effort?,fast?} route. */
|
|
15
|
+
export function scheduleRouteFromModelRef(modelRef, config = null) {
|
|
16
|
+
const ref = parseScheduleModelRef(modelRef);
|
|
17
|
+
if (ref && typeof ref === 'object') return ref;
|
|
18
|
+
const cfg = config || loadConfig({ secrets: false });
|
|
19
|
+
const preset = cfg.presets?.find((p) => p.id === ref || p.name === ref);
|
|
20
|
+
if (!preset) {
|
|
21
|
+
throw new Error(`schedule model "${ref}" is neither a provider/model route nor a known preset name`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
provider: preset.provider,
|
|
25
|
+
model: preset.model,
|
|
26
|
+
...(preset.effort ? { effort: preset.effort } : {}),
|
|
27
|
+
...(preset.fast === true ? { fast: true } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create the visible schedule session and run the prompt in it.
|
|
33
|
+
* `prompt` overrides schedule.prompt when the caller (channels worker) has
|
|
34
|
+
* already resolved/wrapped the prompt body; the override is used verbatim
|
|
35
|
+
* while the fallback gets a small origin header.
|
|
36
|
+
*/
|
|
37
|
+
export async function runScheduleSession(schedule, { config = null, prompt: promptOverride = null } = {}) {
|
|
38
|
+
if (!schedule?.name) throw new Error('runScheduleSession: schedule row required');
|
|
39
|
+
const prompt = String(promptOverride ?? schedule.prompt ?? '').trim();
|
|
40
|
+
if (!prompt) throw new Error(`schedule "${schedule.name}" has no instructions`);
|
|
41
|
+
if (!schedule.model) {
|
|
42
|
+
throw new Error(`schedule "${schedule.name}" has no model configured — edit it and choose a model first`);
|
|
43
|
+
}
|
|
44
|
+
const route = scheduleRouteFromModelRef(schedule.model, config);
|
|
45
|
+
const cwd = schedule.cwd ? String(schedule.cwd) : null;
|
|
46
|
+
const session = createSession({
|
|
47
|
+
provider: route.provider,
|
|
48
|
+
model: route.model,
|
|
49
|
+
...(route.effort ? { effort: route.effort } : {}),
|
|
50
|
+
...(route.fast === true ? { fast: true } : {}),
|
|
51
|
+
owner: 'user',
|
|
52
|
+
sourceType: 'schedule',
|
|
53
|
+
sourceName: schedule.name,
|
|
54
|
+
...(cwd ? { cwd } : {}),
|
|
55
|
+
desktopSession: cwd
|
|
56
|
+
? { classification: 'project', projectPath: cwd }
|
|
57
|
+
: { classification: 'task', projectPath: null },
|
|
58
|
+
});
|
|
59
|
+
// The user message is the schedule's instructions verbatim: the desktop
|
|
60
|
+
// title/preview derive from it, so no "[Scheduled task: …]" header noise.
|
|
61
|
+
const result = await askSession(session.id, prompt, null, null, cwd || undefined);
|
|
62
|
+
return { sessionId: session.id, result: String(result?.content || '') };
|
|
63
|
+
}
|
|
@@ -32,6 +32,7 @@ CREATE TABLE IF NOT EXISTS scheduler.schedules (
|
|
|
32
32
|
target text NOT NULL CHECK (target IN ('channel','session')),
|
|
33
33
|
channel_id text,
|
|
34
34
|
model text,
|
|
35
|
+
cwd text,
|
|
35
36
|
prompt text NOT NULL,
|
|
36
37
|
enabled boolean NOT NULL DEFAULT true,
|
|
37
38
|
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','done')),
|
|
@@ -43,6 +44,7 @@ CREATE TABLE IF NOT EXISTS scheduler.schedules (
|
|
|
43
44
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
44
45
|
CONSTRAINT schedules_when_xor CHECK ((when_at IS NOT NULL) <> (when_cron IS NOT NULL))
|
|
45
46
|
);
|
|
47
|
+
ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS cwd text;
|
|
46
48
|
`;
|
|
47
49
|
|
|
48
50
|
async function getDb(dataDir = resolvePluginData()) {
|
|
@@ -79,6 +81,7 @@ function rowToDef(row) {
|
|
|
79
81
|
target: row.target,
|
|
80
82
|
channelId: row.channel_id,
|
|
81
83
|
model: row.model,
|
|
84
|
+
cwd: row.cwd,
|
|
82
85
|
prompt: row.prompt,
|
|
83
86
|
enabled: row.enabled,
|
|
84
87
|
status: row.status,
|
|
@@ -91,7 +94,7 @@ function rowToDef(row) {
|
|
|
91
94
|
};
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
const COLS = 'name, description, when_at, when_cron, timezone, target, channel_id, model, prompt, enabled, status, last_fired_at, next_fire_at, deferred_until, skipped_until, created_at, updated_at';
|
|
97
|
+
const COLS = 'name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, prompt, enabled, status, last_fired_at, next_fire_at, deferred_until, skipped_until, created_at, updated_at';
|
|
95
98
|
|
|
96
99
|
// ---------------------------------------------------------------------------
|
|
97
100
|
// Public API
|
|
@@ -126,6 +129,7 @@ export async function upsertSchedule(def, { dataDir } = {}) {
|
|
|
126
129
|
def.target,
|
|
127
130
|
def.channelId ?? null,
|
|
128
131
|
def.model ?? null,
|
|
132
|
+
def.cwd ?? null,
|
|
129
133
|
def.prompt,
|
|
130
134
|
def.enabled ?? true,
|
|
131
135
|
def.status ?? 'active',
|
|
@@ -133,8 +137,8 @@ export async function upsertSchedule(def, { dataDir } = {}) {
|
|
|
133
137
|
];
|
|
134
138
|
const { rows } = await db.query(
|
|
135
139
|
`INSERT INTO scheduler.schedules
|
|
136
|
-
(name, description, when_at, when_cron, timezone, target, channel_id, model, prompt, enabled, status, next_fire_at)
|
|
137
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
|
140
|
+
(name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, prompt, enabled, status, next_fire_at)
|
|
141
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
|
138
142
|
ON CONFLICT (name) DO UPDATE SET
|
|
139
143
|
description = EXCLUDED.description,
|
|
140
144
|
when_at = EXCLUDED.when_at,
|
|
@@ -143,6 +147,7 @@ export async function upsertSchedule(def, { dataDir } = {}) {
|
|
|
143
147
|
target = EXCLUDED.target,
|
|
144
148
|
channel_id = EXCLUDED.channel_id,
|
|
145
149
|
model = EXCLUDED.model,
|
|
150
|
+
cwd = EXCLUDED.cwd,
|
|
146
151
|
prompt = EXCLUDED.prompt,
|
|
147
152
|
enabled = EXCLUDED.enabled,
|
|
148
153
|
next_fire_at = EXCLUDED.next_fire_at,
|
|
@@ -449,7 +449,10 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
|
|
|
449
449
|
// Agent/task result cards show only a one-liner; full report via ctrl+o.
|
|
450
450
|
if (answerLine) return truncateSingleLine(stripInlineMarkdown(answerLine), AGENT_SURFACE_BRIEF_MAX);
|
|
451
451
|
const task = /^agent task:\s*(\S+)/mi.exec(text);
|
|
452
|
-
const
|
|
452
|
+
const statusMatch = /^status:\s*([^\s(]+)/mi.exec(text);
|
|
453
|
+
// Defensive twin of the render-side guard: an envelope that still says
|
|
454
|
+
// "status: undefined"/"null" must not surface a titleized "Undefined".
|
|
455
|
+
const status = statusMatch && !/^(?:undefined|null)$/i.test(statusMatch[1]) ? statusMatch : null;
|
|
453
456
|
const agent = /^agent:\s*(.+)$/mi.exec(text);
|
|
454
457
|
const preset = /^preset:\s*(.+)$/mi.exec(text);
|
|
455
458
|
const model = /^model:\s*(.+)$/mi.exec(text);
|
|
@@ -602,17 +602,17 @@ export function aggregateToolCategoryEntry(name, args = {}, category = '') {
|
|
|
602
602
|
}
|
|
603
603
|
|
|
604
604
|
/**
|
|
605
|
-
* Rebuild the per-category count map for the DONE state
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
605
|
+
* Rebuild the per-category count map for the DONE state. Counts ATTEMPTS —
|
|
606
|
+
* failed calls included — so the collapsed header total always agrees with
|
|
607
|
+
* the 'N Ok · N Failed' breakdown rendered beside it ("Ran 5 commands ·
|
|
608
|
+
* 3 Ok · 2 Failed", never "Ran 3 commands · 3 Ok · 2 Failed"). Mirrors the
|
|
609
|
+
* call-time accumulation in turn.mjs (sum aggregateToolCategoryEntry(...).count
|
|
610
|
+
* per key); outcome splitting stays in the failure detail, not the header.
|
|
611
611
|
*/
|
|
612
612
|
export function aggregateDoneCategories(calls = []) {
|
|
613
613
|
const map = new Map();
|
|
614
614
|
for (const rec of calls || []) {
|
|
615
|
-
if (!rec
|
|
615
|
+
if (!rec) continue;
|
|
616
616
|
const entry = aggregateToolCategoryEntry(rec.name, rec.args, rec.category);
|
|
617
617
|
const prev = map.get(entry.key);
|
|
618
618
|
map.set(entry.key, { ...entry, count: Number(prev?.count || 0) + Number(entry.count || 1) });
|
|
@@ -167,6 +167,22 @@ export function createTranscriptWriter({ mixdogHome, sessionId, cwd, pid } = {})
|
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// User prompt row. The channel forwarder ignores plain user text rows
|
|
171
|
+
// (output-forwarder extractNewText only surfaces assistant rows and
|
|
172
|
+
// user rows carrying tool_result), so this never echoes back to the
|
|
173
|
+
// channel — it exists so the memory transcript watcher ingests BOTH
|
|
174
|
+
// sides of the conversation (user rows were previously never written,
|
|
175
|
+
// leaving recall unable to reconstruct recent sessions).
|
|
176
|
+
function appendUser(text) {
|
|
177
|
+
const value = typeof text === 'string' ? text : (text == null ? '' : String(text));
|
|
178
|
+
if (!value.trim()) return;
|
|
179
|
+
appendLine({
|
|
180
|
+
type: 'user',
|
|
181
|
+
sessionId,
|
|
182
|
+
message: { content: [{ type: 'text', text: value }] },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
170
186
|
function appendToolUse(name, input) {
|
|
171
187
|
if (!name) return;
|
|
172
188
|
appendLine({
|
|
@@ -217,6 +233,7 @@ export function createTranscriptWriter({ mixdogHome, sessionId, cwd, pid } = {})
|
|
|
217
233
|
ensureTranscriptFile,
|
|
218
234
|
refresh,
|
|
219
235
|
appendAssistant,
|
|
236
|
+
appendUser,
|
|
220
237
|
appendToolUse,
|
|
221
238
|
appendToolResult,
|
|
222
239
|
};
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
setWebhookEnabled,
|
|
10
10
|
setWebhookConfigAsync,
|
|
11
11
|
} from '../standalone/channel-admin.mjs';
|
|
12
|
+
import { getSchedule } from '../runtime/shared/schedules-db.mjs';
|
|
13
|
+
import { runScheduleSession } from '../runtime/shared/schedule-session-run.mjs';
|
|
12
14
|
|
|
13
15
|
// Channel/webhook/schedule config surface. Extracted verbatim from the runtime
|
|
14
16
|
// API object; the mutating admin helpers are imported directly here and the
|
|
@@ -66,5 +68,14 @@ export function createChannelConfigApi({ flushBackendSave, channels, reloadChann
|
|
|
66
68
|
reloadChannelsSoon();
|
|
67
69
|
return result;
|
|
68
70
|
},
|
|
71
|
+
async runScheduleNow(name) {
|
|
72
|
+
const id = String(name || '').trim();
|
|
73
|
+
const schedule = await getSchedule(id);
|
|
74
|
+
if (!schedule) throw new Error(`schedule "${id}" does not exist`);
|
|
75
|
+
// Runs in the engine process as a VISIBLE schedule session, so it works
|
|
76
|
+
// even while the channels worker is off and the run lands in Recent.
|
|
77
|
+
const { sessionId, result } = await runScheduleSession(schedule);
|
|
78
|
+
return { name: schedule.name, ok: true, sessionId, result };
|
|
79
|
+
},
|
|
69
80
|
};
|
|
70
81
|
}
|
|
@@ -114,18 +114,21 @@ const AUTO_CLEAR_DEFAULT_IDLE_MS = 60 * 60 * 1000;
|
|
|
114
114
|
|
|
115
115
|
// Provider-aware auto-clear idle-sweep defaults. Rationale mirrors the
|
|
116
116
|
// provider cache TTLs documented in agent-runtime/cache-strategy.mjs
|
|
117
|
-
// (explicit breakpoint / managed-cache windows): Anthropic follows the BP4
|
|
118
|
-
// messages-tail TTL
|
|
119
|
-
//
|
|
120
|
-
//
|
|
117
|
+
// (explicit breakpoint / managed-cache windows): Anthropic follows the 1h BP4
|
|
118
|
+
// messages-tail TTL. The 2026-07-16 6.8h trace found no intra-session gaps
|
|
119
|
+
// over 1h; gaps over 5m were common for agent tool executions/reviewer
|
|
120
|
+
// fix-loop reuse, and 24/25 such Lead gaps were agent waits where autoClear
|
|
121
|
+
// cannot fire. Tail-cost simulation favors 1h despite its 2x write premium:
|
|
122
|
+
// Lead 11.75M vs 20.11M and agents 14.46M vs 45.04M input-token equivalents,
|
|
123
|
+
// because cold misses rewrite the accumulated tail. Gemini, xAI and Mistral
|
|
121
124
|
// caches run ~1h, Groq's implicit cache persists ~2h, OpenAI/DeepSeek
|
|
122
125
|
// key-prefix/implicit caches persist far longer (~24h), and OpenAI OAuth's
|
|
123
126
|
// in-memory server cache is short-lived (~5-10min), so we round up slightly
|
|
124
127
|
// to 10m to avoid sweeping a still-warm cache. Unknown/unrecognized
|
|
125
128
|
// providers fall back to the 'default' 1h entry.
|
|
126
129
|
export const AUTO_CLEAR_PROVIDER_IDLE_MS = Object.freeze({
|
|
127
|
-
'anthropic':
|
|
128
|
-
'anthropic-oauth':
|
|
130
|
+
'anthropic': 60 * 60 * 1000,
|
|
131
|
+
'anthropic-oauth': 60 * 60 * 1000,
|
|
129
132
|
'gemini': 60 * 60 * 1000,
|
|
130
133
|
'groq': 2 * 60 * 60 * 1000,
|
|
131
134
|
'openai': 24 * 60 * 60 * 1000,
|
|
@@ -2,10 +2,13 @@ import {
|
|
|
2
2
|
estimateRequestReserveTokens,
|
|
3
3
|
estimateToolSchemaTokens,
|
|
4
4
|
contextMessagesRevision,
|
|
5
|
+
providerTokenCalibration,
|
|
5
6
|
resolveSessionCompactPolicy,
|
|
6
7
|
summarizeContextMessages,
|
|
7
8
|
toolSchemaSignature,
|
|
8
9
|
} from '../runtime/agent/orchestrator/session/context-utils.mjs';
|
|
10
|
+
import { SUMMARY_PREFIX } from '../runtime/agent/orchestrator/session/compact.mjs';
|
|
11
|
+
import { hasUserConversationMessage } from '../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
|
|
9
12
|
import {
|
|
10
13
|
resolveCompactionPressureTokens,
|
|
11
14
|
resolveWorkerCompactPolicy,
|
|
@@ -130,14 +133,85 @@ export function createContextStatus({
|
|
|
130
133
|
function contextStatus() {
|
|
131
134
|
const session = getSession();
|
|
132
135
|
const route = getRoute();
|
|
136
|
+
const committedMessages = Array.isArray(session?.messages) ? session.messages : [];
|
|
137
|
+
const liveMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
|
|
138
|
+
const activityMessages = liveMessages || committedMessages;
|
|
139
|
+
const hasConversationActivity = hasUserConversationMessage(activityMessages)
|
|
140
|
+
|| activityMessages.some((message) => (
|
|
141
|
+
message?.role === 'user'
|
|
142
|
+
&& typeof message.content === 'string'
|
|
143
|
+
&& message.content.startsWith(SUMMARY_PREFIX)
|
|
144
|
+
));
|
|
145
|
+
// A route is not a conversation. Keep a pristine desktop/TUI task truly
|
|
146
|
+
// empty until the first real turn. Remote auto-start may prepare a local
|
|
147
|
+
// session shell containing system/tool templates, but those templates have
|
|
148
|
+
// not entered a provider request and must not appear as consumed context.
|
|
149
|
+
if (!session?.id || !hasConversationActivity) {
|
|
150
|
+
const emptyCompactPolicy = session
|
|
151
|
+
? resolveWorkerCompactPolicy(session, Array.isArray(session.tools) ? session.tools : [])
|
|
152
|
+
: null;
|
|
153
|
+
const routeWindow = Math.max(0, Number(
|
|
154
|
+
session?.compactBoundaryTokens
|
|
155
|
+
|| session?.contextWindow
|
|
156
|
+
|| route?.contextWindow
|
|
157
|
+
|| 0,
|
|
158
|
+
));
|
|
159
|
+
return {
|
|
160
|
+
sessionId: session?.id || null,
|
|
161
|
+
provider: route.provider,
|
|
162
|
+
model: route.model,
|
|
163
|
+
cwd: getCurrentCwd(),
|
|
164
|
+
toolMode: getMode(),
|
|
165
|
+
contextWindow: routeWindow || null,
|
|
166
|
+
effectiveContextWindow: routeWindow || null,
|
|
167
|
+
rawContextWindow: routeWindow || null,
|
|
168
|
+
effectiveContextWindowPercent: null,
|
|
169
|
+
usedTokens: 0,
|
|
170
|
+
usedSource: 'empty',
|
|
171
|
+
currentEstimatedTokens: 0,
|
|
172
|
+
lastApiRequestTokens: 0,
|
|
173
|
+
lastApiRequestStale: false,
|
|
174
|
+
freeTokens: routeWindow,
|
|
175
|
+
compaction: {
|
|
176
|
+
boundaryTokens: Number(session?.compactBoundaryTokens || emptyCompactPolicy?.boundaryTokens || 0) || null,
|
|
177
|
+
triggerTokens: Number(emptyCompactPolicy?.triggerTokens || 0) || null,
|
|
178
|
+
bufferTokens: Number(emptyCompactPolicy?.bufferTokens || 0) || null,
|
|
179
|
+
bufferRatio: Number.isFinite(emptyCompactPolicy?.bufferRatio)
|
|
180
|
+
? emptyCompactPolicy.bufferRatio
|
|
181
|
+
: null,
|
|
182
|
+
currentEstimatedTokens: 0,
|
|
183
|
+
lastApiRequestTokens: 0,
|
|
184
|
+
lastApiRequestStale: false,
|
|
185
|
+
},
|
|
186
|
+
messages: summarizeContextMessages([]),
|
|
187
|
+
request: {
|
|
188
|
+
toolSchemaTokens: 0,
|
|
189
|
+
toolSchemaBreakdown: {},
|
|
190
|
+
requestOverheadTokens: 0,
|
|
191
|
+
reserveTokens: 0,
|
|
192
|
+
},
|
|
193
|
+
usage: {
|
|
194
|
+
lastInputTokens: 0,
|
|
195
|
+
lastUncachedInputTokens: 0,
|
|
196
|
+
lastOutputTokens: 0,
|
|
197
|
+
lastCachedReadTokens: 0,
|
|
198
|
+
lastCacheWriteTokens: 0,
|
|
199
|
+
lastContextTokens: 0,
|
|
200
|
+
totalInputTokens: 0,
|
|
201
|
+
totalUncachedInputTokens: 0,
|
|
202
|
+
totalOutputTokens: 0,
|
|
203
|
+
totalCachedReadTokens: 0,
|
|
204
|
+
totalCacheWriteTokens: 0,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
133
208
|
// Prefer the in-flight working transcript while a turn is running so the
|
|
134
209
|
// context gauge reflects LIVE growth (user turn + tool calls/results) as
|
|
135
210
|
// it accumulates, instead of freezing at the pre-turn committed snapshot.
|
|
136
211
|
// askSession() sets session.liveTurnMessages for the turn duration and
|
|
137
212
|
// clears it on commit/cancel/error, after which we fall back to the
|
|
138
213
|
// authoritative committed transcript.
|
|
139
|
-
const
|
|
140
|
-
const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
|
|
214
|
+
const messages = activityMessages;
|
|
141
215
|
const requestProvider = session?.provider || route.provider;
|
|
142
216
|
// Do not even evaluate live native definitions when an in-flight request
|
|
143
217
|
// scope owns the complete immutable provider surface.
|
|
@@ -185,7 +259,10 @@ export function createContextStatus({
|
|
|
185
259
|
const workerCompactPolicy = resolveWorkerCompactPolicy(session, requestTools);
|
|
186
260
|
const compactPolicy = workerCompactPolicy?.boundaryTokens
|
|
187
261
|
? workerCompactPolicy
|
|
188
|
-
:
|
|
262
|
+
: {
|
|
263
|
+
...resolveSessionCompactPolicy(session || {}, compactBoundaryTokens),
|
|
264
|
+
tokenCalibration: providerTokenCalibration(session?.provider || route.provider),
|
|
265
|
+
};
|
|
189
266
|
// Match the pre-provider-send auto-compact check exactly: the gauge uses
|
|
190
267
|
// the same provider-baseline-or-estimate pressure, including request and
|
|
191
268
|
// configured reserves, rather than a separate transcript-only estimate.
|
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
} from './session-text.mjs';
|
|
12
12
|
import { toolSpecForMode, deferredSurfaceModeForLead } from './effort.mjs';
|
|
13
13
|
import { unregisterLiveSession } from '../runtime/shared/staged-update.mjs';
|
|
14
|
+
import {
|
|
15
|
+
getStoreDir,
|
|
16
|
+
listSessionHeartbeatMtimes,
|
|
17
|
+
} from '../runtime/agent/orchestrator/session/store/paths-heartbeat.mjs';
|
|
14
18
|
|
|
15
19
|
export function resolveResumeCwd(session, currentCwd) {
|
|
16
20
|
const desktop = session?.desktopSession;
|
|
@@ -44,10 +48,90 @@ export function createLifecycleApi(deps) {
|
|
|
44
48
|
withTeardownDeadline, closePatchRuntimeIfLoaded,
|
|
45
49
|
createCurrentSession, refreshRouteEffort,
|
|
46
50
|
invalidateContextStatusCache, invalidatePreSessionToolSurface,
|
|
47
|
-
applyResolvedCwd, resolveRoute, applyDeferredToolSurface,
|
|
51
|
+
applyResolvedCwd, resolveRoute, applyDeferredToolSurface, getStandaloneTools,
|
|
48
52
|
pushTranscriptRebind,
|
|
49
53
|
notificationListeners, remoteStateListeners, desktopSession,
|
|
50
54
|
} = deps;
|
|
55
|
+
const listLeadSessions = (options = {}) => {
|
|
56
|
+
const heartbeatMtimes = listSessionHeartbeatMtimes();
|
|
57
|
+
return mgr.listSessions({
|
|
58
|
+
refreshFromStorage: options?.refreshFromStorage === true,
|
|
59
|
+
}).map(s => {
|
|
60
|
+
const owner = clean(s.owner || 'user').toLowerCase();
|
|
61
|
+
if (owner && !['cli', 'user', 'mixdog', 'legacy'].includes(owner)) return null;
|
|
62
|
+
const sourceType = clean(s.sourceType || '').toLowerCase();
|
|
63
|
+
const sourceName = clean(s.sourceName || '').toLowerCase();
|
|
64
|
+
const agent = clean(s.agent || '').toLowerCase();
|
|
65
|
+
const leadish = agent === 'lead'
|
|
66
|
+
|| sourceType === 'lead'
|
|
67
|
+
|| (sourceType === 'cli')
|
|
68
|
+
// Schedule runs are their own visible type: they surface in desktop
|
|
69
|
+
// Recent / TUI resume next to lead sessions instead of hiding like
|
|
70
|
+
// agent dispatches.
|
|
71
|
+
|| sourceType === 'schedule'
|
|
72
|
+
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|
|
73
|
+
if (!leadish) return null;
|
|
74
|
+
const rawPreview = s.preview || '';
|
|
75
|
+
let preview = isSessionPreviewNoise(rawPreview) ? '' : cleanSessionPreview(rawPreview);
|
|
76
|
+
let messageCount = Math.max(0, Number(s.messageCount) || 0);
|
|
77
|
+
if (!preview && Array.isArray(s.messages)) {
|
|
78
|
+
const msgs = s.messages || [];
|
|
79
|
+
const userPreviews = msgs
|
|
80
|
+
.filter(m => m && m.role === 'user')
|
|
81
|
+
.map(m => sessionMessageText(m.content))
|
|
82
|
+
.filter(text => !isSessionPreviewNoise(text))
|
|
83
|
+
.map(text => cleanSessionPreview(text))
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
preview = userPreviews[0] || '';
|
|
86
|
+
messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
|
|
87
|
+
}
|
|
88
|
+
if (!preview && messageCount === 0) return null;
|
|
89
|
+
return {
|
|
90
|
+
id: s.id,
|
|
91
|
+
updatedAt: s.updatedAt,
|
|
92
|
+
cwd: s.cwd || '',
|
|
93
|
+
model: s.model,
|
|
94
|
+
provider: s.provider,
|
|
95
|
+
messageCount,
|
|
96
|
+
preview,
|
|
97
|
+
heartbeatAt: Math.max(
|
|
98
|
+
Number(s.lastHeartbeatAt) || 0,
|
|
99
|
+
Number(heartbeatMtimes.get(s.id)) || 0,
|
|
100
|
+
),
|
|
101
|
+
desktopSession: s.desktopSession || null,
|
|
102
|
+
};
|
|
103
|
+
}).filter(Boolean);
|
|
104
|
+
};
|
|
105
|
+
// Persist a closing session's conversation into the memory DB. Sessions
|
|
106
|
+
// that never compacted had NO session-sourced rows (ingest_session
|
|
107
|
+
// previously ran only inside compaction), so recall could not reconstruct
|
|
108
|
+
// the most recent conversations. Best-effort and deadline-capped: the row
|
|
109
|
+
// inserts are fast and committed even if the bounded wait elapses during
|
|
110
|
+
// the trailing embedding flush (the raw-embedding backlog sweep embeds
|
|
111
|
+
// them later). Never loads the memory runtime just for this — a null
|
|
112
|
+
// module promise means memory was never used this run, so skip.
|
|
113
|
+
async function ingestSessionIntoMemory(session) {
|
|
114
|
+
try {
|
|
115
|
+
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
116
|
+
if (!session?.id || messages.length === 0) return;
|
|
117
|
+
const modPromise = getMemoryModPromise();
|
|
118
|
+
if (!modPromise) return;
|
|
119
|
+
await withTeardownDeadline(
|
|
120
|
+
Promise.resolve(modPromise)
|
|
121
|
+
.then((mod) => (typeof mod?.handleToolCall === 'function'
|
|
122
|
+
? mod.handleToolCall('memory', {
|
|
123
|
+
action: 'ingest_session',
|
|
124
|
+
sessionId: session.id,
|
|
125
|
+
cwd: session.cwd || getCurrentCwd(),
|
|
126
|
+
messages,
|
|
127
|
+
})
|
|
128
|
+
: null))
|
|
129
|
+
.catch(() => {}),
|
|
130
|
+
2500,
|
|
131
|
+
undefined,
|
|
132
|
+
);
|
|
133
|
+
} catch { /* best-effort: memory ingest must never break lifecycle paths */ }
|
|
134
|
+
}
|
|
51
135
|
return {
|
|
52
136
|
async close(reason = 'cli-exit', options = {}) {
|
|
53
137
|
const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
|
|
@@ -78,6 +162,9 @@ export function createLifecycleApi(deps) {
|
|
|
78
162
|
);
|
|
79
163
|
}
|
|
80
164
|
} catch { /* best-effort: SessionEnd hook must never wedge teardown */ }
|
|
165
|
+
// Ingest the final conversation BEFORE the memory runtime stop below is
|
|
166
|
+
// kicked off, so the write happens against a live module/daemon.
|
|
167
|
+
try { await ingestSessionIntoMemory(getSession()); } catch { /* best-effort */ }
|
|
81
168
|
// Teardown stays async end-to-end across every writer sharing the config
|
|
82
169
|
// lock. Never start a synchronous lock wait while an in-process async
|
|
83
170
|
// holder still needs the event loop to finish and release it.
|
|
@@ -189,47 +276,50 @@ export function createLifecycleApi(deps) {
|
|
|
189
276
|
return mgr.abortSessionTurn(session.id, reason);
|
|
190
277
|
},
|
|
191
278
|
listSessions(options = {}) {
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
279
|
+
return listLeadSessions(options);
|
|
280
|
+
},
|
|
281
|
+
// Desktop watcher hook: absolute path of the on-disk session store so the
|
|
282
|
+
// host can fs.watch it and push sidebar updates instead of polling.
|
|
283
|
+
sessionStoreDir() {
|
|
284
|
+
try { return getStoreDir(); } catch { return null; }
|
|
285
|
+
},
|
|
286
|
+
async deleteSession(id) {
|
|
287
|
+
const sessionId = clean(id);
|
|
288
|
+
if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return false;
|
|
289
|
+
const available = listLeadSessions({ refreshFromStorage: true })
|
|
290
|
+
.some(row => row.id === sessionId);
|
|
291
|
+
if (!available) return false;
|
|
292
|
+
const current = getSession();
|
|
293
|
+
if (current?.id !== sessionId) return mgr.deleteSession(sessionId) === true;
|
|
294
|
+
|
|
295
|
+
const cleanupReason = 'desktop-session-delete';
|
|
296
|
+
try {
|
|
297
|
+
cancelBackgroundTasksForLifecycle({
|
|
298
|
+
reason: cleanupReason,
|
|
299
|
+
notify: false,
|
|
300
|
+
callerSessionId: sessionId,
|
|
301
|
+
});
|
|
302
|
+
} catch {}
|
|
303
|
+
try { agentTool?.closeAll?.(cleanupReason); } catch {}
|
|
304
|
+
statusRoutes?.clearGatewaySessionRoute?.(sessionId);
|
|
305
|
+
// Active sessions retain a tombstone until the normal sweep. Unlinking
|
|
306
|
+
// immediately would let a late provider/save continuation resurrect the
|
|
307
|
+
// deleted conversation after the user has moved to its replacement.
|
|
308
|
+
if (mgr.closeSession(sessionId, cleanupReason, { tombstone: true }) !== true) return false;
|
|
309
|
+
setSession(null);
|
|
310
|
+
invalidateContextStatusCache();
|
|
311
|
+
invalidatePreSessionToolSurface();
|
|
312
|
+
await createCurrentSession();
|
|
313
|
+
pushTranscriptRebind?.();
|
|
314
|
+
return true;
|
|
228
315
|
},
|
|
229
316
|
async switchContext({ cwd, desktopSession: nextDesktopSession } = {}) {
|
|
230
317
|
const session = getSession();
|
|
231
318
|
if (session?.id) {
|
|
232
319
|
const cleanupReason = 'desktop-context-switch';
|
|
320
|
+
// Fire-and-forget: context switch is user-facing latency; the memory
|
|
321
|
+
// runtime outlives the closed session so the write completes safely.
|
|
322
|
+
void ingestSessionIntoMemory(session);
|
|
233
323
|
try {
|
|
234
324
|
cancelBackgroundTasksForLifecycle({
|
|
235
325
|
reason: cleanupReason,
|
|
@@ -247,7 +337,21 @@ export function createLifecycleApi(deps) {
|
|
|
247
337
|
setDesktopSession(nextDesktopSession && typeof nextDesktopSession === 'object'
|
|
248
338
|
? nextDesktopSession
|
|
249
339
|
: null);
|
|
250
|
-
|
|
340
|
+
// Do NOT block the switch on the project MCP reconnect (observed 5s+ per
|
|
341
|
+
// project entry on desktop). The reset still STARTS here synchronously
|
|
342
|
+
// (generation bump + in-flight registration inside applyResolvedCwd), so
|
|
343
|
+
// stale servers cannot be re-adopted, and the ask path gates boundedly on
|
|
344
|
+
// the in-flight connect before the next turn's tool surface is built.
|
|
345
|
+
await applyResolvedCwd(cwd, { markRefresh: false });
|
|
346
|
+
// Resuming a historical session temporarily routes the runtime through
|
|
347
|
+
// that session's provider/model. A fresh desktop task or project must
|
|
348
|
+
// return to the configured Lead route instead of inheriting the route
|
|
349
|
+
// of whichever session happened to be open immediately beforehand.
|
|
350
|
+
if (typeof setRoute === 'function' && typeof getConfig === 'function'
|
|
351
|
+
&& typeof resolveRoute === 'function') {
|
|
352
|
+
setRoute(resolveRoute(getConfig(), {}));
|
|
353
|
+
await refreshRouteEffort?.();
|
|
354
|
+
}
|
|
251
355
|
invalidateContextStatusCache();
|
|
252
356
|
invalidatePreSessionToolSurface();
|
|
253
357
|
return true;
|
|
@@ -255,6 +359,7 @@ export function createLifecycleApi(deps) {
|
|
|
255
359
|
async newSession() {
|
|
256
360
|
const session = getSession();
|
|
257
361
|
if (session?.id) {
|
|
362
|
+
void ingestSessionIntoMemory(session);
|
|
258
363
|
const tombstone = !hasUserConversationMessage(session.messages)
|
|
259
364
|
&& !hasUserConversationMessage(session.liveTurnMessages);
|
|
260
365
|
mgr.closeSession(session.id, 'cli-new', { tombstone });
|
|
@@ -268,12 +373,20 @@ export function createLifecycleApi(deps) {
|
|
|
268
373
|
pushTranscriptRebind?.();
|
|
269
374
|
return getSession().id;
|
|
270
375
|
},
|
|
376
|
+
prefetchSession(id) {
|
|
377
|
+
return mgr.prefetchSession?.(id, toolSpecForMode(getMode())) === true;
|
|
378
|
+
},
|
|
271
379
|
async resume(id) {
|
|
272
380
|
const prev = getSession();
|
|
273
381
|
const previousId = prev?.id || null;
|
|
274
382
|
const previousMessages = prev?.messages || null;
|
|
275
383
|
const previousLive = prev?.liveTurnMessages || null;
|
|
276
|
-
|
|
384
|
+
// A context switch can deliberately clear the desktop marker for legacy
|
|
385
|
+
// sessions. Fall back to the creation-time value only for callers that
|
|
386
|
+
// do not supply mutable context bindings.
|
|
387
|
+
const activeDesktopSession = typeof getDesktopSession === 'function'
|
|
388
|
+
? getDesktopSession()
|
|
389
|
+
: desktopSession;
|
|
277
390
|
const resumeOptions = activeDesktopSession && typeof activeDesktopSession === 'object'
|
|
278
391
|
? { desktopSession: activeDesktopSession }
|
|
279
392
|
: undefined;
|
|
@@ -281,6 +394,7 @@ export function createLifecycleApi(deps) {
|
|
|
281
394
|
if (!resumed) return null;
|
|
282
395
|
if (previousId && previousId !== resumed.id) {
|
|
283
396
|
statusRoutes?.clearGatewaySessionRoute?.(previousId);
|
|
397
|
+
void ingestSessionIntoMemory(prev);
|
|
284
398
|
const tombstone = !hasUserConversationMessage(previousMessages)
|
|
285
399
|
&& !hasUserConversationMessage(previousLive);
|
|
286
400
|
mgr.closeSession(previousId, 'cli-resume', { tombstone });
|
|
@@ -294,7 +408,7 @@ export function createLifecycleApi(deps) {
|
|
|
294
408
|
const session = getSession();
|
|
295
409
|
session.effort = getRoute().effectiveEffort || null;
|
|
296
410
|
session.cwd = getCurrentCwd();
|
|
297
|
-
applyDeferredToolSurface(session, deferredSurfaceModeForLead(getMode()),
|
|
411
|
+
applyDeferredToolSurface(session, deferredSurfaceModeForLead(getMode()), getStandaloneTools(), { provider: getRoute().provider });
|
|
298
412
|
invalidatePreSessionToolSurface();
|
|
299
413
|
invalidateContextStatusCache();
|
|
300
414
|
setSessionNeedsCwdRefresh(false);
|