mixdog 0.9.19 → 0.9.21
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/LICENSE +21 -0
- package/README.md +100 -34
- package/package.json +3 -2
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +0 -1
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/tool-smoke.mjs +150 -45
- package/src/defaults/skills/setup/SKILL.md +327 -0
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3328
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/channels/index.mjs +6 -2183
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +777 -0
- package/src/runtime/memory/index.mjs +163 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +811 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
- package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
- package/src/runtime/memory/lib/memory.mjs +39 -0
- package/src/runtime/memory/lib/query-handlers.mjs +2 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/runtime/shared/atomic-file.mjs +138 -80
- package/src/runtime/shared/child-guardian.mjs +61 -3
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2104 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +67 -7
- package/src/standalone/folder-dialog.mjs +4 -1
- package/src/standalone/memory-runtime-proxy.mjs +154 -17
- package/src/standalone/seeds.mjs +28 -1
- package/src/tui/App.jsx +40 -28
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/doctor.mjs +175 -0
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +6 -0
- package/src/tui/app/use-transcript-scroll.mjs +77 -3
- package/src/tui/dist/index.mjs +2851 -2162
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +564 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +68 -2620
- package/src/tui/index.jsx +7 -0
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- package/src/workflows/sequential/WORKFLOW.md +0 -51
|
@@ -20,6 +20,14 @@ const LOCK_WAIT_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
|
|
|
20
20
|
const DEFAULT_BACKOFFS_MS = Object.freeze([25, 50, 100, 200, 400, 800, 1200, 1600]);
|
|
21
21
|
const DEFAULT_LOCK_TIMEOUT_MS = 8000;
|
|
22
22
|
|
|
23
|
+
// Per-process owner identity. A bare pid is not a durable holder identity:
|
|
24
|
+
// after a holder crashes the OS can recycle its pid onto an unrelated live
|
|
25
|
+
// process (or onto THIS process), making a corpse lock look "held by a live
|
|
26
|
+
// pid" — including "held by me" — which starves every waiter. Stamping a
|
|
27
|
+
// random per-instance token alongside the pid lets reclaim/release tell our
|
|
28
|
+
// current lock apart from a same-pid prior/other instance's leftover.
|
|
29
|
+
const _OWNER_TOKEN = randomBytes(12).toString('hex');
|
|
30
|
+
|
|
23
31
|
function sleepSync(ms) {
|
|
24
32
|
try {
|
|
25
33
|
const buf = new SharedArrayBuffer(4);
|
|
@@ -73,55 +81,11 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
73
81
|
contErr.cause = err;
|
|
74
82
|
throw contErr;
|
|
75
83
|
}
|
|
84
|
+
// Stale-lock reclaim (shared with the async variant). Covers a dead
|
|
85
|
+
// owner pid, a same-pid-but-foreign-token corpse from a reused pid, and
|
|
86
|
+
// a pidless/empty corpse lock; a live foreign holder is never stolen.
|
|
76
87
|
try {
|
|
77
|
-
|
|
78
|
-
// Dead-owner fast path: if the lock's recorded owner pid is gone,
|
|
79
|
-
// force-release immediately instead of waiting out staleMs. A crashed
|
|
80
|
-
// owner would otherwise hold the path until the 30s stale window,
|
|
81
|
-
// starving waiters past the 8s acquire timeout (the restart stall).
|
|
82
|
-
const ownerPidEarly = _readLockOwnerPid(lockPath);
|
|
83
|
-
const ownerDead = ownerPidEarly !== null && _pidIsDead(ownerPidEarly);
|
|
84
|
-
if (ownerDead || Date.now() - st.mtimeMs > staleMs) {
|
|
85
|
-
// Only steal a stale lock when we can prove the owner is
|
|
86
|
-
// gone. Reading the pid from the lock file and probing it
|
|
87
|
-
// with kill(pid, 0) protects a slow-but-live holder from
|
|
88
|
-
// having its lock yanked out from under it; the next
|
|
89
|
-
// process that acquires the lock would otherwise race
|
|
90
|
-
// against the original holder and clobber its write.
|
|
91
|
-
//
|
|
92
|
-
// Two-waiter stale-reclaim race: a pure rename "claim" is
|
|
93
|
-
// not enough. A waiter that proved pid D dead can stall;
|
|
94
|
-
// another waiter can then remove D and acquire a live lock,
|
|
95
|
-
// and the stalled waiter would rename that live lock away.
|
|
96
|
-
// Instead, serialize stale removal behind a tiny pid-stamped
|
|
97
|
-
// reclaim guard, then re-read the lock while the stale file
|
|
98
|
-
// is still present at lockPath. If the owner token no longer
|
|
99
|
-
// matches the pid we proved dead, treat the holder as live
|
|
100
|
-
// and back off. We never move or unlink a mismatched live-
|
|
101
|
-
// token file. Guard cleanup is only a best-effort unblocker:
|
|
102
|
-
// path unlink is not a portable unlink-if-token-still-matches
|
|
103
|
-
// primitive, so live-lock safety rests on the lockPath token
|
|
104
|
-
// reverify immediately before the lockPath unlink below.
|
|
105
|
-
const deadPid = ownerPidEarly;
|
|
106
|
-
if (ownerDead) {
|
|
107
|
-
const reclaim = _tryAcquireReclaimGuard(lockPath, staleMs);
|
|
108
|
-
if (reclaim !== null) {
|
|
109
|
-
let reclaimed = false;
|
|
110
|
-
try {
|
|
111
|
-
const currentSt = statSync(lockPath);
|
|
112
|
-
if (ownerDead || Date.now() - currentSt.mtimeMs > staleMs) {
|
|
113
|
-
const currentPid = _readLockOwnerPid(lockPath);
|
|
114
|
-
if (currentPid === deadPid && _pidIsDead(currentPid)) {
|
|
115
|
-
try { unlinkSync(lockPath); reclaimed = true; } catch {}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} finally {
|
|
119
|
-
_releaseReclaimGuard(reclaim);
|
|
120
|
-
}
|
|
121
|
-
if (reclaimed) continue;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
88
|
+
if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
|
|
125
89
|
} catch {}
|
|
126
90
|
if (Date.now() >= deadline) break;
|
|
127
91
|
const base = DEFAULT_BACKOFFS_MS[Math.min(attempt, DEFAULT_BACKOFFS_MS.length - 1)];
|
|
@@ -130,10 +94,12 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
130
94
|
attempt += 1;
|
|
131
95
|
continue;
|
|
132
96
|
}
|
|
133
|
-
// Stamp
|
|
134
|
-
// finally-unlink below can verify
|
|
135
|
-
// deleting whatever lock file happens to be at this path.
|
|
136
|
-
|
|
97
|
+
// Stamp pid + per-instance token so stale-lock recovery and the
|
|
98
|
+
// finally-unlink below can verify OUR identity instead of blindly
|
|
99
|
+
// deleting whatever lock file happens to be at this path. Format
|
|
100
|
+
// `<pid> <ts> <token>` is a superset of the old `<pid> <ts>`; a
|
|
101
|
+
// tokenless (old-format) lock is still read as pid-authoritative.
|
|
102
|
+
try { writeFileSync(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}\n`, 'utf8'); } catch {}
|
|
137
103
|
// For secret-bearing critical sections, the lock file sits beside
|
|
138
104
|
// the secret in the same (shared-home) directory; clamp it owner-only
|
|
139
105
|
// too. Fail-closed: an unenforceable ACL aborts before fn() runs.
|
|
@@ -148,7 +114,7 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
148
114
|
// owner's lock and break mutual exclusion for whoever is
|
|
149
115
|
// queued behind it.
|
|
150
116
|
try {
|
|
151
|
-
if (
|
|
117
|
+
if (_lockOwnedBySelf(lockPath)) unlinkSync(lockPath);
|
|
152
118
|
} catch {}
|
|
153
119
|
}
|
|
154
120
|
}
|
|
@@ -189,8 +155,109 @@ function _pidIsDead(pid) {
|
|
|
189
155
|
}
|
|
190
156
|
}
|
|
191
157
|
|
|
192
|
-
|
|
193
|
-
|
|
158
|
+
// Full owner identity: pid plus per-instance token. token is null for an
|
|
159
|
+
// old-format (`<pid> <ts>`) lock, in which case callers fall back to
|
|
160
|
+
// pid-authoritative handling so a still-running old-format holder is
|
|
161
|
+
// respected. A read/parse error yields {pid:null, token:null} ("unknown").
|
|
162
|
+
function _readLockOwner(lockPath) {
|
|
163
|
+
try {
|
|
164
|
+
const raw = readFileSync(lockPath, 'utf8');
|
|
165
|
+
const parts = String(raw).trim().split(/\s+/);
|
|
166
|
+
const pid = Number.parseInt(parts[0], 10);
|
|
167
|
+
return {
|
|
168
|
+
pid: Number.isFinite(pid) && pid > 0 ? pid : null,
|
|
169
|
+
token: parts.length >= 3 ? parts[2] : null,
|
|
170
|
+
};
|
|
171
|
+
} catch {
|
|
172
|
+
return { pid: null, token: null };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Is this recorded owner a genuinely live holder?
|
|
177
|
+
// - pid null (empty/unparseable): not live (unknown owner).
|
|
178
|
+
// - pid === our pid: treated as LIVE. A foreign/absent token on our own
|
|
179
|
+
// pid is ambiguous — it may be a recycled-pid corpse, but it may also
|
|
180
|
+
// be a live sibling worker_thread (which shares process.pid yet owns a
|
|
181
|
+
// separate _OWNER_TOKEN module instance). We cannot distinguish them
|
|
182
|
+
// from the token alone, so we never call a same-pid owner instantly
|
|
183
|
+
// dead; the reclaim path gates a same-pid foreign-token steal behind
|
|
184
|
+
// stale mtime (see _ownerIsSelfForeign / _tryReclaimStaleLock) so a
|
|
185
|
+
// live sibling is never yanked, while a real corpse still frees once
|
|
186
|
+
// mtime goes stale.
|
|
187
|
+
// - foreign pid: probed with kill(pid, 0); ESRCH ⇒ dead, else live.
|
|
188
|
+
function _ownerIsLive(owner) {
|
|
189
|
+
if (owner.pid === null) return false;
|
|
190
|
+
if (owner.pid === process.pid) return true;
|
|
191
|
+
try {
|
|
192
|
+
process.kill(owner.pid, 0);
|
|
193
|
+
return true;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return err?.code !== 'ESRCH';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Our own pid but a foreign (present, non-matching) token: either a
|
|
200
|
+
// recycled-pid corpse or a live sibling worker_thread. Reclaimable only
|
|
201
|
+
// once mtime is past staleMs (handled by the pidless-style stale gate).
|
|
202
|
+
function _ownerIsSelfForeign(owner) {
|
|
203
|
+
return owner.pid === process.pid
|
|
204
|
+
&& owner.token !== null
|
|
205
|
+
&& owner.token !== _OWNER_TOKEN;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Release-time guard: unlink only if the lock still carries OUR identity
|
|
209
|
+
// (our pid AND our token). Protects against deleting a lock another
|
|
210
|
+
// instance stole+recreated, and against a same-pid sibling.
|
|
211
|
+
function _lockOwnedBySelf(lockPath) {
|
|
212
|
+
const owner = _readLockOwner(lockPath);
|
|
213
|
+
if (owner.pid !== process.pid) return false;
|
|
214
|
+
return owner.token === null ? true : owner.token === _OWNER_TOKEN;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Shared stale-lock reclaim used by BOTH withFileLockSync and withFileLock.
|
|
218
|
+
// The whole operation is synchronous; the two variants differ only in how
|
|
219
|
+
// they wait between attempts. Returns true iff lockPath was reclaimed.
|
|
220
|
+
//
|
|
221
|
+
// Reclaim decision table (owner = identity recorded in the lock file):
|
|
222
|
+
// owner pid DEAD (ESRCH / self-pid+foreign-token corpse) → reclaim
|
|
223
|
+
// owner pid NULL (empty/unparseable) AND mtime past staleMs → reclaim
|
|
224
|
+
// owner token MISMATCH on guarded re-read (new holder took over) → back off
|
|
225
|
+
// owner LIVE (foreign pid alive, or our own current token) → back off
|
|
226
|
+
function _tryReclaimStaleLock(lockPath, staleMs) {
|
|
227
|
+
let st;
|
|
228
|
+
try { st = statSync(lockPath); } catch { return false; }
|
|
229
|
+
const owner = _readLockOwner(lockPath);
|
|
230
|
+
const stale = Date.now() - st.mtimeMs > staleMs;
|
|
231
|
+
const dead = owner.pid !== null && !_ownerIsLive(owner);
|
|
232
|
+
const pidless = owner.pid === null;
|
|
233
|
+
const selfForeign = _ownerIsSelfForeign(owner);
|
|
234
|
+
// A dead foreign owner reclaims immediately. A pidless corpse, or a
|
|
235
|
+
// same-pid foreign-token owner (which may still be a live sibling
|
|
236
|
+
// worker_thread), reclaims only once mtime is past staleMs; a live
|
|
237
|
+
// foreign holder is never stolen even after mtime goes stale.
|
|
238
|
+
if (!dead && !((pidless || selfForeign) && stale)) return false;
|
|
239
|
+
const reclaim = _tryAcquireReclaimGuard(lockPath, staleMs);
|
|
240
|
+
if (reclaim === null) return false;
|
|
241
|
+
try {
|
|
242
|
+
let cur;
|
|
243
|
+
try { cur = statSync(lockPath); } catch { return false; }
|
|
244
|
+
const curOwner = _readLockOwner(lockPath);
|
|
245
|
+
// Re-verify the SAME identity (pid AND token) we decided to reclaim is
|
|
246
|
+
// still present. A reused-pid new holder that grabbed the lock during
|
|
247
|
+
// the guard window has a different token and is not mistaken for the
|
|
248
|
+
// corpse — so we never yank a freshly-acquired live lock.
|
|
249
|
+
if (curOwner.pid !== owner.pid || curOwner.token !== owner.token) return false;
|
|
250
|
+
const curStale = Date.now() - cur.mtimeMs > staleMs;
|
|
251
|
+
const curDead = curOwner.pid !== null && !_ownerIsLive(curOwner);
|
|
252
|
+
const curPidless = curOwner.pid === null;
|
|
253
|
+
const curSelfForeign = _ownerIsSelfForeign(curOwner);
|
|
254
|
+
if (curDead || ((curPidless || curSelfForeign) && curStale)) {
|
|
255
|
+
try { unlinkSync(lockPath); return true; } catch { return false; }
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
} finally {
|
|
259
|
+
_releaseReclaimGuard(reclaim);
|
|
260
|
+
}
|
|
194
261
|
}
|
|
195
262
|
|
|
196
263
|
function _reclaimGuardIsUnreadableAndStale(guardPath, staleMs) {
|
|
@@ -229,7 +296,16 @@ function _tryClearStaleReclaimGuard(guardPath, staleMs) {
|
|
|
229
296
|
// This is still not a serialized primitive: correctness comes from
|
|
230
297
|
// the lockPath owner-token reverify before unlinking lockPath, not
|
|
231
298
|
// from making guard cleanup itself authoritative.
|
|
232
|
-
|
|
299
|
+
// A guard whose pid was recycled onto a LIVE process would otherwise
|
|
300
|
+
// never clear, starving reclaim permanently; so also clear a guard
|
|
301
|
+
// past staleMs regardless of apparent pid-liveness. Safe because the
|
|
302
|
+
// guard is best-effort — the lockPath owner-token reverify still gates
|
|
303
|
+
// the actual lock unlink.
|
|
304
|
+
let guardStale = false;
|
|
305
|
+
try { guardStale = Date.now() - statSync(guardPath).mtimeMs > staleMs; } catch {}
|
|
306
|
+
if (_pidIsDead(guardPid) || guardStale) {
|
|
307
|
+
_unlinkReclaimGuardIfPidMatches(guardPath, guardPid);
|
|
308
|
+
}
|
|
233
309
|
return;
|
|
234
310
|
}
|
|
235
311
|
// A crash between openSync('wx') and the pid write can leave an empty
|
|
@@ -294,29 +370,11 @@ export async function withFileLock(lockPath, fn, opts = {}) {
|
|
|
294
370
|
contErr.cause = err;
|
|
295
371
|
throw contErr;
|
|
296
372
|
}
|
|
373
|
+
// Stale-lock reclaim (shared with the sync variant): dead owner pid,
|
|
374
|
+
// a same-pid-but-foreign-token corpse from a reused pid, or a
|
|
375
|
+
// pidless/empty corpse lock. A live foreign holder is never stolen.
|
|
297
376
|
try {
|
|
298
|
-
|
|
299
|
-
if (Date.now() - st.mtimeMs > staleMs) {
|
|
300
|
-
const deadPid = _readLockOwnerPid(lockPath);
|
|
301
|
-
if (deadPid !== null && _pidIsDead(deadPid)) {
|
|
302
|
-
const reclaim = _tryAcquireReclaimGuard(lockPath, staleMs);
|
|
303
|
-
if (reclaim !== null) {
|
|
304
|
-
let reclaimed = false;
|
|
305
|
-
try {
|
|
306
|
-
const currentSt = statSync(lockPath);
|
|
307
|
-
if (Date.now() - currentSt.mtimeMs > staleMs) {
|
|
308
|
-
const currentPid = _readLockOwnerPid(lockPath);
|
|
309
|
-
if (currentPid === deadPid && _pidIsDead(currentPid)) {
|
|
310
|
-
try { unlinkSync(lockPath); reclaimed = true; } catch {}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
} finally {
|
|
314
|
-
_releaseReclaimGuard(reclaim);
|
|
315
|
-
}
|
|
316
|
-
if (reclaimed) continue;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
377
|
+
if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
|
|
320
378
|
} catch {}
|
|
321
379
|
if (Date.now() >= deadline) break;
|
|
322
380
|
const base = DEFAULT_BACKOFFS_MS[Math.min(attempt, DEFAULT_BACKOFFS_MS.length - 1)];
|
|
@@ -325,14 +383,14 @@ export async function withFileLock(lockPath, fn, opts = {}) {
|
|
|
325
383
|
attempt += 1;
|
|
326
384
|
continue;
|
|
327
385
|
}
|
|
328
|
-
try { writeFileSync(fd, `${process.pid} ${Date.now()}\n`, 'utf8'); } catch {}
|
|
386
|
+
try { writeFileSync(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}\n`, 'utf8'); } catch {}
|
|
329
387
|
try {
|
|
330
388
|
if (opts.secret === true) _enforceOwnerOnlyAclWin32(lockPath);
|
|
331
389
|
return await fn();
|
|
332
390
|
} finally {
|
|
333
391
|
try { closeSync(fd); } catch {}
|
|
334
392
|
try {
|
|
335
|
-
if (
|
|
393
|
+
if (_lockOwnedBySelf(lockPath)) unlinkSync(lockPath);
|
|
336
394
|
} catch {}
|
|
337
395
|
}
|
|
338
396
|
}
|
|
@@ -1,18 +1,70 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
|
|
5
5
|
function positiveInt(value) {
|
|
6
6
|
const n = Number(value);
|
|
7
7
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// On Windows a windowless, orphaned pwsh->cli chain keeps the immediate parent
|
|
11
|
+
// PID alive after its terminal window is closed, so parent-liveness alone never
|
|
12
|
+
// releases the worker. When the session is genuinely interactive (a TTY), the
|
|
13
|
+
// controlling console window is owned by a host process (conhost.exe) that dies
|
|
14
|
+
// with the terminal — monitoring that PID turns "terminal closed" into a fatal
|
|
15
|
+
// orphan signal. Returns null for non-interactive/service/hidden-console starts
|
|
16
|
+
// (no TTY, or no console window) so those are never treated as terminal loss.
|
|
17
|
+
function detectControllingTerminalPid() {
|
|
18
|
+
if (process.platform !== 'win32') return null;
|
|
19
|
+
// Gate on a real TTY: a service/pipe/hidden launch has no controlling
|
|
20
|
+
// terminal, and probing one there would spawn a transient console whose host
|
|
21
|
+
// dies immediately — a false "terminal lost" that would kill a healthy worker.
|
|
22
|
+
const interactive = Boolean(
|
|
23
|
+
(process.stdin && process.stdin.isTTY) || (process.stdout && process.stdout.isTTY),
|
|
24
|
+
);
|
|
25
|
+
if (!interactive) return null;
|
|
26
|
+
try {
|
|
27
|
+
const systemRoot = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
|
|
28
|
+
const powershell = systemRoot + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
29
|
+
const script = [
|
|
30
|
+
'Add-Type @"',
|
|
31
|
+
'using System;',
|
|
32
|
+
'using System.Runtime.InteropServices;',
|
|
33
|
+
'public class MixdogTerm {',
|
|
34
|
+
' [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();',
|
|
35
|
+
' [DllImport("user32.dll")] public static extern int GetWindowThreadProcessId(IntPtr h, out int pid);',
|
|
36
|
+
'}',
|
|
37
|
+
'"@',
|
|
38
|
+
'$h=[MixdogTerm]::GetConsoleWindow()',
|
|
39
|
+
'if($h -eq [IntPtr]::Zero){ Write-Output 0 } else { $p=0; [void][MixdogTerm]::GetWindowThreadProcessId($h,[ref]$p); Write-Output $p }',
|
|
40
|
+
].join('\n');
|
|
41
|
+
// NO windowsHide: CREATE_NO_WINDOW gives the probe its own/absent console so
|
|
42
|
+
// GetConsoleWindow() returns 0 or a foreign hwnd. A plain console app spawned
|
|
43
|
+
// from a console-attached parent (this runs at the guardian start site, in the
|
|
44
|
+
// guarded parent) inherits that console with no window flash. Under Windows
|
|
45
|
+
// Terminal/ConPTY the owner is the tab's conhost (dies on tab close), which is
|
|
46
|
+
// exactly the pid we want to monitor.
|
|
47
|
+
const res = spawnSync(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
|
|
48
|
+
timeout: 4000,
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
51
|
+
});
|
|
52
|
+
const pid = positiveInt(String((res && res.stdout) || '').trim());
|
|
53
|
+
// Ignore our own PID: that would make the guardian its own kill trigger.
|
|
54
|
+
if (pid && pid !== process.pid) return pid;
|
|
55
|
+
return null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function guardianScript({ parentPid, childPid, childGroupPid, terminalPid, platform, pollMs, orphanGraceMs, forceGraceMs }) {
|
|
11
62
|
return `
|
|
12
63
|
const { spawnSync } = require('node:child_process');
|
|
13
64
|
const parentPid = ${JSON.stringify(parentPid)};
|
|
14
65
|
const childPid = ${JSON.stringify(childPid)};
|
|
15
66
|
const childGroupPid = ${JSON.stringify(childGroupPid || childPid)};
|
|
67
|
+
const terminalPid = ${JSON.stringify(terminalPid || 0)};
|
|
16
68
|
const platform = ${JSON.stringify(platform)};
|
|
17
69
|
const pollMs = ${JSON.stringify(pollMs)};
|
|
18
70
|
const orphanGraceMs = ${JSON.stringify(orphanGraceMs)};
|
|
@@ -42,7 +94,12 @@ let killing = false;
|
|
|
42
94
|
let orphanedAt = 0;
|
|
43
95
|
const timer = setInterval(() => {
|
|
44
96
|
if (!alive(childPid)) process.exit(0);
|
|
45
|
-
|
|
97
|
+
// Orphaned when the parent PID dies OR (interactive only) the controlling
|
|
98
|
+
// terminal's console host dies — the latter catches a windowless pwsh->cli
|
|
99
|
+
// chain that outlives its closed terminal window. terminalPid is 0 for
|
|
100
|
+
// non-interactive starts, so those fall back to pure parent-liveness.
|
|
101
|
+
const terminalLost = terminalPid > 0 && !alive(terminalPid);
|
|
102
|
+
if (alive(parentPid) && !terminalLost) {
|
|
46
103
|
orphanedAt = 0;
|
|
47
104
|
return;
|
|
48
105
|
}
|
|
@@ -77,6 +134,7 @@ export function startChildGuardian({
|
|
|
77
134
|
parentPid: parent,
|
|
78
135
|
childPid: child,
|
|
79
136
|
childGroupPid: positiveInt(childGroupPid) || child,
|
|
137
|
+
terminalPid: detectControllingTerminalPid() || 0,
|
|
80
138
|
platform: process.platform,
|
|
81
139
|
pollMs: Math.max(100, Math.floor(Number(pollMs) || 750)),
|
|
82
140
|
orphanGraceMs: Math.max(100, Math.floor(Number(orphanGraceMs) || Number(graceMs) || 3000)),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { envFlag } from './env.mjs';
|
|
3
|
+
|
|
4
|
+
// Boot-timing profiler + instrumented dynamic import. Extracted verbatim from
|
|
5
|
+
// mixdog-session-runtime.mjs during the facade split. `profiledImport` resolves
|
|
6
|
+
// relative specifiers against this module's directory (src/session-runtime/),
|
|
7
|
+
// which matches runtime-core.mjs, so callers keep passing the same specifiers.
|
|
8
|
+
const BOOT_PROFILE_ENABLED = envFlag('MIXDOG_BOOT_PROFILE');
|
|
9
|
+
const BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance.now());
|
|
10
|
+
|
|
11
|
+
export function bootProfile(event, fields = {}) {
|
|
12
|
+
if (!BOOT_PROFILE_ENABLED) return;
|
|
13
|
+
const elapsedMs = performance.now() - BOOT_PROFILE_START;
|
|
14
|
+
const parts = [`[mixdog-boot] +${elapsedMs.toFixed(1)}ms`, event];
|
|
15
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
16
|
+
if (value === undefined || value === null || value === '') continue;
|
|
17
|
+
parts.push(`${key}=${String(value).replace(/\s+/g, '_')}`);
|
|
18
|
+
}
|
|
19
|
+
try { process.stderr.write(`${parts.join(' ')}\n`); } catch {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function profiledImport(label, spec, { optional = false } = {}) {
|
|
23
|
+
const startedAt = performance.now();
|
|
24
|
+
try {
|
|
25
|
+
const mod = await import(spec);
|
|
26
|
+
bootProfile(`import:${label}`, { ms: (performance.now() - startedAt).toFixed(1) });
|
|
27
|
+
return mod;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
bootProfile(`import:${label}:failed`, {
|
|
30
|
+
ms: (performance.now() - startedAt).toFixed(1),
|
|
31
|
+
error: error?.message || String(error),
|
|
32
|
+
});
|
|
33
|
+
if (optional) return null;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
channelSetup,
|
|
3
|
+
deleteSchedule,
|
|
4
|
+
deleteWebhook,
|
|
5
|
+
setChannel,
|
|
6
|
+
saveSchedule,
|
|
7
|
+
saveWebhook,
|
|
8
|
+
setScheduleEnabled,
|
|
9
|
+
setWebhookEnabled,
|
|
10
|
+
setWebhookConfig,
|
|
11
|
+
} from '../standalone/channel-admin.mjs';
|
|
12
|
+
|
|
13
|
+
// Channel/webhook/schedule config surface. Extracted verbatim from the runtime
|
|
14
|
+
// API object; the mutating admin helpers are imported directly here and the
|
|
15
|
+
// runtime injects only the closure-owned callbacks (backend flush, channel
|
|
16
|
+
// worker handle, soft reload).
|
|
17
|
+
export function createChannelConfigApi({ flushBackendSave, channels, reloadChannelsSoon }) {
|
|
18
|
+
return {
|
|
19
|
+
getChannelSetup() {
|
|
20
|
+
// Flush a pending debounced backend switch first so setup readers
|
|
21
|
+
// (Settings → Channel Setting, remote toggles) never observe the
|
|
22
|
+
// previous backend during the 150ms debounce window.
|
|
23
|
+
try { flushBackendSave(); } catch {}
|
|
24
|
+
return channelSetup();
|
|
25
|
+
},
|
|
26
|
+
getChannelWorkerStatus() {
|
|
27
|
+
return channels.status();
|
|
28
|
+
},
|
|
29
|
+
setChannel(entry) {
|
|
30
|
+
const result = setChannel(entry);
|
|
31
|
+
reloadChannelsSoon();
|
|
32
|
+
return result;
|
|
33
|
+
},
|
|
34
|
+
setWebhookConfig(patch) {
|
|
35
|
+
const result = setWebhookConfig(patch);
|
|
36
|
+
reloadChannelsSoon();
|
|
37
|
+
return result;
|
|
38
|
+
},
|
|
39
|
+
saveSchedule(entry) {
|
|
40
|
+
const result = saveSchedule(entry);
|
|
41
|
+
reloadChannelsSoon();
|
|
42
|
+
return result;
|
|
43
|
+
},
|
|
44
|
+
deleteSchedule(name) {
|
|
45
|
+
const result = deleteSchedule(name);
|
|
46
|
+
reloadChannelsSoon();
|
|
47
|
+
return result;
|
|
48
|
+
},
|
|
49
|
+
setScheduleEnabled(name, enabled) {
|
|
50
|
+
const result = setScheduleEnabled(name, enabled);
|
|
51
|
+
reloadChannelsSoon();
|
|
52
|
+
return result;
|
|
53
|
+
},
|
|
54
|
+
saveWebhook(entry) {
|
|
55
|
+
const result = saveWebhook(entry);
|
|
56
|
+
reloadChannelsSoon();
|
|
57
|
+
return result;
|
|
58
|
+
},
|
|
59
|
+
deleteWebhook(name) {
|
|
60
|
+
const result = deleteWebhook(name);
|
|
61
|
+
reloadChannelsSoon();
|
|
62
|
+
return result;
|
|
63
|
+
},
|
|
64
|
+
setWebhookEnabled(name, enabled) {
|
|
65
|
+
const result = setWebhookEnabled(name, enabled);
|
|
66
|
+
reloadChannelsSoon();
|
|
67
|
+
return result;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
estimateRequestReserveTokens,
|
|
3
|
+
estimateToolSchemaTokens,
|
|
4
|
+
estimateTranscriptContextUsage,
|
|
5
|
+
resolveCompactBufferTokens,
|
|
6
|
+
resolveCompactTriggerTokens,
|
|
7
|
+
summarizeContextMessages,
|
|
8
|
+
} from '../runtime/agent/orchestrator/session/context-utils.mjs';
|
|
9
|
+
import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
|
|
10
|
+
|
|
11
|
+
// Live /context gauge computation + its self-owned memoization cache. Extracted
|
|
12
|
+
// verbatim from the runtime API object; the runtime injects live getters for
|
|
13
|
+
// the mutable session/route/cwd/mode locals. The cache (key + value) is owned
|
|
14
|
+
// here now, so invalidateContextStatusCache() is returned for the runtime to
|
|
15
|
+
// call from the same places it used to clear the inline locals.
|
|
16
|
+
export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMode }) {
|
|
17
|
+
let contextStatusCacheKey = null;
|
|
18
|
+
let contextStatusCacheValue = null;
|
|
19
|
+
|
|
20
|
+
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
21
|
+
const session = getSession();
|
|
22
|
+
const route = getRoute();
|
|
23
|
+
const compaction = session?.compaction || {};
|
|
24
|
+
const lastMessage = messages[messages.length - 1] || null;
|
|
25
|
+
return {
|
|
26
|
+
session,
|
|
27
|
+
sessionId: session?.id || null,
|
|
28
|
+
provider: route.provider,
|
|
29
|
+
model: route.model,
|
|
30
|
+
cwd: getCurrentCwd(),
|
|
31
|
+
mode: getMode(),
|
|
32
|
+
messages,
|
|
33
|
+
messageCount: messages.length,
|
|
34
|
+
lastMessage,
|
|
35
|
+
lastMessageRole: lastMessage?.role || null,
|
|
36
|
+
lastMessageContent: lastMessage?.content || null,
|
|
37
|
+
tools,
|
|
38
|
+
toolCount: tools.length,
|
|
39
|
+
contextWindow: session?.contextWindow || null,
|
|
40
|
+
rawContextWindow: session?.rawContextWindow || null,
|
|
41
|
+
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
42
|
+
autoCompactTokenLimit: Number(session?.autoCompactTokenLimit || 0),
|
|
43
|
+
lastContextTokens: Number(session?.lastContextTokens || 0),
|
|
44
|
+
lastContextTokensUpdatedAt: Number(session?.lastContextTokensUpdatedAt || 0),
|
|
45
|
+
lastContextTokensStaleAfterCompact: session?.lastContextTokensStaleAfterCompact === true,
|
|
46
|
+
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
47
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
48
|
+
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
49
|
+
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
50
|
+
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
51
|
+
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
52
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
53
|
+
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
54
|
+
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
55
|
+
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
56
|
+
compactBoundaryTokens: Number(session?.compactBoundaryTokens || 0),
|
|
57
|
+
compactionBoundaryTokens: Number(compaction.boundaryTokens || 0),
|
|
58
|
+
compactionTriggerTokens: Number(compaction.triggerTokens || 0),
|
|
59
|
+
compactionLastChangedAt: Number(compaction.lastChangedAt || 0),
|
|
60
|
+
compactionLastCompactAt: Number(compaction.lastCompactAt || 0),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sameContextStatusCacheKey(a, b) {
|
|
65
|
+
if (!a || !b) return false;
|
|
66
|
+
for (const key of Object.keys(a)) {
|
|
67
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function invalidateContextStatusCache() {
|
|
73
|
+
contextStatusCacheKey = null;
|
|
74
|
+
contextStatusCacheValue = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function contextStatus() {
|
|
78
|
+
const session = getSession();
|
|
79
|
+
const route = getRoute();
|
|
80
|
+
// Prefer the in-flight working transcript while a turn is running so the
|
|
81
|
+
// context gauge reflects LIVE growth (user turn + tool calls/results) as
|
|
82
|
+
// it accumulates, instead of freezing at the pre-turn committed snapshot.
|
|
83
|
+
// askSession() sets session.liveTurnMessages for the turn duration and
|
|
84
|
+
// clears it on commit/cancel/error, after which we fall back to the
|
|
85
|
+
// authoritative committed transcript.
|
|
86
|
+
const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
|
|
87
|
+
const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
|
|
88
|
+
const tools = Array.isArray(session?.tools) ? session.tools : [];
|
|
89
|
+
const cacheKey = contextStatusCacheKeyFor({ messages, tools });
|
|
90
|
+
if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
|
|
91
|
+
return contextStatusCacheValue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const messageSummary = summarizeContextMessages(messages);
|
|
95
|
+
const toolSchemaTokens = estimateToolSchemaTokens(tools);
|
|
96
|
+
const toolSchemaBreakdown = estimateToolSchemaBreakdown(tools);
|
|
97
|
+
const requestReserveTokens = estimateRequestReserveTokens(tools);
|
|
98
|
+
const requestOverheadTokens = Math.max(0, requestReserveTokens - toolSchemaTokens);
|
|
99
|
+
const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
|
|
100
|
+
const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
|
|
101
|
+
const lastContextTokens = Number(session?.lastContextTokens || 0);
|
|
102
|
+
const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
|
|
103
|
+
messageCount: messageSummary.count,
|
|
104
|
+
});
|
|
105
|
+
const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
|
|
106
|
+
const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
|
|
107
|
+
const lastUsageStale = !!lastContextTokens && (
|
|
108
|
+
session?.lastContextTokensStaleAfterCompact === true
|
|
109
|
+
|| (compactAt > 0 && usageAt > 0 && usageAt <= compactAt)
|
|
110
|
+
|| (compactAt > 0 && usageAt <= 0)
|
|
111
|
+
);
|
|
112
|
+
const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
|
|
113
|
+
const displayWindow = compactBoundaryTokens || effectiveWindow;
|
|
114
|
+
// The transcript estimate is the single source of truth for the displayed
|
|
115
|
+
// context footprint. Provider-reported input_tokens (lastContextTokens)
|
|
116
|
+
// swing non-monotonically and are not window-bounded on some providers
|
|
117
|
+
// (e.g. OpenAI gpt-5.5 Responses API), so they are kept only as secondary
|
|
118
|
+
// metadata (lastApiRequestTokens / usage.lastContextTokens) and never feed
|
|
119
|
+
// the gauge numerator.
|
|
120
|
+
const usedTokens = estimatedContextTokens;
|
|
121
|
+
const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
|
|
122
|
+
// Use the same shared compact-policy math as manager/loop. Do not trust
|
|
123
|
+
// persisted trigger telemetry as an independent policy input: it is an
|
|
124
|
+
// output snapshot and was the source of repeated /context false positives.
|
|
125
|
+
const compactTriggerTokens = resolveCompactTriggerTokens(session || {}, compactBoundaryTokens) || 0;
|
|
126
|
+
const compactBufferTokens = compactBoundaryTokens
|
|
127
|
+
? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
|
|
128
|
+
: resolveCompactBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
129
|
+
const value = {
|
|
130
|
+
sessionId: session?.id || null,
|
|
131
|
+
provider: route.provider,
|
|
132
|
+
model: route.model,
|
|
133
|
+
cwd: getCurrentCwd(),
|
|
134
|
+
toolMode: getMode(),
|
|
135
|
+
contextWindow: displayWindow || effectiveWindow || null,
|
|
136
|
+
effectiveContextWindow: effectiveWindow || null,
|
|
137
|
+
rawContextWindow: rawWindow || null,
|
|
138
|
+
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
139
|
+
usedTokens,
|
|
140
|
+
usedSource: 'estimated',
|
|
141
|
+
currentEstimatedTokens: estimatedContextTokens,
|
|
142
|
+
lastApiRequestTokens: lastContextTokens || 0,
|
|
143
|
+
lastApiRequestStale: lastUsageStale,
|
|
144
|
+
freeTokens,
|
|
145
|
+
compaction: {
|
|
146
|
+
...(session?.compaction || {}),
|
|
147
|
+
boundaryTokens: compactBoundaryTokens || null,
|
|
148
|
+
triggerTokens: compactTriggerTokens || null,
|
|
149
|
+
bufferTokens: compactBufferTokens || null,
|
|
150
|
+
currentEstimatedTokens: estimatedContextTokens,
|
|
151
|
+
lastApiRequestTokens: lastContextTokens || 0,
|
|
152
|
+
lastApiRequestStale: lastUsageStale,
|
|
153
|
+
},
|
|
154
|
+
messages: messageSummary,
|
|
155
|
+
request: {
|
|
156
|
+
toolSchemaTokens,
|
|
157
|
+
toolSchemaBreakdown,
|
|
158
|
+
requestOverheadTokens,
|
|
159
|
+
reserveTokens: requestReserveTokens,
|
|
160
|
+
},
|
|
161
|
+
usage: {
|
|
162
|
+
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
163
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
164
|
+
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
165
|
+
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
166
|
+
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
167
|
+
lastContextTokens,
|
|
168
|
+
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
169
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
170
|
+
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
171
|
+
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
172
|
+
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
contextStatusCacheKey = cacheKey;
|
|
176
|
+
contextStatusCacheValue = value;
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { contextStatus, invalidateContextStatusCache };
|
|
181
|
+
}
|