@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.
@@ -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
- }).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined, { message: "at least one field is required" });
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} --workspace ${task.workspaceId} --status unread' to download unread emails to '/tmp/alook-emails/${task.workspaceId}/${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} --workspace ${task.workspaceId} --email_id <EMAIL_ID> --status read'
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} --workspace ${task.workspaceId} --to <ADDRESS> --subject "<SUBJECT>" --body-file <PATH_TO_HTML>'
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} --workspace ${task.workspaceId} --to foo@bar.com --subject "Weekly report" --body-file /tmp/body.html --from alice@company.com --attachment /tmp/report.pdf'
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} --workspace ${task.workspaceId} --to sender@example.com --subject "Re: Bug report" --body-file /tmp/reply.html --in-reply-to <EMAIL_ID>'
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 === "completed" && entry.context_key === contextKey && entry.provider === provider && entry.session_id && new Date(entry.datetime) >= cutoff) {
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
- updateEntry(timelineDir, task.id, (entry) => {
17197
- entry.pid = null;
17198
- entry.status = "killed";
17199
- entry.errmsg = "killed by signal";
17200
- });
17201
- try {
17202
- await client.failTask(token, task.id, "killed by signal");
17203
- } catch {}
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
- for await (const msg of session2.messages) {
17210
- if (killed)
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 || "unknown 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
- await client.failTask(token, task.id, result.error || "unknown error");
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
- try {
17300
- await client.failTask(input.token, input.task.id, `session-runner crash: ${e}`);
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
- runSession
17498
+ writeMarkerFile,
17499
+ runSession,
17500
+ reportToServer
17311
17501
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Alook CLI — register and run always-on AI coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",