claude-code-session-manager 0.104.1 → 0.105.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 (192) hide show
  1. package/README.md +15 -3
  2. package/bin/cli.cjs +0 -5
  3. package/dist/assets/AgentLibrary-BDtr4oI-.js +2 -0
  4. package/dist/assets/Button-CuXBCQDp.js +1 -0
  5. package/dist/assets/DataModel-DmvRipT5.js +1 -0
  6. package/dist/assets/{History-BP6KKda_.js → History-B-v9pwwr.js} +2 -2
  7. package/dist/assets/Hooks-S_0oHHlr.js +3 -0
  8. package/dist/assets/{Library-BprXNJCG.js → Library-BcbGJxQ3.js} +5 -5
  9. package/dist/assets/ListDetail-DtqHqJHt.js +1 -0
  10. package/dist/assets/{MarkdownEditor-1KlkRAqT.js → MarkdownEditor-B56sPp_l.js} +1 -1
  11. package/dist/assets/{McpServers-FUDN7EnU.js → McpServers-D5VnmxAn.js} +2 -2
  12. package/dist/assets/{Memory-DryJweXu.js → Memory-0yGZLyRP.js} +6 -6
  13. package/dist/assets/Panel-NoAADJwx.js +1 -0
  14. package/dist/assets/Permissions-D2Dh-ee8.js +3 -0
  15. package/dist/assets/Plugins-kGoUgS9b.js +2 -0
  16. package/dist/assets/{ProvenanceBadge-6Hj42klQ.js → ProvenanceBadge-CHwDFTX7.js} +1 -1
  17. package/dist/assets/{SaveBar-xSfjUPiu.js → SaveBar-BtKNMIYK.js} +1 -1
  18. package/dist/assets/{Scheduler-Beh3Vj-Z.js → Scheduler-B0_kMMDs.js} +7 -7
  19. package/dist/assets/{ScopeSwitcher-9R1ANwns.js → ScopeSwitcher-BusohWZ5.js} +1 -1
  20. package/dist/assets/Settings-DuRJntaE.js +3 -0
  21. package/dist/assets/{SkillReferenceGraph-CYLTGabR.js → SkillReferenceGraph-BHOdIpsy.js} +1 -1
  22. package/dist/assets/Skills-DSTLZimI.js +3 -0
  23. package/dist/assets/SystemPrompt-BG7wFWdb.js +1 -0
  24. package/dist/assets/TagLibrary-Bx27aE3_.js +1 -0
  25. package/dist/assets/{TiptapBody-l-BMtrPd.js → TiptapBody-j0O8J7YI.js} +1 -1
  26. package/dist/assets/{Toggle-Bj2Ia7AE.js → Toggle-DzVZ6rC7.js} +1 -1
  27. package/dist/assets/ViewTabs-BizjuynT.js +1 -0
  28. package/dist/assets/claude-settings-schema-COigsajx.js +3 -0
  29. package/dist/assets/{index-D89EUizB.css → index-BLVzrc4k.css} +1 -1
  30. package/dist/assets/index-CLIaYljt.js +3073 -0
  31. package/dist/assets/monaco-editor-BsIlgG7c.js +917 -0
  32. package/dist/assets/monaco-editor-Cu5D__1H.css +1 -0
  33. package/dist/assets/settingsSchema-7MRAxaJk.js +1 -0
  34. package/dist/assets/useAgentsDirWatch-BUwfU2Kq.js +1 -0
  35. package/dist/assets/useConfigDirWatch-ot0VYwQC.js +1 -0
  36. package/dist/index.html +4 -4
  37. package/package.json +12 -14
  38. package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +12 -0
  39. package/plugins/session-manager-dev/skills/develop/SKILL.md +4 -2
  40. package/scripts/scheduler-mcp-server.cjs +7 -17
  41. package/src/main/build-info.json +4 -4
  42. package/src/main/chatRunner.cjs +4 -4
  43. package/src/main/historyAggregator.cjs +7 -1
  44. package/src/main/index.cjs +34 -1
  45. package/src/main/lib/activeSessions.cjs +263 -71
  46. package/src/main/lib/agentPersonaSchema.cjs +8 -1
  47. package/src/main/lib/appRuntime.cjs +57 -0
  48. package/src/main/lib/childWithLog.cjs +10 -10
  49. package/src/main/lib/claudeBin.cjs +40 -16
  50. package/src/main/lib/cleanEnv.cjs +29 -6
  51. package/src/main/lib/defaultShell.cjs +36 -0
  52. package/src/main/lib/definitionOfDone.cjs +6 -2
  53. package/src/main/lib/delegationReadiness.cjs +13 -5
  54. package/src/main/lib/gitWorktree.cjs +6 -3
  55. package/src/main/lib/insideHome.cjs +15 -4
  56. package/src/main/lib/killTree.cjs +80 -0
  57. package/src/main/lib/loadGate.cjs +16 -0
  58. package/src/main/lib/loginShellPath.cjs +82 -0
  59. package/src/main/lib/machineProfile.cjs +4 -2
  60. package/src/main/lib/mcpStdioServer.cjs +127 -0
  61. package/src/main/lib/openExternalApp.cjs +62 -11
  62. package/src/main/lib/otlpTraceExporter.cjs +147 -0
  63. package/src/main/lib/prdMigration.cjs +64 -1
  64. package/src/main/lib/prereqs.cjs +145 -0
  65. package/src/main/lib/procIdentity.cjs +37 -2
  66. package/src/main/lib/queueHistory.cjs +97 -26
  67. package/src/main/lib/rcaReport.cjs +1 -0
  68. package/src/main/lib/reaperHelpers.cjs +5 -2
  69. package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
  70. package/src/main/lib/timeoutShimScript.cjs +14 -2
  71. package/src/main/lib/updateCheck.cjs +41 -11
  72. package/src/main/lib/watchdogHelpers.cjs +23 -3
  73. package/src/main/lib/winSpawn.cjs +67 -0
  74. package/src/main/otel.cjs +18 -62
  75. package/src/main/pty.cjs +82 -12
  76. package/src/main/runVerify.cjs +42 -2
  77. package/src/main/scheduler.cjs +105 -20
  78. package/src/main/seedSchedulerMcp.cjs +11 -2
  79. package/src/main/usage.cjs +4 -1
  80. package/src/preload/api.d.ts +14 -0
  81. package/src/preload/index.cjs +2 -0
  82. package/src/seed/agents/validator.md +2 -2
  83. package/dist/assets/DataModel-BHcPa2nA.js +0 -1
  84. package/dist/assets/Hooks-CnJnddP6.js +0 -3
  85. package/dist/assets/Permissions-Df-LWvdk.js +0 -3
  86. package/dist/assets/Plugins-CIUOw9B2.js +0 -2
  87. package/dist/assets/Settings-gkp0WfEm.js +0 -3
  88. package/dist/assets/Skills-Dw5Am-f-.js +0 -3
  89. package/dist/assets/SystemPrompt-B3GwTLqh.js +0 -1
  90. package/dist/assets/TagLibrary-Dj46gOG2.js +0 -1
  91. package/dist/assets/ViewTabs-BQ_HbQFq.js +0 -1
  92. package/dist/assets/abap-DLDM7-KI.js +0 -1
  93. package/dist/assets/apex-DNDY2TF8.js +0 -1
  94. package/dist/assets/azcli-Y6nb8tq_.js +0 -1
  95. package/dist/assets/bat-BwHxbl9M.js +0 -1
  96. package/dist/assets/bicep-CFznDFnq.js +0 -2
  97. package/dist/assets/cameligo-Bf6VGUru.js +0 -1
  98. package/dist/assets/clojure-Dnu-v4kV.js +0 -1
  99. package/dist/assets/coffee-Bd8akH9Z.js +0 -1
  100. package/dist/assets/cpp-BbWJElDN.js +0 -1
  101. package/dist/assets/csharp-Co3qMtFm.js +0 -1
  102. package/dist/assets/csp-D-4FJmMZ.js +0 -1
  103. package/dist/assets/css-DdJfP1eB.js +0 -3
  104. package/dist/assets/css.worker-B4z49cGk.js +0 -93
  105. package/dist/assets/cssMode-DfqZGMQs.js +0 -1
  106. package/dist/assets/cypher-cTPe9QuQ.js +0 -1
  107. package/dist/assets/dart-BOtBlQCF.js +0 -1
  108. package/dist/assets/dockerfile-BG73LgW2.js +0 -1
  109. package/dist/assets/ecl-BEgZUVRK.js +0 -1
  110. package/dist/assets/elixir-BkW5O-1t.js +0 -1
  111. package/dist/assets/flow9-BeJ5waoc.js +0 -1
  112. package/dist/assets/freemarker2-XTPYh37h.js +0 -3
  113. package/dist/assets/fsharp-PahG7c26.js +0 -1
  114. package/dist/assets/go-acbASCJo.js +0 -1
  115. package/dist/assets/graphql-BxJiqAUM.js +0 -1
  116. package/dist/assets/handlebars-DKUF5VyH.js +0 -1
  117. package/dist/assets/hcl-DtV1sZF8.js +0 -1
  118. package/dist/assets/html-uqoqsIeI.js +0 -1
  119. package/dist/assets/html.worker-DtiGdgqp.js +0 -470
  120. package/dist/assets/htmlMode-aMTQs1su.js +0 -1
  121. package/dist/assets/index-DbR91Z5j.js +0 -3076
  122. package/dist/assets/ini-Kd9XrMLS.js +0 -1
  123. package/dist/assets/java-CXBNlu9o.js +0 -1
  124. package/dist/assets/javascript-BVxRZMds.js +0 -1
  125. package/dist/assets/jsonMode-D04xP2s5.js +0 -7
  126. package/dist/assets/julia-cl7-CwDS.js +0 -1
  127. package/dist/assets/kotlin-s7OhZKlX.js +0 -1
  128. package/dist/assets/less-9HpZscsL.js +0 -2
  129. package/dist/assets/lexon-OrD6JF1K.js +0 -1
  130. package/dist/assets/liquid-BkQHTH2P.js +0 -1
  131. package/dist/assets/lspLanguageFeatures-By9uLznH.js +0 -4
  132. package/dist/assets/lua-Cyyb5UIc.js +0 -1
  133. package/dist/assets/m3-B8OfTtLu.js +0 -1
  134. package/dist/assets/markdown-BFxVWTOG.js +0 -1
  135. package/dist/assets/mdx-Du1IlbjV.js +0 -1
  136. package/dist/assets/mips-CiqrrVzr.js +0 -1
  137. package/dist/assets/monaco-editor-BTnBOi8r.css +0 -1
  138. package/dist/assets/monaco-editor-BW5C4Iv1.js +0 -908
  139. package/dist/assets/msdax-DmeGPVcC.js +0 -1
  140. package/dist/assets/mysql-C_tMU-Nz.js +0 -1
  141. package/dist/assets/objective-c-BDtDVThU.js +0 -1
  142. package/dist/assets/pascal-vHIfCaH5.js +0 -1
  143. package/dist/assets/pascaligo-DtZ0uQbO.js +0 -1
  144. package/dist/assets/perl-Ub6l9XKa.js +0 -1
  145. package/dist/assets/pgsql-BlNEE0v7.js +0 -1
  146. package/dist/assets/php-BBUBE1dy.js +0 -1
  147. package/dist/assets/pla-DSh2-awV.js +0 -1
  148. package/dist/assets/postiats-CocnycG-.js +0 -1
  149. package/dist/assets/powerquery-tScXyioY.js +0 -1
  150. package/dist/assets/powershell-COWaemsV.js +0 -1
  151. package/dist/assets/protobuf-Brw8urJB.js +0 -2
  152. package/dist/assets/pug-8SOpv6rk.js +0 -1
  153. package/dist/assets/python-DSlImqXd.js +0 -1
  154. package/dist/assets/qsharp-Bw9ernYp.js +0 -1
  155. package/dist/assets/r-j7ic8hl3.js +0 -1
  156. package/dist/assets/razor-BmUVyvSK.js +0 -1
  157. package/dist/assets/redis-Bu5POkcn.js +0 -1
  158. package/dist/assets/redshift-Bs9aos_-.js +0 -1
  159. package/dist/assets/restructuredtext-CqXO7rUv.js +0 -1
  160. package/dist/assets/ruby-zBfavPgS.js +0 -1
  161. package/dist/assets/rust-BzKRNQWT.js +0 -1
  162. package/dist/assets/sb-BBc9UKZt.js +0 -1
  163. package/dist/assets/scala-D9hQfWCl.js +0 -1
  164. package/dist/assets/scheme-BPhDTwHR.js +0 -1
  165. package/dist/assets/scss-CBJaRo0y.js +0 -3
  166. package/dist/assets/settingsSchema-cR833AB2.js +0 -1
  167. package/dist/assets/shell-DiJ1NA_G.js +0 -1
  168. package/dist/assets/solidity-Db0IVjzk.js +0 -1
  169. package/dist/assets/sophia-CnS9iZB_.js +0 -1
  170. package/dist/assets/sparql-CJmd_6j2.js +0 -1
  171. package/dist/assets/sql-ClhHkBeG.js +0 -1
  172. package/dist/assets/st-CHwy0fLd.js +0 -1
  173. package/dist/assets/swift-Bqt4WxQ4.js +0 -3
  174. package/dist/assets/systemverilog-Bs9z6M-B.js +0 -1
  175. package/dist/assets/tcl-Dm6ycUr_.js +0 -1
  176. package/dist/assets/ts.worker-59MjiAqk.js +0 -67731
  177. package/dist/assets/tsMode-Btj0TTH7.js +0 -11
  178. package/dist/assets/twig-Csy3S7wG.js +0 -1
  179. package/dist/assets/typescript-Bzelq9vO.js +0 -1
  180. package/dist/assets/typespec-Btyra-wh.js +0 -1
  181. package/dist/assets/vb-Db0cS2oM.js +0 -1
  182. package/dist/assets/wgsl-BTesnYfV.js +0 -298
  183. package/dist/assets/xml-Whd9EaSd.js +0 -1
  184. package/dist/assets/yaml-QYf0-IN8.js +0 -1
  185. package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +0 -112
  186. package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
  187. package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +0 -106
  188. package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
  189. package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +0 -107
  190. package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
  191. package/scripts/postinstall.cjs +0 -52
  192. package/src/preload/__tests__/preload-surface.test.cjs +0 -68
