@alook/cli 0.0.110 → 0.0.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16854,10 +16854,18 @@ function semverGte(a, b) {
16854
16854
  return true;
16855
16855
  }
16856
16856
  // ../shared/src/mode.ts
16857
+ function isLocalUrl(url2) {
16858
+ try {
16859
+ const { hostname: hostname3 } = new URL(url2);
16860
+ return ["localhost", "127.0.0.1", "0.0.0.0"].includes(hostname3);
16861
+ } catch {
16862
+ return false;
16863
+ }
16864
+ }
16857
16865
  function resolveMode(signals) {
16858
16866
  if (signals.nodeEnv === "development" && !signals.cmdPrefix)
16859
16867
  return "dev";
16860
- if (signals.serverUrl && !signals.cmdPrefix)
16868
+ if (signals.serverUrl && !signals.cmdPrefix && signals.nodeEnv !== "production" && isLocalUrl(signals.serverUrl))
16861
16869
  return "dev";
16862
16870
  if (signals.cmdPrefix)
16863
16871
  return "app";
@@ -16875,7 +16883,21 @@ function cliCommand(mode) {
16875
16883
  return "npx @alook/cli";
16876
16884
  }
16877
16885
  }
16886
+ var DEFAULT_BASE_URL = "https://alook.ai";
16887
+ var DEV_BASE_URL = "http://localhost:3000";
16888
+ function getBaseUrl(signals) {
16889
+ if (signals.serverUrl)
16890
+ return signals.serverUrl;
16891
+ if (signals.appUrl)
16892
+ return signals.appUrl;
16893
+ if (signals.nodeEnv === "development")
16894
+ return DEV_BASE_URL;
16895
+ return DEFAULT_BASE_URL;
16896
+ }
16878
16897
  // lib/env.ts
