@staff0rd/assist 0.448.0 → 0.449.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.449.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -27155,13 +27155,6 @@ function greetClient(client, sessions, windowsProxy) {
27155
27155
  void windowsProxy.discover();
27156
27156
  }
27157
27157
 
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
27158
  // src/commands/sessions/daemon/decidePrPreview.ts
27166
27159
  function decidePrPreview(sessions, waiters, notify2, d) {
27167
27160
  const id = d.sessionId;
@@ -27249,373 +27242,105 @@ var PrPreviewCoordinator = class {
27249
27242
  }
27250
27243
  };
27251
27244
 
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
- });
27245
+ // src/commands/sessions/daemon/logSpawnedSession.ts
27246
+ function logSpawnedSession(session) {
27247
+ daemonLog(
27248
+ `session ${session.id} spawned: ${session.name} [${session.commandType}] ${session.cwd ?? ""}`
27249
+ );
27287
27250
  }
27288
27251
 
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
- });
27252
+ // src/commands/sessions/daemon/emitSessionOutput.ts
27253
+ var MAX_SCROLLBACK = 256 * 1024;
27254
+ function emitSessionOutput(session, clients, data) {
27255
+ session.scrollback += data;
27256
+ if (session.scrollback.length > MAX_SCROLLBACK) {
27257
+ session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK);
27258
+ }
27259
+ broadcast(clients, { type: "output", sessionId: session.id, data });
27298
27260
  }
27299
27261
 
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
- });
27262
+ // src/commands/sessions/daemon/exitReason.ts
27263
+ import { existsSync as existsSync64 } from "fs";
27264
+ function exitReason(session, exitCode) {
27265
+ const base = `process exited with code ${exitCode}`;
27266
+ if (session.cwd && !existsSync64(session.cwd))
27267
+ return `${base}: working directory ${session.cwd} no longer exists`;
27268
+ return base;
27327
27269
  }
27328
27270
 
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
- )
27271
+ // src/commands/sessions/daemon/handleFailedResume.ts
27272
+ function handleFailedResume(session, exitCode, onStatusChange) {
27273
+ const failed2 = session.restored === true && exitCode !== 0 && session.scrollback.length === 0;
27274
+ if (!failed2) return false;
27275
+ session.error = `resume process exited with code ${exitCode} before producing any output`;
27276
+ daemonLog(
27277
+ `could not resume restored session "${session.name}" (id ${session.id}): ${session.error}`
27342
27278
  );
27279
+ onStatusChange(session, "error", exitCode);
27280
+ return true;
27343
27281
  }
27344
27282
 
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
- });
27283
+ // src/commands/sessions/daemon/watchActivity.ts
27284
+ import { existsSync as existsSync65, mkdirSync as mkdirSync26, watch as watch2 } from "fs";
27285
+ import { dirname as dirname31 } from "path";
27286
+
27287
+ // src/commands/sessions/daemon/applyReviewPause.ts
27288
+ function applyReviewPause(session, activity2) {
27289
+ const inReview = activity2.kind === "backlog" && activity2.phase !== void 0 && activity2.phase === activity2.totalPhases;
27290
+ if (inReview && !session.reviewStarted) {
27291
+ session.autoAdvance = false;
27292
+ session.reviewStarted = true;
27293
+ } else if (!inReview && session.reviewStarted) {
27294
+ session.reviewStarted = false;
27366
27295
  }
27367
- return [...byId.values()];
27368
27296
  }
27369
- async function readTranscriptUsage(transcriptPath) {
27370
- const content = await fs36.promises.readFile(transcriptPath, "utf8");
27371
- return transcriptUsage(content.split("\n"));
27297
+
27298
+ // src/shared/db/getPhaseActiveMs.ts
27299
+ import { and as and21, eq as eq40 } from "drizzle-orm";
27300
+ async function getPhaseActiveMs(db, itemId2, phaseIdx) {
27301
+ const [row] = await db.select({ activeMs: phaseUsage.activeMs }).from(phaseUsage).where(
27302
+ and21(eq40(phaseUsage.itemId, itemId2), eq40(phaseUsage.phaseIdx, phaseIdx))
27303
+ );
27304
+ return row?.activeMs ?? 0;
27372
27305
  }
27373
27306
 
27374
- // src/commands/sessions/daemon/persistPhaseTokens.ts
27375
- async function persistPhaseTokens(itemId2, phaseIdx, transcriptPath, usedPct, windows) {
27307
+ // src/commands/sessions/daemon/loadPhaseActiveMs.ts
27308
+ async function loadPhaseActiveMs(itemId2, phaseIdx) {
27376
27309
  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
- }
27310
+ if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url)
27311
+ return null;
27312
+ return await getPhaseActiveMs(await getDb(), itemId2, phaseIdx);
27403
27313
  } catch (error) {
27404
- daemonLog(`phase-tokens persist failed: ${String(error)}`);
27405
- }
27406
- }
27407
-
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;
27314
+ daemonLog(`phase-active-ms load failed: ${String(error)}`);
27315
+ return null;
27424
27316
  }
27425
- return false;
27426
27317
  }
27427
27318
 
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();
27319
+ // src/commands/sessions/daemon/seedRunningMsFromUsage.ts
27320
+ async function seedRunningMsFromUsage(session, notify2) {
27321
+ if (session.usageSeeded) return;
27322
+ const phase = sessionBacklogPhase(session);
27323
+ if (!phase) return;
27324
+ session.usageSeeded = true;
27325
+ const priorMs = await loadPhaseActiveMs(phase.itemId, phase.phaseIdx);
27326
+ if (priorMs == null || priorMs <= session.runningMs) return;
27327
+ session.runningMs = priorMs;
27328
+ daemonLog(
27329
+ `session ${session.id} seeded runningMs=${priorMs} from item ${phase.itemId} phase ${phase.phaseIdx} recorded usage`
27330
+ );
27331
+ notify2();
27438
27332
  }
