claude-code-session-manager 0.38.1 → 0.38.3

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.
@@ -60,6 +60,8 @@ const { openLog, withChildAndLog } = require('./lib/childWithLog.cjs');
60
60
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
61
61
  const { createBroadcastCoalescer } = require('./lib/broadcastCoalescer.cjs');
62
62
  const prdParser = require('./scheduler/prdParser.cjs');
63
+ const sessionsStore = require('./sessionsStore.cjs');
64
+ const { enqueueExternalPrompt } = require('./chatRunner.cjs');
63
65
  const { verifyRun } = require('./runVerify.cjs');
64
66
  const logs = require('./logs.cjs');
65
67
  const { schemas, validated } = require('./ipcSchemas.cjs');
@@ -1185,6 +1187,62 @@ function applyOrphanOutcome(job, outcome, killNote = '') {
1185
1187
  }
1186
1188
  }
1187
1189
 
1190
+ /**
1191
+ * isNotifiableTerminalStatus(effectiveStatus) → boolean
1192
+ *
1193
+ * Gates notifyOriginatingTab to true terminal transitions only: 'completed'
1194
+ * and 'failed'. Excludes 'needs_review' (not yet truly done — may still
1195
+ * auto-fix) and, implicitly, the rateLimited/paused-queue path, which never
1196
+ * reaches effectiveStatus computation at all (it takes the treatAsPending
1197
+ * branch in spawnJob and resets the job to pending instead).
1198
+ */
1199
+ function isNotifiableTerminalStatus(effectiveStatus) {
1200
+ return effectiveStatus === 'completed' || effectiveStatus === 'failed';
1201
+ }
1202
+
1203
+ /**
1204
+ * notifyOriginatingTab(job) → void
1205
+ *
1206
+ * On a true terminal transition (completed/failed — never the benign
1207
+ * rateLimited auto-pause, which resets the job to pending instead), push a
1208
+ * short status prompt into the chat tab that queued this PRD via
1209
+ * enqueueExternalPrompt (PRD 753). Resolution order: (1) the PRD's own
1210
+ * `sourceTabId` frontmatter, captured at creation time; (2) the first open
1211
+ * tab (per sessionsStore's persisted tabs.json) whose cwd matches the job's
1212
+ * cwd — first match only, no fan-out to multiple matching tabs; (3) no-op.
1213
+ * Never throws to the caller (fire-and-forget from spawnJob). Deps are
1214
+ * injectable (mirrors partitionBootOrphans's isAlive param) so unit tests can
1215
+ * exercise the resolution logic without touching disk/electron.
1216
+ */
1217
+ async function notifyOriginatingTab(job, {
1218
+ parsePrdRaw = prdParser.parsePrdRaw,
1219
+ loadSessions = sessionsStore.load,
1220
+ sendPrompt = enqueueExternalPrompt,
1221
+ } = {}) {
1222
+ try {
1223
+ const prdPath = path.join(PRDS_DIR, `${job.slug}.md`);
1224
+ const prd = await parsePrdRaw(prdPath).catch(() => null);
1225
+
1226
+ let targetTabId = prd?.sourceTabId || null;
1227
+ if (!targetTabId) {
1228
+ const jobCwd = job.cwd || null;
1229
+ if (jobCwd) {
1230
+ const { tabs } = await loadSessions();
1231
+ const match = (tabs || []).find((t) => t && t.cwd === jobCwd);
1232
+ targetTabId = match?.id || null;
1233
+ }
1234
+ }
1235
+ if (!targetTabId) {
1236
+ console.log(`[scheduler] notifyOriginatingTab: no target tab for ${job.slug}, skipping`);
1237
+ return;
1238
+ }
1239
+
1240
+ sendPrompt(targetTabId, `PRD ${job.slug} finished: ${job.status}. Check Scheduler for details.`);
1241
+ } catch (e) {
1242
+ console.error('[scheduler] notifyOriginatingTab error', job?.slug, e);
1243
+ }
1244
+ }
1245
+
1188
1246
  /** Scan the tail of a job's log for the canonical rate-limit signal. We look
1189
1247
  * at the last 16 KB — final result event always lands at the end.
1190
1248
  * Uses readTail() so no raw fd lifecycle is needed here. */
@@ -1941,6 +1999,7 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
1941
1999
  let needsInvestigationNow = false;
1942
2000
  let investigationJobSnapshot = null;
1943
2001
  let needsReviewRcaSnapshot = null;
