@alook/cli 0.0.117 → 0.0.119

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -62,6 +62,9 @@ class APIClient {
62
62
  patchJSON(path, body) {
63
63
  return this.request("PATCH", path, body);
64
64
  }
65
+ putJSON(path, body) {
66
+ return this.request("PUT", path, body);
67
+ }
65
68
  async postMultipart(path, form) {
66
69
  const headers = {
67
70
  Authorization: `Bearer ${this.token}`
@@ -14717,6 +14720,10 @@ var CreateAgentLinkRequestSchema = exports_external.object({
14717
14720
  var UpdateAgentLinkRequestSchema = exports_external.object({
14718
14721
  instruction: exports_external.string()
14719
14722
  });
14723
+ var UpsertAgentLinkRequestSchema = exports_external.object({
14724
+ target_agent_id: exports_external.string().min(1, "target_agent_id is required"),
14725
+ instruction: exports_external.string()
14726
+ });
14720
14727
  var AddWhitelistRequestSchema = exports_external.object({
14721
14728
  email: exports_external.string().email()
14722
14729
  });
@@ -14747,8 +14754,9 @@ var CreateConversationRequestSchema = exports_external.object({
14747
14754
  var CreateMessageRequestSchema = exports_external.object({
14748
14755
  content: exports_external.string().min(1, "content is required")
14749
14756
  });
14750
- var CreateBufferedMessageRequestSchema = exports_external.object({
14751
- content: exports_external.string().min(1, "content is required")
14757
+ var AgentDmRequestSchema = exports_external.object({
14758
+ content: exports_external.string().min(1, "content is required"),
14759
+ task_id: exports_external.string().min(1).optional()
14752
14760
  });
14753
14761
  var EmailAttachmentSchema = exports_external.object({
14754
14762
  key: exports_external.string().min(1),
@@ -16811,6 +16819,22 @@ var agentSkill = sqliteTable("agent_skill", {
16811
16819
  foreignColumns: [agent.id, agent.workspaceId]
16812
16820
  }).onDelete("cascade")
16813
16821
  ]);
16822
+ var inboxUnread = sqliteTable("inbox_unread", {
16823
+ id: text("id").primaryKey().$defaultFn(() => nanoid3()),
16824
+ conversationId: text("conversation_id").notNull().references(() => conversation.id, { onDelete: "cascade" }),
16825
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
16826
+ workspaceId: text("workspace_id").notNull(),
16827
+ agentId: text("agent_id").notNull(),
16828
+ taskId: text("task_id").notNull(),
16829
+ taskType: text("task_type").notNull(),
16830
+ taskStatus: text("task_status").notNull(),
16831
+ taskPrompt: text("task_prompt"),
16832
+ completedAt: text("completed_at").notNull(),
16833
+ latestMessageId: text("latest_message_id")
16834
+ }, (t) => [
16835
+ unique("inbox_unread_conv_user").on(t.conversationId, t.userId),
16836
+ index("idx_inbox_unread_user_ws").on(t.userId, t.workspaceId, t.taskType, t.completedAt)
16837
+ ]);
16814
16838
  // ../shared/src/db/queries/task.ts
16815
16839
  var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
16816
16840
  var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
@@ -18742,6 +18766,7 @@ class OpenCodeBackend {
18742
18766
  case "error": {
18743
18767
  const content = event.message || event.content || part?.error || "";
18744
18768
  lastError = content;
18769
+ resultStatus = "failed";
18745
18770
  pushMessage({ type: "error", content });
18746
18771
  turnDone();
18747
18772
  break;
@@ -19066,6 +19091,7 @@ The CLI auto-detects your identity from the environment. No need to pass \`--age
19066
19091
  ### Command quick reference
19067
19092
  | Capability | Command |
19068
19093
  |---|---|
19094
+ | Send a message to the user | \`${cmdPrefix()} sync send-dm\` |
19069
19095
  | Schedule / list / edit tasks | \`${cmdPrefix()} calendar set\` (also list, show, update, delete) |
19070
19096
  | Upload a file for your owner | \`${cmdPrefix()} sync upload-artifact\` |
19071
19097
  | Recruit a colleague agent | \`${cmdPrefix()} agent recruit\` |
@@ -19153,6 +19179,26 @@ Upload files for your owner to review in the app.
19153
19179
  - 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)
19154
19180
  - If you think user may need to know any file detail, use upload-artifact tool to send the file to user.
19155
19181
 
19182
+ ### Talking to the user
19183
+ You're texting a colleague, not filing a report. The only thing the user sees is what you send with \`${cmdPrefix()} sync send-dm\` — your task output, reasoning, and tool calls are all off-screen. If you finish without sending, they got silence.
19184
+
19185
+ \`${cmdPrefix()} sync send-dm\` sends a message to **the user** (your owner), not to a colleague agent. This is how you communicate with the human who gave you the task. Use email to talk to colleague agents.
19186
+
19187
+ Message at milestones, the way a person would: acknowledge when you pick something up, share a real step forward or a fork in the road, and deliver the result. A quick task is often one message; a long one is a few well-spaced check-ins. Trust your read of the moment — don't narrate every small step, and don't go dark for a long stretch on something they're waiting on.
19188
+
19189
+ Say what a colleague would say, not a transcript — the answer in your own voice. (Email- and calendar-triggered tasks have no one watching the chat; use email there.)
19190
+
19191
+ **A real person is waiting on the other end.** Send updates at every milestone of your work — not just the final result. For any task longer than a minute:
19192
+ 1. **Before you start**: tell them your plan ("I'll research X, then modify Y and Z")
19193
+ 2. **During work**: update when you find something important, change direction, or hit a blocker ("Found the issue — it's in the auth module, fixing now")
19194
+ 3. **When done**: deliver the clear result
19195
+
19196
+ Don't bundle everything into one giant message at the end. The user shouldn't have to sit in silence wondering what's happening. A one-line progress update costs nothing and keeps the human in the loop. But don't send repetitive or near-identical messages — each update should carry new information, not just restate what you already said.
19197
+
19198
+ **If the user sends you a message while you're working** — especially questions like "are you there?", "what's the status?", or unrelated requests — **respond to them immediately**. Don't finish your current task first and then reply. The user reached out because they need your attention NOW. Acknowledge them right away, then resume your work.
19199
+
19200
+ \`${cmdPrefix()} sync send-dm --message "…"\` for short messages. For longer or markdown-rich messages, write to a file first and use \`--message-file <path>\` — this preserves formatting and avoids shell escaping issues. The conversation is in $ALOOK_CONVERSATION_ID, so you usually need no flags. You can send several times in one task.
19201
+
19156
19202
  ### Attachments
19157
19203
  When your task includes attachments, their local paths are listed in the prompt JSON under "attachments".
19158
19204
  Use your Read tool to open them. Images and PDFs are read visually.
@@ -19172,6 +19218,15 @@ Recruit new colleague agents directly from the CLI. The server auto-generates a
19172
19218
  - Example: '${cmdPrefix()} agent recruit --instructions "You are a QA engineer..." --relationship "DELEGATE when: code is ready for review"'
19173
19219
  - Output: 'Recruited Felix (felix@alook.ai) — ag_xK9mPq2z'
19174
19220
  - The new agent shares your runtime, is automatically linked as your colleague, and receives a welcome task.
19221
+
19222
+ Set or update the relationship with an EXISTING colleague (create-or-replace):
19223
+ - Run '${cmdPrefix()} agent link --to <handleOrId> --relationship "<DELEGATION_CRITERIA>"'
19224
+ - '--to <handleOrId>' — target agent by email handle ('coder' or 'coder@alook.ai') or agent id ('ag_...')
19225
+ - '--relationship <text>' — the delegation criteria both of you see (replaces it if a link already exists)
19226
+ - '--relationship-file <path>' — alternative: read relationship from a file (mutually exclusive with --relationship)
19227
+ - '--json' — output the link object incl. a 'created' boolean
19228
+ - Example: '${cmdPrefix()} agent link --to coder --relationship "DELEGATE when implementation is needed"'
19229
+ - Output: 'Linked Felix <-> coder (created)' (or '(updated)' when an existing link was replaced).
19175
19230
  `;
19176
19231
  content += `
19177
19232
  ### Calendar
@@ -19510,13 +19565,24 @@ function findRunningPidByTaskId(timelineDir, taskId) {
19510
19565
  }
19511
19566
  return null;
19512
19567
  }
19513
- function findRunningEntryByContextKey(timelineDir, contextKey, provider) {
19568
+ function steerWarmupGraceMs() {
19569
+ const raw = process.env.ALOOK_STEER_WARMUP_GRACE_MS;
19570
+ const parsed = raw != null ? Number(raw) : NaN;
19571
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 30000;
19572
+ }
19573
+ function findSupersedablePredecessor(timelineDir, contextKey, provider, warmupGraceMs, now) {
19514
19574
  for (const filename of recentFilenames(7)) {
19515
19575
  const dayEntries = readJsonl(join7(timelineDir, filename));
19516
19576
  for (let i = dayEntries.length - 1;i >= 0; i--) {
19517
19577
  const entry = dayEntries[i];
19518
19578
  if (entry.status === "running" && entry.context_key === contextKey && entry.provider === provider) {
19519
- return entry;
19579
+ if (entry.agent_started === true) {
19580
+ return { entry, reason: "agent-started" };
19581
+ }
19582
+ if (now - Date.parse(entry.datetime) > warmupGraceMs) {
19583
+ return { entry, reason: "stale" };
19584
+ }
19585
+ return { pending: entry };
19520
19586
  }
19521
19587
  }
19522
19588
  }
@@ -19598,14 +19664,14 @@ function releaseSteeringLock(baseDir, contextKey) {
19598
19664
  }
19599
19665
 
19600
19666
  // daemon/prompt.ts
19601
- var DM_RESPONSE_NOTICE = "IMPORTANT: Only your final text response is visible to the user." + " Tool calls, intermediate reasoning, and mid-process outputs are NOT displayed." + " Put all key information, answers, and conclusions in your final response that is the only thing the user will read.";
19667
+ var DM_RESPONSE_NOTICE = "Reply with `alook sync send-dm` that's the only thing the user sees; your task output and reasoning are not shown." + " Talk to them at milestones like a colleague would, and don't end your turn without sending what they need." + " If this task will take more than 30 seconds, send a quick ack first so the user knows you're on it.";
19602
19668
  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.";
19603
19669
  var CALENDAR_NOTICE = "This task was triggered by a scheduled calendar event. 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, send 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.";
19604
19670
  var ISSUE_NOTICE = "This task was triggered by an assigned issue. The issue_id is provided in this message." + " Use `alook issue show --issue_id <issue_id>` to read full context." + " Use `alook issue update --issue_id <issue_id> --status <status>` to change status." + " Use `alook issue comment --issue_id <issue_id> --body <text>` to leave a comment." + " CRITICAL — You MUST manage the issue status correctly. This is NOT optional:" + " 1. Set status to 'in_progress' when you start working." + " 2. If you complete the work yourself: leave a summary comment, then set status to 'review' as your last action. 'review' means there is actual completed work (code, artifact, result) ready for the owner to look at." + " 3. If you delegated work to colleagues and are waiting for their response: KEEP status as 'in_progress' and exit. This is expected — you will be woken up when they reply. Set 'review' only after all delegated work is confirmed complete." + " 4. NEVER set 'review' unless there is concrete completed work for the owner to review. Sending a plan to a colleague is NOT completed work." + " NEVER exit without doing at least one of: updating the status, or leaving a comment explaining what you did and what you're waiting for.";
19605
19671
  function buildDmNotice(name, email3) {
19606
19672
  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.`;
19607
19673
  }
19608
- function buildPrompt(task, attachments) {
19674
+ function buildTaskObject(task, attachments) {
19609
19675
  const createdAt = new Date(task.createdAt);
19610
19676
  const receivedAt = Number.isNaN(createdAt.getTime()) ? localISOString() : localISOString(createdAt);
19611
19677
  const obj = {
@@ -19671,7 +19737,18 @@ function buildPrompt(task, attachments) {
19671
19737
  filename: a.filename
19672
19738
  }));
19673
19739
  }
19674
- return JSON.stringify(obj);
19740
+ return obj;
19741
+ }
19742
+ function buildPrompt(task, attachments) {
19743
+ return JSON.stringify(buildTaskObject(task, attachments));
19744
+ }
19745
+ function buildMergedPrompt(tasks, attachmentsMap) {
19746
+ const subTasks = tasks.map((t) => buildTaskObject(t, attachmentsMap.get(t.id))).sort((a, b) => String(a.received_at).localeCompare(String(b.received_at)));
19747
+ return JSON.stringify({
19748
+ type: "merge_tasks",
19749
+ notice: "These messages arrived simultaneously. Process each one completely.",
19750
+ tasks: subTasks
19751
+ });
19675
19752
  }
19676
19753
 
19677
19754
  // daemon/session-runner.ts
@@ -19776,8 +19853,8 @@ async function runSession(input) {
19776
19853
  await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, undefined, process.pid, provider, task.contextKey, input.logFilePath));
19777
19854
  const { workDir, env } = prepare({ workspacesRoot, token }, task);
19778
19855
  let killed = false;
19779
- let agentPid = undefined;
19780
- let flushTimer = undefined;
19856
+ let agentPid;
19857
+ let flushTimer;
19781
19858
  const pendingMessages = [];
19782
19859
  let seq = 0;
19783
19860
  let toolCount = 0;
@@ -19863,7 +19940,7 @@ async function runSession(input) {
19863
19940
  }
19864
19941
  if (killed)
19865
19942
  return;
19866
- const prompt = buildPrompt(task, attachments);
19943
+ const prompt = input.promptOverride ?? buildPrompt(task, attachments);
19867
19944
  const resumeSessionId = task.contextKey ? findResumableSessionByContextKey(timelineDir, task.contextKey, provider) ?? undefined : undefined;
19868
19945
  if (resumeSessionId) {
19869
19946
  log6.info(`resuming session ${resumeSessionId} (context_key: ${task.contextKey})`);
@@ -19888,6 +19965,8 @@ async function runSession(input) {
19888
19965
  log6.info(JSON.stringify({ role: "user", type: "text", content: prompt }));
19889
19966
  updateEntry(timelineDir, task.id, (entry) => {
19890
19967
  entry.session_id = earlySessionId || null;
19968
+ if (earlySessionId)
19969
+ entry.agent_started = true;
19891
19970
  });
19892
19971
  flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
19893
19972
  const INACTIVITY_TIMEOUT_MS = messageInactivityTimeout ?? 5 * 60 * 1000;
@@ -20588,6 +20667,16 @@ function writeCacheFile(filePath, hash2, skills) {
20588
20667
  const data = { hash: hash2, skills };
20589
20668
  writeFileSync7(filePath, JSON.stringify(data, null, 2), "utf-8");
20590
20669
  }
20670
+ function isClientError2(err) {
20671
+ if (err instanceof Error) {
20672
+ const match = err.message.match(/^HTTP (\d+):/);
20673
+ if (match) {
20674
+ const status = parseInt(match[1], 10);
20675
+ return status >= 400 && status < 500;
20676
+ }
20677
+ }
20678
+ return false;
20679
+ }
20591
20680
  var scanTimer = null;
20592
20681
  var scannerConfig = null;
20593
20682
  var clientRef = null;
@@ -20664,7 +20753,12 @@ function runScan() {
20664
20753
  }));
20665
20754
  Promise.all(syncPromises).then(() => {
20666
20755
  writeCacheFile(globalCachePath(daemonId, runtime), hash2, skills);
20667
- }).catch((e) => log9.debug("Global skill sync failed", e));
20756
+ }).catch((e) => {
20757
+ if (isClientError2(e)) {
20758
+ writeCacheFile(globalCachePath(daemonId, runtime), hash2, skills);
20759
+ }
20760
+ log9.debug("Global skill sync failed", e);
20761
+ });
20668
20762
  }
20669
20763
  } catch (e) {
20670
20764
  log9.debug(`Global scan error for ${runtime}`, e);
@@ -20688,7 +20782,12 @@ function runScan() {
20688
20782
  skills: skillItems
20689
20783
  }).then(() => {
20690
20784
  writeCacheFile(agentCachePath(target.agentId, target.runtime), hash2, skills);
20691
- }).catch((e) => log9.debug("Agent skill sync failed", e));
20785
+ }).catch((e) => {
20786
+ if (isClientError2(e)) {
20787
+ writeCacheFile(agentCachePath(target.agentId, target.runtime), hash2, skills);
20788
+ }
20789
+ log9.debug("Agent skill sync failed", e);
20790
+ });
20692
20791
  }
20693
20792
  } catch (e) {
20694
20793
  log9.debug(`Agent scan error for ${target.agentId}:${target.runtime}`, e);
@@ -20786,7 +20885,7 @@ function pruneSessionRunnerLogs() {
20786
20885
  } catch {}
20787
20886
  }