27439
27333
 
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();
27607
- }
27608
-
27609
- // src/commands/sessions/daemon/watchActivity.ts
27610
- var DEBOUNCE_MS2 = 50;
27611
- function watchActivity(session, notify2, onClaudeSessionId) {
27612
- if (session.commandType !== "assist" || !session.cwd) return;
27613
- const path71 = activityPath(session.id);
27614
- const dir = dirname31(path71);
27615
- try {
27616
- mkdirSync26(dir, { recursive: true });
27617
- } catch {
27618
- return;
27334
+ // src/commands/sessions/daemon/watchActivity.ts
27335
+ var DEBOUNCE_MS2 = 50;
27336
+ function watchActivity(session, notify2, onClaudeSessionId) {
27337
+ if (session.commandType !== "assist" || !session.cwd) return;
27338
+ const path71 = activityPath(session.id);
27339
+ const dir = dirname31(path71);
27340
+ try {
27341
+ mkdirSync26(dir, { recursive: true });
27342
+ } catch {
27343
+ return;
27619
27344
  }
27620
27345
  reconcileActivity(session.id, session.activity);
27621
27346
  let timer = null;
@@ -27721,163 +27446,53 @@ function reportSilentFailure(session, clients) {
27721
27446
  );
27722
27447
  }
27723
27448
 
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";
27449
+ // src/commands/sessions/daemon/watchTranscript.ts
27450
+ import { existsSync as existsSync67, watch as watch3 } from "fs";
27749
27451
 
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;
27452
+ // src/commands/sessions/shared/findTranscriptPathSync.ts
27453
+ import * as fs36 from "fs";
27454
+ import * as os4 from "os";
27455
+ import * as path60 from "path";
27456
+ function projectDirForCwd(cwd) {
27457
+ return path60.join(
27458
+ os4.homedir(),
27459
+ ".claude",
27460
+ "projects",
27461
+ cwd.replace(/[^a-zA-Z0-9]/g, "-")
27462
+ );
27754
27463
  }
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);
27464
+ function transcriptPathFor(cwd, claudeSessionId) {
27465
+ return path60.join(projectDirForCwd(cwd), `${claudeSessionId}.jsonl`);
27466
+ }
27467
+ function findTranscriptPathSync(cwd, claudeSessionId) {
27468
+ const direct = transcriptPathFor(cwd, claudeSessionId);
27469
+ if (fs36.existsSync(direct)) return direct;
27470
+ const dir = projectDirForCwd(cwd);
27471
+ let files;
27472
+ try {
27473
+ files = fs36.readdirSync(dir);
27474
+ } catch {
27772
27475
  return null;
27773
27476
  }
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
- };
27477
+ for (const file of files) {
27478
+ if (!file.endsWith(".jsonl")) continue;
27479
+ if (headContainsSessionId(path60.join(dir, file), claudeSessionId))
27480
+ return path60.join(dir, file);
27785
27481
  }
27786
27482
  return null;
27787
27483
  }
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
- };
27484
+ function headContainsSessionId(filePath, claudeSessionId) {
27485
+ let fd;
27486
+ try {
27487
+ fd = fs36.openSync(filePath, "r");
27488
+ const buf = Buffer.alloc(16384);
27489
+ const bytesRead = fs36.readSync(fd, buf, 0, buf.length, 0);
27490
+ return buf.toString("utf8", 0, bytesRead).includes(claudeSessionId);
27491
+ } catch {
27492
+ return false;
27493
+ } finally {
27494
+ if (fd !== void 0) fs36.closeSync(fd);
27846
27495
  }
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
27496
  }
27882
27497
 
27883
27498
  // src/commands/sessions/shared/normalizeUser.ts
@@ -27994,94 +27609,579 @@ function parseTailEntries(raw) {
27994
27609
  }
27995
27610
  return entries;
27996
27611
  }
