@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/README.md +4 -1
- package/dist/index.js +559 -274
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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.
|
|
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
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
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
|
-
|
|
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
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
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(
|
|
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/
|
|
7247
|
-
var
|
|
7456
|
+
// src/daemon-user-stream.ts
|
|
7457
|
+
var DaemonUserStream = class {
|
|
7458
|
+
stream;
|
|
7248
7459
|
constructor(api, clientId, hooks, clientName) {
|
|
7249
|
-
this.
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
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
|
-
|
|
7285
|
-
if (
|
|
7286
|
-
|
|
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
|
-
|
|
7289
|
-
ws.addEventListener("close", () => this.handleDrop(ws));
|
|
7473
|
+
if (hooks.onConnection) this.stream.onConnection(hooks.onConnection);
|
|
7290
7474
|
}
|
|
7291
|
-
|
|
7292
|
-
|
|
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.
|
|
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
|
-
|
|
7781
|
+
idleTimer = null;
|
|
7782
|
+
watchedAnotherOwner = false;
|
|
7641
7783
|
/** Set by the daemon so a superseded worker can remove itself. */
|
|
7642
7784
|
onEvicted;
|
|
7643
|
-
|
|
7644
|
-
|
|
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
|
|
7698
|
-
(e) => daemonLog(`
|
|
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
|
-
|
|
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
|
|
7961
|
+
const attachThreadById = async (threadId, reason) => {
|
|
7962
|
+
if (workers.has(threadId)) return;
|
|
7748
7963
|
try {
|
|
7749
|
-
const
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
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(
|
|
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)
|
|
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
|
-
|
|
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();
|
|
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)
|
|
8040
|
+
if (fsBusy) {
|
|
8041
|
+
fsAgain = true;
|
|
8042
|
+
return;
|
|
8043
|
+
}
|
|
7881
8044
|
fsBusy = true;
|
|
7882
8045
|
try {
|
|
7883
|
-
|
|
7884
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
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
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
8095
|
+
let recovering = false;
|
|
8096
|
+
let recoverAgain = false;
|
|
8097
|
+
const recoverOnConnect = async () => {
|
|
8098
|
+
if (recovering) {
|
|
8099
|
+
recoverAgain = true;
|
|
7915
8100
|
return;
|
|
7916
8101
|
}
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
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
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
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
|
|
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
|
|
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
|
|
8282
|
-
|
|
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(
|
|
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
|