@standardagents/code 0.12.0 → 0.13.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 CHANGED
@@ -65,7 +65,9 @@ we're letting new folks in every week.
65
65
  transitions, stream and idle execution-bridge heartbeats use Durable Object hibernation
66
66
  auto-responses, and transcript safety requests run only while the agent is active. The bridge
67
67
  switches to an observable liveness heartbeat only during a forwarded tool or approval, so
68
- leaving a terminal attached does not continuously wake an idle thread.
68
+ leaving a terminal attached does not continuously wake an idle thread. A machine daemon keeps
69
+ one account user stream and opens a per-thread execution bridge only while forwarded work is
70
+ active.
69
71
  - **Subagents & research built in.** Delegate focused subtasks to a coding subagent, or spin up a
70
72
  non-blocking web-research agent that returns source-backed findings.
71
73
  - **MCP, first-class.** The CLI is a Model Context Protocol host — connect local MCP servers
package/dist/index.js CHANGED
@@ -2028,7 +2028,8 @@ var HostTools = class {
2028
2028
  }
2029
2029
  const updated = replaceAll ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
2030
2030
  await fsp.writeFile(file2, updated, "utf8");
2031
- return { ok: true, result: `Edited ${path4.relative(this.projectDir, file2)} (${count} replacement${count === 1 ? "" : "s"})` };
2031
+ const startLine = content.slice(0, content.indexOf(oldStr)).split("\n").length;
2032
+ return { ok: true, result: `Edited ${path4.relative(this.projectDir, file2)} (${count} replacement${count === 1 ? "" : "s"}) @ line ${startLine}` };
2032
2033
  }
2033
2034
  /**
2034
2035
  * Copy a file from the THREAD filesystem (e.g. a generated /attachments/*
@@ -6491,7 +6492,7 @@ function readVersion() {
6491
6492
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
6492
6493
  } catch {
6493
6494
  }
6494
- return "0.12.0" ;
6495
+ return "0.13.0" ;
6495
6496
  }
6496
6497
  function isLocalHost(host) {
6497
6498
  return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.endsWith(".localhost") || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
@@ -6606,13 +6607,6 @@ async function readFsResponse(api, machineId) {
6606
6607
  async function readFsRequest(api, machineId) {
6607
6608
  return parseFsRequest(await api.userKvGet(fsRequestKey(machineId)));
6608
6609
  }
6609
- var FS_POLL_HOT_MS = 1e3;
6610
- var FS_POLL_IDLE_MS = 5e3;
6611
- var FS_HOT_WINDOW_MS = 5 * 6e4;
6612
- function fsPollDelay(lastRequestSeenAt, now) {
6613
- if (lastRequestSeenAt !== null && now - lastRequestSeenAt < FS_HOT_WINDOW_MS) return FS_POLL_HOT_MS;
6614
- return FS_POLL_IDLE_MS;
6615
- }
6616
6610
  async function writeFsResponse(api, machineId, res) {
6617
6611
  await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
6618
6612
  }
@@ -6838,17 +6832,6 @@ async function unregisterProject(api, identity, projectDir) {
6838
6832
  }
6839
6833
  });
6840
6834
  }
6841
- async function touchDaemon(api, identity, version) {
6842
- await updateOwnMachineRecord(api, identity, (record2) => {
6843
- const now = Date.now();
6844
- record2.daemon = {
6845
- version,
6846
- installed_at: record2.daemon?.installed_at ?? now,
6847
- last_seen_at: now,
6848
- pid: process.pid
6849
- };
6850
- });
6851
- }
6852
6835
  async function clearDaemon(api, identity) {
6853
6836
  await updateOwnMachineRecord(api, identity, (record2) => {
6854
6837
  record2.daemon = null;
@@ -7242,9 +7225,7 @@ async function awaitApprovalViaRelay(api, threadId, request, options = {}) {
7242
7225
  });
7243
7226
  }
7244
7227
  }
7245
-
7246
- // src/hub.ts
7247
- var HubSocket = class {
7228
+ var DaemonUserStream = class {
7248
7229
  constructor(api, clientId, hooks, clientName) {
7249
7230
  this.api = api;
7250
7231
  this.clientId = clientId;
@@ -7260,13 +7241,13 @@ var HubSocket = class {
7260
7241
  heartbeat = null;
7261
7242
  reconnectAttempt = 0;
7262
7243
  reconnectTimer = null;
7263
- /** Open the socket and keep it connected (reconnect forever on drop). */
7264
7244
  start() {
7265
7245
  this.openSocket();
7266
7246
  }