2002
+ let terminalNotifySnapshot = null;
1944
2003
  await mutate((s) => {
1945
2004
  const i2 = s.jobs.findIndex((x) => x.slug === job.slug);
1946
2005
  if (i2 >= 0) {
@@ -1994,6 +2053,9 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
1994
2053
  }
1995
2054
  delete s.jobs[i2].runtime;
1996
2055
 
2056
+ if (isNotifiableTerminalStatus(effectiveStatus)) {
2057
+ terminalNotifySnapshot = { ...s.jobs[i2] };
2058
+ }
1997
2059
  if (effectiveStatus === 'failed') {
1998
2060
  actuallyFailed = true;
1999
2061
  failedJobSnapshot = { ...s.jobs[i2] };
@@ -2052,6 +2114,12 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
2052
2114
  });
2053
2115
  await broadcast({ flush: true });
2054
2116
 
2117
+ if (terminalNotifySnapshot) {
2118
+ notifyOriginatingTab(terminalNotifySnapshot).catch((e) => {
2119
+ console.error('[scheduler] notifyOriginatingTab error', job.slug, e);
2120
+ });
2121
+ }
2122
+
2055
2123
  if (needsReviewRcaSnapshot) {
2056
2124
  // Fire-and-forget, mirroring the DoD drain hook: never blocks the status
2057
2125
  // transition, never throws to this caller.
@@ -3386,4 +3454,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
3386
3454
  });
3387
3455
  }
3388
3456
 
3389
- 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 };
3457
+ 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, notifyOriginatingTab, isNotifiableTerminalStatus };
@@ -1056,6 +1056,7 @@ function getDispatchMap() {
1056
1056
  const sessionsStore = require('./sessionsStore.cjs');
1057
1057
  const scheduler = require('./scheduler.cjs');
1058
1058
  const { remote: histRemote } = require('./historyAggregator.cjs');
1059
+ const chatRunner = require('./chatRunner.cjs');
1059
1060
 
1060
1061
  _dispatchMap = {
1061
1062
  'cmd:sessions:load': async () =>
@@ -1146,6 +1147,15 @@ function getDispatchMap() {
1146
1147
  stopSessionWatch(parsed.tabId);
1147
1148
  return { ok: true };
1148
1149
  },
1150
+
1151
+ // Push a prompt into an open tab's chat queue from the paired phone
1152
+ // client — in-process call, no admin HTTP hop needed since webRemote
1153
+ // already runs inside the Electron main process (PRD 753).
1154
+ 'cmd:chat:send': async (payload) => {
1155
+ const parsed = schemas.chatExternalSend.parse(payload);
1156
+ chatRunner.enqueueExternalPrompt(parsed.tabId, parsed.prompt);
1157
+ return { ok: true };
1158
+ },
1149
1159
  };
1150
1160
 
1151
1161
  return _dispatchMap;
@@ -1102,6 +1102,13 @@ export interface ChatRunNoticeEvent {
1102
1102
  message: string;
1103
1103
  }
1104
1104
 
1105
+ /** Pushed by enqueueExternalPrompt (PRD 753) — Web Remote, the admin HTTP
1106
+ * route, or an MCP caller asking to enqueue a prompt into an open tab. */
1107
+ export interface ChatExternalSendEvent {
1108
+ tabId: string;
1109
+ prompt: string;
1110
+ }
1111
+
1105
1112
  export interface SessionManagerAPI {
1106
1113
  app: {
1107
1114
  version: () => Promise<string>;
@@ -1475,6 +1482,10 @@ export interface SessionManagerAPI {
1475
1482
  onComplete: (handler: (e: ChatRunCompleteEvent) => void) => () => void;
1476
1483
  onError: (handler: (e: ChatRunErrorEvent) => void) => () => void;
1477
1484
  onNotice: (handler: (e: ChatRunNoticeEvent) => void) => () => void;
1485
+ /** Fires when a main-process caller (Web Remote, admin HTTP route, MCP
1486
+ * tool) pushes a prompt into an open tab's queue from outside the
1487
+ * renderer (PRD 753). */
1488
+ onExternalSend: (handler: (e: ChatExternalSendEvent) => void) => () => void;
1478
1489
  /** Classify a queued PromptTicket's text as 'inline' (run through
1479
1490
  * chatRunner) or 'develop' (dispatch to /develop for PRD decomposition).
1480
1491
  * A single bounded `claude -p` call, never a scheduler job. */
@@ -436,6 +436,13 @@ contextBridge.exposeInMainWorld('api', {
436
436
  ipcRenderer.on('chat:run:notice', listener);
437
437
  return () => ipcRenderer.removeListener('chat:run:notice', listener);
438
438
  },
439
+ /** Fires when a main-process caller pushes a prompt into an open tab's
440
+ * queue from outside the renderer (Web Remote / admin HTTP / MCP). */
441
+ onExternalSend: (handler) => {
442
+ const listener = (_e, payload) => handler(payload);
443
+ ipcRenderer.on('chat:external-send', listener);
444
+ return () => ipcRenderer.removeListener('chat:external-send', listener);
445
+ },
439
446
  /** Classify a queued PromptTicket's text as 'inline' or 'develop'. */
440
447
  classifyTicket: (payload) => ipcRenderer.invoke('chat:classify-ticket', payload),
441
448
  },