@staff0rd/assist 0.318.8 → 0.319.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 +1 -0
- package/dist/index.js +141 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -266,6 +266,7 @@ The first backlog command in a repository that still has a local `.assist/backlo
|
|
|
266
266
|
- `assist daemon status` - Show sessions daemon status, live sessions, and any stray daemon processes or stolen socket
|
|
267
267
|
- `assist daemon stop` - Stop the sessions daemon; running claude sessions resume on next start
|
|
268
268
|
- `assist daemon restart` - Restart the sessions daemon, resuming previously running claude sessions
|
|
269
|
+
- `assist daemon drain` - Remove all sessions from the local daemon for a clean slate (does not affect the Windows daemon)
|
|
269
270
|
|
|
270
271
|
Web sessions are owned by a long-lived daemon process, not the web server: the server is a thin client that relays WebSocket traffic to the daemon over a local IPC socket (unix domain socket at `~/.assist/daemon/daemon.sock`; named pipe `\\.\pipe\assist-sessions-daemon` on Windows). Restarting the web server leaves sessions running with scrollback intact. The daemon logs to `~/.assist/daemon/daemon.log` (timestamped lines tagged with the daemon's pid, including why it spawned and which sessions it restored) and auto-exits once no sessions remain and no client has been connected for 60 seconds (it is respawned on demand by the web server). Daemon spawning is arbitrated by an `O_EXCL` lockfile so racing clients start at most one daemon; sessions are only restored after the daemon owns the IPC socket, and a daemon that loses ownership of `daemon.pid` shuts down its sessions and exits rather than running orphaned.
|
|
271
272
|
|
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.319.0",
|
|
10
10
|
type: "module",
|
|
11
11
|
main: "dist/index.js",
|
|
12
12
|
bin: {
|
|
@@ -19934,6 +19934,94 @@ function reportStrays(pids) {
|
|
|
19934
19934
|
);
|
|
19935
19935
|
}
|
|
19936
19936
|
|
|
19937
|
+
// src/commands/sessions/daemon/drainDaemon.ts
|
|
19938
|
+
import { createInterface as createInterface5 } from "readline";
|
|
19939
|
+
|
|
19940
|
+
// src/commands/sessions/daemon/loadPersistedSessions.ts
|
|
19941
|
+
import { z as z5 } from "zod";
|
|
19942
|
+
var SESSIONS_FILE = "sessions.json";
|
|
19943
|
+
var persistedSessionSchema = z5.object({
|
|
19944
|
+
name: z5.string(),
|
|
19945
|
+
commandType: z5.enum(["claude", "run", "assist"]),
|
|
19946
|
+
cwd: z5.string(),
|
|
19947
|
+
startedAt: z5.number(),
|
|
19948
|
+
runningMs: z5.number().optional(),
|
|
19949
|
+
claudeSessionId: z5.string().optional(),
|
|
19950
|
+
runName: z5.string().optional(),
|
|
19951
|
+
runArgs: z5.array(z5.string()).optional(),
|
|
19952
|
+
assistArgs: z5.array(z5.string()).optional(),
|
|
19953
|
+
activity: activitySchema.optional()
|
|
19954
|
+
});
|
|
19955
|
+
function loadPersistedSessions() {
|
|
19956
|
+
const data = loadJson(SESSIONS_FILE);
|
|
19957
|
+
if (!Array.isArray(data)) return [];
|
|
19958
|
+
return data.flatMap((entry) => {
|
|
19959
|
+
const parsed = persistedSessionSchema.safeParse(entry);
|
|
19960
|
+
return parsed.success ? [parsed.data] : [];
|
|
19961
|
+
});
|
|
19962
|
+
}
|
|
19963
|
+
function savePersistedSessions(sessions) {
|
|
19964
|
+
saveJson(SESSIONS_FILE, sessions);
|
|
19965
|
+
}
|
|
19966
|
+
function persistLiveSessions(sessions) {
|
|
19967
|
+
savePersistedSessions(
|
|
19968
|
+
[...sessions.values()].filter((s) => s.pty && s.status !== "done").map(toPersistedSession)
|
|
19969
|
+
);
|
|
19970
|
+
}
|
|
19971
|
+
function toPersistedSession(session) {
|
|
19972
|
+
return {
|
|
19973
|
+
name: session.name,
|
|
19974
|
+
commandType: session.commandType,
|
|
19975
|
+
cwd: session.cwd ?? process.cwd(),
|
|
19976
|
+
startedAt: session.startedAt,
|
|
19977
|
+
runningMs: accumulatedRunningMs(session),
|
|
19978
|
+
claudeSessionId: session.claudeSessionId,
|
|
19979
|
+
runName: session.runName,
|
|
19980
|
+
runArgs: session.runArgs,
|
|
19981
|
+
assistArgs: session.assistArgs,
|
|
19982
|
+
activity: session.activity
|
|
19983
|
+
};
|
|
19984
|
+
}
|
|
19985
|
+
function accumulatedRunningMs(session) {
|
|
19986
|
+
return session.runningSince != null ? session.runningMs + Date.now() - session.runningSince : session.runningMs;
|
|
19987
|
+
}
|
|
19988
|
+
|
|
19989
|
+
// src/commands/sessions/daemon/drainDaemon.ts
|
|
19990
|
+
var DRAIN_TIMEOUT_MS = 5e3;
|
|
19991
|
+
async function drainDaemon() {
|
|
19992
|
+
let socket;
|
|
19993
|
+
try {
|
|
19994
|
+
socket = await connectToDaemon();
|
|
19995
|
+
} catch {
|
|
19996
|
+
savePersistedSessions([]);
|
|
19997
|
+
console.log("Sessions daemon is not running; cleared persisted sessions");
|
|
19998
|
+
return;
|
|
19999
|
+
}
|
|
20000
|
+
const count6 = await requestDrain(socket);
|
|
20001
|
+
socket.destroy();
|
|
20002
|
+
console.log(`Drained ${count6} session(s)`);
|
|
20003
|
+
}
|
|
20004
|
+
function requestDrain(socket) {
|
|
20005
|
+
socket.write(`${JSON.stringify({ type: "drain" })}
|
|
20006
|
+
`);
|
|
20007
|
+
return new Promise((resolve17) => {
|
|
20008
|
+
const timer = setTimeout(() => resolve17(0), DRAIN_TIMEOUT_MS);
|
|
20009
|
+
const lines = createInterface5({ input: socket });
|
|
20010
|
+
lines.on("error", () => {
|
|
20011
|
+
});
|
|
20012
|
+
lines.on("line", (line) => {
|
|
20013
|
+
try {
|
|
20014
|
+
const data = JSON.parse(line);
|
|
20015
|
+
if (data.type === "drained") {
|
|
20016
|
+
clearTimeout(timer);
|
|
20017
|
+
resolve17(data.count ?? 0);
|
|
20018
|
+
}
|
|
20019
|
+
} catch {
|
|
20020
|
+
}
|
|
20021
|
+
});
|
|
20022
|
+
});
|
|
20023
|
+
}
|
|
20024
|
+
|
|
19937
20025
|
// src/commands/sessions/daemon/runDaemon.ts
|
|
19938
20026
|
import { mkdirSync as mkdirSync23 } from "fs";
|
|
19939
20027
|
|
|
@@ -19957,9 +20045,9 @@ function daemonLog(message) {
|
|
|
19957
20045
|
}
|
|
19958
20046
|
|
|
19959
20047
|
// src/commands/sessions/daemon/loadActiveSelection.ts
|
|
19960
|
-
import { z as
|
|
20048
|
+
import { z as z6 } from "zod";
|
|
19961
20049
|
var ACTIVE_FILE = "active.json";
|
|
19962
|
-
var activeSelectionSchema =
|
|
20050
|
+
var activeSelectionSchema = z6.record(z6.string(), z6.string());
|
|
19963
20051
|
function loadActiveSelection() {
|
|
19964
20052
|
const parsed = activeSelectionSchema.safeParse(
|
|
19965
20053
|
loadJson(ACTIVE_FILE)
|
|
@@ -20005,55 +20093,6 @@ function broadcast(clients, msg) {
|
|
|
20005
20093
|
}
|
|
20006
20094
|
}
|
|
20007
20095
|
|
|
20008
|
-
// src/commands/sessions/daemon/loadPersistedSessions.ts
|
|
20009
|
-
import { z as z6 } from "zod";
|
|
20010
|
-
var SESSIONS_FILE = "sessions.json";
|
|
20011
|
-
var persistedSessionSchema = z6.object({
|
|
20012
|
-
name: z6.string(),
|
|
20013
|
-
commandType: z6.enum(["claude", "run", "assist"]),
|
|
20014
|
-
cwd: z6.string(),
|
|
20015
|
-
startedAt: z6.number(),
|
|
20016
|
-
runningMs: z6.number().optional(),
|
|
20017
|
-
claudeSessionId: z6.string().optional(),
|
|
20018
|
-
runName: z6.string().optional(),
|
|
20019
|
-
runArgs: z6.array(z6.string()).optional(),
|
|
20020
|
-
assistArgs: z6.array(z6.string()).optional(),
|
|
20021
|
-
activity: activitySchema.optional()
|
|
20022
|
-
});
|
|
20023
|
-
function loadPersistedSessions() {
|
|
20024
|
-
const data = loadJson(SESSIONS_FILE);
|
|
20025
|
-
if (!Array.isArray(data)) return [];
|
|
20026
|
-
return data.flatMap((entry) => {
|
|
20027
|
-
const parsed = persistedSessionSchema.safeParse(entry);
|
|
20028
|
-
return parsed.success ? [parsed.data] : [];
|
|
20029
|
-
});
|
|
20030
|
-
}
|
|
20031
|
-
function savePersistedSessions(sessions) {
|
|
20032
|
-
saveJson(SESSIONS_FILE, sessions);
|
|
20033
|
-
}
|
|
20034
|
-
function persistLiveSessions(sessions) {
|
|
20035
|
-
savePersistedSessions(
|
|
20036
|
-
[...sessions.values()].filter((s) => s.pty && s.status !== "done").map(toPersistedSession)
|
|
20037
|
-
);
|
|
20038
|
-
}
|
|
20039
|
-
function toPersistedSession(session) {
|
|
20040
|
-
return {
|
|
20041
|
-
name: session.name,
|
|
20042
|
-
commandType: session.commandType,
|
|
20043
|
-
cwd: session.cwd ?? process.cwd(),
|
|
20044
|
-
startedAt: session.startedAt,
|
|
20045
|
-
runningMs: accumulatedRunningMs(session),
|
|
20046
|
-
claudeSessionId: session.claudeSessionId,
|
|
20047
|
-
runName: session.runName,
|
|
20048
|
-
runArgs: session.runArgs,
|
|
20049
|
-
assistArgs: session.assistArgs,
|
|
20050
|
-
activity: session.activity
|
|
20051
|
-
};
|
|
20052
|
-
}
|
|
20053
|
-
function accumulatedRunningMs(session) {
|
|
20054
|
-
return session.runningSince != null ? session.runningMs + Date.now() - session.runningSince : session.runningMs;
|
|
20055
|
-
}
|
|
20056
|
-
|
|
20057
20096
|
// src/commands/sessions/daemon/toSessionInfo.ts
|
|
20058
20097
|
function toSessionInfo({
|
|
20059
20098
|
id: id2,
|
|
@@ -20481,7 +20520,10 @@ function restoreOne(persisted, spawn12, sessions) {
|
|
|
20481
20520
|
logUnresumable(persisted.name, id2, sessions.get(id2));
|
|
20482
20521
|
} catch (error) {
|
|
20483
20522
|
const reason = logRestoreError(persisted.name, error);
|
|
20484
|
-
|
|
20523
|
+
try {
|
|
20524
|
+
spawn12((id2) => errorSession(id2, persisted, reason));
|
|
20525
|
+
} catch {
|
|
20526
|
+
}
|
|
20485
20527
|
}
|
|
20486
20528
|
}
|
|
20487
20529
|
function logUnresumable(name, id2, session) {
|
|
@@ -20496,11 +20538,35 @@ function logRestoreError(name, error) {
|
|
|
20496
20538
|
return reason;
|
|
20497
20539
|
}
|
|
20498
20540
|
|
|
20541
|
+
// src/commands/sessions/daemon/sessionLimits.ts
|
|
20542
|
+
var MAX_LIVE = 12;
|
|
20543
|
+
var MAX_RESTORE = 10;
|
|
20544
|
+
var sessionLimits = {
|
|
20545
|
+
maxRestore: MAX_RESTORE,
|
|
20546
|
+
/** Allocate the next session id from counter, refusing past the absolute
|
|
20547
|
+
* ceiling regardless of caller (restore, web create, retry) so no trigger
|
|
20548
|
+
* can fan out without bound. The counter advances only when allowed. */
|
|
20549
|
+
nextId(liveCount, counter) {
|
|
20550
|
+
if (liveCount >= MAX_LIVE) {
|
|
20551
|
+
daemonLog(`refusing to spawn: at ceiling of ${MAX_LIVE} live sessions`);
|
|
20552
|
+
throw new Error(`session ceiling of ${MAX_LIVE} reached`);
|
|
20553
|
+
}
|
|
20554
|
+
return String(counter.next++);
|
|
20555
|
+
}
|
|
20556
|
+
};
|
|
20557
|
+
|
|
20499
20558
|
// src/commands/sessions/daemon/restoreAll.ts
|
|
20500
20559
|
function restoreAll(spawn12, sessions) {
|
|
20501
|
-
|
|
20502
|
-
|
|
20503
|
-
|
|
20560
|
+
const persisted = loadPersistedSessions();
|
|
20561
|
+
const toRestore = persisted.slice(0, sessionLimits.maxRestore);
|
|
20562
|
+
const skipped = persisted.length - toRestore.length;
|
|
20563
|
+
if (skipped > 0)
|
|
20564
|
+
daemonLog(
|
|
20565
|
+
`restore capped at ${sessionLimits.maxRestore}; skipping ${skipped} persisted session(s)`
|
|
20566
|
+
);
|
|
20567
|
+
return toRestore.map((persisted2) => {
|
|
20568
|
+
restoreOne(persisted2, spawn12, sessions);
|
|
20569
|
+
return persisted2.name;
|
|
20504
20570
|
});
|
|
20505
20571
|
}
|
|
20506
20572
|
|
|
@@ -20690,10 +20756,10 @@ async function isWindowsDaemonRunning() {
|
|
|
20690
20756
|
import { spawn as spawn10 } from "child_process";
|
|
20691
20757
|
|
|
20692
20758
|
// src/commands/sessions/daemon/logChildStream.ts
|
|
20693
|
-
import { createInterface as
|
|
20759
|
+
import { createInterface as createInterface6 } from "readline";
|
|
20694
20760
|
function logChildStream(stream, label2) {
|
|
20695
20761
|
if (!stream) return;
|
|
20696
|
-
const lines =
|
|
20762
|
+
const lines = createInterface6({ input: stream });
|
|
20697
20763
|
lines.on("line", (line) => daemonLog(`[${label2}] ${line}`));
|
|
20698
20764
|
lines.on("error", () => {
|
|
20699
20765
|
});
|
|
@@ -20965,7 +21031,7 @@ function isWindowsIo(data) {
|
|
|
20965
21031
|
}
|
|
20966
21032
|
|
|
20967
21033
|
// src/commands/sessions/daemon/WindowsConnection.ts
|
|
20968
|
-
import { createInterface as
|
|
21034
|
+
import { createInterface as createInterface7 } from "readline";
|
|
20969
21035
|
|
|
20970
21036
|
// src/commands/sessions/daemon/LaunchCircuitBreaker.ts
|
|
20971
21037
|
var MAX_FAILURES = 3;
|
|
@@ -21045,7 +21111,7 @@ var WindowsConnection = class {
|
|
|
21045
21111
|
return socket;
|
|
21046
21112
|
}
|
|
21047
21113
|
wire(socket) {
|
|
21048
|
-
const lines =
|
|
21114
|
+
const lines = createInterface7({ input: socket });
|
|
21049
21115
|
lines.on("error", () => {
|
|
21050
21116
|
});
|
|
21051
21117
|
lines.on("line", (line) => this.deps.onLine(line));
|
|
@@ -21223,6 +21289,12 @@ function dismissSession(sessions, id2) {
|
|
|
21223
21289
|
sessions.delete(id2);
|
|
21224
21290
|
return true;
|
|
21225
21291
|
}
|
|
21292
|
+
function drainSessions(sessions, onDrained) {
|
|
21293
|
+
const ids = [...sessions.keys()];
|
|
21294
|
+
for (const id2 of ids) dismissSession(sessions, id2);
|
|
21295
|
+
onDrained();
|
|
21296
|
+
return ids.length;
|
|
21297
|
+
}
|
|
21226
21298
|
|
|
21227
21299
|
// src/commands/sessions/daemon/SessionManager.ts
|
|
21228
21300
|
var SessionManager = class {
|
|
@@ -21233,7 +21305,7 @@ var SessionManager = class {
|
|
|
21233
21305
|
// why: dispatch calls active.set() on card click; broadcasts include active.toJSON()
|
|
21234
21306
|
active = new ActiveSelection(() => this.notify());
|
|
21235
21307
|
clients = new ClientHub(persistUsagePeak);
|
|
21236
|
-
|
|
21308
|
+
idCounter = { next: 1 };
|
|
21237
21309
|
shuttingDown = false;
|
|
21238
21310
|
// why: dispatch calls windowsProxy.route() to forward windows-origin sessions
|
|
21239
21311
|
windowsProxy = new WindowsProxy(this.clients, () => this.notify());
|
|
@@ -21254,12 +21326,13 @@ var SessionManager = class {
|
|
|
21254
21326
|
restore() {
|
|
21255
21327
|
return restoreAll(this.spawnWith, this.sessions);
|
|
21256
21328
|
}
|
|
21329
|
+
drain = () => drainSessions(this.sessions, this.notify);
|
|
21257
21330
|
add(session) {
|
|
21258
21331
|
this.wire(session);
|
|
21259
21332
|
watchActivity(session, this.notify);
|
|
21260
21333
|
return session.id;
|
|
21261
21334
|
}
|
|
21262
|
-
spawnWith = (create) => this.add(create(
|
|
21335
|
+
spawnWith = (create) => this.add(create(sessionLimits.nextId(this.sessions.size, this.idCounter)));
|
|
21263
21336
|
spawn(prompt, cwd) {
|
|
21264
21337
|
return this.spawnWith((id2) => createSession(id2, prompt, cwd));
|
|
21265
21338
|
}
|
|
@@ -21322,7 +21395,7 @@ import { unlinkSync as unlinkSync19 } from "fs";
|
|
|
21322
21395
|
import * as net3 from "net";
|
|
21323
21396
|
|
|
21324
21397
|
// src/commands/sessions/daemon/handleConnection.ts
|
|
21325
|
-
import { createInterface as
|
|
21398
|
+
import { createInterface as createInterface8 } from "readline";
|
|
21326
21399
|
|
|
21327
21400
|
// src/commands/sessions/shared/discoverSessions.ts
|
|
21328
21401
|
import * as fs30 from "fs";
|
|
@@ -21662,6 +21735,7 @@ var messageHandlers = {
|
|
|
21662
21735
|
history: handleHistory,
|
|
21663
21736
|
"fetch-transcript": handleFetchTranscript,
|
|
21664
21737
|
shutdown: handleShutdown,
|
|
21738
|
+
drain: (client, m) => sendTo(client, { type: "drained", count: m.drain() }),
|
|
21665
21739
|
limits: (_client, m, d) => m.clients.updateLimits(d.rateLimits),
|
|
21666
21740
|
input: routed(
|
|
21667
21741
|
(_client, m, d) => m.writeToSession(d.sessionId, d.data)
|
|
@@ -21698,7 +21772,7 @@ function handleConnection(socket, manager) {
|
|
|
21698
21772
|
};
|
|
21699
21773
|
manager.addClient(client);
|
|
21700
21774
|
manager.clients.greet(client);
|
|
21701
|
-
const lines =
|
|
21775
|
+
const lines = createInterface8({ input: socket });
|
|
21702
21776
|
lines.on("error", () => {
|
|
21703
21777
|
});
|
|
21704
21778
|
lines.on("line", (line) => {
|
|
@@ -21852,6 +21926,9 @@ function registerDaemon(program2) {
|
|
|
21852
21926
|
cmd.command("restart").description(
|
|
21853
21927
|
"Restart the sessions daemon, resuming previously running claude sessions"
|
|
21854
21928
|
).action(restartDaemon);
|
|
21929
|
+
cmd.command("drain").description(
|
|
21930
|
+
"Remove all sessions from the local daemon for a clean slate (does not affect the Windows daemon)"
|
|
21931
|
+
).action(drainDaemon);
|
|
21855
21932
|
}
|
|
21856
21933
|
|
|
21857
21934
|
// src/commands/sessions/summarise/index.ts
|