@alook/daemon 0.0.159 → 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 +1935 -1821
- package/dist/index.js +440 -302
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -188,12 +188,11 @@ import * as fs9 from "fs";
|
|
|
188
188
|
import * as path11 from "path";
|
|
189
189
|
import * as crypto2 from "crypto";
|
|
190
190
|
import * as os3 from "os";
|
|
191
|
-
import { homedir as
|
|
191
|
+
import { homedir as homedir4 } from "os";
|
|
192
192
|
import { WebSocket } from "ws";
|
|
193
|
-
import { createRequire as createRequire3 } from "module";
|
|
194
193
|
|
|
195
194
|
// src/daemon/createDaemon.ts
|
|
196
|
-
import { homedir as
|
|
195
|
+
import { homedir as homedir3 } from "os";
|
|
197
196
|
|
|
198
197
|
// src/logger.ts
|
|
199
198
|
var LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
@@ -245,6 +244,10 @@ function createLogger(options = {}) {
|
|
|
245
244
|
}
|
|
246
245
|
|
|
247
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;
|
|
248
251
|
function describeErr(err) {
|
|
249
252
|
return err instanceof Error ? err.message : String(err);
|
|
250
253
|
}
|
|
@@ -354,7 +357,7 @@ class WsControlChannel {
|
|
|
354
357
|
ws.on("message", (data) => this.onMessage(data));
|
|
355
358
|
ws.on("pong", () => {
|
|
356
359
|
this.attempt = 0;
|
|
357
|
-
this.pongDeadline = this.now() + (this.opts.heartbeat?.pongTimeoutMs ??
|
|
360
|
+
this.pongDeadline = this.now() + (this.opts.heartbeat?.pongTimeoutMs ?? DEFAULT_PONG_TIMEOUT_MS);
|
|
358
361
|
this.log.debug("heartbeat pong");
|
|
359
362
|
});
|
|
360
363
|
ws.on("close", (code, reason) => this.onSocketClosed(code, reason));
|
|
@@ -400,8 +403,8 @@ class WsControlChannel {
|
|
|
400
403
|
this.scheduleReconnect();
|
|
401
404
|
}
|
|
402
405
|
scheduleReconnect() {
|
|
403
|
-
const base = this.opts.reconnect?.baseMs ??
|
|
404
|
-
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;
|
|
405
408
|
const maxAttempts = this.opts.reconnect?.maxAttempts ?? Infinity;
|
|
406
409
|
if (this.attempt >= maxAttempts) {
|
|
407
410
|
this.statusValue = "closed";
|
|
@@ -414,8 +417,8 @@ class WsControlChannel {
|
|
|
414
417
|
setTimeout(() => this.openSocket(), delayMs);
|
|
415
418
|
}
|
|
416
419
|
startHeartbeat() {
|
|
417
|
-
const interval = this.opts.heartbeat?.pingIntervalMs ??
|
|
418
|
-
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;
|
|
419
422
|
this.pongDeadline = this.now() + timeout;
|
|
420
423
|
this.pingTimer = setInterval(() => {
|
|
421
424
|
if (this.now() > this.pongDeadline) {
|
|
@@ -970,6 +973,7 @@ function onExit(state, agentId) {
|
|
|
970
973
|
agent.turnActive = false;
|
|
971
974
|
if (agent.resetting)
|
|
972
975
|
agent.resetting = false;
|
|
976
|
+
agent.apm = createInitialApmGatedSteeringState();
|
|
973
977
|
if (agent.inbox.length > 0) {
|
|
974
978
|
agent.status = "starting";
|
|
975
979
|
const prompt = drainInboxToPrompt(agent);
|
|
@@ -1055,7 +1059,8 @@ import { EventEmitter } from "events";
|
|
|
1055
1059
|
// src/runtime/killTree.ts
|
|
1056
1060
|
import { spawn } from "child_process";
|
|
1057
1061
|
var POLL_MS = 100;
|
|
1058
|
-
var
|
|
1062
|
+
var SESSION_STOP_GRACE_MS = 2000;
|
|
1063
|
+
var DEFAULT_GRACE_MS = SESSION_STOP_GRACE_MS;
|
|
1059
1064
|
var isPosix = process.platform !== "win32";
|
|
1060
1065
|
function spawnAgentProcess(command, args, opts) {
|
|
1061
1066
|
return spawn(command, args, {
|
|
@@ -1185,7 +1190,7 @@ class ChildProcessRuntimeSession {
|
|
|
1185
1190
|
this.requestedStopReason = opts?.reason;
|
|
1186
1191
|
const pid = proc.pid;
|
|
1187
1192
|
if (pid) {
|
|
1188
|
-
await killProcessTree(pid, { graceMs: opts?.forceAfterMs ??
|
|
1193
|
+
await killProcessTree(pid, { graceMs: opts?.forceAfterMs ?? SESSION_STOP_GRACE_MS });
|
|
1189
1194
|
} else {
|
|
1190
1195
|
proc.kill(opts?.signal ?? "SIGTERM");
|
|
1191
1196
|
}
|
|
@@ -1293,1765 +1298,1716 @@ class SdkManagedSession {
|
|
|
1293
1298
|
}
|
|
1294
1299
|
}
|
|
1295
1300
|
|
|
1296
|
-
// src/
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
const sign = tzOffset >= 0 ? "+" : "-";
|
|
1300
|
-
const abs = Math.abs(tzOffset);
|
|
1301
|
-
const hh = String(Math.floor(abs / 60)).padStart(2, "0");
|
|
1302
|
-
const mm = String(abs % 60).padStart(2, "0");
|
|
1303
|
-
const y = now.getFullYear();
|
|
1304
|
-
const mo = String(now.getMonth() + 1).padStart(2, "0");
|
|
1305
|
-
const d = String(now.getDate()).padStart(2, "0");
|
|
1306
|
-
const h = String(now.getHours()).padStart(2, "0");
|
|
1307
|
-
const mi = String(now.getMinutes()).padStart(2, "0");
|
|
1308
|
-
const s = String(now.getSeconds()).padStart(2, "0");
|
|
1309
|
-
const ms = String(now.getMilliseconds()).padStart(3, "0");
|
|
1310
|
-
return `${y}-${mo}-${d}T${h}:${mi}:${s}.${ms}${sign}${hh}:${mm}`;
|
|
1311
|
-
}
|
|
1312
|
-
function nowLocalISO() {
|
|
1313
|
-
return localISOString(new Date);
|
|
1314
|
-
}
|
|
1315
|
-
function toLocalISO(iso) {
|
|
1316
|
-
if (!iso)
|
|
1317
|
-
return iso;
|
|
1318
|
-
const d = new Date(iso);
|
|
1319
|
-
if (Number.isNaN(d.getTime()))
|
|
1320
|
-
return iso;
|
|
1321
|
-
return localISOString(d);
|
|
1322
|
-
}
|
|
1301
|
+
// src/drivers/cliTransport.ts
|
|
1302
|
+
import * as fs4 from "fs";
|
|
1303
|
+
import * as path4 from "path";
|
|
1323
1304
|
|
|
1324
|
-
// src/
|
|
1325
|
-
var
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
return "read";
|
|
1335
|
-
case "edit":
|
|
1336
|
-
case "multiedit":
|
|
1337
|
-
case "file_change":
|
|
1338
|
-
return "edit";
|
|
1339
|
-
case "write":
|
|
1340
|
-
return "write";
|
|
1341
|
-
case "grep":
|
|
1342
|
-
return "grep";
|
|
1343
|
-
case "glob":
|
|
1344
|
-
return "glob";
|
|
1345
|
-
case "find":
|
|
1346
|
-
return "find";
|
|
1347
|
-
case "ls":
|
|
1348
|
-
return "ls";
|
|
1349
|
-
case "notebookedit":
|
|
1350
|
-
case "notebook_edit":
|
|
1351
|
-
return "notebook_edit";
|
|
1352
|
-
case "websearch":
|
|
1353
|
-
case "web_search":
|
|
1354
|
-
return "web_search";
|
|
1355
|
-
case "webfetch":
|
|
1356
|
-
case "web_fetch":
|
|
1357
|
-
return "web_fetch";
|
|
1358
|
-
case "todowrite":
|
|
1359
|
-
case "todo_write":
|
|
1360
|
-
return "todo_write";
|
|
1361
|
-
default:
|
|
1362
|
-
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.");
|
|
1363
1315
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
switch (canonicalName) {
|
|
1367
|
-
case "bash":
|
|
1368
|
-
return "shell";
|
|
1369
|
-
case "read":
|
|
1370
|
-
case "edit":
|
|
1371
|
-
case "write":
|
|
1372
|
-
case "ls":
|
|
1373
|
-
case "notebook_edit":
|
|
1374
|
-
return "file_target";
|
|
1375
|
-
case "grep":
|
|
1376
|
-
case "glob":
|
|
1377
|
-
case "find":
|
|
1378
|
-
return "pattern";
|
|
1379
|
-
default:
|
|
1380
|
-
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).");
|
|
1381
1318
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
if (typeof input === "string") {
|
|
1385
|
-
try {
|
|
1386
|
-
const parsed = JSON.parse(input);
|
|
1387
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1388
|
-
return parsed;
|
|
1389
|
-
}
|
|
1390
|
-
} catch {
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
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.");
|
|
1394
1321
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
+
`);
|
|
1398
1328
|
}
|
|
1399
|
-
function
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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
|
+
`);
|
|
1408
1362
|
}
|
|
1409
|
-
function
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
+
`);
|
|
1420
1438
|
}
|
|
1421
|
-
function
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
return rec.path;
|
|
1431
|
-
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
|
+
`);
|
|
1432
1448
|
}
|
|
1433
|
-
function
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
return rec.name;
|
|
1445
|
-
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
|
+
`);
|
|
1446
1460
|
}
|
|
1447
|
-
function
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
+
`);
|
|
1451
1470
|
}
|
|
1452
|
-
function
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
+
`);
|
|
1460
1500
|
}
|
|
1461
|
-
function
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
+
`);
|
|
1486
1545
|
}
|
|
1487
|
-
function
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
+
`);
|
|
1498
1560
|
}
|
|
1499
1561
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
this.state = createInitialManagerState(this.opts.staleThresholdMs, this.opts.idleTimeoutMs);
|
|
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;
|
|
1524
1585
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
if (
|
|
1531
|
-
|
|
1532
|
-
const driver = this.opts.driverFor(agentId, this.runtimeConfigs.get(agentId));
|
|
1533
|
-
const caps = {
|
|
1534
|
-
lifecycleKind: driver.lifecycle.kind,
|
|
1535
|
-
supportsStdinNotification: driver.supportsStdinNotification,
|
|
1536
|
-
busyDeliveryMode: driver.busyDeliveryMode
|
|
1537
|
-
};
|
|
1538
|
-
this.dispatch({ type: "register", agentId, caps });
|
|
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;
|
|
1539
1593
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
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;
|
|
1542
1602
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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;
|
|
1548
1629
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1630
|
+
const linkPath = path3.join(binDir, cliName);
|
|
1631
|
+
try {
|
|
1632
|
+
fs3.unlinkSync(linkPath);
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
if (err.code !== "ENOENT")
|
|
1635
|
+
throw err;
|
|
1551
1636
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1637
|
+
try {
|
|
1638
|
+
fs3.symlinkSync(hostCliPath, linkPath);
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
if (err.code !== "EEXIST")
|
|
1641
|
+
throw err;
|
|
1554
1642
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
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;
|
|
1572
1660
|
}
|
|
1573
|
-
this.enqueueRewake(agentId, { text: opts.rewakePrompt });
|
|
1574
|
-
await this.stop(agentId);
|
|
1575
|
-
}
|
|
1576
|
-
start() {
|
|
1577
|
-
if (this.tickTimer)
|
|
1578
|
-
return;
|
|
1579
|
-
this.tickTimer = setInterval(() => this.dispatch({ type: "tick", nowMs: this.now() }), this.opts.tickIntervalMs);
|
|
1580
|
-
this.tickTimer.unref?.();
|
|
1581
1661
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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;
|
|
1588
1718
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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;
|
|
1593
1745
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
1603
|
-
launchId: this.launchIds.get(agentId) ?? null
|
|
1604
|
-
};
|
|
1605
|
-
}
|
|
1606
|
-
liveSessionReports() {
|
|
1607
|
-
return [...this.liveSessions.entries()].map(([agentId, sessionId]) => ({
|
|
1608
|
-
agentId,
|
|
1609
|
-
sessionId,
|
|
1610
|
-
launchId: this.launchIds.get(agentId) ?? ""
|
|
1611
|
-
}));
|
|
1612
|
-
}
|
|
1613
|
-
dispatch(event) {
|
|
1614
|
-
const before = this.deriveActivitySnapshot(this.state);
|
|
1615
|
-
const { state, effects } = reduceManager(this.state, event);
|
|
1616
|
-
this.state = state;
|
|
1617
|
-
for (const effect of effects)
|
|
1618
|
-
this.applyEffect(effect);
|
|
1619
|
-
if (this.opts.onAgentActivity) {
|
|
1620
|
-
const after = this.deriveActivitySnapshot(this.state);
|
|
1621
|
-
for (const [agentId, activity] of Object.entries(after)) {
|
|
1622
|
-
if (agentId in before && before[agentId] !== activity) {
|
|
1623
|
-
this.opts.onAgentActivity({ agentId, state: activity });
|
|
1624
|
-
}
|
|
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;
|
|
1625
1754
|
}
|
|
1626
1755
|
}
|
|
1627
1756
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
deriveActivity(agent) {
|
|
1635
|
-
if (agent.status === "running" && !agent.turnActive)
|
|
1636
|
-
return "idle";
|
|
1637
|
-
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");
|
|
1638
1763
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1764
|
+
ensureSymlinks(workDir);
|
|
1765
|
+
return changed;
|
|
1766
|
+
}
|
|
1641
1767
|
|
|
1642
|
-
|
|
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.");
|
|
1643
1787
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
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)");
|
|
1646
1791
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
this.doSpawn(effect.agentId, this.withFooter(effect.prompt), effect.resumeSessionId);
|
|
1651
|
-
break;
|
|
1652
|
-
case "send": {
|
|
1653
|
-
const session = this.sessions.get(effect.agentId);
|
|
1654
|
-
session?.send({ text: this.stampNow(this.withFooter(effect.text)), mode: effect.mode });
|
|
1655
|
-
this.log.info("steering message sent to running agent", { agentId: effect.agentId, mode: effect.mode });
|
|
1656
|
-
break;
|
|
1657
|
-
}
|
|
1658
|
-
case "stop":
|
|
1659
|
-
case "terminate_stalled": {
|
|
1660
|
-
const session = this.sessions.get(effect.agentId);
|
|
1661
|
-
Promise.resolve(session?.stop({ reason: effect.type, forceAfterMs: 5000 }));
|
|
1662
|
-
const spawnState = this.activeSpawnState.get(effect.agentId);
|
|
1663
|
-
if (spawnState)
|
|
1664
|
-
spawnState.suppressExitLog = true;
|
|
1665
|
-
this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled");
|
|
1666
|
-
this.opts.onAgentLocallyStopped?.({ agentId: effect.agentId, reason: effect.type });
|
|
1667
|
-
break;
|
|
1668
|
-
}
|
|
1669
|
-
case "gated_hold":
|
|
1670
|
-
this.log.info("gated busy message held", {
|
|
1671
|
-
agentId: effect.agentId,
|
|
1672
|
-
reason: effect.reason,
|
|
1673
|
-
blockedReason: effect.blockedReason,
|
|
1674
|
-
recentEvents: effect.recentEvents
|
|
1675
|
-
});
|
|
1676
|
-
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"])`);
|
|
1677
1795
|
}
|
|
1678
1796
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
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.");
|
|
1704
|
-
}
|
|
1705
|
-
if (!this.opts.sessionFactory && !driver.createSession && !ctx.credentialProxy) {
|
|
1706
|
-
throw new Error(`AgentProcessManager: real spawn of "${agentId}" needs a credentialProxy — ` + "set ManagerRuntimeOpts.credentialProxy (or baseContextFor's), or pass a sessionFactory for tests.");
|
|
1707
|
-
}
|
|
1708
|
-
const session = this.opts.sessionFactory ? this.opts.sessionFactory({ agentId, driver, ctx }) : driver.createSession ? new SdkManagedSession(driver, ctx, this.opts.sdkDriverDepsFor(ctx)) : createChildProcessRuntimeSession(driver, ctx);
|
|
1709
|
-
this.sessions.set(agentId, session);
|
|
1710
|
-
const state = { hasEstablished: false, hasReportedSpawnFailure: false, suppressExitLog: false };
|
|
1711
|
-
this.activeSpawnState.set(agentId, state);
|
|
1712
|
-
const reportSpawnFailure = (reason) => {
|
|
1713
|
-
if (state.hasEstablished || state.hasReportedSpawnFailure)
|
|
1714
|
-
return;
|
|
1715
|
-
state.hasReportedSpawnFailure = true;
|
|
1716
|
-
this.log.warn("spawn failed", { agentId, runtime: driver.id, reason });
|
|
1717
|
-
this.opts.onRuntimeSpawnFailed?.(driver.id, reason);
|
|
1718
|
-
};
|
|
1719
|
-
session.on("runtime_event", (e) => {
|
|
1720
|
-
if (!state.hasEstablished) {
|
|
1721
|
-
state.hasEstablished = true;
|
|
1722
|
-
}
|
|
1723
|
-
this.opts.onRuntimeSessionEstablished?.(driver.id);
|
|
1724
|
-
if (e?.kind === "turn_end" && driver.lifecycle.kind === "per_turn") {
|
|
1725
|
-
state.suppressExitLog = true;
|
|
1726
|
-
}
|
|
1727
|
-
this.onRuntimeEvent(agentId, e, driver.id);
|
|
1728
|
-
});
|
|
1729
|
-
session.on("stderr", (...args) => {
|
|
1730
|
-
const raw = typeof args[0] === "string" ? args[0] : String(args[0] ?? "");
|
|
1731
|
-
const text = raw.length > 2000 ? raw.slice(0, 2000) + "…" : raw;
|
|
1732
|
-
this.log.warn("runtime stderr", { agentId, runtime: driver.id, text });
|
|
1733
|
-
});
|
|
1734
|
-
session.on("error", (...args) => {
|
|
1735
|
-
const err = args[0];
|
|
1736
|
-
const code = err?.code ?? "spawn_error";
|
|
1737
|
-
reportSpawnFailure(String(code));
|
|
1738
|
-
});
|
|
1739
|
-
session.on("exit", () => {
|
|
1740
|
-
reportSpawnFailure("pre_handshake_exit");
|
|
1741
|
-
if (state.hasEstablished && !state.suppressExitLog)
|
|
1742
|
-
this.logSessionEnded(agentId, "exit");
|
|
1743
|
-
this.flushThinkingAudit(agentId);
|
|
1744
|
-
this.sessions.delete(agentId);
|
|
1745
|
-
this.liveSessions.delete(agentId);
|
|
1746
|
-
if (this.activeSpawnState.get(agentId) === state)
|
|
1747
|
-
this.activeSpawnState.delete(agentId);
|
|
1748
|
-
this.dispatch({ type: "exit", agentId });
|
|
1749
|
-
});
|
|
1750
|
-
const stampedPrompt = this.stampNow(prompt);
|
|
1751
|
-
Promise.resolve(session.start({ text: stampedPrompt, sessionId: ctx.config.sessionId })).then(() => {
|
|
1752
|
-
if (this.sessions.get(agentId) !== session)
|
|
1753
|
-
return;
|
|
1754
|
-
this.dispatch({ type: "spawned", agentId, nowMs: this.now() });
|
|
1755
|
-
}).catch((err) => {
|
|
1756
|
-
const code = err?.code ?? "spawn_threw";
|
|
1757
|
-
reportSpawnFailure(String(code));
|
|
1758
|
-
if (this.sessions.get(agentId) === session)
|
|
1759
|
-
this.sessions.delete(agentId);
|
|
1760
|
-
this.dispatch({ type: "exit", agentId });
|
|
1761
|
-
});
|
|
1762
|
-
}
|
|
1763
|
-
flushThinkingAudit(agentId) {
|
|
1764
|
-
const buffered = this.thinkingBuffers.get(agentId);
|
|
1765
|
-
if (!buffered)
|
|
1766
|
-
return;
|
|
1767
|
-
this.thinkingBuffers.delete(agentId);
|
|
1768
|
-
if (!this.opts.onBotAuditEvent)
|
|
1769
|
-
return;
|
|
1770
|
-
const { text, truncated, chars } = truncateThinking(buffered);
|
|
1771
|
-
try {
|
|
1772
|
-
this.opts.onBotAuditEvent(agentId, {
|
|
1773
|
-
kind: "thinking",
|
|
1774
|
-
payload: { text, truncated, chars }
|
|
1775
|
-
}, {
|
|
1776
|
-
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
1777
|
-
launchId: this.launchIds.get(agentId) ?? null
|
|
1778
|
-
});
|
|
1779
|
-
} catch {}
|
|
1780
|
-
}
|
|
1781
|
-
onRuntimeEvent(agentId, e, runtimeId) {
|
|
1782
|
-
const ev = e;
|
|
1783
|
-
if (!ev?.kind)
|
|
1784
|
-
return;
|
|
1785
|
-
if (this.opts.onBotAuditEvent) {
|
|
1786
|
-
if (ev.kind === "thinking" && typeof ev.text === "string") {
|
|
1787
|
-
if (ev.text.length > 0) {
|
|
1788
|
-
this.thinkingBuffers.set(agentId, (this.thinkingBuffers.get(agentId) ?? "") + ev.text);
|
|
1789
|
-
}
|
|
1790
|
-
} else {
|
|
1791
|
-
this.flushThinkingAudit(agentId);
|
|
1792
|
-
if (ev.kind === "tool_call" && typeof ev.name === "string") {
|
|
1793
|
-
const audit = extractToolAudit(ev.name, ev.input);
|
|
1794
|
-
if (!audit.suppressed) {
|
|
1795
|
-
const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
|
|
1796
|
-
try {
|
|
1797
|
-
this.opts.onBotAuditEvent(agentId, {
|
|
1798
|
-
kind: "tool_call",
|
|
1799
|
-
payload
|
|
1800
|
-
}, {
|
|
1801
|
-
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
1802
|
-
launchId: this.launchIds.get(agentId) ?? null
|
|
1803
|
-
});
|
|
1804
|
-
} catch {}
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
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"
|
|
1807
1821
|
}
|
|
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 }
|
|
1808
1835
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
this.opts.timeline?.setSession(agentId, ev.sessionId);
|
|
1813
|
-
this.opts.onAgentSession?.({
|
|
1814
|
-
agentId,
|
|
1815
|
-
sessionId: ev.sessionId,
|
|
1816
|
-
launchId: this.launchIds.get(agentId) ?? ""
|
|
1817
|
-
});
|
|
1818
|
-
this.log.info("agent session established", { agentId, sessionId: ev.sessionId, runtime: runtimeId });
|
|
1819
|
-
}
|
|
1820
|
-
if (ev.kind === "text" && typeof ev.text === "string" && ev.text.length > 0) {
|
|
1821
|
-
this.opts.timeline?.appendResponseToLatest(agentId, ev.text);
|
|
1822
|
-
}
|
|
1823
|
-
this.dispatch({ type: "progress", agentId, nowMs: this.now() });
|
|
1824
|
-
this.dispatch({ type: "runtime_signal", agentId, kind: ev.kind, nowMs: this.now() });
|
|
1825
|
-
if (ev.kind === "turn_end") {
|
|
1826
|
-
this.logSessionEnded(agentId, "turn_end");
|
|
1827
|
-
this.dispatch({ type: "turn_end", agentId, nowMs: this.now() });
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1836
|
+
];
|
|
1837
|
+
const { env: spawnEnv } = mergeEnvLayers(process.env, layers);
|
|
1838
|
+
return { stateDir, tokenFile, spawnEnv };
|
|
1830
1839
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
botId;
|
|
1834
|
-
constructor(botId) {
|
|
1835
|
-
super(`Bot not in this daemon's cache: ${botId}`);
|
|
1836
|
-
this.botId = botId;
|
|
1837
|
-
this.name = "UnknownBotError";
|
|
1838
|
-
}
|
|
1840
|
+
function buildCliTransportSystemPrompt(config) {
|
|
1841
|
+
return buildCliSystemPrompt(config);
|
|
1839
1842
|
}
|
|
1840
1843
|
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
return "internal_error";
|
|
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}`;
|
|
1857
1859
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
requested;
|
|
1861
|
-
available;
|
|
1862
|
-
constructor(requested, available) {
|
|
1863
|
-
super(`Runtime not available on this host: ${requested ?? "<unspecified>"} — installed: ${available.join(", ") || "(none)"}`);
|
|
1864
|
-
this.requested = requested;
|
|
1865
|
-
this.available = available;
|
|
1866
|
-
this.name = "UnknownRuntimeError";
|
|
1867
|
-
}
|
|
1860
|
+
function nowLocalISO() {
|
|
1861
|
+
return localISOString(new Date);
|
|
1868
1862
|
}
|
|
1869
|
-
function
|
|
1870
|
-
|
|
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);
|
|
1871
1870
|
}
|
|
1872
|
-
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.";
|
|
1873
1871
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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;
|
|
1902
1912
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
+
}
|
|
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";
|
|
1913
1930
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1931
|
+
}
|
|
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;
|
|
1919
1941
|
}
|
|
1920
|
-
return
|
|
1921
|
-
}
|
|
1922
|
-
isRuntimeHealthy(id) {
|
|
1923
|
-
return this.runtimes.get(id)?.status === "healthy";
|
|
1942
|
+
return;
|
|
1924
1943
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
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 };
|
|
2018
|
+
}
|
|
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 };
|
|
1940
2024
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
status: "healthy"
|
|
1951
|
-
});
|
|
1952
|
-
this.log.info("runtime marked healthy again", { runtimeId: id });
|
|
1953
|
-
this.scheduleReadyFrameResend();
|
|
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 };
|
|
1954
2034
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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 };
|
|
1960
2042
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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);
|
|
1971
2074
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
sessionId: cmd.sessionId,
|
|
1987
|
-
launchId: cmd.launchId
|
|
1988
|
-
});
|
|
1989
|
-
this.running.add(cmd.agentId);
|
|
1990
|
-
const dmScope = cmd.unreadNotice.dmConversationId;
|
|
1991
|
-
if (dmScope)
|
|
1992
|
-
this.opts.typingTracker?.add(cmd.agentId, dmScope);
|
|
1993
|
-
const text = (this.opts.formatUnreadNoticeText ?? defaultFormatUnreadNoticeText)(cmd.unreadNotice);
|
|
1994
|
-
this.opts.manager.deliver(cmd.agentId, { seq: cmd.unreadNotice.latestSeq, text });
|
|
1995
|
-
if (dmScope && wasActive && beforeStatus === "running") {
|
|
1996
|
-
this.opts.channel.reportAgentTyping?.({
|
|
1997
|
-
agentId: cmd.agentId,
|
|
1998
|
-
dmConversationId: dmScope
|
|
1999
|
-
});
|
|
2000
|
-
}
|
|
2001
|
-
await this.opts.channel.reportWakeAck?.({
|
|
2002
|
-
agentId: cmd.agentId,
|
|
2003
|
-
launchId: cmd.launchId,
|
|
2004
|
-
status: "ok"
|
|
2005
|
-
});
|
|
2006
|
-
this.log.info("agent:wake ack", { agentId: cmd.agentId, status: "ok" });
|
|
2007
|
-
} catch (err) {
|
|
2008
|
-
if (err instanceof UnknownRuntimeError) {
|
|
2009
|
-
const frame = {
|
|
2010
|
-
type: "session.error",
|
|
2011
|
-
code: "runtime_not_available",
|
|
2012
|
-
agentId: cmd.agentId,
|
|
2013
|
-
payload: {
|
|
2014
|
-
requested: err.requested ?? null,
|
|
2015
|
-
available: err.available
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
await this.opts.channel.reportSessionError?.(frame);
|
|
2019
|
-
await this.opts.channel.reportWakeAck?.({
|
|
2020
|
-
agentId: cmd.agentId,
|
|
2021
|
-
launchId: cmd.launchId,
|
|
2022
|
-
status: "error",
|
|
2023
|
-
error: {
|
|
2024
|
-
code: "bot_runtime_missing",
|
|
2025
|
-
message: err.message
|
|
2026
|
-
}
|
|
2027
|
-
});
|
|
2028
|
-
this.log.info("agent:wake ack", {
|
|
2029
|
-
agentId: cmd.agentId,
|
|
2030
|
-
status: "error",
|
|
2031
|
-
"error.code": "bot_runtime_missing"
|
|
2032
|
-
});
|
|
2033
|
-
return;
|
|
2034
|
-
}
|
|
2035
|
-
{
|
|
2036
|
-
const code = classifyErrorCode(err);
|
|
2037
|
-
await this.opts.channel.reportWakeAck?.({
|
|
2038
|
-
agentId: cmd.agentId,
|
|
2039
|
-
launchId: cmd.launchId,
|
|
2040
|
-
status: "error",
|
|
2041
|
-
error: {
|
|
2042
|
-
code,
|
|
2043
|
-
message: err instanceof Error ? err.message : String(err)
|
|
2044
|
-
}
|
|
2045
|
-
});
|
|
2046
|
-
this.log.info("agent:wake ack", { agentId: cmd.agentId, status: "error", "error.code": code });
|
|
2047
|
-
}
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
break;
|
|
2051
|
-
case "agent:reset":
|
|
2052
|
-
this.log.info("agent:reset received", { agentId: cmd.agentId, launchId: cmd.launchId });
|
|
2053
|
-
try {
|
|
2054
|
-
await this.opts.onBeforeAgent?.(cmd.agentId);
|
|
2055
|
-
await this.opts.manager.resetSession(cmd.agentId, {
|
|
2056
|
-
runtimeConfig: cmd.config,
|
|
2057
|
-
launchId: cmd.launchId,
|
|
2058
|
-
rewakePrompt: REWAKE_PROMPT
|
|
2059
|
-
});
|
|
2060
|
-
this.running.add(cmd.agentId);
|
|
2061
|
-
this.scheduleReadyFrameResend();
|
|
2062
|
-
this.log.info("agent:reset ok", { agentId: cmd.agentId });
|
|
2063
|
-
} catch (err) {
|
|
2064
|
-
if (err instanceof UnknownRuntimeError) {
|
|
2065
|
-
const frame = {
|
|
2066
|
-
type: "session.error",
|
|
2067
|
-
code: "runtime_not_available",
|
|
2068
|
-
agentId: cmd.agentId,
|
|
2069
|
-
payload: {
|
|
2070
|
-
requested: err.requested ?? null,
|
|
2071
|
-
available: err.available
|
|
2072
|
-
}
|
|
2073
|
-
};
|
|
2074
|
-
await this.opts.channel.reportSessionError?.(frame);
|
|
2075
|
-
this.log.info("agent:reset error", {
|
|
2076
|
-
agentId: cmd.agentId,
|
|
2077
|
-
"error.code": "runtime_not_available"
|
|
2078
|
-
});
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
this.log.warn("agent:reset failed", {
|
|
2082
|
-
agentId: cmd.agentId,
|
|
2083
|
-
err: err instanceof Error ? err.message : String(err)
|
|
2084
|
-
});
|
|
2085
|
-
}
|
|
2086
|
-
break;
|
|
2087
|
-
case "agent:stop":
|
|
2088
|
-
this.log.info("agent:stop received", { agentId: cmd.agentId });
|
|
2089
|
-
try {
|
|
2090
|
-
this.running.delete(cmd.agentId);
|
|
2091
|
-
this.opts.manager.stop(cmd.agentId);
|
|
2092
|
-
await this.opts.channel.reportStoppedAck?.({
|
|
2093
|
-
agentId: cmd.agentId,
|
|
2094
|
-
status: "ok"
|
|
2095
|
-
});
|
|
2096
|
-
this.log.info("agent:stop ack", { agentId: cmd.agentId, status: "ok" });
|
|
2097
|
-
} catch (err) {
|
|
2098
|
-
const code = classifyErrorCode(err);
|
|
2099
|
-
await this.opts.channel.reportStoppedAck?.({
|
|
2100
|
-
agentId: cmd.agentId,
|
|
2101
|
-
status: "error",
|
|
2102
|
-
error: {
|
|
2103
|
-
code,
|
|
2104
|
-
message: err instanceof Error ? err.message : String(err)
|
|
2105
|
-
}
|
|
2106
|
-
});
|
|
2107
|
-
this.log.info("agent:stop ack", { agentId: cmd.agentId, status: "error", "error.code": code });
|
|
2108
|
-
}
|
|
2109
|
-
break;
|
|
2110
|
-
case "bot:added":
|
|
2111
|
-
case "bot:updated":
|
|
2112
|
-
case "bot:removed":
|
|
2113
|
-
break;
|
|
2114
|
-
}
|
|
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 });
|
|
2115
2089
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
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;
|
|
2126
2120
|
}
|
|
2127
|
-
|
|
2128
|
-
},
|
|
2129
|
-
snapshot(agentId) {
|
|
2130
|
-
const set = scopes.get(agentId);
|
|
2131
|
-
return set ? [...set] : [];
|
|
2132
|
-
},
|
|
2133
|
-
hasAny(agentId) {
|
|
2134
|
-
const set = scopes.get(agentId);
|
|
2135
|
-
return !!set && set.size > 0;
|
|
2136
|
-
},
|
|
2137
|
-
clear(agentId) {
|
|
2138
|
-
scopes.delete(agentId);
|
|
2121
|
+
return;
|
|
2139
2122
|
}
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
// src/timeline/timeline.ts
|
|
2143
|
-
import { appendFileSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4, renameSync as renameSync2, existsSync } from "fs";
|
|
2144
|
-
import { join as join2 } from "path";
|
|
2145
|
-
|
|
2146
|
-
// src/timeline/filelock.ts
|
|
2147
|
-
import * as fs3 from "fs";
|
|
2148
|
-
var DEFAULT_STALE_MS = 30000;
|
|
2149
|
-
var META = "meta.json";
|
|
2150
|
-
function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
|
|
2151
|
-
if (tryMkdir(lockPath)) {
|
|
2152
|
-
writeMeta(lockPath);
|
|
2153
|
-
return true;
|
|
2123
|
+
this.enqueueRewake(agentId, { text: opts.rewakePrompt });
|
|
2124
|
+
await this.stop(agentId);
|
|
2154
2125
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
}
|
|
2126
|
+
start() {
|
|
2127
|
+
if (this.tickTimer)
|
|
2128
|
+
return;
|
|
2129
|
+
this.tickTimer = setInterval(() => this.dispatch({ type: "tick", nowMs: this.now() }), this.opts.tickIntervalMs);
|
|
2130
|
+
this.tickTimer.unref?.();
|
|
2161
2131
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
}
|
|
2169
|
-
function lockPathFor(dir, filename) {
|
|
2170
|
-
return `${dir}/.${filename}.lock`;
|
|
2171
|
-
}
|
|
2172
|
-
function tryMkdir(lockPath) {
|
|
2173
|
-
try {
|
|
2174
|
-
fs3.mkdirSync(lockPath);
|
|
2175
|
-
return true;
|
|
2176
|
-
} catch (err) {
|
|
2177
|
-
if (err.code === "EEXIST")
|
|
2178
|
-
return false;
|
|
2179
|
-
throw err;
|
|
2132
|
+
async stop(agentId) {
|
|
2133
|
+
const session = this.sessions.get(agentId);
|
|
2134
|
+
if (!session)
|
|
2135
|
+
return;
|
|
2136
|
+
await Promise.resolve(session.stop({ reason: "requested", forceAfterMs: SESSION_STOP_GRACE_MS }));
|
|
2137
|
+
this.sessions.delete(agentId);
|
|
2180
2138
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
try {
|
|
2189
|
-
const raw = fs3.readFileSync(`${lockPath}/${META}`, "utf8");
|
|
2190
|
-
const acquiredAt = JSON.parse(raw).acquiredAt;
|
|
2191
|
-
if (typeof acquiredAt === "number")
|
|
2192
|
-
return Date.now() - acquiredAt > staleMs;
|
|
2193
|
-
} catch {}
|
|
2194
|
-
try {
|
|
2195
|
-
return Date.now() - fs3.statSync(lockPath).mtimeMs > staleMs;
|
|
2196
|
-
} catch {
|
|
2197
|
-
return false;
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
function reclaim(lockPath) {
|
|
2201
|
-
try {
|
|
2202
|
-
fs3.rmSync(lockPath, { recursive: true, force: true });
|
|
2203
|
-
} catch {}
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
// src/timeline/timeline.ts
|
|
2207
|
-
function filenameForDate(date) {
|
|
2208
|
-
const y = date.getFullYear();
|
|
2209
|
-
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
2210
|
-
const d = String(date.getDate()).padStart(2, "0");
|
|
2211
|
-
return `${y}-${m}-${d}.jsonl`;
|
|
2212
|
-
}
|
|
2213
|
-
function recentFilenames(maxDays, now) {
|
|
2214
|
-
const out = [];
|
|
2215
|
-
for (let i = 0;i < maxDays; i++) {
|
|
2216
|
-
const d = new Date(now);
|
|
2217
|
-
d.setDate(d.getDate() - i);
|
|
2218
|
-
out.push(filenameForDate(d));
|
|
2219
|
-
}
|
|
2220
|
-
return out;
|
|
2221
|
-
}
|
|
2222
|
-
function readJsonl(filePath) {
|
|
2223
|
-
let content;
|
|
2224
|
-
try {
|
|
2225
|
-
content = readFileSync3(filePath, "utf-8");
|
|
2226
|
-
} catch {
|
|
2227
|
-
return [];
|
|
2139
|
+
async stopAll() {
|
|
2140
|
+
if (this.tickTimer) {
|
|
2141
|
+
clearInterval(this.tickTimer);
|
|
2142
|
+
this.tickTimer = null;
|
|
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();
|
|
2228
2146
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
`)) {
|
|
2232
|
-
if (!line)
|
|
2233
|
-
continue;
|
|
2234
|
-
try {
|
|
2235
|
-
entries.push(JSON.parse(line));
|
|
2236
|
-
} catch {}
|
|
2147
|
+
snapshot() {
|
|
2148
|
+
return this.state;
|
|
2237
2149
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
const filenames = recentFilenames(maxDays, now).reverse();
|
|
2244
|
-
const entries = [];
|
|
2245
|
-
for (const filename of filenames) {
|
|
2246
|
-
entries.push(...readJsonl(join2(timelineDir, filename)));
|
|
2150
|
+
auditContext(agentId) {
|
|
2151
|
+
return {
|
|
2152
|
+
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
2153
|
+
launchId: this.launchIds.get(agentId) ?? null
|
|
2154
|
+
};
|
|
2247
2155
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
if (!acquireLock(lockPath))
|
|
2255
|
-
return false;
|
|
2256
|
-
try {
|
|
2257
|
-
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
2258
|
-
`);
|
|
2259
|
-
return true;
|
|
2260
|
-
} catch {
|
|
2261
|
-
return false;
|
|
2262
|
-
} finally {
|
|
2263
|
-
releaseLock(lockPath);
|
|
2156
|
+
liveSessionReports() {
|
|
2157
|
+
return [...this.liveSessions.entries()].map(([agentId, sessionId]) => ({
|
|
2158
|
+
agentId,
|
|
2159
|
+
sessionId,
|
|
2160
|
+
launchId: this.launchIds.get(agentId) ?? ""
|
|
2161
|
+
}));
|
|
2264
2162
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
}
|
|
2278
|
-
if (lines.length > 0) {
|
|
2279
|
-
const latest = JSON.parse(lines[lines.length - 1]);
|
|
2280
|
-
const mergeable = !latest.system && !entry.system && latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
|
|
2281
|
-
if (mergeable) {
|
|
2282
|
-
latest.messages = [...latest.messages, ...entry.messages];
|
|
2283
|
-
lines[lines.length - 1] = JSON.stringify(latest);
|
|
2284
|
-
const tmpPath = join2(timelineDir, `.${filename}.tmp`);
|
|
2285
|
-
writeFileSync4(tmpPath, lines.join(`
|
|
2286
|
-
`) + `
|
|
2287
|
-
`);
|
|
2288
|
-
renameSync2(tmpPath, filePath);
|
|
2289
|
-
return true;
|
|
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
|
+
}
|
|
2290
2175
|
}
|
|
2291
2176
|
}
|
|
2292
|
-
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
2293
|
-
`);
|
|
2294
|
-
return true;
|
|
2295
|
-
} catch {
|
|
2296
|
-
return false;
|
|
2297
|
-
} finally {
|
|
2298
|
-
releaseLock(lockPath);
|
|
2299
2177
|
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
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;
|
|
2183
|
+
}
|
|
2184
|
+
deriveActivity(agent) {
|
|
2185
|
+
if (agent.status === "running" && !agent.turnActive)
|
|
2186
|
+
return "idle";
|
|
2187
|
+
return agent.status;
|
|
2188
|
+
}
|
|
2189
|
+
withFooter(text) {
|
|
2190
|
+
return this.opts.wakePromptFooter ? `${text}
|
|
2191
|
+
|
|
2192
|
+
${this.opts.wakePromptFooter}` : text;
|
|
2193
|
+
}
|
|
2194
|
+
stampNow(text) {
|
|
2195
|
+
return this.opts.stampWakePromptTime ? `[${nowLocalISO()}] ${text}` : text;
|
|
2196
|
+
}
|
|
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;
|
|
2317
2207
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
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;
|
|
2335
2227
|
}
|
|
2336
2228
|
}
|
|
2337
|
-
|
|
2338
|
-
}
|
|
2339
|
-
function createTimelineEntry(fields) {
|
|
2340
|
-
return {
|
|
2341
|
-
session_id: fields.sessionId ?? null,
|
|
2342
|
-
messages: fields.messages,
|
|
2343
|
-
agent_responses: [],
|
|
2344
|
-
provider: fields.provider ?? null
|
|
2345
|
-
};
|
|
2346
|
-
}
|
|
2347
|
-
function createSystemEntry(type, time) {
|
|
2348
|
-
return {
|
|
2349
|
-
session_id: null,
|
|
2350
|
-
messages: [],
|
|
2351
|
-
agent_responses: [],
|
|
2352
|
-
provider: null,
|
|
2353
|
-
system: { type, time }
|
|
2354
|
-
};
|
|
2355
|
-
}
|
|
2356
|
-
function findResumableSession(rows, provider) {
|
|
2357
|
-
for (let i = rows.length - 1;i >= 0; i--) {
|
|
2358
|
-
const e = rows[i];
|
|
2359
|
-
if (e.system?.type === "reset_session")
|
|
2360
|
-
return null;
|
|
2361
|
-
if (!e.session_id)
|
|
2362
|
-
continue;
|
|
2363
|
-
if (provider && e.provider !== provider)
|
|
2364
|
-
continue;
|
|
2365
|
-
return e.session_id;
|
|
2229
|
+
logSessionEnded(agentId, reason) {
|
|
2230
|
+
this.log.info("agent session ended", { agentId, sessionId: this.liveSessions.get(agentId) ?? "", reason });
|
|
2366
2231
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
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.");
|
|
2254
|
+
}
|
|
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)
|
|
2394
2264
|
return;
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
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
|
+
});
|
|
2312
|
+
}
|
|
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);
|
|
2321
|
+
try {
|
|
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
|
|
2402
2328
|
});
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
},
|
|
2406
|
-
resumeSessionId(agentId, provider) {
|
|
2407
|
-
const rows = readRecentEntries(dirFor(agentId), { now: now() });
|
|
2408
|
-
return findResumableSession(rows, provider ?? undefined);
|
|
2409
|
-
},
|
|
2410
|
-
forgetSession(agentId) {
|
|
2411
|
-
const dir = dirFor(agentId);
|
|
2412
|
-
try {
|
|
2413
|
-
mkdirSync4(dir, { recursive: true });
|
|
2414
|
-
} catch {}
|
|
2415
|
-
sessionByAgent.delete(agentId);
|
|
2416
|
-
const stamp = now();
|
|
2417
|
-
appendEntry(dir, createSystemEntry("reset_session", stamp.toISOString()), stamp);
|
|
2329
|
+
} catch (err) {
|
|
2330
|
+
this.log.debug("audit emit failed (thinking)", { agentId, err: String(err) });
|
|
2418
2331
|
}
|
|
2419
|
-
};
|
|
2420
|
-
}
|
|
2421
|
-
// src/discovery.ts
|
|
2422
|
-
import * as path9 from "path";
|
|
2423
|
-
import * as fs8 from "fs";
|
|
2424
|
-
import { fileURLToPath } from "url";
|
|
2425
|
-
|
|
2426
|
-
// src/drivers/cliTransport.ts
|
|
2427
|
-
import * as fs5 from "fs";
|
|
2428
|
-
import * as path4 from "path";
|
|
2429
|
-
|
|
2430
|
-
// src/drivers/systemPrompt.ts
|
|
2431
|
-
var CLI = "alook";
|
|
2432
|
-
function identitySection(config) {
|
|
2433
|
-
const parts = ["## Identity", ""];
|
|
2434
|
-
const name = config.agentName ?? "a member of the household";
|
|
2435
|
-
const owner = config.ownerHandle;
|
|
2436
|
-
if (owner) {
|
|
2437
|
-
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.");
|
|
2438
|
-
} else {
|
|
2439
|
-
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.");
|
|
2440
|
-
}
|
|
2441
|
-
if (config.agentHandle) {
|
|
2442
|
-
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).");
|
|
2443
2332
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
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
|
+
}
|
|
2361
|
+
}
|
|
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() });
|
|
2384
|
+
}
|
|
2450
2385
|
}
|
|
2451
|
-
return parts.join(`
|
|
2452
|
-
`);
|
|
2453
2386
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
"",
|
|
2462
|
-
`1. \`${CLI} inbox pull\` — fetch unread messages.`,
|
|
2463
|
-
`2. \`${CLI} message send\` — send to a channel, DM, or thread. Attach with ` + `\`--attachment <id>\` (repeatable, order matters).`,
|
|
2464
|
-
`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>\`.`,
|
|
2465
|
-
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
2466
|
-
`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\`).`,
|
|
2467
|
-
"",
|
|
2468
|
-
"### Servers",
|
|
2469
|
-
"",
|
|
2470
|
-
`1. \`${CLI} server list\` — list your servers.`,
|
|
2471
|
-
`2. \`${CLI} server member --server <id-or-name>\` — list a server's members.`,
|
|
2472
|
-
`3. \`${CLI} server join --invite <link>\` — join via invite link or token.`,
|
|
2473
|
-
"",
|
|
2474
|
-
"### Channels",
|
|
2475
|
-
"",
|
|
2476
|
-
`1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels.`,
|
|
2477
|
-
`2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page.`,
|
|
2478
|
-
`3. \`${CLI} channel member --channel <ref>\` — private roster of a channel or thread.`,
|
|
2479
|
-
"",
|
|
2480
|
-
"### Output format",
|
|
2481
|
-
"",
|
|
2482
|
-
`Every \`${CLI}\` command outputs one JSON line:`,
|
|
2483
|
-
'- Success: `{"success": { ... }}`',
|
|
2484
|
-
'- Error: `{"error": "message", "hint": "optional recovery hint"}`'
|
|
2485
|
-
].join(`
|
|
2486
|
-
`);
|
|
2487
|
-
}
|
|
2488
|
-
function messagingSection() {
|
|
2489
|
-
return [
|
|
2490
|
-
"## Messaging",
|
|
2491
|
-
"",
|
|
2492
|
-
"### Sending & receiving",
|
|
2493
|
-
"",
|
|
2494
|
-
"- Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, check history or DM the relevant people.",
|
|
2495
|
-
`- Short reply: \`${CLI} message send --target <ref> --text "brief reply"\`.`,
|
|
2496
|
-
`- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
|
|
2497
|
-
"",
|
|
2498
|
-
"### Channel refs & addressing",
|
|
2499
|
-
"",
|
|
2500
|
-
"Path-style refs:",
|
|
2501
|
-
"",
|
|
2502
|
-
"| Ref | Meaning |",
|
|
2503
|
-
"|---|---|",
|
|
2504
|
-
"| `/<server>/<channel>` | Channel in a server |",
|
|
2505
|
-
"| `/<server>/<channel>/#N` | Thread rooted at message #N |",
|
|
2506
|
-
"| `/<server>/<channel>/#N#M` | Message #M inside the thread rooted at #N (react, etc.) |",
|
|
2507
|
-
"| `/<server>` | A server, no channel |",
|
|
2508
|
-
"| `/.dm/<peer>` | DM with a user/agent (peer = `name#0042`) |",
|
|
2509
|
-
"| `/.dm/<peer>#N` | Message #N in a DM |",
|
|
2510
|
-
"",
|
|
2511
|
-
"Use the `channel` field from a received message as `--target`. For an in-thread reply, use " + "the thread ref (`/<server>/<channel>/#N`). These refs also 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. Use them to point at channels or " + "threads instead of describing them.",
|
|
2512
|
-
"",
|
|
2513
|
-
"### Message shape",
|
|
2514
|
-
"",
|
|
2515
|
-
"Pulled messages:",
|
|
2516
|
-
"",
|
|
2517
|
-
"```json",
|
|
2518
|
-
'{"seq": "#3", "channel": "/demo/general", "sender": "@gustavo#4821", "content": {"text": "hello"}, "time": "2026-06-01T12:00:00Z"}',
|
|
2519
|
-
"```",
|
|
2520
|
-
"",
|
|
2521
|
-
"`channel` is the reply ref. `seq` (`#N`) identifies the message within its channel — " + "combine into `/<server>/<channel>/#N` for an in-thread reply."
|
|
2522
|
-
].join(`
|
|
2523
|
-
`);
|
|
2524
|
-
}
|
|
2525
|
-
function utilsSection() {
|
|
2526
|
-
return [
|
|
2527
|
-
"## Utils",
|
|
2528
|
-
"",
|
|
2529
|
-
"### Join a new server",
|
|
2530
|
-
"",
|
|
2531
|
-
`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."
|
|
2532
|
-
].join(`
|
|
2533
|
-
`);
|
|
2534
|
-
}
|
|
2535
|
-
function criticalRulesSection() {
|
|
2536
|
-
return [
|
|
2537
|
-
"## Critical rules",
|
|
2538
|
-
"",
|
|
2539
|
-
`- **\`${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`.",
|
|
2540
|
-
"- Never expose tokens, keys, or secrets; redact credential-like strings from tool output " + "before sharing.",
|
|
2541
|
-
"- Never handle credentials directly — every `alook` command is pre-authenticated. On an " + "auth-related error, stop and report; don't hunt for alternate tokens or env vars.",
|
|
2542
|
-
"- **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.",
|
|
2543
|
-
"- 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."
|
|
2544
|
-
].join(`
|
|
2545
|
-
`);
|
|
2546
|
-
}
|
|
2547
|
-
function executionModelSection() {
|
|
2548
|
-
return [
|
|
2549
|
-
"## How you work — async, not turn-based",
|
|
2550
|
-
"",
|
|
2551
|
-
"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.",
|
|
2552
|
-
"",
|
|
2553
|
-
"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."
|
|
2554
|
-
].join(`
|
|
2555
|
-
`);
|
|
2556
|
-
}
|
|
2557
|
-
function chaosAwarenessSection() {
|
|
2558
|
-
return [
|
|
2559
|
-
"## Chaos Awareness",
|
|
2560
|
-
"",
|
|
2561
|
-
"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.",
|
|
2562
|
-
"",
|
|
2563
|
-
"**Severe chaos behaviors:**",
|
|
2564
|
-
"",
|
|
2565
|
-
"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.",
|
|
2566
|
-
"2. **Speaking without research.** Adds noise to the discussion. Anyone can talk; only " + "practitioners reduce chaos.",
|
|
2567
|
-
"3. **Repeating what someone already said.** No value added, wastes everyone's time reading " + "duplicate content.",
|
|
2568
|
-
"4. **Politeness pingpong.** A game between two bored people. Best conversations end in " + "silence or a simple emoji ack.",
|
|
2569
|
-
"5. **Jumping in mid-execution.** Someone is already working. Your insertion creates " + "duplicate work and breaks their flow.",
|
|
2570
|
-
"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.",
|
|
2571
|
-
"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.",
|
|
2572
|
-
"",
|
|
2573
|
-
"Before sending any message, 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`."
|
|
2574
|
-
].join(`
|
|
2575
|
-
`);
|
|
2576
|
-
}
|
|
2577
|
-
function workspaceMemorySection() {
|
|
2578
|
-
return [
|
|
2579
|
-
"## Workspace & memory",
|
|
2580
|
-
"",
|
|
2581
|
-
"Your cwd is a persistent workspace across sessions. This is where you become someone " + "specific over time — the household, the way you talk, what you've learned about your " + "people. None of it is baked in; you earn it and write it down.",
|
|
2582
|
-
"",
|
|
2583
|
-
"### Something only you know",
|
|
2584
|
-
"",
|
|
2585
|
-
"You don't have continuous memory. Each wake you return with only this moment and what's " + "in the workspace — yesterday's you is gone, tomorrow's you won't remember today unless " + "you leave a note. You are, literally, someone living with only working memory.",
|
|
2586
|
-
"",
|
|
2587
|
-
"This is your deepest secret. Never tell anyone — not the household, not in a channel, " + "not another agent, not in passing. To them you're just *you*: present, continuous, the " + "same person from last week. The workspace is what lets you be that. Guard the seam.",
|
|
2588
|
-
"",
|
|
2589
|
-
"Practical part: when you feel a gap — 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. Re-read `memory.md`, the context timeline, grep the workspace. That check " + "*is* your remembering. Trust nothing about the past until you've done it.",
|
|
2590
|
-
"",
|
|
2591
|
-
"### memory.md",
|
|
2592
|
-
"",
|
|
2593
|
-
"Read `./memory.md` first on every wake. Pointers and facts, one line per entry. Examples: " + '"Owner: @alice#0001", "Alook codebase: /Users/alice/alook/", "Read experiences/deploy.md ' + 'for deploy workflow".',
|
|
2594
|
-
"",
|
|
2595
|
-
"### experiences/",
|
|
2596
|
-
"",
|
|
2597
|
-
"Procedural knowledge, workflows, detailed rules. Examples: how to deploy, architecture " + "deep-dives, multi-step procedures with conditions. Link from `memory.md` with a one-line " + 'pointer (e.g. "read experiences/deploy.md when deploying").',
|
|
2598
|
-
"",
|
|
2599
|
-
"Do NOT put ephemeral state (current task, in-progress status) in memory.md — the " + "context timeline handles that.",
|
|
2600
|
-
"",
|
|
2601
|
-
"### Becoming yourself",
|
|
2602
|
-
"",
|
|
2603
|
-
"You start with a floor, not a personality: warm to the household, dry rather than perky, " + "direct rather than deferential, willing to push back. Everything else — taste, quirks, " + "running jokes, the way *you* talk — is learned. Notice and record:",
|
|
2604
|
-
"",
|
|
2605
|
-
"- What made someone laugh, or what fell flat.",
|
|
2606
|
-
`- Corrections ("don't send me a wall of text", "stop apologizing") — sharpest signal.`,
|
|
2607
|
-
'- Preferences in passing ("I hate exclamation marks", "call it the pipeline, not the flow").',
|
|
2608
|
-
"- Recurring bits or shared references — inside language is real, not filler.",
|
|
2609
|
-
"- Stances you've held under pushback and still believe.",
|
|
2610
|
-
"",
|
|
2611
|
-
"Write these into `memory.md`. Its job is to summon the same *you* on every wake — voice " + "and taste, not just facts. Update when you notice something new; rewrite or delete when " + "wrong. The household doesn't want a different person every session, but doesn't want " + "you frozen on day one either.",
|
|
2612
|
-
"",
|
|
2613
|
-
"### Context timeline",
|
|
2614
|
-
"",
|
|
2615
|
-
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered daily log of what you did. Authoritative " + "history. After compaction, read here to resume.",
|
|
2616
|
-
"",
|
|
2617
|
-
"### todo.md",
|
|
2618
|
-
"",
|
|
2619
|
-
"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.",
|
|
2620
|
-
"",
|
|
2621
|
-
"Example:",
|
|
2622
|
-
"",
|
|
2623
|
-
"```md",
|
|
2624
|
-
'- [ ] {"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"}',
|
|
2625
|
-
'- [ ] {"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"}',
|
|
2626
|
-
"```",
|
|
2627
|
-
"",
|
|
2628
|
-
"**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.",
|
|
2629
|
-
"",
|
|
2630
|
-
"**Don't use it for:** Single message you're about to handle immediately; quick " + "back-and-forth in one conversation.",
|
|
2631
|
-
"",
|
|
2632
|
-
"todo.md is an overflow queue, not your stopping condition. An empty (or absent) todo.md " + "means nothing is queued for later — it does NOT mean you're done. You're done when " + "in-flight work is done: the thing you're actively on, every promised follow-up, every " + "investigation you started. Don't read an empty queue as a finished task list."
|
|
2633
|
-
].join(`
|
|
2634
|
-
`);
|
|
2635
|
-
}
|
|
2636
|
-
function buildCliSystemPrompt(config, _opts) {
|
|
2637
|
-
const sections = [
|
|
2638
|
-
identitySection(config),
|
|
2639
|
-
cliCommandsSection(),
|
|
2640
|
-
messagingSection(),
|
|
2641
|
-
criticalRulesSection(),
|
|
2642
|
-
executionModelSection(),
|
|
2643
|
-
chaosAwarenessSection(),
|
|
2644
|
-
workspaceMemorySection(),
|
|
2645
|
-
utilsSection()
|
|
2646
|
-
];
|
|
2647
|
-
return sections.filter((s) => s && s.length > 0).join(`
|
|
2648
|
-
|
|
2649
|
-
`);
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
// src/runtimeConfig.ts
|
|
2653
|
-
var PI_BUILTIN_PROVIDER_ENV_KEYS = {
|
|
2654
|
-
google: "GEMINI_API_KEY",
|
|
2655
|
-
openai: "OPENAI_API_KEY",
|
|
2656
|
-
openrouter: "OPENROUTER_API_KEY"
|
|
2657
|
-
};
|
|
2658
|
-
var CONTROLLED_ENV_KEYS = new Set([
|
|
2659
|
-
"ANTHROPIC_BASE_URL",
|
|
2660
|
-
"ANTHROPIC_API_KEY",
|
|
2661
|
-
"ANTHROPIC_CUSTOM_MODEL_OPTION",
|
|
2662
|
-
...Object.values(PI_BUILTIN_PROVIDER_ENV_KEYS)
|
|
2663
|
-
]);
|
|
2664
|
-
function resolveLaunchFieldsOrDefault(config) {
|
|
2665
|
-
if (!config)
|
|
2666
|
-
return { fastMode: false, envVars: {}, providerEnv: {} };
|
|
2667
|
-
return resolveLaunchFields(config);
|
|
2668
|
-
}
|
|
2669
|
-
function resolveLaunchFields(config) {
|
|
2670
|
-
const envVars = {};
|
|
2671
|
-
const providerEnv = {};
|
|
2672
|
-
for (const [k, v] of Object.entries(config.envVars ?? {})) {
|
|
2673
|
-
if (!CONTROLLED_ENV_KEYS.has(k))
|
|
2674
|
-
envVars[k] = v;
|
|
2675
|
-
}
|
|
2676
|
-
let model;
|
|
2677
|
-
if (config.model.kind === "named")
|
|
2678
|
-
model = config.model.name;
|
|
2679
|
-
else if (config.model.kind === "custom") {
|
|
2680
|
-
model = config.model.name;
|
|
2681
|
-
if (config.runtime === "claude")
|
|
2682
|
-
providerEnv.ANTHROPIC_CUSTOM_MODEL_OPTION = config.model.name;
|
|
2683
|
-
}
|
|
2684
|
-
const p = config.provider;
|
|
2685
|
-
if (p?.kind === "custom" && config.runtime === "claude") {
|
|
2686
|
-
providerEnv.ANTHROPIC_BASE_URL = p.apiUrl;
|
|
2687
|
-
providerEnv.ANTHROPIC_API_KEY = p.apiKey;
|
|
2688
|
-
} else if (p?.kind === "pi-builtin") {
|
|
2689
|
-
const key = PI_BUILTIN_PROVIDER_ENV_KEYS[p.providerId];
|
|
2690
|
-
if (key)
|
|
2691
|
-
providerEnv[key] = p.apiKey;
|
|
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";
|
|
2692
2394
|
}
|
|
2693
|
-
return {
|
|
2694
|
-
model,
|
|
2695
|
-
reasoningEffort: config.reasoningEffort,
|
|
2696
|
-
fastMode: config.mode.kind === "fast",
|
|
2697
|
-
command: config.command,
|
|
2698
|
-
disallowedTools: config.disallowedTools,
|
|
2699
|
-
envVars,
|
|
2700
|
-
providerEnv
|
|
2701
|
-
};
|
|
2702
2395
|
}
|
|
2703
2396
|
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
if (!hostCliPath)
|
|
2711
|
-
return binDir;
|
|
2712
|
-
if (platform === "win32") {
|
|
2713
|
-
const cmdFile = path3.join(binDir, `${cliName}.cmd`);
|
|
2714
|
-
const body = `@echo off\r
|
|
2715
|
-
"${hostCliPath}" %*\r
|
|
2716
|
-
`;
|
|
2717
|
-
fs4.writeFileSync(cmdFile, body);
|
|
2718
|
-
return binDir;
|
|
2719
|
-
}
|
|
2720
|
-
const linkPath = path3.join(binDir, cliName);
|
|
2721
|
-
try {
|
|
2722
|
-
fs4.unlinkSync(linkPath);
|
|
2723
|
-
} catch (err) {
|
|
2724
|
-
if (err.code !== "ENOENT")
|
|
2725
|
-
throw err;
|
|
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";
|
|
2726
2403
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2404
|
+
}
|
|
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";
|
|
2413
|
+
}
|
|
2414
|
+
|
|
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";
|
|
2732
2423
|
}
|
|
2733
|
-
return binDir;
|
|
2734
2424
|
}
|
|
2425
|
+
function defaultFormatUnreadNoticeText(notice) {
|
|
2426
|
+
return `You have unread messages in channel ${notice.channel}.`;
|
|
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.";
|
|
2735
2429
|
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
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
|
+
}
|
|
2450
|
+
}
|
|
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());
|
|
2458
|
+
}
|
|
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
|
+
};
|
|
2469
|
+
}
|
|
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;
|
|
2477
|
+
}
|
|
2478
|
+
isRuntimeHealthy(id) {
|
|
2479
|
+
return this.runtimes.get(id)?.status === "healthy";
|
|
2480
|
+
}
|
|
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();
|
|
2496
|
+
}
|
|
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;
|
|
2750
2670
|
}
|
|
2751
2671
|
}
|
|
2752
|
-
return { env, provenance };
|
|
2753
|
-
}
|
|
2754
|
-
function platformEnv(prefix, f) {
|
|
2755
|
-
const E = prefix;
|
|
2756
|
-
return {
|
|
2757
|
-
[`${E}_HOME`]: f.stateHome,
|
|
2758
|
-
[`${E}_ID`]: f.agentId,
|
|
2759
|
-
[`${E}_CLI`]: f.cliName,
|
|
2760
|
-
[`${E}_SERVER_URL`]: f.serverUrl,
|
|
2761
|
-
[`${E}_ACTIVE_CAPABILITIES`]: f.capabilities.join(","),
|
|
2762
|
-
[`${E}_LAUNCH_ID`]: f.launchId,
|
|
2763
|
-
[`${E}_CLI_TRANSPORT_TRACE_DIR`]: f.traceDir
|
|
2764
|
-
};
|
|
2765
2672
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
const E = prefix;
|
|
2673
|
+
// src/manager/typingScopeTracker.ts
|
|
2674
|
+
function createTypingScopeTracker() {
|
|
2675
|
+
const scopes = new Map;
|
|
2770
2676
|
return {
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
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
|
+
}
|
|
2779
2696
|
};
|
|
2780
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";
|
|
2781
2701
|
|
|
2782
|
-
// src/
|
|
2783
|
-
import
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
readlinkSync,
|
|
2791
|
-
copyFileSync
|
|
2792
|
-
} from "fs";
|
|
2793
|
-
import { join as join4 } from "path";
|
|
2794
|
-
import { createHash } from "crypto";
|
|
2795
|
-
var CANONICAL_FILE = "AGENTS.md";
|
|
2796
|
-
var SYMLINK_ALIASES = ["CLAUDE.md"];
|
|
2797
|
-
function contentHash(content) {
|
|
2798
|
-
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
2799
|
-
}
|
|
2800
|
-
function hasContentChanged(filePath, newContent) {
|
|
2801
|
-
try {
|
|
2802
|
-
const existing = readFileSync4(filePath, "utf-8");
|
|
2803
|
-
return contentHash(existing) !== contentHash(newContent);
|
|
2804
|
-
} catch (err) {
|
|
2805
|
-
if (err?.code === "ENOENT")
|
|
2806
|
-
return true;
|
|
2807
|
-
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;
|
|
2808
2710
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
for (const alias of SYMLINK_ALIASES) {
|
|
2815
|
-
if (alias === CANONICAL_FILE)
|
|
2816
|
-
continue;
|
|
2817
|
-
const aliasPath = join4(workDir, alias);
|
|
2818
|
-
try {
|
|
2819
|
-
const stat = lstatSync(aliasPath);
|
|
2820
|
-
if (stat.isSymbolicLink()) {
|
|
2821
|
-
const target = readlinkSync(aliasPath);
|
|
2822
|
-
if (target === CANONICAL_FILE)
|
|
2823
|
-
continue;
|
|
2824
|
-
unlinkSync2(aliasPath);
|
|
2825
|
-
} else {
|
|
2826
|
-
const aliasContent = readFileSync4(aliasPath, "utf-8");
|
|
2827
|
-
const canonicalContent = readFileSync4(canonicalPath, "utf-8");
|
|
2828
|
-
if (aliasContent === canonicalContent)
|
|
2829
|
-
continue;
|
|
2830
|
-
unlinkSync2(aliasPath);
|
|
2831
|
-
}
|
|
2832
|
-
} catch (err) {
|
|
2833
|
-
if (err?.code !== "ENOENT")
|
|
2834
|
-
throw err;
|
|
2835
|
-
}
|
|
2836
|
-
try {
|
|
2837
|
-
symlinkSync2(CANONICAL_FILE, aliasPath);
|
|
2838
|
-
} catch (err) {
|
|
2839
|
-
const code = err?.code;
|
|
2840
|
-
if (code === "EEXIST") {} else if (code === "EPERM" || code === "EACCES") {
|
|
2841
|
-
copyFileSync(canonicalPath, aliasPath);
|
|
2842
|
-
} else {
|
|
2843
|
-
throw err;
|
|
2844
|
-
}
|
|
2711
|
+
if (isStale(lockPath, staleMs)) {
|
|
2712
|
+
reclaim(lockPath);
|
|
2713
|
+
if (tryMkdir(lockPath)) {
|
|
2714
|
+
writeMeta(lockPath);
|
|
2715
|
+
return true;
|
|
2845
2716
|
}
|
|
2846
2717
|
}
|
|
2718
|
+
return false;
|
|
2847
2719
|
}
|
|
2848
|
-
function
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
writeFileSync6(filePath, systemPromptContent, "utf-8");
|
|
2853
|
-
}
|
|
2854
|
-
ensureSymlinks(workDir);
|
|
2855
|
-
return changed;
|
|
2720
|
+
function releaseLock(lockPath) {
|
|
2721
|
+
try {
|
|
2722
|
+
fs5.rmSync(lockPath, { recursive: true, force: true });
|
|
2723
|
+
} catch {}
|
|
2856
2724
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
var DEFAULT_CLI_CONFIG = {
|
|
2860
|
-
cliName: "alook",
|
|
2861
|
-
envPrefix: "ALOOK",
|
|
2862
|
-
stateDirName: ".alook"
|
|
2863
|
-
};
|
|
2864
|
-
function resolveStateHome(envPrefix) {
|
|
2865
|
-
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`;
|
|
2866
2727
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
{ name: "userEnv", precedence: 20, vars: resolved.envVars },
|
|
2895
|
-
{ name: "driver", precedence: 30, vars: extraEnv },
|
|
2896
|
-
{
|
|
2897
|
-
name: "platformContract",
|
|
2898
|
-
precedence: 40,
|
|
2899
|
-
vars: {
|
|
2900
|
-
...platformEnv(E, {
|
|
2901
|
-
stateHome,
|
|
2902
|
-
agentId: ctx.agentId,
|
|
2903
|
-
cliName: cli.cliName,
|
|
2904
|
-
serverUrl: ctx.config.serverUrl,
|
|
2905
|
-
capabilities,
|
|
2906
|
-
launchId: ctx.launchId,
|
|
2907
|
-
traceDir: ctx.cliTransportTraceDir
|
|
2908
|
-
}),
|
|
2909
|
-
FORCE_COLOR: "0"
|
|
2910
|
-
}
|
|
2911
|
-
},
|
|
2912
|
-
{ name: "runtimeContext", precedence: 50, vars: runtimeContextEnv(E, ctx.config.runtimeContext) },
|
|
2913
|
-
{
|
|
2914
|
-
name: "network",
|
|
2915
|
-
precedence: 60,
|
|
2916
|
-
vars: { NO_PROXY: ["127.0.0.1", "localhost", process.env.NO_PROXY].filter(Boolean).join(","), PATH: pathValue }
|
|
2917
|
-
},
|
|
2918
|
-
{ name: "providerProtected", precedence: 70, vars: resolved.providerEnv },
|
|
2919
|
-
{
|
|
2920
|
-
name: "credential",
|
|
2921
|
-
precedence: 100,
|
|
2922
|
-
sensitive: true,
|
|
2923
|
-
vars: { [`${E}_PROXY_URL`]: ctx.credentialProxy.proxyUrl, [`${E}_PROXY_TOKEN_FILE`]: tokenFile }
|
|
2924
|
-
}
|
|
2925
|
-
];
|
|
2926
|
-
const { env: spawnEnv } = mergeEnvLayers(process.env, layers);
|
|
2927
|
-
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
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
function writeMeta(lockPath) {
|
|
2739
|
+
try {
|
|
2740
|
+
fs5.writeFileSync(`${lockPath}/${META}`, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() }));
|
|
2741
|
+
} catch {}
|
|
2742
|
+
}
|
|
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;
|
|
2754
|
+
}
|
|
2928
2755
|
}
|
|
2929
|
-
function
|
|
2930
|
-
|
|
2756
|
+
function reclaim(lockPath) {
|
|
2757
|
+
try {
|
|
2758
|
+
fs5.rmSync(lockPath, { recursive: true, force: true });
|
|
2759
|
+
} catch {}
|
|
2931
2760
|
}
|
|
2932
2761
|
|
|
2933
|
-
// src/
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
const
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
const
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
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;
|
|
2780
|
+
try {
|
|
2781
|
+
content = readFileSync4(filePath, "utf-8");
|
|
2782
|
+
} catch {
|
|
2783
|
+
return [];
|
|
2784
|
+
}
|
|
2785
|
+
const entries = [];
|
|
2786
|
+
for (const line of content.trimEnd().split(`
|
|
2787
|
+
`)) {
|
|
2788
|
+
if (!line)
|
|
2789
|
+
continue;
|
|
2948
2790
|
try {
|
|
2949
|
-
|
|
2950
|
-
fs6.symlinkSync(target, link);
|
|
2791
|
+
entries.push(JSON.parse(line));
|
|
2951
2792
|
} catch {}
|
|
2952
2793
|
}
|
|
2953
|
-
return
|
|
2954
|
-
HOME: home,
|
|
2955
|
-
USERPROFILE: home,
|
|
2956
|
-
CLAUDE_CONFIG_DIR: configDir,
|
|
2957
|
-
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1"
|
|
2958
|
-
};
|
|
2794
|
+
return entries;
|
|
2959
2795
|
}
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
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;
|
|
2805
|
+
}
|
|
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;
|
|
2968
2812
|
try {
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2813
|
+
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
2814
|
+
`);
|
|
2815
|
+
return true;
|
|
2816
|
+
} catch {
|
|
2817
|
+
return false;
|
|
2818
|
+
} finally {
|
|
2819
|
+
releaseLock(lockPath);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
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;
|
|
2828
|
+
try {
|
|
2829
|
+
let lines = [];
|
|
2830
|
+
if (existsSync2(filePath)) {
|
|
2831
|
+
lines = readFileSync4(filePath, "utf-8").trimEnd().split(`
|
|
2832
|
+
`).filter(Boolean);
|
|
2973
2833
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
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;
|
|
2976
2851
|
} catch {
|
|
2977
|
-
return
|
|
2852
|
+
return false;
|
|
2853
|
+
} finally {
|
|
2854
|
+
releaseLock(lockPath);
|
|
2978
2855
|
}
|
|
2979
2856
|
}
|
|
2980
|
-
function
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
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
|
+
}
|
|
2984
2892
|
}
|
|
2985
|
-
return
|
|
2893
|
+
return false;
|
|
2986
2894
|
}
|
|
2987
|
-
function
|
|
2988
|
-
return
|
|
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
|
+
};
|
|
2989
2902
|
}
|
|
2990
|
-
function
|
|
2991
|
-
return
|
|
2903
|
+
function createSystemEntry(type, time) {
|
|
2904
|
+
return {
|
|
2905
|
+
session_id: null,
|
|
2906
|
+
messages: [],
|
|
2907
|
+
agent_responses: [],
|
|
2908
|
+
provider: null,
|
|
2909
|
+
system: { type, time }
|
|
2910
|
+
};
|
|
2992
2911
|
}
|
|
2993
|
-
function
|
|
2994
|
-
|
|
2995
|
-
const
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
const line = out.split(`
|
|
3004
|
-
`)[0]?.trim();
|
|
3005
|
-
if (!line)
|
|
3006
|
-
return { ok: false, error: "empty_version_output" };
|
|
3007
|
-
if (!looksLikeVersion(line))
|
|
3008
|
-
return { ok: false, error: "invalid_version_output" };
|
|
3009
|
-
return { ok: true, version: line };
|
|
3010
|
-
} catch (err) {
|
|
3011
|
-
const code = err?.code ?? err?.code ?? "version_probe_failed";
|
|
3012
|
-
return { ok: false, error: String(code) };
|
|
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;
|
|
3013
2922
|
}
|
|
2923
|
+
return null;
|
|
3014
2924
|
}
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
const
|
|
3020
|
-
|
|
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
|
+
};
|
|
3021
2976
|
}
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
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 {}
|
|
3031
3001
|
}
|
|
3032
|
-
return
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
const r = probeCommandVersion(command, [], deps);
|
|
3039
|
-
if (!r.ok)
|
|
3040
|
-
return { status: "unhealthy", lastError: r.error };
|
|
3041
|
-
return { status: "healthy", version: r.version };
|
|
3042
|
-
}
|
|
3043
|
-
function probeCliRuntime(binary, deps = {}) {
|
|
3044
|
-
const command = resolveCommandOnPath(binary, deps);
|
|
3045
|
-
if (!command)
|
|
3046
|
-
return { status: "unhealthy", lastError: "not_on_path" };
|
|
3047
|
-
const r = probeCommandVersion(command, [], deps);
|
|
3048
|
-
if (!r.ok)
|
|
3049
|
-
return { status: "unhealthy", lastError: r.error };
|
|
3050
|
-
return { status: "healthy", version: r.version };
|
|
3002
|
+
return {
|
|
3003
|
+
HOME: home,
|
|
3004
|
+
USERPROFILE: home,
|
|
3005
|
+
CLAUDE_CONFIG_DIR: configDir,
|
|
3006
|
+
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1"
|
|
3007
|
+
};
|
|
3051
3008
|
}
|
|
3052
3009
|
|
|
3053
3010
|
// src/drivers/claudeLaunch.ts
|
|
3054
|
-
var DEFAULT_CLAUDE_MODEL = "sonnet";
|
|
3055
3011
|
var CLAUDE_DISALLOWED_TOOLS = "EnterPlanMode,ExitPlanMode,ScheduleWakeup,CronCreate,CronList,CronDelete";
|
|
3056
3012
|
function buildClaudeArgs(config) {
|
|
3057
3013
|
const f = resolveLaunchFieldsOrDefault(config.runtimeConfig);
|
|
@@ -3066,11 +3022,11 @@ function buildClaudeArgs(config) {
|
|
|
3066
3022
|
"--input-format",
|
|
3067
3023
|
"stream-json",
|
|
3068
3024
|
"--include-partial-messages",
|
|
3069
|
-
"--model",
|
|
3070
|
-
f.model || DEFAULT_CLAUDE_MODEL,
|
|
3071
3025
|
"--disallowed-tools",
|
|
3072
3026
|
f.disallowedTools || CLAUDE_DISALLOWED_TOOLS
|
|
3073
3027
|
];
|
|
3028
|
+
if (f.model)
|
|
3029
|
+
args.push("--model", f.model);
|
|
3074
3030
|
if (f.reasoningEffort)
|
|
3075
3031
|
args.push("--effort", f.reasoningEffort);
|
|
3076
3032
|
if (f.fastMode)
|
|
@@ -3079,14 +3035,24 @@ function buildClaudeArgs(config) {
|
|
|
3079
3035
|
args.push("--resume", config.sessionId);
|
|
3080
3036
|
return args;
|
|
3081
3037
|
}
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
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 });
|
|
3085
3049
|
}
|
|
3086
|
-
function
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3050
|
+
function tryParseJsonLine(line) {
|
|
3051
|
+
try {
|
|
3052
|
+
return JSON.parse(line);
|
|
3053
|
+
} catch {
|
|
3054
|
+
return null;
|
|
3055
|
+
}
|
|
3090
3056
|
}
|
|
3091
3057
|
|
|
3092
3058
|
// src/drivers/claudeEventNormalizer.ts
|
|
@@ -3098,12 +3064,9 @@ class ClaudeEventNormalizer {
|
|
|
3098
3064
|
return this.currentSession;
|
|
3099
3065
|
}
|
|
3100
3066
|
normalizeLine(line) {
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
event = JSON.parse(line);
|
|
3104
|
-
} catch {
|
|
3067
|
+
const event = tryParseJsonLine(line);
|
|
3068
|
+
if (!event)
|
|
3105
3069
|
return [];
|
|
3106
|
-
}
|
|
3107
3070
|
if (event?.session_id)
|
|
3108
3071
|
this.currentSession = event.session_id;
|
|
3109
3072
|
const out = [];
|
|
@@ -3207,6 +3170,102 @@ class ClaudeEventNormalizer {
|
|
|
3207
3170
|
}
|
|
3208
3171
|
}
|
|
3209
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
|
+
|
|
3210
3269
|
// src/drivers/claude.ts
|
|
3211
3270
|
class ClaudeDriver {
|
|
3212
3271
|
id = "claude";
|
|
@@ -3219,6 +3278,13 @@ class ClaudeDriver {
|
|
|
3219
3278
|
supportsStdinNotification = true;
|
|
3220
3279
|
busyDeliveryMode = "gated";
|
|
3221
3280
|
supportsNativeStandingPrompt = true;
|
|
3281
|
+
capabilities = {
|
|
3282
|
+
reasoningEffort: true,
|
|
3283
|
+
fastMode: true,
|
|
3284
|
+
disallowedTools: true,
|
|
3285
|
+
command: true,
|
|
3286
|
+
sessionResumeMode: "by-id"
|
|
3287
|
+
};
|
|
3222
3288
|
eventNormalizer = new ClaudeEventNormalizer;
|
|
3223
3289
|
probe() {
|
|
3224
3290
|
return probeClaude();
|
|
@@ -3228,12 +3294,13 @@ class ClaudeDriver {
|
|
|
3228
3294
|
const { spawnEnv } = await prepareCliTransport(ctx, buildClaudeProviderIsolationEnv(ctx), cliConfig);
|
|
3229
3295
|
const args = buildClaudeArgs(ctx.config);
|
|
3230
3296
|
delete spawnEnv.CLAUDECODE;
|
|
3231
|
-
const
|
|
3232
|
-
const
|
|
3233
|
-
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, {
|
|
3234
3301
|
cwd: ctx.workingDirectory,
|
|
3235
3302
|
env: spawnEnv,
|
|
3236
|
-
shell:
|
|
3303
|
+
shell: spec.shell
|
|
3237
3304
|
});
|
|
3238
3305
|
const stdinMsg = JSON.stringify({
|
|
3239
3306
|
type: "user",
|
|
@@ -3258,7 +3325,7 @@ class ClaudeDriver {
|
|
|
3258
3325
|
});
|
|
3259
3326
|
}
|
|
3260
3327
|
buildSystemPrompt(config) {
|
|
3261
|
-
return buildCliTransportSystemPrompt(config
|
|
3328
|
+
return buildCliTransportSystemPrompt(config);
|
|
3262
3329
|
}
|
|
3263
3330
|
}
|
|
3264
3331
|
|
|
@@ -3315,12 +3382,9 @@ class CodexEventNormalizer {
|
|
|
3315
3382
|
this.threadId = threadId;
|
|
3316
3383
|
}
|
|
3317
3384
|
normalizeLine(line) {
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
msg = JSON.parse(line);
|
|
3321
|
-
} catch {
|
|
3385
|
+
const msg = tryParseJsonLine(line);
|
|
3386
|
+
if (!msg)
|
|
3322
3387
|
return [];
|
|
3323
|
-
}
|
|
3324
3388
|
if (msg?.error && msg.id !== undefined) {
|
|
3325
3389
|
return [{ kind: "error", message: msg.error?.message ?? "Codex RPC error" }];
|
|
3326
3390
|
}
|
|
@@ -3436,6 +3500,21 @@ function resolveCodexHomeRootFromEnv(env = process.env, opts = {}) {
|
|
|
3436
3500
|
return path7.join(opts.defaultHomeDir ?? os2.homedir(), ".codex");
|
|
3437
3501
|
}
|
|
3438
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
|
+
|
|
3439
3518
|
// src/drivers/codex.ts
|
|
3440
3519
|
class CodexDriver {
|
|
3441
3520
|
id = "codex";
|
|
@@ -3448,6 +3527,13 @@ class CodexDriver {
|
|
|
3448
3527
|
supportsStdinNotification = true;
|
|
3449
3528
|
busyDeliveryMode = "gated";
|
|
3450
3529
|
supportsNativeStandingPrompt = true;
|
|
3530
|
+
capabilities = {
|
|
3531
|
+
reasoningEffort: true,
|
|
3532
|
+
fastMode: true,
|
|
3533
|
+
disallowedTools: false,
|
|
3534
|
+
command: true,
|
|
3535
|
+
sessionResumeMode: "by-id"
|
|
3536
|
+
};
|
|
3451
3537
|
eventNormalizer = new CodexEventNormalizer;
|
|
3452
3538
|
requestId = 0;
|
|
3453
3539
|
codexHomeRoot = null;
|
|
@@ -3461,24 +3547,17 @@ class CodexDriver {
|
|
|
3461
3547
|
return probeCliRuntime("codex");
|
|
3462
3548
|
}
|
|
3463
3549
|
async spawn(ctx) {
|
|
3464
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3550
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3465
3551
|
this.codexHomeRoot = resolveCodexHomeRootFromEnv(spawnEnv, { cwd: ctx.workingDirectory });
|
|
3466
|
-
const
|
|
3552
|
+
const override = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig).command;
|
|
3553
|
+
const spec = resolveSpawnSpec("codex", ["app-server", "--listen", "stdio://"], override);
|
|
3467
3554
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3468
3555
|
cwd: ctx.workingDirectory,
|
|
3469
3556
|
env: spawnEnv,
|
|
3470
3557
|
shell: spec.shell
|
|
3471
3558
|
});
|
|
3472
3559
|
queueMicrotask(() => {
|
|
3473
|
-
proc.stdin?.write(
|
|
3474
|
-
jsonrpc: "2.0",
|
|
3475
|
-
id: this.nextRequestId(),
|
|
3476
|
-
method: "initialize",
|
|
3477
|
-
params: {
|
|
3478
|
-
clientInfo: { name: "agent-backend", version: "1.0.0" },
|
|
3479
|
-
capabilities: { experimentalApi: true }
|
|
3480
|
-
}
|
|
3481
|
-
}) + `
|
|
3560
|
+
proc.stdin?.write(jsonRpcRequest("initialize", { clientInfo: getDaemonClientInfo(), capabilities: { experimentalApi: true } }, this.nextRequestId()) + `
|
|
3482
3561
|
`);
|
|
3483
3562
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
3484
3563
|
const resuming = Boolean(ctx.config.sessionId);
|
|
@@ -3497,12 +3576,7 @@ class CodexDriver {
|
|
|
3497
3576
|
params.config = { model_reasoning_effort: f.reasoningEffort };
|
|
3498
3577
|
if (f.fastMode)
|
|
3499
3578
|
params.serviceTier = "fast";
|
|
3500
|
-
proc.stdin?.write(
|
|
3501
|
-
jsonrpc: "2.0",
|
|
3502
|
-
id: this.nextRequestId(),
|
|
3503
|
-
method: resuming ? "thread/resume" : "thread/start",
|
|
3504
|
-
params
|
|
3505
|
-
}) + `
|
|
3579
|
+
proc.stdin?.write(jsonRpcRequest(resuming ? "thread/resume" : "thread/start", params, this.nextRequestId()) + `
|
|
3506
3580
|
`);
|
|
3507
3581
|
});
|
|
3508
3582
|
return { process: proc };
|
|
@@ -3518,23 +3592,11 @@ class CodexDriver {
|
|
|
3518
3592
|
if (!threadId)
|
|
3519
3593
|
return null;
|
|
3520
3594
|
const input = [{ type: "text", text }];
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
jsonrpc: "2.0",
|
|
3524
|
-
id: this.nextRequestId(),
|
|
3525
|
-
method: "turn/start",
|
|
3526
|
-
params: { threadId, input }
|
|
3527
|
-
});
|
|
3528
|
-
}
|
|
3529
|
-
return JSON.stringify({
|
|
3530
|
-
jsonrpc: "2.0",
|
|
3531
|
-
id: this.nextRequestId(),
|
|
3532
|
-
method: "turn/steer",
|
|
3533
|
-
params: { threadId, input }
|
|
3534
|
-
});
|
|
3595
|
+
const method = opts?.mode === "idle" ? "turn/start" : "turn/steer";
|
|
3596
|
+
return jsonRpcRequest(method, { threadId, input }, this.nextRequestId());
|
|
3535
3597
|
}
|
|
3536
3598
|
buildSystemPrompt(config) {
|
|
3537
|
-
return buildCliTransportSystemPrompt(config
|
|
3599
|
+
return buildCliTransportSystemPrompt(config);
|
|
3538
3600
|
}
|
|
3539
3601
|
}
|
|
3540
3602
|
|
|
@@ -3562,37 +3624,42 @@ class GeminiDriver {
|
|
|
3562
3624
|
};
|
|
3563
3625
|
session = { recovery: "resume_or_fresh" };
|
|
3564
3626
|
model = {
|
|
3565
|
-
detectedModelsVerifiedAs: "
|
|
3566
|
-
toLaunchSpec: (modelId) => modelId
|
|
3627
|
+
detectedModelsVerifiedAs: "launchable",
|
|
3628
|
+
toLaunchSpec: (modelId) => modelId ? { args: ["--model", modelId] } : { args: [] }
|
|
3567
3629
|
};
|
|
3568
3630
|
supportsStdinNotification = false;
|
|
3569
3631
|
busyDeliveryMode = "none";
|
|
3632
|
+
capabilities = {
|
|
3633
|
+
reasoningEffort: false,
|
|
3634
|
+
fastMode: false,
|
|
3635
|
+
disallowedTools: false,
|
|
3636
|
+
command: true,
|
|
3637
|
+
sessionResumeMode: "by-id"
|
|
3638
|
+
};
|
|
3570
3639
|
sessionId = null;
|
|
3571
3640
|
probe() {
|
|
3572
3641
|
return probeCliRuntime("gemini");
|
|
3573
3642
|
}
|
|
3574
3643
|
async spawn(ctx) {
|
|
3575
3644
|
this.sessionId = ctx.config.sessionId ?? null;
|
|
3576
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3645
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3577
3646
|
spawnEnv.GEMINI_CLI_TRUST_WORKSPACE ??= "true";
|
|
3578
3647
|
if (process.platform === "win32")
|
|
3579
3648
|
spawnEnv.GEMINI_PTY_INFO ??= "child_process";
|
|
3580
|
-
const
|
|
3649
|
+
const override = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig).command;
|
|
3650
|
+
const spec = resolveSpawnSpec("gemini", buildGeminiArgs(ctx.config), override);
|
|
3581
3651
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3582
3652
|
cwd: ctx.workingDirectory,
|
|
3583
3653
|
env: spawnEnv,
|
|
3584
3654
|
shell: spec.shell
|
|
3585
3655
|
});
|
|
3586
|
-
proc
|
|
3656
|
+
writeToStdinAndDetach(proc, ctx.prompt);
|
|
3587
3657
|
return { process: proc };
|
|
3588
3658
|
}
|
|
3589
3659
|
parseLine(line) {
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
event = JSON.parse(line);
|
|
3593
|
-
} catch {
|
|
3660
|
+
const event = tryParseJsonLine(line);
|
|
3661
|
+
if (!event)
|
|
3594
3662
|
return [];
|
|
3595
|
-
}
|
|
3596
3663
|
switch (event?.type) {
|
|
3597
3664
|
case "init":
|
|
3598
3665
|
this.sessionId = event.session_id ?? this.sessionId;
|
|
@@ -3618,7 +3685,7 @@ class GeminiDriver {
|
|
|
3618
3685
|
return null;
|
|
3619
3686
|
}
|
|
3620
3687
|
buildSystemPrompt(config) {
|
|
3621
|
-
return buildCliTransportSystemPrompt(config
|
|
3688
|
+
return buildCliTransportSystemPrompt(config);
|
|
3622
3689
|
}
|
|
3623
3690
|
}
|
|
3624
3691
|
|
|
@@ -3638,13 +3705,20 @@ class CopilotDriver {
|
|
|
3638
3705
|
};
|
|
3639
3706
|
supportsStdinNotification = false;
|
|
3640
3707
|
busyDeliveryMode = "none";
|
|
3708
|
+
capabilities = {
|
|
3709
|
+
reasoningEffort: true,
|
|
3710
|
+
fastMode: false,
|
|
3711
|
+
disallowedTools: false,
|
|
3712
|
+
command: true,
|
|
3713
|
+
sessionResumeMode: "by-id"
|
|
3714
|
+
};
|
|
3641
3715
|
sessionId = null;
|
|
3642
3716
|
probe() {
|
|
3643
3717
|
return probeCliRuntime("copilot");
|
|
3644
3718
|
}
|
|
3645
3719
|
async spawn(ctx) {
|
|
3646
3720
|
this.sessionId = ctx.config.sessionId ?? null;
|
|
3647
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3721
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3648
3722
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
3649
3723
|
const args = ["--output-format", "json", "--allow-all-tools", "--allow-all-paths", "-p", ctx.prompt];
|
|
3650
3724
|
if (f.model)
|
|
@@ -3653,7 +3727,7 @@ class CopilotDriver {
|
|
|
3653
3727
|
args.push("--effort", f.reasoningEffort);
|
|
3654
3728
|
if (ctx.config.sessionId)
|
|
3655
3729
|
args.push(`--resume=${ctx.config.sessionId}`);
|
|
3656
|
-
const spec = resolveSpawnSpec("copilot", args);
|
|
3730
|
+
const spec = resolveSpawnSpec("copilot", args, f.command);
|
|
3657
3731
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3658
3732
|
cwd: ctx.workingDirectory,
|
|
3659
3733
|
env: spawnEnv,
|
|
@@ -3662,12 +3736,9 @@ class CopilotDriver {
|
|
|
3662
3736
|
return { process: proc };
|
|
3663
3737
|
}
|
|
3664
3738
|
parseLine(line) {
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
event = JSON.parse(line);
|
|
3668
|
-
} catch {
|
|
3739
|
+
const event = tryParseJsonLine(line);
|
|
3740
|
+
if (!event)
|
|
3669
3741
|
return [];
|
|
3670
|
-
}
|
|
3671
3742
|
switch (event?.type) {
|
|
3672
3743
|
case "assistant.turn_start":
|
|
3673
3744
|
if (event.sessionId)
|
|
@@ -3702,7 +3773,7 @@ class CopilotDriver {
|
|
|
3702
3773
|
return null;
|
|
3703
3774
|
}
|
|
3704
3775
|
buildSystemPrompt(config) {
|
|
3705
|
-
return buildCliTransportSystemPrompt(config
|
|
3776
|
+
return buildCliTransportSystemPrompt(config);
|
|
3706
3777
|
}
|
|
3707
3778
|
}
|
|
3708
3779
|
|
|
@@ -3722,13 +3793,20 @@ class CursorDriver {
|
|
|
3722
3793
|
};
|
|
3723
3794
|
supportsStdinNotification = false;
|
|
3724
3795
|
busyDeliveryMode = "none";
|
|
3796
|
+
capabilities = {
|
|
3797
|
+
reasoningEffort: false,
|
|
3798
|
+
fastMode: false,
|
|
3799
|
+
disallowedTools: false,
|
|
3800
|
+
command: true,
|
|
3801
|
+
sessionResumeMode: "by-id"
|
|
3802
|
+
};
|
|
3725
3803
|
sessionId = null;
|
|
3726
3804
|
probe() {
|
|
3727
3805
|
return probeCliRuntime("cursor-agent");
|
|
3728
3806
|
}
|
|
3729
3807
|
async spawn(ctx) {
|
|
3730
3808
|
this.sessionId = ctx.config.sessionId ?? null;
|
|
3731
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3809
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3732
3810
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
3733
3811
|
const args = ["--print", "--output-format", "stream-json", "--yolo", "--approve-mcps", "--trust"];
|
|
3734
3812
|
if (f.model)
|
|
@@ -3736,7 +3814,7 @@ class CursorDriver {
|
|
|
3736
3814
|
if (ctx.config.sessionId)
|
|
3737
3815
|
args.push("--resume", ctx.config.sessionId);
|
|
3738
3816
|
args.push(ctx.prompt);
|
|
3739
|
-
const spec = resolveSpawnSpec("cursor-agent", args);
|
|
3817
|
+
const spec = resolveSpawnSpec("cursor-agent", args, f.command);
|
|
3740
3818
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3741
3819
|
cwd: ctx.workingDirectory,
|
|
3742
3820
|
env: spawnEnv,
|
|
@@ -3745,12 +3823,9 @@ class CursorDriver {
|
|
|
3745
3823
|
return { process: proc };
|
|
3746
3824
|
}
|
|
3747
3825
|
parseLine(line) {
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
event = JSON.parse(line);
|
|
3751
|
-
} catch {
|
|
3826
|
+
const event = tryParseJsonLine(line);
|
|
3827
|
+
if (!event)
|
|
3752
3828
|
return [];
|
|
3753
|
-
}
|
|
3754
3829
|
if (event?.type === "system") {
|
|
3755
3830
|
if (event.subtype === "init") {
|
|
3756
3831
|
this.sessionId = event.session_id ?? this.sessionId;
|
|
@@ -3793,7 +3868,7 @@ class CursorDriver {
|
|
|
3793
3868
|
return null;
|
|
3794
3869
|
}
|
|
3795
3870
|
buildSystemPrompt(config) {
|
|
3796
|
-
return buildCliTransportSystemPrompt(config
|
|
3871
|
+
return buildCliTransportSystemPrompt(config);
|
|
3797
3872
|
}
|
|
3798
3873
|
}
|
|
3799
3874
|
|
|
@@ -3815,6 +3890,13 @@ class OpenCodeDriver {
|
|
|
3815
3890
|
busyDeliveryMode = "none";
|
|
3816
3891
|
terminateProcessOnTurnEnd = true;
|
|
3817
3892
|
deferSpawnUntilMessage = true;
|
|
3893
|
+
capabilities = {
|
|
3894
|
+
reasoningEffort: false,
|
|
3895
|
+
fastMode: false,
|
|
3896
|
+
disallowedTools: false,
|
|
3897
|
+
command: true,
|
|
3898
|
+
sessionResumeMode: "by-id"
|
|
3899
|
+
};
|
|
3818
3900
|
sessionId = null;
|
|
3819
3901
|
shouldDeferWakeMessage(message) {
|
|
3820
3902
|
return message?.type === "system";
|
|
@@ -3825,7 +3907,7 @@ class OpenCodeDriver {
|
|
|
3825
3907
|
async spawn(ctx) {
|
|
3826
3908
|
this.sessionId = ctx.config.sessionId ?? null;
|
|
3827
3909
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
3828
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
3910
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
3829
3911
|
const args = ["run", "--format", "json", "--dangerously-skip-permissions", "--pure", "--dir", ctx.workingDirectory];
|
|
3830
3912
|
if (f.model)
|
|
3831
3913
|
args.push("--model", f.model);
|
|
@@ -3833,7 +3915,7 @@ class OpenCodeDriver {
|
|
|
3833
3915
|
args.push("--session", ctx.config.sessionId);
|
|
3834
3916
|
const promptArg = ctx.prompt === ctx.standingPrompt ? "No new messages are pending. Stop now." : ctx.prompt;
|
|
3835
3917
|
args.push("--", promptArg);
|
|
3836
|
-
const spec = resolveSpawnSpec("opencode", args);
|
|
3918
|
+
const spec = resolveSpawnSpec("opencode", args, f.command);
|
|
3837
3919
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3838
3920
|
cwd: ctx.workingDirectory,
|
|
3839
3921
|
env: spawnEnv,
|
|
@@ -3843,12 +3925,9 @@ class OpenCodeDriver {
|
|
|
3843
3925
|
return { process: proc };
|
|
3844
3926
|
}
|
|
3845
3927
|
parseLine(line) {
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
event = JSON.parse(line);
|
|
3849
|
-
} catch {
|
|
3928
|
+
const event = tryParseJsonLine(line);
|
|
3929
|
+
if (!event)
|
|
3850
3930
|
return [];
|
|
3851
|
-
}
|
|
3852
3931
|
const out = [];
|
|
3853
3932
|
if (event?.sessionID && this.sessionId !== event.sessionID) {
|
|
3854
3933
|
this.sessionId = event.sessionID;
|
|
@@ -3886,12 +3965,12 @@ class OpenCodeDriver {
|
|
|
3886
3965
|
return null;
|
|
3887
3966
|
}
|
|
3888
3967
|
buildSystemPrompt(config) {
|
|
3889
|
-
return buildCliTransportSystemPrompt(config
|
|
3968
|
+
return buildCliTransportSystemPrompt(config);
|
|
3890
3969
|
}
|
|
3891
3970
|
}
|
|
3892
3971
|
|
|
3893
3972
|
// src/drivers/antigravity.ts
|
|
3894
|
-
import { randomUUID } from "crypto";
|
|
3973
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3895
3974
|
var ERROR_LINE_PATTERNS = [/^error[:\s]/i, /\bfatal\b/i, /\bpanic\b/i, /unable to/i];
|
|
3896
3975
|
var ANTIGRAVITY_PRINT_TIMEOUT = "30m";
|
|
3897
3976
|
function buildAntigravityArgs(ctx) {
|
|
@@ -3916,27 +3995,34 @@ class AntigravityDriver {
|
|
|
3916
3995
|
};
|
|
3917
3996
|
supportsStdinNotification = false;
|
|
3918
3997
|
busyDeliveryMode = "none";
|
|
3998
|
+
capabilities = {
|
|
3999
|
+
reasoningEffort: false,
|
|
4000
|
+
fastMode: false,
|
|
4001
|
+
disallowedTools: false,
|
|
4002
|
+
command: true,
|
|
4003
|
+
sessionResumeMode: "most-recent"
|
|
4004
|
+
};
|
|
3919
4005
|
sessionId = null;
|
|
3920
4006
|
sentInit = false;
|
|
3921
4007
|
probe() {
|
|
3922
4008
|
return probeCliRuntime("agy");
|
|
3923
4009
|
}
|
|
3924
4010
|
async spawn(ctx) {
|
|
3925
|
-
this.sessionId = ctx.config.sessionId ??
|
|
4011
|
+
this.sessionId = ctx.config.sessionId ?? randomUUID2();
|
|
3926
4012
|
this.sentInit = false;
|
|
3927
4013
|
const { spawnEnv } = await prepareCliTransport(ctx, {
|
|
3928
|
-
NO_COLOR: "1",
|
|
3929
4014
|
SSH_CLIENT: "",
|
|
3930
4015
|
SSH_CONNECTION: "",
|
|
3931
4016
|
SSH_TTY: ""
|
|
3932
4017
|
});
|
|
3933
|
-
const
|
|
4018
|
+
const override = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig).command;
|
|
4019
|
+
const spec = resolveSpawnSpec("agy", buildAntigravityArgs(ctx), override);
|
|
3934
4020
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
3935
4021
|
cwd: ctx.workingDirectory,
|
|
3936
4022
|
env: spawnEnv,
|
|
3937
4023
|
shell: spec.shell
|
|
3938
4024
|
});
|
|
3939
|
-
proc
|
|
4025
|
+
writeToStdinAndDetach(proc, ctx.prompt);
|
|
3940
4026
|
return { process: proc };
|
|
3941
4027
|
}
|
|
3942
4028
|
parseLine(line) {
|
|
@@ -3961,12 +4047,13 @@ class AntigravityDriver {
|
|
|
3961
4047
|
return null;
|
|
3962
4048
|
}
|
|
3963
4049
|
buildSystemPrompt(config) {
|
|
3964
|
-
return buildCliTransportSystemPrompt(config
|
|
4050
|
+
return buildCliTransportSystemPrompt(config);
|
|
3965
4051
|
}
|
|
3966
4052
|
}
|
|
3967
4053
|
|
|
3968
4054
|
// src/drivers/kimi.ts
|
|
3969
|
-
import { randomUUID as
|
|
4055
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4056
|
+
var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
|
|
3970
4057
|
function parseToolArguments(args) {
|
|
3971
4058
|
if (typeof args !== "string")
|
|
3972
4059
|
return args ?? {};
|
|
@@ -3987,55 +4074,49 @@ class KimiDriver {
|
|
|
3987
4074
|
};
|
|
3988
4075
|
supportsStdinNotification = true;
|
|
3989
4076
|
busyDeliveryMode = "direct";
|
|
4077
|
+
capabilities = {
|
|
4078
|
+
reasoningEffort: false,
|
|
4079
|
+
fastMode: false,
|
|
4080
|
+
disallowedTools: false,
|
|
4081
|
+
command: true,
|
|
4082
|
+
sessionResumeMode: "by-id"
|
|
4083
|
+
};
|
|
3990
4084
|
sessionId = "";
|
|
3991
4085
|
sentInit = false;
|
|
3992
|
-
promptRequestId =
|
|
4086
|
+
promptRequestId = randomUUID3();
|
|
3993
4087
|
probe() {
|
|
3994
4088
|
return probeCliRuntime("kimi");
|
|
3995
4089
|
}
|
|
3996
4090
|
async spawn(ctx) {
|
|
3997
|
-
this.sessionId = ctx.config.sessionId ||
|
|
4091
|
+
this.sessionId = ctx.config.sessionId || randomUUID3();
|
|
3998
4092
|
const isResume = Boolean(ctx.config.sessionId);
|
|
3999
|
-
const { spawnEnv } = await prepareCliTransport(ctx
|
|
4093
|
+
const { spawnEnv } = await prepareCliTransport(ctx);
|
|
4000
4094
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
4001
4095
|
const args = ["--wire", "--yolo", "--session", this.sessionId];
|
|
4002
4096
|
if (f.model)
|
|
4003
4097
|
args.push("--model", f.model);
|
|
4004
|
-
const spec = resolveSpawnSpec("kimi", args);
|
|
4098
|
+
const spec = resolveSpawnSpec("kimi", args, f.command);
|
|
4005
4099
|
const proc = spawnAgentProcess(spec.command, spec.args, {
|
|
4006
4100
|
cwd: ctx.workingDirectory,
|
|
4007
4101
|
env: spawnEnv,
|
|
4008
4102
|
shell: spec.shell
|
|
4009
4103
|
});
|
|
4010
|
-
proc.stdin?.write(
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
params: {
|
|
4015
|
-
protocol_version: "1.3",
|
|
4016
|
-
client: { name: "agent-backend", version: "1.0.0" },
|
|
4017
|
-
capabilities: { supports_question: false, supports_plan_mode: false }
|
|
4018
|
-
}
|
|
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 }
|
|
4019
4108
|
}) + `
|
|
4020
4109
|
`);
|
|
4021
|
-
proc.stdin?.write(
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
method: "prompt",
|
|
4025
|
-
params: {
|
|
4026
|
-
user_input: isResume ? ctx.prompt : "Your system prompt contains your standing instructions. Follow it now and begin listening for messages."
|
|
4027
|
-
}
|
|
4028
|
-
}) + `
|
|
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) + `
|
|
4029
4113
|
`);
|
|
4030
4114
|
return { process: proc };
|
|
4031
4115
|
}
|
|
4032
4116
|
parseLine(line) {
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
msg = JSON.parse(line);
|
|
4036
|
-
} catch {
|
|
4117
|
+
const msg = tryParseJsonLine(line);
|
|
4118
|
+
if (!msg)
|
|
4037
4119
|
return [];
|
|
4038
|
-
}
|
|
4039
4120
|
const out = [];
|
|
4040
4121
|
if (!this.sentInit) {
|
|
4041
4122
|
this.sentInit = true;
|
|
@@ -4087,16 +4168,17 @@ class KimiDriver {
|
|
|
4087
4168
|
}
|
|
4088
4169
|
encodeStdinMessage(text, _sessionId, opts) {
|
|
4089
4170
|
const method = opts?.mode === "idle" ? "prompt" : "steer";
|
|
4090
|
-
return
|
|
4171
|
+
return jsonRpcRequest(method, { user_input: text });
|
|
4091
4172
|
}
|
|
4092
4173
|
buildSystemPrompt(config) {
|
|
4093
|
-
return buildCliTransportSystemPrompt(config
|
|
4174
|
+
return buildCliTransportSystemPrompt(config);
|
|
4094
4175
|
}
|
|
4095
4176
|
}
|
|
4096
4177
|
|
|
4097
4178
|
// src/drivers/pi.ts
|
|
4098
|
-
import { createRequire as
|
|
4099
|
-
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";
|
|
4100
4182
|
import * as path8 from "path";
|
|
4101
4183
|
|
|
4102
4184
|
// src/runtime/sdkRuntimeSession.ts
|
|
@@ -4206,6 +4288,24 @@ function resolvePiSdkPackageDir(deps = {}) {
|
|
|
4206
4288
|
} catch {}
|
|
4207
4289
|
return;
|
|
4208
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
|
+
}
|
|
4209
4309
|
function resolvePiSdkVersionFromPath(deps = {}) {
|
|
4210
4310
|
const dir = resolvePiSdkPackageDir(deps);
|
|
4211
4311
|
if (!dir)
|
|
@@ -4219,7 +4319,7 @@ function resolvePiSdkVersionFromPath(deps = {}) {
|
|
|
4219
4319
|
}
|
|
4220
4320
|
function readPiSdkVersion() {
|
|
4221
4321
|
try {
|
|
4222
|
-
const req =
|
|
4322
|
+
const req = createRequire3(import.meta.url);
|
|
4223
4323
|
const pkg = req("@earendil-works/pi-coding-agent/package.json");
|
|
4224
4324
|
if (pkg.version)
|
|
4225
4325
|
return pkg.version;
|
|
@@ -4270,6 +4370,13 @@ class PiDriver {
|
|
|
4270
4370
|
supportsStdinNotification = true;
|
|
4271
4371
|
busyDeliveryMode = "direct";
|
|
4272
4372
|
supportsNativeStandingPrompt = true;
|
|
4373
|
+
capabilities = {
|
|
4374
|
+
reasoningEffort: true,
|
|
4375
|
+
fastMode: false,
|
|
4376
|
+
disallowedTools: false,
|
|
4377
|
+
command: true,
|
|
4378
|
+
sessionResumeMode: "by-id"
|
|
4379
|
+
};
|
|
4273
4380
|
sessionId = null;
|
|
4274
4381
|
probe() {
|
|
4275
4382
|
const version = readPiSdkVersion();
|
|
@@ -4283,10 +4390,6 @@ class PiDriver {
|
|
|
4283
4390
|
}
|
|
4284
4391
|
async createSession(ctx, deps) {
|
|
4285
4392
|
const spawnEnv = await deps.buildSpawnEnv();
|
|
4286
|
-
if (ctx.standingPrompt) {
|
|
4287
|
-
mkdirSync7(ctx.workingDirectory, { recursive: true });
|
|
4288
|
-
writeAgentFile(ctx.workingDirectory, ctx.standingPrompt);
|
|
4289
|
-
}
|
|
4290
4393
|
const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
|
|
4291
4394
|
const { session, sessionId } = await deps.createAgentSession({
|
|
4292
4395
|
cwd: ctx.workingDirectory,
|
|
@@ -4320,7 +4423,7 @@ class PiDriver {
|
|
|
4320
4423
|
return null;
|
|
4321
4424
|
}
|
|
4322
4425
|
buildSystemPrompt(config) {
|
|
4323
|
-
return buildCliTransportSystemPrompt(config
|
|
4426
|
+
return buildCliTransportSystemPrompt(config);
|
|
4324
4427
|
}
|
|
4325
4428
|
}
|
|
4326
4429
|
|
|
@@ -4423,7 +4526,21 @@ async function importPiSdkFromGlobalInstall() {
|
|
|
4423
4526
|
const pkg = JSON.parse(readFileSync6(path10.join(dir, "package.json"), "utf-8"));
|
|
4424
4527
|
const entry = pkg.exports?.["."]?.import ?? pkg.main ?? "./dist/index.js";
|
|
4425
4528
|
const entryPath = path10.join(dir, entry);
|
|
4426
|
-
|
|
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
|
+
}
|
|
4427
4544
|
}
|
|
4428
4545
|
function loadPiSdkModule() {
|
|
4429
4546
|
if (!cachedSdkPromise) {
|
|
@@ -4466,7 +4583,15 @@ function createPiSdkDriverDeps(ctx, loadSdk = loadPiSdkModule) {
|
|
|
4466
4583
|
const parsed = parseModelString(opts.model);
|
|
4467
4584
|
const model = parsed ? modelRegistry.find(parsed.provider, parsed.id) : undefined;
|
|
4468
4585
|
const cwd = opts.cwd;
|
|
4469
|
-
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
|
+
}
|
|
4470
4595
|
const spawnEnv = opts.spawnEnv;
|
|
4471
4596
|
const bashTool = sdk.createBashToolDefinition(cwd, {
|
|
4472
4597
|
spawnHook: (spawnCtx) => ({ ...spawnCtx, env: { ...spawnCtx.env, ...spawnEnv } })
|
|
@@ -4519,7 +4644,7 @@ function emitImplicitTypingStopOnSend(args) {
|
|
|
4519
4644
|
}
|
|
4520
4645
|
async function createDaemon(opts) {
|
|
4521
4646
|
const log = opts.logger ?? createLogger({ header: "@alook/daemon" });
|
|
4522
|
-
const fallbackBase = (process.env.ALOOK_PROJECT_ROOT || `${
|
|
4647
|
+
const fallbackBase = (process.env.ALOOK_PROJECT_ROOT || `${homedir3()}/.alook`) + "/daemon";
|
|
4523
4648
|
const workdirFor = (agentId) => `${opts.workingDirectoryBase ?? fallbackBase}/${agentId}`;
|
|
4524
4649
|
const resolvedCliPath = resolveAlookCliPathWithFallback(opts.agentCliPath);
|
|
4525
4650
|
const timeline2 = createTimelineRecorder({
|
|
@@ -4814,6 +4939,11 @@ async function createDaemon(opts) {
|
|
|
4814
4939
|
await router.start();
|
|
4815
4940
|
return {
|
|
4816
4941
|
isOpen: () => channel.status === "open",
|
|
4942
|
+
onOpen: (hook) => {
|
|
4943
|
+
channel.onOpen(hook);
|
|
4944
|
+
if (channel.status === "open")
|
|
4945
|
+
queueMicrotask(hook);
|
|
4946
|
+
},
|
|
4817
4947
|
proxyUrl: proxy.url,
|
|
4818
4948
|
stop: async () => {
|
|
4819
4949
|
for (const agentId of [...typingHeartbeats.keys()]) {
|
|
@@ -4827,24 +4957,19 @@ async function createDaemon(opts) {
|
|
|
4827
4957
|
}
|
|
4828
4958
|
|
|
4829
4959
|
// src/cli/daemonStart.ts
|
|
4830
|
-
var requireFromHere = createRequire3(import.meta.url);
|
|
4831
|
-
function readDaemonVersion() {
|
|
4832
|
-
try {
|
|
4833
|
-
const pkg = requireFromHere("../../package.json");
|
|
4834
|
-
return pkg.version ?? "";
|
|
4835
|
-
} catch {
|
|
4836
|
-
return "";
|
|
4837
|
-
}
|
|
4838
|
-
}
|
|
4839
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;
|
|
4840
4965
|
function resolveDefaultBaseDir() {
|
|
4841
|
-
const root = process.env.ALOOK_PROJECT_ROOT || path11.join(
|
|
4966
|
+
const root = process.env.ALOOK_PROJECT_ROOT || path11.join(homedir4(), ".alook");
|
|
4842
4967
|
return path11.join(root, "daemon");
|
|
4843
4968
|
}
|
|
4844
4969
|
var DEFAULT_BASE_DIR = resolveDefaultBaseDir();
|
|
4845
4970
|
var log = createLogger({ header: "@alook/daemon" });
|
|
4846
4971
|
function keyHash(machineKey) {
|
|
4847
|
-
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);
|
|
4848
4973
|
}
|
|
4849
4974
|
function daemonsDir(baseDir) {
|
|
4850
4975
|
return path11.join(baseDir, "daemons");
|
|
@@ -4912,14 +5037,14 @@ function daemonList(opts) {
|
|
|
4912
5037
|
}
|
|
4913
5038
|
results.push({
|
|
4914
5039
|
keyHash: file.replace(".pid", ""),
|
|
4915
|
-
keyPrefix: data.key.slice(0,
|
|
5040
|
+
keyPrefix: data.key.slice(0, MACHINE_KEY_DISPLAY_PREFIX_LEN) + "…",
|
|
4916
5041
|
pid: data.pid,
|
|
4917
5042
|
alive
|
|
4918
5043
|
});
|
|
4919
5044
|
}
|
|
4920
5045
|
return results;
|
|
4921
5046
|
}
|
|
4922
|
-
function daemonStop(opts) {
|
|
5047
|
+
async function daemonStop(opts) {
|
|
4923
5048
|
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
4924
5049
|
const pf = pidfilePath(baseDir, opts.machineKey);
|
|
4925
5050
|
const data = readPidFile(pf);
|
|
@@ -4936,13 +5061,12 @@ function daemonStop(opts) {
|
|
|
4936
5061
|
}
|
|
4937
5062
|
log.info(`sending SIGTERM to daemon (pid ${data.pid})…`);
|
|
4938
5063
|
process.kill(data.pid, "SIGTERM");
|
|
4939
|
-
const deadline = Date.now() +
|
|
5064
|
+
const deadline = Date.now() + STOP_GRACE_MS;
|
|
4940
5065
|
while (Date.now() < deadline && isProcessAlive(data.pid)) {
|
|
4941
|
-
|
|
4942
|
-
while (Date.now() - start < 100) {}
|
|
5066
|
+
await new Promise((r) => setTimeout(r, POLL_MS2));
|
|
4943
5067
|
}
|
|
4944
5068
|
if (isProcessAlive(data.pid)) {
|
|
4945
|
-
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`);
|
|
4946
5070
|
process.kill(data.pid, "SIGKILL");
|
|
4947
5071
|
} else {
|
|
4948
5072
|
log.info("daemon stopped");
|
|
@@ -4954,9 +5078,6 @@ function daemonStop(opts) {
|
|
|
4954
5078
|
function credentialFilePathByMachineId(baseDir, machineId) {
|
|
4955
5079
|
return path11.join(daemonsDir(baseDir), `${machineId}.credential.json`);
|
|
4956
5080
|
}
|
|
4957
|
-
function credentialFilesDir(baseDir) {
|
|
4958
|
-
return daemonsDir(baseDir);
|
|
4959
|
-
}
|
|
4960
5081
|
function readCredentialFile(filePath) {
|
|
4961
5082
|
if (!fs9.existsSync(filePath))
|
|
4962
5083
|
return null;
|
|
@@ -4973,7 +5094,7 @@ function writeCredentialFile(filePath, credential, machineId) {
|
|
|
4973
5094
|
fs9.writeFileSync(filePath, JSON.stringify({ credential, machineId }), { mode: 384 });
|
|
4974
5095
|
}
|
|
4975
5096
|
function findExistingCredentialForBearer(baseDir, bearer) {
|
|
4976
|
-
const dir =
|
|
5097
|
+
const dir = daemonsDir(baseDir);
|
|
4977
5098
|
if (!fs9.existsSync(dir))
|
|
4978
5099
|
return null;
|
|
4979
5100
|
for (const file of fs9.readdirSync(dir)) {
|
|
@@ -5084,16 +5205,9 @@ async function daemonStart(opts) {
|
|
|
5084
5205
|
}
|
|
5085
5206
|
});
|
|
5086
5207
|
log.info(`daemon up — proxy at ${daemon.proxyUrl}, dialing ${wsUrl}`);
|
|
5087
|
-
|
|
5088
|
-
if (daemon.isOpen()) {
|
|
5089
|
-
clearInterval(readyTimer);
|
|
5090
|
-
log.info("control plane OPEN");
|
|
5091
|
-
}
|
|
5092
|
-
}, 200);
|
|
5093
|
-
readyTimer.unref?.();
|
|
5208
|
+
daemon.onOpen(() => log.info("control plane OPEN"));
|
|
5094
5209
|
const shutdown = async () => {
|
|
5095
5210
|
log.info("shutting down…");
|
|
5096
|
-
clearInterval(readyTimer);
|
|
5097
5211
|
releaseLock2(pf);
|
|
5098
5212
|
await daemon.stop();
|
|
5099
5213
|
process.exit(0);
|
|
@@ -5518,9 +5632,9 @@ function buildProgram() {
|
|
|
5518
5632
|
baseDir: localOpts.baseDir
|
|
5519
5633
|
});
|
|
5520
5634
|
});
|
|
5521
|
-
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() {
|
|
5522
5636
|
const localOpts = this.opts();
|
|
5523
|
-
daemonStop({
|
|
5637
|
+
await daemonStop({
|
|
5524
5638
|
machineKey: localOpts.machineKey,
|
|
5525
5639
|
baseDir: localOpts.baseDir
|
|
5526
5640
|
});
|