@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/README.md +2 -0
- package/dist/commands/sessions/web/bundle.js +378 -378
- package/dist/index.js +763 -680
- package/package.json +1 -1
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.
|
|
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/
|
|
27253
|
-
function
|
|
27254
|
-
|
|
27255
|
-
|
|
27256
|
-
|
|
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/
|
|
27290
|
-
|
|
27291
|
-
|
|
27292
|
-
|
|
27293
|
-
|
|
27294
|
-
|
|
27295
|
-
|
|
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/
|
|
27301
|
-
import {
|
|
27302
|
-
|
|
27303
|
-
const
|
|
27304
|
-
|
|
27305
|
-
|
|
27306
|
-
|
|
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/
|
|
27330
|
-
|
|
27331
|
-
|
|
27332
|
-
if (
|
|
27333
|
-
|
|
27334
|
-
|
|
27335
|
-
|
|
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/
|
|
27346
|
-
import
|
|
27347
|
-
|
|
27348
|
-
|
|
27349
|
-
|
|
27350
|
-
|
|
27351
|
-
|
|
27352
|
-
|
|
27353
|
-
|
|
27354
|
-
|
|
27355
|
-
|
|
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
|
-
|
|
27370
|
-
|
|
27371
|
-
|
|
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/
|
|
27375
|
-
async function
|
|
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)
|
|
27378
|
-
|
|
27379
|
-
|
|
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-
|
|
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/
|
|
27429
|
-
function
|
|
27430
|
-
if (
|
|
27431
|
-
|
|
27432
|
-
|
|
27433
|
-
|
|
27434
|
-
|
|
27435
|
-
|
|
27436
|
-
|
|
27437
|
-
|
|
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/
|
|
27441
|
-
|
|
27442
|
-
|
|
27443
|
-
|
|
27444
|
-
|
|
27445
|
-
|
|
27446
|
-
|
|
27447
|
-
|
|
27448
|
-
|
|
27449
|
-
|
|
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/
|
|
27725
|
-
|
|
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/
|
|
27751
|
-
|
|
27752
|
-
|
|
27753
|
-
|
|
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
|
-
|
|
27757
|
-
|
|
27758
|
-
|
|
27759
|
-
|
|
27760
|
-
|
|
27761
|
-
|
|
27762
|
-
|
|
27763
|
-
|
|
27764
|
-
|
|
27765
|
-
|
|
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
|
-
|
|
27775
|
-
|
|
27776
|
-
|
|
27777
|
-
|
|
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
|
|
27789
|
-
|
|
27790
|
-
|
|
27791
|
-
|
|
27792
|
-
|
|
27793
|
-
|
|
27794
|
-
|
|
27795
|
-
|
|
27796
|
-
|
|
27797
|
-
|
|
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
|
-
|
|
28003
|
-
|
|
28004
|
-
|
|
28005
|
-
|
|
28006
|
-
|
|
28007
|
-
|
|
28008
|
-
|
|
28009
|
-
|
|
28010
|
-
|
|
28011
|
-
|
|
28012
|
-
|
|
28013
|
-
|
|
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
|
-
|
|
28016
|
-
|
|
28017
|
-
}
|
|
28018
|
-
|
|
28123
|
+
session.pendingRestart = respawn;
|
|
28124
|
+
killPtyTree(pty2);
|
|
28125
|
+
} else {
|
|
28126
|
+
respawn();
|
|
28019
28127
|
}
|
|
28128
|
+
return true;
|
|
28020
28129
|
}
|
|
28021
|
-
|
|
28022
|
-
|
|
28023
|
-
|
|
28024
|
-
|
|
28025
|
-
|
|
28026
|
-
|
|
28027
|
-
|
|
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
|
-
|
|
28035
|
-
|
|
28036
|
-
|
|
28037
|
-
|
|
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/
|
|
28042
|
-
|
|
28043
|
-
|
|
28044
|
-
|
|
28045
|
-
|
|
28046
|
-
|
|
28047
|
-
|
|
28048
|
-
|
|
28049
|
-
|
|
28050
|
-
|
|
28051
|
-
|
|
28052
|
-
|
|
28053
|
-
|
|
28054
|
-
|
|
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
|
-
|
|
28074
|
-
|
|
28075
|
-
|
|
28076
|
-
|
|
28077
|
-
|
|
28078
|
-
|
|
28079
|
-
|
|
28080
|
-
|
|
28081
|
-
|
|
28082
|
-
|
|
28083
|
-
|
|
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
|
|
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
|
|
28490
|
+
import { existsSync as existsSync68 } from "fs";
|
|
28391
28491
|
async function reclaimVanishedWorktrees(clone, paths) {
|
|
28392
|
-
if (!
|
|
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 (!
|
|
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) =>
|
|
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
|
-
|
|
28938
|
-
|
|
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
|
-
|
|
29599
|
-
this.
|
|
29600
|
-
|
|
29601
|
-
|
|
29602
|
-
|
|
29603
|
-
|
|
29604
|
-
|
|
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
|
-
|
|
29891
|
-
|
|
29892
|
-
|
|
29893
|
-
|
|
29894
|
-
|
|
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
|