@staff0rd/assist 0.447.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.447.0",
9
+ version: "0.449.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -9039,11 +9039,34 @@ function getNestedValue(obj, path71) {
9039
9039
  return current;
9040
9040
  }
9041
9041
 
9042
+ // src/commands/config/readRawConfigLayers.ts
9043
+ function readRawConfigLayers(cwd) {
9044
+ const global = loadRawYaml(getGlobalConfigPath());
9045
+ return {
9046
+ project: loadRawYaml(getConfigPathFrom(cwd)),
9047
+ global,
9048
+ repoOverride: global.repos ? resolveRepoOverride(global, getCurrentOrigin(cwd)) : {}
9049
+ };
9050
+ }
9051
+
9052
+ // src/commands/config/resolveConfigSource.ts
9053
+ function resolveConfigSource(key, layers) {
9054
+ if (getNestedValue(layers.project, key) !== void 0) return "project";
9055
+ if (getNestedValue(layers.repoOverride, key) !== void 0) return "global";
9056
+ if (getNestedValue(layers.global, key) !== void 0) return "global";
9057
+ return "default";
9058
+ }
9059
+
9042
9060
  // src/commands/config/readConfigEntries.ts
9043
9061
  var KEYS_STRIPPED_FROM_MERGED_CONFIG = /* @__PURE__ */ new Set(["repos"]);
9044
9062
  function readConfigEntries(cwd) {
9045
9063
  const config = loadConfigFrom(cwd);
9046
- return describeConfigLeaves(assistConfigSchema).filter((leaf) => !KEYS_STRIPPED_FROM_MERGED_CONFIG.has(leaf.key)).map((leaf) => ({ ...leaf, value: getNestedValue(config, leaf.key) }));
9064
+ const layers = readRawConfigLayers(cwd);
9065
+ return describeConfigLeaves(assistConfigSchema).filter((leaf) => !KEYS_STRIPPED_FROM_MERGED_CONFIG.has(leaf.key)).map((leaf) => ({
9066
+ ...leaf,
9067
+ value: getNestedValue(config, leaf.key),
9068
+ source: resolveConfigSource(leaf.key, layers)
9069
+ }));
9047
9070
  }
9048
9071
 
9049
9072
  // src/commands/sessions/web/getConfig.ts
@@ -27132,13 +27155,6 @@ function greetClient(client, sessions, windowsProxy) {
27132
27155
  void windowsProxy.discover();
27133
27156
  }
27134
27157
 
27135
- // src/commands/sessions/daemon/logSpawnedSession.ts
27136
- function logSpawnedSession(session) {
27137
- daemonLog(
27138
- `session ${session.id} spawned: ${session.name} [${session.commandType}] ${session.cwd ?? ""}`
27139
- );
27140
- }
27141
-
27142
27158
  // src/commands/sessions/daemon/decidePrPreview.ts
27143
27159
  function decidePrPreview(sessions, waiters, notify2, d) {
27144
27160
  const id = d.sessionId;
@@ -27226,361 +27242,93 @@ var PrPreviewCoordinator = class {
27226
27242
  }
27227
27243
  };
27228
27244
 
27229
- // src/commands/sessions/daemon/applySetStatus.ts
27230
- function applySetStatus(sessions, id, status3, source, onStatusChange) {
27231
- const session = sessions.get(id);
27232
- if (!session) {
27233
- daemonLog(
27234
- `set-status for unknown session id=${id} status=${status3} (no live session; ignoring)`
27235
- );
27236
- return;
27237
- }
27238
- if (session.status === "stopped") {
27239
- daemonLog(
27240
- `set-status for stopped session id=${id} status=${status3} (held pending durable close; ignoring)`
27241
- );
27242
- return;
27243
- }
27244
- if (source === "permission") session.permissionActive = true;
27245
- if (status3 === "running") session.permissionActive = false;
27246
- onStatusChange(session, status3);
27247
- }
27248
-
27249
- // src/shared/db/recordPhaseCycleContext.ts
27250
- import { sql as sql21 } from "drizzle-orm";
27251
- async function recordPhaseCycleContext(db, itemId2, phaseIdx, window, resetsAt, pct) {
27252
- if (pct <= 0) return;
27253
- await db.insert(phaseCycleContext).values({ itemId: itemId2, phaseIdx, window, resetsAt, peakContextPct: pct }).onConflictDoUpdate({
27254
- target: [
27255
- phaseCycleContext.itemId,
27256
- phaseCycleContext.phaseIdx,
27257
- phaseCycleContext.window,
27258
- phaseCycleContext.resetsAt
27259
- ],
27260
- set: {
27261
- peakContextPct: sql21`GREATEST(${phaseCycleContext.peakContextPct}, ${pct})`
27262
- }
27263
- });
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
+ );
27264
27250
  }
27265
27251
 
