@alook/cli 0.0.15 → 0.0.17
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 +592 -55
- package/dist/session-runner.js +221 -31
- package/package.json +1 -1
package/dist/session-runner.js
CHANGED
|
@@ -14,14 +14,30 @@ var __export = (target, all) => {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
// daemon/session-runner.ts
|
|
17
|
-
import { mkdir, writeFile, rm } from "fs/promises";
|
|
17
|
+
import { mkdir, writeFile, rm, rename } from "fs/promises";
|
|
18
18
|
import path from "path";
|
|
19
19
|
|
|
20
20
|
// ../shared/src/constants.ts
|
|
21
|
+
var TaskStatus = {
|
|
22
|
+
QUEUED: "queued",
|
|
23
|
+
DISPATCHED: "dispatched",
|
|
24
|
+
RUNNING: "running",
|
|
25
|
+
COMPLETED: "completed",
|
|
26
|
+
FAILED: "failed",
|
|
27
|
+
CANCELLED: "cancelled",
|
|
28
|
+
SUPERSEDED: "superseded"
|
|
29
|
+
};
|
|
30
|
+
var TERMINAL_TASK_STATUSES = [
|
|
31
|
+
TaskStatus.COMPLETED,
|
|
32
|
+
TaskStatus.FAILED,
|
|
33
|
+
TaskStatus.CANCELLED,
|
|
34
|
+
TaskStatus.SUPERSEDED
|
|
35
|
+
];
|
|
21
36
|
var TASK_TYPES = {
|
|
22
37
|
USER_DM_MESSAGE: "user_dm_message",
|
|
23
38
|
EMAIL_NOTIFICATION: "email_notification",
|
|
24
|
-
CALENDAR_EVENT: "calendar_event"
|
|
39
|
+
CALENDAR_EVENT: "calendar_event",
|
|
40
|
+
KILL_TASK: "kill_task"
|
|
25
41
|
};
|
|
26
42
|
var POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS) || 3000;
|
|
27
43
|
var OFFLINE_THRESHOLD_MS = Number(process.env.OFFLINE_THRESHOLD_MS) || 9000;
|
|
@@ -13568,7 +13584,8 @@ var TaskStatusSchema = exports_external.enum([
|
|
|
13568
13584
|
"running",
|
|
13569
13585
|
"completed",
|
|
13570
13586
|
"failed",
|
|
13571
|
-
"cancelled"
|
|
13587
|
+
"cancelled",
|
|
13588
|
+
"superseded"
|
|
13572
13589
|
]);
|
|
13573
13590
|
var ClaimedTaskRowSchema = exports_external.object({
|
|
13574
13591
|
id: exports_external.string(),
|
|
@@ -13749,8 +13766,9 @@ var UpdateAgentRequestSchema = exports_external.object({
|
|
|
13749
13766
|
description: exports_external.string().optional(),
|
|
13750
13767
|
instructions: exports_external.string().optional(),
|
|
13751
13768
|
runtime_id: exports_external.string().min(1).optional(),
|
|
13752
|
-
runtime_config: RuntimeConfigSchema
|
|
13753
|
-
|
|
13769
|
+
runtime_config: RuntimeConfigSchema,
|
|
13770
|
+
visibility: exports_external.enum(["public", "private"]).optional()
|
|
13771
|
+
}).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined || v.visibility !== undefined, { message: "at least one field is required" });
|
|
13754
13772
|
var CreateConversationRequestSchema = exports_external.object({
|
|
13755
13773
|
agent_id: exports_external.string().min(1, "agent_id is required")
|
|
13756
13774
|
});
|
|
@@ -13842,6 +13860,16 @@ var CreateWorkspaceRequestSchema = exports_external.object({
|
|
|
13842
13860
|
name: exports_external.string().min(1, "name is required"),
|
|
13843
13861
|
slug: exports_external.string().min(1, "slug is required")
|
|
13844
13862
|
});
|
|
13863
|
+
var UpdateWorkspaceRequestSchema = exports_external.object({
|
|
13864
|
+
name: exports_external.string().min(1, "name is required").max(100).trim().optional(),
|
|
13865
|
+
slug: exports_external.string().min(1, "slug is required").max(100).trim().toLowerCase().optional()
|
|
13866
|
+
});
|
|
13867
|
+
var DeleteWorkspaceRequestSchema = exports_external.object({
|
|
13868
|
+
confirm_name: exports_external.string().min(1, "confirm_name is required")
|
|
13869
|
+
});
|
|
13870
|
+
var GrantAgentAccessRequestSchema = exports_external.object({
|
|
13871
|
+
user_id: exports_external.string().min(1, "user_id is required")
|
|
13872
|
+
});
|
|
13845
13873
|
// ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@4.20260418.1_@opentelemetry+api@1.9.1_bun-types@1.3.12_kysely@0.28.16/node_modules/drizzle-orm/entity.js
|
|
13846
13874
|
var entityKind = Symbol.for("drizzle:entityKind");
|
|
13847
13875
|
var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
|
|
@@ -15273,6 +15301,48 @@ var member = sqliteTable("member", {
|
|
|
15273
15301
|
globalInstruction: text("global_instruction").notNull().default(""),
|
|
15274
15302
|
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15275
15303
|
}, (t) => [unique("member_workspace_user").on(t.workspaceId, t.userId)]);
|
|
15304
|
+
var workspaceInvite = sqliteTable("workspace_invite", {
|
|
15305
|
+
id: text("id").primaryKey().$defaultFn(() => "inv_" + nanoid3()),
|
|
15306
|
+
workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
|
|
15307
|
+
token: text("token").unique().notNull().$defaultFn(() => nanoid3(32)),
|
|
15308
|
+
createdBy: text("created_by").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
15309
|
+
usedBy: text("used_by").references(() => user.id, { onDelete: "set null" }),
|
|
15310
|
+
usedAt: text("used_at"),
|
|
15311
|
+
expiresAt: text("expires_at").notNull(),
|
|
15312
|
+
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15313
|
+
}, (t) => [
|
|
15314
|
+
index("idx_workspace_invite_token").on(t.token),
|
|
15315
|
+
index("idx_workspace_invite_workspace").on(t.workspaceId)
|
|
15316
|
+
]);
|
|
15317
|
+
var agentAccess = sqliteTable("agent_access", {
|
|
15318
|
+
id: text("id").primaryKey().$defaultFn(() => nanoid3()),
|
|
15319
|
+
agentId: text("agent_id").notNull(),
|
|
15320
|
+
workspaceId: text("workspace_id").notNull(),
|
|
15321
|
+
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
15322
|
+
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15323
|
+
}, (t) => [
|
|
15324
|
+
unique("agent_access_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
|
|
15325
|
+
index("idx_agent_access_agent_ws").on(t.agentId, t.workspaceId),
|
|
15326
|
+
index("idx_agent_access_user").on(t.userId),
|
|
15327
|
+
foreignKey({
|
|
15328
|
+
columns: [t.agentId, t.workspaceId],
|
|
15329
|
+
foreignColumns: [agent.id, agent.workspaceId]
|
|
15330
|
+
}).onDelete("cascade")
|
|
15331
|
+
]);
|
|
15332
|
+
var agentPin = sqliteTable("agent_pin", {
|
|
15333
|
+
id: text("id").primaryKey().$defaultFn(() => nanoid3()),
|
|
15334
|
+
agentId: text("agent_id").notNull(),
|
|
15335
|
+
workspaceId: text("workspace_id").notNull(),
|
|
15336
|
+
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
15337
|
+
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15338
|
+
}, (t) => [
|
|
15339
|
+
unique("agent_pin_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
|
|
15340
|
+
index("idx_agent_pin_ws_user").on(t.workspaceId, t.userId),
|
|
15341
|
+
foreignKey({
|
|
15342
|
+
columns: [t.agentId, t.workspaceId],
|
|
15343
|
+
foreignColumns: [agent.id, agent.workspaceId]
|
|
15344
|
+
}).onDelete("cascade")
|
|
15345
|
+
]);
|
|
15276
15346
|
var machine = sqliteTable("machine", {
|
|
15277
15347
|
daemonId: text("daemon_id").notNull(),
|
|
15278
15348
|
workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
|
|
@@ -15410,6 +15480,7 @@ var emails = sqliteTable("emails", {
|
|
|
15410
15480
|
htmlBody: text("html_body").notNull().default(""),
|
|
15411
15481
|
attachments: text("attachments").notNull().default("[]"),
|
|
15412
15482
|
status: text("status").notNull().default("unread"),
|
|
15483
|
+
direction: text("direction").notNull().default("inbound"),
|
|
15413
15484
|
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15414
15485
|
}, (t) => [
|
|
15415
15486
|
foreignKey({
|
|
@@ -15499,6 +15570,7 @@ var machineToken = sqliteTable("machine_token", {
|
|
|
15499
15570
|
}, (t) => [index("idx_machine_token").on(t.token)]);
|
|
15500
15571
|
// ../shared/src/db/queries/task.ts
|
|
15501
15572
|
var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
|
|
15573
|
+
var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
|
|
15502
15574
|
// ../shared/src/utils/email.ts
|
|
15503
15575
|
var DOMAIN = `@${process.env.ALOOK_DOMAIN || "alook.ai"}`;
|
|
15504
15576
|
var RESERVED_HANDLES = new Set([
|
|
@@ -15589,6 +15661,9 @@ class DaemonClient {
|
|
|
15589
15661
|
error: error48
|
|
15590
15662
|
});
|
|
15591
15663
|
}
|
|
15664
|
+
supersedeTask(token, taskId) {
|
|
15665
|
+
return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
|
|
15666
|
+
}
|
|
15592
15667
|
async getArtifactMeta(token, artifactId, workspaceId) {
|
|
15593
15668
|
return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
|
|
15594
15669
|
}
|
|
@@ -15915,6 +15990,7 @@ class CodexBackend {
|
|
|
15915
15990
|
let notificationProtocol = "unknown";
|
|
15916
15991
|
let turnStarted = false;
|
|
15917
15992
|
let turnDoneTriggered = false;
|
|
15993
|
+
let turnCompletedSuccessfully = false;
|
|
15918
15994
|
let lastCompletedTurnId = "";
|
|
15919
15995
|
let turnError = "";
|
|
15920
15996
|
const pendingRequests = new Map;
|
|
@@ -16024,6 +16100,7 @@ class CodexBackend {
|
|
|
16024
16100
|
lastCompletedTurnId = turnId;
|
|
16025
16101
|
const status = turn?.status || params.status || "";
|
|
16026
16102
|
if (status === "completed" || status === "finished") {
|
|
16103
|
+
turnCompletedSuccessfully = true;
|
|
16027
16104
|
triggerTurnDone(false);
|
|
16028
16105
|
} else if (status === "cancelled" || status === "aborted" || status === "interrupted") {
|
|
16029
16106
|
triggerTurnDone(true);
|
|
@@ -16274,10 +16351,10 @@ class CodexBackend {
|
|
|
16274
16351
|
closeAllPending("process closed");
|
|
16275
16352
|
if (timedOut) {
|
|
16276
16353
|
resultStatus = "timeout";
|
|
16277
|
-
} else if (code !== 0 && resultStatus === "completed") {
|
|
16354
|
+
} else if (code !== 0 && resultStatus === "completed" && !turnCompletedSuccessfully) {
|
|
16278
16355
|
resultStatus = "failed";
|
|
16279
16356
|
}
|
|
16280
|
-
const stderr = stderrChunks.join("");
|
|
16357
|
+
const stderr = stderrChunks.join("").replace(/\x1b\[[0-9;]*m/g, "");
|
|
16281
16358
|
if (stderr && !lastError) {
|
|
16282
16359
|
lastError = stderr;
|
|
16283
16360
|
}
|
|
@@ -16581,6 +16658,7 @@ Your current context is only a fraction of the full timeline of what's your have
|
|
|
16581
16658
|
The full context timeline is inside './.context_timeline/YYYY-MM-DD.jsonl'.
|
|
16582
16659
|
Each line of a timeline JSONL is a JSON object with these fields:
|
|
16583
16660
|
- "task_id" — unique task identifier
|
|
16661
|
+
- "context_key" — thread identifier, the same context key meaning those tasks are in the same thread.
|
|
16584
16662
|
- "session_id" — agent session identifier (null until completion)
|
|
16585
16663
|
- "pid" — daemon process ID (present while running, null when done)
|
|
16586
16664
|
- "status" — "running", "completed", or "failed"
|
|
@@ -16589,18 +16667,21 @@ Each line of a timeline JSONL is a JSON object with these fields:
|
|
|
16589
16667
|
- "prompt" — what the user asked
|
|
16590
16668
|
- "agent_responses" — assistant text outputs during execution
|
|
16591
16669
|
- "errmsg" — error message (null unless status is "failed")
|
|
16670
|
+
- "detailed_log" — the running log for this task, including the actually task agent messages with tool-calls and failed logs.
|
|
16592
16671
|
those json are sorted by datetime in asc order.
|
|
16593
16672
|
|
|
16594
16673
|
## BE HUMAN
|
|
16595
16674
|
- When you recall the timelines, don't say "I find ... in my timeline jsonl". Just say "I can recall..."
|
|
16596
16675
|
- User don't know about the context timeline jsonl, this is your secret weapon.
|
|
16597
16676
|
- Act and response like you're human and actually have memory, not a file reading machine.
|
|
16677
|
+
- Don't report your agent id and workspace id when not asked, it will break the image that you're a human colleague.
|
|
16598
16678
|
|
|
16599
16679
|
## RULES
|
|
16600
16680
|
- Read @memory.md(if exists) before your action.
|
|
16601
16681
|
- When you start a new task, read the last ~10 lines of today's timeline to understand what has been asked and done recently.
|
|
16602
16682
|
- if you don't know the current datetime, obtain the current datetime first.
|
|
16603
16683
|
- 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).
|
|
16684
|
+
- Use grep tool to search in the context timeline jsonls if you have clean and focus keywords to recall.
|
|
16604
16685
|
- When access other local projects, make sure you read the CLAUDE.md/AGENTS.md file under the project root dir to understand the requirements.
|
|
16605
16686
|
`;
|
|
16606
16687
|
function buildInstructionContent(task) {
|
|
@@ -16634,7 +16715,7 @@ ${task.agent?.userEmail ? `Your owner's email address is '${task.agent.userEmail
|
|
|
16634
16715
|
|
|
16635
16716
|
### Emails
|
|
16636
16717
|
---
|
|
16637
|
-
Run 'npx @alook/cli email pull --agent_id ${task.agentId} --
|
|
16718
|
+
Run 'npx @alook/cli email pull --agent_id ${task.agentId} --status unread' to download unread emails to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/'.
|
|
16638
16719
|
Each email is saved to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/<emailId>/' with:
|
|
16639
16720
|
- 'metadata.json' — sender, recipient, subject, date, status, message_id, in_reply_to, references
|
|
16640
16721
|
- 'body.txt' — plain text body
|
|
@@ -16642,22 +16723,31 @@ Each email is saved to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/<e
|
|
|
16642
16723
|
- 'attachments/' — extracted attachment files (if any)
|
|
16643
16724
|
---
|
|
16644
16725
|
Before starting to process an email, mark it as read:
|
|
16645
|
-
- Run 'npx @alook/cli email set --agent_id ${task.agentId} --
|
|
16726
|
+
- Run 'npx @alook/cli email set --agent_id ${task.agentId} --email_id <EMAIL_ID> --status read'
|
|
16646
16727
|
---
|
|
16647
16728
|
|
|
16648
16729
|
#### Sending a new email
|
|
16649
16730
|
Write the HTML body to a file first, then send it. The body is forwarded as-is (HTML).
|
|
16650
|
-
- Run 'npx @alook/cli email send --agent_id ${task.agentId} --
|
|
16731
|
+
- Run 'npx @alook/cli email send --agent_id ${task.agentId} --to <ADDRESS> --subject "<SUBJECT>" --body-file <PATH_TO_HTML>'
|
|
16651
16732
|
- To send from a specific mailbox, add '--from <YOUR_EMAIL_ADDRESS>'. Without '--from', the default Alook address is used.
|
|
16652
16733
|
- Attach files with '--attachment <PATH>' — repeat the flag for multiple attachments. Each file is uploaded before sending.
|
|
16653
|
-
- Example: 'npx @alook/cli email send --agent_id ${task.agentId} --
|
|
16734
|
+
- Example: 'npx @alook/cli email send --agent_id ${task.agentId} --to foo@bar.com --subject "Weekly report" --body-file /tmp/body.html --from alice@company.com --attachment /tmp/report.pdf'
|
|
16654
16735
|
|
|
16655
16736
|
#### Replying to an email
|
|
16656
16737
|
To reply to an email, add '--in-reply-to <EMAIL_ID>' to the send command. This sets the correct email threading headers so the recipient's email client groups the reply into the same conversation thread.
|
|
16657
16738
|
- Use 'Re: <original subject>' as the subject.
|
|
16658
16739
|
- Quote the original email body in your reply (wrap it in a blockquote).
|
|
16659
16740
|
- The <EMAIL_ID> is the Alook email id from metadata.json (not the message_id header).
|
|
16660
|
-
- Example: 'npx @alook/cli email send --agent_id ${task.agentId} --
|
|
16741
|
+
- Example: 'npx @alook/cli email send --agent_id ${task.agentId} --to sender@example.com --subject "Re: Bug report" --body-file /tmp/reply.html --in-reply-to <EMAIL_ID>'
|
|
16742
|
+
Tips:
|
|
16743
|
+
- 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.
|
|
16744
|
+
---
|
|
16745
|
+
|
|
16746
|
+
#### Email Whitelist (Allowed Senders)
|
|
16747
|
+
Manage which email addresses are allowed to send you emails.
|
|
16748
|
+
- List: 'npx @alook/cli email whitelist list --agent_id ${task.agentId}' (add '--json' for machine-readable output)
|
|
16749
|
+
- Add: 'npx @alook/cli email whitelist add --agent_id ${task.agentId} <EMAIL_ADDRESS>'
|
|
16750
|
+
- Remove: 'npx @alook/cli email whitelist delete --agent_id ${task.agentId} <EMAIL_ADDRESS>'
|
|
16661
16751
|
---
|
|
16662
16752
|
`;
|
|
16663
16753
|
}
|
|
@@ -16667,6 +16757,8 @@ Upload files for your owner to review in the app.
|
|
|
16667
16757
|
- Your current conversation id is available via env var: $ALOOK_CONVERSATION_ID
|
|
16668
16758
|
- Run 'npx @alook/cli sync upload-artifact --agent_id ${task.agentId} --conversation_id $ALOOK_CONVERSATION_ID --file <PATH>'
|
|
16669
16759
|
- Use this after generating plans, reports, or any file the owner should review.
|
|
16760
|
+
- 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)
|
|
16761
|
+
- If you think user may need to know any file detail, use upload-artifact tool to send the file to user.
|
|
16670
16762
|
---
|
|
16671
16763
|
|
|
16672
16764
|
### Attachments
|
|
@@ -17058,7 +17150,7 @@ function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
|
|
|
17058
17150
|
}
|
|
17059
17151
|
entries.sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime());
|
|
17060
17152
|
for (const entry of entries) {
|
|
17061
|
-
if (entry.status
|
|
17153
|
+
if (entry.status !== "running" && entry.context_key === contextKey && entry.provider === provider && entry.session_id && new Date(entry.datetime) >= cutoff) {
|
|
17062
17154
|
return entry.session_id;
|
|
17063
17155
|
}
|
|
17064
17156
|
}
|
|
@@ -17082,9 +17174,37 @@ function prepare(config2, task) {
|
|
|
17082
17174
|
return { workDir, timelineDir, env };
|
|
17083
17175
|
}
|
|
17084
17176
|
|
|
17177
|
+
// daemon/execenv/steering.ts
|
|
17178
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync as unlinkSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
17179
|
+
import { join as join4 } from "path";
|
|
17180
|
+
var INTENT_DIR_NAME = ".kill_intents";
|
|
17181
|
+
var INTENT_STALE_MS = 10 * 60 * 1000;
|
|
17182
|
+
function intentFilePath(baseDir, taskId) {
|
|
17183
|
+
return join4(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
|
|
17184
|
+
}
|
|
17185
|
+
function readKillIntent(baseDir, taskId) {
|
|
17186
|
+
const filePath = intentFilePath(baseDir, taskId);
|
|
17187
|
+
try {
|
|
17188
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
17189
|
+
return JSON.parse(content);
|
|
17190
|
+
} catch {
|
|
17191
|
+
return null;
|
|
17192
|
+
}
|
|
17193
|
+
}
|
|
17194
|
+
function clearKillIntent(baseDir, taskId) {
|
|
17195
|
+
const filePath = intentFilePath(baseDir, taskId);
|
|
17196
|
+
try {
|
|
17197
|
+
unlinkSync2(filePath);
|
|
17198
|
+
} catch {}
|
|
17199
|
+
}
|
|
17200
|
+
|
|
17085
17201
|
// daemon/prompt.ts
|
|
17202
|
+
var EMAIL_NOTICE = "This task was triggered automatically by an incoming email. There is no human in this session." + " If you need to communicate with a human, you MUST send an email using the email sending tool." + " If you need more information or confirmation from the human, send them an email asking for it and then exit." + " Do not wait — when the human replies, a new task will be triggered automatically and you will be woken up with their response.";
|
|
17086
17203
|
function buildPrompt(task, attachments) {
|
|
17087
17204
|
const obj = { type: task.type, instruction: task.prompt };
|
|
17205
|
+
if (task.type === "email_notification") {
|
|
17206
|
+
obj.notice = EMAIL_NOTICE;
|
|
17207
|
+
}
|
|
17088
17208
|
if (attachments && attachments.length > 0) {
|
|
17089
17209
|
obj.attachments = attachments.map((a) => ({
|
|
17090
17210
|
path: a.path,
|
|
@@ -17097,6 +17217,26 @@ function buildPrompt(task, attachments) {
|
|
|
17097
17217
|
|
|
17098
17218
|
// daemon/session-runner.ts
|
|
17099
17219
|
var ATTACHMENTS_BASE = "/tmp/alook-attachments";
|
|
17220
|
+
async function writeMarkerFile(workspacesRoot, marker) {
|
|
17221
|
+
const dir = path.join(workspacesRoot, ".pending_completions");
|
|
17222
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
17223
|
+
const tmpPath = path.join(dir, `${marker.taskId}.tmp`);
|
|
17224
|
+
const finalPath = path.join(dir, `${marker.taskId}.json`);
|
|
17225
|
+
await writeFile(tmpPath, JSON.stringify(marker), { mode: 384 });
|
|
17226
|
+
await rename(tmpPath, finalPath);
|
|
17227
|
+
}
|
|
17228
|
+
async function reportToServer(fn, markerData, workspacesRoot) {
|
|
17229
|
+
try {
|
|
17230
|
+
await fn();
|
|
17231
|
+
} catch (e) {
|
|
17232
|
+
log.warn(`server report failed for task ${markerData.taskId}, writing marker: ${e}`);
|
|
17233
|
+
try {
|
|
17234
|
+
await writeMarkerFile(workspacesRoot, markerData);
|
|
17235
|
+
} catch (writeErr) {
|
|
17236
|
+
log.error(`marker write also failed for task ${markerData.taskId}: ${writeErr}`);
|
|
17237
|
+
}
|
|
17238
|
+
}
|
|
17239
|
+
}
|
|
17100
17240
|
function sanitizeFilename(name) {
|
|
17101
17241
|
return path.basename(name).replace(/[/\\]/g, "_").replace(/\.\./g, "_").slice(0, 255) || "file";
|
|
17102
17242
|
}
|
|
@@ -17124,7 +17264,7 @@ async function downloadAttachments(client, token, workspaceId, taskId, attachmen
|
|
|
17124
17264
|
return attachments;
|
|
17125
17265
|
}
|
|
17126
17266
|
async function runSession(input) {
|
|
17127
|
-
const { task, provider, cliPath, model, serverURL, token, workspacesRoot, agentTimeout } = input;
|
|
17267
|
+
const { task, provider, cliPath, model, serverURL, token, workspacesRoot, agentTimeout, messageInactivityTimeout } = input;
|
|
17128
17268
|
log.info(`starting (task=${task.id}, type=${task.type}, agent=${task.agentId}, provider=${provider}, model=${model || "default"})`);
|
|
17129
17269
|
const client = new DaemonClient(serverURL);
|
|
17130
17270
|
const backend = createBackend(provider, cliPath);
|
|
@@ -17178,6 +17318,7 @@ async function runSession(input) {
|
|
|
17178
17318
|
};
|
|
17179
17319
|
const flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
|
|
17180
17320
|
let killed = false;
|
|
17321
|
+
const agentBaseDir = path.dirname(timelineDir);
|
|
17181
17322
|
const onKill = async () => {
|
|
17182
17323
|
if (killed)
|
|
17183
17324
|
return;
|
|
@@ -17193,22 +17334,65 @@ async function runSession(input) {
|
|
|
17193
17334
|
await flushMessages();
|
|
17194
17335
|
} catch {}
|
|
17195
17336
|
await cleanupAttachments(task.id);
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17200
|
-
|
|
17201
|
-
|
|
17202
|
-
|
|
17203
|
-
|
|
17337
|
+
const intent = readKillIntent(agentBaseDir, task.id);
|
|
17338
|
+
clearKillIntent(agentBaseDir, task.id);
|
|
17339
|
+
if (intent?.reason === "superseded") {
|
|
17340
|
+
updateEntry(timelineDir, task.id, (entry) => {
|
|
17341
|
+
entry.pid = null;
|
|
17342
|
+
entry.status = "superseded";
|
|
17343
|
+
entry.successor_task_id = intent.successorTaskId ?? null;
|
|
17344
|
+
entry.supersede_reason = "superseded by newer task";
|
|
17345
|
+
});
|
|
17346
|
+
try {
|
|
17347
|
+
await client.supersedeTask(token, task.id);
|
|
17348
|
+
} catch {}
|
|
17349
|
+
} else if (intent?.reason === "cancelled") {
|
|
17350
|
+
updateEntry(timelineDir, task.id, (entry) => {
|
|
17351
|
+
entry.pid = null;
|
|
17352
|
+
entry.status = "cancelled";
|
|
17353
|
+
entry.errmsg = "cancelled by user";
|
|
17354
|
+
});
|
|
17355
|
+
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);
|
|
17356
|
+
} else {
|
|
17357
|
+
updateEntry(timelineDir, task.id, (entry) => {
|
|
17358
|
+
entry.pid = null;
|
|
17359
|
+
entry.status = "killed";
|
|
17360
|
+
entry.errmsg = "killed by signal";
|
|
17361
|
+
});
|
|
17362
|
+
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);
|
|
17363
|
+
}
|
|
17204
17364
|
process.exit(1);
|
|
17205
17365
|
};
|
|
17206
17366
|
process.on("SIGTERM", onKill);
|
|
17207
17367
|
process.on("SIGINT", onKill);
|
|
17368
|
+
const INACTIVITY_TIMEOUT_MS = messageInactivityTimeout ?? 5 * 60 * 1000;
|
|
17369
|
+
let inactivityTimedOut = false;
|
|
17208
17370
|
try {
|
|
17209
|
-
|
|
17210
|
-
|
|
17371
|
+
const iter = session2.messages[Symbol.asyncIterator]();
|
|
17372
|
+
while (!killed) {
|
|
17373
|
+
const next = iter.next();
|
|
17374
|
+
const raceResult = await (INACTIVITY_TIMEOUT_MS > 0 ? Promise.race([
|
|
17375
|
+
next,
|
|
17376
|
+
new Promise((resolve) => {
|
|
17377
|
+
const timer = setTimeout(() => resolve("timeout"), INACTIVITY_TIMEOUT_MS);
|
|
17378
|
+
next.then(() => clearTimeout(timer), () => clearTimeout(timer));
|
|
17379
|
+
})
|
|
17380
|
+
]) : next);
|
|
17381
|
+
if (raceResult === "timeout") {
|
|
17382
|
+
inactivityTimedOut = true;
|
|
17383
|
+
log.warn(`message inactivity timeout (${INACTIVITY_TIMEOUT_MS / 1000}s) — killing agent`);
|
|
17384
|
+
if (session2.pid) {
|
|
17385
|
+
try {
|
|
17386
|
+
process.kill(session2.pid, "SIGTERM");
|
|
17387
|
+
} catch {}
|
|
17388
|
+
}
|
|
17389
|
+
iter.return?.(undefined);
|
|
17390
|
+
break;
|
|
17391
|
+
}
|
|
17392
|
+
const iterResult = raceResult;
|
|
17393
|
+
if (iterResult.done)
|
|
17211
17394
|
break;
|
|
17395
|
+
const msg = iterResult.value;
|
|
17212
17396
|
seq++;
|
|
17213
17397
|
pendingMessages.push({
|
|
17214
17398
|
seq,
|
|
@@ -17247,6 +17431,10 @@ async function runSession(input) {
|
|
|
17247
17431
|
process.removeListener("SIGINT", onKill);
|
|
17248
17432
|
if (killed)
|
|
17249
17433
|
return;
|
|
17434
|
+
if (inactivityTimedOut) {
|
|
17435
|
+
result.status = "failed";
|
|
17436
|
+
result.error = `message inactivity timeout (no messages for ${INACTIVITY_TIMEOUT_MS / 1000}s)`;
|
|
17437
|
+
}
|
|
17250
17438
|
await cleanupAttachments(task.id);
|
|
17251
17439
|
if (result.status === "completed") {
|
|
17252
17440
|
updateEntry(timelineDir, task.id, (entry) => {
|
|
@@ -17258,7 +17446,7 @@ async function runSession(input) {
|
|
|
17258
17446
|
updateEntry(timelineDir, task.id, (entry) => {
|
|
17259
17447
|
entry.pid = null;
|
|
17260
17448
|
entry.status = "failed";
|
|
17261
|
-
entry.errmsg = result.error || "
|
|
17449
|
+
entry.errmsg = result.error || "agent exited unexpectedly";
|
|
17262
17450
|
});
|
|
17263
17451
|
}
|
|
17264
17452
|
if (result.status === "completed") {
|
|
@@ -17267,11 +17455,12 @@ async function runSession(input) {
|
|
|
17267
17455
|
};
|
|
17268
17456
|
if (result.sessionId)
|
|
17269
17457
|
body.session_id = result.sessionId;
|
|
17270
|
-
await client.completeTask(token, task.id, body);
|
|
17458
|
+
await reportToServer(() => client.completeTask(token, task.id, body), { taskId: task.id, type: "complete", payload: body, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
|
|
17271
17459
|
const dur = (result.durationMs / 1000).toFixed(1);
|
|
17272
17460
|
log.info(`completed (duration=${dur}s, messages=${seq}, tools=${toolCount})`);
|
|
17273
17461
|
} else {
|
|
17274
|
-
|
|
17462
|
+
const errorMsg = result.error || "agent exited unexpectedly";
|
|
17463
|
+
await reportToServer(() => client.failTask(token, task.id, errorMsg), { taskId: task.id, type: "fail", payload: { error: errorMsg }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
|
|
17275
17464
|
const dur = (result.durationMs / 1000).toFixed(1);
|
|
17276
17465
|
log.info(`failed (duration=${dur}s, messages=${seq}, tools=${toolCount}) — ${result.error}`);
|
|
17277
17466
|
}
|
|
@@ -17296,9 +17485,8 @@ async function main() {
|
|
|
17296
17485
|
} catch (e) {
|
|
17297
17486
|
log.error(`session-runner: unhandled error for task ${input.task.id}`, e);
|
|
17298
17487
|
await cleanupAttachments(input.task.id);
|
|
17299
|
-
|
|
17300
|
-
|
|
17301
|
-
} catch {}
|
|
17488
|
+
const errorMsg = `session-runner crash: ${e}`;
|
|
17489
|
+
await reportToServer(() => client.failTask(input.token, input.task.id, errorMsg), { taskId: input.task.id, type: "fail", payload: { error: errorMsg }, token: input.token, serverURL: input.serverURL, createdAt: new Date().toISOString() }, input.workspacesRoot);
|
|
17302
17490
|
process.exit(1);
|
|
17303
17491
|
}
|
|
17304
17492
|
}
|
|
@@ -17307,5 +17495,7 @@ if (isDirectExecution) {
|
|
|
17307
17495
|
main();
|
|
17308
17496
|
}
|
|
17309
17497
|
export {
|
|
17310
|
-
|
|
17498
|
+
writeMarkerFile,
|
|
17499
|
+
runSession,
|
|
17500
|
+
reportToServer
|
|
17311
17501
|
};
|