claude-code-session-manager 0.62.0 → 0.63.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/AgentLibrary-BeJJa_zv.js +1 -0
- package/dist/assets/History-mWbqemhZ.js +2 -0
- package/dist/assets/Hooks-D_8sciZu.js +3 -0
- package/dist/assets/HostBilko-6aUJ7Smz.js +1 -0
- package/dist/assets/Library-CG1LDXLw.js +46 -0
- package/dist/assets/ListDetail-CIIJOWwI.js +1 -0
- package/dist/assets/MarkdownEditor-D0-y8V-f.js +1 -0
- package/dist/assets/McpServers-Kc-djiVi.js +2 -0
- package/dist/assets/Memory-B60DGRTW.js +8 -0
- package/dist/assets/Panel-CbTPsYHq.js +1 -0
- package/dist/assets/Permissions-C8KrSfrd.js +3 -0
- package/dist/assets/Plugins-BT8EumHb.js +2 -0
- package/dist/assets/ProvenanceBadge-cEqPpFsT.js +1 -0
- package/dist/assets/Scheduler-DbKPd0or.js +14 -0
- package/dist/assets/ScopeSwitcher-BNesCJ_t.js +1 -0
- package/dist/assets/Settings-Bjt7AQ6G.js +3 -0
- package/dist/assets/SkillReferenceGraph-DzuaPDM-.js +46 -0
- package/dist/assets/Skills-CVL_4X3T.js +3 -0
- package/dist/assets/SystemPrompt-Ct__nFF1.js +1 -0
- package/dist/assets/TagLibrary-DmrTfw4D.js +1 -0
- package/dist/assets/{TiptapBody-Bn-LMce4.js → TiptapBody-DaS0M_Ni.js} +1 -1
- package/dist/assets/Toggle-C0a-6xV7.js +1 -0
- package/dist/assets/index-CO_7DroC.js +3066 -0
- package/dist/assets/{index-DOXvph-Z.css → index-LlWpj2VJ.css} +1 -1
- package/dist/assets/listSkills-QORduPIk.js +1 -0
- package/dist/assets/settingsSchema-B7dMJaix.js +3 -0
- package/dist/assets/skillFrontmatter-Dif5JIg7.js +10 -0
- package/dist/index.html +2 -2
- package/package.json +1 -3
- package/src/main/__tests__/config-readText-bounded.test.cjs +84 -0
- package/src/main/__tests__/heapSnapshot.test.cjs +121 -0
- package/src/main/__tests__/historyAggregatorIntraday.test.cjs +313 -0
- package/src/main/__tests__/runLogRetention.test.cjs +343 -0
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +215 -0
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +42 -1
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +54 -1
- package/src/main/__tests__/transcripts-batch-flush.test.cjs +249 -0
- package/src/main/__tests__/transcripts-doFlush-array.test.cjs +6 -0
- package/src/main/__tests__/transcripts-paged-reads.test.cjs +6 -0
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +6 -0
- package/src/main/config.cjs +32 -29
- package/src/main/files.cjs +20 -0
- package/src/main/health.cjs +32 -0
- package/src/main/heapSnapshot.cjs +122 -0
- package/src/main/historyAggregator.cjs +176 -36
- package/src/main/index.cjs +10 -63
- package/src/main/ipcSchemas.cjs +20 -209
- package/src/main/lib/opsOwnership.cjs +0 -2
- package/src/main/lib/runLogRetention.cjs +358 -0
- package/src/main/lib/summarize.cjs +0 -1
- package/src/main/runVerify.cjs +55 -6
- package/src/main/scheduler.cjs +46 -7
- package/src/main/transcripts.cjs +29 -5
- package/src/preload/api.d.ts +18 -199
- package/src/preload/index.cjs +12 -102
- package/dist/assets/index-BfiMRkln.js +0 -3225
- package/src/main/__tests__/browserAgentServer.test.cjs +0 -235
- package/src/main/__tests__/browserView-destroyed-handler.test.cjs +0 -85
- package/src/main/__tests__/browserView-oauth-popup.test.cjs +0 -126
- package/src/main/__tests__/web-remote-e2e-pinning.test.cjs +0 -181
- package/src/main/browserAgentServer.cjs +0 -224
- package/src/main/browserCapture.cjs +0 -728
- package/src/main/browserView.cjs +0 -916
- package/src/main/lib/__tests__/browserAgentActions.test.cjs +0 -76
- package/src/main/lib/browserAgentActions.cjs +0 -114
- package/src/main/lib/e2eStateMachine.cjs +0 -39
- package/src/main/webRemote.cjs +0 -1367
- package/src/preload/browserViewPreload.cjs +0 -118
package/src/main/runVerify.cjs
CHANGED
|
@@ -157,6 +157,7 @@ function parseLog(logPath) {
|
|
|
157
157
|
const events = [];
|
|
158
158
|
let resultEvent = null;
|
|
159
159
|
let seq = 0;
|
|
160
|
+
let transcriptCommitLanded = false;
|
|
160
161
|
|
|
161
162
|
for (const line of text.split('\n')) {
|
|
162
163
|
const trimmed = line.trim();
|
|
@@ -166,6 +167,18 @@ function parseLog(logPath) {
|
|
|
166
167
|
try { obj = JSON.parse(trimmed); } catch { continue; }
|
|
167
168
|
if (!obj || typeof obj !== 'object') continue;
|
|
168
169
|
|
|
170
|
+
// Harness-emitted commit evidence: git itself produced these lines (a
|
|
171
|
+
// `vcs_state_changed` system event, or a tool_use_result carrying a
|
|
172
|
+
// gitOperation), so the model cannot fabricate them. Detected here as a
|
|
173
|
+
// plain boolean scan — never pushed into `events`, so the index-based
|
|
174
|
+
// final-20% math elsewhere is unaffected.
|
|
175
|
+
if (obj.type === 'system' && obj.subtype === 'vcs_state_changed' && obj.kind === 'commit') {
|
|
176
|
+
transcriptCommitLanded = true;
|
|
177
|
+
}
|
|
178
|
+
if (obj.tool_use_result?.gitOperation?.commit?.kind === 'committed') {
|
|
179
|
+
transcriptCommitLanded = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
// Final result event.
|
|
170
183
|
if (obj.type === 'result') {
|
|
171
184
|
const ev = {
|
|
@@ -215,7 +228,7 @@ function parseLog(logPath) {
|
|
|
215
228
|
}
|
|
216
229
|
}
|
|
217
230
|
|
|
218
|
-
return { events, resultEvent, error: null };
|
|
231
|
+
return { events, resultEvent, error: null, transcriptCommitLanded };
|
|
219
232
|
}
|
|
220
233
|
|
|
221
234
|
// ─── self-recovery helpers ────────────────────────────────────────────────────
|
|
@@ -591,6 +604,27 @@ function isAncestorCommit({ cwd, sha, timeoutMs = GH_CHECK_TIMEOUT_MS, execImpl
|
|
|
591
604
|
}
|
|
592
605
|
}
|
|
593
606
|
|
|
607
|
+
/**
|
|
608
|
+
* True when `cwd` is inside a git repository. Used to gate the transcript
|
|
609
|
+
* commit-evidence fallback below: a job whose cwd is not a git repo can never
|
|
610
|
+
* produce committedDuringRun=true (gitHead()/committedInWindow() fail closed
|
|
611
|
+
* there), so the fallback must only apply in that narrow case — never when
|
|
612
|
+
* git ancestry is actually observable.
|
|
613
|
+
*/
|
|
614
|
+
function isGitRepo(cwd) {
|
|
615
|
+
if (!cwd) return false;
|
|
616
|
+
try {
|
|
617
|
+
execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
|
|
618
|
+
timeout: 10_000,
|
|
619
|
+
encoding: 'utf8',
|
|
620
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
621
|
+
});
|
|
622
|
+
return true;
|
|
623
|
+
} catch {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
594
628
|
// ─── already-shipped postcondition exemption (original PRD re-runs) ─────────
|
|
595
629
|
|
|
596
630
|
const PRD_DELIVERABLE_PATH_RE = /(?:^|[`\s(])((?:src|scripts|session-manager-operations|test|tests|docs|bin)\/[A-Za-z0-9_./-]*[A-Za-z0-9_-]\.[A-Za-z0-9]+)\b/g;
|
|
@@ -743,12 +777,21 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
743
777
|
}
|
|
744
778
|
|
|
745
779
|
// ── 2. Parse log ───────────────────────────────────────────────────────
|
|
746
|
-
const { events, resultEvent, error: parseError } = parseLog(logPath);
|
|
780
|
+
const { events, resultEvent, error: parseError, transcriptCommitLanded } = parseLog(logPath);
|
|
747
781
|
|
|
748
782
|
if (parseError) {
|
|
749
783
|
return conclude('verify_unavailable', `log unreadable: ${parseError}`, 'needs_review');
|
|
750
784
|
}
|
|
751
785
|
|
|
786
|
+
// A job whose cwd is not a git repo (e.g. an investigation fix-plan that
|
|
787
|
+
// inherited cwd: /tmp) can never produce committedDuringRun=true —
|
|
788
|
+
// gitHead()/committedInWindow() both fail closed there. In that case
|
|
789
|
+
// ONLY, fall back to the harness-emitted commit evidence in the
|
|
790
|
+
// transcript, which git itself produced and the model cannot fabricate.
|
|
791
|
+
const cwdIsGitRepo = isGitRepo(queueEntry?.cwd);
|
|
792
|
+
const commitEvidence = committedDuringRun || (!cwdIsGitRepo && transcriptCommitLanded);
|
|
793
|
+
const commitEvidenceSource = (!committedDuringRun && commitEvidence) ? 'transcript' : null;
|
|
794
|
+
|
|
752
795
|
// ── 3. HALT detection ─────────────────────────────────────────────────
|
|
753
796
|
// Primary: check the final `{"type":"result"}` event's `result` text.
|
|
754
797
|
if (resultEvent) {
|
|
@@ -865,7 +908,7 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
865
908
|
// committed anything nor ever emitted the finish-protocol sentinel likely
|
|
866
909
|
// ended before doing real work (e.g. stopped on a clarifying question).
|
|
867
910
|
// Weaker evidence than a caught transcript error, but still not clean.
|
|
868
|
-
if (sentinel === null && !
|
|
911
|
+
if (sentinel === null && !commitEvidence) {
|
|
869
912
|
issues.push({
|
|
870
913
|
verdict: 'no_verdict_sentinel',
|
|
871
914
|
reason: 'run made no commit and emitted no SCHEDULER_VERDICT sentinel — likely ended before the finish protocol (e.g. stopped on a clarifying question)',
|
|
@@ -894,7 +937,7 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
894
937
|
// bare PASS with no commit from one is still a strong silent-failure
|
|
895
938
|
// signal and stays covered by this check.
|
|
896
939
|
const isFixPlanJob = /^\d+-fix-/.test(queueEntry?.slug || '');
|
|
897
|
-
if (sentinel === 'pass' && !
|
|
940
|
+
if (sentinel === 'pass' && !commitEvidence && !isFixPlanJob) {
|
|
898
941
|
// EXEMPTION: `-merge-main` PRDs (see isMergeMainSlug) can genuinely and
|
|
899
942
|
// correctly find nothing left to do when an out-of-band actor (a human,
|
|
900
943
|
// another agent, a sibling scheduler job) already merged/updated the
|
|
@@ -1015,13 +1058,18 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
1015
1058
|
// MUST NOT apply to halt or deps_unmet — those keep their existing semantics.
|
|
1016
1059
|
if (
|
|
1017
1060
|
sentinel === 'pass'
|
|
1018
|
-
&&
|
|
1061
|
+
&& commitEvidence
|
|
1019
1062
|
&& (top.verdict === 'transcript_errors' || top.verdict === 'verify_unavailable')
|
|
1020
1063
|
) {
|
|
1021
1064
|
return conclude('clean',
|
|
1022
1065
|
`SCHEDULER_VERDICT: PASS + commit landed overrides ${top.verdict}`,
|
|
1023
1066
|
null,
|
|
1024
|
-
{
|
|
1067
|
+
{
|
|
1068
|
+
...(annotations.length ? { annotations } : {}),
|
|
1069
|
+
sentinel,
|
|
1070
|
+
sentinelOverride: top.verdict,
|
|
1071
|
+
...(commitEvidenceSource ? { commitEvidenceSource } : {}),
|
|
1072
|
+
},
|
|
1025
1073
|
);
|
|
1026
1074
|
}
|
|
1027
1075
|
|
|
@@ -1073,4 +1121,5 @@ module.exports = {
|
|
|
1073
1121
|
extractPrdDeliverablePaths,
|
|
1074
1122
|
allDeliverablesAlreadyTracked,
|
|
1075
1123
|
isAncestorCommit,
|
|
1124
|
+
isGitRepo,
|
|
1076
1125
|
};
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -271,6 +271,28 @@ function gitHead(cwd) {
|
|
|
271
271
|
});
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
// True when cwd is inside a git repository. Used to keep a non-git cwd (e.g.
|
|
275
|
+
// a scratch dir like /tmp) from ever being handed to an investigation's
|
|
276
|
+
// fix-plan as its cwd — the commit guard, worktree isolation, and
|
|
277
|
+
// runVerify's PASS+commit sentinel override all read git state from job.cwd,
|
|
278
|
+
// so a fix plan rooted at a non-repo path gets parked in needs_review even
|
|
279
|
+
// when its commit really landed (Incident: 99-fix-e2e-needs-review-test,
|
|
280
|
+
// 2026-08-07 — green, committed 6ecbd40, PASS sentinel, still needs_review).
|
|
281
|
+
// Bounded and never throws.
|
|
282
|
+
function isGitRepoSync(cwd) {
|
|
283
|
+
if (!cwd) return false;
|
|
284
|
+
try {
|
|
285
|
+
execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
|
|
286
|
+
timeout: 10_000,
|
|
287
|
+
windowsHide: true,
|
|
288
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
289
|
+
});
|
|
290
|
+
return true;
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
274
296
|
// Best-effort `git fetch --all --prune` in cwd, bounded and never throwing.
|
|
275
297
|
// A PRD that does its work in a separate `git worktree add` checkout (per
|
|
276
298
|
// standards.md's own recommended pattern for shared repos) commits and pushes
|
|
@@ -2666,6 +2688,10 @@ ${logTail}
|
|
|
2666
2688
|
estimateMinutes: <your time estimate>
|
|
2667
2689
|
---
|
|
2668
2690
|
\`\`\`
|
|
2691
|
+
\`cwd\` must be the git repo root where the fix will actually land. If the failed job's cwd is
|
|
2692
|
+
not that repo (e.g. a scratch dir like \`/tmp\`), set \`cwd:\` to the correct repo root instead —
|
|
2693
|
+
the scheduler's commit guard and post-run verifier read git state from this path, and a
|
|
2694
|
+
non-repo cwd parks even a fully green, committed run in \`needs_review\`.
|
|
2669
2695
|
6. The PRD body MUST be self-contained — \`claude -p\` runs it on a fresh Sonnet session with NO conversation context. Include:
|
|
2670
2696
|
- Root-cause analysis (what went wrong and why)
|
|
2671
2697
|
- Concrete fix steps (specific files / commands / edits)
|
|
@@ -2803,6 +2829,16 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
2803
2829
|
console.warn(`[scheduler] investigation cwd missing (${cwd}); falling back to ${DEFAULT_PROJECT_CWD}`);
|
|
2804
2830
|
cwd = DEFAULT_PROJECT_CWD;
|
|
2805
2831
|
}
|
|
2832
|
+
// A non-git cwd is unusable as a fix-plan cwd: the commit guard, worktree
|
|
2833
|
+
// isolation, and runVerify's PASS+commit sentinel override all read git
|
|
2834
|
+
// state from job.cwd, so a fix plan rooted at e.g. /tmp gets parked in
|
|
2835
|
+
// needs_review even when its commit really landed. (Incident:
|
|
2836
|
+
// 99-fix-e2e-needs-review-test, 2026-08-07 — green, committed 6ecbd40,
|
|
2837
|
+
// PASS sentinel, still needs_review.)
|
|
2838
|
+
if (!isGitRepoSync(cwd)) {
|
|
2839
|
+
console.warn(`[scheduler] investigation cwd is not a git repo (${cwd}); falling back to ${DEFAULT_PROJECT_CWD}`);
|
|
2840
|
+
cwd = DEFAULT_PROJECT_CWD;
|
|
2841
|
+
}
|
|
2806
2842
|
const prompt = buildInvestigationPrompt({ failedJob, cwd, failedLogPath, originalBody, logTail, fixPath, group });
|
|
2807
2843
|
|
|
2808
2844
|
// Phase 1: open log fd for pre-spawn diagnostics.
|
|
@@ -3024,11 +3060,12 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
3024
3060
|
await setPaused('rate_limit', resetIso);
|
|
3025
3061
|
}
|
|
3026
3062
|
|
|
3027
|
-
// Stale queue entry: the PRD already shipped
|
|
3028
|
-
//
|
|
3029
|
-
//
|
|
3030
|
-
//
|
|
3031
|
-
|
|
3063
|
+
// Stale queue entry: the PRD was archived (already shipped) or is gone
|
|
3064
|
+
// from disk entirely (deleted before dispatch). Both are retire-not-fail
|
|
3065
|
+
// results from prdArchivedSkipResult with exitCode 0 and no transcript —
|
|
3066
|
+
// treat any truthy skip reason as a plain completion: no verify pass, no
|
|
3067
|
+
// commit guard, no RCA feedback item.
|
|
3068
|
+
if (res.skipped) {
|
|
3032
3069
|
await mutate((s) => {
|
|
3033
3070
|
const idx = s.jobs.findIndex((x) => x.slug === job.slug);
|
|
3034
3071
|
if (idx >= 0) {
|
|
@@ -4765,7 +4802,9 @@ async function init() {
|
|
|
4765
4802
|
}
|
|
4766
4803
|
}
|
|
4767
4804
|
|
|
4768
|
-
// remote —
|
|
4805
|
+
// remote — in-process (non-IPC) scheduler accessors, used by prdCreate.cjs
|
|
4806
|
+
// and other main-process callers. (Named for the retired web-remote relay,
|
|
4807
|
+
// its original consumer; kept because it still has in-process callers.)
|
|
4769
4808
|
const remote = {
|
|
4770
4809
|
// `cwd` is optional: prdCreate.cjs's create flow passes the target
|
|
4771
4810
|
// project's cwd explicitly (the file may not exist yet, so there's
|
|
@@ -4956,4 +4995,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
|
4956
4995
|
});
|
|
4957
4996
|
}
|
|
4958
4997
|
|
|
4959
|
-
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, healRefusalReason, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, notifyNeedsReview, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, candidateArchivedPrdsDirs, resolveArchivedPrdStatus, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, resolveVerifyPrdPath, resolveNotifyPrd, runPrdMigration, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult };
|
|
4998
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, healRefusalReason, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, isGitRepoSync, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, notifyNeedsReview, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, candidateArchivedPrdsDirs, resolveArchivedPrdStatus, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, resolveVerifyPrdPath, resolveNotifyPrd, runPrdMigration, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult, spawnJob };
|
package/src/main/transcripts.cjs
CHANGED
|
@@ -126,8 +126,23 @@ async function readDelta(sub) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Cap on the number of events carried in a single `transcript:event:<tabId>`
|
|
130
|
+
// IPC message. One flush can classify a large delta (e.g. a rotated/replayed
|
|
131
|
+
// transcript, or the 8 MB MAX_DELTA_BYTES window) into far more than a
|
|
132
|
+
// screenful of events — sending them all as one unbounded array risks a
|
|
133
|
+
// single oversized IPC payload. Above this count, doFlush sends multiple
|
|
134
|
+
// ordered batches instead of one giant one; each still lands as one store
|
|
135
|
+
// commit on the renderer side.
|
|
136
|
+
const MAX_EVENTS_PER_BATCH = 200;
|
|
137
|
+
|
|
129
138
|
async function doFlush(sub, { emit = true } = {}) {
|
|
130
139
|
const lines = await readDelta(sub);
|
|
140
|
+
let batch = [];
|
|
141
|
+
const flushBatch = () => {
|
|
142
|
+
if (batch.length === 0) return;
|
|
143
|
+
if (emit) sendIfAlive(window, `transcript:event:${sub.tabId}`, batch);
|
|
144
|
+
batch = [];
|
|
145
|
+
};
|
|
131
146
|
for (const line of lines) {
|
|
132
147
|
// Index every line — including ones that fail to parse — so line numbers
|
|
133
148
|
// and byte offsets stay correct for paged reads regardless of content.
|
|
@@ -142,13 +157,13 @@ async function doFlush(sub, { emit = true } = {}) {
|
|
|
142
157
|
const ref = { filePath: sub.filePath, byteOffset: line.byteOffset, byteLength: line.byteLength };
|
|
143
158
|
const events = classifyLine(obj, ref);
|
|
144
159
|
for (const ev of events) {
|
|
145
|
-
|
|
160
|
+
batch.push(ev);
|
|
146
161
|
// Mirror to OTEL — no-op when disabled. We emit on the initial drain too
|
|
147
162
|
// so backfilled transcripts show up in the trace store. One span per
|
|
148
|
-
// emitted event, not per line. doFlush only ever processes
|
|
149
|
-
// line once (readDelta never re-returns already-consumed
|
|
150
|
-
// this can't double-record — paged re-reads (readPage) go
|
|
151
|
-
// separate code path below that never touches OTEL.
|
|
163
|
+
// emitted event, not per line or per batch. doFlush only ever processes
|
|
164
|
+
// a given line once (readDelta never re-returns already-consumed
|
|
165
|
+
// bytes), so this can't double-record — paged re-reads (readPage) go
|
|
166
|
+
// through a separate code path below that never touches OTEL.
|
|
152
167
|
otel.recordTranscriptEvent({
|
|
153
168
|
tabId: sub.tabId,
|
|
154
169
|
tabCwd: sub.cwd,
|
|
@@ -156,8 +171,10 @@ async function doFlush(sub, { emit = true } = {}) {
|
|
|
156
171
|
data: ev.data,
|
|
157
172
|
ts: Date.now(),
|
|
158
173
|
});
|
|
174
|
+
if (batch.length >= MAX_EVENTS_PER_BATCH) flushBatch();
|
|
159
175
|
}
|
|
160
176
|
}
|
|
177
|
+
flushBatch();
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
/**
|
|
@@ -525,4 +542,11 @@ module.exports = {
|
|
|
525
542
|
// particular) — asserting a memory ceiling requires inspecting what's
|
|
526
543
|
// actually held, not just what a read API returns.
|
|
527
544
|
__getSubForTest: (tabId) => subs.get(tabId),
|
|
545
|
+
// Test-only: run a live (emit:true) flush directly against a subscription,
|
|
546
|
+
// without going through the chokidar watcher — lets batching/IPC-shape
|
|
547
|
+
// tests stay deterministic instead of racing a filesystem watch event.
|
|
548
|
+
__doFlushForTest: (sub, opts) => doFlush(sub, opts),
|
|
549
|
+
// Batch size cap for a single transcript:event IPC message — exported so
|
|
550
|
+
// tests can size fixtures against it without a magic-number duplicate.
|
|
551
|
+
MAX_EVENTS_PER_BATCH,
|
|
528
552
|
};
|
package/src/preload/api.d.ts
CHANGED
|
@@ -29,82 +29,6 @@ export interface WriteErrorEvent {
|
|
|
29
29
|
reason: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export interface BrowserNavState {
|
|
33
|
-
url: string;
|
|
34
|
-
title: string;
|
|
35
|
-
canGoBack: boolean;
|
|
36
|
-
canGoForward: boolean;
|
|
37
|
-
loading: boolean;
|
|
38
|
-
isSecure: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** One captured recorder step (PRD 408 engine → PRD 409 panel). */
|
|
42
|
-
export interface RecordStep {
|
|
43
|
-
n: number;
|
|
44
|
-
/** `select` is accepted by replay/export (PRD 410) for forward-compat;
|
|
45
|
-
* the live engine does not emit it yet. */
|
|
46
|
-
verb: 'navigate' | 'click' | 'type' | 'select' | 'wait-for' | 'drag';
|
|
47
|
-
target: string;
|
|
48
|
-
kind?: 'nav' | 'assert';
|
|
49
|
-
/** True for `type` steps — the actual typed value is never captured. */
|
|
50
|
-
masked?: boolean;
|
|
51
|
-
/** Engine-suggested `{{var}}` name for a `type` step (e.g. field name). */
|
|
52
|
-
variableSuggestion?: string;
|
|
53
|
-
/** Renderer-owned: set once the user checks "parameterize as {{var}}". */
|
|
54
|
-
variable?: string | null;
|
|
55
|
-
/** `select` steps only — the option value to choose on replay/export. */
|
|
56
|
-
value?: string;
|
|
57
|
-
/** Click position, or drag-start position for `drag` steps. */
|
|
58
|
-
x?: number;
|
|
59
|
-
/** Click position, or drag-start position for `drag` steps. */
|
|
60
|
-
y?: number;
|
|
61
|
-
/** `drag` steps only — drag-end target selector. */
|
|
62
|
-
endTarget?: string;
|
|
63
|
-
/** `drag` steps only — drag-end x position. */
|
|
64
|
-
endX?: number;
|
|
65
|
-
/** `drag` steps only — drag-end y position. */
|
|
66
|
-
endY?: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Per-step replay outcome (PRD 410), streamed as `browser:replay-step:<viewId>`. */
|
|
70
|
-
export interface ReplayStepResult {
|
|
71
|
-
n: number;
|
|
72
|
-
status: 'pass' | 'fail';
|
|
73
|
-
detail?: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** find-in-page result (PRD 402), streamed as `browser:find-result:<viewId>`. */
|
|
77
|
-
export interface FindResult {
|
|
78
|
-
requestId: number;
|
|
79
|
-
matches: number;
|
|
80
|
-
activeMatchOrdinal: number;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Element-picker event (PRD 403), streamed as `browser:picker-event:<viewId>`. */
|
|
84
|
-
export interface PickerEvent {
|
|
85
|
-
type: 'hover' | 'pick' | 'unpick' | 'exit';
|
|
86
|
-
selector?: string;
|
|
87
|
-
label?: string;
|
|
88
|
-
tag?: string;
|
|
89
|
-
rect?: { x: number; y: number; width: number; height: number };
|
|
90
|
-
/** Present on a 'pick' event fired by an unmodified click — the selection was replaced, not accumulated. */
|
|
91
|
-
replace?: boolean;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** One entry in `~/.claude/session-manager/browser/history.json` (PRD 402). */
|
|
95
|
-
export interface BrowserHistoryEntry {
|
|
96
|
-
url: string;
|
|
97
|
-
title: string;
|
|
98
|
-
ts: number;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** One entry in `~/.claude/session-manager/browser/bookmarks.json` (PRD 402). */
|
|
102
|
-
export interface BrowserBookmark {
|
|
103
|
-
url: string;
|
|
104
|
-
title: string;
|
|
105
|
-
ts: number;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
32
|
export interface ReadJsonResult {
|
|
109
33
|
exists: boolean;
|
|
110
34
|
raw: string;
|
|
@@ -119,6 +43,13 @@ export interface ReadTextResult {
|
|
|
119
43
|
text: string;
|
|
120
44
|
mtimeMs: number;
|
|
121
45
|
error: string | null;
|
|
46
|
+
/** True when maxBytes was set and the file is larger than the read prefix. */
|
|
47
|
+
truncated: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ReadTextOptions {
|
|
51
|
+
/** Read at most this many bytes from the start of the file instead of the whole file. */
|
|
52
|
+
maxBytes?: number;
|
|
122
53
|
}
|
|
123
54
|
|
|
124
55
|
/** One resolved node in a CLAUDE.md-like file's `@path` import chain. */
|
|
@@ -133,7 +64,7 @@ export interface ImportRef {
|
|
|
133
64
|
/** Declared owner ids for the single-writer law over a project's
|
|
134
65
|
* session-manager-operations/ root. Mirrors OWNERS in
|
|
135
66
|
* src/main/lib/opsOwnership.cjs — one writer per namespace; everyone reads. */
|
|
136
|
-
export type OpsWriter = 'epics' | 'scheduler' | 'project-home'
|
|
67
|
+
export type OpsWriter = 'epics' | 'scheduler' | 'project-home';
|
|
137
68
|
|
|
138
69
|
export interface WriteResult {
|
|
139
70
|
ok: boolean;
|
|
@@ -1162,46 +1093,6 @@ export interface PluginInstallProgressEvent {
|
|
|
1162
1093
|
line: string;
|
|
1163
1094
|
}
|
|
1164
1095
|
|
|
1165
|
-
// ────────────────────────────────────────────── Web Remote (PRD 08)
|
|
1166
|
-
|
|
1167
|
-
export interface WebRemoteDevice {
|
|
1168
|
-
deviceId: string;
|
|
1169
|
-
deviceName: string;
|
|
1170
|
-
issuedAt: string;
|
|
1171
|
-
lastConnectedAt: string | null;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
export interface WebRemoteStatus {
|
|
1175
|
-
enabled: boolean;
|
|
1176
|
-
remoteControlEnabled: boolean;
|
|
1177
|
-
connected: boolean;
|
|
1178
|
-
e2eActive: boolean;
|
|
1179
|
-
/** True once the user has confirmed the SAS on the desktop. */
|
|
1180
|
-
e2eAuthenticated: boolean;
|
|
1181
|
-
/** Explicit E2E session state for UI rendering. */
|
|
1182
|
-
e2eState: 'idle' | 'pending_sas' | 'authenticated' | 'failed';
|
|
1183
|
-
/** 6-digit Short Authentication String pending user confirmation, or null. */
|
|
1184
|
-
pendingSas: string | null;
|
|
1185
|
-
devices: WebRemoteDevice[];
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
export interface WebRemotePairResult {
|
|
1189
|
-
ok: boolean;
|
|
1190
|
-
deviceId?: string;
|
|
1191
|
-
error?: string;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
export interface WebRemoteMutationResult {
|
|
1195
|
-
ok: boolean;
|
|
1196
|
-
error?: string;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
export interface WebRemoteAuditTailResult {
|
|
1200
|
-
ok: boolean;
|
|
1201
|
-
lines?: string[];
|
|
1202
|
-
error?: string;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
1096
|
// ────────────────────────────────────────────── Chat runner (PRD 319)
|
|
1206
1097
|
|
|
1207
1098
|
export interface ChatRunPayload {
|
|
@@ -1344,63 +1235,6 @@ export interface SessionManagerAPI {
|
|
|
1344
1235
|
onExit: (tabId: string, handler: (info: PtyExit) => void) => () => void;
|
|
1345
1236
|
onWriteError: (handler: (ev: WriteErrorEvent) => void) => () => void;
|
|
1346
1237
|
};
|
|
1347
|
-
browser: {
|
|
1348
|
-
create: (payload: { viewId: string; partition: string }) => Promise<{ ok: boolean }>;
|
|
1349
|
-
setBounds: (payload: { viewId: string; x: number; y: number; width: number; height: number }) => Promise<{ ok: boolean }>;
|
|
1350
|
-
show: (viewId: string) => Promise<{ ok: boolean }>;
|
|
1351
|
-
hide: (viewId: string) => Promise<{ ok: boolean }>;
|
|
1352
|
-
destroy: (viewId: string) => Promise<{ ok: boolean }>;
|
|
1353
|
-
navigate: (payload: { viewId: string; url: string }) => Promise<{ ok: boolean; error?: string }>;
|
|
1354
|
-
back: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1355
|
-
forward: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1356
|
-
reload: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1357
|
-
stop: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1358
|
-
onNavState: (viewId: string, handler: (state: BrowserNavState) => void) => () => void;
|
|
1359
|
-
recordStart: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1360
|
-
recordStop: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1361
|
-
onRecordStep: (viewId: string, handler: (step: RecordStep) => void) => () => void;
|
|
1362
|
-
captureDom: (payload: { viewId: string; kind: 'text' | 'html' }) => Promise<
|
|
1363
|
-
| { ok: true; url: string; title: string; text: string; truncated?: boolean }
|
|
1364
|
-
| { ok: false; error: string }
|
|
1365
|
-
>;
|
|
1366
|
-
captureShot: (viewId: string) => Promise<
|
|
1367
|
-
| { ok: true; url: string; title: string; dataUrl: string }
|
|
1368
|
-
| { ok: false; error: string }
|
|
1369
|
-
>;
|
|
1370
|
-
saveBinary: (path: string, base64: string, writer?: OpsWriter) => Promise<{ ok: boolean; error?: string }>;
|
|
1371
|
-
/** Opens a native "Save As" dialog and writes `text` to the chosen path directly
|
|
1372
|
-
* (bypasses config.cjs's write-boundary check — the path is user-chosen via OS dialog). */
|
|
1373
|
-
saveRecording: (payload: { defaultName: string; text: string }) => Promise<
|
|
1374
|
-
| { ok: true; path: string }
|
|
1375
|
-
| { ok: false; canceled: true }
|
|
1376
|
-
| { ok: false; error: string }
|
|
1377
|
-
>;
|
|
1378
|
-
replay: (payload: {
|
|
1379
|
-
viewId: string;
|
|
1380
|
-
steps: RecordStep[];
|
|
1381
|
-
values?: Record<string, string>;
|
|
1382
|
-
continueOnError?: boolean;
|
|
1383
|
-
}) => Promise<{ ok: boolean; error?: string; stopped?: boolean; failedAt?: number }>;
|
|
1384
|
-
onReplayStep: (viewId: string, handler: (step: ReplayStepResult) => void) => () => void;
|
|
1385
|
-
setZoom: (payload: { viewId: string; factor: number }) => Promise<{ ok: boolean; error?: string; factor?: number }>;
|
|
1386
|
-
find: (payload: { viewId: string; text: string; forward?: boolean }) => Promise<{ ok: boolean; error?: string }>;
|
|
1387
|
-
stopFind: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1388
|
-
onFindResult: (viewId: string, handler: (result: FindResult) => void) => () => void;
|
|
1389
|
-
pickerStart: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1390
|
-
pickerStop: (viewId: string) => Promise<{ ok: boolean; error?: string; wasPicking?: boolean }>;
|
|
1391
|
-
onPickerEvent: (viewId: string, handler: (ev: PickerEvent) => void) => () => void;
|
|
1392
|
-
/** Selection-scoped capture pipeline (PRD 404): filter -> prune -> summarize -> chunk for 'agent', outerHTML for 'html', CDP AX-tree for 'a11y', fallback-chain for 'selector'. */
|
|
1393
|
-
capture: (payload: {
|
|
1394
|
-
viewId: string;
|
|
1395
|
-
selectors: string[];
|
|
1396
|
-
mode: 'agent' | 'html' | 'a11y' | 'selector';
|
|
1397
|
-
}) => Promise<
|
|
1398
|
-
| { ok: true; mode: string; text: string; meta: { chunks?: number; tokens?: number } }
|
|
1399
|
-
| { ok: false; error: string }
|
|
1400
|
-
>;
|
|
1401
|
-
/** Fired when the embedded page tries to open a new window (target="_blank", window.open, ctrl/cmd-click) — the main process denies the native popup and forwards the URL here. */
|
|
1402
|
-
onOpenTabRequest: (handler: (payload: { url: string }) => void) => () => void;
|
|
1403
|
-
};
|
|
1404
1238
|
transcripts: {
|
|
1405
1239
|
subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
|
|
1406
1240
|
/** Release the sub back to the LRU cache (view-switch). Does not destroy the watcher. */
|
|
@@ -1418,7 +1252,10 @@ export interface SessionManagerAPI {
|
|
|
1418
1252
|
* session with no transcript file yet (or one over the main-process size
|
|
1419
1253
|
* cap) maps to null. */
|
|
1420
1254
|
usageFor: (cwd: string, sessionIds: string[]) => Promise<Record<string, { inputTokens: number; outputTokens: number } | null>>;
|
|
1421
|
-
|
|
1255
|
+
/** Fires once per main-process flush with the ORDERED batch of events that
|
|
1256
|
+
* flush produced (never one call per event) — see transcripts.cjs's
|
|
1257
|
+
* doFlush / MAX_EVENTS_PER_BATCH. */
|
|
1258
|
+
onEvent: (tabId: string, handler: (events: TranscriptEvent[]) => void) => () => void;
|
|
1422
1259
|
};
|
|
1423
1260
|
sessions: {
|
|
1424
1261
|
load: () => Promise<LoadedSessions>;
|
|
@@ -1454,7 +1291,7 @@ export interface SessionManagerAPI {
|
|
|
1454
1291
|
};
|
|
1455
1292
|
config: {
|
|
1456
1293
|
readJson: (path: string) => Promise<ReadJsonResult>;
|
|
1457
|
-
readText: (path: string) => Promise<ReadTextResult>;
|
|
1294
|
+
readText: (path: string, opts?: ReadTextOptions) => Promise<ReadTextResult>;
|
|
1458
1295
|
/** `writer` declares the owning surface for the single-writer law — required
|
|
1459
1296
|
* when `path` is inside a project's session-manager-operations/ root. */
|
|
1460
1297
|
writeJson: (path: string, data: unknown, writer?: OpsWriter) => Promise<WriteResult>;
|
|
@@ -1499,6 +1336,10 @@ export interface SessionManagerAPI {
|
|
|
1499
1336
|
status: () => Promise<OtelStatus>;
|
|
1500
1337
|
configPath: () => Promise<string>;
|
|
1501
1338
|
};
|
|
1339
|
+
diagnostics: {
|
|
1340
|
+
/** Rejects unless the main process has SM_HEAP_SNAPSHOT=1 set. */
|
|
1341
|
+
takeHeapSnapshot: () => Promise<{ filePath: string; bytes: number | null; ms: number }>;
|
|
1342
|
+
};
|
|
1502
1343
|
files: {
|
|
1503
1344
|
list: (path: string, showHidden?: boolean) => Promise<FilesListResult>;
|
|
1504
1345
|
read: (path: string) => Promise<FilesReadResult>;
|
|
@@ -1507,6 +1348,7 @@ export interface SessionManagerAPI {
|
|
|
1507
1348
|
rename: (path: string, newName: string) => Promise<FilesRenameResult>;
|
|
1508
1349
|
delete: (path: string) => Promise<FilesDeleteResult>;
|
|
1509
1350
|
duplicate: (path: string) => Promise<FilesDuplicateResult>;
|
|
1351
|
+
saveBinary: (path: string, base64: string, writer?: OpsWriter) => Promise<{ ok: boolean; error?: string }>;
|
|
1510
1352
|
};
|
|
1511
1353
|
docEdit: {
|
|
1512
1354
|
run: (payload: { path: string; before: string; instruction: string; documentText?: string }) => Promise<DocEditResult>;
|
|
@@ -1616,8 +1458,6 @@ export interface SessionManagerAPI {
|
|
|
1616
1458
|
| { ok: true; text: string }
|
|
1617
1459
|
| { ok: false; error?: string }
|
|
1618
1460
|
>;
|
|
1619
|
-
/** Write side — copies a Capture-panel screenshot data URL to the OS clipboard. */
|
|
1620
|
-
copyImage: (dataUrl: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1621
1461
|
/** Write side — copies arbitrary text (e.g. a recorded flow export) to the OS clipboard. */
|
|
1622
1462
|
writeText: (text: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1623
1463
|
};
|
|
@@ -1704,27 +1544,6 @@ export interface SessionManagerAPI {
|
|
|
1704
1544
|
* renderer needs per-row badges without a separate git call per file. */
|
|
1705
1545
|
fileStatus: (cwd: string) => Promise<GitFileStatusMap>;
|
|
1706
1546
|
};
|
|
1707
|
-
webRemote: {
|
|
1708
|
-
getStatus: () => Promise<WebRemoteStatus>;
|
|
1709
|
-
enable: () => Promise<WebRemoteMutationResult>;
|
|
1710
|
-
disable: () => Promise<WebRemoteMutationResult>;
|
|
1711
|
-
/** Allow MUTATE-tier commands (pty spawn/write, scheduler writes). Default off. */
|
|
1712
|
-
enableControl: () => Promise<WebRemoteMutationResult>;
|
|
1713
|
-
/** Block MUTATE-tier commands — mobile becomes read-only mirror. */
|
|
1714
|
-
disableControl: () => Promise<WebRemoteMutationResult>;
|
|
1715
|
-
pair: (otp: string) => Promise<WebRemotePairResult>;
|
|
1716
|
-
revokeDevice: (deviceId: string) => Promise<WebRemoteMutationResult>;
|
|
1717
|
-
auditTail: (lines?: number) => Promise<WebRemoteAuditTailResult>;
|
|
1718
|
-
onStatus: (handler: (status: WebRemoteStatus) => void) => () => void;
|
|
1719
|
-
onTokenRevoked: (handler: (ev: { deviceId: string }) => void) => () => void;
|
|
1720
|
-
/** Revoke ALL paired devices and tear down every session immediately (panic). */
|
|
1721
|
-
revokeAll: () => Promise<WebRemoteMutationResult>;
|
|
1722
|
-
/** Subscribe to the completion event broadcast after revokeAll. */
|
|
1723
|
-
onRevokedAll: (handler: (ev: { revokedCount: number }) => void) => () => void;
|
|
1724
|
-
/** Confirm that the SAS shown on the desktop matches the browser — marks the
|
|
1725
|
-
* E2E session as authenticated and unblocks MUTATE-tier commands. */
|
|
1726
|
-
confirmSas: () => Promise<WebRemoteMutationResult>;
|
|
1727
|
-
};
|
|
1728
1547
|
chat: {
|
|
1729
1548
|
/** Spawn a headless `claude -p` run for a tab. Results arrive via the on* listeners. */
|
|
1730
1549
|
run: (payload: ChatRunPayload) => Promise<{ ok: boolean }>;
|