@staff0rd/assist 0.448.0 → 0.450.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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.448.0",
9
+ version: "0.450.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -1770,6 +1770,8 @@ function collectCommitRefs(message2) {
1770
1770
  const subject = message2.split("\n", 1)[0];
1771
1771
  if (subject) ref.title = subject;
1772
1772
  refs.push(ref);
1773
+ const parent = git("rev-parse HEAD^");
1774
+ if (parent) refs.push({ kind: "commit-parent", ref: parent });
1773
1775
  }
1774
1776
  return refs;
1775
1777
  }
@@ -3715,9 +3717,9 @@ function forbiddenStrings() {
3715
3717
  console.log("No forbidden-strings rules configured.");
3716
3718
  process.exit(0);
3717
3719
  }
3718
- const cache3 = /* @__PURE__ */ new Map();
3720
+ const cache4 = /* @__PURE__ */ new Map();
3719
3721
  const readJson = (file) => {
3720
- if (cache3.has(file)) return cache3.get(file);
3722
+ if (cache4.has(file)) return cache4.get(file);
3721
3723
  if (!existsSync12(file)) {
3722
3724
  console.log(`Forbidden-strings file not found: ${file}`);
3723
3725
  process.exit(1);
@@ -3729,7 +3731,7 @@ function forbiddenStrings() {
3729
3731
  console.log(`Could not parse ${file}: ${error.message}`);
3730
3732
  process.exit(1);
3731
3733
  }
3732
- cache3.set(file, parsed);
3734
+ cache4.set(file, parsed);
3733
3735
  return parsed;
3734
3736
  };
3735
3737
  const violations = findForbiddenStrings(rules, readJson);
@@ -6091,7 +6093,13 @@ var backlogCommentSchema = z3.strictObject({
6091
6093
  timestamp: z3.string(),
6092
6094
  type: backlogCommentTypeSchema
6093
6095
  });
6094
- var gitRefKindSchema = z3.enum(["branch", "commit", "pr", "slack"]);
6096
+ var gitRefKindSchema = z3.enum([
6097
+ "branch",
6098
+ "commit",
6099
+ "commit-parent",
6100
+ "pr",
6101
+ "slack"
6102
+ ]);
6095
6103
  var gitRefSchema = z3.strictObject({
6096
6104
  kind: gitRefKindSchema,
6097
6105
  ref: z3.string(),
@@ -8372,19 +8380,19 @@ import { dirname as dirname19, join as join23 } from "path";
8372
8380
  import { fileURLToPath as fileURLToPath4 } from "url";
8373
8381
  function createBundleHandler(importMetaUrl, bundlePath, contentType = "application/javascript") {
8374
8382
  const file = join23(dirname19(fileURLToPath4(importMetaUrl)), bundlePath);
8375
- let cache3;
8383
+ let cache4;
8376
8384
  return (req, res) => {
8377
8385
  const mtimeMs = statSync2(file).mtimeMs;
8378
- if (cache3?.mtimeMs !== mtimeMs) {
8386
+ if (cache4?.mtimeMs !== mtimeMs) {
8379
8387
  const body = readFileSync19(file, "utf8");
8380
8388
  const etag = `"${createHash("sha256").update(body).digest("hex").slice(0, 16)}"`;
8381
- cache3 = { body, etag, mtimeMs };
8389
+ cache4 = { body, etag, mtimeMs };
8382
8390
  }
8383
8391
  const headers = {
8384
- ETag: cache3.etag,
8392
+ ETag: cache4.etag,
8385
8393
  "Cache-Control": "no-cache"
8386
8394
  };
8387
- if (req.headers["if-none-match"] === cache3.etag) {
8395
+ if (req.headers["if-none-match"] === cache4.etag) {
8388
8396
  res.writeHead(304, headers);
8389
8397
  res.end();
8390
8398
  return;
@@ -8393,7 +8401,7 @@ function createBundleHandler(importMetaUrl, bundlePath, contentType = "applicati
8393
8401
  "Content-Type": contentType,
8394
8402
  ...headers
8395
8403
  });
8396
- res.end(cache3.body);
8404
+ res.end(cache4.body);
8397
8405
  };
8398
8406
  }
8399
8407
 
@@ -8881,6 +8889,73 @@ function getCwdParam(req, res) {
8881
8889
  return cwd;
8882
8890
  }
8883
8891
 
8892
+ // src/commands/sessions/web/getSessionParam.ts
8893
+ function getSessionParam(req) {
8894
+ const url = new URL(req.url ?? "/", "http://localhost");
8895
+ return url.searchParams.get("session") ?? void 0;
8896
+ }
8897
+
8898
+ // src/shared/db/findCommitAnchor.ts
8899
+ import { and as and7, asc as asc6, eq as eq16, inArray as inArray3 } from "drizzle-orm";
8900
+ async function findCommitAnchor(db, itemId2) {
8901
+ const rows = await db.select({ kind: itemGitRefs.kind, ref: itemGitRefs.ref }).from(itemGitRefs).where(
8902
+ and7(
8903
+ eq16(itemGitRefs.itemId, itemId2),
8904
+ inArray3(itemGitRefs.kind, ["commit", "commit-parent"])
8905
+ )
8906
+ ).orderBy(asc6(itemGitRefs.createdAt));
8907
+ const anchor = {};
8908
+ for (const row of rows) {
8909
+ if (row.kind === "commit") anchor.commit ??= row.ref;
8910
+ else anchor.parent ??= row.ref;
8911
+ }
8912
+ return anchor;
8913
+ }
8914
+
8915
+ // src/shared/db/findItemBySessionId.ts
8916
+ import { asc as asc7, eq as eq17 } from "drizzle-orm";
8917
+ async function findItemBySessionId(db, claudeSessionId) {
8918
+ const rows = await db.select({ itemId: phaseUsage.itemId }).from(phaseUsage).where(eq17(phaseUsage.claudeSessionId, claudeSessionId)).orderBy(asc7(phaseUsage.itemId), asc7(phaseUsage.phaseIdx)).limit(1);
8919
+ return rows[0]?.itemId;
8920
+ }
8921
+
8922
+ // src/commands/sessions/web/sessionCommitAnchor.ts
8923
+ var EMPTY_TTL_MS = 3e4;
8924
+ var cache3 = /* @__PURE__ */ new Map();
8925
+ var inFlight = /* @__PURE__ */ new Map();
8926
+ async function sessionCommitAnchor(sessionId) {
8927
+ const cached = cache3.get(sessionId);
8928
+ if (cached && cached.expiresAt > Date.now()) return cached.anchor;
8929
+ const pending = inFlight.get(sessionId);
8930
+ if (pending) return pending;
8931
+ const lookup = loadAnchor(sessionId).finally(
8932
+ () => inFlight.delete(sessionId)
8933
+ );
8934
+ inFlight.set(sessionId, lookup);
8935
+ return lookup;
8936
+ }
8937
+ async function loadAnchor(sessionId) {
8938
+ const anchor = await queryAnchor(sessionId);
8939
+ const found = Boolean(anchor.commit || anchor.parent);
8940
+ cache3.set(sessionId, {
8941
+ anchor,
8942
+ expiresAt: found ? Number.POSITIVE_INFINITY : Date.now() + EMPTY_TTL_MS
8943
+ });
8944
+ return anchor;
8945
+ }
8946
+ async function queryAnchor(sessionId) {
8947
+ try {
8948
+ if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url)
8949
+ return {};
8950
+ const db = await getDb();
8951
+ const itemId2 = await findItemBySessionId(db, sessionId);
8952
+ if (itemId2 === void 0) return {};
8953
+ return await findCommitAnchor(db, itemId2);
8954
+ } catch {
8955
+ return {};
8956
+ }
8957
+ }
8958
+
8884
8959
  // src/commands/sessions/web/windowsCwdToWslPath.ts
8885
8960
  function windowsCwdToWslPath(cwd) {
8886
8961
  const match = /^([A-Za-z]):[\\/](.*)$/.exec(cwd);
@@ -8914,7 +8989,13 @@ async function refExists2(cwd, ref) {
8914
8989
  return false;
8915
8990
  }
8916
8991
  }
8917
- async function resolveDiffBase(cwd) {
8992
+ async function anchoredBase(cwd, sessionId) {
8993
+ const { commit: commit2, parent } = await sessionCommitAnchor(sessionId);
8994
+ if (parent && await refExists2(cwd, parent)) return parent;
8995
+ if (commit2 && await refExists2(cwd, `${commit2}^`)) return `${commit2}^`;
8996
+ return void 0;
8997
+ }
8998
+ async function resolveDiffBase(cwd, sessionId) {
8918
8999
  let defaultBranch;
8919
9000
  try {
8920
9001
  const config = loadConfigFrom(toGitCwd(cwd));
@@ -8923,6 +9004,10 @@ async function resolveDiffBase(cwd) {
8923
9004
  } catch {
8924
9005
  return "HEAD";
8925
9006
  }
9007
+ if (sessionId) {
9008
+ const anchored = await anchoredBase(cwd, sessionId);
9009
+ if (anchored) return anchored;
9010
+ }
8926
9011
  for (const candidate of baseCandidates(defaultBranch)) {
8927
9012
  if (!await refExists2(cwd, candidate)) continue;
8928
9013
  try {
@@ -8960,7 +9045,7 @@ async function diff2(req, res) {
8960
9045
  const cwd = getCwdParam(req, res);
8961
9046
  if (!cwd) return;
8962
9047
  try {
8963
- const base = await resolveDiffBase(cwd);
9048
+ const base = await resolveDiffBase(cwd, getSessionParam(req));
8964
9049
  const tracked = await execGit(cwd, ["diff", base], {
8965
9050
  maxBuffer: MAX_DIFF_BYTES
8966
9051
  });
@@ -9280,7 +9365,7 @@ async function gitStatus(req, res) {
9280
9365
  const cwd = getCwdParam(req, res);
9281
9366
  if (!cwd) return;
9282
9367
  try {
9283
- const base = await resolveDiffBase(cwd);
9368
+ const base = await resolveDiffBase(cwd, getSessionParam(req));
9284
9369
  respondJson(
9285
9370
  res,
9286
9371
  200,
@@ -9569,7 +9654,7 @@ async function listNewsItems(_req, res) {
9569
9654
  }
9570
9655
 
9571
9656
  // src/shared/db/listUsagePeaks.ts
9572
- import { and as and7, count, desc as desc2, eq as eq16, getTableColumns, sql as sql16 } from "drizzle-orm";
9657
+ import { and as and8, count, desc as desc2, eq as eq18, getTableColumns, sql as sql16 } from "drizzle-orm";
9573
9658
  async function listUsagePeaks(db, paging) {
9574
9659
  const avgContext = db.select({
9575
9660
  window: phaseCycleContext.window,
@@ -9585,9 +9670,9 @@ async function listUsagePeaks(db, paging) {
9585
9670
  phaseCount: avgContext.phaseCount
9586
9671
  }).from(usagePeaks).leftJoin(
9587
9672
  avgContext,
9588
- and7(
9589
- eq16(usagePeaks.window, avgContext.window),
9590
- eq16(usagePeaks.resetsAt, avgContext.resetsAt)
9673
+ and8(
9674
+ eq18(usagePeaks.window, avgContext.window),
9675
+ eq18(usagePeaks.resetsAt, avgContext.resetsAt)
9591
9676
  )
9592
9677
  ).orderBy(
9593
9678
  desc2(usagePeaks.resetsAt),
@@ -9644,11 +9729,11 @@ var execFileAsync4 = promisify5(execFile5);
9644
9729
  var CACHE_TTL_MS2 = 3e4;
9645
9730
  function createCachedGhJson(args, parse3, fallback, options2 = {}) {
9646
9731
  const { cacheFallback = true } = options2;
9647
- const cache3 = /* @__PURE__ */ new Map();
9732
+ const cache4 = /* @__PURE__ */ new Map();
9648
9733
  return async (cwd, extraArgs = []) => {
9649
9734
  const key = extraArgs.length ? `${cwd}\0${extraArgs.join("\0")}` : cwd;
9650
9735
  const now = Date.now();
9651
- const cached = cache3.get(key);
9736
+ const cached = cache4.get(key);
9652
9737
  if (cached && cached.expires > now) return cached.value;
9653
9738
  let value = fallback;
9654
9739
  try {
@@ -9661,7 +9746,7 @@ function createCachedGhJson(args, parse3, fallback, options2 = {}) {
9661
9746
  value = fallback;
9662
9747
  }
9663
9748
  if (cacheFallback || value !== fallback)
9664
- cache3.set(key, { value, expires: now + CACHE_TTL_MS2 });
9749
+ cache4.set(key, { value, expires: now + CACHE_TTL_MS2 });
9665
9750
  return value;
9666
9751
  };
9667
9752
  }
@@ -10172,22 +10257,22 @@ import { readFileSync as readFileSync21 } from "fs";
10172
10257
  import { createRequire as createRequire2 } from "module";
10173
10258
  var require3 = createRequire2(import.meta.url);
10174
10259
  function createCssHandler(packageEntry) {
10175
- let cache3;
10260
+ let cache4;
10176
10261
  return (req, res) => {
10177
- if (!cache3) {
10262
+ if (!cache4) {
10178
10263
  const resolved = require3.resolve(packageEntry);
10179
10264
  const body = readFileSync21(resolved, "utf8");
10180
10265
  const etag = `"${createHash2("sha256").update(body).digest("hex").slice(0, 16)}"`;
10181
- cache3 = { body, etag };
10266
+ cache4 = { body, etag };
10182
10267
  }
10183
- const headers = { ETag: cache3.etag, "Cache-Control": "no-cache" };
10184
- if (req.headers["if-none-match"] === cache3.etag) {
10268
+ const headers = { ETag: cache4.etag, "Cache-Control": "no-cache" };
10269
+ if (req.headers["if-none-match"] === cache4.etag) {
10185
10270
  res.writeHead(304, headers);
10186
10271
  res.end();
10187
10272
  return;
10188
10273
  }
10189
10274
  res.writeHead(200, { "Content-Type": "text/css", ...headers });
10190
- res.end(cache3.body);
10275
+ res.end(cache4.body);
10191
10276
  };
10192
10277
  }
10193
10278
 
@@ -10685,11 +10770,11 @@ function registerActivityCommands(cmd) {
10685
10770
 
10686
10771
  // src/commands/backlog/associate-github/index.ts
10687
10772
  import chalk64 from "chalk";
10688
- import { eq as eq18 } from "drizzle-orm";
10773
+ import { eq as eq20 } from "drizzle-orm";
10689
10774
 
10690
10775
  // src/commands/backlog/beginAssociation.ts
10691
10776
  import chalk63 from "chalk";
10692
- import { eq as eq17 } from "drizzle-orm";
10777
+ import { eq as eq19 } from "drizzle-orm";
10693
10778
  async function beginAssociation(id, options2, clearPatch, label2) {
10694
10779
  const found = await findOneItem(id);
10695
10780
  if (!found) {
@@ -10699,7 +10784,7 @@ async function beginAssociation(id, options2, clearPatch, label2) {
10699
10784
  const { orm } = found;
10700
10785
  const itemId2 = found.item.id;
10701
10786
  if (options2.clear) {
10702
- await orm.update(items).set(clearPatch).where(eq17(items.id, itemId2));
10787
+ await orm.update(items).set(clearPatch).where(eq19(items.id, itemId2));
10703
10788
  console.log(
10704
10789
  chalk63.green(
10705
10790
  `Cleared ${label2} association on item ${formatItemId(itemId2)}.`
@@ -10767,7 +10852,7 @@ async function associateGithub(id, issue, options2) {
10767
10852
  return;
10768
10853
  }
10769
10854
  const title = fetchGithubIssueTitle(normalized);
10770
- await orm.update(items).set({ githubIssue: normalized, jiraKey: null }).where(eq18(items.id, itemId2));
10855
+ await orm.update(items).set({ githubIssue: normalized, jiraKey: null }).where(eq20(items.id, itemId2));
10771
10856
  console.log(
10772
10857
  chalk64.green(`Associated ${normalized} with item ${formatItemId(itemId2)}.`),
10773
10858
  title ? chalk64.dim(`(${title})`) : ""
@@ -10781,7 +10866,7 @@ function registerAssociateGithubCommand(cmd) {
10781
10866
 
10782
10867
  // src/commands/backlog/associate-jira/index.ts
10783
10868
  import chalk66 from "chalk";
10784
- import { eq as eq19 } from "drizzle-orm";
10869
+ import { eq as eq21 } from "drizzle-orm";
10785
10870
 
10786
10871
  // src/commands/jira/fetchIssue.ts
10787
10872
  import { execSync as execSync32 } from "child_process";
@@ -10846,7 +10931,7 @@ async function associateJira(id, key, options2) {
10846
10931
  const parsed = fetchIssue(normalized, "summary");
10847
10932
  const fields = parsed?.fields;
10848
10933
  const summary = fields?.summary;
10849
- await orm.update(items).set({ jiraKey: normalized, githubIssue: null }).where(eq19(items.id, itemId2));
10934
+ await orm.update(items).set({ jiraKey: normalized, githubIssue: null }).where(eq21(items.id, itemId2));
10850
10935
  console.log(
10851
10936
  chalk66.green(`Associated ${normalized} with item ${formatItemId(itemId2)}.`),
10852
10937
  summary ? chalk66.dim(`(${summary})`) : ""
@@ -11219,15 +11304,15 @@ function registerImportCommand(cmd) {
11219
11304
  import chalk74 from "chalk";
11220
11305
 
11221
11306
  // src/commands/backlog/insertPhaseAt.ts
11222
- import { eq as eq21 } from "drizzle-orm";
11307
+ import { eq as eq23 } from "drizzle-orm";
11223
11308
 
11224
11309
  // src/commands/backlog/shiftPhasesUp.ts
11225
- import { and as and8, desc as desc3, eq as eq20, gte } from "drizzle-orm";
11310
+ import { and as and9, desc as desc3, eq as eq22, gte } from "drizzle-orm";
11226
11311
  async function shiftPhasesUp(db, itemId2, fromIdx) {
11227
- const toShift = await db.select({ idx: planPhases.idx }).from(planPhases).where(and8(eq20(planPhases.itemId, itemId2), gte(planPhases.idx, fromIdx))).orderBy(desc3(planPhases.idx));
11312
+ const toShift = await db.select({ idx: planPhases.idx }).from(planPhases).where(and9(eq22(planPhases.itemId, itemId2), gte(planPhases.idx, fromIdx))).orderBy(desc3(planPhases.idx));
11228
11313
  for (const p of toShift) {
11229
- await db.update(planTasks).set({ phaseIdx: p.idx + 1 }).where(and8(eq20(planTasks.itemId, itemId2), eq20(planTasks.phaseIdx, p.idx)));
11230
- await db.update(planPhases).set({ idx: p.idx + 1 }).where(and8(eq20(planPhases.itemId, itemId2), eq20(planPhases.idx, p.idx)));
11314
+ await db.update(planTasks).set({ phaseIdx: p.idx + 1 }).where(and9(eq22(planTasks.itemId, itemId2), eq22(planTasks.phaseIdx, p.idx)));
11315
+ await db.update(planPhases).set({ idx: p.idx + 1 }).where(and9(eq22(planPhases.itemId, itemId2), eq22(planPhases.idx, p.idx)));
11231
11316
  }
11232
11317
  }
11233
11318
 
@@ -11240,7 +11325,7 @@ async function insertPhaseAt(orm, itemId2, phaseIdx, name, tasks, manualChecks,
11240
11325
  await tx.insert(planTasks).values(tasks.map((task, i) => ({ itemId: itemId2, phaseIdx, idx: i, task })));
11241
11326
  }
11242
11327
  if (currentPhase !== void 0 && currentPhase - 1 >= phaseIdx) {
11243
- await tx.update(items).set({ currentPhase: phaseIdx + 1 }).where(eq21(items.id, itemId2));
11328
+ await tx.update(items).set({ currentPhase: phaseIdx + 1 }).where(eq23(items.id, itemId2));
11244
11329
  }
11245
11330
  });
11246
11331
  }
@@ -11403,9 +11488,9 @@ import chalk76 from "chalk";
11403
11488
 
11404
11489
  // src/commands/backlog/resolveInsertPosition.ts
11405
11490
  import chalk75 from "chalk";
11406
- import { count as count2, eq as eq22 } from "drizzle-orm";
11491
+ import { count as count2, eq as eq24 } from "drizzle-orm";
11407
11492
  async function resolveInsertPosition(orm, itemId2, position) {
11408
- const [row] = await orm.select({ cnt: count2() }).from(planPhases).where(eq22(planPhases.itemId, itemId2));
11493
+ const [row] = await orm.select({ cnt: count2() }).from(planPhases).where(eq24(planPhases.itemId, itemId2));
11409
11494
  const phaseCount = row?.cnt ?? 0;
11410
11495
  if (position === void 0) return phaseCount;
11411
11496
  const pos = Number.parseInt(position, 10);
@@ -11734,9 +11819,9 @@ function hasCycle(adjacency, fromId, toId) {
11734
11819
  }
11735
11820
 
11736
11821
  // src/commands/backlog/loadDependencyGraph.ts
11737
- import { eq as eq23 } from "drizzle-orm";
11822
+ import { eq as eq25 } from "drizzle-orm";
11738
11823
  async function loadDependencyGraph(orm) {
11739
- const rows = await orm.select({ itemId: links.itemId, targetId: links.targetId }).from(links).where(eq23(links.type, "depends-on"));
11824
+ const rows = await orm.select({ itemId: links.itemId, targetId: links.targetId }).from(links).where(eq25(links.type, "depends-on"));
11740
11825
  const graph = /* @__PURE__ */ new Map();
11741
11826
  for (const { itemId: itemId2, targetId } of rows) {
11742
11827
  const bucket = graph.get(itemId2);
@@ -11803,7 +11888,7 @@ async function link(fromId, toId, opts) {
11803
11888
 
11804
11889
  // src/commands/backlog/unlink.ts
11805
11890
  import chalk82 from "chalk";
11806
- import { and as and9, eq as eq24 } from "drizzle-orm";
11891
+ import { and as and10, eq as eq26 } from "drizzle-orm";
11807
11892
  async function unlink(fromId, toId) {
11808
11893
  const fromNum = parseItemId(fromId);
11809
11894
  const toNum = parseItemId(toId);
@@ -11827,7 +11912,7 @@ async function unlink(fromId, toId) {
11827
11912
  );
11828
11913
  return;
11829
11914
  }
11830
- await orm.delete(links).where(and9(eq24(links.itemId, fromNum), eq24(links.targetId, toNum)));
11915
+ await orm.delete(links).where(and10(eq26(links.itemId, fromNum), eq26(links.targetId, toNum)));
11831
11916
  console.log(
11832
11917
  chalk82.green(
11833
11918
  `Removed link from ${formatItemId(fromNum)} to ${formatItemId(toNum)}.`
@@ -11847,7 +11932,7 @@ function registerLinkCommands(cmd) {
11847
11932
 
11848
11933
  // src/commands/backlog/move-repo/index.ts
11849
11934
  import chalk84 from "chalk";
11850
- import { eq as eq26 } from "drizzle-orm";
11935
+ import { eq as eq28 } from "drizzle-orm";
11851
11936
 
11852
11937
  // src/commands/backlog/move-repo/confirmMove.ts
11853
11938
  import chalk83 from "chalk";
@@ -11862,9 +11947,9 @@ async function confirmMove(cnt, oldOrigin, newOrigin) {
11862
11947
  }
11863
11948
 
11864
11949
  // src/commands/backlog/move-repo/countByOrigin.ts
11865
- import { count as count3, eq as eq25 } from "drizzle-orm";
11950
+ import { count as count3, eq as eq27 } from "drizzle-orm";
11866
11951
  async function countByOrigin(orm, origin) {
11867
- const [{ cnt }] = await orm.select({ cnt: count3() }).from(items).where(eq25(items.origin, origin));
11952
+ const [{ cnt }] = await orm.select({ cnt: count3() }).from(items).where(eq27(items.origin, origin));
11868
11953
  return cnt;
11869
11954
  }
11870
11955
 
@@ -11907,7 +11992,7 @@ async function moveRepo(oldOriginRaw, newOriginRaw, options2 = {}) {
11907
11992
  console.log(chalk84.yellow("Move cancelled; no changes made."));
11908
11993
  return;
11909
11994
  }
11910
- await orm.update(items).set({ origin: newOrigin }).where(eq26(items.origin, oldOrigin));
11995
+ await orm.update(items).set({ origin: newOrigin }).where(eq28(items.origin, oldOrigin));
11911
11996
  console.log(
11912
11997
  chalk84.green(
11913
11998
  `Moved ${pluralItems(cnt)} from "${oldOrigin}" to "${newOrigin}".`
@@ -12324,10 +12409,10 @@ async function start(id) {
12324
12409
 
12325
12410
  // src/commands/backlog/stop/index.ts
12326
12411
  import chalk96 from "chalk";
12327
- import { and as and10, eq as eq27 } from "drizzle-orm";
12412
+ import { and as and11, eq as eq29 } from "drizzle-orm";
12328
12413
  async function stop() {
12329
12414
  const { orm } = await getReady();
12330
- const stopped = await orm.update(items).set({ status: "todo", currentPhase: 1 }).where(and10(eq27(items.status, "in-progress"), eq27(items.origin, getOrigin()))).returning({ id: items.id, name: items.name });
12415
+ const stopped = await orm.update(items).set({ status: "todo", currentPhase: 1 }).where(and11(eq29(items.status, "in-progress"), eq29(items.origin, getOrigin()))).returning({ id: items.id, name: items.name });
12331
12416
  if (stopped.length === 0) {
12332
12417
  console.log(chalk96.yellow("No in-progress items to stop."));
12333
12418
  return;
@@ -12444,13 +12529,13 @@ async function findSubtask(id, index3) {
12444
12529
  }
12445
12530
 
12446
12531
  // src/commands/backlog/updateSubtask.ts
12447
- import { and as and11, eq as eq28 } from "drizzle-orm";
12532
+ import { and as and12, eq as eq30 } from "drizzle-orm";
12448
12533
  async function updateSubtask(orm, itemId2, idx, fields) {
12449
12534
  const set = {};
12450
12535
  if (fields.title !== void 0) set.title = fields.title;
12451
12536
  if (fields.description !== void 0) set.description = fields.description;
12452
12537
  if (fields.status !== void 0) set.status = fields.status;
12453
- const [row] = await orm.update(itemSubtasks).set(set).where(and11(eq28(itemSubtasks.itemId, itemId2), eq28(itemSubtasks.idx, idx))).returning({ title: itemSubtasks.title });
12538
+ const [row] = await orm.update(itemSubtasks).set(set).where(and12(eq30(itemSubtasks.itemId, itemId2), eq30(itemSubtasks.idx, idx))).returning({ title: itemSubtasks.title });
12454
12539
  return row?.title;
12455
12540
  }
12456
12541
 
@@ -12505,16 +12590,16 @@ async function editSubtask(id, index3, options2) {
12505
12590
  import chalk102 from "chalk";
12506
12591
 
12507
12592
  // src/commands/backlog/deleteSubtask.ts
12508
- import { and as and12, asc as asc6, eq as eq29 } from "drizzle-orm";
12593
+ import { and as and13, asc as asc8, eq as eq31 } from "drizzle-orm";
12509
12594
  async function deleteSubtask(orm, itemId2, idx) {
12510
- const [row] = await orm.delete(itemSubtasks).where(and12(eq29(itemSubtasks.itemId, itemId2), eq29(itemSubtasks.idx, idx))).returning({ title: itemSubtasks.title });
12595
+ const [row] = await orm.delete(itemSubtasks).where(and13(eq31(itemSubtasks.itemId, itemId2), eq31(itemSubtasks.idx, idx))).returning({ title: itemSubtasks.title });
12511
12596
  if (!row) return void 0;
12512
- const remaining = await orm.select({ idx: itemSubtasks.idx }).from(itemSubtasks).where(eq29(itemSubtasks.itemId, itemId2)).orderBy(asc6(itemSubtasks.idx));
12597
+ const remaining = await orm.select({ idx: itemSubtasks.idx }).from(itemSubtasks).where(eq31(itemSubtasks.itemId, itemId2)).orderBy(asc8(itemSubtasks.idx));
12513
12598
  for (let i = 0; i < remaining.length; i++) {
12514
12599
  const oldIdx = remaining[i].idx;
12515
12600
  if (oldIdx === i) continue;
12516
12601
  await orm.update(itemSubtasks).set({ idx: i }).where(
12517
- and12(eq29(itemSubtasks.itemId, itemId2), eq29(itemSubtasks.idx, oldIdx))
12602
+ and13(eq31(itemSubtasks.itemId, itemId2), eq31(itemSubtasks.idx, oldIdx))
12518
12603
  );
12519
12604
  }
12520
12605
  return row.title;
@@ -12583,20 +12668,20 @@ function registerSubtaskCommands(cmd) {
12583
12668
 
12584
12669
  // src/commands/backlog/movePhase.ts
12585
12670
  import chalk104 from "chalk";
12586
- import { count as count5, eq as eq32 } from "drizzle-orm";
12671
+ import { count as count5, eq as eq34 } from "drizzle-orm";
12587
12672
 
12588
12673
  // src/commands/backlog/reorderPhaseRows.ts
12589
- import { and as and14, asc as asc8, eq as eq31 } from "drizzle-orm";
12674
+ import { and as and15, asc as asc10, eq as eq33 } from "drizzle-orm";
12590
12675
 
12591
12676
  // src/commands/backlog/reindexPhases.ts
12592
- import { and as and13, asc as asc7, count as count4, eq as eq30 } from "drizzle-orm";
12677
+ import { and as and14, asc as asc9, count as count4, eq as eq32 } from "drizzle-orm";
12593
12678
  async function reindexPhases(db, itemId2) {
12594
- const remaining = await db.select({ idx: planPhases.idx }).from(planPhases).where(eq30(planPhases.itemId, itemId2)).orderBy(asc7(planPhases.idx));
12679
+ const remaining = await db.select({ idx: planPhases.idx }).from(planPhases).where(eq32(planPhases.itemId, itemId2)).orderBy(asc9(planPhases.idx));
12595
12680
  for (let i = 0; i < remaining.length; i++) {
12596
12681
  const oldIdx = remaining[i].idx;
12597
12682
  if (oldIdx === i) continue;
12598
- await db.update(planTasks).set({ phaseIdx: i }).where(and13(eq30(planTasks.itemId, itemId2), eq30(planTasks.phaseIdx, oldIdx)));
12599
- await db.update(planPhases).set({ idx: i }).where(and13(eq30(planPhases.itemId, itemId2), eq30(planPhases.idx, oldIdx)));
12683
+ await db.update(planTasks).set({ phaseIdx: i }).where(and14(eq32(planTasks.itemId, itemId2), eq32(planTasks.phaseIdx, oldIdx)));
12684
+ await db.update(planPhases).set({ idx: i }).where(and14(eq32(planPhases.itemId, itemId2), eq32(planPhases.idx, oldIdx)));
12600
12685
  }
12601
12686
  }
12602
12687
  async function adjustCurrentPhase(db, item, removedIdx) {
@@ -12604,24 +12689,24 @@ async function adjustCurrentPhase(db, item, removedIdx) {
12604
12689
  if (currentPhase === void 0) return;
12605
12690
  const currentIdx = currentPhase - 1;
12606
12691
  if (removedIdx < currentIdx) {
12607
- await db.update(items).set({ currentPhase: currentPhase - 1 }).where(eq30(items.id, item.id));
12692
+ await db.update(items).set({ currentPhase: currentPhase - 1 }).where(eq32(items.id, item.id));
12608
12693
  return;
12609
12694
  }
12610
12695
  if (removedIdx !== currentIdx) return;
12611
- const [row] = await db.select({ cnt: count4() }).from(planPhases).where(eq30(planPhases.itemId, item.id));
12696
+ const [row] = await db.select({ cnt: count4() }).from(planPhases).where(eq32(planPhases.itemId, item.id));
12612
12697
  const cnt = row?.cnt ?? 0;
12613
- await db.update(items).set({ currentPhase: cnt === 0 ? null : Math.min(currentPhase, cnt) }).where(eq30(items.id, item.id));
12698
+ await db.update(items).set({ currentPhase: cnt === 0 ? null : Math.min(currentPhase, cnt) }).where(eq32(items.id, item.id));
12614
12699
  }
12615
12700
 
12616
12701
  // src/commands/backlog/reorderPhaseRows.ts
12617
12702
  async function reorderPhaseRows(orm, itemId2, fromIdx, toIdx) {
12618
12703
  await orm.transaction(async (tx) => {
12619
- const [phaseRow] = await tx.select({ name: planPhases.name, manualChecks: planPhases.manualChecks }).from(planPhases).where(and14(eq31(planPhases.itemId, itemId2), eq31(planPhases.idx, fromIdx)));
12620
- const tasks = await tx.select({ idx: planTasks.idx, task: planTasks.task }).from(planTasks).where(and14(eq31(planTasks.itemId, itemId2), eq31(planTasks.phaseIdx, fromIdx))).orderBy(asc8(planTasks.idx));
12704
+ const [phaseRow] = await tx.select({ name: planPhases.name, manualChecks: planPhases.manualChecks }).from(planPhases).where(and15(eq33(planPhases.itemId, itemId2), eq33(planPhases.idx, fromIdx)));
12705
+ const tasks = await tx.select({ idx: planTasks.idx, task: planTasks.task }).from(planTasks).where(and15(eq33(planTasks.itemId, itemId2), eq33(planTasks.phaseIdx, fromIdx))).orderBy(asc10(planTasks.idx));
12621
12706
  await tx.delete(planTasks).where(
12622
- and14(eq31(planTasks.itemId, itemId2), eq31(planTasks.phaseIdx, fromIdx))
12707
+ and15(eq33(planTasks.itemId, itemId2), eq33(planTasks.phaseIdx, fromIdx))
12623
12708
  );
12624
- await tx.delete(planPhases).where(and14(eq31(planPhases.itemId, itemId2), eq31(planPhases.idx, fromIdx)));
12709
+ await tx.delete(planPhases).where(and15(eq33(planPhases.itemId, itemId2), eq33(planPhases.idx, fromIdx)));
12625
12710
  await reindexPhases(tx, itemId2);
12626
12711
  await shiftPhasesUp(tx, itemId2, toIdx);
12627
12712
  await tx.insert(planPhases).values({
@@ -12662,7 +12747,7 @@ async function movePhase(id, from, to) {
12662
12747
  if (!found) return;
12663
12748
  const { orm, item } = found;
12664
12749
  const itemId2 = item.id;
12665
- const [row] = await orm.select({ cnt: count5() }).from(planPhases).where(eq32(planPhases.itemId, itemId2));
12750
+ const [row] = await orm.select({ cnt: count5() }).from(planPhases).where(eq34(planPhases.itemId, itemId2));
12666
12751
  const phaseCount = row?.cnt ?? 0;
12667
12752
  const fromIdx = toIndex(from, phaseCount);
12668
12753
  if (fromIdx === void 0) return;
@@ -12678,22 +12763,22 @@ async function movePhase(id, from, to) {
12678
12763
  import chalk106 from "chalk";
12679
12764
 
12680
12765
  // src/commands/backlog/applyPhaseUpdate.ts
12681
- import { and as and15, eq as eq33 } from "drizzle-orm";
12766
+ import { and as and16, eq as eq35 } from "drizzle-orm";
12682
12767
  async function applyPhaseUpdate(orm, itemId2, phaseIdx, fields) {
12683
12768
  await orm.transaction(async (tx) => {
12684
12769
  if (fields.name) {
12685
12770
  await tx.update(planPhases).set({ name: fields.name }).where(
12686
- and15(eq33(planPhases.itemId, itemId2), eq33(planPhases.idx, phaseIdx))
12771
+ and16(eq35(planPhases.itemId, itemId2), eq35(planPhases.idx, phaseIdx))
12687
12772
  );
12688
12773
  }
12689
12774
  if (fields.manualCheck) {
12690
12775
  await tx.update(planPhases).set({ manualChecks: JSON.stringify(fields.manualCheck) }).where(
12691
- and15(eq33(planPhases.itemId, itemId2), eq33(planPhases.idx, phaseIdx))
12776
+ and16(eq35(planPhases.itemId, itemId2), eq35(planPhases.idx, phaseIdx))
12692
12777
  );
12693
12778
  }
12694
12779
  if (fields.task) {
12695
12780
  await tx.delete(planTasks).where(
12696
- and15(eq33(planTasks.itemId, itemId2), eq33(planTasks.phaseIdx, phaseIdx))
12781
+ and16(eq35(planTasks.itemId, itemId2), eq35(planTasks.phaseIdx, phaseIdx))
12697
12782
  );
12698
12783
  if (fields.task.length) {
12699
12784
  await tx.insert(planTasks).values(
@@ -12711,14 +12796,14 @@ async function applyPhaseUpdate(orm, itemId2, phaseIdx, fields) {
12711
12796
 
12712
12797
  // src/commands/backlog/findPhase.ts
12713
12798
  import chalk105 from "chalk";
12714
- import { and as and16, count as count6, eq as eq34 } from "drizzle-orm";
12799
+ import { and as and17, count as count6, eq as eq36 } from "drizzle-orm";
12715
12800
  async function findPhase(id, phase) {
12716
12801
  const found = await findOneItem(id);
12717
12802
  if (!found) return void 0;
12718
12803
  const { orm, item } = found;
12719
12804
  const itemId2 = item.id;
12720
12805
  const phaseIdx = Number.parseInt(phase, 10) - 1;
12721
- const [row] = await orm.select({ cnt: count6() }).from(planPhases).where(and16(eq34(planPhases.itemId, itemId2), eq34(planPhases.idx, phaseIdx)));
12806
+ const [row] = await orm.select({ cnt: count6() }).from(planPhases).where(and17(eq36(planPhases.itemId, itemId2), eq36(planPhases.idx, phaseIdx)));
12722
12807
  if (!row || row.cnt === 0) {
12723
12808
  console.log(
12724
12809
  chalk105.red(
@@ -12891,16 +12976,16 @@ function registerUpdatePhaseCommand(cmd) {
12891
12976
 
12892
12977
  // src/commands/backlog/removePhase.ts
12893
12978
  import chalk107 from "chalk";
12894
- import { and as and17, eq as eq35 } from "drizzle-orm";
12979
+ import { and as and18, eq as eq37 } from "drizzle-orm";
12895
12980
  async function removePhase(id, phase) {
12896
12981
  const found = await findPhase(id, phase);
12897
12982
  if (!found) return;
12898
12983
  const { item, orm, itemId: itemId2, phaseIdx } = found;
12899
12984
  await orm.transaction(async (tx) => {
12900
12985
  await tx.delete(planTasks).where(
12901
- and17(eq35(planTasks.itemId, itemId2), eq35(planTasks.phaseIdx, phaseIdx))
12986
+ and18(eq37(planTasks.itemId, itemId2), eq37(planTasks.phaseIdx, phaseIdx))
12902
12987
  );
12903
- await tx.delete(planPhases).where(and17(eq35(planPhases.itemId, itemId2), eq35(planPhases.idx, phaseIdx)));
12988
+ await tx.delete(planPhases).where(and18(eq37(planPhases.itemId, itemId2), eq37(planPhases.idx, phaseIdx)));
12904
12989
  await reindexPhases(tx, itemId2);
12905
12990
  await adjustCurrentPhase(tx, item, phaseIdx);
12906
12991
  });
@@ -12913,7 +12998,7 @@ async function removePhase(id, phase) {
12913
12998
 
12914
12999
  // src/commands/backlog/update/update.ts
12915
13000
  import chalk111 from "chalk";
12916
- import { eq as eq36 } from "drizzle-orm";
13001
+ import { eq as eq38 } from "drizzle-orm";
12917
13002
 
12918
13003
  // src/commands/backlog/update/buildUpdateValues.ts
12919
13004
  import chalk108 from "chalk";
@@ -13024,7 +13109,7 @@ async function update(id, options2) {
13024
13109
  if (!built) return;
13025
13110
  const { orm } = found;
13026
13111
  const itemId2 = found.item.id;
13027
- await orm.update(items).set(built.set).where(eq36(items.id, itemId2));
13112
+ await orm.update(items).set(built.set).where(eq38(items.id, itemId2));
13028
13113
  console.log(
13029
13114
  chalk111.green(`Updated ${built.fields} on item ${formatItemId(itemId2)}.`)
13030
13115
  );
@@ -17107,9 +17192,9 @@ function registerGithub(program2) {
17107
17192
  }
17108
17193
 
17109
17194
  // src/commands/handover/countPendingHandovers.ts
17110
- import { and as and18, eq as eq37, isNull, sql as sql18 } from "drizzle-orm";
17195
+ import { and as and19, eq as eq39, isNull, sql as sql18 } from "drizzle-orm";
17111
17196
  async function countPendingHandovers(orm, origin) {
17112
- const [row] = await orm.select({ count: sql18`count(*)::int` }).from(handovers).where(and18(eq37(handovers.origin, origin), isNull(handovers.recalledAt)));
17197
+ const [row] = await orm.select({ count: sql18`count(*)::int` }).from(handovers).where(and19(eq39(handovers.origin, origin), isNull(handovers.recalledAt)));
17113
17198
  return row?.count ?? 0;
17114
17199
  }
17115
17200
 
@@ -17249,13 +17334,13 @@ async function load2(options2 = {}) {
17249
17334
  }
17250
17335
 
17251
17336
  // src/commands/handover/listPendingHandovers.ts
17252
- import { and as and19, desc as desc4, eq as eq38, isNull as isNull2 } from "drizzle-orm";
17337
+ import { and as and20, desc as desc4, eq as eq40, isNull as isNull2 } from "drizzle-orm";
17253
17338
  async function listPendingHandovers(orm, origin) {
17254
17339
  return orm.select({
17255
17340
  id: handovers.id,
17256
17341
  summary: handovers.summary,
17257
17342
  createdAt: handovers.createdAt
17258
- }).from(handovers).where(and19(eq38(handovers.origin, origin), isNull2(handovers.recalledAt))).orderBy(desc4(handovers.createdAt), desc4(handovers.id));
17343
+ }).from(handovers).where(and20(eq40(handovers.origin, origin), isNull2(handovers.recalledAt))).orderBy(desc4(handovers.createdAt), desc4(handovers.id));
17259
17344
  }
17260
17345
 
17261
17346
  // src/commands/handover/printPendingHandovers.ts
@@ -17268,17 +17353,17 @@ async function printPendingHandovers() {
17268
17353
  }
17269
17354
 
17270
17355
  // src/commands/handover/recallHandover.ts
17271
- import { and as and20, desc as desc5, eq as eq39, isNull as isNull3 } from "drizzle-orm";
17356
+ import { and as and21, desc as desc5, eq as eq41, isNull as isNull3 } from "drizzle-orm";
17272
17357
  async function recallHandover(orm, origin, id) {
17273
17358
  const [row] = await orm.select().from(handovers).where(
17274
- and20(
17275
- eq39(handovers.origin, origin),
17359
+ and21(
17360
+ eq41(handovers.origin, origin),
17276
17361
  isNull3(handovers.recalledAt),
17277
- ...id === void 0 ? [] : [eq39(handovers.id, id)]
17362
+ ...id === void 0 ? [] : [eq41(handovers.id, id)]
17278
17363
  )
17279
17364
  ).orderBy(desc5(handovers.createdAt), desc5(handovers.id)).limit(1);
17280
17365
  if (!row) return void 0;
17281
- await orm.update(handovers).set({ recalledAt: /* @__PURE__ */ new Date() }).where(eq39(handovers.id, row.id));
17366
+ await orm.update(handovers).set({ recalledAt: /* @__PURE__ */ new Date() }).where(eq41(handovers.id, row.id));
17282
17367
  return row.content;
17283
17368
  }
17284
17369
 
@@ -19281,20 +19366,20 @@ function resolveThread(threadId) {
19281
19366
  }
19282
19367
  }
19283
19368
  function requireCache(prNumber) {
19284
- const cache3 = loadCommentsCache(prNumber);
19285
- if (!cache3) {
19369
+ const cache4 = loadCommentsCache(prNumber);
19370
+ if (!cache4) {
19286
19371
  console.error(
19287
19372
  `Error: No cached comments found for PR #${prNumber}. Run "assist prs list-comments" first.`
19288
19373
  );
19289
19374
  process.exit(1);
19290
19375
  }
19291
- return cache3;
19376
+ return cache4;
19292
19377
  }
19293
19378
  function findLineComment(comments3, commentId) {
19294
19379
  return comments3.find((c) => c.type === "line" && c.id === commentId);
19295
19380
  }
19296
- function requireLineComment(cache3, commentId) {
19297
- const comment3 = findLineComment(cache3.comments, commentId);
19381
+ function requireLineComment(cache4, commentId) {
19382
+ const comment3 = findLineComment(cache4.comments, commentId);
19298
19383
  if (!comment3 || comment3.type !== "line" || !comment3.threadId) {
19299
19384
  console.error(
19300
19385
  `Error: Comment #${commentId} not found or has no thread ID.`
@@ -19303,8 +19388,8 @@ function requireLineComment(cache3, commentId) {
19303
19388
  }
19304
19389
  return comment3;
19305
19390
  }
19306
- function cleanupCacheIfDone(cache3, prNumber, commentId) {
19307
- const hasRemaining = cache3.comments.some(
19391
+ function cleanupCacheIfDone(cache4, prNumber, commentId) {
19392
+ const hasRemaining = cache4.comments.some(
19308
19393
  (c) => c.type === "line" && c.id !== commentId
19309
19394
  );
19310
19395
  if (!hasRemaining) deleteCommentsCache(prNumber);
@@ -19312,13 +19397,13 @@ function cleanupCacheIfDone(cache3, prNumber, commentId) {
19312
19397
  function resolveCommentWithReply(commentId, message2) {
19313
19398
  const prNumber = getCurrentPrNumber();
19314
19399
  const { org, repo } = getRepoInfo();
19315
- const cache3 = requireCache(prNumber);
19316
- const comment3 = requireLineComment(cache3, commentId);
19400
+ const cache4 = requireCache(prNumber);
19401
+ const comment3 = requireLineComment(cache4, commentId);
19317
19402
  replyToComment(org, repo, prNumber, commentId, message2);
19318
19403
  console.log("Reply posted successfully.");
19319
19404
  resolveThread(comment3.threadId);
19320
19405
  console.log("Thread resolved successfully.");
19321
- cleanupCacheIfDone(cache3, prNumber, commentId);
19406
+ cleanupCacheIfDone(cache4, prNumber, commentId);
19322
19407
  }
19323
19408
 
19324
19409
  // src/commands/prs/fixed.ts
@@ -26478,6 +26563,7 @@ function toSessionInfo({
26478
26563
  serverOrigin,
26479
26564
  assistArgs,
26480
26565
  cwd,
26566
+ claudeSessionId,
26481
26567
  restored,
26482
26568
  error,
26483
26569
  activity: activity2,
@@ -26508,6 +26594,7 @@ function toSessionInfo({
26508
26594
  remoteOrigin: serverOrigin ?? originForCwd(cwd),
26509
26595
  assistArgs,
26510
26596
  cwd,
26597
+ claudeSessionId,
26511
26598
  repoGroup: repoGroupForCwd(cwd),
26512
26599
  restored,
26513
26600
  error,
@@ -27155,13 +27242,6 @@ function greetClient(client, sessions, windowsProxy) {
27155
27242
  void windowsProxy.discover();
27156
27243
  }
27157
27244
 
27158
- // src/commands/sessions/daemon/logSpawnedSession.ts
27159
- function logSpawnedSession(session) {
27160
- daemonLog(
27161
- `session ${session.id} spawned: ${session.name} [${session.commandType}] ${session.cwd ?? ""}`
27162
- );
27163
- }
27164
-
27165
27245
  // src/commands/sessions/daemon/decidePrPreview.ts
27166
27246
  function decidePrPreview(sessions, waiters, notify2, d) {
27167
27247
  const id = d.sessionId;
@@ -27249,361 +27329,93 @@ var PrPreviewCoordinator = class {
27249
27329
  }
27250
27330
  };
27251
27331
 
27252
- // src/commands/sessions/daemon/applySetStatus.ts
27253
- function applySetStatus(sessions, id, status3, source, onStatusChange) {
27254
- const session = sessions.get(id);
27255
- if (!session) {
27256
- daemonLog(
27257
- `set-status for unknown session id=${id} status=${status3} (no live session; ignoring)`
27258
- );
27259
- return;
27260
- }
27261
- if (session.status === "stopped") {
27262
- daemonLog(
27263
- `set-status for stopped session id=${id} status=${status3} (held pending durable close; ignoring)`
27264
- );
27265
- return;
27266
- }
27267
- if (source === "permission") session.permissionActive = true;
27268
- if (status3 === "running") session.permissionActive = false;
27269
- onStatusChange(session, status3);
27270
- }
27271
-
27272
- // src/shared/db/recordPhaseCycleContext.ts
27273
- import { sql as sql21 } from "drizzle-orm";
27274
- async function recordPhaseCycleContext(db, itemId2, phaseIdx, window, resetsAt, pct) {
27275
- if (pct <= 0) return;
27276
- await db.insert(phaseCycleContext).values({ itemId: itemId2, phaseIdx, window, resetsAt, peakContextPct: pct }).onConflictDoUpdate({
27277
- target: [
27278
- phaseCycleContext.itemId,
27279
- phaseCycleContext.phaseIdx,
27280
- phaseCycleContext.window,
27281
- phaseCycleContext.resetsAt
27282
- ],
27283
- set: {
27284
- peakContextPct: sql21`GREATEST(${phaseCycleContext.peakContextPct}, ${pct})`
27285
- }
27286
- });
27332
+ // src/commands/sessions/daemon/logSpawnedSession.ts
27333
+ function logSpawnedSession(session) {
27334
+ daemonLog(
27335
+ `session ${session.id} spawned: ${session.name} [${session.commandType}] ${session.cwd ?? ""}`
27336
+ );
27287
27337
  }
27288
27338
 
27289
- // src/shared/db/recordPhasePeakContext.ts
27290
- import { sql as sql22 } from "drizzle-orm";
27291
- async function recordPhasePeakContext(db, itemId2, phaseIdx, pct) {
27292
- await db.insert(phaseUsage).values({ itemId: itemId2, phaseIdx, peakContextPct: pct }).onConflictDoUpdate({
27293
- target: [phaseUsage.itemId, phaseUsage.phaseIdx],
27294
- set: {
27295
- peakContextPct: sql22`GREATEST(${phaseUsage.peakContextPct}, ${pct})`
27296
- }
27297
- });
27339
+ // src/commands/sessions/daemon/emitSessionOutput.ts
27340
+ var MAX_SCROLLBACK = 256 * 1024;
27341
+ function emitSessionOutput(session, clients, data) {
27342
+ session.scrollback += data;
27343
+ if (session.scrollback.length > MAX_SCROLLBACK) {
27344
+ session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK);
27345
+ }
27346
+ broadcast(clients, { type: "output", sessionId: session.id, data });
27298
27347
  }
27299
27348
 
27300
- // src/shared/db/recordPhaseTranscriptUsage.ts
27301
- import { sql as sql23 } from "drizzle-orm";
27302
- async function recordPhaseTranscriptUsage(db, itemId2, phaseIdx, responses) {
27303
- const byId = /* @__PURE__ */ new Map();
27304
- for (const r of responses) byId.set(r.messageId, r);
27305
- const unique = [...byId.values()];
27306
- if (unique.length === 0) return { tokensUp: 0, tokensDown: 0 };
27307
- return db.transaction(async (tx) => {
27308
- const inserted = await tx.insert(phaseUsageMessages).values(unique.map((r) => ({ itemId: itemId2, phaseIdx, messageId: r.messageId }))).onConflictDoNothing().returning({ messageId: phaseUsageMessages.messageId });
27309
- let tokensUp = 0;
27310
- let tokensDown = 0;
27311
- for (const { messageId } of inserted) {
27312
- const r = byId.get(messageId);
27313
- if (!r) continue;
27314
- tokensUp += r.outputTokens;
27315
- tokensDown += r.inputTokens;
27316
- }
27317
- if (tokensUp <= 0 && tokensDown <= 0) return { tokensUp: 0, tokensDown: 0 };
27318
- await tx.insert(phaseUsage).values({ itemId: itemId2, phaseIdx, tokensUp, tokensDown }).onConflictDoUpdate({
27319
- target: [phaseUsage.itemId, phaseUsage.phaseIdx],
27320
- set: {
27321
- tokensUp: sql23`${phaseUsage.tokensUp} + ${tokensUp}`,
27322
- tokensDown: sql23`${phaseUsage.tokensDown} + ${tokensDown}`
27323
- }
27324
- });
27325
- return { tokensUp, tokensDown };
27326
- });
27349
+ // src/commands/sessions/daemon/exitReason.ts
27350
+ import { existsSync as existsSync64 } from "fs";
27351
+ function exitReason(session, exitCode) {
27352
+ const base = `process exited with code ${exitCode}`;
27353
+ if (session.cwd && !existsSync64(session.cwd))
27354
+ return `${base}: working directory ${session.cwd} no longer exists`;
27355
+ return base;
27327
27356
  }
27328
27357
 
27329
- // src/shared/db/recordWindowTokens.ts
27330
- import { and as and21, eq as eq40, sql as sql24 } from "drizzle-orm";
27331
- async function recordWindowTokens(db, window, resetsAt, tokensUp, tokensDown) {
27332
- if (tokensUp <= 0 && tokensDown <= 0) return;
27333
- await db.update(usagePeaks).set({
27334
- tokensUp: sql24`${usagePeaks.tokensUp} + ${tokensUp}`,
27335
- tokensDown: sql24`${usagePeaks.tokensDown} + ${tokensDown}`
27336
- }).where(
27337
- and21(
27338
- eq40(usagePeaks.window, window),
27339
- eq40(usagePeaks.resetsAt, resetsAt),
27340
- eq40(usagePeaks.segment, 0)
27341
- )
27358
+ // src/commands/sessions/daemon/handleFailedResume.ts
27359
+ function handleFailedResume(session, exitCode, onStatusChange) {
27360
+ const failed2 = session.restored === true && exitCode !== 0 && session.scrollback.length === 0;
27361
+ if (!failed2) return false;
27362
+ session.error = `resume process exited with code ${exitCode} before producing any output`;
27363
+ daemonLog(
27364
+ `could not resume restored session "${session.name}" (id ${session.id}): ${session.error}`
27342
27365
  );
27366
+ onStatusChange(session, "error", exitCode);
27367
+ return true;
27343
27368
  }
27344
27369
 
27345
- // src/commands/sessions/shared/transcriptUsage.ts
27346
- import * as fs36 from "fs";
27347
- function transcriptUsage(lines) {
27348
- const byId = /* @__PURE__ */ new Map();
27349
- for (const line of lines) {
27350
- if (!line.trim()) continue;
27351
- let entry;
27352
- try {
27353
- entry = JSON.parse(line);
27354
- } catch {
27355
- continue;
27356
- }
27357
- if (entry.type !== "assistant") continue;
27358
- const id = entry.message?.id;
27359
- const usage = entry.message?.usage;
27360
- if (!id || !usage) continue;
27361
- byId.set(id, {
27362
- messageId: id,
27363
- inputTokens: usage.input_tokens ?? 0,
27364
- outputTokens: usage.output_tokens ?? 0
27365
- });
27370
+ // src/commands/sessions/daemon/watchActivity.ts
27371
+ import { existsSync as existsSync65, mkdirSync as mkdirSync26, watch as watch2 } from "fs";
27372
+ import { dirname as dirname31 } from "path";
27373
+
27374
+ // src/commands/sessions/daemon/applyReviewPause.ts
27375
+ function applyReviewPause(session, activity2) {
27376
+ const inReview = activity2.kind === "backlog" && activity2.phase !== void 0 && activity2.phase === activity2.totalPhases;
27377
+ if (inReview && !session.reviewStarted) {
27378
+ session.autoAdvance = false;
27379
+ session.reviewStarted = true;
27380
+ } else if (!inReview && session.reviewStarted) {
27381
+ session.reviewStarted = false;
27366
27382
  }
27367
- return [...byId.values()];
27368
27383
  }
27369
- async function readTranscriptUsage(transcriptPath) {
27370
- const content = await fs36.promises.readFile(transcriptPath, "utf8");
27371
- return transcriptUsage(content.split("\n"));
27384
+
27385
+ // src/shared/db/getPhaseActiveMs.ts
27386
+ import { and as and22, eq as eq42 } from "drizzle-orm";
27387
+ async function getPhaseActiveMs(db, itemId2, phaseIdx) {
27388
+ const [row] = await db.select({ activeMs: phaseUsage.activeMs }).from(phaseUsage).where(
27389
+ and22(eq42(phaseUsage.itemId, itemId2), eq42(phaseUsage.phaseIdx, phaseIdx))
27390
+ );
27391
+ return row?.activeMs ?? 0;
27372
27392
  }
27373
27393
 
27374
- // src/commands/sessions/daemon/persistPhaseTokens.ts
27375
- async function persistPhaseTokens(itemId2, phaseIdx, transcriptPath, usedPct, windows) {
27394
+ // src/commands/sessions/daemon/loadPhaseActiveMs.ts
27395
+ async function loadPhaseActiveMs(itemId2, phaseIdx) {
27376
27396
  try {
27377
- if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url) return;
27378
- const db = await getDb();
27379
- if (usedPct !== void 0) {
27380
- await recordPhasePeakContext(db, itemId2, phaseIdx, usedPct);
27381
- for (const { window, resetsAt } of windows) {
27382
- await recordPhaseCycleContext(
27383
- db,
27384
- itemId2,
27385
- phaseIdx,
27386
- window,
27387
- resetsAt,
27388
- usedPct
27389
- );
27390
- }
27391
- }
27392
- const responses = await readTranscriptUsage(transcriptPath);
27393
- if (responses.length === 0) return;
27394
- const { tokensUp, tokensDown } = await recordPhaseTranscriptUsage(
27395
- db,
27396
- itemId2,
27397
- phaseIdx,
27398
- responses
27399
- );
27400
- for (const { window, resetsAt } of windows) {
27401
- await recordWindowTokens(db, window, resetsAt, tokensUp, tokensDown);
27402
- }
27397
+ if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url)
27398
+ return null;
27399
+ return await getPhaseActiveMs(await getDb(), itemId2, phaseIdx);
27403
27400
  } catch (error) {
27404
- daemonLog(`phase-tokens persist failed: ${String(error)}`);
27401
+ daemonLog(`phase-active-ms load failed: ${String(error)}`);
27402
+ return null;
27405
27403
  }
27406
27404
  }
27407
27405
 
27408
- // src/commands/sessions/daemon/recordSessionUsage.ts
27409
- function recordSessionUsage(sessions, claudeSessionId, transcriptPath, usedPct, windows) {
27410
- for (const session of sessions) {
27411
- if (session.claudeSessionId !== claudeSessionId) continue;
27412
- session.usedPct = usedPct;
27413
- const phase = sessionBacklogPhase(session);
27414
- if (phase && transcriptPath)
27415
- void persistPhaseTokens(
27416
- phase.itemId,
27417
- phase.phaseIdx,
27418
- transcriptPath,
27419
- usedPct,
27420
- windows
27421
- );
27422
- void flushPhaseActiveMs(session);
27423
- return true;
27424
- }
27425
- return false;
27426
- }
27427
-
27428
- // src/commands/sessions/daemon/applyUsageRecord.ts
27429
- function applyUsageRecord(sessions, windows, notify2, claudeSessionId, transcriptPath, usedPct) {
27430
- if (recordSessionUsage(
27431
- sessions,
27432
- claudeSessionId,
27433
- transcriptPath,
27434
- usedPct,
27435
- windows
27436
- ))
27437
- notify2();
27438
- }
27439
-
27440
- // src/commands/sessions/daemon/shouldAutoDismiss.ts
27441
- function shouldAutoDismiss(session, exitCode) {
27442
- if (session.status !== "done" || exitCode !== 0) {
27443
- return false;
27444
- }
27445
- if (session.reviewStarted) {
27446
- return session.autoAdvance === true;
27447
- }
27448
- const args = session.assistArgs;
27449
- if (args === void 0) return false;
27450
- if (args[0] === "update") return true;
27451
- return args.includes("--once") && args[0] !== "next";
27452
- }
27453
-
27454
- // src/commands/sessions/daemon/shouldAutoRun.ts
27455
- function shouldAutoRun(session, status3 = session.status) {
27456
- if (!session.autoRun) return { run: false, reason: null };
27457
- if (status3 !== "done")
27458
- return {
27459
- run: false,
27460
- reason: `session status is "${status3}", not "done"`
27461
- };
27462
- if (session.commandType !== "assist")
27463
- return {
27464
- run: false,
27465
- reason: `command type is "${session.commandType}", not "assist"`
27466
- };
27467
- const cmd = session.assistArgs?.[0];
27468
- if (cmd !== "draft" && cmd !== "bug" && cmd !== "refine")
27469
- return {
27470
- run: false,
27471
- reason: `command "${cmd ?? "(none)"}" is not draft/bug/refine`
27472
- };
27473
- const itemId2 = session.activity?.itemId;
27474
- if (itemId2 == null)
27475
- return {
27476
- run: false,
27477
- reason: "no itemId in activity (draft did not create an item)"
27478
- };
27479
- return { run: true, itemId: itemId2 };
27480
- }
27481
-
27482
- // src/commands/sessions/daemon/applyStatusChange.ts
27483
- function applyStatusChange(session, status3, exitCode, dismiss, notify2, reuseForRun) {
27484
- if (session.status === status3) return;
27485
- const deps2 = { dismiss, notify: notify2, reuseForRun };
27486
- const chainsToNextRun = shouldAutoRun(session, status3).run;
27487
- if (status3 === "done" && session.worktree && !chainsToNextRun) {
27488
- session.closing = true;
27489
- daemonLog(
27490
- `session ${session.id} closing: checking durability of ${session.worktree.path} before reap`
27491
- );
27492
- notify2();
27493
- void resolveCloseDurability(
27494
- session,
27495
- () => finishStatusChange(session, "done", exitCode, deps2),
27496
- notify2
27497
- );
27498
- return;
27499
- }
27500
- finishStatusChange(session, status3, exitCode, deps2);
27501
- }
27502
- function finishStatusChange(session, status3, exitCode, deps2) {
27503
- daemonLog(`session ${session.id} status: ${session.status} -> ${status3}`);
27504
- void flushPhaseActiveMs(session);
27505
- setStatus2(session, status3);
27506
- if (status3 !== "stopped") session.undurable = void 0;
27507
- const autoRun = shouldAutoRun(session);
27508
- if (autoRun.run) {
27509
- deps2.reuseForRun(session, autoRun.itemId);
27510
- deps2.notify();
27511
- return;
27512
- }
27513
- if (status3 === "done" && autoRun.reason != null) {
27514
- daemonLog(
27515
- `session ${session.id} auto-run enabled but skipped (exit ${exitCode ?? "none"}): ${autoRun.reason}`
27516
- );
27517
- }
27518
- if (shouldAutoDismiss(session, exitCode)) deps2.dismiss(session.id);
27519
- else deps2.notify();
27520
- }
27521
-
27522
- // src/commands/sessions/daemon/makeStatusChangeHandler.ts
27523
- function makeStatusChangeHandler(dismiss, notify2, reuseForRun) {
27524
- return (s, status3, exitCode) => applyStatusChange(s, status3, exitCode, dismiss, notify2, reuseForRun);
27525
- }
27526
-
27527
- // src/commands/sessions/daemon/emitSessionOutput.ts
27528
- var MAX_SCROLLBACK = 256 * 1024;
27529
- function emitSessionOutput(session, clients, data) {
27530
- session.scrollback += data;
27531
- if (session.scrollback.length > MAX_SCROLLBACK) {
27532
- session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK);
27533
- }
27534
- broadcast(clients, { type: "output", sessionId: session.id, data });
27535
- }
27536
-
27537
- // src/commands/sessions/daemon/exitReason.ts
27538
- import { existsSync as existsSync64 } from "fs";
27539
- function exitReason(session, exitCode) {
27540
- const base = `process exited with code ${exitCode}`;
27541
- if (session.cwd && !existsSync64(session.cwd))
27542
- return `${base}: working directory ${session.cwd} no longer exists`;
27543
- return base;
27544
- }
27545
-
27546
- // src/commands/sessions/daemon/handleFailedResume.ts
27547
- function handleFailedResume(session, exitCode, onStatusChange) {
27548
- const failed2 = session.restored === true && exitCode !== 0 && session.scrollback.length === 0;
27549
- if (!failed2) return false;
27550
- session.error = `resume process exited with code ${exitCode} before producing any output`;
27551
- daemonLog(
27552
- `could not resume restored session "${session.name}" (id ${session.id}): ${session.error}`
27553
- );
27554
- onStatusChange(session, "error", exitCode);
27555
- return true;
27556
- }
27557
-
27558
- // src/commands/sessions/daemon/watchActivity.ts
27559
- import { existsSync as existsSync65, mkdirSync as mkdirSync26, watch as watch2 } from "fs";
27560
- import { dirname as dirname31 } from "path";
27561
-
27562
- // src/commands/sessions/daemon/applyReviewPause.ts
27563
- function applyReviewPause(session, activity2) {
27564
- const inReview = activity2.kind === "backlog" && activity2.phase !== void 0 && activity2.phase === activity2.totalPhases;
27565
- if (inReview && !session.reviewStarted) {
27566
- session.autoAdvance = false;
27567
- session.reviewStarted = true;
27568
- } else if (!inReview && session.reviewStarted) {
27569
- session.reviewStarted = false;
27570
- }
27571
- }
27572
-
27573
- // src/shared/db/getPhaseActiveMs.ts
27574
- import { and as and22, eq as eq41 } from "drizzle-orm";
27575
- async function getPhaseActiveMs(db, itemId2, phaseIdx) {
27576
- const [row] = await db.select({ activeMs: phaseUsage.activeMs }).from(phaseUsage).where(
27577
- and22(eq41(phaseUsage.itemId, itemId2), eq41(phaseUsage.phaseIdx, phaseIdx))
27578
- );
27579
- return row?.activeMs ?? 0;
27580
- }
27581
-
27582
- // src/commands/sessions/daemon/loadPhaseActiveMs.ts
27583
- async function loadPhaseActiveMs(itemId2, phaseIdx) {
27584
- try {
27585
- if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url)
27586
- return null;
27587
- return await getPhaseActiveMs(await getDb(), itemId2, phaseIdx);
27588
- } catch (error) {
27589
- daemonLog(`phase-active-ms load failed: ${String(error)}`);
27590
- return null;
27591
- }
27592
- }
27593
-
27594
- // src/commands/sessions/daemon/seedRunningMsFromUsage.ts
27595
- async function seedRunningMsFromUsage(session, notify2) {
27596
- if (session.usageSeeded) return;
27597
- const phase = sessionBacklogPhase(session);
27598
- if (!phase) return;
27599
- session.usageSeeded = true;
27600
- const priorMs = await loadPhaseActiveMs(phase.itemId, phase.phaseIdx);
27601
- if (priorMs == null || priorMs <= session.runningMs) return;
27602
- session.runningMs = priorMs;
27603
- daemonLog(
27604
- `session ${session.id} seeded runningMs=${priorMs} from item ${phase.itemId} phase ${phase.phaseIdx} recorded usage`
27605
- );
27606
- notify2();
27406
+ // src/commands/sessions/daemon/seedRunningMsFromUsage.ts
27407
+ async function seedRunningMsFromUsage(session, notify2) {
27408
+ if (session.usageSeeded) return;
27409
+ const phase = sessionBacklogPhase(session);
27410
+ if (!phase) return;
27411
+ session.usageSeeded = true;
27412
+ const priorMs = await loadPhaseActiveMs(phase.itemId, phase.phaseIdx);
27413
+ if (priorMs == null || priorMs <= session.runningMs) return;
27414
+ session.runningMs = priorMs;
27415
+ daemonLog(
27416
+ `session ${session.id} seeded runningMs=${priorMs} from item ${phase.itemId} phase ${phase.phaseIdx} recorded usage`
27417
+ );
27418
+ notify2();
27607
27419
  }
27608
27420
 
27609
27421
  // src/commands/sessions/daemon/watchActivity.ts
@@ -27721,163 +27533,53 @@ function reportSilentFailure(session, clients) {
27721
27533
  );
27722
27534
  }
27723
27535
 
27724
- // src/commands/sessions/daemon/respawnSession.ts
27725
- function respawnSession(session, respawn, status3, clients, onStatusChange) {
27726
- session.gitWatcher?.close();
27727
- session.gitWatcher = void 0;
27728
- session.undurable = void 0;
27729
- session.pendingDismiss = void 0;
27730
- session.closing = void 0;
27731
- session.scrollback = "";
27732
- session.startedAt = Date.now();
27733
- session.runningMs = 0;
27734
- session.runningSince = null;
27735
- setStatus2(session, status3);
27736
- session.restored = void 0;
27737
- session.pty = respawn();
27738
- if (session.cols && session.rows)
27739
- try {
27740
- session.pty?.resize(session.cols, session.rows);
27741
- } catch {
27742
- }
27743
- broadcast(clients, { type: "clear", sessionId: session.id });
27744
- wirePtyEvents(session, clients, onStatusChange);
27745
- }
27746
-
27747
- // src/commands/sessions/daemon/respawnPlan.ts
27748
- import { randomUUID as randomUUID7 } from "crypto";
27536
+ // src/commands/sessions/daemon/watchTranscript.ts
27537
+ import { existsSync as existsSync67, watch as watch3 } from "fs";
27749
27538
 
27750
- // src/commands/sessions/daemon/assistResumeArgs.ts
27751
- function assistResumeArgs(session) {
27752
- const args = ["assist", ...session.assistArgs];
27753
- return session.claudeSessionId ? [...args, "--resume-session", session.claudeSessionId] : args;
27539
+ // src/commands/sessions/shared/findTranscriptPathSync.ts
27540
+ import * as fs36 from "fs";
27541
+ import * as os4 from "os";
27542
+ import * as path60 from "path";
27543
+ function projectDirForCwd(cwd) {
27544
+ return path60.join(
27545
+ os4.homedir(),
27546
+ ".claude",
27547
+ "projects",
27548
+ cwd.replace(/[^a-zA-Z0-9]/g, "-")
27549
+ );
27754
27550
  }
27755
-
27756
- // src/commands/sessions/daemon/respawnPlan.ts
27757
- function respawnPlan(session) {
27758
- const { commandType, claudeSessionId, cwd, assistArgs, initialPrompt } = session;
27759
- if (commandType === "claude") {
27760
- if (claudeSessionId)
27761
- return {
27762
- spawn: () => spawnClaude2({
27763
- resumeSessionId: claudeSessionId,
27764
- cwd,
27765
- sessionId: session.id
27766
- }),
27767
- status: "waiting"
27768
- };
27769
- if (initialPrompt) return freshClaudePlan(session, initialPrompt, cwd);
27770
- if (cwd && session.harness !== "pi")
27771
- return freshClaudePlan(session, void 0, cwd);
27551
+ function transcriptPathFor(cwd, claudeSessionId) {
27552
+ return path60.join(projectDirForCwd(cwd), `${claudeSessionId}.jsonl`);
27553
+ }
27554
+ function findTranscriptPathSync(cwd, claudeSessionId) {
27555
+ const direct = transcriptPathFor(cwd, claudeSessionId);
27556
+ if (fs36.existsSync(direct)) return direct;
27557
+ const dir = projectDirForCwd(cwd);
27558
+ let files;
27559
+ try {
27560
+ files = fs36.readdirSync(dir);
27561
+ } catch {
27772
27562
  return null;
27773
27563
  }
27774
- if (commandType === "assist" && assistArgs) {
27775
- const idle = session.status === "waiting";
27776
- return {
27777
- spawn: () => spawnPty(
27778
- assistResumeArgs({ assistArgs, claudeSessionId }),
27779
- cwd,
27780
- session.id,
27781
- idle ? { ASSIST_RESUME_IDLE: "1" } : void 0
27782
- ),
27783
- status: idle ? "waiting" : "running"
27784
- };
27564
+ for (const file of files) {
27565
+ if (!file.endsWith(".jsonl")) continue;
27566
+ if (headContainsSessionId(path60.join(dir, file), claudeSessionId))
27567
+ return path60.join(dir, file);
27785
27568
  }
27786
27569
  return null;
27787
27570
  }
27788
- function freshClaudePlan(session, prompt, cwd) {
27789
- const claudeSessionId = randomUUID7();
27790
- return {
27791
- spawn: () => {
27792
- session.claudeSessionId = claudeSessionId;
27793
- return spawnClaude2({
27794
- prompt,
27795
- cwd,
27796
- sessionId: session.id,
27797
- claudeSessionId
27798
- });
27799
- },
27800
- status: prompt ? "running" : "waiting"
27801
- };
27802
- }
27803
-
27804
- // src/commands/sessions/daemon/restartSession.ts
27805
- function restartSession(session, clients, onStatusChange) {
27806
- const plan2 = respawnPlan(session);
27807
- if (!plan2) return false;
27808
- const via = session.claudeSessionId ? `resume ${session.claudeSessionId}` : "fresh";
27809
- const respawn = () => {
27810
- respawnSession(session, plan2.spawn, plan2.status, clients, onStatusChange);
27811
- daemonLog(`session ${session.id} restarted (${via}): ${session.name}`);
27812
- };
27813
- const pty2 = session.pty;
27814
- const alive = session.status === "running" || session.status === "waiting";
27815
- if (pty2 && alive) {
27816
- daemonLog(
27817
- `session ${session.id} restart requested: killing running process, will resume on exit`
27818
- );
27819
- session.pendingRestart = respawn;
27820
- killPtyTree(pty2);
27821
- } else {
27822
- respawn();
27823
- }
27824
- return true;
27825
- }
27826
-
27827
- // src/commands/sessions/daemon/restartManagedSession.ts
27828
- function restartManagedSession(sessions, id, clients, onStatusChange) {
27829
- const s = sessions.get(id);
27830
- if (!s) {
27831
- daemonLog(
27832
- `restart requested for unknown session id=${id} (no live session; ignoring)`
27833
- );
27834
- return {
27835
- ok: false,
27836
- reason: `Session ${id} is no longer tracked and can't be restarted.`
27837
- };
27838
- }
27839
- daemonLog(`restart requested: id=${id} status=${s.status} (${s.name})`);
27840
- if (!restartSession(s, clients, onStatusChange)) {
27841
- daemonLog(`restart for session ${id} did nothing: no respawn plan`);
27842
- return {
27843
- ok: false,
27844
- reason: `Session ${s.name} can't be restarted (no resumable command).`
27845
- };
27571
+ function headContainsSessionId(filePath, claudeSessionId) {
27572
+ let fd;
27573
+ try {
27574
+ fd = fs36.openSync(filePath, "r");
27575
+ const buf = Buffer.alloc(16384);
27576
+ const bytesRead = fs36.readSync(fd, buf, 0, buf.length, 0);
27577
+ return buf.toString("utf8", 0, bytesRead).includes(claudeSessionId);
27578
+ } catch {
27579
+ return false;
27580
+ } finally {
27581
+ if (fd !== void 0) fs36.closeSync(fd);
27846
27582
  }
27847
- return { ok: true };
27848
- }
27849
-
27850
- // src/commands/sessions/daemon/restoreBase.ts
27851
- function restoreBase(id, persisted) {
27852
- return {
27853
- id,
27854
- name: persisted.name,
27855
- title: persisted.title,
27856
- subtitle: persisted.subtitle,
27857
- commandType: persisted.commandType,
27858
- harness: persisted.harness,
27859
- scrollback: "",
27860
- cwd: persisted.cwd,
27861
- assistArgs: persisted.assistArgs,
27862
- initialPrompt: persisted.initialPrompt,
27863
- starred: persisted.starred
27864
- };
27865
- }
27866
-
27867
- // src/commands/sessions/daemon/errorSession.ts
27868
- function errorSession(id, persisted, error) {
27869
- return {
27870
- ...restoreBase(id, persisted),
27871
- status: "error",
27872
- startedAt: persisted.startedAt,
27873
- runningMs: persisted.runningMs ?? 0,
27874
- runningSince: null,
27875
- pty: null,
27876
- runName: persisted.runName,
27877
- runArgs: persisted.runArgs,
27878
- error,
27879
- restored: false
27880
- };
27881
27583
  }
27882
27584
 
27883
27585
  // src/commands/sessions/shared/normalizeUser.ts
@@ -27994,94 +27696,579 @@ function parseTailEntries(raw) {
27994
27696
  }
27995
27697
  return entries;
27996
27698
  }
27997
- function dropPartialFirstLine(raw, sliced) {
27998
- if (!sliced) return raw;
27999
- const newline = raw.indexOf("\n");
28000
- return newline === -1 ? "" : raw.slice(newline + 1);
27699
+ function dropPartialFirstLine(raw, sliced) {
27700
+ if (!sliced) return raw;
27701
+ const newline = raw.indexOf("\n");
27702
+ return newline === -1 ? "" : raw.slice(newline + 1);
27703
+ }
27704
+ async function readTranscriptTail(filePath, maxBytes = DEFAULT_MAX_BYTES) {
27705
+ let handle;
27706
+ try {
27707
+ handle = await fs37.promises.open(filePath, "r");
27708
+ const { size } = await handle.stat();
27709
+ const start3 = Math.max(0, size - maxBytes);
27710
+ const length = size - start3;
27711
+ if (length === 0) return [];
27712
+ const buf = Buffer.alloc(length);
27713
+ await handle.read(buf, 0, length, start3);
27714
+ return parseTailEntries(
27715
+ dropPartialFirstLine(buf.toString("utf8"), start3 > 0)
27716
+ );
27717
+ } catch {
27718
+ return [];
27719
+ } finally {
27720
+ await handle?.close();
27721
+ }
27722
+ }
27723
+ function readTranscriptTailSync(filePath, maxBytes = DEFAULT_MAX_BYTES) {
27724
+ let fd;
27725
+ try {
27726
+ fd = fs37.openSync(filePath, "r");
27727
+ const { size } = fs37.fstatSync(fd);
27728
+ const start3 = Math.max(0, size - maxBytes);
27729
+ const length = size - start3;
27730
+ if (length === 0) return [];
27731
+ const buf = Buffer.alloc(length);
27732
+ fs37.readSync(fd, buf, 0, length, start3);
27733
+ return parseTailEntries(
27734
+ dropPartialFirstLine(buf.toString("utf8"), start3 > 0)
27735
+ );
27736
+ } catch {
27737
+ return [];
27738
+ } finally {
27739
+ if (fd !== void 0) fs37.closeSync(fd);
27740
+ }
27741
+ }
27742
+
27743
+ // src/commands/sessions/daemon/reconcileTranscriptStatus.ts
27744
+ async function resolveTranscriptPath(session) {
27745
+ if (session.transcriptPath) return session.transcriptPath;
27746
+ if (!session.claudeSessionId) return null;
27747
+ const direct = session.cwd ? findTranscriptPathSync(session.cwd, session.claudeSessionId) : null;
27748
+ const filePath = direct ?? await findSessionJsonlPath(session.claudeSessionId);
27749
+ if (filePath) session.transcriptPath = filePath;
27750
+ return filePath;
27751
+ }
27752
+ async function reconcileTranscriptStatus(session, onStatusChange) {
27753
+ if (session.status === "done" || session.status === "error" || session.status === "stopped")
27754
+ return;
27755
+ const filePath = await resolveTranscriptPath(session);
27756
+ if (!filePath) return;
27757
+ const entries = await readTranscriptTail(filePath);
27758
+ const derived = deriveTranscriptStatus(entries, {
27759
+ permissionActive: session.permissionActive
27760
+ });
27761
+ if (!derived) return;
27762
+ if (derived === "running") session.permissionActive = false;
27763
+ if (derived === session.status) return;
27764
+ daemonLog(
27765
+ `session ${session.id} transcript reconcile: ${session.status} -> ${derived}`
27766
+ );
27767
+ onStatusChange(session, derived);
27768
+ }
27769
+
27770
+ // src/commands/sessions/daemon/watchTranscript.ts
27771
+ var DEBOUNCE_MS3 = 100;
27772
+ function watchTranscript(session, onStatusChange) {
27773
+ if (!session.cwd || !session.claudeSessionId) return;
27774
+ if (session.watchedTranscriptId === session.claudeSessionId) return;
27775
+ const dir = projectDirForCwd(session.cwd);
27776
+ if (!existsSync67(dir)) return;
27777
+ if (session.transcriptWatcher) {
27778
+ session.transcriptWatcher.close();
27779
+ session.transcriptWatcher = void 0;
27780
+ }
27781
+ session.transcriptPath = void 0;
27782
+ const fileName = `${session.claudeSessionId}.jsonl`;
27783
+ let timer = null;
27784
+ const run4 = () => {
27785
+ timer = null;
27786
+ void reconcileTranscriptStatus(session, onStatusChange);
27787
+ };
27788
+ try {
27789
+ session.transcriptWatcher = watch3(dir, (_event, changed) => {
27790
+ if (changed && changed !== fileName) return;
27791
+ if (timer) clearTimeout(timer);
27792
+ timer = setTimeout(run4, DEBOUNCE_MS3);
27793
+ });
27794
+ } catch (error) {
27795
+ daemonLog(
27796
+ `session ${session.id} transcript watch failed: ${error instanceof Error ? error.message : String(error)}`
27797
+ );
27798
+ return;
27799
+ }
27800
+ session.watchedTranscriptId = session.claudeSessionId;
27801
+ daemonLog(`session ${session.id} watching transcript ${fileName}`);
27802
+ run4();
27803
+ }
27804
+
27805
+ // src/commands/sessions/daemon/wireSessionWatchers.ts
27806
+ function wireSessionWatchers(session, notify2, onStatusChange) {
27807
+ const wireTranscript = (s) => watchTranscript(s, onStatusChange);
27808
+ watchActivity(session, notify2, wireTranscript);
27809
+ wireTranscript(session);
27810
+ }
27811
+
27812
+ // src/commands/sessions/daemon/registerSpawnedSession.ts
27813
+ function registerSpawnedSession(session, sessions, clients, onStatusChange, notify2) {
27814
+ sessions.set(session.id, session);
27815
+ wirePtyEvents(session, clients, onStatusChange);
27816
+ notify2();
27817
+ wireSessionWatchers(session, notify2, onStatusChange);
27818
+ logSpawnedSession(session);
27819
+ return session.id;
27820
+ }
27821
+
27822
+ // src/commands/sessions/daemon/applySetStatus.ts
27823
+ function applySetStatus(sessions, id, status3, source, onStatusChange) {
27824
+ const session = sessions.get(id);
27825
+ if (!session) {
27826
+ daemonLog(
27827
+ `set-status for unknown session id=${id} status=${status3} (no live session; ignoring)`
27828
+ );
27829
+ return;
27830
+ }
27831
+ if (session.status === "stopped") {
27832
+ daemonLog(
27833
+ `set-status for stopped session id=${id} status=${status3} (held pending durable close; ignoring)`
27834
+ );
27835
+ return;
27836
+ }
27837
+ if (source === "permission") session.permissionActive = true;
27838
+ if (status3 === "running") session.permissionActive = false;
27839
+ onStatusChange(session, status3);
27840
+ }
27841
+
27842
+ // src/shared/db/recordPhaseCycleContext.ts
27843
+ import { sql as sql21 } from "drizzle-orm";
27844
+ async function recordPhaseCycleContext(db, itemId2, phaseIdx, window, resetsAt, pct) {
27845
+ if (pct <= 0) return;
27846
+ await db.insert(phaseCycleContext).values({ itemId: itemId2, phaseIdx, window, resetsAt, peakContextPct: pct }).onConflictDoUpdate({
27847
+ target: [
27848
+ phaseCycleContext.itemId,
27849
+ phaseCycleContext.phaseIdx,
27850
+ phaseCycleContext.window,
27851
+ phaseCycleContext.resetsAt
27852
+ ],
27853
+ set: {
27854
+ peakContextPct: sql21`GREATEST(${phaseCycleContext.peakContextPct}, ${pct})`
27855
+ }
27856
+ });
27857
+ }
27858
+
27859
+ // src/shared/db/recordPhasePeakContext.ts
27860
+ import { sql as sql22 } from "drizzle-orm";
27861
+ async function recordPhasePeakContext(db, itemId2, phaseIdx, pct) {
27862
+ await db.insert(phaseUsage).values({ itemId: itemId2, phaseIdx, peakContextPct: pct }).onConflictDoUpdate({
27863
+ target: [phaseUsage.itemId, phaseUsage.phaseIdx],
27864
+ set: {
27865
+ peakContextPct: sql22`GREATEST(${phaseUsage.peakContextPct}, ${pct})`
27866
+ }
27867
+ });
27868
+ }
27869
+
27870
+ // src/shared/db/recordPhaseTranscriptUsage.ts
27871
+ import { sql as sql23 } from "drizzle-orm";
27872
+ async function recordPhaseTranscriptUsage(db, itemId2, phaseIdx, responses) {
27873
+ const byId = /* @__PURE__ */ new Map();
27874
+ for (const r of responses) byId.set(r.messageId, r);
27875
+ const unique = [...byId.values()];
27876
+ if (unique.length === 0) return { tokensUp: 0, tokensDown: 0 };
27877
+ return db.transaction(async (tx) => {
27878
+ const inserted = await tx.insert(phaseUsageMessages).values(unique.map((r) => ({ itemId: itemId2, phaseIdx, messageId: r.messageId }))).onConflictDoNothing().returning({ messageId: phaseUsageMessages.messageId });
27879
+ let tokensUp = 0;
27880
+ let tokensDown = 0;
27881
+ for (const { messageId } of inserted) {
27882
+ const r = byId.get(messageId);
27883
+ if (!r) continue;
27884
+ tokensUp += r.outputTokens;
27885
+ tokensDown += r.inputTokens;
27886
+ }
27887
+ if (tokensUp <= 0 && tokensDown <= 0) return { tokensUp: 0, tokensDown: 0 };
27888
+ await tx.insert(phaseUsage).values({ itemId: itemId2, phaseIdx, tokensUp, tokensDown }).onConflictDoUpdate({
27889
+ target: [phaseUsage.itemId, phaseUsage.phaseIdx],
27890
+ set: {
27891
+ tokensUp: sql23`${phaseUsage.tokensUp} + ${tokensUp}`,
27892
+ tokensDown: sql23`${phaseUsage.tokensDown} + ${tokensDown}`
27893
+ }
27894
+ });
27895
+ return { tokensUp, tokensDown };
27896
+ });
27897
+ }
27898
+
27899
+ // src/shared/db/recordWindowTokens.ts
27900
+ import { and as and23, eq as eq43, sql as sql24 } from "drizzle-orm";
27901
+ async function recordWindowTokens(db, window, resetsAt, tokensUp, tokensDown) {
27902
+ if (tokensUp <= 0 && tokensDown <= 0) return;
27903
+ await db.update(usagePeaks).set({
27904
+ tokensUp: sql24`${usagePeaks.tokensUp} + ${tokensUp}`,
27905
+ tokensDown: sql24`${usagePeaks.tokensDown} + ${tokensDown}`
27906
+ }).where(
27907
+ and23(
27908
+ eq43(usagePeaks.window, window),
27909
+ eq43(usagePeaks.resetsAt, resetsAt),
27910
+ eq43(usagePeaks.segment, 0)
27911
+ )
27912
+ );
27913
+ }
27914
+
27915
+ // src/commands/sessions/shared/transcriptUsage.ts
27916
+ import * as fs38 from "fs";
27917
+ function transcriptUsage(lines) {
27918
+ const byId = /* @__PURE__ */ new Map();
27919
+ for (const line of lines) {
27920
+ if (!line.trim()) continue;
27921
+ let entry;
27922
+ try {
27923
+ entry = JSON.parse(line);
27924
+ } catch {
27925
+ continue;
27926
+ }
27927
+ if (entry.type !== "assistant") continue;
27928
+ const id = entry.message?.id;
27929
+ const usage = entry.message?.usage;
27930
+ if (!id || !usage) continue;
27931
+ byId.set(id, {
27932
+ messageId: id,
27933
+ inputTokens: usage.input_tokens ?? 0,
27934
+ outputTokens: usage.output_tokens ?? 0
27935
+ });
27936
+ }
27937
+ return [...byId.values()];
27938
+ }
27939
+ async function readTranscriptUsage(transcriptPath) {
27940
+ const content = await fs38.promises.readFile(transcriptPath, "utf8");
27941
+ return transcriptUsage(content.split("\n"));
27942
+ }
27943
+
27944
+ // src/commands/sessions/daemon/persistPhaseTokens.ts
27945
+ async function persistPhaseTokens(itemId2, phaseIdx, transcriptPath, usedPct, windows) {
27946
+ try {
27947
+ if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url) return;
27948
+ const db = await getDb();
27949
+ if (usedPct !== void 0) {
27950
+ await recordPhasePeakContext(db, itemId2, phaseIdx, usedPct);
27951
+ for (const { window, resetsAt } of windows) {
27952
+ await recordPhaseCycleContext(
27953
+ db,
27954
+ itemId2,
27955
+ phaseIdx,
27956
+ window,
27957
+ resetsAt,
27958
+ usedPct
27959
+ );
27960
+ }
27961
+ }
27962
+ const responses = await readTranscriptUsage(transcriptPath);
27963
+ if (responses.length === 0) return;
27964
+ const { tokensUp, tokensDown } = await recordPhaseTranscriptUsage(
27965
+ db,
27966
+ itemId2,
27967
+ phaseIdx,
27968
+ responses
27969
+ );
27970
+ for (const { window, resetsAt } of windows) {
27971
+ await recordWindowTokens(db, window, resetsAt, tokensUp, tokensDown);
27972
+ }
27973
+ } catch (error) {
27974
+ daemonLog(`phase-tokens persist failed: ${String(error)}`);
27975
+ }
27976
+ }
27977
+
27978
+ // src/commands/sessions/daemon/recordSessionUsage.ts
27979
+ function recordSessionUsage(sessions, claudeSessionId, transcriptPath, usedPct, windows) {
27980
+ for (const session of sessions) {
27981
+ if (session.claudeSessionId !== claudeSessionId) continue;
27982
+ session.usedPct = usedPct;
27983
+ const phase = sessionBacklogPhase(session);
27984
+ if (phase && transcriptPath)
27985
+ void persistPhaseTokens(
27986
+ phase.itemId,
27987
+ phase.phaseIdx,
27988
+ transcriptPath,
27989
+ usedPct,
27990
+ windows
27991
+ );
27992
+ void flushPhaseActiveMs(session);
27993
+ return true;
27994
+ }
27995
+ return false;
27996
+ }
27997
+
27998
+ // src/commands/sessions/daemon/applyUsageRecord.ts
27999
+ function applyUsageRecord(sessions, windows, notify2, claudeSessionId, transcriptPath, usedPct) {
28000
+ if (recordSessionUsage(
28001
+ sessions,
28002
+ claudeSessionId,
28003
+ transcriptPath,
28004
+ usedPct,
28005
+ windows
28006
+ ))
28007
+ notify2();
28008
+ }
28009
+
28010
+ // src/commands/sessions/daemon/shouldAutoRun.ts
28011
+ function shouldAutoRun(session, status3 = session.status) {
28012
+ if (!session.autoRun) return { run: false, reason: null };
28013
+ if (status3 !== "done")
28014
+ return {
28015
+ run: false,
28016
+ reason: `session status is "${status3}", not "done"`
28017
+ };
28018
+ if (session.commandType !== "assist")
28019
+ return {
28020
+ run: false,
28021
+ reason: `command type is "${session.commandType}", not "assist"`
28022
+ };
28023
+ const cmd = session.assistArgs?.[0];
28024
+ if (cmd !== "draft" && cmd !== "bug" && cmd !== "refine")
28025
+ return {
28026
+ run: false,
28027
+ reason: `command "${cmd ?? "(none)"}" is not draft/bug/refine`
28028
+ };
28029
+ const itemId2 = session.activity?.itemId;
28030
+ if (itemId2 == null)
28031
+ return {
28032
+ run: false,
28033
+ reason: "no itemId in activity (draft did not create an item)"
28034
+ };
28035
+ return { run: true, itemId: itemId2 };
28036
+ }
28037
+
28038
+ // src/commands/sessions/daemon/endsStreamLife.ts
28039
+ function endsStreamLife(session, status3, sharedWithOtherAgents) {
28040
+ if (status3 !== "done" || !session.worktree) return false;
28041
+ if (shouldAutoRun(session, status3).run) return false;
28042
+ if (!sharedWithOtherAgents(session)) return true;
28043
+ daemonLog(
28044
+ `session ${session.id} done in ${session.worktree.path}: workspace kept, other agents are still working there`
28045
+ );
28046
+ return false;
28047
+ }
28048
+
28049
+ // src/commands/sessions/daemon/shouldAutoDismiss.ts
28050
+ function shouldAutoDismiss(session, exitCode) {
28051
+ if (session.status !== "done" || exitCode !== 0) {
28052
+ return false;
28053
+ }
28054
+ if (session.reviewStarted) {
28055
+ return session.autoAdvance === true;
28056
+ }
28057
+ const args = session.assistArgs;
28058
+ if (args === void 0) return false;
28059
+ if (args[0] === "update") return true;
28060
+ return args.includes("--once") && args[0] !== "next";
28061
+ }
28062
+
28063
+ // src/commands/sessions/daemon/applyStatusChange.ts
28064
+ function applyStatusChange(session, status3, exitCode, dismiss, notify2, reuseForRun, sharedWithOtherAgents = () => false) {
28065
+ if (session.status === status3) return;
28066
+ const deps2 = { dismiss, notify: notify2, reuseForRun };
28067
+ if (endsStreamLife(session, status3, sharedWithOtherAgents)) {
28068
+ session.closing = true;
28069
+ daemonLog(
28070
+ `session ${session.id} closing: checking durability of ${session.worktree?.path} before reap`
28071
+ );
28072
+ notify2();
28073
+ void resolveCloseDurability(
28074
+ session,
28075
+ () => finishStatusChange(session, "done", exitCode, deps2),
28076
+ notify2
28077
+ );
28078
+ return;
28079
+ }
28080
+ finishStatusChange(session, status3, exitCode, deps2);
28081
+ }
28082
+ function finishStatusChange(session, status3, exitCode, deps2) {
28083
+ daemonLog(`session ${session.id} status: ${session.status} -> ${status3}`);
28084
+ void flushPhaseActiveMs(session);
28085
+ setStatus2(session, status3);
28086
+ if (status3 !== "stopped") session.undurable = void 0;
28087
+ const autoRun = shouldAutoRun(session);
28088
+ if (autoRun.run) {
28089
+ deps2.reuseForRun(session, autoRun.itemId);
28090
+ deps2.notify();
28091
+ return;
28092
+ }
28093
+ if (status3 === "done" && autoRun.reason != null) {
28094
+ daemonLog(
28095
+ `session ${session.id} auto-run enabled but skipped (exit ${exitCode ?? "none"}): ${autoRun.reason}`
28096
+ );
28097
+ }
28098
+ if (shouldAutoDismiss(session, exitCode)) deps2.dismiss(session.id);
28099
+ else deps2.notify();
28100
+ }
28101
+
28102
+ // src/commands/sessions/daemon/makeStatusChangeHandler.ts
28103
+ function makeStatusChangeHandler(sessions, dismiss, notify2, reuseForRun) {
28104
+ return (s, status3, exitCode) => applyStatusChange(
28105
+ s,
28106
+ status3,
28107
+ exitCode,
28108
+ dismiss,
28109
+ notify2,
28110
+ reuseForRun,
28111
+ (session) => otherTreeHolders(sessions, session).length > 0
28112
+ );
28113
+ }
28114
+
28115
+ // src/commands/sessions/daemon/respawnSession.ts
28116
+ function respawnSession(session, respawn, status3, clients, onStatusChange) {
28117
+ session.gitWatcher?.close();
28118
+ session.gitWatcher = void 0;
28119
+ session.undurable = void 0;
28120
+ session.pendingDismiss = void 0;
28121
+ session.closing = void 0;
28122
+ session.scrollback = "";
28123
+ session.startedAt = Date.now();
28124
+ session.runningMs = 0;
28125
+ session.runningSince = null;
28126
+ setStatus2(session, status3);
28127
+ session.restored = void 0;
28128
+ session.pty = respawn();
28129
+ if (session.cols && session.rows)
28130
+ try {
28131
+ session.pty?.resize(session.cols, session.rows);
28132
+ } catch {
28133
+ }
28134
+ broadcast(clients, { type: "clear", sessionId: session.id });
28135
+ wirePtyEvents(session, clients, onStatusChange);
28136
+ }
28137
+
28138
+ // src/commands/sessions/daemon/respawnPlan.ts
28139
+ import { randomUUID as randomUUID7 } from "crypto";
28140
+
28141
+ // src/commands/sessions/daemon/assistResumeArgs.ts
28142
+ function assistResumeArgs(session) {
28143
+ const args = ["assist", ...session.assistArgs];
28144
+ return session.claudeSessionId ? [...args, "--resume-session", session.claudeSessionId] : args;
28145
+ }
28146
+
28147
+ // src/commands/sessions/daemon/respawnPlan.ts
28148
+ function respawnPlan(session) {
28149
+ const { commandType, claudeSessionId, cwd, assistArgs, initialPrompt } = session;
28150
+ if (commandType === "claude") {
28151
+ if (claudeSessionId)
28152
+ return {
28153
+ spawn: () => spawnClaude2({
28154
+ resumeSessionId: claudeSessionId,
28155
+ cwd,
28156
+ sessionId: session.id
28157
+ }),
28158
+ status: "waiting"
28159
+ };
28160
+ if (initialPrompt) return freshClaudePlan(session, initialPrompt, cwd);
28161
+ if (cwd && session.harness !== "pi")
28162
+ return freshClaudePlan(session, void 0, cwd);
28163
+ return null;
28164
+ }
28165
+ if (commandType === "assist" && assistArgs) {
28166
+ const idle = session.status === "waiting";
28167
+ return {
28168
+ spawn: () => spawnPty(
28169
+ assistResumeArgs({ assistArgs, claudeSessionId }),
28170
+ cwd,
28171
+ session.id,
28172
+ idle ? { ASSIST_RESUME_IDLE: "1" } : void 0
28173
+ ),
28174
+ status: idle ? "waiting" : "running"
28175
+ };
28176
+ }
28177
+ return null;
28178
+ }
28179
+ function freshClaudePlan(session, prompt, cwd) {
28180
+ const claudeSessionId = randomUUID7();
28181
+ return {
28182
+ spawn: () => {
28183
+ session.claudeSessionId = claudeSessionId;
28184
+ return spawnClaude2({
28185
+ prompt,
28186
+ cwd,
28187
+ sessionId: session.id,
28188
+ claudeSessionId
28189
+ });
28190
+ },
28191
+ status: prompt ? "running" : "waiting"
28192
+ };
28001
28193
  }
28002
- async function readTranscriptTail(filePath, maxBytes = DEFAULT_MAX_BYTES) {
28003
- let handle;
28004
- try {
28005
- handle = await fs37.promises.open(filePath, "r");
28006
- const { size } = await handle.stat();
28007
- const start3 = Math.max(0, size - maxBytes);
28008
- const length = size - start3;
28009
- if (length === 0) return [];
28010
- const buf = Buffer.alloc(length);
28011
- await handle.read(buf, 0, length, start3);
28012
- return parseTailEntries(
28013
- dropPartialFirstLine(buf.toString("utf8"), start3 > 0)
28194
+
28195
+ // src/commands/sessions/daemon/restartSession.ts
28196
+ function restartSession(session, clients, onStatusChange) {
28197
+ const plan2 = respawnPlan(session);
28198
+ if (!plan2) return false;
28199
+ const via = session.claudeSessionId ? `resume ${session.claudeSessionId}` : "fresh";
28200
+ const respawn = () => {
28201
+ respawnSession(session, plan2.spawn, plan2.status, clients, onStatusChange);
28202
+ daemonLog(`session ${session.id} restarted (${via}): ${session.name}`);
28203
+ };
28204
+ const pty2 = session.pty;
28205
+ const alive = session.status === "running" || session.status === "waiting";
28206
+ if (pty2 && alive) {
28207
+ daemonLog(
28208
+ `session ${session.id} restart requested: killing running process, will resume on exit`
28014
28209
  );
28015
- } catch {
28016
- return [];
28017
- } finally {
28018
- await handle?.close();
28210
+ session.pendingRestart = respawn;
28211
+ killPtyTree(pty2);
28212
+ } else {
28213
+ respawn();
28019
28214
  }
28215
+ return true;
28020
28216
  }
28021
- function readTranscriptTailSync(filePath, maxBytes = DEFAULT_MAX_BYTES) {
28022
- let fd;
28023
- try {
28024
- fd = fs37.openSync(filePath, "r");
28025
- const { size } = fs37.fstatSync(fd);
28026
- const start3 = Math.max(0, size - maxBytes);
28027
- const length = size - start3;
28028
- if (length === 0) return [];
28029
- const buf = Buffer.alloc(length);
28030
- fs37.readSync(fd, buf, 0, length, start3);
28031
- return parseTailEntries(
28032
- dropPartialFirstLine(buf.toString("utf8"), start3 > 0)
28217
+
28218
+ // src/commands/sessions/daemon/restartManagedSession.ts
28219
+ function restartManagedSession(sessions, id, clients, onStatusChange) {
28220
+ const s = sessions.get(id);
28221
+ if (!s) {
28222
+ daemonLog(
28223
+ `restart requested for unknown session id=${id} (no live session; ignoring)`
28033
28224
  );
28034
- } catch {
28035
- return [];
28036
- } finally {
28037
- if (fd !== void 0) fs37.closeSync(fd);
28225
+ return {
28226
+ ok: false,
28227
+ reason: `Session ${id} is no longer tracked and can't be restarted.`
28228
+ };
28229
+ }
28230
+ daemonLog(`restart requested: id=${id} status=${s.status} (${s.name})`);
28231
+ if (!restartSession(s, clients, onStatusChange)) {
28232
+ daemonLog(`restart for session ${id} did nothing: no respawn plan`);
28233
+ return {
28234
+ ok: false,
28235
+ reason: `Session ${s.name} can't be restarted (no resumable command).`
28236
+ };
28038
28237
  }
28238
+ return { ok: true };
28039
28239
  }
28040
28240
 
28041
- // src/commands/sessions/shared/findTranscriptPathSync.ts
28042
- import * as fs38 from "fs";
28043
- import * as os4 from "os";
28044
- import * as path60 from "path";
28045
- function projectDirForCwd(cwd) {
28046
- return path60.join(
28047
- os4.homedir(),
28048
- ".claude",
28049
- "projects",
28050
- cwd.replace(/[^a-zA-Z0-9]/g, "-")
28051
- );
28052
- }
28053
- function transcriptPathFor(cwd, claudeSessionId) {
28054
- return path60.join(projectDirForCwd(cwd), `${claudeSessionId}.jsonl`);
28055
- }
28056
- function findTranscriptPathSync(cwd, claudeSessionId) {
28057
- const direct = transcriptPathFor(cwd, claudeSessionId);
28058
- if (fs38.existsSync(direct)) return direct;
28059
- const dir = projectDirForCwd(cwd);
28060
- let files;
28061
- try {
28062
- files = fs38.readdirSync(dir);
28063
- } catch {
28064
- return null;
28065
- }
28066
- for (const file of files) {
28067
- if (!file.endsWith(".jsonl")) continue;
28068
- if (headContainsSessionId(path60.join(dir, file), claudeSessionId))
28069
- return path60.join(dir, file);
28070
- }
28071
- return null;
28241
+ // src/commands/sessions/daemon/restoreBase.ts
28242
+ function restoreBase(id, persisted) {
28243
+ return {
28244
+ id,
28245
+ name: persisted.name,
28246
+ title: persisted.title,
28247
+ subtitle: persisted.subtitle,
28248
+ commandType: persisted.commandType,
28249
+ harness: persisted.harness,
28250
+ scrollback: "",
28251
+ cwd: persisted.cwd,
28252
+ assistArgs: persisted.assistArgs,
28253
+ initialPrompt: persisted.initialPrompt,
28254
+ starred: persisted.starred
28255
+ };
28072
28256
  }
28073
- function headContainsSessionId(filePath, claudeSessionId) {
28074
- let fd;
28075
- try {
28076
- fd = fs38.openSync(filePath, "r");
28077
- const buf = Buffer.alloc(16384);
28078
- const bytesRead = fs38.readSync(fd, buf, 0, buf.length, 0);
28079
- return buf.toString("utf8", 0, bytesRead).includes(claudeSessionId);
28080
- } catch {
28081
- return false;
28082
- } finally {
28083
- if (fd !== void 0) fs38.closeSync(fd);
28084
- }
28257
+
28258
+ // src/commands/sessions/daemon/errorSession.ts
28259
+ function errorSession(id, persisted, error) {
28260
+ return {
28261
+ ...restoreBase(id, persisted),
28262
+ status: "error",
28263
+ startedAt: persisted.startedAt,
28264
+ runningMs: persisted.runningMs ?? 0,
28265
+ runningSince: null,
28266
+ pty: null,
28267
+ runName: persisted.runName,
28268
+ runArgs: persisted.runArgs,
28269
+ error,
28270
+ restored: false
28271
+ };
28085
28272
  }
28086
28273
 
28087
28274
  // src/commands/sessions/daemon/deriveRestoreStatus.ts
@@ -28297,7 +28484,7 @@ function rearmStoppedSessions(sessions, notify2) {
28297
28484
  }
28298
28485
 
28299
28486
  // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
28300
- import { existsSync as existsSync68 } from "fs";
28487
+ import { existsSync as existsSync69 } from "fs";
28301
28488
  import { basename as basename18 } from "path";
28302
28489
 
28303
28490
  // src/commands/sessions/daemon/worktree/accountedTrees.ts
@@ -28387,9 +28574,9 @@ function capped(lines) {
28387
28574
  }
28388
28575
 
28389
28576
  // src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
28390
- import { existsSync as existsSync67 } from "fs";
28577
+ import { existsSync as existsSync68 } from "fs";
28391
28578
  async function reclaimVanishedWorktrees(clone, paths) {
28392
- if (!existsSync67(clone)) {
28579
+ if (!existsSync68(clone)) {
28393
28580
  for (const { path: path71 } of paths) forgetWorktree(path71);
28394
28581
  daemonLog(
28395
28582
  `clone ${clone} is gone; forgot ${paths.length} worktree record(s) it owned`
@@ -28482,7 +28669,7 @@ async function recoverOrphanedWorktrees(sessions, spawnWith, notify2) {
28482
28669
  const vanished = /* @__PURE__ */ new Map();
28483
28670
  for (const { path: path71, clone } of readWorktreeRegistry()) {
28484
28671
  if (accounted.has(path71)) continue;
28485
- if (!existsSync68(path71)) {
28672
+ if (!existsSync69(path71)) {
28486
28673
  vanished.set(clone, [
28487
28674
  ...vanished.get(clone) ?? [],
28488
28675
  { path: path71, branch: basename18(path71) }
@@ -28650,86 +28837,23 @@ function startHeldSession(session, sessions, clients, onStatusChange, notify2) {
28650
28837
  notify2();
28651
28838
  }
28652
28839
 
28840
+ // src/commands/sessions/daemon/startHeldInTree.ts
28841
+ function startHeldInTree(seeded, sessions, clients, onStatusChange, notify2) {
28842
+ const start3 = (session) => startHeldSession(session, sessions, clients, onStatusChange, notify2);
28843
+ start3(seeded);
28844
+ for (const session of sessions.values())
28845
+ if (session !== seeded && session.pendingStart && session.cwd === seeded.cwd)
28846
+ start3(session);
28847
+ }
28848
+
28653
28849
  // src/commands/sessions/daemon/treeSpawnContext.ts
28654
28850
  function treeSpawnContext(sessions, spawnWith, notify2, clients, onStatusChange) {
28655
28851
  return {
28656
28852
  sessions,
28657
28853
  spawnWith,
28658
28854
  notify: notify2,
28659
- startHeld: (session) => startHeldSession(session, sessions, clients, onStatusChange, notify2)
28660
- };
28661
- }
28662
-
28663
- // src/commands/sessions/daemon/watchTranscript.ts
28664
- import { existsSync as existsSync69, watch as watch3 } from "fs";
28665
-
28666
- // src/commands/sessions/daemon/reconcileTranscriptStatus.ts
28667
- async function resolveTranscriptPath(session) {
28668
- if (session.transcriptPath) return session.transcriptPath;
28669
- if (!session.claudeSessionId) return null;
28670
- const direct = session.cwd ? findTranscriptPathSync(session.cwd, session.claudeSessionId) : null;
28671
- const filePath = direct ?? await findSessionJsonlPath(session.claudeSessionId);
28672
- if (filePath) session.transcriptPath = filePath;
28673
- return filePath;
28674
- }
28675
- async function reconcileTranscriptStatus(session, onStatusChange) {
28676
- if (session.status === "done" || session.status === "error" || session.status === "stopped")
28677
- return;
28678
- const filePath = await resolveTranscriptPath(session);
28679
- if (!filePath) return;
28680
- const entries = await readTranscriptTail(filePath);
28681
- const derived = deriveTranscriptStatus(entries, {
28682
- permissionActive: session.permissionActive
28683
- });
28684
- if (!derived) return;
28685
- if (derived === "running") session.permissionActive = false;
28686
- if (derived === session.status) return;
28687
- daemonLog(
28688
- `session ${session.id} transcript reconcile: ${session.status} -> ${derived}`
28689
- );
28690
- onStatusChange(session, derived);
28691
- }
28692
-
28693
- // src/commands/sessions/daemon/watchTranscript.ts
28694
- var DEBOUNCE_MS3 = 100;
28695
- function watchTranscript(session, onStatusChange) {
28696
- if (!session.cwd || !session.claudeSessionId) return;
28697
- if (session.watchedTranscriptId === session.claudeSessionId) return;
28698
- const dir = projectDirForCwd(session.cwd);
28699
- if (!existsSync69(dir)) return;
28700
- if (session.transcriptWatcher) {
28701
- session.transcriptWatcher.close();
28702
- session.transcriptWatcher = void 0;
28703
- }
28704
- session.transcriptPath = void 0;
28705
- const fileName = `${session.claudeSessionId}.jsonl`;
28706
- let timer = null;
28707
- const run4 = () => {
28708
- timer = null;
28709
- void reconcileTranscriptStatus(session, onStatusChange);
28855
+ startHeld: (session) => startHeldInTree(session, sessions, clients, onStatusChange, notify2)
28710
28856
  };
28711
- try {
28712
- session.transcriptWatcher = watch3(dir, (_event, changed) => {
28713
- if (changed && changed !== fileName) return;
28714
- if (timer) clearTimeout(timer);
28715
- timer = setTimeout(run4, DEBOUNCE_MS3);
28716
- });
28717
- } catch (error) {
28718
- daemonLog(
28719
- `session ${session.id} transcript watch failed: ${error instanceof Error ? error.message : String(error)}`
28720
- );
28721
- return;
28722
- }
28723
- session.watchedTranscriptId = session.claudeSessionId;
28724
- daemonLog(`session ${session.id} watching transcript ${fileName}`);
28725
- run4();
28726
- }
28727
-
28728
- // src/commands/sessions/daemon/wireSessionWatchers.ts
28729
- function wireSessionWatchers(session, notify2, onStatusChange) {
28730
- const wireTranscript = (s) => watchTranscript(s, onStatusChange);
28731
- watchActivity(session, notify2, wireTranscript);
28732
- wireTranscript(session);
28733
28857
  }
28734
28858
 
28735
28859
  // src/commands/sessions/daemon/connectToWindowsDaemon.ts
@@ -28934,8 +29058,11 @@ async function forwardWindowsCreate(conn, state, client, data) {
28934
29058
  }
28935
29059
  }
28936
29060
  function stripOutboundSessionId(data) {
28937
- if (typeof data.sessionId !== "string") return data;
28938
- return { ...data, sessionId: stripWindowsSessionId(data.sessionId) };
29061
+ const stripped = { ...data };
29062
+ for (const key of ["sessionId", "joinSessionId"])
29063
+ if (typeof stripped[key] === "string")
29064
+ stripped[key] = stripWindowsSessionId(stripped[key]);
29065
+ return stripped;
28939
29066
  }
28940
29067
 
28941
29068
  // src/commands/sessions/daemon/WindowsProxyState.ts
@@ -29388,6 +29515,46 @@ var WindowsProxy = class {
29388
29515
  }
29389
29516
  };
29390
29517
 
29518
+ // src/commands/sessions/daemon/worktree/joinableStream.ts
29519
+ function joinableStream(sessions, targetId) {
29520
+ const target = sessions.get(targetId);
29521
+ if (!target) return { reason: "no such session" };
29522
+ if (target.commandType === "run")
29523
+ return { reason: "a server run has no agent stream" };
29524
+ if (target.closing === true) return { reason: "the session is closing" };
29525
+ if (target.status !== "running" && target.status !== "waiting")
29526
+ return { reason: `the session is ${target.status}` };
29527
+ if (!target.cwd) return { reason: "the session has no working directory" };
29528
+ return { session: target };
29529
+ }
29530
+
29531
+ // src/commands/sessions/daemon/worktree/spawnIntoStream.ts
29532
+ function spawnIntoStream(ctx, target, prompt, harness) {
29533
+ const holdUntilSeeded = target.pendingStart !== void 0;
29534
+ const id = ctx.spawnWith(
29535
+ (sid) => createSession(sid, prompt, target.cwd, false, harness, holdUntilSeeded)
29536
+ );
29537
+ const joined = ctx.sessions.get(id);
29538
+ if (joined && target.worktree) joined.worktree = { ...target.worktree };
29539
+ daemonLog(
29540
+ `session ${id} added to the stream of session ${target.id} in ${target.cwd}: no workspace allocated${holdUntilSeeded ? ", held until that workspace finishes seeding" : ""}`
29541
+ );
29542
+ ctx.notify();
29543
+ return id;
29544
+ }
29545
+
29546
+ // src/commands/sessions/daemon/worktree/addAgentToStream.ts
29547
+ function addAgentToStream(ctx, targetId, prompt, harness) {
29548
+ const joinable = joinableStream(ctx.sessions, targetId);
29549
+ if ("reason" in joinable) {
29550
+ daemonLog(
29551
+ `create: cannot add an agent to session ${targetId}: ${joinable.reason}`
29552
+ );
29553
+ return void 0;
29554
+ }
29555
+ return spawnIntoStream(ctx, joinable.session, prompt, harness);
29556
+ }
29557
+
29391
29558
  // src/commands/sessions/daemon/createAssistSession.ts
29392
29559
  function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
29393
29560
  const startedAt = Date.now();
@@ -29595,13 +29762,13 @@ var SessionManager = class {
29595
29762
  return restoreAllSessions(this.spawnWith, this.sessions, this.notify);
29596
29763
  }
29597
29764
  drain = () => drainSessions(this.sessions, this.notify);
29598
- add(session) {
29599
- this.wire(session);
29600
- wireSessionWatchers(session, this.notify, this.onStatusChange);
29601
- logSpawnedSession(session);
29602
- return session.id;
29603
- }
29604
- spawnWith = (create) => this.add(create(sessionLimits.nextId(this.sessions.size, this.idCounter)));
29765
+ spawnWith = (create) => registerSpawnedSession(
29766
+ create(sessionLimits.nextId(this.sessions.size, this.idCounter)),
29767
+ this.sessions,
29768
+ this.clients,
29769
+ this.onStatusChange,
29770
+ this.notify
29771
+ );
29605
29772
  treeCtx() {
29606
29773
  return treeSpawnContext(
29607
29774
  this.sessions,
@@ -29614,6 +29781,9 @@ var SessionManager = class {
29614
29781
  spawn(prompt, cwd, design, harness) {
29615
29782
  return spawnInTree(this.treeCtx(), prompt, cwd, design, harness);
29616
29783
  }
29784
+ addAgent(targetId, prompt, harness) {
29785
+ return addAgentToStream(this.treeCtx(), targetId, prompt, harness);
29786
+ }
29617
29787
  spawnRun(runName, runArgs, cwd, meta) {
29618
29788
  return this.spawnWith(
29619
29789
  (id) => createRunSession(id, runName, runArgs, cwd, meta)
@@ -29629,15 +29799,11 @@ var SessionManager = class {
29629
29799
  return resumeInTree(this.treeCtx(), sessionId, cwd, name);
29630
29800
  }
29631
29801
  onStatusChange = makeStatusChangeHandler(
29802
+ this.sessions,
29632
29803
  (id) => this.dismissSession(id),
29633
29804
  () => this.notify(),
29634
29805
  (session, itemId2) => reuseSessionForRun(session, itemId2, this.clients, this.onStatusChange)
29635
29806
  );
29636
- wire(session) {
29637
- this.sessions.set(session.id, session);
29638
- wirePtyEvents(session, this.clients, this.onStatusChange);
29639
- this.notify();
29640
- }
29641
29807
  writeToSession(id, data) {
29642
29808
  writeToSession(this.sessions, id, data, this.onStatusChange);
29643
29809
  }
@@ -29883,16 +30049,20 @@ var lifecycleHandlers = {
29883
30049
  function spawnCreate(m, d) {
29884
30050
  const design = d.design === true;
29885
30051
  const harness = d.harness;
30052
+ const prompt = d.prompt;
29886
30053
  if (design)
29887
30054
  daemonLog(`create: design session (cwd=${d.cwd ?? ""})`);
29888
30055
  if (harness && harness !== "claude")
29889
30056
  daemonLog(`create: ${harness} session (cwd=${d.cwd ?? ""})`);
29890
- return m.spawn(
29891
- d.prompt,
29892
- d.cwd,
29893
- design,
29894
- harness
29895
- );
30057
+ const joinSessionId = design ? void 0 : d.joinSessionId;
30058
+ if (joinSessionId) {
30059
+ const joined = m.addAgent(joinSessionId, prompt, harness);
30060
+ if (joined) return joined;
30061
+ daemonLog(
30062
+ `create: falling back to a fresh isolated session (cwd=${d.cwd ?? ""})`
30063
+ );
30064
+ }
30065
+ return m.spawn(prompt, d.cwd, design, harness);
29896
30066
  }
29897
30067
 
29898
30068
  // src/commands/sessions/daemon/messageHandlers.ts
@@ -30195,7 +30365,7 @@ var sessionsConfigHelp = [
30195
30365
  {
30196
30366
  key: "sessions.includeCommittedChanges",
30197
30367
  setter: "assist config set sessions.includeCommittedChanges true -g",
30198
- note: "session card counts and /diff span the whole branch (merge-base with the default branch) instead of just uncommitted work (default off)"
30368
+ note: "session card counts and /diff span the whole session (its first commit's parent, else merge-base with the default branch) instead of just uncommitted work (default off)"
30199
30369
  },
30200
30370
  {
30201
30371
  key: "worktree.enabled",