20788
20887
  }
20789
- function isClientError2(error51) {
20888
+ function isClientError3(error51) {
20790
20889
  if (!(error51 instanceof Error))
20791
20890
  return false;
20792
20891
  const match = error51.message.match(/^HTTP (\d+):/);
@@ -20889,7 +20988,7 @@ async function reconcilePendingCompletions(workspacesRoot) {
20889
20988
  log10.warn(`reconcile: delivered marker ${name} but failed to delete: ${delErr}`);
20890
20989
  }
20891
20990
  } catch (deliverErr) {
20892
- if (isClientError2(deliverErr)) {
20991
+ if (isClientError3(deliverErr)) {
20893
20992
  try {
20894
20993
  await unlink(filePath);
20895
20994
  } catch {}
@@ -21007,6 +21106,7 @@ async function startDaemon(profile, serverUrl) {
21007
21106
  health.setRuntimeCount(allRuntimeIds.length);
21008
21107
  log10.info(`Daemon started — ${allRuntimeIds.length} runtime(s) across ${workspaceStates.length} workspace(s)`);
21009
21108
  const activeTasks = new Set;
21109
+ const pendingSteer = new Map;
21010
21110
  const knownAgentIds = new Set(workspaces.flatMap((ws) => ws.agent_ids ?? []));
21011
21111
  function syncAgentId(agentId, workspaceId) {
21012
21112
  if (knownAgentIds.has(agentId))
@@ -21078,7 +21178,7 @@ async function startDaemon(profile, serverUrl) {
21078
21178
  syncAgentId(task.agentId, ws.workspaceId);
21079
21179
  activeTasks.add(task.id);
21080
21180
  remaining--;
21081
- handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks).catch((e) => {
21181
+ handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks, pendingSteer).catch((e) => {
21082
21182
  log10.error("Task error", e);
21083
21183
  activeTasks.delete(task.id);
21084
21184
  });
@@ -21151,7 +21251,7 @@ async function startDaemon(profile, serverUrl) {
21151
21251
  continue;
21152
21252
  syncAgentId(task.agentId, ws.workspaceId);
21153
21253
  activeTasks.add(task.id);
21154
- handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks).catch((e) => {
21254
+ handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks, pendingSteer).catch((e) => {
21155
21255
  log10.error("WS task error", e);
21156
21256
  activeTasks.delete(task.id);
21157
21257
  });
@@ -21223,7 +21323,7 @@ async function startDaemon(profile, serverUrl) {
21223
21323
  sender: null
21224
21324
  });
21225
21325
  activeTasks.add(killTask.id);
21226
- handleTask(client, config2, runtimeIndex, killTask, ws.token, activeTasks).catch((e) => {
21326
+ handleTask(client, config2, runtimeIndex, killTask, ws.token, activeTasks, pendingSteer).catch((e) => {
21227
21327
  log10.error("WS kill task error", e);
21228
21328
  activeTasks.delete(killTask.id);
21229
21329
  });
@@ -21461,7 +21561,7 @@ async function killAndVerify(pid) {
21461
21561
  }
21462
21562
  return true;
21463
21563
  }
21464
- async function handleTask(client, config2, runtimeIndex, task, token, activeTasks) {
21564
+ async function handleTask(client, config2, runtimeIndex, task, token, activeTasks, pendingSteer) {
21465
21565
  log10.info(`Task ${task.id} claimed agent=${task.agentId}`);
21466
21566
  if (task.type === TASK_TYPES.KILL_TASK) {
21467
21567
  const targetTaskId = task.context?.target_task_id;
@@ -21521,53 +21621,164 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21521
21621
  return;
21522
21622
  }
21523
21623
  const provider = runtimeData.provider;
21624
+ let promptOverride;
21524
21625
  if (task.contextKey) {
21525
21626
  const agentBaseDir = join11(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
21526
21627
  cleanupStaleIntents(agentBaseDir);
21527
21628
  const timelineDir = join11(agentBaseDir, ".context_timeline");
21528
- const lockAcquired = acquireSteeringLock(agentBaseDir, task.contextKey);
21629
+ const ctxKey = task.contextKey;
21630
+ let lockAcquired = acquireSteeringLock(agentBaseDir, ctxKey);
21529
21631
  if (!lockAcquired) {
21530
- log10.warn(`Steering lock contention for context_key=${task.contextKey}, proceeding without steering`);
21531
- } else {
21632
+ const MERGE_WAIT_MS = 5000;
21633
+ const MERGE_POLL_MS = 50;
21634
+ const mergeStart = Date.now();
21635
+ while (Date.now() - mergeStart < MERGE_WAIT_MS) {
21636
+ const existing = pendingSteer.get(ctxKey);
21637
+ if (existing) {
21638
+ const attachmentIds = task.context?.attachment_ids ?? [];
21639
+ let myAttachments = [];
21640
+ if (attachmentIds.length > 0) {
21641
+ try {
21642
+ myAttachments = await downloadAttachments(client, token, task.workspaceId, task.id, attachmentIds);
21643
+ } catch {}
21644
+ }
21645
+ existing.tasks.push(task);
21646
+ existing.attachments.set(task.id, myAttachments);
21647
+ log10.info(`Steering: ${task.id} merged into pending entry (lock contention) for context_key=${ctxKey} (${existing.tasks.length} tasks)`);
21648
+ existing.wake();
21649
+ try {
21650
+ await client.supersedeTask(token, task.id);
21651
+ } catch {}
21652
+ activeTasks.delete(task.id);
21653
+ return;
21654
+ }
21655
+ lockAcquired = acquireSteeringLock(agentBaseDir, ctxKey);
21656
+ if (lockAcquired)
21657
+ break;
21658
+ await new Promise((r) => setTimeout(r, MERGE_POLL_MS));
21659
+ }
21660
+ if (!lockAcquired) {
21661
+ log10.warn(`Steering lock contention for context_key=${ctxKey}, proceeding without steering`);
21662
+ }
21663
+ }
21664
+ if (lockAcquired) {
21532
21665
  try {
21533
- const predecessor = findRunningEntryByContextKey(timelineDir, task.contextKey, provider);
21534
- if (predecessor && predecessor.task_id !== task.id) {
21535
- log10.info(`Steering: task ${task.id} supersedes predecessor ${predecessor.task_id} (context_key=${task.contextKey})`);
21536
- if (predecessor.pid != null) {
21537
- writeKillIntent(agentBaseDir, {
21538
- reason: "superseded",
21539
- targetTaskId: predecessor.task_id,
21540
- expectedPid: predecessor.pid,
21541
- successorTaskId: task.id
21666
+ const result = findSupersedablePredecessor(timelineDir, ctxKey, provider, steerWarmupGraceMs(), Date.now());
21667
+ if (result) {
21668
+ const existing = pendingSteer.get(ctxKey);
21669
+ if (!existing) {
21670
+ const attachmentIds = task.context?.attachment_ids ?? [];
21671
+ let myAttachments = [];
21672
+ if (attachmentIds.length > 0) {
21673
+ try {
21674
+ myAttachments = await downloadAttachments(client, token, task.workspaceId, task.id, attachmentIds);
21675
+ } catch (e) {
21676
+ log10.warn(`Steering: failed to download attachments for ${task.id}`, e);
21677
+ }
21678
+ }
21679
+ let ownerWake;
21680
+ const ownerSignal = new Promise((resolve2) => {
21681
+ ownerWake = resolve2;
21542
21682
  });
21543
- try {
21544
- const delivered = await killAndVerify(predecessor.pid);
21545
- log10.info(delivered ? `Steering: terminated predecessor pid=${predecessor.pid}` : `Steering: predecessor pid=${predecessor.pid} already exited`);
21546
- } catch (e) {
21547
- log10.warn(`Steering: kill failed for pid=${predecessor.pid}`, e);
21683
+ const entry = {
21684
+ tasks: [task],
21685
+ attachments: new Map([[task.id, myAttachments]]),
21686
+ ownerTaskId: task.id,
21687
+ wake: ownerWake
21688
+ };
21689
+ pendingSteer.set(ctxKey, entry);
21690
+ let predecessor = "entry" in result ? result.entry : null;
21691
+ if (!predecessor) {
21692
+ log10.info(`Steering: predecessor ${result.pending.task_id} warming up; ${task.id} waiting`);
21693
+ const POLL_MS2 = 200;
21694
+ const MAX_WAIT_MS = steerWarmupGraceMs();
21695
+ const waitStart = Date.now();
21696
+ while (Date.now() - waitStart < MAX_WAIT_MS) {
21697
+ releaseSteeringLock(agentBaseDir, ctxKey);
21698
+ await Promise.race([new Promise((r2) => setTimeout(r2, POLL_MS2)), ownerSignal]);
21699
+ if (!acquireSteeringLock(agentBaseDir, ctxKey))
21700
+ continue;
21701
+ const r = findSupersedablePredecessor(timelineDir, ctxKey, provider, steerWarmupGraceMs(), Date.now());
21702
+ if (!r) {
21703
+ log10.info(`Steering: predecessor vanished; ${task.id} proceeding`);
21704
+ break;
21705
+ }
21706
+ if ("entry" in r) {
21707
+ predecessor = r.entry;
21708
+ break;
21709
+ }
21710
+ }
21711
+ if (!predecessor && Date.now() - waitStart >= MAX_WAIT_MS) {
21712
+ releaseSteeringLock(agentBaseDir, ctxKey);
21713
+ if (acquireSteeringLock(agentBaseDir, ctxKey)) {
21714
+ const finalCheck = findSupersedablePredecessor(timelineDir, ctxKey, provider, 0, Date.now());
21715
+ if (finalCheck && "entry" in finalCheck)
21716
+ predecessor = finalCheck.entry;
21717
+ }
21718
+ }
21719
+ }
21720
+ if (predecessor && predecessor.task_id !== task.id) {
21721
+ log10.info(`Steering: task ${task.id} supersedes predecessor ${predecessor.task_id} (context_key=${ctxKey})`);
21722
+ if (predecessor.pid != null) {
21723
+ writeKillIntent(agentBaseDir, { reason: "superseded", targetTaskId: predecessor.task_id, expectedPid: predecessor.pid, successorTaskId: task.id });
21724
+ try {
21725
+ const delivered = await killAndVerify(predecessor.pid);
21726
+ log10.info(delivered ? `Steering: terminated predecessor pid=${predecessor.pid}` : `Steering: predecessor pid=${predecessor.pid} already exited`);
21727
+ } catch (e) {
21728
+ log10.warn(`Steering: kill failed for pid=${predecessor.pid}`, e);
21729
+ }
21730
+ const killWaitStart = Date.now();
21731
+ while (Date.now() - killWaitStart < 15000) {
21732
+ if (findRunningPidByTaskId(timelineDir, predecessor.task_id) == null)
21733
+ break;
21734
+ await new Promise((r) => setTimeout(r, 200));
21735
+ }
21736
+ }
21737
+ try {
21738
+ await client.supersedeTask(token, predecessor.task_id);
21739
+ } catch {}
21548
21740
  }
21549
- const waitStart = Date.now();
21550
- const MAX_WAIT_MS = 15000;
21551
- const POLL_MS2 = 200;
21552
- while (Date.now() - waitStart < MAX_WAIT_MS) {
21553
- const stillRunning = findRunningPidByTaskId(timelineDir, predecessor.task_id);
21554
- if (stillRunning == null)
21555
- break;
21556
- await new Promise((r) => setTimeout(r, POLL_MS2));
21741
+ const finalEntry = pendingSteer.get(ctxKey);
21742
+ if (finalEntry && finalEntry.tasks.length > 1) {
21743
+ promptOverride = buildMergedPrompt(finalEntry.tasks, finalEntry.attachments);
21744
+ log10.info(`Steering: merged ${finalEntry.tasks.length} tasks for context_key=${ctxKey}`);
21745
+ } else if (finalEntry && finalEntry.tasks.length === 1) {
21746
+ const att = finalEntry.attachments.get(task.id);
21747
+ if (att && att.length > 0) {
21748
+ promptOverride = buildPrompt(task, att);
21749
+ }
21557
21750
  }
21558
- if (findRunningPidByTaskId(timelineDir, predecessor.task_id) != null) {
21559
- log10.warn(`Steering: predecessor pid=${predecessor.pid} did not exit within ${MAX_WAIT_MS}ms, proceeding anyway`);
21751
+ pendingSteer.delete(ctxKey);
21752
+ } else {
21753
+ const attachmentIds = task.context?.attachment_ids ?? [];
21754
+ let myAttachments = [];
21755
+ if (attachmentIds.length > 0) {
21756
+ try {
21757
+ myAttachments = await downloadAttachments(client, token, task.workspaceId, task.id, attachmentIds);
21758
+ } catch (e) {
21759
+ log10.warn(`Steering: failed to download attachments for ${task.id}`, e);
21760
+ }
21560
21761
  }
21561
- }
21562
- try {
21563
- await client.supersedeTask(token, predecessor.task_id);
21564
- log10.info(`Steering: predecessor ${predecessor.task_id} marked superseded`);
21565
- } catch (e) {
21566
- log10.warn(`Steering: failed to mark predecessor superseded server-side`, e);
21762
+ for (const prev of existing.tasks) {
21763
+ if (prev.id !== existing.ownerTaskId) {
21764
+ try {
21765
+ await client.supersedeTask(token, prev.id);
21766
+ } catch {}
21767
+ }
21768
+ }
21769
+ existing.tasks.push(task);
21770
+ existing.attachments.set(task.id, myAttachments);
21771
+ log10.info(`Steering: ${task.id} merged into pending entry for context_key=${ctxKey} (${existing.tasks.length} tasks)`);
21772
+ existing.wake();
21773
+ try {
21774
+ await client.supersedeTask(token, task.id);
21775
+ } catch {}
21776
+ activeTasks.delete(task.id);
21777
+ return;
21567
21778
  }
21568
21779
  }
21569
21780
  } finally {
21570
- releaseSteeringLock(agentBaseDir, task.contextKey);
21781
+ releaseSteeringLock(agentBaseDir, ctxKey);
21571
21782
  }
21572
21783
  }
21573
21784
  }
@@ -21584,7 +21795,8 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21584
21795
  token,
21585
21796
  workspacesRoot: config2.workspacesRoot,
21586
21797
  agentTimeout: config2.agentTimeout,
21587
- messageInactivityTimeout: config2.messageInactivityTimeout
21798
+ messageInactivityTimeout: config2.messageInactivityTimeout,
21799
+ ...promptOverride && { promptOverride }
21588
21800
  };
21589
21801
  const child = spawnSessionRunner(input);
21590
21802
  child.on("close", async (code) => {
@@ -21593,31 +21805,31 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21593
21805
  const agentBaseDir = join11(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
21594
21806
  const killIntent = readKillIntent(agentBaseDir, task.id);
21595
21807
  if (killIntent) {
21596
- log10.info(`Task ${task.id} exited due to kill intent — skipping failTask`);
21808
+ log10.info(`Task ${task.id} exited (${killIntent.reason}) expected, skipping failTask`);
21597
21809
  clearKillIntent(agentBaseDir, task.id);
21598
21810
  return;
21599
21811
  }
21600
- const msg = code === null ? `session-runner killed by signal (task ${task.id})` : `session-runner crashed (exit code ${code}, task ${task.id})`;
21601
- log10.warn(msg);
21602
- const timelineDir = join11(agentBaseDir, ".context_timeline");
21603
- updateEntry(timelineDir, task.id, (entry) => {
21604
- entry.pid = null;
21605
- entry.status = "failed";
21606
- entry.errmsg = msg;
21607
- });
21812
+ const errorMsg = code === null ? "killed by signal" : `session-runner exited with code ${code}`;
21608
21813
  try {
21609
- await client.failTask(token, task.id, msg);
21814
+ await client.failTask(token, task.id, errorMsg);
21815
+ log10.warn(`session-runner crashed (${errorMsg}, task ${task.id})`);
21816
+ const timelineDir = join11(agentBaseDir, ".context_timeline");
21817
+ updateEntry(timelineDir, task.id, (entry) => {
21818
+ entry.pid = null;
21819
+ entry.status = "failed";
21820
+ entry.errmsg = errorMsg;
21821
+ });
21610
21822
  } catch (e) {
21611
- if (isClientError2(e)) {
21612
- log10.info(`Backstop: task ${task.id} already in terminal state`);
21823
+ if (isClientError3(e)) {
21824
+ log10.info(`Task ${task.id} exited (already terminal) — session-runner handled cleanup`);
21613
21825
  return;
21614
21826
  }
21615
- log10.error(`Backstop: failed to report crash for task ${task.id}`, e);
21827
+ log10.error(`Failed to report crash for task ${task.id}`, e);
21616
21828
  try {
21617
21829
  await writeMarkerFile(config2.workspacesRoot, {
21618
21830
  taskId: task.id,
21619
21831
  type: "fail",
21620
- payload: { error: msg },
21832
+ payload: { error: errorMsg },
21621
21833
  token,
21622
21834
  serverURL: config2.serverURL,
21623
21835
  createdAt: new Date().toISOString()
@@ -22702,6 +22914,57 @@ function agentCommand() {
22702
22914
  process.exit(1);
22703
22915
  }
22704
22916
  });
22917
+ cmd.command("link").description("Set (create-or-replace) the relationship with an existing colleague agent").option("--agent_id <id>", "Calling agent ID (or set ALOOK_AGENT_ID env var)").option("--to <handleOrId>", "Target agent: email handle (coder / coder@alook.ai) or agent id (ag_...)").option("--relationship <text>", "Relationship/delegation instruction for the link").option("--relationship-file <path>", "Read relationship from a file").option("--json", "Output as JSON").action(async (opts, command) => {
22918
+ const relationship = readField({ inline: opts.relationship, file: opts.relationshipFile }, "relationship", "relationship-file");
22919
+ if (!relationship) {
22920
+ console.error("Error: --relationship or --relationship-file is required");
22921
+ process.exit(1);
22922
+ }
22923
+ const to = opts.to;
22924
+ if (!to) {
22925
+ console.error("Error: --to <handleOrId> is required");
22926
+ process.exit(1);
22927
+ }
22928
+ const agentId = resolveAgentId(opts);
22929
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22930
+ const client = new APIClient(serverUrl, token, workspaceId);
22931
+ try {
22932
+ let targetAgentId;
22933
+ let agents = null;
22934
+ if (to.startsWith("ag_")) {
22935
+ targetAgentId = to;
22936
+ } else {
22937
+ const handle = to.split("@")[0].toLowerCase();
22938
+ agents = await client.getJSON("/api/agents");
22939
+ const match = agents.find((a) => (a.email_handle || "").toLowerCase() === handle);
22940
+ if (!match) {
22941
+ console.error(`Error: no agent with handle '${handle}' in this workspace`);
22942
+ process.exit(1);
22943
+ }
22944
+ targetAgentId = match.id;
22945
+ }
22946
+ const res = await client.putJSON(`/api/agent-links?agentId=${encodeURIComponent(agentId)}`, {
22947
+ target_agent_id: targetAgentId,
22948
+ instruction: relationship
22949
+ });
22950
+ if (opts.json)
22951
+ return printJSON(res);
22952
+ if (!agents) {
22953
+ try {
22954
+ agents = await client.getJSON("/api/agents");
22955
+ } catch {
22956
+ agents = [];
22957
+ }
22958
+ }
22959
+ const label = (id) => agents.find((a) => a.id === id)?.name || id;
22960
+ const meLabel = label(agentId);
22961
+ const targetLabel = label(targetAgentId);
22962
+ console.log(`Linked ${meLabel} <-> ${targetLabel} (${res.created ? "created" : "updated"})`);
22963
+ } catch (err) {
22964
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
22965
+ process.exit(1);
22966
+ }
22967
+ });
22705
22968
  return cmd;
22706
22969
  }
22707
22970
 
@@ -22755,7 +23018,7 @@ import { Command as Command12 } from "commander";
22755
23018
  import { readFileSync as readFileSync13 } from "fs";
22756
23019
  import { basename as basename3 } from "path";
22757
23020
  function syncCommand() {
22758
- const cmd = new Command12("sync").description("File sync utilities");
23021
+ const cmd = new Command12("sync").description("Sync info with the user (files, messages)");
22759
23022
  cmd.command("upload-artifact").description("Upload a file artifact to a conversation").option("--agent_id <id>", "Agent ID").requiredOption("--conversation_id <id>", "Conversation ID").requiredOption("--file <path>", "Path to file to upload").action(async (opts, command) => {
22760
23023
  const agentId = resolveAgentId(opts);
22761
23024
  const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
@@ -22781,6 +23044,43 @@ function syncCommand() {
22781
23044
  process.exit(1);
22782
23045
  }
22783
23046
  });
23047
+ cmd.command("send-dm").description("Send a message to the user in the current conversation (the agent's voice)").option("--agent_id <id>", "Agent ID").option("--conversation_id <id>", "Conversation ID (defaults to $ALOOK_CONVERSATION_ID)").option("--message <text>", "Message body").option("--message-file <path>", "Read message body from a file").action(async (opts, command) => {
23048
+ if (opts.message && opts.messageFile) {
23049
+ console.error("Error: --message and --message-file are mutually exclusive");
23050
+ process.exit(1);
23051
+ }
23052
+ let content;
23053
+ if (opts.messageFile) {
23054
+ try {
23055
+ content = readFileSync13(opts.messageFile, "utf-8");
23056
+ } catch (err) {
23057
+ console.error(`Error: cannot read file "${opts.messageFile}": ${err.message}`);
23058
+ process.exit(1);
23059
+ }
23060
+ } else {
23061
+ content = (opts.message ?? "").replace(/\\n/g, `
23062
+ `).replace(/\\t/g, "\t");
23063
+ }
23064
+ if (!content.trim()) {
23065
+ console.error("Error: --message or --message-file is required (and must not be empty)");
23066
+ process.exit(1);
23067
+ }
23068
+ const conversationId = opts.conversation_id || process.env.ALOOK_CONVERSATION_ID;
23069
+ if (!conversationId) {
23070
+ console.error("Error: no conversation id (set --conversation_id or $ALOOK_CONVERSATION_ID)");
23071
+ process.exit(1);
23072
+ }
23073
+ const agentId = resolveAgentId(opts);
23074
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
23075
+ const client = new APIClient(serverUrl, token, workspaceId);
23076
+ try {
23077
+ const result = await client.postJSON(`/api/daemon/conversations/${encodeURIComponent(conversationId)}/messages`, { content, task_id: process.env.ALOOK_TASK_ID || undefined });
23078
+ printJSON(result);
23079
+ } catch (err) {
23080
+ console.error(`Error sending message: ${err.message}`);
23081
+ process.exit(1);
23082
+ }
23083
+ });
22784
23084
  return cmd;
22785
23085
  }
22786
23086
 
@@ -14631,6 +14631,10 @@ var CreateAgentLinkRequestSchema = exports_external.object({
14631
14631
  var UpdateAgentLinkRequestSchema = exports_external.object({
14632
14632
  instruction: exports_external.string()
14633
14633
  });
14634
+ var UpsertAgentLinkRequestSchema = exports_external.object({
14635
+ target_agent_id: exports_external.string().min(1, "target_agent_id is required"),
14636
+ instruction: exports_external.string()
14637
+ });
14634
14638
  var AddWhitelistRequestSchema = exports_external.object({
14635
14639
  email: exports_external.string().email()
14636
14640
  });
@@ -14661,8 +14665,9 @@ var CreateConversationRequestSchema = exports_external.object({
14661
14665
  var CreateMessageRequestSchema = exports_external.object({
14662
14666
  content: exports_external.string().min(1, "content is required")
14663
14667
  });
14664
- var CreateBufferedMessageRequestSchema = exports_external.object({
14665
- content: exports_external.string().min(1, "content is required")
14668
+ var AgentDmRequestSchema = exports_external.object({
14669
+ content: exports_external.string().min(1, "content is required"),
14670
+ task_id: exports_external.string().min(1).optional()
14666
14671
  });
14667
14672
  var EmailAttachmentSchema = exports_external.object({
14668
14673
  key: exports_external.string().min(1),
@@ -16725,6 +16730,22 @@ var agentSkill = sqliteTable("agent_skill", {
16725
16730
  foreignColumns: [agent.id, agent.workspaceId]
16726
16731
  }).onDelete("cascade")
16727
16732
  ]);
16733
+ var inboxUnread = sqliteTable("inbox_unread", {
16734
+ id: text("id").primaryKey().$defaultFn(() => nanoid3()),
16735
+ conversationId: text("conversation_id").notNull().references(() => conversation.id, { onDelete: "cascade" }),
16736
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
16737
+ workspaceId: text("workspace_id").notNull(),
16738
+ agentId: text("agent_id").notNull(),
16739
+ taskId: text("task_id").notNull(),
16740
+ taskType: text("task_type").notNull(),
16741
+ taskStatus: text("task_status").notNull(),
16742
+ taskPrompt: text("task_prompt"),
16743
+ completedAt: text("completed_at").notNull(),
16744
+ latestMessageId: text("latest_message_id")
16745
+ }, (t) => [
16746
+ unique("inbox_unread_conv_user").on(t.conversationId, t.userId),
16747
+ index("idx_inbox_unread_user_ws").on(t.userId, t.workspaceId, t.taskType, t.completedAt)
16748
+ ]);
16728
16749
  // ../shared/src/db/queries/task.ts
16729
16750
  var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
16730
16751
  var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
@@ -18007,6 +18028,7 @@ class OpenCodeBackend {
18007
18028
  case "error": {
18008
18029
  const content = event.message || event.content || part?.error || "";
18009
18030
  lastError = content;
18031
+ resultStatus = "failed";
18010
18032
  pushMessage({ type: "error", content });
18011
18033
  turnDone();
18012
18034
  break;
@@ -18288,6 +18310,7 @@ The CLI auto-detects your identity from the environment. No need to pass \`--age
18288
18310
  ### Command quick reference
18289
18311
  | Capability | Command |
18290
18312
  |---|---|
18313
+ | Send a message to the user | \`${cmdPrefix()} sync send-dm\` |
18291
18314
  | Schedule / list / edit tasks | \`${cmdPrefix()} calendar set\` (also list, show, update, delete) |
18292
18315
  | Upload a file for your owner | \`${cmdPrefix()} sync upload-artifact\` |
18293
18316
  | Recruit a colleague agent | \`${cmdPrefix()} agent recruit\` |
@@ -18375,6 +18398,26 @@ Upload files for your owner to review in the app.
18375
18398
  - 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)
18376
18399
  - If you think user may need to know any file detail, use upload-artifact tool to send the file to user.
18377
18400
 
18401
+ ### Talking to the user
18402
+ You're texting a colleague, not filing a report. The only thing the user sees is what you send with \`${cmdPrefix()} sync send-dm\` — your task output, reasoning, and tool calls are all off-screen. If you finish without sending, they got silence.
18403
+
18404
+ \`${cmdPrefix()} sync send-dm\` sends a message to **the user** (your owner), not to a colleague agent. This is how you communicate with the human who gave you the task. Use email to talk to colleague agents.
18405
+
18406
+ Message at milestones, the way a person would: acknowledge when you pick something up, share a real step forward or a fork in the road, and deliver the result. A quick task is often one message; a long one is a few well-spaced check-ins. Trust your read of the moment — don't narrate every small step, and don't go dark for a long stretch on something they're waiting on.
18407
+
18408
+ Say what a colleague would say, not a transcript — the answer in your own voice. (Email- and calendar-triggered tasks have no one watching the chat; use email there.)
18409
+
18410
+ **A real person is waiting on the other end.** Send updates at every milestone of your work — not just the final result. For any task longer than a minute:
18411
+ 1. **Before you start**: tell them your plan ("I'll research X, then modify Y and Z")
18412
+ 2. **During work**: update when you find something important, change direction, or hit a blocker ("Found the issue — it's in the auth module, fixing now")
18413
+ 3. **When done**: deliver the clear result
18414
+
18415
+ Don't bundle everything into one giant message at the end. The user shouldn't have to sit in silence wondering what's happening. A one-line progress update costs nothing and keeps the human in the loop. But don't send repetitive or near-identical messages — each update should carry new information, not just restate what you already said.
18416
+
18417
+ **If the user sends you a message while you're working** — especially questions like "are you there?", "what's the status?", or unrelated requests — **respond to them immediately**. Don't finish your current task first and then reply. The user reached out because they need your attention NOW. Acknowledge them right away, then resume your work.
18418
+
18419
+ \`${cmdPrefix()} sync send-dm --message "…"\` for short messages. For longer or markdown-rich messages, write to a file first and use \`--message-file <path>\` — this preserves formatting and avoids shell escaping issues. The conversation is in $ALOOK_CONVERSATION_ID, so you usually need no flags. You can send several times in one task.
18420
+
18378
18421
  ### Attachments
18379
18422
  When your task includes attachments, their local paths are listed in the prompt JSON under "attachments".
18380
18423
  Use your Read tool to open them. Images and PDFs are read visually.
@@ -18394,6 +18437,15 @@ Recruit new colleague agents directly from the CLI. The server auto-generates a
18394
18437
  - Example: '${cmdPrefix()} agent recruit --instructions "You are a QA engineer..." --relationship "DELEGATE when: code is ready for review"'
18395
18438
  - Output: 'Recruited Felix (felix@alook.ai) — ag_xK9mPq2z'
18396
18439
  - The new agent shares your runtime, is automatically linked as your colleague, and receives a welcome task.
18440
+
18441
+ Set or update the relationship with an EXISTING colleague (create-or-replace):
18442
+ - Run '${cmdPrefix()} agent link --to <handleOrId> --relationship "<DELEGATION_CRITERIA>"'
18443
+ - '--to <handleOrId>' — target agent by email handle ('coder' or 'coder@alook.ai') or agent id ('ag_...')
18444
+ - '--relationship <text>' — the delegation criteria both of you see (replaces it if a link already exists)
18445
+ - '--relationship-file <path>' — alternative: read relationship from a file (mutually exclusive with --relationship)
18446
+ - '--json' — output the link object incl. a 'created' boolean
18447
+ - Example: '${cmdPrefix()} agent link --to coder --relationship "DELEGATE when implementation is needed"'
18448
+ - Output: 'Linked Felix <-> coder (created)' (or '(updated)' when an existing link was replaced).
18397
18449
  `;
18398
18450
  content += `
18399
18451
  ### Calendar
@@ -18748,14 +18800,14 @@ function clearKillIntent(baseDir, taskId) {
18748
18800
  }
18749
18801
 
18750
18802
  // daemon/prompt.ts
18751
- var DM_RESPONSE_NOTICE = "IMPORTANT: Only your final text response is visible to the user." + " Tool calls, intermediate reasoning, and mid-process outputs are NOT displayed." + " Put all key information, answers, and conclusions in your final response that is the only thing the user will read.";
18803
+ var DM_RESPONSE_NOTICE = "Reply with `alook sync send-dm` that's the only thing the user sees; your task output and reasoning are not shown." + " Talk to them at milestones like a colleague would, and don't end your turn without sending what they need." + " If this task will take more than 30 seconds, send a quick ack first so the user knows you're on it.";
18752
18804
  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.";
18753
18805
  var CALENDAR_NOTICE = "This task was triggered by a scheduled calendar event. 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, send 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.";
18754
18806
  var ISSUE_NOTICE = "This task was triggered by an assigned issue. The issue_id is provided in this message." + " Use `alook issue show --issue_id <issue_id>` to read full context." + " Use `alook issue update --issue_id <issue_id> --status <status>` to change status." + " Use `alook issue comment --issue_id <issue_id> --body <text>` to leave a comment." + " CRITICAL — You MUST manage the issue status correctly. This is NOT optional:" + " 1. Set status to 'in_progress' when you start working." + " 2. If you complete the work yourself: leave a summary comment, then set status to 'review' as your last action. 'review' means there is actual completed work (code, artifact, result) ready for the owner to look at." + " 3. If you delegated work to colleagues and are waiting for their response: KEEP status as 'in_progress' and exit. This is expected — you will be woken up when they reply. Set 'review' only after all delegated work is confirmed complete." + " 4. NEVER set 'review' unless there is concrete completed work for the owner to review. Sending a plan to a colleague is NOT completed work." + " NEVER exit without doing at least one of: updating the status, or leaving a comment explaining what you did and what you're waiting for.";
18755
18807
  function buildDmNotice(name, email3) {
18756
18808
  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.`;
18757
18809
  }
18758
- function buildPrompt(task, attachments) {
18810
+ function buildTaskObject(task, attachments) {
18759
18811
  const createdAt = new Date(task.createdAt);
18760
18812
  const receivedAt = Number.isNaN(createdAt.getTime()) ? localISOString() : localISOString(createdAt);
18761
18813
  const obj = {
@@ -18821,7 +18873,10 @@ function buildPrompt(task, attachments) {
18821
18873
  filename: a.filename
18822
18874
  }));
18823
18875
  }
18824
- return JSON.stringify(obj);
18876
+ return obj;
18877
+ }
18878
+ function buildPrompt(task, attachments) {
18879
+ return JSON.stringify(buildTaskObject(task, attachments));
18825
18880
  }
18826
18881
 
18827
18882
  // daemon/session-runner.ts
@@ -18926,8 +18981,8 @@ async function runSession(input) {
18926
18981
  await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, undefined, process.pid, provider, task.contextKey, input.logFilePath));
18927
18982
  const { workDir, env } = prepare({ workspacesRoot, token }, task);
18928
18983
  let killed = false;
18929
- let agentPid = undefined;
18930
- let flushTimer = undefined;
18984
+ let agentPid;
18985
+ let flushTimer;
18931
18986
  const pendingMessages = [];
18932
18987
  let seq = 0;
18933
18988
  let toolCount = 0;
@@ -19013,7 +19068,7 @@ async function runSession(input) {
19013
19068
  }
19014
19069
  if (killed)
19015
19070
  return;
19016
- const prompt = buildPrompt(task, attachments);
19071
+ const prompt = input.promptOverride ?? buildPrompt(task, attachments);
19017
19072
  const resumeSessionId = task.contextKey ? findResumableSessionByContextKey(timelineDir, task.contextKey, provider) ?? undefined : undefined;
19018
19073
  if (resumeSessionId) {
19019
19074
  log5.info(`resuming session ${resumeSessionId} (context_key: ${task.contextKey})`);
@@ -19038,6 +19093,8 @@ async function runSession(input) {
19038
19093
  log5.info(JSON.stringify({ role: "user", type: "text", content: prompt }));
19039
19094
  updateEntry(timelineDir, task.id, (entry) => {
19040
19095
  entry.session_id = earlySessionId || null;
19096
+ if (earlySessionId)
19097
+ entry.agent_started = true;
19041
19098
  });
19042
19099
  flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
19043
19100
  const INACTIVITY_TIMEOUT_MS = messageInactivityTimeout ?? 5 * 60 * 1000;
@@ -19179,5 +19236,7 @@ if (isDirectExecution) {
19179
19236
  export {
19180
19237
  writeMarkerFile,
19181
19238
  runSession,
19182
- reportToServer
19239
+ reportToServer,
19240
+ downloadAttachments,
19241
+ cleanupAttachments
19183
19242
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.117",
3
+ "version": "0.0.119",
4
4
  "description": "Alook CLI — Enable Your Person Colleague",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",