@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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +141 -64
  3. 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.318.8",
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 z5 } from "zod";
20048
+ import { z as z6 } from "zod";
19961
20049
  var ACTIVE_FILE = "active.json";
19962
- var activeSelectionSchema = z5.record(z5.string(), z5.string());
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
- spawn12((id2) => errorSession(id2, persisted, reason));
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
- return loadPersistedSessions().map((persisted) => {
20502
- restoreOne(persisted, spawn12, sessions);
20503
- return persisted.name;
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 createInterface5 } from "readline";
20759
+ import { createInterface as createInterface6 } from "readline";
20694
20760
  function logChildStream(stream, label2) {
20695
20761
  if (!stream) return;
20696
- const lines = createInterface5({ input: stream });
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 createInterface6 } from "readline";
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 = createInterface6({ input: socket });
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
- nextId = 1;
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(String(this.nextId++)));
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 createInterface7 } from "readline";
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 = createInterface7({ input: socket });
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.318.8",
3
+ "version": "0.319.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {