@threadbase-sh/streamer 1.15.4 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -596,6 +596,7 @@ type ApiDeps = {
596
596
  handleGetOutput: (sessionId: string, res: ServerResponse) => void;
597
597
  handleSendInput: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
598
598
  handleCancel: (sessionId: string, res: ServerResponse) => void;
599
+ handleStopSession: (sessionId: string, res: ServerResponse) => Promise<void>;
599
600
  handleSetSessionName: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
600
601
  handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
601
602
  handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
@@ -725,6 +726,7 @@ declare class StreamerServer {
725
726
  private log;
726
727
  private agentConfig;
727
728
  private agentClient;
729
+ private sessionStatusBus;
728
730
  constructor(config: ServerConfig & {
729
731
  apiKey: string;
730
732
  });
@@ -750,6 +752,7 @@ declare class StreamerServer {
750
752
  private checkExchangeRateLimit;
751
753
  private handleListConversations;
752
754
  private handleConversationsCount;
755
+ private refreshCountInBackground;
753
756
  private handleSessionsCount;
754
757
  private handleGetRecentSessions;
755
758
  private handleGetPopularProjects;
@@ -771,6 +774,7 @@ declare class StreamerServer {
771
774
  private handleUploadFile;
772
775
  private handleGetOutput;
773
776
  private handleCancel;
777
+ private handleStopSession;
774
778
  private handleAdopt;
775
779
  private handleStartSession;
776
780
  private linkSessionToProject;
package/dist/index.d.ts CHANGED
@@ -596,6 +596,7 @@ type ApiDeps = {
596
596
  handleGetOutput: (sessionId: string, res: ServerResponse) => void;
597
597
  handleSendInput: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
598
598
  handleCancel: (sessionId: string, res: ServerResponse) => void;
599
+ handleStopSession: (sessionId: string, res: ServerResponse) => Promise<void>;
599
600
  handleSetSessionName: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
600
601
  handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
601
602
  handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
@@ -725,6 +726,7 @@ declare class StreamerServer {
725
726
  private log;
726
727
  private agentConfig;
727
728
  private agentClient;
729
+ private sessionStatusBus;
728
730
  constructor(config: ServerConfig & {
729
731
  apiKey: string;
730
732
  });
@@ -750,6 +752,7 @@ declare class StreamerServer {
750
752
  private checkExchangeRateLimit;
751
753
  private handleListConversations;
752
754
  private handleConversationsCount;
755
+ private refreshCountInBackground;
753
756
  private handleSessionsCount;
754
757
  private handleGetRecentSessions;
755
758
  private handleGetPopularProjects;
@@ -771,6 +774,7 @@ declare class StreamerServer {
771
774
  private handleUploadFile;
772
775
  private handleGetOutput;
773
776
  private handleCancel;
777
+ private handleStopSession;
774
778
  private handleAdopt;
775
779
  private handleStartSession;
776
780
  private linkSessionToProject;
package/dist/index.js CHANGED
@@ -1189,6 +1189,7 @@ import {
1189
1189
  ConversationScanner,
1190
1190
  search
1191
1191
  } from "@threadbase-sh/scanner";
1192
+ import { EventEmitter } from "events";
1192
1193
  import {
1193
1194
  createReadStream,
1194
1195
  existsSync as existsSync6,
@@ -1845,6 +1846,10 @@ var createSessionRoutes = (deps) => {
1845
1846
  await deps.handleAdopt(c.req.param("id"), c.env.outgoing);
1846
1847
  return alreadyHandled6();
1847
1848
  });
1849
+ app.post("/:id/stop", async (c) => {
1850
+ await deps.handleStopSession(c.req.param("id"), c.env.outgoing);
1851
+ return alreadyHandled6();
1852
+ });
1848
1853
  app.get("/:id", (c) => {
1849
1854
  deps.handleGetSession(c.req.param("id"), c.env.outgoing);
1850
1855
  return alreadyHandled6();
@@ -3927,7 +3932,9 @@ var StreamerServer = class {
3927
3932
  log = getLogger("server");
3928
3933
  agentConfig;
3929
3934
  agentClient = null;
3935
+ sessionStatusBus = new EventEmitter();
3930
3936
  constructor(config) {
3937
+ this.sessionStatusBus.setMaxListeners(0);
3931
3938
  this.apiKey = config.apiKey;
3932
3939
  this.localNoAuth = config.localNoAuth ?? false;
3933
3940
  this.verbose = config.verbose ?? false;
@@ -4044,6 +4051,7 @@ var StreamerServer = class {
4044
4051
  if (resp) {
4045
4052
  this.wsHub.broadcast({ type: "session_update", session: resp });
4046
4053
  }
4054
+ this.sessionStatusBus.emit(`status:${session.id}`, session.status);
4047
4055
  }
4048
4056
  });
4049
4057
  this.agentConfig = readAgentConfig();
@@ -4089,6 +4097,7 @@ var StreamerServer = class {
4089
4097
  handleGetOutput: (id, res) => this.handleGetOutput(id, res),
4090
4098
  handleSendInput: (id, req, res) => this.handleSendInput(id, req, res),
4091
4099
  handleCancel: (id, res) => this.handleCancel(id, res),
4100
+ handleStopSession: (id, res) => this.handleStopSession(id, res),
4092
4101
  handleSetSessionName: (id, req, res) => this.handleSetSessionName(id, req, res),
4093
4102
  handleUploadFile: (id, req, res) => this.handleUploadFile(id, req, res),
4094
4103
  handleAdopt: (id, res) => this.handleAdopt(id, res),
@@ -4597,22 +4606,36 @@ var StreamerServer = class {
4597
4606
  async handleConversationsCount(url, res) {
4598
4607
  const project = url.searchParams.get("project") ?? void 0;
4599
4608
  const bustCache = url.searchParams.get("refresh") === "1";
4600
- if (bustCache) {
4601
- this.cache?.invalidate();
4602
- this.scanner = null;
4603
- this.scannerReady = null;
4604
- }
4605
- if (this.cache && !bustCache) {
4609
+ if (this.cache) {
4606
4610
  const { total } = this.cache.listConversations({ project, limit: 0, offset: 0 });
4607
4611
  json(res, 200, { total });
4612
+ if (bustCache) this.refreshCountInBackground();
4608
4613
  return;
4609
4614
  }
4610
- const scanner = await this.getScanner();
4615
+ const scanner = await this.getScanner(true);
4611
4616
  let metas = [...scanner.getMetadataCache().values()];
4612
4617
  metas = applyIncludeFilter(metas, "conversations");
4613
4618
  if (project) metas = applyProjectFilter(metas, project);
4614
4619
  json(res, 200, { total: metas.length });
4615
4620
  }
4621
+ // Fire-and-forget full rescan that reconciles the SQLite cache from disk so a
4622
+ // later count reflects new/removed conversations. Never awaited by the request
4623
+ // path — refresh=1 returns the cached total synchronously and this catches up.
4624
+ refreshCountInBackground() {
4625
+ void (async () => {
4626
+ try {
4627
+ const scanner = await this.getFreshScanner();
4628
+ if (this.cache) {
4629
+ this.cache.upsertFromScannerMeta([...scanner.getMetadataCache().values()]);
4630
+ }
4631
+ } catch (err) {
4632
+ this.log.warn(
4633
+ `Background count refresh failed: ${err instanceof Error ? err.message : String(err)}`,
4634
+ { event: "count.refresh_failed" }
4635
+ );
4636
+ }
4637
+ })();
4638
+ }
4616
4639
  handleSessionsCount(res) {
4617
4640
  json(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });
4618
4641
  }
@@ -5267,6 +5290,54 @@ var StreamerServer = class {
5267
5290
  json(res, 400, { error: message });
5268
5291
  }
5269
5292
  }
5293
+ async handleStopSession(sessionId, res) {
5294
+ const STOP_TIMEOUT_MS = 5e3;
5295
+ const session = this.ptyManager.getSession(sessionId);
5296
+ if (!session) {
5297
+ res.writeHead(404, { "Content-Type": "application/json" });
5298
+ res.end(JSON.stringify({ error: "Session not found" }));
5299
+ return;
5300
+ }
5301
+ if (session.status === "idle") {
5302
+ res.writeHead(200, { "Content-Type": "application/json" });
5303
+ res.end(JSON.stringify({ status: "already_idle", sessionId }));
5304
+ return;
5305
+ }
5306
+ res.writeHead(200, {
5307
+ "Content-Type": "application/x-ndjson",
5308
+ "Transfer-Encoding": "chunked",
5309
+ "Cache-Control": "no-cache",
5310
+ "X-Accel-Buffering": "no"
5311
+ });
5312
+ res.write(`${JSON.stringify({ event: "stopping", sessionId })}
5313
+ `);
5314
+ const idlePromise = new Promise((resolve2) => {
5315
+ const handler = (status) => {
5316
+ if (status === "idle") {
5317
+ this.sessionStatusBus.off(`status:${sessionId}`, handler);
5318
+ resolve2("idle");
5319
+ }
5320
+ };
5321
+ this.sessionStatusBus.on(`status:${sessionId}`, handler);
5322
+ });
5323
+ const timeoutPromise = new Promise(
5324
+ (resolve2) => setTimeout(() => resolve2("timeout"), STOP_TIMEOUT_MS)
5325
+ );
5326
+ this.ptyManager.putOnHold(sessionId);
5327
+ this.discoveryCache = null;
5328
+ const outcome = await Promise.race([idlePromise, timeoutPromise]);
5329
+ if (outcome === "idle") {
5330
+ res.write(`${JSON.stringify({ event: "stopped", sessionId })}
5331
+ `);
5332
+ } else {
5333
+ res.write(`${JSON.stringify({ event: "timeout", sessionId })}
5334
+ `);
5335
+ this.log.warn(
5336
+ `[stop] session ${sessionId.slice(0, 8)} did not idle within ${STOP_TIMEOUT_MS}ms`
5337
+ );
5338
+ }
5339
+ res.end();
5340
+ }
5270
5341
  async handleAdopt(sessionId, res) {
5271
5342
  const discovered = await discoverClaudeProcesses();
5272
5343
  this.sessionStore.setDiscovered(discovered);