mixdog 0.9.91 → 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/tool-overhead-microbench.mjs +60 -0
- package/scripts/tui-transcript-jitter-harness.mjs +2 -18
- package/src/agents/debugger/agent.json +1 -1
- package/src/agents/explore/agent.json +1 -1
- package/src/agents/heavy-worker/agent.json +1 -1
- package/src/agents/maintainer/agent.json +1 -1
- package/src/agents/reviewer/agent.json +1 -1
- package/src/agents/worker/agent.json +1 -1
- package/src/lib/rules-builder.cjs +5 -5
- package/src/output-styles/simple.md +1 -1
- 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 -21
- 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 +90 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -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-oauth-ws.mjs +10 -0
- 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/providers/retry-classifier.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +34 -5
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +35 -29
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -0
- 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/result-classification.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +71 -2
- 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/builtin/task-tool.mjs +5 -0
- 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/output-styles.mjs +1 -4
- package/src/session-runtime/tool-catalog-data.mjs +5 -2
- package/src/session-runtime/workflow.mjs +25 -13
- package/src/standalone/agent-tool/tag-registry.mjs +5 -1
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/dist/index.mjs +100 -46
- package/src/tui/engine/agent-envelope.mjs +52 -3
- package/src/tui/engine/session-api.mjs +17 -0
- package/src/tui/engine/tui-steering-persist.mjs +24 -1
- package/src/tui/engine/turn.mjs +8 -9
- package/src/tui/engine.mjs +18 -41
- package/src/workflows/default/WORKFLOW.md +7 -18
- package/src/workflows/solo/WORKFLOW.md +0 -6
- package/src/workflows/solo-bench/WORKFLOW.md +17 -0
|
@@ -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();
|
|
@@ -5,11 +5,8 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
5
5
|
import { clean } from './session-text.mjs';
|
|
6
6
|
import { readJsonSafe } from './fs-utils.mjs';
|
|
7
7
|
|
|
8
|
-
const OUTPUT_STYLE_ORDER = ['
|
|
8
|
+
const OUTPUT_STYLE_ORDER = ['detailed', 'simple', 'minimal', 'extreme-minimal'];
|
|
9
9
|
const OUTPUT_STYLE_ALIASES = new Map([
|
|
10
|
-
['compact', 'simple'],
|
|
11
|
-
['normal', 'simple'],
|
|
12
|
-
['detail', 'detailed'],
|
|
13
10
|
['extreme', 'extreme-minimal'],
|
|
14
11
|
['extremesimple', 'extreme-minimal'],
|
|
15
12
|
['extreme-simple', 'extreme-minimal'],
|
|
@@ -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([
|
|
@@ -16,12 +16,18 @@ export const FIXED_AGENT_SLOTS = Object.freeze([
|
|
|
16
16
|
// (user: 창이 크지 않으니 설명은 짧게).
|
|
17
17
|
{ id: 'explore', label: 'Explore', description: 'Repository exploration', workflowSlot: 'explorer' },
|
|
18
18
|
{ id: 'maintainer', label: 'Maintainer', description: 'Memory and upkeep', workflowSlot: 'memory' },
|
|
19
|
-
{ id: 'worker', label: 'Worker', description: '
|
|
20
|
-
{ id: 'heavy-worker', label: 'Heavy Worker', description: '
|
|
19
|
+
{ id: 'worker', label: 'Worker', description: 'Simple tasks' },
|
|
20
|
+
{ id: 'heavy-worker', label: 'Heavy Worker', description: 'Complex tasks' },
|
|
21
21
|
{ id: 'reviewer', label: 'Reviewer', description: 'Diff and risk review' },
|
|
22
22
|
{ id: 'debugger', label: 'Debugger', description: 'Root-cause debugging' },
|
|
23
23
|
]);
|
|
24
24
|
const AGENT_ROLE_IDS = new Set(FIXED_AGENT_SLOTS.map((agent) => agent.id));
|
|
25
|
+
// Slot-backed built-ins (explore/maintainer) run through their own dedicated
|
|
26
|
+
// channels — the explore tool and the memory cycle — so they are never
|
|
27
|
+
// Lead-delegation targets and stay out of the Available Agents catalog.
|
|
28
|
+
const BUILTIN_SLOT_AGENT_IDS = new Set(
|
|
29
|
+
FIXED_AGENT_SLOTS.filter((agent) => agent.workflowSlot).map((agent) => agent.id),
|
|
30
|
+
);
|
|
25
31
|
export const DEFAULT_WORKFLOW_ID = 'default';
|
|
26
32
|
|
|
27
33
|
const SEARCH_CAPABLE_PROVIDERS = new Set([
|
|
@@ -202,11 +208,13 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
202
208
|
if (pack && !pack.hidden) byId.set(pack.id, pack);
|
|
203
209
|
}
|
|
204
210
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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));
|
|
210
218
|
}
|
|
211
219
|
|
|
212
220
|
function activeWorkflowId(config) {
|
|
@@ -282,16 +290,20 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
282
290
|
lines.push(pack.body);
|
|
283
291
|
// A hand-edited pack may name a hidden role in its `agents:` frontmatter;
|
|
284
292
|
// internal roles are never delegatable, so they never enter the catalog.
|
|
293
|
+
// Slot-backed built-ins are equally non-delegatable (they ride the explore
|
|
294
|
+
// tool / memory cycle), so they are filtered even when a pack names them.
|
|
285
295
|
const agentIds = (pack.agentsConfigured ? pack.agents : FIXED_AGENT_SLOTS.map((agent) => agent.id))
|
|
286
|
-
.filter((id) => !isHiddenAgent(id));
|
|
296
|
+
.filter((id) => !isHiddenAgent(id) && !BUILTIN_SLOT_AGENT_IDS.has(id));
|
|
287
297
|
const agentBlocks = agentIds.map((id) => loadAgentDefinition(dir, id)).filter(Boolean);
|
|
288
298
|
if (agentBlocks.length) {
|
|
289
299
|
lines.push('# Available Agents');
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
300
|
+
// Name + description only: the AGENT.md body is the worker's own system
|
|
301
|
+
// prompt and rides in the worker session at spawn time — repeating it in
|
|
302
|
+
// the Lead prompt only bloats context. Lead picks agents by description
|
|
303
|
+
// (Claude Code whenToUse pattern); the workflow body carries the rules.
|
|
304
|
+
lines.push(agentBlocks
|
|
305
|
+
.map((agent) => `- ${agent.name} (${agent.id})${agent.description ? `: ${agent.description}` : ''}`)
|
|
306
|
+
.join('\n'));
|
|
295
307
|
}
|
|
296
308
|
return lines.join('\n\n');
|
|
297
309
|
}
|
|
@@ -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: {
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -25135,7 +25135,7 @@ function parseAgentResultEnvelope(text, fallback = {}) {
|
|
|
25135
25135
|
fast: fallback.fast ?? attrs.fast
|
|
25136
25136
|
},
|
|
25137
25137
|
result: body || agentJobStatusText({ status: fallback.status || attrs.status || "completed", taskId: fallback.taskId || attrs.task_id || attrs.taskid || "" }),
|
|
25138
|
-
isError: /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(fallback.status || attrs.status || "")
|
|
25138
|
+
isError: /^(failed|error|timeout|cancelled|canceled|killed|denied)$/i.test(fallback.status || attrs.status || "")
|
|
25139
25139
|
};
|
|
25140
25140
|
}
|
|
25141
25141
|
function parseBackgroundTaskEnvelope(text) {
|
|
@@ -25193,7 +25193,7 @@ function parseBackgroundTaskEnvelope(text) {
|
|
|
25193
25193
|
},
|
|
25194
25194
|
result: resultBody || (!errorText ? [status ? `status: ${status}` : "", taskId ? `task_id: ${taskId}` : ""].filter(Boolean).join(" \xB7 ") : ""),
|
|
25195
25195
|
rawResult: value,
|
|
25196
|
-
isError: /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(status) || /^error:/i.test(body) || Boolean(errorText)
|
|
25196
|
+
isError: /^(failed|error|timeout|cancelled|canceled|killed|denied)$/i.test(status) || /^error:/i.test(body) || Boolean(errorText)
|
|
25197
25197
|
};
|
|
25198
25198
|
}
|
|
25199
25199
|
function isStatusOnlyAgentCompletionNotification(text) {
|
|
@@ -25255,7 +25255,7 @@ function parseSyntheticAgentMessage(text) {
|
|
|
25255
25255
|
label,
|
|
25256
25256
|
args: agentArgsWithResultMetadata({ type: agentJob.type || "notification", description: "agent notification" }, agentJob),
|
|
25257
25257
|
result: result || agentJobStatusText(agentJob) || "agent notification",
|
|
25258
|
-
isError: /^(failed|error|timeout|cancelled|killed)$/i.test(label)
|
|
25258
|
+
isError: /^(failed|error|timeout|cancelled|canceled|killed|denied)$/i.test(label)
|
|
25259
25259
|
};
|
|
25260
25260
|
}
|
|
25261
25261
|
if (/<task-notification\b/i.test(value)) {
|
|
@@ -25273,6 +25273,54 @@ function parseSyntheticAgentMessage(text) {
|
|
|
25273
25273
|
}
|
|
25274
25274
|
return null;
|
|
25275
25275
|
}
|
|
25276
|
+
function buildExecutionResponseToolItem(text, {
|
|
25277
|
+
id,
|
|
25278
|
+
responseKey = "",
|
|
25279
|
+
now = Date.now()
|
|
25280
|
+
} = {}) {
|
|
25281
|
+
const synthetic = parseSyntheticAgentMessage(text);
|
|
25282
|
+
if (!synthetic) return null;
|
|
25283
|
+
const label = synthetic.label || "notification";
|
|
25284
|
+
const isAgent = synthetic.name === "agent";
|
|
25285
|
+
const args = {
|
|
25286
|
+
...synthetic.args && typeof synthetic.args === "object" ? synthetic.args : {
|
|
25287
|
+
type: label,
|
|
25288
|
+
task_id: synthetic.taskId || void 0,
|
|
25289
|
+
description: synthetic.summary || "execution notification"
|
|
25290
|
+
},
|
|
25291
|
+
...isAgent ? { type: "result" } : {}
|
|
25292
|
+
};
|
|
25293
|
+
const rawResult = synthetic.rawResult ?? text;
|
|
25294
|
+
const responseHasBody = /\n\s*\n[\s\S]*\S/.test(String(text || ""));
|
|
25295
|
+
const key = String(responseKey || args.task_id || "").trim();
|
|
25296
|
+
return {
|
|
25297
|
+
kind: "tool",
|
|
25298
|
+
id,
|
|
25299
|
+
name: synthetic.name || "task",
|
|
25300
|
+
args,
|
|
25301
|
+
result: synthetic.result,
|
|
25302
|
+
rawResult,
|
|
25303
|
+
isError: synthetic.isError ?? /^(failed|error|timeout|cancelled|canceled|killed|denied)$/i.test(label),
|
|
25304
|
+
expanded: false,
|
|
25305
|
+
count: 1,
|
|
25306
|
+
completedCount: 1,
|
|
25307
|
+
startedAt: now,
|
|
25308
|
+
completedAt: now,
|
|
25309
|
+
...isAgent ? {
|
|
25310
|
+
agentDirection: "inbound",
|
|
25311
|
+
agentResponseKey: key,
|
|
25312
|
+
agentResponseHasBody: responseHasBody,
|
|
25313
|
+
agentResponseAggregate: false,
|
|
25314
|
+
agentResponseEntries: [{
|
|
25315
|
+
key,
|
|
25316
|
+
raw: String(rawResult ?? "").trim(),
|
|
25317
|
+
result: synthetic.result,
|
|
25318
|
+
hasBody: responseHasBody,
|
|
25319
|
+
isError: synthetic.isError === true
|
|
25320
|
+
}]
|
|
25321
|
+
} : {}
|
|
25322
|
+
};
|
|
25323
|
+
}
|
|
25276
25324
|
function parseAgentJob(text) {
|
|
25277
25325
|
const value = String(text || "");
|
|
25278
25326
|
const idMatch = /^agent task:\s*([^\s]+)/m.exec(value) || /^task_id:\s*([^\s]+)/m.exec(value);
|
|
@@ -26573,6 +26621,7 @@ function drainTuiSteeringPersist(leadSessionId) {
|
|
|
26573
26621
|
return _serialize(async () => {
|
|
26574
26622
|
let drained = [];
|
|
26575
26623
|
let droppedStale = 0;
|
|
26624
|
+
let prunedOrphans = 0;
|
|
26576
26625
|
try {
|
|
26577
26626
|
await updateJsonAtomic(pendingMessagesPath(), (raw) => {
|
|
26578
26627
|
const next = normalizePendingStore(raw);
|
|
@@ -26586,7 +26635,21 @@ function drainTuiSteeringPersist(leadSessionId) {
|
|
|
26586
26635
|
return !stale;
|
|
26587
26636
|
});
|
|
26588
26637
|
drained = fresh.map(drainedRowToRestore).filter(Boolean);
|
|
26589
|
-
|
|
26638
|
+
for (const otherKey of Object.keys(next.sessions)) {
|
|
26639
|
+
if (otherKey === key) continue;
|
|
26640
|
+
const rows = Array.isArray(next.sessions[otherKey]) ? next.sessions[otherKey] : [];
|
|
26641
|
+
const otherTouched = Number(next.sessionTouchedAt?.[otherKey]) || 0;
|
|
26642
|
+
const allStale = rows.every((row) => {
|
|
26643
|
+
const at = Number(row?.at) || otherTouched;
|
|
26644
|
+
return at > 0 && now - at > STALE_STEERING_RESTORE_TTL_MS;
|
|
26645
|
+
});
|
|
26646
|
+
if (rows.length === 0 || allStale) {
|
|
26647
|
+
delete next.sessions[otherKey];
|
|
26648
|
+
if (next.sessionTouchedAt) delete next.sessionTouchedAt[otherKey];
|
|
26649
|
+
prunedOrphans += rows.length;
|
|
26650
|
+
}
|
|
26651
|
+
}
|
|
26652
|
+
if (drained.length === 0 && droppedStale === 0 && prunedOrphans === 0) return void 0;
|
|
26590
26653
|
delete next.sessions[key];
|
|
26591
26654
|
if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
|
|
26592
26655
|
next.updatedAt = Date.now();
|
|
@@ -26602,6 +26665,13 @@ function drainTuiSteeringPersist(leadSessionId) {
|
|
|
26602
26665
|
if (droppedStale > 0) {
|
|
26603
26666
|
try {
|
|
26604
26667
|
process.stderr.write(`[tui] dropped ${droppedStale} stale steering row(s) sessionId=${leadSessionId}
|
|
26668
|
+
`);
|
|
26669
|
+
} catch {
|
|
26670
|
+
}
|
|
26671
|
+
}
|
|
26672
|
+
if (prunedOrphans > 0) {
|
|
26673
|
+
try {
|
|
26674
|
+
process.stderr.write(`[tui] pruned ${prunedOrphans} orphaned steering row(s) from stale sessions
|
|
26605
26675
|
`);
|
|
26606
26676
|
} catch {
|
|
26607
26677
|
}
|
|
@@ -27403,6 +27473,7 @@ function createTurnWatchdog({
|
|
|
27403
27473
|
}
|
|
27404
27474
|
|
|
27405
27475
|
// src/tui/engine/turn.mjs
|
|
27476
|
+
var STREAM_BATCH_INTERVAL_MS = TUI_FRAME_MS;
|
|
27406
27477
|
function createRunTurn(bag) {
|
|
27407
27478
|
const {
|
|
27408
27479
|
runtime,
|
|
@@ -27893,7 +27964,6 @@ function createRunTurn(bag) {
|
|
|
27893
27964
|
thinkingSegmentStartedAt = 0;
|
|
27894
27965
|
return now;
|
|
27895
27966
|
};
|
|
27896
|
-
const STREAM_BATCH_INTERVAL_MS = 16;
|
|
27897
27967
|
let _batchTimer = null;
|
|
27898
27968
|
let _pendingTextFlush = false;
|
|
27899
27969
|
let _pendingThinkFlush = false;
|
|
@@ -30212,6 +30282,15 @@ function createEngineApiA(bag) {
|
|
|
30212
30282
|
restoreState.requeueEntries = [];
|
|
30213
30283
|
restoreState.discardExecutionPendingResumeKeys = [];
|
|
30214
30284
|
}
|
|
30285
|
+
const pendingAfterAbortKick = setTimeout(() => {
|
|
30286
|
+
try {
|
|
30287
|
+
if (flags.disposed) return;
|
|
30288
|
+
if (getState().busy) return;
|
|
30289
|
+
if (pending.length > 0 && typeof drain === "function") void drain();
|
|
30290
|
+
} catch {
|
|
30291
|
+
}
|
|
30292
|
+
}, 150);
|
|
30293
|
+
pendingAfterAbortKick.unref?.();
|
|
30215
30294
|
const abortEpoch = flags.leadTurnEpoch;
|
|
30216
30295
|
const recoveryMs = Number(flags.manualAbortRecoveryMs) > 0 ? Number(flags.manualAbortRecoveryMs) : MANUAL_ABORT_RECOVERY_MS;
|
|
30217
30296
|
const recoveryTimer = setTimeout(() => {
|
|
@@ -31787,56 +31866,31 @@ async function createEngineSession({
|
|
|
31787
31866
|
});
|
|
31788
31867
|
};
|
|
31789
31868
|
const pushAsyncAgentResponse = (text, id = nextId(), origin = "injected", metadata = {}) => {
|
|
31790
|
-
const
|
|
31791
|
-
|
|
31792
|
-
|
|
31793
|
-
|
|
31794
|
-
|
|
31795
|
-
|
|
31796
|
-
|
|
31797
|
-
|
|
31798
|
-
}
|
|
31799
|
-
const responseKey = String(metadata.responseKey || metadata.executionId || args.task_id || "").trim();
|
|
31869
|
+
const responseItem = buildExecutionResponseToolItem(text, {
|
|
31870
|
+
id,
|
|
31871
|
+
responseKey: metadata.responseKey || metadata.executionId
|
|
31872
|
+
});
|
|
31873
|
+
if (!responseItem) return pushUserOrSyntheticItem(text, id, origin);
|
|
31874
|
+
if (responseItem.name !== "agent") {
|
|
31875
|
+
pushItem(responseItem);
|
|
31876
|
+
return true;
|
|
31877
|
+
}
|
|
31800
31878
|
const previous = state.items.at(-1);
|
|
31801
31879
|
if (previous?.kind === "tool" && previous.agentDirection === "inbound") {
|
|
31802
31880
|
const patch = appendAgentResponseTail(previous, {
|
|
31803
|
-
key:
|
|
31804
|
-
args,
|
|
31805
|
-
result:
|
|
31806
|
-
rawResult,
|
|
31807
|
-
hasBody:
|
|
31808
|
-
isError:
|
|
31881
|
+
key: responseItem.agentResponseKey,
|
|
31882
|
+
args: responseItem.args,
|
|
31883
|
+
result: responseItem.result,
|
|
31884
|
+
rawResult: responseItem.rawResult,
|
|
31885
|
+
hasBody: responseItem.agentResponseHasBody,
|
|
31886
|
+
isError: responseItem.isError
|
|
31809
31887
|
});
|
|
31810
31888
|
if (patch) {
|
|
31811
31889
|
patchItem(previous.id, patch);
|
|
31812
31890
|
return true;
|
|
31813
31891
|
}
|
|
31814
31892
|
}
|
|
31815
|
-
pushItem(
|
|
31816
|
-
kind: "tool",
|
|
31817
|
-
id,
|
|
31818
|
-
name: "agent",
|
|
31819
|
-
args,
|
|
31820
|
-
result: synthetic.result,
|
|
31821
|
-
rawResult,
|
|
31822
|
-
isError: synthetic.isError === true,
|
|
31823
|
-
expanded: false,
|
|
31824
|
-
count: 1,
|
|
31825
|
-
completedCount: 1,
|
|
31826
|
-
startedAt: Date.now(),
|
|
31827
|
-
completedAt: Date.now(),
|
|
31828
|
-
agentDirection: "inbound",
|
|
31829
|
-
agentResponseKey: responseKey,
|
|
31830
|
-
agentResponseHasBody: responseHasBody,
|
|
31831
|
-
agentResponseAggregate: false,
|
|
31832
|
-
agentResponseEntries: [{
|
|
31833
|
-
key: responseKey,
|
|
31834
|
-
raw: String(rawResult ?? "").trim(),
|
|
31835
|
-
result: synthetic.result,
|
|
31836
|
-
hasBody: responseHasBody,
|
|
31837
|
-
isError: synthetic.isError === true
|
|
31838
|
-
}]
|
|
31839
|
-
});
|
|
31893
|
+
pushItem(responseItem);
|
|
31840
31894
|
return true;
|
|
31841
31895
|
};
|
|
31842
31896
|
const pushToast = (text, tone = "info", ttlMs = 3e3) => {
|
|
@@ -84,7 +84,7 @@ export function parseAgentResultEnvelope(text, fallback = {}) {
|
|
|
84
84
|
fast: fallback.fast ?? attrs.fast,
|
|
85
85
|
},
|
|
86
86
|
result: body || agentJobStatusText({ status: fallback.status || attrs.status || 'completed', taskId: fallback.taskId || attrs.task_id || attrs.taskid || '' }),
|
|
87
|
-
isError: /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(fallback.status || attrs.status || ''),
|
|
87
|
+
isError: /^(failed|error|timeout|cancelled|canceled|killed|denied)$/i.test(fallback.status || attrs.status || ''),
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -143,7 +143,7 @@ export function parseBackgroundTaskEnvelope(text) {
|
|
|
143
143
|
},
|
|
144
144
|
result: resultBody || (!errorText ? [status ? `status: ${status}` : '', taskId ? `task_id: ${taskId}` : ''].filter(Boolean).join(' · ') : ''),
|
|
145
145
|
rawResult: value,
|
|
146
|
-
isError: /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(status) || /^error:/i.test(body) || Boolean(errorText),
|
|
146
|
+
isError: /^(failed|error|timeout|cancelled|canceled|killed|denied)$/i.test(status) || /^error:/i.test(body) || Boolean(errorText),
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -223,7 +223,7 @@ export function parseSyntheticAgentMessage(text) {
|
|
|
223
223
|
label,
|
|
224
224
|
args: agentArgsWithResultMetadata({ type: agentJob.type || 'notification', description: 'agent notification' }, agentJob),
|
|
225
225
|
result: result || agentJobStatusText(agentJob) || 'agent notification',
|
|
226
|
-
isError: /^(failed|error|timeout|cancelled|killed)$/i.test(label),
|
|
226
|
+
isError: /^(failed|error|timeout|cancelled|canceled|killed|denied)$/i.test(label),
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
if (/<task-notification\b/i.test(value)) {
|
|
@@ -242,6 +242,55 @@ export function parseSyntheticAgentMessage(text) {
|
|
|
242
242
|
return null;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
export function buildExecutionResponseToolItem(text, {
|
|
246
|
+
id,
|
|
247
|
+
responseKey = '',
|
|
248
|
+
now = Date.now(),
|
|
249
|
+
} = {}) {
|
|
250
|
+
const synthetic = parseSyntheticAgentMessage(text);
|
|
251
|
+
if (!synthetic) return null;
|
|
252
|
+
const label = synthetic.label || 'notification';
|
|
253
|
+
const isAgent = synthetic.name === 'agent';
|
|
254
|
+
const args = {
|
|
255
|
+
...(synthetic.args && typeof synthetic.args === 'object' ? synthetic.args : {
|
|
256
|
+
type: label,
|
|
257
|
+
task_id: synthetic.taskId || undefined,
|
|
258
|
+
description: synthetic.summary || 'execution notification',
|
|
259
|
+
}),
|
|
260
|
+
...(isAgent ? { type: 'result' } : {}),
|
|
261
|
+
};
|
|
262
|
+
const rawResult = synthetic.rawResult ?? text;
|
|
263
|
+
const responseHasBody = /\n\s*\n[\s\S]*\S/.test(String(text || ''));
|
|
264
|
+
const key = String(responseKey || args.task_id || '').trim();
|
|
265
|
+
return {
|
|
266
|
+
kind: 'tool',
|
|
267
|
+
id,
|
|
268
|
+
name: synthetic.name || 'task',
|
|
269
|
+
args,
|
|
270
|
+
result: synthetic.result,
|
|
271
|
+
rawResult,
|
|
272
|
+
isError: synthetic.isError ?? /^(failed|error|timeout|cancelled|canceled|killed|denied)$/i.test(label),
|
|
273
|
+
expanded: false,
|
|
274
|
+
count: 1,
|
|
275
|
+
completedCount: 1,
|
|
276
|
+
startedAt: now,
|
|
277
|
+
completedAt: now,
|
|
278
|
+
...(isAgent ? {
|
|
279
|
+
agentDirection: 'inbound',
|
|
280
|
+
agentResponseKey: key,
|
|
281
|
+
agentResponseHasBody: responseHasBody,
|
|
282
|
+
agentResponseAggregate: false,
|
|
283
|
+
agentResponseEntries: [{
|
|
284
|
+
key,
|
|
285
|
+
raw: String(rawResult ?? '').trim(),
|
|
286
|
+
result: synthetic.result,
|
|
287
|
+
hasBody: responseHasBody,
|
|
288
|
+
isError: synthetic.isError === true,
|
|
289
|
+
}],
|
|
290
|
+
} : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
245
294
|
export function parseAgentJob(text) {
|
|
246
295
|
const value = String(text || '');
|
|
247
296
|
const idMatch = /^agent task:\s*([^\s]+)/m.exec(value) || /^task_id:\s*([^\s]+)/m.exec(value);
|
|
@@ -616,6 +616,23 @@ export function createEngineApiA(bag) {
|
|
|
616
616
|
// ── Bounded manual-abort recovery ───────────────────────────────────
|
|
617
617
|
// runtime.abort() above normally rejects the in-flight runtime.ask() so
|
|
618
618
|
// the turn's own finally clears busy within a tick. If that unwind is
|
|
619
|
+
// starved the recovery timer below hard-releases busy. Separately from
|
|
620
|
+
// recovery: queued work must never strand behind a cancelled turn
|
|
621
|
+
// (Codex parity — abort preserves pending input for the next turn; CC
|
|
622
|
+
// parity — the command queue survives cancel and fires when idle). The
|
|
623
|
+
// drain loop that owns a normal turn continues on its own; this bounded
|
|
624
|
+
// kick covers unwinds where no drain owner re-checks pending after busy
|
|
625
|
+
// clears. drain() self-guards (busy/draining/commandBusy), so the
|
|
626
|
+
// drain-owned path is unchanged and a duplicate kick is a no-op.
|
|
627
|
+
const pendingAfterAbortKick = setTimeout(() => {
|
|
628
|
+
try {
|
|
629
|
+
if (flags.disposed) return;
|
|
630
|
+
if (getState().busy) return;
|
|
631
|
+
if (pending.length > 0 && typeof drain === 'function') void drain();
|
|
632
|
+
} catch { /* best-effort */ }
|
|
633
|
+
}, 150);
|
|
634
|
+
pendingAfterAbortKick.unref?.();
|
|
635
|
+
// If that unwind is
|
|
619
636
|
// starved — e.g. a provider abort that never settles after a post-tool
|
|
620
637
|
// fetch stall — busy would stay true until the far-larger turn watchdog
|
|
621
638
|
// trips, wedging the TUI with dead input. Arm a short grace timer that
|
|
@@ -195,6 +195,7 @@ export function drainTuiSteeringPersist(leadSessionId) {
|
|
|
195
195
|
return _serialize(async () => {
|
|
196
196
|
let drained = [];
|
|
197
197
|
let droppedStale = 0;
|
|
198
|
+
let prunedOrphans = 0;
|
|
198
199
|
try {
|
|
199
200
|
await updateJsonAtomic(pendingMessagesPath(), (raw) => {
|
|
200
201
|
const next = normalizePendingStore(raw);
|
|
@@ -210,7 +211,26 @@ export function drainTuiSteeringPersist(leadSessionId) {
|
|
|
210
211
|
return !stale;
|
|
211
212
|
});
|
|
212
213
|
drained = fresh.map(drainedRowToRestore).filter(Boolean);
|
|
213
|
-
|
|
214
|
+
// Orphan cleanup: buckets of OTHER sessions whose every row already
|
|
215
|
+
// aged past the restore TTL can never be restored (restore is keyed
|
|
216
|
+
// by the live session id), so they only grow the file forever
|
|
217
|
+
// (observed live: queued rows from sessions closed days ago). Prune
|
|
218
|
+
// them under the same lock/write.
|
|
219
|
+
for (const otherKey of Object.keys(next.sessions)) {
|
|
220
|
+
if (otherKey === key) continue;
|
|
221
|
+
const rows = Array.isArray(next.sessions[otherKey]) ? next.sessions[otherKey] : [];
|
|
222
|
+
const otherTouched = Number(next.sessionTouchedAt?.[otherKey]) || 0;
|
|
223
|
+
const allStale = rows.every((row) => {
|
|
224
|
+
const at = Number(row?.at) || otherTouched;
|
|
225
|
+
return at > 0 && (now - at) > STALE_STEERING_RESTORE_TTL_MS;
|
|
226
|
+
});
|
|
227
|
+
if (rows.length === 0 || allStale) {
|
|
228
|
+
delete next.sessions[otherKey];
|
|
229
|
+
if (next.sessionTouchedAt) delete next.sessionTouchedAt[otherKey];
|
|
230
|
+
prunedOrphans += rows.length;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (drained.length === 0 && droppedStale === 0 && prunedOrphans === 0) return undefined;
|
|
214
234
|
delete next.sessions[key];
|
|
215
235
|
if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
|
|
216
236
|
next.updatedAt = Date.now();
|
|
@@ -222,6 +242,9 @@ export function drainTuiSteeringPersist(leadSessionId) {
|
|
|
222
242
|
if (droppedStale > 0) {
|
|
223
243
|
try { process.stderr.write(`[tui] dropped ${droppedStale} stale steering row(s) sessionId=${leadSessionId}\n`); } catch {}
|
|
224
244
|
}
|
|
245
|
+
if (prunedOrphans > 0) {
|
|
246
|
+
try { process.stderr.write(`[tui] pruned ${prunedOrphans} orphaned steering row(s) from stale sessions\n`); } catch {}
|
|
247
|
+
}
|
|
225
248
|
return drained;
|
|
226
249
|
});
|
|
227
250
|
}
|