@standardagents/code 0.12.1 → 0.13.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.js CHANGED
@@ -632,6 +632,17 @@ var ApiClient = class {
632
632
  }
633
633
  /** Write one value to the signed-in user's account-wide KV (null deletes). */
634
634
  async userKvSet(key, value) {
635
+ if (key.startsWith("standardcode.machine.")) {
636
+ try {
637
+ await this.json(`/api/users/me/standardcode/machine_value`, {
638
+ method: "POST",
639
+ body: JSON.stringify({ key, value })
640
+ });
641
+ return;
642
+ } catch (error) {
643
+ if (!(error instanceof ApiHttpError) || error.status !== 404) throw error;
644
+ }
645
+ }
635
646
  await this.json(`/api/users/me/kv`, {
636
647
  method: "POST",
637
648
  body: JSON.stringify({ key, value })
@@ -2028,7 +2039,8 @@ var HostTools = class {
2028
2039
  }
2029
2040
  const updated = replaceAll ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
2030
2041
  await fsp.writeFile(file2, updated, "utf8");
2031
- return { ok: true, result: `Edited ${path4.relative(this.projectDir, file2)} (${count} replacement${count === 1 ? "" : "s"})` };
2042
+ const startLine = content.slice(0, content.indexOf(oldStr)).split("\n").length;
2043
+ return { ok: true, result: `Edited ${path4.relative(this.projectDir, file2)} (${count} replacement${count === 1 ? "" : "s"}) @ line ${startLine}` };
2032
2044
  }
2033
2045
  /**
2034
2046
  * Copy a file from the THREAD filesystem (e.g. a generated /attachments/*
@@ -3094,6 +3106,147 @@ var MessageStream = class {
3094
3106
  }
3095
3107
  }
3096
3108
  };
3109
+ var AccountUserStream = class {
3110
+ constructor(api, clientId, options) {
3111
+ this.api = api;
3112
+ this.clientId = clientId;
3113
+ this.options = options;
3114
+ }
3115
+ api;
3116
+ clientId;
3117
+ options;
3118
+ ws = null;
3119
+ closed = false;
3120
+ started = false;
3121
+ heartbeat = null;
3122
+ reconnectAttempt = 0;
3123
+ reconnectTimer = null;
3124
+ eventListeners = /* @__PURE__ */ new Set();
3125
+ connectionListeners = /* @__PURE__ */ new Set();
3126
+ connected = false;
3127
+ start() {
3128
+ if (this.started || this.closed) return;
3129
+ this.started = true;
3130
+ this.openSocket();
3131
+ }
3132
+ onEvent(listener) {
3133
+ this.eventListeners.add(listener);
3134
+ return () => this.eventListeners.delete(listener);
3135
+ }
3136
+ onConnection(listener) {
3137
+ this.connectionListeners.add(listener);
3138
+ return () => this.connectionListeners.delete(listener);
3139
+ }
3140
+ /** Wait for a live socket. The timeout is local and performs no network work. */
3141
+ waitUntilConnected(timeoutMs = 15e3) {
3142
+ if (this.connected) return Promise.resolve();
3143
+ this.start();
3144
+ return new Promise((resolve, reject) => {
3145
+ let timer;
3146
+ const remove = this.onConnection((state) => {
3147
+ if (state !== "connected") return;
3148
+ clearTimeout(timer);
3149
+ remove();
3150
+ resolve();
3151
+ });
3152
+ timer = setTimeout(() => {
3153
+ remove();
3154
+ reject(new Error("The account stream did not connect in time."));
3155
+ }, timeoutMs);
3156
+ });
3157
+ }
3158
+ openSocket() {
3159
+ if (this.closed) return;
3160
+ const clientId = this.options.uniqueConnectionId ? `${this.clientId}:${crypto.randomBytes(8).toString("hex")}` : this.clientId;
3161
+ let url = `${this.api.wsEndpoint}/api/users/me/stream?token=${encodeURIComponent(this.api.bearer)}&client_id=${encodeURIComponent(clientId)}&client_kind=${encodeURIComponent(this.options.clientKind)}`;
3162
+ if (this.options.clientName) url += `&client_name=${encodeURIComponent(this.options.clientName)}`;
3163
+ let ws;
3164
+ try {
3165
+ ws = new WebSocket(url);
3166
+ } catch {
3167
+ this.scheduleReconnect();
3168
+ return;
3169
+ }
3170
+ this.ws = ws;
3171
+ ws.addEventListener("open", () => {
3172
+ if (this.ws !== ws) return;
3173
+ this.connected = true;
3174
+ this.reconnectAttempt = 0;
3175
+ this.startHeartbeat(ws);
3176
+ this.emitConnection("connected", 0);
3177
+ });
3178
+ ws.addEventListener("message", (event) => {
3179
+ if (this.ws === ws) this.heartbeat?.markAlive();
3180
+ this.onMessage(String(event.data));
3181
+ });
3182
+ ws.addEventListener("error", () => this.handleDrop(ws));
3183
+ ws.addEventListener("close", () => this.handleDrop(ws));
3184
+ }
3185
+ onMessage(raw) {
3186
+ let message;
3187
+ try {
3188
+ message = JSON.parse(raw);
3189
+ } catch {
3190
+ return;
3191
+ }
3192
+ if (!message || typeof message !== "object") return;
3193
+ const frame = message;
3194
+ if (typeof frame.event === "string") {
3195
+ this.emitEvent(frame.event, frame.data);
3196
+ return;
3197
+ }
3198
+ if (frame.type === "wake" && typeof frame.threadId === "string") {
3199
+ this.emitEvent("standardagents.wake", { thread_id: frame.threadId });
3200
+ }
3201
+ }
3202
+ emitEvent(event, data) {
3203
+ for (const listener of this.eventListeners) listener(event, data);
3204
+ }
3205
+ emitConnection(state, attempt) {
3206
+ for (const listener of this.connectionListeners) listener(state, attempt);
3207
+ }
3208
+ handleDrop(ws) {
3209
+ if (this.ws !== ws) return;
3210
+ this.ws = null;
3211
+ this.connected = false;
3212
+ this.stopHeartbeat();
3213
+ this.scheduleReconnect();
3214
+ }
3215
+ scheduleReconnect() {
3216
+ if (this.closed || this.reconnectTimer) return;
3217
+ this.reconnectAttempt++;
3218
+ this.emitConnection("reconnecting", this.reconnectAttempt);
3219
+ const base = Math.min(500 * 2 ** (this.reconnectAttempt - 1), 15e3);
3220
+ const delay = base + Math.floor(Math.random() * 400);
3221
+ this.reconnectTimer = setTimeout(() => {
3222
+ this.reconnectTimer = null;
3223
+ this.openSocket();
3224
+ }, delay);
3225
+ }
3226
+ startHeartbeat(ws) {
3227
+ this.stopHeartbeat();
3228
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
3229
+ this.heartbeat.start();
3230
+ }
3231
+ stopHeartbeat() {
3232
+ this.heartbeat?.stop();
3233
+ this.heartbeat = null;
3234
+ }
3235
+ close() {
3236
+ this.closed = true;
3237
+ this.connected = false;
3238
+ this.stopHeartbeat();
3239
+ if (this.reconnectTimer) {
3240
+ clearTimeout(this.reconnectTimer);
3241
+ this.reconnectTimer = null;
3242
+ }
3243
+ const ws = this.ws;
3244
+ this.ws = null;
3245
+ ws?.close();
3246
+ this.eventListeners.clear();
3247
+ this.connectionListeners.clear();
3248
+ }
3249
+ };
3097
3250
 
3098
3251
  // src/events-stream.ts
3099
3252
  var SystemEvents = class {
@@ -6491,7 +6644,7 @@ function readVersion() {
6491
6644
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
6492
6645
  } catch {
6493
6646
  }
6494
- return "0.12.1" ;
6647
+ return "0.13.1" ;
6495
6648
  }
6496
6649
  function isLocalHost(host) {
6497
6650
  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,12 +6759,67 @@ async function readFsResponse(api, machineId) {
6606
6759
  async function readFsRequest(api, machineId) {
6607
6760
  return parseFsRequest(await api.userKvGet(fsRequestKey(machineId)));
6608
6761
  }
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;
6762
+ async function requestFsBrowse(api, accountStream, machineId, req, timeoutMs = 15e3) {
6763
+ await accountStream.waitUntilConnected(timeoutMs);
6764
+ const responseKey = fsResponseKey(machineId);
6765
+ return new Promise((resolve, reject) => {
6766
+ let settled = false;
6767
+ let reading = false;
6768
+ let readAgain = false;
6769
+ let timer;
6770
+ let removeEvent = () => {
6771
+ };
6772
+ let removeConnection = () => {
6773
+ };
6774
+ const cleanup = () => {
6775
+ clearTimeout(timer);
6776
+ removeEvent();
6777
+ removeConnection();
6778
+ };
6779
+ const fail = (error) => {
6780
+ if (settled) return;
6781
+ settled = true;
6782
+ cleanup();
6783
+ reject(error);
6784
+ };
6785
+ const read = async () => {
6786
+ if (settled) return;
6787
+ if (reading) {
6788
+ readAgain = true;
6789
+ return;
6790
+ }
6791
+ reading = true;
6792
+ try {
6793
+ do {
6794
+ readAgain = false;
6795
+ const response = await readFsResponse(api, machineId);
6796
+ if (response?.nonce === req.nonce) {
6797
+ settled = true;
6798
+ cleanup();
6799
+ resolve(response);
6800
+ return;
6801
+ }
6802
+ } while (readAgain && !settled);
6803
+ } catch (error) {
6804
+ fail(error);
6805
+ } finally {
6806
+ reading = false;
6807
+ }
6808
+ };
6809
+ removeEvent = accountStream.onEvent((event, data) => {
6810
+ if (event !== "standardcode.machine_changed") return;
6811
+ const key = data && typeof data === "object" ? data.key : null;
6812
+ if (key === responseKey) void read();
6813
+ });
6814
+ removeConnection = accountStream.onConnection((state) => {
6815
+ if (state === "connected") void read();
6816
+ });
6817
+ timer = setTimeout(
6818
+ () => fail(new Error("The remote machine did not answer in time.")),
6819
+ timeoutMs
6820
+ );
6821
+ void writeFsRequest(api, machineId, req).catch(fail);
6822
+ });
6615
6823
  }
6616
6824
  async function writeFsResponse(api, machineId, res) {
6617
6825
  await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
@@ -6762,8 +6970,11 @@ async function loadMachine(api, machineId) {
6762
6970
  overlayProjectNames(rec, projNames);
6763
6971
  return rec;
6764
6972
  }
6973
+ var CONNECTED_LAST_SEEN_AT = Number.MAX_SAFE_INTEGER;
6765
6974
  function daemonOnline(record2, now = Date.now()) {
6766
- return !!record2.daemon && now - record2.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
6975
+ if (!record2.daemon) return false;
6976
+ if (typeof record2.daemon.connected === "boolean") return record2.daemon.connected;
6977
+ return now - record2.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
6767
6978
  }
6768
6979
  function newRecord(identity) {
6769
6980
  const now = Date.now();
@@ -6792,6 +7003,18 @@ async function updateOwnMachineRecord(api, identity, mutate) {
6792
7003
  await api.userKvSet(machineKey(identity.machine_id), record2);
6793
7004
  return record2;
6794
7005
  }
7006
+ async function setDaemonPresence(api, identity, version, connected, pid = process.pid) {
7007
+ await updateOwnMachineRecord(api, identity, (record2) => {
7008
+ const now = Date.now();
7009
+ record2.daemon = {
7010
+ version,
7011
+ installed_at: record2.daemon?.installed_at ?? now,
7012
+ last_seen_at: connected ? CONNECTED_LAST_SEEN_AT : 0,
7013
+ connected,
7014
+ pid
7015
+ };
7016
+ });
7017
+ }
6795
7018
  function projectRepository(projectDir) {
6796
7019
  try {
6797
7020
  const url = execFileSync("git", ["-C", projectDir, "remote", "get-url", "origin"], {
@@ -6838,17 +7061,6 @@ async function unregisterProject(api, identity, projectDir) {
6838
7061
  }
6839
7062
  });
6840
7063
  }
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
7064
  async function clearDaemon(api, identity) {
6853
7065
  await updateOwnMachineRecord(api, identity, (record2) => {
6854
7066
  record2.daemon = null;
@@ -7045,22 +7257,20 @@ function parseBrowseResult(value) {
7045
7257
  error: typeof r.error === "string" ? r.error : void 0
7046
7258
  };
7047
7259
  }
7048
- function remoteBrowseBackend(api, machineId, label) {
7260
+ function remoteBrowseBackend(api, accountStream, machineId, label) {
7049
7261
  const rpc = async (req) => {
7050
7262
  const nonce = crypto.randomBytes(8).toString("hex");
7051
- await writeFsRequest(api, machineId, { nonce, ...req });
7052
- const deadline = Date.now() + 15e3;
7053
- while (Date.now() < deadline) {
7054
- await new Promise((r) => setTimeout(r, 700));
7055
- const res = await readFsResponse(api, machineId).catch(() => null);
7056
- if (res && res.nonce === nonce) {
7057
- if (!res.ok) throw new Error(res.error || "Browse failed on the remote machine.");
7058
- const parsed = parseBrowseResult(res.result);
7059
- if (!parsed) throw new Error("The remote machine sent an unreadable listing.");
7060
- return parsed;
7263
+ const res = await requestFsBrowse(api, accountStream, machineId, { nonce, ...req }).catch(
7264
+ (error) => {
7265
+ throw new Error(
7266
+ error instanceof Error && error.message === "The remote machine did not answer in time." ? `${label} didn't answer \u2014 is its daemon online?` : error instanceof Error ? error.message : String(error)
7267
+ );
7061
7268
  }
7062
- }
7063
- throw new Error(`${label} didn't answer \u2014 is its daemon online?`);
7269
+ );
7270
+ if (!res.ok) throw new Error(res.error || "Browse failed on the remote machine.");
7271
+ const parsed = parseBrowseResult(res.result);
7272
+ if (!parsed) throw new Error("The remote machine sent an unreadable listing.");
7273
+ return parsed;
7064
7274
  };
7065
7275
  return {
7066
7276
  label,
@@ -7243,105 +7453,30 @@ async function awaitApprovalViaRelay(api, threadId, request, options = {}) {
7243
7453
  }
7244
7454
  }
7245
7455
 
7246
- // src/hub.ts
7247
- var HubSocket = class {
7456
+ // src/daemon-user-stream.ts
7457
+ var DaemonUserStream = class {
7458
+ stream;
7248
7459
  constructor(api, clientId, hooks, clientName) {
7249
- this.api = api;
7250
- this.clientId = clientId;
7251
- this.hooks = hooks;
7252
- this.clientName = clientName;
7253
- }
7254
- api;
7255
- clientId;
7256
- hooks;
7257
- clientName;
7258
- ws = null;
7259
- closed = false;
7260
- heartbeat = null;
7261
- reconnectAttempt = 0;
7262
- reconnectTimer = null;
7263
- /** Open the socket and keep it connected (reconnect forever on drop). */
7264
- start() {
7265
- this.openSocket();
7266
- }
7267
- openSocket() {
7268
- 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`;
7270
- if (this.clientName) url += `&client_name=${encodeURIComponent(this.clientName)}`;
7271
- let ws;
7272
- try {
7273
- ws = new WebSocket(url);
7274
- } catch {
7275
- this.scheduleReconnect();
7276
- return;
7277
- }
7278
- this.ws = ws;
7279
- ws.addEventListener("open", () => {
7280
- this.reconnectAttempt = 0;
7281
- this.startHeartbeat(ws);
7282
- this.hooks.onConnection?.("connected", 0);
7460
+ this.stream = new AccountUserStream(api, clientId, {
7461
+ clientKind: "daemon",
7462
+ clientName,
7463
+ uniqueConnectionId: true
7283
7464
  });
7284
- ws.addEventListener("message", (ev) => {
7285
- if (this.ws === ws) this.heartbeat?.markAlive();
7286
- this.onMessage(String(ev.data));
7465
+ this.stream.onEvent((event, data) => {
7466
+ if (event === "standardagents.wake") {
7467
+ const threadId = data?.thread_id;
7468
+ if (typeof threadId === "string") hooks.onWake(threadId);
7469
+ return;
7470
+ }
7471
+ hooks.onEvent(event, data);
7287
7472
  });
7288
- ws.addEventListener("error", () => this.handleDrop(ws));
7289
- ws.addEventListener("close", () => this.handleDrop(ws));
7473
+ if (hooks.onConnection) this.stream.onConnection(hooks.onConnection);
7290
7474
  }
7291
- onMessage(raw) {
7292
- let msg;
7293
- try {
7294
- msg = JSON.parse(raw);
7295
- } catch {
7296
- return;
7297
- }
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);
7303
- return;
7304
- }
7305
- if (frame.type === "wake" && typeof frame.threadId === "string") {
7306
- this.hooks.onWake(frame.threadId);
7307
- }
7308
- }
7309
- handleDrop(ws) {
7310
- if (this.ws !== ws) return;
7311
- this.ws = null;
7312
- this.stopHeartbeat();
7313
- this.scheduleReconnect();
7314
- }
7315
- scheduleReconnect() {
7316
- if (this.closed || this.reconnectTimer) return;
7317
- this.reconnectAttempt++;
7318
- this.hooks.onConnection?.("reconnecting", this.reconnectAttempt);
7319
- const base = Math.min(500 * 2 ** (this.reconnectAttempt - 1), 15e3);
7320
- const delay = base + Math.floor(Math.random() * 400);
7321
- this.reconnectTimer = setTimeout(() => {
7322
- this.reconnectTimer = null;
7323
- this.openSocket();
7324
- }, delay);
7325
- }
7326
- startHeartbeat(ws) {
7327
- this.stopHeartbeat();
7328
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
7329
- this.heartbeat.start();
7330
- }
7331
- stopHeartbeat() {
7332
- if (this.heartbeat) {
7333
- this.heartbeat.stop();
7334
- this.heartbeat = null;
7335
- }
7475
+ start() {
7476
+ this.stream.start();
7336
7477
  }
7337
7478
  close() {
7338
- this.closed = true;
7339
- this.stopHeartbeat();
7340
- if (this.reconnectTimer) {
7341
- clearTimeout(this.reconnectTimer);
7342
- this.reconnectTimer = null;
7343
- }
7344
- this.ws?.close();
7479
+ this.stream.close();
7345
7480
  }
7346
7481
  };
7347
7482
  var PKG_NAME = "@standardagents/code";
@@ -7522,12 +7657,10 @@ function runUpdate(pm) {
7522
7657
  }
7523
7658
 
7524
7659
  // src/daemon.ts
7525
- var HEARTBEAT_MS = 3e4;
7526
- var COMMAND_POLL_MS = 8e3;
7527
- var RECLAIM_PROBE_MS = 2 * 6e4;
7528
7660
  var UPDATE_CHECK_MS = 6 * 60 * 6e4;
7529
- var SWEEP_MS = 10 * 6e4;
7530
7661
  var MAX_WORKERS = 30;
7662
+ var BRIDGE_ATTACH_GRACE_MS = 15e3;
7663
+ var BRIDGE_IDLE_MS = 2e3;
7531
7664
  var LOG_MAX_BYTES = 1e6;
7532
7665
  var LOG_FILE = path4.join(os8.homedir(), ".standardagents", "daemon.log");
7533
7666
  function daemonLog(line) {
@@ -7573,6 +7706,8 @@ var ThreadWorker = class {
7573
7706
  onStatus: (_id, summary) => {
7574
7707
  this.inFlight += summary ? 1 : -1;
7575
7708
  if (this.inFlight < 0) this.inFlight = 0;
7709
+ if (summary) this.clearIdleTimer();
7710
+ else if (this.inFlight === 0) this.scheduleIdle(BRIDGE_IDLE_MS);
7576
7711
  },
7577
7712
  onConnection: (state, attempt) => {
7578
7713
  if (state !== "reconnecting" || attempt === 1 || attempt % 10 === 0) {
@@ -7580,10 +7715,16 @@ var ThreadWorker = class {
7580
7715
  }
7581
7716
  },
7582
7717
  onOwnership: (isOwner, owner) => {
7583
- this.isOwner = isOwner;
7584
7718
  daemonLog(
7585
7719
  `[${threadId.slice(0, 8)}] ownership: ${isOwner ? "OWNER" : "watcher"}` + (owner ? ` (owner: ${owner.client_name || owner.client_id})` : "")
7586
7720
  );
7721
+ if (isOwner) this.watchedAnotherOwner = false;
7722
+ else if (owner) this.watchedAnotherOwner = true;
7723
+ else if (this.watchedAnotherOwner) {
7724
+ this.watchedAnotherOwner = false;
7725
+ this.session.bridge.setClaim("if_unowned");
7726
+ this.session.bridge.refresh();
7727
+ }
7587
7728
  },
7588
7729
  onSuperseded: () => {
7589
7730
  daemonLog(`[${threadId.slice(0, 8)}] superseded by another process with this identity \u2014 standing down`);
@@ -7637,22 +7778,36 @@ var ThreadWorker = class {
7637
7778
  session;
7638
7779
  perm;
7639
7780
  inFlight = 0;
7640
- isOwner = false;
7781
+ idleTimer = null;
7782
+ watchedAnotherOwner = false;
7641
7783
  /** Set by the daemon so a superseded worker can remove itself. */
7642
7784
  onEvicted;
7643
- get bridge() {
7644
- return this.session.bridge;
7645
- }
7785
+ /** Set by the daemon so an inactive bridge removes itself. */
7786
+ onIdle;
7646
7787
  get busy() {
7647
7788
  return this.inFlight > 0;
7648
7789
  }
7790
+ clearIdleTimer() {
7791
+ if (!this.idleTimer) return;
7792
+ clearTimeout(this.idleTimer);
7793
+ this.idleTimer = null;
7794
+ }
7795
+ scheduleIdle(delay) {
7796
+ this.clearIdleTimer();
7797
+ this.idleTimer = setTimeout(() => {
7798
+ this.idleTimer = null;
7799
+ if (!this.busy) this.onIdle?.(this.threadId);
7800
+ }, delay);
7801
+ }
7649
7802
  async start() {
7650
7803
  await this.session.loadApprovals().catch(() => {
7651
7804
  });
7652
7805
  this.session.writeSessionInfo();
7653
7806
  await this.session.bridge.connect();
7807
+ if (!this.busy) this.scheduleIdle(BRIDGE_ATTACH_GRACE_MS);
7654
7808
  }
7655
7809
  stop() {
7810
+ this.clearIdleTimer();
7656
7811
  this.session.stop();
7657
7812
  }
7658
7813
  };
@@ -7694,18 +7849,67 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7694
7849
  process.on("unhandledRejection", (err) => {
7695
7850
  daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
7696
7851
  });
7697
- await touchDaemon(api, identity, version).catch(
7698
- (e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
7852
+ await setDaemonPresence(api, identity, version, false).catch(
7853
+ (e) => daemonLog(`machine registration failed: ${e instanceof Error ? e.message : String(e)}`)
7699
7854
  );
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
7855
  const workers = /* @__PURE__ */ new Map();
7856
+ let lineActiveThreads = /* @__PURE__ */ new Set();
7857
+ let samaActiveThreads = /* @__PURE__ */ new Set();
7858
+ let hub = null;
7859
+ let updateTimer = null;
7860
+ let updateReady = false;
7861
+ let forcedUpdatePending = false;
7862
+ let restartInProgress = false;
7863
+ let shuttingDown = false;
7864
+ let presenceWrite = Promise.resolve();
7865
+ function publishPresence(connected) {
7866
+ presenceWrite = presenceWrite.then(() => setDaemonPresence(api, identity, version, connected)).catch((error) => {
7867
+ daemonLog(
7868
+ `presence ${connected ? "publish" : "clear"} failed: ${error instanceof Error ? error.message : String(error)}`
7869
+ );
7870
+ });
7871
+ return presenceWrite;
7872
+ }
7873
+ async function shutdown(code) {
7874
+ if (shuttingDown) return;
7875
+ shuttingDown = true;
7876
+ if (updateTimer) clearInterval(updateTimer);
7877
+ for (const worker of workers.values()) worker.stop();
7878
+ workers.clear();
7879
+ hub?.close();
7880
+ await Promise.race([
7881
+ publishPresence(false),
7882
+ new Promise((resolve) => setTimeout(resolve, 2e3))
7883
+ ]);
7884
+ daemonLog(`daemon exiting (code ${code})`);
7885
+ process.exit(code);
7886
+ }
7887
+ async function restartIfIdle() {
7888
+ if (restartInProgress || [...workers.values()].some((worker) => worker.busy)) return;
7889
+ if (forcedUpdatePending) {
7890
+ const pm = detectPackageManager();
7891
+ forcedUpdatePending = false;
7892
+ if (!pm) {
7893
+ daemonLog("forced update requested but this is not an installed build \u2014 ignoring");
7894
+ return;
7895
+ }
7896
+ restartInProgress = true;
7897
+ daemonLog(`forced update: running ${pm} install now`);
7898
+ const { ok, output: output4 } = await runUpdate(pm);
7899
+ daemonLog(`forced update ${ok ? "succeeded" : "failed"}: ${output4.trim().split("\n").slice(-2).join(" | ")}`);
7900
+ if (ok) {
7901
+ daemonLog("restarting to apply the forced update");
7902
+ await shutdown(0);
7903
+ return;
7904
+ }
7905
+ restartInProgress = false;
7906
+ }
7907
+ if (updateReady) {
7908
+ daemonLog("restarting to apply the installed update");
7909
+ await shutdown(0);
7910
+ }
7911
+ }
7912
+ const threadIsActive = (threadId) => lineActiveThreads.has(threadId) || samaActiveThreads.has(threadId);
7709
7913
  const attach = async (threadId, tags, createdAt2 = 0) => {
7710
7914
  if (workers.has(threadId)) return;
7711
7915
  const projectDir = pathFromTags(tags);
@@ -7725,9 +7929,19 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7725
7929
  }
7726
7930
  const worker = new ThreadWorker(api, identity, displayName, threadId, projectDir, createdAt2 || Date.now());
7727
7931
  worker.onEvicted = (id) => detach(id);
7932
+ worker.onIdle = (id) => {
7933
+ if (threadIsActive(id)) return;
7934
+ detach(id);
7935
+ void restartIfIdle();
7936
+ };
7728
7937
  workers.set(threadId, worker);
7729
7938
  daemonLog(`attached ${threadId.slice(0, 8)} \u2192 ${projectDir}`);
7730
- await worker.start();
7939
+ try {
7940
+ await worker.start();
7941
+ } catch (error) {
7942
+ detach(threadId);
7943
+ throw error;
7944
+ }
7731
7945
  void registerProject(api, identity, projectDir).catch(() => {
7732
7946
  });
7733
7947
  const rawPath = tags.find((t) => t.startsWith("path:"))?.slice("path:".length);
@@ -7744,66 +7958,17 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7744
7958
  workers.delete(threadId);
7745
7959
  daemonLog(`detached ${threadId.slice(0, 8)}`);
7746
7960
  };
7747
- const sweep = async () => {
7961
+ const attachThreadById = async (threadId, reason) => {
7962
+ if (workers.has(threadId)) return;
7748
7963
  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
- }
7964
+ const thread = await api.getThread(threadId);
7965
+ if (!thread || thread.terminated || !thread.tags.includes(runnerTag)) return;
7966
+ daemonLog(`${reason}: attaching ${threadId.slice(0, 8)}`);
7967
+ await attach(threadId, thread.tags, (thread.created_at ?? 0) * 1e3);
7754
7968
  } catch (e) {
7755
- daemonLog(`sweep failed: ${e instanceof Error ? e.message : String(e)}`);
7969
+ daemonLog(`${reason} attach failed: ${e instanceof Error ? e.message : String(e)}`);
7756
7970
  }
7757
7971
  };
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
7972
  const checkUpdates = async () => {
7808
7973
  try {
7809
7974
  const info = await checkForUpdate(version);
@@ -7818,15 +7983,18 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7818
7983
  if (state && state.version === info.latest && state.exitCode === 0) {
7819
7984
  daemonLog(`auto-update: v${info.latest} installed \u2014 restarting when idle`);
7820
7985
  updateReady = true;
7986
+ await restartIfIdle();
7821
7987
  }
7822
7988
  } catch {
7823
7989
  }
7824
7990
  };
7825
- void checkUpdates();
7826
- const updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
7827
7991
  let draining = false;
7992
+ let drainAgain = false;
7828
7993
  const drainCommands = async () => {
7829
- if (draining) return;
7994
+ if (draining) {
7995
+ drainAgain = true;
7996
+ return;
7997
+ }
7830
7998
  draining = true;
7831
7999
  try {
7832
8000
  const cmds = await readMachineCommands(api, identity.machine_id);
@@ -7851,39 +8019,40 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7851
8019
  await clearMachineCommands(api, identity.machine_id, applied2).catch(() => {
7852
8020
  });
7853
8021
  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();
8022
+ forcedUpdatePending = true;
8023
+ if ([...workers.values()].some((worker) => worker.busy)) {
8024
+ daemonLog("forced update deferred until active tool calls finish");
7868
8025
  }
8026
+ await restartIfIdle();
7869
8027
  }
7870
8028
  } finally {
7871
8029
  draining = false;
8030
+ if (drainAgain) {
8031
+ drainAgain = false;
8032
+ void drainCommands();
8033
+ }
7872
8034
  }
7873
8035
  };
7874
- void drainCommands();
7875
- const commandTimer = setInterval(() => void drainCommands(), COMMAND_POLL_MS);
7876
8036
  let lastFsNonce = null;
7877
- let lastFsRequestSeenAt = null;
7878
8037
  let fsBusy = false;
8038
+ let fsAgain = false;
7879
8039
  const drainFsRequests = async () => {
7880
- if (fsBusy) return;
8040
+ if (fsBusy) {
8041
+ fsAgain = true;
8042
+ return;
8043
+ }
7881
8044
  fsBusy = true;
7882
8045
  try {
7883
- const req = await readFsRequest(api, identity.machine_id).catch(() => null);
7884
- if (req) lastFsRequestSeenAt = Math.max(lastFsRequestSeenAt ?? 0, req.requested_at);
8046
+ let req;
8047
+ try {
8048
+ req = await readFsRequest(api, identity.machine_id);
8049
+ } catch (error2) {
8050
+ daemonLog(`fs-browse request read failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
8051
+ return;
8052
+ }
7885
8053
  if (!req || req.nonce === lastFsNonce) return;
7886
8054
  lastFsNonce = req.nonce;
8055
+ daemonLog(`fs-browse ${req.op} received (${req.nonce})`);
7887
8056
  let ok = true;
7888
8057
  let result;
7889
8058
  let error;
@@ -7893,46 +8062,144 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7893
8062
  ok = false;
7894
8063
  error = err instanceof Error ? err.message : "browse failed";
7895
8064
  }
7896
- await writeFsResponse(api, identity.machine_id, { nonce: req.nonce, ok, result, error }).catch(() => {
7897
- });
8065
+ try {
8066
+ await writeFsResponse(api, identity.machine_id, { nonce: req.nonce, ok, result, error });
8067
+ daemonLog(`fs-browse ${req.op} ${ok ? "answered" : "failed"} (${req.nonce})${error ? `: ${error}` : ""}`);
8068
+ } catch (writeError) {
8069
+ daemonLog(`fs-browse response write failed (${req.nonce}): ${writeError instanceof Error ? writeError.message : String(writeError)}`);
8070
+ }
7898
8071
  } finally {
7899
8072
  fsBusy = false;
8073
+ if (fsAgain) {
8074
+ fsAgain = false;
8075
+ void drainFsRequests();
8076
+ }
7900
8077
  }
7901
8078
  };
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()));
8079
+ const activeThreadSnapshot = async () => {
8080
+ const response = await fetch(`${api.origin}/api/users/me/standardcode/sidebar`, {
8081
+ headers: { Authorization: `Bearer ${api.bearer}` }
8082
+ });
8083
+ if (!response.ok) throw new Error(`sidebar snapshot returned ${response.status}`);
8084
+ const snapshot = await response.json();
8085
+ const lines = /* @__PURE__ */ new Set();
8086
+ const sama = /* @__PURE__ */ new Set();
8087
+ for (const entry of snapshot.lines?.active ?? []) {
8088
+ if (typeof entry.thread_id === "string") lines.add(entry.thread_id);
8089
+ }
8090
+ for (const entry of snapshot.activity?.busy ?? []) {
8091
+ if (typeof entry.thread_id === "string") sama.add(entry.thread_id);
8092
+ }
8093
+ return { lines, sama };
7909
8094
  };
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);
8095
+ let recovering = false;
8096
+ let recoverAgain = false;
8097
+ const recoverOnConnect = async () => {
8098
+ if (recovering) {
8099
+ recoverAgain = true;
7915
8100
  return;
7916
8101
  }
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);
8102
+ recovering = true;
8103
+ try {
8104
+ await Promise.all([
8105
+ drainCommands(),
8106
+ drainFsRequests(),
8107
+ getMachineName(api, identity.machine_id).then((name) => {
8108
+ displayName = name || os8.hostname();
8109
+ }).catch(() => {
8110
+ })
8111
+ ]);
8112
+ const snapshot = await activeThreadSnapshot();
8113
+ lineActiveThreads = snapshot.lines;
8114
+ samaActiveThreads = snapshot.sama;
8115
+ const active = [.../* @__PURE__ */ new Set([...snapshot.lines, ...snapshot.sama])].slice(0, MAX_WORKERS);
8116
+ for (const threadId of active) {
8117
+ await attachThreadById(threadId, "recovery");
8118
+ }
8119
+ } catch (error) {
8120
+ daemonLog(`user-stream recovery failed: ${error instanceof Error ? error.message : String(error)}`);
8121
+ } finally {
8122
+ recovering = false;
8123
+ if (recoverAgain) {
8124
+ recoverAgain = false;
8125
+ void recoverOnConnect();
8126
+ }
8127
+ }
7932
8128
  };
7933
- process.on("SIGTERM", () => shutdown(0));
7934
- process.on("SIGINT", () => shutdown(0));
7935
- daemonLog(`daemon ready \u2014 watching for threads tagged ${runnerTag}`);
8129
+ const idsFromEvent = (data, field) => {
8130
+ if (!data || typeof data !== "object") return null;
8131
+ const entries = data[field];
8132
+ if (!Array.isArray(entries)) return null;
8133
+ const ids = /* @__PURE__ */ new Set();
8134
+ for (const entry of entries) {
8135
+ if (entry && typeof entry === "object" && typeof entry.thread_id === "string") {
8136
+ ids.add(entry.thread_id);
8137
+ }
8138
+ }
8139
+ return ids;
8140
+ };
8141
+ const releaseInactiveWorkers = () => {
8142
+ for (const worker of [...workers.values()]) {
8143
+ if (!worker.busy && !threadIsActive(worker.threadId)) detach(worker.threadId);
8144
+ }
8145
+ void restartIfIdle();
8146
+ };
8147
+ const machinePrefix = `standardcode.machine.${identity.machine_id}`;
8148
+ hub = new DaemonUserStream(
8149
+ api,
8150
+ daemonClientId(identity),
8151
+ {
8152
+ onWake: (threadId) => void attachThreadById(threadId, "wake"),
8153
+ onEvent: (event, data) => {
8154
+ if (event === "standardcode.lines_changed") {
8155
+ const ids = idsFromEvent(data, "active");
8156
+ if (ids) {
8157
+ lineActiveThreads = ids;
8158
+ releaseInactiveWorkers();
8159
+ }
8160
+ return;
8161
+ }
8162
+ if (event === "standardcode.activity_changed") {
8163
+ const ids = idsFromEvent(data, "busy");
8164
+ if (ids) {
8165
+ samaActiveThreads = ids;
8166
+ releaseInactiveWorkers();
8167
+ }
8168
+ return;
8169
+ }
8170
+ if (event !== "standardcode.machine_changed") return;
8171
+ const key = data && typeof data === "object" ? data.key : null;
8172
+ daemonLog(`machine event: ${typeof key === "string" ? key : "missing key"}`);
8173
+ if (key === `${machinePrefix}.cmd`) void drainCommands();
8174
+ else if (key === `${machinePrefix}.fsreq`) {
8175
+ void drainFsRequests();
8176
+ } else if (key === `${machinePrefix}.name`) {
8177
+ void getMachineName(api, identity.machine_id).then((name) => {
8178
+ displayName = name || os8.hostname();
8179
+ }).catch(() => {
8180
+ });
8181
+ }
8182
+ },
8183
+ onConnection: (state, attempt) => {
8184
+ if (state === "reconnecting") {
8185
+ if (attempt === 1) {
8186
+ daemonLog("user stream: reconnecting");
8187
+ void publishPresence(false);
8188
+ }
8189
+ return;
8190
+ }
8191
+ daemonLog("user stream: connected");
8192
+ void publishPresence(true).then(() => recoverOnConnect());
8193
+ }
8194
+ },
8195
+ displayName
8196
+ );
8197
+ hub.start();
8198
+ void checkUpdates();
8199
+ updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
8200
+ process.on("SIGTERM", () => void shutdown(0));
8201
+ process.on("SIGINT", () => void shutdown(0));
8202
+ daemonLog(`daemon ready \u2014 waiting for work tagged ${runnerTag}`);
7936
8203
  await new Promise(() => {
7937
8204
  });
7938
8205
  }
@@ -8220,7 +8487,7 @@ async function installCommand(endpointFlag) {
8220
8487
  }
8221
8488
  stdout.write(`${c3.green}\u2713${c3.reset} ${result.detail}
8222
8489
  `);
8223
- stdout.write(`${c3.dim}Waiting for the daemon's first heartbeat\u2026${c3.reset}
8490
+ stdout.write(`${c3.dim}Waiting for the daemon to connect\u2026${c3.reset}
8224
8491
  `);
8225
8492
  const deadline = Date.now() + 6e4;
8226
8493
  let alive = false;
@@ -8243,7 +8510,7 @@ or add one now: standardcode daemon add-project <path>${c3.reset}
8243
8510
  );
8244
8511
  } else {
8245
8512
  stdout.write(
8246
- `${c3.yellow}\u26A0${c3.reset} The service installed but no heartbeat arrived yet.
8513
+ `${c3.yellow}\u26A0${c3.reset} The service installed but has not connected yet.
8247
8514
  ${c3.dim}Check ~/.standardagents/daemon.log and \`standardcode daemon status\`.${c3.reset}
8248
8515
  `
8249
8516
  );
@@ -8278,8 +8545,9 @@ async function statusCommand() {
8278
8545
  return;
8279
8546
  }
8280
8547
  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}
8548
+ const streamConnected = !!record2.daemon && (record2.daemon.connected ?? record2.daemon.last_seen_at > Date.now());
8549
+ const seen = streamConnected && record2.daemon ? `user stream connected (v${record2.daemon.version})` : record2.daemon?.connected === false ? `user stream disconnected (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";
8550
+ stdout.write(`${c3.bold}Registry:${c3.reset} ${online ? `${c3.green}online${c3.reset}` : `${c3.yellow}offline${c3.reset}`} \xB7 ${seen}
8283
8551
  `);
8284
8552
  const projects = Object.keys(record2.projects);
8285
8553
  stdout.write(`${c3.bold}Projects:${c3.reset} ${projects.length ? "" : c3.dim + "none registered" + c3.reset}
@@ -8933,6 +9201,12 @@ ${c5.dim}Press Control-C again to exit${c5.reset}
8933
9201
  handoffClosing = true;
8934
9202
  reader.rl?.close();
8935
9203
  const identity = loadMachineIdentity();
9204
+ const accountStream = new AccountUserStream(api, interactiveClientId(identity), {
9205
+ clientKind: "cli",
9206
+ clientName: `${machine} (terminal)`
9207
+ });
9208
+ accountStream.start();
9209
+ process.once("exit", () => accountStream.close());
8936
9210
  void registerProject(api, identity, projectDir).catch(() => {
8937
9211
  });
8938
9212
  const tui = new Tui(1);
@@ -9060,7 +9334,7 @@ ${c5.bold}This machine has no always-on daemon${c5.reset} ${c5.dim}\u2014 sessio
9060
9334
  session.remotePath = void 0;
9061
9335
  projectDir = launchDir;
9062
9336
  if (where) {
9063
- const remotePath = await pickRemoteProject(tui, api, where);
9337
+ const remotePath = await pickRemoteProject(tui, api, accountStream, where);
9064
9338
  if (!remotePath) {
9065
9339
  step = "machine";
9066
9340
  continue;
@@ -9180,7 +9454,18 @@ ${c5.bold}This machine has no always-on daemon${c5.reset} ${c5.dim}\u2014 sessio
9180
9454
  }
9181
9455
  for (; ; ) {
9182
9456
  const agentTitle = AGENT_CHOICES.find((a) => a.id === selectedAgent)?.title ?? selectedAgent;
9183
- await runInteractive(tui, api, threadId, projectDir, machine, resumed, session, agentTitle, historySeed);
9457
+ await runInteractive(
9458
+ tui,
9459
+ api,
9460
+ accountStream,
9461
+ threadId,
9462
+ projectDir,
9463
+ machine,
9464
+ resumed,
9465
+ session,
9466
+ agentTitle,
9467
+ historySeed
9468
+ );
9184
9469
  historySeed = threadId;
9185
9470
  threadId = await createSessionThread();
9186
9471
  await api.kvSet(threadId, "lease_supersedes", historySeed);
@@ -9284,7 +9569,7 @@ async function pickLocalProject(tui, api, self, cwd, machineName) {
9284
9569
  if (picked !== NEW) return picked;
9285
9570
  return await pickDirectory(tui, localBrowseBackend(label), { startPath: cwd, loader: startLoader });
9286
9571
  }
9287
- async function pickRemoteProject(tui, api, runner) {
9572
+ async function pickRemoteProject(tui, api, accountStream, runner) {
9288
9573
  const ENTER_PATH = "__enter_path__";
9289
9574
  const projects = Object.entries(runner.projects).sort(
9290
9575
  (a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
@@ -9306,7 +9591,7 @@ async function pickRemoteProject(tui, api, runner) {
9306
9591
  );
9307
9592
  if (!picked) return null;
9308
9593
  if (picked !== ENTER_PATH) return picked;
9309
- return await pickDirectory(tui, remoteBrowseBackend(api, runner.id, runner.name), {
9594
+ return await pickDirectory(tui, remoteBrowseBackend(api, accountStream, runner.id, runner.name), {
9310
9595
  loader: startLoader
9311
9596
  });
9312
9597
  }
@@ -9374,7 +9659,7 @@ async function printHistory(api, threadId, tui) {
9374
9659
  else printAssistant(tui, text);
9375
9660
  }
9376
9661
  }
9377
- async function runInteractive(tui, api, threadId, projectDir, machine, resumed, session, agentTitle, historySeedThreadId) {
9662
+ async function runInteractive(tui, api, accountStream, threadId, projectDir, machine, resumed, session, agentTitle, historySeedThreadId) {
9378
9663
  const remote = session.mode === "remote";
9379
9664
  const runnerName = session.runner?.name ?? "the remote machine";
9380
9665
  let currentOwner = null;
@@ -10034,7 +10319,7 @@ ${c5.gray}Close another session (its slot frees within ~90s), then resend your m
10034
10319
  name: "machines",
10035
10320
  label: "Your machines",
10036
10321
  hint: "list, rename, update, manage projects",
10037
- run: () => runMachinesMenu(tui, api, session.identity)
10322
+ run: () => runMachinesMenu(tui, api, accountStream, session.identity)
10038
10323
  },
10039
10324
  {
10040
10325
  name: "daemon",
@@ -10511,7 +10796,7 @@ async function runLevelMenu(tui, perm) {
10511
10796
  perm.level = picked;
10512
10797
  }
10513
10798
  }
10514
- async function runMachinesMenu(tui, api, self) {
10799
+ async function runMachinesMenu(tui, api, accountStream, self) {
10515
10800
  let machines;
10516
10801
  try {
10517
10802
  machines = await loadMachines(api);
@@ -10540,9 +10825,9 @@ async function runMachinesMenu(tui, api, self) {
10540
10825
  );
10541
10826
  if (!picked) return;
10542
10827
  const machine = machines.find((m) => m.id === picked);
10543
- await manageMachine(tui, api, self, machine);
10828
+ await manageMachine(tui, api, accountStream, self, machine);
10544
10829
  }
10545
- async function manageMachine(tui, api, self, machine) {
10830
+ async function manageMachine(tui, api, accountStream, self, machine) {
10546
10831
  const isSelf = machine.id === self.machine_id;
10547
10832
  const online = daemonOnline(machine);
10548
10833
  const canRunCommands = isSelf || !!machine.daemon;
@@ -10580,7 +10865,7 @@ async function manageMachine(tui, api, self, machine) {
10580
10865
  machine.icon = trimmed || void 0;
10581
10866
  tui.print(`${c5.green}\u2713${c5.reset} icon ${trimmed ? `set to ${trimmed}` : "reset"} for ${machine.name}`);
10582
10867
  }
10583
- return manageMachine(tui, api, self, machine);
10868
+ return manageMachine(tui, api, accountStream, self, machine);
10584
10869
  }
10585
10870
  const dispatch = async (kind, args) => {
10586
10871
  if (isSelf) {
@@ -10608,10 +10893,10 @@ async function manageMachine(tui, api, self, machine) {
10608
10893
  tui.print(`${c5.green}\u2713${c5.reset} Update ${c5.gray}${applyNote} (its daemon updates and restarts on the new version).${c5.reset}`);
10609
10894
  }
10610
10895
  } else if (action === "projects") {
10611
- await manageMachineProjects(tui, api, self, machine, dispatch, applyNote);
10896
+ await manageMachineProjects(tui, api, accountStream, self, machine, dispatch, applyNote);
10612
10897
  }
10613
10898
  }
10614
- async function manageMachineProjects(tui, api, self, machine, dispatch, applyNote) {
10899
+ async function manageMachineProjects(tui, api, accountStream, self, machine, dispatch, applyNote) {
10615
10900
  const ADD = "__add__";
10616
10901
  const paths = Object.keys(machine.projects).sort();
10617
10902
  const projLabels = projectDisplayLabels(machine.projects);
@@ -10630,7 +10915,7 @@ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNot
10630
10915
  if (!picked) return;
10631
10916
  if (picked === ADD) {
10632
10917
  const isSelf = machine.id === self.machine_id;
10633
- const backend = isSelf ? localBrowseBackend(machine.name) : remoteBrowseBackend(api, machine.id, machine.name);
10918
+ const backend = isSelf ? localBrowseBackend(machine.name) : remoteBrowseBackend(api, accountStream, machine.id, machine.name);
10634
10919
  const chosen = await pickDirectory(tui, backend, {
10635
10920
  startPath: isSelf ? process.cwd() : null,
10636
10921
  loader: startLoader