@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 +368 -68
- package/dist/session-runner.js +68 -9
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
|
19780
|
-
let flushTimer
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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 (
|
|
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
|
|
21629
|
+
const ctxKey = task.contextKey;
|
|
21630
|
+
let lockAcquired = acquireSteeringLock(agentBaseDir, ctxKey);
|
|
21529
21631
|
if (!lockAcquired) {
|
|
21530
|
-
|
|
21531
|
-
|
|
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
|
|
21534
|
-
if (
|
|
21535
|
-
|
|
21536
|
-
if (
|
|
21537
|
-
|
|
21538
|
-
|
|
21539
|
-
|
|
21540
|
-
|
|
21541
|
-
|
|
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
|
-
|
|
21544
|
-
|
|
21545
|
-
|
|
21546
|
-
|
|
21547
|
-
|
|
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
|
|
21550
|
-
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
|
|
21554
|
-
|
|
21555
|
-
|
|
21556
|
-
|
|
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
|
-
|
|
21559
|
-
|
|
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
|
-
|
|
21563
|
-
|
|
21564
|
-
|
|
21565
|
-
|
|
21566
|
-
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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 (
|
|
21612
|
-
log10.info(`
|
|
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(`
|
|
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:
|
|
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("
|
|
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
|
|
package/dist/session-runner.js
CHANGED
|
@@ -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
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
|
18930
|
-
let flushTimer
|
|
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
|
};
|