27266
- // src/shared/db/recordPhasePeakContext.ts
27267
- import { sql as sql22 } from "drizzle-orm";
27268
- async function recordPhasePeakContext(db, itemId2, phaseIdx, pct) {
27269
- await db.insert(phaseUsage).values({ itemId: itemId2, phaseIdx, peakContextPct: pct }).onConflictDoUpdate({
27270
- target: [phaseUsage.itemId, phaseUsage.phaseIdx],
27271
- set: {
27272
- peakContextPct: sql22`GREATEST(${phaseUsage.peakContextPct}, ${pct})`
27273
- }
27274
- });
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 });
27275
27260
  }
27276
27261
 
27277
- // src/shared/db/recordPhaseTranscriptUsage.ts
27278
- import { sql as sql23 } from "drizzle-orm";
27279
- async function recordPhaseTranscriptUsage(db, itemId2, phaseIdx, responses) {
27280
- const byId = /* @__PURE__ */ new Map();
27281
- for (const r of responses) byId.set(r.messageId, r);
27282
- const unique = [...byId.values()];
27283
- if (unique.length === 0) return { tokensUp: 0, tokensDown: 0 };
27284
- return db.transaction(async (tx) => {
27285
- const inserted = await tx.insert(phaseUsageMessages).values(unique.map((r) => ({ itemId: itemId2, phaseIdx, messageId: r.messageId }))).onConflictDoNothing().returning({ messageId: phaseUsageMessages.messageId });
27286
- let tokensUp = 0;
27287
- let tokensDown = 0;
27288
- for (const { messageId } of inserted) {
27289
- const r = byId.get(messageId);
27290
- if (!r) continue;
27291
- tokensUp += r.outputTokens;
27292
- tokensDown += r.inputTokens;
27293
- }
27294
- if (tokensUp <= 0 && tokensDown <= 0) return { tokensUp: 0, tokensDown: 0 };
27295
- await tx.insert(phaseUsage).values({ itemId: itemId2, phaseIdx, tokensUp, tokensDown }).onConflictDoUpdate({
27296
- target: [phaseUsage.itemId, phaseUsage.phaseIdx],
27297
- set: {
27298
- tokensUp: sql23`${phaseUsage.tokensUp} + ${tokensUp}`,
27299
- tokensDown: sql23`${phaseUsage.tokensDown} + ${tokensDown}`
27300
- }
27301
- });
27302
- return { tokensUp, tokensDown };
27303
- });
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;
27304
27269
  }
27305
27270
 
