claude-code-session-manager 0.92.1 → 0.94.0
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/dist/assets/DataModel-B0LDnnSL.js +1 -0
- package/dist/assets/{History-BOR_fJNr.js → History-C685Kytt.js} +2 -2
- package/dist/assets/{Hooks-BtD436iL.js → Hooks-CwLnp0Z_.js} +3 -3
- package/dist/assets/HostBilko-CwEHEKYk.js +1 -0
- package/dist/assets/{Library-DeKdY-tD.js → Library-BU9np05E.js} +25 -25
- package/dist/assets/{MarkdownEditor-CbzC-Oqf.js → MarkdownEditor-B74ogK2f.js} +1 -1
- package/dist/assets/McpServers-BLM3ftzP.js +2 -0
- package/dist/assets/{Memory-B68AJjg2.js → Memory-DAP1t8bA.js} +6 -6
- package/dist/assets/{Permissions-Cr0RRvNy.js → Permissions-3pGLJmxQ.js} +3 -3
- package/dist/assets/Plugins-Q1KoLEzn.js +2 -0
- package/dist/assets/ProvenanceBadge-BnjBQiV9.js +1 -0
- package/dist/assets/{SaveBar-87ZJfscX.js → SaveBar-Zq2NfcPf.js} +1 -1
- package/dist/assets/Scheduler-D488ebKm.js +16 -0
- package/dist/assets/{ScopeSwitcher-CKzpqjJ_.js → ScopeSwitcher-Cnojv22D.js} +1 -1
- package/dist/assets/Settings-D5Lhj7ga.js +3 -0
- package/dist/assets/{SkillReferenceGraph-D_-wg4r9.js → SkillReferenceGraph-BRDiE6Hv.js} +2 -2
- package/dist/assets/Skills-BQDqp-EN.js +3 -0
- package/dist/assets/SystemPrompt-tFr19Od6.js +1 -0
- package/dist/assets/TagLibrary-BjyrnaRE.js +1 -0
- package/dist/assets/{TiptapBody-DYXIp8aG.js → TiptapBody-DGzSg3BO.js} +1 -1
- package/dist/assets/{Toggle-CDXR5l3m.js → Toggle-DzoROibf.js} +1 -1
- package/dist/assets/index-CCl4tz-u.js +3079 -0
- package/dist/assets/{index-D3P_jldk.css → index-CKH5Uxik.css} +1 -1
- package/dist/assets/settingsSchema-BYKVe1WM.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +6 -3
- package/scripts/README.md +3 -0
- package/src/main/__tests__/agentEffortResolve.test.cjs +117 -0
- package/src/main/__tests__/agentLibrary.test.cjs +21 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +16 -0
- package/src/main/__tests__/agentOverlayWrite.test.cjs +95 -0
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +5 -2
- package/src/main/__tests__/chat-exit-close-race.test.cjs +8 -2
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +5 -2
- package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +44 -0
- package/src/main/__tests__/intradayRefresh.test.cjs +39 -0
- package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +25 -0
- package/src/main/__tests__/opsErrorLog.test.cjs +22 -0
- package/src/main/__tests__/personaMerge.test.cjs +169 -0
- package/src/main/__tests__/prdCreatePlanId.test.cjs +132 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +13 -0
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +21 -0
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +14 -0
- package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +26 -0
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +11 -1
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +11 -1
- package/src/main/__tests__/runVerify.test.cjs +12 -1
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +47 -2
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +112 -1
- package/src/main/agentLibrary.cjs +86 -40
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +50 -10
- package/src/main/git.cjs +9 -2
- package/src/main/historyAggregator.cjs +2 -19
- package/src/main/index.cjs +17 -8
- package/src/main/ipcSchemas.cjs +25 -3
- package/src/main/lib/__tests__/childWithLog.test.cjs +180 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +19 -0
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +10 -5
- package/src/main/lib/__tests__/gitCacheBound.test.cjs +69 -0
- package/src/main/lib/__tests__/modelCatalog.test.cjs +202 -0
- package/src/main/lib/agentEffortResolve.cjs +74 -0
- package/src/main/lib/agentModelResolve.cjs +52 -59
- package/src/main/lib/agentPersonaSchema.cjs +5 -0
- package/src/main/lib/childWithLog.cjs +92 -51
- package/src/main/lib/delegationReadiness.cjs +3 -1
- package/src/main/lib/effectiveModelInfo.cjs +38 -20
- package/src/main/lib/epicMint.cjs +5 -2
- package/src/main/lib/epicSpawnPlan.cjs +27 -8
- package/src/main/lib/epicTranscriptPath.cjs +5 -1
- package/src/main/lib/epicWorktreeMerge.cjs +7 -6
- package/src/main/lib/headTailBuffer.cjs +43 -0
- package/src/main/lib/intradayRefresh.cjs +33 -0
- package/src/main/lib/lruCache.cjs +39 -0
- package/src/main/lib/modelCatalog.cjs +243 -0
- package/src/main/lib/openExternalApp.cjs +27 -9
- package/src/main/lib/opsErrorLog.cjs +22 -0
- package/src/main/lib/personaMerge.cjs +166 -0
- package/src/main/lib/prdCreate.cjs +23 -5
- package/src/main/lib/prdDisposition.cjs +46 -0
- package/src/main/lib/prdFrontmatter.cjs +6 -2
- package/src/main/lib/promptSessionSchema.cjs +5 -0
- package/src/main/lib/promptSessionsCreateEpic.cjs +5 -3
- package/src/main/lib/rendererRecovery.cjs +141 -0
- package/src/main/lib/scheduleJobSchema.cjs +3 -0
- package/src/main/runVerify.cjs +3 -1
- package/src/main/scheduler/prdParser.cjs +2 -0
- package/src/main/scheduler.cjs +381 -262
- package/src/main/templates/PRD_AUTHORING.md +4 -0
- package/src/main/transcripts.cjs +54 -11
- package/src/preload/api.d.ts +56 -3
- package/src/preload/index.cjs +4 -0
- package/dist/assets/AgentLibrary-Ci6u03bq.js +0 -3
- package/dist/assets/DataModel-BIqcjsv2.js +0 -1
- package/dist/assets/HostBilko-CMe5cH3H.js +0 -1
- package/dist/assets/ListDetail-CtVjrHKB.js +0 -1
- package/dist/assets/McpServers-DKN75Shn.js +0 -2
- package/dist/assets/Panel-C2YTW-qP.js +0 -1
- package/dist/assets/Plugins-DbZkJs5l.js +0 -2
- package/dist/assets/ProvenanceBadge-kJ__BOQv.js +0 -1
- package/dist/assets/Scheduler-DdEaxku8.js +0 -16
- package/dist/assets/Settings-CjywVsNB.js +0 -3
- package/dist/assets/Skills-Cd_yPO_l.js +0 -3
- package/dist/assets/SystemPrompt-2z6eGPSF.js +0 -1
- package/dist/assets/TagLibrary-DcaWQSp4.js +0 -1
- package/dist/assets/index-D5H_H5wC.js +0 -3074
- package/dist/assets/settingsSchema-JRDfjgHb.js +0 -3
package/src/main/chatRunner.cjs
CHANGED
|
@@ -53,8 +53,14 @@ const { classifyPromptTicket } = require('./lib/classifyPromptTicket.cjs');
|
|
|
53
53
|
const sessionSlots = require('./lib/sessionSlots.cjs');
|
|
54
54
|
const opsErrorLog = require('./lib/opsErrorLog.cjs');
|
|
55
55
|
const agentModelResolve = require('./lib/agentModelResolve.cjs');
|
|
56
|
+
const { resolveEpicEffort, effortArgs } = require('./lib/agentEffortResolve.cjs');
|
|
56
57
|
const { planEpicSpawn } = require('./lib/epicSpawnPlan.cjs');
|
|
57
58
|
const logs = require('./logs.cjs');
|
|
59
|
+
const { createHeadTailBuffer } = require('./lib/headTailBuffer.cjs');
|
|
60
|
+
|
|
61
|
+
// Caps on per-run stream buffers (a run can last hours).
|
|
62
|
+
const STDERR_TAIL_MAX = 64 * 1024;
|
|
63
|
+
const LINE_BUFFER_MAX = 8 * 1024 * 1024;
|
|
58
64
|
|
|
59
65
|
// The only two CLI errors that mean "wrong session flag" (verified against the
|
|
60
66
|
// real CLI). "in use" → the transcript exists, so resume; "no conversation" →
|
|
@@ -316,6 +322,13 @@ const waiting = []; // [{ tabId, sessionId, prompt, cwd, resume, silent, onSilen
|
|
|
316
322
|
// Closes the microtask-wide window in which per-tab exclusivity would
|
|
317
323
|
// otherwise not hold. See pump().
|
|
318
324
|
const dispatching = new Set();
|
|
325
|
+
// sessionIds with a live CLI child: added when pump() dispatches a run, dropped
|
|
326
|
+
// in the same .finally() that frees the lane — i.e. only after executeRun has
|
|
327
|
+
// settled from `close`/`error`/spawn failure. A cancelled child still exiting
|
|
328
|
+
// therefore keeps holding it. Two `claude -p` against one sessionId die with
|
|
329
|
+
// "Session ID … is already in use", so pump() refuses to start a second run
|
|
330
|
+
// for a held sessionId (it waits in `waiting` like any other blocked run).
|
|
331
|
+
const liveSessions = new Set();
|
|
319
332
|
let activeCount = 0;
|
|
320
333
|
|
|
321
334
|
// Indirection so tests can stub the spawn without launching claude.
|
|
@@ -331,6 +344,7 @@ function __setExecutor(fn) { executor = fn || executeRun; }
|
|
|
331
344
|
function __resetQueueForTests() {
|
|
332
345
|
inFlight.clear();
|
|
333
346
|
dispatching.clear();
|
|
347
|
+
liveSessions.clear();
|
|
334
348
|
waiting.length = 0;
|
|
335
349
|
activeCount = 0;
|
|
336
350
|
sessionSlots.__resetForTests();
|
|
@@ -379,6 +393,7 @@ function run(opts) {
|
|
|
379
393
|
// visible in `inFlight` — the same window pump() guards against.
|
|
380
394
|
const tabBusy = inFlight.has(opts.tabId)
|
|
381
395
|
|| dispatching.has(opts.tabId)
|
|
396
|
+
|| liveSessions.has(opts.sessionId)
|
|
382
397
|
|| waiting.some((w) => w.tabId === opts.tabId);
|
|
383
398
|
if (tabBusy && opts.silent) {
|
|
384
399
|
// Silent probes stay best-effort and invisible: a probe that collides is
|
|
@@ -401,7 +416,8 @@ function pump() {
|
|
|
401
416
|
// it in `inFlight` (which happens a microtask later) — without it, one
|
|
402
417
|
// synchronous sweep of this loop could start two runs for the same tab and
|
|
403
418
|
// race two --resume processes against a single sessionId.
|
|
404
|
-
const idx = waiting.findIndex((w) => !inFlight.has(w.tabId) && !dispatching.has(w.tabId)
|
|
419
|
+
const idx = waiting.findIndex((w) => !inFlight.has(w.tabId) && !dispatching.has(w.tabId)
|
|
420
|
+
&& !liveSessions.has(w.sessionId));
|
|
405
421
|
// Everything queued is blocked behind its own tab's live run; the settle()
|
|
406
422
|
// of that run re-pumps.
|
|
407
423
|
if (idx === -1) break;
|
|
@@ -414,6 +430,7 @@ function pump() {
|
|
|
414
430
|
const [job] = waiting.splice(idx, 1);
|
|
415
431
|
activeCount += 1;
|
|
416
432
|
dispatching.add(job.tabId);
|
|
433
|
+
liveSessions.add(job.sessionId);
|
|
417
434
|
Promise.resolve()
|
|
418
435
|
.then(() => {
|
|
419
436
|
const donePromise = executor(job);
|
|
@@ -424,6 +441,7 @@ function pump() {
|
|
|
424
441
|
.catch(() => { /* executeRun never rejects; defensive */ })
|
|
425
442
|
.finally(() => {
|
|
426
443
|
dispatching.delete(job.tabId);
|
|
444
|
+
liveSessions.delete(job.sessionId);
|
|
427
445
|
sessionSlots.release(slotToken);
|
|
428
446
|
activeCount -= 1;
|
|
429
447
|
pump();
|
|
@@ -477,6 +495,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
477
495
|
// funnels through here so the exit path can emit a fallback only when the
|
|
478
496
|
// run settled without one.
|
|
479
497
|
let terminalSent = false;
|
|
498
|
+
let lineOverflowLogged = false;
|
|
480
499
|
const emitTerminal = (channel, payload) => {
|
|
481
500
|
if (terminalSent) return;
|
|
482
501
|
terminalSent = true;
|
|
@@ -546,6 +565,9 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
546
565
|
// Epic, no agentType, or the persona has no model/'inherit' — --model
|
|
547
566
|
// must never be left unpinned (CLAUDE.md model-pinning rule).
|
|
548
567
|
const model = agentModelResolve.resolveEpicModel({ cwd, claudeSessionId: sessionId });
|
|
568
|
+
// Persona `effort:` (same overlay-aware persona, agentEffortResolve.cjs);
|
|
569
|
+
// effortArgs() yields NO tokens for inherit/absent/dangling.
|
|
570
|
+
const effortFlags = effortArgs(resolveEpicEffort({ cwd, claudeSessionId: sessionId }).effort);
|
|
549
571
|
|
|
550
572
|
// Main is authoritative about --resume vs --session-id AND the spawn cwd, and it decides
|
|
551
573
|
// them TOGETHER (epicSpawnPlan.cjs): a transcript under an encoding the spawn cwd cannot see
|
|
@@ -579,6 +601,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
579
601
|
const a = [
|
|
580
602
|
'-p', fullPrompt,
|
|
581
603
|
'--model', model,
|
|
604
|
+
...effortFlags,
|
|
582
605
|
'--dangerously-skip-permissions',
|
|
583
606
|
'--output-format', 'stream-json',
|
|
584
607
|
'--verbose',
|
|
@@ -695,7 +718,9 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
695
718
|
// Per-attempt state — a retry starts from empty buffers.
|
|
696
719
|
let lineBuffer = '';
|
|
697
720
|
let finalAssistantText = '';
|
|
698
|
-
|
|
721
|
+
const stderrBuffer = createHeadTailBuffer(STDERR_TAIL_MAX);
|
|
722
|
+
|
|
723
|
+
const stderrText = () => stderrBuffer.matchText();
|
|
699
724
|
|
|
700
725
|
const processLine = (line) => {
|
|
701
726
|
if (!line) return;
|
|
@@ -787,10 +812,25 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
787
812
|
processLine(lineBuffer.slice(0, nl).trim());
|
|
788
813
|
lineBuffer = lineBuffer.slice(nl + 1);
|
|
789
814
|
}
|
|
815
|
+
if (lineBuffer.length > LINE_BUFFER_MAX) {
|
|
816
|
+
// Un-terminated line past the cap: drop it rather than grow forever.
|
|
817
|
+
lineBuffer = '';
|
|
818
|
+
if (!lineOverflowLogged) {
|
|
819
|
+
lineOverflowLogged = true;
|
|
820
|
+
opsErrorLog.appendError({
|
|
821
|
+
cwd,
|
|
822
|
+
scope: 'chatRunner',
|
|
823
|
+
tabId,
|
|
824
|
+
tags: silent ? ['silent-probe'] : [],
|
|
825
|
+
message: `stdout line exceeded ${LINE_BUFFER_MAX} bytes without a newline; buffer dropped`,
|
|
826
|
+
meta: { sessionId, silent },
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
790
830
|
});
|
|
791
831
|
|
|
792
832
|
thisChild.stderr.on('data', (chunk) => {
|
|
793
|
-
stderrBuffer
|
|
833
|
+
stderrBuffer.append(chunk.toString('utf8'));
|
|
794
834
|
});
|
|
795
835
|
|
|
796
836
|
thisChild.on('error', (err) => {
|
|
@@ -819,10 +859,10 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
819
859
|
// terminal latch stay at executeRun level, so this is still one turn.
|
|
820
860
|
if (!terminalSent && !killed && code !== 0 && attemptNo === 1
|
|
821
861
|
&& finalAssistantText === '' && recentToolUses.length === 0
|
|
822
|
-
&& !/rate.?limit/i.test(
|
|
862
|
+
&& !/rate.?limit/i.test(stderrText())) {
|
|
823
863
|
const swapTo = resumeFlag
|
|
824
|
-
? (NO_CONVERSATION_RE.test(
|
|
825
|
-
: (SESSION_IN_USE_RE.test(
|
|
864
|
+
? (NO_CONVERSATION_RE.test(stderrText()) ? false : null)
|
|
865
|
+
: (SESSION_IN_USE_RE.test(stderrText()) ? true : null);
|
|
826
866
|
if (swapTo !== null) {
|
|
827
867
|
if (!silent) {
|
|
828
868
|
broadcast('chat:run:notice', {
|
|
@@ -851,7 +891,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
851
891
|
sessionId,
|
|
852
892
|
message: 'run cancelled',
|
|
853
893
|
});
|
|
854
|
-
} else if (SESSION_IN_USE_RE.test(
|
|
894
|
+
} else if (SESSION_IN_USE_RE.test(stderrText()) || NO_CONVERSATION_RE.test(stderrText())) {
|
|
855
895
|
// A session-flag rejection that survived the one-shot swap (or was
|
|
856
896
|
// not eligible for it). The raw stderr dump names neither the flag,
|
|
857
897
|
// the cwd nor the transcript — a real report cost a full diagnostic
|
|
@@ -870,9 +910,8 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
870
910
|
+ 'the session-flag resolution logic lives in main, and a stale build resolves it with older rules.',
|
|
871
911
|
});
|
|
872
912
|
} else {
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
: '';
|
|
913
|
+
const preview = stderrBuffer.preview();
|
|
914
|
+
const errDetail = preview ? `: ${preview}` : '';
|
|
876
915
|
emitTerminal('chat:run:error', {
|
|
877
916
|
tabId,
|
|
878
917
|
sessionId,
|
|
@@ -991,6 +1030,7 @@ module.exports = {
|
|
|
991
1030
|
run,
|
|
992
1031
|
cancel,
|
|
993
1032
|
__resetQueueForTests,
|
|
1033
|
+
__liveSessionCount: () => liveSessions.size,
|
|
994
1034
|
attachWindow,
|
|
995
1035
|
registerChatHandlers,
|
|
996
1036
|
parseStopSignal,
|
package/src/main/git.cjs
CHANGED
|
@@ -23,6 +23,7 @@ const { spawn } = require('node:child_process');
|
|
|
23
23
|
const path = require('node:path');
|
|
24
24
|
const { schemas } = require('./ipcSchemas.cjs');
|
|
25
25
|
const { validatePath } = require('./config.cjs');
|
|
26
|
+
const { LRUCache } = require('./lib/lruCache.cjs');
|
|
26
27
|
|
|
27
28
|
const GIT_TIMEOUT_MS = 5_000;
|
|
28
29
|
const CACHE_TTL_MS = 5_000;
|
|
@@ -30,8 +31,11 @@ const MAX_BUFFER = 10 * 1024 * 1024; // 10 MiB — same cap Unleashed uses
|
|
|
30
31
|
|
|
31
32
|
// Per-cwd in-memory caches. Keyed by the realpath-validated cwd so symlinks
|
|
32
33
|
// to the same repo dedupe. Each entry: { value, expiresAt }.
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// Bounded LRU: worktree cwds are unique per Epic/job, so an unbounded Map grew
|
|
35
|
+
// for the app's lifetime and pinned every files[] array.
|
|
36
|
+
const CACHE_MAX_CWDS = 16;
|
|
37
|
+
const statusCache = new LRUCache(CACHE_MAX_CWDS);
|
|
38
|
+
const fileStatusCache = new LRUCache(CACHE_MAX_CWDS);
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
41
|
* Run `git <args>` in cwd with a hard 5s SIGKILL ceiling. Resolves
|
|
@@ -241,6 +245,7 @@ async function getStatus(cwd) {
|
|
|
241
245
|
const now = Date.now();
|
|
242
246
|
const cached = statusCache.get(realCwd);
|
|
243
247
|
if (cached && cached.expiresAt > now) return cached.value;
|
|
248
|
+
if (cached) statusCache.delete(realCwd);
|
|
244
249
|
|
|
245
250
|
// Single porcelain call gets us branch + ahead/behind + files in one shot.
|
|
246
251
|
const result = await runGit(
|
|
@@ -281,6 +286,7 @@ async function getFileStatus(cwd) {
|
|
|
281
286
|
const now = Date.now();
|
|
282
287
|
const cached = fileStatusCache.get(realCwd);
|
|
283
288
|
if (cached && cached.expiresAt > now) return cached.value;
|
|
289
|
+
if (cached) fileStatusCache.delete(realCwd);
|
|
284
290
|
|
|
285
291
|
const result = await runGit(
|
|
286
292
|
['status', '--porcelain=v1', '-u', '-z'],
|
|
@@ -330,4 +336,5 @@ module.exports = {
|
|
|
330
336
|
parsePorcelain,
|
|
331
337
|
mapStatus,
|
|
332
338
|
clearCache,
|
|
339
|
+
_caches: { statusCache, fileStatusCache, CACHE_MAX_CWDS },
|
|
333
340
|
};
|
|
@@ -49,25 +49,8 @@ function resolvePricingKey(modelId) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// ── LRU cache ─────────────────────────────────────────────────────────────────
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
constructor(max) {
|
|
55
|
-
this._max = max;
|
|
56
|
-
this._m = new Map();
|
|
57
|
-
}
|
|
58
|
-
get(k) {
|
|
59
|
-
if (!this._m.has(k)) return undefined;
|
|
60
|
-
const v = this._m.get(k);
|
|
61
|
-
this._m.delete(k);
|
|
62
|
-
this._m.set(k, v);
|
|
63
|
-
return v;
|
|
64
|
-
}
|
|
65
|
-
set(k, v) {
|
|
66
|
-
this._m.delete(k);
|
|
67
|
-
this._m.set(k, v);
|
|
68
|
-
if (this._m.size > this._max) this._m.delete(this._m.keys().next().value);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
52
|
+
// Shared implementation: lib/lruCache.cjs (re-exported below for existing callers).
|
|
53
|
+
const { LRUCache } = require('./lib/lruCache.cjs');
|
|
71
54
|
|
|
72
55
|
/**
|
|
73
56
|
* Cache for parseJSONL results.
|
package/src/main/index.cjs
CHANGED
|
@@ -43,7 +43,9 @@ const epicWorktreeMerge = require('./lib/epicWorktreeMerge.cjs');
|
|
|
43
43
|
const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs');
|
|
44
44
|
const agentLibrary = require('./agentLibrary.cjs');
|
|
45
45
|
const agentModelResolve = require('./lib/agentModelResolve.cjs');
|
|
46
|
+
const agentEffortResolve = require('./lib/agentEffortResolve.cjs');
|
|
46
47
|
const { resolveEffectiveModelInfo } = require('./lib/effectiveModelInfo.cjs');
|
|
48
|
+
const { resolveModelCatalog } = require('./lib/modelCatalog.cjs');
|
|
47
49
|
const { checkDelegationReadiness, ensureGuardsInstalled, installPrdWriteGuard, installDestructiveGitGuard, installInlineImplementationGuard, installSelfScheduleGuard } = require('./lib/delegationReadiness.cjs');
|
|
48
50
|
const { resolveProjectContext } = require('./lib/projectRootResolve.cjs');
|
|
49
51
|
const upgradeDrain = require('./lib/upgradeDrain.cjs');
|
|
@@ -80,6 +82,7 @@ const { registerHistoryAggregatorHandlers, finalizeClosedDays, refreshIntradayTo
|
|
|
80
82
|
const runLogRetention = require('./lib/runLogRetention.cjs');
|
|
81
83
|
const { registerHistoryDashboardHandlers } = require('./historyDashboard.cjs');
|
|
82
84
|
const { tryAcquireLock, releaseLock, DEFAULT_LOCK_PATH } = require('./lib/watchdogHelpers.cjs');
|
|
85
|
+
const { createIntradayRefresh } = require('./lib/intradayRefresh.cjs');
|
|
83
86
|
const schedulerConfig = require('./lib/schedulerConfig.cjs');
|
|
84
87
|
const memoryTool = require('./memoryTool.cjs');
|
|
85
88
|
const { registerMemoryAggregateIpc } = require('./memoryAggregate.cjs');
|
|
@@ -202,14 +205,9 @@ const REBOOT_LOG = path.join(os.homedir(), '.claude', 'session-manager-reboot.lo
|
|
|
202
205
|
// finalize pass never interleave writes to the rollup file. A contended lock
|
|
203
206
|
// just means this tick is skipped — the next timer tick (or the next boot)
|
|
204
207
|
// retries, so skipping is always safe.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
.catch((e) => {
|
|
209
|
-
logs.writeLine({ scope: 'history-rollup', level: 'error', message: 'refreshIntradayToday failed', meta: { error: e?.message } });
|
|
210
|
-
})
|
|
211
|
-
.finally(() => releaseLock(DEFAULT_LOCK_PATH));
|
|
212
|
-
}
|
|
208
|
+
const runIntradayRefresh = createIntradayRefresh({
|
|
209
|
+
tryAcquireLock, releaseLock, lockPath: DEFAULT_LOCK_PATH, refresh: refreshIntradayToday, logs,
|
|
210
|
+
});
|
|
213
211
|
|
|
214
212
|
function logReboot(line) {
|
|
215
213
|
try {
|
|
@@ -442,6 +440,9 @@ function createWindow() {
|
|
|
442
440
|
});
|
|
443
441
|
});
|
|
444
442
|
|
|
443
|
+
// Bounded auto-recovery for a crashed / hung / failed-to-load renderer.
|
|
444
|
+
require('./lib/rendererRecovery.cjs').attachRendererRecovery(mainWindow.webContents, { logs });
|
|
445
|
+
|
|
445
446
|
// Native right-click menu — Copy / Paste / Select All everywhere. Roles
|
|
446
447
|
// hook into Electron's built-in clipboard/selection plumbing, which xterm.js
|
|
447
448
|
// (and Monaco, and Tiptap) all participate in via the standard DOM
|
|
@@ -565,6 +566,10 @@ ipcMain.handle('agents:get-persona-body', validated(schemas.agentsGetPersonaBody
|
|
|
565
566
|
// is merely supposed to. Never throws (resolveEpicModel's own contract).
|
|
566
567
|
ipcMain.handle('agents:resolve-epic-model', validated(schemas.agentsResolveEpicModel, (payload) =>
|
|
567
568
|
agentModelResolve.resolveEpicModel(payload)));
|
|
569
|
+
// Effort twin of the above (agentEffortResolve.cjs): Terminal appends
|
|
570
|
+
// `--effort <level>` only when `effort` is non-null. Never throws.
|
|
571
|
+
ipcMain.handle('agents:resolve-epic-effort', validated(schemas.agentsResolveEpicEffort, (payload) =>
|
|
572
|
+
agentEffortResolve.resolveEpicEffort(payload)));
|
|
568
573
|
|
|
569
574
|
// "What will this Epic actually run as?" (PRD: effective-model-resolver) —
|
|
570
575
|
// persona alias + evidence-based concrete model id + provenance, for a given
|
|
@@ -572,6 +577,10 @@ ipcMain.handle('agents:resolve-epic-model', validated(schemas.agentsResolveEpicM
|
|
|
572
577
|
ipcMain.handle('agents:resolve-model-info', validated(schemas.agentsResolveModelInfo, (payload) =>
|
|
573
578
|
resolveEffectiveModelInfo(payload)));
|
|
574
579
|
|
|
580
|
+
// Live model/effort catalog (modelCatalog.cjs) — never throws; `force` bypasses its cache.
|
|
581
|
+
ipcMain.handle('models:catalog', validated(schemas.modelsCatalog, (payload) =>
|
|
582
|
+
resolveModelCatalog(payload)));
|
|
583
|
+
|
|
575
584
|
// "Can this project actually delegate?" probe (PRD: delegation-readiness).
|
|
576
585
|
// Structured data over the preconditions for scheduler_create_prd being in an
|
|
577
586
|
// agent's tool list at all — surfacing it in the UI is a sibling PRD.
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
/**
|
|
2
3
|
* Zod schemas for all IPC handler payloads. Applied at the boundary between
|
|
3
4
|
* renderer and main — rejects malformed data before it reaches any business
|
|
@@ -152,20 +153,22 @@ const PROMPT_SESSION_ID_RE = /^(?!__proto__$|constructor$|prototype$)[A-Za-z0-9_
|
|
|
152
153
|
// renderer only logs the failure). Dropping just the offending row matches how
|
|
153
154
|
// the rest of this pipeline already fails: per-id (activeIndexMerge.cjs's
|
|
154
155
|
// tombstone/resurrection handling), never per-batch.
|
|
156
|
+
/** @param {unknown} raw */
|
|
155
157
|
const dropInvalidSessions = (raw) => {
|
|
156
158
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return raw;
|
|
159
|
+
const rec = /** @type {Record<string, unknown>} */ (raw);
|
|
157
160
|
const kept = Object.create(null);
|
|
158
|
-
for (const id of Object.keys(
|
|
161
|
+
for (const id of Object.keys(rec)) {
|
|
159
162
|
if (!PROMPT_SESSION_ID_RE.test(id)) {
|
|
160
163
|
console.warn(`[ipc] merge-active-index: dropping session with invalid id ${JSON.stringify(id)}`);
|
|
161
164
|
continue;
|
|
162
165
|
}
|
|
163
|
-
const parsed = PromptSessionSchema.safeParse(
|
|
166
|
+
const parsed = PromptSessionSchema.safeParse(rec[id]);
|
|
164
167
|
if (!parsed.success) {
|
|
165
168
|
console.warn(`[ipc] merge-active-index: dropping malformed session ${id} — ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`);
|
|
166
169
|
continue;
|
|
167
170
|
}
|
|
168
|
-
kept[id] =
|
|
171
|
+
kept[id] = rec[id];
|
|
169
172
|
}
|
|
170
173
|
return kept;
|
|
171
174
|
};
|
|
@@ -198,6 +201,9 @@ const promptSessionsCreateEpic = z.object({
|
|
|
198
201
|
goalText: z.string().min(1).max(20000),
|
|
199
202
|
tag: EpicTagSchema.optional(),
|
|
200
203
|
agentType: z.string().min(1).max(256).optional(),
|
|
204
|
+
// Per-Epic runtime overrides (New Session card) — absent = use the persona's own value.
|
|
205
|
+
model: z.string().min(1).max(256).optional(),
|
|
206
|
+
effort: z.string().min(1).max(64).optional(),
|
|
201
207
|
source: EpicSourceSchema.optional(),
|
|
202
208
|
// The full first-prompt body + its labeled sections (epicIntake.ts's
|
|
203
209
|
// composeEpicIntake) — both optional, since not every mint caller (e.g. a
|
|
@@ -661,6 +667,12 @@ const agentsResolveModelInfo = z.object({
|
|
|
661
667
|
agentType: z.string().min(1).max(200),
|
|
662
668
|
}).strict();
|
|
663
669
|
|
|
670
|
+
// ──────────────────────────────────────────── Live model/effort catalog (modelCatalog.cjs)
|
|
671
|
+
const modelsCatalog = z.object({
|
|
672
|
+
cwd: z.string().optional(),
|
|
673
|
+
force: z.boolean().optional(),
|
|
674
|
+
}).strict();
|
|
675
|
+
|
|
664
676
|
// ──────────────────────────────────────────── Epic launch model (PRD: unify-epic-model-resolution)
|
|
665
677
|
// Terminal's (EpicTerminalPane.tsx) call into agentModelResolve.cjs's
|
|
666
678
|
// resolveEpicModel — the same launch-model authority chatRunner.cjs's Chat
|
|
@@ -669,6 +681,7 @@ const agentsResolveEpicModel = z.object({
|
|
|
669
681
|
cwd: z.string().min(1).max(4096),
|
|
670
682
|
claudeSessionId: z.string().min(1).max(200),
|
|
671
683
|
}).strict();
|
|
684
|
+
const agentsResolveEpicEffort = agentsResolveEpicModel;
|
|
672
685
|
|
|
673
686
|
// ──────────────────────────────────────────── Project Pages (PRD 932)
|
|
674
687
|
// Same validation split as projectBriefCwd above: real path validation is
|
|
@@ -1036,6 +1049,13 @@ const telemetrySetConfig = z.object({
|
|
|
1036
1049
|
* call sites already rely on throw semantics for malformed input, so we keep
|
|
1037
1050
|
* that behavior for backwards compatibility.
|
|
1038
1051
|
*/
|
|
1052
|
+
/**
|
|
1053
|
+
* @template T
|
|
1054
|
+
* @template R
|
|
1055
|
+
* @param {{ parse: (payload: unknown) => T }} schema
|
|
1056
|
+
* @param {(parsed: T) => R} handler
|
|
1057
|
+
* @returns {(_event: unknown, payload: unknown) => R}
|
|
1058
|
+
*/
|
|
1039
1059
|
function validated(schema, handler) {
|
|
1040
1060
|
return (_event, payload) => {
|
|
1041
1061
|
const parsed = schema.parse(payload);
|
|
@@ -1146,7 +1166,9 @@ module.exports = {
|
|
|
1146
1166
|
agentsSavePersona,
|
|
1147
1167
|
agentsGetPersonaBody,
|
|
1148
1168
|
agentsResolveModelInfo,
|
|
1169
|
+
modelsCatalog,
|
|
1149
1170
|
agentsResolveEpicModel,
|
|
1171
|
+
agentsResolveEpicEffort,
|
|
1150
1172
|
delegationReadinessCwd,
|
|
1151
1173
|
telemetrySetConfig,
|
|
1152
1174
|
},
|
|
@@ -139,3 +139,183 @@ describe('childWithLog onExit leakedDescendants reporting (PRD 1110)', () => {
|
|
|
139
139
|
15000,
|
|
140
140
|
);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
// PRD 1353 — async enumeration must never block/alter the kill cascade, and a
|
|
144
|
+
// throwing watchdog must not break the wall-clock kill path.
|
|
145
|
+
describe('childWithLog async enumeration + guarded watchdog (PRD 1353)', () => {
|
|
146
|
+
const { _deps } = require('../childWithLog.cjs');
|
|
147
|
+
const realExecFile = _deps.execFile;
|
|
148
|
+
afterEach(() => { _deps.execFile = realExecFile; });
|
|
149
|
+
|
|
150
|
+
it.skipIf(process.platform !== 'linux')(
|
|
151
|
+
'enumeration failure omits the report, logs, and still kills the group',
|
|
152
|
+
async () => {
|
|
153
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-child-enumfail-'));
|
|
154
|
+
const logPath = path.join(dir, 'job.log');
|
|
155
|
+
const pidFile = path.join(dir, 'grandchild.pid');
|
|
156
|
+
const { fd, safeLog, closeFd } = openLog(logPath);
|
|
157
|
+
_deps.execFile = (_c, _a, _o, cb) => { setImmediate(() => cb(new Error('ps boom'))); };
|
|
158
|
+
|
|
159
|
+
const exitInfo = await new Promise((resolve) => {
|
|
160
|
+
withChildAndLog({
|
|
161
|
+
fd, logPath, safeLog, closeFd,
|
|
162
|
+
spawn: {
|
|
163
|
+
command: 'sh',
|
|
164
|
+
args: ['-c', `sleep 30 & echo $! > ${pidFile}; exec sleep 0.2`],
|
|
165
|
+
options: { detached: true },
|
|
166
|
+
},
|
|
167
|
+
onExit: (info) => { info.safeLog('onExit-ran\n'); resolve(info); },
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
expect(exitInfo.leakedDescendants).toEqual([]);
|
|
171
|
+
for (let i = 0; i < 20 && !fs.existsSync(pidFile); i++) await new Promise((r) => setTimeout(r, 50));
|
|
172
|
+
const gc = Number(fs.readFileSync(pidFile, 'utf8').trim());
|
|
173
|
+
await new Promise((r) => setTimeout(r, 500)); // SIGTERM lands immediately, no grace wait
|
|
174
|
+
expect(() => process.kill(gc, 0)).toThrow();
|
|
175
|
+
expect(fs.readFileSync(logPath, 'utf8')).toContain('enumeration failed');
|
|
176
|
+
},
|
|
177
|
+
15000,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
function runWatchdog(wd) {
|
|
181
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-child-wd-'));
|
|
182
|
+
const logPath = path.join(dir, 'job.log');
|
|
183
|
+
const { fd, safeLog, closeFd } = openLog(logPath);
|
|
184
|
+
let handle;
|
|
185
|
+
const done = new Promise((resolve) => {
|
|
186
|
+
handle = withChildAndLog({
|
|
187
|
+
fd, logPath, safeLog, closeFd,
|
|
188
|
+
spawn: { command: 'sleep', args: ['5'], options: {} },
|
|
189
|
+
watchdogs: [{ label: 'w', intervalMs: 20, ...wd }],
|
|
190
|
+
onExit: resolve,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
return { logPath, handle, done };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
it('a throwing shouldFire does not stop a later tick firing the action', async () => {
|
|
197
|
+
let calls = 0;
|
|
198
|
+
let fired = 0;
|
|
199
|
+
const { handle, logPath, done } = runWatchdog({
|
|
200
|
+
shouldFire: () => { calls++; if (calls < 3) throw new Error('sf boom'); return true; },
|
|
201
|
+
action: (ctx) => { fired++; ctx.killedByWatchdog = 'w'; ctx.killTree('SIGTERM'); },
|
|
202
|
+
});
|
|
203
|
+
const info = await done;
|
|
204
|
+
expect(fired).toBe(1);
|
|
205
|
+
expect(info.killedByWatchdog).toBe('w');
|
|
206
|
+
expect(fs.readFileSync(logPath, 'utf8')).toContain('watchdog "w" threw: sf boom');
|
|
207
|
+
handle.cancel();
|
|
208
|
+
}, 10000);
|
|
209
|
+
|
|
210
|
+
it('a throwing action clears its interval and is logged', async () => {
|
|
211
|
+
let should = 0;
|
|
212
|
+
let acts = 0;
|
|
213
|
+
const { handle, logPath, done } = runWatchdog({
|
|
214
|
+
shouldFire: () => { should++; return true; },
|
|
215
|
+
action: () => { acts++; throw new Error('act boom'); },
|
|
216
|
+
});
|
|
217
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
218
|
+
expect(acts).toBe(1);
|
|
219
|
+
expect(should).toBe(1);
|
|
220
|
+
expect(fs.readFileSync(logPath, 'utf8')).toContain('watchdog "w" threw: act boom');
|
|
221
|
+
handle.cancel();
|
|
222
|
+
await done;
|
|
223
|
+
}, 10000);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// PRD 1370 — enumerate BEFORE SIGTERM; no-enumeration paths stay synchronous.
|
|
227
|
+
describe('childWithLog sweep ordering + sync paths (PRD 1370)', () => {
|
|
228
|
+
const { _deps } = require('../childWithLog.cjs');
|
|
229
|
+
const realExecFile = _deps.execFile;
|
|
230
|
+
const realKill = process.kill;
|
|
231
|
+
afterEach(() => { _deps.execFile = realExecFile; process.kill = realKill; });
|
|
232
|
+
|
|
233
|
+
function setup(name) {
|
|
234
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), name));
|
|
235
|
+
const logPath = path.join(dir, 'job.log');
|
|
236
|
+
return { logPath, ...openLog(logPath) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
it.skipIf(process.platform !== 'linux').each([
|
|
240
|
+
['successful', (cb) => cb(null, 'PID PGRP %CPU ELAPSED COMMAND\n')],
|
|
241
|
+
['failing', (cb) => cb(new Error('ps boom'))],
|
|
242
|
+
])('SIGTERM waits for the %s enumeration', async (label, respond) => {
|
|
243
|
+
const { logPath, fd, safeLog, closeFd } = setup('sm-child-order-');
|
|
244
|
+
let psCb;
|
|
245
|
+
_deps.execFile = (_c, _a, _o, cb) => { psCb = cb; };
|
|
246
|
+
const groupKills = [];
|
|
247
|
+
process.kill = (pid, sig) => {
|
|
248
|
+
if (pid < -1 && sig === 'SIGTERM') { groupKills.push(pid); return true; }
|
|
249
|
+
return realKill.call(process, pid, sig);
|
|
250
|
+
};
|
|
251
|
+
let info;
|
|
252
|
+
const done = new Promise((resolve) => {
|
|
253
|
+
withChildAndLog({
|
|
254
|
+
fd, logPath, safeLog, closeFd,
|
|
255
|
+
spawn: { command: 'sh', args: ['-c', 'exit 0'], options: { detached: true } },
|
|
256
|
+
onExit: (i) => { info = i; resolve(); },
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
for (let i = 0; i < 100 && !psCb; i++) await new Promise((r) => setTimeout(r, 20));
|
|
260
|
+
expect(psCb).toBeTruthy();
|
|
261
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
262
|
+
expect(groupKills).toEqual([]);
|
|
263
|
+
respond(psCb);
|
|
264
|
+
await done; // onExit runs only after the kill was issued
|
|
265
|
+
expect(groupKills.length).toBe(1);
|
|
266
|
+
expect(info.leakedDescendants).toEqual([]);
|
|
267
|
+
if (label === 'failing') expect(fs.readFileSync(logPath, 'utf8')).toContain('enumeration failed');
|
|
268
|
+
}, 15000);
|
|
269
|
+
|
|
270
|
+
it('runs onExit synchronously when the spawn throws', () => {
|
|
271
|
+
const { logPath, fd, safeLog, closeFd } = setup('sm-child-spawnfail-');
|
|
272
|
+
let ran = false;
|
|
273
|
+
const r = withChildAndLog({
|
|
274
|
+
fd, logPath, safeLog, closeFd,
|
|
275
|
+
spawn: { command: 'sh', args: [], options: { cwd: '/nonexistent\0bad', detached: true } },
|
|
276
|
+
onExit: (i) => { ran = i.spawnFailed === true; },
|
|
277
|
+
});
|
|
278
|
+
expect(r.child).toBeNull();
|
|
279
|
+
expect(ran).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it.skipIf(process.platform !== 'linux')('a throwing deferred onExit is logged and still closes the fd', async () => {
|
|
283
|
+
const { logPath, fd, safeLog, closeFd } = setup('sm-child-throw-');
|
|
284
|
+
_deps.execFile = (_c, _a, _o, cb) => { setImmediate(() => cb(null, 'h\n')); };
|
|
285
|
+
const unhandled = [];
|
|
286
|
+
const h = (e) => unhandled.push(e);
|
|
287
|
+
process.on('unhandledRejection', h);
|
|
288
|
+
let closed = false;
|
|
289
|
+
const wrapClose = () => { closed = true; closeFd(); };
|
|
290
|
+
await new Promise((resolve) => {
|
|
291
|
+
withChildAndLog({
|
|
292
|
+
fd, logPath, safeLog, closeFd: wrapClose,
|
|
293
|
+
spawn: { command: 'sh', args: ['-c', 'exit 0'], options: { detached: true } },
|
|
294
|
+
onExit: () => { setImmediate(resolve); throw new Error('exit boom'); },
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
298
|
+
process.off('unhandledRejection', h);
|
|
299
|
+
expect(unhandled).toEqual([]);
|
|
300
|
+
expect(closed).toBe(true);
|
|
301
|
+
expect(fs.readFileSync(logPath, 'utf8')).toContain('onExit threw: exit boom');
|
|
302
|
+
}, 15000);
|
|
303
|
+
|
|
304
|
+
it('a throwing synchronous onExit is logged, closes the fd, and is re-thrown', async () => {
|
|
305
|
+
const { logPath, fd, safeLog, closeFd } = setup('sm-child-throw-sync-');
|
|
306
|
+
let closed = false;
|
|
307
|
+
const wrapClose = () => { closed = true; closeFd(); };
|
|
308
|
+
const thrown = await new Promise((resolve) => {
|
|
309
|
+
const onUncaught = (e) => { process.off('uncaughtException', onUncaught); resolve(e); };
|
|
310
|
+
process.on('uncaughtException', onUncaught);
|
|
311
|
+
withChildAndLog({
|
|
312
|
+
fd, logPath, safeLog, closeFd: wrapClose,
|
|
313
|
+
spawn: { command: 'sh', args: ['-c', 'exit 0'] },
|
|
314
|
+
onExit: () => { throw new Error('exit boom'); },
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
expect(thrown.message).toBe('exit boom');
|
|
318
|
+
expect(closed).toBe(true);
|
|
319
|
+
expect(fs.readFileSync(logPath, 'utf8')).toContain('onExit threw: exit boom');
|
|
320
|
+
}, 15000);
|
|
321
|
+
});
|
|
@@ -1154,3 +1154,22 @@ test('ensureGuardsInstalled: records every auto-install attempt in the ops error
|
|
|
1154
1154
|
expect(delegationLines.every((l) => l.level === 'info')).toBe(true);
|
|
1155
1155
|
expect(delegationLines.every((l) => l.meta?.action === 'installed')).toBe(true);
|
|
1156
1156
|
}, 15_000);
|
|
1157
|
+
|
|
1158
|
+
test('ensureGuardsInstalled: an already-installed re-run writes no new ops error-log rows', async () => {
|
|
1159
|
+
const cwd = await mkTmp('sm-ensure-guards-noop-');
|
|
1160
|
+
const homeDir = await mkTmp('sm-ensure-guards-noop-home-');
|
|
1161
|
+
|
|
1162
|
+
await ensureGuardsInstalled(cwd, { homeDir });
|
|
1163
|
+
const logFile = opsErrorLogTodayFile(cwd);
|
|
1164
|
+
const before = fs.readFileSync(logFile, 'utf8');
|
|
1165
|
+
|
|
1166
|
+
clearGuardsInstalledCache();
|
|
1167
|
+
const again = await ensureGuardsInstalled(cwd, { homeDir });
|
|
1168
|
+
expect(Object.values(again.guards).every((g) => g.action === 'already-installed')).toBe(true);
|
|
1169
|
+
expect(fs.readFileSync(logFile, 'utf8')).toBe(before);
|
|
1170
|
+
}, 15_000);
|
|
1171
|
+
|
|
1172
|
+
test('delegationReadiness.cjs source contains no raw NUL byte', () => {
|
|
1173
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'delegationReadiness.cjs'));
|
|
1174
|
+
expect(src.includes(0)).toBe(false);
|
|
1175
|
+
});
|
|
@@ -18,7 +18,7 @@ const fs = require('node:fs');
|
|
|
18
18
|
const path = require('node:path');
|
|
19
19
|
const os = require('node:os');
|
|
20
20
|
const { encodeCwd } = require('../encodeCwd.cjs');
|
|
21
|
-
const { resolveEffectiveModelInfo, isConcreteModelId,
|
|
21
|
+
const { resolveEffectiveModelInfo, isConcreteModelId, computeEffortEnvReachable } = require('../effectiveModelInfo.cjs');
|
|
22
22
|
// config.cjs's `allowedRoots` is a module-level singleton seeded from
|
|
23
23
|
// os.homedir() at FIRST require — since this test's HOME changes every test
|
|
24
24
|
// (fresh mkdtemp), a persona lookup (getPersonaBody -> validatePath) would
|
|
@@ -223,17 +223,22 @@ test('isConcreteModelId treats known CLI aliases as non-concrete and everything
|
|
|
223
223
|
expect(isConcreteModelId('opus')).toBe(false);
|
|
224
224
|
expect(isConcreteModelId('sonnet')).toBe(false);
|
|
225
225
|
expect(isConcreteModelId('haiku')).toBe(false);
|
|
226
|
+
expect(isConcreteModelId('opus[1m]')).toBe(false);
|
|
227
|
+
expect(isConcreteModelId('best')).toBe(false);
|
|
228
|
+
expect(isConcreteModelId('opusplan')).toBe(false);
|
|
229
|
+
expect(isConcreteModelId('default')).toBe(false);
|
|
230
|
+
expect(isConcreteModelId('claude-opus-4-7[1m]')).toBe(true);
|
|
226
231
|
expect(isConcreteModelId('fable')).toBe(false);
|
|
227
232
|
expect(isConcreteModelId(null)).toBe(false);
|
|
228
233
|
expect(isConcreteModelId('claude-opus-5')).toBe(true);
|
|
229
234
|
});
|
|
230
235
|
|
|
231
|
-
test('
|
|
232
|
-
expect(
|
|
236
|
+
test('effortEnvReachable is always false for an app-launched session — cleanChildEnv strips CLAUDE_EFFORT and CLAUDE_CODE_* before a child ever sees them', () => {
|
|
237
|
+
expect(computeEffortEnvReachable({})).toBe(false);
|
|
233
238
|
});
|
|
234
239
|
|
|
235
|
-
test('resolveEffectiveModelInfo surfaces
|
|
240
|
+
test('resolveEffectiveModelInfo surfaces effortEnvReachable alongside the model fields', async () => {
|
|
236
241
|
writeGlobalPersona('any', ['model: sonnet']);
|
|
237
242
|
const info = await resolveEffectiveModelInfo({ cwd, agentType: 'any' });
|
|
238
|
-
expect(info.
|
|
243
|
+
expect(info.effortEnvReachable).toBe(false);
|
|
239
244
|
});
|