7267
7247
  openSocket() {
7268
7248
  if (this.closed) return;
7269
- let url = `${this.api.wsEndpoint}/api/users/me/stream?token=${encodeURIComponent(this.api.bearer)}&client_id=${encodeURIComponent(this.clientId)}&client_kind=daemon`;
7249
+ const connectionClientId = `${this.clientId}:${crypto.randomBytes(8).toString("hex")}`;
7250
+ let url = `${this.api.wsEndpoint}/api/users/me/stream?token=${encodeURIComponent(this.api.bearer)}&client_id=${encodeURIComponent(connectionClientId)}&client_kind=daemon`;
7270
7251
  if (this.clientName) url += `&client_name=${encodeURIComponent(this.clientName)}`;
7271
7252
  let ws;
7272
7253
  try {
@@ -7281,25 +7262,29 @@ var HubSocket = class {
7281
7262
  this.startHeartbeat(ws);
7282
7263
  this.hooks.onConnection?.("connected", 0);
7283
7264
  });
7284
- ws.addEventListener("message", (ev) => {
7265
+ ws.addEventListener("message", (event) => {
7285
7266
  if (this.ws === ws) this.heartbeat?.markAlive();
7286
- this.onMessage(String(ev.data));
7267
+ this.onMessage(String(event.data));
7287
7268
  });
7288
7269
  ws.addEventListener("error", () => this.handleDrop(ws));
7289
7270
  ws.addEventListener("close", () => this.handleDrop(ws));
7290
7271
  }
7291
7272
  onMessage(raw) {
7292
- let msg;
7273
+ let message;
7293
7274
  try {
7294
- msg = JSON.parse(raw);
7275
+ message = JSON.parse(raw);
7295
7276
  } catch {
7296
7277
  return;
7297
7278
  }
7298
- if (!msg || typeof msg !== "object") return;
7299
- const frame = msg;
7300
- if (frame.event === "standardagents.wake") {
7301
- const threadId = frame.data?.thread_id;
7302
- if (typeof threadId === "string") this.hooks.onWake(threadId);
7279
+ if (!message || typeof message !== "object") return;
7280
+ const frame = message;
7281
+ if (typeof frame.event === "string") {
7282
+ if (frame.event === "standardagents.wake") {
7283
+ const threadId = frame.data?.thread_id;
7284
+ if (typeof threadId === "string") this.hooks.onWake(threadId);
7285
+ } else {
7286
+ this.hooks.onEvent(frame.event, frame.data);
7287
+ }
7303
7288
  return;
7304
7289
  }
7305
7290
  if (frame.type === "wake" && typeof frame.threadId === "string") {
@@ -7329,10 +7314,8 @@ var HubSocket = class {
7329
7314
  this.heartbeat.start();
7330
7315
  }
7331
7316
  stopHeartbeat() {
7332
- if (this.heartbeat) {
7333
- this.heartbeat.stop();
7334
- this.heartbeat = null;
7335
- }
7317
+ this.heartbeat?.stop();
7318
+ this.heartbeat = null;
7336
7319
  }
7337
7320
  close() {
7338
7321
  this.closed = true;
@@ -7349,7 +7332,7 @@ var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
7349
7332
  var CACHE_REL_DIR = ".config/standardagents-cli";
7350
7333
  var CACHE_FILE = "update-check.json";
7351
7334
  var STATE_FILE = "auto-update.json";
7352
- var CACHE_TTL_MS = 1e3 * 60 * 60 * 6;
7335
+ var CACHE_TTL_MS = 1e3 * 60 * 60 * 3;
7353
7336
  var CHECK_TIMEOUT_MS = 4e3;
7354
7337
  var IN_FLIGHT_TTL_MS = 15 * 60 * 1e3;
7355
7338
  function cacheDir() {
@@ -7522,12 +7505,10 @@ function runUpdate(pm) {
7522
7505
  }
7523
7506
 
7524
7507
  // src/daemon.ts
7525
- var HEARTBEAT_MS = 3e4;
7526
- var COMMAND_POLL_MS = 8e3;
7527
- var RECLAIM_PROBE_MS = 2 * 6e4;
7528
7508
  var UPDATE_CHECK_MS = 6 * 60 * 6e4;
7529
- var SWEEP_MS = 10 * 6e4;
7530
7509
  var MAX_WORKERS = 30;
7510
+ var BRIDGE_ATTACH_GRACE_MS = 15e3;
7511
+ var BRIDGE_IDLE_MS = 2e3;
7531
7512
  var LOG_MAX_BYTES = 1e6;
7532
7513
  var LOG_FILE = path4.join(os8.homedir(), ".standardagents", "daemon.log");
7533
7514
  function daemonLog(line) {
@@ -7573,6 +7554,8 @@ var ThreadWorker = class {
7573
7554
  onStatus: (_id, summary) => {
7574
7555
  this.inFlight += summary ? 1 : -1;
7575
7556
  if (this.inFlight < 0) this.inFlight = 0;
7557
+ if (summary) this.clearIdleTimer();
7558
+ else if (this.inFlight === 0) this.scheduleIdle(BRIDGE_IDLE_MS);
7576
7559
  },
7577
7560
  onConnection: (state, attempt) => {
7578
7561
  if (state !== "reconnecting" || attempt === 1 || attempt % 10 === 0) {
@@ -7580,10 +7563,16 @@ var ThreadWorker = class {
7580
7563
  }
7581
7564
  },
7582
7565
  onOwnership: (isOwner, owner) => {
7583
- this.isOwner = isOwner;
7584
7566
  daemonLog(
7585
7567
  `[${threadId.slice(0, 8)}] ownership: ${isOwner ? "OWNER" : "watcher"}` + (owner ? ` (owner: ${owner.client_name || owner.client_id})` : "")
7586
7568
  );
7569
+ if (isOwner) this.watchedAnotherOwner = false;
7570
+ else if (owner) this.watchedAnotherOwner = true;
7571
+ else if (this.watchedAnotherOwner) {
7572
+ this.watchedAnotherOwner = false;
7573
+ this.session.bridge.setClaim("if_unowned");
7574
+ this.session.bridge.refresh();
7575
+ }
7587
7576
  },
7588
7577
  onSuperseded: () => {
7589
7578
  daemonLog(`[${threadId.slice(0, 8)}] superseded by another process with this identity \u2014 standing down`);
@@ -7637,22 +7626,36 @@ var ThreadWorker = class {
7637
7626
  session;
7638
7627
  perm;
7639
7628
  inFlight = 0;
7640
- isOwner = false;
7629
+ idleTimer = null;
7630
+ watchedAnotherOwner = false;
7641
7631
  /** Set by the daemon so a superseded worker can remove itself. */
7642
7632
  onEvicted;
7643
- get bridge() {
7644
- return this.session.bridge;
7645
- }
7633
+ /** Set by the daemon so an inactive bridge removes itself. */
7634
+ onIdle;
7646
7635
  get busy() {
7647
7636
  return this.inFlight > 0;
7648
7637
  }
7638
+ clearIdleTimer() {
7639
+ if (!this.idleTimer) return;
7640
+ clearTimeout(this.idleTimer);
7641
+ this.idleTimer = null;
7642
+ }
7643
+ scheduleIdle(delay) {
7644
+ this.clearIdleTimer();
7645
+ this.idleTimer = setTimeout(() => {
7646
+ this.idleTimer = null;
7647
+ if (!this.busy) this.onIdle?.(this.threadId);
7648
+ }, delay);
7649
+ }
7649
7650
  async start() {
7650
7651
  await this.session.loadApprovals().catch(() => {
7651
7652
  });
7652
7653
  this.session.writeSessionInfo();
7653
7654
  await this.session.bridge.connect();
7655
+ if (!this.busy) this.scheduleIdle(BRIDGE_ATTACH_GRACE_MS);
7654
7656
  }
7655
7657
  stop() {
7658
+ this.clearIdleTimer();
7656
7659
  this.session.stop();
7657
7660
  }
7658
7661
  };
@@ -7694,18 +7697,62 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7694
7697
  process.on("unhandledRejection", (err) => {
7695
7698
  daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
7696
7699
  });
7697
- await touchDaemon(api, identity, version).catch(
7698
- (e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
7700
+ await updateOwnMachineRecord(api, identity, (record2) => {
7701
+ const now = Date.now();
7702
+ record2.daemon = {
7703
+ version,
7704
+ installed_at: record2.daemon?.installed_at ?? now,
7705
+ last_seen_at: 0,
7706
+ pid: process.pid
7707
+ };
7708
+ }).catch(
7709
+ (e) => daemonLog(`machine registration failed: ${e instanceof Error ? e.message : String(e)}`)
7699
7710
  );
7700
- const heartbeat = setInterval(() => {
7701
- void touchDaemon(api, identity, version).catch(() => {
7702
- });
7703
- void getMachineName(api, identity.machine_id).then((n) => {
7704
- if (n) displayName = n;
7705
- }).catch(() => {
7706
- });
7707
- }, HEARTBEAT_MS);
7708
7711
  const workers = /* @__PURE__ */ new Map();
7712
+ let lineActiveThreads = /* @__PURE__ */ new Set();
7713
+ let samaActiveThreads = /* @__PURE__ */ new Set();
7714
+ let hub = null;
7715
+ let updateTimer = null;
7716
+ let updateReady = false;
7717
+ let forcedUpdatePending = false;
7718
+ let restartInProgress = false;
7719
+ let shuttingDown = false;
7720
+ function shutdown(code) {
7721
+ if (shuttingDown) return;
7722
+ shuttingDown = true;
7723
+ if (updateTimer) clearInterval(updateTimer);
7724
+ for (const worker of workers.values()) worker.stop();
7725
+ workers.clear();
7726
+ hub?.close();
7727
+ daemonLog(`daemon exiting (code ${code})`);
7728
+ process.exit(code);
7729
+ }
7730
+ async function restartIfIdle() {
7731
+ if (restartInProgress || [...workers.values()].some((worker) => worker.busy)) return;
7732
+ if (forcedUpdatePending) {
7733
+ const pm = detectPackageManager();
7734
+ forcedUpdatePending = false;
7735
+ if (!pm) {
7736
+ daemonLog("forced update requested but this is not an installed build \u2014 ignoring");
7737
+ return;
7738
+ }
7739
+ restartInProgress = true;
7740
+ daemonLog(`forced update: running ${pm} install now`);
7741
+ const { ok, output: output4 } = await runUpdate(pm);
7742
+ daemonLog(`forced update ${ok ? "succeeded" : "failed"}: ${output4.trim().split("\n").slice(-2).join(" | ")}`);
7743
+ if (ok) {
7744
+ daemonLog("restarting to apply the forced update");
7745
+ shutdown(0);
7746
+ return;
7747
+ }
7748
+ restartInProgress = false;
7749
+ }
7750
+ if (updateReady) {
7751
+ daemonLog("restarting to apply the installed update");
7752
+ shutdown(0);
7753
+ }
7754
+ }
7755
+ const threadIsActive = (threadId) => lineActiveThreads.has(threadId) || samaActiveThreads.has(threadId);
7709
7756
  const attach = async (threadId, tags, createdAt2 = 0) => {
7710
7757
  if (workers.has(threadId)) return;
7711
7758
  const projectDir = pathFromTags(tags);
@@ -7725,9 +7772,19 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7725
7772
  }
7726
7773
  const worker = new ThreadWorker(api, identity, displayName, threadId, projectDir, createdAt2 || Date.now());
7727
7774
  worker.onEvicted = (id) => detach(id);
7775
+ worker.onIdle = (id) => {
7776
+ if (threadIsActive(id)) return;
7777
+ detach(id);
7778
+ void restartIfIdle();
7779
+ };
7728
7780
  workers.set(threadId, worker);
7729
7781
  daemonLog(`attached ${threadId.slice(0, 8)} \u2192 ${projectDir}`);
7730
- await worker.start();
7782
+ try {
7783
+ await worker.start();
7784
+ } catch (error) {
7785
+ detach(threadId);
7786
+ throw error;
7787
+ }
7731
7788
  void registerProject(api, identity, projectDir).catch(() => {
7732
7789
  });
7733
7790
  const rawPath = tags.find((t) => t.startsWith("path:"))?.slice("path:".length);
@@ -7744,66 +7801,17 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7744
7801
  workers.delete(threadId);
7745
7802
  daemonLog(`detached ${threadId.slice(0, 8)}`);
7746
7803
  };
7747
- const sweep = async () => {
7804
+ const attachThreadById = async (threadId, reason) => {
7805
+ if (workers.has(threadId)) return;
7748
7806
  try {
7749
- const threads = await api.listThreads([...AGENT_ID_VARIANTS, OPENSAMA_AGENT_ID], [runnerTag]);
7750
- const recent = threads.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)).slice(0, MAX_WORKERS);
7751
- for (const t of recent) {
7752
- await attach(t.id, t.tags, (t.created_at ?? 0) * 1e3);
7753
- }
7807
+ const thread = await api.getThread(threadId);
7808
+ if (!thread || thread.terminated || !thread.tags.includes(runnerTag)) return;
7809
+ daemonLog(`${reason}: attaching ${threadId.slice(0, 8)}`);
7810
+ await attach(threadId, thread.tags, (thread.created_at ?? 0) * 1e3);
7754
7811
  } catch (e) {
7755
- daemonLog(`sweep failed: ${e instanceof Error ? e.message : String(e)}`);
7812
+ daemonLog(`${reason} attach failed: ${e instanceof Error ? e.message : String(e)}`);
7756
7813
  }
7757
7814
  };
7758
- const events = new SystemEvents(api, {
7759
- onOpen: () => void sweep(),
7760
- onThreadCreated: (t) => {
7761
- if (t.tags?.includes(runnerTag)) void attach(t.id, t.tags ?? [], (t.created_at ?? 0) * 1e3);
7762
- },
7763
- onThreadUpdated: (t) => {
7764
- if (t.terminated) detach(t.id);
7765
- else if (t.tags?.includes(runnerTag)) void attach(t.id, t.tags ?? [], (t.created_at ?? 0) * 1e3);
7766
- },
7767
- onThreadDeleted: (id) => detach(id)
7768
- });
7769
- events.connect();
7770
- const hub = new HubSocket(
7771
- api,
7772
- daemonClientId(identity),
7773
- {
7774
- onWake: (threadId) => {
7775
- if (workers.has(threadId)) return;
7776
- void (async () => {
7777
- try {
7778
- const thread = await api.getThread(threadId);
7779
- if (!thread || thread.terminated) return;
7780
- if (!thread.tags?.includes(runnerTag)) return;
7781
- daemonLog(`wake: attaching ${threadId.slice(0, 8)} on hub signal`);
7782
- await attach(threadId, thread.tags ?? [], (thread.created_at ?? 0) * 1e3);
7783
- } catch (e) {
7784
- daemonLog(`wake attach failed: ${e instanceof Error ? e.message : String(e)}`);
7785
- }
7786
- })();
7787
- },
7788
- onConnection: (state, attempt) => {
7789
- if (state === "reconnecting" && attempt === 1) daemonLog("hub: reconnecting");
7790
- }
7791
- },
7792
- displayName
7793
- );
7794
- hub.start();
7795
- await sweep();
7796
- const reclaim = setInterval(() => {
7797
- for (const worker of workers.values()) {
7798
- if (!worker.isOwner) {
7799
- worker.bridge.setClaim("if_stale");
7800
- worker.bridge.refresh();
7801
- } else {
7802
- worker.bridge.setClaim("if_unowned");
7803
- }
7804
- }
7805
- }, RECLAIM_PROBE_MS);
7806
- let updateReady = false;
7807
7815
  const checkUpdates = async () => {
7808
7816
  try {
7809
7817
  const info = await checkForUpdate(version);
@@ -7818,15 +7826,18 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7818
7826
  if (state && state.version === info.latest && state.exitCode === 0) {
7819
7827
  daemonLog(`auto-update: v${info.latest} installed \u2014 restarting when idle`);
7820
7828
  updateReady = true;
7829
+ await restartIfIdle();
7821
7830
  }
7822
7831
  } catch {
7823
7832
  }
7824
7833
  };
7825
- void checkUpdates();
7826
- const updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
7827
7834
  let draining = false;
7835
+ let drainAgain = false;
7828
7836
  const drainCommands = async () => {
7829
- if (draining) return;
7837
+ if (draining) {
7838
+ drainAgain = true;
7839
+ return;
7840
+ }
7830
7841
  draining = true;
7831
7842
  try {
7832
7843
  const cmds = await readMachineCommands(api, identity.machine_id);
@@ -7851,37 +7862,31 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7851
7862
  await clearMachineCommands(api, identity.machine_id, applied2).catch(() => {
7852
7863
  });
7853
7864
  if (forcedUpdate) {
7854
- const pm = detectPackageManager();
7855
- if (!pm) {
7856
- daemonLog("forced update requested but this is not an installed build \u2014 ignoring");
7857
- } else if (![...workers.values()].some((w) => w.busy)) {
7858
- daemonLog(`forced update: running ${pm} install now`);
7859
- const { ok, output: output4 } = await runUpdate(pm);
7860
- daemonLog(`forced update ${ok ? "succeeded" : "failed"}: ${output4.trim().split("\n").slice(-2).join(" | ")}`);
7861
- if (ok) {
7862
- daemonLog("restarting to apply the forced update");
7863
- shutdown(0);
7864
- }
7865
- } else {
7866
- daemonLog("forced update deferred \u2014 a session is busy; will retry");
7867
- void checkUpdates();
7865
+ forcedUpdatePending = true;
7866
+ if ([...workers.values()].some((worker) => worker.busy)) {
7867
+ daemonLog("forced update deferred until active tool calls finish");
7868
7868
  }
7869
+ await restartIfIdle();
7869
7870
  }
7870
7871
  } finally {
7871
7872
  draining = false;
7873
+ if (drainAgain) {
7874
+ drainAgain = false;
7875
+ void drainCommands();
7876
+ }
7872
7877
  }
7873
7878
  };
7874
- void drainCommands();
7875
- const commandTimer = setInterval(() => void drainCommands(), COMMAND_POLL_MS);
7876
7879
  let lastFsNonce = null;
7877
- let lastFsRequestSeenAt = null;
7878
7880
  let fsBusy = false;
7881
+ let fsAgain = false;
7879
7882
  const drainFsRequests = async () => {
7880
- if (fsBusy) return;
7883
+ if (fsBusy) {
7884
+ fsAgain = true;
7885
+ return;
7886
+ }
7881
7887
  fsBusy = true;
7882
7888
  try {
7883
7889
  const req = await readFsRequest(api, identity.machine_id).catch(() => null);
7884
- if (req) lastFsRequestSeenAt = Math.max(lastFsRequestSeenAt ?? 0, req.requested_at);
7885
7890
  if (!req || req.nonce === lastFsNonce) return;
7886
7891
  lastFsNonce = req.nonce;
7887
7892
  let ok = true;
@@ -7897,42 +7902,131 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7897
7902
  });
7898
7903
  } finally {
7899
7904
  fsBusy = false;
7905
+ if (fsAgain) {
7906
+ fsAgain = false;
7907
+ void drainFsRequests();
7908
+ }
7900
7909
  }
7901
7910
  };
7902
- let fsTimer = null;
7903
- let fsStopped = false;
7904
- const scheduleFsPoll = () => {
7905
- if (fsStopped) return;
7906
- fsTimer = setTimeout(() => {
7907
- void drainFsRequests().finally(scheduleFsPoll);
7908
- }, fsPollDelay(lastFsRequestSeenAt, Date.now()));
7911
+ const activeThreadSnapshot = async () => {
7912
+ const response = await fetch(`${api.origin}/api/users/me/standardcode/sidebar`, {
7913
+ headers: { Authorization: `Bearer ${api.bearer}` }
7914
+ });
7915
+ if (!response.ok) throw new Error(`sidebar snapshot returned ${response.status}`);
7916
+ const snapshot = await response.json();
7917
+ const lines = /* @__PURE__ */ new Set();
7918
+ const sama = /* @__PURE__ */ new Set();
7919
+ for (const entry of snapshot.lines?.active ?? []) {
7920
+ if (typeof entry.thread_id === "string") lines.add(entry.thread_id);
7921
+ }
7922
+ for (const entry of snapshot.activity?.busy ?? []) {
7923
+ if (typeof entry.thread_id === "string") sama.add(entry.thread_id);
7924
+ }
7925
+ return { lines, sama };
7909
7926
  };
7910
- scheduleFsPoll();
7911
- const sweeper = setInterval(() => {
7912
- if (updateReady && ![...workers.values()].some((w) => w.busy)) {
7913
- daemonLog("restarting to apply the installed update");
7914
- shutdown(0);
7927
+ let recovering = false;
7928
+ let recoverAgain = false;
7929
+ const recoverOnConnect = async () => {
7930
+ if (recovering) {
7931
+ recoverAgain = true;
7915
7932
  return;
7916
7933
  }
7917
- void sweep();
7918
- }, SWEEP_MS);
7919
- const shutdown = (code) => {
7920
- clearInterval(heartbeat);
7921
- clearInterval(reclaim);
7922
- clearInterval(updateTimer);
7923
- clearInterval(commandTimer);
7924
- fsStopped = true;
7925
- if (fsTimer) clearTimeout(fsTimer);
7926
- clearInterval(sweeper);
7927
- for (const worker of workers.values()) worker.stop();
7928
- events.close();
7929
- hub.close();
7930
- daemonLog(`daemon exiting (code ${code})`);
7931
- process.exit(code);
7934
+ recovering = true;
7935
+ try {
7936
+ await Promise.all([
7937
+ drainCommands(),
7938
+ drainFsRequests(),
7939
+ getMachineName(api, identity.machine_id).then((name) => {
7940
+ displayName = name || os8.hostname();
7941
+ }).catch(() => {
7942
+ })
7943
+ ]);
7944
+ const snapshot = await activeThreadSnapshot();
7945
+ lineActiveThreads = snapshot.lines;
7946
+ samaActiveThreads = snapshot.sama;
7947
+ const active = [.../* @__PURE__ */ new Set([...snapshot.lines, ...snapshot.sama])].slice(0, MAX_WORKERS);
7948
+ for (const threadId of active) {
7949
+ await attachThreadById(threadId, "recovery");
7950
+ }
7951
+ } catch (error) {
7952
+ daemonLog(`user-stream recovery failed: ${error instanceof Error ? error.message : String(error)}`);
7953
+ } finally {
7954
+ recovering = false;
7955
+ if (recoverAgain) {
7956
+ recoverAgain = false;
7957
+ void recoverOnConnect();
7958
+ }
7959
+ }
7960
+ };
7961
+ const idsFromEvent = (data, field) => {
7962
+ if (!data || typeof data !== "object") return null;
7963
+ const entries = data[field];
7964
+ if (!Array.isArray(entries)) return null;
7965
+ const ids = /* @__PURE__ */ new Set();
7966
+ for (const entry of entries) {
7967
+ if (entry && typeof entry === "object" && typeof entry.thread_id === "string") {
7968
+ ids.add(entry.thread_id);
7969
+ }
7970
+ }
7971
+ return ids;
7972
+ };
7973
+ const releaseInactiveWorkers = () => {
7974
+ for (const worker of [...workers.values()]) {
7975
+ if (!worker.busy && !threadIsActive(worker.threadId)) detach(worker.threadId);
7976
+ }
7977
+ void restartIfIdle();
7932
7978
  };
7979
+ const machinePrefix = `standardcode.machine.${identity.machine_id}`;
7980
+ hub = new DaemonUserStream(
7981
+ api,
7982
+ daemonClientId(identity),
7983
+ {
7984
+ onWake: (threadId) => void attachThreadById(threadId, "wake"),
7985
+ onEvent: (event, data) => {
7986
+ if (event === "standardcode.lines_changed") {
7987
+ const ids = idsFromEvent(data, "active");
7988
+ if (ids) {
7989
+ lineActiveThreads = ids;
7990
+ releaseInactiveWorkers();
7991
+ }
7992
+ return;
7993
+ }
7994
+ if (event === "standardcode.activity_changed") {
7995
+ const ids = idsFromEvent(data, "busy");
7996
+ if (ids) {
7997
+ samaActiveThreads = ids;
7998
+ releaseInactiveWorkers();
7999
+ }
8000
+ return;
8001
+ }
8002
+ if (event !== "standardcode.machine_changed") return;
8003
+ const key = data && typeof data === "object" ? data.key : null;
8004
+ if (key === `${machinePrefix}.cmd`) void drainCommands();
8005
+ else if (key === `${machinePrefix}.fsreq`) void drainFsRequests();
8006
+ else if (key === `${machinePrefix}.name`) {
8007
+ void getMachineName(api, identity.machine_id).then((name) => {
8008
+ displayName = name || os8.hostname();
8009
+ }).catch(() => {
8010
+ });
8011
+ }
8012
+ },
8013
+ onConnection: (state, attempt) => {
8014
+ if (state === "reconnecting") {
8015
+ if (attempt === 1) daemonLog("user stream: reconnecting");
8016
+ return;
8017
+ }
8018
+ daemonLog("user stream: connected");
8019
+ void recoverOnConnect();
8020
+ }
8021
+ },
8022
+ displayName
8023
+ );
8024
+ hub.start();
8025
+ void checkUpdates();
8026
+ updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
7933
8027
  process.on("SIGTERM", () => shutdown(0));
7934
8028
  process.on("SIGINT", () => shutdown(0));
7935
- daemonLog(`daemon ready \u2014 watching for threads tagged ${runnerTag}`);
8029
+ daemonLog(`daemon ready \u2014 waiting for work tagged ${runnerTag}`);
7936
8030
  await new Promise(() => {
7937
8031
  });
7938
8032
  }
@@ -8220,7 +8314,7 @@ async function installCommand(endpointFlag) {
8220
8314
  }
8221
8315
  stdout.write(`${c3.green}\u2713${c3.reset} ${result.detail}
8222
8316
  `);
8223
- stdout.write(`${c3.dim}Waiting for the daemon's first heartbeat\u2026${c3.reset}
8317
+ stdout.write(`${c3.dim}Waiting for the daemon to connect\u2026${c3.reset}
8224
8318
  `);
8225
8319
  const deadline = Date.now() + 6e4;
8226
8320
  let alive = false;
@@ -8243,7 +8337,7 @@ or add one now: standardcode daemon add-project <path>${c3.reset}
8243
8337
  );
8244
8338
  } else {
8245
8339
  stdout.write(
8246
- `${c3.yellow}\u26A0${c3.reset} The service installed but no heartbeat arrived yet.
8340
+ `${c3.yellow}\u26A0${c3.reset} The service installed but has not connected yet.
8247
8341
  ${c3.dim}Check ~/.standardagents/daemon.log and \`standardcode daemon status\`.${c3.reset}
8248
8342
  `
8249
8343
  );
@@ -8278,8 +8372,9 @@ async function statusCommand() {
8278
8372
  return;
8279
8373
  }
8280
8374
  const online = daemonOnline(record2);
8281
- const seen = record2.daemon ? `${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
8282
- stdout.write(`${c3.bold}Registry:${c3.reset} ${online ? `${c3.green}online${c3.reset}` : `${c3.yellow}offline${c3.reset}`} \xB7 last heartbeat ${seen}
8375
+ const streamConnected = !!record2.daemon && record2.daemon.last_seen_at > Date.now();
8376
+ const seen = streamConnected && record2.daemon ? `user stream connected (v${record2.daemon.version})` : record2.daemon ? `last legacy heartbeat ${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
8377
+ stdout.write(`${c3.bold}Registry:${c3.reset} ${online ? `${c3.green}online${c3.reset}` : `${c3.yellow}offline${c3.reset}`} \xB7 ${seen}
8283
8378
  `);
8284
8379
  const projects = Object.keys(record2.projects);
8285
8380
  stdout.write(`${c3.bold}Projects:${c3.reset} ${projects.length ? "" : c3.dim + "none registered" + c3.reset}