@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 +460 -309
- package/dist/meeting-runner.js +1 -2
- package/dist/session-runner.js +365 -308
- package/package.json +1 -1
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(
|
|
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 ||
|
|
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 ||
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
###
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
-
|
|
18826
|
-
|
|
18827
|
-
|
|
18828
|
-
|
|
18829
|
-
|
|
18830
|
-
|
|
18831
|
-
-
|
|
18832
|
-
|
|
18833
|
-
|
|
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
|
-
|
|
18838
|
-
The full context timeline
|
|
18839
|
-
|
|
18840
|
-
-
|
|
18841
|
-
-
|
|
18842
|
-
-
|
|
18843
|
-
-
|
|
18844
|
-
-
|
|
18845
|
-
-
|
|
18846
|
-
-
|
|
18847
|
-
-
|
|
18848
|
-
-
|
|
18849
|
-
-
|
|
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
|
-
-
|
|
18856
|
-
- Act and
|
|
18857
|
-
- Don't report your agent id and workspace id when not asked
|
|
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
|
|
18863
|
-
- Use grep tool to search
|
|
18864
|
-
-
|
|
18865
|
-
- When
|
|
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
|
|
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
|
-
|
|
18902
|
-
|
|
18903
|
-
|
|
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
|
-
|
|
18937
|
-
|
|
18938
|
-
|
|
18939
|
-
|
|
18940
|
-
|
|
18941
|
-
|
|
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
|
-
${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19468
|
+
log4.debug(`Timeline updateEntry failed for ${filename}`, err);
|
|
19328
19469
|
}
|
|
19329
19470
|
}
|
|
19330
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
19671
|
-
process.on("SIGINT",
|
|
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
|
-
|
|
19848
|
+
log6.info(`downloading ${attachmentIds.length} attachment(s)`);
|
|
19676
19849
|
try {
|
|
19677
19850
|
attachments = await downloadAttachments(client, token, task.workspaceId, task.id, attachmentIds);
|
|
19678
|
-
|
|
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
|
-
|
|
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",
|
|
19690
|
-
process.removeListener("SIGINT",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19709
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19793
|
-
if (session2.pid) {
|
|
19794
|
-
|
|
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
|
-
|
|
19926
|
+
log6.info(JSON.stringify({ role: "assistant", ...msg, output: msg.output.slice(0, 500) + `... (${msg.output.length} chars)` }));
|
|
19810
19927
|
} else {
|
|
19811
|
-
|
|
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
|
-
|
|
19837
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
20071
|
+
log7.info("connecting", { url: wsUrl });
|
|
19958
20072
|
try {
|
|
19959
20073
|
this.ws = new WebSocket(wsUrl);
|
|
19960
20074
|
} catch (err) {
|
|
19961
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20109
|
+
log7.debug("message parse error", { err: String(err) });
|
|
19996
20110
|
}
|
|
19997
20111
|
});
|
|
19998
20112
|
this.ws.addEventListener("error", () => {
|
|
19999
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20257
|
+
log8.info(`CLI updated to v${version3} — restarting`);
|
|
20144
20258
|
onSuccess();
|
|
20145
20259
|
} else {
|
|
20146
20260
|
retryCount++;
|
|
20147
|
-
|
|
20261
|
+
log8.error(`CLI update failed (attempt ${retryCount}/${MAX_RETRIES}): ${result.output}`);
|
|
20148
20262
|
}
|
|
20149
20263
|
} catch (e) {
|
|
20150
20264
|
retryCount++;
|
|
20151
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
20670
|
+
}).catch((e) => log9.debug("Global skill sync failed", e));
|
|
20557
20671
|
}
|
|
20558
20672
|
} catch (e) {
|
|
20559
|
-
|
|
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
|
-
|
|
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) =>
|
|
20694
|
+
}).catch((e) => log9.debug("Agent skill sync failed", e));
|
|
20581
20695
|
}
|
|
20582
20696
|
} catch (e) {
|
|
20583
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20900
|
+
log10.debug(`reconcile: delivery failed for ${name}, will retry next cycle`);
|
|
20787
20901
|
}
|
|
20788
20902
|
}
|
|
20789
20903
|
} catch (e) {
|
|
20790
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20928
|
+
log10.info(`Cleared update marker — now running v${config2.cliVersion}`);
|
|
20815
20929
|
} else {
|
|
20816
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20962
|
+
log10.error("No agent CLI tools found on PATH.");
|
|
20849
20963
|
process.exit(1);
|
|
20850
20964
|
return;
|
|
20851
20965
|
}
|
|
20852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20987
|
+
log10.warn(`Workspace ${ws.id} token invalid — skipping (run '${cmdPrefix()} register --token <token>' to fix)`);
|
|
20874
20988
|
} else {
|
|
20875
|
-
|
|
20989
|
+
log10.error(`Failed to register workspace ${ws.id}, skipping`, e);
|
|
20876
20990
|
}
|
|
20877
20991
|
continue;
|
|
20878
20992
|
}
|
|
20879
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
21114
|
+
log10.warn(`Workspace ${ws.workspaceId} poll returned 401 — will retry next cycle`);
|
|
21001
21115
|
} else {
|
|
21002
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21244
|
+
log10.info("WS connected — switching to low-frequency poll");
|
|
21131
21245
|
updatePollInterval(config2.wsPollInterval);
|
|
21132
21246
|
},
|
|
21133
21247
|
onDisconnected: () => {
|
|
21134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21362
|
+
log10.info(`Workspace ${ws.id} added — ${runtimeIds.length} runtime(s)`);
|
|
21249
21363
|
} catch (e) {
|
|
21250
|
-
|
|
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
|
-
|
|
21369
|
+
log10.info(`Reload complete — now polling ${workspaceStates.length} workspace(s)`);
|
|
21256
21370
|
} else {
|
|
21257
|
-
|
|
21371
|
+
log10.info("Reload complete — no new workspaces found");
|
|
21258
21372
|
}
|
|
21259
21373
|
} catch (e) {
|
|
21260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
21359
|
-
|
|
21360
|
-
|
|
21361
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21411
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
21559
|
+
await new Promise((r) => setTimeout(r, POLL_MS2));
|
|
21427
21560
|
}
|
|
21428
21561
|
if (findRunningPidByTaskId(timelineDir, predecessor.task_id) != null) {
|
|
21429
|
-
|
|
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
|
-
|
|
21567
|
+
log10.info(`Steering: predecessor ${predecessor.task_id} marked superseded`);
|
|
21435
21568
|
} catch (e) {
|
|
21436
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21615
|
+
log10.info(`Backstop: task ${task.id} already in terminal state`);
|
|
21483
21616
|
return;
|
|
21484
21617
|
}
|
|
21485
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 ?
|
|
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);
|