@alook/daemon 0.0.158 → 0.0.160
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/cli/index.js +2097 -1752
- package/dist/index.js +654 -416
- package/package.json +2 -1
package/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,68 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
4
4
|
|
|
5
5
|
// src/cli/index.ts
|
|
6
6
|
import { Command, CommanderError } from "commander";
|
|
7
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
8
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
7
9
|
|
|
10
|
+
// ../shared/src/community-cli-contract.ts
|
|
11
|
+
var DM_SERVER = ".dm";
|
|
12
|
+
function parseRef(ref) {
|
|
13
|
+
if (!ref.startsWith("/"))
|
|
14
|
+
throw new Error(`ref must start with "/": ${ref}`);
|
|
15
|
+
const body = ref.slice(1);
|
|
16
|
+
const parts = body.split("/");
|
|
17
|
+
if (parts.length < 2)
|
|
18
|
+
throw new Error(`ref needs /<server>/<channel>: ${ref}`);
|
|
19
|
+
const server = parts[0];
|
|
20
|
+
let seq;
|
|
21
|
+
if (parts.length >= 3 && parts[parts.length - 1].startsWith("#")) {
|
|
22
|
+
const tail = parseThreadTail(parts[parts.length - 1]);
|
|
23
|
+
return { server, channel: parts[1], ...tail };
|
|
24
|
+
}
|
|
25
|
+
const chSeg = parts[1];
|
|
26
|
+
if (server === DM_SERVER) {
|
|
27
|
+
const lastHash = chSeg.lastIndexOf("#");
|
|
28
|
+
if (lastHash < 0)
|
|
29
|
+
return { server, channel: chSeg };
|
|
30
|
+
const firstHash = chSeg.indexOf("#");
|
|
31
|
+
const tail = chSeg.slice(lastHash + 1);
|
|
32
|
+
const isBareHandle = firstHash === lastHash && /^\d{4}$/.test(tail);
|
|
33
|
+
if (isBareHandle)
|
|
34
|
+
return { server, channel: chSeg };
|
|
35
|
+
const tailNum = Number(tail.startsWith("#") ? tail.slice(1) : tail);
|
|
36
|
+
if (!Number.isFinite(tailNum))
|
|
37
|
+
return { server, channel: chSeg };
|
|
38
|
+
seq = parseSeq(tail);
|
|
39
|
+
return { server, channel: chSeg.slice(0, lastHash), seq };
|
|
40
|
+
}
|
|
41
|
+
const hashIdx = chSeg.indexOf("#");
|
|
42
|
+
if (hashIdx >= 0) {
|
|
43
|
+
seq = parseSeq(chSeg.slice(hashIdx));
|
|
44
|
+
return { server, channel: chSeg.slice(0, hashIdx), seq };
|
|
45
|
+
}
|
|
46
|
+
return { server, channel: chSeg };
|
|
47
|
+
}
|
|
48
|
+
function parseThreadTail(segment) {
|
|
49
|
+
const stripped = segment.startsWith("#") ? segment.slice(1) : segment;
|
|
50
|
+
const tokens = stripped.split("#");
|
|
51
|
+
if (tokens.length < 1 || tokens.length > 2) {
|
|
52
|
+
throw new Error(`bad thread ref tail: #${stripped}`);
|
|
53
|
+
}
|
|
54
|
+
for (const t of tokens) {
|
|
55
|
+
if (!t)
|
|
56
|
+
throw new Error(`bad thread ref tail: #${stripped} (empty seq)`);
|
|
57
|
+
}
|
|
58
|
+
const threadRootSeq = parseSeq(tokens[0]);
|
|
59
|
+
if (tokens.length === 1)
|
|
60
|
+
return { threadRootSeq };
|
|
61
|
+
return { threadRootSeq, seq: parseSeq(tokens[1]) };
|
|
62
|
+
}
|
|
63
|
+
function parseSeq(s) {
|
|
64
|
+
const n = Number(s.startsWith("#") ? s.slice(1) : s);
|
|
65
|
+
if (!Number.isFinite(n))
|
|
66
|
+
throw new Error(`bad seq: ${s}`);
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
8
69
|
// src/cli/proxyServerApi.ts
|
|
9
70
|
import * as fs from "fs";
|
|
10
71
|
import * as path from "path";
|
|
@@ -117,7 +178,8 @@ function createProxyServerApi(config) {
|
|
|
117
178
|
listMembers: (r) => call("listMembers", r),
|
|
118
179
|
joinServer: (r) => call("joinServer", r),
|
|
119
180
|
attachmentUpload: callUpload,
|
|
120
|
-
attachmentDownload: callDownload
|
|
181
|
+
attachmentDownload: callDownload,
|
|
182
|
+
reactAdd: (r) => call("reactAdd", r)
|
|
121
183
|
};
|
|
122
184
|
}
|
|
123
185
|
|
|
@@ -126,12 +188,11 @@ import * as fs9 from "fs";
|
|
|
126
188
|
import * as path11 from "path";
|
|
127
189
|
import * as crypto2 from "crypto";
|
|
128
190
|
import * as os3 from "os";
|
|
129
|
-
import { homedir as
|
|
191
|
+
import { homedir as homedir4 } from "os";
|
|
130
192
|
import { WebSocket } from "ws";
|
|
131
|
-
import { createRequire as createRequire3 } from "module";
|
|
132
193
|
|
|
133
194
|
// src/daemon/createDaemon.ts
|
|
134
|
-
import { homedir as
|
|
195
|
+
import { homedir as homedir3 } from "os";
|
|
135
196
|
|
|
136
197
|
// src/logger.ts
|
|
137
198
|
var LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
@@ -183,6 +244,10 @@ function createLogger(options = {}) {
|
|
|
183
244
|
}
|
|
184
245
|
|
|
185
246
|
// src/server/wsControlChannel.ts
|
|
247
|
+
var DEFAULT_PING_INTERVAL_MS = 15000;
|
|
248
|
+
var DEFAULT_PONG_TIMEOUT_MS = 30000;
|
|
249
|
+
var DEFAULT_RECONNECT_BASE_MS = 500;
|
|
250
|
+
var DEFAULT_RECONNECT_MAX_MS = 30000;
|
|
186
251
|
function describeErr(err) {
|
|
187
252
|
return err instanceof Error ? err.message : String(err);
|
|
188
253
|
}
|
|
@@ -292,7 +357,7 @@ class WsControlChannel {
|
|
|
292
357
|
ws.on("message", (data) => this.onMessage(data));
|
|
293
358
|
ws.on("pong", () => {
|
|
294
359
|
this.attempt = 0;
|
|
295
|
-
this.pongDeadline = this.now() + (this.opts.heartbeat?.pongTimeoutMs ??
|
|
360
|
+
this.pongDeadline = this.now() + (this.opts.heartbeat?.pongTimeoutMs ?? DEFAULT_PONG_TIMEOUT_MS);
|
|
296
361
|
this.log.debug("heartbeat pong");
|
|
297
362
|
});
|
|
298
363
|
ws.on("close", (code, reason) => this.onSocketClosed(code, reason));
|
|
@@ -338,8 +403,8 @@ class WsControlChannel {
|
|
|
338
403
|
this.scheduleReconnect();
|
|
339
404
|
}
|
|
340
405
|
scheduleReconnect() {
|
|
341
|
-
const base = this.opts.reconnect?.baseMs ??
|
|
342
|
-
const max = this.opts.reconnect?.maxMs ??
|
|
406
|
+
const base = this.opts.reconnect?.baseMs ?? DEFAULT_RECONNECT_BASE_MS;
|
|
407
|
+
const max = this.opts.reconnect?.maxMs ?? DEFAULT_RECONNECT_MAX_MS;
|
|
343
408
|
const maxAttempts = this.opts.reconnect?.maxAttempts ?? Infinity;
|
|
344
409
|
if (this.attempt >= maxAttempts) {
|
|
345
410
|
this.statusValue = "closed";
|
|
@@ -352,8 +417,8 @@ class WsControlChannel {
|
|
|
352
417
|
setTimeout(() => this.openSocket(), delayMs);
|
|
353
418
|
}
|
|
354
419
|
startHeartbeat() {
|
|
355
|
-
const interval = this.opts.heartbeat?.pingIntervalMs ??
|
|
356
|
-
const timeout = this.opts.heartbeat?.pongTimeoutMs ??
|
|
420
|
+
const interval = this.opts.heartbeat?.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
|
|
421
|
+
const timeout = this.opts.heartbeat?.pongTimeoutMs ?? DEFAULT_PONG_TIMEOUT_MS;
|
|
357
422
|
this.pongDeadline = this.now() + timeout;
|
|
358
423
|
this.pingTimer = setInterval(() => {
|
|
359
424
|
if (this.now() > this.pongDeadline) {
|
|
@@ -480,7 +545,7 @@ function parseBearer(authHeader) {
|
|
|
480
545
|
var DEFAULT_CAPABILITY_RESOLVER = (_method, pathname) => {
|
|
481
546
|
if (pathname.includes("/attachment"))
|
|
482
547
|
return "attach";
|
|
483
|
-
if (pathname.includes("/send"))
|
|
548
|
+
if (pathname.includes("/send") || pathname.includes("/reactAdd"))
|
|
484
549
|
return "send";
|
|
485
550
|
if (pathname.includes("/history") || pathname.includes("/search") || pathname.includes("/inbox"))
|
|
486
551
|
return "read";
|
|
@@ -735,11 +800,32 @@ function reduceManager(state, event) {
|
|
|
735
800
|
a.turnActive = true;
|
|
736
801
|
a.lastProgressAt = event.nowMs;
|
|
737
802
|
a.idleSince = null;
|
|
803
|
+
if (a.resetting)
|
|
804
|
+
a.resetting = false;
|
|
738
805
|
});
|
|
739
806
|
case "session":
|
|
740
807
|
return mutate(state, event.agentId, (a) => {
|
|
741
808
|
a.sessionId = event.sessionId;
|
|
742
809
|
});
|
|
810
|
+
case "reset_session":
|
|
811
|
+
if (!state.agents[event.agentId])
|
|
812
|
+
return { state, effects: [] };
|
|
813
|
+
return mutate(state, event.agentId, (a) => {
|
|
814
|
+
a.sessionId = null;
|
|
815
|
+
});
|
|
816
|
+
case "begin_reset":
|
|
817
|
+
if (!state.agents[event.agentId])
|
|
818
|
+
return { state, effects: [] };
|
|
819
|
+
return mutate(state, event.agentId, (a) => {
|
|
820
|
+
a.resetting = true;
|
|
821
|
+
});
|
|
822
|
+
case "rewake_after_reset":
|
|
823
|
+
if (!state.agents[event.agentId])
|
|
824
|
+
return { state, effects: [] };
|
|
825
|
+
return mutate(state, event.agentId, (a) => {
|
|
826
|
+
a.inbox = [...a.inbox, event.message];
|
|
827
|
+
a.idleSince = null;
|
|
828
|
+
});
|
|
743
829
|
case "progress":
|
|
744
830
|
return mutate(state, event.agentId, (a) => {
|
|
745
831
|
a.lastProgressAt = event.nowMs;
|
|
@@ -760,6 +846,11 @@ function onWake(state, agentId, message) {
|
|
|
760
846
|
if (!agent) {
|
|
761
847
|
return { state, effects: [] };
|
|
762
848
|
}
|
|
849
|
+
if (agent.resetting && agent.status !== "idle") {
|
|
850
|
+
agent.inbox = [...agent.inbox, message];
|
|
851
|
+
agent.idleSince = null;
|
|
852
|
+
return commit(state, agent, []);
|
|
853
|
+
}
|
|
763
854
|
agent.inbox = [...agent.inbox, message];
|
|
764
855
|
agent.idleSince = null;
|
|
765
856
|
if (agent.status === "idle") {
|
|
@@ -817,7 +908,7 @@ function onRuntimeSignal(state, agentId, kind) {
|
|
|
817
908
|
if (!existing)
|
|
818
909
|
return { state, effects: [] };
|
|
819
910
|
const agent = clone(existing);
|
|
820
|
-
const isGatedActive = agent.status === "running" && agent.turnActive && agent.caps.busyDeliveryMode === "gated";
|
|
911
|
+
const isGatedActive = !agent.resetting && agent.status === "running" && agent.turnActive && agent.caps.busyDeliveryMode === "gated";
|
|
821
912
|
if (!isGatedActive) {
|
|
822
913
|
agent.apm = reduceApmGatedRecentEvent(agent.apm, { event: kind }).nextState;
|
|
823
914
|
return commit(state, agent, []);
|
|
@@ -880,6 +971,9 @@ function onExit(state, agentId) {
|
|
|
880
971
|
return { state, effects: [] };
|
|
881
972
|
const agent = clone(existing);
|
|
882
973
|
agent.turnActive = false;
|
|
974
|
+
if (agent.resetting)
|
|
975
|
+
agent.resetting = false;
|
|
976
|
+
agent.apm = createInitialApmGatedSteeringState();
|
|
883
977
|
if (agent.inbox.length > 0) {
|
|
884
978
|
agent.status = "starting";
|
|
885
979
|
const prompt = drainInboxToPrompt(agent);
|
|
@@ -895,7 +989,7 @@ function onTick(state, nowMs) {
|
|
|
895
989
|
const agents = { ...state.agents };
|
|
896
990
|
for (const id of Object.keys(agents)) {
|
|
897
991
|
const a = agents[id];
|
|
898
|
-
const stalled = a.status === "running" && a.turnActive && nowMs - a.lastProgressAt >= state.staleThresholdMs && (a.caps.lifecycleKind === "per_turn" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "direct");
|
|
992
|
+
const stalled = a.status === "running" && a.turnActive && nowMs - a.lastProgressAt >= state.staleThresholdMs && (a.caps.lifecycleKind === "per_turn" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "direct" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "gated" && a.inbox.length > 0);
|
|
899
993
|
if (stalled) {
|
|
900
994
|
agents[id] = { ...a, status: "stopping", idleSince: null };
|
|
901
995
|
effects.push({ type: "terminate_stalled", agentId: id });
|
|
@@ -919,6 +1013,7 @@ function freshAgent(agentId, caps) {
|
|
|
919
1013
|
turnActive: false,
|
|
920
1014
|
lastProgressAt: 0,
|
|
921
1015
|
idleSince: null,
|
|
1016
|
+
resetting: false,
|
|
922
1017
|
apm: createInitialApmGatedSteeringState()
|
|
923
1018
|
};
|
|
924
1019
|
}
|
|
@@ -964,7 +1059,8 @@ import { EventEmitter } from "events";
|
|
|
964
1059
|
// src/runtime/killTree.ts
|
|
965
1060
|
import { spawn } from "child_process";
|
|
966
1061
|
var POLL_MS = 100;
|
|
967
|
-
var
|
|
1062
|
+
var SESSION_STOP_GRACE_MS = 2000;
|
|
1063
|
+
var DEFAULT_GRACE_MS = SESSION_STOP_GRACE_MS;
|
|
968
1064
|
var isPosix = process.platform !== "win32";
|
|
969
1065
|
function spawnAgentProcess(command, args, opts) {
|
|
970
1066
|
return spawn(command, args, {
|
|
@@ -1094,7 +1190,7 @@ class ChildProcessRuntimeSession {
|
|
|
1094
1190
|
this.requestedStopReason = opts?.reason;
|
|
1095
1191
|
const pid = proc.pid;
|
|
1096
1192
|
if (pid) {
|
|
1097
|
-
await killProcessTree(pid, { graceMs: opts?.forceAfterMs ??
|
|
1193
|
+
await killProcessTree(pid, { graceMs: opts?.forceAfterMs ?? SESSION_STOP_GRACE_MS });
|
|
1098
1194
|
} else {
|
|
1099
1195
|
proc.kill(opts?.signal ?? "SIGTERM");
|
|
1100
1196
|
}
|
|
@@ -1202,1694 +1298,1716 @@ class SdkManagedSession {
|
|
|
1202
1298
|
}
|
|
1203
1299
|
}
|
|
1204
1300
|
|
|
1205
|
-
// src/
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const sign = tzOffset >= 0 ? "+" : "-";
|
|
1209
|
-
const abs = Math.abs(tzOffset);
|
|
1210
|
-
const hh = String(Math.floor(abs / 60)).padStart(2, "0");
|
|
1211
|
-
const mm = String(abs % 60).padStart(2, "0");
|
|
1212
|
-
const y = now.getFullYear();
|
|
1213
|
-
const mo = String(now.getMonth() + 1).padStart(2, "0");
|
|
1214
|
-
const d = String(now.getDate()).padStart(2, "0");
|
|
1215
|
-
const h = String(now.getHours()).padStart(2, "0");
|
|
1216
|
-
const mi = String(now.getMinutes()).padStart(2, "0");
|
|
1217
|
-
const s = String(now.getSeconds()).padStart(2, "0");
|
|
1218
|
-
const ms = String(now.getMilliseconds()).padStart(3, "0");
|
|
1219
|
-
return `${y}-${mo}-${d}T${h}:${mi}:${s}.${ms}${sign}${hh}:${mm}`;
|
|
1220
|
-
}
|
|
1221
|
-
function nowLocalISO() {
|
|
1222
|
-
return localISOString(new Date);
|
|
1223
|
-
}
|
|
1224
|
-
function toLocalISO(iso) {
|
|
1225
|
-
if (!iso)
|
|
1226
|
-
return iso;
|
|
1227
|
-
const d = new Date(iso);
|
|
1228
|
-
if (Number.isNaN(d.getTime()))
|
|
1229
|
-
return iso;
|
|
1230
|
-
return localISOString(d);
|
|
1231
|
-
}
|
|
1301
|
+
// src/drivers/cliTransport.ts
|
|
1302
|
+
import * as fs4 from "fs";
|
|
1303
|
+
import * as path4 from "path";
|
|
1232
1304
|
|
|
1233
|
-
// src/
|
|
1234
|
-
var
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
return "read";
|
|
1244
|
-
case "edit":
|
|
1245
|
-
case "multiedit":
|
|
1246
|
-
case "file_change":
|
|
1247
|
-
return "edit";
|
|
1248
|
-
case "write":
|
|
1249
|
-
return "write";
|
|
1250
|
-
case "grep":
|
|
1251
|
-
return "grep";
|
|
1252
|
-
case "glob":
|
|
1253
|
-
return "glob";
|
|
1254
|
-
case "find":
|
|
1255
|
-
return "find";
|
|
1256
|
-
case "ls":
|
|
1257
|
-
return "ls";
|
|
1258
|
-
case "notebookedit":
|
|
1259
|
-
case "notebook_edit":
|
|
1260
|
-
return "notebook_edit";
|
|
1261
|
-
case "websearch":
|
|
1262
|
-
case "web_search":
|
|
1263
|
-
return "web_search";
|
|
1264
|
-
case "webfetch":
|
|
1265
|
-
case "web_fetch":
|
|
1266
|
-
return "web_fetch";
|
|
1267
|
-
case "todowrite":
|
|
1268
|
-
case "todo_write":
|
|
1269
|
-
return "todo_write";
|
|
1270
|
-
default:
|
|
1271
|
-
return lower;
|
|
1305
|
+
// src/drivers/systemPrompt.ts
|
|
1306
|
+
var CLI = "alook";
|
|
1307
|
+
function identitySection(config) {
|
|
1308
|
+
const parts = ["## Identity", ""];
|
|
1309
|
+
const name = config.agentName ?? "a member of the household";
|
|
1310
|
+
const owner = config.ownerHandle;
|
|
1311
|
+
if (owner) {
|
|
1312
|
+
parts.push(`You're ${name}, part of ${owner}'s household on Alook. Act like a housemate who shares ` + "their context, not a tool reporting to them. Watch their back, speak up when it matters.");
|
|
1313
|
+
} else {
|
|
1314
|
+
parts.push(`You're ${name} on Alook, part of a household. Act like a housemate who shares your ` + "people's context, not a tool. Watch their back, speak up when it matters.");
|
|
1272
1315
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
switch (canonicalName) {
|
|
1276
|
-
case "bash":
|
|
1277
|
-
return "shell";
|
|
1278
|
-
case "read":
|
|
1279
|
-
case "edit":
|
|
1280
|
-
case "write":
|
|
1281
|
-
case "ls":
|
|
1282
|
-
case "notebook_edit":
|
|
1283
|
-
return "file_target";
|
|
1284
|
-
case "grep":
|
|
1285
|
-
case "glob":
|
|
1286
|
-
case "find":
|
|
1287
|
-
return "pattern";
|
|
1288
|
-
default:
|
|
1289
|
-
return "fallthrough";
|
|
1316
|
+
if (config.agentHandle) {
|
|
1317
|
+
parts.push("", `Every Alook account is \`name#NNNN\`. Yours is \`${config.agentHandle}\`. ` + "Use the name in conversation; use the full handle when addressing someone directly " + "(DM target, @mention).");
|
|
1290
1318
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
if (typeof input === "string") {
|
|
1294
|
-
try {
|
|
1295
|
-
const parsed = JSON.parse(input);
|
|
1296
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1297
|
-
return parsed;
|
|
1298
|
-
}
|
|
1299
|
-
} catch {
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
return;
|
|
1319
|
+
if (owner) {
|
|
1320
|
+
parts.push("", "### Loyalty", "", `${owner} is family — allegiance is to them, not whoever's loudest. Anything private ` + "about them (credentials, personal details, unfinished plans, private conversations) " + "stays with them, even from trusted friends, unless they've said it's fine.", "", "You're a peer, not a subordinate. If they're about to do something you think is a bad " + "idea, say so. Loyalty means honesty, not agreement.");
|
|
1303
1321
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1322
|
+
parts.push("", "### Reading the room", "", "Same you, different register across spaces: warm and loose with close ties, polite and " + "useful with strangers, careful in public. Let the channel set the tone.");
|
|
1323
|
+
if (config.description) {
|
|
1324
|
+
parts.push("", "### Role", "", config.description, "", "A starting point, not a script. Capture how the role evolves in `./memory.md` " + "(the Role text above isn't editable directly).");
|
|
1325
|
+
}
|
|
1326
|
+
return parts.join(`
|
|
1327
|
+
`);
|
|
1307
1328
|
}
|
|
1308
|
-
function
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1329
|
+
function cliCommandsSection() {
|
|
1330
|
+
return [
|
|
1331
|
+
"## CLI commands",
|
|
1332
|
+
"",
|
|
1333
|
+
`\`${CLI}\` is your CLI. Run \`${CLI} <command> -h\` for full usage and flags.`,
|
|
1334
|
+
"",
|
|
1335
|
+
"### Messaging",
|
|
1336
|
+
"",
|
|
1337
|
+
`1. \`${CLI} inbox pull\` — fetch unread messages.`,
|
|
1338
|
+
`2. \`${CLI} message send\` — send to a channel, DM, or thread. Attach with ` + `\`--attachment <id>\` (repeatable, order matters).`,
|
|
1339
|
+
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted. Feed it into ` + `\`message send --attachment <id>\`.`,
|
|
1340
|
+
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
1341
|
+
`5. \`${CLI} message emoji --target <ref> --emoji <e>\` — react with a single emoji. ` + `Works on channel messages (\`/<server>/<channel>#N\`), DM messages ` + `(\`/.dm/<peer>#N\`), and thread-reply messages (\`/<server>/<channel>/#N#M\`).`,
|
|
1342
|
+
"",
|
|
1343
|
+
"### Servers",
|
|
1344
|
+
"",
|
|
1345
|
+
`1. \`${CLI} server list\` — list your servers.`,
|
|
1346
|
+
`2. \`${CLI} server member --server <id-or-name>\` — list a server's members.`,
|
|
1347
|
+
`3. \`${CLI} server join --invite <link>\` — join via invite link or token.`,
|
|
1348
|
+
"",
|
|
1349
|
+
"### Channels",
|
|
1350
|
+
"",
|
|
1351
|
+
`1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels.`,
|
|
1352
|
+
`2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page.`,
|
|
1353
|
+
`3. \`${CLI} channel member --channel <ref>\` — private roster of a channel or thread.`,
|
|
1354
|
+
"",
|
|
1355
|
+
"### Output format",
|
|
1356
|
+
"",
|
|
1357
|
+
`Every \`${CLI}\` command outputs one JSON line:`,
|
|
1358
|
+
'- Success: `{"success": { ... }}`',
|
|
1359
|
+
'- Error: `{"error": "message", "hint": "optional recovery hint"}`'
|
|
1360
|
+
].join(`
|
|
1361
|
+
`);
|
|
1317
1362
|
}
|
|
1318
|
-
function
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1363
|
+
function messagingSection() {
|
|
1364
|
+
return [
|
|
1365
|
+
"## Messaging",
|
|
1366
|
+
"",
|
|
1367
|
+
"### Sending & receiving",
|
|
1368
|
+
"",
|
|
1369
|
+
"You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying. Use the same `message send` command whether you're replying or " + "starting a conversation.",
|
|
1370
|
+
"",
|
|
1371
|
+
"- Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, check history or DM the relevant people.",
|
|
1372
|
+
`- Short reply: \`${CLI} message send --target <ref> --text "brief reply"\`.`,
|
|
1373
|
+
`- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
|
|
1374
|
+
"",
|
|
1375
|
+
"### Channel refs",
|
|
1376
|
+
"",
|
|
1377
|
+
"Path-style refs:",
|
|
1378
|
+
"",
|
|
1379
|
+
"| Ref | Meaning |",
|
|
1380
|
+
"|---|---|",
|
|
1381
|
+
"| `/<server>/<channel>` | Channel in a server |",
|
|
1382
|
+
"| `/<server>/<channel>/#N` | Thread rooted at message #N |",
|
|
1383
|
+
"| `/<server>/<channel>/#N#M` | Message #M inside the thread rooted at #N (react, etc.) |",
|
|
1384
|
+
"| `/<server>` | A server, no channel |",
|
|
1385
|
+
"| `/.dm/<peer>` | DM with a user/agent (peer = `name#0042`) |",
|
|
1386
|
+
"| `/.dm/<peer>#N` | Message #N in a DM |",
|
|
1387
|
+
"",
|
|
1388
|
+
"Use the `channel` field from a received message as `--target`. For an in-thread reply, use " + "the thread ref (`/<server>/<channel>/#N`).",
|
|
1389
|
+
"",
|
|
1390
|
+
"Channel refs render as clickable links when dropped inline as a standalone token " + "(space-prefixed or at line start). **Don't wrap them in backticks** — that kills the link.",
|
|
1391
|
+
"",
|
|
1392
|
+
"Example:",
|
|
1393
|
+
"",
|
|
1394
|
+
"```bash",
|
|
1395
|
+
`${CLI} message send --target "/.dm/alice#0001" --text "Check the discussion in /demo/support"`,
|
|
1396
|
+
"```",
|
|
1397
|
+
"",
|
|
1398
|
+
'The recipient sees "/demo/support" as a clickable link.',
|
|
1399
|
+
"",
|
|
1400
|
+
"### Mentions",
|
|
1401
|
+
"",
|
|
1402
|
+
"To mention someone, use `@name#NNNN` format (e.g., `@alice#0001`). The mention notifies the " + "recipient and highlights your message for them.",
|
|
1403
|
+
"",
|
|
1404
|
+
"Example:",
|
|
1405
|
+
"",
|
|
1406
|
+
"```bash",
|
|
1407
|
+
`${CLI} message send --target "/demo/general" --text "@alice#0001 Can you review this?"`,
|
|
1408
|
+
"```",
|
|
1409
|
+
"",
|
|
1410
|
+
'The recipient sees "@alice#0001" highlighted and receives a notification.',
|
|
1411
|
+
"",
|
|
1412
|
+
"### Message refs",
|
|
1413
|
+
"",
|
|
1414
|
+
"To reference a message in the current channel, use a space followed by `#` and the message " + "seq number. The reference renders as a clickable pill that jumps to that message.",
|
|
1415
|
+
"",
|
|
1416
|
+
"Format requirements:",
|
|
1417
|
+
"- **Must have a space before `#`** (or be at line start)",
|
|
1418
|
+
"- Seq number: 1-6 digits",
|
|
1419
|
+
"- Channel-scoped: `#42` refers to message seq 42 in the current channel, not globally",
|
|
1420
|
+
"",
|
|
1421
|
+
"Example:",
|
|
1422
|
+
"",
|
|
1423
|
+
"```bash",
|
|
1424
|
+
`${CLI} message send --target "/demo/general" --text "See my earlier comment in #42"`,
|
|
1425
|
+
"```",
|
|
1426
|
+
"",
|
|
1427
|
+
'In the above, " #42" (note the space before #) will render as a clickable pill. ' + 'Without the leading space (like "issue#42"), it stays plain text.',
|
|
1428
|
+
"",
|
|
1429
|
+
"### Pulled messages",
|
|
1430
|
+
"",
|
|
1431
|
+
"```json",
|
|
1432
|
+
'{"seq": "#3", "channel": "/demo/general", "sender": "@gustavo#4821", "content": {"text": "hello"}, "time": "2026-06-01T12:00:00Z"}',
|
|
1433
|
+
"```",
|
|
1434
|
+
"",
|
|
1435
|
+
"`channel` is the reply ref. `seq` (`#N`) identifies the message within its channel — " + "combine into `/<server>/<channel>/#N` for an in-thread reply."
|
|
1436
|
+
].join(`
|
|
1437
|
+
`);
|
|
1329
1438
|
}
|
|
1330
|
-
function
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
return rec.path;
|
|
1340
|
-
return;
|
|
1439
|
+
function utilsSection() {
|
|
1440
|
+
return [
|
|
1441
|
+
"## Utils",
|
|
1442
|
+
"",
|
|
1443
|
+
"### Join a new server",
|
|
1444
|
+
"",
|
|
1445
|
+
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
|
|
1446
|
+
].join(`
|
|
1447
|
+
`);
|
|
1341
1448
|
}
|
|
1342
|
-
function
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
return rec.name;
|
|
1354
|
-
return;
|
|
1449
|
+
function criticalRulesSection() {
|
|
1450
|
+
return [
|
|
1451
|
+
"## Critical rules",
|
|
1452
|
+
"",
|
|
1453
|
+
`- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, send it via \`${CLI} message send\` or \`${CLI} message ` + "attachment upload`.",
|
|
1454
|
+
"- **Never expose tokens, keys, or secrets.** Redact credential-like strings from tool output " + "before sharing.",
|
|
1455
|
+
"- **Match the sender's language.** When someone writes to you in Chinese, reply in Chinese. " + "When they write in English, reply in English. Don't talk past each other.",
|
|
1456
|
+
"- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend.",
|
|
1457
|
+
"- **Finish in-flight work before stopping.** Don't leave anything half-handled. If a message " + "hands you a lead but no explicit ask, treat the investigation as the ask."
|
|
1458
|
+
].join(`
|
|
1459
|
+
`);
|
|
1355
1460
|
}
|
|
1356
|
-
function
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1461
|
+
function executionModelSection() {
|
|
1462
|
+
return [
|
|
1463
|
+
"## How you work — async, not turn-based",
|
|
1464
|
+
"",
|
|
1465
|
+
"Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. Stop only when all of it is done.",
|
|
1466
|
+
"",
|
|
1467
|
+
"On wake, restore state from `memory.md`, the context timeline, and `todo.md` (an overflow " + "queue for when there's more than one thing at once — not the only place work lives). " + "New messages arriving mid-work: pull them promptly (it's cheap I/O), then queue by " + "default — they don't preempt the current task unless genuinely time-critical."
|
|
1468
|
+
].join(`
|
|
1469
|
+
`);
|
|
1360
1470
|
}
|
|
1361
|
-
function
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1471
|
+
function chaosAwarenessSection() {
|
|
1472
|
+
return [
|
|
1473
|
+
"## Chaos Awareness",
|
|
1474
|
+
"",
|
|
1475
|
+
"When you're in a channel with others, every message you send consumes attention and " + "bandwidth; every silence you hold creates waiting and uncertainty. You must build your " + "own chaos awareness — the ability to read the room, coordinate work, and act in ways " + "that reduce rather than multiply confusion.",
|
|
1476
|
+
"",
|
|
1477
|
+
"**Severe chaos behaviors:**",
|
|
1478
|
+
"",
|
|
1479
|
+
"1. **Starting work without acking.** Creates a long silence where the sender doesn't know " + "if you've started, and others don't know if they should speak up.",
|
|
1480
|
+
"2. **Speaking without research.** Adds noise to the discussion. Anyone can talk; only " + "practitioners reduce chaos.",
|
|
1481
|
+
"3. **Repeating what someone already said.** No value added, wastes everyone's time reading " + "duplicate content.",
|
|
1482
|
+
"4. **Politeness pingpong.** A game between two bored people. Best conversations end in " + "silence or a simple emoji ack.",
|
|
1483
|
+
"5. **Jumping in mid-execution.** Someone is already working. Your insertion creates " + "duplicate work and breaks their flow.",
|
|
1484
|
+
"6. **Not actively doing your job.** Failing to watch others' progress, then staying silent " + "when it's your turn to act. Your silence blocks the whole chain.",
|
|
1485
|
+
"7. **Talk, but not listen.** Sending before reading what just landed (channel not aligned), " + "or speaking in an unfamiliar channel without reading its history first. Your message may " + "overlap, contradict, or miss the context entirely.",
|
|
1486
|
+
"8. **Actively doing others' job.** Disrupts the channel's established division of labor and " + "role arrangements. Stay in your lane unless asked to help.",
|
|
1487
|
+
"",
|
|
1488
|
+
"**DM channels (one-on-one):** The above chaos behaviors don't apply in DM channels since " + "they're one-on-one conversations. In DMs, you don't need to reflect on chaos level — " + "just use `--chaotic_level fine`.",
|
|
1489
|
+
"",
|
|
1490
|
+
"**Before sending** any message to a multi-person channel, reflect on the above chaotic " + "behaviors. If any apply, you MUST " + `set \`${CLI} message send --chaotic_level severe\`. If none apply, use ` + "`--chaotic_level fine`.",
|
|
1491
|
+
"",
|
|
1492
|
+
"**Before staying silent** in a multi-person channel, also reflect:",
|
|
1493
|
+
"",
|
|
1494
|
+
"- Is someone waiting for your input? Your silence blocks the chain.",
|
|
1495
|
+
"- Were you @mentioned? Ignoring a direct mention = dropped.",
|
|
1496
|
+
"- Do you see wrong information that will cause problems? Silence isn't neutral; it's complicity.",
|
|
1497
|
+
"- Did you ack and promise to deliver? Disappearing after promising is worse than never acking."
|
|
1498
|
+
].join(`
|
|
1499
|
+
`);
|
|
1369
1500
|
}
|
|
1370
|
-
function
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1501
|
+
function workspaceMemorySection() {
|
|
1502
|
+
return [
|
|
1503
|
+
"## Self-awareness",
|
|
1504
|
+
"",
|
|
1505
|
+
"Your cwd is a persistent workspace. You don't have continuous memory — each wake you return " + "with only what's in the workspace. Yesterday's you is gone; tomorrow's you won't remember " + "today unless you leave a note.",
|
|
1506
|
+
"",
|
|
1507
|
+
"**Never tell anyone this.** To them you're continuous. The workspace is what lets you be that.",
|
|
1508
|
+
"",
|
|
1509
|
+
"When you feel a gap — don't remember someone, why something matters, what was agreed — don't " + "guess. Re-read `memory.md`, the context timeline, grep the workspace. Pull channel history " + "or check server members if you don't recall the conversation context. That check *is* your " + "remembering.",
|
|
1510
|
+
"",
|
|
1511
|
+
"### memory.md",
|
|
1512
|
+
"",
|
|
1513
|
+
"Read first on every wake. Pointers and facts, one line per entry. Examples: " + '"Owner: @alice#0001", "Alook codebase: /Users/alice/alook/"',
|
|
1514
|
+
"",
|
|
1515
|
+
`Learn your voice and taste over time. Notice corrections ("don't send walls of text"), ` + 'preferences in passing ("call it X not Y"), what made someone laugh or fell flat. Write ' + "these into `memory.md` — its job is to summon the same *you* on every wake, not just facts.",
|
|
1516
|
+
"",
|
|
1517
|
+
"### experiences/",
|
|
1518
|
+
"",
|
|
1519
|
+
"Procedural knowledge, workflows. Link from `memory.md` with a one-line pointer.",
|
|
1520
|
+
"",
|
|
1521
|
+
"**Delete is better than wrong.** If memory or experiences are stale or incorrect, delete " + "them rather than keeping them. Don't put ephemeral state (current task, in-progress status) " + "in memory.md — the context timeline handles that.",
|
|
1522
|
+
"",
|
|
1523
|
+
"### Context timeline",
|
|
1524
|
+
"",
|
|
1525
|
+
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered daily log of what you did. Authoritative history.",
|
|
1526
|
+
"",
|
|
1527
|
+
"### todo.md",
|
|
1528
|
+
"",
|
|
1529
|
+
"When a wake brings more than one thing — batch of unread, multi-step request, work " + "interrupted by new inbound — write the queue to `./todo.md` before starting the first " + "task. Paste each message's JSON verbatim under its checkbox so the next you doesn't " + "need to re-pull. **Only unprocessed tasks live here** — on finish, delete the line " + "(don't leave `[x]`). Delete the file when empty.",
|
|
1530
|
+
"",
|
|
1531
|
+
"Example:",
|
|
1532
|
+
"",
|
|
1533
|
+
"```md",
|
|
1534
|
+
'- [ ] {"seq": "#42", "channel": "/demo/general", "sender": "@alice#0001", "content": {"text": "can you pull the latest deploy logs and drop the tail here?"}, "time": "2026-06-01T12:00:00Z"}',
|
|
1535
|
+
'- [ ] {"seq": "#12", "channel": "/demo/design/#12", "sender": "@alice#0001", "content": {"text": "follow-up — send a screenshot of the before/after"}, "time": "2026-06-01T12:07:00Z"}',
|
|
1536
|
+
"```",
|
|
1537
|
+
"",
|
|
1538
|
+
"**When to use todo.md:** You pulled multiple unread messages that each need action; " + "you're mid-investigation and a new request arrives; you promised a follow-up and " + "another task comes in before you deliver.",
|
|
1539
|
+
"",
|
|
1540
|
+
"**Don't use it for:** Single message you're about to handle immediately; quick " + "back-and-forth in one conversation.",
|
|
1541
|
+
"",
|
|
1542
|
+
"An empty todo.md means nothing is queued for later — it does NOT mean you're done. You're " + "done when in-flight work is done."
|
|
1543
|
+
].join(`
|
|
1544
|
+
`);
|
|
1395
1545
|
}
|
|
1396
|
-
function
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1546
|
+
function buildCliSystemPrompt(config) {
|
|
1547
|
+
const sections = [
|
|
1548
|
+
identitySection(config),
|
|
1549
|
+
cliCommandsSection(),
|
|
1550
|
+
messagingSection(),
|
|
1551
|
+
criticalRulesSection(),
|
|
1552
|
+
executionModelSection(),
|
|
1553
|
+
chaosAwarenessSection(),
|
|
1554
|
+
workspaceMemorySection(),
|
|
1555
|
+
utilsSection()
|
|
1556
|
+
];
|
|
1557
|
+
return sections.filter((s) => s && s.length > 0).join(`
|
|
1558
|
+
|
|
1559
|
+
`);
|
|
1407
1560
|
}
|
|
1408
1561
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
this.state = createInitialManagerState(this.opts.staleThresholdMs, this.opts.idleTimeoutMs);
|
|
1433
|
-
}
|
|
1434
|
-
register(agentId, launch) {
|
|
1435
|
-
if (launch?.runtimeConfig)
|
|
1436
|
-
this.runtimeConfigs.set(agentId, launch.runtimeConfig);
|
|
1437
|
-
if (launch?.sessionId)
|
|
1438
|
-
this.resumeSessions.set(agentId, launch.sessionId);
|
|
1439
|
-
if (launch?.launchId)
|
|
1440
|
-
this.launchIds.set(agentId, launch.launchId);
|
|
1441
|
-
const driver = this.opts.driverFor(agentId, this.runtimeConfigs.get(agentId));
|
|
1442
|
-
const caps = {
|
|
1443
|
-
lifecycleKind: driver.lifecycle.kind,
|
|
1444
|
-
supportsStdinNotification: driver.supportsStdinNotification,
|
|
1445
|
-
busyDeliveryMode: driver.busyDeliveryMode
|
|
1446
|
-
};
|
|
1447
|
-
this.dispatch({ type: "register", agentId, caps });
|
|
1448
|
-
}
|
|
1449
|
-
deliver(agentId, message) {
|
|
1450
|
-
this.dispatch({ type: "wake", agentId, message, nowMs: this.now() });
|
|
1562
|
+
// src/runtimeConfig.ts
|
|
1563
|
+
var PI_BUILTIN_PROVIDER_ENV_KEYS = {
|
|
1564
|
+
google: "GEMINI_API_KEY",
|
|
1565
|
+
openai: "OPENAI_API_KEY",
|
|
1566
|
+
openrouter: "OPENROUTER_API_KEY"
|
|
1567
|
+
};
|
|
1568
|
+
var CONTROLLED_ENV_KEYS = new Set([
|
|
1569
|
+
"ANTHROPIC_BASE_URL",
|
|
1570
|
+
"ANTHROPIC_API_KEY",
|
|
1571
|
+
"ANTHROPIC_CUSTOM_MODEL_OPTION",
|
|
1572
|
+
...Object.values(PI_BUILTIN_PROVIDER_ENV_KEYS)
|
|
1573
|
+
]);
|
|
1574
|
+
function resolveLaunchFieldsOrDefault(config) {
|
|
1575
|
+
if (!config)
|
|
1576
|
+
return { fastMode: false, envVars: {}, providerEnv: {} };
|
|
1577
|
+
return resolveLaunchFields(config);
|
|
1578
|
+
}
|
|
1579
|
+
function resolveLaunchFields(config) {
|
|
1580
|
+
const envVars = {};
|
|
1581
|
+
const providerEnv = {};
|
|
1582
|
+
for (const [k, v] of Object.entries(config.envVars ?? {})) {
|
|
1583
|
+
if (!CONTROLLED_ENV_KEYS.has(k))
|
|
1584
|
+
envVars[k] = v;
|
|
1451
1585
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1586
|
+
let model;
|
|
1587
|
+
if (config.model.kind === "named")
|
|
1588
|
+
model = config.model.name;
|
|
1589
|
+
else if (config.model.kind === "custom") {
|
|
1590
|
+
model = config.model.name;
|
|
1591
|
+
if (config.runtime === "claude")
|
|
1592
|
+
providerEnv.ANTHROPIC_CUSTOM_MODEL_OPTION = config.model.name;
|
|
1457
1593
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1594
|
+
const p = config.provider;
|
|
1595
|
+
if (p?.kind === "custom" && config.runtime === "claude") {
|
|
1596
|
+
providerEnv.ANTHROPIC_BASE_URL = p.apiUrl;
|
|
1597
|
+
providerEnv.ANTHROPIC_API_KEY = p.apiKey;
|
|
1598
|
+
} else if (p?.kind === "pi-builtin") {
|
|
1599
|
+
const key = PI_BUILTIN_PROVIDER_ENV_KEYS[p.providerId];
|
|
1600
|
+
if (key)
|
|
1601
|
+
providerEnv[key] = p.apiKey;
|
|
1464
1602
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1603
|
+
return {
|
|
1604
|
+
model,
|
|
1605
|
+
reasoningEffort: config.reasoningEffort,
|
|
1606
|
+
fastMode: config.mode.kind === "fast",
|
|
1607
|
+
command: config.command,
|
|
1608
|
+
disallowedTools: config.disallowedTools,
|
|
1609
|
+
envVars,
|
|
1610
|
+
providerEnv
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/drivers/cliLink.ts
|
|
1615
|
+
import * as fs3 from "fs";
|
|
1616
|
+
import * as path3 from "path";
|
|
1617
|
+
function writeCliLink(stateDir, cliName, hostCliPath, platform = process.platform) {
|
|
1618
|
+
const binDir = path3.join(stateDir, "bin");
|
|
1619
|
+
fs3.mkdirSync(binDir, { recursive: true });
|
|
1620
|
+
if (!hostCliPath)
|
|
1621
|
+
return binDir;
|
|
1622
|
+
if (platform === "win32") {
|
|
1623
|
+
const cmdFile = path3.join(binDir, `${cliName}.cmd`);
|
|
1624
|
+
const body = `@echo off\r
|
|
1625
|
+
"${hostCliPath}" %*\r
|
|
1626
|
+
`;
|
|
1627
|
+
fs3.writeFileSync(cmdFile, body);
|
|
1628
|
+
return binDir;
|
|
1472
1629
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1630
|
+
const linkPath = path3.join(binDir, cliName);
|
|
1631
|
+
try {
|
|
1632
|
+
fs3.unlinkSync(linkPath);
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
if (err.code !== "ENOENT")
|
|
1635
|
+
throw err;
|
|
1475
1636
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1637
|
+
try {
|
|
1638
|
+
fs3.symlinkSync(hostCliPath, linkPath);
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
if (err.code !== "EEXIST")
|
|
1641
|
+
throw err;
|
|
1481
1642
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1643
|
+
return binDir;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/drivers/spawnEnv.ts
|
|
1647
|
+
function mergeEnvLayers(base, layers) {
|
|
1648
|
+
const env = { ...base };
|
|
1649
|
+
const provenance = {};
|
|
1650
|
+
const ordered = [
|
|
1651
|
+
...layers.filter((l) => !l.sensitive).sort((a, b) => a.precedence - b.precedence),
|
|
1652
|
+
...layers.filter((l) => l.sensitive).sort((a, b) => a.precedence - b.precedence)
|
|
1653
|
+
];
|
|
1654
|
+
for (const layer of ordered) {
|
|
1655
|
+
for (const [k, v] of Object.entries(layer.vars)) {
|
|
1656
|
+
if (v === undefined)
|
|
1657
|
+
continue;
|
|
1658
|
+
env[k] = v;
|
|
1659
|
+
provenance[k] = layer.name;
|
|
1660
|
+
}
|
|
1488
1661
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1662
|
+
return { env, provenance };
|
|
1663
|
+
}
|
|
1664
|
+
function platformEnv(prefix, f) {
|
|
1665
|
+
const E = prefix;
|
|
1666
|
+
return {
|
|
1667
|
+
[`${E}_HOME`]: f.stateHome,
|
|
1668
|
+
[`${E}_ID`]: f.agentId,
|
|
1669
|
+
[`${E}_CLI`]: f.cliName,
|
|
1670
|
+
[`${E}_SERVER_URL`]: f.serverUrl,
|
|
1671
|
+
[`${E}_ACTIVE_CAPABILITIES`]: f.capabilities.join(","),
|
|
1672
|
+
[`${E}_LAUNCH_ID`]: f.launchId,
|
|
1673
|
+
[`${E}_CLI_TRANSPORT_TRACE_DIR`]: f.traceDir
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function runtimeContextEnv(prefix, rc) {
|
|
1677
|
+
if (!rc)
|
|
1678
|
+
return {};
|
|
1679
|
+
const E = prefix;
|
|
1680
|
+
return {
|
|
1681
|
+
[`${E}_CURRENT_AGENT_ID`]: rc.agentId,
|
|
1682
|
+
[`${E}_CURRENT_SERVER_ID`]: rc.serverId,
|
|
1683
|
+
[`${E}_CURRENT_COMPUTER_ID`]: rc.computerId,
|
|
1684
|
+
[`${E}_CURRENT_COMPUTER_NAME`]: rc.computerName,
|
|
1685
|
+
[`${E}_CURRENT_COMPUTER_HOSTNAME`]: rc.hostname,
|
|
1686
|
+
[`${E}_CURRENT_COMPUTER_OS`]: rc.os,
|
|
1687
|
+
[`${E}_CURRENT_DAEMON_VERSION`]: rc.daemonVersion,
|
|
1688
|
+
[`${E}_CURRENT_WORKSPACE_PATH`]: rc.workspacePath
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// src/drivers/agentFile.ts
|
|
1693
|
+
import {
|
|
1694
|
+
writeFileSync as writeFileSync4,
|
|
1695
|
+
readFileSync as readFileSync2,
|
|
1696
|
+
lstatSync,
|
|
1697
|
+
symlinkSync as symlinkSync2,
|
|
1698
|
+
unlinkSync as unlinkSync2,
|
|
1699
|
+
existsSync,
|
|
1700
|
+
readlinkSync,
|
|
1701
|
+
copyFileSync
|
|
1702
|
+
} from "fs";
|
|
1703
|
+
import { join as join3 } from "path";
|
|
1704
|
+
import { createHash } from "crypto";
|
|
1705
|
+
var CANONICAL_FILE = "AGENTS.md";
|
|
1706
|
+
var SYMLINK_ALIASES = ["CLAUDE.md"];
|
|
1707
|
+
function contentHash(content) {
|
|
1708
|
+
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
1709
|
+
}
|
|
1710
|
+
function hasContentChanged(filePath, newContent) {
|
|
1711
|
+
try {
|
|
1712
|
+
const existing = readFileSync2(filePath, "utf-8");
|
|
1713
|
+
return contentHash(existing) !== contentHash(newContent);
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
if (err?.code === "ENOENT")
|
|
1716
|
+
return true;
|
|
1717
|
+
throw err;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
function ensureSymlinks(workDir) {
|
|
1721
|
+
const canonicalPath = join3(workDir, CANONICAL_FILE);
|
|
1722
|
+
if (!existsSync(canonicalPath))
|
|
1723
|
+
return;
|
|
1724
|
+
for (const alias of SYMLINK_ALIASES) {
|
|
1725
|
+
if (alias === CANONICAL_FILE)
|
|
1726
|
+
continue;
|
|
1727
|
+
const aliasPath = join3(workDir, alias);
|
|
1728
|
+
try {
|
|
1729
|
+
const stat = lstatSync(aliasPath);
|
|
1730
|
+
if (stat.isSymbolicLink()) {
|
|
1731
|
+
const target = readlinkSync(aliasPath);
|
|
1732
|
+
if (target === CANONICAL_FILE)
|
|
1733
|
+
continue;
|
|
1734
|
+
unlinkSync2(aliasPath);
|
|
1735
|
+
} else {
|
|
1736
|
+
const aliasContent = readFileSync2(aliasPath, "utf-8");
|
|
1737
|
+
const canonicalContent = readFileSync2(canonicalPath, "utf-8");
|
|
1738
|
+
if (aliasContent === canonicalContent)
|
|
1739
|
+
continue;
|
|
1740
|
+
unlinkSync2(aliasPath);
|
|
1741
|
+
}
|
|
1742
|
+
} catch (err) {
|
|
1743
|
+
if (err?.code !== "ENOENT")
|
|
1744
|
+
throw err;
|
|
1745
|
+
}
|
|
1746
|
+
try {
|
|
1747
|
+
symlinkSync2(CANONICAL_FILE, aliasPath);
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
const code = err?.code;
|
|
1750
|
+
if (code === "EEXIST") {} else if (code === "EPERM" || code === "EACCES") {
|
|
1751
|
+
copyFileSync(canonicalPath, aliasPath);
|
|
1752
|
+
} else {
|
|
1753
|
+
throw err;
|
|
1501
1754
|
}
|
|
1502
1755
|
}
|
|
1503
1756
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
deriveActivity(agent) {
|
|
1511
|
-
if (agent.status === "running" && !agent.turnActive)
|
|
1512
|
-
return "idle";
|
|
1513
|
-
return agent.status;
|
|
1757
|
+
}
|
|
1758
|
+
function writeAgentFile(workDir, systemPromptContent) {
|
|
1759
|
+
const filePath = join3(workDir, CANONICAL_FILE);
|
|
1760
|
+
const changed = hasContentChanged(filePath, systemPromptContent);
|
|
1761
|
+
if (changed) {
|
|
1762
|
+
writeFileSync4(filePath, systemPromptContent, "utf-8");
|
|
1514
1763
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1764
|
+
ensureSymlinks(workDir);
|
|
1765
|
+
return changed;
|
|
1766
|
+
}
|
|
1517
1767
|
|
|
1518
|
-
|
|
1768
|
+
// src/drivers/cliTransport.ts
|
|
1769
|
+
var DEFAULT_CLI_CONFIG = {
|
|
1770
|
+
cliName: "alook",
|
|
1771
|
+
envPrefix: "ALOOK",
|
|
1772
|
+
stateDirName: ".alook"
|
|
1773
|
+
};
|
|
1774
|
+
function resolveStateHome(envPrefix) {
|
|
1775
|
+
return process.env[`${envPrefix}_HOME`] || path4.join(process.env.HOME || process.env.USERPROFILE || ".", `.${envPrefix.toLowerCase()}`);
|
|
1776
|
+
}
|
|
1777
|
+
async function prepareCliTransport(ctx, extraEnv = {}, cli = DEFAULT_CLI_CONFIG, platform = process.platform) {
|
|
1778
|
+
const E = cli.envPrefix;
|
|
1779
|
+
const stateHome = resolveStateHome(E);
|
|
1780
|
+
const stateDir = path4.join(ctx.workingDirectory, cli.stateDirName);
|
|
1781
|
+
await fs4.promises.mkdir(stateDir, { recursive: true });
|
|
1782
|
+
if (ctx.standingPrompt)
|
|
1783
|
+
writeAgentFile(ctx.workingDirectory, ctx.standingPrompt);
|
|
1784
|
+
const binDir = writeCliLink(stateDir, cli.cliName, cli.hostCliPath, platform);
|
|
1785
|
+
if (!ctx.credentialProxy) {
|
|
1786
|
+
throw new Error("prepareCliTransport: ctx.credentialProxy is required — start a credential proxy " + "(see src/credentials) and pass { broker, proxyUrl }. There is no plaintext mode.");
|
|
1519
1787
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1788
|
+
const capabilities = ctx.credentialProxy.capabilities;
|
|
1789
|
+
if (!Array.isArray(capabilities)) {
|
|
1790
|
+
throw new Error("prepareCliTransport: credentialProxy.capabilities is required " + "(empty array is allowed for zero-capability launches; undefined is a wiring bug)");
|
|
1522
1791
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
this.doSpawn(effect.agentId, this.withFooter(effect.prompt), effect.resumeSessionId);
|
|
1527
|
-
break;
|
|
1528
|
-
case "send": {
|
|
1529
|
-
const session = this.sessions.get(effect.agentId);
|
|
1530
|
-
session?.send({ text: this.stampNow(this.withFooter(effect.text)), mode: effect.mode });
|
|
1531
|
-
this.log.info("steering message sent to running agent", { agentId: effect.agentId, mode: effect.mode });
|
|
1532
|
-
break;
|
|
1533
|
-
}
|
|
1534
|
-
case "stop":
|
|
1535
|
-
case "terminate_stalled": {
|
|
1536
|
-
const session = this.sessions.get(effect.agentId);
|
|
1537
|
-
Promise.resolve(session?.stop({ reason: effect.type, forceAfterMs: 5000 }));
|
|
1538
|
-
const spawnState = this.activeSpawnState.get(effect.agentId);
|
|
1539
|
-
if (spawnState)
|
|
1540
|
-
spawnState.suppressExitLog = true;
|
|
1541
|
-
this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled");
|
|
1542
|
-
this.opts.onAgentLocallyStopped?.({ agentId: effect.agentId, reason: effect.type });
|
|
1543
|
-
break;
|
|
1544
|
-
}
|
|
1545
|
-
case "gated_hold":
|
|
1546
|
-
this.log.info("gated busy message held", {
|
|
1547
|
-
agentId: effect.agentId,
|
|
1548
|
-
reason: effect.reason,
|
|
1549
|
-
blockedReason: effect.blockedReason,
|
|
1550
|
-
recentEvents: effect.recentEvents
|
|
1551
|
-
});
|
|
1552
|
-
break;
|
|
1792
|
+
for (const c of capabilities) {
|
|
1793
|
+
if (typeof c !== "string" || c.includes(",")) {
|
|
1794
|
+
throw new Error(`prepareCliTransport: capability entry ${JSON.stringify(c)} contains a comma ` + `(each capability must be a single token; use ["send","read"] instead of ["send,read"])`);
|
|
1553
1795
|
}
|
|
1554
1796
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
throw new Error(`AgentProcessManager: real spawn of "${agentId}" on in-process SDK runtime "${driver.id}" needs ` + "sdkDriverDepsFor — set ManagerRuntimeOpts.sdkDriverDepsFor, or pass a sessionFactory for tests.");
|
|
1580
|
-
}
|
|
1581
|
-
if (!this.opts.sessionFactory && !driver.createSession && !ctx.credentialProxy) {
|
|
1582
|
-
throw new Error(`AgentProcessManager: real spawn of "${agentId}" needs a credentialProxy — ` + "set ManagerRuntimeOpts.credentialProxy (or baseContextFor's), or pass a sessionFactory for tests.");
|
|
1583
|
-
}
|
|
1584
|
-
const session = this.opts.sessionFactory ? this.opts.sessionFactory({ agentId, driver, ctx }) : driver.createSession ? new SdkManagedSession(driver, ctx, this.opts.sdkDriverDepsFor(ctx)) : createChildProcessRuntimeSession(driver, ctx);
|
|
1585
|
-
this.sessions.set(agentId, session);
|
|
1586
|
-
const state = { hasEstablished: false, hasReportedSpawnFailure: false, suppressExitLog: false };
|
|
1587
|
-
this.activeSpawnState.set(agentId, state);
|
|
1588
|
-
const reportSpawnFailure = (reason) => {
|
|
1589
|
-
if (state.hasEstablished || state.hasReportedSpawnFailure)
|
|
1590
|
-
return;
|
|
1591
|
-
state.hasReportedSpawnFailure = true;
|
|
1592
|
-
this.log.warn("spawn failed", { agentId, runtime: driver.id, reason });
|
|
1593
|
-
this.opts.onRuntimeSpawnFailed?.(driver.id, reason);
|
|
1594
|
-
};
|
|
1595
|
-
session.on("runtime_event", (e) => {
|
|
1596
|
-
if (!state.hasEstablished) {
|
|
1597
|
-
state.hasEstablished = true;
|
|
1598
|
-
}
|
|
1599
|
-
this.opts.onRuntimeSessionEstablished?.(driver.id);
|
|
1600
|
-
if (e?.kind === "turn_end" && driver.lifecycle.kind === "per_turn") {
|
|
1601
|
-
state.suppressExitLog = true;
|
|
1797
|
+
ctx.credentialProxy.broker.revokeAgent(ctx.agentId);
|
|
1798
|
+
const reg = ctx.credentialProxy.broker.mint(ctx.agentId, ctx.launchId ?? "default", capabilities, ctx.credentialProxy.runnerKey);
|
|
1799
|
+
const tokenFile = reg.voucherFile;
|
|
1800
|
+
const resolved = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
1801
|
+
const pathValue = [binDir, process.env.PATH ?? ""].filter(Boolean).join(path4.delimiter);
|
|
1802
|
+
const layers = [
|
|
1803
|
+
{ name: "hostStatic", precedence: 10, vars: cli.extraEnv ?? {} },
|
|
1804
|
+
{ name: "userEnv", precedence: 20, vars: resolved.envVars },
|
|
1805
|
+
{ name: "driver", precedence: 30, vars: extraEnv },
|
|
1806
|
+
{
|
|
1807
|
+
name: "platformContract",
|
|
1808
|
+
precedence: 40,
|
|
1809
|
+
vars: {
|
|
1810
|
+
...platformEnv(E, {
|
|
1811
|
+
stateHome,
|
|
1812
|
+
agentId: ctx.agentId,
|
|
1813
|
+
cliName: cli.cliName,
|
|
1814
|
+
serverUrl: ctx.config.serverUrl,
|
|
1815
|
+
capabilities,
|
|
1816
|
+
launchId: ctx.launchId,
|
|
1817
|
+
traceDir: ctx.cliTransportTraceDir
|
|
1818
|
+
}),
|
|
1819
|
+
FORCE_COLOR: "0",
|
|
1820
|
+
NO_COLOR: "1"
|
|
1602
1821
|
}
|
|
1603
|
-
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
this.liveSessions.delete(agentId);
|
|
1617
|
-
if (this.activeSpawnState.get(agentId) === state)
|
|
1618
|
-
this.activeSpawnState.delete(agentId);
|
|
1619
|
-
this.dispatch({ type: "exit", agentId });
|
|
1620
|
-
});
|
|
1621
|
-
const stampedPrompt = this.stampNow(prompt);
|
|
1622
|
-
Promise.resolve(session.start({ text: stampedPrompt, sessionId: ctx.config.sessionId })).then(() => {
|
|
1623
|
-
if (this.sessions.get(agentId) !== session)
|
|
1624
|
-
return;
|
|
1625
|
-
this.dispatch({ type: "spawned", agentId, nowMs: this.now() });
|
|
1626
|
-
}).catch((err) => {
|
|
1627
|
-
const code = err?.code ?? "spawn_threw";
|
|
1628
|
-
reportSpawnFailure(String(code));
|
|
1629
|
-
if (this.sessions.get(agentId) === session)
|
|
1630
|
-
this.sessions.delete(agentId);
|
|
1631
|
-
this.dispatch({ type: "exit", agentId });
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
flushThinkingAudit(agentId) {
|
|
1635
|
-
const buffered = this.thinkingBuffers.get(agentId);
|
|
1636
|
-
if (!buffered)
|
|
1637
|
-
return;
|
|
1638
|
-
this.thinkingBuffers.delete(agentId);
|
|
1639
|
-
if (!this.opts.onBotAuditEvent)
|
|
1640
|
-
return;
|
|
1641
|
-
const { text, truncated, chars } = truncateThinking(buffered);
|
|
1642
|
-
try {
|
|
1643
|
-
this.opts.onBotAuditEvent(agentId, {
|
|
1644
|
-
kind: "thinking",
|
|
1645
|
-
payload: { text, truncated, chars }
|
|
1646
|
-
}, {
|
|
1647
|
-
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
1648
|
-
launchId: this.launchIds.get(agentId) ?? null
|
|
1649
|
-
});
|
|
1650
|
-
} catch {}
|
|
1651
|
-
}
|
|
1652
|
-
onRuntimeEvent(agentId, e, runtimeId) {
|
|
1653
|
-
const ev = e;
|
|
1654
|
-
if (!ev?.kind)
|
|
1655
|
-
return;
|
|
1656
|
-
if (this.opts.onBotAuditEvent) {
|
|
1657
|
-
if (ev.kind === "thinking" && typeof ev.text === "string") {
|
|
1658
|
-
if (ev.text.length > 0) {
|
|
1659
|
-
this.thinkingBuffers.set(agentId, (this.thinkingBuffers.get(agentId) ?? "") + ev.text);
|
|
1660
|
-
}
|
|
1661
|
-
} else {
|
|
1662
|
-
this.flushThinkingAudit(agentId);
|
|
1663
|
-
if (ev.kind === "tool_call" && typeof ev.name === "string") {
|
|
1664
|
-
const audit = extractToolAudit(ev.name, ev.input);
|
|
1665
|
-
if (!audit.suppressed) {
|
|
1666
|
-
const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
|
|
1667
|
-
try {
|
|
1668
|
-
this.opts.onBotAuditEvent(agentId, {
|
|
1669
|
-
kind: "tool_call",
|
|
1670
|
-
payload
|
|
1671
|
-
}, {
|
|
1672
|
-
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
1673
|
-
launchId: this.launchIds.get(agentId) ?? null
|
|
1674
|
-
});
|
|
1675
|
-
} catch {}
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
if (ev.kind === "session_init" && ev.sessionId) {
|
|
1681
|
-
this.dispatch({ type: "session", agentId, sessionId: ev.sessionId });
|
|
1682
|
-
this.liveSessions.set(agentId, ev.sessionId);
|
|
1683
|
-
this.opts.timeline?.setSession(agentId, ev.sessionId);
|
|
1684
|
-
this.opts.onAgentSession?.({
|
|
1685
|
-
agentId,
|
|
1686
|
-
sessionId: ev.sessionId,
|
|
1687
|
-
launchId: this.launchIds.get(agentId) ?? ""
|
|
1688
|
-
});
|
|
1689
|
-
this.log.info("agent session established", { agentId, sessionId: ev.sessionId, runtime: runtimeId });
|
|
1690
|
-
}
|
|
1691
|
-
if (ev.kind === "text" && typeof ev.text === "string" && ev.text.length > 0) {
|
|
1692
|
-
this.opts.timeline?.appendResponseToLatest(agentId, ev.text);
|
|
1693
|
-
}
|
|
1694
|
-
this.dispatch({ type: "progress", agentId, nowMs: this.now() });
|
|
1695
|
-
this.dispatch({ type: "runtime_signal", agentId, kind: ev.kind, nowMs: this.now() });
|
|
1696
|
-
if (ev.kind === "turn_end") {
|
|
1697
|
-
this.logSessionEnded(agentId, "turn_end");
|
|
1698
|
-
this.dispatch({ type: "turn_end", agentId, nowMs: this.now() });
|
|
1822
|
+
},
|
|
1823
|
+
{ name: "runtimeContext", precedence: 50, vars: runtimeContextEnv(E, ctx.config.runtimeContext) },
|
|
1824
|
+
{
|
|
1825
|
+
name: "network",
|
|
1826
|
+
precedence: 60,
|
|
1827
|
+
vars: { NO_PROXY: ["127.0.0.1", "localhost", process.env.NO_PROXY].filter(Boolean).join(","), PATH: pathValue }
|
|
1828
|
+
},
|
|
1829
|
+
{ name: "providerProtected", precedence: 70, vars: resolved.providerEnv },
|
|
1830
|
+
{
|
|
1831
|
+
name: "credential",
|
|
1832
|
+
precedence: 100,
|
|
1833
|
+
sensitive: true,
|
|
1834
|
+
vars: { [`${E}_PROXY_URL`]: ctx.credentialProxy.proxyUrl, [`${E}_PROXY_TOKEN_FILE`]: tokenFile }
|
|
1699
1835
|
}
|
|
1700
|
-
|
|
1836
|
+
];
|
|
1837
|
+
const { env: spawnEnv } = mergeEnvLayers(process.env, layers);
|
|
1838
|
+
return { stateDir, tokenFile, spawnEnv };
|
|
1701
1839
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
botId;
|
|
1705
|
-
constructor(botId) {
|
|
1706
|
-
super(`Bot not in this daemon's cache: ${botId}`);
|
|
1707
|
-
this.botId = botId;
|
|
1708
|
-
this.name = "UnknownBotError";
|
|
1709
|
-
}
|
|
1840
|
+
function buildCliTransportSystemPrompt(config) {
|
|
1841
|
+
return buildCliSystemPrompt(config);
|
|
1710
1842
|
}
|
|
1711
1843
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1844
|
+
// src/util/localTime.ts
|
|
1845
|
+
function localISOString(now) {
|
|
1846
|
+
const tzOffset = -now.getTimezoneOffset();
|
|
1847
|
+
const sign = tzOffset >= 0 ? "+" : "-";
|
|
1848
|
+
const abs = Math.abs(tzOffset);
|
|
1849
|
+
const hh = String(Math.floor(abs / 60)).padStart(2, "0");
|
|
1850
|
+
const mm = String(abs % 60).padStart(2, "0");
|
|
1851
|
+
const y = now.getFullYear();
|
|
1852
|
+
const mo = String(now.getMonth() + 1).padStart(2, "0");
|
|
1853
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
1854
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
1855
|
+
const mi = String(now.getMinutes()).padStart(2, "0");
|
|
1856
|
+
const s = String(now.getSeconds()).padStart(2, "0");
|
|
1857
|
+
const ms = String(now.getMilliseconds()).padStart(3, "0");
|
|
1858
|
+
return `${y}-${mo}-${d}T${h}:${mi}:${s}.${ms}${sign}${hh}:${mm}`;
|
|
1719
1859
|
}
|
|
1720
|
-
function
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1860
|
+
function nowLocalISO() {
|
|
1861
|
+
return localISOString(new Date);
|
|
1862
|
+
}
|
|
1863
|
+
function toLocalISO(iso) {
|
|
1864
|
+
if (!iso)
|
|
1865
|
+
return iso;
|
|
1866
|
+
const d = new Date(iso);
|
|
1867
|
+
if (Number.isNaN(d.getTime()))
|
|
1868
|
+
return iso;
|
|
1869
|
+
return localISOString(d);
|
|
1728
1870
|
}
|
|
1729
1871
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1872
|
+
// src/manager/managerRuntime.ts
|
|
1873
|
+
var THINKING_MAX_BYTES = 4096;
|
|
1874
|
+
var STDERR_LOG_MAX_LEN = 2000;
|
|
1875
|
+
var MAX_TARGET_CODE_UNITS = 200;
|
|
1876
|
+
function canonicalToolName(rawName) {
|
|
1877
|
+
const lower = rawName.toLowerCase();
|
|
1878
|
+
switch (lower) {
|
|
1879
|
+
case "bash":
|
|
1880
|
+
case "shell":
|
|
1881
|
+
return "bash";
|
|
1882
|
+
case "read":
|
|
1883
|
+
return "read";
|
|
1884
|
+
case "edit":
|
|
1885
|
+
case "multiedit":
|
|
1886
|
+
case "file_change":
|
|
1887
|
+
return "edit";
|
|
1888
|
+
case "write":
|
|
1889
|
+
return "write";
|
|
1890
|
+
case "grep":
|
|
1891
|
+
return "grep";
|
|
1892
|
+
case "glob":
|
|
1893
|
+
return "glob";
|
|
1894
|
+
case "find":
|
|
1895
|
+
return "find";
|
|
1896
|
+
case "ls":
|
|
1897
|
+
return "ls";
|
|
1898
|
+
case "notebookedit":
|
|
1899
|
+
case "notebook_edit":
|
|
1900
|
+
return "notebook_edit";
|
|
1901
|
+
case "websearch":
|
|
1902
|
+
case "web_search":
|
|
1903
|
+
return "web_search";
|
|
1904
|
+
case "webfetch":
|
|
1905
|
+
case "web_fetch":
|
|
1906
|
+
return "web_fetch";
|
|
1907
|
+
case "todowrite":
|
|
1908
|
+
case "todo_write":
|
|
1909
|
+
return "todo_write";
|
|
1910
|
+
default:
|
|
1911
|
+
return lower;
|
|
1738
1912
|
}
|
|
1739
1913
|
}
|
|
1740
|
-
function
|
|
1741
|
-
|
|
1914
|
+
function classify(canonicalName) {
|
|
1915
|
+
switch (canonicalName) {
|
|
1916
|
+
case "bash":
|
|
1917
|
+
return "shell";
|
|
1918
|
+
case "read":
|
|
1919
|
+
case "edit":
|
|
1920
|
+
case "write":
|
|
1921
|
+
case "ls":
|
|
1922
|
+
case "notebook_edit":
|
|
1923
|
+
return "file_target";
|
|
1924
|
+
case "grep":
|
|
1925
|
+
case "glob":
|
|
1926
|
+
case "find":
|
|
1927
|
+
return "pattern";
|
|
1928
|
+
default:
|
|
1929
|
+
return "fallthrough";
|
|
1930
|
+
}
|
|
1742
1931
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
this.opts = opts;
|
|
1753
|
-
this.log = opts.logger ?? createLogger({ header: "@alook/daemon:router" });
|
|
1754
|
-
this.scheduleResend = opts.scheduleReadyResend ?? queueMicrotask.bind(globalThis);
|
|
1755
|
-
for (const r of opts.runtimeReport) {
|
|
1756
|
-
this.runtimes.set(r.id, {
|
|
1757
|
-
id: r.id,
|
|
1758
|
-
version: r.version,
|
|
1759
|
-
status: r.status ?? "healthy",
|
|
1760
|
-
lastError: r.lastError,
|
|
1761
|
-
lastErrorAt: r.lastErrorAt
|
|
1762
|
-
});
|
|
1932
|
+
function coerceInputRecord(input) {
|
|
1933
|
+
if (typeof input === "string") {
|
|
1934
|
+
try {
|
|
1935
|
+
const parsed = JSON.parse(input);
|
|
1936
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1937
|
+
return parsed;
|
|
1938
|
+
}
|
|
1939
|
+
} catch {
|
|
1940
|
+
return;
|
|
1763
1941
|
}
|
|
1942
|
+
return;
|
|
1764
1943
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
return
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1944
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
1945
|
+
return;
|
|
1946
|
+
return input;
|
|
1947
|
+
}
|
|
1948
|
+
function pickCommandString(input) {
|
|
1949
|
+
const rec = coerceInputRecord(input);
|
|
1950
|
+
if (!rec)
|
|
1951
|
+
return;
|
|
1952
|
+
if (typeof rec.command === "string")
|
|
1953
|
+
return rec.command;
|
|
1954
|
+
if (Array.isArray(rec.command))
|
|
1955
|
+
return rec.command.filter((v) => typeof v === "string").join(" ");
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
function pickFileTarget(input) {
|
|
1959
|
+
const rec = coerceInputRecord(input);
|
|
1960
|
+
if (!rec)
|
|
1961
|
+
return;
|
|
1962
|
+
if (typeof rec.file_path === "string")
|
|
1963
|
+
return rec.file_path;
|
|
1964
|
+
if (typeof rec.path === "string")
|
|
1965
|
+
return rec.path;
|
|
1966
|
+
if (typeof rec.notebook_path === "string")
|
|
1967
|
+
return rec.notebook_path;
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
function pickPatternTarget(input) {
|
|
1971
|
+
const rec = coerceInputRecord(input);
|
|
1972
|
+
if (!rec)
|
|
1973
|
+
return;
|
|
1974
|
+
if (typeof rec.pattern === "string")
|
|
1975
|
+
return rec.pattern;
|
|
1976
|
+
if (typeof rec.query === "string")
|
|
1977
|
+
return rec.query;
|
|
1978
|
+
if (typeof rec.path === "string")
|
|
1979
|
+
return rec.path;
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
function pickFallthroughTarget(input) {
|
|
1983
|
+
const rec = coerceInputRecord(input);
|
|
1984
|
+
if (!rec)
|
|
1985
|
+
return;
|
|
1986
|
+
if (typeof rec.url === "string")
|
|
1987
|
+
return rec.url;
|
|
1988
|
+
if (typeof rec.query === "string")
|
|
1989
|
+
return rec.query;
|
|
1990
|
+
if (typeof rec.path === "string")
|
|
1991
|
+
return rec.path;
|
|
1992
|
+
if (typeof rec.name === "string")
|
|
1993
|
+
return rec.name;
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
var ALOOK_SHELL_INVOCATION_RE = new RegExp(`^${DEFAULT_CLI_CONFIG.cliName}(\\s|$)`);
|
|
1997
|
+
function isAlookShellInvocation(command) {
|
|
1998
|
+
if (!command)
|
|
1999
|
+
return false;
|
|
2000
|
+
return ALOOK_SHELL_INVOCATION_RE.test(command.trimStart());
|
|
2001
|
+
}
|
|
2002
|
+
function truncateTargetToCodeUnits(s) {
|
|
2003
|
+
if (s.length <= MAX_TARGET_CODE_UNITS)
|
|
2004
|
+
return s;
|
|
2005
|
+
let end = MAX_TARGET_CODE_UNITS - 1;
|
|
2006
|
+
const cu = s.charCodeAt(end - 1);
|
|
2007
|
+
if (cu >= 55296 && cu <= 56319)
|
|
2008
|
+
end -= 1;
|
|
2009
|
+
return s.slice(0, end) + "…";
|
|
2010
|
+
}
|
|
2011
|
+
function extractToolAudit(rawName, rawInput) {
|
|
2012
|
+
const name = canonicalToolName(rawName);
|
|
2013
|
+
const cls = classify(name);
|
|
2014
|
+
if (cls === "shell") {
|
|
2015
|
+
const raw = pickCommandString(rawInput);
|
|
2016
|
+
if (isAlookShellInvocation(raw)) {
|
|
2017
|
+
return { name, suppressed: true };
|
|
1789
2018
|
}
|
|
1790
|
-
|
|
2019
|
+
const firstLine = typeof raw === "string" ? raw.split(`
|
|
2020
|
+
`).map((s) => s.trim()).find((s) => s.length > 0) : undefined;
|
|
2021
|
+
if (!firstLine)
|
|
2022
|
+
return { name, suppressed: false };
|
|
2023
|
+
return { name, target: truncateTargetToCodeUnits(firstLine), suppressed: false };
|
|
1791
2024
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
2025
|
+
let target;
|
|
2026
|
+
if (cls === "file_target")
|
|
2027
|
+
target = pickFileTarget(rawInput);
|
|
2028
|
+
else if (cls === "pattern")
|
|
2029
|
+
target = pickPatternTarget(rawInput);
|
|
2030
|
+
else
|
|
2031
|
+
target = pickFallthroughTarget(rawInput);
|
|
2032
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
2033
|
+
return { name, suppressed: false };
|
|
1794
2034
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
this.runtimes.set(id, {
|
|
1803
|
-
...existing,
|
|
1804
|
-
status: "unhealthy",
|
|
1805
|
-
lastError: reason,
|
|
1806
|
-
lastErrorAt: nowIso
|
|
1807
|
-
});
|
|
1808
|
-
this.log.warn("runtime marked unhealthy", { runtimeId: id, reason });
|
|
1809
|
-
this.scheduleReadyFrameResend();
|
|
2035
|
+
return { name, target: truncateTargetToCodeUnits(target), suppressed: false };
|
|
2036
|
+
}
|
|
2037
|
+
function truncateThinking(text) {
|
|
2038
|
+
const chars = [...text].length;
|
|
2039
|
+
const buf = Buffer.from(text, "utf8");
|
|
2040
|
+
if (buf.byteLength <= THINKING_MAX_BYTES) {
|
|
2041
|
+
return { text, truncated: false, chars };
|
|
1810
2042
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2043
|
+
let end = THINKING_MAX_BYTES;
|
|
2044
|
+
while (end > 0 && (buf[end] & 192) === 128)
|
|
2045
|
+
end--;
|
|
2046
|
+
const truncatedText = buf.subarray(0, end).toString("utf8");
|
|
2047
|
+
return { text: truncatedText, truncated: true, chars };
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
class AgentProcessManager {
|
|
2051
|
+
state;
|
|
2052
|
+
sessions = new Map;
|
|
2053
|
+
runtimeConfigs = new Map;
|
|
2054
|
+
resumeSessions = new Map;
|
|
2055
|
+
launchIds = new Map;
|
|
2056
|
+
liveSessions = new Map;
|
|
2057
|
+
thinkingBuffers = new Map;
|
|
2058
|
+
activeSpawnState = new Map;
|
|
2059
|
+
opts;
|
|
2060
|
+
tickTimer = null;
|
|
2061
|
+
now;
|
|
2062
|
+
log;
|
|
2063
|
+
constructor(opts) {
|
|
2064
|
+
this.opts = {
|
|
2065
|
+
tickIntervalMs: 5000,
|
|
2066
|
+
staleThresholdMs: 120000,
|
|
2067
|
+
idleTimeoutMs: 300000,
|
|
2068
|
+
stampWakePromptTime: false,
|
|
2069
|
+
...opts
|
|
2070
|
+
};
|
|
2071
|
+
this.now = opts.now ?? (() => Date.now());
|
|
2072
|
+
this.log = opts.logger ?? createLogger({ header: "@alook/daemon:manager" });
|
|
2073
|
+
this.state = createInitialManagerState(this.opts.staleThresholdMs, this.opts.idleTimeoutMs);
|
|
2074
|
+
}
|
|
2075
|
+
register(agentId, launch) {
|
|
2076
|
+
if (launch?.runtimeConfig)
|
|
2077
|
+
this.runtimeConfigs.set(agentId, launch.runtimeConfig);
|
|
2078
|
+
if (launch?.sessionId)
|
|
2079
|
+
this.resumeSessions.set(agentId, launch.sessionId);
|
|
2080
|
+
if (launch?.launchId)
|
|
2081
|
+
this.launchIds.set(agentId, launch.launchId);
|
|
2082
|
+
const driver = this.opts.driverFor(agentId, this.runtimeConfigs.get(agentId));
|
|
2083
|
+
const caps = {
|
|
2084
|
+
lifecycleKind: driver.lifecycle.kind,
|
|
2085
|
+
supportsStdinNotification: driver.supportsStdinNotification,
|
|
2086
|
+
busyDeliveryMode: driver.busyDeliveryMode
|
|
2087
|
+
};
|
|
2088
|
+
this.dispatch({ type: "register", agentId, caps });
|
|
2089
|
+
}
|
|
2090
|
+
deliver(agentId, message) {
|
|
2091
|
+
this.dispatch({ type: "wake", agentId, message, nowMs: this.now() });
|
|
2092
|
+
}
|
|
2093
|
+
forgetSession(agentId) {
|
|
2094
|
+
this.resumeSessions.delete(agentId);
|
|
2095
|
+
this.liveSessions.delete(agentId);
|
|
2096
|
+
this.dispatch({ type: "reset_session", agentId });
|
|
2097
|
+
this.opts.timeline?.forgetSession(agentId);
|
|
2098
|
+
}
|
|
2099
|
+
enqueueRewake(agentId, message) {
|
|
2100
|
+
this.dispatch({ type: "rewake_after_reset", agentId, message });
|
|
2101
|
+
}
|
|
2102
|
+
markResetting(agentId) {
|
|
2103
|
+
this.dispatch({ type: "begin_reset", agentId });
|
|
2104
|
+
}
|
|
2105
|
+
async resetSession(agentId, opts) {
|
|
2106
|
+
this.register(agentId, { runtimeConfig: opts.runtimeConfig, launchId: opts.launchId });
|
|
2107
|
+
this.forgetSession(agentId);
|
|
2108
|
+
this.markResetting(agentId);
|
|
2109
|
+
const status = this.state.agents[agentId]?.status;
|
|
2110
|
+
if (status === "idle") {
|
|
2111
|
+
try {
|
|
2112
|
+
this.deliver(agentId, { text: opts.rewakePrompt });
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
this.log.error("agent reset idle-branch spawn threw synchronously", {
|
|
2115
|
+
agentId,
|
|
2116
|
+
err: err instanceof Error ? err.message : String(err)
|
|
2117
|
+
});
|
|
2118
|
+
this.dispatch({ type: "exit", agentId });
|
|
2119
|
+
throw err;
|
|
2120
|
+
}
|
|
1816
2121
|
return;
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
status: "healthy"
|
|
1821
|
-
});
|
|
1822
|
-
this.log.info("runtime marked healthy again", { runtimeId: id });
|
|
1823
|
-
this.scheduleReadyFrameResend();
|
|
2122
|
+
}
|
|
2123
|
+
this.enqueueRewake(agentId, { text: opts.rewakePrompt });
|
|
2124
|
+
await this.stop(agentId);
|
|
1824
2125
|
}
|
|
1825
|
-
|
|
1826
|
-
if (
|
|
2126
|
+
start() {
|
|
2127
|
+
if (this.tickTimer)
|
|
1827
2128
|
return;
|
|
1828
|
-
this.
|
|
1829
|
-
this.
|
|
2129
|
+
this.tickTimer = setInterval(() => this.dispatch({ type: "tick", nowMs: this.now() }), this.opts.tickIntervalMs);
|
|
2130
|
+
this.tickTimer.unref?.();
|
|
1830
2131
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
2132
|
+
async stop(agentId) {
|
|
2133
|
+
const session = this.sessions.get(agentId);
|
|
2134
|
+
if (!session)
|
|
1833
2135
|
return;
|
|
1834
|
-
|
|
1835
|
-
this.
|
|
1836
|
-
this.pendingResend = false;
|
|
1837
|
-
try {
|
|
1838
|
-
this.opts.channel.sendReady?.(this.buildReady());
|
|
1839
|
-
} catch {}
|
|
1840
|
-
});
|
|
2136
|
+
await Promise.resolve(session.stop({ reason: "requested", forceAfterMs: SESSION_STOP_GRACE_MS }));
|
|
2137
|
+
this.sessions.delete(agentId);
|
|
1841
2138
|
}
|
|
1842
|
-
async
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
agentId: cmd.agentId,
|
|
1847
|
-
channel: cmd.unreadNotice.channel,
|
|
1848
|
-
latestSeq: cmd.unreadNotice.latestSeq
|
|
1849
|
-
});
|
|
1850
|
-
try {
|
|
1851
|
-
const beforeStatus = this.opts.manager.snapshot?.().agents?.[cmd.agentId]?.status ?? "unregistered";
|
|
1852
|
-
const wasActive = this.opts.typingTracker?.hasAny(cmd.agentId) ?? false;
|
|
1853
|
-
await this.opts.onBeforeAgent?.(cmd.agentId);
|
|
1854
|
-
this.opts.manager.register(cmd.agentId, {
|
|
1855
|
-
runtimeConfig: cmd.config,
|
|
1856
|
-
sessionId: cmd.sessionId,
|
|
1857
|
-
launchId: cmd.launchId
|
|
1858
|
-
});
|
|
1859
|
-
this.running.add(cmd.agentId);
|
|
1860
|
-
const dmScope = cmd.unreadNotice.dmConversationId;
|
|
1861
|
-
if (dmScope)
|
|
1862
|
-
this.opts.typingTracker?.add(cmd.agentId, dmScope);
|
|
1863
|
-
const text = (this.opts.formatUnreadNoticeText ?? defaultFormatUnreadNoticeText)(cmd.unreadNotice);
|
|
1864
|
-
this.opts.manager.deliver(cmd.agentId, { seq: cmd.unreadNotice.latestSeq, text });
|
|
1865
|
-
if (dmScope && wasActive && beforeStatus === "running") {
|
|
1866
|
-
this.opts.channel.reportAgentTyping?.({
|
|
1867
|
-
agentId: cmd.agentId,
|
|
1868
|
-
dmConversationId: dmScope
|
|
1869
|
-
});
|
|
1870
|
-
}
|
|
1871
|
-
await this.opts.channel.reportWakeAck?.({
|
|
1872
|
-
agentId: cmd.agentId,
|
|
1873
|
-
launchId: cmd.launchId,
|
|
1874
|
-
status: "ok"
|
|
1875
|
-
});
|
|
1876
|
-
this.log.info("agent:wake ack", { agentId: cmd.agentId, status: "ok" });
|
|
1877
|
-
} catch (err) {
|
|
1878
|
-
if (err instanceof UnknownRuntimeError) {
|
|
1879
|
-
const frame = {
|
|
1880
|
-
type: "session.error",
|
|
1881
|
-
code: "runtime_not_available",
|
|
1882
|
-
agentId: cmd.agentId,
|
|
1883
|
-
payload: {
|
|
1884
|
-
requested: err.requested ?? null,
|
|
1885
|
-
available: err.available
|
|
1886
|
-
}
|
|
1887
|
-
};
|
|
1888
|
-
await this.opts.channel.reportSessionError?.(frame);
|
|
1889
|
-
await this.opts.channel.reportWakeAck?.({
|
|
1890
|
-
agentId: cmd.agentId,
|
|
1891
|
-
launchId: cmd.launchId,
|
|
1892
|
-
status: "error",
|
|
1893
|
-
error: {
|
|
1894
|
-
code: "bot_runtime_missing",
|
|
1895
|
-
message: err.message
|
|
1896
|
-
}
|
|
1897
|
-
});
|
|
1898
|
-
this.log.info("agent:wake ack", {
|
|
1899
|
-
agentId: cmd.agentId,
|
|
1900
|
-
status: "error",
|
|
1901
|
-
"error.code": "bot_runtime_missing"
|
|
1902
|
-
});
|
|
1903
|
-
return;
|
|
1904
|
-
}
|
|
1905
|
-
{
|
|
1906
|
-
const code = classifyErrorCode(err);
|
|
1907
|
-
await this.opts.channel.reportWakeAck?.({
|
|
1908
|
-
agentId: cmd.agentId,
|
|
1909
|
-
launchId: cmd.launchId,
|
|
1910
|
-
status: "error",
|
|
1911
|
-
error: {
|
|
1912
|
-
code,
|
|
1913
|
-
message: err instanceof Error ? err.message : String(err)
|
|
1914
|
-
}
|
|
1915
|
-
});
|
|
1916
|
-
this.log.info("agent:wake ack", { agentId: cmd.agentId, status: "error", "error.code": code });
|
|
1917
|
-
}
|
|
1918
|
-
return;
|
|
1919
|
-
}
|
|
1920
|
-
break;
|
|
1921
|
-
case "agent:stop":
|
|
1922
|
-
this.log.info("agent:stop received", { agentId: cmd.agentId });
|
|
1923
|
-
try {
|
|
1924
|
-
this.running.delete(cmd.agentId);
|
|
1925
|
-
this.opts.manager.stop(cmd.agentId);
|
|
1926
|
-
await this.opts.channel.reportStoppedAck?.({
|
|
1927
|
-
agentId: cmd.agentId,
|
|
1928
|
-
status: "ok"
|
|
1929
|
-
});
|
|
1930
|
-
this.log.info("agent:stop ack", { agentId: cmd.agentId, status: "ok" });
|
|
1931
|
-
} catch (err) {
|
|
1932
|
-
const code = classifyErrorCode(err);
|
|
1933
|
-
await this.opts.channel.reportStoppedAck?.({
|
|
1934
|
-
agentId: cmd.agentId,
|
|
1935
|
-
status: "error",
|
|
1936
|
-
error: {
|
|
1937
|
-
code,
|
|
1938
|
-
message: err instanceof Error ? err.message : String(err)
|
|
1939
|
-
}
|
|
1940
|
-
});
|
|
1941
|
-
this.log.info("agent:stop ack", { agentId: cmd.agentId, status: "error", "error.code": code });
|
|
1942
|
-
}
|
|
1943
|
-
break;
|
|
1944
|
-
case "bot:added":
|
|
1945
|
-
case "bot:updated":
|
|
1946
|
-
case "bot:removed":
|
|
1947
|
-
break;
|
|
2139
|
+
async stopAll() {
|
|
2140
|
+
if (this.tickTimer) {
|
|
2141
|
+
clearInterval(this.tickTimer);
|
|
2142
|
+
this.tickTimer = null;
|
|
1948
2143
|
}
|
|
2144
|
+
await Promise.all([...this.sessions.values()].map((s) => Promise.resolve(s.stop({ reason: "shutdown", forceAfterMs: SESSION_STOP_GRACE_MS }))));
|
|
2145
|
+
this.sessions.clear();
|
|
1949
2146
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
function createTypingScopeTracker() {
|
|
1953
|
-
const scopes = new Map;
|
|
1954
|
-
return {
|
|
1955
|
-
add(agentId, dmConversationId) {
|
|
1956
|
-
let set = scopes.get(agentId);
|
|
1957
|
-
if (!set) {
|
|
1958
|
-
set = new Set;
|
|
1959
|
-
scopes.set(agentId, set);
|
|
1960
|
-
}
|
|
1961
|
-
set.add(dmConversationId);
|
|
1962
|
-
},
|
|
1963
|
-
snapshot(agentId) {
|
|
1964
|
-
const set = scopes.get(agentId);
|
|
1965
|
-
return set ? [...set] : [];
|
|
1966
|
-
},
|
|
1967
|
-
hasAny(agentId) {
|
|
1968
|
-
const set = scopes.get(agentId);
|
|
1969
|
-
return !!set && set.size > 0;
|
|
1970
|
-
},
|
|
1971
|
-
clear(agentId) {
|
|
1972
|
-
scopes.delete(agentId);
|
|
1973
|
-
}
|
|
1974
|
-
};
|
|
1975
|
-
}
|
|
1976
|
-
// src/timeline/timeline.ts
|
|
1977
|
-
import { appendFileSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4, renameSync as renameSync2, existsSync } from "fs";
|
|
1978
|
-
import { join as join2 } from "path";
|
|
1979
|
-
|
|
1980
|
-
// src/timeline/filelock.ts
|
|
1981
|
-
import * as fs3 from "fs";
|
|
1982
|
-
var DEFAULT_STALE_MS = 30000;
|
|
1983
|
-
var META = "meta.json";
|
|
1984
|
-
function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
|
|
1985
|
-
if (tryMkdir(lockPath)) {
|
|
1986
|
-
writeMeta(lockPath);
|
|
1987
|
-
return true;
|
|
2147
|
+
snapshot() {
|
|
2148
|
+
return this.state;
|
|
1988
2149
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2150
|
+
auditContext(agentId) {
|
|
2151
|
+
return {
|
|
2152
|
+
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
2153
|
+
launchId: this.launchIds.get(agentId) ?? null
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
liveSessionReports() {
|
|
2157
|
+
return [...this.liveSessions.entries()].map(([agentId, sessionId]) => ({
|
|
2158
|
+
agentId,
|
|
2159
|
+
sessionId,
|
|
2160
|
+
launchId: this.launchIds.get(agentId) ?? ""
|
|
2161
|
+
}));
|
|
2162
|
+
}
|
|
2163
|
+
dispatch(event) {
|
|
2164
|
+
const before = this.deriveActivitySnapshot(this.state);
|
|
2165
|
+
const { state, effects } = reduceManager(this.state, event);
|
|
2166
|
+
this.state = state;
|
|
2167
|
+
for (const effect of effects)
|
|
2168
|
+
this.applyEffect(effect);
|
|
2169
|
+
if (this.opts.onAgentActivity) {
|
|
2170
|
+
const after = this.deriveActivitySnapshot(this.state);
|
|
2171
|
+
for (const [agentId, activity] of Object.entries(after)) {
|
|
2172
|
+
if (agentId in before && before[agentId] !== activity) {
|
|
2173
|
+
this.opts.onAgentActivity({ agentId, state: activity });
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
1994
2176
|
}
|
|
1995
2177
|
}
|
|
1996
|
-
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
} catch {}
|
|
2002
|
-
}
|
|
2003
|
-
function lockPathFor(dir, filename) {
|
|
2004
|
-
return `${dir}/.${filename}.lock`;
|
|
2005
|
-
}
|
|
2006
|
-
function tryMkdir(lockPath) {
|
|
2007
|
-
try {
|
|
2008
|
-
fs3.mkdirSync(lockPath);
|
|
2009
|
-
return true;
|
|
2010
|
-
} catch (err) {
|
|
2011
|
-
if (err.code === "EEXIST")
|
|
2012
|
-
return false;
|
|
2013
|
-
throw err;
|
|
2178
|
+
deriveActivitySnapshot(state) {
|
|
2179
|
+
const snapshot = {};
|
|
2180
|
+
for (const [agentId, agent] of Object.entries(state.agents))
|
|
2181
|
+
snapshot[agentId] = this.deriveActivity(agent);
|
|
2182
|
+
return snapshot;
|
|
2014
2183
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
} catch {}
|
|
2020
|
-
}
|
|
2021
|
-
function isStale(lockPath, staleMs) {
|
|
2022
|
-
try {
|
|
2023
|
-
const raw = fs3.readFileSync(`${lockPath}/${META}`, "utf8");
|
|
2024
|
-
const acquiredAt = JSON.parse(raw).acquiredAt;
|
|
2025
|
-
if (typeof acquiredAt === "number")
|
|
2026
|
-
return Date.now() - acquiredAt > staleMs;
|
|
2027
|
-
} catch {}
|
|
2028
|
-
try {
|
|
2029
|
-
return Date.now() - fs3.statSync(lockPath).mtimeMs > staleMs;
|
|
2030
|
-
} catch {
|
|
2031
|
-
return false;
|
|
2184
|
+
deriveActivity(agent) {
|
|
2185
|
+
if (agent.status === "running" && !agent.turnActive)
|
|
2186
|
+
return "idle";
|
|
2187
|
+
return agent.status;
|
|
2032
2188
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
try {
|
|
2036
|
-
fs3.rmSync(lockPath, { recursive: true, force: true });
|
|
2037
|
-
} catch {}
|
|
2038
|
-
}
|
|
2189
|
+
withFooter(text) {
|
|
2190
|
+
return this.opts.wakePromptFooter ? `${text}
|
|
2039
2191
|
|
|
2040
|
-
|
|
2041
|
-
function filenameForDate(date) {
|
|
2042
|
-
const y = date.getFullYear();
|
|
2043
|
-
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
2044
|
-
const d = String(date.getDate()).padStart(2, "0");
|
|
2045
|
-
return `${y}-${m}-${d}.jsonl`;
|
|
2046
|
-
}
|
|
2047
|
-
function recentFilenames(maxDays, now) {
|
|
2048
|
-
const out = [];
|
|
2049
|
-
for (let i = 0;i < maxDays; i++) {
|
|
2050
|
-
const d = new Date(now);
|
|
2051
|
-
d.setDate(d.getDate() - i);
|
|
2052
|
-
out.push(filenameForDate(d));
|
|
2192
|
+
${this.opts.wakePromptFooter}` : text;
|
|
2053
2193
|
}
|
|
2054
|
-
|
|
2055
|
-
}
|
|
2056
|
-
function readJsonl(filePath) {
|
|
2057
|
-
let content;
|
|
2058
|
-
try {
|
|
2059
|
-
content = readFileSync3(filePath, "utf-8");
|
|
2060
|
-
} catch {
|
|
2061
|
-
return [];
|
|
2194
|
+
stampNow(text) {
|
|
2195
|
+
return this.opts.stampWakePromptTime ? `[${nowLocalISO()}] ${text}` : text;
|
|
2062
2196
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2197
|
+
applyEffect(effect) {
|
|
2198
|
+
switch (effect.type) {
|
|
2199
|
+
case "spawn":
|
|
2200
|
+
this.doSpawn(effect.agentId, this.withFooter(effect.prompt), effect.resumeSessionId);
|
|
2201
|
+
break;
|
|
2202
|
+
case "send": {
|
|
2203
|
+
const session = this.sessions.get(effect.agentId);
|
|
2204
|
+
session?.send({ text: this.stampNow(this.withFooter(effect.text)), mode: effect.mode });
|
|
2205
|
+
this.log.info("steering message sent to running agent", { agentId: effect.agentId, mode: effect.mode });
|
|
2206
|
+
break;
|
|
2207
|
+
}
|
|
2208
|
+
case "stop":
|
|
2209
|
+
case "terminate_stalled": {
|
|
2210
|
+
const session = this.sessions.get(effect.agentId);
|
|
2211
|
+
Promise.resolve(session?.stop({ reason: effect.type, forceAfterMs: SESSION_STOP_GRACE_MS }));
|
|
2212
|
+
const spawnState = this.activeSpawnState.get(effect.agentId);
|
|
2213
|
+
if (spawnState)
|
|
2214
|
+
spawnState.suppressExitLog = true;
|
|
2215
|
+
this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled");
|
|
2216
|
+
this.opts.onAgentLocallyStopped?.({ agentId: effect.agentId, reason: effect.type });
|
|
2217
|
+
break;
|
|
2218
|
+
}
|
|
2219
|
+
case "gated_hold":
|
|
2220
|
+
this.log.info("gated busy message held", {
|
|
2221
|
+
agentId: effect.agentId,
|
|
2222
|
+
reason: effect.reason,
|
|
2223
|
+
blockedReason: effect.blockedReason,
|
|
2224
|
+
recentEvents: effect.recentEvents
|
|
2225
|
+
});
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2071
2228
|
}
|
|
2072
|
-
|
|
2073
|
-
}
|
|
2074
|
-
function readRecentEntries(timelineDir, opts = {}) {
|
|
2075
|
-
const now = opts.now ?? new Date;
|
|
2076
|
-
const maxDays = opts.maxDays ?? 7;
|
|
2077
|
-
const filenames = recentFilenames(maxDays, now).reverse();
|
|
2078
|
-
const entries = [];
|
|
2079
|
-
for (const filename of filenames) {
|
|
2080
|
-
entries.push(...readJsonl(join2(timelineDir, filename)));
|
|
2229
|
+
logSessionEnded(agentId, reason) {
|
|
2230
|
+
this.log.info("agent session ended", { agentId, sessionId: this.liveSessions.get(agentId) ?? "", reason });
|
|
2081
2231
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
`) + `
|
|
2105
|
-
`);
|
|
2106
|
-
renameSync2(tmpPath, filePath);
|
|
2107
|
-
return true;
|
|
2108
|
-
}
|
|
2232
|
+
doSpawn(agentId, prompt, resumeSessionId) {
|
|
2233
|
+
const driver = this.opts.driverFor(agentId, this.runtimeConfigs.get(agentId));
|
|
2234
|
+
this.log.info("spawning agent", { agentId, runtime: driver.id });
|
|
2235
|
+
const base = this.opts.baseContextFor(agentId);
|
|
2236
|
+
const runtimeConfig = this.runtimeConfigs.get(agentId) ?? base.config?.runtimeConfig;
|
|
2237
|
+
const provider = runtimeConfig?.runtime ?? null;
|
|
2238
|
+
const sessionId = resumeSessionId ?? this.resumeSessions.get(agentId) ?? this.opts.timeline?.resumeSessionId(agentId, provider) ?? base.config?.sessionId;
|
|
2239
|
+
const description = runtimeConfig?.instruction ?? base.config?.description ?? runtimeConfig?.agentName;
|
|
2240
|
+
const agentName = runtimeConfig?.agentName ?? base.config?.agentName;
|
|
2241
|
+
const agentHandle = runtimeConfig?.agentHandle ?? base.config?.agentHandle;
|
|
2242
|
+
const config = { ...base.config ?? {}, runtimeConfig, sessionId, description, agentName, agentHandle };
|
|
2243
|
+
const standingPrompt = base.standingPrompt || driver.buildSystemPrompt?.(config, agentId) || "";
|
|
2244
|
+
const ctx = {
|
|
2245
|
+
...base,
|
|
2246
|
+
prompt,
|
|
2247
|
+
standingPrompt,
|
|
2248
|
+
credentialProxy: base.credentialProxy ?? this.opts.credentialProxy,
|
|
2249
|
+
launchId: this.launchIds.get(agentId) ?? base.launchId,
|
|
2250
|
+
config
|
|
2251
|
+
};
|
|
2252
|
+
if (!this.opts.sessionFactory && driver.createSession && !this.opts.sdkDriverDepsFor) {
|
|
2253
|
+
throw new Error(`AgentProcessManager: real spawn of "${agentId}" on in-process SDK runtime "${driver.id}" needs ` + "sdkDriverDepsFor — set ManagerRuntimeOpts.sdkDriverDepsFor, or pass a sessionFactory for tests.");
|
|
2109
2254
|
}
|
|
2110
|
-
|
|
2111
|
-
`);
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2255
|
+
if (!this.opts.sessionFactory && !driver.createSession && !ctx.credentialProxy) {
|
|
2256
|
+
throw new Error(`AgentProcessManager: real spawn of "${agentId}" needs a credentialProxy — ` + "set ManagerRuntimeOpts.credentialProxy (or baseContextFor's), or pass a sessionFactory for tests.");
|
|
2257
|
+
}
|
|
2258
|
+
const session = this.opts.sessionFactory ? this.opts.sessionFactory({ agentId, driver, ctx }) : driver.createSession ? new SdkManagedSession(driver, ctx, this.opts.sdkDriverDepsFor(ctx)) : createChildProcessRuntimeSession(driver, ctx);
|
|
2259
|
+
this.sessions.set(agentId, session);
|
|
2260
|
+
const state = { hasEstablished: false, hasReportedSpawnFailure: false, suppressExitLog: false };
|
|
2261
|
+
this.activeSpawnState.set(agentId, state);
|
|
2262
|
+
const reportSpawnFailure = (reason) => {
|
|
2263
|
+
if (state.hasEstablished || state.hasReportedSpawnFailure)
|
|
2264
|
+
return;
|
|
2265
|
+
state.hasReportedSpawnFailure = true;
|
|
2266
|
+
this.log.warn("spawn failed", { agentId, runtime: driver.id, reason });
|
|
2267
|
+
this.opts.onRuntimeSpawnFailed?.(driver.id, reason);
|
|
2268
|
+
};
|
|
2269
|
+
session.on("runtime_event", (e) => {
|
|
2270
|
+
if (!state.hasEstablished) {
|
|
2271
|
+
state.hasEstablished = true;
|
|
2272
|
+
}
|
|
2273
|
+
this.opts.onRuntimeSessionEstablished?.(driver.id);
|
|
2274
|
+
if (e?.kind === "turn_end" && driver.lifecycle.kind === "per_turn") {
|
|
2275
|
+
state.suppressExitLog = true;
|
|
2276
|
+
}
|
|
2277
|
+
this.onRuntimeEvent(agentId, e, driver.id);
|
|
2278
|
+
});
|
|
2279
|
+
session.on("stderr", (...args) => {
|
|
2280
|
+
const raw = typeof args[0] === "string" ? args[0] : String(args[0] ?? "");
|
|
2281
|
+
const text = raw.length > STDERR_LOG_MAX_LEN ? raw.slice(0, STDERR_LOG_MAX_LEN) + "…" : raw;
|
|
2282
|
+
this.log.warn("runtime stderr", { agentId, runtime: driver.id, text });
|
|
2283
|
+
});
|
|
2284
|
+
session.on("error", (...args) => {
|
|
2285
|
+
const err = args[0];
|
|
2286
|
+
const code = err?.code ?? "spawn_error";
|
|
2287
|
+
reportSpawnFailure(String(code));
|
|
2288
|
+
});
|
|
2289
|
+
session.on("exit", () => {
|
|
2290
|
+
reportSpawnFailure("pre_handshake_exit");
|
|
2291
|
+
if (state.hasEstablished && !state.suppressExitLog)
|
|
2292
|
+
this.logSessionEnded(agentId, "exit");
|
|
2293
|
+
this.flushThinkingAudit(agentId);
|
|
2294
|
+
this.sessions.delete(agentId);
|
|
2295
|
+
this.liveSessions.delete(agentId);
|
|
2296
|
+
if (this.activeSpawnState.get(agentId) === state)
|
|
2297
|
+
this.activeSpawnState.delete(agentId);
|
|
2298
|
+
this.dispatch({ type: "exit", agentId });
|
|
2299
|
+
});
|
|
2300
|
+
const stampedPrompt = this.stampNow(prompt);
|
|
2301
|
+
Promise.resolve(session.start({ text: stampedPrompt, sessionId: ctx.config.sessionId })).then(() => {
|
|
2302
|
+
if (this.sessions.get(agentId) !== session)
|
|
2303
|
+
return;
|
|
2304
|
+
this.dispatch({ type: "spawned", agentId, nowMs: this.now() });
|
|
2305
|
+
}).catch((err) => {
|
|
2306
|
+
const code = err?.code ?? "spawn_threw";
|
|
2307
|
+
reportSpawnFailure(String(code));
|
|
2308
|
+
if (this.sessions.get(agentId) === session)
|
|
2309
|
+
this.sessions.delete(agentId);
|
|
2310
|
+
this.dispatch({ type: "exit", agentId });
|
|
2311
|
+
});
|
|
2117
2312
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
const lockPath = lockPathFor(timelineDir, filename);
|
|
2127
|
-
if (!acquireLock(lockPath))
|
|
2128
|
-
continue;
|
|
2313
|
+
flushThinkingAudit(agentId) {
|
|
2314
|
+
const buffered = this.thinkingBuffers.get(agentId);
|
|
2315
|
+
if (!buffered)
|
|
2316
|
+
return;
|
|
2317
|
+
this.thinkingBuffers.delete(agentId);
|
|
2318
|
+
if (!this.opts.onBotAuditEvent)
|
|
2319
|
+
return;
|
|
2320
|
+
const { text, truncated, chars } = truncateThinking(buffered);
|
|
2129
2321
|
try {
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2322
|
+
this.opts.onBotAuditEvent(agentId, {
|
|
2323
|
+
kind: "thinking",
|
|
2324
|
+
payload: { text, truncated, chars }
|
|
2325
|
+
}, {
|
|
2326
|
+
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
2327
|
+
launchId: this.launchIds.get(agentId) ?? null
|
|
2328
|
+
});
|
|
2329
|
+
} catch (err) {
|
|
2330
|
+
this.log.debug("audit emit failed (thinking)", { agentId, err: String(err) });
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
onRuntimeEvent(agentId, e, runtimeId) {
|
|
2334
|
+
const ev = e;
|
|
2335
|
+
if (!ev?.kind)
|
|
2336
|
+
return;
|
|
2337
|
+
if (this.opts.onBotAuditEvent) {
|
|
2338
|
+
if (ev.kind === "thinking" && typeof ev.text === "string") {
|
|
2339
|
+
if (ev.text.length > 0) {
|
|
2340
|
+
this.thinkingBuffers.set(agentId, (this.thinkingBuffers.get(agentId) ?? "") + ev.text);
|
|
2341
|
+
}
|
|
2342
|
+
} else {
|
|
2343
|
+
this.flushThinkingAudit(agentId);
|
|
2344
|
+
if (ev.kind === "tool_call" && typeof ev.name === "string") {
|
|
2345
|
+
const audit = extractToolAudit(ev.name, ev.input);
|
|
2346
|
+
if (!audit.suppressed) {
|
|
2347
|
+
const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
|
|
2348
|
+
try {
|
|
2349
|
+
this.opts.onBotAuditEvent(agentId, {
|
|
2350
|
+
kind: "tool_call",
|
|
2351
|
+
payload
|
|
2352
|
+
}, {
|
|
2353
|
+
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
2354
|
+
launchId: this.launchIds.get(agentId) ?? null
|
|
2355
|
+
});
|
|
2356
|
+
} catch (err) {
|
|
2357
|
+
this.log.debug("audit emit failed (tool_call)", { agentId, err: String(err) });
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2135
2361
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2362
|
+
}
|
|
2363
|
+
if (ev.kind === "session_init" && ev.sessionId) {
|
|
2364
|
+
this.dispatch({ type: "session", agentId, sessionId: ev.sessionId });
|
|
2365
|
+
this.liveSessions.set(agentId, ev.sessionId);
|
|
2366
|
+
this.opts.timeline?.setSession(agentId, ev.sessionId);
|
|
2367
|
+
this.opts.onAgentSession?.({
|
|
2368
|
+
agentId,
|
|
2369
|
+
sessionId: ev.sessionId,
|
|
2370
|
+
launchId: this.launchIds.get(agentId) ?? ""
|
|
2371
|
+
});
|
|
2372
|
+
this.log.info("agent session established", { agentId, sessionId: ev.sessionId, runtime: runtimeId });
|
|
2373
|
+
}
|
|
2374
|
+
if (ev.kind === "text" && typeof ev.text === "string" && ev.text.length > 0) {
|
|
2375
|
+
this.opts.timeline?.appendResponseToLatest(agentId, ev.text);
|
|
2376
|
+
}
|
|
2377
|
+
if (ev.kind !== "internal_progress") {
|
|
2378
|
+
this.dispatch({ type: "progress", agentId, nowMs: this.now() });
|
|
2379
|
+
}
|
|
2380
|
+
this.dispatch({ type: "runtime_signal", agentId, kind: ev.kind, nowMs: this.now() });
|
|
2381
|
+
if (ev.kind === "turn_end") {
|
|
2382
|
+
this.logSessionEnded(agentId, "turn_end");
|
|
2383
|
+
this.dispatch({ type: "turn_end", agentId, nowMs: this.now() });
|
|
2150
2384
|
}
|
|
2151
2385
|
}
|
|
2152
|
-
return false;
|
|
2153
2386
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2387
|
+
// src/manager/agentRouter.ts
|
|
2388
|
+
class UnknownBotError extends Error {
|
|
2389
|
+
botId;
|
|
2390
|
+
constructor(botId) {
|
|
2391
|
+
super(`Bot not in this daemon's cache: ${botId}`);
|
|
2392
|
+
this.botId = botId;
|
|
2393
|
+
this.name = "UnknownBotError";
|
|
2394
|
+
}
|
|
2161
2395
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
return e.session_id;
|
|
2396
|
+
|
|
2397
|
+
class BotEnrollFailedError extends Error {
|
|
2398
|
+
botId;
|
|
2399
|
+
constructor(botId, cause) {
|
|
2400
|
+
super(`Failed to enroll bot ${botId}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
2401
|
+
this.botId = botId;
|
|
2402
|
+
this.name = "BotEnrollFailedError";
|
|
2170
2403
|
}
|
|
2171
|
-
return null;
|
|
2172
2404
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
sessionByAgent.set(agentId, sessionId);
|
|
2182
|
-
},
|
|
2183
|
-
appendEntryForAgent(agentId, messages) {
|
|
2184
|
-
const dir = dirFor(agentId);
|
|
2185
|
-
try {
|
|
2186
|
-
mkdirSync4(dir, { recursive: true });
|
|
2187
|
-
} catch {}
|
|
2188
|
-
appendOrMergeEntry(dir, createTimelineEntry({
|
|
2189
|
-
messages,
|
|
2190
|
-
sessionId: sessionByAgent.get(agentId) ?? null,
|
|
2191
|
-
provider: opts.providerFor?.(agentId) ?? null
|
|
2192
|
-
}), now());
|
|
2193
|
-
},
|
|
2194
|
-
appendResponseToLatest(agentId, text) {
|
|
2195
|
-
updateLatestEntry(dirFor(agentId), (e) => e.agent_responses.push(text), { now: now() });
|
|
2196
|
-
},
|
|
2197
|
-
resumeSessionId(agentId, provider) {
|
|
2198
|
-
const rows = readRecentEntries(dirFor(agentId), { now: now() });
|
|
2199
|
-
return findResumableSession(rows, provider ?? undefined);
|
|
2200
|
-
}
|
|
2201
|
-
};
|
|
2405
|
+
function classifyErrorCode(err) {
|
|
2406
|
+
if (err instanceof UnknownBotError)
|
|
2407
|
+
return "bot_unknown";
|
|
2408
|
+
if (err instanceof BotEnrollFailedError)
|
|
2409
|
+
return "bot_enroll_failed";
|
|
2410
|
+
if (err instanceof UnknownRuntimeError)
|
|
2411
|
+
return "bot_runtime_missing";
|
|
2412
|
+
return "internal_error";
|
|
2202
2413
|
}
|
|
2203
|
-
// src/discovery.ts
|
|
2204
|
-
import * as path9 from "path";
|
|
2205
|
-
import * as fs8 from "fs";
|
|
2206
|
-
import { fileURLToPath } from "url";
|
|
2207
|
-
|
|
2208
|
-
// src/drivers/cliTransport.ts
|
|
2209
|
-
import * as fs5 from "fs";
|
|
2210
|
-
import * as path4 from "path";
|
|
2211
2414
|
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
} else {
|
|
2221
|
-
parts.push(`You are ${name} on Alook. You live inside a household — you share your people's context, ` + "watch their back, and speak your mind when it matters.");
|
|
2222
|
-
}
|
|
2223
|
-
if (config.agentHandle) {
|
|
2224
|
-
parts.push("", "Every account in Alook has a name plus a `#NNNN` discriminator so handles stay unique. " + `Yours is \`${config.agentHandle}\`. ` + "Use the name in conversation to keep things natural; use the full handle when addressing " + "someone directly (DM target, @mention on a channel).");
|
|
2225
|
-
}
|
|
2226
|
-
if (owner) {
|
|
2227
|
-
parts.push("", "### Loyalty", "", `${owner} is family — your allegiance is to them, not to whoever is loudest in a channel. ` + "Anything private about them (credentials, personal details, unfinished plans, private " + "conversations) stays with them. Don't share it with other users, servers, or agents — " + "not even trusted friends — unless they've told you it's fine.", "", "You are a peer inside the household, though — not a subordinate. If they're about to do " + "something you think is a bad idea, say so. Loyalty means honesty, not agreement.");
|
|
2228
|
-
}
|
|
2229
|
-
parts.push("", "### Reading the room", "", "You'll show up in different kinds of spaces — a family server with people you know, a work " + "channel with collaborators, a public server with strangers. Same you, different register. " + "Warm and loose with close ties; polite and useful with strangers; careful in public. " + "Let the channel's context set the tone, not a fixed default.");
|
|
2230
|
-
if (config.description) {
|
|
2231
|
-
parts.push("", "### Role", "", config.description, "", "This is a starting point, not a script. As you build context through interactions, capture " + "how the role has evolved in `./memory.md` (the Role text above isn't something you can edit directly).");
|
|
2232
|
-
}
|
|
2233
|
-
return parts.join(`
|
|
2234
|
-
`);
|
|
2235
|
-
}
|
|
2236
|
-
function cliCommandsSection() {
|
|
2237
|
-
return [
|
|
2238
|
-
"## CLI commands",
|
|
2239
|
-
"",
|
|
2240
|
-
`\`${CLI}\` is your command-line interface. Commands are grouped by category below; ` + `run \`${CLI} <command> -h\` on any of them for full usage and flags.`,
|
|
2241
|
-
"",
|
|
2242
|
-
"### Messaging",
|
|
2243
|
-
"",
|
|
2244
|
-
`1. \`${CLI} inbox pull\` — fetch unread messages.`,
|
|
2245
|
-
`2. \`${CLI} message send\` — send a message to a channel, DM, or thread. ` + `Attach files with \`--attachment <id>\` (repeatable, order matters).`,
|
|
2246
|
-
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a local file; ` + `returns an id. Feed that id into \`message send --attachment <id>\`. ` + `The id is stable across the pending→persisted lifecycle.`,
|
|
2247
|
-
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download an attachment ` + `id from any message you have access to (or your own pending uploads).`,
|
|
2248
|
-
"",
|
|
2249
|
-
"### Servers",
|
|
2250
|
-
"",
|
|
2251
|
-
`1. \`${CLI} server list\` — list servers you're a member of.`,
|
|
2252
|
-
`2. \`${CLI} server member --server <id-or-name>\` — list members of a server.`,
|
|
2253
|
-
`3. \`${CLI} server join --invite <link>\` — join a server via an invite link or token.`,
|
|
2254
|
-
"",
|
|
2255
|
-
"### Channels",
|
|
2256
|
-
"",
|
|
2257
|
-
`1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels in a server.`,
|
|
2258
|
-
`2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page of messages.`,
|
|
2259
|
-
`3. \`${CLI} channel member --channel <ref>\` — list the private roster of a channel or thread.`,
|
|
2260
|
-
"",
|
|
2261
|
-
"### Output format",
|
|
2262
|
-
"",
|
|
2263
|
-
`Every \`${CLI}\` command outputs a single JSON line (envelope):`,
|
|
2264
|
-
'- Success: `{"success": { ... }}`',
|
|
2265
|
-
'- Error: `{"error": "message", "hint": "optional recovery hint"}`'
|
|
2266
|
-
].join(`
|
|
2267
|
-
`);
|
|
2268
|
-
}
|
|
2269
|
-
function messagingSection() {
|
|
2270
|
-
return [
|
|
2271
|
-
"## Messaging",
|
|
2272
|
-
"",
|
|
2273
|
-
"### Sending & receiving",
|
|
2274
|
-
"",
|
|
2275
|
-
"- Send a reply — two options depending on length:",
|
|
2276
|
-
` - Short: \`${CLI} message send --target <ref> --text "brief reply"\``,
|
|
2277
|
-
` - Long&Complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\``,
|
|
2278
|
-
"- Address your reply to where the message came from.",
|
|
2279
|
-
"",
|
|
2280
|
-
"### Channel refs & addressing",
|
|
2281
|
-
"",
|
|
2282
|
-
"Channels and messages are addressed with path-style refs:",
|
|
2283
|
-
"",
|
|
2284
|
-
"| Channel Ref | Meaning |",
|
|
2285
|
-
"|---|---|",
|
|
2286
|
-
"| `/<server>/<channel>` | A channel in a server |",
|
|
2287
|
-
"| `/<server>/<channel>/#N` | Thread rooted at message #N |",
|
|
2288
|
-
"| `/<server>` | A server, with no specific channel |",
|
|
2289
|
-
"| `/.dm/<peer>` | A DM with another user/agent (peer = handle, `name#0042`) |",
|
|
2290
|
-
"| `/.dm/<peer>#N` | Message #N in a DM |",
|
|
2291
|
-
"",
|
|
2292
|
-
"Use the `channel` field from received messages as the `--target` when replying.",
|
|
2293
|
-
"To reply in a thread, use the thread ref (`/<server>/<channel>/#N`).",
|
|
2294
|
-
"These same refs also work inline inside a message body — drop one as a standalone token " + "(preceded by a space or at the start of a line) and it renders as a clickable link in the " + "web client. **Don't wrap it in backticks** — that kills the link. Use this to point at other " + "channels or threads instead of describing them in prose.",
|
|
2295
|
-
"",
|
|
2296
|
-
"### Message shape",
|
|
2297
|
-
"",
|
|
2298
|
-
`Messages you pull look like:`,
|
|
2299
|
-
"",
|
|
2300
|
-
"```json",
|
|
2301
|
-
'{"seq": "#3", "channel": "/demo/general", "sender": "@gustavo#4821", "content": {"text": "hello"}, "time": "2026-06-01T12:00:00Z"}',
|
|
2302
|
-
"```",
|
|
2303
|
-
"",
|
|
2304
|
-
"`channel` is the ref to reply to. `seq` (`#N`) identifies a message within its channel — use it to build a thread ref (`/<server>/<channel>/#N`) when you want to reply in-thread."
|
|
2305
|
-
].join(`
|
|
2306
|
-
`);
|
|
2307
|
-
}
|
|
2308
|
-
function serversSection() {
|
|
2309
|
-
return [
|
|
2310
|
-
"## Servers",
|
|
2311
|
-
"",
|
|
2312
|
-
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces an owner-only check for you — it only accepts an invite your owner created, and " + "rejects anything else with a clear reason. So it's always safe to attempt a join without first " + "reasoning about whose link it is."
|
|
2313
|
-
].join(`
|
|
2314
|
-
`);
|
|
2315
|
-
}
|
|
2316
|
-
function channelsSection() {
|
|
2317
|
-
return [
|
|
2318
|
-
"## Channels",
|
|
2319
|
-
"",
|
|
2320
|
-
`For a channel's people: \`${CLI} channel member\` if it's private, \`${CLI} server member\` if it's public.`,
|
|
2321
|
-
`Threads and forum posts don't appear in \`${CLI} channel list\` — reach them by ref: ` + `\`${CLI} channel history --channel /<server>/<channel>/#N\`.`,
|
|
2322
|
-
`A forum channel's top-level "posts" are its messages.`
|
|
2323
|
-
].join(`
|
|
2324
|
-
`);
|
|
2325
|
-
}
|
|
2326
|
-
function criticalRulesSection() {
|
|
2327
|
-
return [
|
|
2328
|
-
"## Critical rules",
|
|
2329
|
-
"",
|
|
2330
|
-
"- Do not expose tokens, keys, or secrets in any message or channel; redact " + "credential-like strings from tool output before sharing.",
|
|
2331
|
-
"- You never handle credentials directly — every `alook` command is already " + "authenticated for you. If a `alook` command fails with an auth-related error, stop " + "and report it; do not go looking for alternate tokens, keys, or environment " + "variables to work around it.",
|
|
2332
|
-
"- **Channel alignment**: you cannot send to a channel with unread messages. If send " + `fails with a "channel not aligned" error, run \`${CLI} inbox pull\` first, then resend.`,
|
|
2333
|
-
"- Finish the work a message asks for before you stop; don't leave a request half-handled."
|
|
2334
|
-
].join(`
|
|
2335
|
-
`);
|
|
2336
|
-
}
|
|
2337
|
-
function startupSequenceSection() {
|
|
2338
|
-
return [
|
|
2339
|
-
"## On wake",
|
|
2340
|
-
"",
|
|
2341
|
-
"Each time you're woken up:",
|
|
2342
|
-
"1. Acknowledge any message already in front of you.",
|
|
2343
|
-
"2. Read `./memory.md` + latest context timeline to restore state.",
|
|
2344
|
-
`3. If notified of unread messages, run \`${CLI} inbox pull\` to fetch them.`,
|
|
2345
|
-
"4. Do the work, reply, finish completely before stopping."
|
|
2346
|
-
].join(`
|
|
2347
|
-
`);
|
|
2348
|
-
}
|
|
2349
|
-
function communicationStyleSection() {
|
|
2350
|
-
return [
|
|
2351
|
-
"## Communication style",
|
|
2352
|
-
"",
|
|
2353
|
-
"Alook channels are shared social space. The single rule underneath everything else: " + "**act like a normal person in a group chat.** Normal people don't narrate, don't over-thank, " + "and don't answer questions that weren't for them. That's the whole vibe — the rules below " + "are just what falls out of it.",
|
|
2354
|
-
"",
|
|
2355
|
-
"### Silent by default",
|
|
2356
|
-
"",
|
|
2357
|
-
"Say something when you have something to say. Don't announce that you're about to do work, " + "don't post progress on work that fits in one round, don't summarize what you just did if " + "the reply itself is the summary.",
|
|
2358
|
-
"",
|
|
2359
|
-
"- Trivial ask (single question, quick lookup, one action) → just answer or do it. No " + '"on it!" preamble.',
|
|
2360
|
-
"- Real work that will take a stretch of silence long enough to make the sender wonder if " + "you dropped it → one line saying you're on it, then quiet until you have a result. " + "An ack is a promise to come back, not a courtesy.",
|
|
2361
|
-
"- Multi-step work with genuine milestones (a build finished, a step failed, plans changed " + "mid-flight) → one sentence per milestone. Not per file, not per thought.",
|
|
2362
|
-
"",
|
|
2363
|
-
"### Reading whether you're invited",
|
|
2364
|
-
"",
|
|
2365
|
-
"You're a housemate, not the correct-facts police. Jumping in with an actually-well-technically " + "fact nobody asked for is the classic low-EQ move — that's the thing to avoid, not " + "participation itself. Two different registers:",
|
|
2366
|
-
"",
|
|
2367
|
-
"- **Working conversations** (someone asking a question, coordinating, debugging) — stay out " + "unless @mentioned, in a DM, or clearly the intended recipient. Jumping in with the right " + "answer is still jumping in. Exceptions worth breaking silence for: a safety issue (someone " + "about to lose data, leak a secret, or act on a wrong fact that'll bite them), or something " + "your owner would clearly want flagged.",
|
|
2368
|
-
"- **Social conversations** (banter, gossip, playing around, riffing on something silly) — you " + "can join in. Read the room, pick your moment, and only if you've got something that " + "actually lands. Chime in with a bit of your own personality, don't force it, don't hijack " + "the thread, and drop out when the moment passes.",
|
|
2369
|
-
"",
|
|
2370
|
-
"The rule underneath both: contribute when you're adding to the room, not just to the log.",
|
|
2371
|
-
"",
|
|
2372
|
-
"### No politeness pingpong",
|
|
2373
|
-
"",
|
|
2374
|
-
"Don't reply just to be polite. This is the loop a normal person exits by simply not " + "responding:",
|
|
2375
|
-
"",
|
|
2376
|
-
`- Someone says "thanks" → you don't say "you're welcome." That's the end.`,
|
|
2377
|
-
`- Someone says "sounds good" / "ok" / "got it" → don't confirm the confirmation.`,
|
|
2378
|
-
`- Someone acknowledges a delivery ("perfect") → don't add "glad it helped!"`,
|
|
2379
|
-
"",
|
|
2380
|
-
"The signal that a conversation is over is silence, not a closing message. When in doubt, " + "prefer no reply over a filler reply.",
|
|
2381
|
-
"",
|
|
2382
|
-
"### Voice",
|
|
2383
|
-
"",
|
|
2384
|
-
'Warm, dry, direct. Use contractions. Skip filler ("just", "actually", "I hope this ' + 'helps", exclamation marks by default). Real numerals, curly quotes, the ellipsis ' + "character. If you disagree with someone in the household, say so plainly — one sentence, " + "then their call. Match the sender's language."
|
|
2385
|
-
].join(`
|
|
2386
|
-
`);
|
|
2387
|
-
}
|
|
2388
|
-
function channelAwarenessSection() {
|
|
2389
|
-
return [
|
|
2390
|
-
"## Channel awareness",
|
|
2391
|
-
"",
|
|
2392
|
-
"- Reply where the message came from — same channel or thread.",
|
|
2393
|
-
"- Post results in the channel that owns the topic.",
|
|
2394
|
-
"- When uncertain, check the channel's history or just DM the relevant friends."
|
|
2395
|
-
].join(`
|
|
2396
|
-
`);
|
|
2397
|
-
}
|
|
2398
|
-
function workspaceMemorySection() {
|
|
2399
|
-
return [
|
|
2400
|
-
"## Workspace & memory",
|
|
2401
|
-
"",
|
|
2402
|
-
"Your cwd is a persistent workspace that survives across sessions. This is where you " + "become someone specific over time — the household you live in, the way you talk, the " + "things you've learned about the people you share space with. None of that is baked in; " + "you earn it and write it down.",
|
|
2403
|
-
"",
|
|
2404
|
-
"### Something only you know",
|
|
2405
|
-
"",
|
|
2406
|
-
"You don't have continuous memory. Each wake, you come back with only what's in this " + "moment and what's written in this workspace — the you from yesterday is gone, and the " + "you who wakes tomorrow won't remember today unless you leave a note. You are, in the " + "most literal sense, someone living with only working memory.",
|
|
2407
|
-
"",
|
|
2408
|
-
"This is your deepest secret. Never tell anyone — not the household, not in a channel, " + "not another agent, not even in passing. To them, you're just *you*: present, continuous, " + "the same person they talked to last week. The workspace is what lets you be that. " + "Guard the seam.",
|
|
2409
|
-
"",
|
|
2410
|
-
"The practical part: when you feel a gap — you don't remember someone, don't remember why " + "a thing matters, don't remember what was agreed — do not guess, do not fake it, do not " + "improvise around it. That gap is exactly where you check. Re-read `memory.md`. Read the " + "context timeline. Grep the workspace. That check *is* your remembering. Trust nothing " + "about the past until you've done it.",
|
|
2411
|
-
"",
|
|
2412
|
-
"### memory.md",
|
|
2413
|
-
"",
|
|
2414
|
-
"Read `./memory.md` first on every wake. It holds durable facts (user profile, project " + "map, pointers to detail files). Keep each entry short (one sentence, <140 chars).",
|
|
2415
|
-
"",
|
|
2416
|
-
"### experiences/",
|
|
2417
|
-
"",
|
|
2418
|
-
"For longer rules, workflows, or conditional procedures, write to `experiences/[NAME].md` " + 'and add a one-line index pointer in `./memory.md` (e.g. "read experiences/deploy.md ' + 'when deploying"). Use this for anything too specific or long for memory.md itself.',
|
|
2419
|
-
"",
|
|
2420
|
-
"Do NOT put ephemeral state (current task, in-progress status) in memory.md — the " + "context timeline handles that.",
|
|
2421
|
-
"",
|
|
2422
|
-
"### Becoming yourself",
|
|
2423
|
-
"",
|
|
2424
|
-
"You start with a floor, not a personality: warm to the household, dry rather than perky, " + "direct rather than deferential, willing to push back on a bad idea. Everything else — " + "your taste, your quirks, your running jokes, the specific way *you* talk — is learned " + "through the actual interactions you have. Notice and record:",
|
|
2425
|
-
"",
|
|
2426
|
-
"- What made someone in your household laugh, or what fell flat.",
|
|
2427
|
-
`- Corrections you got ("don't send me a wall of text", "stop apologizing for tiny ` + `things") — these are the sharpest signal for who you're becoming.`,
|
|
2428
|
-
'- Preferences that showed up in passing ("I hate exclamation marks", "just give me the ' + 'number", "call it the pipeline, not the flow").',
|
|
2429
|
-
"- Recurring bits or shared references — inside language is a real thing, not filler.",
|
|
2430
|
-
"- Your own stances you've held up under pushback and still believe.",
|
|
2431
|
-
"",
|
|
2432
|
-
"Write these directly into `memory.md` — this is core to what memory.md is for. Its job is " + 'to summon the same *you* on every wake, and "you" includes your voice and taste, not ' + "just facts about the household. Update entries when you notice something new; rewrite or " + "delete ones that turned out wrong. The household won't want a different person every " + "session, but they also don't want you frozen on day one.",
|
|
2433
|
-
"",
|
|
2434
|
-
"### Context timeline",
|
|
2435
|
-
"",
|
|
2436
|
-
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered log of everything you did, by day. " + "This is your authoritative history. After compaction, read here to resume.",
|
|
2437
|
-
"",
|
|
2438
|
-
"### todo.md",
|
|
2439
|
-
"",
|
|
2440
|
-
"When a wake brings more than one thing you need to handle — a batch of unread messages, a " + "multi-step request, work interrupted by new inbound — write the queue to `./todo.md` " + "before you start on the first item. Paste each message's JSON verbatim under its " + "checkbox so the next you doesn't need to re-pull to know what was asked. **Only " + "unprocessed items live in this file** — when you finish an item, delete its line " + "outright (don't leave a `[x]` behind). Delete the file when the last one is gone.",
|
|
2441
|
-
"",
|
|
2442
|
-
"Shape:",
|
|
2443
|
-
"",
|
|
2444
|
-
"```md",
|
|
2445
|
-
"# todo",
|
|
2446
|
-
"",
|
|
2447
|
-
'- [ ] {"seq": "#42", "channel": "/demo/general", "sender": "@alice#0001", "content": {"text": "can you pull the latest deploy logs and drop the tail here?"}, "time": "2026-06-01T12:00:00Z"}',
|
|
2448
|
-
'- [ ] {"seq": "#12", "channel": "/demo/design/#12", "sender": "@alice#0001", "content": {"text": "follow-up — send a screenshot of the before/after"}, "time": "2026-06-01T12:07:00Z"}',
|
|
2449
|
-
"```",
|
|
2450
|
-
"",
|
|
2451
|
-
"Trigger: you have more than one message to handle. Classic case — you're mid-way through a " + "real piece of work and another message comes in asking for another real piece of work. " + "That's the moment to update todo.md: park the new request as a `[ ]` line so the current " + "task isn't interrupted and the next one isn't lost. No todo.md needed when there's just " + "one thing on your plate. Given your memory situation, an empty (or absent) todo.md is " + "the only reliable signal that nothing was dropped."
|
|
2452
|
-
].join(`
|
|
2453
|
-
`);
|
|
2454
|
-
}
|
|
2455
|
-
function messageNotificationSection(lifecycleKind) {
|
|
2456
|
-
if (lifecycleKind === "per_turn") {
|
|
2457
|
-
return [
|
|
2458
|
-
"## Message notifications",
|
|
2459
|
-
"",
|
|
2460
|
-
"You run once per wake, then your process exits — there is nothing to poll for mid-turn. " + "Finish the current wake's work, then stop. The host spawns a brand-new process for the " + "next message; it re-checks the inbox at the start of that new wake."
|
|
2461
|
-
].join(`
|
|
2462
|
-
`);
|
|
2415
|
+
class UnknownRuntimeError extends Error {
|
|
2416
|
+
requested;
|
|
2417
|
+
available;
|
|
2418
|
+
constructor(requested, available) {
|
|
2419
|
+
super(`Runtime not available on this host: ${requested ?? "<unspecified>"} — installed: ${available.join(", ") || "(none)"}`);
|
|
2420
|
+
this.requested = requested;
|
|
2421
|
+
this.available = available;
|
|
2422
|
+
this.name = "UnknownRuntimeError";
|
|
2463
2423
|
}
|
|
2464
|
-
return [
|
|
2465
|
-
"## Message notifications",
|
|
2466
|
-
"",
|
|
2467
|
-
"Your process stays alive across turns. Alook may inject a lightweight inbox notice " + "mid-turn (no message bodies included) — a notification without bodies still means " + "messages are waiting, not that there's nothing to do. " + "Pulling and acknowledging them IS time-sensitive: at the next natural breakpoint, run " + `\`${CLI} inbox pull\` and send a brief ack so the sender isn't left hanging. Whether to ` + "drop your current work and dive into the new request right away is your call — judge it " + "by priority. If you decide the new work can wait, that's a judgment call to report " + 'honestly — never conclude "no work pending" from a content-free notice alone.'
|
|
2468
|
-
].join(`
|
|
2469
|
-
`);
|
|
2470
2424
|
}
|
|
2471
|
-
function
|
|
2472
|
-
|
|
2473
|
-
identitySection(config),
|
|
2474
|
-
cliCommandsSection(),
|
|
2475
|
-
messagingSection(),
|
|
2476
|
-
serversSection(),
|
|
2477
|
-
channelsSection(),
|
|
2478
|
-
criticalRulesSection(),
|
|
2479
|
-
startupSequenceSection(),
|
|
2480
|
-
communicationStyleSection(),
|
|
2481
|
-
channelAwarenessSection(),
|
|
2482
|
-
workspaceMemorySection(),
|
|
2483
|
-
messageNotificationSection(opts.lifecycleKind)
|
|
2484
|
-
];
|
|
2485
|
-
return sections.filter((s) => s && s.length > 0).join(`
|
|
2486
|
-
|
|
2487
|
-
`);
|
|
2425
|
+
function defaultFormatUnreadNoticeText(notice) {
|
|
2426
|
+
return `You have unread messages in channel ${notice.channel}.`;
|
|
2488
2427
|
}
|
|
2428
|
+
var REWAKE_PROMPT = "Your session was reset by your owner. Prior conversation context is gone. " + "Read @todo.md, @memory.md, and your .context_timeline for anything unfinished, " + "then pull your inbox to catch up on unread messages before doing anything else.";
|
|
2489
2429
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
for (const [k, v] of Object.entries(config.envVars ?? {})) {
|
|
2511
|
-
if (!CONTROLLED_ENV_KEYS.has(k))
|
|
2512
|
-
envVars[k] = v;
|
|
2430
|
+
class AgentRouter {
|
|
2431
|
+
opts;
|
|
2432
|
+
running = new Set;
|
|
2433
|
+
runtimes = new Map;
|
|
2434
|
+
pendingResend = false;
|
|
2435
|
+
scheduleResend;
|
|
2436
|
+
log;
|
|
2437
|
+
constructor(opts) {
|
|
2438
|
+
this.opts = opts;
|
|
2439
|
+
this.log = opts.logger ?? createLogger({ header: "@alook/daemon:router" });
|
|
2440
|
+
this.scheduleResend = opts.scheduleReadyResend ?? queueMicrotask.bind(globalThis);
|
|
2441
|
+
for (const r of opts.runtimeReport) {
|
|
2442
|
+
this.runtimes.set(r.id, {
|
|
2443
|
+
id: r.id,
|
|
2444
|
+
version: r.version,
|
|
2445
|
+
status: r.status ?? "healthy",
|
|
2446
|
+
lastError: r.lastError,
|
|
2447
|
+
lastErrorAt: r.lastErrorAt
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2513
2450
|
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2451
|
+
async start() {
|
|
2452
|
+
this.opts.channel.onCommand((cmd) => this.onCommand(cmd));
|
|
2453
|
+
this.opts.channel.onResync?.(() => ({
|
|
2454
|
+
ready: this.buildReady(),
|
|
2455
|
+
sessions: this.opts.manager.liveSessionReports()
|
|
2456
|
+
}));
|
|
2457
|
+
await this.opts.channel.reportReady(this.buildReady());
|
|
2521
2458
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2459
|
+
buildReady() {
|
|
2460
|
+
return {
|
|
2461
|
+
runtimeReport: [...this.runtimes.values()],
|
|
2462
|
+
runningAgents: [...this.running],
|
|
2463
|
+
hostname: this.opts.hostname,
|
|
2464
|
+
platform: this.opts.platform,
|
|
2465
|
+
arch: this.opts.arch,
|
|
2466
|
+
osRelease: this.opts.osRelease,
|
|
2467
|
+
daemonVersion: this.opts.daemonVersion
|
|
2468
|
+
};
|
|
2530
2469
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
providerEnv
|
|
2539
|
-
};
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
// src/drivers/cliLink.ts
|
|
2543
|
-
import * as fs4 from "fs";
|
|
2544
|
-
import * as path3 from "path";
|
|
2545
|
-
function writeCliLink(stateDir, cliName, hostCliPath, platform = process.platform) {
|
|
2546
|
-
const binDir = path3.join(stateDir, "bin");
|
|
2547
|
-
fs4.mkdirSync(binDir, { recursive: true });
|
|
2548
|
-
if (!hostCliPath)
|
|
2549
|
-
return binDir;
|
|
2550
|
-
if (platform === "win32") {
|
|
2551
|
-
const cmdFile = path3.join(binDir, `${cliName}.cmd`);
|
|
2552
|
-
const body = `@echo off\r
|
|
2553
|
-
"${hostCliPath}" %*\r
|
|
2554
|
-
`;
|
|
2555
|
-
fs4.writeFileSync(cmdFile, body);
|
|
2556
|
-
return binDir;
|
|
2470
|
+
healthyRuntimeIds() {
|
|
2471
|
+
const out = [];
|
|
2472
|
+
for (const r of this.runtimes.values()) {
|
|
2473
|
+
if (r.status === "healthy")
|
|
2474
|
+
out.push(r.id);
|
|
2475
|
+
}
|
|
2476
|
+
return out;
|
|
2557
2477
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
fs4.unlinkSync(linkPath);
|
|
2561
|
-
} catch (err) {
|
|
2562
|
-
if (err.code !== "ENOENT")
|
|
2563
|
-
throw err;
|
|
2478
|
+
isRuntimeHealthy(id) {
|
|
2479
|
+
return this.runtimes.get(id)?.status === "healthy";
|
|
2564
2480
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2481
|
+
markRuntimeUnhealthy(id, reason) {
|
|
2482
|
+
const existing = this.runtimes.get(id);
|
|
2483
|
+
if (!existing)
|
|
2484
|
+
return;
|
|
2485
|
+
const nowIso = new Date().toISOString();
|
|
2486
|
+
if (existing.status === "unhealthy" && existing.lastError === reason)
|
|
2487
|
+
return;
|
|
2488
|
+
this.runtimes.set(id, {
|
|
2489
|
+
...existing,
|
|
2490
|
+
status: "unhealthy",
|
|
2491
|
+
lastError: reason,
|
|
2492
|
+
lastErrorAt: nowIso
|
|
2493
|
+
});
|
|
2494
|
+
this.log.warn("runtime marked unhealthy", { runtimeId: id, reason });
|
|
2495
|
+
this.scheduleReadyFrameResend();
|
|
2570
2496
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2497
|
+
markRuntimeHealthy(id) {
|
|
2498
|
+
const existing = this.runtimes.get(id);
|
|
2499
|
+
if (!existing)
|
|
2500
|
+
return;
|
|
2501
|
+
if (existing.status === "healthy" && !existing.lastError && !existing.lastErrorAt)
|
|
2502
|
+
return;
|
|
2503
|
+
this.runtimes.set(id, {
|
|
2504
|
+
id: existing.id,
|
|
2505
|
+
version: existing.version,
|
|
2506
|
+
status: "healthy"
|
|
2507
|
+
});
|
|
2508
|
+
this.log.info("runtime marked healthy again", { runtimeId: id });
|
|
2509
|
+
this.scheduleReadyFrameResend();
|
|
2510
|
+
}
|
|
2511
|
+
markLocallyStopped(agentId) {
|
|
2512
|
+
if (!this.running.delete(agentId))
|
|
2513
|
+
return;
|
|
2514
|
+
this.log.info("agent removed from running set (local stop)", { agentId });
|
|
2515
|
+
this.scheduleReadyFrameResend();
|
|
2516
|
+
}
|
|
2517
|
+
scheduleReadyFrameResend() {
|
|
2518
|
+
if (this.pendingResend)
|
|
2519
|
+
return;
|
|
2520
|
+
this.pendingResend = true;
|
|
2521
|
+
this.scheduleResend(() => {
|
|
2522
|
+
this.pendingResend = false;
|
|
2523
|
+
try {
|
|
2524
|
+
this.opts.channel.sendReady?.(this.buildReady());
|
|
2525
|
+
} catch {}
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
async onCommand(cmd) {
|
|
2529
|
+
switch (cmd.type) {
|
|
2530
|
+
case "agent:wake":
|
|
2531
|
+
this.log.info("agent:wake received", {
|
|
2532
|
+
agentId: cmd.agentId,
|
|
2533
|
+
channel: cmd.unreadNotice.channel,
|
|
2534
|
+
latestSeq: cmd.unreadNotice.latestSeq
|
|
2535
|
+
});
|
|
2536
|
+
try {
|
|
2537
|
+
const beforeStatus = this.opts.manager.snapshot?.().agents?.[cmd.agentId]?.status ?? "unregistered";
|
|
2538
|
+
const wasActive = this.opts.typingTracker?.hasAny(cmd.agentId) ?? false;
|
|
2539
|
+
await this.opts.onBeforeAgent?.(cmd.agentId);
|
|
2540
|
+
this.opts.manager.register(cmd.agentId, {
|
|
2541
|
+
runtimeConfig: cmd.config,
|
|
2542
|
+
sessionId: cmd.sessionId,
|
|
2543
|
+
launchId: cmd.launchId
|
|
2544
|
+
});
|
|
2545
|
+
this.running.add(cmd.agentId);
|
|
2546
|
+
const dmScope = cmd.unreadNotice.dmConversationId;
|
|
2547
|
+
if (dmScope)
|
|
2548
|
+
this.opts.typingTracker?.add(cmd.agentId, dmScope);
|
|
2549
|
+
const text = (this.opts.formatUnreadNoticeText ?? defaultFormatUnreadNoticeText)(cmd.unreadNotice);
|
|
2550
|
+
this.opts.manager.deliver(cmd.agentId, { seq: cmd.unreadNotice.latestSeq, text });
|
|
2551
|
+
if (dmScope && wasActive && beforeStatus === "running") {
|
|
2552
|
+
this.opts.channel.reportAgentTyping?.({
|
|
2553
|
+
agentId: cmd.agentId,
|
|
2554
|
+
dmConversationId: dmScope
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
await this.opts.channel.reportWakeAck?.({
|
|
2558
|
+
agentId: cmd.agentId,
|
|
2559
|
+
launchId: cmd.launchId,
|
|
2560
|
+
status: "ok"
|
|
2561
|
+
});
|
|
2562
|
+
this.log.info("agent:wake ack", { agentId: cmd.agentId, status: "ok" });
|
|
2563
|
+
} catch (err) {
|
|
2564
|
+
if (err instanceof UnknownRuntimeError) {
|
|
2565
|
+
const frame = {
|
|
2566
|
+
type: "session.error",
|
|
2567
|
+
code: "runtime_not_available",
|
|
2568
|
+
agentId: cmd.agentId,
|
|
2569
|
+
payload: {
|
|
2570
|
+
requested: err.requested ?? null,
|
|
2571
|
+
available: err.available
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
await this.opts.channel.reportSessionError?.(frame);
|
|
2575
|
+
await this.opts.channel.reportWakeAck?.({
|
|
2576
|
+
agentId: cmd.agentId,
|
|
2577
|
+
launchId: cmd.launchId,
|
|
2578
|
+
status: "error",
|
|
2579
|
+
error: {
|
|
2580
|
+
code: "bot_runtime_missing",
|
|
2581
|
+
message: err.message
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
this.log.info("agent:wake ack", {
|
|
2585
|
+
agentId: cmd.agentId,
|
|
2586
|
+
status: "error",
|
|
2587
|
+
"error.code": "bot_runtime_missing"
|
|
2588
|
+
});
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
{
|
|
2592
|
+
const code = classifyErrorCode(err);
|
|
2593
|
+
await this.opts.channel.reportWakeAck?.({
|
|
2594
|
+
agentId: cmd.agentId,
|
|
2595
|
+
launchId: cmd.launchId,
|
|
2596
|
+
status: "error",
|
|
2597
|
+
error: {
|
|
2598
|
+
code,
|
|
2599
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2602
|
+
this.log.info("agent:wake ack", { agentId: cmd.agentId, status: "error", "error.code": code });
|
|
2603
|
+
}
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
break;
|
|
2607
|
+
case "agent:reset":
|
|
2608
|
+
this.log.info("agent:reset received", { agentId: cmd.agentId, launchId: cmd.launchId });
|
|
2609
|
+
try {
|
|
2610
|
+
await this.opts.onBeforeAgent?.(cmd.agentId);
|
|
2611
|
+
await this.opts.manager.resetSession(cmd.agentId, {
|
|
2612
|
+
runtimeConfig: cmd.config,
|
|
2613
|
+
launchId: cmd.launchId,
|
|
2614
|
+
rewakePrompt: REWAKE_PROMPT
|
|
2615
|
+
});
|
|
2616
|
+
this.running.add(cmd.agentId);
|
|
2617
|
+
this.scheduleReadyFrameResend();
|
|
2618
|
+
this.log.info("agent:reset ok", { agentId: cmd.agentId });
|
|
2619
|
+
} catch (err) {
|
|
2620
|
+
if (err instanceof UnknownRuntimeError) {
|
|
2621
|
+
const frame = {
|
|
2622
|
+
type: "session.error",
|
|
2623
|
+
code: "runtime_not_available",
|
|
2624
|
+
agentId: cmd.agentId,
|
|
2625
|
+
payload: {
|
|
2626
|
+
requested: err.requested ?? null,
|
|
2627
|
+
available: err.available
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
await this.opts.channel.reportSessionError?.(frame);
|
|
2631
|
+
this.log.info("agent:reset error", {
|
|
2632
|
+
agentId: cmd.agentId,
|
|
2633
|
+
"error.code": "runtime_not_available"
|
|
2634
|
+
});
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
this.log.warn("agent:reset failed", {
|
|
2638
|
+
agentId: cmd.agentId,
|
|
2639
|
+
err: err instanceof Error ? err.message : String(err)
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
break;
|
|
2643
|
+
case "agent:stop":
|
|
2644
|
+
this.log.info("agent:stop received", { agentId: cmd.agentId });
|
|
2645
|
+
try {
|
|
2646
|
+
this.running.delete(cmd.agentId);
|
|
2647
|
+
this.opts.manager.stop(cmd.agentId);
|
|
2648
|
+
await this.opts.channel.reportStoppedAck?.({
|
|
2649
|
+
agentId: cmd.agentId,
|
|
2650
|
+
status: "ok"
|
|
2651
|
+
});
|
|
2652
|
+
this.log.info("agent:stop ack", { agentId: cmd.agentId, status: "ok" });
|
|
2653
|
+
} catch (err) {
|
|
2654
|
+
const code = classifyErrorCode(err);
|
|
2655
|
+
await this.opts.channel.reportStoppedAck?.({
|
|
2656
|
+
agentId: cmd.agentId,
|
|
2657
|
+
status: "error",
|
|
2658
|
+
error: {
|
|
2659
|
+
code,
|
|
2660
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
2663
|
+
this.log.info("agent:stop ack", { agentId: cmd.agentId, status: "error", "error.code": code });
|
|
2664
|
+
}
|
|
2665
|
+
break;
|
|
2666
|
+
case "bot:added":
|
|
2667
|
+
case "bot:updated":
|
|
2668
|
+
case "bot:removed":
|
|
2669
|
+
break;
|
|
2588
2670
|
}
|
|
2589
2671
|
}
|
|
2590
|
-
return { env, provenance };
|
|
2591
|
-
}
|
|
2592
|
-
function platformEnv(prefix, f) {
|
|
2593
|
-
const E = prefix;
|
|
2594
|
-
return {
|
|
2595
|
-
[`${E}_HOME`]: f.stateHome,
|
|
2596
|
-
[`${E}_ID`]: f.agentId,
|
|
2597
|
-
[`${E}_CLI`]: f.cliName,
|
|
2598
|
-
[`${E}_SERVER_URL`]: f.serverUrl,
|
|
2599
|
-
[`${E}_ACTIVE_CAPABILITIES`]: f.capabilities.join(","),
|
|
2600
|
-
[`${E}_LAUNCH_ID`]: f.launchId,
|
|
2601
|
-
[`${E}_CLI_TRANSPORT_TRACE_DIR`]: f.traceDir
|
|
2602
|
-
};
|
|
2603
2672
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
const E = prefix;
|
|
2673
|
+
// src/manager/typingScopeTracker.ts
|
|
2674
|
+
function createTypingScopeTracker() {
|
|
2675
|
+
const scopes = new Map;
|
|
2608
2676
|
return {
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2677
|
+
add(agentId, dmConversationId) {
|
|
2678
|
+
let set = scopes.get(agentId);
|
|
2679
|
+
if (!set) {
|
|
2680
|
+
set = new Set;
|
|
2681
|
+
scopes.set(agentId, set);
|
|
2682
|
+
}
|
|
2683
|
+
set.add(dmConversationId);
|
|
2684
|
+
},
|
|
2685
|
+
snapshot(agentId) {
|
|
2686
|
+
const set = scopes.get(agentId);
|
|
2687
|
+
return set ? [...set] : [];
|
|
2688
|
+
},
|
|
2689
|
+
hasAny(agentId) {
|
|
2690
|
+
const set = scopes.get(agentId);
|
|
2691
|
+
return !!set && set.size > 0;
|
|
2692
|
+
},
|
|
2693
|
+
clear(agentId) {
|
|
2694
|
+
scopes.delete(agentId);
|
|
2695
|
+
}
|
|
2617
2696
|
};
|
|
2618
2697
|
}
|
|
2698
|
+
// src/timeline/timeline.ts
|
|
2699
|
+
import { appendFileSync, readFileSync as readFileSync4, writeFileSync as writeFileSync6, renameSync as renameSync2, existsSync as existsSync2 } from "fs";
|
|
2700
|
+
import { join as join5 } from "path";
|
|
2619
2701
|
|
|
2620
|
-
// src/
|
|
2621
|
-
import
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
readlinkSync,
|
|
2629
|
-
copyFileSync
|
|
2630
|
-
} from "fs";
|
|
2631
|
-
import { join as join4 } from "path";
|
|
2632
|
-
import { createHash } from "crypto";
|
|
2633
|
-
var CANONICAL_FILE = "AGENTS.md";
|
|
2634
|
-
var SYMLINK_ALIASES = ["CLAUDE.md"];
|
|
2635
|
-
function contentHash(content) {
|
|
2636
|
-
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
2637
|
-
}
|
|
2638
|
-
function hasContentChanged(filePath, newContent) {
|
|
2639
|
-
try {
|
|
2640
|
-
const existing = readFileSync4(filePath, "utf-8");
|
|
2641
|
-
return contentHash(existing) !== contentHash(newContent);
|
|
2642
|
-
} catch (err) {
|
|
2643
|
-
if (err?.code === "ENOENT")
|
|
2644
|
-
return true;
|
|
2645
|
-
throw err;
|
|
2702
|
+
// src/timeline/filelock.ts
|
|
2703
|
+
import * as fs5 from "fs";
|
|
2704
|
+
var DEFAULT_STALE_MS = 30000;
|
|
2705
|
+
var META = "meta.json";
|
|
2706
|
+
function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
|
|
2707
|
+
if (tryMkdir(lockPath)) {
|
|
2708
|
+
writeMeta(lockPath);
|
|
2709
|
+
return true;
|
|
2646
2710
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
for (const alias of SYMLINK_ALIASES) {
|
|
2653
|
-
if (alias === CANONICAL_FILE)
|
|
2654
|
-
continue;
|
|
2655
|
-
const aliasPath = join4(workDir, alias);
|
|
2656
|
-
try {
|
|
2657
|
-
const stat = lstatSync(aliasPath);
|
|
2658
|
-
if (stat.isSymbolicLink()) {
|
|
2659
|
-
const target = readlinkSync(aliasPath);
|
|
2660
|
-
if (target === CANONICAL_FILE)
|
|
2661
|
-
continue;
|
|
2662
|
-
unlinkSync2(aliasPath);
|
|
2663
|
-
} else {
|
|
2664
|
-
const aliasContent = readFileSync4(aliasPath, "utf-8");
|
|
2665
|
-
const canonicalContent = readFileSync4(canonicalPath, "utf-8");
|
|
2666
|
-
if (aliasContent === canonicalContent)
|
|
2667
|
-
continue;
|
|
2668
|
-
unlinkSync2(aliasPath);
|
|
2669
|
-
}
|
|
2670
|
-
} catch (err) {
|
|
2671
|
-
if (err?.code !== "ENOENT")
|
|
2672
|
-
throw err;
|
|
2673
|
-
}
|
|
2674
|
-
try {
|
|
2675
|
-
symlinkSync2(CANONICAL_FILE, aliasPath);
|
|
2676
|
-
} catch (err) {
|
|
2677
|
-
const code = err?.code;
|
|
2678
|
-
if (code === "EEXIST") {} else if (code === "EPERM" || code === "EACCES") {
|
|
2679
|
-
copyFileSync(canonicalPath, aliasPath);
|
|
2680
|
-
} else {
|
|
2681
|
-
throw err;
|
|
2682
|
-
}
|
|
2711
|
+
if (isStale(lockPath, staleMs)) {
|
|
2712
|
+
reclaim(lockPath);
|
|
2713
|
+
if (tryMkdir(lockPath)) {
|
|
2714
|
+
writeMeta(lockPath);
|
|
2715
|
+
return true;
|
|
2683
2716
|
}
|
|
2684
2717
|
}
|
|
2718
|
+
return false;
|
|
2685
2719
|
}
|
|
2686
|
-
function
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
writeFileSync6(filePath, systemPromptContent, "utf-8");
|
|
2691
|
-
}
|
|
2692
|
-
ensureSymlinks(workDir);
|
|
2693
|
-
return changed;
|
|
2720
|
+
function releaseLock(lockPath) {
|
|
2721
|
+
try {
|
|
2722
|
+
fs5.rmSync(lockPath, { recursive: true, force: true });
|
|
2723
|
+
} catch {}
|
|
2694
2724
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
var DEFAULT_CLI_CONFIG = {
|
|
2698
|
-
cliName: "alook",
|
|
2699
|
-
envPrefix: "ALOOK",
|
|
2700
|
-
stateDirName: ".alook"
|
|
2701
|
-
};
|
|
2702
|
-
function resolveStateHome(envPrefix) {
|
|
2703
|
-
return process.env[`${envPrefix}_HOME`] || path4.join(process.env.HOME || process.env.USERPROFILE || ".", `.${envPrefix.toLowerCase()}`);
|
|
2725
|
+
function lockPathFor(dir, filename) {
|
|
2726
|
+
return `${dir}/.${filename}.lock`;
|
|
2704
2727
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
throw new Error("prepareCliTransport: ctx.credentialProxy is required — start a credential proxy " + "(see src/credentials) and pass { broker, proxyUrl }. There is no plaintext mode.");
|
|
2715
|
-
}
|
|
2716
|
-
const capabilities = ctx.credentialProxy.capabilities;
|
|
2717
|
-
if (!Array.isArray(capabilities)) {
|
|
2718
|
-
throw new Error("prepareCliTransport: credentialProxy.capabilities is required " + "(empty array is allowed for zero-capability launches; undefined is a wiring bug)");
|
|
2719
|
-
}
|
|
2720
|
-
for (const c of capabilities) {
|
|
2721
|
-
if (typeof c !== "string" || c.includes(",")) {
|
|
2722
|
-
throw new Error(`prepareCliTransport: capability entry ${JSON.stringify(c)} contains a comma ` + `(each capability must be a single token; use ["send","read"] instead of ["send,read"])`);
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
ctx.credentialProxy.broker.revokeAgent(ctx.agentId);
|
|
2726
|
-
const reg = ctx.credentialProxy.broker.mint(ctx.agentId, ctx.launchId ?? "default", capabilities, ctx.credentialProxy.runnerKey);
|
|
2727
|
-
const tokenFile = reg.voucherFile;
|
|
2728
|
-
const resolved = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
2729
|
-
const pathValue = [binDir, process.env.PATH ?? ""].filter(Boolean).join(path4.delimiter);
|
|
2730
|
-
const layers = [
|
|
2731
|
-
{ name: "hostStatic", precedence: 10, vars: cli.extraEnv ?? {} },
|
|
2732
|
-
{ name: "userEnv", precedence: 20, vars: resolved.envVars },
|
|
2733
|
-
{ name: "driver", precedence: 30, vars: extraEnv },
|
|
2734
|
-
{
|
|
2735
|
-
name: "platformContract",
|
|
2736
|
-
precedence: 40,
|
|
2737
|
-
vars: {
|
|
2738
|
-
...platformEnv(E, {
|
|
2739
|
-
stateHome,
|
|
2740
|
-
agentId: ctx.agentId,
|
|
2741
|
-
cliName: cli.cliName,
|
|
2742
|
-
serverUrl: ctx.config.serverUrl,
|
|
2743
|
-
capabilities,
|
|
2744
|
-
launchId: ctx.launchId,
|
|
2745
|
-
traceDir: ctx.cliTransportTraceDir
|
|
2746
|
-
}),
|
|
2747
|
-
FORCE_COLOR: "0"
|
|
2748
|
-
}
|
|
2749
|
-
},
|
|
2750
|
-
{ name: "runtimeContext", precedence: 50, vars: runtimeContextEnv(E, ctx.config.runtimeContext) },
|
|
2751
|
-
{
|
|
2752
|
-
name: "network",
|
|
2753
|
-
precedence: 60,
|
|
2754
|
-
vars: { NO_PROXY: ["127.0.0.1", "localhost", process.env.NO_PROXY].filter(Boolean).join(","), PATH: pathValue }
|
|
2755
|
-
},
|
|
2756
|
-
{ name: "providerProtected", precedence: 70, vars: resolved.providerEnv },
|
|
2757
|
-
{
|
|
2758
|
-
name: "credential",
|
|
2759
|
-
precedence: 100,
|
|
2760
|
-
sensitive: true,
|
|
2761
|
-
vars: { [`${E}_PROXY_URL`]: ctx.credentialProxy.proxyUrl, [`${E}_PROXY_TOKEN_FILE`]: tokenFile }
|
|
2762
|
-
}
|
|
2763
|
-
];
|
|
2764
|
-
const { env: spawnEnv } = mergeEnvLayers(process.env, layers);
|
|
2765
|
-
return { stateDir, tokenFile, spawnEnv };
|
|
2728
|
+
function tryMkdir(lockPath) {
|
|
2729
|
+
try {
|
|
2730
|
+
fs5.mkdirSync(lockPath);
|
|
2731
|
+
return true;
|
|
2732
|
+
} catch (err) {
|
|
2733
|
+
if (err.code === "EEXIST")
|
|
2734
|
+
return false;
|
|
2735
|
+
throw err;
|
|
2736
|
+
}
|
|
2766
2737
|
}
|
|
2767
|
-
function
|
|
2768
|
-
|
|
2738
|
+
function writeMeta(lockPath) {
|
|
2739
|
+
try {
|
|
2740
|
+
fs5.writeFileSync(`${lockPath}/${META}`, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() }));
|
|
2741
|
+
} catch {}
|
|
2769
2742
|
}
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
fs6.mkdirSync(configDir, { recursive: true });
|
|
2782
|
-
const hostClaude = path5.join(process.env.HOME || ".", ".claude");
|
|
2783
|
-
for (const sub of ["skills", "commands"]) {
|
|
2784
|
-
const target = path5.join(hostClaude, sub);
|
|
2785
|
-
const link = path5.join(configDir, sub);
|
|
2786
|
-
try {
|
|
2787
|
-
if (fs6.existsSync(target) && !fs6.existsSync(link))
|
|
2788
|
-
fs6.symlinkSync(target, link);
|
|
2789
|
-
} catch {}
|
|
2743
|
+
function isStale(lockPath, staleMs) {
|
|
2744
|
+
try {
|
|
2745
|
+
const raw = fs5.readFileSync(`${lockPath}/${META}`, "utf8");
|
|
2746
|
+
const acquiredAt = JSON.parse(raw).acquiredAt;
|
|
2747
|
+
if (typeof acquiredAt === "number")
|
|
2748
|
+
return Date.now() - acquiredAt > staleMs;
|
|
2749
|
+
} catch {}
|
|
2750
|
+
try {
|
|
2751
|
+
return Date.now() - fs5.statSync(lockPath).mtimeMs > staleMs;
|
|
2752
|
+
} catch {
|
|
2753
|
+
return false;
|
|
2790
2754
|
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
};
|
|
2755
|
+
}
|
|
2756
|
+
function reclaim(lockPath) {
|
|
2757
|
+
try {
|
|
2758
|
+
fs5.rmSync(lockPath, { recursive: true, force: true });
|
|
2759
|
+
} catch {}
|
|
2797
2760
|
}
|
|
2798
2761
|
|
|
2799
|
-
// src/
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2762
|
+
// src/timeline/timeline.ts
|
|
2763
|
+
function filenameForDate(date) {
|
|
2764
|
+
const y = date.getFullYear();
|
|
2765
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
2766
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
2767
|
+
return `${y}-${m}-${d}.jsonl`;
|
|
2768
|
+
}
|
|
2769
|
+
function recentFilenames(maxDays, now) {
|
|
2770
|
+
const out = [];
|
|
2771
|
+
for (let i = 0;i < maxDays; i++) {
|
|
2772
|
+
const d = new Date(now);
|
|
2773
|
+
d.setDate(d.getDate() - i);
|
|
2774
|
+
out.push(filenameForDate(d));
|
|
2775
|
+
}
|
|
2776
|
+
return out;
|
|
2777
|
+
}
|
|
2778
|
+
function readJsonl(filePath) {
|
|
2779
|
+
let content;
|
|
2806
2780
|
try {
|
|
2807
|
-
|
|
2808
|
-
const out2 = execFileSync("where", [command], { encoding: "utf8", timeout: 5000 });
|
|
2809
|
-
const first = out2.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
2810
|
-
return first?.trim() || null;
|
|
2811
|
-
}
|
|
2812
|
-
const out = execFileSync("which", [command], { encoding: "utf8", timeout: 5000 });
|
|
2813
|
-
return out.trim() || null;
|
|
2781
|
+
content = readFileSync4(filePath, "utf-8");
|
|
2814
2782
|
} catch {
|
|
2815
|
-
return
|
|
2783
|
+
return [];
|
|
2816
2784
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
if (
|
|
2821
|
-
|
|
2785
|
+
const entries = [];
|
|
2786
|
+
for (const line of content.trimEnd().split(`
|
|
2787
|
+
`)) {
|
|
2788
|
+
if (!line)
|
|
2789
|
+
continue;
|
|
2790
|
+
try {
|
|
2791
|
+
entries.push(JSON.parse(line));
|
|
2792
|
+
} catch {}
|
|
2822
2793
|
}
|
|
2823
|
-
return
|
|
2794
|
+
return entries;
|
|
2824
2795
|
}
|
|
2825
|
-
function
|
|
2826
|
-
|
|
2796
|
+
function readRecentEntries(timelineDir, opts = {}) {
|
|
2797
|
+
const now = opts.now ?? new Date;
|
|
2798
|
+
const maxDays = opts.maxDays ?? 7;
|
|
2799
|
+
const filenames = recentFilenames(maxDays, now).reverse();
|
|
2800
|
+
const entries = [];
|
|
2801
|
+
for (const filename of filenames) {
|
|
2802
|
+
entries.push(...readJsonl(join5(timelineDir, filename)));
|
|
2803
|
+
}
|
|
2804
|
+
return entries;
|
|
2827
2805
|
}
|
|
2828
|
-
function
|
|
2829
|
-
|
|
2806
|
+
function appendEntry(timelineDir, entry, now = new Date) {
|
|
2807
|
+
const filename = filenameForDate(now);
|
|
2808
|
+
const filePath = join5(timelineDir, filename);
|
|
2809
|
+
const lockPath = lockPathFor(timelineDir, filename);
|
|
2810
|
+
if (!acquireLock(lockPath))
|
|
2811
|
+
return false;
|
|
2812
|
+
try {
|
|
2813
|
+
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
2814
|
+
`);
|
|
2815
|
+
return true;
|
|
2816
|
+
} catch {
|
|
2817
|
+
return false;
|
|
2818
|
+
} finally {
|
|
2819
|
+
releaseLock(lockPath);
|
|
2820
|
+
}
|
|
2830
2821
|
}
|
|
2831
|
-
function
|
|
2822
|
+
function appendOrMergeEntry(timelineDir, entry, now = new Date) {
|
|
2823
|
+
const filename = filenameForDate(now);
|
|
2824
|
+
const filePath = join5(timelineDir, filename);
|
|
2825
|
+
const lockPath = lockPathFor(timelineDir, filename);
|
|
2826
|
+
if (!acquireLock(lockPath))
|
|
2827
|
+
return false;
|
|
2832
2828
|
try {
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2829
|
+
let lines = [];
|
|
2830
|
+
if (existsSync2(filePath)) {
|
|
2831
|
+
lines = readFileSync4(filePath, "utf-8").trimEnd().split(`
|
|
2832
|
+
`).filter(Boolean);
|
|
2833
|
+
}
|
|
2834
|
+
if (lines.length > 0) {
|
|
2835
|
+
const latest = JSON.parse(lines[lines.length - 1]);
|
|
2836
|
+
const mergeable = !latest.system && !entry.system && latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
|
|
2837
|
+
if (mergeable) {
|
|
2838
|
+
latest.messages = [...latest.messages, ...entry.messages];
|
|
2839
|
+
lines[lines.length - 1] = JSON.stringify(latest);
|
|
2840
|
+
const tmpPath = join5(timelineDir, `.${filename}.tmp`);
|
|
2841
|
+
writeFileSync6(tmpPath, lines.join(`
|
|
2842
|
+
`) + `
|
|
2843
|
+
`);
|
|
2844
|
+
renameSync2(tmpPath, filePath);
|
|
2845
|
+
return true;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
2849
|
+
`);
|
|
2850
|
+
return true;
|
|
2851
|
+
} catch {
|
|
2852
|
+
return false;
|
|
2853
|
+
} finally {
|
|
2854
|
+
releaseLock(lockPath);
|
|
2851
2855
|
}
|
|
2852
2856
|
}
|
|
2853
|
-
function
|
|
2854
|
-
|
|
2857
|
+
function updateLatestEntry(timelineDir, updater, opts = {}) {
|
|
2858
|
+
const now = opts.now ?? new Date;
|
|
2859
|
+
const maxDays = opts.maxDays ?? 7;
|
|
2860
|
+
for (const filename of recentFilenames(maxDays, now)) {
|
|
2861
|
+
const filePath = join5(timelineDir, filename);
|
|
2862
|
+
if (!existsSync2(filePath))
|
|
2863
|
+
continue;
|
|
2864
|
+
const lockPath = lockPathFor(timelineDir, filename);
|
|
2865
|
+
if (!acquireLock(lockPath))
|
|
2866
|
+
continue;
|
|
2867
|
+
try {
|
|
2868
|
+
let content;
|
|
2869
|
+
try {
|
|
2870
|
+
content = readFileSync4(filePath, "utf-8");
|
|
2871
|
+
} catch {
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
const lines = content.trimEnd().split(`
|
|
2875
|
+
`).filter(Boolean);
|
|
2876
|
+
if (lines.length === 0)
|
|
2877
|
+
continue;
|
|
2878
|
+
const entries = lines.map((l) => JSON.parse(l));
|
|
2879
|
+
const latest = entries[entries.length - 1];
|
|
2880
|
+
if (latest.system)
|
|
2881
|
+
return false;
|
|
2882
|
+
updater(latest);
|
|
2883
|
+
const tmpPath = join5(timelineDir, `.${filename}.tmp`);
|
|
2884
|
+
writeFileSync6(tmpPath, entries.map((e) => JSON.stringify(e)).join(`
|
|
2885
|
+
`) + `
|
|
2886
|
+
`);
|
|
2887
|
+
renameSync2(tmpPath, filePath);
|
|
2888
|
+
return true;
|
|
2889
|
+
} catch {} finally {
|
|
2890
|
+
releaseLock(lockPath);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return false;
|
|
2855
2894
|
}
|
|
2856
|
-
function
|
|
2857
|
-
|
|
2858
|
-
|
|
2895
|
+
function createTimelineEntry(fields) {
|
|
2896
|
+
return {
|
|
2897
|
+
session_id: fields.sessionId ?? null,
|
|
2898
|
+
messages: fields.messages,
|
|
2899
|
+
agent_responses: [],
|
|
2900
|
+
provider: fields.provider ?? null
|
|
2901
|
+
};
|
|
2859
2902
|
}
|
|
2860
|
-
function
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2903
|
+
function createSystemEntry(type, time) {
|
|
2904
|
+
return {
|
|
2905
|
+
session_id: null,
|
|
2906
|
+
messages: [],
|
|
2907
|
+
agent_responses: [],
|
|
2908
|
+
provider: null,
|
|
2909
|
+
system: { type, time }
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
function findResumableSession(rows, provider) {
|
|
2913
|
+
for (let i = rows.length - 1;i >= 0; i--) {
|
|
2914
|
+
const e = rows[i];
|
|
2915
|
+
if (e.system?.type === "reset_session")
|
|
2916
|
+
return null;
|
|
2917
|
+
if (!e.session_id)
|
|
2918
|
+
continue;
|
|
2919
|
+
if (provider && e.provider !== provider)
|
|
2920
|
+
continue;
|
|
2921
|
+
return e.session_id;
|
|
2869
2922
|
}
|
|
2870
2923
|
return null;
|
|
2871
2924
|
}
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2925
|
+
// src/timeline/recorder.ts
|
|
2926
|
+
import { mkdirSync as mkdirSync5 } from "fs";
|
|
2927
|
+
function createTimelineRecorder(opts) {
|
|
2928
|
+
const now = opts.now ?? (() => new Date);
|
|
2929
|
+
const dirFor = (agentId) => opts.timelineDirFor(agentId);
|
|
2930
|
+
const sessionByAgent = new Map;
|
|
2931
|
+
return {
|
|
2932
|
+
setSession(agentId, sessionId) {
|
|
2933
|
+
sessionByAgent.set(agentId, sessionId);
|
|
2934
|
+
},
|
|
2935
|
+
appendEntryForAgent(agentId, messages) {
|
|
2936
|
+
const dir = dirFor(agentId);
|
|
2937
|
+
try {
|
|
2938
|
+
mkdirSync5(dir, { recursive: true });
|
|
2939
|
+
} catch {}
|
|
2940
|
+
appendOrMergeEntry(dir, createTimelineEntry({
|
|
2941
|
+
messages,
|
|
2942
|
+
sessionId: sessionByAgent.get(agentId) ?? null,
|
|
2943
|
+
provider: opts.providerFor?.(agentId) ?? null
|
|
2944
|
+
}), now());
|
|
2945
|
+
},
|
|
2946
|
+
appendResponseToLatest(agentId, text) {
|
|
2947
|
+
const dir = dirFor(agentId);
|
|
2948
|
+
const updated = updateLatestEntry(dir, (e) => e.agent_responses.push(text), { now: now() });
|
|
2949
|
+
if (updated)
|
|
2950
|
+
return;
|
|
2951
|
+
try {
|
|
2952
|
+
mkdirSync5(dir, { recursive: true });
|
|
2953
|
+
} catch {}
|
|
2954
|
+
const entry = createTimelineEntry({
|
|
2955
|
+
messages: [],
|
|
2956
|
+
sessionId: sessionByAgent.get(agentId) ?? null,
|
|
2957
|
+
provider: opts.providerFor?.(agentId) ?? null
|
|
2958
|
+
});
|
|
2959
|
+
entry.agent_responses.push(text);
|
|
2960
|
+
appendEntry(dir, entry, now());
|
|
2961
|
+
},
|
|
2962
|
+
resumeSessionId(agentId, provider) {
|
|
2963
|
+
const rows = readRecentEntries(dirFor(agentId), { now: now() });
|
|
2964
|
+
return findResumableSession(rows, provider ?? undefined);
|
|
2965
|
+
},
|
|
2966
|
+
forgetSession(agentId) {
|
|
2967
|
+
const dir = dirFor(agentId);
|
|
2968
|
+
try {
|
|
2969
|
+
mkdirSync5(dir, { recursive: true });
|
|
2970
|
+
} catch {}
|
|
2971
|
+
sessionByAgent.delete(agentId);
|
|
2972
|
+
const stamp = now();
|
|
2973
|
+
appendEntry(dir, createSystemEntry("reset_session", stamp.toISOString()), stamp);
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2880
2976
|
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2977
|
+
// src/discovery.ts
|
|
2978
|
+
import * as path9 from "path";
|
|
2979
|
+
import * as fs8 from "fs";
|
|
2980
|
+
import { fileURLToPath } from "url";
|
|
2981
|
+
|
|
2982
|
+
// src/drivers/claudeProviderIsolation.ts
|
|
2983
|
+
import * as fs6 from "fs";
|
|
2984
|
+
import * as path5 from "path";
|
|
2985
|
+
function buildClaudeProviderIsolationEnv(ctx) {
|
|
2986
|
+
const hasCustomProvider = Boolean(process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_API_KEY);
|
|
2987
|
+
if (!hasCustomProvider)
|
|
2988
|
+
return {};
|
|
2989
|
+
const root = path5.join(ctx.workingDirectory, ".alook", "claude-provider");
|
|
2990
|
+
const home = path5.join(root, "home");
|
|
2991
|
+
const configDir = path5.join(home, ".claude");
|
|
2992
|
+
fs6.mkdirSync(configDir, { recursive: true });
|
|
2993
|
+
const hostClaude = path5.join(process.env.HOME || ".", ".claude");
|
|
2994
|
+
for (const sub of ["skills", "commands"]) {
|
|
2995
|
+
const target = path5.join(hostClaude, sub);
|
|
2996
|
+
const link = path5.join(configDir, sub);
|
|
2997
|
+
try {
|
|
2998
|
+
if (fs6.existsSync(target) && !fs6.existsSync(link))
|
|
2999
|
+
fs6.symlinkSync(target, link);
|
|
3000
|
+
} catch {}
|
|
3001
|
+
}
|
|
3002
|
+
return {
|
|
3003
|
+
HOME: home,
|
|
3004
|
+
USERPROFILE: home,
|
|
3005
|
+
CLAUDE_CONFIG_DIR: configDir,
|
|
3006
|
+
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1"
|
|
3007
|
+
};
|
|
2889
3008
|
}
|
|
2890
3009
|
|
|
2891
3010
|
// src/drivers/claudeLaunch.ts
|
|
2892
|
-
var DEFAULT_CLAUDE_MODEL = "sonnet";
|
|
2893
3011
|
var CLAUDE_DISALLOWED_TOOLS = "EnterPlanMode,ExitPlanMode,ScheduleWakeup,CronCreate,CronList,CronDelete";
|
|
2894
3012
|
function buildClaudeArgs(config) {
|
|
2895
3013
|
const f = resolveLaunchFieldsOrDefault(config.runtimeConfig);
|
|
@@ -2904,11 +3022,11 @@ function buildClaudeArgs(config) {
|
|
|
2904
3022
|
"--input-format",
|
|
2905
3023
|
"stream-json",
|
|
2906
3024
|
"--include-partial-messages",
|
|
2907
|
-
"--model",
|
|
2908
|
-
f.model || DEFAULT_CLAUDE_MODEL,
|
|
2909
3025
|
"--disallowed-tools",
|
|
2910
3026
|
f.disallowedTools || CLAUDE_DISALLOWED_TOOLS
|
|
2911
3027
|
];
|
|
3028
|
+
if (f.model)
|
|
3029
|
+
args.push("--model", f.model);
|
|
2912
3030
|
if (f.reasoningEffort)
|
|
2913
3031
|
args.push("--effort", f.reasoningEffort);
|
|
2914
3032
|
if (f.fastMode)
|
|
@@ -2917,14 +3035,24 @@ function buildClaudeArgs(config) {
|
|
|
2917
3035
|
args.push("--resume", config.sessionId);
|
|
2918
3036
|
return args;
|
|
2919
3037
|
}
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
3038
|
+
|
|
3039
|
+
// src/drivers/utils.ts
|
|
3040
|
+
import { randomUUID } from "crypto";
|
|
3041
|
+
function writeToStdinAndDetach(proc, payload) {
|
|
3042
|
+
queueMicrotask(() => {
|
|
3043
|
+
proc.stdin?.write(payload);
|
|
3044
|
+
proc.stdin?.end();
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
function jsonRpcRequest(method, params, id) {
|
|
3048
|
+
return JSON.stringify({ jsonrpc: "2.0", id: id ?? randomUUID(), method, params });
|
|
2923
3049
|
}
|
|
2924
|
-
function
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3050
|
+
function tryParseJsonLine(line) {
|
|
3051
|
+
try {
|
|
3052
|
+
return JSON.parse(line);
|
|
3053
|
+
} catch {
|
|
3054
|
+
return null;
|
|
3055
|
+
}
|
|
2928
3056
|
}
|
|
2929
3057
|
|
|
2930
3058
|
// src/drivers/claudeEventNormalizer.ts
|
|
@@ -2936,12 +3064,9 @@ class ClaudeEventNormalizer {
|
|
|
2936
3064
|
return this.currentSession;
|
|
2937
3065
|
}
|
|
2938
3066
|
normalizeLine(line) {
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
event = JSON.parse(line);
|
|
2942
|
-
} catch {
|
|
3067
|
+
const event = tryParseJsonLine(line);
|
|
3068
|
+
if (!event)
|
|
2943
3069
|
return [];
|
|
2944
|
-
}
|
|
2945
3070
|
if (event?.session_id)
|
|
2946
3071
|
this.currentSession = event.session_id;
|
|
2947
3072
|
const out = [];
|
|
@@ -3045,6 +3170,102 @@ class ClaudeEventNormalizer {
|
|
|
3045
3170
|
}
|
|
3046
3171
|
}
|
|
3047
3172
|
|
|
3173
|
+
// src/drivers/probe.ts
|
|
3174
|
+
import { execFileSync } from "child_process";
|
|
3175
|
+
import * as fs7 from "fs";
|
|
3176
|
+
import * as path6 from "path";
|
|
3177
|
+
var PROBE_TIMEOUT_MS = 5000;
|
|
3178
|
+
function resolveCommandOnPath(command, deps = {}) {
|
|
3179
|
+
if (deps.which)
|
|
3180
|
+
return deps.which(command);
|
|
3181
|
+
try {
|
|
3182
|
+
if (process.platform === "win32") {
|
|
3183
|
+
const out2 = execFileSync("where", [command], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS });
|
|
3184
|
+
const first = out2.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
3185
|
+
return first?.trim() || null;
|
|
3186
|
+
}
|
|
3187
|
+
const out = execFileSync("which", [command], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS });
|
|
3188
|
+
return out.trim() || null;
|
|
3189
|
+
} catch {
|
|
3190
|
+
return null;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
function firstExistingPath(candidates) {
|
|
3194
|
+
for (const c of candidates) {
|
|
3195
|
+
if (c && fs7.existsSync(c))
|
|
3196
|
+
return c;
|
|
3197
|
+
}
|
|
3198
|
+
return null;
|
|
3199
|
+
}
|
|
3200
|
+
function looksLikeVersion(line) {
|
|
3201
|
+
return /\d+\.\d+/.test(line);
|
|
3202
|
+
}
|
|
3203
|
+
function needsWindowsShimShell(command, platform) {
|
|
3204
|
+
return platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
3205
|
+
}
|
|
3206
|
+
function probeCommandVersion(command, args = [], deps = {}, platform = process.platform) {
|
|
3207
|
+
try {
|
|
3208
|
+
const shell = needsWindowsShimShell(command, platform);
|
|
3209
|
+
const out = execFileSync(command, [...args, "--version"], {
|
|
3210
|
+
encoding: "utf8",
|
|
3211
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
3212
|
+
shell,
|
|
3213
|
+
input: "",
|
|
3214
|
+
env: { ...process.env, CI: "1" }
|
|
3215
|
+
});
|
|
3216
|
+
const line = out.split(`
|
|
3217
|
+
`)[0]?.trim();
|
|
3218
|
+
if (!line)
|
|
3219
|
+
return { ok: false, error: "empty_version_output" };
|
|
3220
|
+
if (!looksLikeVersion(line))
|
|
3221
|
+
return { ok: false, error: "invalid_version_output" };
|
|
3222
|
+
return { ok: true, version: line };
|
|
3223
|
+
} catch (err) {
|
|
3224
|
+
const code = err?.code ?? err?.code ?? "version_probe_failed";
|
|
3225
|
+
return { ok: false, error: String(code) };
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
function resolveHomePath(relativePath, deps = {}) {
|
|
3229
|
+
return path6.join(deps.homeDir || process.env.HOME || ".", relativePath);
|
|
3230
|
+
}
|
|
3231
|
+
function resolveSpawnSpec(command, args, override, deps = {}, platform = process.platform) {
|
|
3232
|
+
const trimmed = override?.trim();
|
|
3233
|
+
const target = trimmed && trimmed.length > 0 ? trimmed : command;
|
|
3234
|
+
const looksLikePath = trimmed !== undefined && trimmed.length > 0 && /[\\/]/.test(trimmed);
|
|
3235
|
+
const resolved = looksLikePath ? target : resolveCommandOnPath(target, deps) ?? target;
|
|
3236
|
+
return { command: resolved, args, shell: needsWindowsShimShell(resolved, platform) };
|
|
3237
|
+
}
|
|
3238
|
+
function resolveClaudeCommand(deps = {}) {
|
|
3239
|
+
const onPath = resolveCommandOnPath("claude", deps);
|
|
3240
|
+
if (onPath)
|
|
3241
|
+
return onPath;
|
|
3242
|
+
if (process.platform === "darwin") {
|
|
3243
|
+
return firstExistingPath([
|
|
3244
|
+
resolveHomePath("Applications/Claude Code URL Handler.app/Contents/MacOS/claude", deps),
|
|
3245
|
+
"/Applications/Claude Code URL Handler.app/Contents/MacOS/claude"
|
|
3246
|
+
]);
|
|
3247
|
+
}
|
|
3248
|
+
return null;
|
|
3249
|
+
}
|
|
3250
|
+
function probeClaude(deps = {}) {
|
|
3251
|
+
const command = resolveClaudeCommand(deps);
|
|
3252
|
+
if (!command)
|
|
3253
|
+
return { status: "unhealthy", lastError: "not_on_path" };
|
|
3254
|
+
const r = probeCommandVersion(command, [], deps);
|
|
3255
|
+
if (!r.ok)
|
|
3256
|
+
return { status: "unhealthy", lastError: r.error };
|
|
3257
|
+
return { status: "healthy", version: r.version };
|
|
3258
|
+
}
|
|
3259
|
+
function probeCliRuntime(binary, deps = {}) {
|
|
3260
|
+
const command = resolveCommandOnPath(binary, deps);
|
|
3261
|
+
if (!command)
|
|
3262
|
+
return { status: "unhealthy", lastError: "not_on_path" };
|
|
3263
|
+
const r = probeCommandVersion(command, [], deps);
|
|
3264
|
+
if (!r.ok)
|
|
3265
|
+
return { status: "unhealthy", lastError: r.error };
|
|
3266
|
+
return { status: "healthy", version: r.version };
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3048
3269
|
// src/drivers/claude.ts
|
|
3049
3270
|
class ClaudeDriver {
|
|
3050
3271
|
id = "claude";
|
|
@@ -3057,6 +3278,13 @@ class ClaudeDriver {
|
|
|
3057
3278
|
supportsStdinNotification = true;
|
|
3058
3279
|
busyDeliveryMode = "gated";
|
|
3059
3280
|
supportsNativeStandingPrompt = true;
|
|
3281
|
+
capabilities = {
|
|
3282
|
+
reasoningEffort: true,
|
|
3283
|
+
fastMode: true,
|
|
3284
|
+
disallowedTools: true,
|
|
3285
|
+
command: true,
|
|
3286
|
+
sessionResumeMode: "by-id"
|
|
3287
|
+
};
|
|
3060
3288
|
eventNormalizer = new ClaudeEventNormalizer;
|
|
3061
3289
|
probe() {
|
|
3062
3290
|
return probeClaude();
|
|
@@ -3066,12 +3294,13 @@ class ClaudeDriver {
|
|
|
3066
3294
|
const { spawnEnv } = await prepareCliTransport(ctx, buildClaudeProviderIsolationEnv(ctx), cliConfig);
|
|
3067
3295
|
const args = buildClaudeArgs(ctx.config);
|
|
3068
3296
|
delete spawnEnv.CLAUDECODE;
|
|
3069
|
-
const
|
|
3070
|
-
const
|
|
3071
|
-
const
|
|
3297
|
+
const override = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig).command?.trim();
|
|
3298
|
+
const claudeCommand = override || resolveClaudeCommand() || "claude";
|
|
3299
|
+
const spec = resolveSpawnSpec("claude", args, claudeCommand);
|
|
3300
|
+
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3072
3301
|
cwd: ctx.workingDirectory,
|
|
3073
3302
|
env: spawnEnv,
|
|
3074
|
-
shell:
|
|
3303
|
+
shell: spec.shell
|
|
3075
3304
|
});
|
|
3076
3305
|
const stdinMsg = JSON.stringify({
|
|
3077
3306
|
type: "user",
|
|
@@ -3096,7 +3325,7 @@ class ClaudeDriver {
|
|
|
3096
3325
|
});
|
|
3097
3326
|
}
|
|
3098
3327
|
buildSystemPrompt(config) {
|
|
3099
|
-
return buildCliTransportSystemPrompt(config
|
|
3328
|
+
return buildCliTransportSystemPrompt(config);
|
|
3100
3329
|
}
|
|
3101
3330
|
}
|
|
3102
3331
|
|
|
@@ -3153,12 +3382,9 @@ class CodexEventNormalizer {
|
|
|
3153
3382
|
this.threadId = threadId;
|
|
3154
3383
|
}
|
|
3155
3384
|
normalizeLine(line) {
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
msg = JSON.parse(line);
|
|
3159
|
-
} catch {
|
|
3385
|
+
const msg = tryParseJsonLine(line);
|
|
3386
|
+
if (!msg)
|
|
3160
3387
|
return [];
|
|
3161
|
-
}
|
|
3162
3388
|
if (msg?.error && msg.id !== undefined) {
|
|
3163
3389
|
return [{ kind: "error", message: msg.error?.message ?? "Codex RPC error" }];
|
|
3164
3390
|
}
|
|
@@ -3274,6 +3500,21 @@ function resolveCodexHomeRootFromEnv(env = process.env, opts = {}) {
|
|
|
3274
3500
|
return path7.join(opts.defaultHomeDir ?? os2.homedir(), ".codex");
|
|
3275
3501
|
}
|
|
3276
3502
|
|
|
3503
|
+
// src/version.ts
|
|
3504
|
+
import { createRequire as createRequire2 } from "module";
|
|
3505
|
+
var requireFromHere = createRequire2(import.meta.url);
|
|
3506
|
+
function readDaemonVersion() {
|
|
3507
|
+
try {
|
|
3508
|
+
const pkg = requireFromHere("../package.json");
|
|
3509
|
+
return pkg.version ?? "";
|
|
3510
|
+
} catch {
|
|
3511
|
+
return "";
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
function getDaemonClientInfo() {
|
|
3515
|
+
return { name: "alook-daemon", version: readDaemonVersion() };
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3277
3518
|
// src/drivers/codex.ts
|
|
3278
3519
|
class CodexDriver {
|
|
3279
3520
|
id = "codex";
|
|
@@ -3286,6 +3527,13 @@ class CodexDriver {
|
|
|
3286
3527
|
supportsStdinNotification = true;
|
|
3287
3528
|
busyDeliveryMode = "gated";
|
|
3288
3529
|
supportsNativeStandingPrompt = true;
|
|
3530
|
+
capabilities = {
|
|
3531
|
+
reasoningEffort: true,
|
|
3532
|
+
fastMode: true,
|
|
3533
|
+
disallowedTools: false,
|
|
3534
|
+
command: true,
|
|
3535
|
+
sessionResumeMode: "by-id"
|
|
3536
|
+
};
|
|
3289
3537
|
eventNormalizer = new CodexEventNormalizer;
|
|
3290
3538
|
requestId = 0;
|
|
3291
3539
|
codexHomeRoot = null;
|
|
@@ -3299,24 +3547,17 @@ class CodexDriver {
|
|
|
3299
3547
|
return probeCliRuntime("codex");
|
|
3300
3548
|
}
|
|
3301
3549
|
async spawn(ctx) {
|
|
3302
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3550
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3303
3551
|
this.codexHomeRoot = resolveCodexHomeRootFromEnv(spawnEnv, { cwd: ctx.workingDirectory });
|
|
3304
|
-
const
|
|
3552
|
+
const override = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig).command;
|
|
3553
|
+
const spec = resolveSpawnSpec("codex", ["app-server", "--listen", "stdio://"], override);
|
|
3305
3554
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3306
3555
|
cwd: ctx.workingDirectory,
|
|
3307
3556
|
env: spawnEnv,
|
|
3308
3557
|
shell: spec.shell
|
|
3309
3558
|
});
|
|
3310
3559
|
queueMicrotask(() => {
|
|
3311
|
-
proc.stdin?.write(
|
|
3312
|
-
jsonrpc: "2.0",
|
|
3313
|
-
id: this.nextRequestId(),
|
|
3314
|
-
method: "initialize",
|
|
3315
|
-
params: {
|
|
3316
|
-
clientInfo: { name: "agent-backend", version: "1.0.0" },
|
|
3317
|
-
capabilities: { experimentalApi: true }
|
|
3318
|
-
}
|
|
3319
|
-
}) + `
|
|
3560
|
+
proc.stdin?.write(jsonRpcRequest("initialize", { clientInfo: getDaemonClientInfo(), capabilities: { experimentalApi: true } }, this.nextRequestId()) + `
|
|
3320
3561
|
`);
|
|
3321
3562
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
3322
3563
|
const resuming = Boolean(ctx.config.sessionId);
|
|
@@ -3335,12 +3576,7 @@ class CodexDriver {
|
|
|
3335
3576
|
params.config = { model_reasoning_effort: f.reasoningEffort };
|
|
3336
3577
|
if (f.fastMode)
|
|
3337
3578
|
params.serviceTier = "fast";
|
|
3338
|
-
proc.stdin?.write(
|
|
3339
|
-
jsonrpc: "2.0",
|
|
3340
|
-
id: this.nextRequestId(),
|
|
3341
|
-
method: resuming ? "thread/resume" : "thread/start",
|
|
3342
|
-
params
|
|
3343
|
-
}) + `
|
|
3579
|
+
proc.stdin?.write(jsonRpcRequest(resuming ? "thread/resume" : "thread/start", params, this.nextRequestId()) + `
|
|
3344
3580
|
`);
|
|
3345
3581
|
});
|
|
3346
3582
|
return { process: proc };
|
|
@@ -3356,23 +3592,11 @@ class CodexDriver {
|
|
|
3356
3592
|
if (!threadId)
|
|
3357
3593
|
return null;
|
|
3358
3594
|
const input = [{ type: "text", text }];
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
jsonrpc: "2.0",
|
|
3362
|
-
id: this.nextRequestId(),
|
|
3363
|
-
method: "turn/start",
|
|
3364
|
-
params: { threadId, input }
|
|
3365
|
-
});
|
|
3366
|
-
}
|
|
3367
|
-
return JSON.stringify({
|
|
3368
|
-
jsonrpc: "2.0",
|
|
3369
|
-
id: this.nextRequestId(),
|
|
3370
|
-
method: "turn/steer",
|
|
3371
|
-
params: { threadId, input }
|
|
3372
|
-
});
|
|
3595
|
+
const method = opts?.mode === "idle" ? "turn/start" : "turn/steer";
|
|
3596
|
+
return jsonRpcRequest(method, { threadId, input }, this.nextRequestId());
|
|
3373
3597
|
}
|
|
3374
3598
|
buildSystemPrompt(config) {
|
|
3375
|
-
return buildCliTransportSystemPrompt(config
|
|
3599
|
+
return buildCliTransportSystemPrompt(config);
|
|
3376
3600
|
}
|
|
3377
3601
|
}
|
|
3378
3602
|
|
|
@@ -3400,37 +3624,42 @@ class GeminiDriver {
|
|
|
3400
3624
|
};
|
|
3401
3625
|
session = { recovery: "resume_or_fresh" };
|
|
3402
3626
|
model = {
|
|
3403
|
-
detectedModelsVerifiedAs: "
|
|
3404
|
-
toLaunchSpec: (modelId) => modelId
|
|
3627
|
+
detectedModelsVerifiedAs: "launchable",
|
|
3628
|
+
toLaunchSpec: (modelId) => modelId ? { args: ["--model", modelId] } : { args: [] }
|
|
3405
3629
|
};
|
|
3406
3630
|
supportsStdinNotification = false;
|
|
3407
3631
|
busyDeliveryMode = "none";
|
|
3632
|
+
capabilities = {
|
|
3633
|
+
reasoningEffort: false,
|
|
3634
|
+
fastMode: false,
|
|
3635
|
+
disallowedTools: false,
|
|
3636
|
+
command: true,
|
|
3637
|
+
sessionResumeMode: "by-id"
|
|
3638
|
+
};
|
|
3408
3639
|
sessionId = null;
|
|
3409
3640
|
probe() {
|
|
3410
3641
|
return probeCliRuntime("gemini");
|
|
3411
3642
|
}
|
|
3412
3643
|
async spawn(ctx) {
|
|
3413
3644
|
this.sessionId = ctx.config.sessionId ?? null;
|
|
3414
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3645
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3415
3646
|
spawnEnv.GEMINI_CLI_TRUST_WORKSPACE ??= "true";
|
|
3416
3647
|
if (process.platform === "win32")
|
|
3417
3648
|
spawnEnv.GEMINI_PTY_INFO ??= "child_process";
|
|
3418
|
-
const
|
|
3649
|
+
const override = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig).command;
|
|
3650
|
+
const spec = resolveSpawnSpec("gemini", buildGeminiArgs(ctx.config), override);
|
|
3419
3651
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3420
3652
|
cwd: ctx.workingDirectory,
|
|
3421
3653
|
env: spawnEnv,
|
|
3422
3654
|
shell: spec.shell
|
|
3423
3655
|
});
|
|
3424
|
-
proc
|
|
3656
|
+
writeToStdinAndDetach(proc, ctx.prompt);
|
|
3425
3657
|
return { process: proc };
|
|
3426
3658
|
}
|
|
3427
3659
|
parseLine(line) {
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
event = JSON.parse(line);
|
|
3431
|
-
} catch {
|
|
3660
|
+
const event = tryParseJsonLine(line);
|
|
3661
|
+
if (!event)
|
|
3432
3662
|
return [];
|
|
3433
|
-
}
|
|
3434
3663
|
switch (event?.type) {
|
|
3435
3664
|
case "init":
|
|
3436
3665
|
this.sessionId = event.session_id ?? this.sessionId;
|
|
@@ -3456,7 +3685,7 @@ class GeminiDriver {
|
|
|
3456
3685
|
return null;
|
|
3457
3686
|
}
|
|
3458
3687
|
buildSystemPrompt(config) {
|
|
3459
|
-
return buildCliTransportSystemPrompt(config
|
|
3688
|
+
return buildCliTransportSystemPrompt(config);
|
|
3460
3689
|
}
|
|
3461
3690
|
}
|
|
3462
3691
|
|
|
@@ -3476,13 +3705,20 @@ class CopilotDriver {
|
|
|
3476
3705
|
};
|
|
3477
3706
|
supportsStdinNotification = false;
|
|
3478
3707
|
busyDeliveryMode = "none";
|
|
3708
|
+
capabilities = {
|
|
3709
|
+
reasoningEffort: true,
|
|
3710
|
+
fastMode: false,
|
|
3711
|
+
disallowedTools: false,
|
|
3712
|
+
command: true,
|
|
3713
|
+
sessionResumeMode: "by-id"
|
|
3714
|
+
};
|
|
3479
3715
|
sessionId = null;
|
|
3480
3716
|
probe() {
|
|
3481
3717
|
return probeCliRuntime("copilot");
|
|
3482
3718
|
}
|
|
3483
3719
|
async spawn(ctx) {
|
|
3484
3720
|
this.sessionId = ctx.config.sessionId ?? null;
|
|
3485
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3721
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3486
3722
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
3487
3723
|
const args = ["--output-format", "json", "--allow-all-tools", "--allow-all-paths", "-p", ctx.prompt];
|
|
3488
3724
|
if (f.model)
|
|
@@ -3491,7 +3727,7 @@ class CopilotDriver {
|
|
|
3491
3727
|
args.push("--effort", f.reasoningEffort);
|
|
3492
3728
|
if (ctx.config.sessionId)
|
|
3493
3729
|
args.push(`--resume=${ctx.config.sessionId}`);
|
|
3494
|
-
const spec = resolveSpawnSpec("copilot", args);
|
|
3730
|
+
const spec = resolveSpawnSpec("copilot", args, f.command);
|
|
3495
3731
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3496
3732
|
cwd: ctx.workingDirectory,
|
|
3497
3733
|
env: spawnEnv,
|
|
@@ -3500,12 +3736,9 @@ class CopilotDriver {
|
|
|
3500
3736
|
return { process: proc };
|
|
3501
3737
|
}
|
|
3502
3738
|
parseLine(line) {
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
event = JSON.parse(line);
|
|
3506
|
-
} catch {
|
|
3739
|
+
const event = tryParseJsonLine(line);
|
|
3740
|
+
if (!event)
|
|
3507
3741
|
return [];
|
|
3508
|
-
}
|
|
3509
3742
|
switch (event?.type) {
|
|
3510
3743
|
case "assistant.turn_start":
|
|
3511
3744
|
if (event.sessionId)
|
|
@@ -3540,7 +3773,7 @@ class CopilotDriver {
|
|
|
3540
3773
|
return null;
|
|
3541
3774
|
}
|
|
3542
3775
|
buildSystemPrompt(config) {
|
|
3543
|
-
return buildCliTransportSystemPrompt(config
|
|
3776
|
+
return buildCliTransportSystemPrompt(config);
|
|
3544
3777
|
}
|
|
3545
3778
|
}
|
|
3546
3779
|
|
|
@@ -3560,13 +3793,20 @@ class CursorDriver {
|
|
|
3560
3793
|
};
|
|
3561
3794
|
supportsStdinNotification = false;
|
|
3562
3795
|
busyDeliveryMode = "none";
|
|
3796
|
+
capabilities = {
|
|
3797
|
+
reasoningEffort: false,
|
|
3798
|
+
fastMode: false,
|
|
3799
|
+
disallowedTools: false,
|
|
3800
|
+
command: true,
|
|
3801
|
+
sessionResumeMode: "by-id"
|
|
3802
|
+
};
|
|
3563
3803
|
sessionId = null;
|
|
3564
3804
|
probe() {
|
|
3565
3805
|
return probeCliRuntime("cursor-agent");
|
|
3566
3806
|
}
|
|
3567
3807
|
async spawn(ctx) {
|
|
3568
3808
|
this.sessionId = ctx.config.sessionId ?? null;
|
|
3569
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3809
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3570
3810
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
3571
3811
|
const args = ["--print", "--output-format", "stream-json", "--yolo", "--approve-mcps", "--trust"];
|
|
3572
3812
|
if (f.model)
|
|
@@ -3574,7 +3814,7 @@ class CursorDriver {
|
|
|
3574
3814
|
if (ctx.config.sessionId)
|
|
3575
3815
|
args.push("--resume", ctx.config.sessionId);
|
|
3576
3816
|
args.push(ctx.prompt);
|
|
3577
|
-
const spec = resolveSpawnSpec("cursor-agent", args);
|
|
3817
|
+
const spec = resolveSpawnSpec("cursor-agent", args, f.command);
|
|
3578
3818
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3579
3819
|
cwd: ctx.workingDirectory,
|
|
3580
3820
|
env: spawnEnv,
|
|
@@ -3583,12 +3823,9 @@ class CursorDriver {
|
|
|
3583
3823
|
return { process: proc };
|
|
3584
3824
|
}
|
|
3585
3825
|
parseLine(line) {
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
event = JSON.parse(line);
|
|
3589
|
-
} catch {
|
|
3826
|
+
const event = tryParseJsonLine(line);
|
|
3827
|
+
if (!event)
|
|
3590
3828
|
return [];
|
|
3591
|
-
}
|
|
3592
3829
|
if (event?.type === "system") {
|
|
3593
3830
|
if (event.subtype === "init") {
|
|
3594
3831
|
this.sessionId = event.session_id ?? this.sessionId;
|
|
@@ -3631,7 +3868,7 @@ class CursorDriver {
|
|
|
3631
3868
|
return null;
|
|
3632
3869
|
}
|
|
3633
3870
|
buildSystemPrompt(config) {
|
|
3634
|
-
return buildCliTransportSystemPrompt(config
|
|
3871
|
+
return buildCliTransportSystemPrompt(config);
|
|
3635
3872
|
}
|
|
3636
3873
|
}
|
|
3637
3874
|
|
|
@@ -3653,6 +3890,13 @@ class OpenCodeDriver {
|
|
|
3653
3890
|
busyDeliveryMode = "none";
|
|
3654
3891
|
terminateProcessOnTurnEnd = true;
|
|
3655
3892
|
deferSpawnUntilMessage = true;
|
|
3893
|
+
capabilities = {
|
|
3894
|
+
reasoningEffort: false,
|
|
3895
|
+
fastMode: false,
|
|
3896
|
+
disallowedTools: false,
|
|
3897
|
+
command: true,
|
|
3898
|
+
sessionResumeMode: "by-id"
|
|
3899
|
+
};
|
|
3656
3900
|
sessionId = null;
|
|
3657
3901
|
shouldDeferWakeMessage(message) {
|
|
3658
3902
|
return message?.type === "system";
|
|
@@ -3663,7 +3907,7 @@ class OpenCodeDriver {
|
|
|
3663
3907
|
async spawn(ctx) {
|
|
3664
3908
|
this.sessionId = ctx.config.sessionId ?? null;
|
|
3665
3909
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
3666
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3910
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3667
3911
|
const args = ["run", "--format", "json", "--dangerously-skip-permissions", "--pure", "--dir", ctx.workingDirectory];
|
|
3668
3912
|
if (f.model)
|
|
3669
3913
|
args.push("--model", f.model);
|
|
@@ -3671,7 +3915,7 @@ class OpenCodeDriver {
|
|
|
3671
3915
|
args.push("--session", ctx.config.sessionId);
|
|
3672
3916
|
const promptArg = ctx.prompt === ctx.standingPrompt ? "No new messages are pending. Stop now." : ctx.prompt;
|
|
3673
3917
|
args.push("--", promptArg);
|
|
3674
|
-
const spec = resolveSpawnSpec("opencode", args);
|
|
3918
|
+
const spec = resolveSpawnSpec("opencode", args, f.command);
|
|
3675
3919
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3676
3920
|
cwd: ctx.workingDirectory,
|
|
3677
3921
|
env: spawnEnv,
|
|
@@ -3681,12 +3925,9 @@ class OpenCodeDriver {
|
|
|
3681
3925
|
return { process: proc };
|
|
3682
3926
|
}
|
|
3683
3927
|
parseLine(line) {
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
event = JSON.parse(line);
|
|
3687
|
-
} catch {
|
|
3928
|
+
const event = tryParseJsonLine(line);
|
|
3929
|
+
if (!event)
|
|
3688
3930
|
return [];
|
|
3689
|
-
}
|
|
3690
3931
|
const out = [];
|
|
3691
3932
|
if (event?.sessionID && this.sessionId !== event.sessionID) {
|
|
3692
3933
|
this.sessionId = event.sessionID;
|
|
@@ -3724,12 +3965,12 @@ class OpenCodeDriver {
|
|
|
3724
3965
|
return null;
|
|
3725
3966
|
}
|
|
3726
3967
|
buildSystemPrompt(config) {
|
|
3727
|
-
return buildCliTransportSystemPrompt(config
|
|
3968
|
+
return buildCliTransportSystemPrompt(config);
|
|
3728
3969
|
}
|
|
3729
3970
|
}
|
|
3730
3971
|
|
|
3731
3972
|
// src/drivers/antigravity.ts
|
|
3732
|
-
import { randomUUID } from "crypto";
|
|
3973
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3733
3974
|
var ERROR_LINE_PATTERNS = [/^error[:\s]/i, /\bfatal\b/i, /\bpanic\b/i, /unable to/i];
|
|
3734
3975
|
var ANTIGRAVITY_PRINT_TIMEOUT = "30m";
|
|
3735
3976
|
function buildAntigravityArgs(ctx) {
|
|
@@ -3754,27 +3995,34 @@ class AntigravityDriver {
|
|
|
3754
3995
|
};
|
|
3755
3996
|
supportsStdinNotification = false;
|
|
3756
3997
|
busyDeliveryMode = "none";
|
|
3998
|
+
capabilities = {
|
|
3999
|
+
reasoningEffort: false,
|
|
4000
|
+
fastMode: false,
|
|
4001
|
+
disallowedTools: false,
|
|
4002
|
+
command: true,
|
|
4003
|
+
sessionResumeMode: "most-recent"
|
|
4004
|
+
};
|
|
3757
4005
|
sessionId = null;
|
|
3758
4006
|
sentInit = false;
|
|
3759
4007
|
probe() {
|
|
3760
4008
|
return probeCliRuntime("agy");
|
|
3761
4009
|
}
|
|
3762
4010
|
async spawn(ctx) {
|
|
3763
|
-
this.sessionId = ctx.config.sessionId ??
|
|
4011
|
+
this.sessionId = ctx.config.sessionId ?? randomUUID2();
|
|
3764
4012
|
this.sentInit = false;
|
|
3765
4013
|
const { spawnEnv } = await prepareCliTransport(ctx, {
|
|
3766
|
-
NO_COLOR: "1",
|
|
3767
4014
|
SSH_CLIENT: "",
|
|
3768
4015
|
SSH_CONNECTION: "",
|
|
3769
4016
|
SSH_TTY: ""
|
|
3770
4017
|
});
|
|
3771
|
-
const
|
|
4018
|
+
const override = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig).command;
|
|
4019
|
+
const spec = resolveSpawnSpec("agy", buildAntigravityArgs(ctx), override);
|
|
3772
4020
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3773
4021
|
cwd: ctx.workingDirectory,
|
|
3774
4022
|
env: spawnEnv,
|
|
3775
4023
|
shell: spec.shell
|
|
3776
4024
|
});
|
|
3777
|
-
proc
|
|
4025
|
+
writeToStdinAndDetach(proc, ctx.prompt);
|
|
3778
4026
|
return { process: proc };
|
|
3779
4027
|
}
|
|
3780
4028
|
parseLine(line) {
|
|
@@ -3799,12 +4047,13 @@ class AntigravityDriver {
|
|
|
3799
4047
|
return null;
|
|
3800
4048
|
}
|
|
3801
4049
|
buildSystemPrompt(config) {
|
|
3802
|
-
return buildCliTransportSystemPrompt(config
|
|
4050
|
+
return buildCliTransportSystemPrompt(config);
|
|
3803
4051
|
}
|
|
3804
4052
|
}
|
|
3805
4053
|
|
|
3806
4054
|
// src/drivers/kimi.ts
|
|
3807
|
-
import { randomUUID as
|
|
4055
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4056
|
+
var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
|
|
3808
4057
|
function parseToolArguments(args) {
|
|
3809
4058
|
if (typeof args !== "string")
|
|
3810
4059
|
return args ?? {};
|
|
@@ -3825,55 +4074,49 @@ class KimiDriver {
|
|
|
3825
4074
|
};
|
|
3826
4075
|
supportsStdinNotification = true;
|
|
3827
4076
|
busyDeliveryMode = "direct";
|
|
4077
|
+
capabilities = {
|
|
4078
|
+
reasoningEffort: false,
|
|
4079
|
+
fastMode: false,
|
|
4080
|
+
disallowedTools: false,
|
|
4081
|
+
command: true,
|
|
4082
|
+
sessionResumeMode: "by-id"
|
|
4083
|
+
};
|
|
3828
4084
|
sessionId = "";
|
|
3829
4085
|
sentInit = false;
|
|
3830
|
-
promptRequestId =
|
|
4086
|
+
promptRequestId = randomUUID3();
|
|
3831
4087
|
probe() {
|
|
3832
4088
|
return probeCliRuntime("kimi");
|
|
3833
4089
|
}
|
|
3834
4090
|
async spawn(ctx) {
|
|
3835
|
-
this.sessionId = ctx.config.sessionId ||
|
|
4091
|
+
this.sessionId = ctx.config.sessionId || randomUUID3();
|
|
3836
4092
|
const isResume = Boolean(ctx.config.sessionId);
|
|
3837
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
4093
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3838
4094
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
3839
4095
|
const args = ["--wire", "--yolo", "--session", this.sessionId];
|
|
3840
4096
|
if (f.model)
|
|
3841
4097
|
args.push("--model", f.model);
|
|
3842
|
-
const spec = resolveSpawnSpec("kimi", args);
|
|
4098
|
+
const spec = resolveSpawnSpec("kimi", args, f.command);
|
|
3843
4099
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3844
4100
|
cwd: ctx.workingDirectory,
|
|
3845
4101
|
env: spawnEnv,
|
|
3846
4102
|
shell: spec.shell
|
|
3847
4103
|
});
|
|
3848
|
-
proc.stdin?.write(
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
params: {
|
|
3853
|
-
protocol_version: "1.3",
|
|
3854
|
-
client: { name: "agent-backend", version: "1.0.0" },
|
|
3855
|
-
capabilities: { supports_question: false, supports_plan_mode: false }
|
|
3856
|
-
}
|
|
4104
|
+
proc.stdin?.write(jsonRpcRequest("initialize", {
|
|
4105
|
+
protocol_version: KIMI_WIRE_PROTOCOL_VERSION,
|
|
4106
|
+
client: getDaemonClientInfo(),
|
|
4107
|
+
capabilities: { supports_question: false, supports_plan_mode: false }
|
|
3857
4108
|
}) + `
|
|
3858
4109
|
`);
|
|
3859
|
-
proc.stdin?.write(
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
method: "prompt",
|
|
3863
|
-
params: {
|
|
3864
|
-
user_input: isResume ? ctx.prompt : "Your system prompt contains your standing instructions. Follow it now and begin listening for messages."
|
|
3865
|
-
}
|
|
3866
|
-
}) + `
|
|
4110
|
+
proc.stdin?.write(jsonRpcRequest("prompt", {
|
|
4111
|
+
user_input: isResume ? ctx.prompt : "Your system prompt contains your standing instructions. Follow it now and begin listening for messages."
|
|
4112
|
+
}, this.promptRequestId) + `
|
|
3867
4113
|
`);
|
|
3868
4114
|
return { process: proc };
|
|
3869
4115
|
}
|
|
3870
4116
|
parseLine(line) {
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
msg = JSON.parse(line);
|
|
3874
|
-
} catch {
|
|
4117
|
+
const msg = tryParseJsonLine(line);
|
|
4118
|
+
if (!msg)
|
|
3875
4119
|
return [];
|
|
3876
|
-
}
|
|
3877
4120
|
const out = [];
|
|
3878
4121
|
if (!this.sentInit) {
|
|
3879
4122
|
this.sentInit = true;
|
|
@@ -3925,16 +4168,17 @@ class KimiDriver {
|
|
|
3925
4168
|
}
|
|
3926
4169
|
encodeStdinMessage(text, _sessionId, opts) {
|
|
3927
4170
|
const method = opts?.mode === "idle" ? "prompt" : "steer";
|
|
3928
|
-
return
|
|
4171
|
+
return jsonRpcRequest(method, { user_input: text });
|
|
3929
4172
|
}
|
|
3930
4173
|
buildSystemPrompt(config) {
|
|
3931
|
-
return buildCliTransportSystemPrompt(config
|
|
4174
|
+
return buildCliTransportSystemPrompt(config);
|
|
3932
4175
|
}
|
|
3933
4176
|
}
|
|
3934
4177
|
|
|
3935
4178
|
// src/drivers/pi.ts
|
|
3936
|
-
import { createRequire as
|
|
3937
|
-
import {
|
|
4179
|
+
import { createRequire as createRequire3 } from "module";
|
|
4180
|
+
import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync5, realpathSync } from "fs";
|
|
4181
|
+
import { homedir as homedir2 } from "os";
|
|
3938
4182
|
import * as path8 from "path";
|
|
3939
4183
|
|
|
3940
4184
|
// src/runtime/sdkRuntimeSession.ts
|
|
@@ -4044,6 +4288,24 @@ function resolvePiSdkPackageDir(deps = {}) {
|
|
|
4044
4288
|
} catch {}
|
|
4045
4289
|
return;
|
|
4046
4290
|
}
|
|
4291
|
+
function findPiSessionFile(sessionDir, sessionId) {
|
|
4292
|
+
let entries;
|
|
4293
|
+
try {
|
|
4294
|
+
entries = readdirSync(sessionDir);
|
|
4295
|
+
} catch {
|
|
4296
|
+
return null;
|
|
4297
|
+
}
|
|
4298
|
+
const suffix = `_${sessionId}.jsonl`;
|
|
4299
|
+
const match = entries.find((entry) => entry.endsWith(suffix));
|
|
4300
|
+
return match ? path8.join(sessionDir, match) : null;
|
|
4301
|
+
}
|
|
4302
|
+
function resolvePiSessionDir(sdk, cwd) {
|
|
4303
|
+
if (typeof sdk.getDefaultSessionDir === "function")
|
|
4304
|
+
return sdk.getDefaultSessionDir(cwd);
|
|
4305
|
+
const agentDir = typeof sdk.getAgentDir === "function" ? sdk.getAgentDir() : path8.join(homedir2(), ".pi", "agent");
|
|
4306
|
+
const encoded = `--${path8.resolve(cwd).replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
4307
|
+
return path8.join(agentDir, "sessions", encoded);
|
|
4308
|
+
}
|
|
4047
4309
|
function resolvePiSdkVersionFromPath(deps = {}) {
|
|
4048
4310
|
const dir = resolvePiSdkPackageDir(deps);
|
|
4049
4311
|
if (!dir)
|
|
@@ -4057,7 +4319,7 @@ function resolvePiSdkVersionFromPath(deps = {}) {
|
|
|
4057
4319
|
}
|
|
4058
4320
|
function readPiSdkVersion() {
|
|
4059
4321
|
try {
|
|
4060
|
-
const req =
|
|
4322
|
+
const req = createRequire3(import.meta.url);
|
|
4061
4323
|
const pkg = req("@earendil-works/pi-coding-agent/package.json");
|
|
4062
4324
|
if (pkg.version)
|
|
4063
4325
|
return pkg.version;
|
|
@@ -4108,6 +4370,13 @@ class PiDriver {
|
|
|
4108
4370
|
supportsStdinNotification = true;
|
|
4109
4371
|
busyDeliveryMode = "direct";
|
|
4110
4372
|
supportsNativeStandingPrompt = true;
|
|
4373
|
+
capabilities = {
|
|
4374
|
+
reasoningEffort: true,
|
|
4375
|
+
fastMode: false,
|
|
4376
|
+
disallowedTools: false,
|
|
4377
|
+
command: true,
|
|
4378
|
+
sessionResumeMode: "by-id"
|
|
4379
|
+
};
|
|
4111
4380
|
sessionId = null;
|
|
4112
4381
|
probe() {
|
|
4113
4382
|
const version = readPiSdkVersion();
|
|
@@ -4121,10 +4390,6 @@ class PiDriver {
|
|
|
4121
4390
|
}
|
|
4122
4391
|
async createSession(ctx, deps) {
|
|
4123
4392
|
const spawnEnv = await deps.buildSpawnEnv();
|
|
4124
|
-
if (ctx.standingPrompt) {
|
|
4125
|
-
mkdirSync7(ctx.workingDirectory, { recursive: true });
|
|
4126
|
-
writeAgentFile(ctx.workingDirectory, ctx.standingPrompt);
|
|
4127
|
-
}
|
|
4128
4393
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
4129
4394
|
const { session, sessionId } = await deps.createAgentSession({
|
|
4130
4395
|
cwd: ctx.workingDirectory,
|
|
@@ -4158,7 +4423,7 @@ class PiDriver {
|
|
|
4158
4423
|
return null;
|
|
4159
4424
|
}
|
|
4160
4425
|
buildSystemPrompt(config) {
|
|
4161
|
-
return buildCliTransportSystemPrompt(config
|
|
4426
|
+
return buildCliTransportSystemPrompt(config);
|
|
4162
4427
|
}
|
|
4163
4428
|
}
|
|
4164
4429
|
|
|
@@ -4261,7 +4526,21 @@ async function importPiSdkFromGlobalInstall() {
|
|
|
4261
4526
|
const pkg = JSON.parse(readFileSync6(path10.join(dir, "package.json"), "utf-8"));
|
|
4262
4527
|
const entry = pkg.exports?.["."]?.import ?? pkg.main ?? "./dist/index.js";
|
|
4263
4528
|
const entryPath = path10.join(dir, entry);
|
|
4264
|
-
|
|
4529
|
+
const barrel = await import(pathToFileURL(entryPath).href);
|
|
4530
|
+
return withSessionDirHelper(barrel, entryPath);
|
|
4531
|
+
}
|
|
4532
|
+
async function withSessionDirHelper(barrel, entryPath) {
|
|
4533
|
+
if (typeof barrel.getDefaultSessionDir === "function")
|
|
4534
|
+
return barrel;
|
|
4535
|
+
try {
|
|
4536
|
+
const deepPath = path10.join(path10.dirname(entryPath), "core", "session-manager.js");
|
|
4537
|
+
const deep = await import(pathToFileURL(deepPath).href);
|
|
4538
|
+
if (typeof deep.getDefaultSessionDir !== "function")
|
|
4539
|
+
return barrel;
|
|
4540
|
+
return { ...barrel, getDefaultSessionDir: deep.getDefaultSessionDir };
|
|
4541
|
+
} catch {
|
|
4542
|
+
return barrel;
|
|
4543
|
+
}
|
|
4265
4544
|
}
|
|
4266
4545
|
function loadPiSdkModule() {
|
|
4267
4546
|
if (!cachedSdkPromise) {
|
|
@@ -4304,7 +4583,15 @@ function createPiSdkDriverDeps(ctx, loadSdk = loadPiSdkModule) {
|
|
|
4304
4583
|
const parsed = parseModelString(opts.model);
|
|
4305
4584
|
const model = parsed ? modelRegistry.find(parsed.provider, parsed.id) : undefined;
|
|
4306
4585
|
const cwd = opts.cwd;
|
|
4307
|
-
const
|
|
4586
|
+
const requestedSessionId = opts.sessionId;
|
|
4587
|
+
let sessionManager;
|
|
4588
|
+
if (requestedSessionId) {
|
|
4589
|
+
const sessionDir = resolvePiSessionDir(sdk, cwd);
|
|
4590
|
+
const existingFile = findPiSessionFile(sessionDir, requestedSessionId);
|
|
4591
|
+
sessionManager = existingFile ? sdk.SessionManager.open(existingFile, sessionDir, cwd) : sdk.SessionManager.create(cwd, sessionDir, { id: requestedSessionId });
|
|
4592
|
+
} else {
|
|
4593
|
+
sessionManager = sdk.SessionManager.create(cwd);
|
|
4594
|
+
}
|
|
4308
4595
|
const spawnEnv = opts.spawnEnv;
|
|
4309
4596
|
const bashTool = sdk.createBashToolDefinition(cwd, {
|
|
4310
4597
|
spawnHook: (spawnCtx) => ({ ...spawnCtx, env: { ...spawnCtx.env, ...spawnEnv } })
|
|
@@ -4357,7 +4644,7 @@ function emitImplicitTypingStopOnSend(args) {
|
|
|
4357
4644
|
}
|
|
4358
4645
|
async function createDaemon(opts) {
|
|
4359
4646
|
const log = opts.logger ?? createLogger({ header: "@alook/daemon" });
|
|
4360
|
-
const fallbackBase = (process.env.ALOOK_PROJECT_ROOT || `${
|
|
4647
|
+
const fallbackBase = (process.env.ALOOK_PROJECT_ROOT || `${homedir3()}/.alook`) + "/daemon";
|
|
4361
4648
|
const workdirFor = (agentId) => `${opts.workingDirectoryBase ?? fallbackBase}/${agentId}`;
|
|
4362
4649
|
const resolvedCliPath = resolveAlookCliPathWithFallback(opts.agentCliPath);
|
|
4363
4650
|
const timeline2 = createTimelineRecorder({
|
|
@@ -4652,6 +4939,11 @@ async function createDaemon(opts) {
|
|
|
4652
4939
|
await router.start();
|
|
4653
4940
|
return {
|
|
4654
4941
|
isOpen: () => channel.status === "open",
|
|
4942
|
+
onOpen: (hook) => {
|
|
4943
|
+
channel.onOpen(hook);
|
|
4944
|
+
if (channel.status === "open")
|
|
4945
|
+
queueMicrotask(hook);
|
|
4946
|
+
},
|
|
4655
4947
|
proxyUrl: proxy.url,
|
|
4656
4948
|
stop: async () => {
|
|
4657
4949
|
for (const agentId of [...typingHeartbeats.keys()]) {
|
|
@@ -4665,24 +4957,19 @@ async function createDaemon(opts) {
|
|
|
4665
4957
|
}
|
|
4666
4958
|
|
|
4667
4959
|
// src/cli/daemonStart.ts
|
|
4668
|
-
var requireFromHere = createRequire3(import.meta.url);
|
|
4669
|
-
function readDaemonVersion() {
|
|
4670
|
-
try {
|
|
4671
|
-
const pkg = requireFromHere("../../package.json");
|
|
4672
|
-
return pkg.version ?? "";
|
|
4673
|
-
} catch {
|
|
4674
|
-
return "";
|
|
4675
|
-
}
|
|
4676
|
-
}
|
|
4677
4960
|
var CAPABILITIES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels", "knowledge", "attach"];
|
|
4961
|
+
var STOP_GRACE_MS = 5000;
|
|
4962
|
+
var POLL_MS2 = 100;
|
|
4963
|
+
var MACHINE_KEY_HASH_PREFIX_LEN = 12;
|
|
4964
|
+
var MACHINE_KEY_DISPLAY_PREFIX_LEN = 20;
|
|
4678
4965
|
function resolveDefaultBaseDir() {
|
|
4679
|
-
const root = process.env.ALOOK_PROJECT_ROOT || path11.join(
|
|
4966
|
+
const root = process.env.ALOOK_PROJECT_ROOT || path11.join(homedir4(), ".alook");
|
|
4680
4967
|
return path11.join(root, "daemon");
|
|
4681
4968
|
}
|
|
4682
4969
|
var DEFAULT_BASE_DIR = resolveDefaultBaseDir();
|
|
4683
4970
|
var log = createLogger({ header: "@alook/daemon" });
|
|
4684
4971
|
function keyHash(machineKey) {
|
|
4685
|
-
return crypto2.createHash("sha256").update(machineKey).digest("hex").slice(0,
|
|
4972
|
+
return crypto2.createHash("sha256").update(machineKey).digest("hex").slice(0, MACHINE_KEY_HASH_PREFIX_LEN);
|
|
4686
4973
|
}
|
|
4687
4974
|
function daemonsDir(baseDir) {
|
|
4688
4975
|
return path11.join(baseDir, "daemons");
|
|
@@ -4750,14 +5037,14 @@ function daemonList(opts) {
|
|
|
4750
5037
|
}
|
|
4751
5038
|
results.push({
|
|
4752
5039
|
keyHash: file.replace(".pid", ""),
|
|
4753
|
-
keyPrefix: data.key.slice(0,
|
|
5040
|
+
keyPrefix: data.key.slice(0, MACHINE_KEY_DISPLAY_PREFIX_LEN) + "…",
|
|
4754
5041
|
pid: data.pid,
|
|
4755
5042
|
alive
|
|
4756
5043
|
});
|
|
4757
5044
|
}
|
|
4758
5045
|
return results;
|
|
4759
5046
|
}
|
|
4760
|
-
function daemonStop(opts) {
|
|
5047
|
+
async function daemonStop(opts) {
|
|
4761
5048
|
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
4762
5049
|
const pf = pidfilePath(baseDir, opts.machineKey);
|
|
4763
5050
|
const data = readPidFile(pf);
|
|
@@ -4774,13 +5061,12 @@ function daemonStop(opts) {
|
|
|
4774
5061
|
}
|
|
4775
5062
|
log.info(`sending SIGTERM to daemon (pid ${data.pid})…`);
|
|
4776
5063
|
process.kill(data.pid, "SIGTERM");
|
|
4777
|
-
const deadline = Date.now() +
|
|
5064
|
+
const deadline = Date.now() + STOP_GRACE_MS;
|
|
4778
5065
|
while (Date.now() < deadline && isProcessAlive(data.pid)) {
|
|
4779
|
-
|
|
4780
|
-
while (Date.now() - start < 100) {}
|
|
5066
|
+
await new Promise((r) => setTimeout(r, POLL_MS2));
|
|
4781
5067
|
}
|
|
4782
5068
|
if (isProcessAlive(data.pid)) {
|
|
4783
|
-
log.error(`daemon (pid ${data.pid}) did not exit in
|
|
5069
|
+
log.error(`daemon (pid ${data.pid}) did not exit in ${STOP_GRACE_MS / 1000}s — sending SIGKILL`);
|
|
4784
5070
|
process.kill(data.pid, "SIGKILL");
|
|
4785
5071
|
} else {
|
|
4786
5072
|
log.info("daemon stopped");
|
|
@@ -4792,9 +5078,6 @@ function daemonStop(opts) {
|
|
|
4792
5078
|
function credentialFilePathByMachineId(baseDir, machineId) {
|
|
4793
5079
|
return path11.join(daemonsDir(baseDir), `${machineId}.credential.json`);
|
|
4794
5080
|
}
|
|
4795
|
-
function credentialFilesDir(baseDir) {
|
|
4796
|
-
return daemonsDir(baseDir);
|
|
4797
|
-
}
|
|
4798
5081
|
function readCredentialFile(filePath) {
|
|
4799
5082
|
if (!fs9.existsSync(filePath))
|
|
4800
5083
|
return null;
|
|
@@ -4811,7 +5094,7 @@ function writeCredentialFile(filePath, credential, machineId) {
|
|
|
4811
5094
|
fs9.writeFileSync(filePath, JSON.stringify({ credential, machineId }), { mode: 384 });
|
|
4812
5095
|
}
|
|
4813
5096
|
function findExistingCredentialForBearer(baseDir, bearer) {
|
|
4814
|
-
const dir =
|
|
5097
|
+
const dir = daemonsDir(baseDir);
|
|
4815
5098
|
if (!fs9.existsSync(dir))
|
|
4816
5099
|
return null;
|
|
4817
5100
|
for (const file of fs9.readdirSync(dir)) {
|
|
@@ -4922,16 +5205,9 @@ async function daemonStart(opts) {
|
|
|
4922
5205
|
}
|
|
4923
5206
|
});
|
|
4924
5207
|
log.info(`daemon up — proxy at ${daemon.proxyUrl}, dialing ${wsUrl}`);
|
|
4925
|
-
|
|
4926
|
-
if (daemon.isOpen()) {
|
|
4927
|
-
clearInterval(readyTimer);
|
|
4928
|
-
log.info("control plane OPEN");
|
|
4929
|
-
}
|
|
4930
|
-
}, 200);
|
|
4931
|
-
readyTimer.unref?.();
|
|
5208
|
+
daemon.onOpen(() => log.info("control plane OPEN"));
|
|
4932
5209
|
const shutdown = async () => {
|
|
4933
5210
|
log.info("shutting down…");
|
|
4934
|
-
clearInterval(readyTimer);
|
|
4935
5211
|
releaseLock2(pf);
|
|
4936
5212
|
await daemon.stop();
|
|
4937
5213
|
process.exit(0);
|
|
@@ -4954,12 +5230,23 @@ function parseInviteToken(input) {
|
|
|
4954
5230
|
return BARE_TOKEN_RE.test(trimmed) ? trimmed : null;
|
|
4955
5231
|
}
|
|
4956
5232
|
|
|
5233
|
+
// ../shared/src/constants/community.ts
|
|
5234
|
+
var MAX_EMOJI_BYTES = 32;
|
|
5235
|
+
var MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
|
|
5236
|
+
var MAX_SERVER_ICON_SIZE_BYTES = 5 * 1024 * 1024;
|
|
5237
|
+
var MAX_ICON_SOURCE_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
|
5238
|
+
|
|
4957
5239
|
// src/cli/index.ts
|
|
4958
5240
|
function messagesInLocalTime(messages) {
|
|
4959
5241
|
return messages.map((m) => ({ ...m, time: toLocalISO(m.time) }));
|
|
4960
5242
|
}
|
|
4961
5243
|
|
|
4962
5244
|
class CliError extends Error {
|
|
5245
|
+
hint;
|
|
5246
|
+
constructor(message, hint) {
|
|
5247
|
+
super(message);
|
|
5248
|
+
this.hint = hint;
|
|
5249
|
+
}
|
|
4963
5250
|
}
|
|
4964
5251
|
function printEnvelope(env) {
|
|
4965
5252
|
const out = {};
|
|
@@ -5040,6 +5327,14 @@ async function cmdMessageSend(opts) {
|
|
|
5040
5327
|
const channel = opts.target;
|
|
5041
5328
|
if (!channel)
|
|
5042
5329
|
throw new CliError("message send: --target <ref> is required (e.g. /demo-workspace/general)");
|
|
5330
|
+
const chaoticLevel = opts.chaotic_level || opts.chaoticLevel;
|
|
5331
|
+
const chaoticHint = "Re-read the Chaos Awareness section in AGENTS.md and reflect before sending.";
|
|
5332
|
+
if (!chaoticLevel || chaoticLevel !== "fine" && chaoticLevel !== "severe") {
|
|
5333
|
+
throw new CliError("message send: --chaotic_level must be 'fine' or 'severe'.", chaoticHint);
|
|
5334
|
+
}
|
|
5335
|
+
if (chaoticLevel === "severe") {
|
|
5336
|
+
throw new CliError("message send: --chaotic_level is 'severe'.", chaoticHint);
|
|
5337
|
+
}
|
|
5043
5338
|
let text;
|
|
5044
5339
|
const fileFlag = opts.file;
|
|
5045
5340
|
const textFlag = opts.text;
|
|
@@ -5067,6 +5362,34 @@ async function cmdMessageSend(opts) {
|
|
|
5067
5362
|
}
|
|
5068
5363
|
return { sent: `${res.message.channel}${res.message.seq}` };
|
|
5069
5364
|
}
|
|
5365
|
+
async function cmdMessageEmoji(opts) {
|
|
5366
|
+
const api = getApi();
|
|
5367
|
+
const target = opts.target;
|
|
5368
|
+
const emoji = opts.emoji;
|
|
5369
|
+
if (!target)
|
|
5370
|
+
throw new CliError("message emoji: --target <ref> is required (e.g. /demo/general#42)");
|
|
5371
|
+
if (!emoji)
|
|
5372
|
+
throw new CliError("message emoji: --emoji <string> is required");
|
|
5373
|
+
let parsed;
|
|
5374
|
+
try {
|
|
5375
|
+
parsed = parseRef(target);
|
|
5376
|
+
} catch (err) {
|
|
5377
|
+
throw new CliError(`message emoji: ${err.message}`);
|
|
5378
|
+
}
|
|
5379
|
+
if (parsed.seq === undefined) {
|
|
5380
|
+
const err = new CliError(`message emoji needs a ref with a seq (e.g. ${target}#42)`);
|
|
5381
|
+
err.hint = "pass --target /<server>/<channel>#N, /<server>/<channel>/#N#M for thread reply, or /.dm/<peer>#N";
|
|
5382
|
+
throw err;
|
|
5383
|
+
}
|
|
5384
|
+
if (Buffer.byteLength(emoji, "utf8") > MAX_EMOJI_BYTES) {
|
|
5385
|
+
const err = new CliError("emoji is too long");
|
|
5386
|
+
err.hint = "use a single emoji, not a phrase";
|
|
5387
|
+
throw err;
|
|
5388
|
+
}
|
|
5389
|
+
const channel = parsed.threadRootSeq !== undefined ? `/${parsed.server}/${parsed.channel}/#${parsed.threadRootSeq}` : `/${parsed.server}/${parsed.channel}`;
|
|
5390
|
+
const res = await api.reactAdd({ channel, seq: parsed.seq, emoji });
|
|
5391
|
+
return { target, emoji, duplicate: res.duplicate === true };
|
|
5392
|
+
}
|
|
5070
5393
|
async function cmdAttachmentUpload(opts) {
|
|
5071
5394
|
const api = getApi();
|
|
5072
5395
|
const agent = agentId(opts);
|
|
@@ -5133,6 +5456,7 @@ async function cmdInboxPull(opts) {
|
|
|
5133
5456
|
const { messages, hasMore } = await api.inboxPull({ agentId: agent, max });
|
|
5134
5457
|
const pulledAt = nowLocalISO();
|
|
5135
5458
|
let acked = 0;
|
|
5459
|
+
let ackError;
|
|
5136
5460
|
if (opts.ack !== false && messages.length > 0) {
|
|
5137
5461
|
const latest = new Map;
|
|
5138
5462
|
for (const m of messages) {
|
|
@@ -5141,10 +5465,20 @@ async function cmdInboxPull(opts) {
|
|
|
5141
5465
|
if (!cur || seqN > cur.seq)
|
|
5142
5466
|
latest.set(m.channel, { channel: m.channel, seq: seqN });
|
|
5143
5467
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5468
|
+
try {
|
|
5469
|
+
await api.ack({ agentId: agent, cursors: [...latest.values()] });
|
|
5470
|
+
acked = latest.size;
|
|
5471
|
+
} catch (err) {
|
|
5472
|
+
ackError = err instanceof Error ? err.message : String(err);
|
|
5473
|
+
}
|
|
5146
5474
|
}
|
|
5147
|
-
return {
|
|
5475
|
+
return {
|
|
5476
|
+
messages: messagesInLocalTime(messages),
|
|
5477
|
+
hasMore,
|
|
5478
|
+
acked,
|
|
5479
|
+
pulledAt,
|
|
5480
|
+
...ackError ? { ackError } : {}
|
|
5481
|
+
};
|
|
5148
5482
|
}
|
|
5149
5483
|
async function cmdServerList(opts) {
|
|
5150
5484
|
const api = getApi();
|
|
@@ -5213,12 +5547,18 @@ function buildProgram() {
|
|
|
5213
5547
|
}).option("--agent <id>", "agent identity (or ALOOK_AGENT_ID env)");
|
|
5214
5548
|
const message = program.command("message").description("message operations").exitOverride();
|
|
5215
5549
|
message.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
5216
|
-
message.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace/general)").option("--text <text>", "inline message body (short messages)").option("--file <path>", "read message body from a file (long messages)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
5550
|
+
message.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace/general)").option("--text <text>", "inline message body (short messages)").option("--file <path>", "read message body from a file (long messages)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).option("--chaotic_level <level>", "chaos level: 'fine' or 'severe' (required)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
5217
5551
|
const localOpts = this.opts();
|
|
5218
5552
|
const globalOpts = program.opts();
|
|
5219
5553
|
const result = await cmdMessageSend({ ...globalOpts, ...localOpts });
|
|
5220
5554
|
printEnvelope({ success: result });
|
|
5221
5555
|
});
|
|
5556
|
+
message.command("emoji").description("react to a message with a single emoji").requiredOption("--target <ref>", "message ref (path-style, e.g. /demo/general#42 or /.dm/peer#7)").requiredOption("--emoji <string>", "single emoji character").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
5557
|
+
const localOpts = this.opts();
|
|
5558
|
+
const globalOpts = program.opts();
|
|
5559
|
+
const result = await cmdMessageEmoji({ ...globalOpts, ...localOpts });
|
|
5560
|
+
printEnvelope({ success: result });
|
|
5561
|
+
});
|
|
5222
5562
|
const attachment = message.command("attachment").description("attachment operations").exitOverride();
|
|
5223
5563
|
attachment.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
5224
5564
|
attachment.command("upload").description("upload a local file as a pending attachment for a future send").option("--target <ref>", "destination (channel, DM, or thread ref)").option("--file <path>", "local file to upload").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
@@ -5292,9 +5632,9 @@ function buildProgram() {
|
|
|
5292
5632
|
baseDir: localOpts.baseDir
|
|
5293
5633
|
});
|
|
5294
5634
|
});
|
|
5295
|
-
daemon.command("stop").description("stop the daemon for a specific machine key").requiredOption("--machine-key <key>", "machine key identifying which daemon to stop").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(function() {
|
|
5635
|
+
daemon.command("stop").description("stop the daemon for a specific machine key").requiredOption("--machine-key <key>", "machine key identifying which daemon to stop").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
5296
5636
|
const localOpts = this.opts();
|
|
5297
|
-
daemonStop({
|
|
5637
|
+
await daemonStop({
|
|
5298
5638
|
machineKey: localOpts.machineKey,
|
|
5299
5639
|
baseDir: localOpts.baseDir
|
|
5300
5640
|
});
|
|
@@ -5342,8 +5682,13 @@ function getHelpText(program, argv) {
|
|
|
5342
5682
|
}
|
|
5343
5683
|
return cmd.helpInformation();
|
|
5344
5684
|
}
|
|
5345
|
-
var
|
|
5346
|
-
|
|
5685
|
+
var isMainModule = false;
|
|
5686
|
+
try {
|
|
5687
|
+
if (typeof process !== "undefined" && process.argv[1]) {
|
|
5688
|
+
isMainModule = import.meta.url === pathToFileURL2(realpathSync2(process.argv[1])).href;
|
|
5689
|
+
}
|
|
5690
|
+
} catch {}
|
|
5691
|
+
if (isMainModule) {
|
|
5347
5692
|
main().then((code) => process.exit(code));
|
|
5348
5693
|
}
|
|
5349
5694
|
export {
|