mixdog 0.9.87 → 0.9.89
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 +29 -35
- 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 +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- 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/stored-tool-args.mjs +50 -8
- 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/builtin-tools.mjs +5 -5
- 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-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- 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 +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -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 +73 -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/explore-tool.mjs +1 -1
- 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/route-pickers.mjs +11 -117
- 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 +243 -223
- 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 +5 -3
- 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
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { renameWithRetrySync } from '../../../../shared/atomic-file.mjs';
|
|
4
4
|
import { sanitizeContentForStoredHistory } from '../../providers/media-normalization.mjs';
|
|
5
|
+
import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from '../lifecycle-scan.mjs';
|
|
5
6
|
|
|
6
7
|
// The live in-memory session (and every model request)
|
|
7
8
|
// retains attached image bytes across turns so multi-turn recognition works.
|
|
@@ -9,6 +10,21 @@ import { sanitizeContentForStoredHistory } from '../../providers/media-normaliza
|
|
|
9
10
|
// text placeholder at serialization time — keeping session files small without
|
|
10
11
|
// starving the model of the image mid-conversation. Returns the same object
|
|
11
12
|
// reference when nothing changed (no-image sessions pay only a shallow scan).
|
|
13
|
+
// Per-message disk projection shared by _sessionForDisk and the save-worker
|
|
14
|
+
// delta path (tail-only projection). Returns the SAME array reference when no
|
|
15
|
+
// message changed. Idempotent: already-sanitized content passes through
|
|
16
|
+
// unchanged, so projecting a projected message is a no-op.
|
|
17
|
+
export function _messagesForDisk(messages) {
|
|
18
|
+
let changed = false;
|
|
19
|
+
const out = messages.map((m) => {
|
|
20
|
+
if (!m || typeof m !== 'object') return m;
|
|
21
|
+
const content = sanitizeContentForStoredHistory(m.content);
|
|
22
|
+
if (content !== m.content) { changed = true; return { ...m, content }; }
|
|
23
|
+
return m;
|
|
24
|
+
});
|
|
25
|
+
return changed ? out : messages;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
export function _sessionForDisk(session) {
|
|
13
29
|
// Strip transient in-flight aliases askSession sets for the turn duration:
|
|
14
30
|
// - liveTurnMessages: live working transcript (so contextStatus() can
|
|
@@ -26,14 +42,8 @@ export function _sessionForDisk(session) {
|
|
|
26
42
|
const { liveTurnMessages: _dropLTM, toolApprovalHook: _dropTAH, ...rest } = session;
|
|
27
43
|
return rest;
|
|
28
44
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!m || typeof m !== 'object') return m;
|
|
32
|
-
const content = sanitizeContentForStoredHistory(m.content);
|
|
33
|
-
if (content !== m.content) { changed = true; return { ...m, content }; }
|
|
34
|
-
return m;
|
|
35
|
-
});
|
|
36
|
-
if (!changed) {
|
|
45
|
+
const out = _messagesForDisk(messages);
|
|
46
|
+
if (out === messages) {
|
|
37
47
|
if (!hasTransient) return session;
|
|
38
48
|
const { liveTurnMessages: _dropLTM, toolApprovalHook: _dropTAH, ...rest } = session;
|
|
39
49
|
return rest;
|
|
@@ -58,13 +68,36 @@ export function _ensureLifecycleFields(session) {
|
|
|
58
68
|
return session;
|
|
59
69
|
}
|
|
60
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Sentinel for "the file is THERE and could not be read" (EIO/EACCES/EBUSY…),
|
|
73
|
+
* as opposed to `null` = "read fine, but the record is corrupt/foreign".
|
|
74
|
+
* Callers that own retention (summary scan) must never treat the former as an
|
|
75
|
+
* empty/invalid row.
|
|
76
|
+
*/
|
|
77
|
+
export const STORED_SESSION_UNREADABLE = Symbol('stored-session-unreadable');
|
|
78
|
+
|
|
61
79
|
export function _storedSessionFromFile(dir, filename, ensureLifecycle = true) {
|
|
62
80
|
if (!filename.endsWith('.json')) return null;
|
|
63
81
|
const storageId = filename.slice(0, -5);
|
|
64
82
|
if (!storageId || !/^[A-Za-z0-9_-]+$/.test(storageId)) return null;
|
|
83
|
+
let text;
|
|
84
|
+
try {
|
|
85
|
+
text = readFileSync(join(dir, filename), 'utf-8');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const code = err?.code || 'EUNKNOWN';
|
|
88
|
+
// Only ENOENT/ENOTDIR is absence; everything else is present-but-
|
|
89
|
+
// unreadable and must fail closed for retention decisions.
|
|
90
|
+
return (code === 'ENOENT' || code === 'ENOTDIR') ? null : STORED_SESSION_UNREADABLE;
|
|
91
|
+
}
|
|
65
92
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
// One strict authority for identity: a plain JSON.parse resolves a
|
|
94
|
+
// duplicate top-level `id` LAST-WINS, so `{"id":"other",…,"id":"me"}`
|
|
95
|
+
// would be listed (and later acted on) as this file's own session.
|
|
96
|
+
// Ambiguous/malformed records are treated exactly like corrupt ones:
|
|
97
|
+
// the caller keeps owning the identity but gets no session object.
|
|
98
|
+
const record = readTopLevelLifecycleRecord(text);
|
|
99
|
+
if (isLifecycleUnreadable(record) || record.id !== storageId) return null;
|
|
100
|
+
const session = record.doc;
|
|
68
101
|
return ensureLifecycle ? _ensureLifecycleFields(session) : session;
|
|
69
102
|
} catch {
|
|
70
103
|
return null;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { readdirSync
|
|
1
|
+
import { readdirSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { getPluginData } from '../../config.mjs';
|
|
4
4
|
import { getStoreDir } from './paths-heartbeat.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { probePath, PROBE_PRESENT, PROBE_ABSENT } from './fs-probe.mjs';
|
|
6
|
+
import { STORED_SESSION_UNREADABLE, _storedSessionFromFile } from './serialize.mjs';
|
|
6
7
|
import { _sessionSummary, _normalizeSummaryIndex, _upsertSessionSummaryRow, _removeSessionSummary, _pruneSummaryIndexIds } from '../store-summary-index.mjs';
|
|
7
8
|
|
|
8
9
|
// Listing is much hotter than writing, especially while the desktop session
|
|
@@ -108,6 +109,17 @@ export function _rollbackCachedSessionSummary(id, version) {
|
|
|
108
109
|
export function _queueSessionSummaryUpsert(session, version = null) {
|
|
109
110
|
const latest = version === null ? null : _summaryCacheLatestRows.get(session?.id);
|
|
110
111
|
const row = latest?.version === version ? latest.row : _sessionSummary(session);
|
|
112
|
+
return _queueSessionSummaryUpsertRow(row, version);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Publish an ALREADY-CAPTURED row. Async writers snapshot the row at the
|
|
117
|
+
* moment their payload is built and publish THAT snapshot when the write
|
|
118
|
+
* lands, so an older completion can never republish metadata read from the
|
|
119
|
+
* (since-mutated) live session — the row always describes the bytes that
|
|
120
|
+
* actually reached disk.
|
|
121
|
+
*/
|
|
122
|
+
export function _queueSessionSummaryUpsertRow(row, version = null) {
|
|
111
123
|
if (!row) return;
|
|
112
124
|
_setCachedBaseSummary(row);
|
|
113
125
|
if (version === null || (_summaryCacheVersions.get(row.id) || 0) === version) {
|
|
@@ -141,11 +153,22 @@ const _summaryScanCache = new Map(); // filename → { mtimeMs, size, row|null }
|
|
|
141
153
|
|
|
142
154
|
export function _scanStoredSessionSummaryRows() {
|
|
143
155
|
const dir = getStoreDir();
|
|
144
|
-
|
|
156
|
+
const dirProbe = probePath(dir);
|
|
157
|
+
if (dirProbe.state === PROBE_ABSENT) {
|
|
145
158
|
const changed = _summaryScanCache.size > 0;
|
|
146
159
|
_summaryScanCache.clear();
|
|
147
160
|
return { rows: [], invalidStorageIds: new Set(), changed };
|
|
148
161
|
}
|
|
162
|
+
if (dirProbe.state !== PROBE_PRESENT) {
|
|
163
|
+
// The store dir exists as far as we know and simply cannot be probed
|
|
164
|
+
// (EACCES/EIO). Reporting "no sessions" here would let the caller
|
|
165
|
+
// rewrite the sidecar index from an empty scan — i.e. delete every
|
|
166
|
+
// row because of a transient stat error. Fail loudly instead; the
|
|
167
|
+
// refresh caller treats a throw as "storage not enumerable".
|
|
168
|
+
const err = new Error(`[session-store] session dir not readable (${dirProbe.code})`);
|
|
169
|
+
err.code = dirProbe.code;
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
149
172
|
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
150
173
|
const present = new Set(files);
|
|
151
174
|
let changed = false;
|
|
@@ -162,21 +185,43 @@ export function _scanStoredSessionSummaryRows() {
|
|
|
162
185
|
if (/^[A-Za-z0-9_-]+$/.test(storageId)) invalidStorageIds.add(storageId);
|
|
163
186
|
};
|
|
164
187
|
for (const f of files) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
188
|
+
const fileProbe = probePath(join(dir, f));
|
|
189
|
+
// Proven absence only: the file was deleted mid-scan, so its cached
|
|
190
|
+
// row legitimately retires.
|
|
191
|
+
if (fileProbe.state === PROBE_ABSENT) {
|
|
168
192
|
if (_summaryScanCache.delete(f)) changed = true;
|
|
169
193
|
continue;
|
|
170
194
|
}
|
|
195
|
+
if (fileProbe.state !== PROBE_PRESENT) {
|
|
196
|
+
// Unreadable probe (EACCES/EIO/EBUSY): NOT a deletion. Keep the
|
|
197
|
+
// cache entry exactly as it is — `changed` stays untouched so no
|
|
198
|
+
// sidecar rewrite can drop this row — and keep serving the last
|
|
199
|
+
// known row. With no cached row the id is marked invalid instead,
|
|
200
|
+
// so no pending/live overlay may stand in for the unreadable file.
|
|
201
|
+
const stale = _summaryScanCache.get(f);
|
|
202
|
+
if (stale?.row) rows.push(stale.row);
|
|
203
|
+
else markInvalid(f);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
171
206
|
const cached = _summaryScanCache.get(f);
|
|
172
|
-
if (cached && cached.mtimeMs ===
|
|
207
|
+
if (cached && cached.mtimeMs === fileProbe.mtimeMs && cached.size === fileProbe.size) {
|
|
173
208
|
if (cached.row) rows.push(cached.row);
|
|
174
209
|
else markInvalid(f);
|
|
175
210
|
continue;
|
|
176
211
|
}
|
|
177
212
|
const session = _storedSessionFromFile(dir, f);
|
|
213
|
+
if (session === STORED_SESSION_UNREADABLE) {
|
|
214
|
+
// Stat said PRESENT, the READ failed: identical contract to an
|
|
215
|
+
// unreadable probe. Retain the last known row, leave `changed`
|
|
216
|
+
// untouched (no sidecar rewrite may drop it) and never cache a
|
|
217
|
+
// null row for a file we could not read.
|
|
218
|
+
const stale = _summaryScanCache.get(f);
|
|
219
|
+
if (stale?.row) rows.push(stale.row);
|
|
220
|
+
else markInvalid(f);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
178
223
|
const row = session ? _sessionSummary(session) : null;
|
|
179
|
-
_summaryScanCache.set(f, { mtimeMs:
|
|
224
|
+
_summaryScanCache.set(f, { mtimeMs: fileProbe.mtimeMs, size: fileProbe.size, row });
|
|
180
225
|
changed = true;
|
|
181
226
|
if (row) rows.push(row);
|
|
182
227
|
else markInvalid(f);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
2
|
+
|
|
3
|
+
const dataDir = String(workerData?.dataDir || '');
|
|
4
|
+
if (dataDir) process.env.MIXDOG_DATA_DIR = dataDir;
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const [{ _scanStoredSessionSummaryRows }, { _writeSummaryIndex }] = await Promise.all([
|
|
8
|
+
import('./summary-cache.mjs'),
|
|
9
|
+
import('../store-summary-index.mjs'),
|
|
10
|
+
]);
|
|
11
|
+
const { rows } = _scanStoredSessionSummaryRows();
|
|
12
|
+
const indexedRows = _writeSummaryIndex(rows);
|
|
13
|
+
parentPort?.postMessage({ ok: true, dataDir, rows: indexedRows });
|
|
14
|
+
} catch (error) {
|
|
15
|
+
parentPort?.postMessage({
|
|
16
|
+
ok: false,
|
|
17
|
+
dataDir,
|
|
18
|
+
error: error?.message || String(error),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -1,21 +1,51 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Module-level map tracking per-session cancellation generations, commit locks
|
|
3
|
+
* and the highest save epoch that actually LANDED for the id. All three live
|
|
4
|
+
* in one SharedArrayBuffer per id, so the save worker (which receives the
|
|
5
|
+
* buffer inside the write guard) observes the same state as the parent.
|
|
6
|
+
*/
|
|
2
7
|
const _writeControls = new Map();
|
|
3
8
|
|
|
9
|
+
// Layout of the per-id SharedArrayBuffer:
|
|
10
|
+
// int32 [0] cancellation generation (equality-compared, wrap-safe)
|
|
11
|
+
// int32 [1] commit lock
|
|
12
|
+
// int64 @8 highest LANDED save epoch — 64-bit and 8-byte aligned so the
|
|
13
|
+
// fence can never wrap or read back negative, unlike an int32
|
|
14
|
+
// slot which a long-lived process could in principle overflow.
|
|
15
|
+
const CONTROL_I32_LENGTH = 2;
|
|
16
|
+
const LANDED_EPOCH_BYTE_OFFSET = 8;
|
|
17
|
+
const CONTROL_BYTES = LANDED_EPOCH_BYTE_OFFSET + BigInt64Array.BYTES_PER_ELEMENT;
|
|
18
|
+
|
|
4
19
|
function writeControl(id) {
|
|
5
20
|
let control = _writeControls.get(id);
|
|
6
21
|
if (!control) {
|
|
7
|
-
|
|
8
|
-
control = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 2));
|
|
22
|
+
control = new Int32Array(new SharedArrayBuffer(CONTROL_BYTES), 0, CONTROL_I32_LENGTH);
|
|
9
23
|
_writeControls.set(id, control);
|
|
10
24
|
}
|
|
11
25
|
return control;
|
|
12
26
|
}
|
|
13
27
|
|
|
14
|
-
|
|
28
|
+
// Both realms build this view over the SAME SharedArrayBuffer they received in
|
|
29
|
+
// the guard, so parent and save worker observe one landed-epoch value.
|
|
30
|
+
function landedEpochView(buffer) {
|
|
31
|
+
return new BigInt64Array(buffer, LANDED_EPOCH_BYTE_OFFSET, 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function guardedSaveOptions(id, opts, epoch = null) {
|
|
15
35
|
const control = writeControl(id);
|
|
36
|
+
// Only exact integers may fence a commit; anything else is treated as
|
|
37
|
+
// "no identity" rather than silently truncated.
|
|
38
|
+
const issued = Number.isSafeInteger(epoch) ? epoch : null;
|
|
16
39
|
return {
|
|
17
40
|
...(opts || {}),
|
|
18
|
-
_sessionWriteGuard: {
|
|
41
|
+
_sessionWriteGuard: {
|
|
42
|
+
buffer: control.buffer,
|
|
43
|
+
version: Atomics.load(control, 0),
|
|
44
|
+
// Identity of the snapshot this write carries. Absent for the
|
|
45
|
+
// lifecycle barriers (tombstone/detach/delete), which are
|
|
46
|
+
// authoritative by construction and never epoch-fenced.
|
|
47
|
+
...(issued === null ? {} : { epoch: issued }),
|
|
48
|
+
},
|
|
19
49
|
};
|
|
20
50
|
}
|
|
21
51
|
|
|
@@ -33,18 +63,80 @@ export function isCancelledWrite(opts) {
|
|
|
33
63
|
}
|
|
34
64
|
}
|
|
35
65
|
|
|
36
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Publish the epoch of a write that just renamed onto the canonical path.
|
|
68
|
+
* Monotonic max, so a late/older commit can never lower the bar.
|
|
69
|
+
*/
|
|
70
|
+
export function publishLandedWriteEpoch(opts, epoch) {
|
|
71
|
+
const guard = opts?._sessionWriteGuard;
|
|
72
|
+
if (!guard?.buffer || !Number.isSafeInteger(epoch)) return;
|
|
73
|
+
try {
|
|
74
|
+
const view = landedEpochView(guard.buffer);
|
|
75
|
+
const next = BigInt(epoch);
|
|
76
|
+
let current = Atomics.load(view, 0);
|
|
77
|
+
while (next > current) {
|
|
78
|
+
const previous = Atomics.compareExchange(view, 0, current, next);
|
|
79
|
+
if (previous === current) return;
|
|
80
|
+
current = previous;
|
|
81
|
+
}
|
|
82
|
+
} catch { /* buffer gone: nothing to fence */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when a STRICTLY NEWER save already landed on disk for this id, i.e.
|
|
87
|
+
* committing this snapshot would revert durable history. Covers the exit
|
|
88
|
+
* drain of an outstanding older payload and an older worker write finishing
|
|
89
|
+
* after a newer sync/async write (the commit lock serialises renames, it does
|
|
90
|
+
* not order them by content age).
|
|
91
|
+
*/
|
|
92
|
+
export function isStaleWriteEpoch(opts) {
|
|
93
|
+
const guard = opts?._sessionWriteGuard;
|
|
94
|
+
if (!guard?.buffer || !Number.isSafeInteger(guard.epoch)) return false;
|
|
95
|
+
try {
|
|
96
|
+
return BigInt(guard.epoch) < Atomics.load(landedEpochView(guard.buffer), 0);
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sentinel returned when `timeoutMs` elapsed before the id's commit lock could
|
|
104
|
+
* be taken. Distinct from `false` (cancelled): nothing is wrong with the
|
|
105
|
+
* write, this realm simply may not block any longer (exit drain).
|
|
106
|
+
*/
|
|
107
|
+
export const WRITE_COMMIT_TIMEOUT = Symbol('session-write-commit-timeout');
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sentinel returned when a newer write for the id landed while this one waited
|
|
111
|
+
* for the lock: the caller must discard its scratch file and commit nothing.
|
|
112
|
+
*/
|
|
113
|
+
export const WRITE_COMMIT_STALE = Symbol('session-write-commit-stale');
|
|
114
|
+
|
|
115
|
+
export function acquireWriteCommit(opts, options = {}) {
|
|
37
116
|
const guard = opts?._sessionWriteGuard;
|
|
38
117
|
if (!guard?.buffer) return null;
|
|
39
118
|
const control = new Int32Array(guard.buffer);
|
|
119
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs >= 0
|
|
120
|
+
? options.timeoutMs
|
|
121
|
+
: Infinity;
|
|
122
|
+
const deadline = timeoutMs === Infinity ? Infinity : Date.now() + timeoutMs;
|
|
40
123
|
while (Atomics.compareExchange(control, 1, 0, 1) !== 0) {
|
|
41
|
-
|
|
124
|
+
if (Date.now() >= deadline) return WRITE_COMMIT_TIMEOUT;
|
|
125
|
+
const slice = deadline === Infinity ? 25 : Math.min(25, Math.max(1, deadline - Date.now()));
|
|
126
|
+
Atomics.wait(control, 1, 1, slice);
|
|
42
127
|
}
|
|
43
128
|
if (isCancelledWrite(opts)) {
|
|
44
129
|
Atomics.store(control, 1, 0);
|
|
45
130
|
Atomics.notify(control, 1);
|
|
46
131
|
return false;
|
|
47
132
|
}
|
|
133
|
+
// Re-checked UNDER the lock: the newer write may have landed while this
|
|
134
|
+
// one was waiting for it.
|
|
135
|
+
if (isStaleWriteEpoch(opts)) {
|
|
136
|
+
Atomics.store(control, 1, 0);
|
|
137
|
+
Atomics.notify(control, 1);
|
|
138
|
+
return WRITE_COMMIT_STALE;
|
|
139
|
+
}
|
|
48
140
|
return control;
|
|
49
141
|
}
|
|
50
142
|
|
|
@@ -54,9 +146,21 @@ export function releaseWriteCommit(control) {
|
|
|
54
146
|
Atomics.notify(control, 1);
|
|
55
147
|
}
|
|
56
148
|
|
|
57
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Wait until no commit holds this id's rename lock. `timeoutMs` bounds the
|
|
151
|
+
* wait: the exit drain must never hang behind a worker/thread that is stuck
|
|
152
|
+
* inside its commit, and a timeout is safe — the caller's own write still
|
|
153
|
+
* serialises on the same Atomics lock. Returns true when the lock was
|
|
154
|
+
* observed free, false on timeout.
|
|
155
|
+
*/
|
|
156
|
+
export function waitForWriteCommit(id, opts = {}) {
|
|
58
157
|
const control = writeControl(id);
|
|
158
|
+
const timeoutMs = Number.isFinite(opts.timeoutMs) && opts.timeoutMs >= 0 ? opts.timeoutMs : Infinity;
|
|
159
|
+
const deadline = timeoutMs === Infinity ? Infinity : Date.now() + timeoutMs;
|
|
59
160
|
while (Atomics.load(control, 1) !== 0) {
|
|
60
|
-
|
|
161
|
+
if (Date.now() >= deadline) return false;
|
|
162
|
+
const slice = deadline === Infinity ? 25 : Math.min(25, Math.max(1, deadline - Date.now()));
|
|
163
|
+
Atomics.wait(control, 1, 1, slice);
|
|
61
164
|
}
|
|
165
|
+
return true;
|
|
62
166
|
}
|
|
@@ -150,6 +150,7 @@ export function _sessionSummary(session) {
|
|
|
150
150
|
permission: session.permission || null,
|
|
151
151
|
toolPermission: session.toolPermission || null,
|
|
152
152
|
messageCount: messageProjection.count,
|
|
153
|
+
title: _cleanPreview(session.title || '', 100),
|
|
153
154
|
preview: messageProjection.preview,
|
|
154
155
|
generation: typeof session.generation === 'number' ? session.generation : 0,
|
|
155
156
|
implicitBashSessionId: session.implicitBashSessionId || null,
|
|
@@ -185,6 +186,7 @@ function _normalizeSummaryRow(row) {
|
|
|
185
186
|
permission: row.permission || null,
|
|
186
187
|
toolPermission: row.toolPermission || null,
|
|
187
188
|
messageCount: Math.max(0, Math.floor(Number(row.messageCount) || 0)),
|
|
189
|
+
title: _cleanPreview(row.title || '', 100),
|
|
188
190
|
preview: _cleanPreview(row.preview || ''),
|
|
189
191
|
generation: typeof row.generation === 'number' ? row.generation : 0,
|
|
190
192
|
implicitBashSessionId: row.implicitBashSessionId || null,
|