@@ -236,11 +236,96 @@ async function readHistoryFile(target, cap) {
236
236
  let historyCacheKey = null;
237
237
  let historyCacheMap = null;
238
238
 
239
+ // Per-file byte-offset cache backing historyTerminalBySlug's incremental
240
+ // read: path -> { ino, size, offset, map }. `map` is this file's own
241
+ // slug -> record result as of `offset` bytes; `offset` always sits on a
242
+ // line boundary (the byte right after the last consumed '\n') so a partial
243
+ // trailing line never gets counted as consumed.
244
+ const perFileCache = new Map();
245
+
246
+ /**
247
+ * needs_review_entry/needs_review_resolution lines (needsReviewLedger.cjs)
248
+ * share this file but aren't a job's terminal record — skip them, or a
249
+ * needs_review episode logged for a slug AFTER its real terminal row (e.g.
250
+ * it re-parked on a later run) would overwrite a true 'completed' with a
251
+ * kind-less, status-less row read as "not terminal".
252
+ */
253
+ function applyTerminalLines(text, map) {
254
+ if (!text) return;
255
+ for (const line of text.split('\n')) {
256
+ if (!line.trim()) continue;
257
+ try {
258
+ const j = JSON.parse(line);
259
+ if (j?.kind && j.kind !== 'terminal') continue;
260
+ if (j?.slug) map.set(j.slug, { status: j.status, finishedAt: j.finishedAt, landedCommit: j.landedCommit ?? null });
261
+ } catch {
262
+ // corrupt/partial line — ignore
263
+ }
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Returns this one file's slug -> record map, reading only the bytes
269
+ * appended since the last call when possible. Falls back to a full
270
+ * re-parse when the file is new to the cache, its inode changed, or its
271
+ * size went backwards (rotation/truncation).
272
+ */
273
+ async function readFileTerminalMap(target, stat) {
274
+ const cached = perFileCache.get(target);
275
+ const { ino, size } = stat;
276
+
277
+ if (cached && cached.ino === ino && size >= cached.size) {
278
+ if (size === cached.size) return cached.map;
279
+
280
+ const len = size - cached.offset;
281
+ const fh = await fsp.open(target, 'r');
282
+ let buf;
283
+ try {
284
+ buf = Buffer.alloc(len);
285
+ await fh.read(buf, 0, len, cached.offset);
286
+ } finally {
287
+ await fh.close();
288
+ }
289
+
290
+ // Stop at the last complete newline — a partial trailing line (still
291
+ // being written) is left unconsumed so the next pass re-reads it once
292
+ // it's whole, instead of permanently skipping or mis-parsing it.
293
+ const lastNl = buf.lastIndexOf(0x0a);
294
+ const map = new Map(cached.map);
295
+ let offset = cached.offset;
296
+ if (lastNl !== -1) {
297
+ applyTerminalLines(buf.subarray(0, lastNl + 1).toString('utf8'), map);
298
+ offset = cached.offset + lastNl + 1;
299
+ }
300
+ const updated = { ino, size, offset, map };
301
+ perFileCache.set(target, updated);
302
+ return map;
303
+ }
304
+
305
+ // Full (re)parse: first time seeing this path, its inode changed, or it
306
+ // shrank (log rotation/truncation) — the existing offset is no longer
307
+ // trustworthy.
308
+ let text = '';
309
+ try {
310
+ text = await fsp.readFile(target, 'utf8');
311
+ } catch (e) {
312
+ if (e.code !== 'ENOENT') throw e;
313
+ }
314
+ const map = new Map();
315
+ applyTerminalLines(text, map);
316
+ const lastNl = Buffer.from(text, 'utf8').lastIndexOf(0x0a);
317
+ const offset = lastNl === -1 ? 0 : lastNl + 1;
318
+ perFileCache.set(target, { ino, size, offset, map });
319
+ return map;
320
+ }
321
+
239
322
  /**
240
323
  * Full-file scan of history.jsonl, returning slug -> { status, finishedAt }
241
324
  * for the most recently appended record of each slug. O(H) where H is the
242
325
  * line count of history.jsonl; cached by the file's mtime so a reconcile()
243
- * pass with no new archives since the last call is O(1).
326
+ * pass with no new archives since the last call is O(1). When the mtime
327
+ * check misses, each file is re-read incrementally via readFileTerminalMap
328
+ * rather than re-parsed from byte zero.
244
329
  *
245
330
  * Exists so reconcile() can tell "this on-disk PRD's job row already left
246
331
  * jobs[] into history — do not resurrect it as a fresh pending job" apart
@@ -248,36 +333,22 @@ let historyCacheMap = null;
248
333
  */
249
334
  async function historyTerminalBySlug() {
250
335
  const paths = historyPaths();
251
- const mtimes = await Promise.all(paths.map(async (p2) => {
252
- try { return (await fsp.stat(p2)).mtimeMs; } catch { return -1; }
336
+ const stats = await Promise.all(paths.map(async (p2) => {
337
+ try { return await fsp.stat(p2); } catch { return null; }
253
338
  }));
254
- const cacheKey = paths.map((p2, i) => `${p2}:${mtimes[i]}`).join('|');
339
+ const cacheKey = paths.map((p2, i) => `${p2}:${stats[i] ? stats[i].mtimeMs : -1}`).join('|');
255
340
  if (cacheKey === historyCacheKey && historyCacheMap) return historyCacheMap;
256
341
 
257
342
  const map = new Map();
258
- for (const p2 of paths) {
259
- let text = '';
260
- try {
261
- text = await fsp.readFile(p2, 'utf8');
262
- } catch (e) {
263
- if (e.code !== 'ENOENT') throw e;
264
- }
265
- if (!text) continue;
266
- for (const line of text.split('\n')) {
267
- if (!line.trim()) continue;
268
- try {
269
- const j = JSON.parse(line);
270
- // needs_review_entry/needs_review_resolution lines (needsReviewLedger.cjs)
271
- // share this file but aren't a job's terminal record — skip them, or a
272
- // needs_review episode logged for a slug AFTER its real terminal row
273
- // (e.g. it re-parked on a later run) would overwrite a true 'completed'
274
- // with a kind-less, status-less row read as "not terminal".
275
- if (j?.kind && j.kind !== 'terminal') continue;
276
- if (j?.slug) map.set(j.slug, { status: j.status, finishedAt: j.finishedAt, landedCommit: j.landedCommit ?? null });
277
- } catch {
278
- // corrupt/partial line — ignore
279
- }
343
+ for (let i = 0; i < paths.length; i++) {
344
+ const p2 = paths[i];
345
+ const stat = stats[i];
346
+ if (!stat) {
347
+ perFileCache.delete(p2);
348
+ continue;
280
349
  }
350
+ const fileMap = await readFileTerminalMap(p2, stat);
351
+ for (const [slug, rec] of fileMap) map.set(slug, rec);
281
352
  }
282
353
  historyCacheKey = cacheKey;
283
354
  historyCacheMap = map;
@@ -50,6 +50,7 @@ const VERDICT_LABELS = {
50
50
  no_verdict_sentinel: 'no commit or verdict sentinel',
51
51
  abandoned_background_task: 'abandoned auto-backgrounded task',
52
52
  pass_no_commit: 'PASS sentinel but no commit landed',
53
+ sentinel_fail: 'executor reported SCHEDULER_VERDICT: FAIL',
53
54
  pass_no_commit_already_shipped: 'PASS with no commit — deliverables already shipped',
54
55
  pass_no_commit_prior_run_verified: 'PASS with no commit — prior run of this slug already landed the work',
55
56
  silent_no_op: 'no commit, clean tree — no evidence of work',
@@ -30,9 +30,11 @@ const { pidAlive } = require('./pidAlive.cjs');
30
30
  * Conservative by design: a false negative (live process treated as dead) is
31
31
  * far worse than a late reap.
32
32
  */
33
- function claudePidAlive(pid) {
33
+ function claudePidAlive(pid, { platform = process.platform } = {}) {
34
34
  if (!pid || typeof pid !== 'number' || pid <= 1) return false;
35
35
  if (!pidAlive(pid)) return false;
36
+ // Windows has no /proc — cannot verify identity, so never call it dead.
37
+ if (platform === 'win32') return true;
36
38
  try {
37
39
  const cmd = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ');
38
40
  return /\bclaude\b/.test(cmd);
@@ -73,7 +75,8 @@ function claudePidAlive(pid) {
73
75
  * — i.e. exactly today's behaviour (fail toward terminalizing), never a hang
74
76
  * or a thrown error propagating to the caller.
75
77
  */
76
- function findLiveProcessForJob(job, { worktreeDir, runCwd } = {}) {
78
+ function findLiveProcessForJob(job, { worktreeDir, runCwd, platform = process.platform } = {}) {
79
+ if (platform === 'win32') return null; // no /proc — skip every read
77
80
  const slug = job?.slug;
78
81
  let entries;
79
82
  try {
@@ -207,6 +207,16 @@ const SHIPPED_PERSONA_SEEDS = deepFreeze({
207
207
  tools: "Read, Grep, Glob, Bash",
208
208
  },
209
209
  },
210
+ {
211
+ commit: "<fill in>",
212
+ bodySha256: "ca159cd47ac4b618af842c6ccd59107fac4a34b0efcfa5dfe7d06875736156ed",
213
+ fm: {
214
+ description: "Validates a finished PLAN (the PRDs its validate PRD lists) once, after its last PRD lands — re-runs each PRD's gate, checks every acceptance criterion against the real tree, reviews the plan's combined diff, and reports one VERIFIED/REFUTED verdict per PRD via sentinel lines. Runs headless as a scheduled PRD (agentType: validator); never edits product code and never queues work.",
215
+ model: "sonnet",
216
+ title: "Engineering — Plan Validator",
217
+ tools: "Read, Grep, Glob, Bash",
218
+ },
219
+ },
210
220
  ],
211
221
  "demo-video-builder": [
212
222
  {
@@ -222,16 +222,28 @@ function main() {
222
222
  // spawned, without detaching further, dies too); fall back to the direct
223
223
  // pid only when the group signal itself fails (e.g. --foreground, where
224
224
  // the child shares OUR group rather than leading its own).
225
+ // Inlined (not require('./killTree.cjs')): this script is copied to ~ and
226
+ // runs standalone, so it cannot resolve app-relative modules. win32 has no
227
+ // process groups, so taskkill /T /F stands in for the group signal.
225
228
  function killGroup(sig) {
229
+ if (process.platform === 'win32') {
230
+ try {
231
+ require('node:child_process').spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
232
+ } catch {
233
+ try { process.kill(child.pid, sig); } catch { /* already gone */ }
234
+ }
235
+ return;
236
+ }
226
237
  try {
227
- process.kill(-child.pid, sig);
238
+ const groupPid = -child.pid; // negative pid = whole POSIX process group
239
+ process.kill(groupPid, sig);
228
240
  } catch {
229
241
  try { process.kill(child.pid, sig); } catch { /* already gone */ }
230
242
  }
231
243
  }
232
244
 
233
245
  try {
234
- child = spawn(command, commandArgs, { stdio: 'inherit', detached: !opts.foreground });
246
+ child = spawn(command, commandArgs, { stdio: 'inherit', detached: !opts.foreground, windowsHide: true });
235
247
  } catch (err) {
236
248
  finish(err && err.code === 'EACCES' ? 126 : 127);
237
249
  return;
@@ -3,9 +3,10 @@
3
3
  /**
4
4
  * updateCheck.cjs — bounded, offline-safe, opt-out "is a newer build published?" probe.
5
5
  *
6
- * getUpdateStatus() → { current, latest, behind }. Never throws/rejects; any failure
7
- * (offline, DNS, non-200, bad JSON, timeout) → { current, latest: null, behind: false }.
8
- * The request is a bare GET (no headers/cookies/query/body) so it carries nothing that
6
+ * getUpdateStatus() → { current, latest, behind, channel, downloadUrl? } (installer builds compare
7
+ * against the latest GitHub Release; npx/dev builds against the npm registry). Never throws/rejects; any failure
8
+ * (offline, DNS, non-200, bad JSON, timeout) → { current, latest: null, behind: false, channel }.
9
+ * The request is a bare GET (only a static User-Agent; no cookies/query/body) so it carries nothing that
9
10
  * identifies the user or machine. Disabled by SM_UPDATE_CHECK=0 or E2E mode (SM_E2E=1).
10
11
  */
11
12
 
@@ -16,11 +17,14 @@ const path = require('node:path');
16
17
  const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
17
18
  const REQUEST_TIMEOUT_MS = 3000;
18
19
  const CACHE_FILE = 'update-check.json';
20
+ const GITHUB_CACHE_FILE = 'update-check-github.json';
21
+ const GITHUB_RELEASE_URL = 'https://api.github.com/repos/StanislavBG/claude-code-session-manager/releases/latest';
19
22
 
20
23
  /** Plain GET → parsed JSON. Rejects on non-200, bad JSON, or timeout. */
21
24
  function httpGetJson(url, timeoutMs) {
22
25
  return new Promise((resolve, reject) => {
23
- const req = https.get(url, { timeout: timeoutMs }, (res) => {
26
+ // GitHub's API rejects requests without a User-Agent; it names the app only, not the user.
27
+ const req = https.get(url, { timeout: timeoutMs, headers: { 'User-Agent': 'claude-code-session-manager' } }, (res) => {
24
28
  if (res.statusCode !== 200) {
25
29
  res.resume();
26
30
  reject(new Error(`HTTP ${res.statusCode}`));
@@ -58,15 +62,20 @@ function isBehind(current, latest) {
58
62
  return false;
59
63
  }
60
64
 
61
- function readCache(file, now) {
65
+ function readCacheEntry(file, now) {
62
66
  try {
63
67
  const c = JSON.parse(fs.readFileSync(file, 'utf8'));
64
68
  if (c && typeof c.latest === 'string' && typeof c.checkedAt === 'number'
65
- && now - c.checkedAt >= 0 && now - c.checkedAt < CACHE_TTL_MS) return c.latest;
69
+ && now - c.checkedAt >= 0 && now - c.checkedAt < CACHE_TTL_MS) return c;
66
70
  } catch { /* missing/corrupt cache = miss */ }
67
71
  return null;
68
72
  }
69
73
 
74
+ function readCache(file, now) {
75
+ const c = readCacheEntry(file, now);
76
+ return c ? c.latest : null;
77
+ }
78
+
70
79
  function defaultPackageName() {
71
80
  return require(path.join(__dirname, '..', '..', '..', 'package.json')).name;
72
81
  }
@@ -74,13 +83,34 @@ function defaultPackageName() {
74
83
  async function getUpdateStatus({ deps } = {}) {
75
84
  const d = deps || {};
76
85
  let current = '';
86
+ let channel = 'npm';
77
87
  try {
78
88
  const app = d.app || require('electron').app;
79
89
  current = app.getVersion();
90
+ const packaged = (d.isPackaged || require('./appRuntime.cjs').isPackagedApp)();
91
+ channel = packaged ? 'installer' : 'npm';
80
92
  const env = d.env || process.env;
81
- if (env.SM_UPDATE_CHECK === '0' || env.SM_E2E === '1') return { current, latest: null, behind: false };
93
+ if (env.SM_UPDATE_CHECK === '0' || env.SM_E2E === '1') return { current, latest: null, behind: false, channel };
82
94
 
83
95
  const now = (d.now || Date.now)();
96
+ const write = d.writeJson || require('../config.cjs').writeJson;
97
+ if (packaged) {
98
+ const file = path.join(app.getPath('userData'), GITHUB_CACHE_FILE);
99
+ let entry = readCacheEntry(file, now);
100
+ if (entry == null) {
101
+ const rel = await (d.fetchJson || httpGetJson)(GITHUB_RELEASE_URL, REQUEST_TIMEOUT_MS);
102
+ const tag = rel && typeof rel.tag_name === 'string' ? rel.tag_name.replace(/^v/, '') : '';
103
+ if (!tag) return { current, latest: null, behind: false, channel };
104
+ const downloadUrl = typeof rel.html_url === 'string' ? rel.html_url : null;
105
+ entry = { checkedAt: now, latest: tag, downloadUrl };
106
+ try { await write(file, entry); } catch { /* cache is best-effort */ }
107
+ }
108
+ return {
109
+ current, latest: entry.latest, behind: isBehind(current, entry.latest), channel,
110
+ downloadUrl: typeof entry.downloadUrl === 'string' ? entry.downloadUrl : null,
111
+ };
112
+ }
113
+
84
114
  const file = path.join(app.getPath('userData'), CACHE_FILE);
85
115
  let latest = readCache(file, now);
86
116
  if (latest == null) {
@@ -88,14 +118,14 @@ async function getUpdateStatus({ deps } = {}) {
88
118
  const url = `https://registry.npmjs.org/-/package/${name}/dist-tags`;
89
119
  const tags = await (d.fetchJson || httpGetJson)(url, REQUEST_TIMEOUT_MS);
90
120
  latest = tags && typeof tags.latest === 'string' ? tags.latest : null;
91
- if (latest == null) return { current, latest: null, behind: false };
121
+ if (latest == null) return { current, latest: null, behind: false, channel };
92
122
  try {
93
- await (d.writeJson || require('../config.cjs').writeJson)(file, { checkedAt: now, latest });
123
+ await write(file, { checkedAt: now, latest });
94
124
  } catch { /* cache is best-effort */ }
95
125
  }
96
- return { current, latest, behind: isBehind(current, latest) };
126
+ return { current, latest, behind: isBehind(current, latest), channel };
97
127
  } catch {
98
- return { current, latest: null, behind: false };
128
+ return { current, latest: null, behind: false, channel };
99
129
  }
100
130
  }
101
131
 
@@ -9,6 +9,7 @@ const schedulerPaths = require('./schedulerPaths.cjs');
9
9
  const { isRestartingMarkerActive } = require('./upgradeDrain.cjs');
10
10
  const { pidAlive: pidAliveCore } = require('./pidAlive.cjs');
11
11
  const atomicFs = require('./atomicFs.cjs');
12
+ const { isPackagedApp } = require('./appRuntime.cjs');
12
13
 
13
14
 
14
15
 
@@ -230,14 +231,32 @@ function logRelaunchLine(logPath, message) {
230
231
  * (short-lived) process doesn't block on the launched app and the app
231
232
  * survives the watchdog exiting.
232
233
  */
233
- function defaultSpawnRelaunch({ logPath = schedulerPaths.watchdogRelaunchLogPath() } = {}) {
234
- const { spawn } = require('node:child_process');
234
+ /**
235
+ * relaunchSpec(opts?) — what the watchdog spawns to bring the app back.
236
+ * Packaged installer builds have no npx: relaunch the app binary itself.
237
+ * @param {{ packaged?: boolean, execPath?: string }} [opts]
238
+ * @returns {{ command: string, args: string[], packaged: boolean }}
239
+ */
240
+ function relaunchSpec(opts) {
241
+ if (isPackagedApp(opts)) {
242
+ return { command: (opts && opts.execPath) || process.execPath, args: [], packaged: true };
243
+ }
244
+ return { command: 'npx', args: ['claude-code-session-manager@latest'], packaged: false };
245
+ }
246
+
247
+ function defaultSpawnRelaunch({ logPath = schedulerPaths.watchdogRelaunchLogPath(), packaged, execPath, spawnFn } = {}) {
248
+ const spawn = spawnFn || require('node:child_process').spawn;
249
+ const spec = relaunchSpec({ packaged, execPath });
235
250
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
236
251
  const fd = fs.openSync(logPath, 'a');
237
252
  try {
238
- const child = spawn('npx', ['claude-code-session-manager@latest'], {
253
+ const env = { ...process.env };
254
+ // The watchdog may itself run as node via the app binary; the relaunched app must not.
255
+ if (spec.packaged) delete env.ELECTRON_RUN_AS_NODE;
256
+ const child = spawn(spec.command, spec.args, {
239
257
  detached: true,
240
258
  stdio: ['ignore', fd, fd],
259
+ env,
241
260
  });
242
261
  child.on('error', (e) => {
243
262
  logRelaunchLine(logPath, `spawn error: ${e?.message}`);
@@ -460,6 +479,7 @@ module.exports = {
460
479
  readRelaunchState,
461
480
  writeRelaunchState,
462
481
  defaultSpawnRelaunch,
482
+ relaunchSpec,
463
483
  maybeRelaunchApp,
464
484
  DEFAULT_MAX_AGE_MS,
465
485
  DEFAULT_DISPATCH_DEAD_MS,
@@ -0,0 +1,67 @@
1
+ /**
2
+ * winSpawn.cjs — make spawn() work for Windows .cmd/.bat shims (npx, npm) without
3
+ * shell:true. Node refuses to spawn those directly (EINVAL), so they are wrapped in
4
+ * an explicit `cmd.exe /d /s /c "<line>"` argv with every cmd metacharacter escaped.
5
+ * Non-win32 input passes through untouched.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('node:fs');
11
+ const path = require('node:path');
12
+
13
+ const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
14
+ const META = /([()\][%!^"`<>&|;, *?])/g;
15
+
16
+ function envGet(env, name) {
17
+ if (env[name] !== undefined) return env[name];
18
+ const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
19
+ return key ? env[key] : undefined;
20
+ }
21
+
22
+ function findOnPath(command, env, exists) {
23
+ const w = path.win32;
24
+ const exts = (envGet(env, 'PATHEXT') || DEFAULT_PATHEXT)
25
+ .split(';').filter(Boolean).map((e) => e.toLowerCase());
26
+ const hasDir = /[\\/]/.test(command);
27
+ const dirs = hasDir ? [''] : (envGet(env, 'PATH') || '').split(';').filter(Boolean);
28
+ const hasExt = exts.includes(w.extname(command).toLowerCase());
29
+ for (const dir of dirs) {
30
+ const base = dir ? w.join(dir, command) : command;
31
+ const candidates = hasExt ? [base] : [];
32
+ for (const e of exts) candidates.push(base + e);
33
+ // PATHEXT order is by extension; keep the casing the filesystem probe accepts.
34
+ for (const cand of candidates) {
35
+ if (exists(cand)) return cand;
36
+ const upper = cand.slice(0, cand.length - w.extname(cand).length) + w.extname(cand).toUpperCase();
37
+ if (upper !== cand && exists(upper)) return upper;
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+
43
+ // Quote for CreateProcess argv parsing, then ^-escape every cmd metacharacter.
44
+ function quoteArg(arg) {
45
+ let s = String(arg);
46
+ s = s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, '$1$1');
47
+ s = `"${s}"`;
48
+ return s.replace(META, '^$1');
49
+ }
50
+
51
+ function resolveSpawn(command, args = [], { platform = process.platform, env = process.env, exists = fs.existsSync } = {}) {
52
+ if (platform !== 'win32') return { command, args };
53
+ const resolved = findOnPath(command, env, exists);
54
+ if (!resolved) return { command, args };
55
+ const ext = path.win32.extname(resolved).toLowerCase();
56
+ if (ext === '.cmd' || ext === '.bat') {
57
+ const line = [resolved, ...args].map(quoteArg).join(' ');
58
+ return {
59
+ command: envGet(env, 'ComSpec') || 'cmd.exe',
60
+ args: ['/d', '/s', '/c', `"${line}"`],
61
+ windowsVerbatimArguments: true,
62
+ };
63
+ }
64
+ return { command: resolved, args };
65
+ }
66
+
67
+ module.exports = { resolveSpawn };
package/src/main/otel.cjs CHANGED
@@ -8,58 +8,36 @@
8
8
  * plan text, and agent prompts are excluded unless `includeContent` is on
9
9
  * (matches upstream OTEL_LOG_USER_PROMPTS opt-in).
10
10
  *
11
- * The OTEL SDK is required lazily — if any of the @opentelemetry/* packages
12
- * fail to load, the module is left in an inert state and `recordTranscriptEvent`
13
- * becomes a no-op. The app must keep working without telemetry.
14
- *
15
- * Threading: the BatchSpanProcessor batches/sends spans on its own timer; we
16
- * never block the transcript flush.
11
+ * Spans are POSTed as OTLP/HTTP JSON by lib/otlpTraceExporter.cjs (zero
12
+ * dependencies). recordSpan() only queues; the exporter batches/sends on its
13
+ * own unref()'d timer, so we never block the transcript flush. The app must
14
+ * keep working without telemetry: a failed export is surfaced, never thrown.
17
15
  */
18
16
 
19
17
  const otelSettings = require('./otelSettings.cjs');
18
+ const { createOtlpTraceExporter } = require('./lib/otlpTraceExporter.cjs');
20
19
 
21
- let provider = null;
22
- let tracer = null;
20
+ let exporter = null;
23
21
  let enabled = false;
24
22
  let includeContent = false;
25
23
  let lastError = null;
26
24
  let initialized = false;
27
25
 
28
26
  /**
29
- * Lazy-require all @opentelemetry/* packages. Returns null if any are missing.
30
- * We catch the per-require so a partial install doesn't half-init.
31
- */
32
- function loadDeps() {
33
- try {
34
- const api = require('@opentelemetry/api');
35
- const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
36
- const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
37
- const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
38
- const { resourceFromAttributes } = require('@opentelemetry/resources');
39
- const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
40
- return { api, NodeTracerProvider, BatchSpanProcessor, OTLPTraceExporter, resourceFromAttributes, ATTR_SERVICE_NAME };
41
- } catch (err) {
42
- return { error: err };
43
- }
44
- }
45
-
46
- /**
47
- * Tear down any existing provider. Best-effort — flushes pending spans on a
27
+ * Tear down any existing exporter. Best-effort — flushes pending spans on a
48
28
  * 2s ceiling so a wedged exporter doesn't block app shutdown / config reload.
49
29
  */
50
30
  async function shutdown() {
51
- if (!provider) {
31
+ if (!exporter) {
52
32
  enabled = false;
53
- tracer = null;
54
33
  return;
55
34
  }
56
- const p = provider;
57
- provider = null;
58
- tracer = null;
35
+ const e = exporter;
36
+ exporter = null;
59
37
  enabled = false;
60
38
  try {
61
39
  await Promise.race([
62
- p.shutdown(),
40
+ e.shutdown(),
63
41
  new Promise((resolve) => setTimeout(resolve, 2000)),
64
42
  ]);
65
43
  } catch (err) {
@@ -68,7 +46,7 @@ async function shutdown() {
68
46
  }
69
47
 
70
48
  /**
71
- * Configure the tracer provider. Tears down any prior provider first so a
49
+ * Configure the exporter. Tears down any prior exporter first so a
72
50
  * settings change reliably re-points at the new endpoint.
73
51
  *
74
52
  * Returns { ok, error?: string } — the renderer surfaces `error` in the UI.
@@ -77,31 +55,13 @@ async function init({ endpoint, headers, serviceName, includeContent: ic } = {})
77
55
  await shutdown();
78
56
  initialized = true;
79
57
 
80
- const deps = loadDeps();
81
- if (deps.error) {
82
- lastError = `OTEL packages unavailable: ${deps.error.message}`;
83
- return { ok: false, error: lastError };
84
- }
85
-
86
- const { NodeTracerProvider, BatchSpanProcessor, OTLPTraceExporter, resourceFromAttributes, ATTR_SERVICE_NAME } = deps;
87
-
88
58
  try {
89
- const exporter = new OTLPTraceExporter({
59
+ exporter = createOtlpTraceExporter({
90
60
  url: String(endpoint || 'http://localhost:4318/v1/traces'),
91
61
  headers: typeof headers === 'object' && headers ? headers : {},
62
+ serviceName: String(serviceName || 'session-manager'),
63
+ scopeName: 'session-manager-transcripts',
92
64
  });
93
- const tp = new NodeTracerProvider({
94
- resource: resourceFromAttributes({
95
- [ATTR_SERVICE_NAME]: String(serviceName || 'session-manager'),
96
- }),
97
- spanProcessors: [new BatchSpanProcessor(exporter)],
98
- });
99
- // Note: we deliberately do NOT call tp.register() — that installs a global
100
- // tracer provider, which would intercept any other OTEL user in the
101
- // process. We only want to emit our own spans, so we use the provider
102
- // directly via getTracer().
103
- provider = tp;
104
- tracer = tp.getTracer('session-manager-transcripts');
105
65
  includeContent = !!ic;
106
66
  enabled = true;
107
67
  lastError = null;
@@ -205,11 +165,11 @@ function pruneAttrs(obj) {
205
165
 
206
166
  /**
207
167
  * Emit a 0-duration span for one classified transcript event.
208
- * Synchronous + non-blocking: the BatchSpanProcessor handles network I/O
168
+ * Synchronous + non-blocking: the exporter handles network I/O
209
169
  * on its own schedule.
210
170
  */
211
171
  function recordTranscriptEvent({ tabId, tabCwd, kind, data, ts }) {
212
- if (!enabled || !tracer) return;
172
+ if (!enabled || !exporter) return;
213
173
  try {
214
174
  const startTime = typeof ts === 'number' ? ts : Date.now();
215
175
  const attrs = pruneAttrs({
@@ -218,11 +178,7 @@ function recordTranscriptEvent({ tabId, tabCwd, kind, data, ts }) {
218
178
  kind,
219
179
  ...attrsFor(kind, data),
220
180
  });
221
- const span = tracer.startSpan(`transcript.${kind}`, {
222
- startTime,
223
- attributes: attrs,
224
- });
225
- span.end(startTime);
181
+ exporter.recordSpan({ name: `transcript.${kind}`, timeMs: startTime, attributes: attrs });
226
182
  } catch (err) {
227
183
  // A failure here must never break transcript ingestion. Log once-ish.
228
184
  if (!recordTranscriptEvent._warned) {