27306
- // src/shared/db/recordWindowTokens.ts
27307
- import { and as and21, eq as eq40, sql as sql24 } from "drizzle-orm";
27308
- async function recordWindowTokens(db, window, resetsAt, tokensUp, tokensDown) {
27309
- if (tokensUp <= 0 && tokensDown <= 0) return;
27310
- await db.update(usagePeaks).set({
27311
- tokensUp: sql24`${usagePeaks.tokensUp} + ${tokensUp}`,
27312
- tokensDown: sql24`${usagePeaks.tokensDown} + ${tokensDown}`
27313
- }).where(
27314
- and21(
27315
- eq40(usagePeaks.window, window),
27316
- eq40(usagePeaks.resetsAt, resetsAt),
27317
- eq40(usagePeaks.segment, 0)
27318
- )
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}`
27319
27278
  );
27279
+ onStatusChange(session, "error", exitCode);
27280
+ return true;
27320
27281
  }
27321
27282
 
27322
- // src/commands/sessions/shared/transcriptUsage.ts
27323
- import * as fs36 from "fs";
27324
- function transcriptUsage(lines) {
27325
- const byId = /* @__PURE__ */ new Map();
27326
- for (const line of lines) {
27327
- if (!line.trim()) continue;
27328
- let entry;
27329
- try {
27330
- entry = JSON.parse(line);
27331
- } catch {
27332
- continue;
27333
- }
27334
- if (entry.type !== "assistant") continue;
27335
- const id = entry.message?.id;
27336
- const usage = entry.message?.usage;
27337
- if (!id || !usage) continue;
27338
- byId.set(id, {
27339
- messageId: id,
27340
- inputTokens: usage.input_tokens ?? 0,
27341
- outputTokens: usage.output_tokens ?? 0
27342
- });
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;
27343
27295
  }
27344
- return [...byId.values()];
27345
27296
  }
27346
- async function readTranscriptUsage(transcriptPath) {
27347
- const content = await fs36.promises.readFile(transcriptPath, "utf8");
27348
- 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;
27349
27305
  }
27350
27306
 
27351
- // src/commands/sessions/daemon/persistPhaseTokens.ts
27352
- async function persistPhaseTokens(itemId2, phaseIdx, transcriptPath, usedPct, windows) {
27307
+ // src/commands/sessions/daemon/loadPhaseActiveMs.ts
27308
+ async function loadPhaseActiveMs(itemId2, phaseIdx) {
27353
27309
  try {
27354
- if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url) return;
27355
- const db = await getDb();
27356
- if (usedPct !== void 0) {
27357
- await recordPhasePeakContext(db, itemId2, phaseIdx, usedPct);
27358
- for (const { window, resetsAt } of windows) {
27359
- await recordPhaseCycleContext(
27360
- db,
27361
- itemId2,
27362
- phaseIdx,
27363
- window,
27364
- resetsAt,
27365
- usedPct
27366
- );
27367
- }
27368
- }
27369
- const responses = await readTranscriptUsage(transcriptPath);
27370
- if (responses.length === 0) return;
27371
- const { tokensUp, tokensDown } = await recordPhaseTranscriptUsage(
27372
- db,
27373
- itemId2,
27374
- phaseIdx,
27375
- responses
27376
- );
27377
- for (const { window, resetsAt } of windows) {
27378
- await recordWindowTokens(db, window, resetsAt, tokensUp, tokensDown);
27379
- }
27310
+ if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url)
27311
+ return null;
27312
+ return await getPhaseActiveMs(await getDb(), itemId2, phaseIdx);
27380
27313
  } catch (error) {
27381
- daemonLog(`phase-tokens persist failed: ${String(error)}`);
27382
- }
27383
- }
27384
-
27385
- // src/commands/sessions/daemon/recordSessionUsage.ts
27386
- function recordSessionUsage(sessions, claudeSessionId, transcriptPath, usedPct, windows) {
27387
- for (const session of sessions) {
27388
- if (session.claudeSessionId !== claudeSessionId) continue;
27389
- session.usedPct = usedPct;
27390
- const phase = sessionBacklogPhase(session);
27391
- if (phase && transcriptPath)
27392
- void persistPhaseTokens(
27393
- phase.itemId,
27394
- phase.phaseIdx,
27395
- transcriptPath,
27396
- usedPct,
27397
- windows
27398
- );
27399
- void flushPhaseActiveMs(session);
27400
- return true;
27314
+ daemonLog(`phase-active-ms load failed: ${String(error)}`);
27315
+ return null;
27401
27316
  }
27402
- return false;
27403
27317
  }
27404
27318
 
27405
- // src/commands/sessions/daemon/applyUsageRecord.ts
27406
- function applyUsageRecord(sessions, windows, notify2, claudeSessionId, transcriptPath, usedPct) {
27407
- if (recordSessionUsage(
27408
- sessions,
27409
- claudeSessionId,
27410
- transcriptPath,
27411
- usedPct,
27412
- windows
27413
- ))
27414
- notify2();
27415
- }
27416
-
27417
- // src/commands/sessions/daemon/shouldAutoDismiss.ts
27418
- function shouldAutoDismiss(session, exitCode) {
27419
- if (session.status !== "done" || exitCode !== 0) {
27420
- return false;
27421
- }
27422
- if (session.reviewStarted) {
27423
- return session.autoAdvance === true;
27424
- }
27425
- const args = session.assistArgs;
27426
- if (args === void 0) return false;
27427
- if (args[0] === "update") return true;
27428
- return args.includes("--once") && args[0] !== "next";
27429
- }
27430
-
27431
- // src/commands/sessions/daemon/shouldAutoRun.ts
27432
- function shouldAutoRun(session, status3 = session.status) {
27433
- if (!session.autoRun) return { run: false, reason: null };
27434
- if (status3 !== "done")
27435
- return {
27436
- run: false,
27437
- reason: `session status is "${status3}", not "done"`
27438
- };
27439
- if (session.commandType !== "assist")
27440
- return {
27441
- run: false,
27442
- reason: `command type is "${session.commandType}", not "assist"`
27443
- };
27444
- const cmd = session.assistArgs?.[0];
27445
- if (cmd !== "draft" && cmd !== "bug" && cmd !== "refine")
27446
- return {
27447
- run: false,
27448
- reason: `command "${cmd ?? "(none)"}" is not draft/bug/refine`
27449
- };
27450
- const itemId2 = session.activity?.itemId;
27451
- if (itemId2 == null)
27452
- return {
27453
- run: false,
27454
- reason: "no itemId in activity (draft did not create an item)"
27455
- };
27456
- return { run: true, itemId: itemId2 };
27457
- }
27458
-
27459
- // src/commands/sessions/daemon/applyStatusChange.ts
27460
- function applyStatusChange(session, status3, exitCode, dismiss, notify2, reuseForRun) {
27461
- if (session.status === status3) return;
27462
- const deps2 = { dismiss, notify: notify2, reuseForRun };
27463
- const chainsToNextRun = shouldAutoRun(session, status3).run;
27464
- if (status3 === "done" && session.worktree && !chainsToNextRun) {
27465
- session.closing = true;
27466
- daemonLog(
27467
- `session ${session.id} closing: checking durability of ${session.worktree.path} before reap`
27468
- );
27469
- notify2();
27470
- void resolveCloseDurability(
27471
- session,
27472
- () => finishStatusChange(session, "done", exitCode, deps2),
27473
- notify2
27474
- );
27475
- return;
27476
- }
27477
- finishStatusChange(session, status3, exitCode, deps2);
27478
- }
27479
- function finishStatusChange(session, status3, exitCode, deps2) {
27480
- daemonLog(`session ${session.id} status: ${session.status} -> ${status3}`);
27481
- void flushPhaseActiveMs(session);
27482
- setStatus2(session, status3);
27483
- if (status3 !== "stopped") session.undurable = void 0;
27484
- const autoRun = shouldAutoRun(session);
27485
- if (autoRun.run) {
27486
- deps2.reuseForRun(session, autoRun.itemId);
27487
- deps2.notify();
27488
- return;
27489
- }
27490
- if (status3 === "done" && autoRun.reason != null) {
27491
- daemonLog(
27492
- `session ${session.id} auto-run enabled but skipped (exit ${exitCode ?? "none"}): ${autoRun.reason}`
27493
- );
27494
- }
27495
- if (shouldAutoDismiss(session, exitCode)) deps2.dismiss(session.id);
27496
- else deps2.notify();
27497
- }
27498
-
27499
- // src/commands/sessions/daemon/makeStatusChangeHandler.ts
27500
- function makeStatusChangeHandler(dismiss, notify2, reuseForRun) {
27501
- return (s, status3, exitCode) => applyStatusChange(s, status3, exitCode, dismiss, notify2, reuseForRun);
27502
- }
27503
-
27504
- // src/commands/sessions/daemon/emitSessionOutput.ts
27505
- var MAX_SCROLLBACK = 256 * 1024;
27506
- function emitSessionOutput(session, clients, data) {
27507
- session.scrollback += data;
27508
- if (session.scrollback.length > MAX_SCROLLBACK) {
27509
- session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK);
27510
- }
27511
- broadcast(clients, { type: "output", sessionId: session.id, data });
27512
- }
27513
-
27514
- // src/commands/sessions/daemon/exitReason.ts
27515
- import { existsSync as existsSync64 } from "fs";
27516
- function exitReason(session, exitCode) {
27517
- const base = `process exited with code ${exitCode}`;
27518
- if (session.cwd && !existsSync64(session.cwd))
27519
- return `${base}: working directory ${session.cwd} no longer exists`;
27520
- return base;
27521
- }
27522
-
27523
- // src/commands/sessions/daemon/handleFailedResume.ts
27524
- function handleFailedResume(session, exitCode, onStatusChange) {
27525
- const failed2 = session.restored === true && exitCode !== 0 && session.scrollback.length === 0;
27526
- if (!failed2) return false;
27527
- session.error = `resume process exited with code ${exitCode} before producing any output`;
27528
- daemonLog(
27529
- `could not resume restored session "${session.name}" (id ${session.id}): ${session.error}`
27530
- );
27531
- onStatusChange(session, "error", exitCode);
27532
- return true;
27533
- }
27534
-
27535
- // src/commands/sessions/daemon/watchActivity.ts
27536
- import { existsSync as existsSync65, mkdirSync as mkdirSync26, watch as watch2 } from "fs";
27537
- import { dirname as dirname31 } from "path";
27538
-
27539
- // src/commands/sessions/daemon/applyReviewPause.ts
27540
- function applyReviewPause(session, activity2) {
27541
- const inReview = activity2.kind === "backlog" && activity2.phase !== void 0 && activity2.phase === activity2.totalPhases;
27542
- if (inReview && !session.reviewStarted) {
27543
- session.autoAdvance = false;
27544
- session.reviewStarted = true;
27545
- } else if (!inReview && session.reviewStarted) {
27546
- session.reviewStarted = false;
27547
- }
27548
- }
27549
-
27550
- // src/shared/db/getPhaseActiveMs.ts
27551
- import { and as and22, eq as eq41 } from "drizzle-orm";
27552
- async function getPhaseActiveMs(db, itemId2, phaseIdx) {
27553
- const [row] = await db.select({ activeMs: phaseUsage.activeMs }).from(phaseUsage).where(
27554
- and22(eq41(phaseUsage.itemId, itemId2), eq41(phaseUsage.phaseIdx, phaseIdx))
27555
- );
27556
- return row?.activeMs ?? 0;
27557
- }
27558
-
27559
- // src/commands/sessions/daemon/loadPhaseActiveMs.ts
27560
- async function loadPhaseActiveMs(itemId2, phaseIdx) {
27561
- try {
27562
- if (!process.env.ASSIST_DATABASE_URL && !loadConfig().database.url)
27563
- return null;
27564
- return await getPhaseActiveMs(await getDb(), itemId2, phaseIdx);
27565
- } catch (error) {
27566
- daemonLog(`phase-active-ms load failed: ${String(error)}`);
27567
- return null;
27568
- }
27569
- }
27570
-
27571
- // src/commands/sessions/daemon/seedRunningMsFromUsage.ts
27572
- async function seedRunningMsFromUsage(session, notify2) {
27573
- if (session.usageSeeded) return;
27574
- const phase = sessionBacklogPhase(session);
27575
- if (!phase) return;
27576
- session.usageSeeded = true;
27577
- const priorMs = await loadPhaseActiveMs(phase.itemId, phase.phaseIdx);
27578
- if (priorMs == null || priorMs <= session.runningMs) return;
27579
- session.runningMs = priorMs;
27580
- daemonLog(
27581
- `session ${session.id} seeded runningMs=${priorMs} from item ${phase.itemId} phase ${phase.phaseIdx} recorded usage`
27582
- );
27583
- 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();
27584
27332
  }
27585
27333
 
27586
27334
  // src/commands/sessions/daemon/watchActivity.ts
@@ -27698,163 +27446,53 @@ function reportSilentFailure(session, clients) {
27698
27446
  );
27699
27447
  }
27700
27448
 
27701
- // src/commands/sessions/daemon/respawnSession.ts
27702
- function respawnSession(session, respawn, status3, clients, onStatusChange) {
27703
- session.gitWatcher?.close();
27704
- session.gitWatcher = void 0;
27705
- session.undurable = void 0;
27706
- session.pendingDismiss = void 0;
27707
- session.closing = void 0;
27708
- session.scrollback = "";
27709
- session.startedAt = Date.now();
27710
- session.runningMs = 0;
27711
- session.runningSince = null;
27712
- setStatus2(session, status3);
27713
- session.restored = void 0;
27714
- session.pty = respawn();
27715
- if (session.cols && session.rows)
27716
- try {
27717
- session.pty?.resize(session.cols, session.rows);
27718
- } catch {
27719
- }
27720
- broadcast(clients, { type: "clear", sessionId: session.id });
27721
- wirePtyEvents(session, clients, onStatusChange);
27722
- }
27723
-
27724
- // src/commands/sessions/daemon/respawnPlan.ts
27725
- import { randomUUID as randomUUID7 } from "crypto";
27449
+ // src/commands/sessions/daemon/watchTranscript.ts
27450
+ import { existsSync as existsSync67, watch as watch3 } from "fs";
27726
27451
 
27727
- // src/commands/sessions/daemon/assistResumeArgs.ts
27728
- function assistResumeArgs(session) {
27729
- const args = ["assist", ...session.assistArgs];
27730
- 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
+ );
27731
27463
  }
27732
-
27733
- // src/commands/sessions/daemon/respawnPlan.ts
27734
- function respawnPlan(session) {
27735
- const { commandType, claudeSessionId, cwd, assistArgs, initialPrompt } = session;
27736
- if (commandType === "claude") {
27737
- if (claudeSessionId)
27738
- return {
27739
- spawn: () => spawnClaude2({
27740
- resumeSessionId: claudeSessionId,
27741
- cwd,
27742
- sessionId: session.id
27743
- }),
27744
- status: "waiting"
27745
- };
27746
- if (initialPrompt) return freshClaudePlan(session, initialPrompt, cwd);
27747
- if (cwd && session.harness !== "pi")
27748
- 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 {
27749
27475
  return null;
27750
27476
  }
27751
- if (commandType === "assist" && assistArgs) {
27752
- const idle = session.status === "waiting";
27753
- return {
27754
- spawn: () => spawnPty(
27755
- assistResumeArgs({ assistArgs, claudeSessionId }),
27756
- cwd,
27757
- session.id,
27758
- idle ? { ASSIST_RESUME_IDLE: "1" } : void 0
27759
- ),
27760
- status: idle ? "waiting" : "running"
27761
- };
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);
27762
27481
  }
27763
27482
  return null;
27764
27483
  }
27765
- function freshClaudePlan(session, prompt, cwd) {
27766
- const claudeSessionId = randomUUID7();
27767
- return {
27768
- spawn: () => {
27769
- session.claudeSessionId = claudeSessionId;
27770
- return spawnClaude2({
27771
- prompt,
27772
- cwd,
27773
- sessionId: session.id,
27774
- claudeSessionId
27775
- });
27776
- },
27777
- status: prompt ? "running" : "waiting"
27778
- };
27779
- }
27780
-
27781
- // src/commands/sessions/daemon/restartSession.ts
27782
- function restartSession(session, clients, onStatusChange) {
27783
- const plan2 = respawnPlan(session);
27784
- if (!plan2) return false;
27785
- const via = session.claudeSessionId ? `resume ${session.claudeSessionId}` : "fresh";
27786
- const respawn = () => {
27787
- respawnSession(session, plan2.spawn, plan2.status, clients, onStatusChange);
27788
- daemonLog(`session ${session.id} restarted (${via}): ${session.name}`);
27789
- };
27790
- const pty2 = session.pty;
27791
- const alive = session.status === "running" || session.status === "waiting";
27792
- if (pty2 && alive) {
27793
- daemonLog(
27794
- `session ${session.id} restart requested: killing running process, will resume on exit`
27795
- );
27796
- session.pendingRestart = respawn;
27797
- killPtyTree(pty2);
27798
- } else {
27799
- respawn();
27800
- }
27801
- return true;
27802
- }
27803
-
27804
- // src/commands/sessions/daemon/restartManagedSession.ts
27805
- function restartManagedSession(sessions, id, clients, onStatusChange) {
27806
- const s = sessions.get(id);
27807
- if (!s) {
27808
- daemonLog(
27809
- `restart requested for unknown session id=${id} (no live session; ignoring)`
27810
- );
27811
- return {
27812
- ok: false,
27813
- reason: `Session ${id} is no longer tracked and can't be restarted.`
27814
- };
27815
- }
27816
- daemonLog(`restart requested: id=${id} status=${s.status} (${s.name})`);
27817
- if (!restartSession(s, clients, onStatusChange)) {
27818
- daemonLog(`restart for session ${id} did nothing: no respawn plan`);
27819
- return {
27820
- ok: false,
27821
- reason: `Session ${s.name} can't be restarted (no resumable command).`
27822
- };
27823
- }
27824
- return { ok: true };
27825
- }
27826
-
27827
- // src/commands/sessions/daemon/restoreBase.ts
27828
- function restoreBase(id, persisted) {
27829
- return {
27830
- id,
27831
- name: persisted.name,
27832
- title: persisted.title,
27833
- subtitle: persisted.subtitle,
27834
- commandType: persisted.commandType,
27835
- harness: persisted.harness,
27836
- scrollback: "",
27837
- cwd: persisted.cwd,
27838
- assistArgs: persisted.assistArgs,
27839
- initialPrompt: persisted.initialPrompt,
27840
- starred: persisted.starred
27841
- };
27842
- }
27843
-
27844
- // src/commands/sessions/daemon/errorSession.ts
27845
- function errorSession(id, persisted, error) {
27846
- return {
27847
- ...restoreBase(id, persisted),
27848
- status: "error",
27849
- startedAt: persisted.startedAt,
27850
- runningMs: persisted.runningMs ?? 0,
27851
- runningSince: null,
27852
- pty: null,
27853
- runName: persisted.runName,
27854
- runArgs: persisted.runArgs,
27855
- error,
27856
- restored: false
27857
- };
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);
27495
+ }
27858
27496
  }
