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.
Files changed (31) hide show
  1. package/package.json +10 -58
  2. package/src/rules/shared/01-tool.md +15 -6
  3. package/src/runtime/agent/orchestrator/config.mjs +0 -1
  4. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
  5. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +3 -1
  6. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +11 -1
  7. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -2
  8. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +74 -3
  9. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +26 -7
  10. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -1
  11. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
  12. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -5
  13. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +7 -7
  14. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +14 -30
  15. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -2
  16. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +3 -3
  18. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +2 -2
  19. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +40 -0
  20. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +0 -1
  21. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  22. package/src/runtime/shared/agent-route-config.mjs +0 -5
  23. package/src/session-runtime/settings-api.mjs +9 -1
  24. package/src/session-runtime/tool-defs.mjs +2 -2
  25. package/src/standalone/daemon.mjs +1 -0
  26. package/src/standalone/session-protocol.mjs +1 -0
  27. package/src/tui/session/session-api.mjs +2 -1
  28. package/src/tui/session/session-flow.mjs +25 -1
  29. package/src/ui/statusline-segments.mjs +20 -5
  30. package/scripts/run-suite.mjs +0 -101
  31. 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
- }