mixdog 0.9.111 → 0.9.113
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 +10 -58
- package/src/rules/shared/01-tool.md +15 -6
- package/src/runtime/agent/orchestrator/config.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +74 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +26 -7
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -5
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +14 -30
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +0 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/shared/agent-route-config.mjs +0 -5
- package/src/session-runtime/settings-api.mjs +9 -1
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/standalone/daemon.mjs +1 -0
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/tui/session/session-api.mjs +2 -1
- package/src/tui/session/session-flow.mjs +25 -1
- package/src/ui/statusline-segments.mjs +20 -5
- package/scripts/run-suite.mjs +0 -101
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +0 -188
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
// Shell state carry-over for the one-shot shell tool.
|
|
3
|
-
//
|
|
4
|
-
// No live shell process is kept. Instead, after each sync command we chain a
|
|
5
|
-
// trailing cwd probe that writes the final working directory to a per-session
|
|
6
|
-
// state file. The next shell call (without an explicit cwd arg) reads that file
|
|
7
|
-
// and starts there — mimicking a persistent terminal's cwd without a PTY.
|
|
8
|
-
//
|
|
9
|
-
// Style mirrors shell-snapshot.mjs: files live under getPluginData(), one file
|
|
10
|
-
// per session key, with a stale-file sweep on module init.
|
|
11
|
-
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, utimesSync, writeFileSync } from 'node:fs';
|
|
13
|
-
import { readdir as readdirAsync, stat as statAsync, unlink as unlinkAsync } from 'node:fs/promises';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { createHash } from 'node:crypto';
|
|
16
|
-
import { getPluginData } from '../config.mjs';
|
|
17
|
-
|
|
18
|
-
function _stateDir() {
|
|
19
|
-
return join(getPluginData(), 'shell-state');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// One file per session key. Hash the key so arbitrary session ids (which may
|
|
23
|
-
// contain path-hostile chars) map to a safe, collision-resistant filename.
|
|
24
|
-
function _keyFor(sessionKey) {
|
|
25
|
-
const raw = String(sessionKey == null || sessionKey === '' ? '__default__' : sessionKey);
|
|
26
|
-
return createHash('sha1').update(raw).digest('hex').slice(0, 16);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function _stateFileFor(sessionKey) {
|
|
30
|
-
return join(_stateDir(), `cwd-${_keyFor(sessionKey)}.txt`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function _probeFileFor(sessionKey, probeId) {
|
|
34
|
-
return join(_stateDir(), `cwd-${_keyFor(sessionKey)}.probe-${_keyFor(probeId)}.txt`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Stale-file sweep on module init. Mirrors shell-snapshot's _sweepStaleSnapshots:
|
|
38
|
-
// state files older than the cutoff belong to sessions that are long gone.
|
|
39
|
-
const STATE_STALE_MS = 24 * 60 * 60 * 1000;
|
|
40
|
-
async function _sweepStaleState() {
|
|
41
|
-
const dir = _stateDir();
|
|
42
|
-
if (!existsSync(dir)) return;
|
|
43
|
-
const cutoff = Date.now() - STATE_STALE_MS;
|
|
44
|
-
let names;
|
|
45
|
-
try { names = await readdirAsync(dir); } catch { return; }
|
|
46
|
-
await Promise.all(names.map(async (name) => {
|
|
47
|
-
if (!name.startsWith('cwd-') || !name.endsWith('.txt')) return;
|
|
48
|
-
const p = join(dir, name);
|
|
49
|
-
try {
|
|
50
|
-
const st = await statAsync(p);
|
|
51
|
-
if (st.mtimeMs < cutoff) {
|
|
52
|
-
// Tolerate the stat→unlink race: a concurrent write may have
|
|
53
|
-
// refreshed mtime, or another sweeper may have already removed
|
|
54
|
-
// it. ENOENT and any transient error are non-fatal.
|
|
55
|
-
try { await unlinkAsync(p); } catch (err) { if (err && err.code !== 'ENOENT') { /* swallow */ } }
|
|
56
|
-
}
|
|
57
|
-
} catch {}
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
_sweepStaleState();
|
|
61
|
-
|
|
62
|
-
// Resolve the effective cwd for a shell call. Explicit cwd always wins (the
|
|
63
|
-
// caller resolved/validated it already). Otherwise, if a stored cwd exists and
|
|
64
|
-
// still points at a live directory, use it; else fall back to defaultCwd.
|
|
65
|
-
export function resolveSessionCwd(sessionKey, explicitCwd, defaultCwd) {
|
|
66
|
-
if (explicitCwd) return explicitCwd;
|
|
67
|
-
// No nonblank session key → no carry-over (mirrors stateFilePath). Without a
|
|
68
|
-
// real key every caller would share the `__default__` file and read each
|
|
69
|
-
// other's cwd, so skip the store entirely and use the default.
|
|
70
|
-
if (sessionKey == null || String(sessionKey).trim() === '') return defaultCwd;
|
|
71
|
-
try {
|
|
72
|
-
const file = _stateFileFor(sessionKey);
|
|
73
|
-
if (!existsSync(file)) return defaultCwd;
|
|
74
|
-
const stored = readFileSync(file, 'utf-8').trim();
|
|
75
|
-
if (!stored) return defaultCwd;
|
|
76
|
-
const st = statSync(stored);
|
|
77
|
-
if (st.isDirectory()) return stored;
|
|
78
|
-
} catch {}
|
|
79
|
-
return defaultCwd;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Path to the state file for this session (created lazily). Returned so the
|
|
83
|
-
// caller can embed it in the probe chain. Returns null if the dir can't be made
|
|
84
|
-
// OR when there is no nonblank session key — without a real key all callers
|
|
85
|
-
// would collapse onto a single shared `__default__` file and cross-contaminate
|
|
86
|
-
// each other's cwd, so we skip carry-over entirely in that case.
|
|
87
|
-
export function stateFilePath(sessionKey, probeId = null) {
|
|
88
|
-
if (sessionKey == null || String(sessionKey).trim() === '') return null;
|
|
89
|
-
const dir = _stateDir();
|
|
90
|
-
try { mkdirSync(dir, { recursive: true }); } catch { return null; }
|
|
91
|
-
if (probeId != null && String(probeId).trim() !== '') {
|
|
92
|
-
return _probeFileFor(sessionKey, probeId);
|
|
93
|
-
}
|
|
94
|
-
const file = _stateFileFor(sessionKey);
|
|
95
|
-
// Refresh mtime on every write so an old-but-live session file is not
|
|
96
|
-
// reaped by the stale sweep mid-session. `touch`-equivalent: bump atime/
|
|
97
|
-
// mtime to now if the file already exists; a fresh file gets its mtime on
|
|
98
|
-
// first write by the probe redirection.
|
|
99
|
-
try {
|
|
100
|
-
if (existsSync(file)) {
|
|
101
|
-
const now = new Date();
|
|
102
|
-
utimesSync(file, now, now);
|
|
103
|
-
}
|
|
104
|
-
} catch {}
|
|
105
|
-
return file;
|
|
106
|
-
}
|
|
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
|
-
|
|
131
|
-
// PowerShell single-quote escape ('' escapes a literal quote inside '...').
|
|
132
|
-
function _psQuote(s) {
|
|
133
|
-
return `'${String(s).replace(/'/g, "''")}'`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// POSIX single-quote escape.
|
|
137
|
-
function _shQuote(s) {
|
|
138
|
-
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Append a trailing cwd probe so the final working directory is persisted after
|
|
142
|
-
// the user command runs. The probe is joined with `;` (statement separator, not
|
|
143
|
-
// `&&`) so it runs regardless of the command's exit status, and it does not
|
|
144
|
-
// touch the exit code the runtime reports: the runtime reads the child's exit
|
|
145
|
-
// code, and neither `Out-File` nor a redirection changes `$?`/`$LASTEXITCODE`
|
|
146
|
-
// semantics observed by the *process* exit — the shell's exit status is the
|
|
147
|
-
// last statement's, but see wrapWithCwdProbe callers which capture the code
|
|
148
|
-
// BEFORE the probe. For PowerShell we snapshot $LASTEXITCODE/$? and restore the
|
|
149
|
-
// process exit via `exit` so the probe cannot mask a non-zero command.
|
|
150
|
-
export function wrapPowerShellWithCwdProbe(command, stateFile) {
|
|
151
|
-
if (!stateFile) return command;
|
|
152
|
-
const qFile = _psQuote(stateFile);
|
|
153
|
-
// $?-driven, bash-like exit semantics. $LASTEXITCODE persists the last
|
|
154
|
-
// *native* process's code indefinitely, so a prologue reset cannot cover
|
|
155
|
-
// intra-command staleness (`cmd /c exit 5; Write-Output ok` leaves the
|
|
156
|
-
// trailing cmdlet succeeding but $LASTEXITCODE still 5). Instead we mirror
|
|
157
|
-
// bash: the exit code is the LAST statement's success. $? immediately after
|
|
158
|
-
// the command reflects that statement — false only when it failed, and for
|
|
159
|
-
// a failing native call $LASTEXITCODE holds its real code (case3 → 7).
|
|
160
|
-
//
|
|
161
|
-
// The `if (-not $?)` check MUST be the first statement after the command;
|
|
162
|
-
// any intervening statement resets $?. Terminating errors (`throw`,
|
|
163
|
-
// `-ErrorAction Stop`, .NET exceptions) skip to catch → $__ec = 1. A
|
|
164
|
-
// *caught* terminating error is NOT auto-written to the error stream, so
|
|
165
|
-
// without re-emitting it the caller only sees `[exit code: 1] (no output)`.
|
|
166
|
-
// Re-surface the ErrorRecord on stderr so the failure cause reaches the
|
|
167
|
-
// result envelope. The cwd probe runs in finally regardless, and we exit
|
|
168
|
-
// with $__ec so the probe never masks the observed code.
|
|
169
|
-
return '$global:LASTEXITCODE = 0\n'
|
|
170
|
-
+ '$__ec = 0\n'
|
|
171
|
-
+ 'try {\n'
|
|
172
|
-
+ `${command}\n`
|
|
173
|
-
+ ' if (-not $?) { $__ec = if ($LASTEXITCODE) { $LASTEXITCODE } else { 1 } }\n'
|
|
174
|
-
+ '} catch { [Console]::Error.WriteLine(($_ | Out-String).Trim()); $__ec = 1 }\n'
|
|
175
|
-
+ `finally { (Get-Location).Path | Out-File -Encoding utf8 ${qFile} }\n`
|
|
176
|
-
+ 'exit $__ec';
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function wrapBashWithCwdProbe(command, stateFile) {
|
|
180
|
-
if (!stateFile) return command;
|
|
181
|
-
const qFile = _shQuote(stateFile);
|
|
182
|
-
// Capture $? before the probe, run the probe unconditionally (`;`), then
|
|
183
|
-
// re-exit with the saved status so `pwd`/redirection cannot mask it.
|
|
184
|
-
return `${command}\n`
|
|
185
|
-
+ '__mixdog_ec=$?;\n'
|
|
186
|
-
+ `pwd -P >| ${qFile} 2>/dev/null || true;\n`
|
|
187
|
-
+ 'exit $__mixdog_ec';
|
|
188
|
-
}
|