27859
27497
 
27860
27498
  // src/commands/sessions/shared/normalizeUser.ts
@@ -27971,94 +27609,579 @@ function parseTailEntries(raw) {
27971
27609
  }
27972
27610
  return entries;
27973
27611
  }
27974
- function dropPartialFirstLine(raw, sliced) {
27975
- if (!sliced) return raw;
27976
- const newline = raw.indexOf("\n");
27977
- 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
+ };
27978
28106
  }
27979
- async function readTranscriptTail(filePath, maxBytes = DEFAULT_MAX_BYTES) {
27980
- let handle;
27981
- try {
27982
- handle = await fs37.promises.open(filePath, "r");
27983
- const { size } = await handle.stat();
27984
- const start3 = Math.max(0, size - maxBytes);
27985
- const length = size - start3;
27986
- if (length === 0) return [];
27987
- const buf = Buffer.alloc(length);
27988
- await handle.read(buf, 0, length, start3);
27989
- return parseTailEntries(
27990
- 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`
27991
28122
  );
27992
- } catch {
27993
- return [];
27994
- } finally {
27995
- await handle?.close();
28123
+ session.pendingRestart = respawn;
28124
+ killPtyTree(pty2);
28125
+ } else {
28126
+ respawn();
27996
28127
  }
28128
+ return true;
27997
28129
  }
27998
- function readTranscriptTailSync(filePath, maxBytes = DEFAULT_MAX_BYTES) {
27999
- let fd;
28000
- try {
28001
- fd = fs37.openSync(filePath, "r");
28002
- const { size } = fs37.fstatSync(fd);
28003
- const start3 = Math.max(0, size - maxBytes);
28004
- const length = size - start3;
28005
- if (length === 0) return [];
28006
- const buf = Buffer.alloc(length);
28007
- fs37.readSync(fd, buf, 0, length, start3);
28008
- return parseTailEntries(
28009
- 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)`
28010
28137
  );
28011
- } catch {
28012
- return [];
28013
- } finally {
28014
- 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
+ };
28015
28150
  }
