claude-code-session-manager 0.35.3 → 0.35.5

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.
@@ -1151,6 +1151,86 @@ function isPromotableOriginal(status) {
1151
1151
  return status === 'failed' || status === 'needs_review';
1152
1152
  }
1153
1153
 
1154
+ /**
1155
+ * Pure helper: given a completed fix-plan slug (e.g. '451-fix-foo'), find the
1156
+ * SPECIFIC promotable original job it heals. Matches on the full
1157
+ * numeric-prefixed slug ('451-foo'), not just the base ('foo') — two
1158
+ * needs_review jobs can share a base slug with different NN prefixes
1159
+ * (e.g. '451-foo' and '453-foo'), and a fix plan must only heal the
1160
+ * lineage whose NN it was authored against. Exported for tests.
1161
+ */
1162
+ function healTargetForFix(fixSlug, jobs) {
1163
+ const originalSlug = fixSlug.replace(/^(\d+)-fix-/, '$1-');
1164
+ return jobs.find((x) => x.slug === originalSlug && isPromotableOriginal(x.status)) || null;
1165
+ }
1166
+
1167
+ /**
1168
+ * Build the Opus investigation prompt. Pure/hermetic so its content can be
1169
+ * unit-tested (no spawn, no fs). Inputs are the already-resolved values that
1170
+ * spawnInvestigation computes.
1171
+ */
1172
+ function buildInvestigationPrompt({ failedJob, cwd, failedLogPath, originalBody, logTail, fixPath, group }) {
1173
+ return `You are investigating a failed scheduled job in the session-manager queue. Your ONLY job is to write a fix-plan PRD file. Do NOT attempt the fix yourself.
1174
+
1175
+ # Failed job
1176
+ - Slug: ${failedJob.slug}
1177
+ - Title: ${failedJob.title}
1178
+ - cwd: ${cwd}
1179
+ - Exit code: ${failedJob.exitCode}
1180
+ - Full failure log: ${failedLogPath}
1181
+
1182
+ # Original PRD body (this is what the job was trying to do)
1183
+ \`\`\`
1184
+ ${originalBody}
1185
+ \`\`\`
1186
+
1187
+ # Last ~16KB of the failure log (stream-json format from \`claude -p\`)
1188
+ \`\`\`
1189
+ ${logTail}
1190
+ \`\`\`
1191
+
1192
+ # Your task
1193
+ 1. Read the full failure log at ${failedLogPath} if the tail above isn't sufficient.
1194
+ 2. Read source files in ${cwd} as needed to understand the context.
1195
+ 3. Identify the root cause of the failure.
1196
+ 4. Write a NEW fix-plan PRD file at exactly this path:
1197
+
1198
+ ${fixPath}
1199
+
1200
+ 5. The frontmatter MUST be exactly this format (no extra keys):
1201
+ \`\`\`
1202
+ ---
1203
+ title: Fix: <short summary of the fix>
1204
+ cwd: ${cwd}
1205
+ parallelGroup: ${group}
1206
+ estimateMinutes: <your time estimate>
1207
+ ---
1208
+ \`\`\`
1209
+ 6. The PRD body MUST be self-contained — \`claude -p\` runs it on a fresh Sonnet session with NO conversation context. Include:
1210
+ - Root-cause analysis (what went wrong and why)
1211
+ - Concrete fix steps (specific files / commands / edits)
1212
+ - Verification command(s) the next agent should run to confirm the fix
1213
+ - Acceptance criteria
1214
+ - Before writing the fix-plan PRD body, read
1215
+ \`plugins/session-manager-dev/skills/develop/standards.md\` (resolve it relative to the failed
1216
+ job's repo root — for session-manager-authored PRDs that is this cwd). Inline its
1217
+ \`## Execution discipline (headless runs)\` section VERBATIM into the new fix-plan PRD under an
1218
+ \`## Engineering standards\` heading, exactly as the \`/develop\` skill's Phase 1 does for normal
1219
+ PRDs. Reuse that convention — do not invent a second, paraphrased set of warnings. The headless
1220
+ Sonnet executor that runs your fix-plan sees no skills and no conversation, so inlining these
1221
+ rules verbatim is the only way they reach it.
1222
+ - Known failure class — "delegated instead of executed": if the failure log tail above shows the
1223
+ failed run invoked the \`Skill\` tool with a \`session-manager-dev:develop\` or
1224
+ \`session-manager-dev:process-feedback\` argument, and/or called \`ScheduleWakeup\`, and then exited
1225
+ 0 without producing the diff/tests its PRD demanded, recognize this as a self-delegation failure.
1226
+ In that case LEAD the fix-plan PRD by quoting VERBATIM the canonical rule from standards.md's
1227
+ Execution discipline section — "You ARE the executor — never re-queue or self-schedule" — rather
1228
+ than authoring new prose. Instruct the fix-plan's executor plainly that a queued PRD is the task,
1229
+ not evidence of completion, and that the deliverable is the code diff.
1230
+
1231
+ DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediately.`;
1232
+ }
1233
+
1154
1234
  /**
1155
1235
  * Spawn an Opus investigation session for a failed job. The investigator's job
1156
1236
  * is to read the failure log + original PRD, identify the root cause, and write
@@ -1223,49 +1303,7 @@ async function spawnInvestigation(failedJob, runDir) {
1223
1303
  console.warn(`[scheduler] investigation cwd missing (${cwd}); falling back to ${DEFAULT_PROJECT_CWD}`);
1224
1304
  cwd = DEFAULT_PROJECT_CWD;
1225
1305
  }
1226
- const prompt = `You are investigating a failed scheduled job in the session-manager queue. Your ONLY job is to write a fix-plan PRD file. Do NOT attempt the fix yourself.
1227
-
1228
- # Failed job
1229
- - Slug: ${failedJob.slug}
1230
- - Title: ${failedJob.title}
1231
- - cwd: ${cwd}
1232
- - Exit code: ${failedJob.exitCode}
1233
- - Full failure log: ${failedLogPath}
1234
-
1235
- # Original PRD body (this is what the job was trying to do)
1236
- \`\`\`
1237
- ${originalBody}
1238
- \`\`\`
1239
-
1240
- # Last ~16KB of the failure log (stream-json format from \`claude -p\`)
1241
- \`\`\`
1242
- ${logTail}
1243
- \`\`\`
1244
-
1245
- # Your task
1246
- 1. Read the full failure log at ${failedLogPath} if the tail above isn't sufficient.
1247
- 2. Read source files in ${cwd} as needed to understand the context.
1248
- 3. Identify the root cause of the failure.
1249
- 4. Write a NEW fix-plan PRD file at exactly this path:
1250
-
1251
- ${fixPath}
1252
-
1253
- 5. The frontmatter MUST be exactly this format (no extra keys):
1254
- \`\`\`
1255
- ---
1256
- title: Fix: <short summary of the fix>
1257
- cwd: ${cwd}
1258
- parallelGroup: ${group}
1259
- estimateMinutes: <your time estimate>
1260
- ---
1261
- \`\`\`
1262
- 6. The PRD body MUST be self-contained — \`claude -p\` runs it on a fresh Sonnet session with NO conversation context. Include:
1263
- - Root-cause analysis (what went wrong and why)
1264
- - Concrete fix steps (specific files / commands / edits)
1265
- - Verification command(s) the next agent should run to confirm the fix
1266
- - Acceptance criteria
1267
-
1268
- DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediately.`;
1306
+ const prompt = buildInvestigationPrompt({ failedJob, cwd, failedLogPath, originalBody, logTail, fixPath, group });
1269
1307
 
1270
1308
  // Phase 1: open log fd for pre-spawn diagnostics.
1271
1309
  const { fd, safeLog, closeFd } = openLog(investigationLogPath);
@@ -1329,6 +1367,13 @@ DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediat
1329
1367
  console.log(`[scheduler] investigation produced fix plan: ${fixSlug}`);
1330
1368
  } else {
1331
1369
  console.log(`[scheduler] investigation finished WITHOUT producing fix plan (slug=${failedJob.slug}, code=${exitCode})`);
1370
+ // Record the no-plan outcome so selectAutoFixTargets can offer one
1371
+ // bounded retry instead of permanently dead-ending behind the
1372
+ // 1-attempt cap (autoFixAttempted stays true).
1373
+ mutate((s) => {
1374
+ const j = s.jobs.find((x) => x.slug === failedJob.slug);
1375
+ if (j) j.autoFixOutcome = 'no-plan';
1376
+ }).catch(() => {});
1332
1377
  }
1333
1378
  // Trigger a tick so the new fix plan is reconciled into the queue and fired.
1334
1379
  tickQueue().catch(() => {});
@@ -1525,17 +1570,16 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
1525
1570
  // this, the queue stalls indefinitely behind a stale failure even
1526
1571
  // though the auto-recovery did its job.
1527
1572
  if (effectiveStatus === 'completed' && isFixPlanSlug(job.slug)) {
1528
- const originalSlug = job.slug.replace(/^(\d+)-fix-/, '$1-');
1529
- const orig = s.jobs.findIndex((x) => x.slug === originalSlug && isPromotableOriginal(x.status));
1530
- if (orig >= 0) {
1531
- const priorStatus = s.jobs[orig].status;
1532
- console.log(`[scheduler] auto-promote: ${originalSlug} (${priorStatus}) → completed because ${job.slug} succeeded`);
1533
- s.jobs[orig].status = 'completed';
1534
- s.jobs[orig].exitCode = 0;
1535
- s.jobs[orig].error = null;
1536
- s.jobs[orig].completedBy = job.slug;
1573
+ const orig = healTargetForFix(job.slug, s.jobs);
1574
+ if (orig) {
1575
+ const priorStatus = orig.status;
1576
+ console.log(`[scheduler] auto-promote: ${orig.slug} (${priorStatus}) completed because ${job.slug} succeeded`);
1577
+ orig.status = 'completed';
1578
+ orig.exitCode = 0;
1579
+ orig.error = null;
1580
+ orig.completedBy = job.slug;
1537
1581
  if (priorStatus === 'needs_review') {
1538
- delete s.jobs[orig].verifierVerdict;
1582
+ delete orig.verifierVerdict;
1539
1583
  }
1540
1584
  }
1541
1585
  }
@@ -1881,16 +1925,57 @@ function selectHistoryJobs(jobs, limit) {
1881
1925
  // heal a genuinely-unfinished job.
1882
1926
  const RESCANNABLE_VERDICTS = new Set(['transcript_errors', 'verify_unavailable']);
1883
1927
 
1928
+ /**
1929
+ * Backfill a job's missing runId by scanning RUNS_DIR for a run directory
1930
+ * whose contents reference this job's slug (a '<slug>.log' file inside it).
1931
+ * A needs_review job can lose its runId (e.g. an old queue-schema gap) and
1932
+ * become invisible to every auto-resolution path that requires job.runId
1933
+ * truthy. O(number of run dirs) — a single readdir + per-dir existsSync
1934
+ * check, no nested loop over user-scaled data. Dir names are ISO timestamps,
1935
+ * so lexical-descending sort picks the newest match. Exported for tests.
1936
+ */
1937
+ function resolveRunId(job, { runsDir = RUNS_DIR } = {}) {
1938
+ if (!job || job.runId) return job?.runId || null;
1939
+ if (!job.slug) return null;
1940
+ let dirs;
1941
+ try {
1942
+ dirs = fs.readdirSync(runsDir);
1943
+ } catch {
1944
+ return null;
1945
+ }
1946
+ const matches = dirs.filter((d) => {
1947
+ try {
1948
+ return fs.existsSync(path.join(runsDir, d, `${job.slug}.log`));
1949
+ } catch {
1950
+ return false;
1951
+ }
1952
+ });
1953
+ if (!matches.length) return null;
1954
+ matches.sort().reverse();
1955
+ return matches[0];
1956
+ }
1957
+
1958
+ /**
1959
+ * Pure predicate: a needs_review job with no runId and no backfillable run
1960
+ * directory can never self-heal or get an auto-fix investigation — it must
1961
+ * be surfaced for a human rather than silently dead-ended. Exported for
1962
+ * tests.
1963
+ */
1964
+ function isUnresolvableNeedsReview(job, { hasRunDir }) {
1965
+ return !!job && job.status === 'needs_review' && !job.runId && !hasRunDir;
1966
+ }
1967
+
1884
1968
  /**
1885
1969
  * Pure predicate: is this job eligible for the boot re-verify self-heal? Only
1886
- * needs_review jobs with a run log AND a transcript-scan verdict. Crucially
1887
- * EXCLUDES 'uncommitted_changes' (git commit-guard) — verifyRun can't see git,
1888
- * so re-scanning it would falsely heal an unfinished job. Exported for tests.
1970
+ * needs_review jobs with a run log (own or backfilled via resolveRunId) AND a
1971
+ * transcript-scan verdict. Crucially EXCLUDES 'uncommitted_changes' (git
1972
+ * commit-guard) — verifyRun can't see git, so re-scanning it would falsely
1973
+ * heal an unfinished job. Exported for tests.
1889
1974
  */
