mixdog 0.9.95 → 0.9.96
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/package.json +12 -9
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/shared/01-tool.md +20 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +26 -11
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -4
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +118 -18
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +23 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +87 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +29 -1
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +10 -10
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +2 -2
- package/src/runtime/channels/lib/inbound-handler.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/memory/index.mjs +5 -38
- package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +8 -0
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +60 -31
- package/src/runtime/shared/config.mjs +130 -44
- package/src/runtime/shared/resource-admission.mjs +146 -97
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-surface.mjs +0 -5
- package/src/runtime/shared/turn-snapshot.mjs +31 -5
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/runtime-core.mjs +11 -11
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/session-lifecycle.mjs +9 -2
- package/src/session-runtime/session-turn-api.mjs +41 -9
- package/src/session-runtime/settings-api.mjs +5 -17
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/backend-daemon.mjs +97 -36
- package/src/standalone/channel-admin.mjs +12 -12
- package/src/standalone/channel-daemon-client.mjs +17 -1
- package/src/standalone/channel-daemon-transport.mjs +90 -8
- package/src/standalone/engine-daemon-client.mjs +380 -205
- package/src/standalone/engine-daemon-protocol.mjs +3 -4
- package/src/standalone/engine-daemon-service.mjs +378 -134
- package/src/standalone/engine-daemon-transport.mjs +268 -77
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/session-protocol.mjs +188 -0
- package/src/tui/App.jsx +30 -17
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +1 -2
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +46 -41
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +6 -8
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +9 -2
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/dist/index.mjs +920 -1592
- package/src/tui/engine/live-share.mjs +34 -0
- package/src/tui/engine/session-api-ext.mjs +1 -4
- package/src/tui/engine/session-api.mjs +103 -33
- package/src/tui/engine/session-flow.mjs +7 -1
- package/src/tui/engine/tool-card-results.mjs +4 -2
- package/src/tui/engine/tool-result-text.mjs +10 -0
- package/src/tui/engine/turn.mjs +9 -69
- package/src/tui/engine-local-session.mjs +47 -31
- package/src/tui/index.jsx +1 -2
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Steering / pending-message queue with sync buffered + atomic-file persistence.
|
|
2
2
|
// Extracted verbatim from manager.mjs (behavior-preserving).
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { readFileSync
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { stat } from 'fs/promises';
|
|
5
6
|
import { createHash, randomBytes } from 'crypto';
|
|
6
7
|
import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
|
|
7
8
|
import { updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
|
|
@@ -20,8 +21,8 @@ const PENDING_ORPHAN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
20
21
|
const PENDING_ORPHAN_GRACE_MS = 60 * 60 * 1000;
|
|
21
22
|
// Replay window for genuine user/steering entries. A cross-surface submit is
|
|
22
23
|
// meant for a LIVE owner; entries that predate this process and exceeded the
|
|
23
|
-
// window are still DELIVERED (
|
|
24
|
-
//
|
|
24
|
+
// window are still DELIVERED (queued user input is never silently
|
|
25
|
+
// discarded), but annotated with an
|
|
25
26
|
// explicit late-delivery header so a session resumed hours later reads them
|
|
26
27
|
// as clearly-late input instead of a surprise self-injection.
|
|
27
28
|
const STALE_USER_INJECTION_TTL_MS = 30 * 60 * 1000;
|
|
@@ -44,7 +45,7 @@ function isStaleUserInjection(entry, now = Date.now()) {
|
|
|
44
45
|
return (now - enqueuedAt) > STALE_USER_INJECTION_TTL_MS;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
//
|
|
48
|
+
// Never silently discard queued user input. A stale entry is
|
|
48
49
|
// delivered with this explicit age-annotated header so neither the user nor
|
|
49
50
|
// the model mistakes it for fresh input.
|
|
50
51
|
function lateDeliveryText(text, entry, now = Date.now()) {
|
|
@@ -823,7 +824,7 @@ export function hydratePendingMessages(sessionId, options = {}) {
|
|
|
823
824
|
if (!id || inDelivery.has(id) || acked.has(id)) return false;
|
|
824
825
|
return true;
|
|
825
826
|
});
|
|
826
|
-
//
|
|
827
|
+
// Stale genuine user/steering entries DELIVER with a
|
|
827
828
|
// late-delivery header instead of being silently dropped (the old
|
|
828
829
|
// behavior discarded them; user report: remote/steering sends
|
|
829
830
|
// silently ignored around owner restarts). Completion entries
|
|
@@ -1092,8 +1093,14 @@ export function enqueueRemotePendingMessage(sessionId, message) {
|
|
|
1092
1093
|
// several sessions (desktop tabs, TUI + engine hosts) and a single shared
|
|
1093
1094
|
// counter let the first drain of a tick swallow the mtime bump for every
|
|
1094
1095
|
// other session, stranding their foreign submits until the next spool write.
|
|
1095
|
-
const FOREIGN_SPOOL_SCAN_LIMIT =
|
|
1096
|
+
const FOREIGN_SPOOL_SCAN_LIMIT = Math.max(
|
|
1097
|
+
128,
|
|
1098
|
+
Number(process.env.MIXDOG_FOREIGN_SPOOL_SCAN_LIMIT) || 512,
|
|
1099
|
+
);
|
|
1096
1100
|
const _foreignSpoolScanMtimes = new Map();
|
|
1101
|
+
const _foreignDrainRequests = new Map();
|
|
1102
|
+
let _foreignDrainScheduled = false;
|
|
1103
|
+
let _foreignDrainRunning = false;
|
|
1097
1104
|
|
|
1098
1105
|
function _rememberForeignSpoolScan(sessionId, mtime) {
|
|
1099
1106
|
_foreignSpoolScanMtimes.delete(sessionId);
|
|
@@ -1105,28 +1112,7 @@ function _rememberForeignSpoolScan(sessionId, mtime) {
|
|
|
1105
1112
|
}
|
|
1106
1113
|
}
|
|
1107
1114
|
|
|
1108
|
-
|
|
1109
|
-
* Owner-side drain of FOREIGN user injections for a session this process
|
|
1110
|
-
* owns: atomically removes (and returns the text of) genuine user/steering
|
|
1111
|
-
* entries that were persisted by ANOTHER process — entries known locally
|
|
1112
|
-
* (own steering buffers, hydrated, in-delivery, acked) and completion/
|
|
1113
|
-
* internal-notification entries are left untouched for the normal
|
|
1114
|
-
* askSession hydrate path.
|
|
1115
|
-
*/
|
|
1116
|
-
export async function drainForeignUserInjections(sessionId) {
|
|
1117
|
-
if (!isValidPendingSessionId(sessionId)) return [];
|
|
1118
|
-
// DELIVERY path, not cleanup: this takes rows out of the spool and hands
|
|
1119
|
-
// them to a live turn. A tombstoned (or otherwise invalidated) session may
|
|
1120
|
-
// neither receive nor lose input, so the tombstone-TOLERANT epoch check
|
|
1121
|
-
// used by the explicit cleanup mutations (ack / clear) is wrong here — the
|
|
1122
|
-
// token is refused outright when it was captured from a closed session.
|
|
1123
|
-
// Checked BEFORE the mtime memo so a later reopen still sees this spool
|
|
1124
|
-
// state as unscanned.
|
|
1125
|
-
const epochToken = currentPendingLifecycleToken(sessionId);
|
|
1126
|
-
if (pendingLifecycleInvalidated(sessionId)) return [];
|
|
1127
|
-
let mtime = 0;
|
|
1128
|
-
try { mtime = statSync(pendingMessagesPath()).mtimeMs || 0; } catch { return []; }
|
|
1129
|
-
if (_foreignSpoolScanMtimes.get(sessionId) === mtime) return [];
|
|
1115
|
+
function _foreignLocalIds(sessionId) {
|
|
1130
1116
|
const localIds = new Set();
|
|
1131
1117
|
for (const map of [_sessionPendingMessages, _pendingPersistBuffers, _hydratedPendingMessages]) {
|
|
1132
1118
|
for (const entry of map.get(sessionId) || []) {
|
|
@@ -1146,59 +1132,86 @@ export async function drainForeignUserInjections(sessionId) {
|
|
|
1146
1132
|
if (typeof id === 'string' && id) localIds.add(id);
|
|
1147
1133
|
}
|
|
1148
1134
|
} catch { /* ledger unavailable — in-memory sets still guard */ }
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1135
|
+
return localIds;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function _settleForeignDrain(request, value) {
|
|
1139
|
+
for (const resolve of request.waiters) {
|
|
1140
|
+
try { resolve(value); } catch { /* a consumer cannot break the batch */ }
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function _scheduleForeignDrainBatch() {
|
|
1145
|
+
if (_foreignDrainScheduled || _foreignDrainRunning || _foreignDrainRequests.size === 0) return;
|
|
1146
|
+
_foreignDrainScheduled = true;
|
|
1147
|
+
setImmediate(() => {
|
|
1148
|
+
_foreignDrainScheduled = false;
|
|
1149
|
+
void _flushForeignDrainBatch();
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function _flushForeignDrainBatch() {
|
|
1154
|
+
if (_foreignDrainRunning) return;
|
|
1155
|
+
_foreignDrainRunning = true;
|
|
1156
|
+
const batch = [..._foreignDrainRequests.values()];
|
|
1157
|
+
_foreignDrainRequests.clear();
|
|
1158
1158
|
try {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1159
|
+
let mtime = 0;
|
|
1160
|
+
try { mtime = (await stat(pendingMessagesPath())).mtimeMs || 0; }
|
|
1161
|
+
catch {
|
|
1162
|
+
for (const request of batch) _settleForeignDrain(request, []);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const candidates = batch.filter((request) =>
|
|
1166
|
+
_foreignSpoolScanMtimes.get(request.sessionId) !== mtime
|
|
1167
|
+
&& !pendingLifecycleInvalidated(request.sessionId, request.epochToken));
|
|
1168
|
+
for (const request of batch) {
|
|
1169
|
+
if (!candidates.includes(request)) _settleForeignDrain(request, []);
|
|
1170
|
+
}
|
|
1171
|
+
if (candidates.length === 0) return;
|
|
1172
|
+
for (const request of candidates) {
|
|
1173
|
+
request.localIds = _foreignLocalIds(request.sessionId);
|
|
1174
|
+
request.taken = [];
|
|
1175
|
+
request.lifecycleDecided = false;
|
|
1176
|
+
}
|
|
1165
1177
|
await updateJsonAtomic(pendingMessagesPath(), (raw) => {
|
|
1166
|
-
// Same authority re-read INSIDE the spool lock: a close/detach
|
|
1167
|
-
// landing in this window must neither deliver to the old owner nor
|
|
1168
|
-
// remove the reopened owner's rows.
|
|
1169
|
-
runPendingTestHook('foreignDrain:beforeCommit', { sessionId });
|
|
1170
|
-
if (pendingLifecycleInvalidated(sessionId, epochToken)) return undefined;
|
|
1171
|
-
// Authoritative from here on: whatever this pass decides (take,
|
|
1172
|
-
// drop-stale, or "nothing foreign here") is a real answer for THIS
|
|
1173
|
-
// spool state, so the memo may skip re-scanning it.
|
|
1174
|
-
lifecycleDecided = true;
|
|
1175
1178
|
const next = normalizePendingStore(raw);
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1179
|
+
let changed = false;
|
|
1180
|
+
for (const request of candidates) {
|
|
1181
|
+
const { sessionId, epochToken, localIds, taken } = request;
|
|
1182
|
+
// Same authority re-read INSIDE the spool lock: a close/detach
|
|
1183
|
+
// landing in this window must neither deliver to the old owner
|
|
1184
|
+
// nor remove the reopened owner's rows.
|
|
1185
|
+
runPendingTestHook('foreignDrain:beforeCommit', { sessionId });
|
|
1186
|
+
if (pendingLifecycleInvalidated(sessionId, epochToken)) continue;
|
|
1187
|
+
request.lifecycleDecided = true;
|
|
1188
|
+
const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
|
|
1189
|
+
if (q.length === 0) continue;
|
|
1190
|
+
const kept = [];
|
|
1191
|
+
for (const entry of q) {
|
|
1192
|
+
const id = pendingMessageId(entry);
|
|
1193
|
+
const text = pendingMessageText(entry);
|
|
1194
|
+
const foreignUser = id && !localIds.has(id)
|
|
1195
|
+
&& !isCompletionNotificationEntry(entry)
|
|
1196
|
+
&& !isLegacyUnmarkedCompletionNotification(text)
|
|
1197
|
+
&& text && !isInternalRuntimeNotificationText(text);
|
|
1198
|
+
if (foreignUser && isStaleUserInjection(entry)) {
|
|
1199
|
+
taken.push({ text: lateDeliveryText(text, entry), id });
|
|
1200
|
+
} else if (foreignUser) {
|
|
1201
|
+
taken.push({ text, id });
|
|
1202
|
+
} else {
|
|
1203
|
+
kept.push(entry);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (taken.length === 0) continue;
|
|
1207
|
+
changed = true;
|
|
1208
|
+
if (kept.length > 0) next.sessions[sessionId] = kept;
|
|
1209
|
+
else {
|
|
1210
|
+
delete next.sessions[sessionId];
|
|
1211
|
+
if (next.sessionTouchedAt) delete next.sessionTouchedAt[sessionId];
|
|
1192
1212
|
}
|
|
1193
|
-
else if (foreignUser) taken.push({ text, id });
|
|
1194
|
-
else kept.push(entry);
|
|
1195
|
-
}
|
|
1196
|
-
if (taken.length === 0) return undefined;
|
|
1197
|
-
if (kept.length > 0) next.sessions[sessionId] = kept;
|
|
1198
|
-
else {
|
|
1199
|
-
delete next.sessions[sessionId];
|
|
1200
|
-
if (next.sessionTouchedAt) delete next.sessionTouchedAt[sessionId];
|
|
1201
1213
|
}
|
|
1214
|
+
if (!changed) return undefined;
|
|
1202
1215
|
next.updatedAt = Date.now();
|
|
1203
1216
|
return next;
|
|
1204
1217
|
}, {
|
|
@@ -1206,18 +1219,63 @@ export async function drainForeignUserInjections(sessionId) {
|
|
|
1206
1219
|
lock: true,
|
|
1207
1220
|
mode: PENDING_MESSAGES_MODE,
|
|
1208
1221
|
fsync: false,
|
|
1209
|
-
// Delivery polling must never wait
|
|
1210
|
-
//
|
|
1211
|
-
// from ever reaching its finally/release path.
|
|
1222
|
+
// Delivery polling must never wait behind a spool writer. A later
|
|
1223
|
+
// fs.watch/poll tick retries unchanged requests.
|
|
1212
1224
|
timeoutMs: 0,
|
|
1213
1225
|
});
|
|
1226
|
+
for (const request of candidates) {
|
|
1227
|
+
if (request.lifecycleDecided) _rememberForeignSpoolScan(request.sessionId, mtime);
|
|
1228
|
+
_settleForeignDrain(request, request.taken);
|
|
1229
|
+
}
|
|
1214
1230
|
} catch (err) {
|
|
1215
|
-
if (err?.code
|
|
1216
|
-
|
|
1217
|
-
|
|
1231
|
+
if (err?.code !== 'ELOCKCONTENDED') {
|
|
1232
|
+
try { process.stderr.write(`[session] foreign-injection drain failed: ${err?.message || err}\n`); } catch {}
|
|
1233
|
+
}
|
|
1234
|
+
for (const request of batch) {
|
|
1235
|
+
if (request.waiters.length > 0) _settleForeignDrain(request, []);
|
|
1236
|
+
}
|
|
1237
|
+
} finally {
|
|
1238
|
+
_foreignDrainRunning = false;
|
|
1239
|
+
_scheduleForeignDrainBatch();
|
|
1218
1240
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Owner-side drain of FOREIGN user injections for a session this process
|
|
1245
|
+
* owns: atomically removes (and returns the text of) genuine user/steering
|
|
1246
|
+
* entries that were persisted by ANOTHER process — entries known locally
|
|
1247
|
+
* (own steering buffers, hydrated, in-delivery, acked) and completion/
|
|
1248
|
+
* internal-notification entries are left untouched for the normal
|
|
1249
|
+
* askSession hydrate path.
|
|
1250
|
+
*/
|
|
1251
|
+
export async function drainForeignUserInjections(sessionId) {
|
|
1252
|
+
if (!isValidPendingSessionId(sessionId)) return [];
|
|
1253
|
+
// DELIVERY path, not cleanup: this takes rows out of the spool and hands
|
|
1254
|
+
// them to a live turn. A tombstoned (or otherwise invalidated) session may
|
|
1255
|
+
// neither receive nor lose input, so the tombstone-TOLERANT epoch check
|
|
1256
|
+
// used by the explicit cleanup mutations (ack / clear) is wrong here — the
|
|
1257
|
+
// token is refused outright when it was captured from a closed session.
|
|
1258
|
+
// Checked BEFORE the mtime memo so a later reopen still sees this spool
|
|
1259
|
+
// state as unscanned.
|
|
1260
|
+
const epochToken = currentPendingLifecycleToken(sessionId);
|
|
1261
|
+
if (pendingLifecycleInvalidated(sessionId)) return [];
|
|
1262
|
+
return new Promise((resolve) => {
|
|
1263
|
+
const existing = _foreignDrainRequests.get(sessionId);
|
|
1264
|
+
if (existing && existing.epochToken === epochToken) {
|
|
1265
|
+
existing.waiters.push(resolve);
|
|
1266
|
+
} else {
|
|
1267
|
+
if (existing) _settleForeignDrain(existing, []);
|
|
1268
|
+
_foreignDrainRequests.set(sessionId, {
|
|
1269
|
+
sessionId,
|
|
1270
|
+
epochToken,
|
|
1271
|
+
waiters: [resolve],
|
|
1272
|
+
localIds: null,
|
|
1273
|
+
taken: [],
|
|
1274
|
+
lifecycleDecided: false,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
_scheduleForeignDrainBatch();
|
|
1278
|
+
});
|
|
1221
1279
|
}
|
|
1222
1280
|
|
|
1223
1281
|
export function drainPendingMessages(sessionId) {
|
|
@@ -17,6 +17,10 @@ export async function _getAgentLoop() {
|
|
|
17
17
|
if (typeof mod.agentLoop !== 'function') throw new Error('agent loop runtime is not available');
|
|
18
18
|
return mod.agentLoop;
|
|
19
19
|
}
|
|
20
|
+
export async function prewarmAgentLoop() {
|
|
21
|
+
await _getAgentLoop();
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
20
24
|
export function _closeBashSessionLazy(sessionId, reason) {
|
|
21
25
|
if (!sessionId) return;
|
|
22
26
|
_bashSessionRuntimePromise ??= import('../../tools/bash-session.mjs');
|
|
@@ -6,7 +6,7 @@ import { loadSession, saveSessionAsync, setLiveSession, evictLiveSession, listSt
|
|
|
6
6
|
import { estimateMessagesTokens, estimateTranscriptContextUsage } from '../context-utils.mjs';
|
|
7
7
|
import { normalizeCompactType, DEFAULT_COMPACT_TYPE, SUMMARY_PREFIX } from '../compact.mjs';
|
|
8
8
|
import { runSessionCompaction } from './compaction-runner.mjs';
|
|
9
|
-
import { hasUserConversationMessage } from './prompt-utils.mjs';
|
|
9
|
+
import { hasUserConversationMessage, promptContentText } from './prompt-utils.mjs';
|
|
10
10
|
import { getProvider } from '../../providers/registry.mjs';
|
|
11
11
|
import { isSessionCompactionBlocked, getSessionAbortSignal, _runtimeEntries } from './runtime-liveness.mjs';
|
|
12
12
|
import { mintSessionId } from './session-id.mjs';
|
|
@@ -314,3 +314,33 @@ export async function updateSessionStatus(id, status) {
|
|
|
314
314
|
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
315
315
|
return true;
|
|
316
316
|
}
|
|
317
|
+
|
|
318
|
+
// --- Rewind to a previous user message (message selector) ---
|
|
319
|
+
// Claude Code's "restore conversation": the selected prompt and everything
|
|
320
|
+
// after it leave the model history so the user can edit and resubmit it.
|
|
321
|
+
// Earlier turns — including any compaction rewrite that precedes them — stay
|
|
322
|
+
// exactly as they are. Idle-only; the caller interrupts a live turn first.
|
|
323
|
+
export async function rewindSessionMessagesTo(sessionId, options = {}) {
|
|
324
|
+
const session = loadSession(sessionId);
|
|
325
|
+
if (!session || session.closed === true) return null;
|
|
326
|
+
const target = String(options?.text ?? '').trim();
|
|
327
|
+
if (!target) return null;
|
|
328
|
+
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
329
|
+
let start = -1;
|
|
330
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
331
|
+
const message = messages[index];
|
|
332
|
+
if (message?.role !== 'user') continue;
|
|
333
|
+
if (promptContentText(message.content).trim() !== target) continue;
|
|
334
|
+
start = index;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
if (start < 0) return null;
|
|
338
|
+
const removed = messages.length - start;
|
|
339
|
+
session.messages = messages.slice(0, start);
|
|
340
|
+
// The provider cache is keyed on the exact prefix we just truncated.
|
|
341
|
+
session.providerState = undefined;
|
|
342
|
+
session.updatedAt = Date.now();
|
|
343
|
+
session.lastUsedAt = Date.now();
|
|
344
|
+
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
345
|
+
return { removed, remaining: session.messages.length };
|
|
346
|
+
}
|
|
@@ -41,18 +41,25 @@ function assistantToolCallIds(message) {
|
|
|
41
41
|
return ids;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function
|
|
44
|
+
function provisionalUserTurnIndex(messages, currentUserContent) {
|
|
45
45
|
const currentText = promptContentText(currentUserContent);
|
|
46
46
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
47
47
|
const message = messages[i];
|
|
48
48
|
if (message?.role !== 'user') continue;
|
|
49
49
|
if (message.content === currentUserContent
|
|
50
50
|
|| promptContentText(message.content) === currentText) {
|
|
51
|
+
return i;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return -1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function rewindProvisionalUserTurn(messages, currentUserContent) {
|
|
58
|
+
const index = provisionalUserTurnIndex(messages, currentUserContent);
|
|
59
|
+
if (index >= 0) {
|
|
51
60
|
// Remove the provisional user turn plus pre-send hooks appended
|
|
52
61
|
// after it, while retaining any compaction rewrite before it.
|
|
53
|
-
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
62
|
+
messages.splice(index);
|
|
56
63
|
}
|
|
57
64
|
return messages;
|
|
58
65
|
}
|
|
@@ -69,13 +76,26 @@ function finalizeInterruptedTurn({
|
|
|
69
76
|
abortReason,
|
|
70
77
|
}) {
|
|
71
78
|
const messages = filterModelVisibleSessionMessages(turnOutgoing).slice();
|
|
79
|
+
// Null/unknown reasons keep the legacy rewind (status quo for wrapped
|
|
80
|
+
// aborts without a closeReason enum); named non-user reasons preserve.
|
|
81
|
+
const userCancelled = abortReason == null
|
|
82
|
+
|| USER_CANCEL_ABORT_REASONS.has(abortReason);
|
|
83
|
+
// Claude Code parity: a turn cancelled while the model was only THINKING
|
|
84
|
+
// produced no model-visible message, so the provisional user turn is
|
|
85
|
+
// rewound and the prompt goes back to the input box. Anything the model
|
|
86
|
+
// actually said or did — committed messages from this turn, buffered text,
|
|
87
|
+
// tool calls/results — keeps the turn (reasoning alone does not).
|
|
88
|
+
const producedOutput = String(partialAssistantContent || '').trim() !== ''
|
|
89
|
+
|| observedToolCalls.size > 0
|
|
90
|
+
|| observedToolResults.size > 0
|
|
91
|
+
|| (() => {
|
|
92
|
+
const index = provisionalUserTurnIndex(messages, currentUserContent);
|
|
93
|
+
return index >= 0 && index < messages.length - 1;
|
|
94
|
+
})();
|
|
72
95
|
const preserveResponse = responseStarted
|
|
73
|
-
&& !isInternalRuntimeNotificationText(currentUserContent)
|
|
96
|
+
&& !isInternalRuntimeNotificationText(currentUserContent)
|
|
97
|
+
&& (producedOutput || !userCancelled);
|
|
74
98
|
if (!preserveResponse) {
|
|
75
|
-
// Null/unknown reasons keep the legacy rewind (status quo for wrapped
|
|
76
|
-
// aborts without a closeReason enum); named non-user reasons preserve.
|
|
77
|
-
const userCancelled = abortReason == null
|
|
78
|
-
|| USER_CANCEL_ABORT_REASONS.has(abortReason);
|
|
79
99
|
if (!userCancelled) {
|
|
80
100
|
return {
|
|
81
101
|
messages,
|
|
@@ -101,6 +101,7 @@ export {
|
|
|
101
101
|
// ── Session lifecycle / ask / crud / close / cleanup ──────────────────────
|
|
102
102
|
export { SessionClosedError } from './manager/session-errors.mjs';
|
|
103
103
|
export { setAgentRuntime } from './manager/agent-runtime-singleton.mjs';
|
|
104
|
+
export { prewarmAgentLoop } from './manager/runtime-loaders.mjs';
|
|
104
105
|
export {
|
|
105
106
|
createSession,
|
|
106
107
|
updateSessionRoute,
|
|
@@ -120,6 +121,7 @@ export {
|
|
|
120
121
|
listSessions,
|
|
121
122
|
findSessionByScopeKey,
|
|
122
123
|
clearSessionMessages,
|
|
124
|
+
rewindSessionMessagesTo,
|
|
123
125
|
compactSessionMessages,
|
|
124
126
|
updateSessionStatus,
|
|
125
127
|
updateSessionGeneratedTitle,
|
|
@@ -96,3 +96,68 @@ export function isInformationalShellExitOne(result) {
|
|
|
96
96
|
if (payload.startsWith('[stderr') || payload.includes('\n[stderr')) return false;
|
|
97
97
|
return true;
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Shell-result outcome test. The shell tool reports failure through its OWN
|
|
102
|
+
* status markers (bash-tool.mjs `_shellFailureStatus`): `Error:
|
|
103
|
+
* [shell-run-failed] …`, `Error: [shell-tool-failed] …`, or a bare
|
|
104
|
+
* `[exit code: N]` / `[timeout: …]` / `[signal: …]` marker — optionally behind
|
|
105
|
+
* ⚠️ destructive-warning lines, which are prepended AFTER the error prefix is
|
|
106
|
+
* composed. A generic leading `Error:` is NOT a shell failure: it is just as
|
|
107
|
+
* likely the command's own stdout, which is why classifyResultKind (correct
|
|
108
|
+
* for tools that own the `Error:` convention) over-reports on shell output.
|
|
109
|
+
*
|
|
110
|
+
* @param {unknown} result
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
export function isShellFailureResult(result) {
|
|
114
|
+
if (typeof result !== 'string') return false;
|
|
115
|
+
const body = result.replace(/^(?:\s*⚠️[^\n]*\n)+/, '').trimStart();
|
|
116
|
+
if (/^error:\s*\[shell-(?:run|tool)-failed\]/i.test(body)) return true;
|
|
117
|
+
return /^\[(?:exit code:|timeout:|signal:)/i.test(body);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Evidence that a command's OUTPUT reports a real failure. Used to separate a
|
|
121
|
+
// legitimate non-zero exit (a probe or report that prints its result and ends
|
|
122
|
+
// with 1) from a genuine failure that happens to write only to stdout — test
|
|
123
|
+
// runners, compilers and package managers all announce themselves in the text.
|
|
124
|
+
// Deliberately literal: only well-known failure banners count, so an unknown
|
|
125
|
+
// tool's report is never mislabelled a failure.
|
|
126
|
+
const SHELL_OUTPUT_FAILURE_EVIDENCE = [
|
|
127
|
+
/^not ok \d/m,
|
|
128
|
+
/^# fail [1-9]/m,
|
|
129
|
+
/\bAssertionError\b/,
|
|
130
|
+
/\bTraceback \(most recent call last\)/,
|
|
131
|
+
/^npm ERR!/m,
|
|
132
|
+
/\berror TS\d+\b/,
|
|
133
|
+
// Indented banners count too: runners print " FAIL <case>" under a header.
|
|
134
|
+
/^\s*FAIL(?:ED|URE)?\b/mi,
|
|
135
|
+
/\bFAILED[:!]/,
|
|
136
|
+
/^\s*✗/m,
|
|
137
|
+
/\b\d+ (?:tests? )?failed\b/i,
|
|
138
|
+
/^fatal:/m,
|
|
139
|
+
/^error:/mi,
|
|
140
|
+
/\bSyntaxError\b|\bReferenceError\b|\bTypeError\b/,
|
|
141
|
+
/\bCannot find module\b/,
|
|
142
|
+
];
|
|
143
|
+
export function shellOutputReportsFailure(text) {
|
|
144
|
+
const body = String(text ?? '');
|
|
145
|
+
if (!body.trim()) return false;
|
|
146
|
+
return SHELL_OUTPUT_FAILURE_EVIDENCE.some((pattern) => pattern.test(body));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Classify a non-zero shell exit as a real failure or a legitimate report.
|
|
151
|
+
* Legitimate: exit code 1 exactly, no signal, NOTHING on stderr, real output
|
|
152
|
+
* on stdout, and no failure banner in that output. Everything else — any other
|
|
153
|
+
* exit code, a signal, any stderr, an empty capture, or a failure banner —
|
|
154
|
+
* stays a failure.
|
|
155
|
+
*/
|
|
156
|
+
export function isLegitimateShellExit({ exitCode, signal, stdout, stderr } = {}) {
|
|
157
|
+
if (signal) return false;
|
|
158
|
+
if (exitCode !== 1) return false;
|
|
159
|
+
if (String(stderr ?? '').trim()) return false;
|
|
160
|
+
const out = String(stdout ?? '').trim();
|
|
161
|
+
if (!out || out === '(no output)') return false;
|
|
162
|
+
return !shellOutputReportsFailure(out);
|
|
163
|
+
}
|