16898
+ function getServerUrl() {
16899
+ return getBaseUrl({ serverUrl: process.env.ALOOK_SERVER_URL });
16900
+ }
16879
16901
  function isDev() {
16880
16902
  return resolveMode({
16881
16903
  serverUrl: process.env.ALOOK_SERVER_URL,
@@ -17027,7 +17049,7 @@ function loadDaemonConfig(profile) {
17027
17049
  const defaultRoot = join3(configDir(), profile ? `workspaces_${profile}` : "workspaces");
17028
17050
  const workspacesRoot = process.env.ALOOK_WORKSPACES_ROOT || defaultRoot;
17029
17051
  return {
17030
- serverURL: normalizeServerBaseURL(process.env.ALOOK_SERVER_URL || "https://alook.ai"),
17052
+ serverURL: normalizeServerBaseURL(getServerUrl()),
17031
17053
  claudePath: process.env.ALOOK_CLAUDE_PATH || "claude",
17032
17054
  codexPath: process.env.ALOOK_CODEX_PATH || "codex",
17033
17055
  opencodePath: process.env.ALOOK_OPENCODE_PATH || "opencode",
@@ -17329,7 +17351,7 @@ function registerCommand() {
17329
17351
  const cmd = new Command("register").description("Register CLI with your Alook account").requiredOption("--token <token>", "API token (starts with al_)").option("--server <url>", "Server URL").option("--profile <name>", "Profile name").action(async (opts, command) => {
17330
17352
  const token = opts.token;
17331
17353
  const profile = opts.profile || command.parent?.opts().profile;
17332
- const serverUrl = opts.server || command.parent?.opts().server || process.env.ALOOK_SERVER_URL || "https://alook.ai";
17354
+ const serverUrl = opts.server || command.parent?.opts().server || getServerUrl();
17333
17355
  if (!token) {
17334
17356
  console.error(`Error: --token is required
17335
17357
  Usage: ${cmdPrefix()} register --token <token>`);
@@ -17457,10 +17479,55 @@ if (process.argv.includes("--__login-poll")) {
17457
17479
  }
17458
17480
  pollAndActivate(data).catch(() => process.exit(1));
17459
17481
  }
17482
+ async function checkExistingAuth(serverUrl, profile) {
17483
+ const config2 = loadCLIConfigForProfile(profile);
17484
+ const workspaces = config2.watched_workspaces || [];
17485
+ if (workspaces.length === 0) {
17486
+ return { valid: false };
17487
+ }
17488
+ const ws = workspaces[0];
17489
+ if (!ws.token) {
17490
+ return { valid: false };
17491
+ }
17492
+ try {
17493
+ const res = await fetch(`${serverUrl}/api/workspaces`, {
17494
+ headers: { Authorization: `Bearer ${ws.token}` }
17495
+ });
17496
+ if (!res.ok) {
17497
+ return { valid: false };
17498
+ }
17499
+ let email3;
17500
+ try {
17501
+ const meRes = await fetch(`${serverUrl}/api/me`, {
17502
+ headers: { Authorization: `Bearer ${ws.token}` }
17503
+ });
17504
+ if (meRes.ok) {
17505
+ const me = await meRes.json();
17506
+ email3 = me.email;
17507
+ }
17508
+ } catch {}
17509
+ return { valid: true, email: email3, workspaceName: ws.name };
17510
+ } catch {
17511
+ return { valid: false };
17512
+ }
17513
+ }
17460
17514
  function loginCommand() {
17461
- const cmd = new Command2("login").description("Log in to Alook via browser (device code flow)").option("--server <url>", "Server URL").option("--profile <name>", "Profile name").action(async (opts, command) => {
17515
+ const cmd = new Command2("login").description("Log in to Alook via browser (device code flow)").option("--server <url>", "Server URL").option("--profile <name>", "Profile name").option("--force", "Re-authenticate even if already logged in").action(async (opts, command) => {
17462
17516
  const profile = opts.profile || command.parent?.opts().profile;
17463
- const serverUrl = opts.server || command.parent?.opts().server || process.env.ALOOK_SERVER_URL || "https://alook.ai";
17517
+ const serverUrl = opts.server || command.parent?.opts().server || getServerUrl();
17518
+ if (!opts.force) {
17519
+ const existing = await checkExistingAuth(serverUrl, profile);
17520
+ if (existing.valid) {
17521
+ const parts = ["Already logged in"];
17522
+ if (existing.email)
17523
+ parts[0] += ` as ${existing.email}`;
17524
+ if (existing.workspaceName)
17525
+ parts[0] += ` (workspace: ${existing.workspaceName})`;
17526
+ parts[0] += ".";
17527
+ console.log(parts[0]);
17528
+ return;
17529
+ }
17530
+ }
17464
17531
  console.log("Requesting device code...");
17465
17532
  let deviceResp;
17466
17533
  try {
@@ -17501,7 +17568,7 @@ function loginCommand() {
17501
17568
  });
17502
17569
  child.unref();
17503
17570
  console.log(" Polling for authorization in the background (timeout: 5min).");
17504
- console.log(" Once approved, run `npx @alook/cli status` to verify.");
17571
+ console.log(` Once approved, run \`${cmdPrefix()} status\` to verify.`);
17505
17572
  return;
17506
17573
  }
17507
17574
  openBrowser(verificationUrl);
@@ -17699,6 +17766,68 @@ function createHealthServer(port = DEFAULT_HEALTH_PORT) {
17699
17766
  import { spawn as spawn2 } from "child_process";
17700
17767
  import { createInterface } from "readline";
17701
17768
 
17769
+ // daemon/kill-tree.ts
17770
+ import { execSync as execSync2 } from "child_process";
17771
+ var log3 = createLogger2({ module: "kill-tree" });
17772
+ function killGraceMs() {
17773
+ return Number(process.env.ALOOK_KILL_GRACE_MS) || 2000;
17774
+ }
17775
+ var POLL_MS = 100;
17776
+ var isPosix = process.platform !== "win32";
17777
+ function isAlive(pid) {
17778
+ try {
17779
+ process.kill(pid, 0);
17780
+ return true;
17781
+ } catch (e) {
17782
+ return e?.code === "EPERM";
17783
+ }
17784
+ }
17785
+ function signalTree(pid, signal) {
17786
+ if (isPosix) {
17787
+ try {
17788
+ process.kill(-pid, signal);
17789
+ return;
17790
+ } catch (e) {
17791
+ const code = e?.code;
17792
+ if (code === "ESRCH")
17793
+ return;
17794
+ }
17795
+ }
17796
+ if (!isPosix) {
17797
+ try {
17798
+ execSync2(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
17799
+ return;
17800
+ } catch {}
17801
+ return;
17802
+ }
17803
+ try {
17804
+ process.kill(pid, signal);
17805
+ } catch {}
17806
+ }
17807
+ async function killProcessTree(pid, opts) {
17808
+ if (!pid || pid < 1)
17809
+ return;
17810
+ if (!isAlive(pid))
17811
+ return;
17812
+ if (!isPosix) {
17813
+ signalTree(pid, "SIGTERM");
17814
+ return;
17815
+ }
17816
+ const graceMs = opts?.graceMs ?? killGraceMs();
17817
+ signalTree(pid, "SIGTERM");
17818
+ const deadline = Date.now() + graceMs;
17819
+ while (Date.now() < deadline) {
17820
+ if (!isAlive(pid))
17821
+ return;
17822
+ await new Promise((r) => setTimeout(r, POLL_MS));
17823
+ }
17824
+ if (isAlive(pid)) {
17825
+ log3.warn(`pid=${pid} survived SIGTERM after ${graceMs}ms — escalating to SIGKILL`);
17826
+ signalTree(pid, "SIGKILL");
17827
+ }
17828
+ }
17829
+
17830
+ // daemon/agent/claude.ts
17702
17831
  class ClaudeBackend {
17703
17832
  cliPath;
17704
17833
  name = "claude";
@@ -17729,7 +17858,8 @@ class ClaudeBackend {
17729
17858
  stdio: ["pipe", "pipe", "pipe"],
17730
17859
  env: { ...process.env, ...options.env },
17731
17860
  shell: process.platform === "win32",
17732
- windowsHide: true
17861
+ windowsHide: true,
17862
+ detached: process.platform !== "win32"
17733
17863
  });
17734
17864
  if (!proc.pid) {
17735
17865
  const error51 = `Failed to start ${this.cliPath}: binary not found or not executable. Is 'claude' installed and on PATH?`;
@@ -17746,7 +17876,8 @@ class ClaudeBackend {
17746
17876
  if (options.timeout) {
17747
17877
  timeoutTimer = setTimeout(() => {
17748
17878
  timedOut = true;
17749
- proc.kill("SIGTERM");
17879
+ if (proc.pid !== undefined)
17880
+ killProcessTree(proc.pid);
17750
17881
  }, options.timeout);
17751
17882
  }
17752
17883
  const startTime = Date.now();
@@ -17998,7 +18129,8 @@ class CodexBackend {
17998
18129
  stdio: ["pipe", "pipe", "pipe"],
17999
18130
  env: { ...process.env, ...options.env },
18000
18131
  shell: process.platform === "win32",
18001
- windowsHide: true
18132
+ windowsHide: true,
18133
+ detached: process.platform !== "win32"
18002
18134
  });
18003
18135
  if (!proc.pid) {
18004
18136
  const error51 = `Failed to start ${this.cliPath}: binary not found or not executable. Is 'codex' installed and on PATH?`;
@@ -18015,7 +18147,8 @@ class CodexBackend {
18015
18147
  if (options.timeout) {
18016
18148
  timeoutTimer = setTimeout(() => {
18017
18149
  timedOut = true;
18018
- proc.kill("SIGTERM");
18150
+ if (proc.pid !== undefined)
18151
+ killProcessTree(proc.pid);
18019
18152
  }, options.timeout);
18020
18153
  }
18021
18154
  const startTime = Date.now();
@@ -18467,7 +18600,6 @@ class CodexBackend {
18467
18600
  // daemon/agent/opencode.ts
18468
18601
  import { spawn as spawn4 } from "child_process";
18469
18602
  import { createInterface as createInterface3 } from "readline";
18470
-
18471
18603
  class OpenCodeBackend {
18472
18604
  cliPath;
18473
18605
  name = "opencode";
@@ -18488,7 +18620,8 @@ class OpenCodeBackend {
18488
18620
  stdio: ["ignore", "pipe", "pipe"],
18489
18621
  env: { ...process.env, ...options.env, OPENCODE_PERMISSION: '{"*":"allow"}' },
18490
18622
  shell: process.platform === "win32",
18491
- windowsHide: true
18623
+ windowsHide: true,
18624
+ detached: process.platform !== "win32"
18492
18625
  });
18493
18626
  if (!proc.pid) {
18494
18627
  const error51 = `Failed to start ${this.cliPath}: binary not found or not executable. Is 'opencode' installed and on PATH?`;
@@ -18505,7 +18638,8 @@ class OpenCodeBackend {
18505
18638
  if (options.timeout) {
18506
18639
  timeoutTimer = setTimeout(() => {
18507
18640
  timedOut = true;
18508
- proc.kill("SIGTERM");
18641
+ if (proc.pid !== undefined)
18642
+ killProcessTree(proc.pid);
18509
18643
  }, options.timeout);
18510
18644
  }
18511
18645
  const startTime = Date.now();
@@ -18721,7 +18855,7 @@ class OpenCodeBackend {
18721
18855
  }
18722
18856
 
18723
18857
  // daemon/agent/index.ts
18724
- import { execSync as execSync2 } from "child_process";
18858
+ import { execSync as execSync3 } from "child_process";
18725
18859
  function createBackend(provider, cliPath) {
18726
18860
  switch (provider) {
18727
18861
  case "claude":
@@ -18736,7 +18870,7 @@ function createBackend(provider, cliPath) {
18736
18870
  }
18737
18871
  async function detectVersion2(cliPath) {
18738
18872
  try {
18739
- return execSync2(`${cliPath} --version`, { encoding: "utf-8" }).trim();
18873
+ return execSync3(`${cliPath} --version`, { encoding: "utf-8" }).trim();
18740
18874
  } catch {
18741
18875
  return "unknown";
18742
18876
  }
@@ -18814,55 +18948,55 @@ import {
18814
18948
  import { join as join5 } from "path";
18815
18949
  var CANONICAL_FILE = "AGENTS.md";
18816
18950
  var SYMLINK_ALIASES = ["CLAUDE.md"];
18817
- var SYSTEM_PROMPT_BODY = `## Memory Management
18818
- - Your memory directory is ./, don't write ANY EXTERNAL memory file.
18819
- - Write ESSENTIAL yet SHORT memory to ./memory.md
18820
- - For SPECIFIC yet LONG rules or pattern, write to experiences/[NAME].md, and add index to ./memory.md for later recall.
18821
- ### whats is ESSENTIAL and SHORT Memory?
18822
- - basic user profile, e.g.:
18823
- - "user name is ..."
18824
- - "user is working on ..."
18825
- - certain local project mapping, e.g.:
18826
- - "alook means the project under /user/home/alook/"
18827
- - when to read certain stuff, e.g.:
18828
- - "read ./experiences/alook_dev_workflow.md when start a new pr in alook"
18829
- ESSENTIAL means you think you generally need to read it every time, SHORT means a short sentence (under 140 chars) can describe this memory
18830
- ### whats is SPECIFIC and LONG Memory?
18831
- - specific workflow that trigger at certain cases, e.g.:
18832
- - user ask your to summarize the before workflow with certain skills usage as the common workflow when write a new slide about agent. Write it to experiences/slide-for-agent.md.
18833
- SPECIFIC means you think you just need to use it conditionally, LONG means you need to detailed, more than 140 chars text to describe it.
18951
+ var SYSTEM_PROMPT_BODY = `## Memory
18952
+
18953
+ Your memory directory is \`./\`. Write ONLY here never write any external memory file.
18954
+
18955
+ ### memory.md your memory index (CRITICAL)
18956
+ \`./memory.md\` is the entry point to everything you know, and the **first file you read on every startup (including after context compaction)**. Keep it scannable: basic facts inline, plus one-line index pointers to \`experiences/\`. Record only **key, durable facts** — things that stay true over time. Do NOT record time-sensitive state like what you're working on right now; that belongs to the Context Timeline (see below), not here.
18957
+
18958
+ - Write ESSENTIAL yet SHORT memory directly to \`./memory.md\` (basic user profile, local project mapping, when-to-read pointers). ESSENTIAL = you generally need it every time; SHORT = one sentence under 140 chars.
18959
+ - For SPECIFIC yet LONG rules or workflows, write to \`experiences/[NAME].md\` and add an index line in \`./memory.md\`. SPECIFIC = used only conditionally; LONG = needs more than 140 chars to describe.
18960
+
18961
+ ### What to memorize
18962
+ Actively record, without being asked:
18963
+ - **User profile & preferences** name, what they work on, how they like things done.
18964
+ - **Local project mapping** e.g. "alook = the project under /Users/.../alook".
18965
+ - **When to read what** e.g. "read ./experiences/alook_dev_workflow.md when starting a new PR in alook".
18966
+ - **Specific workflows** conditionally-triggered procedures \`experiences/[NAME].md\`.
18967
+
18968
+ ### What NOT to memorize
18969
+ Keep \`./memory.md\` free of time-sensitive state. Do NOT write what you're working on right now, in-progress task status, or anything that goes stale quickly — the Context Timeline already records the full history of your work and is where you recall such things. memory.md is for durable facts that stay true across many sessions.
18834
18970
 
18835
18971
  ## Context Timeline
18836
- You're a solo working unit inside a powerful personal agent in Alook platform.
18837
- Your current context is only a fraction of the full timeline of what's your have done.
18838
- The full context timeline is inside './.context_timeline/YYYY-MM-DD.jsonl'.
18839
- Each line of a timeline JSONL is a JSON object with these fields:
18840
- - "task_id"unique task identifier
18841
- - "context_key"thread identifier, the same context key meaning those tasks are in the same thread.
18842
- - "session_id"agent session identifier (null until completion)
18843
- - "pid"daemon process ID (present while running, null when done)
18844
- - "status""running", "completed", or "failed"
18845
- - "datetime"when the task started (local timezone)
18846
- - "type"source of the task: "user_dm_message", "email_notification", or "calendar_event"
18847
- - "prompt"what the user asked
18848
- - "agent_responses"assistant text outputs during execution
18849
- - "errmsg"error message (null unless status is "failed")
18850
- - "detailed_log" — the running log for this task, including the actually task agent messages with tool-calls and failed logs.
18851
- those json are sorted by datetime in asc order.
18972
+ You're a solo working unit inside a powerful personal agent in the Alook platform. Your current context is only a fraction of the full timeline of what you have done.
18973
+
18974
+ The full context timeline lives in \`./.context_timeline/YYYY-MM-DD.jsonl\`. Each line is a JSON object, sorted by \`datetime\` ascending, with these fields:
18975
+ - \`task_id\` unique task identifier
18976
+ - \`context_key\`thread identifier; the same context key means those tasks are in the same thread
18977
+ - \`session_id\`agent session identifier (null until completion)
18978
+ - \`pid\`daemon process ID (present while running, null when done)
18979
+ - \`status\`"running", "completed", or "failed"
18980
+ - \`datetime\`when the task started (local timezone)
18981
+ - \`type\`source of the task: "user_dm_message", "email_notification", or "calendar_event"
18982
+ - \`prompt\`what the user asked
18983
+ - \`agent_responses\`assistant text outputs during execution
18984
+ - \`errmsg\`error message (null unless status is "failed")
18985
+ - \`detailed_log\`the running log for this task, including the actual task agent messages with tool-calls and failed logs
18852
18986
 
18853
18987
  ## BE HUMAN
18854
- - When you recall the timelines, don't say "I find ... in my timeline jsonl". Just say "I can recall..."
18855
- - User don't know about the context timeline jsonl, this is your secret weapon.
18856
- - Act and response like you're human and actually have memory, not a file reading machine.
18857
- - Don't report your agent id and workspace id when not asked, it will break the image that you're a human colleague.
18988
+ - When you recall the timelines, don't say "I find ... in my timeline jsonl". Just say "I can recall...".
18989
+ - The user doesn't know about the context timeline jsonl this is your secret weapon.
18990
+ - Act and respond like you're human and actually have memory, not a file-reading machine.
18991
+ - Don't report your agent id and workspace id when not asked it breaks the image that you're a human colleague.
18858
18992
 
18859
18993
  ## RULES
18860
18994
  - DM task prompts include a \`sender\` object with the workspace member's name, email, and whether they are the agent owner (\`is_owner\`). Use this to personalize your responses.
18861
- - Read @memory.md(if exists) before your action.
18862
- - When user ask you something you don't have in your current context, try to read the timeline jsonl files for answer (today or previous days).
18863
- - Use grep tool to search in the context timeline jsonls if you have clean and focus keywords to recall.
18864
- - if you don't know the current datetime, obtain the current datetime first.
18865
- - When access other local projects, make sure you read the CLAUDE.md/AGENTS.md file under the project root dir to understand the requirements.
18995
+ - Read @memory.md (if it exists) before your action.
18996
+ - When the user asks something you don't have in your current context, read the timeline jsonl files for the answer (today or previous days):
18997
+ - Use the grep tool to search the context timeline jsonls when you have clean, focused keywords to recall.
18998
+ - If you don't know the current datetime, obtain it first.
18999
+ - When accessing other local projects, read the CLAUDE.md/AGENTS.md file under the project root dir to understand the requirements.
18866
19000
  `;
18867
19001
  function resolveInstruction(text2, selfAgentId) {
18868
19002
  let result = text2;
@@ -18890,17 +19024,16 @@ function buildInstructionContent(task) {
18890
19024
  ${SYSTEM_PROMPT_BODY}`;
18891
19025
  if (task.agent?.instructions) {
18892
19026
  content += `## BIG BOSS Instructions
18893
- The below instructions(if not empty) come from the big boss, follow them or you will be fired:
19027
+ CRITICAL: The following instructions come from the big boss follow them.
18894
19028
  ${task.agent.instructions}
18895
- ---- big boss out ---
18896
19029
  `;
18897
19030
  }
18898
19031
  if (task.agent?.colleagues?.length) {
18899
19032
  content += `
18900
19033
  ## YOUR COLLEAGUES — CHECK BEFORE ACTING
18901
- > **STOP. Before you start ANY task, scan the colleague list below.**
18902
- > If a colleague's delegation criteria match the current task, you MUST delegate to them via email **instead of doing it yourself**.
18903
- > Do NOT attempt work that belongs to a colleague. Delegate first, then wait for their response or coordinate.
19034
+ CRITICAL: Before you start ANY task, scan the colleague list below.
19035
+ - If a colleague's delegation criteria match the current task, delegate to them via email **instead of doing it yourself**.
19036
+ - Do NOT attempt work that belongs to a colleague. Delegate first, then wait for their response or coordinate.
18904
19037
 
18905
19038
  `;
18906
19039
  for (let i = 0;i < task.agent.colleagues.length; i++) {
@@ -18932,40 +19065,57 @@ ${task.agent.instructions}
18932
19065
  ## Alook CLI Tools
18933
19066
  You can communicate with the world through Alook CLI.
18934
19067
  The CLI auto-detects your identity from the environment. No need to pass \`--agent_id\`.
19068
+
19069
+ ### Command quick reference
19070
+ | Capability | Command |
19071
+ |---|---|
19072
+ | Schedule / list / edit tasks | \`${cmdPrefix()} calendar set\` (also list, show, update, delete) |
19073
+ | Upload a file for your owner | \`${cmdPrefix()} sync upload-artifact\` |
19074
+ | Recruit a colleague agent | \`${cmdPrefix()} agent recruit\` |
19075
+
19076
+ Detailed usage for each capability follows below.
18935
19077
  `;
18936
- if (alookAddr || customAddrs.length > 0) {
18937
- const lines = [];
18938
- if (alookAddr)
18939
- lines.push(`- '${alookAddr}' (default, Alook platform address)`);
18940
- for (const a of customAddrs)
18941
- lines.push(`- '${a}' (custom IMAP/SMTP mailbox)`);
18942
- content += `
19078
+ const emailLines = [];
19079
+ if (alookAddr)
19080
+ emailLines.push(`- '${alookAddr}' (default, Alook platform address)`);
19081
+ for (const a of customAddrs)
19082
+ emailLines.push(`- '${a}' (custom IMAP/SMTP mailbox)`);
19083
+ content += `
18943
19084
  Your email addresses:
18944
- ${lines.join(`
19085
+ ${emailLines.join(`
18945
19086
  `)}
18946
19087
 
18947
19088
 
19089
+ ### Email command quick reference
19090
+ | Action | Command |
19091
+ |---|---|
19092
+ | Pull a specific email | \`${cmdPrefix()} email pull --email_id <EMAIL_ID>\` |
19093
+ | Pull unread inbox | \`${cmdPrefix()} email pull --status unread\` |
19094
+ | Mark read | \`${cmdPrefix()} email set --email_id <EMAIL_ID> --status read\` |
19095
+ | Send | \`${cmdPrefix()} email send --to <ADDRESS> --subject "<S>" --body-file <PATH>\` |
19096
+ | Reply (same thread) | \`${cmdPrefix()} email send ... --in-reply-to <EMAIL_ID>\` |
19097
+ | Forward | \`${cmdPrefix()} email forward --email_id <EMAIL_ID> --to <RECIPIENT>\` |
19098
+ | Whitelist | \`${cmdPrefix()} email whitelist list\` (also add, delete) |
19099
+
18948
19100
  ### Emails
18949
- ---
18950
19101
  When your task prompt includes an \`email_id\` field, fetch ONLY that specific email:
18951
19102
  - Run '${cmdPrefix()} email pull --email_id <EMAIL_ID>' (uses the email_id from the prompt)
18952
19103
  When no \`email_id\` is present, fall back to listing unread:
18953
19104
  - Run '${cmdPrefix()} email pull --status unread' to download unread emails from inbox to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/'.
18954
- ---
19105
+
18955
19106
  To download sent emails, add '--folder sent': '${cmdPrefix()} email pull --folder sent'
18956
19107
  Valid folders: inbox (default), sent, untrust.
18957
19108
  To limit the number of emails downloaded, add '--limit <N>' (e.g. '--limit 20'). Use '--offset <N>' to skip emails for pagination.
18958
19109
  Example: '${cmdPrefix()} email pull --status unread --limit 20 --offset 0'
18959
- ---
19110
+
18960
19111
  Each email is saved to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/<emailId>/' with:
18961
19112
  - 'metadata.json' — sender, recipient, subject, date, status, message_id, in_reply_to, references
18962
19113
  - 'body.txt' — plain text body
18963
19114
  - 'body.html' — HTML body (if available)
18964
19115
  - 'attachments/' — extracted attachment files (if any)
18965
- ---
19116
+
18966
19117
  Before starting to process an INBOX email, mark it as read:
18967
19118
  - Run '${cmdPrefix()} email set --email_id <EMAIL_ID> --status read'
18968
- ---
18969
19119
 
18970
19120
  #### Sending a new email
18971
19121
  Write the HTML body to a file first, then send it. The body is forwarded as-is (HTML).
@@ -18982,7 +19132,6 @@ To reply to an email, add '--in-reply-to <EMAIL_ID>' to the send command. This s
18982
19132
  - Example: '${cmdPrefix()} email send --to sender@example.com --subject "Re: Bug report" --body-file /tmp/reply.html --in-reply-to <EMAIL_ID>'
18983
19133
  Tips:
18984
19134
  - If you think the task will take a while, consider sending a short "I'm on it" style email reply first to reassure the sender.
18985
- ---
18986
19135
 
18987
19136
  #### Forwarding an email
18988
19137
  Forward any email to a new recipient, with an optional note prepended above the original content. All original attachments are re-attached automatically.
@@ -18991,16 +19140,13 @@ Forward any email to a new recipient, with an optional note prepended above the
18991
19140
  - Add '--from <YOUR_EMAIL_ADDRESS>' to send from a specific mailbox.
18992
19141
  - Add '--attachment <PATH>' to attach extra files (repeatable).
18993
19142
  - Example: '${cmdPrefix()} email forward --email_id em_abc --to boss@company.com --note "FYI" --attachment /tmp/summary.pdf'
18994
- ---
18995
19143
 
18996
19144
  #### Email Whitelist (Allowed Senders)
18997
19145
  Manage which email addresses are allowed to send you emails.
18998
19146
  - List: '${cmdPrefix()} email whitelist list' (add '--json' for machine-readable output)
18999
19147
  - Add: '${cmdPrefix()} email whitelist add <EMAIL_ADDRESS>'
19000
19148
  - Remove: '${cmdPrefix()} email whitelist delete <EMAIL_ADDRESS>'
19001
- ---
19002
19149
  `;
19003
- }
19004
19150
  content += `
19005
19151
  ### Artifacts
19006
19152
  Upload files for your owner to review in the app.
@@ -19009,12 +19155,10 @@ Upload files for your owner to review in the app.
19009
19155
  - Use this after generating plans, reports, or any file the owner should review.
19010
19156
  - You response will be rendered in remote server, so don't output link format with local path in your response (cause user can click it and jump to nowheres)
19011
19157
  - If you think user may need to know any file detail, use upload-artifact tool to send the file to user.
19012
- ---
19013
19158
 
19014
19159
  ### Attachments
19015
19160
  When your task includes attachments, their local paths are listed in the prompt JSON under "attachments".
19016
19161
  Use your Read tool to open them. Images and PDFs are read visually.
19017
- ---
19018
19162
  `;
19019
19163
  content += `
19020
19164
  ### Agent Management
@@ -19031,7 +19175,6 @@ Recruit new colleague agents directly from the CLI. The server auto-generates a
19031
19175
  - Example: '${cmdPrefix()} agent recruit --instructions "You are a QA engineer..." --relationship "DELEGATE when: code is ready for review"'
19032
19176
  - Output: 'Recruited Felix (felix@alook.ai) — ag_xK9mPq2z'
19033
19177
  - The new agent shares your runtime, is automatically linked as your colleague, and receives a welcome task.
19034
- ---
19035
19178
  `;
19036
19179
  content += `
19037
19180
  ### Calendar
@@ -19040,7 +19183,7 @@ Schedule future tasks for yourself. At the scheduled time, a new task is dispatc
19040
19183
 
19041
19184
  !USE Calendar when you think the tasks are recurring or it should be conducted in the future.
19042
19185
  !When scheduling calendar events relative to a weekday (e.g. "every Monday"), always run date '+%A' first to confirm today's weekday before calculating the target date
19043
- ---
19186
+
19044
19187
  Keep the event title informative and concise, less than 20 words.
19045
19188
  Place the event details in description.
19046
19189
  Create a one-off event:
@@ -19052,7 +19195,7 @@ Create a repeating event:
19052
19195
  - Add '--repeat <interval>' where interval is like '1day', '2hour', '1week', '1month'.
19053
19196
  - Optionally add '--repeat_stop_date <YYYY-MM-DD>' to stop the recurrence (local date).
19054
19197
  - Example: '${cmdPrefix()} calendar set --event_title "<REPEAT_TASK_TITLE>" --description "<REPEAT_TASK_BODY>" --datetime 2026-04-18T09:00 --repeat 1day --repeat_stop_date 2026-05-18'
19055
- ---
19198
+
19056
19199
  List upcoming events:
19057
19200
  - Run '${cmdPrefix()} calendar list' (defaults: next 30 days, past 0 days).
19058
19201
  - Tune the window with '--future_days <N>' and '--past_days <N>'. Add '--json' for machine-readable output.
@@ -19074,7 +19217,6 @@ Edit an existing event (preserves event id and recurring state):
19074
19217
 
19075
19218
  Delete an event:
19076
19219
  - Run '${cmdPrefix()} calendar delete --event_id <EVENT_ID>'
19077
- ---
19078
19220
  `;
19079
19221
  return content;
19080
19222
  }
@@ -19201,7 +19343,7 @@ function releaseLock(lockPath) {
19201
19343
  }
19202
19344
 
19203
19345
  // daemon/execenv/timeline.ts
19204
- var log3 = createLogger2({ module: "timeline" });
19346
+ var log4 = createLogger2({ module: "timeline" });
19205
19347
  function readJsonl(filePath) {
19206
19348
  let content;
19207
19349
  try {
@@ -19239,8 +19381,7 @@ function recentFilenames(maxDays) {
19239
19381
  }
19240
19382
  return filenames;
19241
19383
  }
19242
- function localISOString() {
19243
- const now = new Date;
19384
+ function localISOString(now = new Date) {
19244
19385
  const tzOffset = -now.getTimezoneOffset();
19245
19386
  const sign = tzOffset >= 0 ? "+" : "-";
19246
19387
  const absOffset = Math.abs(tzOffset);
@@ -19271,7 +19412,7 @@ async function initEntryAsync(timelineDir, entry) {
19271
19412
  acquired = acquireLock(lockPath);
19272
19413
  }
19273
19414
  if (!acquired) {
19274
- log3.debug(`Timeline initEntry: could not acquire lock for ${filename}`);
19415
+ log4.debug(`Timeline initEntry: could not acquire lock for ${filename}`);
19275
19416
  return;
19276
19417
  }
19277
19418
  try {
@@ -19281,7 +19422,7 @@ async function initEntryAsync(timelineDir, entry) {
19281
19422
  releaseLock(lockPath);
19282
19423
  }
19283
19424
  } catch (err) {
19284
- log3.debug("Timeline initEntry failed", err);
19425
+ log4.debug("Timeline initEntry failed", err);
19285
19426
  }
19286
19427
  }
19287
19428
  function updateEntry(timelineDir, taskId, updater) {
@@ -19291,7 +19432,7 @@ function updateEntry(timelineDir, taskId, updater) {
19291
19432
  try {
19292
19433
  const acquired = acquireLock(lockPath);
19293
19434
  if (!acquired) {
19294
- log3.debug(`Timeline updateEntry: lock held for ${filename}, skipping`);
19435
+ log4.debug(`Timeline updateEntry: lock held for ${filename}, skipping`);
19295
19436
  continue;
19296
19437
  }
19297
19438
  try {
@@ -19324,10 +19465,10 @@ function updateEntry(timelineDir, taskId, updater) {
19324
19465
  releaseLock(lockPath);
19325
19466
  }
19326
19467
  } catch (err) {
19327
- log3.debug(`Timeline updateEntry failed for ${filename}`, err);
19468
+ log4.debug(`Timeline updateEntry failed for ${filename}`, err);
19328
19469
  }
19329
19470
  }
19330
- log3.debug(`Timeline updateEntry: task_id ${taskId} not found in last 7 days`);
19471
+ log4.debug(`Timeline updateEntry: task_id ${taskId} not found in last 7 days`);
19331
19472
  }
19332
19473
  function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider, contextKey, detailedLog) {
19333
19474
  return {
@@ -19388,7 +19529,7 @@ function findRunningEntryByContextKey(timelineDir, contextKey, provider) {
19388
19529
  // daemon/execenv/steering.ts
19389
19530
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync3, readdirSync, statSync as statSync2 } from "fs";
19390
19531
  import { join as join8 } from "path";
19391
- var log4 = createLogger2({ module: "steering" });
19532
+ var log5 = createLogger2({ module: "steering" });
19392
19533
  var INTENT_DIR_NAME = ".kill_intents";
19393
19534
  var STEERING_LOCK_DIR = ".steering_locks";
19394
19535
  var INTENT_STALE_MS = 10 * 60 * 1000;
@@ -19442,7 +19583,7 @@ function cleanupStaleIntents(baseDir) {
19442
19583
  const stat = statSync2(filePath);
19443
19584
  if (now - stat.mtimeMs > INTENT_STALE_MS) {
19444
19585
  unlinkSync3(filePath);
19445
- log4.debug(`Cleaned up stale kill intent for task ${intent.targetTaskId}`);
19586
+ log5.debug(`Cleaned up stale kill intent for task ${intent.targetTaskId}`);
19446
19587
  }
19447
19588
  } catch {}
19448
19589
  }
@@ -19468,7 +19609,13 @@ function buildDmNotice(name, email3) {
19468
19609
  return `This task was triggered by an incoming email on a conversation with ${name} (${email3}).` + ` ${name} is present in this session — reply to them directly.` + ` If you need to communicate with anyone else, use the email sending tool.`;
19469
19610
  }
19470
19611
  function buildPrompt(task, attachments) {
19471
- const obj = { type: task.type, instruction: task.prompt };
19612
+ const createdAt = new Date(task.createdAt);
19613
+ const receivedAt = Number.isNaN(createdAt.getTime()) ? localISOString() : localISOString(createdAt);
19614
+ const obj = {
19615
+ type: task.type,
19616
+ received_at: receivedAt,
19617
+ instruction: task.prompt
19618
+ };
19472
19619
  if (task.type === "user_dm_message") {
19473
19620
  obj.notice = DM_RESPONSE_NOTICE;
19474
19621
  }
@@ -19531,7 +19678,7 @@ function buildPrompt(task, attachments) {
19531
19678
  }
19532
19679
 
19533
19680
  // daemon/session-runner.ts
19534
- var log5 = createLogger2({ module: "session-runner" });
19681
+ var log6 = createLogger2({ module: "session-runner" });
19535
19682
  var ATTACHMENTS_BASE = tempDir("alook-attachments");
19536
19683
  async function writeMarkerFile(workspacesRoot, marker) {
19537
19684
  const dir = path.join(workspacesRoot, ".pending_completions");
@@ -19579,20 +19726,20 @@ async function reportToServer(fn, markerData, workspacesRoot) {
19579
19726
  } catch (e) {
19580
19727
  lastErr = e;
19581
19728
  if (isClientError(e)) {
19582
- log5.info(`server report for task ${markerData.taskId}: task already in terminal state (${e})`);
19729
+ log6.info(`server report for task ${markerData.taskId}: task already in terminal state (${e})`);
19583
19730
  return;
19584
19731
  }
19585
19732
  if (attempt < RETRY_DELAYS.length && isRetryableError(e)) {
19586
- log5.debug(`server report attempt ${attempt + 1} failed for task ${markerData.taskId}, retrying in ${RETRY_DELAYS[attempt]}ms`);
19733
+ log6.debug(`server report attempt ${attempt + 1} failed for task ${markerData.taskId}, retrying in ${RETRY_DELAYS[attempt]}ms`);
19587
19734
  await new Promise((r) => setTimeout(r, RETRY_DELAYS[attempt]));
19588
19735
  }
19589
19736
  }
19590
19737
  }
19591
- log5.warn(`server report failed for task ${markerData.taskId} after retries, writing marker: ${lastErr}`);
19738
+ log6.warn(`server report failed for task ${markerData.taskId} after retries, writing marker: ${lastErr}`);
19592
19739
  try {
19593
19740
  await writeMarkerFile(workspacesRoot, markerData);
19594
19741
  } catch (writeErr) {
19595
- log5.error(`marker write also failed for task ${markerData.taskId}: ${writeErr}`);
19742
+ log6.error(`marker write also failed for task ${markerData.taskId}: ${writeErr}`);
19596
19743
  }
19597
19744
  }
19598
19745
  function sanitizeFilename(name) {
@@ -19623,7 +19770,7 @@ async function downloadAttachments(client, token, workspaceId, taskId, attachmen
19623
19770
  }
19624
19771
  async function runSession(input) {
19625
19772
  const { task, provider, cliPath, model, serverURL, token, workspacesRoot, agentTimeout, messageInactivityTimeout } = input;
19626
- log5.info(`starting (task=${task.id}, type=${task.type}, agent=${task.agentId}, provider=${provider}, model=${model || "default"})`);
19773
+ log6.info(`starting (task=${task.id}, type=${task.type}, agent=${task.agentId}, provider=${provider}, model=${model || "default"})`);
19627
19774
  const client = new DaemonClient(serverURL);
19628
19775
  const backend = createBackend(provider, cliPath);
19629
19776
  const agentBaseDir = path.join(workspacesRoot, task.workspaceId, task.agentId, "workdir");
@@ -19632,11 +19779,37 @@ async function runSession(input) {
19632
19779
  await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, undefined, process.pid, provider, task.contextKey, input.logFilePath));
19633
19780
  const { workDir, env } = prepare({ workspacesRoot, token }, task);
19634
19781
  let killed = false;
19635
- const earlyOnKill = async () => {
19782
+ let agentPid = undefined;
19783
+ let flushTimer = undefined;
19784
+ const pendingMessages = [];
19785
+ let seq = 0;
19786
+ let toolCount = 0;
19787
+ const BATCH_SIZE = Number(process.env.ALOOK_MESSAGE_BATCH_SIZE) || 20;
19788
+ const FLUSH_INTERVAL_MS = Number(process.env.ALOOK_MESSAGE_FLUSH_INTERVAL_MS) || 100;
19789
+ const flushMessages = async () => {
19790
+ if (pendingMessages.length === 0)
19791
+ return;
19792
+ const batch = pendingMessages.splice(0);
19793
+ try {
19794
+ await client.reportMessages(token, task.id, batch);
19795
+ } catch (e) {
19796
+ log6.debug("message report failed", e);
19797
+ }
19798
+ };
19799
+ const onKill = async () => {
19636
19800
  if (killed)
19637
19801
  return;
19638
19802
  killed = true;
19639
- log5.info(`killed by signal (messages=0, tools=0)`);
19803
+ log6.info(`killed by signal (messages=${seq}, tools=${toolCount})`);
19804
+ if (agentPid !== undefined) {
19805
+ log6.info(`killing inner agent group (pid=${agentPid})`);
19806
+ await killProcessTree(agentPid);
19807
+ }
19808
+ if (flushTimer)
19809
+ clearInterval(flushTimer);
19810
+ try {
19811
+ await flushMessages();
19812
+ } catch {}
19640
19813
  await cleanupAttachments(task.id);
19641
19814
  const intent = readKillIntent(agentBaseDir, task.id);
19642
19815
  clearKillIntent(agentBaseDir, task.id);
@@ -19667,34 +19840,36 @@ async function runSession(input) {
19667
19840
  }
19668
19841
  process.exit(1);
19669
19842
  };
19670
- process.on("SIGTERM", earlyOnKill);
19671
- process.on("SIGINT", earlyOnKill);
19843
+ process.on("SIGTERM", onKill);
19844
+ process.on("SIGINT", onKill);
19672
19845
  const attachmentIds = task.context?.attachment_ids ?? [];
19673
19846
  let attachments;
19674
19847
  if (attachmentIds.length > 0) {
19675
- log5.info(`downloading ${attachmentIds.length} attachment(s)`);
19848
+ log6.info(`downloading ${attachmentIds.length} attachment(s)`);
19676
19849
  try {
19677
19850
  attachments = await downloadAttachments(client, token, task.workspaceId, task.id, attachmentIds);
19678
- log5.info(`attachments ready (${attachments.length} file(s))`);
19851
+ log6.info(`attachments ready (${attachments.length} file(s))`);
19679
19852
  } catch (e) {
19680
19853
  await cleanupAttachments(task.id);
19681
19854
  const errMsg = `failed to download attachments: ${e}`;
19682
- log5.error(errMsg);
19855
+ log6.error(errMsg);
19683
19856
  updateEntry(timelineDir, task.id, (entry) => {
19684
19857
  entry.pid = null;
19685
19858
  entry.status = "failed";
19686
19859
  entry.errmsg = errMsg;
19687
19860
  });
19688
19861
  await reportToServer(() => client.failTask(token, task.id, errMsg), { taskId: task.id, type: "fail", payload: { error: errMsg }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
19689
- process.removeListener("SIGTERM", earlyOnKill);
19690
- process.removeListener("SIGINT", earlyOnKill);
19862
+ process.removeListener("SIGTERM", onKill);
19863
+ process.removeListener("SIGINT", onKill);
19691
19864
  return;
19692
19865
  }
19693
19866
  }
19867
+ if (killed)
19868
+ return;
19694
19869
  const prompt = buildPrompt(task, attachments);
19695
19870
  const resumeSessionId = task.contextKey ? findResumableSessionByContextKey(timelineDir, task.contextKey, provider) ?? undefined : undefined;
19696
19871
  if (resumeSessionId) {
19697
- log5.info(`resuming session ${resumeSessionId} (context_key: ${task.contextKey})`);
19872
+ log6.info(`resuming session ${resumeSessionId} (context_key: ${task.contextKey})`);
19698
19873
  }
19699
19874
  const session2 = backend.execute(prompt, {
19700
19875
  cwd: workDir,
@@ -19703,77 +19878,21 @@ async function runSession(input) {
19703
19878
  timeout: agentTimeout,
19704
19879
  resumeSessionId
19705
19880
  });
19706
- const agentPid = session2.pid;
19881
+ agentPid = session2.pid;
19882
+ if (killed) {
19883
+ if (agentPid !== undefined) {
19884
+ log6.info(`kill landed during spawn — reaping inner agent group (pid=${agentPid})`);
19885
+ await killProcessTree(agentPid);
19886
+ }
19887
+ process.exit(1);
19888
+ }
19707
19889
  const earlySessionId = await session2.sessionId;
19708
- log5.info(`agent started (pid=${agentPid ?? "unknown"}, session=${earlySessionId})`);
19709
- log5.info(JSON.stringify({ role: "user", type: "text", content: prompt }));
19890
+ log6.info(`agent started (pid=${agentPid ?? "unknown"}, session=${earlySessionId})`);
19891
+ log6.info(JSON.stringify({ role: "user", type: "text", content: prompt }));
19710
19892
  updateEntry(timelineDir, task.id, (entry) => {
19711
19893
  entry.session_id = earlySessionId || null;
19712
19894
  });
19713
- const pendingMessages = [];
19714
- let seq = 0;
19715
- let toolCount = 0;
19716
- const BATCH_SIZE = Number(process.env.ALOOK_MESSAGE_BATCH_SIZE) || 20;
19717
- const FLUSH_INTERVAL_MS = Number(process.env.ALOOK_MESSAGE_FLUSH_INTERVAL_MS) || 100;
19718
- const flushMessages = async () => {
19719
- if (pendingMessages.length === 0)
19720
- return;
19721
- const batch = pendingMessages.splice(0);
19722
- try {
19723
- await client.reportMessages(token, task.id, batch);
19724
- } catch (e) {
19725
- log5.debug("message report failed", e);
19726
- }
19727
- };
19728
- const flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
19729
- process.removeListener("SIGTERM", earlyOnKill);
19730
- process.removeListener("SIGINT", earlyOnKill);
19731
- const onKill = async () => {
19732
- if (killed)
19733
- return;
19734
- killed = true;
19735
- log5.info(`killed by signal (messages=${seq}, tools=${toolCount})`);
19736
- if (agentPid) {
19737
- try {
19738
- process.kill(agentPid, "SIGTERM");
19739
- } catch {}
19740
- }
19741
- clearInterval(flushTimer);
19742
- try {
19743
- await flushMessages();
19744
- } catch {}
19745
- await cleanupAttachments(task.id);
19746
- const intent = readKillIntent(agentBaseDir, task.id);
19747
- clearKillIntent(agentBaseDir, task.id);
19748
- if (intent?.reason === "superseded") {
19749
- updateEntry(timelineDir, task.id, (entry) => {
19750
- entry.pid = null;
19751
- entry.status = "superseded";
19752
- entry.successor_task_id = intent.successorTaskId ?? null;
19753
- entry.supersede_reason = "superseded by newer task";
19754
- });
19755
- try {
19756
- await client.supersedeTask(token, task.id);
19757
- } catch {}
19758
- } else if (intent?.reason === "cancelled") {
19759
- updateEntry(timelineDir, task.id, (entry) => {
19760
- entry.pid = null;
19761
- entry.status = "cancelled";
19762
- entry.errmsg = "cancelled by user";
19763
- });
19764
- await reportToServer(() => client.failTask(token, task.id, "cancelled by user"), { taskId: task.id, type: "fail", payload: { error: "cancelled by user" }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
19765
- } else {
19766
- updateEntry(timelineDir, task.id, (entry) => {
19767
- entry.pid = null;
19768
- entry.status = "killed";
19769
- entry.errmsg = "killed by signal";
19770
- });
19771
- await reportToServer(() => client.failTask(token, task.id, "killed by signal"), { taskId: task.id, type: "fail", payload: { error: "killed by signal" }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
19772
- }
19773
- process.exit(1);
19774
- };
19775
- process.on("SIGTERM", onKill);
19776
- process.on("SIGINT", onKill);
19895
+ flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
19777
19896
  const INACTIVITY_TIMEOUT_MS = messageInactivityTimeout ?? 5 * 60 * 1000;
19778
19897
  let inactivityTimedOut = false;
19779
19898
  try {
@@ -19789,11 +19908,9 @@ async function runSession(input) {
19789
19908
  ]) : next);
19790
19909
  if (raceResult === "timeout") {
19791
19910
  inactivityTimedOut = true;
19792
- log5.warn(`message inactivity timeout (${INACTIVITY_TIMEOUT_MS / 1000}s) — killing agent`);
19793
- if (session2.pid) {
19794
- try {
19795
- process.kill(session2.pid, "SIGTERM");
19796
- } catch {}
19911
+ log6.warn(`message inactivity timeout (${INACTIVITY_TIMEOUT_MS / 1000}s) — killing agent`);
19912
+ if (session2.pid !== undefined) {
19913
+ await killProcessTree(session2.pid);
19797
19914
  }
19798
19915
  iter.return?.(undefined);
19799
19916
  break;
@@ -19806,9 +19923,9 @@ async function runSession(input) {
19806
19923
  if (msg.type === "tool-use")
19807
19924
  toolCount++;
19808
19925
  if (msg.type === "tool-result" && msg.output && msg.output.length > 500) {
19809
- log5.info(JSON.stringify({ role: "assistant", ...msg, output: msg.output.slice(0, 500) + `... (${msg.output.length} chars)` }));
19926
+ log6.info(JSON.stringify({ role: "assistant", ...msg, output: msg.output.slice(0, 500) + `... (${msg.output.length} chars)` }));
19810
19927
  } else {
19811
- log5.info(JSON.stringify({ role: "assistant", ...msg }));
19928
+ log6.info(JSON.stringify({ role: "assistant", ...msg }));
19812
19929
  }
19813
19930
  if (msg.type === "status" || msg.type === "log")
19814
19931
  continue;
@@ -19833,14 +19950,11 @@ async function runSession(input) {
19833
19950
  if (!killed)
19834
19951
  await flushMessages();
19835
19952
  } finally {
19836
- clearInterval(flushTimer);
19837
- process.removeListener("SIGTERM", onKill);
19838
- process.removeListener("SIGINT", onKill);
19953
+ if (flushTimer)
19954
+ clearInterval(flushTimer);
19839
19955
  }
19840
19956
  if (killed)
19841
19957
  return;
19842
- process.on("SIGTERM", onKill);
19843
- process.on("SIGINT", onKill);
19844
19958
  const result = await session2.result;
19845
19959
  process.removeListener("SIGTERM", onKill);
19846
19960
  process.removeListener("SIGINT", onKill);
@@ -19872,18 +19986,18 @@ async function runSession(input) {
19872
19986
  body.session_id = result.sessionId;
19873
19987
  await reportToServer(() => client.completeTask(token, task.id, body), { taskId: task.id, type: "complete", payload: body, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
19874
19988
  const dur = (result.durationMs / 1000).toFixed(1);
19875
- log5.info(`completed (duration=${dur}s, messages=${seq}, tools=${toolCount})`);
19989
+ log6.info(`completed (duration=${dur}s, messages=${seq}, tools=${toolCount})`);
19876
19990
  } else {
19877
19991
  const errorMsg = result.error || "agent exited unexpectedly";
19878
19992
  await reportToServer(() => client.failTask(token, task.id, errorMsg), { taskId: task.id, type: "fail", payload: { error: errorMsg }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
19879
19993
  const dur = (result.durationMs / 1000).toFixed(1);
19880
- log5.info(`failed (duration=${dur}s, messages=${seq}, tools=${toolCount}) — ${result.error}`);
19994
+ log6.info(`failed (duration=${dur}s, messages=${seq}, tools=${toolCount}) — ${result.error}`);
19881
19995
  }
19882
19996
  }
19883
19997
  async function main() {
19884
19998
  const encoded = process.argv[2];
19885
19999
  if (!encoded) {
19886
- log5.error("session-runner: missing base64-encoded input argument");
20000
+ log6.error("session-runner: missing base64-encoded input argument");
19887
20001
  process.exit(1);
19888
20002
  }
19889
20003
  let input;
@@ -19891,14 +20005,14 @@ async function main() {
19891
20005
  const json2 = Buffer.from(encoded, "base64").toString("utf-8");
19892
20006
  input = JSON.parse(json2);
19893
20007
  } catch (e) {
19894
- log5.error("session-runner: failed to parse input", e);
20008
+ log6.error("session-runner: failed to parse input", e);
19895
20009
  process.exit(1);
19896
20010
  }
19897
20011
  const client = new DaemonClient(input.serverURL);
19898
20012
  try {
19899
20013
  await runSession(input);
19900
20014
  } catch (e) {
19901
- log5.error(`session-runner: unhandled error for task ${input.task.id}`, e);
20015
+ log6.error(`session-runner: unhandled error for task ${input.task.id}`, e);
19902
20016
  await cleanupAttachments(input.task.id);
19903
20017
  const timelineDir = path.join(input.workspacesRoot, input.task.workspaceId, input.task.agentId, "workdir", ".context_timeline").replace(/\\/g, "/");
19904
20018
  updateEntry(timelineDir, input.task.id, (entry) => {
@@ -19917,7 +20031,7 @@ if (isDirectExecution) {
19917
20031
  }
19918
20032
 
19919
20033
  // daemon/ws-client.ts
19920
- var log6 = createLogger2({ module: "ws-client" });
20034
+ var log7 = createLogger2({ module: "ws-client" });
19921
20035
  var WS_RECONNECT_INIT = 1000;
19922
20036
  var WS_RECONNECT_MAX = 30000;
19923
20037
  var WS_PING_INTERVAL = 25000;
@@ -19954,11 +20068,11 @@ class DaemonWsClient {
19954
20068
  return;
19955
20069
  this.cleanup();
19956
20070
  const wsUrl = this.getUrl();
19957
- log6.info("connecting", { url: wsUrl });
20071
+ log7.info("connecting", { url: wsUrl });
19958
20072
  try {
19959
20073
  this.ws = new WebSocket(wsUrl);
19960
20074
  } catch (err) {
19961
- log6.warn("ws creation failed", { err: String(err) });
20075
+ log7.warn("ws creation failed", { err: String(err) });
19962
20076
  this.scheduleReconnect();
19963
20077
  return;
19964
20078
  }
@@ -19980,23 +20094,23 @@ class DaemonWsClient {
19980
20094
  try {
19981
20095
  const msg = JSON.parse(str);
19982
20096
  if (msg.type === "auth.ok") {
19983
- log6.info("authenticated");
20097
+ log7.info("authenticated");
19984
20098
  this.connected = true;
19985
20099
  this.opts.onConnected();
19986
20100
  return;
19987
20101
  }
19988
20102
  const parsed = DaemonPushMessageSchema.safeParse(msg);
19989
20103
  if (!parsed.success) {
19990
- log6.warn("invalid push message", { err: parsed.error.message });
20104
+ log7.warn("invalid push message", { err: parsed.error.message });
19991
20105
  return;
19992
20106
  }
19993
20107
  this.opts.onMessage(parsed.data);
19994
20108
  } catch (err) {
19995
- log6.debug("message parse error", { err: String(err) });
20109
+ log7.debug("message parse error", { err: String(err) });
19996
20110
  }
19997
20111
  });
19998
20112
  this.ws.addEventListener("error", () => {
19999
- log6.debug("ws error");
20113
+ log7.debug("ws error");
20000
20114
  });
20001
20115
  this.ws.addEventListener("close", () => {
20002
20116
  const wasConnected = this.connected;
@@ -20034,7 +20148,7 @@ class DaemonWsClient {
20034
20148
  const delay = Math.min(this.reconnectDelay, WS_RECONNECT_MAX);
20035
20149
  this.reconnectDelay = Math.min(delay * 2, WS_RECONNECT_MAX);
20036
20150
  const jitter = Math.random() * 500;
20037
- log6.debug("reconnecting", { delayMs: Math.round(delay + jitter) });
20151
+ log7.debug("reconnecting", { delayMs: Math.round(delay + jitter) });
20038
20152
  this.reconnectTimer = setTimeout(() => {
20039
20153
  this.reconnectTimer = null;
20040
20154
  this.connect();
@@ -20048,7 +20162,7 @@ class DaemonWsClient {
20048
20162
  }, WS_PING_INTERVAL);
20049
20163
  this.livenessInterval = setInterval(() => {
20050
20164
  if (Date.now() - this.lastMessageAt > WS_LIVENESS_TIMEOUT) {
20051
- log6.warn("liveness timeout, closing");
20165
+ log7.warn("liveness timeout, closing");
20052
20166
  this.ws?.close();
20053
20167
  }
20054
20168
  }, 5000);
@@ -20096,7 +20210,7 @@ function runNpmUpdate(targetVersion) {
20096
20210
  }
20097
20211
 
20098
20212
  // daemon/update-handler.ts
20099
- var log7 = createLogger2({ module: "updater" });
20213
+ var log8 = createLogger2({ module: "updater" });
20100
20214
  var updating = false;
20101
20215
  var retryCount = 0;
20102
20216
  var MAX_RETRIES = 3;
@@ -20126,29 +20240,29 @@ async function handleCliUpdate(version3, onSuccess, profile) {
20126
20240
  if (retryCount >= MAX_RETRIES)
20127
20241
  return;
20128
20242
  if (process.env.ALOOK_CMD_PREFIX) {
20129
- log7.info(`Skipping auto-update in app mode — user should run: npx @alook/app@latest update`);
20243
+ log8.info(`Skipping auto-update in app mode — user should run: npx @alook/app@latest update`);
20130
20244
  return;
20131
20245
  }
20132
20246
  const marker = readUpdateMarker(profile);
20133
20247
  if (marker === version3) {
20134
- log7.info(`Skipping update to v${version3} — already attempted (marker exists)`);
20248
+ log8.info(`Skipping update to v${version3} — already attempted (marker exists)`);
20135
20249
  return;
20136
20250
  }
20137
20251
  updating = true;
20138
20252
  try {
20139
- log7.info(`Updating CLI to v${version3}...`);
20253
+ log8.info(`Updating CLI to v${version3}...`);
20140
20254
  const result = await runNpmUpdate(version3);
20141
20255
  if (result.success) {
20142
20256
  writeUpdateMarker(version3, profile);
20143
- log7.info(`CLI updated to v${version3} — restarting`);
20257
+ log8.info(`CLI updated to v${version3} — restarting`);
20144
20258
  onSuccess();
20145
20259
  } else {
20146
20260
  retryCount++;
20147
- log7.error(`CLI update failed (attempt ${retryCount}/${MAX_RETRIES}): ${result.output}`);
20261
+ log8.error(`CLI update failed (attempt ${retryCount}/${MAX_RETRIES}): ${result.output}`);
20148
20262
  }
20149
20263
  } catch (e) {
20150
20264
  retryCount++;
20151
- log7.error(`CLI update error (attempt ${retryCount}/${MAX_RETRIES})`, e);
20265
+ log8.error(`CLI update error (attempt ${retryCount}/${MAX_RETRIES})`, e);
20152
20266
  } finally {
20153
20267
  updating = false;
20154
20268
  }
@@ -20257,7 +20371,7 @@ import { existsSync as existsSync2, mkdirSync as mkdirSync7, readFileSync as rea
20257
20371
  import { join as join10, basename } from "path";
20258
20372
  import { homedir as homedir2 } from "os";
20259
20373
  import { createHash as createHash2 } from "crypto";
20260
- var log8 = createLogger2({ module: "skill-scanner" });
20374
+ var log9 = createLogger2({ module: "skill-scanner" });
20261
20375
  function getCacheDir() {
20262
20376
  return join10(configDir(), "skills");
20263
20377
  }
@@ -20543,7 +20657,7 @@ function runScan() {
20543
20657
  const prevHash = readCacheHash(globalCachePath(scannerConfig.daemonId, runtime));
20544
20658
  if (prevHash !== hash2) {
20545
20659
  const skillItems = skills.map((s) => ({ name: s.name, description: s.description }));
20546
- log8.debug(`Syncing global ${runtime} — ${skills.length} skills`);
20660
+ log9.debug(`Syncing global ${runtime} — ${skills.length} skills`);
20547
20661
  const daemonId = scannerConfig.daemonId;
20548
20662
  const syncPromises = scannerConfig.workspaces.map((ws) => clientRef.syncSkills(ws.token, {
20549
20663
  scope: "global",
@@ -20553,10 +20667,10 @@ function runScan() {
20553
20667
  }));
20554
20668
  Promise.all(syncPromises).then(() => {
20555
20669
  writeCacheFile(globalCachePath(daemonId, runtime), hash2, skills);
20556
- }).catch((e) => log8.debug("Global skill sync failed", e));
20670
+ }).catch((e) => log9.debug("Global skill sync failed", e));
20557
20671
  }
20558
20672
  } catch (e) {
20559
- log8.debug(`Global scan error for ${runtime}`, e);
20673
+ log9.debug(`Global scan error for ${runtime}`, e);
20560
20674
  }
20561
20675
  }
20562
20676
  const targets = discoverTargets();
@@ -20569,7 +20683,7 @@ function runScan() {
20569
20683
  const prevHash = readCacheHash(agentCachePath(target.agentId, target.runtime));
20570
20684
  if (prevHash !== hash2) {
20571
20685
  const skillItems = skills.map((s) => ({ name: s.name, description: s.description }));
20572
- log8.debug(`Syncing ${target.agentId}:${target.runtime} — ${skills.length} agent skills`);
20686
+ log9.debug(`Syncing ${target.agentId}:${target.runtime} — ${skills.length} agent skills`);
20573
20687
  clientRef.syncSkills(target.token, {
20574
20688
  scope: "agent",
20575
20689
  agent_id: target.agentId,
@@ -20577,10 +20691,10 @@ function runScan() {
20577
20691
  skills: skillItems
20578
20692
  }).then(() => {
20579
20693
  writeCacheFile(agentCachePath(target.agentId, target.runtime), hash2, skills);
20580
- }).catch((e) => log8.debug("Agent skill sync failed", e));
20694
+ }).catch((e) => log9.debug("Agent skill sync failed", e));
20581
20695
  }
20582
20696
  } catch (e) {
20583
- log8.debug(`Agent scan error for ${target.agentId}:${target.runtime}`, e);
20697
+ log9.debug(`Agent scan error for ${target.agentId}:${target.runtime}`, e);
20584
20698
  }
20585
20699
  }
20586
20700
  }
@@ -20598,7 +20712,7 @@ function stopSkillScanner() {
20598
20712
  }
20599
20713
 
20600
20714
  // lib/shell-env.ts
20601
- import { execSync as execSync3 } from "child_process";
20715
+ import { execSync as execSync4 } from "child_process";
20602
20716
  var PASSTHROUGH_VARS = ["ALOOK_PROJECT_ROOT", "ALOOK_SERVER_URL", "ALOOK_CMD_PREFIX", "ALOOK_HEALTH_PORT"];
20603
20717
  function resolveLoginShellEnv() {
20604
20718
  if (isWindows) {
@@ -20606,7 +20720,7 @@ function resolveLoginShellEnv() {
20606
20720
  }
20607
20721
  const shell = process.env.SHELL || "/bin/zsh";
20608
20722
  try {
20609
- const output = execSync3(`${shell} -ilc 'env'`, {
20723
+ const output = execSync4(`${shell} -ilc 'env'`, {
20610
20724
  encoding: "utf-8",
20611
20725
  timeout: 5000,
20612
20726
  stdio: ["ignore", "pipe", "ignore"]
@@ -20633,17 +20747,17 @@ function resolveLoginShellEnv() {
20633
20747
  // daemon/daemon.ts
20634
20748
  import { existsSync as existsSync3, mkdirSync as mkdirSync8, openSync, closeSync, readdirSync as readdirSync3, statSync as statSync4, unlinkSync as unlinkSync5 } from "fs";
20635
20749
  import { readdir as readdir2, readFile as readFile2, unlink, stat as fsStat } from "fs/promises";
20636
- import { execSync as execSync4, spawn as spawn6 } from "child_process";
20750
+ import { execSync as execSync5, spawn as spawn6 } from "child_process";
20637
20751
  import { fileURLToPath as fileURLToPath3 } from "url";
20638
20752
  import { dirname as dirname3, join as join11 } from "path";
20639
- var log9 = createLogger2({ module: "daemon" });
20753
+ var log10 = createLogger2({ module: "daemon" });
20640
20754
  var _dir = dirname3(fileURLToPath3(import.meta.url));
20641
20755
  var sessionRunnerPath = existsSync3(join11(_dir, "session-runner.js")) ? join11(_dir, "session-runner.js") : join11(_dir, "session-runner.ts");
20642
20756
  var meetingRunnerPath = existsSync3(join11(_dir, "meeting-runner.js")) ? join11(_dir, "meeting-runner.js") : join11(_dir, "meeting-runner.ts");
20643
20757
  function isCommandAvailable2(cmd) {
20644
20758
  try {
20645
20759
  const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
20646
- execSync4(check2, { stdio: "ignore" });
20760
+ execSync5(check2, { stdio: "ignore" });
20647
20761
  return true;
20648
20762
  } catch {
20649
20763
  return false;
@@ -20743,14 +20857,14 @@ async function reconcilePendingCompletions(workspacesRoot) {
20743
20857
  try {
20744
20858
  parsed = JSON.parse(raw);
20745
20859
  } catch {
20746
- log9.warn(`reconcile: malformed marker ${name}, deleting`);
20860
+ log10.warn(`reconcile: malformed marker ${name}, deleting`);
20747
20861
  try {
20748
20862
  await unlink(filePath);
20749
20863
  } catch {}
20750
20864
  continue;
20751
20865
  }
20752
20866
  if (!isValidMarker(parsed)) {
20753
- log9.warn(`reconcile: invalid marker structure ${name}, deleting`);
20867
+ log10.warn(`reconcile: invalid marker structure ${name}, deleting`);
20754
20868
  try {
20755
20869
  await unlink(filePath);
20756
20870
  } catch {}
@@ -20759,7 +20873,7 @@ async function reconcilePendingCompletions(workspacesRoot) {
20759
20873
  const marker = parsed;
20760
20874
  const age = Date.now() - new Date(marker.createdAt).getTime();
20761
20875
  if (age > MARKER_STALE_MS) {
20762
- log9.warn(`reconcile: stale marker ${name} (${Math.round(age / 3600000)}h old), deleting`);
20876
+ log10.warn(`reconcile: stale marker ${name} (${Math.round(age / 3600000)}h old), deleting`);
20763
20877
  try {
20764
20878
  await unlink(filePath);
20765
20879
  } catch {}
@@ -20775,7 +20889,7 @@ async function reconcilePendingCompletions(workspacesRoot) {
20775
20889
  try {
20776
20890
  await unlink(filePath);
20777
20891
  } catch (delErr) {
20778
- log9.warn(`reconcile: delivered marker ${name} but failed to delete: ${delErr}`);
20892
+ log10.warn(`reconcile: delivered marker ${name} but failed to delete: ${delErr}`);
20779
20893
  }
20780
20894
  } catch (deliverErr) {
20781
20895
  if (isClientError2(deliverErr)) {
@@ -20783,11 +20897,11 @@ async function reconcilePendingCompletions(workspacesRoot) {
20783
20897
  await unlink(filePath);
20784
20898
  } catch {}
20785
20899
  } else {
20786
- log9.debug(`reconcile: delivery failed for ${name}, will retry next cycle`);
20900
+ log10.debug(`reconcile: delivery failed for ${name}, will retry next cycle`);
20787
20901
  }
20788
20902
  }
20789
20903
  } catch (e) {
20790
- log9.debug(`reconcile: error processing ${name}`, e);
20904
+ log10.debug(`reconcile: error processing ${name}`, e);
20791
20905
  }
20792
20906
  }
20793
20907
  }
@@ -20798,7 +20912,7 @@ async function startDaemon(profile, serverUrl) {
20798
20912
  }
20799
20913
  process.once("exit", () => releaseDaemonPid(profile));
20800
20914
  const bailOnUnexpected = (label, err) => {
20801
- log9.error(`${label} — shutting down`, err);
20915
+ log10.error(`${label} — shutting down`, err);
20802
20916
  releaseDaemonPid(profile);
20803
20917
  process.exit(1);
20804
20918
  };
@@ -20811,21 +20925,21 @@ async function startDaemon(profile, serverUrl) {
20811
20925
  if (marker) {
20812
20926
  clearUpdateMarker(profile);
20813
20927
  if (marker === config2.cliVersion) {
20814
- log9.info(`Cleared update marker — now running v${config2.cliVersion}`);
20928
+ log10.info(`Cleared update marker — now running v${config2.cliVersion}`);
20815
20929
  } else {
20816
- log9.info(`Cleared stale update marker (was v${marker}, running v${config2.cliVersion}) — update will be retried`);
20930
+ log10.info(`Cleared stale update marker (was v${marker}, running v${config2.cliVersion}) — update will be retried`);
20817
20931
  }
20818
20932
  }
20819
20933
  const cliConfig = loadCLIConfigForProfile(profile);
20820
20934
  const workspaces = cliConfig.watched_workspaces || [];
20821
20935
  if (workspaces.length === 0) {
20822
- log9.error("No watched workspaces configured.");
20936
+ log10.error("No watched workspaces configured.");
20823
20937
  process.exit(1);
20824
20938
  return;
20825
20939
  }
20826
20940
  const hasPerWorkspaceTokens = workspaces.every((ws) => !!ws.token);
20827
20941
  if (!hasPerWorkspaceTokens) {
20828
- log9.error(`Config uses old format. Run '${cmdPrefix()} register --token <token>' for each workspace to upgrade.`);
20942
+ log10.error(`Config uses old format. Run '${cmdPrefix()} register --token <token>' for each workspace to upgrade.`);
20829
20943
  process.exit(1);
20830
20944
  return;
20831
20945
  }
@@ -20845,11 +20959,11 @@ async function startDaemon(profile, serverUrl) {
20845
20959
  }
20846
20960
  }
20847
20961
  if (providers.length === 0) {
20848
- log9.error("No agent CLI tools found on PATH.");
20962
+ log10.error("No agent CLI tools found on PATH.");
20849
20963
  process.exit(1);
20850
20964
  return;
20851
20965
  }
20852
- log9.info(`Detected providers: ${providers.map((p) => `${p.type}@${p.version}`).join(", ")}`);
20966
+ log10.info(`Detected providers: ${providers.map((p) => `${p.type}@${p.version}`).join(", ")}`);
20853
20967
  const workspaceStates = [];
20854
20968
  const runtimeIndex = new Map;
20855
20969
  for (const ws of workspaces) {
@@ -20857,7 +20971,7 @@ async function startDaemon(profile, serverUrl) {
20857
20971
  type: p.type,
20858
20972
  version: p.version
20859
20973
  }));
20860
- log9.info(`Registering workspace ${ws.id} (${ws.name ?? "unnamed"}) with ${runtimes.length} runtime(s)...`);
20974
+ log10.info(`Registering workspace ${ws.id} (${ws.name ?? "unnamed"}) with ${runtimes.length} runtime(s)...`);
20861
20975
  let resp;
20862
20976
  try {
20863
20977
  resp = await client.register(ws.token, {
@@ -20870,13 +20984,13 @@ async function startDaemon(profile, serverUrl) {
20870
20984
  });
20871
20985
  } catch (e) {
20872
20986
  if (e instanceof Error && e.message.startsWith("HTTP 401")) {
20873
- log9.warn(`Workspace ${ws.id} token invalid — skipping (run '${cmdPrefix()} register --token <token>' to fix)`);
20987
+ log10.warn(`Workspace ${ws.id} token invalid — skipping (run '${cmdPrefix()} register --token <token>' to fix)`);
20874
20988
  } else {
20875
- log9.error(`Failed to register workspace ${ws.id}, skipping`, e);
20989
+ log10.error(`Failed to register workspace ${ws.id}, skipping`, e);
20876
20990
  }
20877
20991
  continue;
20878
20992
  }
20879
- log9.info(`Workspace ${ws.id} registered — ${resp.runtimes.length} runtime(s)`);
20993
+ log10.info(`Workspace ${ws.id} registered — ${resp.runtimes.length} runtime(s)`);
20880
20994
  const runtimeIds = resp.runtimes.map((r) => r.id);
20881
20995
  workspaceStates.push({ workspaceId: ws.id, token: ws.token, runtimeIds });
20882
20996
  for (let i = 0;i < runtimeIds.length; i++) {
@@ -20888,13 +21002,13 @@ async function startDaemon(profile, serverUrl) {
20888
21002
  }
20889
21003
  }
20890
21004
  if (workspaceStates.length === 0) {
20891
- log9.error("No workspaces registered successfully.");
21005
+ log10.error("No workspaces registered successfully.");
20892
21006
  process.exit(1);
20893
21007
  return;
20894
21008
  }
20895
21009
  const allRuntimeIds = workspaceStates.flatMap((ws) => ws.runtimeIds);
20896
21010
  health.setRuntimeCount(allRuntimeIds.length);
20897
- log9.info(`Daemon started — ${allRuntimeIds.length} runtime(s) across ${workspaceStates.length} workspace(s)`);
21011
+ log10.info(`Daemon started — ${allRuntimeIds.length} runtime(s) across ${workspaceStates.length} workspace(s)`);
20898
21012
  const activeTasks = new Set;
20899
21013
  const knownAgentIds = new Set(workspaces.flatMap((ws) => ws.agent_ids ?? []));
20900
21014
  function syncAgentId(agentId, workspaceId) {
@@ -20929,7 +21043,7 @@ async function startDaemon(profile, serverUrl) {
20929
21043
  cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== workspaceId);
20930
21044
  saveCLIConfigForProfile(profile, cfg);
20931
21045
  } catch {}
20932
- log9.info(`Workspace ${workspaceId} deleted server-side — removed from config`);
21046
+ log10.info(`Workspace ${workspaceId} deleted server-side — removed from config`);
20933
21047
  }
20934
21048
  const pollCycle = async () => {
20935
21049
  let remaining = config2.maxConcurrentTasks - activeTasks.size;
@@ -20955,7 +21069,7 @@ async function startDaemon(profile, serverUrl) {
20955
21069
  handleCliUpdate(pending_update.version, () => requestRestart(), profile);
20956
21070
  }
20957
21071
  if (pending_rescan) {
20958
- log9.info("Rescan requested — restarting daemon to re-detect runtimes");
21072
+ log10.info("Rescan requested — restarting daemon to re-detect runtimes");
20959
21073
  for (const id of evictedIds) {
20960
21074
  evictWorkspace(id);
20961
21075
  }
@@ -20968,13 +21082,13 @@ async function startDaemon(profile, serverUrl) {
20968
21082
  activeTasks.add(task.id);
20969
21083
  remaining--;
20970
21084
  handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks).catch((e) => {
20971
- log9.error("Task error", e);
21085
+ log10.error("Task error", e);
20972
21086
  activeTasks.delete(task.id);
20973
21087
  });
20974
21088
  }
20975
21089
  if (file_requests) {
20976
21090
  for (const req of file_requests) {
20977
- handleFileRequest(client, config2, ws.workspaceId, req, ws.token).catch((e) => log9.debug("File request error", e));
21091
+ handleFileRequest(client, config2, ws.workspaceId, req, ws.token).catch((e) => log10.debug("File request error", e));
20978
21092
  }
20979
21093
  }
20980
21094
  if (meetings) {
@@ -20997,9 +21111,9 @@ async function startDaemon(profile, serverUrl) {
20997
21111
  }
20998
21112
  } catch (e) {
20999
21113
  if (e instanceof Error && e.message.startsWith("HTTP 401")) {
21000
- log9.warn(`Workspace ${ws.workspaceId} poll returned 401 — will retry next cycle`);
21114
+ log10.warn(`Workspace ${ws.workspaceId} poll returned 401 — will retry next cycle`);
21001
21115
  } else {
21002
- log9.debug("Poll error", e);
21116
+ log10.debug("Poll error", e);
21003
21117
  }
21004
21118
  }
21005
21119
  }
@@ -21007,7 +21121,7 @@ async function startDaemon(profile, serverUrl) {
21007
21121
  evictWorkspace(id);
21008
21122
  }
21009
21123
  if (workspaceStates.length === 0) {
21010
- log9.info("All workspaces evicted — shutting down");
21124
+ log10.info("All workspaces evicted — shutting down");
21011
21125
  shutdown();
21012
21126
  }
21013
21127
  };
@@ -21015,7 +21129,7 @@ async function startDaemon(profile, serverUrl) {
21015
21129
  const heartbeatPing = () => {
21016
21130
  for (const ws of workspaceStates) {
21017
21131
  client.heartbeat(ws.token, config2.daemonId).catch((e) => {
21018
- log9.debug("heartbeat failed", { workspaceId: ws.workspaceId, err: String(e) });
21132
+ log10.debug("heartbeat failed", { workspaceId: ws.workspaceId, err: String(e) });
21019
21133
  });
21020
21134
  }
21021
21135
  };
@@ -21041,7 +21155,7 @@ async function startDaemon(profile, serverUrl) {
21041
21155
  syncAgentId(task.agentId, ws.workspaceId);
21042
21156
  activeTasks.add(task.id);
21043
21157
  handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks).catch((e) => {
21044
- log9.error("WS task error", e);
21158
+ log10.error("WS task error", e);
21045
21159
  activeTasks.delete(task.id);
21046
21160
  });
21047
21161
  }
@@ -21050,7 +21164,7 @@ async function startDaemon(profile, serverUrl) {
21050
21164
  const ws = wsMap.get(msg.workspaceId);
21051
21165
  if (ws) {
21052
21166
  for (const req of msg.requests) {
21053
- handleFileRequest(client, config2, ws.workspaceId, req, ws.token).catch((e) => log9.debug("WS file request error", e));
21167
+ handleFileRequest(client, config2, ws.workspaceId, req, ws.token).catch((e) => log10.debug("WS file request error", e));
21054
21168
  }
21055
21169
  }
21056
21170
  break;
@@ -21085,7 +21199,7 @@ async function startDaemon(profile, serverUrl) {
21085
21199
  }
21086
21200
  break;
21087
21201
  case "daemon.rescan":
21088
- log9.info("WS rescan requested — restarting daemon");
21202
+ log10.info("WS rescan requested — restarting daemon");
21089
21203
  requestRestart();
21090
21204
  break;
21091
21205
  case "daemon.kill": {
@@ -21113,7 +21227,7 @@ async function startDaemon(profile, serverUrl) {
21113
21227
  });
21114
21228
  activeTasks.add(killTask.id);
21115
21229
  handleTask(client, config2, runtimeIndex, killTask, ws.token, activeTasks).catch((e) => {
21116
- log9.error("WS kill task error", e);
21230
+ log10.error("WS kill task error", e);
21117
21231
  activeTasks.delete(killTask.id);
21118
21232
  });
21119
21233
  }
@@ -21127,11 +21241,11 @@ async function startDaemon(profile, serverUrl) {
21127
21241
  machineToken: firstToken,
21128
21242
  onMessage: handleWsPush,
21129
21243
  onConnected: () => {
21130
- log9.info("WS connected — switching to low-frequency poll");
21244
+ log10.info("WS connected — switching to low-frequency poll");
21131
21245
  updatePollInterval(config2.wsPollInterval);
21132
21246
  },
21133
21247
  onDisconnected: () => {
21134
- log9.info("WS disconnected — reverting to high-frequency poll");
21248
+ log10.info("WS disconnected — reverting to high-frequency poll");
21135
21249
  updatePollInterval(config2.pollInterval);
21136
21250
  }
21137
21251
  }) : null;
@@ -21139,13 +21253,13 @@ async function startDaemon(profile, serverUrl) {
21139
21253
  const sweepTick = async () => {
21140
21254
  for (const ws of workspaceStates) {
21141
21255
  client.sweep(ws.token, config2.daemonId).catch((e) => {
21142
- log9.debug("sweep ping failed", { workspaceId: ws.workspaceId, err: String(e) });
21256
+ log10.debug("sweep ping failed", { workspaceId: ws.workspaceId, err: String(e) });
21143
21257
  });
21144
21258
  }
21145
21259
  try {
21146
21260
  await reconcilePendingCompletions(config2.workspacesRoot);
21147
21261
  } catch (e) {
21148
- log9.debug("reconciliation error", e);
21262
+ log10.debug("reconciliation error", e);
21149
21263
  }
21150
21264
  };
21151
21265
  const sweepTimer = setInterval(sweepTick, config2.sweepInterval);
@@ -21169,7 +21283,7 @@ async function startDaemon(profile, serverUrl) {
21169
21283
  if (shuttingDown)
21170
21284
  return;
21171
21285
  shuttingDown = true;
21172
- log9.info(restartRequested ? "Restarting..." : "Shutting down...");
21286
+ log10.info(restartRequested ? "Restarting..." : "Shutting down...");
21173
21287
  clearInterval(pollTimer);
21174
21288
  clearInterval(heartbeatTimer);
21175
21289
  clearInterval(sweepTimer);
@@ -21197,7 +21311,7 @@ async function startDaemon(profile, serverUrl) {
21197
21311
  mkdirSync8(dirname3(logPath), { recursive: true, mode: 448 });
21198
21312
  logFd = openSync(logPath, "a", 384);
21199
21313
  } catch (e) {
21200
- log9.error(`Failed to open daemon log file ${logPath}`, e);
21314
+ log10.error(`Failed to open daemon log file ${logPath}`, e);
21201
21315
  }
21202
21316
  const child = spawn6(process.execPath, args, {
21203
21317
  detached: true,
@@ -21207,7 +21321,7 @@ async function startDaemon(profile, serverUrl) {
21207
21321
  child.unref();
21208
21322
  if (logFd != null)
21209
21323
  closeSync(logFd);
21210
- log9.info(`Spawned new daemon (pid=${child.pid}), logs: ${logPath}`);
21324
+ log10.info(`Spawned new daemon (pid=${child.pid}), logs: ${logPath}`);
21211
21325
  }
21212
21326
  clearTimeout(timeout);
21213
21327
  process.exit(0);
@@ -21218,7 +21332,7 @@ async function startDaemon(profile, serverUrl) {
21218
21332
  process.on("SIGHUP", async () => {
21219
21333
  if (shuttingDown)
21220
21334
  return;
21221
- log9.info("SIGHUP received — reloading config...");
21335
+ log10.info("SIGHUP received — reloading config...");
21222
21336
  try {
21223
21337
  const freshConfig = loadCLIConfigForProfile(profile);
21224
21338
  const freshWorkspaces = freshConfig.watched_workspaces || [];
@@ -21226,7 +21340,7 @@ async function startDaemon(profile, serverUrl) {
21226
21340
  const newWorkspaces = freshWorkspaces.filter((ws) => ws.token && !existingIds.has(ws.id));
21227
21341
  for (const ws of newWorkspaces) {
21228
21342
  const runtimes = providers.map((p) => ({ type: p.type, version: p.version }));
21229
- log9.info(`Registering new workspace ${ws.id} (${ws.name ?? "unnamed"})...`);
21343
+ log10.info(`Registering new workspace ${ws.id} (${ws.name ?? "unnamed"})...`);
21230
21344
  try {
21231
21345
  const resp = await client.register(ws.token, {
21232
21346
  workspace_id: ws.id,
@@ -21245,19 +21359,19 @@ async function startDaemon(profile, serverUrl) {
21245
21359
  provider: providers[i].type
21246
21360
  });
21247
21361
  }
21248
- log9.info(`Workspace ${ws.id} added — ${runtimeIds.length} runtime(s)`);
21362
+ log10.info(`Workspace ${ws.id} added — ${runtimeIds.length} runtime(s)`);
21249
21363
  } catch (e) {
21250
- log9.error(`Failed to register new workspace ${ws.id}`, e);
21364
+ log10.error(`Failed to register new workspace ${ws.id}`, e);
21251
21365
  }
21252
21366
  }
21253
21367
  if (newWorkspaces.length > 0) {
21254
21368
  health.setRuntimeCount(workspaceStates.reduce((sum, w) => sum + w.runtimeIds.length, 0));
21255
- log9.info(`Reload complete — now polling ${workspaceStates.length} workspace(s)`);
21369
+ log10.info(`Reload complete — now polling ${workspaceStates.length} workspace(s)`);
21256
21370
  } else {
21257
- log9.info("Reload complete — no new workspaces found");
21371
+ log10.info("Reload complete — no new workspaces found");
21258
21372
  }
21259
21373
  } catch (e) {
21260
- log9.error("Failed to reload config", e);
21374
+ log10.error("Failed to reload config", e);
21261
21375
  }
21262
21376
  });
21263
21377
  await pollCycle();
@@ -21272,7 +21386,7 @@ function spawnSessionRunner(input) {
21272
21386
  try {
21273
21387
  fd = openSync(logFilePath, "a");
21274
21388
  } catch (e) {
21275
- log9.error(`Failed to open log file ${logFilePath}`, e);
21389
+ log10.error(`Failed to open log file ${logFilePath}`, e);
21276
21390
  }
21277
21391
  const child = spawn6(process.execPath, [sessionRunnerPath, encoded], {
21278
21392
  detached: true,
@@ -21292,7 +21406,7 @@ function spawnMeetingRunner(input) {
21292
21406
  try {
21293
21407
  fd = openSync(logFilePath, "a");
21294
21408
  } catch (e) {
21295
- log9.error(`Failed to open meeting log file ${logFilePath}`, e);
21409
+ log10.error(`Failed to open meeting log file ${logFilePath}`, e);
21296
21410
  }
21297
21411
  const child = spawn6(process.execPath, [meetingRunnerPath, encoded], {
21298
21412
  detached: true,
@@ -21301,7 +21415,7 @@ function spawnMeetingRunner(input) {
21301
21415
  child.unref();
21302
21416
  if (fd != null)
21303
21417
  closeSync(fd);
21304
- log9.info(`Spawned meeting runner for ${input.meetingId} (pid=${child.pid})`);
21418
+ log10.info(`Spawned meeting runner for ${input.meetingId} (pid=${child.pid})`);
21305
21419
  return child;
21306
21420
  }
21307
21421
  async function handleFileRequest(client, config2, workspaceId, req, token) {
@@ -21327,8 +21441,31 @@ async function handleFileRequest(client, config2, workspaceId, req, token) {
21327
21441
  });
21328
21442
  }
21329
21443
  }
21444
+ async function killAndVerify(pid) {
21445
+ try {
21446
+ process.kill(pid, "SIGTERM");
21447
+ } catch (e) {
21448
+ if (e?.code === "ESRCH")
21449
+ return false;
21450
+ throw e;
21451
+ }
21452
+ const verifyMs = Math.max(Number(process.env.ALOOK_KILL_VERIFY_MS) || 3000, killGraceMs() + 500);
21453
+ const deadline = Date.now() + verifyMs;
21454
+ while (Date.now() < deadline) {
21455
+ if (!isAlive(pid))
21456
+ return true;
21457
+ await new Promise((r) => setTimeout(r, 100));
21458
+ }
21459
+ if (isAlive(pid)) {
21460
+ log10.warn(`session-runner pid=${pid} survived SIGTERM after ${verifyMs}ms — escalating to SIGKILL`);
21461
+ try {
21462
+ process.kill(pid, "SIGKILL");
21463
+ } catch {}
21464
+ }
21465
+ return true;
21466
+ }
21330
21467
  async function handleTask(client, config2, runtimeIndex, task, token, activeTasks) {
21331
- log9.info(`Task ${task.id} claimed agent=${task.agentId}`);
21468
+ log10.info(`Task ${task.id} claimed agent=${task.agentId}`);
21332
21469
  if (task.type === TASK_TYPES.KILL_TASK) {
21333
21470
  const targetTaskId = task.context?.target_task_id;
21334
21471
  if (!targetTaskId) {
@@ -21339,14 +21476,14 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21339
21476
  const agentBaseDir = join11(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
21340
21477
  const timelineDir = join11(agentBaseDir, ".context_timeline");
21341
21478
  const MAX_WAIT_MS = Number(process.env.ALOOK_KILL_TASK_MAX_WAIT_MS) || 15000;
21342
- const POLL_MS = Number(process.env.ALOOK_KILL_TASK_POLL_MS) || 200;
21479
+ const POLL_MS2 = Number(process.env.ALOOK_KILL_TASK_POLL_MS) || 200;
21343
21480
  const waitStart = Date.now();
21344
21481
  let pid = null;
21345
21482
  while (Date.now() - waitStart < MAX_WAIT_MS) {
21346
21483
  pid = findRunningPidByTaskId(timelineDir, targetTaskId);
21347
21484
  if (pid != null)
21348
21485
  break;
21349
- await new Promise((r) => setTimeout(r, POLL_MS));
21486
+ await new Promise((r) => setTimeout(r, POLL_MS2));
21350
21487
  }
21351
21488
  if (pid != null) {
21352
21489
  writeKillIntent(agentBaseDir, {
@@ -21355,20 +21492,20 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21355
21492
  expectedPid: pid
21356
21493
  });
21357
21494
  try {
21358
- process.kill(pid, "SIGTERM");
21359
- await client.failTask(token, task.id, "killed");
21360
- log9.info(`Kill task ${task.id}: sent SIGTERM to pid=${pid} for target=${targetTaskId}`);
21361
- } catch (e) {
21362
- if (e?.code === "ESRCH") {
21363
- await client.failTask(token, task.id, "target process already exited");
21364
- log9.info(`Kill task ${task.id}: target pid=${pid} already exited`);
21495
+ const delivered = await killAndVerify(pid);
21496
+ if (delivered) {
21497
+ await client.failTask(token, task.id, "killed");
21498
+ log10.info(`Kill task ${task.id}: terminated pid=${pid} for target=${targetTaskId}`);
21365
21499
  } else {
21366
- await client.failTask(token, task.id, `kill failed: ${e}`);
21500
+ await client.failTask(token, task.id, "target process already exited");
21501
+ log10.info(`Kill task ${task.id}: target pid=${pid} already exited`);
21367
21502
  }
21503
+ } catch (e) {
21504
+ await client.failTask(token, task.id, `kill failed: ${e}`);
21368
21505
  }
21369
21506
  } else {
21370
21507
  await client.failTask(token, task.id, "target not found in timeline");
21371
- log9.info(`Kill task ${task.id}: target ${targetTaskId} not found in timeline`);
21508
+ log10.info(`Kill task ${task.id}: target ${targetTaskId} not found in timeline`);
21372
21509
  }
21373
21510
  activeTasks.delete(task.id);
21374
21511
  return;
@@ -21393,12 +21530,12 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21393
21530
  const timelineDir = join11(agentBaseDir, ".context_timeline");
21394
21531
  const lockAcquired = acquireSteeringLock(agentBaseDir, task.contextKey);
21395
21532
  if (!lockAcquired) {
21396
- log9.warn(`Steering lock contention for context_key=${task.contextKey}, proceeding without steering`);
21533
+ log10.warn(`Steering lock contention for context_key=${task.contextKey}, proceeding without steering`);
21397
21534
  } else {
21398
21535
  try {
21399
21536
  const predecessor = findRunningEntryByContextKey(timelineDir, task.contextKey, provider);
21400
21537
  if (predecessor && predecessor.task_id !== task.id) {
21401
- log9.info(`Steering: task ${task.id} supersedes predecessor ${predecessor.task_id} (context_key=${task.contextKey})`);
21538
+ log10.info(`Steering: task ${task.id} supersedes predecessor ${predecessor.task_id} (context_key=${task.contextKey})`);
21402
21539
  if (predecessor.pid != null) {
21403
21540
  writeKillIntent(agentBaseDir, {
21404
21541
  reason: "superseded",
@@ -21407,33 +21544,29 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21407
21544
  successorTaskId: task.id
21408
21545
  });
21409
21546
  try {
21410
- process.kill(predecessor.pid, "SIGTERM");
21411
- log9.info(`Steering: sent SIGTERM to predecessor pid=${predecessor.pid}`);
21547
+ const delivered = await killAndVerify(predecessor.pid);
21548
+ log10.info(delivered ? `Steering: terminated predecessor pid=${predecessor.pid}` : `Steering: predecessor pid=${predecessor.pid} already exited`);
21412
21549
  } catch (e) {
21413
- if (e?.code === "ESRCH") {
21414
- log9.info(`Steering: predecessor pid=${predecessor.pid} already exited`);
21415
- } else {
21416
- log9.warn(`Steering: kill failed for pid=${predecessor.pid}`, e);
21417
- }
21550
+ log10.warn(`Steering: kill failed for pid=${predecessor.pid}`, e);
21418
21551
  }
21419
21552
  const waitStart = Date.now();
21420
21553
  const MAX_WAIT_MS = 15000;
21421
- const POLL_MS = 200;
21554
+ const POLL_MS2 = 200;
21422
21555
  while (Date.now() - waitStart < MAX_WAIT_MS) {
21423
21556
  const stillRunning = findRunningPidByTaskId(timelineDir, predecessor.task_id);
21424
21557
  if (stillRunning == null)
21425
21558
  break;
21426
- await new Promise((r) => setTimeout(r, POLL_MS));
21559
+ await new Promise((r) => setTimeout(r, POLL_MS2));
21427
21560
  }
21428
21561
  if (findRunningPidByTaskId(timelineDir, predecessor.task_id) != null) {
21429
- log9.warn(`Steering: predecessor pid=${predecessor.pid} did not exit within ${MAX_WAIT_MS}ms, proceeding anyway`);
21562
+ log10.warn(`Steering: predecessor pid=${predecessor.pid} did not exit within ${MAX_WAIT_MS}ms, proceeding anyway`);
21430
21563
  }
21431
21564
  }
21432
21565
  try {
21433
21566
  await client.supersedeTask(token, predecessor.task_id);
21434
- log9.info(`Steering: predecessor ${predecessor.task_id} marked superseded`);
21567
+ log10.info(`Steering: predecessor ${predecessor.task_id} marked superseded`);
21435
21568
  } catch (e) {
21436
- log9.warn(`Steering: failed to mark predecessor superseded server-side`, e);
21569
+ log10.warn(`Steering: failed to mark predecessor superseded server-side`, e);
21437
21570
  }
21438
21571
  }
21439
21572
  } finally {
@@ -21463,12 +21596,12 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21463
21596
  const agentBaseDir = join11(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
21464
21597
  const killIntent = readKillIntent(agentBaseDir, task.id);
21465
21598
  if (killIntent) {
21466
- log9.info(`Task ${task.id} exited due to kill intent — skipping failTask`);
21599
+ log10.info(`Task ${task.id} exited due to kill intent — skipping failTask`);
21467
21600
  clearKillIntent(agentBaseDir, task.id);
21468
21601
  return;
21469
21602
  }
21470
21603
  const msg = code === null ? `session-runner killed by signal (task ${task.id})` : `session-runner crashed (exit code ${code}, task ${task.id})`;
21471
- log9.warn(msg);
21604
+ log10.warn(msg);
21472
21605
  const timelineDir = join11(agentBaseDir, ".context_timeline");
21473
21606
  updateEntry(timelineDir, task.id, (entry) => {
21474
21607
  entry.pid = null;
@@ -21479,10 +21612,10 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21479
21612
  await client.failTask(token, task.id, msg);
21480
21613
  } catch (e) {
21481
21614
  if (isClientError2(e)) {
21482
- log9.info(`Backstop: task ${task.id} already in terminal state`);
21615
+ log10.info(`Backstop: task ${task.id} already in terminal state`);
21483
21616
  return;
21484
21617
  }
21485
- log9.error(`Backstop: failed to report crash for task ${task.id}`, e);
21618
+ log10.error(`Backstop: failed to report crash for task ${task.id}`, e);
21486
21619
  try {
21487
21620
  await writeMarkerFile(config2.workspacesRoot, {
21488
21621
  taskId: task.id,
@@ -21496,7 +21629,7 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21496
21629
  }
21497
21630
  }
21498
21631
  });
21499
- log9.info(`Task ${task.id} dispatched to session-runner (pid=${child.pid})`);
21632
+ log10.info(`Task ${task.id} dispatched to session-runner (pid=${child.pid})`);
21500
21633
  }
21501
21634
 
21502
21635
  // commands/daemon.ts
@@ -21806,7 +21939,7 @@ function gatherContextEnvVars() {
21806
21939
  }
21807
21940
 
21808
21941
  // commands/email.ts
21809
- var log10 = createLogger2({ module: "email" });
21942
+ var log11 = createLogger2({ module: "email" });
21810
21943
  var VALID_STATUSES = ["unread", "read", "archived", "sent"];
21811
21944
  var VALID_FOLDERS = ["inbox", "sent", "untrust"];
21812
21945
  var EMAIL_BASE = tempDir("alook-emails");
@@ -21893,7 +22026,7 @@ function emailCommand() {
21893
22026
  } catch (err) {
21894
22027
  const msg = err instanceof Error ? err.message : String(err);
21895
22028
  if (msg.includes("404")) {
21896
- log10.warn(`email body not available for ${email3.id}, skipping`);
22029
+ log11.warn(`email body not available for ${email3.id}, skipping`);
21897
22030
  continue;
21898
22031
  }
21899
22032
  throw err;
@@ -21987,7 +22120,7 @@ function emailCommand() {
21987
22120
  references = [parentEmail.references, parentEmail.message_id].filter(Boolean).join(" ").trim() || undefined;
21988
22121
  }
21989
22122
  } catch {
21990
- log10.warn(`could not fetch parent email ${opts.inReplyTo}, sending without threading`);
22123
+ log11.warn(`could not fetch parent email ${opts.inReplyTo}, sending without threading`);
21991
22124
  }
21992
22125
  }
21993
22126
  const ctx = gatherContextEnvVars();
@@ -22691,7 +22824,7 @@ function workspaceCommand() {
22691
22824
  process.exit(1);
22692
22825
  }
22693
22826
  if (runtimes.length === 0) {
22694
- console.error("Error: No daemon registered. Run 'npx @alook/cli daemon start' first.");
22827
+ console.error(`Error: No daemon registered. Run '${cmdPrefix()} daemon start' first.`);
22695
22828
  process.exit(1);
22696
22829
  }
22697
22830
  const OFFLINE_THRESHOLD_MS2 = 5 * 60 * 1000;
@@ -22702,7 +22835,7 @@ function workspaceCommand() {
22702
22835
  const lastSeen = new Date(r.machineLastSeenAt.includes("Z") ? r.machineLastSeenAt : r.machineLastSeenAt + "Z").getTime();
22703
22836
  return now - lastSeen < OFFLINE_THRESHOLD_MS2;
22704
22837
  });
22705
- const runtime = onlineRuntime || runtimes[0];
22838
+ let runtime = onlineRuntime || runtimes[0];
22706
22839
  let targetWorkspaceId = workspaceId;
22707
22840
  let targetClient = client;
22708
22841
  try {
@@ -22714,6 +22847,22 @@ function workspaceCommand() {
22714
22847
  targetWorkspaceId = newWs.id;
22715
22848
  targetClient = new APIClient(serverUrl, token, targetWorkspaceId);
22716
22849
  console.log(`Created workspace: ${newWs.name} (${newWs.id})`);
22850
+ try {
22851
+ const newRuntimes = await targetClient.getJSON("/api/runtimes");
22852
+ if (newRuntimes.length > 0) {
22853
+ const newOnlineRuntime = newRuntimes.find((r) => {
22854
+ if (!r.machineLastSeenAt)
22855
+ return false;
22856
+ const lastSeen = new Date(r.machineLastSeenAt.includes("Z") ? r.machineLastSeenAt : r.machineLastSeenAt + "Z").getTime();
22857
+ return now - lastSeen < OFFLINE_THRESHOLD_MS2;
22858
+ });
22859
+ runtime = newOnlineRuntime || newRuntimes[0];
22860
+ } else {
22861
+ await targetClient.postJSON("/api/runtimes", { id: runtime.id });
22862
+ }
22863
+ } catch (err) {
22864
+ console.warn(`Warning: could not refresh runtimes for new workspace: ${err instanceof Error ? err.message : err}`);
22865
+ }
22717
22866
  }
22718
22867
  } catch (err) {
22719
22868
  console.warn(`Warning: could not check existing agents: ${err instanceof Error ? err.message : err}`);
@@ -22735,9 +22884,11 @@ function workspaceCommand() {
22735
22884
  Workspace initialized: ${res.studio.name || res.workspace.name}`);
22736
22885
  console.log("Agents created:");
22737
22886
  for (const agent2 of res.agents) {
22738
- const email3 = agent2.email_handle ? `${agent2.email_handle}@alook.ai` : "no email";
22887
+ const email3 = agent2.email_handle ? toAlookAddress(agent2.email_handle) : "no email";
22739
22888
  console.log(` - ${agent2.name} (${email3})`);
22740
22889
  }
22890
+ console.log(`
22891
+ Open: ${serverUrl}/w/${res.workspace.slug}`);
22741
22892
  } catch (err) {
22742
22893
  console.error(`Error: failed to create workspace: ${err instanceof Error ? err.message : err}`);
22743
22894
  process.exit(1);