@standardagents/code 0.12.1 → 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 +3 -1
- package/dist/index.js +263 -168
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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
|
-
|
|
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", (
|
|
7265
|
+
ws.addEventListener("message", (event) => {
|
|
7285
7266
|
if (this.ws === ws) this.heartbeat?.markAlive();
|
|
7286
|
-
this.onMessage(String(
|
|
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
|
|
7273
|
+
let message;
|
|
7293
7274
|
try {
|
|
7294
|
-
|
|
7275
|
+
message = JSON.parse(raw);
|
|
7295
7276
|
} catch {
|
|
7296
7277
|
return;
|
|
7297
7278
|
}
|
|
7298
|
-
if (!
|
|
7299
|
-
const frame =
|
|
7300
|
-
if (frame.event === "
|
|
7301
|
-
|
|
7302
|
-
|
|
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
|
-
|
|
7333
|
-
|
|
7334
|
-
this.heartbeat = null;
|
|
7335
|
-
}
|
|
7317
|
+
this.heartbeat?.stop();
|
|
7318
|
+
this.heartbeat = null;
|
|
7336
7319
|
}
|
|
7337
7320
|
close() {
|
|
7338
7321
|
this.closed = true;
|
|
@@ -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
|
-
|
|
7629
|
+
idleTimer = null;
|
|
7630
|
+
watchedAnotherOwner = false;
|
|
7641
7631
|
/** Set by the daemon so a superseded worker can remove itself. */
|
|
7642
7632
|
onEvicted;
|
|
7643
|
-
|
|
7644
|
-
|
|
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
|
|
7698
|
-
|
|
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
|
-
|
|
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
|
|
7804
|
+
const attachThreadById = async (threadId, reason) => {
|
|
7805
|
+
if (workers.has(threadId)) return;
|
|
7748
7806
|
try {
|
|
7749
|
-
const
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
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(
|
|
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)
|
|
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
|
-
|
|
7855
|
-
if (
|
|
7856
|
-
daemonLog("forced update
|
|
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)
|
|
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
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
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
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7927
|
+
let recovering = false;
|
|
7928
|
+
let recoverAgain = false;
|
|
7929
|
+
const recoverOnConnect = async () => {
|
|
7930
|
+
if (recovering) {
|
|
7931
|
+
recoverAgain = true;
|
|
7915
7932
|
return;
|
|
7916
7933
|
}
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8282
|
-
|
|
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}
|