@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.
Files changed (3) hide show
  1. package/dist/cli/index.js +1935 -1821
  2. package/dist/index.js +440 -302
  3. 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 homedir3 } from "os";
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 homedir2 } from "os";
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 ?? 30000);
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 ?? 500;
404
- const max = this.opts.reconnect?.maxMs ?? 30000;
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 ?? 15000;
418
- const timeout = this.opts.heartbeat?.pongTimeoutMs ?? 30000;
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 DEFAULT_GRACE_MS = 2000;
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 ?? 2000 });
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/util/localTime.ts
1297
- function localISOString(now) {
1298
- const tzOffset = -now.getTimezoneOffset();
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/manager/managerRuntime.ts
1325
- var THINKING_MAX_BYTES = 4096;
1326
- var MAX_TARGET_CODE_UNITS = 200;
1327
- function canonicalToolName(rawName) {
1328
- const lower = rawName.toLowerCase();
1329
- switch (lower) {
1330
- case "bash":
1331
- case "shell":
1332
- return "bash";
1333
- case "read":
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
- function classify(canonicalName) {
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
- function coerceInputRecord(input) {
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
- if (!input || typeof input !== "object" || Array.isArray(input))
1396
- return;
1397
- return input;
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 pickCommandString(input) {
1400
- const rec = coerceInputRecord(input);
1401
- if (!rec)
1402
- return;
1403
- if (typeof rec.command === "string")
1404
- return rec.command;
1405
- if (Array.isArray(rec.command))
1406
- return rec.command.filter((v) => typeof v === "string").join(" ");
1407
- return;
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 pickFileTarget(input) {
1410
- const rec = coerceInputRecord(input);
1411
- if (!rec)
1412
- return;
1413
- if (typeof rec.file_path === "string")
1414
- return rec.file_path;
1415
- if (typeof rec.path === "string")
1416
- return rec.path;
1417
- if (typeof rec.notebook_path === "string")
1418
- return rec.notebook_path;
1419
- return;
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 pickPatternTarget(input) {
1422
- const rec = coerceInputRecord(input);
1423
- if (!rec)
1424
- return;
1425
- if (typeof rec.pattern === "string")
1426
- return rec.pattern;
1427
- if (typeof rec.query === "string")
1428
- return rec.query;
1429
- if (typeof rec.path === "string")
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 pickFallthroughTarget(input) {
1434
- const rec = coerceInputRecord(input);
1435
- if (!rec)
1436
- return;
1437
- if (typeof rec.url === "string")
1438
- return rec.url;
1439
- if (typeof rec.query === "string")
1440
- return rec.query;
1441
- if (typeof rec.path === "string")
1442
- return rec.path;
1443
- if (typeof rec.name === "string")
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 isAlookShellInvocation(command) {
1448
- if (!command)
1449
- return false;
1450
- return /^alook(\s|$)/.test(command.trimStart());
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 truncateTargetToCodeUnits(s) {
1453
- if (s.length <= MAX_TARGET_CODE_UNITS)
1454
- return s;
1455
- let end = MAX_TARGET_CODE_UNITS - 1;
1456
- const cu = s.charCodeAt(end - 1);
1457
- if (cu >= 55296 && cu <= 56319)
1458
- end -= 1;
1459
- return s.slice(0, end) + "";
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 extractToolAudit(rawName, rawInput) {
1462
- const name = canonicalToolName(rawName);
1463
- const cls = classify(name);
1464
- if (cls === "shell") {
1465
- const raw = pickCommandString(rawInput);
1466
- if (isAlookShellInvocation(raw)) {
1467
- return { name, suppressed: true };
1468
- }
1469
- const firstLine = typeof raw === "string" ? raw.split(`
1470
- `).map((s) => s.trim()).find((s) => s.length > 0) : undefined;
1471
- if (!firstLine)
1472
- return { name, suppressed: false };
1473
- return { name, target: truncateTargetToCodeUnits(firstLine), suppressed: false };
1474
- }
1475
- let target;
1476
- if (cls === "file_target")
1477
- target = pickFileTarget(rawInput);
1478
- else if (cls === "pattern")
1479
- target = pickPatternTarget(rawInput);
1480
- else
1481
- target = pickFallthroughTarget(rawInput);
1482
- if (typeof target !== "string" || target.length === 0) {
1483
- return { name, suppressed: false };
1484
- }
1485
- return { name, target: truncateTargetToCodeUnits(target), suppressed: false };
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 truncateThinking(text) {
1488
- const chars = [...text].length;
1489
- const buf = Buffer.from(text, "utf8");
1490
- if (buf.byteLength <= THINKING_MAX_BYTES) {
1491
- return { text, truncated: false, chars };
1492
- }
1493
- let end = THINKING_MAX_BYTES;
1494
- while (end > 0 && (buf[end] & 192) === 128)
1495
- end--;
1496
- const truncatedText = buf.subarray(0, end).toString("utf8");
1497
- return { text: truncatedText, truncated: true, chars };
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
- class AgentProcessManager {
1501
- state;
1502
- sessions = new Map;
1503
- runtimeConfigs = new Map;
1504
- resumeSessions = new Map;
1505
- launchIds = new Map;
1506
- liveSessions = new Map;
1507
- thinkingBuffers = new Map;
1508
- activeSpawnState = new Map;
1509
- opts;
1510
- tickTimer = null;
1511
- now;
1512
- log;
1513
- constructor(opts) {
1514
- this.opts = {
1515
- tickIntervalMs: 5000,
1516
- staleThresholdMs: 120000,
1517
- idleTimeoutMs: 300000,
1518
- stampWakePromptTime: false,
1519
- ...opts
1520
- };
1521
- this.now = opts.now ?? (() => Date.now());
1522
- this.log = opts.logger ?? createLogger({ header: "@alook/daemon:manager" });
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
- register(agentId, launch) {
1526
- if (launch?.runtimeConfig)
1527
- this.runtimeConfigs.set(agentId, launch.runtimeConfig);
1528
- if (launch?.sessionId)
1529
- this.resumeSessions.set(agentId, launch.sessionId);
1530
- if (launch?.launchId)
1531
- this.launchIds.set(agentId, launch.launchId);
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
- deliver(agentId, message) {
1541
- this.dispatch({ type: "wake", agentId, message, nowMs: this.now() });
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
- forgetSession(agentId) {
1544
- this.resumeSessions.delete(agentId);
1545
- this.liveSessions.delete(agentId);
1546
- this.dispatch({ type: "reset_session", agentId });
1547
- this.opts.timeline?.forgetSession(agentId);
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
- enqueueRewake(agentId, message) {
1550
- this.dispatch({ type: "rewake_after_reset", agentId, message });
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
- markResetting(agentId) {
1553
- this.dispatch({ type: "begin_reset", agentId });
1637
+ try {
1638
+ fs3.symlinkSync(hostCliPath, linkPath);
1639
+ } catch (err) {
1640
+ if (err.code !== "EEXIST")
1641
+ throw err;
1554
1642
  }
1555
- async resetSession(agentId, opts) {
1556
- this.register(agentId, { runtimeConfig: opts.runtimeConfig, launchId: opts.launchId });
1557
- this.forgetSession(agentId);
1558
- this.markResetting(agentId);
1559
- const status = this.state.agents[agentId]?.status;
1560
- if (status === "idle") {
1561
- try {
1562
- this.deliver(agentId, { text: opts.rewakePrompt });
1563
- } catch (err) {
1564
- this.log.error("agent reset idle-branch spawn threw synchronously", {
1565
- agentId,
1566
- err: err instanceof Error ? err.message : String(err)
1567
- });
1568
- this.dispatch({ type: "exit", agentId });
1569
- throw err;
1570
- }
1571
- return;
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
- async stop(agentId) {
1583
- const session = this.sessions.get(agentId);
1584
- if (!session)
1585
- return;
1586
- await Promise.resolve(session.stop({ reason: "requested", forceAfterMs: 5000 }));
1587
- this.sessions.delete(agentId);
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
- async stopAll() {
1590
- if (this.tickTimer) {
1591
- clearInterval(this.tickTimer);
1592
- this.tickTimer = null;
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
- await Promise.all([...this.sessions.values()].map((s) => Promise.resolve(s.stop({ reason: "shutdown" }))));
1595
- this.sessions.clear();
1596
- }
1597
- snapshot() {
1598
- return this.state;
1599
- }
1600
- auditContext(agentId) {
1601
- return {
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
- deriveActivitySnapshot(state) {
1629
- const snapshot = {};
1630
- for (const [agentId, agent] of Object.entries(state.agents))
1631
- snapshot[agentId] = this.deriveActivity(agent);
1632
- return snapshot;
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
- withFooter(text) {
1640
- return this.opts.wakePromptFooter ? `${text}
1764
+ ensureSymlinks(workDir);
1765
+ return changed;
1766
+ }
1641
1767
 
1642
- ${this.opts.wakePromptFooter}` : text;
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
- stampNow(text) {
1645
- return this.opts.stampWakePromptTime ? `[${nowLocalISO()}] ${text}` : text;
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
- applyEffect(effect) {
1648
- switch (effect.type) {
1649
- case "spawn":
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
- logSessionEnded(agentId, reason) {
1680
- this.log.info("agent session ended", { agentId, sessionId: this.liveSessions.get(agentId) ?? "", reason });
1681
- }
1682
- doSpawn(agentId, prompt, resumeSessionId) {
1683
- const driver = this.opts.driverFor(agentId, this.runtimeConfigs.get(agentId));
1684
- this.log.info("spawning agent", { agentId, runtime: driver.id });
1685
- const base = this.opts.baseContextFor(agentId);
1686
- const runtimeConfig = this.runtimeConfigs.get(agentId) ?? base.config?.runtimeConfig;
1687
- const provider = runtimeConfig?.runtime ?? null;
1688
- const sessionId = resumeSessionId ?? this.resumeSessions.get(agentId) ?? this.opts.timeline?.resumeSessionId(agentId, provider) ?? base.config?.sessionId;
1689
- const description = runtimeConfig?.instruction ?? base.config?.description ?? runtimeConfig?.agentName;
1690
- const agentName = runtimeConfig?.agentName ?? base.config?.agentName;
1691
- const agentHandle = runtimeConfig?.agentHandle ?? base.config?.agentHandle;
1692
- const config = { ...base.config ?? {}, runtimeConfig, sessionId, description, agentName, agentHandle };
1693
- const standingPrompt = base.standingPrompt || driver.buildSystemPrompt?.(config, agentId) || "";
1694
- const ctx = {
1695
- ...base,
1696
- prompt,
1697
- standingPrompt,
1698
- credentialProxy: base.credentialProxy ?? this.opts.credentialProxy,
1699
- launchId: this.launchIds.get(agentId) ?? base.launchId,
1700
- config
1701
- };
1702
- if (!this.opts.sessionFactory && driver.createSession && !this.opts.sdkDriverDepsFor) {
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
- if (ev.kind === "session_init" && ev.sessionId) {
1810
- this.dispatch({ type: "session", agentId, sessionId: ev.sessionId });
1811
- this.liveSessions.set(agentId, ev.sessionId);
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
- // src/manager/agentRouter.ts
1832
- class UnknownBotError extends Error {
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
- class BotEnrollFailedError extends Error {
1842
- botId;
1843
- constructor(botId, cause) {
1844
- super(`Failed to enroll bot ${botId}: ${cause instanceof Error ? cause.message : String(cause)}`);
1845
- this.botId = botId;
1846
- this.name = "BotEnrollFailedError";
1847
- }
1848
- }
1849
- function classifyErrorCode(err) {
1850
- if (err instanceof UnknownBotError)
1851
- return "bot_unknown";
1852
- if (err instanceof BotEnrollFailedError)
1853
- return "bot_enroll_failed";
1854
- if (err instanceof UnknownRuntimeError)
1855
- return "bot_runtime_missing";
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
- class UnknownRuntimeError extends Error {
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 defaultFormatUnreadNoticeText(notice) {
1870
- return `You have unread messages in channel ${notice.channel}.`;
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
- class AgentRouter {
1875
- opts;
1876
- running = new Set;
1877
- runtimes = new Map;
1878
- pendingResend = false;
1879
- scheduleResend;
1880
- log;
1881
- constructor(opts) {
1882
- this.opts = opts;
1883
- this.log = opts.logger ?? createLogger({ header: "@alook/daemon:router" });
1884
- this.scheduleResend = opts.scheduleReadyResend ?? queueMicrotask.bind(globalThis);
1885
- for (const r of opts.runtimeReport) {
1886
- this.runtimes.set(r.id, {
1887
- id: r.id,
1888
- version: r.version,
1889
- status: r.status ?? "healthy",
1890
- lastError: r.lastError,
1891
- lastErrorAt: r.lastErrorAt
1892
- });
1893
- }
1894
- }
1895
- async start() {
1896
- this.opts.channel.onCommand((cmd) => this.onCommand(cmd));
1897
- this.opts.channel.onResync?.(() => ({
1898
- ready: this.buildReady(),
1899
- sessions: this.opts.manager.liveSessionReports()
1900
- }));
1901
- await this.opts.channel.reportReady(this.buildReady());
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
- buildReady() {
1904
- return {
1905
- runtimeReport: [...this.runtimes.values()],
1906
- runningAgents: [...this.running],
1907
- hostname: this.opts.hostname,
1908
- platform: this.opts.platform,
1909
- arch: this.opts.arch,
1910
- osRelease: this.opts.osRelease,
1911
- daemonVersion: this.opts.daemonVersion
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
- healthyRuntimeIds() {
1915
- const out = [];
1916
- for (const r of this.runtimes.values()) {
1917
- if (r.status === "healthy")
1918
- out.push(r.id);
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 out;
1921
- }
1922
- isRuntimeHealthy(id) {
1923
- return this.runtimes.get(id)?.status === "healthy";
1942
+ return;
1924
1943
  }
1925
- markRuntimeUnhealthy(id, reason) {
1926
- const existing = this.runtimes.get(id);
1927
- if (!existing)
1928
- return;
1929
- const nowIso = new Date().toISOString();
1930
- if (existing.status === "unhealthy" && existing.lastError === reason)
1931
- return;
1932
- this.runtimes.set(id, {
1933
- ...existing,
1934
- status: "unhealthy",
1935
- lastError: reason,
1936
- lastErrorAt: nowIso
1937
- });
1938
- this.log.warn("runtime marked unhealthy", { runtimeId: id, reason });
1939
- this.scheduleReadyFrameResend();
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
- markRuntimeHealthy(id) {
1942
- const existing = this.runtimes.get(id);
1943
- if (!existing)
1944
- return;
1945
- if (existing.status === "healthy" && !existing.lastError && !existing.lastErrorAt)
1946
- return;
1947
- this.runtimes.set(id, {
1948
- id: existing.id,
1949
- version: existing.version,
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
- markLocallyStopped(agentId) {
1956
- if (!this.running.delete(agentId))
1957
- return;
1958
- this.log.info("agent removed from running set (local stop)", { agentId });
1959
- this.scheduleReadyFrameResend();
2035
+ return { name, target: truncateTargetToCodeUnits(target), suppressed: false };
2036
+ }
2037
+ function truncateThinking(text) {
2038
+ const chars = [...text].length;
2039
+ const buf = Buffer.from(text, "utf8");
2040
+ if (buf.byteLength <= THINKING_MAX_BYTES) {
2041
+ return { text, truncated: false, chars };
1960
2042
  }
1961
- scheduleReadyFrameResend() {
1962
- if (this.pendingResend)
1963
- return;
1964
- this.pendingResend = true;
1965
- this.scheduleResend(() => {
1966
- this.pendingResend = false;
1967
- try {
1968
- this.opts.channel.sendReady?.(this.buildReady());
1969
- } catch {}
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
- async onCommand(cmd) {
1973
- switch (cmd.type) {
1974
- case "agent:wake":
1975
- this.log.info("agent:wake received", {
1976
- agentId: cmd.agentId,
1977
- channel: cmd.unreadNotice.channel,
1978
- latestSeq: cmd.unreadNotice.latestSeq
1979
- });
1980
- try {
1981
- const beforeStatus = this.opts.manager.snapshot?.().agents?.[cmd.agentId]?.status ?? "unregistered";
1982
- const wasActive = this.opts.typingTracker?.hasAny(cmd.agentId) ?? false;
1983
- await this.opts.onBeforeAgent?.(cmd.agentId);
1984
- this.opts.manager.register(cmd.agentId, {
1985
- runtimeConfig: cmd.config,
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
- // src/manager/typingScopeTracker.ts
2118
- function createTypingScopeTracker() {
2119
- const scopes = new Map;
2120
- return {
2121
- add(agentId, dmConversationId) {
2122
- let set = scopes.get(agentId);
2123
- if (!set) {
2124
- set = new Set;
2125
- scopes.set(agentId, set);
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
- set.add(dmConversationId);
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
- if (isStale(lockPath, staleMs)) {
2156
- reclaim(lockPath);
2157
- if (tryMkdir(lockPath)) {
2158
- writeMeta(lockPath);
2159
- return true;
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
- return false;
2163
- }
2164
- function releaseLock(lockPath) {
2165
- try {
2166
- fs3.rmSync(lockPath, { recursive: true, force: true });
2167
- } catch {}
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
- function writeMeta(lockPath) {
2183
- try {
2184
- fs3.writeFileSync(`${lockPath}/${META}`, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() }));
2185
- } catch {}
2186
- }
2187
- function isStale(lockPath, staleMs) {
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
- const entries = [];
2230
- for (const line of content.trimEnd().split(`
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
- return entries;
2239
- }
2240
- function readRecentEntries(timelineDir, opts = {}) {
2241
- const now = opts.now ?? new Date;
2242
- const maxDays = opts.maxDays ?? 7;
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
- return entries;
2249
- }
2250
- function appendEntry(timelineDir, entry, now = new Date) {
2251
- const filename = filenameForDate(now);
2252
- const filePath = join2(timelineDir, filename);
2253
- const lockPath = lockPathFor(timelineDir, filename);
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
- function appendOrMergeEntry(timelineDir, entry, now = new Date) {
2267
- const filename = filenameForDate(now);
2268
- const filePath = join2(timelineDir, filename);
2269
- const lockPath = lockPathFor(timelineDir, filename);
2270
- if (!acquireLock(lockPath))
2271
- return false;
2272
- try {
2273
- let lines = [];
2274
- if (existsSync(filePath)) {
2275
- lines = readFileSync3(filePath, "utf-8").trimEnd().split(`
2276
- `).filter(Boolean);
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
- function updateLatestEntry(timelineDir, updater, opts = {}) {
2302
- const now = opts.now ?? new Date;
2303
- const maxDays = opts.maxDays ?? 7;
2304
- for (const filename of recentFilenames(maxDays, now)) {
2305
- const filePath = join2(timelineDir, filename);
2306
- if (!existsSync(filePath))
2307
- continue;
2308
- const lockPath = lockPathFor(timelineDir, filename);
2309
- if (!acquireLock(lockPath))
2310
- continue;
2311
- try {
2312
- let content;
2313
- try {
2314
- content = readFileSync3(filePath, "utf-8");
2315
- } catch {
2316
- continue;
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
- const lines = content.trimEnd().split(`
2319
- `).filter(Boolean);
2320
- if (lines.length === 0)
2321
- continue;
2322
- const entries = lines.map((l) => JSON.parse(l));
2323
- const latest = entries[entries.length - 1];
2324
- if (latest.system)
2325
- return false;
2326
- updater(latest);
2327
- const tmpPath = join2(timelineDir, `.${filename}.tmp`);
2328
- writeFileSync4(tmpPath, entries.map((e) => JSON.stringify(e)).join(`
2329
- `) + `
2330
- `);
2331
- renameSync2(tmpPath, filePath);
2332
- return true;
2333
- } catch {} finally {
2334
- releaseLock(lockPath);
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
- return false;
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
- return null;
2368
- }
2369
- // src/timeline/recorder.ts
2370
- import { mkdirSync as mkdirSync4 } from "fs";
2371
- function createTimelineRecorder(opts) {
2372
- const now = opts.now ?? (() => new Date);
2373
- const dirFor = (agentId) => opts.timelineDirFor(agentId);
2374
- const sessionByAgent = new Map;
2375
- return {
2376
- setSession(agentId, sessionId) {
2377
- sessionByAgent.set(agentId, sessionId);
2378
- },
2379
- appendEntryForAgent(agentId, messages) {
2380
- const dir = dirFor(agentId);
2381
- try {
2382
- mkdirSync4(dir, { recursive: true });
2383
- } catch {}
2384
- appendOrMergeEntry(dir, createTimelineEntry({
2385
- messages,
2386
- sessionId: sessionByAgent.get(agentId) ?? null,
2387
- provider: opts.providerFor?.(agentId) ?? null
2388
- }), now());
2389
- },
2390
- appendResponseToLatest(agentId, text) {
2391
- const dir = dirFor(agentId);
2392
- const updated = updateLatestEntry(dir, (e) => e.agent_responses.push(text), { now: now() });
2393
- if (updated)
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
- try {
2396
- mkdirSync4(dir, { recursive: true });
2397
- } catch {}
2398
- const entry = createTimelineEntry({
2399
- messages: [],
2400
- sessionId: sessionByAgent.get(agentId) ?? null,
2401
- provider: opts.providerFor?.(agentId) ?? null
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
- entry.agent_responses.push(text);
2404
- appendEntry(dir, entry, now());
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
- if (owner) {
2445
- 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.");
2446
- }
2447
- 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.");
2448
- if (config.description) {
2449
- 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).");
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
- function cliCommandsSection() {
2455
- return [
2456
- "## CLI commands",
2457
- "",
2458
- `\`${CLI}\` is your CLI. Run \`${CLI} <command> -h\` for full usage and flags.`,
2459
- "",
2460
- "### Messaging",
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
- // src/drivers/cliLink.ts
2705
- import * as fs4 from "fs";
2706
- import * as path3 from "path";
2707
- function writeCliLink(stateDir, cliName, hostCliPath, platform = process.platform) {
2708
- const binDir = path3.join(stateDir, "bin");
2709
- fs4.mkdirSync(binDir, { recursive: true });
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
- try {
2728
- fs4.symlinkSync(hostCliPath, linkPath);
2729
- } catch (err) {
2730
- if (err.code !== "EEXIST")
2731
- throw err;
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
- // src/drivers/spawnEnv.ts
2737
- function mergeEnvLayers(base, layers) {
2738
- const env = { ...base };
2739
- const provenance = {};
2740
- const ordered = [
2741
- ...layers.filter((l) => !l.sensitive).sort((a, b) => a.precedence - b.precedence),
2742
- ...layers.filter((l) => l.sensitive).sort((a, b) => a.precedence - b.precedence)
2743
- ];
2744
- for (const layer of ordered) {
2745
- for (const [k, v] of Object.entries(layer.vars)) {
2746
- if (v === undefined)
2747
- continue;
2748
- env[k] = v;
2749
- provenance[k] = layer.name;
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
- function runtimeContextEnv(prefix, rc) {
2767
- if (!rc)
2768
- return {};
2769
- const E = prefix;
2673
+ // src/manager/typingScopeTracker.ts
2674
+ function createTypingScopeTracker() {
2675
+ const scopes = new Map;
2770
2676
  return {
2771
- [`${E}_CURRENT_AGENT_ID`]: rc.agentId,
2772
- [`${E}_CURRENT_SERVER_ID`]: rc.serverId,
2773
- [`${E}_CURRENT_COMPUTER_ID`]: rc.computerId,
2774
- [`${E}_CURRENT_COMPUTER_NAME`]: rc.computerName,
2775
- [`${E}_CURRENT_COMPUTER_HOSTNAME`]: rc.hostname,
2776
- [`${E}_CURRENT_COMPUTER_OS`]: rc.os,
2777
- [`${E}_CURRENT_DAEMON_VERSION`]: rc.daemonVersion,
2778
- [`${E}_CURRENT_WORKSPACE_PATH`]: rc.workspacePath
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/drivers/agentFile.ts
2783
- import {
2784
- writeFileSync as writeFileSync6,
2785
- readFileSync as readFileSync4,
2786
- lstatSync,
2787
- symlinkSync as symlinkSync2,
2788
- unlinkSync as unlinkSync2,
2789
- existsSync as existsSync2,
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
- function ensureSymlinks(workDir) {
2811
- const canonicalPath = join4(workDir, CANONICAL_FILE);
2812
- if (!existsSync2(canonicalPath))
2813
- return;
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 writeAgentFile(workDir, systemPromptContent) {
2849
- const filePath = join4(workDir, CANONICAL_FILE);
2850
- const changed = hasContentChanged(filePath, systemPromptContent);
2851
- if (changed) {
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
- // src/drivers/cliTransport.ts
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
- async function prepareCliTransport(ctx, extraEnv = {}, cli = DEFAULT_CLI_CONFIG, platform = process.platform) {
2868
- const E = cli.envPrefix;
2869
- const stateHome = resolveStateHome(E);
2870
- const stateDir = path4.join(ctx.workingDirectory, cli.stateDirName);
2871
- await fs5.promises.mkdir(stateDir, { recursive: true });
2872
- if (ctx.standingPrompt)
2873
- writeAgentFile(ctx.workingDirectory, ctx.standingPrompt);
2874
- const binDir = writeCliLink(stateDir, cli.cliName, cli.hostCliPath, platform);
2875
- if (!ctx.credentialProxy) {
2876
- throw new Error("prepareCliTransport: ctx.credentialProxy is required — start a credential proxy " + "(see src/credentials) and pass { broker, proxyUrl }. There is no plaintext mode.");
2877
- }
2878
- const capabilities = ctx.credentialProxy.capabilities;
2879
- if (!Array.isArray(capabilities)) {
2880
- throw new Error("prepareCliTransport: credentialProxy.capabilities is required " + "(empty array is allowed for zero-capability launches; undefined is a wiring bug)");
2881
- }
2882
- for (const c of capabilities) {
2883
- if (typeof c !== "string" || c.includes(",")) {
2884
- 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"])`);
2885
- }
2886
- }
2887
- ctx.credentialProxy.broker.revokeAgent(ctx.agentId);
2888
- const reg = ctx.credentialProxy.broker.mint(ctx.agentId, ctx.launchId ?? "default", capabilities, ctx.credentialProxy.runnerKey);
2889
- const tokenFile = reg.voucherFile;
2890
- const resolved = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
2891
- const pathValue = [binDir, process.env.PATH ?? ""].filter(Boolean).join(path4.delimiter);
2892
- const layers = [
2893
- { name: "hostStatic", precedence: 10, vars: cli.extraEnv ?? {} },
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 buildCliTransportSystemPrompt(config, opts) {
2930
- return buildCliSystemPrompt(config, opts);
2756
+ function reclaim(lockPath) {
2757
+ try {
2758
+ fs5.rmSync(lockPath, { recursive: true, force: true });
2759
+ } catch {}
2931
2760
  }
2932
2761
 
2933
- // src/drivers/claudeProviderIsolation.ts
2934
- import * as fs6 from "fs";
2935
- import * as path5 from "path";
2936
- function buildClaudeProviderIsolationEnv(ctx) {
2937
- const hasCustomProvider = Boolean(process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_API_KEY);
2938
- if (!hasCustomProvider)
2939
- return {};
2940
- const root = path5.join(ctx.workingDirectory, ".alook", "claude-provider");
2941
- const home = path5.join(root, "home");
2942
- const configDir = path5.join(home, ".claude");
2943
- fs6.mkdirSync(configDir, { recursive: true });
2944
- const hostClaude = path5.join(process.env.HOME || ".", ".claude");
2945
- for (const sub of ["skills", "commands"]) {
2946
- const target = path5.join(hostClaude, sub);
2947
- const link = path5.join(configDir, sub);
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
- if (fs6.existsSync(target) && !fs6.existsSync(link))
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
- // src/drivers/probe.ts
2962
- import { execFileSync } from "child_process";
2963
- import * as fs7 from "fs";
2964
- import * as path6 from "path";
2965
- function resolveCommandOnPath(command, deps = {}) {
2966
- if (deps.which)
2967
- return deps.which(command);
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
- if (process.platform === "win32") {
2970
- const out2 = execFileSync("where", [command], { encoding: "utf8", timeout: 5000 });
2971
- const first = out2.split(/\r?\n/).find((line) => line.trim().length > 0);
2972
- return first?.trim() || null;
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
- const out = execFileSync("which", [command], { encoding: "utf8", timeout: 5000 });
2975
- return out.trim() || null;
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 null;
2852
+ return false;
2853
+ } finally {
2854
+ releaseLock(lockPath);
2978
2855
  }
2979
2856
  }
2980
- function firstExistingPath(candidates) {
2981
- for (const c of candidates) {
2982
- if (c && fs7.existsSync(c))
2983
- return c;
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 null;
2893
+ return false;
2986
2894
  }
2987
- function looksLikeVersion(line) {
2988
- return /\d+\.\d+/.test(line);
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 needsWindowsShimShell(command, platform) {
2991
- return platform === "win32" && /\.(cmd|bat)$/i.test(command);
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 probeCommandVersion(command, args = [], deps = {}, platform = process.platform) {
2994
- try {
2995
- const shell = needsWindowsShimShell(command, platform);
2996
- const out = execFileSync(command, [...args, "--version"], {
2997
- encoding: "utf8",
2998
- timeout: 5000,
2999
- shell,
3000
- input: "",
3001
- env: { ...process.env, CI: "1" }
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
- function resolveHomePath(relativePath, deps = {}) {
3016
- return path6.join(deps.homeDir || process.env.HOME || ".", relativePath);
3017
- }
3018
- function resolveSpawnSpec(command, args, deps = {}, platform = process.platform) {
3019
- const resolved = resolveCommandOnPath(command, deps) ?? command;
3020
- return { command: resolved, args, shell: needsWindowsShimShell(resolved, platform) };
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
- function resolveClaudeCommand(deps = {}) {
3023
- const onPath = resolveCommandOnPath("claude", deps);
3024
- if (onPath)
3025
- return onPath;
3026
- if (process.platform === "darwin") {
3027
- return firstExistingPath([
3028
- resolveHomePath("Applications/Claude Code URL Handler.app/Contents/MacOS/claude", deps),
3029
- "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude"
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 null;
3033
- }
3034
- function probeClaude(deps = {}) {
3035
- const command = resolveClaudeCommand(deps);
3036
- if (!command)
3037
- return { status: "unhealthy", lastError: "not_on_path" };
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
- function resolveClaudeLaunchCommand(config) {
3083
- const override = resolveLaunchFieldsOrDefault(config.runtimeConfig).command?.trim();
3084
- return override || resolveClaudeCommand() || "claude";
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 buildClaudeSpawnSpec(claudeCommand, platform = process.platform) {
3087
- const command = claudeCommand ?? "claude";
3088
- const shell = platform === "win32" && (!command || /\.(cmd|bat)$/i.test(command));
3089
- return { command, shell };
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
- let event;
3102
- try {
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 claudeCommand = resolveClaudeLaunchCommand(ctx.config);
3232
- const spawnSpec = buildClaudeSpawnSpec(claudeCommand);
3233
- const proc = spawnAgentProcess(spawnSpec.command, args, {
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: spawnSpec.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, { lifecycleKind: this.lifecycle.kind });
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
- let msg;
3319
- try {
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, { NO_COLOR: "1" });
3550
+ const { spawnEnv } = await prepareCliTransport(ctx);
3465
3551
  this.codexHomeRoot = resolveCodexHomeRootFromEnv(spawnEnv, { cwd: ctx.workingDirectory });
3466
- const spec = resolveSpawnSpec("codex", ["app-server", "--listen", "stdio://"]);
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(JSON.stringify({
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(JSON.stringify({
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
- if (opts?.mode === "idle") {
3522
- return JSON.stringify({
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, { lifecycleKind: this.lifecycle.kind });
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: "suggestion_only",
3566
- toLaunchSpec: (modelId) => modelId && modelId !== "default" ? { args: ["--model", modelId] } : { args: [] }
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, { NO_COLOR: "1" });
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 spec = resolveSpawnSpec("gemini", buildGeminiArgs(ctx.config));
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.stdin?.end(ctx.prompt);
3656
+ writeToStdinAndDetach(proc, ctx.prompt);
3587
3657
  return { process: proc };
3588
3658
  }
3589
3659
  parseLine(line) {
3590
- let event;
3591
- try {
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, { lifecycleKind: this.lifecycle.kind });
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, { NO_COLOR: "1" });
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
- let event;
3666
- try {
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, { lifecycleKind: this.lifecycle.kind });
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, { NO_COLOR: "1" });
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
- let event;
3749
- try {
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, { lifecycleKind: this.lifecycle.kind });
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, { NO_COLOR: "1" });
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
- let event;
3847
- try {
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, { lifecycleKind: this.lifecycle.kind });
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 ?? randomUUID();
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 spec = resolveSpawnSpec("agy", buildAntigravityArgs(ctx));
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.stdin?.end(ctx.prompt);
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, { lifecycleKind: this.lifecycle.kind });
4050
+ return buildCliTransportSystemPrompt(config);
3965
4051
  }
3966
4052
  }
3967
4053
 
3968
4054
  // src/drivers/kimi.ts
3969
- import { randomUUID as randomUUID2 } from "crypto";
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 = randomUUID2();
4086
+ promptRequestId = randomUUID3();
3993
4087
  probe() {
3994
4088
  return probeCliRuntime("kimi");
3995
4089
  }
3996
4090
  async spawn(ctx) {
3997
- this.sessionId = ctx.config.sessionId || randomUUID2();
4091
+ this.sessionId = ctx.config.sessionId || randomUUID3();
3998
4092
  const isResume = Boolean(ctx.config.sessionId);
3999
- const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
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(JSON.stringify({
4011
- jsonrpc: "2.0",
4012
- id: randomUUID2(),
4013
- method: "initialize",
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(JSON.stringify({
4022
- jsonrpc: "2.0",
4023
- id: this.promptRequestId,
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
- let msg;
4034
- try {
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 JSON.stringify({ jsonrpc: "2.0", id: randomUUID2(), method, params: { user_input: text } });
4171
+ return jsonRpcRequest(method, { user_input: text });
4091
4172
  }
4092
4173
  buildSystemPrompt(config) {
4093
- return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
4174
+ return buildCliTransportSystemPrompt(config);
4094
4175
  }
4095
4176
  }
4096
4177
 
4097
4178
  // src/drivers/pi.ts
4098
- import { createRequire as createRequire2 } from "module";
4099
- import { mkdirSync as mkdirSync7, existsSync as existsSync5, readFileSync as readFileSync5, realpathSync } from "fs";
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 = createRequire2(import.meta.url);
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, { lifecycleKind: this.lifecycle.kind });
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
- return import(pathToFileURL(entryPath).href);
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 sessionManager = opts.sessionId ? sdk.SessionManager.continueRecent(cwd) : sdk.SessionManager.create(cwd);
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 || `${homedir2()}/.alook`) + "/daemon";
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(homedir3(), ".alook");
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, 12);
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, 20) + "…",
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() + 5000;
5064
+ const deadline = Date.now() + STOP_GRACE_MS;
4940
5065
  while (Date.now() < deadline && isProcessAlive(data.pid)) {
4941
- const start = Date.now();
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 5s — sending SIGKILL`);
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 = credentialFilesDir(baseDir);
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
- const readyTimer = setInterval(() => {
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
  });