mixdog 0.9.92 → 0.9.93
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/README.md +147 -51
- package/package.json +6 -5
- package/scripts/code-graph-description-contract.mjs +6 -8
- package/scripts/tmp-cdp-errors.mjs +41 -0
- package/scripts/tmp-cdp-inspect.mjs +41 -0
- package/scripts/tui-transcript-jitter-harness.mjs +2 -18
- package/src/rules/agent/00-core.md +1 -2
- package/src/rules/lead/01-general.md +1 -0
- package/src/rules/shared/01-tool.md +19 -22
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +16 -3
- package/src/runtime/agent/orchestrator/agent-trace.mjs +17 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +79 -21
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +40 -19
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +44 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +22 -5
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +35 -29
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +5 -6
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +31 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +60 -31
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +30 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -23
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -21
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +12 -6
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +47 -14
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +100 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +6 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +32 -2
- package/src/runtime/channels/backends/discord-gateway.mjs +6 -32
- package/src/runtime/channels/lib/inbound-handler.mjs +19 -3
- package/src/runtime/channels/lib/scheduler.mjs +51 -3
- package/src/runtime/channels/lib/worker-main.mjs +4 -0
- package/src/runtime/channels/tool-defs.mjs +4 -2
- package/src/runtime/memory/lib/query-handlers.mjs +11 -3
- package/src/runtime/memory/tool-defs.mjs +5 -5
- package/src/runtime/shared/channel-notification-routing.mjs +8 -2
- package/src/runtime/shared/llm/http-agent.mjs +11 -0
- package/src/session-runtime/lifecycle-api.mjs +26 -1
- package/src/session-runtime/tool-catalog-data.mjs +5 -2
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/tag-registry.mjs +5 -1
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/dist/index.mjs +68 -45
- package/src/tui/engine/agent-envelope.mjs +52 -3
- package/src/tui/engine/turn.mjs +8 -9
- package/src/tui/engine.mjs +18 -41
- package/src/workflows/solo/WORKFLOW.md +0 -6
- package/src/workflows/solo-bench/WORKFLOW.md +17 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Style mirrors shell-snapshot.mjs: files live under getPluginData(), one file
|
|
10
10
|
// per session key, with a stale-file sweep on module init.
|
|
11
11
|
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, statSync, utimesSync } from 'node:fs';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, utimesSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { readdir as readdirAsync, stat as statAsync, unlink as unlinkAsync } from 'node:fs/promises';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
@@ -30,6 +30,10 @@ function _stateFileFor(sessionKey) {
|
|
|
30
30
|
return join(_stateDir(), `cwd-${_keyFor(sessionKey)}.txt`);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function _probeFileFor(sessionKey, probeId) {
|
|
34
|
+
return join(_stateDir(), `cwd-${_keyFor(sessionKey)}.probe-${_keyFor(probeId)}.txt`);
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
// Stale-file sweep on module init. Mirrors shell-snapshot's _sweepStaleSnapshots:
|
|
34
38
|
// state files older than the cutoff belong to sessions that are long gone.
|
|
35
39
|
const STATE_STALE_MS = 24 * 60 * 60 * 1000;
|
|
@@ -80,10 +84,13 @@ export function resolveSessionCwd(sessionKey, explicitCwd, defaultCwd) {
|
|
|
80
84
|
// OR when there is no nonblank session key — without a real key all callers
|
|
81
85
|
// would collapse onto a single shared `__default__` file and cross-contaminate
|
|
82
86
|
// each other's cwd, so we skip carry-over entirely in that case.
|
|
83
|
-
export function stateFilePath(sessionKey) {
|
|
87
|
+
export function stateFilePath(sessionKey, probeId = null) {
|
|
84
88
|
if (sessionKey == null || String(sessionKey).trim() === '') return null;
|
|
85
89
|
const dir = _stateDir();
|
|
86
90
|
try { mkdirSync(dir, { recursive: true }); } catch { return null; }
|
|
91
|
+
if (probeId != null && String(probeId).trim() !== '') {
|
|
92
|
+
return _probeFileFor(sessionKey, probeId);
|
|
93
|
+
}
|
|
87
94
|
const file = _stateFileFor(sessionKey);
|
|
88
95
|
// Refresh mtime on every write so an old-but-live session file is not
|
|
89
96
|
// reaped by the stale sweep mid-session. `touch`-equivalent: bump atime/
|
|
@@ -98,6 +105,29 @@ export function stateFilePath(sessionKey) {
|
|
|
98
105
|
return file;
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
// A parallel shell-only batch starts every call from the same cwd snapshot.
|
|
109
|
+
// Each call writes its final cwd to a private probe file; the serial batch
|
|
110
|
+
// result collector commits those probes in model call order, making the final
|
|
111
|
+
// carried cwd deterministic and avoiding concurrent writes to one Windows
|
|
112
|
+
// Out-File target.
|
|
113
|
+
export function commitSessionCwdProbe(sessionKey, probeId) {
|
|
114
|
+
if (sessionKey == null || String(sessionKey).trim() === '') return false;
|
|
115
|
+
if (probeId == null || String(probeId).trim() === '') return false;
|
|
116
|
+
const probe = _probeFileFor(sessionKey, probeId);
|
|
117
|
+
try {
|
|
118
|
+
if (!existsSync(probe)) return false;
|
|
119
|
+
const stored = readFileSync(probe, 'utf-8').trim();
|
|
120
|
+
if (!stored || !statSync(stored).isDirectory()) return false;
|
|
121
|
+
mkdirSync(_stateDir(), { recursive: true });
|
|
122
|
+
writeFileSync(_stateFileFor(sessionKey), `${stored}\n`, 'utf-8');
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
} finally {
|
|
127
|
+
try { unlinkSync(probe); } catch {}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
101
131
|
// PowerShell single-quote escape ('' escapes a literal quote inside '...').
|
|
102
132
|
function _psQuote(s) {
|
|
103
133
|
return `'${String(s).replace(/'/g, "''")}'`;
|
|
@@ -86,18 +86,6 @@ function registerEventListeners(self) {
|
|
|
86
86
|
} catch {}
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
|
-
if (interaction.isChatInputCommand() && interaction.commandName === "stop") {
|
|
90
|
-
await interaction.reply({ content: "\u23F9 Stopping...", ephemeral: true });
|
|
91
|
-
if (self.onInteraction) {
|
|
92
|
-
self.onInteraction({
|
|
93
|
-
type: "button",
|
|
94
|
-
customId: "stop_task",
|
|
95
|
-
userId: interaction.user.id,
|
|
96
|
-
channelId: interaction.channelId ?? ""
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
89
|
if (interaction.isModalSubmit()) {
|
|
102
90
|
if (self.onInteraction) {
|
|
103
91
|
const fields = {};
|
|
@@ -154,31 +142,17 @@ function registerSlashCommands(self) {
|
|
|
154
142
|
process.stderr.write(`mixdog discord: gateway connected as ${c.user.tag}
|
|
155
143
|
`);
|
|
156
144
|
try {
|
|
157
|
-
//
|
|
158
|
-
//
|
|
145
|
+
// Mixdog registers NO application commands. Clear the global slot and
|
|
146
|
+
// every guild's set so entries from prior installs (including the old
|
|
147
|
+
// /stop) disappear from the Discord UI.
|
|
159
148
|
await c.application?.commands.set([]);
|
|
160
|
-
process.stderr.write(`mixdog discord: global application commands cleared
|
|
161
|
-
`);
|
|
162
|
-
|
|
163
|
-
// Replace each guild's command set with just /stop. set() overwrites,
|
|
164
|
-
// so the desired set is the only one that survives.
|
|
165
|
-
const desiredCommands = [
|
|
166
|
-
{ name: "stop", description: "Stop the current Mixdog response" },
|
|
167
|
-
];
|
|
168
149
|
for (const [guildId] of c.guilds.cache) {
|
|
169
|
-
await c.application?.commands.set(
|
|
170
|
-
}
|
|
171
|
-
// Register /stop globally so it is available in DM bridge contexts
|
|
172
|
-
// where there is no guild scope.
|
|
173
|
-
try {
|
|
174
|
-
await c.application?.commands.set(desiredCommands);
|
|
175
|
-
} catch (e) {
|
|
176
|
-
process.stderr.write(`mixdog discord: global /stop register failed: ${e?.message}\n`);
|
|
150
|
+
await c.application?.commands.set([], guildId);
|
|
177
151
|
}
|
|
178
|
-
process.stderr.write(`mixdog discord:
|
|
152
|
+
process.stderr.write(`mixdog discord: application commands cleared (${c.guilds.cache.size} guild(s))
|
|
179
153
|
`);
|
|
180
154
|
} catch (err) {
|
|
181
|
-
process.stderr.write(`mixdog discord: slash command
|
|
155
|
+
process.stderr.write(`mixdog discord: slash command cleanup failed: ${err}
|
|
182
156
|
`);
|
|
183
157
|
}
|
|
184
158
|
});
|
|
@@ -58,11 +58,27 @@ const inboundQueue = (() => {
|
|
|
58
58
|
getBackend().onMessage = (msg) => {
|
|
59
59
|
const receivedAtMs = Number.isFinite(msg.receivedAtMs) ? msg.receivedAtMs : Date.now();
|
|
60
60
|
const onMessageAtMs = Date.now();
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const gateBlocked = (!getBridgeRuntimeConnected() || !getBridgeOwnershipSnapshot().owned)
|
|
62
|
+
? "bridge-seat"
|
|
63
|
+
: (!getChannelBridgeActive() ? "bridge-inactive" : "");
|
|
64
|
+
if (gateBlocked) {
|
|
65
|
+
if (gateBlocked === "bridge-seat") refreshBridgeOwnershipSafe();
|
|
66
|
+
// CC parity: an inbound message is never silently swallowed. The bridge
|
|
67
|
+
// seat may be mid-handover or the channel bridge may still be activating
|
|
68
|
+
// (app relaunch / boot); retry with backoff before giving up, and make
|
|
69
|
+
// the final drop loud so a real loss stays diagnosable.
|
|
70
|
+
const attempt = (msg.__bridgeGateRetries || 0) + 1;
|
|
71
|
+
if (attempt <= 5) {
|
|
72
|
+
msg.__bridgeGateRetries = attempt;
|
|
73
|
+
msg.receivedAtMs = receivedAtMs;
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
try { getBackend().onMessage?.(msg); } catch { /* best-effort */ }
|
|
76
|
+
}, 2000 * attempt)?.unref?.();
|
|
77
|
+
} else {
|
|
78
|
+
try { process.stderr.write(`mixdog: inbound message dropped after ${attempt - 1} ${gateBlocked} retries (channel=${msg.chatId}, id=${msg.messageId})\n`); } catch {}
|
|
79
|
+
}
|
|
63
80
|
return;
|
|
64
81
|
}
|
|
65
|
-
if (!getChannelBridgeActive()) return;
|
|
66
82
|
if (shouldDropDuplicateInbound(msg)) return;
|
|
67
83
|
// No label concept anymore: the channel id IS the identifier.
|
|
68
84
|
recordFetchedMessages(msg.chatId, msg.chatId, [{ id: msg.messageId }], { markRead: true });
|
|
@@ -88,6 +88,7 @@ class Scheduler {
|
|
|
88
88
|
running = /* @__PURE__ */ new Set();
|
|
89
89
|
injectFn = null;
|
|
90
90
|
sendFn = null;
|
|
91
|
+
injectReadyFn = null;
|
|
91
92
|
pendingCheck = null;
|
|
92
93
|
// Activity tracking
|
|
93
94
|
lastActivity = 0;
|
|
@@ -114,6 +115,16 @@ class Scheduler {
|
|
|
114
115
|
setSendHandler(fn) {
|
|
115
116
|
this.sendFn = fn;
|
|
116
117
|
}
|
|
118
|
+
/** Probe for a live Lead bridge seat. When set, the CC-parity interactive
|
|
119
|
+
* inject only fires while it reports true; otherwise the visible-session
|
|
120
|
+
* fallback keeps the fire from being lost. Unset → inject when wired. */
|
|
121
|
+
setInjectReadyCheck(fn) {
|
|
122
|
+
this.injectReadyFn = typeof fn === 'function' ? fn : null;
|
|
123
|
+
}
|
|
124
|
+
injectReady() {
|
|
125
|
+
if (!this.injectReadyFn) return true;
|
|
126
|
+
try { return !!this.injectReadyFn(); } catch { return false; }
|
|
127
|
+
}
|
|
117
128
|
setPendingCheck(fn) {
|
|
118
129
|
this.pendingCheck = typeof fn === 'function' ? fn : null;
|
|
119
130
|
}
|
|
@@ -473,6 +484,7 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
473
484
|
await this.markDoneWithRetry(schedule.name);
|
|
474
485
|
} catch (err) {
|
|
475
486
|
process.stderr.write(`mixdog scheduler: ${schedule.name} one-shot failed: ${err} — leaving pending for retry\n`);
|
|
487
|
+
this.notifyFailure(schedule, `one-shot failed: ${err?.message || err} — left pending for retry`);
|
|
476
488
|
}
|
|
477
489
|
}
|
|
478
490
|
restart() {
|
|
@@ -556,6 +568,7 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
556
568
|
if (!prompt) {
|
|
557
569
|
process.stderr.write(`mixdog scheduler: prompt not found for "${schedule.name}"
|
|
558
570
|
`);
|
|
571
|
+
this.notifyFailure(schedule, "prompt not found — fire skipped");
|
|
559
572
|
return false;
|
|
560
573
|
}
|
|
561
574
|
// target 'channel' → relay the run result to the schedule's channel_id
|
|
@@ -570,14 +583,34 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
570
583
|
async fireTimedPrompt(schedule, type, prompt, channelId, { awaitDispatch = false } = {}) {
|
|
571
584
|
logSchedule(`firing ${schedule.name} (${type})
|
|
572
585
|
`);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
586
|
+
if (this.running.has(schedule.name)) {
|
|
587
|
+
// Not silent anymore: overlap skips were invisible and read as the
|
|
588
|
+
// scheduler "ignoring" a fire.
|
|
589
|
+
logSchedule(`${schedule.name}: skipped — previous run still in progress\n`);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
// CC parity (reference messageQueueManager.ts): interactive schedules
|
|
593
|
+
// enqueue into the live Lead session's command queue as system-generated
|
|
594
|
+
// task notifications (priority 'later'), exactly like inbound channel
|
|
595
|
+
// messages — enqueue counts as the fire. The visible-session run below
|
|
596
|
+
// remains the non-interactive path AND the fallback when no live Lead
|
|
597
|
+
// seat is attached, so a fire is never lost.
|
|
598
|
+
if (type === "interactive" && this.injectFn && this.injectReady()) {
|
|
599
|
+
try {
|
|
600
|
+
const wrapped = this.wrapPrompt(schedule.name, prompt, type);
|
|
601
|
+
this.injectFn(channelId, `schedule:${schedule.name}`, " ", { type: "schedule", instruction: wrapped });
|
|
602
|
+
logSchedule(`${schedule.name}: injected into Lead session queue (CC-parity interactive fire)\n`);
|
|
603
|
+
return true;
|
|
604
|
+
} catch (err) {
|
|
605
|
+
logSchedule(`${schedule.name}: Lead inject failed (${err?.message || err}) — falling back to visible session run\n`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
576
608
|
this.running.add(schedule.name);
|
|
577
609
|
const presetId = schedule.model;
|
|
578
610
|
if (!presetId) {
|
|
579
611
|
this.running.delete(schedule.name);
|
|
580
612
|
logSchedule(`${schedule.name}: missing required "model" in schedule config — dispatch rejected\n`);
|
|
613
|
+
this.notifyFailure(schedule, 'missing "model" in schedule config — dispatch rejected');
|
|
581
614
|
return false;
|
|
582
615
|
}
|
|
583
616
|
// Cron (recurring) keeps the fire-and-forget contract: return truthy now
|
|
@@ -601,6 +634,10 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
601
634
|
.catch((err) => {
|
|
602
635
|
this.running.delete(schedule.name);
|
|
603
636
|
logSchedule(`${schedule.name} LLM error: ${err.message}\n`);
|
|
637
|
+
// The cron fire-and-forget contract swallowed failures entirely;
|
|
638
|
+
// surface them on the channel so a failed scheduled run is never
|
|
639
|
+
// silent (one-shots propagate via the awaitDispatch throw instead).
|
|
640
|
+
if (!awaitDispatch) this.notifyFailure(schedule, `run failed: ${err.message}`);
|
|
604
641
|
if (awaitDispatch) throw err;
|
|
605
642
|
return false;
|
|
606
643
|
});
|
|
@@ -608,6 +645,17 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
608
645
|
return true;
|
|
609
646
|
}
|
|
610
647
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
648
|
+
/** Best-effort failure notice to the schedule's channel (or the main
|
|
649
|
+
* channel) so scheduled fires never fail silently. Respects an explicit
|
|
650
|
+
* channel opt-out ("false" → resolveChannel yields "" → no notice). */
|
|
651
|
+
notifyFailure(schedule, reason) {
|
|
652
|
+
try {
|
|
653
|
+
if (!this.sendFn) return;
|
|
654
|
+
const target = this.resolveChannel(schedule?.channelId);
|
|
655
|
+
if (!target) return;
|
|
656
|
+
Promise.resolve(this.sendFn(target, `schedule "${schedule?.name}": ${reason}`)).catch(() => {});
|
|
657
|
+
} catch { /* best-effort */ }
|
|
658
|
+
}
|
|
611
659
|
/** Resolve prompt: inline text from the row, with a prompts-dir file
|
|
612
660
|
* fallback for legacy `<name>.md` references. */
|
|
613
661
|
resolvePrompt(schedule) {
|
|
@@ -435,6 +435,10 @@ function forwardLifecycleToDiscord(channelId, content) {
|
|
|
435
435
|
scheduler.setInjectHandler((channelId, name, content, options) => {
|
|
436
436
|
injectAndRecord(channelId, name, content, options);
|
|
437
437
|
});
|
|
438
|
+
// CC-parity interactive schedule fires may only inject while this process
|
|
439
|
+
// actually holds the live bridge seat; otherwise the scheduler falls back to
|
|
440
|
+
// the visible-session run so the fire is never lost.
|
|
441
|
+
scheduler.setInjectReadyCheck(() => bridgeRuntimeConnected && currentOwnerState().owned);
|
|
438
442
|
scheduler.setSendHandler(async (channelId, text) => {
|
|
439
443
|
// Skip protocol: a scheduler-task emitting `[meta:silent]` has nothing to
|
|
440
444
|
// report — suppress the channel send entirely (no noise). Mirrors the
|
|
@@ -25,7 +25,8 @@ export const TOOL_DEFS = [
|
|
|
25
25
|
description: "Discord components."
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
|
-
required: ["message"]
|
|
28
|
+
required: ["message"],
|
|
29
|
+
additionalProperties: false
|
|
29
30
|
}
|
|
30
31
|
},
|
|
31
32
|
{
|
|
@@ -39,7 +40,8 @@ export const TOOL_DEFS = [
|
|
|
39
40
|
channel: { type: "string", description: "Discord channel id." },
|
|
40
41
|
limit: { type: "number", description: "Max messages." }
|
|
41
42
|
},
|
|
42
|
-
required: ["channel"]
|
|
43
|
+
required: ["channel"],
|
|
44
|
+
additionalProperties: false
|
|
43
45
|
}
|
|
44
46
|
},
|
|
45
47
|
// memory and recall_memory tools are now provided by memory-service.mjs via MCP
|
|
@@ -697,11 +697,15 @@ export function createQueryHandlers({
|
|
|
697
697
|
}
|
|
698
698
|
const excludeStatuses = includeArchived ? [] : ['archived']
|
|
699
699
|
const VALID_LAST_CATS = new Set(['rule', 'constraint', 'decision', 'fact', 'goal', 'preference', 'task', 'issue'])
|
|
700
|
-
const
|
|
700
|
+
const requestedCats = category == null
|
|
701
701
|
? []
|
|
702
|
-
: (Array.isArray(category) ? category : [category])
|
|
702
|
+
: [...new Set((Array.isArray(category) ? category : [category])
|
|
703
703
|
.map((c) => String(c).trim().toLowerCase())
|
|
704
|
-
.filter((c) => VALID_LAST_CATS.has(c))
|
|
704
|
+
.filter((c) => VALID_LAST_CATS.has(c)))]
|
|
705
|
+
// Asking for every public category is semantically unfiltered. Keeping
|
|
706
|
+
// it as a restrictive filter drops fresh unclassified raw turns before
|
|
707
|
+
// cycle1 assigns a category, which can hide the immediately prior chat.
|
|
708
|
+
const catList = requestedCats.length === VALID_LAST_CATS.size ? [] : requestedCats
|
|
705
709
|
// 1) rank sessions by most-recent activity (MAX(ts)) over the SAME pool
|
|
706
710
|
// the fill draws from — roots (status-filtered) + unchunked raw
|
|
707
711
|
// leaves, member rows excluded — so a session ranked here can never
|
|
@@ -720,6 +724,10 @@ export function createQueryHandlers({
|
|
|
720
724
|
selParams.push(projectScope)
|
|
721
725
|
selWhere.push(`(project_id IS NULL OR project_id = $${selParams.length})`)
|
|
722
726
|
}
|
|
727
|
+
if (catList.length > 0) {
|
|
728
|
+
const ph = catList.map((c) => { selParams.push(c); return `$${selParams.length}` }).join(',')
|
|
729
|
+
selWhere.push(`lower(coalesce(category, '')) IN (${ph})`)
|
|
730
|
+
}
|
|
723
731
|
if (excludeStatuses.length > 0) {
|
|
724
732
|
const ph = excludeStatuses.map((s) => { selParams.push(s); return `$${selParams.length}` }).join(',')
|
|
725
733
|
selWhere.push(`(status IS NULL OR status NOT IN (${ph}))`)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// they never reach an external LLM. `aiWrapped: true` routes dispatches
|
|
5
5
|
// through ai-wrapped-dispatch.mjs instead of the module's handleToolCall.
|
|
6
6
|
// Shared period grammar for recall/search_memories.
|
|
7
|
-
const PERIOD_DESCRIPTION = "last (recent sessions; +query topic-filter
|
|
7
|
+
const PERIOD_DESCRIPTION = "last (recent sessions; +query topic-filter), Nm/Nh/Nd, today/yesterday/this_week/last_week, all, YYYY-MM-DD, date~date, or HH:MM~HH:MM.";
|
|
8
8
|
export const TOOL_DEFS = [
|
|
9
9
|
{
|
|
10
10
|
name: 'memory',
|
|
@@ -48,7 +48,7 @@ export const TOOL_DEFS = [
|
|
|
48
48
|
name: 'recall',
|
|
49
49
|
title: 'Recall',
|
|
50
50
|
annotations: { title: 'Recall', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
51
|
-
description: 'Retrieve stored memory/session history
|
|
51
|
+
description: 'Retrieve stored memory/session history (prior work, resumes, decisions). Query is semantic, not regex; period=time window; id=exact follow-up.',
|
|
52
52
|
inputSchema: {
|
|
53
53
|
type: 'object',
|
|
54
54
|
properties: {
|
|
@@ -60,10 +60,10 @@ export const TOOL_DEFS = [
|
|
|
60
60
|
sort: { type: 'string', enum: ['importance', 'date'], description: 'importance or date.' },
|
|
61
61
|
category: { anyOf: [{ type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, { type: 'array', items: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, minItems: 1 }], description: 'Category filter.' },
|
|
62
62
|
includeArchived: { type: 'boolean', description: 'Include archived entries.' },
|
|
63
|
-
includeMembers: { type: 'boolean', description: 'Include chunk members
|
|
64
|
-
includeRaw: { type: 'boolean', description: 'Include
|
|
63
|
+
includeMembers: { type: 'boolean', description: 'Include chunk members.' },
|
|
64
|
+
includeRaw: { type: 'boolean', description: 'Include raw/episode rows.' },
|
|
65
65
|
sessionOnly: { type: 'boolean', description: 'Search this session only.' },
|
|
66
|
-
projectScope: { type: 'string', description: '
|
|
66
|
+
projectScope: { type: 'string', description: 'Pool: inferred from cwd, common, all, or slug.' },
|
|
67
67
|
},
|
|
68
68
|
additionalProperties: false,
|
|
69
69
|
},
|
|
@@ -7,6 +7,12 @@ export function channelNotificationModelContent(params = {}) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function shouldMirrorChannelNotificationToPending(meta = {}) {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// CC parity (reference messageQueueManager.ts): every inbound channel/
|
|
11
|
+
// schedule notification flows through the single queue — when no live
|
|
12
|
+
// listener handled it (engine not subscribed yet, pane disposed, headless),
|
|
13
|
+
// it is mirrored into the session pending queue for the next turn instead
|
|
14
|
+
// of being dropped. Silent-to-agent stays excluded; the upstream content
|
|
15
|
+
// gate (channelNotificationModelContent → '') already enforces it, this is
|
|
16
|
+
// a defensive second gate.
|
|
17
|
+
return !(meta?.silent_to_agent === true || meta?.silent_to_agent === 'true');
|
|
12
18
|
}
|
|
@@ -88,6 +88,17 @@ export function getLlmDispatcher() {
|
|
|
88
88
|
keepAliveTimeout: envInt('MIXDOG_LLM_KEEPALIVE_MS', 60_000),
|
|
89
89
|
keepAliveMaxTimeout: envInt('MIXDOG_LLM_KEEPALIVE_MAX_MS', 90_000),
|
|
90
90
|
connections: envInt('MIXDOG_LLM_CONNECTIONS', 16),
|
|
91
|
+
// undici defaults both of these to 300s, which beheaded the Anthropic
|
|
92
|
+
// NON-STREAMING stall fallback (2026-08-03 regex-chess probe:
|
|
93
|
+
// UND_ERR_HEADERS_TIMEOUT at exactly 300s) — a non-streaming completion
|
|
94
|
+
// sends NO response headers until generation finishes, so a long silent
|
|
95
|
+
// generation legitimately exceeds 300s of header wait. Raise both to
|
|
96
|
+
// 600s: first-byte/idle protection is owned by the app-level watchdogs
|
|
97
|
+
// (provider first-byte + semantic idle + agent stall abort), which all
|
|
98
|
+
// fire below this transport ceiling; this layer is only the last-resort
|
|
99
|
+
// dead-socket bound.
|
|
100
|
+
headersTimeout: envInt('MIXDOG_LLM_HEADERS_TIMEOUT_MS', 600_000),
|
|
101
|
+
bodyTimeout: envInt('MIXDOG_LLM_BODY_TIMEOUT_MS', 600_000),
|
|
91
102
|
})
|
|
92
103
|
}
|
|
93
104
|
// mixdog standalone: separate undici instance from Node's fetch undici, so
|
|
@@ -322,8 +322,27 @@ export function createLifecycleApi(deps) {
|
|
|
322
322
|
const available = listLeadSessions({ refreshFromStorage: true })
|
|
323
323
|
.some(row => row.id === sessionId);
|
|
324
324
|
if (!available) return false;
|
|
325
|
+
let ownedAgentSessionIds = [];
|
|
326
|
+
try {
|
|
327
|
+
const candidates = mgr.listOwnedAgentSessionIds?.(sessionId);
|
|
328
|
+
if (Array.isArray(candidates)) {
|
|
329
|
+
ownedAgentSessionIds = [...new Set(candidates
|
|
330
|
+
.map(clean)
|
|
331
|
+
.filter(childId => childId && childId !== sessionId && /^[A-Za-z0-9_-]+$/.test(childId)))];
|
|
332
|
+
}
|
|
333
|
+
} catch { /* parent deletion remains available if child enumeration fails */ }
|
|
325
334
|
const current = getSession();
|
|
326
|
-
if (current?.id !== sessionId)
|
|
335
|
+
if (current?.id !== sessionId) {
|
|
336
|
+
const deleted = mgr.deleteSession(sessionId) === true;
|
|
337
|
+
if (!deleted) return false;
|
|
338
|
+
// The parent is already irreversibly gone, so child cleanup is
|
|
339
|
+
// best-effort and idempotent. Any vetoed child becomes sweep-eligible
|
|
340
|
+
// because its retained-parent proof disappeared with the parent.
|
|
341
|
+
for (const childId of ownedAgentSessionIds) {
|
|
342
|
+
try { mgr.deleteSession(childId); } catch {}
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
327
346
|
|
|
328
347
|
const cleanupReason = 'desktop-session-delete';
|
|
329
348
|
try {
|
|
@@ -339,6 +358,12 @@ export function createLifecycleApi(deps) {
|
|
|
339
358
|
// immediately would let a late provider/save continuation resurrect the
|
|
340
359
|
// deleted conversation after the user has moved to its replacement.
|
|
341
360
|
if (mgr.closeSession(sessionId, cleanupReason, { tombstone: true }) !== true) return false;
|
|
361
|
+
// Active parent deletion uses the same durable tombstone boundary for
|
|
362
|
+
// every linked child. Their files then mature with the parent instead of
|
|
363
|
+
// disappearing while the parent task is still retained.
|
|
364
|
+
for (const childId of ownedAgentSessionIds) {
|
|
365
|
+
try { mgr.closeSession(childId, cleanupReason, { tombstone: true }); } catch {}
|
|
366
|
+
}
|
|
342
367
|
setSession(null);
|
|
343
368
|
invalidateContextStatusCache();
|
|
344
369
|
invalidatePreSessionToolSurface();
|
|
@@ -26,8 +26,11 @@ export const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
|
|
|
26
26
|
]);
|
|
27
27
|
export const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
|
|
28
28
|
'read', 'code_graph', 'grep', 'find', 'glob', 'list', 'shell', 'task',
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// cwd / session_manage / web_fetch demoted to the deferred manifest 2026-08:
|
|
30
|
+
// 0 / 0 / 10 calls in a 3-day 7.6k-call trace window; they auto-load on
|
|
31
|
+
// first direct call.
|
|
32
|
+
'explore', 'apply_patch', 'agent', 'recall', 'search',
|
|
33
|
+
'Skill', 'load_tool',
|
|
31
34
|
]);
|
|
32
35
|
|
|
33
36
|
export const READONLY_TOOL_NAMES = new Set([
|
|
@@ -208,11 +208,13 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
208
208
|
if (pack && !pack.hidden) byId.set(pack.id, pack);
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
// Solo leads (user decision: solo is the default working mode), the
|
|
212
|
+
// cowork pack (built-in id `default`) second, then customs alphabetically
|
|
213
|
+
// — every picker (TUI, desktop sidebar, onboarding) shares this order.
|
|
214
|
+
const weight = (pack) => pack.id === 'solo' ? 0
|
|
215
|
+
: (pack.id === DEFAULT_WORKFLOW_ID || pack.id === 'cowork') ? 1 : 2;
|
|
216
|
+
return [...byId.values()].sort((a, b) =>
|
|
217
|
+
(weight(a) - weight(b)) || a.name.localeCompare(b.name));
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
function activeWorkflowId(config) {
|
|
@@ -259,7 +259,11 @@ export function createTagRegistry({
|
|
|
259
259
|
const tag = tagForSession(sessionId);
|
|
260
260
|
tombstoneTerminalSession(tag, sessionId, session);
|
|
261
261
|
clearAgentStatuslineRoute(sessionId);
|
|
262
|
-
|
|
262
|
+
// Reaping expires only the reusable tag/runtime lease. The transcript is
|
|
263
|
+
// user-visible from the parent task's agent tab, so terminal cleanup must
|
|
264
|
+
// never tombstone it ahead of that parent. The normal completion path
|
|
265
|
+
// unloads immediately; this delayed call is an idempotent race fallback.
|
|
266
|
+
try { mgr.unloadSessionRuntime?.(sessionId, 'terminal-reap'); } catch {}
|
|
263
267
|
}, reapMs);
|
|
264
268
|
handle.unref?.();
|
|
265
269
|
reapTimers.set(sessionId, handle);
|
|
@@ -18,7 +18,7 @@ export const EXPLORE_TOOL = {
|
|
|
18
18
|
name: 'explore',
|
|
19
19
|
title: 'Explore',
|
|
20
20
|
annotations: { title: 'Explore', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
21
|
-
description: '
|
|
21
|
+
description: 'Repo- or machine-wide locator (dot dirs included). query[] fans out up to 8 independent facets in parallel.',
|
|
22
22
|
inputSchema: {
|
|
23
23
|
type: 'object',
|
|
24
24
|
properties: {
|