27997
- function dropPartialFirstLine(raw, sliced) {
27998
- if (!sliced) return raw;
27999
- const newline = raw.indexOf("\n");
28000
- return newline === -1 ? "" : raw.slice(newline + 1);
27612
+ function dropPartialFirstLine(raw, sliced) {
27613
+ if (!sliced) return raw;
27614
+ const newline = raw.indexOf("\n");
27615
+ return newline === -1 ? "" : raw.slice(newline + 1);
27616
+ }
27617
+ async function readTranscriptTail(filePath, maxBytes = DEFAULT_MAX_BYTES) {
27618
+ let handle;
27619
+ try {
27620
+ handle = await fs37.promises.open(filePath, "r");
27621
+ const { size } = await handle.stat();
27622
+ const start3 = Math.max(0, size - maxBytes);
27623
+ const length = size - start3;
27624
+ if (length === 0) return [];
27625
+ const buf = Buffer.alloc(length);
27626
+ await handle.read(buf, 0, length, start3);
27627
+ return parseTailEntries(
27628
+ dropPartialFirstLine(buf.toString("utf8"), start3 > 0)
27629
+ );
27630
+ } catch {
27631
+ return [];
27632
+ } finally {
27633
+ await handle?.close();
27634
+ }
27635
+ }
27636
+ function readTranscriptTailSync(filePath, maxBytes = DEFAULT_MAX_BYTES) {
27637
+ let fd;
27638
+ try {
27639
+ fd = fs37.openSync(filePath, "r");
27640
+ const { size } = fs37.fstatSync(fd);
27641
+ const start3 = Math.max(0, size - maxBytes);
27642
+ const length = size - start3;
27643
+ if (length === 0) return [];
27644
+ const buf = Buffer.alloc(length);
27645
+ fs37.readSync(fd, buf, 0, length, start3);
27646
+ return parseTailEntries(
27647
+ dropPartialFirstLine(buf.toString("utf8"), start3 > 0)
27648
+ );
27649
+ } catch {
27650
+ return [];
27651
+ } finally {
27652
+ if (fd !== void 0) fs37.closeSync(fd);
27653
+ }
27654
+ }
27655
+
27656
+ // src/commands/sessions/daemon/reconcileTranscriptStatus.ts
27657
+ async function resolveTranscriptPath(session) {
27658
+ if (session.transcriptPath) return session.transcriptPath;
27659
+ if (!session.claudeSessionId) return null;
27660
+ const direct = session.cwd ? findTranscriptPathSync(session.cwd, session.claudeSessionId) : null;
27661
+ const filePath = direct ?? await findSessionJsonlPath(session.claudeSessionId);
27662
+ if (filePath) session.transcriptPath = filePath;
27663
+ return filePath;
27664
+ }
27665
+ async function reconcileTranscriptStatus(session, onStatusChange) {
27666
+ if (session.status === "done" || session.status === "error" || session.status === "stopped")
27667
+ return;
27668
+ const filePath = await resolveTranscriptPath(session);
27669
+ if (!filePath) return;
27670
+ const entries = await readTranscriptTail(filePath);
27671
+ const derived = deriveTranscriptStatus(entries, {
27672
+ permissionActive: session.permissionActive
27673
+ });
27674
+ if (!derived) return;
27675
+ if (derived === "running") session.permissionActive = false;
27676
+ if (derived === session.status) return;
27677
+ daemonLog(
27678
+ `session ${session.id} transcript reconcile: ${session.status} -> ${derived}`
27679
+ );
27680
+ onStatusChange(session, derived);
27681
+ }
27682
+
27683
+ // src/commands/sessions/daemon/watchTranscript.ts
27684
+ var DEBOUNCE_MS3 = 100;
27685
+ function watchTranscript(session, onStatusChange) {
27686
+ if (!session.cwd || !session.claudeSessionId) return;
27687
+ if (session.watchedTranscriptId === session.claudeSessionId) return;
27688
+ const dir = projectDirForCwd(session.cwd);
27689
+ if (!existsSync67(dir)) return;
27690
+ if (session.transcriptWatcher) {
27691
+ session.transcriptWatcher.close();
27692
+ session.transcriptWatcher = void 0;
27693
+ }
27694
+ session.transcriptPath = void 0;
27695
+ const fileName = `${session.claudeSessionId}.jsonl`;
27696
+ let timer = null;
27697
+ const run4 = () => {
27698
+ timer = null;
27699
+ void reconcileTranscriptStatus(session, onStatusChange);
27700
+ };
27701
+ try {
27702
+ session.transcriptWatcher = watch3(dir, (_event, changed) => {
27703
+ if (changed && changed !== fileName) return;
27704
+ if (timer) clearTimeout(timer);
27705
+ timer = setTimeout(run4, DEBOUNCE_MS3);
27706
+ });
27707
+ } catch (error) {
27708
+ daemonLog(
27709
+ `session ${session.id} transcript watch failed: ${error instanceof Error ? error.message : String(error)}`
27710
+ );
27711
+ return;
27712
+ }
27713
+ session.watchedTranscriptId = session.claudeSessionId;
27714
+ daemonLog(`session ${session.id} watching transcript ${fileName}`);
27715
+ run4();
27716
+ }
27717
+
27718
+ // src/commands/sessions/daemon/wireSessionWatchers.ts
27719
+ function wireSessionWatchers(session, notify2, onStatusChange) {
27720
+ const wireTranscript = (s) => watchTranscript(s, onStatusChange);
27721
+ watchActivity(session, notify2, wireTranscript);
27722
+ wireTranscript(session);
27723
+ }
27724
+
27725
+ // src/commands/sessions/daemon/registerSpawnedSession.ts
27726
+ function registerSpawnedSession(session, sessions, clients, onStatusChange, notify2) {
27727
+ sessions.set(session.id, session);
27728
+ wirePtyEvents(session, clients, onStatusChange);
27729
+ notify2();
27730
+ wireSessionWatchers(session, notify2, onStatusChange);
27731
+ logSpawnedSession(session);
27732
+ return session.id;
27733
+ }
27734
+
27735
+ // src/commands/sessions/daemon/applySetStatus.ts
27736
+ function applySetStatus(sessions, id, status3, source, onStatusChange) {
27737
+ const session = sessions.get(id);
27738
+ if (!session) {
27739
+ daemonLog(
27740
+ `set-status for unknown session id=${id} status=${status3} (no live session; ignoring)`
27741
+ );
27742
+ return;
27743
+ }
27744
+ if (session.status === "stopped") {
27745
+ daemonLog(
27746
+ `set-status for stopped session id=${id} status=${status3} (held pending durable close; ignoring)`
27747
+ );
27748
+ return;
27749
+ }
27750
+ if (source === "permission") session.permissionActive = true;
27751
+ if (status3 === "running") session.permissionActive = false;
27752
+ onStatusChange(session, status3);
27753
+ }
27754
+
27755
+ // src/shared/db/recordPhaseCycleContext.ts
27756
+ import { sql as sql21 } from "drizzle-orm";
27757
+ async function recordPhaseCycleContext(db, itemId2, phaseIdx, window, resetsAt, pct) {
27758
+ if (pct <= 0) return;
27759
+ await db.insert(phaseCycleContext).values({ itemId: itemId2, phaseIdx, window, resetsAt, peakContextPct: pct }).onConflictDoUpdate({
27760
+ target: [
27761
+ phaseCycleContext.itemId,
27762
+ phaseCycleContext.phaseIdx,
27763
+ phaseCycleContext.window,
27764
+ phaseCycleContext.resetsAt
27765
+ ],
27766
+ set: {
27767
+ peakContextPct: sql21`GREATEST(${phaseCycleContext.peakContextPct}, ${pct})`
27768
+ }
27769
+ });
27770
+ }
27771
+
27772
+ // src/shared/db/recordPhasePeakContext.ts
27773
+ import { sql as sql22 } from "drizzle-orm";
27774
+ async function recordPhasePeakContext(db, itemId2, phaseIdx, pct) {
27775
+ await db.insert(phaseUsage).values({ itemId: itemId2, phaseIdx, peakContextPct: pct }).onConflictDoUpdate({
27776
+ target: [phaseUsage.itemId, phaseUsage.phaseIdx],
27777
+ set: {
27778
+ peakContextPct: sql22`GREATEST(${phaseUsage.peakContextPct}, ${pct})`
27779
+ }
27780
+ });
27781
+ }
27782
+
27783
+ // src/shared/db/recordPhaseTranscriptUsage.ts
27784
+ import { sql as sql23 } from "drizzle-orm";
27785
+ async function recordPhaseTranscriptUsage(db, itemId2, phaseIdx, responses) {
27786
+ const byId = /* @__PURE__ */ new Map();
27787
+ for (const r of responses) byId.set(r.messageId, r);
27788
+ const unique = [...byId.values()];
27789
+ if (unique.length === 0) return { tokensUp: 0, tokensDown: 0 };
27790
+ return db.transaction(async (tx) => {
27791
+ const inserted = await tx.insert(phaseUsageMessages).values(unique.map((r) => ({ itemId: itemId2, phaseIdx, messageId: r.messageId }))).onConflictDoNothing().returning({ messageId: phaseUsageMessages.messageId });
27792
+ let tokensUp = 0;
27793
+ let tokensDown = 0;
27794
+ for (const { messageId } of inserted) {
27795
+ const r = byId.get(messageId);
27796
+ if (!r) continue;
27797
+ tokensUp += r.outputTokens;
27798
+ tokensDown += r.inputTokens;
27799
+ }
27800
+ if (tokensUp <= 0 && tokensDown <= 0) return { tokensUp: 0, tokensDown: 0 };
27801
+ await tx.insert(phaseUsage).values({ itemId: itemId2, phaseIdx, tokensUp, tokensDown }).onConflictDoUpdate({
27802
+ target: [phaseUsage.itemId, phaseUsage.phaseIdx],
27803
+ set: {
27804
+ tokensUp: sql23`${phaseUsage.tokensUp} + ${tokensUp}`,
27805
+ tokensDown: sql23`${phaseUsage.tokensDown} + ${tokensDown}`
27806
+ }
27807
+ });
27808
+ return { tokensUp, tokensDown };
27809
+ });
27810
+ }
27811
+
27812
+ // src/shared/db/recordWindowTokens.ts
27813
+ import { and as and22, eq as eq41, sql as sql24 } from "drizzle-orm";
27814
+ async function recordWindowTokens(db, window, resetsAt, tokensUp, tokensDown) {
27815
+ if (tokensUp <= 0 && tokensDown <= 0) return;
27816
+ await db.update(usagePeaks).set({
27817
+ tokensUp: sql24`${usagePeaks.tokensUp} + ${tokensUp}`,
27818
+ tokensDown: sql24`${usagePeaks.tokensDown} + ${tokensDown}`
27819
+ }).where(
27820
+ and22(
27821
+ eq41(usagePeaks.window, window),
27822
+ eq41(usagePeaks.resetsAt, resetsAt),
27823
+ eq41(usagePeaks.segment, 0)
27824
+ )
27825
+ );
27826
+ }
27827
+
27828
+ // src/commands/sessions/shared/transcriptUsage.ts
27829
+ import * as fs38 from "fs";
27830
+ function transcriptUsage(lines) {
27831
+ const byId = /* @__PURE__ */ new Map();
27832
+ for (const line of lines) {
27833
+ if (!line.trim()) continue;
27834
+ let entry;
27835
+ try {
27836
+ entry = JSON.parse(line);
27837
+ } catch {
27838
+ continue;
27839
+ }
27840
+ if (entry.type !== "assistant") continue;
27841
+ const id = entry.message?.id;
27842
+ const usage = entry.message?.usage;
27843
+ if (!id || !usage) continue;
27844
+ byId.set(id, {
27845
+ messageId: id,
27846
+ inputTokens: usage.input_tokens ?? 0,
27847
+ outputTokens: usage.output_tokens ?? 0
27848
+ });
27849
+ }
27850
+ return [...byId.values()];
27851
+ }
27852
+ async function readTranscriptUsage(transcriptPath) {
27853
+ const content = await fs38.promises.readFile(transcriptPath, "utf8");
27854
+ return transcriptUsage(content.split("\n"));
27855
+ }
27856
+
27857
+ // src/commands/sessions/daemon/persistPhaseTokens.ts
27858
+ async function persistPhaseTokens(itemId2, phaseIdx, transcriptPath, usedPct, windows) {
27859
+ try {
27860
+ if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url) return;
27861
+ const db = await getDb();
27862
+ if (usedPct !== void 0) {
27863
+ await recordPhasePeakContext(db, itemId2, phaseIdx, usedPct);
27864
+ for (const { window, resetsAt } of windows) {
27865
+ await recordPhaseCycleContext(
27866
+ db,
27867
+ itemId2,
27868
+ phaseIdx,
27869
+ window,
27870
+ resetsAt,
27871
+ usedPct
27872
+ );
27873
+ }
27874
+ }
27875
+ const responses = await readTranscriptUsage(transcriptPath);
27876
+ if (responses.length === 0) return;
27877
+ const { tokensUp, tokensDown } = await recordPhaseTranscriptUsage(
27878
+ db,
27879
+ itemId2,
27880
+ phaseIdx,
27881
+ responses
27882
+ );
27883
+ for (const { window, resetsAt } of windows) {
27884
+ await recordWindowTokens(db, window, resetsAt, tokensUp, tokensDown);
27885
+ }
27886
+ } catch (error) {
27887
+ daemonLog(`phase-tokens persist failed: ${String(error)}`);
27888
+ }
27889
+ }
27890
+
27891
+ // src/commands/sessions/daemon/recordSessionUsage.ts
27892
+ function recordSessionUsage(sessions, claudeSessionId, transcriptPath, usedPct, windows) {
27893
+ for (const session of sessions) {
27894
+ if (session.claudeSessionId !== claudeSessionId) continue;
27895
+ session.usedPct = usedPct;
27896
+ const phase = sessionBacklogPhase(session);
27897
+ if (phase && transcriptPath)
27898
+ void persistPhaseTokens(
27899
+ phase.itemId,
27900
+ phase.phaseIdx,
27901
+ transcriptPath,
27902
+ usedPct,
27903
+ windows
27904
+ );
27905
+ void flushPhaseActiveMs(session);
27906
+ return true;
27907
+ }
27908
+ return false;
27909
+ }
27910
+
27911
+ // src/commands/sessions/daemon/applyUsageRecord.ts
27912
+ function applyUsageRecord(sessions, windows, notify2, claudeSessionId, transcriptPath, usedPct) {
27913
+ if (recordSessionUsage(
27914
+ sessions,
27915
+ claudeSessionId,
27916
+ transcriptPath,
27917
+ usedPct,
27918
+ windows
27919
+ ))
27920
+ notify2();
27921
+ }
27922
+
27923
+ // src/commands/sessions/daemon/shouldAutoRun.ts
27924
+ function shouldAutoRun(session, status3 = session.status) {
27925
+ if (!session.autoRun) return { run: false, reason: null };
27926
+ if (status3 !== "done")
27927
+ return {
27928
+ run: false,
27929
+ reason: `session status is "${status3}", not "done"`
27930
+ };
27931
+ if (session.commandType !== "assist")
27932
+ return {
27933
+ run: false,
27934
+ reason: `command type is "${session.commandType}", not "assist"`
27935
+ };
27936
+ const cmd = session.assistArgs?.[0];
27937
+ if (cmd !== "draft" && cmd !== "bug" && cmd !== "refine")
27938
+ return {
27939
+ run: false,
27940
+ reason: `command "${cmd ?? "(none)"}" is not draft/bug/refine`
27941
+ };
27942
+ const itemId2 = session.activity?.itemId;
27943
+ if (itemId2 == null)
27944
+ return {
27945
+ run: false,
27946
+ reason: "no itemId in activity (draft did not create an item)"
27947
+ };
27948
+ return { run: true, itemId: itemId2 };
27949
+ }
27950
+
27951
+ // src/commands/sessions/daemon/endsStreamLife.ts
27952
+ function endsStreamLife(session, status3, sharedWithOtherAgents) {
27953
+ if (status3 !== "done" || !session.worktree) return false;
27954
+ if (shouldAutoRun(session, status3).run) return false;
27955
+ if (!sharedWithOtherAgents(session)) return true;
27956
+ daemonLog(
27957
+ `session ${session.id} done in ${session.worktree.path}: workspace kept, other agents are still working there`
27958
+ );
27959
+ return false;
27960
+ }
27961
+
27962
+ // src/commands/sessions/daemon/shouldAutoDismiss.ts
27963
+ function shouldAutoDismiss(session, exitCode) {
27964
+ if (session.status !== "done" || exitCode !== 0) {
27965
+ return false;
27966
+ }
27967
+ if (session.reviewStarted) {
27968
+ return session.autoAdvance === true;
27969
+ }
27970
+ const args = session.assistArgs;
27971
+ if (args === void 0) return false;
27972
+ if (args[0] === "update") return true;
27973
+ return args.includes("--once") && args[0] !== "next";
27974
+ }
27975
+
27976
+ // src/commands/sessions/daemon/applyStatusChange.ts
27977
+ function applyStatusChange(session, status3, exitCode, dismiss, notify2, reuseForRun, sharedWithOtherAgents = () => false) {
27978
+ if (session.status === status3) return;
27979
+ const deps2 = { dismiss, notify: notify2, reuseForRun };
27980
+ if (endsStreamLife(session, status3, sharedWithOtherAgents)) {
27981
+ session.closing = true;
27982
+ daemonLog(
27983
+ `session ${session.id} closing: checking durability of ${session.worktree?.path} before reap`
27984
+ );
27985
+ notify2();
27986
+ void resolveCloseDurability(
27987
+ session,
27988
+ () => finishStatusChange(session, "done", exitCode, deps2),
27989
+ notify2
27990
+ );
27991
+ return;
27992
+ }
27993
+ finishStatusChange(session, status3, exitCode, deps2);
27994
+ }
27995
+ function finishStatusChange(session, status3, exitCode, deps2) {
27996
+ daemonLog(`session ${session.id} status: ${session.status} -> ${status3}`);
27997
+ void flushPhaseActiveMs(session);
27998
+ setStatus2(session, status3);
27999
+ if (status3 !== "stopped") session.undurable = void 0;
28000
+ const autoRun = shouldAutoRun(session);
28001
+ if (autoRun.run) {
28002
+ deps2.reuseForRun(session, autoRun.itemId);
28003
+ deps2.notify();
28004
+ return;
28005
+ }
28006
+ if (status3 === "done" && autoRun.reason != null) {
28007
+ daemonLog(
28008
+ `session ${session.id} auto-run enabled but skipped (exit ${exitCode ?? "none"}): ${autoRun.reason}`
28009
+ );
28010
+ }
28011
+ if (shouldAutoDismiss(session, exitCode)) deps2.dismiss(session.id);
28012
+ else deps2.notify();
28013
+ }
28014
+
28015
+ // src/commands/sessions/daemon/makeStatusChangeHandler.ts
28016
+ function makeStatusChangeHandler(sessions, dismiss, notify2, reuseForRun) {
28017
+ return (s, status3, exitCode) => applyStatusChange(
28018
+ s,
28019
+ status3,
28020
+ exitCode,
28021
+ dismiss,
28022
+ notify2,
28023
+ reuseForRun,
28024
+ (session) => otherTreeHolders(sessions, session).length > 0
28025
+ );
28026
+ }
28027
+
28028
+ // src/commands/sessions/daemon/respawnSession.ts
28029
+ function respawnSession(session, respawn, status3, clients, onStatusChange) {
28030
+ session.gitWatcher?.close();
28031
+ session.gitWatcher = void 0;
28032
+ session.undurable = void 0;
28033
+ session.pendingDismiss = void 0;
28034
+ session.closing = void 0;
28035
+ session.scrollback = "";
28036
+ session.startedAt = Date.now();
28037
+ session.runningMs = 0;
28038
+ session.runningSince = null;
28039
+ setStatus2(session, status3);
28040
+ session.restored = void 0;
28041
+ session.pty = respawn();
28042
+ if (session.cols && session.rows)
28043
+ try {
28044
+ session.pty?.resize(session.cols, session.rows);
28045
+ } catch {
28046
+ }
28047
+ broadcast(clients, { type: "clear", sessionId: session.id });
28048
+ wirePtyEvents(session, clients, onStatusChange);
28049
+ }
28050
+
28051
+ // src/commands/sessions/daemon/respawnPlan.ts
28052
+ import { randomUUID as randomUUID7 } from "crypto";
28053
+
28054
+ // src/commands/sessions/daemon/assistResumeArgs.ts
28055
+ function assistResumeArgs(session) {
28056
+ const args = ["assist", ...session.assistArgs];
28057
+ return session.claudeSessionId ? [...args, "--resume-session", session.claudeSessionId] : args;
28058
+ }
28059
+
28060
+ // src/commands/sessions/daemon/respawnPlan.ts
28061
+ function respawnPlan(session) {
28062
+ const { commandType, claudeSessionId, cwd, assistArgs, initialPrompt } = session;
28063
+ if (commandType === "claude") {
28064
+ if (claudeSessionId)
28065
+ return {
28066
+ spawn: () => spawnClaude2({
28067
+ resumeSessionId: claudeSessionId,
28068
+ cwd,
28069
+ sessionId: session.id
28070
+ }),
28071
+ status: "waiting"
28072
+ };
28073
+ if (initialPrompt) return freshClaudePlan(session, initialPrompt, cwd);
28074
+ if (cwd && session.harness !== "pi")
28075
+ return freshClaudePlan(session, void 0, cwd);
28076
+ return null;
28077
+ }
28078
+ if (commandType === "assist" && assistArgs) {
28079
+ const idle = session.status === "waiting";
28080
+ return {
28081
+ spawn: () => spawnPty(
28082
+ assistResumeArgs({ assistArgs, claudeSessionId }),
28083
+ cwd,
28084
+ session.id,
28085
+ idle ? { ASSIST_RESUME_IDLE: "1" } : void 0
28086
+ ),
28087
+ status: idle ? "waiting" : "running"
28088
+ };
28089
+ }
28090
+ return null;
28091
+ }
28092
+ function freshClaudePlan(session, prompt, cwd) {
28093
+ const claudeSessionId = randomUUID7();
28094
+ return {
28095
+ spawn: () => {
28096
+ session.claudeSessionId = claudeSessionId;
28097
+ return spawnClaude2({
28098
+ prompt,
28099
+ cwd,
28100
+ sessionId: session.id,
28101
+ claudeSessionId
28102
+ });
28103
+ },
28104
+ status: prompt ? "running" : "waiting"
28105
+ };
28001
28106
  }
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)
28107
+
28108
+ // src/commands/sessions/daemon/restartSession.ts
28109
+ function restartSession(session, clients, onStatusChange) {
28110
+ const plan2 = respawnPlan(session);
28111
+ if (!plan2) return false;
28112
+ const via = session.claudeSessionId ? `resume ${session.claudeSessionId}` : "fresh";
28113
+ const respawn = () => {
28114
+ respawnSession(session, plan2.spawn, plan2.status, clients, onStatusChange);
28115
+ daemonLog(`session ${session.id} restarted (${via}): ${session.name}`);
28116
+ };
28117
+ const pty2 = session.pty;
28118
+ const alive = session.status === "running" || session.status === "waiting";
28119
+ if (pty2 && alive) {
28120
+ daemonLog(
28121
+ `session ${session.id} restart requested: killing running process, will resume on exit`
28014
28122
  );
28015
- } catch {
28016
- return [];
28017
- } finally {
28018
- await handle?.close();
28123
+ session.pendingRestart = respawn;
28124
+ killPtyTree(pty2);
28125
+ } else {
28126
+ respawn();
28019
28127
  }
28128
+ return true;
28020
28129
  }
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)
28130
+
28131
+ // src/commands/sessions/daemon/restartManagedSession.ts
28132
+ function restartManagedSession(sessions, id, clients, onStatusChange) {
28133
+ const s = sessions.get(id);
28134
+ if (!s) {
28135
+ daemonLog(
28136
+ `restart requested for unknown session id=${id} (no live session; ignoring)`
28033
28137
  );
28034
- } catch {
28035
- return [];
28036
- } finally {
28037
- if (fd !== void 0) fs37.closeSync(fd);
28138
+ return {
28139
+ ok: false,
28140
+ reason: `Session ${id} is no longer tracked and can't be restarted.`
28141
+ };
28142
+ }
28143
+ daemonLog(`restart requested: id=${id} status=${s.status} (${s.name})`);
28144
+ if (!restartSession(s, clients, onStatusChange)) {
28145
+ daemonLog(`restart for session ${id} did nothing: no respawn plan`);
28146
+ return {
28147
+ ok: false,
28148
+ reason: `Session ${s.name} can't be restarted (no resumable command).`
28149
+ };
28038
28150
  }
28151
+ return { ok: true };
28039
28152
  }
