mixdog 0.9.41 → 0.9.44
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 -2
- package/scripts/agent-tag-reuse-smoke.mjs +124 -10
- package/scripts/ansi-color-capability-test.mjs +90 -0
- package/scripts/anthropic-oauth-refresh-race-test.mjs +59 -0
- package/scripts/arg-guard-test.mjs +16 -0
- package/scripts/compact-pressure-test.mjs +256 -1
- package/scripts/generate-runtime-manifest.mjs +39 -22
- package/scripts/grok-oauth-refresh-race-test.mjs +198 -0
- package/scripts/internal-comms-bench.mjs +0 -1
- package/scripts/internal-comms-smoke.mjs +38 -39
- package/scripts/internal-tools-normalization-test.mjs +52 -0
- package/scripts/maintenance-default-routes-test.mjs +164 -0
- package/scripts/max-output-recovery-test.mjs +49 -0
- package/scripts/mcp-client-normalization-test.mjs +45 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +123 -0
- package/scripts/provider-toolcall-test.mjs +23 -0
- package/scripts/result-classification-test.mjs +75 -0
- package/scripts/routing-corpus.mjs +1 -1
- package/scripts/shell-jobs-windows-hide-test.mjs +164 -0
- package/scripts/smoke.mjs +1 -1
- package/scripts/submit-commandbusy-race-test.mjs +99 -7
- package/scripts/tui-transcript-jitter-harness-entry.jsx +302 -0
- package/scripts/tui-transcript-jitter-harness.mjs +58 -0
- package/scripts/tui-transcript-perf-test.mjs +56 -1
- package/src/agents/reviewer/AGENT.md +5 -3
- package/src/rules/lead/lead-brief.md +5 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +40 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -14
- package/src/runtime/agent/orchestrator/internal-tools.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +17 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +49 -6
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +46 -21
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +59 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +140 -81
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -5
- package/src/runtime/memory/data/runtime-manifest.json +11 -11
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -2
- package/src/runtime/memory/tool-defs.mjs +4 -3
- package/src/runtime/shared/tool-surface.mjs +1 -2
- package/src/session-runtime/context-status.mjs +8 -2
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +267 -72
- package/src/standalone/explore-tool.mjs +17 -16
- package/src/tui/app/provider-setup-picker.mjs +1 -0
- package/src/tui/app/use-transcript-window.mjs +5 -1
- package/src/tui/components/StatusLine.jsx +11 -32
- package/src/tui/dist/index.mjs +257 -106
- package/src/tui/engine/session-api.mjs +3 -0
- package/src/tui/engine/session-flow.mjs +19 -8
- package/src/tui/engine/turn.mjs +24 -2
- package/src/tui/engine.mjs +0 -1
- package/src/tui/index.jsx +3 -2
- package/src/tui/markdown/streaming-markdown.mjs +35 -9
- package/src/tui/statusline-ansi-bridge.mjs +17 -7
- package/src/ui/ansi.mjs +85 -5
- package/src/ui/statusline-agents.mjs +0 -8
- package/src/ui/statusline-format.mjs +7 -7
- package/src/ui/statusline.mjs +2 -4
- package/src/workflows/default/WORKFLOW.md +29 -20
- package/src/workflows/solo-review/WORKFLOW.md +47 -0
- package/src/workflows/bench/WORKFLOW.md +0 -60
|
@@ -985,10 +985,12 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
985
985
|
// the execution policy on Windows PowerShell; pwsh on macOS/Linux
|
|
986
986
|
// accepts the parameter as a no-op, so it is safe unconditionally.
|
|
987
987
|
" $argList = @('-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $innerPath)",
|
|
988
|
-
//
|
|
989
|
-
//
|
|
988
|
+
// Do not set WindowStyle: Hidden creates a separate transient conhost.
|
|
989
|
+
// On Windows, explicitly reuse the wrapper's CREATE_NO_WINDOW console.
|
|
990
|
+
// NoNewWindow is unavailable on non-Windows pwsh, so only add it for
|
|
991
|
+
// Windows (where $IsWindows is absent/null in Windows PowerShell 5.1).
|
|
990
992
|
' $spArgs = @{ FilePath = $exe; ArgumentList = $argList; RedirectStandardOutput = $stdoutPath; RedirectStandardError = $stderrPath; PassThru = $true }',
|
|
991
|
-
' if ($IsWindows -or $null -eq $IsWindows) { $spArgs[\'
|
|
993
|
+
' if ($IsWindows -or $null -eq $IsWindows) { $spArgs[\'NoNewWindow\'] = $true }',
|
|
992
994
|
' $p = Start-Process @spArgs',
|
|
993
995
|
' if ($timeoutMs -gt 0 -and -not $p.WaitForExit($timeoutMs)) {',
|
|
994
996
|
// Kill the whole process TREE, not just the direct child: Start-Process
|
|
@@ -1039,8 +1041,8 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
1039
1041
|
// No `-WindowStyle Hidden` CLI switch: windowsHide:true on the spawn below
|
|
1040
1042
|
// already gives CREATE_NO_WINDOW, and the visible command-line token trips
|
|
1041
1043
|
// Defender's hidden-PowerShell dropper signature (PowhidSubExec). The
|
|
1042
|
-
// in-wrapper Start-Process
|
|
1043
|
-
//
|
|
1044
|
+
// in-wrapper Start-Process uses Windows-only NoNewWindow to reuse this
|
|
1045
|
+
// hidden console rather than creating a transient conhost window.
|
|
1044
1046
|
// `-ExecutionPolicy` only applies to Windows PowerShell; build per-platform.
|
|
1045
1047
|
const isWin = process.platform === 'win32';
|
|
1046
1048
|
const wrapperArgs = ['-NoLogo', '-NoProfile', '-NonInteractive'];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"generated_at": "2026-
|
|
3
|
+
"generated_at": "2026-07-13T06:08:53.718Z",
|
|
4
4
|
"release_tag": "runtime-v0.4.0",
|
|
5
5
|
"pg": {
|
|
6
6
|
"major": 16,
|
|
@@ -12,28 +12,28 @@
|
|
|
12
12
|
"assets": {
|
|
13
13
|
"darwin-arm64": {
|
|
14
14
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-darwin-arm64-pg16.4-pgvector0.8.2.tar.gz",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "c11f4b3046f0eeecf88790d2fff88cee1834231f96c11ec315a7a97180ea1413",
|
|
16
|
+
"size": 23397436
|
|
17
17
|
},
|
|
18
18
|
"darwin-x64": {
|
|
19
19
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-darwin-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
20
|
-
"sha256": "
|
|
21
|
-
"size":
|
|
20
|
+
"sha256": "ef93b837b1e3ea0b30591f2d31de10f9a628db88ad00183d271d539fe3cb0adb",
|
|
21
|
+
"size": 23687427
|
|
22
22
|
},
|
|
23
23
|
"linux-arm64": {
|
|
24
24
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-linux-arm64-pg16.4-pgvector0.8.2.tar.gz",
|
|
25
|
-
"sha256": "
|
|
26
|
-
"size":
|
|
25
|
+
"sha256": "288d566fd38dd80ce2dcfa68083a76b6a9400fbd8020453cf72197d3cc161315",
|
|
26
|
+
"size": 22884871
|
|
27
27
|
},
|
|
28
28
|
"linux-x64": {
|
|
29
29
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-linux-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
30
|
-
"sha256": "
|
|
31
|
-
"size":
|
|
30
|
+
"sha256": "31b40d32d51afb85963e33557c3140e378b21c7cc54cec794049276a2160afd3",
|
|
31
|
+
"size": 23282291
|
|
32
32
|
},
|
|
33
33
|
"win32-x64": {
|
|
34
34
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-win32-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
35
|
-
"sha256": "
|
|
36
|
-
"size":
|
|
35
|
+
"sha256": "5d3d9d6e3ae4a93bb0bde615365bdda3ab5ff7206c0d1a066ed51b316932a8f8",
|
|
36
|
+
"size": 44037023
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -37,7 +37,6 @@ const BUNDLED_MANIFEST_PATH = fileURLToPath(new URL('../data/runtime-manifest.js
|
|
|
37
37
|
|
|
38
38
|
// GitHub raw URL fallback — used only when no cached or bundled manifest exists.
|
|
39
39
|
const MANIFEST_URL = 'https://raw.githubusercontent.com/tribgames/mixdog/main/src/runtime/memory/data/runtime-manifest.json'
|
|
40
|
-
const LEGACY_RUNTIME_RELEASE_REPOSITORY = 'trib-plugin/mixdog'
|
|
41
40
|
|
|
42
41
|
// ---------------------------------------------------------------------------
|
|
43
42
|
// Platform key
|
|
@@ -183,7 +182,7 @@ function runtimeAssetUrlCandidates(url) {
|
|
|
183
182
|
.split(/[\s,;]+/u)
|
|
184
183
|
.map((repo) => repo.trim())
|
|
185
184
|
.filter(Boolean)
|
|
186
|
-
for (const repo of
|
|
185
|
+
for (const repo of overrides) {
|
|
187
186
|
if (repo && repo !== releaseRepo) add(value.replace(`/github.com/${releaseRepo}/`, `/github.com/${repo}/`))
|
|
188
187
|
}
|
|
189
188
|
return out
|
|
@@ -8,21 +8,22 @@ export const TOOL_DEFS = [
|
|
|
8
8
|
name: 'memory',
|
|
9
9
|
title: 'Memory Cycle',
|
|
10
10
|
annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
11
|
-
description: 'Core-memory mutation and status. Use recall for retrieval. Persist with action:"core" op:"add" when: user corrects/confirms your approach, states a durable preference, or says "remember". Only what code/git/docs cannot derive; include the why. Never transient task state. No prior recall dedup needed — the cycle dedups.',
|
|
11
|
+
description: 'Core-memory mutation and status. Use recall for retrieval. Persist with action:"core" op:"add" when: user corrects/confirms your approach, states a durable preference, or says "remember". Core add/edit requires project_id + category + element + summary. Only what code/git/docs cannot derive; include the why. Never transient task state. No prior recall dedup needed — the cycle dedups.',
|
|
12
12
|
inputSchema: {
|
|
13
13
|
type: 'object',
|
|
14
14
|
properties: {
|
|
15
15
|
action: { type: 'string', enum: ['core','status'], description: 'Operation.' },
|
|
16
16
|
op: { type: 'string', enum: ['add','edit','delete','list','candidates','promote','dismiss'], description: 'Mutation op. candidates/promote/dismiss drive core-memory proposal approval.' },
|
|
17
17
|
id: { type: 'number', description: 'Exact memory id.' },
|
|
18
|
-
element: { type: 'string', description: 'Memory key/title. Max 40 chars.' },
|
|
19
|
-
summary: { type: 'string', description: 'Memory content: 1 fact, 1-2 sentences, max 100 chars.' },
|
|
18
|
+
element: { type: 'string', maxLength: 40, description: 'Memory key/title. Max 40 chars.' },
|
|
19
|
+
summary: { type: 'string', maxLength: 100, description: 'Memory content: 1 fact, 1-2 sentences, max 100 chars.' },
|
|
20
20
|
category: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'], description: 'Category.' },
|
|
21
21
|
status: { type: 'string', enum: ['pending','active','archived'], description: 'Lifecycle status.' },
|
|
22
22
|
limit: { type: 'number', description: 'Max rows/items.' },
|
|
23
23
|
confirm: { type: 'string', description: 'Exact confirmation phrase for destructive actions.' },
|
|
24
24
|
project_id: { type: 'string', description: 'Core pool: common, slug, or *. Required for core add/edit; there is no default pool.' },
|
|
25
25
|
},
|
|
26
|
+
additionalProperties: false,
|
|
26
27
|
required: ['action'],
|
|
27
28
|
},
|
|
28
29
|
},
|
|
@@ -514,8 +514,7 @@ export function toolWorkUnit(name, args = {}, category = '') {
|
|
|
514
514
|
case 'memory': {
|
|
515
515
|
const action = String(a.action || '').toLowerCase();
|
|
516
516
|
const op = String(a.op || '').toLowerCase();
|
|
517
|
-
const isMutation = op === 'add' || op === 'edit' || op === 'delete' || op === 'promote' || op === 'dismiss'
|
|
518
|
-
|| (action === 'core' && !op);
|
|
517
|
+
const isMutation = op === 'add' || op === 'edit' || op === 'delete' || op === 'promote' || op === 'dismiss';
|
|
519
518
|
if (isMutation) return unitDescriptor('Memory', { count: queryCount(a, 'entries', 'items', 'memories', 'query', 'text', 'value') || 1, active: 'Writing', done: 'Wrote', noun: 'memory item' });
|
|
520
519
|
return unitDescriptor('Memory', { count: queryCount(a, 'entries', 'items', 'memories', 'query', 'text', 'value') || 1, active: 'Checking', done: 'Checked', noun: 'memory item' });
|
|
521
520
|
}
|
|
@@ -2,8 +2,10 @@ import {
|
|
|
2
2
|
estimateRequestReserveTokens,
|
|
3
3
|
estimateToolSchemaTokens,
|
|
4
4
|
estimateTranscriptContextUsage,
|
|
5
|
+
contextMessagesRevision,
|
|
5
6
|
resolveSessionCompactPolicy,
|
|
6
7
|
summarizeContextMessages,
|
|
8
|
+
toolSchemaSignature,
|
|
7
9
|
} from '../runtime/agent/orchestrator/session/context-utils.mjs';
|
|
8
10
|
import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
|
|
9
11
|
|
|
@@ -16,7 +18,7 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
|
|
|
16
18
|
let contextStatusCacheKey = null;
|
|
17
19
|
let contextStatusCacheValue = null;
|
|
18
20
|
|
|
19
|
-
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
21
|
+
function contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature }) {
|
|
20
22
|
const session = getSession();
|
|
21
23
|
const route = getRoute();
|
|
22
24
|
const compaction = session?.compaction || {};
|
|
@@ -30,11 +32,13 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
|
|
|
30
32
|
mode: getMode(),
|
|
31
33
|
messages,
|
|
32
34
|
messageCount: messages.length,
|
|
35
|
+
messagesRevision,
|
|
33
36
|
lastMessage,
|
|
34
37
|
lastMessageRole: lastMessage?.role || null,
|
|
35
38
|
lastMessageContent: lastMessage?.content || null,
|
|
36
39
|
tools,
|
|
37
40
|
toolCount: tools.length,
|
|
41
|
+
toolsSignature,
|
|
38
42
|
contextWindow: session?.contextWindow || null,
|
|
39
43
|
rawContextWindow: session?.rawContextWindow || null,
|
|
40
44
|
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
@@ -85,7 +89,9 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
|
|
|
85
89
|
const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
|
|
86
90
|
const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
|
|
87
91
|
const tools = Array.isArray(session?.tools) ? session.tools : [];
|
|
88
|
-
const
|
|
92
|
+
const messagesRevision = contextMessagesRevision(messages);
|
|
93
|
+
const toolsSignature = toolSchemaSignature(tools);
|
|
94
|
+
const cacheKey = contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature });
|
|
89
95
|
if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
|
|
90
96
|
return contextStatusCacheValue;
|
|
91
97
|
}
|
|
@@ -37,6 +37,8 @@ export function abnormalEmptyFinishError(result, agent) {
|
|
|
37
37
|
// Only tagged for PUBLIC agents (hidden agents legitimately emit empty
|
|
38
38
|
// terminal turns and are left untagged by the loop).
|
|
39
39
|
return `agent '${agent}' finished without a final answer (stopReason=${stopReason ?? 'none'}, ${iterations} iterations, ${toolCallsTotal} tool calls)`;
|
|
40
|
+
case 'refusal':
|
|
41
|
+
return `agent '${agent}' response was refused by the safety classifier (${iterations} iterations, ${toolCallsTotal} tool calls)`;
|
|
40
42
|
default:
|
|
41
43
|
return null;
|
|
42
44
|
}
|
|
@@ -90,6 +90,72 @@ const DEFAULT_SPAWN_PREP_TIMEOUT_MS = envTimeoutMs('MIXDOG_AGENT_SPAWN_PREP_TIME
|
|
|
90
90
|
// startDeferredSpawnJob) so the agent tool call itself still returns task_id
|
|
91
91
|
// immediately.
|
|
92
92
|
const SPAWN_STAGGER_MS = envTimeoutMs('MIXDOG_SPAWN_STAGGER_MS', 0);
|
|
93
|
+
const TAG_TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
94
|
+
const MAX_TAG_TOMBSTONES = 500;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the route for a public agent spawn. Explore and Maintainer inherit
|
|
98
|
+
* Main only after their explicit agent, workflow, and maintenance routes.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveAgentSpawnPreset(config, args = {}) {
|
|
101
|
+
if (args.provider && args.model) {
|
|
102
|
+
return {
|
|
103
|
+
presetName: args.preset || '__direct__',
|
|
104
|
+
preset: {
|
|
105
|
+
id: '__direct__',
|
|
106
|
+
name: '__DIRECT__',
|
|
107
|
+
type: 'agent',
|
|
108
|
+
provider: clean(args.provider),
|
|
109
|
+
model: clean(args.model),
|
|
110
|
+
effort: clean(args.effort) || undefined,
|
|
111
|
+
fast: args.fast === true,
|
|
112
|
+
tools: 'full',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const agentName = normalizeAgentName(args.agent);
|
|
118
|
+
const configuredDefault = clean(config?.defaultProvider);
|
|
119
|
+
const fallbackProvider = configuredDefault && isKnownProvider(configuredDefault)
|
|
120
|
+
? configuredDefault
|
|
121
|
+
: DEFAULT_PROVIDER;
|
|
122
|
+
const workflowSlot = agentName === 'explore' ? 'explorer'
|
|
123
|
+
: (agentName === 'maintainer' ? 'memory' : '');
|
|
124
|
+
const maintenanceSlot = agentName === 'explore' ? 'explore'
|
|
125
|
+
: (agentName === 'maintainer' ? 'memory' : '');
|
|
126
|
+
const agentRoute = !clean(args.preset)
|
|
127
|
+
? (normalizeAgentRoute(config?.agents?.[agentName], fallbackProvider)
|
|
128
|
+
|| (agentName === 'maintainer' ? normalizeAgentRoute(config?.agents?.maintenance, fallbackProvider) : null)
|
|
129
|
+
|| normalizeAgentRoute(config?.workflowRoutes?.[workflowSlot], fallbackProvider)
|
|
130
|
+
|| normalizeAgentRoute(config?.maintenance?.[maintenanceSlot], fallbackProvider))
|
|
131
|
+
: null;
|
|
132
|
+
if (agentRoute) {
|
|
133
|
+
return {
|
|
134
|
+
presetName: agentPresetName(agentName),
|
|
135
|
+
preset: {
|
|
136
|
+
id: `agent-${agentName}`,
|
|
137
|
+
name: agentPresetName(agentName),
|
|
138
|
+
type: 'agent',
|
|
139
|
+
provider: agentRoute.provider,
|
|
140
|
+
model: agentRoute.model,
|
|
141
|
+
effort: agentRoute.effort,
|
|
142
|
+
fast: agentRoute.fast === true,
|
|
143
|
+
tools: 'full',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const mainPreset = !clean(args.preset) && (agentName === 'explore' || agentName === 'maintainer')
|
|
149
|
+
? findPreset(config, config?.default)
|
|
150
|
+
: null;
|
|
151
|
+
if (mainPreset) return { presetName: mainPreset.id || mainPreset.name, preset: mainPreset };
|
|
152
|
+
|
|
153
|
+
const presetName = clean(args.preset) || DEFAULT_AGENT_PRESETS[agentName];
|
|
154
|
+
if (!presetName) throw new Error(`agent: agent "${agentName}" has no model assignment`);
|
|
155
|
+
const preset = findPreset(config, presetName) || synthesizePreset(config, presetName);
|
|
156
|
+
if (!preset) throw new Error(`agent: preset "${presetName}" not found`);
|
|
157
|
+
return { presetName, preset };
|
|
158
|
+
}
|
|
93
159
|
let lastSpawnStartAt = 0;
|
|
94
160
|
async function waitForSpawnStagger() {
|
|
95
161
|
if (SPAWN_STAGGER_MS <= 0) return;
|
|
@@ -177,13 +243,48 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
177
243
|
.filter(keepWorkerRow);
|
|
178
244
|
}
|
|
179
245
|
|
|
246
|
+
function normalizeTagTombstones(value, { cap = true, priorityKeys = null } = {}) {
|
|
247
|
+
const source = Array.isArray(value?.tombstones)
|
|
248
|
+
? value.tombstones
|
|
249
|
+
: (value?.tombstones && typeof value.tombstones === 'object'
|
|
250
|
+
? Object.values(value.tombstones)
|
|
251
|
+
: []);
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
const cutoff = now - TAG_TOMBSTONE_TTL_MS;
|
|
254
|
+
const rows = source
|
|
255
|
+
.filter((row) => row && typeof row === 'object')
|
|
256
|
+
.map((row) => {
|
|
257
|
+
const parsedReapedAt = Date.parse(clean(row.reapedAt)) || 0;
|
|
258
|
+
return {
|
|
259
|
+
tag: clean(row.tag),
|
|
260
|
+
agent: clean(row.agent) || null,
|
|
261
|
+
cwd: clean(row.cwd) || null,
|
|
262
|
+
clientHostPid: positiveInt(row.clientHostPid),
|
|
263
|
+
// A future clock must not outrank tombstones created by this process.
|
|
264
|
+
reapedAt: parsedReapedAt ? new Date(Math.min(parsedReapedAt, now)).toISOString() : null,
|
|
265
|
+
};
|
|
266
|
+
})
|
|
267
|
+
.filter((row) => row.tag && row.reapedAt && (Date.parse(row.reapedAt) || 0) >= cutoff)
|
|
268
|
+
.sort((a, b) => {
|
|
269
|
+
const aPriority = priorityKeys?.has(tagTombstoneKey(a)) ? 1 : 0;
|
|
270
|
+
const bPriority = priorityKeys?.has(tagTombstoneKey(b)) ? 1 : 0;
|
|
271
|
+
return bPriority - aPriority
|
|
272
|
+
|| (Date.parse(b.reapedAt) || 0) - (Date.parse(a.reapedAt) || 0);
|
|
273
|
+
});
|
|
274
|
+
return cap ? rows.slice(0, MAX_TAG_TOMBSTONES) : rows;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function tagTombstoneKey(row = {}) {
|
|
278
|
+
return `${positiveInt(row.clientHostPid) || 0}\0${clean(row.tag)}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
180
281
|
// Mtime-keyed parse cache for the worker index. A single spawn calls
|
|
181
282
|
// refreshTagsFromSessions()/resolveTag()/nextTag() which each re-read and
|
|
182
283
|
// re-JSON.parse this file; across a parallel fanout that is O(spawns^2)
|
|
183
284
|
// synchronous reads of the same bytes on the event loop. Cache the parsed,
|
|
184
285
|
// normalized rows and reuse them while the file mtime+size is unchanged.
|
|
185
286
|
// Writes bump _workerRowsCacheDirty so the very next read re-parses.
|
|
186
|
-
let _workerRowsCache = null; // { mtimeMs, size, rows }
|
|
287
|
+
let _workerRowsCache = null; // { mtimeMs, size, rows, tombstones }
|
|
187
288
|
let _workerRowsCacheDirty = true;
|
|
188
289
|
function invalidateWorkerRowsCache() {
|
|
189
290
|
_workerRowsCacheDirty = true;
|
|
@@ -200,15 +301,26 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
200
301
|
return _workerRowsCache.rows;
|
|
201
302
|
}
|
|
202
303
|
let rows = [];
|
|
304
|
+
let tombstones = [];
|
|
203
305
|
try {
|
|
204
|
-
|
|
306
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
307
|
+
rows = normalizeWorkerRows(parsed);
|
|
308
|
+
tombstones = normalizeTagTombstones(parsed);
|
|
205
309
|
} catch {
|
|
206
310
|
rows = [];
|
|
311
|
+
tombstones = [];
|
|
207
312
|
}
|
|
208
|
-
_workerRowsCache = { mtimeMs: st.mtimeMs, size: st.size, rows };
|
|
313
|
+
_workerRowsCache = { mtimeMs: st.mtimeMs, size: st.size, rows, tombstones };
|
|
209
314
|
_workerRowsCacheDirty = false;
|
|
210
315
|
return rows;
|
|
211
316
|
}
|
|
317
|
+
function readAllTagTombstones() {
|
|
318
|
+
readAllWorkerRows();
|
|
319
|
+
return _workerRowsCache?.tombstones || [];
|
|
320
|
+
}
|
|
321
|
+
function readTagTombstones(context = {}) {
|
|
322
|
+
return readAllTagTombstones().filter((row) => rowMatchesContext(row, context));
|
|
323
|
+
}
|
|
212
324
|
function readWorkerRows(context = {}) {
|
|
213
325
|
const rows = readAllWorkerRows();
|
|
214
326
|
if (rows.length === 0) return rows;
|
|
@@ -229,13 +341,25 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
229
341
|
const key = workerRowKey(row);
|
|
230
342
|
if (key) byKey.set(key, row);
|
|
231
343
|
}
|
|
232
|
-
|
|
344
|
+
const tombstonesByKey = new Map();
|
|
345
|
+
for (const row of normalizeTagTombstones(cur, { cap: false })) {
|
|
346
|
+
tombstonesByKey.set(tagTombstoneKey(row), row);
|
|
347
|
+
}
|
|
348
|
+
const priorityTombstoneKeys = new Set();
|
|
349
|
+
mutator(byKey, tombstonesByKey, priorityTombstoneKeys);
|
|
233
350
|
const workers = {};
|
|
234
351
|
for (const row of [...byKey.values()].filter(keepWorkerRow)) {
|
|
235
352
|
const key = workerRowKey(row);
|
|
236
353
|
if (key) workers[key] = row;
|
|
237
354
|
}
|
|
238
|
-
|
|
355
|
+
const tombstones = {};
|
|
356
|
+
for (const row of normalizeTagTombstones(
|
|
357
|
+
{ tombstones: [...tombstonesByKey.values()] },
|
|
358
|
+
{ priorityKeys: priorityTombstoneKeys },
|
|
359
|
+
)) {
|
|
360
|
+
tombstones[tagTombstoneKey(row)] = row;
|
|
361
|
+
}
|
|
362
|
+
return { version: 2, updatedAt: new Date().toISOString(), workers, tombstones };
|
|
239
363
|
}, { lock: true });
|
|
240
364
|
// This process just rewrote the index; force the next read to re-parse
|
|
241
365
|
// even if the new mtime/size happen to collide with the cached stat.
|
|
@@ -504,6 +628,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
504
628
|
}
|
|
505
629
|
|
|
506
630
|
function refreshTagsFromSessions({ scanSessions = false, context = {} } = {}) {
|
|
631
|
+
transitionStaleNonterminalRows(context);
|
|
507
632
|
const indexedRows = refreshTagsFromIndex(context);
|
|
508
633
|
const indexedKeys = new Set(indexedRows.map((row) => `${row.tag}\0${row.sessionId}`));
|
|
509
634
|
for (const [tag, sessionId] of [...tags.entries()]) {
|
|
@@ -555,6 +680,50 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
555
680
|
if (id) removeWorkerRow({ sessionId: id });
|
|
556
681
|
}
|
|
557
682
|
|
|
683
|
+
function tombstoneTerminalSession(tag, sessionId, session = null) {
|
|
684
|
+
const value = clean(tag);
|
|
685
|
+
const id = clean(sessionId);
|
|
686
|
+
if (!value || !id) {
|
|
687
|
+
forgetTerminalSession(value, id);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (tags.get(value) === id) {
|
|
691
|
+
tags.delete(value);
|
|
692
|
+
tagAgents.delete(value);
|
|
693
|
+
tagCwds.delete(value);
|
|
694
|
+
}
|
|
695
|
+
const tombstone = {
|
|
696
|
+
tag: value,
|
|
697
|
+
agent: clean(session?.agent) || null,
|
|
698
|
+
cwd: clean(session?.cwd) || null,
|
|
699
|
+
clientHostPid: positiveInt(session?.clientHostPid),
|
|
700
|
+
reapedAt: new Date().toISOString(),
|
|
701
|
+
};
|
|
702
|
+
flushWorkerIndexMutations();
|
|
703
|
+
writeWorkerRows((byKey, tombstonesByKey, priorityTombstoneKeys) => {
|
|
704
|
+
for (const [key, row] of [...byKey.entries()]) {
|
|
705
|
+
if (clean(row.sessionId) === id) byKey.delete(key);
|
|
706
|
+
}
|
|
707
|
+
const tombstoneKey = tagTombstoneKey(tombstone);
|
|
708
|
+
tombstonesByKey.set(tombstoneKey, tombstone);
|
|
709
|
+
priorityTombstoneKeys.add(tombstoneKey);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function tagTombstoneForTag(tag, context = {}) {
|
|
714
|
+
const value = clean(tag);
|
|
715
|
+
if (!value || value.startsWith('sess_')) return null;
|
|
716
|
+
return readTagTombstones(context).find((row) => row.tag === value) || null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function consumeTagTombstone(tombstone) {
|
|
720
|
+
if (!tombstone?.tag) return false;
|
|
721
|
+
const key = tagTombstoneKey(tombstone);
|
|
722
|
+
flushWorkerIndexMutations();
|
|
723
|
+
writeWorkerRows((_byKey, tombstonesByKey) => tombstonesByKey.delete(key));
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
|
|
558
727
|
function cancelReap(sessionId) {
|
|
559
728
|
const handle = reapTimers.get(sessionId);
|
|
560
729
|
if (!handle) return false;
|
|
@@ -571,9 +740,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
571
740
|
if (reapMs == null) return;
|
|
572
741
|
const handle = setTimeout(() => {
|
|
573
742
|
reapTimers.delete(sessionId);
|
|
743
|
+
const session = getLiveSession(sessionId);
|
|
574
744
|
try { mgr.hideSessionFromList?.(sessionId); } catch {}
|
|
575
745
|
const tag = tagForSession(sessionId);
|
|
576
|
-
|
|
746
|
+
tombstoneTerminalSession(tag, sessionId, session);
|
|
577
747
|
clearAgentStatuslineRoute(sessionId);
|
|
578
748
|
try { mgr.closeSession(sessionId, 'terminal-reap'); } catch {}
|
|
579
749
|
}, reapMs);
|
|
@@ -581,6 +751,47 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
581
751
|
reapTimers.set(sessionId, handle);
|
|
582
752
|
}
|
|
583
753
|
|
|
754
|
+
function transitionStaleNonterminalRows(context = {}) {
|
|
755
|
+
const staleRows = readWorkerRows(context).filter((row) => {
|
|
756
|
+
if (isTerminalWorkerStatus(row.status || row.stage)) return false;
|
|
757
|
+
if (getLiveSession(clean(row.sessionId))) return false;
|
|
758
|
+
const rowTime = workerRowTime(row);
|
|
759
|
+
const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), row.provider);
|
|
760
|
+
// A row with no timestamp has no usable heartbeat at all. Explicitly
|
|
761
|
+
// disabled terminal reaping still gets the tombstone TTL as a finite
|
|
762
|
+
// stale-heartbeat bound, so malformed/running index rows cannot block a
|
|
763
|
+
// tag forever.
|
|
764
|
+
return rowTime <= 0 || Date.now() - rowTime >= (reapMs ?? TAG_TOMBSTONE_TTL_MS);
|
|
765
|
+
});
|
|
766
|
+
if (staleRows.length === 0) return false;
|
|
767
|
+
flushWorkerIndexMutations();
|
|
768
|
+
const nowIso = new Date().toISOString();
|
|
769
|
+
writeWorkerRows((byKey, tombstonesByKey, priorityTombstoneKeys) => {
|
|
770
|
+
for (const row of staleRows) {
|
|
771
|
+
const sessionId = clean(row.sessionId);
|
|
772
|
+
for (const [key, candidate] of [...byKey.entries()]) {
|
|
773
|
+
if (clean(candidate.sessionId) === sessionId) byKey.delete(key);
|
|
774
|
+
}
|
|
775
|
+
const tombstone = {
|
|
776
|
+
tag: clean(row.tag),
|
|
777
|
+
agent: clean(row.agent) || null,
|
|
778
|
+
cwd: clean(row.cwd) || null,
|
|
779
|
+
clientHostPid: positiveInt(row.clientHostPid),
|
|
780
|
+
reapedAt: nowIso,
|
|
781
|
+
};
|
|
782
|
+
const tombstoneKey = tagTombstoneKey(tombstone);
|
|
783
|
+
tombstonesByKey.set(tombstoneKey, tombstone);
|
|
784
|
+
priorityTombstoneKeys.add(tombstoneKey);
|
|
785
|
+
if (tags.get(tombstone.tag) === sessionId) {
|
|
786
|
+
tags.delete(tombstone.tag);
|
|
787
|
+
tagAgents.delete(tombstone.tag);
|
|
788
|
+
tagCwds.delete(tombstone.tag);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
|
|
584
795
|
function isSessionBusy(sessionId) {
|
|
585
796
|
const runtime = mgr.getSessionRuntime?.(sessionId);
|
|
586
797
|
if (runtime?.controller?.signal && !runtime.controller.signal.aborted) return true;
|
|
@@ -594,55 +805,6 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
594
805
|
// chain-gate defaults to the spawn-prep cap (see provider-init.mjs comments).
|
|
595
806
|
const { ensureProvider } = createProviderInit(reg, DEFAULT_SPAWN_PREP_TIMEOUT_MS);
|
|
596
807
|
|
|
597
|
-
function resolvePreset(config, args) {
|
|
598
|
-
if (args.provider && args.model) {
|
|
599
|
-
return {
|
|
600
|
-
presetName: args.preset || '__direct__',
|
|
601
|
-
preset: {
|
|
602
|
-
id: '__direct__',
|
|
603
|
-
name: '__DIRECT__',
|
|
604
|
-
type: 'agent',
|
|
605
|
-
provider: clean(args.provider),
|
|
606
|
-
model: clean(args.model),
|
|
607
|
-
effort: clean(args.effort) || undefined,
|
|
608
|
-
fast: args.fast === true,
|
|
609
|
-
tools: 'full',
|
|
610
|
-
},
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const agentName = normalizeAgentName(args.agent);
|
|
615
|
-
const configuredDefault = clean(config?.defaultProvider);
|
|
616
|
-
const fallbackProvider = configuredDefault && isKnownProvider(configuredDefault)
|
|
617
|
-
? configuredDefault
|
|
618
|
-
: DEFAULT_PROVIDER;
|
|
619
|
-
const agentRoute = !clean(args.preset)
|
|
620
|
-
? (normalizeAgentRoute(config?.agents?.[agentName], fallbackProvider)
|
|
621
|
-
|| (agentName === 'maintainer' ? normalizeAgentRoute(config?.agents?.maintenance, fallbackProvider) : null))
|
|
622
|
-
: null;
|
|
623
|
-
if (agentRoute) {
|
|
624
|
-
return {
|
|
625
|
-
presetName: agentPresetName(agentName),
|
|
626
|
-
preset: {
|
|
627
|
-
id: `agent-${agentName}`,
|
|
628
|
-
name: agentPresetName(agentName),
|
|
629
|
-
type: 'agent',
|
|
630
|
-
provider: agentRoute.provider,
|
|
631
|
-
model: agentRoute.model,
|
|
632
|
-
effort: agentRoute.effort,
|
|
633
|
-
fast: agentRoute.fast === true,
|
|
634
|
-
tools: 'full',
|
|
635
|
-
},
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const presetName = clean(args.preset) || DEFAULT_AGENT_PRESETS[agentName];
|
|
640
|
-
if (!presetName) throw new Error(`agent: agent "${agentName}" has no model assignment`);
|
|
641
|
-
const preset = findPreset(config, presetName) || synthesizePreset(config, presetName);
|
|
642
|
-
if (!preset) throw new Error(`agent: preset "${presetName}" not found`);
|
|
643
|
-
return { presetName, preset };
|
|
644
|
-
}
|
|
645
|
-
|
|
646
808
|
function list({ scanSessions = false, context = {} } = {}) {
|
|
647
809
|
refreshTagsFromSessions({ scanSessions, context });
|
|
648
810
|
const now = Date.now();
|
|
@@ -1028,7 +1190,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1028
1190
|
if (!agent) throw new Error('agent spawn: agent is required');
|
|
1029
1191
|
const agentPermission = readAgentFrontmatterPermission(agent, dataDir, STANDALONE_SOURCE_ROOT);
|
|
1030
1192
|
const agentPerm = normalizeAgentPermission(agentPermission) || null;
|
|
1031
|
-
const { presetName, preset } =
|
|
1193
|
+
const { presetName, preset } = resolveAgentSpawnPreset(config, args);
|
|
1032
1194
|
await ensureProvider(config, preset.provider);
|
|
1033
1195
|
if (prepState?.timedOut) {
|
|
1034
1196
|
throw new Error('agent spawn prep timed out before session bind');
|
|
@@ -1522,7 +1684,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1522
1684
|
tagAgents.clear();
|
|
1523
1685
|
tagCwds.clear();
|
|
1524
1686
|
flushWorkerIndexMutations();
|
|
1525
|
-
writeWorkerRows((byKey) =>
|
|
1687
|
+
writeWorkerRows((byKey, tombstonesByKey) => {
|
|
1688
|
+
byKey.clear();
|
|
1689
|
+
tombstonesByKey.clear();
|
|
1690
|
+
});
|
|
1526
1691
|
return { closed, failed };
|
|
1527
1692
|
}
|
|
1528
1693
|
|
|
@@ -1589,25 +1754,26 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1589
1754
|
if (!fallbackTag || fallbackTag.startsWith('sess_') || !isDeadTarget) throw err;
|
|
1590
1755
|
const prompt = clean(args.message || args.prompt);
|
|
1591
1756
|
if (!prompt) throw err;
|
|
1592
|
-
// A retained row
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1757
|
+
// A retained row or reap tombstone proves that this terminal owned
|
|
1758
|
+
// the tag. Unknown tags stay errors even when the caller supplies an
|
|
1759
|
+
// agent/cwd: typo absorption requires persisted same-tag evidence.
|
|
1760
|
+
// Absorption identity is always terminal-local, even when live
|
|
1761
|
+
// resolution was explicitly requested across all terminals.
|
|
1762
|
+
const ownershipContext = context;
|
|
1595
1763
|
let inheritedRow = null;
|
|
1596
1764
|
try {
|
|
1597
|
-
inheritedRow = readWorkerRows(
|
|
1765
|
+
inheritedRow = readWorkerRows(ownershipContext).find((row) => clean(row.tag) === fallbackTag) || null;
|
|
1598
1766
|
} catch { inheritedRow = null; }
|
|
1599
|
-
|
|
1600
|
-
// by another terminal. In-memory maps can contain peer entries, so
|
|
1601
|
-
// inspect the persisted index directly.
|
|
1602
|
-
const peerRow = readAllWorkerRows().find((row) => (
|
|
1603
|
-
clean(row.tag) === fallbackTag && !rowMatchesContext(row, scopedContext)
|
|
1604
|
-
));
|
|
1767
|
+
const inheritedTombstone = tagTombstoneForTag(fallbackTag, ownershipContext);
|
|
1605
1768
|
const explicitAgent = clean(args.agent);
|
|
1606
|
-
|
|
1607
|
-
|
|
1769
|
+
// A local proof wins even if another terminal also owns this tag.
|
|
1770
|
+
// With no local proof, foreign rows/tombstones and unknown tags are
|
|
1771
|
+
// both non-absorbable and retain the original not-found error.
|
|
1772
|
+
if (!inheritedRow && !inheritedTombstone) throw err;
|
|
1608
1773
|
const inheritedSessionId = clean(inheritedRow?.sessionId);
|
|
1609
|
-
const inheritedAgent = explicitAgent || clean(inheritedRow?.agent) ||
|
|
1610
|
-
const inheritedCwd = clean(args.cwd) || clean(inheritedRow?.cwd) || clean(callerCwd);
|
|
1774
|
+
const inheritedAgent = explicitAgent || clean(inheritedRow?.agent) || clean(inheritedTombstone?.agent);
|
|
1775
|
+
const inheritedCwd = clean(args.cwd) || clean(inheritedRow?.cwd) || clean(inheritedTombstone?.cwd) || clean(callerCwd);
|
|
1776
|
+
if (!inheritedAgent || !inheritedCwd) throw err;
|
|
1611
1777
|
// Drop this terminal's in-memory trace and remove ONLY the persisted
|
|
1612
1778
|
// row matching inheritedRow.sessionId. Do NOT call forgetTag here: it
|
|
1613
1779
|
// does a tag-wide removeWorkerRow({tag,sessionId}) (L556) whose OR
|
|
@@ -1618,6 +1784,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1618
1784
|
try { tags.delete(fallbackTag); tagAgents.delete(fallbackTag); tagCwds.delete(fallbackTag); } catch {}
|
|
1619
1785
|
}
|
|
1620
1786
|
if (inheritedSessionId) { try { removeWorkerRow({ sessionId: inheritedSessionId }); } catch {} }
|
|
1787
|
+
if (inheritedTombstone) consumeTagTombstone(inheritedTombstone);
|
|
1621
1788
|
const spawnArgs = {
|
|
1622
1789
|
...args,
|
|
1623
1790
|
type: 'spawn',
|
|
@@ -1638,6 +1805,8 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1638
1805
|
// 3) lingering terminal trace -> reap trace and fresh spawn under same tag
|
|
1639
1806
|
// 4) genuinely new tag -> fresh deferred spawn
|
|
1640
1807
|
const explicitTag = clean(args.tag);
|
|
1808
|
+
let respawned = false;
|
|
1809
|
+
let spawnArgs = args;
|
|
1641
1810
|
if (explicitTag) {
|
|
1642
1811
|
// Resolve a LIVE same-tag session in this terminal (busy or idle).
|
|
1643
1812
|
let liveSessionId = null;
|
|
@@ -1658,9 +1827,35 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1658
1827
|
}
|
|
1659
1828
|
if (hasTerminalTrace(explicitTag, scopedContext)) {
|
|
1660
1829
|
reapTerminalTraceForTag(explicitTag, scopedContext);
|
|
1830
|
+
respawned = true;
|
|
1831
|
+
} else {
|
|
1832
|
+
// Tombstone inheritance never honors allTerminals/global scope.
|
|
1833
|
+
const tombstone = tagTombstoneForTag(explicitTag, context);
|
|
1834
|
+
if (tombstone) {
|
|
1835
|
+
consumeTagTombstone(tombstone);
|
|
1836
|
+
spawnArgs = {
|
|
1837
|
+
...args,
|
|
1838
|
+
agent: clean(args.agent) || clean(tombstone.agent),
|
|
1839
|
+
...(clean(args.cwd) || !clean(tombstone.cwd) ? {} : { cwd: tombstone.cwd }),
|
|
1840
|
+
};
|
|
1841
|
+
respawned = true;
|
|
1842
|
+
} else {
|
|
1843
|
+
const foreignTombstone = readAllTagTombstones().find((row) => (
|
|
1844
|
+
clean(row.tag) === explicitTag && !rowMatchesContext(row, context)
|
|
1845
|
+
));
|
|
1846
|
+
if (foreignTombstone) {
|
|
1847
|
+
throw new Error(`agent spawn: tag "${explicitTag}" belongs to another terminal`);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1661
1850
|
}
|
|
1662
1851
|
}
|
|
1663
|
-
const job = startDeferredSpawnJob(
|
|
1852
|
+
const job = startDeferredSpawnJob(
|
|
1853
|
+
spawnArgs,
|
|
1854
|
+
callerCwd,
|
|
1855
|
+
context,
|
|
1856
|
+
notifyContext,
|
|
1857
|
+
respawned ? { respawned: true } : {},
|
|
1858
|
+
);
|
|
1664
1859
|
return renderResult(renderJob(job, false));
|
|
1665
1860
|
}
|
|
1666
1861
|
throw new Error(`agent: unknown type "${type}"`);
|