1890
1975
  function isRescanCandidate(job) {
1891
1976
  return !!job
1892
1977
  && job.status === 'needs_review'
1893
- && !!job.runId
1978
+ && !!(job.runId || resolveRunId(job))
1894
1979
  && RESCANNABLE_VERDICTS.has(job.verifierVerdict);
1895
1980
  }
1896
1981
 
@@ -1906,23 +1991,32 @@ function isRescanCandidate(job) {
1906
1991
  * @returns {Promise<{rescanned:number, healed:string[]}>}
1907
1992
  */
1908
1993
  /**
1909
- * Pure helper — no I/O. Returns the subset of jobs eligible for automatic
1910
- * fix-plan authoring after a reverify pass leaves them still in needs_review.
1994
+ * Pure helper — no I/O by default (resolveJobRunId is injectable; production
1995
+ * caller passes resolveRunId, which does touch disk). Returns the subset of
1996
+ * jobs eligible for automatic fix-plan authoring after a reverify pass
1997
+ * leaves them still in needs_review.
1911
1998
  *
1912
1999
  * Exclusion rules (all must pass):
1913
2000
  * - status === 'needs_review'
1914
- * - truthy runId (need a run log to investigate)
1915
- * - autoFixAttempted !== true (1-attempt cap)
2001
+ * - a runId, own or backfilled via resolveJobRunId (need a run log to investigate)
1916
2002
  * - not itself a fix-plan slug (avoids infinite recursion)
2003
+ * - autoFixAttempted cap: a job with no prior attempt is eligible; a job
2004
+ * whose prior attempt outcome was 'no-plan' gets ONE bounded retry
2005
+ * (autoFixRetries < 1); any other attempted job (or an exhausted
2006
+ * no-plan retry) is excluded
1917
2007
  * - no fix sibling on disk (fixSlugExists) or already in the queue
1918
2008
  */
1919
- function selectAutoFixTargets(jobs, { fixSlugExists }) {
2009
+ function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRunId }) {
1920
2010
  const slugsInQueue = new Set(jobs.map((j) => j.slug));
1921
2011
  return jobs.filter((job) => {
1922
2012
  if (job.status !== 'needs_review') return false;
1923
- if (!job.runId) return false;
1924
- if (job.autoFixAttempted) return false;
2013
+ const runId = job.runId || resolveJobRunId(job);
2014
+ if (!runId) return false;
1925
2015
  if (isFixPlanSlug(job.slug)) return false;
2016
+ if (job.autoFixAttempted) {
2017
+ if (job.autoFixOutcome !== 'no-plan') return false;
2018
+ if ((job.autoFixRetries ?? 0) >= 1) return false;
2019
+ }
1926
2020
  const fixSlug = `${String(job.parallelGroup ?? 99).padStart(2, '0')}-fix-${job.slug.replace(/^\d+-/, '')}`;
1927
2021
  if (fixSlugExists(fixSlug)) return false;
1928
2022
  if (slugsInQueue.has(fixSlug)) return false;
@@ -1936,7 +2030,7 @@ async function reverifyNeedsReview() {
1936
2030
  const healed = [];
1937
2031
  const leftForReview = [];
1938
2032
  for (const job of candidates) {
1939
- const runDir = path.join(RUNS_DIR, job.runId);
2033
+ const runDir = path.join(RUNS_DIR, job.runId || resolveRunId(job));
1940
2034
  const prdPath = path.join(PRDS_DIR, `${job.slug}.md`);
1941
2035
  // Derive committedDuringRun from the recorded run window. The live
1942
2036
  // commit-guard uses gitHead() (before/after HEAD diff); here the run is
@@ -1978,24 +2072,72 @@ async function reverifyNeedsReview() {
1978
2072
  console.log(`[scheduler] boot reverify: left for review: ${detail}`);
1979
2073
  }
1980
2074
 
2075
+ // Surface needs_review jobs that can never self-heal or get an auto-fix
2076
+ // investigation (no runId, no backfillable run dir) instead of leaving
2077
+ // them silently stranded. Also surface no-plan auto-fix jobs whose one
2078
+ // bounded retry is already exhausted. Both are annotated, never looped on.
2079
+ const afterHealForAnnotate = await readQueue();
2080
+ const unresolvable = afterHealForAnnotate.jobs.filter(
2081
+ (j) => isUnresolvableNeedsReview(j, { hasRunDir: !!resolveRunId(j) }) && j.verifierVerdict !== 'no_run_artifacts',
2082
+ );
2083
+ const exhaustedNoPlan = afterHealForAnnotate.jobs.filter(
2084
+ (j) => j.status === 'needs_review'
2085
+ && j.autoFixAttempted && j.autoFixOutcome === 'no-plan' && (j.autoFixRetries ?? 0) >= 1
2086
+ && j.verifierVerdict !== 'autofix_no_plan',
2087
+ );
2088
+ if (unresolvable.length || exhaustedNoPlan.length) {
2089
+ const unresolvableSet = new Set(unresolvable.map((j) => j.slug));
2090
+ const exhaustedSet = new Set(exhaustedNoPlan.map((j) => j.slug));
2091
+ await mutate((s) => {
2092
+ for (const j of s.jobs) {
2093
+ if (unresolvableSet.has(j.slug)) {
2094
+ j.verifierVerdict = 'no_run_artifacts';
2095
+ j.error = 'no run artifacts — manual review';
2096
+ } else if (exhaustedSet.has(j.slug)) {
2097
+ j.verifierVerdict = 'autofix_no_plan';
2098
+ j.error = 'auto-fix investigation produced no fix plan after retry — manual review';
2099
+ }
2100
+ }
2101
+ });
2102
+ if (unresolvable.length) {
2103
+ console.log(`[scheduler] boot reverify: no run artifacts for ${unresolvable.map((j) => j.slug).join(', ')} — flagged for manual review`);
2104
+ }
2105
+ if (exhaustedNoPlan.length) {
2106
+ console.log(`[scheduler] boot reverify: auto-fix retry exhausted for ${exhaustedNoPlan.map((j) => j.slug).join(', ')} — flagged for manual review`);
2107
+ }
2108
+ await broadcast();
2109
+ }
2110
+
1981
2111
  // Auto-fix: spawn a fix-plan investigation for each job still in
1982
2112
  // needs_review after the heal pass (kill-switch: SM_AUTOFIX_DISABLE=1).
2113
+ // spawnInvestigation early-returns once investigationsInFlight reaches
2114
+ // MAX_CONCURRENT_INVESTIGATIONS (queues the rest for retry), so this loop
2115
+ // cannot fan out past the cap regardless of how many targets are selected.
1983
2116
  if (process.env.SM_AUTOFIX_DISABLE !== '1') {
1984
2117
  const afterHeal = await readQueue();
1985
2118
  const targets = selectAutoFixTargets(afterHeal.jobs, {
1986
2119
  fixSlugExists: (s) => fs.existsSync(path.join(PRDS_DIR, `${s}.md`)),
1987
2120
  });
1988
2121
  for (const job of targets) {
1989
- const runDir = path.join(RUNS_DIR, job.runId);
2122
+ const runId = job.runId || resolveRunId(job);
2123
+ const runDir = path.join(RUNS_DIR, runId);
2124
+ const isNoPlanRetry = job.autoFixAttempted && job.autoFixOutcome === 'no-plan';
1990
2125
  // Persist the attempt BEFORE spawning — a crash mid-investigation still
1991
2126
  // counts it (mirrors orphanRetries). Safe even when the slot is busy: the
1992
2127
  // investigation is queued and drained as slots free, so it is genuinely
1993
2128
  // attempted rather than silently dropped.
1994
2129
  await mutate((s) => {
1995
2130
  const j = s.jobs.find((x) => x.slug === job.slug);
1996
- if (j) j.autoFixAttempted = true;
2131
+ if (j) {
2132
+ j.autoFixAttempted = true;
2133
+ if (!j.runId && runId) j.runId = runId;
2134
+ if (isNoPlanRetry) {
2135
+ j.autoFixRetries = (j.autoFixRetries ?? 0) + 1;
2136
+ delete j.autoFixOutcome;
2137
+ }
2138
+ }
1997
2139
  });
1998
- console.log(`[scheduler] auto-fix: needs_review ${job.slug} → authoring fix-plan (1/1)`);
2140
+ console.log(`[scheduler] auto-fix: needs_review ${job.slug} → authoring fix-plan (${isNoPlanRetry ? 'retry' : '1/1'})`);
1999
2141
  spawnInvestigation(job, runDir).catch((e) => {
2000
2142
  console.error('[scheduler] auto-fix spawnInvestigation error', job.slug, e);
2001
2143
  });
@@ -2335,10 +2477,16 @@ async function init() {
2335
2477
  // Periodic self-heal: re-run the verifier over stale needs_review jobs so a
2336
2478
  // job whose work actually landed (committed in-window, no FAIL sentinel)
2337
2479
  // auto-clears WITHOUT waiting for the next app restart. Cheap-guarded — the
2338
- // log scan only runs when something is actually flagged.
2339
- const s = readQueueSync();
2340
- if (s.jobs.some((j) => j.status === 'needs_review')) {
2341
- reverifyNeedsReview().catch(() => {});
2480
+ // log scan only runs when something is actually flagged. Kill-switch:
2481
+ // SM_REVERIFY_PERIODIC_DISABLE=1 (boot reverify above stays always-on).
2482
+ // reverifyNeedsReview's auto-fix loop is capped downstream by
2483
+ // MAX_CONCURRENT_INVESTIGATIONS (spawnInvestigation queues/early-returns
2484
+ // past it), so this interval firing cannot fan out investigations.
2485
+ if (process.env.SM_REVERIFY_PERIODIC_DISABLE !== '1') {
2486
+ const s = readQueueSync();
2487
+ if (s.jobs.some((j) => j.status === 'needs_review')) {
2488
+ reverifyNeedsReview().catch(() => {});
2489
+ }
2342
2490
  }
2343
2491
  }, 10 * 60_000);
2344
2492
 
@@ -2510,4 +2658,4 @@ const remote = {
2510
2658
  },
2511
2659
  };
2512
2660
 
2513
- module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets };
2661
+ module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt };
@@ -198,6 +198,21 @@ export type BillingFetchResult =
198
198
  | { kind: 'transient'; message: string; httpStatus: number | null }
199
199
  | { kind: 'config'; message: string };
200
200
 
201
+ // ── MCP server live connection probe (`claude mcp list`)
202
+ export interface McpServerStatus {
203
+ name: string;
204
+ target: string;
205
+ transport: 'stdio' | 'http' | 'sse' | 'ws' | 'unknown';
206
+ status: 'connected' | 'failed' | 'needs-auth' | 'pending' | 'unknown';
207
+ }
208
+
209
+ export interface McpStatusResult {
210
+ ok: boolean;
211
+ servers: McpServerStatus[];
212
+ error?: string;
213
+ checkedAt: number;
214
+ }
215
+
201
216
  // ── Usage matrix (AgOps dashboard, main-side aggregator)
202
217
  export type UsageMatrixState = 'idle' | 'executing' | 'plan' | 'background';
203
218
  export type UsageMatrixIntensity = 'idle' | 'low' | 'medium' | 'critical';
@@ -978,6 +993,12 @@ export interface ChatRunErrorEvent {
978
993
  message: string;
979
994
  }
980
995
 
996
+ export interface ChatRunNoticeEvent {
997
+ tabId: string;
998
+ sessionId: string;
999
+ message: string;
1000
+ }
1001
+
981
1002
  export interface SessionManagerAPI {
982
1003
  app: {
983
1004
  version: () => Promise<string>;
@@ -1058,6 +1079,8 @@ export interface SessionManagerAPI {
1058
1079
  | { ok: true; mode: string; text: string; meta: { chunks?: number; tokens?: number } }
1059
1080
  | { ok: false; error: string }
1060
1081
  >;
1082
+ /** 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. */
1083
+ onOpenTabRequest: (handler: (payload: { url: string }) => void) => () => void;
1061
1084
  };
1062
1085
  transcripts: {
1063
1086
  subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
@@ -1076,6 +1099,9 @@ export interface SessionManagerAPI {
1076
1099
  billing: {
1077
1100
  fetch: () => Promise<BillingFetchResult>;
1078
1101
  };
1102
+ mcp: {
1103
+ status: () => Promise<McpStatusResult>;
1104
+ };
1079
1105
  usageMatrix: {
1080
1106
  snapshot: () => Promise<UsageMatrixSnapshot>;
1081
1107
  onTick: (handler: (snap: UsageMatrixSnapshot) => void) => () => void;
@@ -1226,6 +1252,13 @@ export interface SessionManagerAPI {
1226
1252
  | { ok: true; path: string; bytes: number }
1227
1253
  | { ok: false; empty?: true; error?: string }
1228
1254
  >;
1255
+ /** Ctrl+V text paste — reads OS clipboard text via Electron's native API
1256
+ * (renderer's navigator.clipboard.readText() is denied by the
1257
+ * permission-request handler, which only allows media permissions). */
1258
+ pasteText: () => Promise<
1259
+ | { ok: true; text: string }
1260
+ | { ok: false; error?: string }
1261
+ >;
1229
1262
  /** Write side — copies a Capture-panel screenshot data URL to the OS clipboard. */
1230
1263
  copyImage: (dataUrl: string) => Promise<{ ok: boolean; error?: string }>;
1231
1264
  };
@@ -1311,6 +1344,7 @@ export interface SessionManagerAPI {
1311
1344
  onNeedsInput: (handler: (e: ChatRunNeedsInputEvent) => void) => () => void;
1312
1345
  onComplete: (handler: (e: ChatRunCompleteEvent) => void) => () => void;
1313
1346
  onError: (handler: (e: ChatRunErrorEvent) => void) => () => void;
1347
+ onNotice: (handler: (e: ChatRunNoticeEvent) => void) => () => void;
1314
1348
  };
1315
1349
  exchanges: {
1316
1350
  /** Durable per-exchange log entries for a project, newest-first (max 100 by default). */
@@ -107,6 +107,11 @@ contextBridge.exposeInMainWorld('api', {
107
107
  return () => ipcRenderer.removeListener(channel, listener);
108
108
  },
109
109
  capture: (payload) => ipcRenderer.invoke('browser:capture', payload),
110
+ onOpenTabRequest: (handler) => {
111
+ const listener = (_e, payload) => handler(payload);
112
+ ipcRenderer.on('browser:open-tab-request', listener);
113
+ return () => ipcRenderer.removeListener('browser:open-tab-request', listener);
114
+ },
110
115
  },
111
116
  transcripts: {
112
117
  subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
@@ -129,6 +134,9 @@ contextBridge.exposeInMainWorld('api', {
129
134
  billing: {
130
135
  fetch: () => ipcRenderer.invoke('billing:fetch'),
131
136
  },
137
+ mcp: {
138
+ status: () => ipcRenderer.invoke('mcp:status'),
139
+ },
132
140
  usageMatrix: {
133
141
  snapshot: () => ipcRenderer.invoke('usage:matrix:snapshot'),
134
142
  onTick: (handler) => {
@@ -279,6 +287,7 @@ contextBridge.exposeInMainWorld('api', {
279
287
  },
280
288
  clipboard: {
281
289
  pasteImage: () => ipcRenderer.invoke('clipboard:paste-image'),
290
+ pasteText: () => ipcRenderer.invoke('clipboard:paste-text'),
282
291
  copyImage: (dataUrl) => ipcRenderer.invoke('browser:copy-image', { dataUrl }),
283
292
  },
284
293
  memory: {
@@ -406,6 +415,11 @@ contextBridge.exposeInMainWorld('api', {
406
415
  ipcRenderer.on('chat:run:error', listener);
407
416
  return () => ipcRenderer.removeListener('chat:run:error', listener);
408
417
  },
418
+ onNotice: (handler) => {
419
+ const listener = (_e, payload) => handler(payload);
420
+ ipcRenderer.on('chat:run:notice', listener);
421
+ return () => ipcRenderer.removeListener('chat:run:notice', listener);
422
+ },
409
423
  },
410
424
  exchanges: {
411
425
  /** List exchanges for a project (durable chat-run log), newest-first.