28151
+ return { ok: true };
28016
28152
  }
28017
28153
 
28018
- // src/commands/sessions/shared/findTranscriptPathSync.ts
28019
- import * as fs38 from "fs";
28020
- import * as os4 from "os";
28021
- import * as path60 from "path";
28022
- function projectDirForCwd(cwd) {
28023
- return path60.join(
28024
- os4.homedir(),
28025
- ".claude",
28026
- "projects",
28027
- cwd.replace(/[^a-zA-Z0-9]/g, "-")
28028
- );
28029
- }
28030
- function transcriptPathFor(cwd, claudeSessionId) {
28031
- return path60.join(projectDirForCwd(cwd), `${claudeSessionId}.jsonl`);
28032
- }
28033
- function findTranscriptPathSync(cwd, claudeSessionId) {
28034
- const direct = transcriptPathFor(cwd, claudeSessionId);
28035
- if (fs38.existsSync(direct)) return direct;
28036
- const dir = projectDirForCwd(cwd);
28037
- let files;
28038
- try {
28039
- files = fs38.readdirSync(dir);
28040
- } catch {
28041
- return null;
28042
- }
28043
- for (const file of files) {
28044
- if (!file.endsWith(".jsonl")) continue;
28045
- if (headContainsSessionId(path60.join(dir, file), claudeSessionId))
28046
- return path60.join(dir, file);
28047
- }
28048
- 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
+ };
28049
28169
  }
