claude-code-session-manager 0.37.1 → 0.38.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/bin/cli.cjs +12 -1
- package/dist/assets/{TiptapBody-DXQDiOUx.js → TiptapBody-GMk5LS9Y.js} +1 -1
- package/dist/assets/index-CD_LuJZF.css +32 -0
- package/dist/assets/{index--a8m5_ET.js → index-CQqSHpIq.js} +444 -436
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +134 -36
- package/plugins/session-manager-dev/skills/develop/standards.md +25 -0
- package/plugins/session-manager-dev/skills/issue-address/0-select/0-fetch-pool/SKILL.md +38 -0
- package/plugins/session-manager-dev/skills/issue-address/0-select/1-reject-fixed/SKILL.md +64 -0
- package/plugins/session-manager-dev/skills/issue-address/0-select/2-reject-claimed/SKILL.md +71 -0
- package/plugins/session-manager-dev/skills/issue-address/0-select/3-rank-impact/SKILL.md +39 -0
- package/plugins/session-manager-dev/skills/issue-address/0-select/SKILL.md +75 -0
- package/plugins/session-manager-dev/skills/issue-address/1-confirm-open/SKILL.md +39 -0
- package/plugins/session-manager-dev/skills/issue-address/2-claim/SKILL.md +57 -0
- package/plugins/session-manager-dev/skills/issue-address/3-reproduce/SKILL.md +40 -0
- package/plugins/session-manager-dev/skills/issue-address/4-fix/SKILL.md +33 -0
- package/plugins/session-manager-dev/skills/issue-address/5-verify/SKILL.md +40 -0
- package/plugins/session-manager-dev/skills/issue-address/SKILL.md +120 -0
- package/plugins/session-manager-dev/skills/pr-review-sweep/0-fetch/SKILL.md +41 -0
- package/plugins/session-manager-dev/skills/pr-review-sweep/1-classify/SKILL.md +45 -0
- package/plugins/session-manager-dev/skills/pr-review-sweep/2-check-fixed/SKILL.md +49 -0
- package/plugins/session-manager-dev/skills/pr-review-sweep/3-queue/SKILL.md +50 -0
- package/plugins/session-manager-dev/skills/pr-review-sweep/4-land-and-resolve/SKILL.md +60 -0
- package/plugins/session-manager-dev/skills/pr-review-sweep/SKILL.md +124 -0
- package/plugins/session-manager-dev/skills/pr-signal/SKILL.md +67 -0
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +20 -17
- package/scripts/lib/watchdogHelpers.cjs +296 -166
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +31 -0
- package/src/main/__tests__/chat-exit-close-race.test.cjs +140 -0
- package/src/main/__tests__/classifyTranscriptLine.test.cjs +90 -0
- package/src/main/__tests__/docEdit.test.cjs +164 -2
- package/src/main/__tests__/prdCreate.test.cjs +265 -25
- package/src/main/__tests__/scheduler-admin-routes.test.cjs +150 -0
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +172 -0
- package/src/main/__tests__/scheduler-committed-in-window.test.cjs +64 -0
- package/src/main/browserAgentServer.cjs +11 -10
- package/src/main/chatRunner.cjs +58 -14
- package/src/main/docEdit.cjs +125 -5
- package/src/main/health.cjs +15 -0
- package/src/main/index.cjs +12 -6
- package/src/main/ipcSchemas.cjs +16 -3
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +120 -0
- package/src/main/lib/classifyTranscriptLine.cjs +89 -0
- package/src/main/lib/localAdminHttp.cjs +157 -0
- package/src/main/lib/personaImportHealth.cjs +161 -0
- package/src/main/lib/prdCreate.cjs +107 -17
- package/src/main/lib/rcaFeedbackHook.cjs +2 -2
- package/src/main/lib/singleInstanceGuard.cjs +20 -0
- package/src/main/scheduler.cjs +223 -56
- package/src/main/sessionsStore.cjs +4 -5
- package/src/main/templates/PRD_AUTHORING.md +4 -4
- package/src/main/transcripts.cjs +1 -85
- package/src/main/webRemote.cjs +3 -3
- package/src/preload/api.d.ts +16 -6
- package/src/preload/index.cjs +9 -2
- package/dist/assets/index-CTTjT08J.css +0 -32
- package/plugins/session-manager-dev/skills/security-review/SKILL.md +0 -105
- package/src/main/__tests__/adminServer.test.cjs +0 -380
- package/src/main/adminServer.cjs +0 -242
|
@@ -38,8 +38,8 @@ const PRDS_DIR = path.join(os.homedir(), '.claude', 'session-manager', 'schedule
|
|
|
38
38
|
const SM_REPO_ROOT = path.join(os.homedir(), 'Projects', 'session-manager');
|
|
39
39
|
const SM_REPO_FEEDBACK_DIR = path.join(SM_REPO_ROOT, 'session-manager-operations', 'feedback');
|
|
40
40
|
|
|
41
|
-
// NEVER write here: ~/.claude/session-manager/feedback/ is the
|
|
42
|
-
//
|
|
41
|
+
// NEVER write here: ~/.claude/session-manager/feedback/ is the scheduler's
|
|
42
|
+
// auto-PRD intake (src/main/scheduler.cjs's periodic feedback sweep) — it
|
|
43
43
|
// converts anything dropped there into a scheduled PRD automatically. Filing
|
|
44
44
|
// an RCA there would create an RCA → auto-PRD → run → needs_review → RCA
|
|
45
45
|
// cycle with no human in the loop. Both real destinations below are project
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* singleInstanceGuard.cjs — the losing side of Electron's single-instance
|
|
3
|
+
* lock must actually terminate the process, not just request a quit.
|
|
4
|
+
*
|
|
5
|
+
* app.quit() only requests a graceful shutdown via the normal
|
|
6
|
+
* window-all-closed -> quit -> exit chain; a losing instance calls it before
|
|
7
|
+
* app.whenReady() ever fires (no window, no IPC), so there's nothing for
|
|
8
|
+
* that chain to hook into and the process can sit as a zombie indefinitely.
|
|
9
|
+
* app.exit() terminates immediately without waiting on that chain; the
|
|
10
|
+
* setTimeout is a defense-in-depth backstop in case app.exit() itself is
|
|
11
|
+
* ever delayed by an Electron internal on a future version.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
function terminateLosingInstance(app, { fallbackDelayMs = 2000 } = {}) {
|
|
16
|
+
setTimeout(() => process.exit(0), fallbackDelayMs).unref?.();
|
|
17
|
+
app.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { terminateLosingInstance };
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -63,6 +63,7 @@ const prdParser = require('./scheduler/prdParser.cjs');
|
|
|
63
63
|
const { verifyRun } = require('./runVerify.cjs');
|
|
64
64
|
const logs = require('./logs.cjs');
|
|
65
65
|
const { schemas, validated } = require('./ipcSchemas.cjs');
|
|
66
|
+
const { readBody, sendJson } = require('./lib/localAdminHttp.cjs');
|
|
66
67
|
const {
|
|
67
68
|
POLL_INTERVAL_MS,
|
|
68
69
|
USAGE_REFRESH_INTERVAL_MS,
|
|
@@ -74,6 +75,12 @@ const { runDefinitionOfDoneOnDrain } = require('./lib/dodDrainHook.cjs');
|
|
|
74
75
|
const { fileRcaFeedback, extractRcaBlock } = require('./lib/rcaFeedbackHook.cjs');
|
|
75
76
|
const queueHistory = require('./lib/queueHistory.cjs');
|
|
76
77
|
const queueOps = require('./queueOps.cjs');
|
|
78
|
+
// Feedback-auto-PRD sweep — formerly only run by the external scheduler-watchdog
|
|
79
|
+
// while the app was down (PRD 686 moved it in-app so it also runs while alive).
|
|
80
|
+
// Plain Node module, no Electron dependency; queuePath/prdsDir defaults already
|
|
81
|
+
// match ROOT/QUEUE_PATH below since both resolve the same ~/.claude/session-manager
|
|
82
|
+
// home-dir layout.
|
|
83
|
+
const { sweep: sweepFeedback } = require('../../scripts/lib/watchdogHelpers.cjs');
|
|
77
84
|
|
|
78
85
|
const MAX_INVESTIGATION_DURATION_MS = 30 * 60_000;
|
|
79
86
|
|
|
@@ -201,15 +208,38 @@ function gitHead(cwd) {
|
|
|
201
208
|
});
|
|
202
209
|
}
|
|
203
210
|
|
|
211
|
+
// Best-effort `git fetch --all --prune` in cwd, bounded and never throwing.
|
|
212
|
+
// A PRD that does its work in a separate `git worktree add` checkout (per
|
|
213
|
+
// standards.md's own recommended pattern for shared repos) commits and pushes
|
|
214
|
+
// from THAT worktree, then removes it — the commit never touches job.cwd's
|
|
215
|
+
// own refs, so job.cwd's remote-tracking branches can be stale relative to
|
|
216
|
+
// what was actually pushed. Refreshing them here is what lets the git log
|
|
217
|
+
// --all scan below see a worktree-pushed commit. Resolves once fetch settles
|
|
218
|
+
// (or times out / errors) — callers don't need the result, just the refresh.
|
|
219
|
+
function fetchAllRefs(cwd) {
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
if (!cwd) { resolve(); return; }
|
|
222
|
+
execFile(
|
|
223
|
+
'git',
|
|
224
|
+
['-C', cwd, 'fetch', '--all', '--prune'],
|
|
225
|
+
{ timeout: 20_000, windowsHide: true },
|
|
226
|
+
() => resolve(),
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
204
231
|
// Returns true if ≥1 commit landed on any ref (branch, remote-tracking branch,
|
|
205
232
|
// or tag) in cwd between startedAt and finishedAt (with 60s slack) — not just
|
|
206
|
-
// the currently checked-out branch. Used by the self-heal pass
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
233
|
+
// the currently checked-out branch. Used both by the self-heal pass and by the
|
|
234
|
+
// live commit-guard's fallback (see computeCommittedDuringRun) to derive
|
|
235
|
+
// committedDuringRun from the recorded run window. Fetches remotes first (see
|
|
236
|
+
// fetchAllRefs) so a commit pushed from a separate worktree checkout that was
|
|
237
|
+
// since removed is still visible via job.cwd's remote-tracking refs. Never
|
|
238
|
+
// throws; git-unavailable → false (no override, job stays as-is).
|
|
239
|
+
async function committedInWindow(cwd, startedAt, finishedAt) {
|
|
240
|
+
if (!cwd || !startedAt) return false;
|
|
241
|
+
await fetchAllRefs(cwd);
|
|
211
242
|
return new Promise((resolve) => {
|
|
212
|
-
if (!cwd || !startedAt) { resolve(false); return; }
|
|
213
243
|
const until = finishedAt
|
|
214
244
|
? new Date(Date.parse(finishedAt) + 60_000).toISOString()
|
|
215
245
|
: new Date().toISOString();
|
|
@@ -222,6 +252,16 @@ function committedInWindow(cwd, startedAt, finishedAt) {
|
|
|
222
252
|
});
|
|
223
253
|
}
|
|
224
254
|
|
|
255
|
+
// Live commit-guard: cheap HEAD-diff fast path, falling back to the
|
|
256
|
+
// git-log --all scan when HEAD didn't move on the starting branch. A PRD that
|
|
257
|
+
// checks out other branches, commits real work on each, then checks its
|
|
258
|
+
// starting branch back out before exit leaves HEAD unchanged even though
|
|
259
|
+
// commits landed — the fallback catches that case. Never throws.
|
|
260
|
+
async function computeCommittedDuringRun(cwd, headBefore, headAfter, startedAt, untilIso) {
|
|
261
|
+
if (headBefore && headAfter && headBefore !== headAfter) return true;
|
|
262
|
+
return committedInWindow(cwd, startedAt, untilIso);
|
|
263
|
+
}
|
|
264
|
+
|
|
225
265
|
const ROOT = path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans');
|
|
226
266
|
const PRDS_DIR = path.join(ROOT, 'prds');
|
|
227
267
|
const RUNS_DIR = path.join(ROOT, 'runs');
|
|
@@ -817,6 +857,17 @@ let resumeTimer = null;
|
|
|
817
857
|
let pollLoopTimer = null;
|
|
818
858
|
let rescheduleInterval = null;
|
|
819
859
|
let heartbeatInterval = null;
|
|
860
|
+
// Feedback sweep piggybacks on the 60s heartbeat tick but runs far less often —
|
|
861
|
+
// every Nth tick — since it's a readdir-per-active-project scan, not free.
|
|
862
|
+
// 5 ticks = 5 minutes; well inside sweep()/activeProjectCwds()'s 90-minute
|
|
863
|
+
// active-session window, so no active project can be missed between sweeps.
|
|
864
|
+
const FEEDBACK_SWEEP_TICK_INTERVAL = 5;
|
|
865
|
+
let feedbackSweepTickCount = 0;
|
|
866
|
+
/** Pure gate for the tick counter above — exported so the wiring is testable
|
|
867
|
+
* without waiting on real setInterval timers. */
|
|
868
|
+
function feedbackSweepDue(tickCount, interval = FEEDBACK_SWEEP_TICK_INTERVAL) {
|
|
869
|
+
return tickCount >= interval;
|
|
870
|
+
}
|
|
820
871
|
// In-memory set of slugs currently spawned in this process. Prevents
|
|
821
872
|
// double-spawn when runDueJobs() is called while jobs are in flight.
|
|
822
873
|
const runningSet = new Set();
|
|
@@ -1059,6 +1110,79 @@ function resetJobFields(job, errorMsg) {
|
|
|
1059
1110
|
delete job.verifierVerdict;
|
|
1060
1111
|
}
|
|
1061
1112
|
|
|
1113
|
+
// Grace period between a boot orphan's SIGTERM and reading its log to
|
|
1114
|
+
// classify the outcome — matches killOrphanClaudePid's own internal 5s
|
|
1115
|
+
// SIGKILL follow-up delay, plus a small margin so classification always runs
|
|
1116
|
+
// after that SIGKILL has had a chance to land.
|
|
1117
|
+
const BOOT_ORPHAN_KILL_GRACE_MS = 6000;
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* partitionBootOrphans(jobs, isAlive?) → { immediate: string[], deferred: string[] }
|
|
1121
|
+
*
|
|
1122
|
+
* Pure decision split for boot reconciliation. A 'running' job whose recorded
|
|
1123
|
+
* pid is still alive must NOT be classified from its log yet — the orphaned
|
|
1124
|
+
* process may still be writing to it, so reading now risks misclassifying a
|
|
1125
|
+
* job that is about to emit result:success as no_result and double-running it.
|
|
1126
|
+
* Ported from reconcileQueueOffline's cross-tick escalation (see
|
|
1127
|
+
* scripts/lib/watchdogHelpers.cjs) — here it's a single deferred window since
|
|
1128
|
+
* this process stays up to revisit it, rather than a separate short-lived
|
|
1129
|
+
* watchdog process needing another tick.
|
|
1130
|
+
*/
|
|
1131
|
+
function partitionBootOrphans(jobs, isAlive = claudePidAlive) {
|
|
1132
|
+
const immediate = [];
|
|
1133
|
+
const deferred = [];
|
|
1134
|
+
for (const j of jobs) {
|
|
1135
|
+
if (j.status !== 'running') continue;
|
|
1136
|
+
const pid = j.runtime?.pid;
|
|
1137
|
+
if (pid && isAlive(pid)) {
|
|
1138
|
+
deferred.push(j.slug);
|
|
1139
|
+
} else {
|
|
1140
|
+
immediate.push(j.slug);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return { immediate, deferred };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* applyOrphanOutcome(job, outcome, killNote?) → void
|
|
1148
|
+
*
|
|
1149
|
+
* Mutates `job` in place to finalize a boot-orphaned 'running' job given its
|
|
1150
|
+
* classified run outcome: success/failed finalize terminally; no_result/unknown
|
|
1151
|
+
* re-queues to pending bounded by ORPHAN_REQUEUE_CAP. The status-mutation
|
|
1152
|
+
* semantics (and the cap-exhaustion boundary) match the now-deleted
|
|
1153
|
+
* reconcileQueueOffline (scripts/lib/watchdogHelpers.cjs) verbatim; killNote
|
|
1154
|
+
* plumbing differs slightly (see call sites) since this path always knows
|
|
1155
|
+
* pid liveness up front rather than re-checking per tick.
|
|
1156
|
+
*/
|
|
1157
|
+
function applyOrphanOutcome(job, outcome, killNote = '') {
|
|
1158
|
+
const now = new Date().toISOString();
|
|
1159
|
+
if (outcome === 'success') {
|
|
1160
|
+
job.status = 'completed';
|
|
1161
|
+
job.exitCode = 0;
|
|
1162
|
+
job.error = null;
|
|
1163
|
+
job.finishedAt = now;
|
|
1164
|
+
delete job.runtime;
|
|
1165
|
+
} else if (outcome === 'failed') {
|
|
1166
|
+
job.status = 'failed';
|
|
1167
|
+
job.exitCode = job.exitCode ?? 1;
|
|
1168
|
+
job.error = `orphaned: app restarted while running${killNote}`;
|
|
1169
|
+
job.finishedAt = now;
|
|
1170
|
+
delete job.runtime;
|
|
1171
|
+
} else {
|
|
1172
|
+
const tries = job.orphanRetries ?? 0;
|
|
1173
|
+
if (tries < ORPHAN_REQUEUE_CAP) {
|
|
1174
|
+
resetJobFields(job, `orphaned: app restarted mid-run, re-queued (attempt ${tries + 1}/${ORPHAN_REQUEUE_CAP})${killNote}`);
|
|
1175
|
+
job.orphanRetries = tries + 1;
|
|
1176
|
+
} else {
|
|
1177
|
+
job.status = 'failed';
|
|
1178
|
+
job.exitCode = job.exitCode ?? 1;
|
|
1179
|
+
job.error = `orphaned: app restarted while running, exhausted ${ORPHAN_REQUEUE_CAP} re-queue attempts${killNote}`;
|
|
1180
|
+
job.finishedAt = now;
|
|
1181
|
+
delete job.runtime;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1062
1186
|
/** Scan the tail of a job's log for the canonical rate-limit signal. We look
|
|
1063
1187
|
* at the last 16 KB — final result event always lands at the end.
|
|
1064
1188
|
* Uses readTail() so no raw fd lifecycle is needed here. */
|
|
@@ -1730,7 +1854,13 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
1730
1854
|
// Used by the sentinel override: SCHEDULER_VERDICT: PASS + a landed
|
|
1731
1855
|
// commit together override incidental transcript noise verdicts.
|
|
1732
1856
|
const headAtExit = await gitHead(guardCwd);
|
|
1733
|
-
const committedDuringRun =
|
|
1857
|
+
const committedDuringRun = await computeCommittedDuringRun(
|
|
1858
|
+
guardCwd,
|
|
1859
|
+
guardHeadBefore,
|
|
1860
|
+
headAtExit,
|
|
1861
|
+
job.startedAt,
|
|
1862
|
+
new Date().toISOString(),
|
|
1863
|
+
);
|
|
1734
1864
|
|
|
1735
1865
|
const prdPath = path.join(PRDS_DIR, `${job.slug}.md`);
|
|
1736
1866
|
const stateForDeps = await readQueue();
|
|
@@ -2944,65 +3074,60 @@ async function init() {
|
|
|
2944
3074
|
// classifyRunOutcome calls readTail → fs.readFileSync (up to 64 KB per job).
|
|
2945
3075
|
// Pre-compute all outcomes BEFORE entering the mutate lock so the blocking I/O
|
|
2946
3076
|
// does not stall the event loop or hold the mutateTail chain during startup.
|
|
3077
|
+
//
|
|
3078
|
+
// Jobs whose recorded pid is still alive are deferred (not classified here) —
|
|
3079
|
+
// see partitionBootOrphans. Everything else (dead pid or no pid) is safe to
|
|
3080
|
+
// classify immediately below.
|
|
2947
3081
|
const bootSnap = readQueueSync();
|
|
3082
|
+
const { immediate: immediateSlugs, deferred: deferredSlugs } = partitionBootOrphans(bootSnap.jobs);
|
|
2948
3083
|
const bootOutcomes = new Map();
|
|
2949
3084
|
for (const j of bootSnap.jobs) {
|
|
2950
|
-
if (j.
|
|
3085
|
+
if (!immediateSlugs.includes(j.slug)) continue;
|
|
2951
3086
|
const logPath = j.runId ? path.join(RUNS_DIR, j.runId, `${j.slug}.log`) : null;
|
|
2952
3087
|
bootOutcomes.set(j.slug, logPath ? classifyRunOutcome(logPath) : 'unknown');
|
|
2953
3088
|
}
|
|
2954
3089
|
await mutate((state) => {
|
|
2955
3090
|
for (const j of state.jobs) {
|
|
2956
|
-
if (j.status !== 'running') continue;
|
|
2957
|
-
const pid = j.runtime?.pid;
|
|
2958
|
-
let killNote = '';
|
|
2959
|
-
if (pid) {
|
|
2960
|
-
const result = killOrphanClaudePid(pid);
|
|
2961
|
-
killNote = ` (orphan pid=${pid}: ${result})`;
|
|
2962
|
-
if (result === 'killed') {
|
|
2963
|
-
console.log(`[scheduler] boot: SIGTERM'd orphan claude pid=${pid} for ${j.slug}`);
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
3091
|
+
if (j.status !== 'running' || !immediateSlugs.includes(j.slug)) continue;
|
|
2966
3092
|
const outcome = bootOutcomes.get(j.slug) ?? 'unknown';
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
j.error = null;
|
|
2972
|
-
j.finishedAt = new Date().toISOString();
|
|
2973
|
-
delete j.runtime;
|
|
2974
|
-
console.log(`[scheduler] boot reconcile: slug=${j.slug} outcome=success → completed`);
|
|
2975
|
-
} else if (outcome === 'failed') {
|
|
2976
|
-
// The log carries a real failure result event — a genuine failure, keep it.
|
|
2977
|
-
j.status = 'failed';
|
|
2978
|
-
j.exitCode = j.exitCode ?? 1;
|
|
2979
|
-
j.error = `orphaned: app restarted while running${killNote}`;
|
|
2980
|
-
j.finishedAt = new Date().toISOString();
|
|
2981
|
-
delete j.runtime;
|
|
2982
|
-
console.log(`[scheduler] boot reconcile: slug=${j.slug} outcome=failed → failed`);
|
|
2983
|
-
} else {
|
|
2984
|
-
// no_result / unknown: the run was interrupted (host died / app restarted)
|
|
2985
|
-
// with NO evidence it failed on its own merits. Punishing the PRD here is
|
|
2986
|
-
// the wrong call — it demands a manual flip and burns an Opus fix-plan on a
|
|
2987
|
-
// job that never actually failed. Re-queue it (bounded) so an app restart
|
|
2988
|
-
// self-recovers. Mirrors the transient-kill auto-retry on the live path.
|
|
2989
|
-
const tries = j.orphanRetries ?? 0;
|
|
2990
|
-
if (tries < ORPHAN_REQUEUE_CAP) {
|
|
2991
|
-
resetJobFields(j, `orphaned: app restarted mid-run, re-queued (attempt ${tries + 1}/${ORPHAN_REQUEUE_CAP})${killNote}`);
|
|
2992
|
-
j.orphanRetries = tries + 1;
|
|
2993
|
-
console.log(`[scheduler] boot reconcile: slug=${j.slug} outcome=${outcome} → re-queued (${tries + 1}/${ORPHAN_REQUEUE_CAP})`);
|
|
2994
|
-
} else {
|
|
2995
|
-
j.status = 'failed';
|
|
2996
|
-
j.exitCode = j.exitCode ?? 1;
|
|
2997
|
-
j.error = `orphaned: app restarted while running, exhausted ${ORPHAN_REQUEUE_CAP} re-queue attempts${killNote}`;
|
|
2998
|
-
j.finishedAt = new Date().toISOString();
|
|
2999
|
-
delete j.runtime;
|
|
3000
|
-
console.log(`[scheduler] boot reconcile: slug=${j.slug} outcome=${outcome} → failed (orphan retries exhausted)`);
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3093
|
+
const pid = j.runtime?.pid;
|
|
3094
|
+
const killNote = pid ? ` (orphan pid=${pid}: dead)` : '';
|
|
3095
|
+
applyOrphanOutcome(j, outcome, killNote);
|
|
3096
|
+
console.log(`[scheduler] boot reconcile: slug=${j.slug} outcome=${outcome} → status=${j.status}`);
|
|
3003
3097
|
}
|
|
3004
3098
|
});
|
|
3005
3099
|
|
|
3100
|
+
// Still-alive orphans: SIGTERM (+ killOrphanClaudePid's own deferred SIGKILL
|
|
3101
|
+
// follow-up) now, but classification waits until BOOT_ORPHAN_KILL_GRACE_MS
|
|
3102
|
+
// later — reading the log while the orphan might still be writing to it
|
|
3103
|
+
// could misclassify an about-to-succeed run as no_result and double-run the
|
|
3104
|
+
// same PRD (2026-05-21 incident this guard exists for).
|
|
3105
|
+
for (const slug of deferredSlugs) {
|
|
3106
|
+
const j = bootSnap.jobs.find((x) => x.slug === slug);
|
|
3107
|
+
const pid = j?.runtime?.pid;
|
|
3108
|
+
const bootRunId = j?.runId ?? null; // captured now — guards against reconciling a DIFFERENT later run of the same slug
|
|
3109
|
+
if (!pid) continue;
|
|
3110
|
+
const result = killOrphanClaudePid(pid);
|
|
3111
|
+
const killNote = ` (orphan pid=${pid}: ${result})`;
|
|
3112
|
+
if (result === 'killed') {
|
|
3113
|
+
console.log(`[scheduler] boot: SIGTERM'd orphan claude pid=${pid} for ${slug} — deferring finalize ${BOOT_ORPHAN_KILL_GRACE_MS}ms`);
|
|
3114
|
+
}
|
|
3115
|
+
setTimeout(() => {
|
|
3116
|
+
const logPath = j.runId ? path.join(RUNS_DIR, j.runId, `${j.slug}.log`) : null;
|
|
3117
|
+
const outcome = logPath ? classifyRunOutcome(logPath) : 'unknown';
|
|
3118
|
+
mutate((state) => {
|
|
3119
|
+
const cur = state.jobs.find((x) => x.slug === slug);
|
|
3120
|
+
// Race guard: bail if the job already resolved, OR if it's already been
|
|
3121
|
+
// re-picked into a NEW run (different runId) within the grace window —
|
|
3122
|
+
// that new run is not the boot orphan we SIGTERM'd and must not be
|
|
3123
|
+
// touched by this stale classification.
|
|
3124
|
+
if (!cur || cur.status !== 'running' || cur.runId !== bootRunId) return;
|
|
3125
|
+
applyOrphanOutcome(cur, outcome, killNote);
|
|
3126
|
+
console.log(`[scheduler] boot reconcile (deferred): slug=${slug} outcome=${outcome} → status=${cur.status}`);
|
|
3127
|
+
}).catch((e) => console.error(`[scheduler] deferred boot reconcile failed for ${slug}:`, e?.message));
|
|
3128
|
+
}, BOOT_ORPHAN_KILL_GRACE_MS).unref?.();
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3006
3131
|
// If we boot up while paused with a resumeAt in the past, clear it. This
|
|
3007
3132
|
// happens when the app was closed across the reset window.
|
|
3008
3133
|
const boot = await readQueue();
|
|
@@ -3074,6 +3199,16 @@ async function init() {
|
|
|
3074
3199
|
utilization: cachedUtilization,
|
|
3075
3200
|
consecutiveFailures,
|
|
3076
3201
|
});
|
|
3202
|
+
|
|
3203
|
+
feedbackSweepTickCount++;
|
|
3204
|
+
if (feedbackSweepDue(feedbackSweepTickCount, FEEDBACK_SWEEP_TICK_INTERVAL)) {
|
|
3205
|
+
feedbackSweepTickCount = 0;
|
|
3206
|
+
try {
|
|
3207
|
+
sweepFeedback();
|
|
3208
|
+
} catch (e) {
|
|
3209
|
+
console.warn('[scheduler] feedback sweep failed', e?.message);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3077
3212
|
}, 60_000);
|
|
3078
3213
|
if (heartbeatInterval.unref) heartbeatInterval.unref();
|
|
3079
3214
|
|
|
@@ -3188,7 +3323,7 @@ const remote = {
|
|
|
3188
3323
|
},
|
|
3189
3324
|
|
|
3190
3325
|
// Exposes the module-level allocateParallelGroup (PRD 548) to callers that
|
|
3191
|
-
// only hold the `remote` object (
|
|
3326
|
+
// only hold the `remote` object (lib/prdCreate.cjs's create-prd route) —
|
|
3192
3327
|
// reuses the same allocator the file-based /develop authoring path relies
|
|
3193
3328
|
// on implicitly, rather than re-deriving NN here.
|
|
3194
3329
|
allocateParallelGroup,
|
|
@@ -3216,4 +3351,36 @@ const remote = {
|
|
|
3216
3351
|
},
|
|
3217
3352
|
};
|
|
3218
3353
|
|
|
3219
|
-
|
|
3354
|
+
// Registers the two job-management admin HTTP routes (PRD 689 — moved
|
|
3355
|
+
// verbatim out of the former standalone admin HTTP server module's
|
|
3356
|
+
// handleRequest, no behavior change) against an injected localAdminHttp.cjs
|
|
3357
|
+
// transport. `remoteObj` is accepted as a parameter (defaults to this
|
|
3358
|
+
// module's own `remote`) so the route logic stays testable in isolation
|
|
3359
|
+
// without booting Electron, matching that former module's original
|
|
3360
|
+
// dependency-injection pattern.
|
|
3361
|
+
function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
3362
|
+
adminHttp.registerRoute('GET', '/admin/scheduler/jobs', async (req, res) => {
|
|
3363
|
+
const jobs = await remoteObj.listJobs();
|
|
3364
|
+
sendJson(res, 200, jobs);
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3367
|
+
adminHttp.registerRoute('POST', '/admin/scheduler/reset-job', async (req, res) => {
|
|
3368
|
+
const raw = await readBody(req);
|
|
3369
|
+
let parsed;
|
|
3370
|
+
try {
|
|
3371
|
+
parsed = raw ? JSON.parse(raw) : {};
|
|
3372
|
+
} catch {
|
|
3373
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
const slug = typeof parsed.slug === 'string' ? parsed.slug : null;
|
|
3377
|
+
if (!slug) {
|
|
3378
|
+
sendJson(res, 400, { ok: false, error: 'missing slug' });
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
const result = await remoteObj.resetJob(slug);
|
|
3382
|
+
sendJson(res, 200, result);
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, computeCommittedDuringRun, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, feedbackSweepDue, FEEDBACK_SWEEP_TICK_INTERVAL, sweepFeedback, registerAdminRoutes };
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Storage: ~/.config/session-manager/tabs.json
|
|
6
6
|
* Shape: { tabs: PersistedTab[], activeTabId: string | null, savedAt: number }
|
|
7
7
|
*
|
|
8
|
-
* Only serializable, durable fields are persisted: id,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Only serializable, durable fields are persisted: id, sessionId, cwd,
|
|
9
|
+
* label, presetId. Runtime-only fields (pid, status, startupCommand,
|
|
10
|
+
* exitCode) are recomputed on boot.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const fs = require('node:fs');
|
|
@@ -58,8 +58,7 @@ async function markFreshRestart() {
|
|
|
58
58
|
const crypto = require('node:crypto');
|
|
59
59
|
const freshTabs = tabs.map((t) => ({
|
|
60
60
|
...t,
|
|
61
|
-
|
|
62
|
-
chatSessionId: crypto.randomUUID(),
|
|
61
|
+
sessionId: crypto.randomUUID(),
|
|
63
62
|
}));
|
|
64
63
|
await save({ tabs: freshTabs, activeTabId, freshStart: true });
|
|
65
64
|
}
|
|
@@ -328,7 +328,7 @@ loop but had no documented programmatic queueing path. This section is that path
|
|
|
328
328
|
### The `scheduler_create_prd` MCP tool
|
|
329
329
|
|
|
330
330
|
Wraps `POST /admin/scheduler/create-prd` on the loopback admin API
|
|
331
|
-
(`src/main/
|
|
331
|
+
(`src/main/lib/localAdminHttp.cjs` + `src/main/lib/prdCreate.cjs`, PRD 549/688) via `scripts/scheduler-mcp-server.cjs`, registered in this
|
|
332
332
|
repo's `.mcp.json` as the `session-manager-scheduler` MCP server. An external project wanting to
|
|
333
333
|
call it from its own automation needs the equivalent MCP server registration pointing at this
|
|
334
334
|
repo's `scripts/scheduler-mcp-server.cjs`, or can call the admin HTTP route directly (same
|
|
@@ -348,7 +348,7 @@ request/response shape) using the token at `~/.claude/session-manager/admin-api.
|
|
|
348
348
|
| `slug` | string | no | kebab-case; derived from `title` if omitted |
|
|
349
349
|
| `parallelGroup` | number | no | opt into an existing `NN` group instead of allocating a new one |
|
|
350
350
|
|
|
351
|
-
**Return** (`{nn, filename, status}`, per `
|
|
351
|
+
**Return** (`{nn, filename, status}`, per `prdCreate.cjs`'s registerAdminRoute):
|
|
352
352
|
```json
|
|
353
353
|
{ "nn": 550, "filename": "550-my-feature.md", "status": "queued" }
|
|
354
354
|
```
|
|
@@ -380,9 +380,9 @@ produces by hand.
|
|
|
380
380
|
### The app-must-be-running caveat (read this first)
|
|
381
381
|
|
|
382
382
|
**`scheduler_create_prd` only works while the session-manager Electron app is running on this
|
|
383
|
-
machine.** The admin server it depends on (`src/main/
|
|
383
|
+
machine.** The admin server it depends on (`src/main/lib/localAdminHttp.cjs`) is hosted *inside* the
|
|
384
384
|
Electron process — it binds a loopback port and writes its token to
|
|
385
|
-
`~/.claude/session-manager/admin-api.json` on app boot (see `CLAUDE.md`'s `
|
|
385
|
+
`~/.claude/session-manager/admin-api.json` on app boot (see `CLAUDE.md`'s `localAdminHttp.cjs`
|
|
386
386
|
architecture entry) and stops existing the moment the app quits — it is not a standalone daemon.
|
|
387
387
|
If the app is closed, `scheduler-mcp-server.cjs` cannot read a live port/token and every call
|
|
388
388
|
returns the error `session-manager app is not running (admin API unreachable) — start it first`.
|
package/src/main/transcripts.cjs
CHANGED
|
@@ -38,100 +38,16 @@ function attachWindow(w) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const { encodeCwd } = require('./lib/encodeCwd.cjs');
|
|
41
|
+
const { classifyLine } = require('./lib/classifyTranscriptLine.cjs');
|
|
41
42
|
|
|
42
43
|
function transcriptPath(cwd, sessionUuid) {
|
|
43
44
|
return path.join(os.homedir(), '.claude', 'projects', encodeCwd(cwd), `${sessionUuid}.jsonl`);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
const MAX_RAW_STR = 4096;
|
|
47
47
|
// Cap bytes read per readDelta pass. Bounds memory on first attach to a very
|
|
48
48
|
// large transcript (hundreds of MB) — see readDelta's seek-to-tail branch.
|
|
49
49
|
const MAX_DELTA_BYTES = 8 * 1024 * 1024;
|
|
50
50
|
|
|
51
|
-
// Block types whose text/content fields are parsed structurally by
|
|
52
|
-
// orchestrator.ts / race.ts — truncating them produces mid-token "…" and
|
|
53
|
-
// unparseable JSON, so they are exempt from the size cap.
|
|
54
|
-
const EXEMPT_TYPES = new Set(['tool_result', 'tool_use']);
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Cap string fields in a content block array so arbitrary tool output doesn't
|
|
58
|
-
* bloat the ring buffer. Blocks whose type is in EXEMPT_TYPES are passed
|
|
59
|
-
* through intact so that structured result payloads survive to the digest
|
|
60
|
-
* parsers in race.ts / orchestrator.ts.
|
|
61
|
-
*/
|
|
62
|
-
function trimContentArray(content) {
|
|
63
|
-
if (!Array.isArray(content)) return content;
|
|
64
|
-
return content.map((block) => {
|
|
65
|
-
if (!block || typeof block !== 'object') return block;
|
|
66
|
-
if (EXEMPT_TYPES.has(block.type)) return block;
|
|
67
|
-
const b = { ...block };
|
|
68
|
-
if (typeof b.text === 'string' && b.text.length > MAX_RAW_STR) {
|
|
69
|
-
b.text = b.text.slice(0, MAX_RAW_STR) + '…';
|
|
70
|
-
}
|
|
71
|
-
if (typeof b.content === 'string' && b.content.length > MAX_RAW_STR) {
|
|
72
|
-
b.content = b.content.slice(0, MAX_RAW_STR) + '…';
|
|
73
|
-
}
|
|
74
|
-
if (Array.isArray(b.content)) {
|
|
75
|
-
b.content = trimContentArray(b.content);
|
|
76
|
-
}
|
|
77
|
-
return b;
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Build the slim raw projection used by race.ts and orchestrator.ts. */
|
|
82
|
-
function makeRaw(obj) {
|
|
83
|
-
const msgContent = obj?.message?.content;
|
|
84
|
-
return { message: { content: trimContentArray(msgContent) } };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Parse one JSONL line defensively. Real schema drifts, so we pass through
|
|
89
|
-
* anything that parses and tag a coarse `kind`.
|
|
90
|
-
*/
|
|
91
|
-
function classifyLine(obj) {
|
|
92
|
-
if (!obj || typeof obj !== 'object') return null;
|
|
93
|
-
// Many shapes exist — try several common fields.
|
|
94
|
-
const type = obj.type || obj.event || obj.role;
|
|
95
|
-
const msg = obj.message || obj;
|
|
96
|
-
const content = msg?.content;
|
|
97
|
-
|
|
98
|
-
// Usage rollups arrive as summary events.
|
|
99
|
-
if (obj.usage || msg?.usage) {
|
|
100
|
-
return { kind: 'usage', data: obj.usage || msg.usage, raw: makeRaw(obj) };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Tool uses: scan content array for tool_use blocks.
|
|
104
|
-
if (Array.isArray(content)) {
|
|
105
|
-
for (const block of content) {
|
|
106
|
-
if (block?.type === 'tool_use') {
|
|
107
|
-
if (block.name === 'TodoWrite') {
|
|
108
|
-
return { kind: 'todo_write', data: block.input?.todos || block.input || [], raw: makeRaw(obj) };
|
|
109
|
-
}
|
|
110
|
-
if (block.name === 'ExitPlanMode' || block.name === 'EnterPlanMode') {
|
|
111
|
-
return { kind: 'plan', data: block.input, raw: makeRaw(obj) };
|
|
112
|
-
}
|
|
113
|
-
if (block.name === 'Agent' || block.name === 'Task') {
|
|
114
|
-
// Include block.id as toolUseId so the live store can match the
|
|
115
|
-
// corresponding tool_result and update per-agent lastActivityAt.
|
|
116
|
-
return { kind: 'agent_spawn', data: { ...block.input, toolUseId: block.id }, raw: makeRaw(obj) };
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
kind: 'tool_use',
|
|
120
|
-
data: { name: block.name, input: block.input, id: block.id },
|
|
121
|
-
raw: makeRaw(obj),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
// tool_result carries the tool_use_id of the completed Task/Agent call.
|
|
125
|
-
// The live store uses this to update the agent's lastActivityAt bookend.
|
|
126
|
-
if (block?.type === 'tool_result' && block.tool_use_id) {
|
|
127
|
-
return { kind: 'tool_result', data: { toolUseId: block.tool_use_id }, raw: makeRaw(obj) };
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return { kind: type || 'message', data: obj, raw: makeRaw(obj) };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
51
|
/**
|
|
136
52
|
* Read new bytes from sub.filePath into sub.offset/pending/inode in place.
|
|
137
53
|
* Resets offset+pending when the file inode changes (rename+replace rotation).
|
package/src/main/webRemote.cjs
CHANGED
|
@@ -729,13 +729,13 @@ async function pushSessionList() {
|
|
|
729
729
|
if (_e2e.state !== 'authenticated') return;
|
|
730
730
|
const sessionsStore = require('./sessionsStore.cjs');
|
|
731
731
|
const data = await sessionsStore.load();
|
|
732
|
-
// Normalize persisted tabs → SessionMeta. tabId ===
|
|
732
|
+
// Normalize persisted tabs → SessionMeta. tabId === sessionId so it
|
|
733
733
|
// matches the transcript JSONL name used by cmd:session:subscribe.
|
|
734
734
|
const sessions = (data?.tabs ?? []).map((t) => ({
|
|
735
|
-
tabId: t.
|
|
735
|
+
tabId: t.sessionId,
|
|
736
736
|
cwd: t.cwd,
|
|
737
737
|
title: t.label || t.cwd,
|
|
738
|
-
state: _sessionWatchers.get(t.
|
|
738
|
+
state: _sessionWatchers.get(t.sessionId)?.state ?? null,
|
|
739
739
|
}));
|
|
740
740
|
const payload = { sessions, activeTabId: data?.activeTabId ?? null };
|
|
741
741
|
const json = JSON.stringify(payload);
|
package/src/preload/api.d.ts
CHANGED
|
@@ -163,9 +163,7 @@ export interface SubscribeResult {
|
|
|
163
163
|
|
|
164
164
|
export interface PersistedTab {
|
|
165
165
|
id: string;
|
|
166
|
-
|
|
167
|
-
/** Chat mode's own session id, decoupled from claudeSessionId. Optional for backwards-compat with tabs.json written before this field existed. */
|
|
168
|
-
chatSessionId?: string;
|
|
166
|
+
sessionId: string;
|
|
169
167
|
cwd: string;
|
|
170
168
|
label: string;
|
|
171
169
|
presetId: string | null;
|
|
@@ -650,6 +648,7 @@ export interface FilesRenameResult { ok: boolean; newPath?: string; error: strin
|
|
|
650
648
|
export interface FilesDeleteResult { ok: boolean; error: string | null }
|
|
651
649
|
export interface FilesDuplicateResult { ok: boolean; path?: string; error?: string | null }
|
|
652
650
|
export interface DocEditResult { ok: boolean; after?: string; error?: string }
|
|
651
|
+
export interface DocEditSessionResult { tabId: string; requestId: string; ok: boolean; after?: string; error?: string }
|
|
653
652
|
|
|
654
653
|
export interface SearchFileEntry {
|
|
655
654
|
name: string;
|
|
@@ -1272,7 +1271,17 @@ export interface SessionManagerAPI {
|
|
|
1272
1271
|
duplicate: (path: string) => Promise<FilesDuplicateResult>;
|
|
1273
1272
|
};
|
|
1274
1273
|
docEdit: {
|
|
1275
|
-
run: (payload: { path: string; before: string; instruction: string }) => Promise<DocEditResult>;
|
|
1274
|
+
run: (payload: { path: string; before: string; instruction: string; documentText?: string }) => Promise<DocEditResult>;
|
|
1275
|
+
runInSession: (payload: {
|
|
1276
|
+
tabId: string;
|
|
1277
|
+
sessionId: string;
|
|
1278
|
+
cwd: string;
|
|
1279
|
+
before: string;
|
|
1280
|
+
instruction: string;
|
|
1281
|
+
documentText?: string;
|
|
1282
|
+
requestId: string;
|
|
1283
|
+
}) => Promise<{ ok: boolean }>;
|
|
1284
|
+
onSessionResult: (handler: (payload: DocEditSessionResult) => void) => () => void;
|
|
1276
1285
|
};
|
|
1277
1286
|
/** Consolidated shell open/reveal. One method, discriminated on `as`, replaces
|
|
1278
1287
|
* the former app.openIn* / app.openExternal / files.openExternal / files.showInFinder.
|
|
@@ -1452,8 +1461,9 @@ export interface SessionManagerAPI {
|
|
|
1452
1461
|
chat: {
|
|
1453
1462
|
/** Spawn a headless `claude -p` run for a tab. Results arrive via the on* listeners. */
|
|
1454
1463
|
run: (payload: ChatRunPayload) => Promise<{ ok: boolean }>;
|
|
1455
|
-
/** Cancel the in-flight run for a tab (SIGTERM→SIGKILL).
|
|
1456
|
-
|
|
1464
|
+
/** Cancel the in-flight run for a tab (SIGTERM→SIGKILL). Resolves once the
|
|
1465
|
+
* run has fully settled (terminal event fired). No-op when idle. */
|
|
1466
|
+
cancel: (tabId: string) => Promise<void>;
|
|
1457
1467
|
onQueued: (handler: (e: ChatRunQueuedEvent) => void) => () => void;
|
|
1458
1468
|
onRunStarted: (handler: (e: ChatRunStartedEvent) => void) => () => void;
|
|
1459
1469
|
onOutput: (handler: (e: ChatRunOutputEvent) => void) => () => void;
|
package/src/preload/index.cjs
CHANGED
|
@@ -229,6 +229,12 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
229
229
|
},
|
|
230
230
|
docEdit: {
|
|
231
231
|
run: (payload) => ipcRenderer.invoke('docedit:run', payload),
|
|
232
|
+
runInSession: (payload) => ipcRenderer.invoke('docedit:run-in-session', payload),
|
|
233
|
+
onSessionResult: (handler) => {
|
|
234
|
+
const listener = (_e, payload) => handler(payload);
|
|
235
|
+
ipcRenderer.on('docedit:session-result', listener);
|
|
236
|
+
return () => ipcRenderer.removeListener('docedit:session-result', listener);
|
|
237
|
+
},
|
|
232
238
|
},
|
|
233
239
|
// Consolidated shell open/reveal — see shell:open in index.cjs.
|
|
234
240
|
// as: 'editor' | 'fileInEditor' | 'finder' | 'terminal' | 'external' | 'openPath' | 'revealPath'
|
|
@@ -387,8 +393,9 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
387
393
|
chat: {
|
|
388
394
|
/** Spawn a headless claude -p job. Results arrive via the on* listeners. */
|
|
389
395
|
run: (payload) => ipcRenderer.invoke('chat:run', payload),
|
|
390
|
-
/** Cancel an in-flight run for the given tabId.
|
|
391
|
-
|
|
396
|
+
/** Cancel an in-flight run for the given tabId. Resolves once the run has
|
|
397
|
+
* fully settled (terminal event fired). No-op if none running. */
|
|
398
|
+
cancel: (tabId) => ipcRenderer.invoke('chat:cancel', { tabId }),
|
|
392
399
|
onQueued: (handler) => {
|
|
393
400
|
const listener = (_e, payload) => handler(payload);
|
|
394
401
|
ipcRenderer.on('chat:run:queued', listener);
|