@anhlp/alp-runtime 0.0.0-beta.2 → 0.0.1-beta.3
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/claude-transport.d.ts +3 -0
- package/dist/claude-transport.js +59 -8
- package/dist/contracts.d.ts +2 -1
- package/dist/runtime-context.d.ts +1 -1
- package/dist/runtime-context.js +1 -1
- package/dist/runtime.js +244 -122
- package/dist/session-config.d.ts +24 -0
- package/dist/session-config.js +3 -3
- package/dist/session-state.d.ts +8 -1
- package/dist/tool-detail.js +4 -1
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +7 -5
- package/package.json +3 -3
- package/src/claude-transport.ts +64 -8
- package/src/contracts.ts +2 -2
- package/src/runtime-context.ts +2 -2
- package/src/runtime.ts +261 -115
- package/src/session-config.ts +5 -2
- package/src/session-state.ts +8 -1
- package/src/tool-detail.ts +4 -1
- package/src/tools.ts +6 -4
package/dist/runtime.js
CHANGED
|
@@ -47,6 +47,7 @@ import { addGate, checkGates, resolveGate, blockersOf, childrenOf, closeTask, ep
|
|
|
47
47
|
var MAX_WAKES = 8;
|
|
48
48
|
var USER_REPLY_MS = 6e4;
|
|
49
49
|
var USER_WROTE = "The user just wrote to you; their message follows. Answer them first in a short message they can read (tool calls alone show them nothing), and steer an assignment with alp_send when their words change its brief. Your assignments keep running: then alp_wait again, or end your turn and ALP wakes you with their results.";
|
|
50
|
+
var TURN_ENDED = "Turn ended before the result arrived. Running assignments keep running; ALP wakes you with their results as mail.";
|
|
50
51
|
var CHECKED_IN = "Not a result: the assignment keeps running. Act on this mail first (a steer from your requester or the user's words may change what your assignments should do: pass that on with alp_send), then alp_wait again or end your turn.";
|
|
51
52
|
var RESTART_LIMIT = 3;
|
|
52
53
|
var RESTART_WINDOW_MS = 10 * 6e4;
|
|
@@ -139,11 +140,12 @@ function createAlpRuntime(options = {}) {
|
|
|
139
140
|
...session.parent ? { parentId: session.parent } : {},
|
|
140
141
|
...session.toolCallId ? { toolCallId: session.toolCallId } : {},
|
|
141
142
|
...session.active ? { activeTurnId: session.active } : {},
|
|
142
|
-
|
|
143
|
+
// turn.ended listeners persist the intent before terminal() finishes parking it.
|
|
144
|
+
...session.parked?.reason ?? session.parkReason ? { parked: session.parked?.reason ?? session.parkReason } : {},
|
|
143
145
|
...session.instructionsSha ? { instructionsSha: session.instructionsSha } : {},
|
|
144
146
|
...takesImages(session) ? { promptImages: true } : {},
|
|
145
147
|
...session.usage ? { usage: session.usage } : {},
|
|
146
|
-
busy: !!(session.active || session.pending || session.children.size || session.assignments.size || hasActiveMail(session) || reviewing(session))
|
|
148
|
+
busy: !!(session.active || session.pending || session.parked || session.parkReason || session.children.size || session.assignments.size || hasActiveMail(session) || reviewing(session))
|
|
147
149
|
};
|
|
148
150
|
}
|
|
149
151
|
function terminal(sessionId, session, state, error) {
|
|
@@ -161,7 +163,7 @@ function createAlpRuntime(options = {}) {
|
|
|
161
163
|
clearTimeout(waiter.timer);
|
|
162
164
|
waiter.resolve(null);
|
|
163
165
|
}
|
|
164
|
-
cancelQuestions(sessionId);
|
|
166
|
+
cancelQuestions(sessionId, true);
|
|
165
167
|
if (session.parent) sessions.get(session.parent)?.assignments.get(sessionId)?.ask?.resolve(toolResult(false, { error: "Turn ended" }));
|
|
166
168
|
session.mail = session.mail.filter((event) => !(event.deliveredTurn === turnId && state === "completed"));
|
|
167
169
|
for (const event of session.mail) {
|
|
@@ -319,7 +321,7 @@ function createAlpRuntime(options = {}) {
|
|
|
319
321
|
if (method === "turn/completed" && params.turn.id === session.active) {
|
|
320
322
|
const state = params.turn.status === "completed" ? "completed" : params.turn.status === "interrupted" ? "canceled" : "failed";
|
|
321
323
|
if (state === "failed" && LIMIT_ERRORS.includes(params.turn.error?.codexErrorInfo)) {
|
|
322
|
-
if (session.
|
|
324
|
+
if (session.role !== "supervisor") session.parkReason = `the ${label(session.runtimeKind)} usage limit was reached`;
|
|
323
325
|
void limitReached(session, params.turn.error);
|
|
324
326
|
}
|
|
325
327
|
terminal(
|
|
@@ -335,6 +337,7 @@ function createAlpRuntime(options = {}) {
|
|
|
335
337
|
if (!session || session.closed) return;
|
|
336
338
|
session.closed = true;
|
|
337
339
|
cancelQuestions(sessionId);
|
|
340
|
+
approvalLedger.delete(sessionId);
|
|
338
341
|
releaseClaims(sessionId, session.mapping.agent.projectRoot);
|
|
339
342
|
await Promise.all(
|
|
340
343
|
[...session.children, ...session.supervisor ? [session.supervisor] : []].map(closeSession)
|
|
@@ -601,7 +604,7 @@ ${output.slice(0, 4e3)}` : ""}`;
|
|
|
601
604
|
const root = sessions.get(rootId);
|
|
602
605
|
const watcher = root?.mapping.team?.supervisor ? root.mapping.team.supervisor.agent : "supervisor";
|
|
603
606
|
if (!root?.supervisor || entry.from === "supervisor" || entry.agent === watcher) return;
|
|
604
|
-
const line = entry.event === "assignment.started" ? `${entry.parentAgent} \u2192 ${entry.agent} assignment ${entry.assignmentId}${entry.taskId ? ` for task ${entry.taskId}` : ""} (${entry.model}, ${entry.thinking ?? "default effort"}, ${entry.mode}, ${entry.isolation}${entry.wait ? "" : ", async"}): ${clip(entry.task)}` : entry.event === "assignment.finished" ? `${entry.agent} assignment ${entry.assignmentId} ${entry.status}${entry.handoff ? `, handoff ${entry.handoff.outcome}${entry.handoff.verdict ? `, ${verdictLine(entry.handoff.verdict)}` : ""}: ${clip(entry.handoff.summary)}` : ", no handoff"}${entry.error ? ` (${clip(entry.error, 200)})` : ""}${entry.handoff?.discovered?.length ? `; discovered: ${clip(entry.handoff.discovered.join("; "))}` : ""}` : entry.event === "mail" && entry.kind !== "result" && entry.kind !== "board" ? `mail ${entry.kind} from ${entry.from}${entry.replyTo ? ` (reply to ${entry.replyTo})` : ""}: ${clip(entry.body ?? "")}` : entry.event === "human.question" ? `${entry.agent} asked the user: ${clip(entry.body)}` : entry.event === "human.answer" ? `user ${entry.outcome} ${entry.questionId}${entry.answer ? `: ${clip(entry.answer)}` : ""}` : entry.event === "board.pin" ? `${entry.agent} pinned ${entry.kind}${entry.paths ? ` ${entry.paths.join(", ")}` : ""}: ${clip(entry.body)}` : entry.event === "lesson" ? `${entry.agent} recorded a ${entry.scope} lesson: ${clip(entry.lesson)}` : entry.event === "skill" && Array.isArray(entry.roles) ? `${entry.agent} saved skill ${entry.name} for ${entry.roles.join(", ")} with the user's approval` : entry.event === "task" ? `${entry.agent} ${TASK_PAST[entry.action] ?? entry.action} task ${entry.id} "${clip(entry.title, 120)}"${entry.status ? ` (now ${entry.status})` : ""}${entry.detail ? `: ${clip(entry.detail)}` : ""}` : entry.event === "epic.landed" ? `${entry.agent} closed ${entry.id} "${clip(entry.title, 120)}": ${entry.tasks} tasks, ${entry.reworked} reworked, ${entry.unverified?.length ?? 0} closed unverified` : entry.event === "recall" ? `${entry.agent} recalled ${entry.recalled} assignment ${entry.assignmentId}: ${clip(entry.question)}` : entry.event === "issue" ? `${entry.action === "create" ? `opened issue "${clip(entry.title, 200)}"` : `commented on issue #${entry.issue}`} in ${entry.repo} with the user's approval: ${entry.url}` : entry.event?.startsWith("worktree.") ? `${entry.event.slice("worktree.".length)} worktree of ${entry.assignmentId} (${entry.branch})${entry.status ? `: ${entry.status}` : ""}` : void 0;
|
|
607
|
+
const line = entry.event === "assignment.started" ? `${entry.parentAgent} \u2192 ${entry.agent} assignment ${entry.assignmentId}${entry.taskId ? ` for task ${entry.taskId}` : ""} (${entry.model}, ${entry.thinking ?? "default effort"}, ${entry.mode}, ${entry.isolation}${entry.wait ? "" : ", async"}): ${clip(entry.task)}` : entry.event === "assignment.finished" ? `${entry.agent} assignment ${entry.assignmentId} ${entry.status}${entry.handoff ? `, handoff ${entry.handoff.outcome}${entry.handoff.verdict ? `, ${verdictLine(entry.handoff.verdict)}` : ""}: ${clip(entry.handoff.summary)}` : ", no handoff"}${entry.error ? ` (${clip(entry.error, 200)})` : ""}${entry.handoff?.discovered?.length ? `; discovered: ${clip(entry.handoff.discovered.join("; "))}` : ""}` : entry.event === "mail" && entry.kind !== "result" && entry.kind !== "board" ? `mail ${entry.kind} from ${entry.from}${entry.replyTo ? ` (reply to ${entry.replyTo})` : ""}: ${clip(entry.body ?? "")}` : entry.event === "human.question" ? `${entry.agent} asked the user: ${clip(entry.body)}` : entry.event === "human.answer" ? `user ${entry.outcome} ${entry.questionId}${entry.answer ? `: ${clip(entry.answer)}` : ""}` : entry.event === "board.pin" ? `${entry.agent} pinned ${entry.kind}${entry.paths ? ` ${entry.paths.join(", ")}` : ""}: ${clip(entry.body)}` : entry.event === "lesson" ? `${entry.agent} recorded a ${entry.scope} lesson: ${clip(entry.lesson)}` : entry.event === "skill" && Array.isArray(entry.roles) ? `${entry.agent} saved skill ${entry.name} for ${entry.roles.join(", ")} with the user's approval` : entry.event === "task" ? `${entry.agent} ${TASK_PAST[entry.action] ?? entry.action} task ${entry.id} "${clip(entry.title, 120)}"${entry.status ? ` (now ${entry.status})` : ""}${entry.detail ? `: ${clip(entry.detail)}` : ""}` : entry.event === "epic.landed" ? `${entry.agent} closed ${entry.id} "${clip(entry.title, 120)}": ${entry.tasks} tasks, ${entry.reworked} reworked, ${entry.unverified?.length ?? 0} closed unverified` : entry.event === "recall" ? `${entry.agent} recalled ${entry.recalled} assignment ${entry.assignmentId}: ${clip(entry.question)}` : entry.event === "skill.approval" && !entry.saved ? `skill approval ${entry.approvalId} for ${entry.name}: not saved (${entry.error ?? entry.outcome})` : entry.event === "issue.approval" ? `issue approval ${entry.approvalId} in ${entry.repo}: ${entry.posted ? entry.url : entry.error ?? entry.outcome}` : entry.event === "issue" ? `${entry.action === "create" ? `opened issue "${clip(entry.title, 200)}"` : `commented on issue #${entry.issue}`} in ${entry.repo} with the user's approval: ${entry.url}` : entry.event?.startsWith("worktree.") ? `${entry.event.slice("worktree.".length)} worktree of ${entry.assignmentId} (${entry.branch})${entry.status ? `: ${entry.status}` : ""}` : void 0;
|
|
605
608
|
if (line) root.journal.push(stamp(line));
|
|
606
609
|
}
|
|
607
610
|
const eventful = (journal) => journal.some((line) => !unstamped(line).startsWith("turn ended:"));
|
|
@@ -1036,20 +1039,34 @@ Review the process against ALP.md, ${agent}'s AGENT.md and the recorded lessons.
|
|
|
1036
1039
|
else post(session.parent, { kind: "stalled", from: assignment.agent, assignment: sessionId, body: `Parked: ${session.parked.reason}. ALP continues this assignment where it stopped when ${label(session.runtimeKind)} is resumed (${options.autoResume !== false && (pauseOf(session.runtimeKind)?.by ?? "alpd") === "alpd" ? `ALP resumes it as soon as ${label(session.runtimeKind)} says the limit lifted` : "the user runs alp resume"}). Wait for it, start other work, or stop it with alp_cancel and give the rest to an agent on another runtime (pass model claude:\u2026 or codex:\u2026); what it changed stays in your checkout or its worktree.` });
|
|
1037
1040
|
}
|
|
1038
1041
|
function continueParked(sessionId, session) {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1042
|
+
void enqueue(async () => {
|
|
1043
|
+
if (session.closed || session.active || session.pending || !session.parked || pauseOf(session.runtimeKind)) return;
|
|
1044
|
+
const parked = session.parked;
|
|
1045
|
+
const { reason, prompt } = parked;
|
|
1046
|
+
session.parked = void 0;
|
|
1047
|
+
session.wakeHeld = false;
|
|
1048
|
+
session.lastActivity = Date.now();
|
|
1049
|
+
const parent = session.parent ? sessions.get(session.parent) : void 0;
|
|
1050
|
+
const assignment = parent?.assignments.get(sessionId);
|
|
1051
|
+
runLog(rootOf(sessionId), { event: "assignment.resumed", assignmentId: sessionId, agent: session.mapping.agent.name });
|
|
1052
|
+
if (assignment) emit(session.parent, { type: "assignment", assignment: assignmentSnapshot(assignment, "running") });
|
|
1053
|
+
try {
|
|
1054
|
+
await startPrompt(sessionId, {
|
|
1055
|
+
clientMessageId: `alp-resume-${randomUUID()}`,
|
|
1056
|
+
delivery: "auto",
|
|
1057
|
+
content: [{ type: "text", text: prompt ?? (session.parent ? `ALP resumed this assignment; it had stopped because ${reason}. Continue where you left off: your earlier work in this session and its files are intact. Finish with alp_handoff as before.` : `ALP resumed this session; it had stopped because ${reason}. Continue the user's request where you left off: your earlier work in this thread and its files are intact.`) }]
|
|
1058
|
+
}, session.parent ? "assignment" : "wake");
|
|
1059
|
+
session.resumedTurn = session.active;
|
|
1060
|
+
emit(sessionId, { type: "session.updated", session: snapshot(sessionId, session) });
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
if (!session.closed && !session.active) {
|
|
1063
|
+
session.parked = parked;
|
|
1064
|
+
emit(sessionId, { type: "session.updated", session: snapshot(sessionId, session) });
|
|
1065
|
+
if (assignment) emit(session.parent, { type: "assignment", assignment: assignmentSnapshot(assignment, "parked") });
|
|
1066
|
+
}
|
|
1067
|
+
notice("warning", `Could not resume ${session.mapping.agent.name}: ${errorData(error).message}`);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1053
1070
|
}
|
|
1054
1071
|
async function limitReset(session, error) {
|
|
1055
1072
|
const at = (seconds) => typeof seconds === "number" && seconds > 0 ? new Date(seconds * 1e3).toISOString() : void 0;
|
|
@@ -1066,8 +1083,9 @@ Review the process against ALP.md, ${agent}'s AGENT.md and the recorded lessons.
|
|
|
1066
1083
|
}
|
|
1067
1084
|
async function limitReached(session, error) {
|
|
1068
1085
|
const kind = session.runtimeKind;
|
|
1069
|
-
if (
|
|
1086
|
+
if (pauseOf(kind)) return;
|
|
1070
1087
|
const resetsAt = await limitReset(session, error);
|
|
1088
|
+
if (closed || session.closed || pauseOf(kind)) return;
|
|
1071
1089
|
const pause = { since: (/* @__PURE__ */ new Date()).toISOString(), by: "alpd", reason: `${label(kind)} usage limit reached`, ...resetsAt ? { resetsAt } : {} };
|
|
1072
1090
|
pauseRuntime(kind, pause);
|
|
1073
1091
|
const other = RUNTIMES.find((candidate) => candidate !== kind && !pauseOf(candidate));
|
|
@@ -1208,10 +1226,10 @@ ${cut(session.brief, BRIEF_CHARS)}`);
|
|
|
1208
1226
|
function lessonFiles(mapping) {
|
|
1209
1227
|
return [path.join(mapping.agent.projectRoot, ".alp", LESSONS_FILE), ...options.libraryDir ? [path.join(options.libraryDir, LESSONS_FILE)] : []];
|
|
1210
1228
|
}
|
|
1211
|
-
async function confirm(sessionId, session, body) {
|
|
1229
|
+
async function confirm(sessionId, session, body, approval) {
|
|
1212
1230
|
if ([...userQuestions.values()].some((pending) => pending.question.sessionId === sessionId)) return { approved: false, outcome: "busy", feedback: "A question is already waiting for the user" };
|
|
1213
1231
|
const say = wordsFor(session);
|
|
1214
|
-
const result = await askUser(sessionId, session, void 0, body, [say.approve, say.reject]);
|
|
1232
|
+
const result = await askUser(sessionId, session, void 0, body, [say.approve, say.reject], approval);
|
|
1215
1233
|
let value;
|
|
1216
1234
|
try {
|
|
1217
1235
|
value = JSON.parse(result.contentItems[0].text);
|
|
@@ -1222,25 +1240,45 @@ ${cut(session.brief, BRIEF_CHARS)}`);
|
|
|
1222
1240
|
const answer = String(value.answer).trim();
|
|
1223
1241
|
return APPROVALS.includes(answer.toLowerCase()) ? { approved: true } : { approved: false, outcome: "answered", feedback: answer };
|
|
1224
1242
|
}
|
|
1225
|
-
const declined = (decision) => toolResult(true, {
|
|
1226
|
-
approved: false,
|
|
1227
|
-
outcome: decision.outcome,
|
|
1228
|
-
...decision.feedback ? { feedback: decision.feedback } : {},
|
|
1229
|
-
next: "Nothing was saved or posted. Revise from the feedback and propose again, or drop it."
|
|
1230
|
-
});
|
|
1231
1243
|
const text = (value, limit) => typeof value === "string" && !!value.trim() && value.length <= limit;
|
|
1244
|
+
const approvalCancellation = /* @__PURE__ */ new WeakMap();
|
|
1245
|
+
const approvalLedger = /* @__PURE__ */ new Map();
|
|
1246
|
+
const approvalsOf = (sessionId) => {
|
|
1247
|
+
let approvals = approvalLedger.get(sessionId);
|
|
1248
|
+
if (!approvals) approvalLedger.set(sessionId, approvals = /* @__PURE__ */ new Map());
|
|
1249
|
+
return approvals;
|
|
1250
|
+
};
|
|
1251
|
+
const approvalCanceled = (session, cancellation) => closed || session.closed || cancellation !== (approvalCancellation.get(session) ?? 0);
|
|
1232
1252
|
async function skillTool(sessionId, session, args) {
|
|
1233
|
-
|
|
1253
|
+
const cancellation = approvalCancellation.get(session) ?? 0;
|
|
1254
|
+
args = structuredClone(args);
|
|
1255
|
+
if (!plainObject(args, ["name", "description", "body", "roles", "lessons", "replace", "requestId"]) || typeof args.name !== "string" || !/^[a-z0-9][a-z0-9-]{1,62}$/.test(args.name) || !text(args.description, 300) || !text(args.body, SKILL_BODY_CHARS) || !Array.isArray(args.roles) || !args.roles.length || args.roles.length > 10 || !args.roles.every((role) => typeof role === "string" && /^[\w.-]+$/.test(role) && role !== "." && role !== "..") || args.lessons !== void 0 && (!Array.isArray(args.lessons) || args.lessons.length > 50 || !args.lessons.every((lesson) => text(lesson, LESSON_CHARS))) || args.replace !== void 0 && typeof args.replace !== "boolean") {
|
|
1234
1256
|
return toolResult(false, { error: `A skill needs a name (lowercase letters, digits, hyphens), a one-line description, a body of at most ${SKILL_BODY_CHARS} characters, and the roles that get it` });
|
|
1235
1257
|
}
|
|
1236
|
-
if (!
|
|
1237
|
-
const
|
|
1258
|
+
if (args.requestId !== void 0 && !text(args.requestId, 100)) return toolResult(false, { error: "requestId must be a nonempty string of at most 100 characters" });
|
|
1259
|
+
const library = options.libraryDir;
|
|
1260
|
+
if (!library) return toolResult(false, { error: "This host keeps no user skill library" });
|
|
1261
|
+
const known = /* @__PURE__ */ new Set([...STARTER_ROLES, ...await discoverAgents(session.mapping.agent.projectRoot, { library, templates: options.templates })]);
|
|
1238
1262
|
const unknown = args.roles.filter((role) => !known.has(role));
|
|
1239
1263
|
if (unknown.length) return toolResult(false, { error: `Unknown roles: ${unknown.join(", ")}. Roles are ${[...known].join(", ")}` });
|
|
1240
|
-
const file = path.join(
|
|
1264
|
+
const file = path.join(library, "skills", args.name, "SKILL.md");
|
|
1265
|
+
const content = `---
|
|
1266
|
+
name: ${args.name}
|
|
1267
|
+
description: ${JSON.stringify(args.description.replace(/\s+/g, " ").trim())}
|
|
1268
|
+
---
|
|
1269
|
+
|
|
1270
|
+
${args.body.trim()}
|
|
1271
|
+
`;
|
|
1272
|
+
const lessons = (args.lessons ?? []).map((lesson) => lesson.replace(/\s+/g, " ").trim());
|
|
1273
|
+
const draftKey = JSON.stringify(["skill", content, args.roles, lessons, !!args.replace]);
|
|
1274
|
+
const key = `skill:${args.requestId ?? draftKey}`;
|
|
1275
|
+
const approvals = approvalsOf(sessionId);
|
|
1276
|
+
const previous = approvals.get(key);
|
|
1277
|
+
if (previous && previous.draftKey !== draftKey) return toolResult(false, { error: "requestId was already used for a different draft" });
|
|
1278
|
+
if (previous) return toolResult(true, previous.result ?? { status: "pending_approval", approvalId: previous.approvalId, file });
|
|
1241
1279
|
const existing = await readFile(file, "utf8").catch(() => void 0);
|
|
1242
1280
|
if (existing !== void 0 && !args.replace) return toolResult(false, { error: `The library already has a skill named ${args.name} (${file}); read it, then pass replace: true to propose a new version, or choose another name` });
|
|
1243
|
-
const rolesFile = path.join(
|
|
1281
|
+
const rolesFile = path.join(library, "role-skills.json");
|
|
1244
1282
|
const readRoles = async () => {
|
|
1245
1283
|
const raw = await readFile(rolesFile, "utf8").catch(() => "{}");
|
|
1246
1284
|
const roles = JSON.parse(raw);
|
|
@@ -1252,62 +1290,77 @@ ${cut(session.brief, BRIEF_CHARS)}`);
|
|
|
1252
1290
|
} catch (error) {
|
|
1253
1291
|
return toolResult(false, { error: errorData(error).message });
|
|
1254
1292
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
${args.body.trim()}
|
|
1261
|
-
`;
|
|
1262
|
-
const lessons = (args.lessons ?? []).map((lesson) => lesson.replace(/\s+/g, " ").trim());
|
|
1293
|
+
if (approvalCanceled(session, cancellation)) return toolResult(false, { error: "Skill proposal canceled or session closed; no approval was created" });
|
|
1294
|
+
if ([...userQuestions.values()].some((pending) => pending.question.sessionId === sessionId)) return toolResult(false, { error: "A question is already waiting for the user; finish it before proposing a skill" });
|
|
1295
|
+
const approvalId = `q-${randomUUID()}`;
|
|
1296
|
+
const record2 = { approvalId, draftKey };
|
|
1297
|
+
approvals.set(key, record2);
|
|
1263
1298
|
const say = wordsFor(session);
|
|
1264
|
-
const decision =
|
|
1299
|
+
const decision = confirm(
|
|
1265
1300
|
sessionId,
|
|
1266
1301
|
session,
|
|
1267
1302
|
say.skill(session.mapping.agent.name, existing !== void 0, file, args.roles.join(", "), lessons) + `
|
|
1268
1303
|
${say.answerLine(session.mapping.agent.name)}
|
|
1269
1304
|
|
|
1270
|
-
${content}
|
|
1305
|
+
${content}`,
|
|
1306
|
+
{ id: approvalId }
|
|
1271
1307
|
);
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const
|
|
1291
|
-
const
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
+
const save = async () => {
|
|
1309
|
+
if (existing === void 0 && !args.replace && await readFile(file, "utf8").then(() => true, () => false)) {
|
|
1310
|
+
return { approved: true, saved: false, error: `A skill named ${args.name} was saved meanwhile (${file}); nothing was overwritten`, next: "Read it, then propose again with replace: true if yours should replace it." };
|
|
1311
|
+
}
|
|
1312
|
+
try {
|
|
1313
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
1314
|
+
await writeFile(file, content);
|
|
1315
|
+
const roles = await readRoles();
|
|
1316
|
+
for (const role of args.roles) {
|
|
1317
|
+
const list = Array.isArray(roles[role]) ? roles[role] : [];
|
|
1318
|
+
if (!list.includes(args.name)) list.push(args.name);
|
|
1319
|
+
roles[role] = list;
|
|
1320
|
+
}
|
|
1321
|
+
await writeFile(rolesFile, JSON.stringify(roles, null, 2) + "\n");
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
return { approved: true, saved: false, error: errorData(error).message, next: "Tell the user; nothing is retried automatically." };
|
|
1324
|
+
}
|
|
1325
|
+
const removed = /* @__PURE__ */ new Set();
|
|
1326
|
+
for (const lessonFile of lessonFiles(session.mapping)) {
|
|
1327
|
+
const current = await readFile(lessonFile, "utf8").catch(() => void 0);
|
|
1328
|
+
if (current === void 0) continue;
|
|
1329
|
+
const kept = current.split("\n").filter((line) => {
|
|
1330
|
+
const match = /^- \d{4}-\d\d-\d\d: (.*)$/.exec(line);
|
|
1331
|
+
if (!match || !lessons.includes(match[1].trim())) return true;
|
|
1332
|
+
removed.add(match[1].trim());
|
|
1333
|
+
return false;
|
|
1334
|
+
});
|
|
1335
|
+
if (kept.length !== current.split("\n").length) await writeFile(lessonFile, kept.join("\n")).catch(() => {
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
runLog(rootOf(sessionId), { event: "skill", agent: session.mapping.agent.name, name: args.name, roles: args.roles, replaced: existing !== void 0, lessons: [...removed] });
|
|
1339
|
+
return {
|
|
1340
|
+
approved: true,
|
|
1341
|
+
saved: file,
|
|
1342
|
+
roles: args.roles,
|
|
1343
|
+
lessonsRemoved: [...removed],
|
|
1344
|
+
...lessons.length > removed.size ? { lessonsNotFound: lessons.filter((lesson) => !removed.has(lesson)) } : {},
|
|
1345
|
+
next: "New sessions of those roles list the skill; read the file now if you need it in this one."
|
|
1346
|
+
};
|
|
1347
|
+
};
|
|
1348
|
+
void (async () => {
|
|
1349
|
+
const answer = await decision;
|
|
1350
|
+
const canceled = approvalCanceled(session, cancellation);
|
|
1351
|
+
const result = !answer.approved || canceled ? { approved: false, saved: false, outcome: canceled ? "canceled" : answer.outcome, ...answer.feedback ? { feedback: answer.feedback } : {}, next: "Nothing was saved. Revise from the feedback and propose again with a fresh requestId, or drop it." } : await save();
|
|
1352
|
+
record2.result = { status: "resolved", approvalId, name: args.name, ...result };
|
|
1353
|
+
runLog(rootOf(sessionId), { event: "skill.approval", agent: session.mapping.agent.name, ...record2.result });
|
|
1354
|
+
if (!closed && !session.closed && sessions.get(sessionId) === session) {
|
|
1355
|
+
post(sessionId, { kind: "result", from: "ALP", assignment: approvalId, body: `Skill approval result: ${JSON.stringify(record2.result)}` });
|
|
1356
|
+
}
|
|
1357
|
+
})();
|
|
1358
|
+
return toolResult(true, { status: "pending_approval", approvalId, file, next: "Continue other work or end your turn. ALP sends the user's decision, and whether the skill was saved, by mail; do not resubmit this draft." });
|
|
1308
1359
|
}
|
|
1309
1360
|
async function issueTool(sessionId, session, args) {
|
|
1310
|
-
|
|
1361
|
+
const cancellation = approvalCancellation.get(session) ?? 0;
|
|
1362
|
+
args = structuredClone(args);
|
|
1363
|
+
if (!plainObject(args, ["action", "target", "query", "title", "body", "issue", "labels", "requestId"]) || !["search", "create", "comment"].includes(args.action) || !["project", "alp"].includes(args.target)) {
|
|
1311
1364
|
return toolResult(false, { error: "An issue call needs action (search, create or comment) and target (project or alp)" });
|
|
1312
1365
|
}
|
|
1313
1366
|
const project = session.mapping.agent.projectRoot;
|
|
@@ -1333,6 +1386,7 @@ ${content}`
|
|
|
1333
1386
|
if (args.labels !== void 0 && (args.action !== "create" || !Array.isArray(args.labels) || args.labels.length > 10 || !args.labels.every((label2) => text(label2, 50)))) {
|
|
1334
1387
|
return toolResult(false, { error: "labels are up to 10 existing label names, for create" });
|
|
1335
1388
|
}
|
|
1389
|
+
if (args.requestId !== void 0 && !text(args.requestId, 100)) return toolResult(false, { error: "requestId must be a nonempty string of at most 100 characters" });
|
|
1336
1390
|
if (args.action === "create" && args.labels?.length) {
|
|
1337
1391
|
try {
|
|
1338
1392
|
const labels = JSON.parse(await github(["label", "list", "-R", repo, "--limit", "1000", "--json", "name"], { cwd: project }));
|
|
@@ -1343,9 +1397,20 @@ ${content}`
|
|
|
1343
1397
|
return toolResult(false, { repo, error: `Could not validate issue labels: ${errorData(error).message}` });
|
|
1344
1398
|
}
|
|
1345
1399
|
}
|
|
1400
|
+
if (approvalCanceled(session, cancellation)) return toolResult(false, { error: "Issue proposal canceled or session closed; no approval was created" });
|
|
1401
|
+
const draftKey = JSON.stringify([repo, args.action, args.title ?? null, args.issue ?? null, args.body, [...args.labels ?? []].sort()]);
|
|
1402
|
+
const key = `issue:${args.requestId ?? draftKey}`;
|
|
1403
|
+
const approvals = approvalsOf(sessionId);
|
|
1404
|
+
const previous = approvals.get(key);
|
|
1405
|
+
if (previous && previous.draftKey !== draftKey) return toolResult(false, { error: "requestId was already used for a different draft" });
|
|
1406
|
+
if (previous) return toolResult(true, previous.result ?? { status: "pending_approval", approvalId: previous.approvalId, repo });
|
|
1407
|
+
if ([...userQuestions.values()].some((pending) => pending.question.sessionId === sessionId)) return toolResult(false, { error: "A question is already waiting for the user; finish it before proposing another issue draft" });
|
|
1408
|
+
const approvalId = `q-${randomUUID()}`;
|
|
1409
|
+
const record2 = { approvalId, draftKey };
|
|
1410
|
+
approvals.set(key, record2);
|
|
1346
1411
|
const where = `${repo}${args.target === "alp" ? " (ALP itself)" : ""}`;
|
|
1347
1412
|
const say = wordsFor(session);
|
|
1348
|
-
const decision =
|
|
1413
|
+
const decision = confirm(
|
|
1349
1414
|
sessionId,
|
|
1350
1415
|
session,
|
|
1351
1416
|
say.issue(session.mapping.agent.name, args.action === "create", args.issue, where) + `
|
|
@@ -1354,18 +1419,31 @@ ${say.answerLine(session.mapping.agent.name)}
|
|
|
1354
1419
|
` + (args.action === "create" ? `${say.issueTitle}: ${args.title}
|
|
1355
1420
|
${args.labels?.length ? `${say.issueLabels}: ${args.labels.join(", ")}
|
|
1356
1421
|
` : ""}
|
|
1357
|
-
` : "") + args.body
|
|
1422
|
+
` : "") + args.body.trim() + ISSUE_FOOTER,
|
|
1423
|
+
{ id: approvalId }
|
|
1358
1424
|
);
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1425
|
+
void (async () => {
|
|
1426
|
+
const answer = await decision;
|
|
1427
|
+
let result;
|
|
1428
|
+
const canceled = approvalCanceled(session, cancellation);
|
|
1429
|
+
if (!answer.approved || canceled) {
|
|
1430
|
+
result = { approved: false, posted: false, outcome: canceled ? "canceled" : answer.outcome, ...answer.feedback ? { feedback: answer.feedback } : {} };
|
|
1431
|
+
} else {
|
|
1432
|
+
try {
|
|
1433
|
+
const body = args.body.trim() + ISSUE_FOOTER;
|
|
1434
|
+
const output = args.action === "create" ? await github(["issue", "create", "-R", repo, "--title", args.title, "--body-file", "-", ...(args.labels ?? []).flatMap((label2) => ["--label", label2])], { cwd: project, input: body }) : await github(["issue", "comment", String(args.issue), "-R", repo, "--body-file", "-"], { cwd: project, input: body });
|
|
1435
|
+
result = { approved: true, posted: true, url: output.trim().split("\n").at(-1) ?? "" };
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
result = { approved: true, posted: false, error: errorData(error).message, next: "Tell the user; do not retry this draft automatically: GitHub may have accepted it before reporting an error." };
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
record2.result = { status: "resolved", approvalId, repo, ...result };
|
|
1441
|
+
runLog(rootOf(sessionId), { event: "issue.approval", action: args.action, ...args.title ? { title: args.title } : {}, ...args.issue ? { issue: args.issue } : {}, ...record2.result });
|
|
1442
|
+
if (!closed && !session.closed && sessions.get(sessionId) === session) {
|
|
1443
|
+
post(sessionId, { kind: "result", from: "ALP", assignment: approvalId, body: `Issue approval result: ${JSON.stringify(record2.result)}` });
|
|
1444
|
+
}
|
|
1445
|
+
})();
|
|
1446
|
+
return toolResult(true, { status: "pending_approval", approvalId, repo, next: "Continue other work. ALP sends the decision and posting result by mail; do not resubmit this draft." });
|
|
1369
1447
|
}
|
|
1370
1448
|
function touch(sessionId, session) {
|
|
1371
1449
|
const now = Date.now();
|
|
@@ -1389,6 +1467,8 @@ ${args.labels?.length ? `${say.issueLabels}: ${args.labels.join(", ")}
|
|
|
1389
1467
|
if (session && !session.closed) {
|
|
1390
1468
|
emit(sessionId, { type: "mail", mail: publicEvent(mail) });
|
|
1391
1469
|
session.mail.push(mail);
|
|
1470
|
+
if (mail.kind === "result" || mail.kind === "question") session.wakes = 0;
|
|
1471
|
+
if (mail.kind === "result" || mail.kind === "question" || mail.kind === "checkin") session.wakeFailures = 0;
|
|
1392
1472
|
deliver(sessionId, session);
|
|
1393
1473
|
}
|
|
1394
1474
|
return mail;
|
|
@@ -1411,7 +1491,7 @@ ${args.labels?.length ? `${say.issueLabels}: ${args.labels.join(", ")}
|
|
|
1411
1491
|
if (session.mail.some((event) => !event.deliveredTurn && !event.passive && !event.defer)) void steerMail(sessionId, session);
|
|
1412
1492
|
} else if (!waking) {
|
|
1413
1493
|
return;
|
|
1414
|
-
} else if (!session.wakeBlocked && session.wakes < MAX_WAKES) {
|
|
1494
|
+
} else if (!session.wakeBlocked && session.wakeFailures < MAX_WAKES && (session.wakes < MAX_WAKES || session.mail.some((event) => !event.deliveredTurn && event.kind === "checkin"))) {
|
|
1415
1495
|
autoWake(sessionId, session);
|
|
1416
1496
|
} else if (session.parent && !session.wakeBlocked && !held(session)) {
|
|
1417
1497
|
session.settle?.("failed", `Wake limit (${MAX_WAKES}) reached with mail outstanding`);
|
|
@@ -1445,6 +1525,7 @@ ${args.labels?.length ? `${say.issueLabels}: ${args.labels.join(", ")}
|
|
|
1445
1525
|
session.wakeHeld = true;
|
|
1446
1526
|
return;
|
|
1447
1527
|
}
|
|
1528
|
+
const checkIn2 = session.mail.find((event) => !event.deliveredTurn && event.kind === "checkin");
|
|
1448
1529
|
const batch = takeBatch(session.mail, () => true);
|
|
1449
1530
|
if (!batch.length) return;
|
|
1450
1531
|
for (const event of batch) event.deliveredTurn = STARTING;
|
|
@@ -1457,11 +1538,13 @@ ${args.labels?.length ? `${say.issueLabels}: ${args.labels.join(", ")}
|
|
|
1457
1538
|
release();
|
|
1458
1539
|
return deliver(sessionId, session);
|
|
1459
1540
|
}
|
|
1541
|
+
if (session.wakeFailures >= MAX_WAKES || session.wakes >= MAX_WAKES && (!checkIn2 || !session.mail.includes(checkIn2))) return release();
|
|
1460
1542
|
if (!batch.every((event) => event.kind === "checkin")) session.wakes++;
|
|
1461
1543
|
const id = `alp-wake-${randomUUID()}`;
|
|
1462
1544
|
try {
|
|
1463
1545
|
await startPrompt(sessionId, { clientMessageId: id, delivery: "auto", content: [{ type: "text", text: renderMail(batch, session.parentAgent) }] }, "wake", batch);
|
|
1464
1546
|
} catch {
|
|
1547
|
+
session.wakeFailures++;
|
|
1465
1548
|
release();
|
|
1466
1549
|
deliver(sessionId, session);
|
|
1467
1550
|
}
|
|
@@ -1561,6 +1644,17 @@ ${args.labels?.length ? `${say.issueLabels}: ${args.labels.join(", ")}
|
|
|
1561
1644
|
waiter.resolve("user");
|
|
1562
1645
|
}
|
|
1563
1646
|
}
|
|
1647
|
+
function releaseQuestions(sessionId) {
|
|
1648
|
+
for (const { question, settle, asked } of [...userQuestions.values()]) {
|
|
1649
|
+
if (question.sessionId !== sessionId || !asked) continue;
|
|
1650
|
+
settle("canceled", void 0, "the user wrote instead of answering", toolResult(true, {
|
|
1651
|
+
status: "user_wrote",
|
|
1652
|
+
userMessage: true,
|
|
1653
|
+
question: question.id,
|
|
1654
|
+
next: `${USER_WROTE} Their message may be the answer to your question; if it is not, ask again later.`
|
|
1655
|
+
}));
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1564
1658
|
const assignmentSnapshot = (assignment, status) => ({
|
|
1565
1659
|
id: assignment.id,
|
|
1566
1660
|
agent: assignment.agent,
|
|
@@ -1770,7 +1864,7 @@ ${refused}`, next: "Fix what it reports, then call alp_handoff again." });
|
|
|
1770
1864
|
const pending = [...session.assignments.keys()].some((id) => !ids || ids.has(id)) || takeBatch(session.mail, (event) => event.kind !== "board" && !event.defer && accept(event)).length > 0;
|
|
1771
1865
|
const events = pending ? await waitFor(sessionId, session, accept, Math.min(args.timeoutMs ?? 3e5, 9e5)) : [];
|
|
1772
1866
|
if (events === "user") return toolResult(true, { events: [], userMessage: true, running: running(session), next: USER_WROTE });
|
|
1773
|
-
if (!events) return toolResult(false, { error: "Turn ended" });
|
|
1867
|
+
if (!events) return toolResult(false, session.closed || session.wakeBlocked ? { error: "Turn ended" } : { error: TURN_ENDED, running: running(session) });
|
|
1774
1868
|
return toolResult(true, { events: events.map(publicEvent), running: running(session) });
|
|
1775
1869
|
}
|
|
1776
1870
|
function sendTool(sessionId, session, args) {
|
|
@@ -1800,10 +1894,10 @@ ${refused}`, next: "Fix what it reports, then call alp_handoff again." });
|
|
|
1800
1894
|
}
|
|
1801
1895
|
return toolResult(true, { sent: post(assignment.id, { kind: args.kind, from, assignment: assignment.id, body: args.body, ...args.replyTo ? { replyTo: args.replyTo } : {} }).id });
|
|
1802
1896
|
}
|
|
1803
|
-
function askUser(sessionId, session, assignment, body, options2) {
|
|
1897
|
+
function askUser(sessionId, session, assignment, body, options2, approval, asked = false) {
|
|
1804
1898
|
const rootId = rootOf(sessionId);
|
|
1805
1899
|
const question = {
|
|
1806
|
-
id: `q-${randomUUID().slice(0, 8)}`,
|
|
1900
|
+
id: approval?.id ?? `q-${randomUUID().slice(0, 8)}`,
|
|
1807
1901
|
sessionId,
|
|
1808
1902
|
rootId,
|
|
1809
1903
|
agent: session.mapping.agent.name,
|
|
@@ -1829,7 +1923,7 @@ The user answered: "${answer}"`);
|
|
|
1829
1923
|
next: "Decide, and record the assumption in your handoff or final message; or report that you are blocked."
|
|
1830
1924
|
})));
|
|
1831
1925
|
};
|
|
1832
|
-
userQuestions.set(question.id, { question, settle });
|
|
1926
|
+
userQuestions.set(question.id, { question, settle, persistent: !!approval, asked });
|
|
1833
1927
|
if (assignment) assignment.ask = { id: question.id, resolve: (result) => settle("canceled", void 0, void 0, result) };
|
|
1834
1928
|
emit(sessionId, { type: "question", question });
|
|
1835
1929
|
runLog(rootId, { event: "human.question", questionId: question.id, sessionId, agent: question.agent, body, ...options2?.length ? { options: options2 } : {} });
|
|
@@ -1842,9 +1936,9 @@ The user answered: "${answer}"`);
|
|
|
1842
1936
|
if (!session.parent || !sessions.has(session.parent)) return;
|
|
1843
1937
|
post(session.parent, { kind: "note", from: session.mapping.agent.name, assignment: sessionId, body: `[ALP, on behalf of ${session.mapping.agent.name}] ${body}` });
|
|
1844
1938
|
}
|
|
1845
|
-
function cancelQuestions(sessionId) {
|
|
1846
|
-
for (const { question, settle } of [...userQuestions.values()]) {
|
|
1847
|
-
if (question.sessionId === sessionId) settle("canceled", void 0, void 0, toolResult(false, { error: "Turn ended" }));
|
|
1939
|
+
function cancelQuestions(sessionId, turnOnly = false) {
|
|
1940
|
+
for (const { question, settle, persistent } of [...userQuestions.values()]) {
|
|
1941
|
+
if (question.sessionId === sessionId && (!turnOnly || !persistent)) settle("canceled", void 0, void 0, toolResult(false, { error: "Turn ended" }));
|
|
1848
1942
|
}
|
|
1849
1943
|
}
|
|
1850
1944
|
function askTool(sessionId, session, args) {
|
|
@@ -1864,7 +1958,7 @@ The user answered: "${answer}"`);
|
|
|
1864
1958
|
if (to === "user" && session.parent && !session.userOpened) {
|
|
1865
1959
|
return Promise.resolve(toolResult(false, { error: `Only main talks to the user; ask ${session.parentAgent ?? "your requester"} with alp_ask instead` }));
|
|
1866
1960
|
}
|
|
1867
|
-
if (to === "user") return askUser(sessionId, session, assignment, args.question, args.options);
|
|
1961
|
+
if (to === "user") return askUser(sessionId, session, assignment, args.question, args.options, void 0, true);
|
|
1868
1962
|
if (!session.parent || !parent || !assignment) return Promise.resolve(toolResult(false, { error: 'This session has no requester; ask the user with to: "user"' }));
|
|
1869
1963
|
const parentId = session.parent;
|
|
1870
1964
|
return new Promise((resolve) => {
|
|
@@ -2011,9 +2105,11 @@ The user answered: "${answer}"`);
|
|
|
2011
2105
|
}
|
|
2012
2106
|
const rootId = rootOf(sessionId);
|
|
2013
2107
|
const root = sessions.get(rootId);
|
|
2014
|
-
|
|
2108
|
+
const advisor = ["advisor", "reviewer"].includes(roleOf(session.mapping, args.agent) ?? "") && childMode === "read-only";
|
|
2109
|
+
const budget = advisor ? "advisorCalls" : "calls";
|
|
2110
|
+
if (root[budget] >= 16) {
|
|
2015
2111
|
return toolResult(false, {
|
|
2016
|
-
error: "
|
|
2112
|
+
error: `${advisor ? "Advisor" : "Implementation"} delegation budget exhausted (16 starts across the root session tree since its last new user turn). Mail wakes and child turns do not reset it; a new accepted user prompt in root (including steering) resets both budgets.`
|
|
2017
2113
|
});
|
|
2018
2114
|
}
|
|
2019
2115
|
const peer = roleOf(session.mapping, args.agent) === "peer";
|
|
@@ -2026,7 +2122,7 @@ The user answered: "${answer}"`);
|
|
|
2026
2122
|
const continued = args.continueFrom !== void 0 ? session.worktrees.get(args.continueFrom) : void 0;
|
|
2027
2123
|
if (args.continueFrom !== void 0 && !continued && !sharedContinuation) return toolResult(false, { error: `${args.continueFrom} was merged or discarded meanwhile` });
|
|
2028
2124
|
if (continued) session.worktrees.delete(args.continueFrom);
|
|
2029
|
-
root
|
|
2125
|
+
root[budget]++;
|
|
2030
2126
|
if (peer) root.peerCount++;
|
|
2031
2127
|
const childId = `alp-child-${randomUUID()}`;
|
|
2032
2128
|
const startedAt = Date.now();
|
|
@@ -2186,12 +2282,15 @@ ${digest}` : "")
|
|
|
2186
2282
|
const events = await waitFor(sessionId, session, (event2) => event2.assignment === childId && (event2.kind === "result" || event2.kind === "question"), void 0, true);
|
|
2187
2283
|
if (events === "user") return toolResult(true, { assignmentId: childId, agent: args.agent, status: "running", userMessage: true, next: USER_WROTE });
|
|
2188
2284
|
if (!events) {
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2285
|
+
if (session.closed || session.wakeBlocked || !session.assignments.has(childId)) {
|
|
2286
|
+
return toolResult(false, {
|
|
2287
|
+
agent: args.agent,
|
|
2288
|
+
assignmentId: childId,
|
|
2289
|
+
status: "canceled",
|
|
2290
|
+
error: "Parent assignment stopped"
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
return toolResult(false, { agent: args.agent, assignmentId: childId, status: "running", error: TURN_ENDED });
|
|
2195
2294
|
}
|
|
2196
2295
|
const event = events.find((candidate) => candidate.kind === "result") ?? events.find((candidate) => candidate.kind === "question");
|
|
2197
2296
|
if (!event) return toolResult(true, { assignmentId: childId, agent: args.agent, status: "running", events: events.map(publicEvent), next: CHECKED_IN });
|
|
@@ -2257,10 +2356,12 @@ ${digest}` : "")
|
|
|
2257
2356
|
peerCount: 0,
|
|
2258
2357
|
assignments: /* @__PURE__ */ new Map(),
|
|
2259
2358
|
calls: 0,
|
|
2359
|
+
advisorCalls: 0,
|
|
2260
2360
|
mail: [],
|
|
2261
2361
|
waiters: [],
|
|
2262
2362
|
lastActivity: Date.now(),
|
|
2263
2363
|
wakes: 0,
|
|
2364
|
+
wakeFailures: 0,
|
|
2264
2365
|
wakeBlocked: false,
|
|
2265
2366
|
toolCalls: /* @__PURE__ */ new Map(),
|
|
2266
2367
|
acknowledged: Promise.resolve(),
|
|
@@ -2565,7 +2666,7 @@ Write scope: change files only under ${session.spec.writeRoot}. Your working dir
|
|
|
2565
2666
|
runLog(entry.rootId, { event: "assignment.finished", assignmentId: entry.assignmentId, reconciled: true, ...result });
|
|
2566
2667
|
if (parent && !parent.closed) post(entry.parentId, { kind: "result", from: entry.agent, assignment: entry.assignmentId, result });
|
|
2567
2668
|
}
|
|
2568
|
-
async function recoverTree(rootId, continueRoot) {
|
|
2669
|
+
async function recoverTree(rootId, continueRoot, parkedRoot) {
|
|
2569
2670
|
const root = sessions.get(rootId);
|
|
2570
2671
|
if (!root || root.closed) throw new Error(`Session ${rootId} is not open`);
|
|
2571
2672
|
const entries = inherited.filter((entry) => entry.rootId === rootId).sort((a, b) => a.ancestry.length - b.ancestry.length);
|
|
@@ -2602,7 +2703,11 @@ Write scope: change files only under ${session.spec.writeRoot}. Your working dir
|
|
|
2602
2703
|
runLog(rootId, { event: "assignment.recovered", assignmentId: entry.assignmentId, agent: entry.agent, ...entry.taskId ? { taskId: entry.taskId } : {} });
|
|
2603
2704
|
proceed(entry.assignmentId, child, `ALP: ${cause} while you worked on this assignment, and reopened this session. Your earlier work in it${entry.worktree ? " and your worktree" : ""} is intact; check where you stopped (for example with git status), then continue. Finish with alp_handoff as before.${listed(resumed.get(entry.assignmentId))}`);
|
|
2604
2705
|
}
|
|
2605
|
-
if (
|
|
2706
|
+
if (parkedRoot) {
|
|
2707
|
+
root.parked = { reason: parkedRoot, since: Date.now() };
|
|
2708
|
+
emit(rootId, { type: "session.updated", session: snapshot(rootId, root) });
|
|
2709
|
+
if (options.autoResume !== false && options.recoveryResume !== false && !pauseOf(root.runtimeKind)) continueParked(rootId, root);
|
|
2710
|
+
} else if (continueRoot) proceed(rootId, root, `ALP: ${cause} during your turn, and reopened this session. Continue the user's request where you stopped.${listed(resumed.get(rootId))}`);
|
|
2606
2711
|
else for (const assignment of resumed.get(rootId) ?? []) post(rootId, { kind: "note", from: assignment.agent, assignment: assignment.id, passive: true, body: `${cause}; ALP reopened this assignment and it continues. It reports when done.` });
|
|
2607
2712
|
const failed = outcomes.filter((outcome) => outcome.outcome === "failed");
|
|
2608
2713
|
if (entries.length || continueRoot) {
|
|
@@ -2649,6 +2754,11 @@ Write scope: change files only under ${session.spec.writeRoot}. Your working dir
|
|
|
2649
2754
|
const activeTurn = session.active;
|
|
2650
2755
|
session.wakeBlocked = true;
|
|
2651
2756
|
session.userStopped = true;
|
|
2757
|
+
session.parked = void 0;
|
|
2758
|
+
session.parkReason = void 0;
|
|
2759
|
+
emit(sessionId, { type: "session.updated", session: snapshot(sessionId, session) });
|
|
2760
|
+
approvalCancellation.set(session, (approvalCancellation.get(session) ?? 0) + 1);
|
|
2761
|
+
cancelQuestions(sessionId);
|
|
2652
2762
|
if (activeTurn && session.runtimeKind === "codex" && session.threadId) {
|
|
2653
2763
|
const partial = [...session.text.values()].join("\n").trim();
|
|
2654
2764
|
if (partial) interruptedThreads.set(session.threadId, partial.slice(-4e3));
|
|
@@ -2721,11 +2831,9 @@ Write scope: change files only under ${session.spec.writeRoot}. Your working dir
|
|
|
2721
2831
|
if (!wake) {
|
|
2722
2832
|
session.handoff = void 0;
|
|
2723
2833
|
session.wakes = 0;
|
|
2834
|
+
session.wakeFailures = 0;
|
|
2724
2835
|
session.wakeBlocked = false;
|
|
2725
2836
|
session.userStopped = false;
|
|
2726
|
-
if (!session.parent) {
|
|
2727
|
-
session.calls = 0;
|
|
2728
|
-
}
|
|
2729
2837
|
}
|
|
2730
2838
|
}
|
|
2731
2839
|
try {
|
|
@@ -2767,6 +2875,10 @@ ${interrupted}`), text_elements: [] }] : [],
|
|
|
2767
2875
|
);
|
|
2768
2876
|
const turnId = prompt.delivery === "steer" ? session.active : result.turn.id;
|
|
2769
2877
|
for (const event of mail) event.deliveredTurn = turnId;
|
|
2878
|
+
if (!session.parent && origin === "user") {
|
|
2879
|
+
session.calls = 0;
|
|
2880
|
+
session.advisorCalls = 0;
|
|
2881
|
+
}
|
|
2770
2882
|
session.seen.add(
|
|
2771
2883
|
prompt.clientMessageId
|
|
2772
2884
|
);
|
|
@@ -2782,7 +2894,10 @@ ${interrupted}`), text_elements: [] }] : [],
|
|
|
2782
2894
|
});
|
|
2783
2895
|
if (origin === "user" && !session.parent) session.userWaiting ??= Date.now();
|
|
2784
2896
|
if (session.supervisor && origin === "user") session.journal.push(stamp(`user ${prompt.delivery === "steer" ? "steered" : "asked"} ${session.mapping.agent.name}: ${clip(text2, 1500)}`));
|
|
2785
|
-
if (prompt.delivery === "steer" && origin === "user" && !session.parent)
|
|
2897
|
+
if (prompt.delivery === "steer" && origin === "user" && !session.parent) {
|
|
2898
|
+
releaseWaiters(session);
|
|
2899
|
+
releaseQuestions(sessionId);
|
|
2900
|
+
}
|
|
2786
2901
|
emit(sessionId, {
|
|
2787
2902
|
type: "prompt.accepted",
|
|
2788
2903
|
clientMessageId: prompt.clientMessageId,
|
|
@@ -2823,12 +2938,19 @@ ${interrupted}`), text_elements: [] }] : [],
|
|
|
2823
2938
|
return opened;
|
|
2824
2939
|
}),
|
|
2825
2940
|
prompt: (sessionId, input) => enqueue(async () => {
|
|
2941
|
+
const session = sessions.get(sessionId);
|
|
2942
|
+
const parked = session?.parked;
|
|
2826
2943
|
try {
|
|
2827
|
-
|
|
2944
|
+
const prompt = session?.active && session.resumedTurn === session.active && input.delivery === "auto" ? { ...input, delivery: "steer" } : input;
|
|
2945
|
+
await startPrompt(sessionId, prompt, "user");
|
|
2946
|
+
if (session && parked && session.parked === parked) {
|
|
2947
|
+
session.parked = void 0;
|
|
2948
|
+
emit(sessionId, { type: "session.updated", session: snapshot(sessionId, session) });
|
|
2949
|
+
}
|
|
2828
2950
|
} catch (error) {
|
|
2829
|
-
const
|
|
2830
|
-
if (
|
|
2831
|
-
|
|
2951
|
+
const session2 = sessions.get(sessionId);
|
|
2952
|
+
if (session2?.seen.has(input.clientMessageId)) return;
|
|
2953
|
+
session2?.seen.add(input.clientMessageId);
|
|
2832
2954
|
emit(sessionId, { type: "prompt.failed", clientMessageId: input.clientMessageId, error: errorData(error) });
|
|
2833
2955
|
}
|
|
2834
2956
|
}),
|
|
@@ -2972,8 +3094,8 @@ ${interrupted}`), text_elements: [] }] : [],
|
|
|
2972
3094
|
recoverable() {
|
|
2973
3095
|
return [...inherited];
|
|
2974
3096
|
},
|
|
2975
|
-
recover(rootId, { continueRoot = false } = {}) {
|
|
2976
|
-
return enqueue(() => recoverTree(rootId, continueRoot));
|
|
3097
|
+
recover(rootId, { continueRoot = false, parkedRoot } = {}) {
|
|
3098
|
+
return enqueue(() => recoverTree(rootId, continueRoot, parkedRoot));
|
|
2977
3099
|
},
|
|
2978
3100
|
async abandon(rootId, reason) {
|
|
2979
3101
|
const entries = inherited.filter((entry) => entry.rootId === rootId);
|