28050
- function headContainsSessionId(filePath, claudeSessionId) {
28051
- let fd;
28052
- try {
28053
- fd = fs38.openSync(filePath, "r");
28054
- const buf = Buffer.alloc(16384);
28055
- const bytesRead = fs38.readSync(fd, buf, 0, buf.length, 0);
28056
- return buf.toString("utf8", 0, bytesRead).includes(claudeSessionId);
28057
- } catch {
28058
- return false;
28059
- } finally {
28060
- if (fd !== void 0) fs38.closeSync(fd);
28061
- }
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
+ };
28062
28185
  }
28063
28186
 
28064
28187
  // src/commands/sessions/daemon/deriveRestoreStatus.ts
@@ -28274,7 +28397,7 @@ function rearmStoppedSessions(sessions, notify2) {
28274
28397
  }
28275
28398
 
28276
28399
  // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
28277
- import { existsSync as existsSync68 } from "fs";
28400
+ import { existsSync as existsSync69 } from "fs";
28278
28401
  import { basename as basename18 } from "path";
28279
28402
 
28280
28403
  // src/commands/sessions/daemon/worktree/accountedTrees.ts
@@ -28364,9 +28487,9 @@ function capped(lines) {
28364
28487
  }
28365
28488
 
28366
28489
  // src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
