mixdog 0.9.87 → 0.9.88
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 +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +144 -10
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +67 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +203 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +73 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +11 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +52 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +233 -121
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +1 -1
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -1,66 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* for JSON.parse on the session-store save-guard hot path.
|
|
2
|
+
* Authoritative top-level lifecycle read (`id` / `closed` / `generation`) for
|
|
3
|
+
* a serialized session JSON document.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* entire messages array, unescaping every string) on the hot path even
|
|
11
|
-
* though only two scalar fields are ever consulted.
|
|
5
|
+
* ONE verdict, no fallbacks. Every caller of this module makes a destructive
|
|
6
|
+
* or ownership-deciding choice (drop a save, delete a file, close a session,
|
|
7
|
+
* deliver cross-process input), so the module answers with exactly two
|
|
8
|
+
* outcomes:
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* naively-anchored regex cannot tell that occurrence apart from the real
|
|
17
|
-
* top-level lifecycle field, so it is spoofable. This scanner walks the raw
|
|
18
|
-
* text as a real (bracket-depth + string-escape aware) tokenizer, but only
|
|
19
|
-
* *interprets* key/value pairs at depth 1 (directly inside the root
|
|
20
|
-
* object); every nested object/array/string value — including the entire
|
|
21
|
-
* `messages` array — is skipped by depth counting alone, never allocated
|
|
22
|
-
* or parsed. That keeps it both spoof-proof (nested `"closed"` can never be
|
|
23
|
-
* mistaken for the top-level one) and cheap (no JS object/array/string
|
|
24
|
-
* allocation for content we don't care about).
|
|
10
|
+
* • a lifecycle record — the document is a single, complete, well-formed
|
|
11
|
+
* JSON object whose top-level lifecycle fields are unambiguous;
|
|
12
|
+
* • LIFECYCLE_SCAN_CONFLICT — "unreadable": ANY doubt at all.
|
|
25
13
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
14
|
+
* There is deliberately no third "couldn't tell, parse it yourself" state.
|
|
15
|
+
* The previous incarnation was a hand-rolled depth-1 tokenizer that returned
|
|
16
|
+
* `null` for everything it did not positively confirm and let each caller
|
|
17
|
+
* fall back to `JSON.parse` — which silently resolves duplicate keys
|
|
18
|
+
* LAST-WINS, so a record like `{"id":"mine",…,"id":"other"}` (or a malformed
|
|
19
|
+
* separator the scanner refused) was accepted as this session's own state by
|
|
20
|
+
* the very code paths the scanner existed to protect. The fallback WAS the
|
|
21
|
+
* vulnerability. It is gone.
|
|
29
22
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
23
|
+
* Strategy (simplification over more parser patches):
|
|
24
|
+
* 1. strict full `JSON.parse` — the only validator. It rejects truncation,
|
|
25
|
+
* missing/trailing commas, missing colons, trailing garbage after the
|
|
26
|
+
* root object, and any non-object root, with no bespoke grammar of ours
|
|
27
|
+
* to get subtly wrong;
|
|
28
|
+
* 2. an explicit top-level duplicate-key walk, because `JSON.parse` alone
|
|
29
|
+
* is last-wins and cannot report that a key appeared twice. ANY repeated
|
|
30
|
+
* top-level key is a conflict — not just the lifecycle three: consumers
|
|
31
|
+
* of this record RESERIALIZE it (the tombstone/detach barriers rewrite
|
|
32
|
+
* the whole document) and ACT on other top-level fields (`status`,
|
|
33
|
+
* `updatedAt`, `owner`, `messages` drive sweep maturity, blank-scratch
|
|
34
|
+
* reaping and transcript reads), so a last-wins `status`/`updatedAt`
|
|
35
|
+
* would silently decide a delete or a rewrite. The walk runs on a
|
|
36
|
+
* document ALREADY proven well-formed by step 1, so it only counts
|
|
37
|
+
* depth-1 keys (skipping nested objects/arrays/strings by bracket
|
|
38
|
+
* depth) and never has to double as a validator;
|
|
39
|
+
* 3. type validation of the three lifecycle fields. Our writer only ever
|
|
40
|
+
* emits a non-empty string `id`, a boolean `closed` and a numeric
|
|
41
|
+
* `generation`; anything else present under those names is not a record
|
|
42
|
+
* we can speak for → unreadable.
|
|
43
|
+
*
|
|
44
|
+
* Duplicate detection is type- and order-independent by construction: the
|
|
45
|
+
* walk counts key OCCURRENCES before any value is interpreted, so
|
|
46
|
+
* `{"id":7,"id":"sess_a"}` (non-string first) and `{"closed":false,
|
|
47
|
+
* "closed":true}` conflict exactly like the string-first case.
|
|
48
|
+
*
|
|
49
|
+
* NESTED duplicates stay compatible on purpose: a duplicate key inside
|
|
50
|
+
* `messages[i].content` (a pasted JSON blob, a tool result) is message DATA.
|
|
51
|
+
* It is skipped by bracket depth, is never an ownership or action input, and
|
|
52
|
+
* the whole nested value is reserialized verbatim by JSON.stringify from the
|
|
53
|
+
* parsed document — the last-wins resolution there cannot move a lifecycle,
|
|
54
|
+
* sweep or delete decision.
|
|
55
|
+
*
|
|
56
|
+
* The parsed document is returned alongside the lifecycle fields (`doc`) so
|
|
57
|
+
* consumers that need the whole record (the sweep reads owner/status/
|
|
58
|
+
* timestamps/messages) reuse THIS parse instead of parsing again — a second
|
|
59
|
+
* parse would be both slower and a second, divergent authority.
|
|
41
60
|
*/
|
|
42
61
|
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
// Single authoritative rejection marker: ambiguous OR malformed OR
|
|
63
|
+
// wrong-typed. Frozen and deliberately carrying no `id`/`closed`/`generation`
|
|
64
|
+
// so a lenient consumer that forgets the check still reads nothing usable.
|
|
65
|
+
export const LIFECYCLE_SCAN_CONFLICT = Object.freeze({ conflict: 'unreadable' });
|
|
66
|
+
|
|
67
|
+
/** True for the rejection marker (and for anything that isn't a record). */
|
|
68
|
+
export function isLifecycleUnreadable(result) {
|
|
69
|
+
return !result || typeof result !== 'object' || result.conflict !== undefined;
|
|
45
70
|
}
|
|
46
71
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// (e.g. a concatenated second document, or corruption) must not be silently
|
|
50
|
-
// accepted as if `raw` were exactly one well-formed object.
|
|
51
|
-
function isTrailingWhitespaceOnly(raw, i) {
|
|
52
|
-
const len = raw.length;
|
|
53
|
-
while (i < len) {
|
|
54
|
-
if (!isWs(raw[i])) return false;
|
|
55
|
-
i++;
|
|
56
|
-
}
|
|
57
|
-
return true;
|
|
72
|
+
function isWs(ch) {
|
|
73
|
+
return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r';
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
// Advances past a JSON string literal starting at raw[i] === '"'.
|
|
61
|
-
// Escape handling only needs to skip exactly one char after each `\` —
|
|
62
|
-
//
|
|
63
|
-
//
|
|
77
|
+
// Escape handling only needs to skip exactly one char after each `\` — even
|
|
78
|
+
// for `\u0041`-style escapes the trailing hex digits can never be `"` or `\`,
|
|
79
|
+
// so a naive 2-char skip never misses the real closing quote.
|
|
64
80
|
function skipString(raw, i) {
|
|
65
81
|
const len = raw.length;
|
|
66
82
|
i++; // opening quote
|
|
@@ -73,11 +89,8 @@ function skipString(raw, i) {
|
|
|
73
89
|
return i;
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
// Advances past one JSON value
|
|
77
|
-
//
|
|
78
|
-
// counting (any `{`/`[` opens, any `}`/`]` closes) with string-awareness so
|
|
79
|
-
// braces inside string content never perturb the count — never descends
|
|
80
|
-
// into the structure to interpret its keys.
|
|
92
|
+
// Advances past one JSON value. Objects/arrays are skipped by string-aware
|
|
93
|
+
// bracket-depth counting — never descended into, never allocated.
|
|
81
94
|
function skipValue(raw, i) {
|
|
82
95
|
const len = raw.length;
|
|
83
96
|
const c = raw[i];
|
|
@@ -99,79 +112,96 @@ function skipValue(raw, i) {
|
|
|
99
112
|
return i;
|
|
100
113
|
}
|
|
101
114
|
|
|
102
|
-
|
|
115
|
+
/**
|
|
116
|
+
* True when EVERY depth-1 key occurs exactly once.
|
|
117
|
+
*
|
|
118
|
+
* PRECONDITION: `raw` already parsed cleanly as a JSON object (see
|
|
119
|
+
* readTopLevelLifecycleRecord). That is what lets this be a counter rather
|
|
120
|
+
* than a validator: separator/grammar correctness was already decided by
|
|
121
|
+
* JSON.parse. Returns false on ANY repeated top-level key, or when the walk
|
|
122
|
+
* cannot complete (which cannot happen for a well-formed document — treated
|
|
123
|
+
* as unreadable rather than assumed unique).
|
|
124
|
+
*/
|
|
125
|
+
function hasUniqueTopLevelKeys(raw) {
|
|
126
|
+
// A Set (not an object) so a document key named `constructor`/`__proto__`
|
|
127
|
+
// is counted as data, never as an inherited property.
|
|
128
|
+
const seen = new Set();
|
|
103
129
|
const len = raw.length;
|
|
104
130
|
let i = 0;
|
|
105
131
|
while (i < len && isWs(raw[i])) i++;
|
|
106
|
-
if (raw[i] !== '{') return
|
|
132
|
+
if (raw[i] !== '{') return false;
|
|
107
133
|
i++;
|
|
108
|
-
const result = {};
|
|
109
|
-
let found = 0;
|
|
110
134
|
while (i < len) {
|
|
111
135
|
while (i < len && isWs(raw[i])) i++;
|
|
112
|
-
if (i >= len) return
|
|
113
|
-
if (raw[i] === '}')
|
|
114
|
-
i++;
|
|
115
|
-
return isTrailingWhitespaceOnly(raw, i) ? result : null;
|
|
116
|
-
}
|
|
136
|
+
if (i >= len) return false;
|
|
137
|
+
if (raw[i] === '}') return true;
|
|
117
138
|
if (raw[i] === ',') { i++; continue; }
|
|
118
|
-
if (raw[i] !== '"') return
|
|
139
|
+
if (raw[i] !== '"') return false;
|
|
119
140
|
const keyStart = i;
|
|
120
141
|
i = skipString(raw, i);
|
|
121
142
|
let key;
|
|
122
|
-
try { key = JSON.parse(raw.slice(keyStart, i)); } catch { return
|
|
143
|
+
try { key = JSON.parse(raw.slice(keyStart, i)); } catch { return false; }
|
|
123
144
|
while (i < len && isWs(raw[i])) i++;
|
|
124
|
-
if (raw[i] !== ':') return
|
|
145
|
+
if (raw[i] !== ':') return false;
|
|
125
146
|
i++;
|
|
126
147
|
while (i < len && isWs(raw[i])) i++;
|
|
127
|
-
if (i >= len) return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (stack.length === 0) {
|
|
162
|
-
// Root object closed with '}' — only trust it if
|
|
163
|
-
// nothing but whitespace follows to EOF.
|
|
164
|
-
return isTrailingWhitespaceOnly(raw, i) ? result : null;
|
|
165
|
-
}
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
i++;
|
|
169
|
-
}
|
|
170
|
-
return null; // ran off the end (or hit EOF) before the root object closed: truncated
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
i = skipValue(raw, i);
|
|
174
|
-
}
|
|
148
|
+
if (i >= len) return false;
|
|
149
|
+
// Recorded BEFORE the value is looked at, so a duplicate is detected
|
|
150
|
+
// regardless of the value types or which copy comes first.
|
|
151
|
+
if (seen.has(key)) return false;
|
|
152
|
+
seen.add(key);
|
|
153
|
+
i = skipValue(raw, i);
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Read `raw` as a session record.
|
|
160
|
+
* Returns LIFECYCLE_SCAN_CONFLICT (unreadable) or
|
|
161
|
+
* `{ doc, id, closed, generation }` where each lifecycle field is `undefined`
|
|
162
|
+
* when the document simply does not carry it (legacy records predate them).
|
|
163
|
+
*/
|
|
164
|
+
export function readTopLevelLifecycleRecord(raw) {
|
|
165
|
+
if (typeof raw !== 'string') return LIFECYCLE_SCAN_CONFLICT;
|
|
166
|
+
let doc;
|
|
167
|
+
// Strict and complete: trailing bytes, truncation, missing/trailing
|
|
168
|
+
// commas and every other separator fault are rejected here.
|
|
169
|
+
try { doc = JSON.parse(raw); } catch { return LIFECYCLE_SCAN_CONFLICT; }
|
|
170
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return LIFECYCLE_SCAN_CONFLICT;
|
|
171
|
+
// JSON.parse is last-wins for duplicate keys; this is where that is caught
|
|
172
|
+
// — for EVERY top-level key, since consumers reserialize and act on the
|
|
173
|
+
// whole record, not only on the lifecycle three.
|
|
174
|
+
if (!hasUniqueTopLevelKeys(raw)) return LIFECYCLE_SCAN_CONFLICT;
|
|
175
|
+
const has = (key) => Object.prototype.hasOwnProperty.call(doc, key);
|
|
176
|
+
const record = { doc, id: undefined, closed: undefined, generation: undefined };
|
|
177
|
+
if (has('id')) {
|
|
178
|
+
// Identity we cannot positively confirm is worse than no identity: a
|
|
179
|
+
// consumer must not read an empty/non-string id as "probably ours".
|
|
180
|
+
if (typeof doc.id !== 'string' || !doc.id) return LIFECYCLE_SCAN_CONFLICT;
|
|
181
|
+
record.id = doc.id;
|
|
175
182
|
}
|
|
176
|
-
|
|
183
|
+
if (has('closed')) {
|
|
184
|
+
if (typeof doc.closed !== 'boolean') return LIFECYCLE_SCAN_CONFLICT;
|
|
185
|
+
record.closed = doc.closed;
|
|
186
|
+
}
|
|
187
|
+
if (has('generation')) {
|
|
188
|
+
if (typeof doc.generation !== 'number' || !Number.isFinite(doc.generation)) return LIFECYCLE_SCAN_CONFLICT;
|
|
189
|
+
record.generation = doc.generation;
|
|
190
|
+
}
|
|
191
|
+
return record;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Lifecycle fields only (no parsed document) for callers that consult nothing
|
|
196
|
+
* else. Same two outcomes: the fields present at top level, or
|
|
197
|
+
* LIFECYCLE_SCAN_CONFLICT.
|
|
198
|
+
*/
|
|
199
|
+
export function scanTopLevelLifecycle(raw) {
|
|
200
|
+
const record = readTopLevelLifecycleRecord(raw);
|
|
201
|
+
if (isLifecycleUnreadable(record)) return LIFECYCLE_SCAN_CONFLICT;
|
|
202
|
+
const out = {};
|
|
203
|
+
if (record.id !== undefined) out.id = record.id;
|
|
204
|
+
if (record.closed !== undefined) out.closed = record.closed;
|
|
205
|
+
if (record.generation !== undefined) out.generation = record.generation;
|
|
206
|
+
return out;
|
|
177
207
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Codex-style turn stop hook (refs/codex core/src/session/turn.rs:372-404).
|
|
2
|
+
//
|
|
3
|
+
// Codex semantics: a sampling request that needs no follow-up ends the turn.
|
|
4
|
+
// Before breaking, the turn runs its stop hooks; a hook may block ONCE by
|
|
5
|
+
// returning continuation fragments, which are recorded as a prompt message
|
|
6
|
+
// before sampling resumes (`stop_hook_active = true`, never cleared inside the
|
|
7
|
+
// turn). Nothing else can force another turn: no mandatory terminal tool, no
|
|
8
|
+
// completion strikes, no lexical inspection of the assistant text.
|
|
9
|
+
//
|
|
10
|
+
// The single hook implemented here preserves the original failed-apply_patch
|
|
11
|
+
// protection: while a real tool failure is unresolved, the first terminal
|
|
12
|
+
// assistant message is blocked once with a structural continuation prompt.
|
|
13
|
+
// Afterwards ordinary final text (or further tool calls) is accepted.
|
|
14
|
+
|
|
15
|
+
export const STOP_HOOK_SOURCE = 'tool-failure-stop-hook';
|
|
16
|
+
|
|
17
|
+
// Only a genuinely EXECUTED result resolves a failure, i.e. kind 'normal'.
|
|
18
|
+
// Cache hits ('cache-hit' / 'scoped-cache-hit') replay an earlier result
|
|
19
|
+
// without running anything, and dedup/guard skips ('skipped') execute nothing
|
|
20
|
+
// either, so both are neutral: they neither arm nor clear the hook.
|
|
21
|
+
const EXECUTED_SUCCESS_TOOL_KINDS = new Set(['normal']);
|
|
22
|
+
|
|
23
|
+
function bareToolName(name) {
|
|
24
|
+
const raw = String(name || '');
|
|
25
|
+
return raw.startsWith('mcp__') ? raw.split('__').pop() : raw;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function toolFailureContinuationPrompt(failedTool) {
|
|
29
|
+
const which = failedTool ? `\`${failedTool}\`` : 'a tool call';
|
|
30
|
+
return `[mixdog-runtime] Stop hook: ${which} failed and no tool call has succeeded since.`
|
|
31
|
+
+ ' Re-run the fixed action (or a verifying tool) if it is recoverable; otherwise say in your'
|
|
32
|
+
+ ' final message that it stays unresolved and why. This hook fires once — your next message ends the turn.';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Structural unresolved-tool-failure stop hook.
|
|
37
|
+
* - observeToolResult(message) / endBatch(calls): a batch containing any
|
|
38
|
+
* failed tool result arms it; a later batch with a genuinely executed
|
|
39
|
+
* success clears it.
|
|
40
|
+
* - takeContinuationPrompt(): returns the continuation prompt the FIRST time
|
|
41
|
+
* a terminal assistant message arrives while a failure is unresolved, then
|
|
42
|
+
* stays silent for the rest of the turn (Codex `stop_hook_active`).
|
|
43
|
+
*/
|
|
44
|
+
export function createToolFailureStopHook() {
|
|
45
|
+
let unresolvedFailure = false;
|
|
46
|
+
let lastFailedTool = null;
|
|
47
|
+
let batchFailure = false;
|
|
48
|
+
let batchSuccess = false;
|
|
49
|
+
let failedCallIds = new Set();
|
|
50
|
+
let active = false;
|
|
51
|
+
return {
|
|
52
|
+
get unresolvedFailure() { return unresolvedFailure; },
|
|
53
|
+
get lastFailedTool() { return lastFailedTool; },
|
|
54
|
+
get active() { return active; },
|
|
55
|
+
observeToolResult(message) {
|
|
56
|
+
if (!message || message.role !== 'tool') return;
|
|
57
|
+
// Guard skips (repeat-failure guard) are tagged as errors so every
|
|
58
|
+
// downstream consumer keeps treating the call as unresolved, but
|
|
59
|
+
// nothing was dispatched — they must not arm the hook.
|
|
60
|
+
if (message.guardSkip === true) return;
|
|
61
|
+
if (message.toolKind === 'error') {
|
|
62
|
+
batchFailure = true;
|
|
63
|
+
if (message.toolCallId) failedCallIds.add(message.toolCallId);
|
|
64
|
+
} else if (EXECUTED_SUCCESS_TOOL_KINDS.has(message.toolKind)) {
|
|
65
|
+
batchSuccess = true;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
endBatch(calls) {
|
|
69
|
+
if (batchFailure) {
|
|
70
|
+
unresolvedFailure = true;
|
|
71
|
+
const failed = (Array.isArray(calls) ? calls : [])
|
|
72
|
+
.find((call) => call?.id && failedCallIds.has(call.id));
|
|
73
|
+
if (failed) lastFailedTool = bareToolName(failed.name);
|
|
74
|
+
} else if (batchSuccess) {
|
|
75
|
+
unresolvedFailure = false;
|
|
76
|
+
lastFailedTool = null;
|
|
77
|
+
}
|
|
78
|
+
batchFailure = false;
|
|
79
|
+
batchSuccess = false;
|
|
80
|
+
failedCallIds = new Set();
|
|
81
|
+
},
|
|
82
|
+
takeContinuationPrompt() {
|
|
83
|
+
if (active || !unresolvedFailure) return null;
|
|
84
|
+
active = true;
|
|
85
|
+
return toolFailureContinuationPrompt(lastFailedTool);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -24,6 +24,31 @@ export function isOutputLimitStopReason(reason) {
|
|
|
24
24
|
&& OUTPUT_LIMIT_STOP_REASONS.has(reason.trim().toLowerCase());
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Provider-declared continuation signals. A response carrying one of these is
|
|
28
|
+
// NOT a completed assistant turn even when it holds final-looking text: the
|
|
29
|
+
// provider explicitly stated it intends to keep sampling inside the SAME user
|
|
30
|
+
// turn. Two structured shapes are honored:
|
|
31
|
+
// - `endTurn === false` (Codex/Responses `end_turn=false`; see
|
|
32
|
+
// codex-rs/core/src/session/turn.rs Completed{ end_turn } → needs_follow_up)
|
|
33
|
+
// - Anthropic `stop_reason: 'pause_turn'` (server-side pause; the turn is
|
|
34
|
+
// resumed by replaying the transcript unchanged)
|
|
35
|
+
// Structural only — no lexical/progress-text heuristic is consulted, and a
|
|
36
|
+
// provider that omits both fields is unaffected (returns null → terminal).
|
|
37
|
+
const CONTINUATION_STOP_REASONS = new Set(['pause_turn']);
|
|
38
|
+
|
|
39
|
+
export function providerContinuationSignal(response) {
|
|
40
|
+
if (!response || typeof response !== 'object') return null;
|
|
41
|
+
// Only an explicit boolean false continues; undefined/null/absent means
|
|
42
|
+
// "provider did not signal" and keeps legacy terminal semantics.
|
|
43
|
+
if (response.endTurn === false || response.end_turn === false) return 'end_turn_false';
|
|
44
|
+
const stopReason = response.stopReason ?? response.stop_reason ?? null;
|
|
45
|
+
if (typeof stopReason === 'string'
|
|
46
|
+
&& CONTINUATION_STOP_REASONS.has(stopReason.trim().toLowerCase())) {
|
|
47
|
+
return 'pause_turn';
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
// Classify WHY the loop ended so agent-tool can promote an empty/abnormal
|
|
28
53
|
// finish to an explicit Lead-facing error instead of a silent empty
|
|
29
54
|
// "completed". Determine "has content" exactly the way the no-tool-call
|
|
@@ -179,7 +179,7 @@ export async function executeTool(name, args, cwd, callerSessionId, sessionRef,
|
|
|
179
179
|
if (name === 'code_graph') {
|
|
180
180
|
// cwd chain: args.cwd (caller-explicit) → session cwd → undefined (handler throws)
|
|
181
181
|
const graphCwd = (typeof args?.cwd === 'string' && args.cwd.trim()) ? args.cwd.trim() : cwd;
|
|
182
|
-
return executeCodeGraphToolLazy(name, args, graphCwd, null, toolOpts);
|
|
182
|
+
return executeCodeGraphToolLazy(name, args, graphCwd, executeOpts.signal || null, toolOpts);
|
|
183
183
|
}
|
|
184
184
|
if (isInternalTool(name)) {
|
|
185
185
|
// callerSessionId propagates into server.mjs dispatchTool so that
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// helper it used in manager.mjs is now imported from its split module.
|
|
6
6
|
import { createHash, randomUUID } from 'crypto';
|
|
7
7
|
import { getProvider } from '../../providers/registry.mjs';
|
|
8
|
+
import { readStreamOutcome } from '../../providers/lib/stream-outcome.mjs';
|
|
8
9
|
import { normalizeCompactType, DEFAULT_COMPACT_TYPE } from '../compact.mjs';
|
|
9
10
|
import { loadSession, saveSession, saveSessionAsync, saveSessionAsyncDeferred, readSessionLifecycleFromDisk } from '../store.mjs';
|
|
10
11
|
import { createAbortController } from '../../../../shared/abort-controller.mjs';
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
} from './message-sanitize.mjs';
|
|
58
59
|
import { createTurnInterruptionTracker } from './turn-interruption.mjs';
|
|
59
60
|
import {
|
|
61
|
+
cancelPendingTurnCheckpoint,
|
|
60
62
|
clearTurnCheckpoint,
|
|
61
63
|
recoverTurnCheckpoint,
|
|
62
64
|
turnMessagesForCheckpoint,
|
|
@@ -342,6 +344,13 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
342
344
|
currentUserContent: cancelledUserTurnContent,
|
|
343
345
|
turnMessages: turnMessagesForCheckpoint(_turnOutgoing, cancelledUserTurnContent),
|
|
344
346
|
interruption: _turnInterruption.snapshot(),
|
|
347
|
+
}, {
|
|
348
|
+
// The turn's first checkpoint is the crash-durability
|
|
349
|
+
// anchor for the prompt and must land before the provider
|
|
350
|
+
// runs; every later streaming/tool-boundary flush moves to
|
|
351
|
+
// the async latest-wins lane so serialization+disk never
|
|
352
|
+
// stall the engine thread mid-stream.
|
|
353
|
+
sync: _turnCheckpointLastAt === 0,
|
|
345
354
|
});
|
|
346
355
|
if (written) _turnCheckpointLastAt = Date.now();
|
|
347
356
|
return written;
|
|
@@ -373,6 +382,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
373
382
|
_turnCheckpointStopped = true;
|
|
374
383
|
if (_turnCheckpointTimer) clearTimeout(_turnCheckpointTimer);
|
|
375
384
|
_turnCheckpointTimer = null;
|
|
385
|
+
cancelPendingTurnCheckpoint(sessionId);
|
|
376
386
|
};
|
|
377
387
|
const _sessionStartMetaInjectedBeforeTurn = preSession.sessionStartMetaInjected === true;
|
|
378
388
|
let _interruptionSnapshot = null;
|
|
@@ -679,11 +689,31 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
679
689
|
if (Array.isArray(drained)) out.push(...drained);
|
|
680
690
|
} catch { /* best-effort steering drain */ }
|
|
681
691
|
}
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
// queued_command filters.
|
|
686
|
-
//
|
|
692
|
+
// Manager/pending-messages entries carry no
|
|
693
|
+
// mode/priority/slash metadata, so they stay OUT of the
|
|
694
|
+
// mid-chain (post-tool-batch) drain — that would bypass
|
|
695
|
+
// Claude Code's queued_command filters. At the TERMINAL
|
|
696
|
+
// boundary they are exactly Codex `has_pending_input`
|
|
697
|
+
// (turn.rs:304-318): an `agent type=send` queued while
|
|
698
|
+
// the terminal sample was in flight must be folded into
|
|
699
|
+
// THIS turn before any stop hook runs, instead of losing
|
|
700
|
+
// its slot to a synthetic continuation prompt. The mutex
|
|
701
|
+
// is held for the whole ask, so this drain races nothing.
|
|
702
|
+
// Entries consumed here join _turnPendingEntries: their
|
|
703
|
+
// delivery/ack (and release on failure) rides this turn,
|
|
704
|
+
// and the post-loop drain can no longer see them.
|
|
705
|
+
if (drainOptions?.stage === 'terminal') {
|
|
706
|
+
const _pendingNow = drainPendingMessages(sessionId);
|
|
707
|
+
if (_pendingNow.length > 0) {
|
|
708
|
+
const _mergedNow = _mergePendingMessageEntries(_pendingNow);
|
|
709
|
+
if (_mergedNow?.content) {
|
|
710
|
+
_turnPendingEntries.push(..._pendingNow);
|
|
711
|
+
out.push(_mergedNow);
|
|
712
|
+
} else {
|
|
713
|
+
releasePendingMessages(sessionId, _pendingNow);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
687
717
|
return out;
|
|
688
718
|
},
|
|
689
719
|
onSteerMessage: (text, detail) => {
|
|
@@ -1029,8 +1059,19 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1029
1059
|
// non-streaming request starts. If that restart fails, restore the
|
|
1030
1060
|
// tombstone and persist the one exposed partial as interruption
|
|
1031
1061
|
// history. Failed/absent acknowledgements never tombstoned it.
|
|
1062
|
+
// Provider-specific legacy flags are NOT consulted here: the
|
|
1063
|
+
// canonical stream-outcome contract answers "did the provider
|
|
1064
|
+
// produce output we must commit?" for every transport. Anthropic's
|
|
1065
|
+
// stall/truncation errors carry neither liveTextEmitted nor
|
|
1066
|
+
// unsafeToRetry — only partialContent/partialToolCalls — and used
|
|
1067
|
+
// to drop the streamed summary from history on that provider.
|
|
1068
|
+
const _providerOutcome = readStreamOutcome(err);
|
|
1032
1069
|
const preserveProviderPartial = restoredResetText
|
|
1033
|
-
||
|
|
1070
|
+
|| _providerOutcome.observedOutput === true
|
|
1071
|
+
// Positive exposure evidence only: an error with no observed
|
|
1072
|
+
// output must not be mistaken for a turn that produced
|
|
1073
|
+
// provider output.
|
|
1074
|
+
|| _providerOutcome.replayUnsafe === true
|
|
1034
1075
|
|| err?.unsafeToRetry === true;
|
|
1035
1076
|
let _errorStateDurable = false;
|
|
1036
1077
|
if (preserveProviderPartial && activeSession && _turnInterruption.hasResponseStarted()) {
|