28040
28153
 
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;
28154
+ // src/commands/sessions/daemon/restoreBase.ts
28155
+ function restoreBase(id, persisted) {
28156
+ return {
28157
+ id,
28158
+ name: persisted.name,
28159
+ title: persisted.title,
28160
+ subtitle: persisted.subtitle,
28161
+ commandType: persisted.commandType,
28162
+ harness: persisted.harness,
28163
+ scrollback: "",
28164
+ cwd: persisted.cwd,
28165
+ assistArgs: persisted.assistArgs,
28166
+ initialPrompt: persisted.initialPrompt,
28167
+ starred: persisted.starred
28168
+ };
28072
28169
  }
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
- }
28170
+
28171
+ // src/commands/sessions/daemon/errorSession.ts
28172
+ function errorSession(id, persisted, error) {
28173
+ return {
28174
+ ...restoreBase(id, persisted),
28175
+ status: "error",
28176
+ startedAt: persisted.startedAt,
28177
+ runningMs: persisted.runningMs ?? 0,
28178
+ runningSince: null,
28179
+ pty: null,
28180
+ runName: persisted.runName,
28181
+ runArgs: persisted.runArgs,
28182
+ error,
28183
+ restored: false
28184
+ };
28085
28185
  }
28086
28186
 
28087
28187
  // src/commands/sessions/daemon/deriveRestoreStatus.ts