28367
- import { existsSync as existsSync67 } from "fs";
28490
+ import { existsSync as existsSync68 } from "fs";
28368
28491
  async function reclaimVanishedWorktrees(clone, paths) {
28369
- if (!existsSync67(clone)) {
28492
+ if (!existsSync68(clone)) {
28370
28493
  for (const { path: path71 } of paths) forgetWorktree(path71);
28371
28494
  daemonLog(
28372
28495
  `clone ${clone} is gone; forgot ${paths.length} worktree record(s) it owned`
@@ -28459,7 +28582,7 @@ async function recoverOrphanedWorktrees(sessions, spawnWith, notify2) {
28459
28582
  const vanished = /* @__PURE__ */ new Map();
28460
28583
  for (const { path: path71, clone } of readWorktreeRegistry()) {
28461
28584
  if (accounted.has(path71)) continue;
28462
- if (!existsSync68(path71)) {
28585
+ if (!existsSync69(path71)) {
28463
28586
  vanished.set(clone, [
28464
28587
  ...vanished.get(clone) ?? [],
28465
28588
  { path: path71, branch: basename18(path71) }
@@ -28627,86 +28750,23 @@ function startHeldSession(session, sessions, clients, onStatusChange, notify2) {
28627
28750
  notify2();
28628
28751
  }
28629
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
+
28630
28762
  // src/commands/sessions/daemon/treeSpawnContext.ts
28631
28763
  function treeSpawnContext(sessions, spawnWith, notify2, clients, onStatusChange) {
28632
28764
  return {
28633
28765
  sessions,
28634
28766
  spawnWith,
28635
28767
  notify: notify2,
28636
- startHeld: (session) => startHeldSession(session, sessions, clients, onStatusChange, notify2)
28637
- };
28638
- }
28639
-
28640
- // src/commands/sessions/daemon/watchTranscript.ts
28641
- import { existsSync as existsSync69, watch as watch3 } from "fs";
28642
-
28643
- // src/commands/sessions/daemon/reconcileTranscriptStatus.ts
28644
- async function resolveTranscriptPath(session) {
28645
- if (session.transcriptPath) return session.transcriptPath;
28646
- if (!session.claudeSessionId) return null;
28647
- const direct = session.cwd ? findTranscriptPathSync(session.cwd, session.claudeSessionId) : null;
28648
- const filePath = direct ?? await findSessionJsonlPath(session.claudeSessionId);
28649
- if (filePath) session.transcriptPath = filePath;
28650
- return filePath;
28651
- }
28652
- async function reconcileTranscriptStatus(session, onStatusChange) {
28653
- if (session.status === "done" || session.status === "error" || session.status === "stopped")
28654
- return;
28655
- const filePath = await resolveTranscriptPath(session);
28656
- if (!filePath) return;
28657
- const entries = await readTranscriptTail(filePath);
28658
- const derived = deriveTranscriptStatus(entries, {
28659
- permissionActive: session.permissionActive
28660
- });
28661
- if (!derived) return;
28662
- if (derived === "running") session.permissionActive = false;
28663
- if (derived === session.status) return;
28664
- daemonLog(
28665
- `session ${session.id} transcript reconcile: ${session.status} -> ${derived}`
28666
- );
28667
- onStatusChange(session, derived);
28668
- }
28669
-
28670
- // src/commands/sessions/daemon/watchTranscript.ts
28671
- var DEBOUNCE_MS3 = 100;
28672
- function watchTranscript(session, onStatusChange) {
28673
- if (!session.cwd || !session.claudeSessionId) return;
28674
- if (session.watchedTranscriptId === session.claudeSessionId) return;
28675
- const dir = projectDirForCwd(session.cwd);
28676
- if (!existsSync69(dir)) return;
28677
- if (session.transcriptWatcher) {
28678
- session.transcriptWatcher.close();
28679
- session.transcriptWatcher = void 0;
28680
- }
28681
- session.transcriptPath = void 0;
28682
- const fileName = `${session.claudeSessionId}.jsonl`;
28683
- let timer = null;
28684
- const run4 = () => {
28685
- timer = null;
28686
- void reconcileTranscriptStatus(session, onStatusChange);
28768
+ startHeld: (session) => startHeldInTree(session, sessions, clients, onStatusChange, notify2)
28687
28769
  };
28688
- try {
28689
- session.transcriptWatcher = watch3(dir, (_event, changed) => {
28690
- if (changed && changed !== fileName) return;
28691
- if (timer) clearTimeout(timer);
28692
- timer = setTimeout(run4, DEBOUNCE_MS3);
28693
- });
28694
- } catch (error) {
28695
- daemonLog(
28696
- `session ${session.id} transcript watch failed: ${error instanceof Error ? error.message : String(error)}`
28697
- );
28698
- return;
28699
- }
28700
- session.watchedTranscriptId = session.claudeSessionId;
28701
- daemonLog(`session ${session.id} watching transcript ${fileName}`);
28702
- run4();
28703
- }
28704
-
28705
- // src/commands/sessions/daemon/wireSessionWatchers.ts
28706
- function wireSessionWatchers(session, notify2, onStatusChange) {
28707
- const wireTranscript = (s) => watchTranscript(s, onStatusChange);
28708
- watchActivity(session, notify2, wireTranscript);
28709
- wireTranscript(session);
28710
28770
  }
28711
28771
 
28712
28772
  // src/commands/sessions/daemon/connectToWindowsDaemon.ts
@@ -28911,8 +28971,11 @@ async function forwardWindowsCreate(conn, state, client, data) {
28911
28971
  }
28912
28972
  }
28913
28973
  function stripOutboundSessionId(data) {
28914
- if (typeof data.sessionId !== "string") return data;
28915
- 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;
28916
28979
  }
28917
28980
 
28918
28981
  // src/commands/sessions/daemon/WindowsProxyState.ts
@@ -29365,6 +29428,46 @@ var WindowsProxy = class {
29365
29428
  }
29366
29429
  };
29367
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
+
29368
29471
  // src/commands/sessions/daemon/createAssistSession.ts
29369
29472
  function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
29370
29473
  const startedAt = Date.now();
@@ -29572,13 +29675,13 @@ var SessionManager = class {
29572
29675
  return restoreAllSessions(this.spawnWith, this.sessions, this.notify);
29573
29676
  }
29574
29677
  drain = () => drainSessions(this.sessions, this.notify);
29575
- add(session) {
29576
- this.wire(session);
29577
- wireSessionWatchers(session, this.notify, this.onStatusChange);
29578
- logSpawnedSession(session);
29579
- return session.id;
29580
- }
29581
- 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
+ );
29582
29685
  treeCtx() {
29583
29686
  return treeSpawnContext(
29584
29687
  this.sessions,
@@ -29591,6 +29694,9 @@ var SessionManager = class {
29591
29694
  spawn(prompt, cwd, design, harness) {
29592
29695
  return spawnInTree(this.treeCtx(), prompt, cwd, design, harness);
29593
29696
  }
29697
+ addAgent(targetId, prompt, harness) {
29698
+ return addAgentToStream(this.treeCtx(), targetId, prompt, harness);
29699
+ }
29594
29700
  spawnRun(runName, runArgs, cwd, meta) {
29595
29701
  return this.spawnWith(
29596
29702
  (id) => createRunSession(id, runName, runArgs, cwd, meta)
@@ -29606,15 +29712,11 @@ var SessionManager = class {
29606
29712
  return resumeInTree(this.treeCtx(), sessionId, cwd, name);
29607
29713
  }
29608
29714
  onStatusChange = makeStatusChangeHandler(
29715
+ this.sessions,
29609
29716
  (id) => this.dismissSession(id),
29610
29717
  () => this.notify(),
29611
29718
  (session, itemId2) => reuseSessionForRun(session, itemId2, this.clients, this.onStatusChange)
29612
29719
  );
29613
- wire(session) {
29614
- this.sessions.set(session.id, session);
29615
- wirePtyEvents(session, this.clients, this.onStatusChange);
29616
- this.notify();
29617
- }
29618
29720
  writeToSession(id, data) {
29619
29721
  writeToSession(this.sessions, id, data, this.onStatusChange);
29620
29722
  }
@@ -29860,16 +29962,20 @@ var lifecycleHandlers = {
29860
29962
  function spawnCreate(m, d) {
29861
29963
  const design = d.design === true;
29862
29964
  const harness = d.harness;
29965
+ const prompt = d.prompt;
29863
29966
  if (design)
29864
29967
  daemonLog(`create: design session (cwd=${d.cwd ?? ""})`);
29865
29968
  if (harness && harness !== "claude")
29866
29969
  daemonLog(`create: ${harness} session (cwd=${d.cwd ?? ""})`);
29867
- return m.spawn(
29868
- d.prompt,
29869
- d.cwd,
29870
- design,
29871
- harness
29872
- );
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);
29873
29979
  }
29874
29980
 
29875
29981
  // src/commands/sessions/daemon/messageHandlers.ts