@alook/cli 0.0.117 → 0.0.118
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 +355 -68
- package/dist/session-runner.js +56 -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;
|
|
@@ -19153,6 +19178,15 @@ Upload files for your owner to review in the app.
|
|
|
19153
19178
|
- 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
19179
|
- If you think user may need to know any file detail, use upload-artifact tool to send the file to user.
|
|
19155
19180
|
|
|
19181
|
+
### Talking to the user
|
|
19182
|
+
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.
|
|
19183
|
+
|
|
19184
|
+
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.
|
|
19185
|
+
|
|
19186
|
+
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.)
|
|
19187
|
+
|
|
19188
|
+
\`${cmdPrefix()} sync send-dm --message "…"\` (or \`--message-file <path>\` for long/markdown). The conversation is in $ALOOK_CONVERSATION_ID, so you usually need no flags. You can send several times in one task.
|
|
19189
|
+
|
|
19156
19190
|
### Attachments
|
|
19157
19191
|
When your task includes attachments, their local paths are listed in the prompt JSON under "attachments".
|
|
19158
19192
|
Use your Read tool to open them. Images and PDFs are read visually.
|
|
@@ -19172,6 +19206,15 @@ Recruit new colleague agents directly from the CLI. The server auto-generates a
|
|
|
19172
19206
|
- Example: '${cmdPrefix()} agent recruit --instructions "You are a QA engineer..." --relationship "DELEGATE when: code is ready for review"'
|
|
19173
19207
|
- Output: 'Recruited Felix (felix@alook.ai) — ag_xK9mPq2z'
|
|
19174
19208
|
- The new agent shares your runtime, is automatically linked as your colleague, and receives a welcome task.
|
|
19209
|
+
|
|
19210
|
+
Set or update the relationship with an EXISTING colleague (create-or-replace):
|
|
19211
|
+
- Run '${cmdPrefix()} agent link --to <handleOrId> --relationship "<DELEGATION_CRITERIA>"'
|
|
19212
|
+
- '--to <handleOrId>' — target agent by email handle ('coder' or 'coder@alook.ai') or agent id ('ag_...')
|
|
19213
|
+
- '--relationship <text>' — the delegation criteria both of you see (replaces it if a link already exists)
|
|
19214
|
+
- '--relationship-file <path>' — alternative: read relationship from a file (mutually exclusive with --relationship)
|
|
19215
|
+
- '--json' — output the link object incl. a 'created' boolean
|
|
19216
|
+
- Example: '${cmdPrefix()} agent link --to coder --relationship "DELEGATE when implementation is needed"'
|
|
19217
|
+
- Output: 'Linked Felix <-> coder (created)' (or '(updated)' when an existing link was replaced).
|
|
19175
19218
|
`;
|
|
19176
19219
|
content += `
|
|
19177
19220
|
### Calendar
|
|
@@ -19510,13 +19553,24 @@ function findRunningPidByTaskId(timelineDir, taskId) {
|
|
|
19510
19553
|
}
|
|
19511
19554
|
return null;
|
|
19512
19555
|
}
|
|
19513
|
-
function
|
|
19556
|
+
function steerWarmupGraceMs() {
|
|
19557
|
+
const raw = process.env.ALOOK_STEER_WARMUP_GRACE_MS;
|
|
19558
|
+
const parsed = raw != null ? Number(raw) : NaN;
|
|
19559
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 30000;
|
|
19560
|
+
}
|
|
19561
|
+
function findSupersedablePredecessor(timelineDir, contextKey, provider, warmupGraceMs, now) {
|
|
19514
19562
|
for (const filename of recentFilenames(7)) {
|
|
19515
19563
|
const dayEntries = readJsonl(join7(timelineDir, filename));
|
|
19516
19564
|
for (let i = dayEntries.length - 1;i >= 0; i--) {
|
|
19517
19565
|
const entry = dayEntries[i];
|
|
19518
19566
|
if (entry.status === "running" && entry.context_key === contextKey && entry.provider === provider) {
|
|
19519
|
-
|
|
19567
|
+
if (entry.agent_started === true) {
|
|
19568
|
+
return { entry, reason: "agent-started" };
|
|
19569
|
+
}
|
|
19570
|
+
if (now - Date.parse(entry.datetime) > warmupGraceMs) {
|
|
19571
|
+
return { entry, reason: "stale" };
|
|
19572
|
+
}
|
|
19573
|
+
return { pending: entry };
|
|
19520
19574
|
}
|
|
19521
19575
|
}
|
|
19522
19576
|
}
|
|
@@ -19598,14 +19652,14 @@ function releaseSteeringLock(baseDir, contextKey) {
|
|
|
19598
19652
|
}
|
|
19599
19653
|
|
|
19600
19654
|
// daemon/prompt.ts
|
|
19601
|
-
var DM_RESPONSE_NOTICE = "
|
|
19655
|
+
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.";
|
|
19602
19656
|
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
19657
|
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
19658
|
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
19659
|
function buildDmNotice(name, email3) {
|
|
19606
19660
|
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
19661
|
}
|
|
19608
|
-
function
|
|
19662
|
+
function buildTaskObject(task, attachments) {
|
|
19609
19663
|
const createdAt = new Date(task.createdAt);
|
|
19610
19664
|
const receivedAt = Number.isNaN(createdAt.getTime()) ? localISOString() : localISOString(createdAt);
|
|
19611
19665
|
const obj = {
|
|
@@ -19671,7 +19725,18 @@ function buildPrompt(task, attachments) {
|
|
|
19671
19725
|
filename: a.filename
|
|
19672
19726
|
}));
|
|
19673
19727
|
}
|
|
19674
|
-
return
|
|
19728
|
+
return obj;
|
|
19729
|
+
}
|
|
19730
|
+
function buildPrompt(task, attachments) {
|
|
19731
|
+
return JSON.stringify(buildTaskObject(task, attachments));
|
|
19732
|
+
}
|
|
19733
|
+
function buildMergedPrompt(tasks, attachmentsMap) {
|
|
19734
|
+
const subTasks = tasks.map((t) => buildTaskObject(t, attachmentsMap.get(t.id))).sort((a, b) => String(a.received_at).localeCompare(String(b.received_at)));
|
|
19735
|
+
return JSON.stringify({
|
|
19736
|
+
type: "merge_tasks",
|
|
19737
|
+
notice: "These messages arrived simultaneously. Process each one completely.",
|
|
19738
|
+
tasks: subTasks
|
|
19739
|
+
});
|
|
19675
19740
|
}
|
|
19676
19741
|
|
|
19677
19742
|
// daemon/session-runner.ts
|
|
@@ -19776,8 +19841,8 @@ async function runSession(input) {
|
|
|
19776
19841
|
await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, undefined, process.pid, provider, task.contextKey, input.logFilePath));
|
|
19777
19842
|
const { workDir, env } = prepare({ workspacesRoot, token }, task);
|
|
19778
19843
|
let killed = false;
|
|
19779
|
-
let agentPid
|
|
19780
|
-
let flushTimer
|
|
19844
|
+
let agentPid;
|
|
19845
|
+
let flushTimer;
|
|
19781
19846
|
const pendingMessages = [];
|
|
19782
19847
|
let seq = 0;
|
|
19783
19848
|
let toolCount = 0;
|
|
@@ -19863,7 +19928,7 @@ async function runSession(input) {
|
|
|
19863
19928
|
}
|
|
19864
19929
|
if (killed)
|
|
19865
19930
|
return;
|
|
19866
|
-
const prompt = buildPrompt(task, attachments);
|
|
19931
|
+
const prompt = input.promptOverride ?? buildPrompt(task, attachments);
|
|
19867
19932
|
const resumeSessionId = task.contextKey ? findResumableSessionByContextKey(timelineDir, task.contextKey, provider) ?? undefined : undefined;
|
|
19868
19933
|
if (resumeSessionId) {
|
|
19869
19934
|
log6.info(`resuming session ${resumeSessionId} (context_key: ${task.contextKey})`);
|
|
@@ -19888,6 +19953,8 @@ async function runSession(input) {
|
|
|
19888
19953
|
log6.info(JSON.stringify({ role: "user", type: "text", content: prompt }));
|
|
19889
19954
|
updateEntry(timelineDir, task.id, (entry) => {
|
|
19890
19955
|
entry.session_id = earlySessionId || null;
|
|
19956
|
+
if (earlySessionId)
|
|
19957
|
+
entry.agent_started = true;
|
|
19891
19958
|
});
|
|
19892
19959
|
flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
|
|
19893
19960
|
const INACTIVITY_TIMEOUT_MS = messageInactivityTimeout ?? 5 * 60 * 1000;
|
|
@@ -20588,6 +20655,16 @@ function writeCacheFile(filePath, hash2, skills) {
|
|
|
20588
20655
|
const data = { hash: hash2, skills };
|
|
20589
20656
|
writeFileSync7(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
20590
20657
|
}
|
|
20658
|
+
function isClientError2(err) {
|
|
20659
|
+
if (err instanceof Error) {
|
|
20660
|
+
const match = err.message.match(/^HTTP (\d+):/);
|
|
20661
|
+
if (match) {
|
|
20662
|
+
const status = parseInt(match[1], 10);
|
|
20663
|
+
return status >= 400 && status < 500;
|
|
20664
|
+
}
|
|
20665
|
+
}
|
|
20666
|
+
return false;
|
|
20667
|
+
}
|
|
20591
20668
|
var scanTimer = null;
|
|
20592
20669
|
var scannerConfig = null;
|
|
20593
20670
|
var clientRef = null;
|
|
@@ -20664,7 +20741,12 @@ function runScan() {
|
|
|
20664
20741
|
}));
|
|
20665
20742
|
Promise.all(syncPromises).then(() => {
|
|
20666
20743
|
writeCacheFile(globalCachePath(daemonId, runtime), hash2, skills);
|
|
20667
|
-
}).catch((e) =>
|
|
20744
|
+
}).catch((e) => {
|
|
20745
|
+
if (isClientError2(e)) {
|
|
20746
|
+
writeCacheFile(globalCachePath(daemonId, runtime), hash2, skills);
|
|
20747
|
+
}
|
|
20748
|
+
log9.debug("Global skill sync failed", e);
|
|
20749
|
+
});
|
|
20668
20750
|
}
|
|
20669
20751
|
} catch (e) {
|
|
20670
20752
|
log9.debug(`Global scan error for ${runtime}`, e);
|
|
@@ -20688,7 +20770,12 @@ function runScan() {
|
|
|
20688
20770
|
skills: skillItems
|
|
20689
20771
|
}).then(() => {
|
|
20690
20772
|
writeCacheFile(agentCachePath(target.agentId, target.runtime), hash2, skills);
|
|
20691
|
-
}).catch((e) =>
|
|
20773
|
+
}).catch((e) => {
|
|
20774
|
+
if (isClientError2(e)) {
|
|
20775
|
+
writeCacheFile(agentCachePath(target.agentId, target.runtime), hash2, skills);
|
|
20776
|
+
}
|
|
20777
|
+
log9.debug("Agent skill sync failed", e);
|
|
20778
|
+
});
|
|
20692
20779
|
}
|
|
20693
20780
|
} catch (e) {
|
|
20694
20781
|
log9.debug(`Agent scan error for ${target.agentId}:${target.runtime}`, e);
|
|
@@ -20786,7 +20873,7 @@ function pruneSessionRunnerLogs() {
|
|
|
20786
20873
|
} catch {}
|
|
20787
20874
|
}
|
|
20788
20875
|
}
|
|
20789
|
-
function
|
|
20876
|
+
function isClientError3(error51) {
|
|
20790
20877
|
if (!(error51 instanceof Error))
|
|
20791
20878
|
return false;
|
|
20792
20879
|
const match = error51.message.match(/^HTTP (\d+):/);
|
|
@@ -20889,7 +20976,7 @@ async function reconcilePendingCompletions(workspacesRoot) {
|
|
|
20889
20976
|
log10.warn(`reconcile: delivered marker ${name} but failed to delete: ${delErr}`);
|
|
20890
20977
|
}
|
|
20891
20978
|
} catch (deliverErr) {
|
|
20892
|
-
if (
|
|
20979
|
+
if (isClientError3(deliverErr)) {
|
|
20893
20980
|
try {
|
|
20894
20981
|
await unlink(filePath);
|
|
20895
20982
|
} catch {}
|
|
@@ -21007,6 +21094,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
21007
21094
|
health.setRuntimeCount(allRuntimeIds.length);
|
|
21008
21095
|
log10.info(`Daemon started — ${allRuntimeIds.length} runtime(s) across ${workspaceStates.length} workspace(s)`);
|
|
21009
21096
|
const activeTasks = new Set;
|
|
21097
|
+
const pendingSteer = new Map;
|
|
21010
21098
|
const knownAgentIds = new Set(workspaces.flatMap((ws) => ws.agent_ids ?? []));
|
|
21011
21099
|
function syncAgentId(agentId, workspaceId) {
|
|
21012
21100
|
if (knownAgentIds.has(agentId))
|
|
@@ -21078,7 +21166,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
21078
21166
|
syncAgentId(task.agentId, ws.workspaceId);
|
|
21079
21167
|
activeTasks.add(task.id);
|
|
21080
21168
|
remaining--;
|
|
21081
|
-
handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks).catch((e) => {
|
|
21169
|
+
handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks, pendingSteer).catch((e) => {
|
|
21082
21170
|
log10.error("Task error", e);
|
|
21083
21171
|
activeTasks.delete(task.id);
|
|
21084
21172
|
});
|
|
@@ -21151,7 +21239,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
21151
21239
|
continue;
|
|
21152
21240
|
syncAgentId(task.agentId, ws.workspaceId);
|
|
21153
21241
|
activeTasks.add(task.id);
|
|
21154
|
-
handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks).catch((e) => {
|
|
21242
|
+
handleTask(client, config2, runtimeIndex, task, ws.token, activeTasks, pendingSteer).catch((e) => {
|
|
21155
21243
|
log10.error("WS task error", e);
|
|
21156
21244
|
activeTasks.delete(task.id);
|
|
21157
21245
|
});
|
|
@@ -21223,7 +21311,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
21223
21311
|
sender: null
|
|
21224
21312
|
});
|
|
21225
21313
|
activeTasks.add(killTask.id);
|
|
21226
|
-
handleTask(client, config2, runtimeIndex, killTask, ws.token, activeTasks).catch((e) => {
|
|
21314
|
+
handleTask(client, config2, runtimeIndex, killTask, ws.token, activeTasks, pendingSteer).catch((e) => {
|
|
21227
21315
|
log10.error("WS kill task error", e);
|
|
21228
21316
|
activeTasks.delete(killTask.id);
|
|
21229
21317
|
});
|
|
@@ -21461,7 +21549,7 @@ async function killAndVerify(pid) {
|
|
|
21461
21549
|
}
|
|
21462
21550
|
return true;
|
|
21463
21551
|
}
|
|
21464
|
-
async function handleTask(client, config2, runtimeIndex, task, token, activeTasks) {
|
|
21552
|
+
async function handleTask(client, config2, runtimeIndex, task, token, activeTasks, pendingSteer) {
|
|
21465
21553
|
log10.info(`Task ${task.id} claimed agent=${task.agentId}`);
|
|
21466
21554
|
if (task.type === TASK_TYPES.KILL_TASK) {
|
|
21467
21555
|
const targetTaskId = task.context?.target_task_id;
|
|
@@ -21521,53 +21609,164 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
|
|
|
21521
21609
|
return;
|
|
21522
21610
|
}
|
|
21523
21611
|
const provider = runtimeData.provider;
|
|
21612
|
+
let promptOverride;
|
|
21524
21613
|
if (task.contextKey) {
|
|
21525
21614
|
const agentBaseDir = join11(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
|
|
21526
21615
|
cleanupStaleIntents(agentBaseDir);
|
|
21527
21616
|
const timelineDir = join11(agentBaseDir, ".context_timeline");
|
|
21528
|
-
const
|
|
21617
|
+
const ctxKey = task.contextKey;
|
|
21618
|
+
let lockAcquired = acquireSteeringLock(agentBaseDir, ctxKey);
|
|
21529
21619
|
if (!lockAcquired) {
|
|
21530
|
-
|
|
21531
|
-
|
|
21620
|
+
const MERGE_WAIT_MS = 5000;
|
|
21621
|
+
const MERGE_POLL_MS = 50;
|
|
21622
|
+
const mergeStart = Date.now();
|
|
21623
|
+
while (Date.now() - mergeStart < MERGE_WAIT_MS) {
|
|
21624
|
+
const existing = pendingSteer.get(ctxKey);
|
|
21625
|
+
if (existing) {
|
|
21626
|
+
const attachmentIds = task.context?.attachment_ids ?? [];
|
|
21627
|
+
let myAttachments = [];
|
|
21628
|
+
if (attachmentIds.length > 0) {
|
|
21629
|
+
try {
|
|
21630
|
+
myAttachments = await downloadAttachments(client, token, task.workspaceId, task.id, attachmentIds);
|
|
21631
|
+
} catch {}
|
|
21632
|
+
}
|
|
21633
|
+
existing.tasks.push(task);
|
|
21634
|
+
existing.attachments.set(task.id, myAttachments);
|
|
21635
|
+
log10.info(`Steering: ${task.id} merged into pending entry (lock contention) for context_key=${ctxKey} (${existing.tasks.length} tasks)`);
|
|
21636
|
+
existing.wake();
|
|
21637
|
+
try {
|
|
21638
|
+
await client.supersedeTask(token, task.id);
|
|
21639
|
+
} catch {}
|
|
21640
|
+
activeTasks.delete(task.id);
|
|
21641
|
+
return;
|
|
21642
|
+
}
|
|
21643
|
+
lockAcquired = acquireSteeringLock(agentBaseDir, ctxKey);
|
|
21644
|
+
if (lockAcquired)
|
|
21645
|
+
break;
|
|
21646
|
+
await new Promise((r) => setTimeout(r, MERGE_POLL_MS));
|
|
21647
|
+
}
|
|
21648
|
+
if (!lockAcquired) {
|
|
21649
|
+
log10.warn(`Steering lock contention for context_key=${ctxKey}, proceeding without steering`);
|
|
21650
|
+
}
|
|
21651
|
+
}
|
|
21652
|
+
if (lockAcquired) {
|
|
21532
21653
|
try {
|
|
21533
|
-
const
|
|
21534
|
-
if (
|
|
21535
|
-
|
|
21536
|
-
if (
|
|
21537
|
-
|
|
21538
|
-
|
|
21539
|
-
|
|
21540
|
-
|
|
21541
|
-
|
|
21654
|
+
const result = findSupersedablePredecessor(timelineDir, ctxKey, provider, steerWarmupGraceMs(), Date.now());
|
|
21655
|
+
if (result) {
|
|
21656
|
+
const existing = pendingSteer.get(ctxKey);
|
|
21657
|
+
if (!existing) {
|
|
21658
|
+
const attachmentIds = task.context?.attachment_ids ?? [];
|
|
21659
|
+
let myAttachments = [];
|
|
21660
|
+
if (attachmentIds.length > 0) {
|
|
21661
|
+
try {
|
|
21662
|
+
myAttachments = await downloadAttachments(client, token, task.workspaceId, task.id, attachmentIds);
|
|
21663
|
+
} catch (e) {
|
|
21664
|
+
log10.warn(`Steering: failed to download attachments for ${task.id}`, e);
|
|
21665
|
+
}
|
|
21666
|
+
}
|
|
21667
|
+
let ownerWake;
|
|
21668
|
+
const ownerSignal = new Promise((resolve2) => {
|
|
21669
|
+
ownerWake = resolve2;
|
|
21542
21670
|
});
|
|
21543
|
-
|
|
21544
|
-
|
|
21545
|
-
|
|
21546
|
-
|
|
21547
|
-
|
|
21671
|
+
const entry = {
|
|
21672
|
+
tasks: [task],
|
|
21673
|
+
attachments: new Map([[task.id, myAttachments]]),
|
|
21674
|
+
ownerTaskId: task.id,
|
|
21675
|
+
wake: ownerWake
|
|
21676
|
+
};
|
|
21677
|
+
pendingSteer.set(ctxKey, entry);
|
|
21678
|
+
let predecessor = "entry" in result ? result.entry : null;
|
|
21679
|
+
if (!predecessor) {
|
|
21680
|
+
log10.info(`Steering: predecessor ${result.pending.task_id} warming up; ${task.id} waiting`);
|
|
21681
|
+
const POLL_MS2 = 200;
|
|
21682
|
+
const MAX_WAIT_MS = steerWarmupGraceMs();
|
|
21683
|
+
const waitStart = Date.now();
|
|
21684
|
+
while (Date.now() - waitStart < MAX_WAIT_MS) {
|
|
21685
|
+
releaseSteeringLock(agentBaseDir, ctxKey);
|
|
21686
|
+
await Promise.race([new Promise((r2) => setTimeout(r2, POLL_MS2)), ownerSignal]);
|
|
21687
|
+
if (!acquireSteeringLock(agentBaseDir, ctxKey))
|
|
21688
|
+
continue;
|
|
21689
|
+
const r = findSupersedablePredecessor(timelineDir, ctxKey, provider, steerWarmupGraceMs(), Date.now());
|
|
21690
|
+
if (!r) {
|
|
21691
|
+
log10.info(`Steering: predecessor vanished; ${task.id} proceeding`);
|
|
21692
|
+
break;
|
|
21693
|
+
}
|
|
21694
|
+
if ("entry" in r) {
|
|
21695
|
+
predecessor = r.entry;
|
|
21696
|
+
break;
|
|
21697
|
+
}
|
|
21698
|
+
}
|
|
21699
|
+
if (!predecessor && Date.now() - waitStart >= MAX_WAIT_MS) {
|
|
21700
|
+
releaseSteeringLock(agentBaseDir, ctxKey);
|
|
21701
|
+
if (acquireSteeringLock(agentBaseDir, ctxKey)) {
|
|
21702
|
+
const finalCheck = findSupersedablePredecessor(timelineDir, ctxKey, provider, 0, Date.now());
|
|
21703
|
+
if (finalCheck && "entry" in finalCheck)
|
|
21704
|
+
predecessor = finalCheck.entry;
|
|
21705
|
+
}
|
|
21706
|
+
}
|
|
21548
21707
|
}
|
|
21549
|
-
|
|
21550
|
-
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
|
|
21554
|
-
|
|
21555
|
-
|
|
21556
|
-
|
|
21708
|
+
if (predecessor && predecessor.task_id !== task.id) {
|
|
21709
|
+
log10.info(`Steering: task ${task.id} supersedes predecessor ${predecessor.task_id} (context_key=${ctxKey})`);
|
|
21710
|
+
if (predecessor.pid != null) {
|
|
21711
|
+
writeKillIntent(agentBaseDir, { reason: "superseded", targetTaskId: predecessor.task_id, expectedPid: predecessor.pid, successorTaskId: task.id });
|
|
21712
|
+
try {
|
|
21713
|
+
const delivered = await killAndVerify(predecessor.pid);
|
|
21714
|
+
log10.info(delivered ? `Steering: terminated predecessor pid=${predecessor.pid}` : `Steering: predecessor pid=${predecessor.pid} already exited`);
|
|
21715
|
+
} catch (e) {
|
|
21716
|
+
log10.warn(`Steering: kill failed for pid=${predecessor.pid}`, e);
|
|
21717
|
+
}
|
|
21718
|
+
const killWaitStart = Date.now();
|
|
21719
|
+
while (Date.now() - killWaitStart < 15000) {
|
|
21720
|
+
if (findRunningPidByTaskId(timelineDir, predecessor.task_id) == null)
|
|
21721
|
+
break;
|
|
21722
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
21723
|
+
}
|
|
21724
|
+
}
|
|
21725
|
+
try {
|
|
21726
|
+
await client.supersedeTask(token, predecessor.task_id);
|
|
21727
|
+
} catch {}
|
|
21557
21728
|
}
|
|
21558
|
-
|
|
21559
|
-
|
|
21729
|
+
const finalEntry = pendingSteer.get(ctxKey);
|
|
21730
|
+
if (finalEntry && finalEntry.tasks.length > 1) {
|
|
21731
|
+
promptOverride = buildMergedPrompt(finalEntry.tasks, finalEntry.attachments);
|
|
21732
|
+
log10.info(`Steering: merged ${finalEntry.tasks.length} tasks for context_key=${ctxKey}`);
|
|
21733
|
+
} else if (finalEntry && finalEntry.tasks.length === 1) {
|
|
21734
|
+
const att = finalEntry.attachments.get(task.id);
|
|
21735
|
+
if (att && att.length > 0) {
|
|
21736
|
+
promptOverride = buildPrompt(task, att);
|
|
21737
|
+
}
|
|
21560
21738
|
}
|
|
21561
|
-
|
|
21562
|
-
|
|
21563
|
-
|
|
21564
|
-
|
|
21565
|
-
|
|
21566
|
-
|
|
21739
|
+
pendingSteer.delete(ctxKey);
|
|
21740
|
+
} else {
|
|
21741
|
+
const attachmentIds = task.context?.attachment_ids ?? [];
|
|
21742
|
+
let myAttachments = [];
|
|
21743
|
+
if (attachmentIds.length > 0) {
|
|
21744
|
+
try {
|
|
21745
|
+
myAttachments = await downloadAttachments(client, token, task.workspaceId, task.id, attachmentIds);
|
|
21746
|
+
} catch (e) {
|
|
21747
|
+
log10.warn(`Steering: failed to download attachments for ${task.id}`, e);
|
|
21748
|
+
}
|
|
21749
|
+
}
|
|
21750
|
+
for (const prev of existing.tasks) {
|
|
21751
|
+
if (prev.id !== existing.ownerTaskId) {
|
|
21752
|
+
try {
|
|
21753
|
+
await client.supersedeTask(token, prev.id);
|
|
21754
|
+
} catch {}
|
|
21755
|
+
}
|
|
21756
|
+
}
|
|
21757
|
+
existing.tasks.push(task);
|
|
21758
|
+
existing.attachments.set(task.id, myAttachments);
|
|
21759
|
+
log10.info(`Steering: ${task.id} merged into pending entry for context_key=${ctxKey} (${existing.tasks.length} tasks)`);
|
|
21760
|
+
existing.wake();
|
|
21761
|
+
try {
|
|
21762
|
+
await client.supersedeTask(token, task.id);
|
|
21763
|
+
} catch {}
|
|
21764
|
+
activeTasks.delete(task.id);
|
|
21765
|
+
return;
|
|
21567
21766
|
}
|
|
21568
21767
|
}
|
|
21569
21768
|
} finally {
|
|
21570
|
-
releaseSteeringLock(agentBaseDir,
|
|
21769
|
+
releaseSteeringLock(agentBaseDir, ctxKey);
|
|
21571
21770
|
}
|
|
21572
21771
|
}
|
|
21573
21772
|
}
|
|
@@ -21584,7 +21783,8 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
|
|
|
21584
21783
|
token,
|
|
21585
21784
|
workspacesRoot: config2.workspacesRoot,
|
|
21586
21785
|
agentTimeout: config2.agentTimeout,
|
|
21587
|
-
messageInactivityTimeout: config2.messageInactivityTimeout
|
|
21786
|
+
messageInactivityTimeout: config2.messageInactivityTimeout,
|
|
21787
|
+
...promptOverride && { promptOverride }
|
|
21588
21788
|
};
|
|
21589
21789
|
const child = spawnSessionRunner(input);
|
|
21590
21790
|
child.on("close", async (code) => {
|
|
@@ -21593,31 +21793,31 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
|
|
|
21593
21793
|
const agentBaseDir = join11(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
|
|
21594
21794
|
const killIntent = readKillIntent(agentBaseDir, task.id);
|
|
21595
21795
|
if (killIntent) {
|
|
21596
|
-
log10.info(`Task ${task.id} exited
|
|
21796
|
+
log10.info(`Task ${task.id} exited (${killIntent.reason}) — expected, skipping failTask`);
|
|
21597
21797
|
clearKillIntent(agentBaseDir, task.id);
|
|
21598
21798
|
return;
|
|
21599
21799
|
}
|
|
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
|
-
});
|
|
21800
|
+
const errorMsg = code === null ? "killed by signal" : `session-runner exited with code ${code}`;
|
|
21608
21801
|
try {
|
|
21609
|
-
await client.failTask(token, task.id,
|
|
21802
|
+
await client.failTask(token, task.id, errorMsg);
|
|
21803
|
+
log10.warn(`session-runner crashed (${errorMsg}, task ${task.id})`);
|
|
21804
|
+
const timelineDir = join11(agentBaseDir, ".context_timeline");
|
|
21805
|
+
updateEntry(timelineDir, task.id, (entry) => {
|
|
21806
|
+
entry.pid = null;
|
|
21807
|
+
entry.status = "failed";
|
|
21808
|
+
entry.errmsg = errorMsg;
|
|
21809
|
+
});
|
|
21610
21810
|
} catch (e) {
|
|
21611
|
-
if (
|
|
21612
|
-
log10.info(`
|
|
21811
|
+
if (isClientError3(e)) {
|
|
21812
|
+
log10.info(`Task ${task.id} exited (already terminal) — session-runner handled cleanup`);
|
|
21613
21813
|
return;
|
|
21614
21814
|
}
|
|
21615
|
-
log10.error(`
|
|
21815
|
+
log10.error(`Failed to report crash for task ${task.id}`, e);
|
|
21616
21816
|
try {
|
|
21617
21817
|
await writeMarkerFile(config2.workspacesRoot, {
|
|
21618
21818
|
taskId: task.id,
|
|
21619
21819
|
type: "fail",
|
|
21620
|
-
payload: { error:
|
|
21820
|
+
payload: { error: errorMsg },
|
|
21621
21821
|
token,
|
|
21622
21822
|
serverURL: config2.serverURL,
|
|
21623
21823
|
createdAt: new Date().toISOString()
|
|
@@ -22702,6 +22902,57 @@ function agentCommand() {
|
|
|
22702
22902
|
process.exit(1);
|
|
22703
22903
|
}
|
|
22704
22904
|
});
|
|
22905
|
+
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) => {
|
|
22906
|
+
const relationship = readField({ inline: opts.relationship, file: opts.relationshipFile }, "relationship", "relationship-file");
|
|
22907
|
+
if (!relationship) {
|
|
22908
|
+
console.error("Error: --relationship or --relationship-file is required");
|
|
22909
|
+
process.exit(1);
|
|
22910
|
+
}
|
|
22911
|
+
const to = opts.to;
|
|
22912
|
+
if (!to) {
|
|
22913
|
+
console.error("Error: --to <handleOrId> is required");
|
|
22914
|
+
process.exit(1);
|
|
22915
|
+
}
|
|
22916
|
+
const agentId = resolveAgentId(opts);
|
|
22917
|
+
const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
|
|
22918
|
+
const client = new APIClient(serverUrl, token, workspaceId);
|
|
22919
|
+
try {
|
|
22920
|
+
let targetAgentId;
|
|
22921
|
+
let agents = null;
|
|
22922
|
+
if (to.startsWith("ag_")) {
|
|
22923
|
+
targetAgentId = to;
|
|
22924
|
+
} else {
|
|
22925
|
+
const handle = to.split("@")[0].toLowerCase();
|
|
22926
|
+
agents = await client.getJSON("/api/agents");
|
|
22927
|
+
const match = agents.find((a) => (a.email_handle || "").toLowerCase() === handle);
|
|
22928
|
+
if (!match) {
|
|
22929
|
+
console.error(`Error: no agent with handle '${handle}' in this workspace`);
|
|
22930
|
+
process.exit(1);
|
|
22931
|
+
}
|
|
22932
|
+
targetAgentId = match.id;
|
|
22933
|
+
}
|
|
22934
|
+
const res = await client.putJSON(`/api/agent-links?agentId=${encodeURIComponent(agentId)}`, {
|
|
22935
|
+
target_agent_id: targetAgentId,
|
|
22936
|
+
instruction: relationship
|
|
22937
|
+
});
|
|
22938
|
+
if (opts.json)
|
|
22939
|
+
return printJSON(res);
|
|
22940
|
+
if (!agents) {
|
|
22941
|
+
try {
|
|
22942
|
+
agents = await client.getJSON("/api/agents");
|
|
22943
|
+
} catch {
|
|
22944
|
+
agents = [];
|
|
22945
|
+
}
|
|
22946
|
+
}
|
|
22947
|
+
const label = (id) => agents.find((a) => a.id === id)?.name || id;
|
|
22948
|
+
const meLabel = label(agentId);
|
|
22949
|
+
const targetLabel = label(targetAgentId);
|
|
22950
|
+
console.log(`Linked ${meLabel} <-> ${targetLabel} (${res.created ? "created" : "updated"})`);
|
|
22951
|
+
} catch (err) {
|
|
22952
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
22953
|
+
process.exit(1);
|
|
22954
|
+
}
|
|
22955
|
+
});
|
|
22705
22956
|
return cmd;
|
|
22706
22957
|
}
|
|
22707
22958
|
|
|
@@ -22755,7 +23006,7 @@ import { Command as Command12 } from "commander";
|
|
|
22755
23006
|
import { readFileSync as readFileSync13 } from "fs";
|
|
22756
23007
|
import { basename as basename3 } from "path";
|
|
22757
23008
|
function syncCommand() {
|
|
22758
|
-
const cmd = new Command12("sync").description("
|
|
23009
|
+
const cmd = new Command12("sync").description("Sync info with the user (files, messages)");
|
|
22759
23010
|
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
23011
|
const agentId = resolveAgentId(opts);
|
|
22761
23012
|
const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
|
|
@@ -22781,6 +23032,42 @@ function syncCommand() {
|
|
|
22781
23032
|
process.exit(1);
|
|
22782
23033
|
}
|
|
22783
23034
|
});
|
|
23035
|
+
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) => {
|
|
23036
|
+
if (opts.message && opts.messageFile) {
|
|
23037
|
+
console.error("Error: --message and --message-file are mutually exclusive");
|
|
23038
|
+
process.exit(1);
|
|
23039
|
+
}
|
|
23040
|
+
let content;
|
|
23041
|
+
if (opts.messageFile) {
|
|
23042
|
+
try {
|
|
23043
|
+
content = readFileSync13(opts.messageFile, "utf-8");
|
|
23044
|
+
} catch (err) {
|
|
23045
|
+
console.error(`Error: cannot read file "${opts.messageFile}": ${err.message}`);
|
|
23046
|
+
process.exit(1);
|
|
23047
|
+
}
|
|
23048
|
+
} else {
|
|
23049
|
+
content = opts.message ?? "";
|
|
23050
|
+
}
|
|
23051
|
+
if (!content.trim()) {
|
|
23052
|
+
console.error("Error: --message or --message-file is required (and must not be empty)");
|
|
23053
|
+
process.exit(1);
|
|
23054
|
+
}
|
|
23055
|
+
const conversationId = opts.conversation_id || process.env.ALOOK_CONVERSATION_ID;
|
|
23056
|
+
if (!conversationId) {
|
|
23057
|
+
console.error("Error: no conversation id (set --conversation_id or $ALOOK_CONVERSATION_ID)");
|
|
23058
|
+
process.exit(1);
|
|
23059
|
+
}
|
|
23060
|
+
const agentId = resolveAgentId(opts);
|
|
23061
|
+
const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
|
|
23062
|
+
const client = new APIClient(serverUrl, token, workspaceId);
|
|
23063
|
+
try {
|
|
23064
|
+
const result = await client.postJSON(`/api/daemon/conversations/${encodeURIComponent(conversationId)}/messages`, { content, task_id: process.env.ALOOK_TASK_ID || undefined });
|
|
23065
|
+
printJSON(result);
|
|
23066
|
+
} catch (err) {
|
|
23067
|
+
console.error(`Error sending message: ${err.message}`);
|
|
23068
|
+
process.exit(1);
|
|
23069
|
+
}
|
|
23070
|
+
});
|
|
22784
23071
|
return cmd;
|
|
22785
23072
|
}
|
|
22786
23073
|
|
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;
|
|
@@ -18375,6 +18397,15 @@ Upload files for your owner to review in the app.
|
|
|
18375
18397
|
- 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
18398
|
- If you think user may need to know any file detail, use upload-artifact tool to send the file to user.
|
|
18377
18399
|
|
|
18400
|
+
### Talking to the user
|
|
18401
|
+
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.
|
|
18402
|
+
|
|
18403
|
+
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.
|
|
18404
|
+
|
|
18405
|
+
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.)
|
|
18406
|
+
|
|
18407
|
+
\`${cmdPrefix()} sync send-dm --message "…"\` (or \`--message-file <path>\` for long/markdown). The conversation is in $ALOOK_CONVERSATION_ID, so you usually need no flags. You can send several times in one task.
|
|
18408
|
+
|
|
18378
18409
|
### Attachments
|
|
18379
18410
|
When your task includes attachments, their local paths are listed in the prompt JSON under "attachments".
|
|
18380
18411
|
Use your Read tool to open them. Images and PDFs are read visually.
|
|
@@ -18394,6 +18425,15 @@ Recruit new colleague agents directly from the CLI. The server auto-generates a
|
|
|
18394
18425
|
- Example: '${cmdPrefix()} agent recruit --instructions "You are a QA engineer..." --relationship "DELEGATE when: code is ready for review"'
|
|
18395
18426
|
- Output: 'Recruited Felix (felix@alook.ai) — ag_xK9mPq2z'
|
|
18396
18427
|
- The new agent shares your runtime, is automatically linked as your colleague, and receives a welcome task.
|
|
18428
|
+
|
|
18429
|
+
Set or update the relationship with an EXISTING colleague (create-or-replace):
|
|
18430
|
+
- Run '${cmdPrefix()} agent link --to <handleOrId> --relationship "<DELEGATION_CRITERIA>"'
|
|
18431
|
+
- '--to <handleOrId>' — target agent by email handle ('coder' or 'coder@alook.ai') or agent id ('ag_...')
|
|
18432
|
+
- '--relationship <text>' — the delegation criteria both of you see (replaces it if a link already exists)
|
|
18433
|
+
- '--relationship-file <path>' — alternative: read relationship from a file (mutually exclusive with --relationship)
|
|
18434
|
+
- '--json' — output the link object incl. a 'created' boolean
|
|
18435
|
+
- Example: '${cmdPrefix()} agent link --to coder --relationship "DELEGATE when implementation is needed"'
|
|
18436
|
+
- Output: 'Linked Felix <-> coder (created)' (or '(updated)' when an existing link was replaced).
|
|
18397
18437
|
`;
|
|
18398
18438
|
content += `
|
|
18399
18439
|
### Calendar
|
|
@@ -18748,14 +18788,14 @@ function clearKillIntent(baseDir, taskId) {
|
|
|
18748
18788
|
}
|
|
18749
18789
|
|
|
18750
18790
|
// daemon/prompt.ts
|
|
18751
|
-
var DM_RESPONSE_NOTICE = "
|
|
18791
|
+
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.";
|
|
18752
18792
|
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
18793
|
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
18794
|
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
18795
|
function buildDmNotice(name, email3) {
|
|
18756
18796
|
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
18797
|
}
|
|
18758
|
-
function
|
|
18798
|
+
function buildTaskObject(task, attachments) {
|
|
18759
18799
|
const createdAt = new Date(task.createdAt);
|
|
18760
18800
|
const receivedAt = Number.isNaN(createdAt.getTime()) ? localISOString() : localISOString(createdAt);
|
|
18761
18801
|
const obj = {
|
|
@@ -18821,7 +18861,10 @@ function buildPrompt(task, attachments) {
|
|
|
18821
18861
|
filename: a.filename
|
|
18822
18862
|
}));
|
|
18823
18863
|
}
|
|
18824
|
-
return
|
|
18864
|
+
return obj;
|
|
18865
|
+
}
|
|
18866
|
+
function buildPrompt(task, attachments) {
|
|
18867
|
+
return JSON.stringify(buildTaskObject(task, attachments));
|
|
18825
18868
|
}
|
|
18826
18869
|
|
|
18827
18870
|
// daemon/session-runner.ts
|
|
@@ -18926,8 +18969,8 @@ async function runSession(input) {
|
|
|
18926
18969
|
await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, undefined, process.pid, provider, task.contextKey, input.logFilePath));
|
|
18927
18970
|
const { workDir, env } = prepare({ workspacesRoot, token }, task);
|
|
18928
18971
|
let killed = false;
|
|
18929
|
-
let agentPid
|
|
18930
|
-
let flushTimer
|
|
18972
|
+
let agentPid;
|
|
18973
|
+
let flushTimer;
|
|
18931
18974
|
const pendingMessages = [];
|
|
18932
18975
|
let seq = 0;
|
|
18933
18976
|
let toolCount = 0;
|
|
@@ -19013,7 +19056,7 @@ async function runSession(input) {
|
|
|
19013
19056
|
}
|
|
19014
19057
|
if (killed)
|
|
19015
19058
|
return;
|
|
19016
|
-
const prompt = buildPrompt(task, attachments);
|
|
19059
|
+
const prompt = input.promptOverride ?? buildPrompt(task, attachments);
|
|
19017
19060
|
const resumeSessionId = task.contextKey ? findResumableSessionByContextKey(timelineDir, task.contextKey, provider) ?? undefined : undefined;
|
|
19018
19061
|
if (resumeSessionId) {
|
|
19019
19062
|
log5.info(`resuming session ${resumeSessionId} (context_key: ${task.contextKey})`);
|
|
@@ -19038,6 +19081,8 @@ async function runSession(input) {
|
|
|
19038
19081
|
log5.info(JSON.stringify({ role: "user", type: "text", content: prompt }));
|
|
19039
19082
|
updateEntry(timelineDir, task.id, (entry) => {
|
|
19040
19083
|
entry.session_id = earlySessionId || null;
|
|
19084
|
+
if (earlySessionId)
|
|
19085
|
+
entry.agent_started = true;
|
|
19041
19086
|
});
|
|
19042
19087
|
flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
|
|
19043
19088
|
const INACTIVITY_TIMEOUT_MS = messageInactivityTimeout ?? 5 * 60 * 1000;
|
|
@@ -19179,5 +19224,7 @@ if (isDirectExecution) {
|
|
|
19179
19224
|
export {
|
|
19180
19225
|
writeMarkerFile,
|
|
19181
19226
|
runSession,
|
|
19182
|
-
reportToServer
|
|
19227
|
+
reportToServer,
|
|
19228
|
+
downloadAttachments,
|
|
19229
|
+
cleanupAttachments
|
|
19183
19230
|
};
|