@@ -28297,7 +28397,7 @@ function rearmStoppedSessions(sessions, notify2) {
28297
28397
  }
28298
28398
 
28299
28399
  // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
28300
- import { existsSync as existsSync68 } from "fs";
28400
+ import { existsSync as existsSync69 } from "fs";
28301
28401
  import { basename as basename18 } from "path";
28302
28402
 
28303
28403
  // src/commands/sessions/daemon/worktree/accountedTrees.ts
@@ -28387,9 +28487,9 @@ function capped(lines) {
28387
28487
  }
28388
28488
 
28389
28489
  // src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
28390
- import { existsSync as existsSync67 } from "fs";
28490
+ import { existsSync as existsSync68 } from "fs";
28391
28491
  async function reclaimVanishedWorktrees(clone, paths) {
28392
- if (!existsSync67(clone)) {
28492
+ if (!existsSync68(clone)) {
28393
28493
  for (const { path: path71 } of paths) forgetWorktree(path71);
28394
28494
  daemonLog(
28395
28495
  `clone ${clone} is gone; forgot ${paths.length} worktree record(s) it owned`
@@ -28482,7 +28582,7 @@ async function recoverOrphanedWorktrees(sessions, spawnWith, notify2) {
28482
28582
  const vanished = /* @__PURE__ */ new Map();
28483
28583
  for (const { path: path71, clone } of readWorktreeRegistry()) {
28484
28584
  if (accounted.has(path71)) continue;
28485
- if (!existsSync68(path71)) {
28585
+ if (!existsSync69(path71)) {
28486
28586
  vanished.set(clone, [
28487
28587
  ...vanished.get(clone) ?? [],
28488
28588
  { path: path71, branch: basename18(path71) }
@@ -28650,86 +28750,23 @@ function startHeldSession(session, sessions, clients, onStatusChange, notify2) {
28650
28750
  notify2();
28651
28751
  }
28652
28752
 
28753
+ // src/commands/sessions/daemon/startHeldInTree.ts
28754
+ function startHeldInTree(seeded, sessions, clients, onStatusChange, notify2) {
28755
+ const start3 = (session) => startHeldSession(session, sessions, clients, onStatusChange, notify2);
28756
+ start3(seeded);
28757
+ for (const session of sessions.values())
28758
+ if (session !== seeded && session.pendingStart && session.cwd === seeded.cwd)
28759
+ start3(session);
28760
+ }
28761
+
28653
28762
  // src/commands/sessions/daemon/treeSpawnContext.ts
28654
28763
  function treeSpawnContext(sessions, spawnWith, notify2, clients, onStatusChange) {
28655
28764
  return {
28656
28765
  sessions,
28657
28766
  spawnWith,
28658
28767
  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);
28768
+ startHeld: (session) => startHeldInTree(session, sessions, clients, onStatusChange, notify2)
28710
28769
  };
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
28770
  }
28734
28771
 
28735
28772
  // src/commands/sessions/daemon/connectToWindowsDaemon.ts
@@ -28934,8 +28971,11 @@ async function forwardWindowsCreate(conn, state, client, data) {
28934
28971
  }
28935
28972
  }
28936
28973
  function stripOutboundSessionId(data) {
28937
- if (typeof data.sessionId !== "string") return data;
28938
- return { ...data, sessionId: stripWindowsSessionId(data.sessionId) };
28974
+ const stripped = { ...data };
28975
+ for (const key of ["sessionId", "joinSessionId"])
28976
+ if (typeof stripped[key] === "string")
28977
+ stripped[key] = stripWindowsSessionId(stripped[key]);
28978
+ return stripped;
28939
28979
  }
28940
28980
 
28941
28981
  // src/commands/sessions/daemon/WindowsProxyState.ts
@@ -29388,6 +29428,46 @@ var WindowsProxy = class {
29388
29428
  }
29389
29429
  };
29390
29430
 
29431
+ // src/commands/sessions/daemon/worktree/joinableStream.ts
29432
+ function joinableStream(sessions, targetId) {
29433
+ const target = sessions.get(targetId);
29434
+ if (!target) return { reason: "no such session" };
29435
+ if (target.commandType === "run")
29436
+ return { reason: "a server run has no agent stream" };
29437
+ if (target.closing === true) return { reason: "the session is closing" };
29438
+ if (target.status !== "running" && target.status !== "waiting")
29439
+ return { reason: `the session is ${target.status}` };
29440
+ if (!target.cwd) return { reason: "the session has no working directory" };
29441
+ return { session: target };
29442
+ }
29443
+
29444
+ // src/commands/sessions/daemon/worktree/spawnIntoStream.ts
29445
+ function spawnIntoStream(ctx, target, prompt, harness) {
29446
+ const holdUntilSeeded = target.pendingStart !== void 0;
29447
+ const id = ctx.spawnWith(
29448
+ (sid) => createSession(sid, prompt, target.cwd, false, harness, holdUntilSeeded)
29449
+ );
29450
+ const joined = ctx.sessions.get(id);
29451
+ if (joined && target.worktree) joined.worktree = { ...target.worktree };
29452
+ daemonLog(
29453
+ `session ${id} added to the stream of session ${target.id} in ${target.cwd}: no workspace allocated${holdUntilSeeded ? ", held until that workspace finishes seeding" : ""}`
29454
+ );
29455
+ ctx.notify();
29456
+ return id;
29457
+ }
29458
+
29459
+ // src/commands/sessions/daemon/worktree/addAgentToStream.ts
29460
+ function addAgentToStream(ctx, targetId, prompt, harness) {
29461
+ const joinable = joinableStream(ctx.sessions, targetId);
29462
+ if ("reason" in joinable) {
29463
+ daemonLog(
29464
+ `create: cannot add an agent to session ${targetId}: ${joinable.reason}`
29465
+ );
29466
+ return void 0;
29467
+ }
29468
+ return spawnIntoStream(ctx, joinable.session, prompt, harness);
29469
+ }
29470
+
29391
29471
  // src/commands/sessions/daemon/createAssistSession.ts
29392
29472
  function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
29393
29473
  const startedAt = Date.now();
@@ -29595,13 +29675,13 @@ var SessionManager = class {
29595
29675
  return restoreAllSessions(this.spawnWith, this.sessions, this.notify);
29596
29676
  }
29597
29677
  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)));
29678
+ spawnWith = (create) => registerSpawnedSession(
29679
+ create(sessionLimits.nextId(this.sessions.size, this.idCounter)),
29680
+ this.sessions,
29681
+ this.clients,
29682
+ this.onStatusChange,
29683
+ this.notify
29684
+ );
29605
29685
  treeCtx() {
29606
29686
  return treeSpawnContext(
29607
29687
  this.sessions,
@@ -29614,6 +29694,9 @@ var SessionManager = class {
29614
29694
  spawn(prompt, cwd, design, harness) {
29615
29695
  return spawnInTree(this.treeCtx(), prompt, cwd, design, harness);
29616
29696
  }
29697
+ addAgent(targetId, prompt, harness) {
29698
+ return addAgentToStream(this.treeCtx(), targetId, prompt, harness);
29699
+ }
29617
29700
  spawnRun(runName, runArgs, cwd, meta) {
29618
29701
  return this.spawnWith(
29619
29702
  (id) => createRunSession(id, runName, runArgs, cwd, meta)
@@ -29629,15 +29712,11 @@ var SessionManager = class {
29629
29712
  return resumeInTree(this.treeCtx(), sessionId, cwd, name);
29630
29713
  }
29631
29714
  onStatusChange = makeStatusChangeHandler(
29715
+ this.sessions,
29632
29716
  (id) => this.dismissSession(id),
29633
29717
  () => this.notify(),
29634
29718
  (session, itemId2) => reuseSessionForRun(session, itemId2, this.clients, this.onStatusChange)
29635
29719
  );
29636
- wire(session) {
29637
- this.sessions.set(session.id, session);
29638
- wirePtyEvents(session, this.clients, this.onStatusChange);
29639
- this.notify();
29640
- }
29641
29720
  writeToSession(id, data) {
29642
29721
  writeToSession(this.sessions, id, data, this.onStatusChange);
29643
29722
  }
@@ -29883,16 +29962,20 @@ var lifecycleHandlers = {
29883
29962
  function spawnCreate(m, d) {
29884
29963
  const design = d.design === true;
29885
29964
  const harness = d.harness;
29965
+ const prompt = d.prompt;
29886
29966
  if (design)
29887
29967
  daemonLog(`create: design session (cwd=${d.cwd ?? ""})`);
29888
29968
  if (harness && harness !== "claude")
29889
29969
  daemonLog(`create: ${harness} session (cwd=${d.cwd ?? ""})`);
29890
- return m.spawn(
29891
- d.prompt,
29892
- d.cwd,
29893
- design,
29894
- harness
29895
- );
29970
+ const joinSessionId = design ? void 0 : d.joinSessionId;
29971
+ if (joinSessionId) {
29972
+ const joined = m.addAgent(joinSessionId, prompt, harness);
29973
+ if (joined) return joined;
29974
+ daemonLog(
29975
+ `create: falling back to a fresh isolated session (cwd=${d.cwd ?? ""})`
29976
+ );
29977
+ }
29978
+ return m.spawn(prompt, d.cwd, design, harness);
29896
29979
  }
29897
29980
 
29898
29981
  // src/commands/sessions/daemon/messageHandlers.ts