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.
Files changed (68) hide show
  1. package/dist/assets/AgentLibrary-BeJJa_zv.js +1 -0
  2. package/dist/assets/History-mWbqemhZ.js +2 -0
  3. package/dist/assets/Hooks-D_8sciZu.js +3 -0
  4. package/dist/assets/HostBilko-6aUJ7Smz.js +1 -0
  5. package/dist/assets/Library-CG1LDXLw.js +46 -0
  6. package/dist/assets/ListDetail-CIIJOWwI.js +1 -0
  7. package/dist/assets/MarkdownEditor-D0-y8V-f.js +1 -0
  8. package/dist/assets/McpServers-Kc-djiVi.js +2 -0
  9. package/dist/assets/Memory-B60DGRTW.js +8 -0
  10. package/dist/assets/Panel-CbTPsYHq.js +1 -0
  11. package/dist/assets/Permissions-C8KrSfrd.js +3 -0
  12. package/dist/assets/Plugins-BT8EumHb.js +2 -0
  13. package/dist/assets/ProvenanceBadge-cEqPpFsT.js +1 -0
  14. package/dist/assets/Scheduler-DbKPd0or.js +14 -0
  15. package/dist/assets/ScopeSwitcher-BNesCJ_t.js +1 -0
  16. package/dist/assets/Settings-Bjt7AQ6G.js +3 -0
  17. package/dist/assets/SkillReferenceGraph-DzuaPDM-.js +46 -0
  18. package/dist/assets/Skills-CVL_4X3T.js +3 -0
  19. package/dist/assets/SystemPrompt-Ct__nFF1.js +1 -0
  20. package/dist/assets/TagLibrary-DmrTfw4D.js +1 -0
  21. package/dist/assets/{TiptapBody-Bn-LMce4.js → TiptapBody-DaS0M_Ni.js} +1 -1
  22. package/dist/assets/Toggle-C0a-6xV7.js +1 -0
  23. package/dist/assets/index-CO_7DroC.js +3066 -0
  24. package/dist/assets/{index-DOXvph-Z.css → index-LlWpj2VJ.css} +1 -1
  25. package/dist/assets/listSkills-QORduPIk.js +1 -0
  26. package/dist/assets/settingsSchema-B7dMJaix.js +3 -0
  27. package/dist/assets/skillFrontmatter-Dif5JIg7.js +10 -0
  28. package/dist/index.html +2 -2
  29. package/package.json +1 -3
  30. package/src/main/__tests__/config-readText-bounded.test.cjs +84 -0
  31. package/src/main/__tests__/heapSnapshot.test.cjs +121 -0
  32. package/src/main/__tests__/historyAggregatorIntraday.test.cjs +313 -0
  33. package/src/main/__tests__/runLogRetention.test.cjs +343 -0
  34. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +215 -0
  35. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +42 -1
  36. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +54 -1
  37. package/src/main/__tests__/transcripts-batch-flush.test.cjs +249 -0
  38. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +6 -0
  39. package/src/main/__tests__/transcripts-paged-reads.test.cjs +6 -0
  40. package/src/main/__tests__/transcriptsUsageFor.test.cjs +6 -0
  41. package/src/main/config.cjs +32 -29
  42. package/src/main/files.cjs +20 -0
  43. package/src/main/health.cjs +32 -0
  44. package/src/main/heapSnapshot.cjs +122 -0
  45. package/src/main/historyAggregator.cjs +176 -36
  46. package/src/main/index.cjs +10 -63
  47. package/src/main/ipcSchemas.cjs +20 -209
  48. package/src/main/lib/opsOwnership.cjs +0 -2
  49. package/src/main/lib/runLogRetention.cjs +358 -0
  50. package/src/main/lib/summarize.cjs +0 -1
  51. package/src/main/runVerify.cjs +55 -6
  52. package/src/main/scheduler.cjs +46 -7
  53. package/src/main/transcripts.cjs +29 -5
  54. package/src/preload/api.d.ts +18 -199
  55. package/src/preload/index.cjs +12 -102
  56. package/dist/assets/index-BfiMRkln.js +0 -3225
  57. package/src/main/__tests__/browserAgentServer.test.cjs +0 -235
  58. package/src/main/__tests__/browserView-destroyed-handler.test.cjs +0 -85
  59. package/src/main/__tests__/browserView-oauth-popup.test.cjs +0 -126
  60. package/src/main/__tests__/web-remote-e2e-pinning.test.cjs +0 -181
  61. package/src/main/browserAgentServer.cjs +0 -224
  62. package/src/main/browserCapture.cjs +0 -728
  63. package/src/main/browserView.cjs +0 -916
  64. package/src/main/lib/__tests__/browserAgentActions.test.cjs +0 -76
  65. package/src/main/lib/browserAgentActions.cjs +0 -114
  66. package/src/main/lib/e2eStateMachine.cjs +0 -39
  67. package/src/main/webRemote.cjs +0 -1367
  68. package/src/preload/browserViewPreload.cjs +0 -118
@@ -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 && !committedDuringRun) {
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' && !committedDuringRun && !isFixPlanJob) {
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
- && committedDuringRun
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
- { ...(annotations.length ? { annotations } : {}), sentinel, sentinelOverride: top.verdict },
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
  };
@@ -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 and was archived before this
3028
- // run fired (see archivedTwinExists in executeJob). Treat it as a plain
3029
- // completion — no verify pass, no commit guard, no RCA feedback — since
3030
- // there is no real transcript/commit to check.
3031
- if (res.skipped === 'prd-archived') {
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 — callable from webRemote.cjs without going through IPC.
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 };
@@ -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
- if (emit) sendIfAlive(window, `transcript:event:${sub.tabId}`, ev);
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 a given
149
- // line once (readDelta never re-returns already-consumed bytes), so
150
- // this can't double-record — paged re-reads (readPage) go through a
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
  };
@@ -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' | 'browser';
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
- onEvent: (tabId: string, handler: (ev: TranscriptEvent) => void) => () => void;
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 }>;