@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/README.md +2 -0
- package/dist/commands/sessions/web/bundle.js +385 -385
- package/dist/index.js +778 -672
- 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: {
|
|
@@ -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
|
-
|
|
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/
|
|
27230
|
-
function
|
|
27231
|
-
|
|
27232
|
-
|
|
27233
|
-
|
|
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/
|
|
27267
|
-
|
|
27268
|
-
|
|
27269
|
-
|
|
27270
|
-
|
|
27271
|
-
|
|
27272
|
-
|
|
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/
|
|
27278
|
-
import {
|
|
27279
|
-
|
|
27280
|
-
const
|
|
27281
|
-
|
|
27282
|
-
|
|
27283
|
-
|
|
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/
|
|
27307
|
-
|
|
27308
|
-
|
|
27309
|
-
if (
|
|
27310
|
-
|
|
27311
|
-
|
|
27312
|
-
|
|
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/
|
|
27323
|
-
import
|
|
27324
|
-
|
|
27325
|
-
|
|
27326
|
-
|
|
27327
|
-
|
|
27328
|
-
|
|
27329
|
-
|
|
27330
|
-
|
|
27331
|
-
|
|
27332
|
-
|
|
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
|
-
|
|
27347
|
-
|
|
27348
|
-
|
|
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/
|
|
27352
|
-
async function
|
|
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)
|
|
27355
|
-
|
|
27356
|
-
|
|
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-
|
|
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/
|
|
27406
|
-
function
|
|
27407
|
-
if (
|
|
27408
|
-
|
|
27409
|
-
|
|
27410
|
-
|
|
27411
|
-
|
|
27412
|
-
|
|
27413
|
-
|
|
27414
|
-
|
|
27415
|
-
}
|
|
27416
|
-
|
|
27417
|
-
|
|
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/
|
|
27702
|
-
|
|
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/
|
|
27728
|
-
|
|
27729
|
-
|
|
27730
|
-
|
|
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
|
-
|
|
27734
|
-
|
|
27735
|
-
|
|
27736
|
-
|
|
27737
|
-
|
|
27738
|
-
|
|
27739
|
-
|
|
27740
|
-
|
|
27741
|
-
|
|
27742
|
-
|
|
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
|
-
|
|
27752
|
-
|
|
27753
|
-
|
|
27754
|
-
|
|
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
|
|
27766
|
-
|
|
27767
|
-
|
|
27768
|
-
|
|
27769
|
-
|
|
27770
|
-
|
|
27771
|
-
|
|
27772
|
-
|
|
27773
|
-
|
|
27774
|
-
|
|
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
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
|
|
27983
|
-
|
|
27984
|
-
|
|
27985
|
-
|
|
27986
|
-
|
|
27987
|
-
|
|
27988
|
-
|
|
27989
|
-
|
|
27990
|
-
|
|
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
|
-
|
|
27993
|
-
|
|
27994
|
-
}
|
|
27995
|
-
|
|
28123
|
+
session.pendingRestart = respawn;
|
|
28124
|
+
killPtyTree(pty2);
|
|
28125
|
+
} else {
|
|
28126
|
+
respawn();
|
|
27996
28127
|
}
|
|
28128
|
+
return true;
|
|
27997
28129
|
}
|
|
27998
|
-
|
|
27999
|
-
|
|
28000
|
-
|
|
28001
|
-
|
|
28002
|
-
|
|
28003
|
-
|
|
28004
|
-
|
|
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
|
-
|
|
28012
|
-
|
|
28013
|
-
|
|
28014
|
-
|
|
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/
|
|
28019
|
-
|
|
28020
|
-
|
|
28021
|
-
|
|
28022
|
-
|
|
28023
|
-
|
|
28024
|
-
|
|
28025
|
-
|
|
28026
|
-
|
|
28027
|
-
|
|
28028
|
-
|
|
28029
|
-
|
|
28030
|
-
|
|
28031
|
-
|
|
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
|
-
|
|
28051
|
-
|
|
28052
|
-
|
|
28053
|
-
|
|
28054
|
-
|
|
28055
|
-
|
|
28056
|
-
|
|
28057
|
-
|
|
28058
|
-
|
|
28059
|
-
|
|
28060
|
-
|
|
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
|
|
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
|
|
28490
|
+
import { existsSync as existsSync68 } from "fs";
|
|
28368
28491
|
async function reclaimVanishedWorktrees(clone, paths) {
|
|
28369
|
-
if (!
|
|
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 (!
|
|
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) =>
|
|
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
|
-
|
|
28915
|
-
|
|
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
|
-
|
|
29576
|
-
this.
|
|
29577
|
-
|
|
29578
|
-
|
|
29579
|
-
|
|
29580
|
-
|
|
29581
|
-
|
|
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
|
-
|
|
29868
|
-
|
|
29869
|
-
|
|
29870
|
-
|
|
29871
|
-
|
|
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
|