@bojackduy/opencode-loopd 1.12.4 → 1.13.1
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/CHANGELOG.md +17 -0
- package/dist/server.js +1203 -656
- package/dist/tui.js +81 -32
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -47,6 +47,9 @@ function acquireLease(rt, timeoutMs) {
|
|
|
47
47
|
idleConfirmFailedGeneration: undefined,
|
|
48
48
|
idleStuckNotifiedGeneration: undefined,
|
|
49
49
|
workerAbortedAt: undefined,
|
|
50
|
+
activePromptDispatchAt: undefined,
|
|
51
|
+
activePromptAcceptedAt: undefined,
|
|
52
|
+
activeInboxLines: undefined,
|
|
50
53
|
activePromptObservedAt: undefined,
|
|
51
54
|
activeAssistantMessageID: undefined,
|
|
52
55
|
activeAssistantCompletedAt: undefined,
|
|
@@ -180,7 +183,7 @@ var init_event_tail = () => {};
|
|
|
180
183
|
import { promises as fs2 } from "fs";
|
|
181
184
|
import path from "path";
|
|
182
185
|
import os from "os";
|
|
183
|
-
import { createHash } from "crypto";
|
|
186
|
+
import { createHash, randomUUID } from "crypto";
|
|
184
187
|
function emptyState() {
|
|
185
188
|
return { version: CURRENT_VERSION, revision: 0, goals: [], runtimes: [], commandLedger: [], commands: [] };
|
|
186
189
|
}
|
|
@@ -516,37 +519,80 @@ function inboxFile(directory, goalID) {
|
|
|
516
519
|
return path.join(loopDir(directory), "inboxes", `${goalID}.jsonl`);
|
|
517
520
|
}
|
|
518
521
|
async function appendGoalInbox(directory, goalID, from, text) {
|
|
519
|
-
|
|
520
|
-
await
|
|
521
|
-
|
|
522
|
-
|
|
522
|
+
let accepted = false;
|
|
523
|
+
await mutateState(directory, `inbox.enqueue:${goalID}`, async (state) => {
|
|
524
|
+
if (!state.goals.some((goal) => goal.id === goalID))
|
|
525
|
+
return state;
|
|
526
|
+
await acquireLock(directory, `inbox-${goalID}`, "inbox.append");
|
|
527
|
+
try {
|
|
528
|
+
const dir = path.join(loopDir(directory), "inboxes");
|
|
529
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
530
|
+
const msg = { id: randomUUID(), from, text, at: new Date().toISOString() };
|
|
531
|
+
await fs2.appendFile(inboxFile(directory, goalID), JSON.stringify(msg) + `
|
|
523
532
|
`, "utf8");
|
|
533
|
+
accepted = true;
|
|
534
|
+
} finally {
|
|
535
|
+
await releaseLock(directory, `inbox-${goalID}`);
|
|
536
|
+
}
|
|
537
|
+
return state;
|
|
538
|
+
});
|
|
539
|
+
return accepted;
|
|
524
540
|
}
|
|
525
|
-
async function
|
|
526
|
-
|
|
541
|
+
async function deleteGoalInbox(directory, goalID) {
|
|
542
|
+
await acquireLock(directory, `inbox-${goalID}`, "inbox.delete");
|
|
527
543
|
try {
|
|
528
|
-
|
|
544
|
+
await fs2.rm(inboxFile(directory, goalID), { force: true });
|
|
545
|
+
} finally {
|
|
546
|
+
await releaseLock(directory, `inbox-${goalID}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function acknowledgeGoalInbox(directory, goalID, delivered) {
|
|
550
|
+
if (!delivered.length)
|
|
551
|
+
return;
|
|
552
|
+
await acquireLock(directory, `inbox-${goalID}`, "inbox.acknowledge");
|
|
553
|
+
try {
|
|
554
|
+
let raw;
|
|
555
|
+
try {
|
|
556
|
+
raw = await fs2.readFile(inboxFile(directory, goalID), "utf8");
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (error?.code === "ENOENT")
|
|
559
|
+
return;
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
529
562
|
const lines = raw.trim().split(`
|
|
530
563
|
`).filter(Boolean);
|
|
531
|
-
if (
|
|
532
|
-
return
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
564
|
+
if (!delivered.every((line, index) => lines[index] === line))
|
|
565
|
+
return;
|
|
566
|
+
const rest = lines.slice(delivered.length);
|
|
567
|
+
if (rest.length)
|
|
568
|
+
await writeAtomic(inboxFile(directory, goalID), rest.join(`
|
|
569
|
+
`) + `
|
|
570
|
+
`);
|
|
571
|
+
else
|
|
572
|
+
await fs2.rm(inboxFile(directory, goalID), { force: true });
|
|
573
|
+
} finally {
|
|
574
|
+
await releaseLock(directory, `inbox-${goalID}`);
|
|
538
575
|
}
|
|
539
576
|
}
|
|
540
|
-
async function
|
|
541
|
-
|
|
577
|
+
async function snapshotGoalInbox(directory, goalID) {
|
|
578
|
+
await acquireLock(directory, `inbox-${goalID}`, "inbox.snapshot");
|
|
542
579
|
try {
|
|
543
|
-
const raw = await fs2.readFile(
|
|
580
|
+
const raw = await fs2.readFile(inboxFile(directory, goalID), "utf8");
|
|
544
581
|
const lines = raw.trim().split(`
|
|
545
582
|
`).filter(Boolean);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
583
|
+
const messages = lines.map((line) => JSON.parse(line));
|
|
584
|
+
return { lines, messages: messages.map((m) => `[${m.from}] ${m.text}`) };
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (error?.code === "ENOENT")
|
|
587
|
+
return { lines: [], messages: [] };
|
|
588
|
+
throw error;
|
|
589
|
+
} finally {
|
|
590
|
+
await releaseLock(directory, `inbox-${goalID}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function peekGoalInbox(directory, goalID) {
|
|
594
|
+
try {
|
|
595
|
+
return (await snapshotGoalInbox(directory, goalID)).messages;
|
|
550
596
|
} catch {
|
|
551
597
|
return [];
|
|
552
598
|
}
|
|
@@ -621,6 +667,42 @@ var init_state_repository = __esm(() => {
|
|
|
621
667
|
init_event_tail();
|
|
622
668
|
});
|
|
623
669
|
|
|
670
|
+
// src/infrastructure/server-log.ts
|
|
671
|
+
import { appendFile } from "fs/promises";
|
|
672
|
+
async function logServerEvent(directory, event, details = {}) {
|
|
673
|
+
try {
|
|
674
|
+
await appendFile(SERVER_LOG_FILE, `${JSON.stringify({
|
|
675
|
+
timestamp: new Date().toISOString(),
|
|
676
|
+
directory,
|
|
677
|
+
event,
|
|
678
|
+
...details
|
|
679
|
+
}, errorReplacer)}
|
|
680
|
+
`);
|
|
681
|
+
} catch {}
|
|
682
|
+
}
|
|
683
|
+
function describeError(value) {
|
|
684
|
+
if (value instanceof Error)
|
|
685
|
+
return value.message;
|
|
686
|
+
if (typeof value === "string")
|
|
687
|
+
return value;
|
|
688
|
+
if (typeof value === "object" && value !== null && "message" in value) {
|
|
689
|
+
return String(value.message);
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
return JSON.stringify(value, errorReplacer);
|
|
693
|
+
} catch {
|
|
694
|
+
return String(value);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function errorReplacer(_key, value) {
|
|
698
|
+
if (value instanceof Error) {
|
|
699
|
+
return { name: value.name, message: value.message, stack: value.stack };
|
|
700
|
+
}
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
var SERVER_LOG_FILE = "/tmp/loopd-server.log";
|
|
704
|
+
var init_server_log = () => {};
|
|
705
|
+
|
|
624
706
|
// src/domain/command-await.ts
|
|
625
707
|
function isTerminalCommandStatus(status) {
|
|
626
708
|
return TERMINAL_COMMAND_STATUSES.includes(status);
|
|
@@ -886,11 +968,11 @@ var init_command_watch = __esm(() => {
|
|
|
886
968
|
});
|
|
887
969
|
|
|
888
970
|
// src/application/command-await.ts
|
|
889
|
-
import { randomUUID } from "crypto";
|
|
971
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
890
972
|
function ledgerEvent(base) {
|
|
891
973
|
return {
|
|
892
974
|
version: 1,
|
|
893
|
-
eventID:
|
|
975
|
+
eventID: randomUUID2(),
|
|
894
976
|
timestamp: new Date().toISOString(),
|
|
895
977
|
revision: 0,
|
|
896
978
|
...base
|
|
@@ -1266,20 +1348,8 @@ async function wakeGoalForAwait(directory, continuation, goalID) {
|
|
|
1266
1348
|
if (goal.status !== "active") {
|
|
1267
1349
|
return { woke: false, message: `Goal is ${goal.status}; evidence waits in pendingInbox.` };
|
|
1268
1350
|
}
|
|
1269
|
-
await
|
|
1270
|
-
|
|
1271
|
-
if (!rt)
|
|
1272
|
-
return s;
|
|
1273
|
-
rt.phase = "idle";
|
|
1274
|
-
rt.activeRunID = undefined;
|
|
1275
|
-
rt.idleCandidateAt = undefined;
|
|
1276
|
-
rt.activePromptMessageID = undefined;
|
|
1277
|
-
rt.activeToolCallIDs = [];
|
|
1278
|
-
rt.updatedAt = new Date().toISOString();
|
|
1279
|
-
return s;
|
|
1280
|
-
});
|
|
1281
|
-
await continuation.continueTurn(directory, goalID, { force: true });
|
|
1282
|
-
return { woke: true, message: `Woke goal "${goal.name}" for command exit.` };
|
|
1351
|
+
const result = await continuation.nudge(directory, goalID);
|
|
1352
|
+
return { woke: result.ok && result.outcome === "sent", message: result.message };
|
|
1283
1353
|
}
|
|
1284
1354
|
var init_command_await2 = __esm(() => {
|
|
1285
1355
|
init_command_await();
|
|
@@ -1350,12 +1420,367 @@ function scopeConflictMessage(conflict) {
|
|
|
1350
1420
|
}
|
|
1351
1421
|
var init_workspace_scope = () => {};
|
|
1352
1422
|
|
|
1423
|
+
// src/application/workspace-execution.ts
|
|
1424
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1425
|
+
import path3 from "path";
|
|
1426
|
+
function structuredWritePaths(tool, args) {
|
|
1427
|
+
if (tool === "write" || tool === "edit") {
|
|
1428
|
+
if (typeof args.filePath !== "string")
|
|
1429
|
+
throw new Error(`Scope denied: ${tool} requires filePath.`);
|
|
1430
|
+
return [args.filePath];
|
|
1431
|
+
}
|
|
1432
|
+
if (tool !== "apply_patch")
|
|
1433
|
+
return;
|
|
1434
|
+
const source = args.patchText ?? args.patch;
|
|
1435
|
+
if (typeof source !== "string" || !source.startsWith(`*** Begin Patch
|
|
1436
|
+
`) || !source.trimEnd().endsWith("*** End Patch")) {
|
|
1437
|
+
throw new Error("Scope denied: unsupported apply_patch format.");
|
|
1438
|
+
}
|
|
1439
|
+
const paths = [];
|
|
1440
|
+
let section = false;
|
|
1441
|
+
for (const line of source.split(`
|
|
1442
|
+
`)) {
|
|
1443
|
+
const header = /^\*\*\* (?:Add File|Update File|Delete File): (.+)$/.exec(line);
|
|
1444
|
+
if (header) {
|
|
1445
|
+
paths.push(header[1]);
|
|
1446
|
+
section = true;
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
const move = /^\*\*\* Move to: (.+)$/.exec(line);
|
|
1450
|
+
if (move) {
|
|
1451
|
+
if (!section)
|
|
1452
|
+
throw new Error("Scope denied: rename outside patch section.");
|
|
1453
|
+
paths.push(move[1]);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (line.startsWith("*** ") && !["*** Begin Patch", "*** End Patch", "*** End of File"].includes(line)) {
|
|
1457
|
+
throw new Error("Scope denied: unknown patch operation.");
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (!paths.length)
|
|
1461
|
+
throw new Error("Scope denied: patch has no file operations.");
|
|
1462
|
+
return paths;
|
|
1463
|
+
}
|
|
1464
|
+
function isSyncStaleWorkspaceCall(_state, _call, _now = Date.now()) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
function isStaleWorkspaceCall(state, call, liveness = {}) {
|
|
1468
|
+
return isSyncStaleWorkspaceCall(state, call, liveness.now ?? Date.now());
|
|
1469
|
+
}
|
|
1470
|
+
function sweepStaleWorkspaceState(state, liveness = {}) {
|
|
1471
|
+
const now = liveness.now ?? Date.now();
|
|
1472
|
+
const summary = { reapedCalls: [], resetClosing: [], finalizedClears: [], forceCleared: [] };
|
|
1473
|
+
const kept = state.workspaceCalls ?? [];
|
|
1474
|
+
for (const goal of [...state.goals]) {
|
|
1475
|
+
const referenced = kept.some((call) => call.goalID === goal.id) || state.workspaceOperation?.goalID === goal.id;
|
|
1476
|
+
if (goal.scopeClosing && !referenced && !(liveness.isWorkerLive?.(goal.id, goal.workerSessionID) ?? false)) {
|
|
1477
|
+
goal.scopeClosing = false;
|
|
1478
|
+
goal.updatedAt = new Date(now).toISOString();
|
|
1479
|
+
summary.resetClosing.push(goal.id);
|
|
1480
|
+
}
|
|
1481
|
+
if (goal.scopeClearPending) {
|
|
1482
|
+
retireWorker(state, goal);
|
|
1483
|
+
state.goals = state.goals.filter((g) => g.id !== goal.id);
|
|
1484
|
+
state.runtimes = state.runtimes.filter((r) => r.goalID !== goal.id);
|
|
1485
|
+
state.commandAwaits = (state.commandAwaits ?? []).filter((a) => a.goalID !== goal.id);
|
|
1486
|
+
summary.finalizedClears.push(goal.id);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return summary;
|
|
1490
|
+
}
|
|
1491
|
+
function stampCallGeneration(state, goalID) {
|
|
1492
|
+
if (!goalID)
|
|
1493
|
+
return;
|
|
1494
|
+
return state.runtimes.find((runtime) => runtime.goalID === goalID)?.runGeneration ?? 0;
|
|
1495
|
+
}
|
|
1496
|
+
function heldGoals(state) {
|
|
1497
|
+
const now = Date.now();
|
|
1498
|
+
return state.goals.filter((goal) => goal.status === "active" || goal.scopeClosing || (state.workspaceCalls ?? []).some((call) => call.goalID === goal.id && !isSyncStaleWorkspaceCall(state, call, now)) || state.workspaceOperation?.goalID === goal.id);
|
|
1499
|
+
}
|
|
1500
|
+
function mayBeAlive(pid) {
|
|
1501
|
+
if (!pid || pid < 1)
|
|
1502
|
+
return true;
|
|
1503
|
+
try {
|
|
1504
|
+
process.kill(pid, 0);
|
|
1505
|
+
return true;
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
return error?.code !== "ESRCH";
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function assertScopeAvailable(state, goal) {
|
|
1511
|
+
const now = Date.now();
|
|
1512
|
+
const liveCalls = (state.workspaceCalls ?? []).filter((call) => call.goalID === goal.id && !isSyncStaleWorkspaceCall(state, call, now));
|
|
1513
|
+
if (goal.scopeClosing || liveCalls.length) {
|
|
1514
|
+
throw new Error("Goal still has closing/in-flight writes; ownership cannot be reacquired before quiescence.");
|
|
1515
|
+
}
|
|
1516
|
+
if (state.workspaceOperation)
|
|
1517
|
+
throw new Error(`Workspace operation ${state.workspaceOperation.id} is active.`);
|
|
1518
|
+
if (goal.config.write_scope !== undefined || goal.config.workspaceWrite === false) {
|
|
1519
|
+
const orphan = [...state.orphanedCommandProcesses || [], ...(state.commands || []).filter((command) => command.status === "missing").map((command) => ({ commandID: command.id, pid: command.pid }))].find((process2) => mayBeAlive(process2.pid));
|
|
1520
|
+
if (orphan)
|
|
1521
|
+
throw new Error(`Protected activation denied: command ${orphan.commandID} lost its handle and process exit is unproved. Stop/verify the orphan process externally; removing its log does not release this fence.`);
|
|
1522
|
+
if (state.workspaceCalls?.some((call) => call.uncontrolled))
|
|
1523
|
+
throw new Error("Protected goal activation denied: an uncontrolled tool/shell is still executing. Wait for its exit event, do not poll claims.");
|
|
1524
|
+
const running = state.commands?.find((command) => command.status === "running");
|
|
1525
|
+
if (running)
|
|
1526
|
+
throw new Error(`Protected goal activation denied: uncontrolled command ${running.id} is still running. Terminate it before starting scoped/read-only work.`);
|
|
1527
|
+
}
|
|
1528
|
+
const conflict = findScopeConflict(heldGoals(state).map((owner) => ({ ...owner, status: "active" })), goal);
|
|
1529
|
+
if (conflict) {
|
|
1530
|
+
const owner = state.goals.find((item) => item.id === conflict.goalID);
|
|
1531
|
+
if (goal.config.write_scope === undefined && owner?.config.write_scope === undefined) {
|
|
1532
|
+
throw new Error(`Workspace-writing goal "${conflict.name}" (${conflict.goalID}) is already active (owned by session ${conflict.ownerSessionID}). Pause, block, complete, or clear it before activating another workspace-writing goal. Note: list_background_goals shows only this session's goals.`);
|
|
1533
|
+
}
|
|
1534
|
+
throw new Error(scopeConflictMessage(conflict));
|
|
1535
|
+
}
|
|
1536
|
+
if (goal.config.workspaceWrite !== false && goal.config.write_scope?.length !== 0) {
|
|
1537
|
+
const now2 = Date.now();
|
|
1538
|
+
const call = state.workspaceCalls?.find((call) => call.goalID !== goal.id && !isSyncStaleWorkspaceCall(state, call, now2) && (goal.config.write_scope === undefined || call.paths.some((file) => goal.config.write_scope.includes(file))));
|
|
1539
|
+
if (call)
|
|
1540
|
+
throw new Error(`Scope conflict with in-flight call ${call.callID}, session ${call.sessionID}, path ${call.paths[0]}.`);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
function retireWorker(state, goal) {
|
|
1544
|
+
if (!goal.workerSessionID)
|
|
1545
|
+
return;
|
|
1546
|
+
state.retiredWorkerSessions = [...new Set([...state.retiredWorkerSessions || [], goal.workerSessionID])];
|
|
1547
|
+
}
|
|
1548
|
+
async function beforeWorkspaceTool(directory, input, args) {
|
|
1549
|
+
if (readTools.has(input.tool))
|
|
1550
|
+
return;
|
|
1551
|
+
await mutateState(directory, `workspace.before:${input.callID}`, async (state) => {
|
|
1552
|
+
const goal = state.goals.find((item) => item.workerSessionID === input.sessionID);
|
|
1553
|
+
if (state.retiredWorkerSessions?.includes(input.sessionID))
|
|
1554
|
+
throw new Error("Scope denied: retired worker session; goal cleared or worker rotated. Stop work; late executions are fenced.");
|
|
1555
|
+
const held = heldGoals(state);
|
|
1556
|
+
const protectedGoals = held.filter((item) => item.config.write_scope !== undefined || item.config.workspaceWrite === false);
|
|
1557
|
+
if (!goal && ownerTools.has(input.tool) && (!protectedGoals.length || state.goals.some((item) => item.ownerSessionID === input.sessionID)))
|
|
1558
|
+
return state;
|
|
1559
|
+
if (!state.workspaceOperation && !protectedGoals.length && (!goal || goal.config.write_scope === undefined && goal.config.workspaceWrite !== false)) {
|
|
1560
|
+
let paths = [];
|
|
1561
|
+
let uncontrolled = false;
|
|
1562
|
+
try {
|
|
1563
|
+
const writes = structuredWritePaths(input.tool, args);
|
|
1564
|
+
if (writes)
|
|
1565
|
+
paths = await Promise.all(writes.map((file) => canonicalScopePath(directory, file)));
|
|
1566
|
+
else
|
|
1567
|
+
uncontrolled = true;
|
|
1568
|
+
} catch {
|
|
1569
|
+
uncontrolled = true;
|
|
1570
|
+
}
|
|
1571
|
+
state.workspaceCalls ??= [];
|
|
1572
|
+
if (!state.workspaceCalls.some((call) => call.callID === input.callID && call.sessionID === input.sessionID)) {
|
|
1573
|
+
state.workspaceCalls.push({ callID: input.callID, sessionID: input.sessionID, goalID: goal?.id, generation: stampCallGeneration(state, goal?.id), paths, uncontrolled, at: new Date().toISOString() });
|
|
1574
|
+
}
|
|
1575
|
+
return state;
|
|
1576
|
+
}
|
|
1577
|
+
const writes = structuredWritePaths(input.tool, args);
|
|
1578
|
+
if (!writes)
|
|
1579
|
+
throw new Error(`Scope denied: uncontrolled tool "${input.tool}". Shell, commands, custom tools, batch and subagents are not allowed while protected goals exist. Use structured edits or run_goal_checks.`);
|
|
1580
|
+
if (state.workspaceOperation)
|
|
1581
|
+
throw new Error(`Workspace operation ${state.workspaceOperation.id} is active; source writes are denied until it finishes.`);
|
|
1582
|
+
if (goal && (goal.status !== "active" || goal.scopeClosing))
|
|
1583
|
+
throw new Error(`Scope denied: worker goal is ${goal.scopeClosing ? "closing" : goal.status}.`);
|
|
1584
|
+
if (!goal && !state.goals.some((item) => item.ownerSessionID === input.sessionID)) {
|
|
1585
|
+
throw new Error("Scope denied: unrecognized session/child. Subagent writes are unsupported.");
|
|
1586
|
+
}
|
|
1587
|
+
const paths = await Promise.all(writes.map((file) => canonicalScopePath(directory, file)));
|
|
1588
|
+
const artifact = goal?.config.artifactDir ? path3.relative(directory, goal.config.artifactDir).split(path3.sep).join("/") : undefined;
|
|
1589
|
+
for (const file of paths) {
|
|
1590
|
+
const inArtifact = artifact && file.startsWith(`${artifact}/`);
|
|
1591
|
+
if (goal && !inArtifact && (goal.config.workspaceWrite === false || goal.config.write_scope !== undefined && !goal.config.write_scope.includes(file))) {
|
|
1592
|
+
throw new Error(`Scope denied: "${file}" is outside this goal's write_scope. Claim it before editing.`);
|
|
1593
|
+
}
|
|
1594
|
+
const owner = !inArtifact && held.find((item) => item.id !== goal?.id && item.config.workspaceWrite !== false && (item.config.write_scope === undefined || item.config.write_scope.includes(file)));
|
|
1595
|
+
if (owner)
|
|
1596
|
+
throw new Error(scopeConflictMessage({ goalID: owner.id, ownerSessionID: owner.ownerSessionID, name: owner.name, path: file }));
|
|
1597
|
+
const now3 = Date.now();
|
|
1598
|
+
const call = state.workspaceCalls?.find((item) => !isSyncStaleWorkspaceCall(state, item, now3) && item.paths.includes(file) && !(item.callID === input.callID && item.sessionID === input.sessionID));
|
|
1599
|
+
if (call)
|
|
1600
|
+
throw new Error(`Scope denied: "${file}" is being written by session ${call.sessionID}, call ${call.callID}.`);
|
|
1601
|
+
}
|
|
1602
|
+
if (!state.workspaceCalls)
|
|
1603
|
+
state.workspaceCalls = [];
|
|
1604
|
+
if (!state.workspaceCalls.some((call) => call.callID === input.callID && call.sessionID === input.sessionID)) {
|
|
1605
|
+
state.workspaceCalls.push({ callID: input.callID, sessionID: input.sessionID, goalID: goal?.id, generation: stampCallGeneration(state, goal?.id), paths, at: new Date().toISOString() });
|
|
1606
|
+
}
|
|
1607
|
+
return state;
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
async function afterWorkspaceTool(directory, sessionID, callID) {
|
|
1611
|
+
await mutateState(directory, `workspace.after:${callID}`, async (state) => {
|
|
1612
|
+
state.workspaceCalls = state.workspaceCalls?.filter((call) => call.sessionID !== sessionID || call.callID !== callID);
|
|
1613
|
+
const now = Date.now();
|
|
1614
|
+
state.goals = state.goals.filter((goal) => {
|
|
1615
|
+
const live = (state.workspaceCalls ?? []).some((call) => call.goalID === goal.id && !isSyncStaleWorkspaceCall(state, call, now));
|
|
1616
|
+
if (!goal.scopeClearPending || live || state.workspaceOperation?.goalID === goal.id)
|
|
1617
|
+
return true;
|
|
1618
|
+
retireWorker(state, goal);
|
|
1619
|
+
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => !(call.goalID === goal.id && isSyncStaleWorkspaceCall(state, call, now)));
|
|
1620
|
+
state.runtimes = state.runtimes.filter((runtime) => runtime.goalID !== goal.id);
|
|
1621
|
+
return false;
|
|
1622
|
+
});
|
|
1623
|
+
return state;
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
async function releaseTerminalWorkspaceCalls(directory, evidence) {
|
|
1627
|
+
if (!evidence.length)
|
|
1628
|
+
return 0;
|
|
1629
|
+
const snapshot = await readState(directory);
|
|
1630
|
+
const relevant = evidence.some((proof) => snapshot.workspaceCalls?.some((call) => call.sessionID === proof.sessionID && call.callID === proof.callID) || snapshot.goals.some((goal) => goal.workerSessionID === proof.sessionID && snapshot.runtimes.some((rt) => rt.goalID === goal.id && rt.activeToolCallIDs?.includes(proof.callID))));
|
|
1631
|
+
if (!relevant)
|
|
1632
|
+
return 0;
|
|
1633
|
+
let released = 0;
|
|
1634
|
+
await mutateState(directory, "workspace.terminal-evidence", async (state) => {
|
|
1635
|
+
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => {
|
|
1636
|
+
const proof = evidence.find((item) => item.sessionID === call.sessionID && item.callID === call.callID && (item.status === "completed" || item.status === "error") && Number.isFinite(item.endedAt) && typeof call.at === "string" && Number.isFinite(Date.parse(call.at)) && item.endedAt >= Date.parse(call.at));
|
|
1637
|
+
if (!proof)
|
|
1638
|
+
return true;
|
|
1639
|
+
released += 1;
|
|
1640
|
+
const goal = state.goals.find((item) => item.id === call.goalID && item.workerSessionID === call.sessionID);
|
|
1641
|
+
const rt = goal && state.runtimes.find((item) => item.goalID === goal.id);
|
|
1642
|
+
if (rt && (call.generation === undefined || rt.runGeneration === call.generation)) {
|
|
1643
|
+
rt.activeToolCallIDs = rt.activeToolCallIDs?.filter((id) => id !== call.callID);
|
|
1644
|
+
}
|
|
1645
|
+
return false;
|
|
1646
|
+
});
|
|
1647
|
+
for (const goal of state.goals) {
|
|
1648
|
+
const rt = state.runtimes.find((item) => item.goalID === goal.id);
|
|
1649
|
+
const started = rt?.turnStartedAt ? Date.parse(rt.turnStartedAt) : NaN;
|
|
1650
|
+
if (!rt?.activeToolCallIDs?.length || !Number.isFinite(started))
|
|
1651
|
+
continue;
|
|
1652
|
+
rt.activeToolCallIDs = rt.activeToolCallIDs.filter((callID) => {
|
|
1653
|
+
if (state.workspaceCalls?.some((call) => call.sessionID === goal.workerSessionID && call.callID === callID))
|
|
1654
|
+
return true;
|
|
1655
|
+
return !evidence.some((item) => item.sessionID === goal.workerSessionID && item.callID === callID && (item.status === "completed" || item.status === "error") && Number.isFinite(item.endedAt) && item.endedAt >= started);
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
return state;
|
|
1659
|
+
});
|
|
1660
|
+
if (released)
|
|
1661
|
+
await logServerEvent(directory, "workspace.terminal-evidence-released", { released }).catch(() => {});
|
|
1662
|
+
return released;
|
|
1663
|
+
}
|
|
1664
|
+
async function reconcileTerminalWorkspaceCalls(directory, host) {
|
|
1665
|
+
if (!host.readToolExecutions)
|
|
1666
|
+
return;
|
|
1667
|
+
const state = await readState(directory);
|
|
1668
|
+
const sessions = [...new Set([
|
|
1669
|
+
...(state.workspaceCalls ?? []).map((call) => call.sessionID),
|
|
1670
|
+
...state.goals.filter((goal) => goal.workerSessionID && state.runtimes.some((rt) => rt.goalID === goal.id && rt.activeToolCallIDs?.length)).map((goal) => goal.workerSessionID)
|
|
1671
|
+
])];
|
|
1672
|
+
if (!sessions.length)
|
|
1673
|
+
return;
|
|
1674
|
+
const proof = await Promise.all(sessions.map(async (sessionID) => {
|
|
1675
|
+
try {
|
|
1676
|
+
return (await host.readToolExecutions(sessionID)).filter((item) => item.sessionID === sessionID);
|
|
1677
|
+
} catch {
|
|
1678
|
+
return [];
|
|
1679
|
+
}
|
|
1680
|
+
}));
|
|
1681
|
+
await releaseTerminalWorkspaceCalls(directory, proof.flat());
|
|
1682
|
+
}
|
|
1683
|
+
function guardV2ToolEditor(directory, editor) {
|
|
1684
|
+
if (typeof editor.list !== "function" || typeof editor.update !== "function")
|
|
1685
|
+
throw new Error("Scoped execution requires the v2 tool editor list/update execution surface.");
|
|
1686
|
+
for (const entry of editor.list()) {
|
|
1687
|
+
editor.update(entry.id, (definition) => {
|
|
1688
|
+
const execute = definition.execute;
|
|
1689
|
+
definition.execute = async (args, context) => {
|
|
1690
|
+
await beforeWorkspaceTool(directory, { tool: entry.name, sessionID: context.sessionID, callID: context.id }, args);
|
|
1691
|
+
try {
|
|
1692
|
+
return await execute(args, context);
|
|
1693
|
+
} finally {
|
|
1694
|
+
await afterWorkspaceTool(directory, context.sessionID, context.id);
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
async function withWorkspaceOperation(directory, goalID, sessionID, run) {
|
|
1701
|
+
const id = randomUUID4();
|
|
1702
|
+
await mutateState(directory, `workspace.operation:${id}`, async (state) => {
|
|
1703
|
+
const now = Date.now();
|
|
1704
|
+
const live = (state.workspaceCalls ?? []).filter((call) => !isSyncStaleWorkspaceCall(state, call, now));
|
|
1705
|
+
if (state.workspaceOperation || live.length)
|
|
1706
|
+
throw new Error("Workspace is busy with an operation or in-flight writes; verification denied until quiescent.");
|
|
1707
|
+
state.workspaceOperation = { id, goalID, sessionID };
|
|
1708
|
+
return state;
|
|
1709
|
+
});
|
|
1710
|
+
try {
|
|
1711
|
+
return await run();
|
|
1712
|
+
} finally {
|
|
1713
|
+
await mutateState(directory, `workspace.operation-end:${id}`, async (state) => {
|
|
1714
|
+
if (state.workspaceOperation?.id === id)
|
|
1715
|
+
state.workspaceOperation = undefined;
|
|
1716
|
+
const now = Date.now();
|
|
1717
|
+
state.goals = state.goals.filter((goal) => {
|
|
1718
|
+
const live = (state.workspaceCalls ?? []).some((call) => call.goalID === goal.id && !isSyncStaleWorkspaceCall(state, call, now));
|
|
1719
|
+
if (!goal.scopeClearPending || live || state.workspaceOperation?.goalID === goal.id)
|
|
1720
|
+
return true;
|
|
1721
|
+
retireWorker(state, goal);
|
|
1722
|
+
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => !(call.goalID === goal.id && isSyncStaleWorkspaceCall(state, call, now)));
|
|
1723
|
+
state.runtimes = state.runtimes.filter((runtime) => runtime.goalID !== goal.id);
|
|
1724
|
+
return false;
|
|
1725
|
+
});
|
|
1726
|
+
return state;
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
var readTools, ownerTools, STALE_WORKSPACE_CALL_TTL_MS, CLEAR_PENDING_FORCE_TTL_MS, OWNER_CALL_TTL_MS;
|
|
1731
|
+
var init_workspace_execution = __esm(() => {
|
|
1732
|
+
init_state_repository();
|
|
1733
|
+
init_server_log();
|
|
1734
|
+
init_workspace_scope();
|
|
1735
|
+
readTools = new Set([
|
|
1736
|
+
"read",
|
|
1737
|
+
"glob",
|
|
1738
|
+
"grep",
|
|
1739
|
+
"list",
|
|
1740
|
+
"webfetch",
|
|
1741
|
+
"websearch",
|
|
1742
|
+
"todowrite",
|
|
1743
|
+
"skill",
|
|
1744
|
+
"get_goal",
|
|
1745
|
+
"claim_goal_scope",
|
|
1746
|
+
"report_goal_progress",
|
|
1747
|
+
"complete_goal",
|
|
1748
|
+
"block_goal",
|
|
1749
|
+
"list_background_goals",
|
|
1750
|
+
"inspect_background_goal",
|
|
1751
|
+
"read_goal_transcript",
|
|
1752
|
+
"loopd_command_get",
|
|
1753
|
+
"loopd_command_list",
|
|
1754
|
+
"loopd_command_await",
|
|
1755
|
+
"loopd_command_interrupt",
|
|
1756
|
+
"loopd_command_terminate",
|
|
1757
|
+
"loopd_command_remove",
|
|
1758
|
+
"loopd_list_agents",
|
|
1759
|
+
"loopd_list_models",
|
|
1760
|
+
"run_goal_checks",
|
|
1761
|
+
"codegraph_codegraph_context",
|
|
1762
|
+
"codegraph_codegraph_search",
|
|
1763
|
+
"codegraph_codegraph_node",
|
|
1764
|
+
"codegraph_codegraph_explore",
|
|
1765
|
+
"codegraph_codegraph_files",
|
|
1766
|
+
"codegraph_codegraph_trace",
|
|
1767
|
+
"codegraph_codegraph_callers",
|
|
1768
|
+
"codegraph_codegraph_callees",
|
|
1769
|
+
"codegraph_codegraph_impact",
|
|
1770
|
+
"codegraph_codegraph_status"
|
|
1771
|
+
]);
|
|
1772
|
+
ownerTools = new Set(["loopd_create_goal", "switch_goal_identity", "pause_goal", "resume_goal", "clear_goal", "abort_goal_worker", "nudge_goal", "send_goal_input", "force_block_goal", "force_complete_goal"]);
|
|
1773
|
+
STALE_WORKSPACE_CALL_TTL_MS = 10 * 60000;
|
|
1774
|
+
CLEAR_PENDING_FORCE_TTL_MS = 30 * 60000;
|
|
1775
|
+
OWNER_CALL_TTL_MS = 30 * 60000;
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1353
1778
|
// src/server/plugin.ts
|
|
1354
1779
|
import { tool as v1Tool } from "@opencode-ai/plugin/tool";
|
|
1355
1780
|
|
|
1356
1781
|
// src/application/control-worker.ts
|
|
1357
1782
|
init_state_repository();
|
|
1358
|
-
import { randomUUID as
|
|
1783
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1359
1784
|
|
|
1360
1785
|
// src/application/goal-policy.ts
|
|
1361
1786
|
function resolveGoalCreationConfig(input) {
|
|
@@ -1428,42 +1853,8 @@ function cleanList(value) {
|
|
|
1428
1853
|
return value.map(cleanText).filter((item) => Boolean(item));
|
|
1429
1854
|
}
|
|
1430
1855
|
|
|
1431
|
-
// src/infrastructure/server-log.ts
|
|
1432
|
-
import { appendFile } from "fs/promises";
|
|
1433
|
-
var SERVER_LOG_FILE = "/tmp/loopd-server.log";
|
|
1434
|
-
async function logServerEvent(directory, event, details = {}) {
|
|
1435
|
-
try {
|
|
1436
|
-
await appendFile(SERVER_LOG_FILE, `${JSON.stringify({
|
|
1437
|
-
timestamp: new Date().toISOString(),
|
|
1438
|
-
directory,
|
|
1439
|
-
event,
|
|
1440
|
-
...details
|
|
1441
|
-
}, errorReplacer)}
|
|
1442
|
-
`);
|
|
1443
|
-
} catch {}
|
|
1444
|
-
}
|
|
1445
|
-
function describeError(value) {
|
|
1446
|
-
if (value instanceof Error)
|
|
1447
|
-
return value.message;
|
|
1448
|
-
if (typeof value === "string")
|
|
1449
|
-
return value;
|
|
1450
|
-
if (typeof value === "object" && value !== null && "message" in value) {
|
|
1451
|
-
return String(value.message);
|
|
1452
|
-
}
|
|
1453
|
-
try {
|
|
1454
|
-
return JSON.stringify(value, errorReplacer);
|
|
1455
|
-
} catch {
|
|
1456
|
-
return String(value);
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
function errorReplacer(_key, value) {
|
|
1460
|
-
if (value instanceof Error) {
|
|
1461
|
-
return { name: value.name, message: value.message, stack: value.stack };
|
|
1462
|
-
}
|
|
1463
|
-
return value;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
1856
|
// src/application/control-worker.ts
|
|
1857
|
+
init_server_log();
|
|
1467
1858
|
init_command_await2();
|
|
1468
1859
|
var MAX_LEDGER_SIZE = 100;
|
|
1469
1860
|
var RESPONSE_CLEANUP_AGE_MS = 60 * 60 * 1000;
|
|
@@ -1513,12 +1904,24 @@ function createControlWorker(options) {
|
|
|
1513
1904
|
continue;
|
|
1514
1905
|
processing.add(request.requestID);
|
|
1515
1906
|
options.onRequest?.(request);
|
|
1907
|
+
logServerEvent(directory, "control.request.claimed", {
|
|
1908
|
+
requestID: request.requestID,
|
|
1909
|
+
command: request.command,
|
|
1910
|
+
goalID: request.goalID
|
|
1911
|
+
}).catch(() => {});
|
|
1912
|
+
runClaimed(request).finally(() => {
|
|
1913
|
+
processing.delete(request.requestID);
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
async function runClaimed(request) {
|
|
1918
|
+
try {
|
|
1919
|
+
const response = await handleRequest(request);
|
|
1920
|
+
await writeControlResponse(directory, response);
|
|
1921
|
+
options.onResponse?.(response);
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
const detail = describeError(error);
|
|
1516
1924
|
try {
|
|
1517
|
-
const response = await handleRequest(request);
|
|
1518
|
-
await writeControlResponse(directory, response);
|
|
1519
|
-
options.onResponse?.(response);
|
|
1520
|
-
} catch (error) {
|
|
1521
|
-
const detail = describeError(error);
|
|
1522
1925
|
await logServerEvent(directory, "control.request.failed", {
|
|
1523
1926
|
requestID: request.requestID,
|
|
1524
1927
|
command: request.command,
|
|
@@ -1534,9 +1937,7 @@ function createControlWorker(options) {
|
|
|
1534
1937
|
};
|
|
1535
1938
|
await writeControlResponse(directory, response);
|
|
1536
1939
|
options.onResponse?.(response);
|
|
1537
|
-
}
|
|
1538
|
-
processing.delete(request.requestID);
|
|
1539
|
-
}
|
|
1940
|
+
} catch {}
|
|
1540
1941
|
}
|
|
1541
1942
|
}
|
|
1542
1943
|
async function handleRequest(request) {
|
|
@@ -1617,23 +2018,25 @@ function createControlWorker(options) {
|
|
|
1617
2018
|
break;
|
|
1618
2019
|
}
|
|
1619
2020
|
case "resume": {
|
|
1620
|
-
await goalSvc.resume(directory, request.goalID);
|
|
2021
|
+
const result = await goalSvc.resume(directory, request.goalID);
|
|
1621
2022
|
const state = await readState(directory);
|
|
1622
|
-
const goal = state.goals.find((g) => g.id === request.goalID);
|
|
1623
2023
|
response = {
|
|
1624
2024
|
...base,
|
|
1625
|
-
|
|
2025
|
+
ok: result.ok,
|
|
2026
|
+
message: result.message,
|
|
2027
|
+
errorCode: result.ok ? undefined : result.outcome,
|
|
1626
2028
|
stateRevision: state.revision
|
|
1627
2029
|
};
|
|
1628
2030
|
break;
|
|
1629
2031
|
}
|
|
1630
2032
|
case "retry": {
|
|
1631
|
-
await goalSvc.retry(directory, request.goalID);
|
|
2033
|
+
const result = await goalSvc.retry(directory, request.goalID);
|
|
1632
2034
|
const state = await readState(directory);
|
|
1633
|
-
const goal = state.goals.find((g) => g.id === request.goalID);
|
|
1634
2035
|
response = {
|
|
1635
2036
|
...base,
|
|
1636
|
-
|
|
2037
|
+
ok: result.ok,
|
|
2038
|
+
message: result.message,
|
|
2039
|
+
errorCode: result.ok ? undefined : result.outcome,
|
|
1637
2040
|
stateRevision: state.revision
|
|
1638
2041
|
};
|
|
1639
2042
|
break;
|
|
@@ -1649,6 +2052,7 @@ function createControlWorker(options) {
|
|
|
1649
2052
|
...base,
|
|
1650
2053
|
ok: result.ok,
|
|
1651
2054
|
message: result.message,
|
|
2055
|
+
errorCode: result.ok ? undefined : result.outcome,
|
|
1652
2056
|
stateRevision: state.revision
|
|
1653
2057
|
};
|
|
1654
2058
|
break;
|
|
@@ -1743,6 +2147,7 @@ function createControlWorker(options) {
|
|
|
1743
2147
|
...base,
|
|
1744
2148
|
ok: sent.ok,
|
|
1745
2149
|
message: sent.message,
|
|
2150
|
+
errorCode: sent.ok ? undefined : sent.outcome,
|
|
1746
2151
|
stateRevision: state.revision
|
|
1747
2152
|
};
|
|
1748
2153
|
break;
|
|
@@ -1805,7 +2210,7 @@ function createControlWorker(options) {
|
|
|
1805
2210
|
});
|
|
1806
2211
|
await appendEvent(directory, {
|
|
1807
2212
|
version: 1,
|
|
1808
|
-
eventID:
|
|
2213
|
+
eventID: randomUUID3(),
|
|
1809
2214
|
goalID: goal.id,
|
|
1810
2215
|
type: "goal.completed",
|
|
1811
2216
|
summary: goal.completionEvidence.summary,
|
|
@@ -1860,7 +2265,7 @@ function createControlWorker(options) {
|
|
|
1860
2265
|
});
|
|
1861
2266
|
await appendEvent(directory, {
|
|
1862
2267
|
version: 1,
|
|
1863
|
-
eventID:
|
|
2268
|
+
eventID: randomUUID3(),
|
|
1864
2269
|
goalID: goal.id,
|
|
1865
2270
|
type: "goal.blocked",
|
|
1866
2271
|
reason: goal.blocker.reason,
|
|
@@ -2028,45 +2433,18 @@ function createControlWorker(options) {
|
|
|
2028
2433
|
|
|
2029
2434
|
// src/application/loop-engine.ts
|
|
2030
2435
|
init_state_repository();
|
|
2031
|
-
|
|
2436
|
+
init_workspace_execution();
|
|
2437
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2032
2438
|
|
|
2033
|
-
// src/domain/
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
budget_limited: ["complete", "blocked"],
|
|
2039
|
-
usage_limited: [],
|
|
2040
|
-
complete: []
|
|
2041
|
-
};
|
|
2042
|
-
var USER_TRANSITIONS = {
|
|
2043
|
-
active: ["paused"],
|
|
2044
|
-
paused: ["active"],
|
|
2045
|
-
blocked: ["active"],
|
|
2046
|
-
budget_limited: ["active"],
|
|
2047
|
-
usage_limited: ["active"],
|
|
2048
|
-
complete: ["active"]
|
|
2049
|
-
};
|
|
2050
|
-
var SYSTEM_TRANSITIONS = {
|
|
2051
|
-
active: ["budget_limited", "usage_limited"],
|
|
2052
|
-
paused: [],
|
|
2053
|
-
blocked: [],
|
|
2054
|
-
budget_limited: [],
|
|
2055
|
-
usage_limited: [],
|
|
2056
|
-
complete: []
|
|
2057
|
-
};
|
|
2058
|
-
function canTransition(current, target, caller) {
|
|
2059
|
-
const table = caller === "model" ? MODEL_TRANSITIONS : caller === "user" ? USER_TRANSITIONS : SYSTEM_TRANSITIONS;
|
|
2060
|
-
return table[current]?.includes(target) ?? false;
|
|
2061
|
-
}
|
|
2062
|
-
function isTerminal(status) {
|
|
2063
|
-
return status === "complete";
|
|
2064
|
-
}
|
|
2065
|
-
function createGoal(input) {
|
|
2066
|
-
const now = new Date().toISOString();
|
|
2067
|
-
return { ...input, tokensUsed: 0, costUsed: 0, timeUsedSeconds: 0, createdAt: now, updatedAt: now };
|
|
2439
|
+
// src/domain/tool-execution.ts
|
|
2440
|
+
function terminalToolExecution(part) {
|
|
2441
|
+
if (part?.type !== "tool" || typeof part.sessionID !== "string" || !part.sessionID || typeof part.callID !== "string" || !part.callID || part.state?.status !== "completed" && part.state?.status !== "error" || typeof part.state?.time?.end !== "number" || !Number.isFinite(part.state.time.end) || part.state.time.end <= 0)
|
|
2442
|
+
return;
|
|
2443
|
+
return { sessionID: part.sessionID, callID: part.callID, status: part.state.status, endedAt: part.state.time.end };
|
|
2068
2444
|
}
|
|
2445
|
+
|
|
2069
2446
|
// src/application/loop-engine.ts
|
|
2447
|
+
init_server_log();
|
|
2070
2448
|
var CONFIRM_IDLE_DURATION_MS = 2000;
|
|
2071
2449
|
var HANDLED_EVENT_TYPES = new Set([
|
|
2072
2450
|
"session.idle",
|
|
@@ -2087,6 +2465,7 @@ function createLoopEngine(options) {
|
|
|
2087
2465
|
const idleRecoverMs = options.idleRecoverMs ?? 3 * 60000;
|
|
2088
2466
|
let running = false;
|
|
2089
2467
|
let maintenanceTimer;
|
|
2468
|
+
let maintaining = false;
|
|
2090
2469
|
let knownWorkerSessions = new Set;
|
|
2091
2470
|
let knownWorkerSessionsLoaded = false;
|
|
2092
2471
|
const inflightContinuations = new Set;
|
|
@@ -2118,8 +2497,12 @@ function createLoopEngine(options) {
|
|
|
2118
2497
|
running = true;
|
|
2119
2498
|
loadWorkerSessionsIfneeded().catch(() => {});
|
|
2120
2499
|
maintenanceTimer = setInterval(() => {
|
|
2121
|
-
if (running)
|
|
2122
|
-
|
|
2500
|
+
if (running && !maintaining) {
|
|
2501
|
+
maintaining = true;
|
|
2502
|
+
maintenance().catch(() => {}).finally(() => {
|
|
2503
|
+
maintaining = false;
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2123
2506
|
}, maintenanceMs);
|
|
2124
2507
|
}
|
|
2125
2508
|
function stop() {
|
|
@@ -2132,13 +2515,21 @@ function createLoopEngine(options) {
|
|
|
2132
2515
|
function isRunning() {
|
|
2133
2516
|
return running;
|
|
2134
2517
|
}
|
|
2135
|
-
async function continueGoal(goalID) {
|
|
2518
|
+
async function continueGoal(goalID, opts) {
|
|
2136
2519
|
if (inflightContinuations.has(goalID))
|
|
2137
2520
|
return false;
|
|
2138
2521
|
inflightContinuations.add(goalID);
|
|
2139
2522
|
try {
|
|
2140
|
-
await
|
|
2141
|
-
|
|
2523
|
+
const state = await readState(directory);
|
|
2524
|
+
const goal = state.goals.find((g) => g.id === goalID);
|
|
2525
|
+
const rt = state.runtimes.find((r) => r.goalID === goalID);
|
|
2526
|
+
if (!running || !goal || goal.status !== "active" || goal.interactive || !rt)
|
|
2527
|
+
return false;
|
|
2528
|
+
const forceFinish = opts?.forceFinish || enforceLimits(goal, rt).stop === "force_finish";
|
|
2529
|
+
const result = await goalService.continueTurn(directory, goalID, { forceFinish });
|
|
2530
|
+
if (!result.ok)
|
|
2531
|
+
await logServerEvent(directory, "engine.continuation-denied", { goalID, outcome: result.outcome, reason: result.message });
|
|
2532
|
+
return result.ok && result.outcome === "sent";
|
|
2142
2533
|
} finally {
|
|
2143
2534
|
inflightContinuations.delete(goalID);
|
|
2144
2535
|
}
|
|
@@ -2149,6 +2540,11 @@ function createLoopEngine(options) {
|
|
|
2149
2540
|
const type = event.type;
|
|
2150
2541
|
if (!type || !HANDLED_EVENT_TYPES.has(type))
|
|
2151
2542
|
return false;
|
|
2543
|
+
if (type === "message.part.updated") {
|
|
2544
|
+
const proof = terminalToolExecution(event.properties?.part);
|
|
2545
|
+
if (proof)
|
|
2546
|
+
await releaseTerminalWorkspaceCalls(directory, [proof]);
|
|
2547
|
+
}
|
|
2152
2548
|
const sessionID = eventSessionID(event);
|
|
2153
2549
|
if (!sessionID)
|
|
2154
2550
|
return false;
|
|
@@ -2192,8 +2588,9 @@ function createLoopEngine(options) {
|
|
|
2192
2588
|
async function handleMessageActivity(goal, event) {
|
|
2193
2589
|
let matched = false;
|
|
2194
2590
|
await mutateState(directory, `message-activity:${goal.id}`, async (s) => {
|
|
2591
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
2195
2592
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2196
|
-
if (!rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2593
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2197
2594
|
return s;
|
|
2198
2595
|
if (event.type === "message.updated") {
|
|
2199
2596
|
const info = event.properties?.info;
|
|
@@ -2203,6 +2600,7 @@ function createLoopEngine(options) {
|
|
|
2203
2600
|
matched = true;
|
|
2204
2601
|
} else if (info?.role === "assistant" && info.parentID === rt.activePromptMessageID) {
|
|
2205
2602
|
Object.assign(rt, recordActivity(rt));
|
|
2603
|
+
rt.activePromptObservedAt ??= new Date().toISOString();
|
|
2206
2604
|
rt.activeAssistantMessageID = info.id;
|
|
2207
2605
|
if (info.time?.completed) {
|
|
2208
2606
|
rt.activeAssistantCompletedAt = new Date(info.time.completed).toISOString();
|
|
@@ -2225,8 +2623,9 @@ function createLoopEngine(options) {
|
|
|
2225
2623
|
async function handleExecutionSucceeded(goal) {
|
|
2226
2624
|
let matched = false;
|
|
2227
2625
|
await mutateState(directory, `execution-succeeded:${goal.id}`, async (s) => {
|
|
2626
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
2228
2627
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2229
|
-
if (!rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2628
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2230
2629
|
return s;
|
|
2231
2630
|
Object.assign(rt, recordActivity(rt));
|
|
2232
2631
|
rt.activeAssistantCompletedAt = new Date().toISOString();
|
|
@@ -2245,7 +2644,7 @@ function createLoopEngine(options) {
|
|
|
2245
2644
|
const g = s.goals.find((item) => item.id === goalID);
|
|
2246
2645
|
if (!g)
|
|
2247
2646
|
return s;
|
|
2248
|
-
if (
|
|
2647
|
+
if (g.status !== "active" || g.interactive || g.workerSessionID !== goal.workerSessionID)
|
|
2249
2648
|
return s;
|
|
2250
2649
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2251
2650
|
if (!rt)
|
|
@@ -2310,7 +2709,7 @@ function createLoopEngine(options) {
|
|
|
2310
2709
|
if (newlyStamped) {
|
|
2311
2710
|
await appendEvent(directory, {
|
|
2312
2711
|
version: 1,
|
|
2313
|
-
eventID:
|
|
2712
|
+
eventID: randomUUID5(),
|
|
2314
2713
|
goalID,
|
|
2315
2714
|
type: "idle.confirm-failed",
|
|
2316
2715
|
reason: failureReason,
|
|
@@ -2324,7 +2723,7 @@ function createLoopEngine(options) {
|
|
|
2324
2723
|
afterIdle = await mutateState(directory, `idle.confirm:${goalID}`, async (s) => {
|
|
2325
2724
|
const g = s.goals.find((item) => item.id === goalID);
|
|
2326
2725
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2327
|
-
if (!g || !rt ||
|
|
2726
|
+
if (!g || !rt || g.status !== "active" || g.interactive || g.workerSessionID !== goal.workerSessionID)
|
|
2328
2727
|
return s;
|
|
2329
2728
|
if (rt.phase !== "running")
|
|
2330
2729
|
return s;
|
|
@@ -2355,7 +2754,7 @@ function createLoopEngine(options) {
|
|
|
2355
2754
|
if (completedRunID) {
|
|
2356
2755
|
await appendEvent(directory, {
|
|
2357
2756
|
version: 1,
|
|
2358
|
-
eventID:
|
|
2757
|
+
eventID: randomUUID5(),
|
|
2359
2758
|
goalID,
|
|
2360
2759
|
type: "run.completed",
|
|
2361
2760
|
runID: completedRunID,
|
|
@@ -2375,13 +2774,7 @@ function createLoopEngine(options) {
|
|
|
2375
2774
|
const limitResult = enforceLimits(freshGoal, freshRuntime);
|
|
2376
2775
|
if (limitResult.stop === "force_finish") {
|
|
2377
2776
|
if (!freshRuntime.forceFinishRequested) {
|
|
2378
|
-
await
|
|
2379
|
-
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2380
|
-
if (rt)
|
|
2381
|
-
rt.forceFinishRequested = true;
|
|
2382
|
-
return s;
|
|
2383
|
-
});
|
|
2384
|
-
await goalService.continueTurn(directory, goalID, { forceFinish: true });
|
|
2777
|
+
await continueGoal(goalID, { forceFinish: true });
|
|
2385
2778
|
return true;
|
|
2386
2779
|
}
|
|
2387
2780
|
const blockedKey = goalID;
|
|
@@ -2420,7 +2813,7 @@ function createLoopEngine(options) {
|
|
|
2420
2813
|
});
|
|
2421
2814
|
await appendEvent(directory, {
|
|
2422
2815
|
version: 1,
|
|
2423
|
-
eventID:
|
|
2816
|
+
eventID: randomUUID5(),
|
|
2424
2817
|
goalID,
|
|
2425
2818
|
type: "goal.blocked",
|
|
2426
2819
|
reason: limitResult.reason + " (force-finish ignored)",
|
|
@@ -2444,7 +2837,7 @@ function createLoopEngine(options) {
|
|
|
2444
2837
|
});
|
|
2445
2838
|
await appendEvent(directory, {
|
|
2446
2839
|
version: 1,
|
|
2447
|
-
eventID:
|
|
2840
|
+
eventID: randomUUID5(),
|
|
2448
2841
|
goalID,
|
|
2449
2842
|
type: "goal.status_changed",
|
|
2450
2843
|
from: "active",
|
|
@@ -2492,8 +2885,9 @@ function createLoopEngine(options) {
|
|
|
2492
2885
|
if (statusType === "idle")
|
|
2493
2886
|
return handleSessionIdle(state, goal);
|
|
2494
2887
|
await mutateState(directory, `status:${goal.id}`, async (s) => {
|
|
2888
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
2495
2889
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2496
|
-
if (!rt)
|
|
2890
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.runGeneration !== runtime.runGeneration)
|
|
2497
2891
|
return s;
|
|
2498
2892
|
if (statusType === "busy" || statusType === "retry") {
|
|
2499
2893
|
Object.assign(rt, recordActivity(rt));
|
|
@@ -2514,7 +2908,7 @@ function createLoopEngine(options) {
|
|
|
2514
2908
|
const newState = await mutateState(directory, `error:${goal.id}`, async (s) => {
|
|
2515
2909
|
const g = s.goals.find((item) => item.id === goal.id);
|
|
2516
2910
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2517
|
-
if (!rt)
|
|
2911
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.runGeneration !== runtime.runGeneration)
|
|
2518
2912
|
return s;
|
|
2519
2913
|
rt.consecutiveFailures += 1;
|
|
2520
2914
|
rt.lastError = message;
|
|
@@ -2546,7 +2940,7 @@ function createLoopEngine(options) {
|
|
|
2546
2940
|
});
|
|
2547
2941
|
await appendEvent(directory, {
|
|
2548
2942
|
version: 1,
|
|
2549
|
-
eventID:
|
|
2943
|
+
eventID: randomUUID5(),
|
|
2550
2944
|
goalID: goal.id,
|
|
2551
2945
|
type: "run.failed",
|
|
2552
2946
|
runID: runtime.activeRunID || "unknown",
|
|
@@ -2560,7 +2954,7 @@ function createLoopEngine(options) {
|
|
|
2560
2954
|
if (updatedGoal?.status === "blocked") {
|
|
2561
2955
|
await appendEvent(directory, {
|
|
2562
2956
|
version: 1,
|
|
2563
|
-
eventID:
|
|
2957
|
+
eventID: randomUUID5(),
|
|
2564
2958
|
goalID: goal.id,
|
|
2565
2959
|
type: "goal.blocked",
|
|
2566
2960
|
reason: `Failed ${runtime.consecutiveFailures + 1} times`,
|
|
@@ -2588,7 +2982,7 @@ function createLoopEngine(options) {
|
|
|
2588
2982
|
});
|
|
2589
2983
|
await appendEvent(directory, {
|
|
2590
2984
|
version: 1,
|
|
2591
|
-
eventID:
|
|
2985
|
+
eventID: randomUUID5(),
|
|
2592
2986
|
goalID: goal.id,
|
|
2593
2987
|
type: "compaction.completed",
|
|
2594
2988
|
timestamp: new Date().toISOString(),
|
|
@@ -2658,7 +3052,7 @@ function createLoopEngine(options) {
|
|
|
2658
3052
|
});
|
|
2659
3053
|
await appendEvent(directory, {
|
|
2660
3054
|
version: 1,
|
|
2661
|
-
eventID:
|
|
3055
|
+
eventID: randomUUID5(),
|
|
2662
3056
|
goalID: goal.id,
|
|
2663
3057
|
type: "compaction.started",
|
|
2664
3058
|
timestamp: new Date().toISOString(),
|
|
@@ -2716,7 +3110,7 @@ function createLoopEngine(options) {
|
|
|
2716
3110
|
return false;
|
|
2717
3111
|
await appendEvent(directory, {
|
|
2718
3112
|
version: 1,
|
|
2719
|
-
eventID:
|
|
3113
|
+
eventID: randomUUID5(),
|
|
2720
3114
|
goalID: goal.id,
|
|
2721
3115
|
type: "goal.status_changed",
|
|
2722
3116
|
from: "active",
|
|
@@ -2738,7 +3132,34 @@ function createLoopEngine(options) {
|
|
|
2738
3132
|
function hasExecutionLeak(runtime) {
|
|
2739
3133
|
return runtime.phase !== "idle" || Boolean(runtime.activeRunID) || Boolean(runtime.leaseExpiresAt) || Boolean(runtime.turnStartedAt) || Boolean(runtime.activePromptMessageID) || Boolean(runtime.activePromptObservedAt) || Boolean(runtime.activeAssistantMessageID) || Boolean(runtime.activeAssistantCompletedAt) || Boolean(runtime.idleCandidateAt) || (runtime.activeToolCallIDs?.length ?? 0) > 0;
|
|
2740
3134
|
}
|
|
3135
|
+
async function sweepStaleWorkspaceOwnership() {
|
|
3136
|
+
try {
|
|
3137
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
3138
|
+
const snapshot = await readState(directory);
|
|
3139
|
+
if (!snapshot.workspaceCalls?.length && !snapshot.goals.some((g) => g.scopeClosing || g.scopeClearPending)) {
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
await Promise.resolve().then(() => init_workspace_execution());
|
|
3143
|
+
let summary = { reapedCalls: [], resetClosing: [], finalizedClears: [], forceCleared: [] };
|
|
3144
|
+
await mutateState(directory, "maintenance.sweep-stale-workspace", async (s) => {
|
|
3145
|
+
summary = sweepStaleWorkspaceState(s, {
|
|
3146
|
+
isWorkerLive: () => true,
|
|
3147
|
+
now: Date.now()
|
|
3148
|
+
});
|
|
3149
|
+
return s;
|
|
3150
|
+
});
|
|
3151
|
+
if (summary.reapedCalls.length || summary.resetClosing.length || summary.finalizedClears.length || summary.forceCleared.length) {
|
|
3152
|
+
await logServerEvent(directory, "maintenance.sweep-stale-workspace", {
|
|
3153
|
+
reapedCalls: summary.reapedCalls,
|
|
3154
|
+
resetClosing: summary.resetClosing,
|
|
3155
|
+
finalizedClears: summary.finalizedClears,
|
|
3156
|
+
forceCleared: summary.forceCleared
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
} catch {}
|
|
3160
|
+
}
|
|
2741
3161
|
async function maintenance() {
|
|
3162
|
+
await sweepStaleWorkspaceOwnership();
|
|
2742
3163
|
syncWorkerSessionsFromService();
|
|
2743
3164
|
if (knownWorkerSessions.size === 0)
|
|
2744
3165
|
return;
|
|
@@ -2791,7 +3212,7 @@ function createLoopEngine(options) {
|
|
|
2791
3212
|
}
|
|
2792
3213
|
return s;
|
|
2793
3214
|
});
|
|
2794
|
-
|
|
3215
|
+
await continueGoal(goal.id);
|
|
2795
3216
|
}
|
|
2796
3217
|
}
|
|
2797
3218
|
if (!manual && (runtime.phase === "running" || runtime.phase === "idle") && goal.workerSessionID) {
|
|
@@ -2807,22 +3228,6 @@ function createLoopEngine(options) {
|
|
|
2807
3228
|
});
|
|
2808
3229
|
await logServerEvent(directory, "maintenance.stale-run-cleared", { goalID: goal.id });
|
|
2809
3230
|
}
|
|
2810
|
-
if ((runtime.activeToolCallIDs?.length ?? 0) > 0 && runtime.lastActivityAt) {
|
|
2811
|
-
const age = Date.now() - Date.parse(runtime.lastActivityAt);
|
|
2812
|
-
if (age > 30000) {
|
|
2813
|
-
await mutateState(directory, `maintenance.toolcall-ttl:${goal.id}`, async (s) => {
|
|
2814
|
-
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2815
|
-
if (rt && (rt.activeToolCallIDs?.length ?? 0) > 0) {
|
|
2816
|
-
rt.activeToolCallIDs = [];
|
|
2817
|
-
rt.idleCandidateAt = undefined;
|
|
2818
|
-
rt.idleCandidateGeneration = undefined;
|
|
2819
|
-
rt.updatedAt = new Date().toISOString();
|
|
2820
|
-
}
|
|
2821
|
-
return s;
|
|
2822
|
-
});
|
|
2823
|
-
await logServerEvent(directory, "maintenance.toolcall-ttl-cleared", { goalID: goal.id, age });
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
3231
|
const status = await host.sessionStatus(goal.workerSessionID);
|
|
2827
3232
|
if (status === "unknown") {
|
|
2828
3233
|
let shouldNotify = false;
|
|
@@ -2876,10 +3281,14 @@ function createLoopEngine(options) {
|
|
|
2876
3281
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2877
3282
|
if (!g || !rt || g.status !== "active")
|
|
2878
3283
|
return s;
|
|
3284
|
+
if (g.interactive || g.scopeClosing || g.workerSessionID !== goal.workerSessionID)
|
|
3285
|
+
return s;
|
|
2879
3286
|
if (rt.phase !== "running")
|
|
2880
3287
|
return s;
|
|
2881
3288
|
if (rt.runGeneration !== generation)
|
|
2882
3289
|
return s;
|
|
3290
|
+
if (s.workspaceOperation || (s.workspaceCalls ?? []).some((call) => call.goalID === goal.id) || rt.activeToolCallIDs?.length)
|
|
3291
|
+
return s;
|
|
2883
3292
|
clearedToolCalls = rt.activeToolCallIDs?.length ?? 0;
|
|
2884
3293
|
Object.assign(rt, releaseLease(rt));
|
|
2885
3294
|
rt.activeRunID = undefined;
|
|
@@ -2893,7 +3302,7 @@ function createLoopEngine(options) {
|
|
|
2893
3302
|
if (recovered) {
|
|
2894
3303
|
await appendEvent(directory, {
|
|
2895
3304
|
version: 1,
|
|
2896
|
-
eventID:
|
|
3305
|
+
eventID: randomUUID5(),
|
|
2897
3306
|
goalID: goal.id,
|
|
2898
3307
|
type: "run.recovered",
|
|
2899
3308
|
runID: stalledRunID ?? "unknown",
|
|
@@ -2927,7 +3336,7 @@ function createLoopEngine(options) {
|
|
|
2927
3336
|
const stuckSeconds = Math.floor((Date.now() - Date.parse(failedAt)) / 1000);
|
|
2928
3337
|
await appendEvent(directory, {
|
|
2929
3338
|
version: 1,
|
|
2930
|
-
eventID:
|
|
3339
|
+
eventID: randomUUID5(),
|
|
2931
3340
|
goalID: goal.id,
|
|
2932
3341
|
type: "run.stuck",
|
|
2933
3342
|
runID: runtime.activeRunID ?? "unknown",
|
|
@@ -2967,7 +3376,7 @@ function createLoopEngine(options) {
|
|
|
2967
3376
|
});
|
|
2968
3377
|
await appendEvent(directory, {
|
|
2969
3378
|
version: 1,
|
|
2970
|
-
eventID:
|
|
3379
|
+
eventID: randomUUID5(),
|
|
2971
3380
|
goalID: goal.id,
|
|
2972
3381
|
type: "run.stuck",
|
|
2973
3382
|
runID: runtime.activeRunID,
|
|
@@ -2990,14 +3399,54 @@ function createLoopEngine(options) {
|
|
|
2990
3399
|
}
|
|
2991
3400
|
|
|
2992
3401
|
// src/application/goal-service.ts
|
|
2993
|
-
import { randomUUID as
|
|
3402
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
3403
|
+
|
|
3404
|
+
// src/domain/goal.ts
|
|
3405
|
+
var MODEL_TRANSITIONS = {
|
|
3406
|
+
active: ["complete", "blocked"],
|
|
3407
|
+
paused: [],
|
|
3408
|
+
blocked: [],
|
|
3409
|
+
budget_limited: ["complete", "blocked"],
|
|
3410
|
+
usage_limited: [],
|
|
3411
|
+
complete: []
|
|
3412
|
+
};
|
|
3413
|
+
var USER_TRANSITIONS = {
|
|
3414
|
+
active: ["paused"],
|
|
3415
|
+
paused: ["active"],
|
|
3416
|
+
blocked: ["active"],
|
|
3417
|
+
budget_limited: ["active"],
|
|
3418
|
+
usage_limited: ["active"],
|
|
3419
|
+
complete: ["active"]
|
|
3420
|
+
};
|
|
3421
|
+
var SYSTEM_TRANSITIONS = {
|
|
3422
|
+
active: ["budget_limited", "usage_limited"],
|
|
3423
|
+
paused: [],
|
|
3424
|
+
blocked: [],
|
|
3425
|
+
budget_limited: [],
|
|
3426
|
+
usage_limited: [],
|
|
3427
|
+
complete: []
|
|
3428
|
+
};
|
|
3429
|
+
function canTransition(current, target, caller) {
|
|
3430
|
+
const table = caller === "model" ? MODEL_TRANSITIONS : caller === "user" ? USER_TRANSITIONS : SYSTEM_TRANSITIONS;
|
|
3431
|
+
return table[current]?.includes(target) ?? false;
|
|
3432
|
+
}
|
|
3433
|
+
function isTerminal(status) {
|
|
3434
|
+
return status === "complete";
|
|
3435
|
+
}
|
|
3436
|
+
function createGoal(input) {
|
|
3437
|
+
const now = new Date().toISOString();
|
|
3438
|
+
return { ...input, tokensUsed: 0, costUsed: 0, timeUsedSeconds: 0, createdAt: now, updatedAt: now };
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// src/application/goal-service.ts
|
|
2994
3442
|
init_state_repository();
|
|
2995
3443
|
init_command_await2();
|
|
2996
3444
|
import * as path4 from "path";
|
|
2997
3445
|
import { promises as fs4 } from "fs";
|
|
2998
3446
|
|
|
2999
3447
|
// src/server/host-adapter.ts
|
|
3000
|
-
|
|
3448
|
+
init_server_log();
|
|
3449
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3001
3450
|
|
|
3002
3451
|
// src/v2/native-rpc.ts
|
|
3003
3452
|
var NATIVE_RPC_ID = "loopd.native";
|
|
@@ -3223,7 +3672,7 @@ function parseModelRef(value) {
|
|
|
3223
3672
|
return { providerID, modelID };
|
|
3224
3673
|
}
|
|
3225
3674
|
function newPromptMessageID() {
|
|
3226
|
-
return `msg_${
|
|
3675
|
+
return `msg_${randomUUID6()}`;
|
|
3227
3676
|
}
|
|
3228
3677
|
function isV2PromptMessageID(messageID) {
|
|
3229
3678
|
return messageID.startsWith("msg_");
|
|
@@ -3332,7 +3781,7 @@ function createRealHost(client, directory) {
|
|
|
3332
3781
|
},
|
|
3333
3782
|
async readSession(sessionID) {
|
|
3334
3783
|
try {
|
|
3335
|
-
const result = await client.session.get({ path: { id: sessionID } });
|
|
3784
|
+
const result = await withTimeout(client.session.get({ path: { id: sessionID } }), 1e4, "OpenCode session.get");
|
|
3336
3785
|
if (result?.error)
|
|
3337
3786
|
return;
|
|
3338
3787
|
const data = result?.data;
|
|
@@ -3356,7 +3805,7 @@ function createRealHost(client, directory) {
|
|
|
3356
3805
|
},
|
|
3357
3806
|
async sessionStatus(sessionID) {
|
|
3358
3807
|
try {
|
|
3359
|
-
const result = await client.session.status({});
|
|
3808
|
+
const result = await withTimeout(client.session.status({}), 1e4, "OpenCode session.status");
|
|
3360
3809
|
if (result?.error)
|
|
3361
3810
|
return "unknown";
|
|
3362
3811
|
const data = result?.data;
|
|
@@ -3379,15 +3828,15 @@ function createRealHost(client, directory) {
|
|
|
3379
3828
|
},
|
|
3380
3829
|
async abortSession(sessionID) {
|
|
3381
3830
|
try {
|
|
3382
|
-
await client.session.abort({ path: { id: sessionID } });
|
|
3831
|
+
await withTimeout(client.session.abort({ path: { id: sessionID } }), 1e4, "OpenCode session.abort");
|
|
3383
3832
|
} catch {}
|
|
3384
3833
|
},
|
|
3385
3834
|
async readMessages(sessionID, limit = 10) {
|
|
3386
3835
|
try {
|
|
3387
|
-
const result = await client.session.messages({
|
|
3836
|
+
const result = await withTimeout(client.session.messages({
|
|
3388
3837
|
path: { id: sessionID },
|
|
3389
3838
|
query: { limit }
|
|
3390
|
-
});
|
|
3839
|
+
}), 1e4, "OpenCode session.messages");
|
|
3391
3840
|
const data = result?.data;
|
|
3392
3841
|
if (!Array.isArray(data))
|
|
3393
3842
|
return [];
|
|
@@ -3418,12 +3867,25 @@ function createRealHost(client, directory) {
|
|
|
3418
3867
|
return [];
|
|
3419
3868
|
}
|
|
3420
3869
|
},
|
|
3870
|
+
async readToolExecutions(sessionID) {
|
|
3871
|
+
try {
|
|
3872
|
+
const result = await withTimeout(client.session.messages({ path: { id: sessionID } }), 1e4, "OpenCode session.messages (tool terminal evidence)");
|
|
3873
|
+
if (!Array.isArray(result?.data))
|
|
3874
|
+
return [];
|
|
3875
|
+
return result.data.flatMap((message) => (message.parts ?? []).flatMap((part) => {
|
|
3876
|
+
const evidence = terminalToolExecution(part);
|
|
3877
|
+
return evidence?.sessionID === sessionID ? [evidence] : [];
|
|
3878
|
+
}));
|
|
3879
|
+
} catch {
|
|
3880
|
+
return [];
|
|
3881
|
+
}
|
|
3882
|
+
},
|
|
3421
3883
|
async compactSession(sessionID, model) {
|
|
3422
3884
|
try {
|
|
3423
3885
|
const identity = model ?? (await this.readSession(sessionID))?.model;
|
|
3424
3886
|
if (!identity || typeof client.session.summarize !== "function")
|
|
3425
3887
|
return;
|
|
3426
|
-
await client.session.summarize({ path: { id: sessionID }, body: identity });
|
|
3888
|
+
await withTimeout(client.session.summarize({ path: { id: sessionID }, body: identity }), 1e4, "OpenCode session.summarize");
|
|
3427
3889
|
} catch {}
|
|
3428
3890
|
},
|
|
3429
3891
|
async notifyOwner(ownerSessionID, message, agent) {
|
|
@@ -3434,7 +3896,7 @@ function createRealHost(client, directory) {
|
|
|
3434
3896
|
let resolvedAgent = agent?.trim() || undefined;
|
|
3435
3897
|
if (!resolvedAgent) {
|
|
3436
3898
|
try {
|
|
3437
|
-
const sess = await client.session.get({ path: { id: ownerSessionID } });
|
|
3899
|
+
const sess = await withTimeout(client.session.get({ path: { id: ownerSessionID } }), 1e4, "OpenCode session.get (parent identity)");
|
|
3438
3900
|
const liveAgent = sess?.data?.agent;
|
|
3439
3901
|
if (typeof liveAgent === "string" && liveAgent.trim())
|
|
3440
3902
|
resolvedAgent = liveAgent.trim();
|
|
@@ -3484,7 +3946,7 @@ function createV2Host(context, statuses, options = {}) {
|
|
|
3484
3946
|
async switchSessionAgent(sessionID, agent) {
|
|
3485
3947
|
if (typeof context.session.switchAgent !== "function")
|
|
3486
3948
|
return "unsupported";
|
|
3487
|
-
await context.session.switchAgent({ sessionID, agent });
|
|
3949
|
+
await withTimeout(context.session.switchAgent({ sessionID, agent }), 1e4, "OpenCode v2 session.switchAgent");
|
|
3488
3950
|
return "applied";
|
|
3489
3951
|
},
|
|
3490
3952
|
async listModels() {
|
|
@@ -3507,7 +3969,7 @@ function createV2Host(context, statuses, options = {}) {
|
|
|
3507
3969
|
async switchSessionModel(sessionID, model) {
|
|
3508
3970
|
if (typeof context.session.switchModel !== "function")
|
|
3509
3971
|
return "unsupported";
|
|
3510
|
-
await context.session.switchModel({ sessionID, model: { id: model.modelID, providerID: model.providerID } });
|
|
3972
|
+
await withTimeout(context.session.switchModel({ sessionID, model: { id: model.modelID, providerID: model.providerID } }), 1e4, "OpenCode v2 session.switchModel");
|
|
3511
3973
|
return "applied";
|
|
3512
3974
|
},
|
|
3513
3975
|
async createWorker({ parentID, title, agent, model, goalID }) {
|
|
@@ -3532,7 +3994,7 @@ function createV2Host(context, statuses, options = {}) {
|
|
|
3532
3994
|
}
|
|
3533
3995
|
if (outcome.kind === "native-child") {
|
|
3534
3996
|
const childID = outcome.childSessionID;
|
|
3535
|
-
const child = await context.session.get({ sessionID: childID });
|
|
3997
|
+
const child = await withTimeout(context.session.get({ sessionID: childID }), 1e4, "OpenCode v2 session.get (native parent verify)");
|
|
3536
3998
|
const actualParent = resolveNativeParentID(child);
|
|
3537
3999
|
if (actualParent !== parentID) {
|
|
3538
4000
|
const detail = `resolved-parent=${JSON.stringify(actualParent)} expected=${JSON.stringify(parentID)}`;
|
|
@@ -3540,16 +4002,16 @@ function createV2Host(context, statuses, options = {}) {
|
|
|
3540
4002
|
throw new Error(`loopd native worker creation failed for parent "${parentID}" (parent-mismatch): ${detail}`);
|
|
3541
4003
|
}
|
|
3542
4004
|
if (agent)
|
|
3543
|
-
await context.session.switchAgent({ sessionID: childID, agent });
|
|
4005
|
+
await withTimeout(context.session.switchAgent({ sessionID: childID, agent }), 1e4, "OpenCode v2 session.switchAgent (native re-apply)");
|
|
3544
4006
|
if (model) {
|
|
3545
|
-
await context.session.switchModel({
|
|
4007
|
+
await withTimeout(context.session.switchModel({
|
|
3546
4008
|
sessionID: childID,
|
|
3547
4009
|
model: { id: model.modelID, providerID: model.providerID }
|
|
3548
|
-
});
|
|
4010
|
+
}), 1e4, "OpenCode v2 session.switchModel (native re-apply)");
|
|
3549
4011
|
}
|
|
3550
4012
|
const rename = context.session.update;
|
|
3551
4013
|
if (rename)
|
|
3552
|
-
await rename({ sessionID: childID, title });
|
|
4014
|
+
await withTimeout(rename({ sessionID: childID, title }), 1e4, "OpenCode v2 session.update (native title)");
|
|
3553
4015
|
statuses.set(childID, "idle");
|
|
3554
4016
|
await logServerEvent(directory, "worker.created", {
|
|
3555
4017
|
parentID,
|
|
@@ -3566,13 +4028,13 @@ function createV2Host(context, statuses, options = {}) {
|
|
|
3566
4028
|
reason: outcome.kind === "fallback-safe" ? outcome.reason : "unclaimed"
|
|
3567
4029
|
});
|
|
3568
4030
|
}
|
|
3569
|
-
const session = await context.session.create({
|
|
4031
|
+
const session = await withTimeout(context.session.create({
|
|
3570
4032
|
title,
|
|
3571
4033
|
...agent !== undefined ? { agent } : {},
|
|
3572
4034
|
...model !== undefined ? { model: { id: model.modelID, providerID: model.providerID } } : {},
|
|
3573
4035
|
location: { directory },
|
|
3574
4036
|
metadata: { "loopd.parentID": parentID }
|
|
3575
|
-
});
|
|
4037
|
+
}), 1e4, "OpenCode v2 session.create");
|
|
3576
4038
|
statuses.set(session.id, "idle");
|
|
3577
4039
|
await logServerEvent(directory, "worker.created", {
|
|
3578
4040
|
parentID,
|
|
@@ -3584,28 +4046,28 @@ function createV2Host(context, statuses, options = {}) {
|
|
|
3584
4046
|
},
|
|
3585
4047
|
async promptWorker({ sessionID, prompt, messageID, model, agent }) {
|
|
3586
4048
|
if (agent)
|
|
3587
|
-
await context.session.switchAgent({ sessionID, agent });
|
|
4049
|
+
await withTimeout(context.session.switchAgent({ sessionID, agent }), 1e4, "OpenCode v2 session.switchAgent (prompt)");
|
|
3588
4050
|
if (model) {
|
|
3589
|
-
await context.session.switchModel({
|
|
4051
|
+
await withTimeout(context.session.switchModel({
|
|
3590
4052
|
sessionID,
|
|
3591
4053
|
model: { id: model.modelID, providerID: model.providerID }
|
|
3592
|
-
});
|
|
4054
|
+
}), 1e4, "OpenCode v2 session.switchModel (prompt)");
|
|
3593
4055
|
}
|
|
3594
4056
|
if (messageID !== undefined && !isV2PromptMessageID(messageID)) {
|
|
3595
4057
|
throw new Error(`loopd prompt ID "${messageID}" is invalid for OpenCode v2: must start with "msg_". ` + `Generate IDs with newPromptMessageID() so persisted, delivered, and correlated IDs agree.`);
|
|
3596
4058
|
}
|
|
3597
|
-
const result = await context.session.prompt({
|
|
4059
|
+
const result = await withTimeout(context.session.prompt({
|
|
3598
4060
|
sessionID,
|
|
3599
4061
|
id: messageID,
|
|
3600
4062
|
text: prompt
|
|
3601
|
-
});
|
|
4063
|
+
}), 30000, "OpenCode v2 session.prompt (acceptance)");
|
|
3602
4064
|
statuses.set(sessionID, "busy");
|
|
3603
4065
|
await logServerEvent(directory, "worker.prompted", { sessionID });
|
|
3604
4066
|
return { messageID: result?.id };
|
|
3605
4067
|
},
|
|
3606
4068
|
async readSession(sessionID) {
|
|
3607
4069
|
try {
|
|
3608
|
-
const session = await context.session.get({ sessionID });
|
|
4070
|
+
const session = await withTimeout(context.session.get({ sessionID }), 1e4, "OpenCode v2 session.get");
|
|
3609
4071
|
const model = session.model ? { providerID: session.model.providerID, modelID: session.model.id } : undefined;
|
|
3610
4072
|
if (!session.agent && !model)
|
|
3611
4073
|
return;
|
|
@@ -3625,7 +4087,7 @@ function createV2Host(context, statuses, options = {}) {
|
|
|
3625
4087
|
},
|
|
3626
4088
|
async readMessages(sessionID, limit = 10) {
|
|
3627
4089
|
try {
|
|
3628
|
-
const messages = await context.session.context({ sessionID });
|
|
4090
|
+
const messages = await withTimeout(context.session.context({ sessionID }), 1e4, "OpenCode v2 session.context");
|
|
3629
4091
|
let parentMessageID;
|
|
3630
4092
|
return messages.flatMap((message) => {
|
|
3631
4093
|
if (message.type === "user") {
|
|
@@ -3669,14 +4131,14 @@ function createV2Host(context, statuses, options = {}) {
|
|
|
3669
4131
|
if (model) {
|
|
3670
4132
|
if (typeof context.session.switchModel !== "function")
|
|
3671
4133
|
return;
|
|
3672
|
-
await context.session.switchModel({ sessionID, model: { id: model.modelID, providerID: model.providerID } });
|
|
4134
|
+
await withTimeout(context.session.switchModel({ sessionID, model: { id: model.modelID, providerID: model.providerID } }), 1e4, "OpenCode v2 session.switchModel (compact)");
|
|
3673
4135
|
}
|
|
3674
|
-
await context.session.command({ sessionID, name: "compact", text: "" });
|
|
4136
|
+
await withTimeout(context.session.command({ sessionID, name: "compact", text: "" }), 1e4, "OpenCode v2 session.command (compact)");
|
|
3675
4137
|
} catch {}
|
|
3676
4138
|
},
|
|
3677
4139
|
async notifyOwner(ownerSessionID, message) {
|
|
3678
4140
|
try {
|
|
3679
|
-
await context.session.prompt({ sessionID: ownerSessionID, text: message });
|
|
4141
|
+
await withTimeout(context.session.prompt({ sessionID: ownerSessionID, text: message }), 1e4, "OpenCode v2 parent notify (acceptance)");
|
|
3680
4142
|
await logServerEvent(directory, "parent.notified", { ownerSessionID, preview: message.slice(0, 160) });
|
|
3681
4143
|
return true;
|
|
3682
4144
|
} catch (error) {
|
|
@@ -3868,273 +4330,32 @@ function buildContinuationSteering(goal, runtime, context) {
|
|
|
3868
4330
|
else
|
|
3869
4331
|
parts.push(`- checks: not yet run`);
|
|
3870
4332
|
}
|
|
3871
|
-
if (v.artifactSummary)
|
|
3872
|
-
parts.push(`- artifacts: ${v.artifactSummary}`);
|
|
3873
|
-
}
|
|
3874
|
-
}
|
|
3875
|
-
parts.push(``, `## COMPLETION REVIEW`, `You are the semantic reviewer. The host is the acceptance authority.`, `Before proposing completion:`, `1. Derive concrete requirements from the objective and any referenced files/plans/specs/issues.`, `2. For each requirement, identify authoritative evidence: files, command output, test results.`, `3. Judge each: proves | contradicts | incomplete | missing.`, `4. Only call complete_goal when you have verified every requirement yourself.`, `5. If objective and checks appear contradictory, call block_goal \u2014 do not silently violate either.`);
|
|
3876
|
-
if (context?.forceFinish) {
|
|
3877
|
-
parts.push(``, `## FINAL REPORT REQUIRED \u2014 STOPPING SOON`, `The system requires you to wrap up now. Do NOT start new work.`, `Call complete_goal NOW with:`, `- summary: a specific semantic summary of what was accomplished (files changed, results, key findings)`, `- evidence: concrete proof (commands run, files created, checks passed)`, `If you cannot complete truthfully, call block_goal with the reason \u2014 do not fabricate evidence.`);
|
|
3878
|
-
} else {
|
|
3879
|
-
parts.push(``, `## INSTRUCTIONS`, `1. Inspect current workspace state \u2014 read files, check what exists. Do NOT redo completed work.`, `2. Continue concrete progress toward the objective.`, `3. After completing a batch, call report_goal_progress with what you did and what's next.`, `4. Use the built-in question tool only for genuinely risky ambiguity.`);
|
|
3880
|
-
}
|
|
3881
|
-
}
|
|
3882
|
-
if (context?.inboxMessages && context.inboxMessages.length > 0) {
|
|
3883
|
-
parts.push(``, `## USER INSTRUCTIONS`);
|
|
3884
|
-
for (const msg of context.inboxMessages) {
|
|
3885
|
-
parts.push(`- ${msg}`);
|
|
3886
|
-
}
|
|
3887
|
-
}
|
|
3888
|
-
parts.push(...scopeInstructions);
|
|
3889
|
-
return parts.join(`
|
|
3890
|
-
`);
|
|
3891
|
-
}
|
|
3892
|
-
|
|
3893
|
-
// src/application/workspace-execution.ts
|
|
3894
|
-
init_state_repository();
|
|
3895
|
-
init_workspace_scope();
|
|
3896
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
3897
|
-
import path3 from "path";
|
|
3898
|
-
var readTools = new Set([
|
|
3899
|
-
"read",
|
|
3900
|
-
"glob",
|
|
3901
|
-
"grep",
|
|
3902
|
-
"list",
|
|
3903
|
-
"webfetch",
|
|
3904
|
-
"websearch",
|
|
3905
|
-
"todowrite",
|
|
3906
|
-
"skill",
|
|
3907
|
-
"get_goal",
|
|
3908
|
-
"claim_goal_scope",
|
|
3909
|
-
"report_goal_progress",
|
|
3910
|
-
"complete_goal",
|
|
3911
|
-
"block_goal",
|
|
3912
|
-
"list_background_goals",
|
|
3913
|
-
"inspect_background_goal",
|
|
3914
|
-
"read_goal_transcript",
|
|
3915
|
-
"loopd_command_get",
|
|
3916
|
-
"loopd_command_list",
|
|
3917
|
-
"loopd_command_await",
|
|
3918
|
-
"loopd_command_interrupt",
|
|
3919
|
-
"loopd_command_terminate",
|
|
3920
|
-
"loopd_command_remove",
|
|
3921
|
-
"loopd_list_agents",
|
|
3922
|
-
"loopd_list_models",
|
|
3923
|
-
"run_goal_checks",
|
|
3924
|
-
"codegraph_codegraph_context",
|
|
3925
|
-
"codegraph_codegraph_search",
|
|
3926
|
-
"codegraph_codegraph_node",
|
|
3927
|
-
"codegraph_codegraph_explore",
|
|
3928
|
-
"codegraph_codegraph_files",
|
|
3929
|
-
"codegraph_codegraph_trace",
|
|
3930
|
-
"codegraph_codegraph_callers",
|
|
3931
|
-
"codegraph_codegraph_callees",
|
|
3932
|
-
"codegraph_codegraph_impact",
|
|
3933
|
-
"codegraph_codegraph_status"
|
|
3934
|
-
]);
|
|
3935
|
-
var ownerTools = new Set(["loopd_create_goal", "switch_goal_identity", "pause_goal", "resume_goal", "clear_goal", "abort_goal_worker", "nudge_goal", "send_goal_input", "force_block_goal", "force_complete_goal"]);
|
|
3936
|
-
function structuredWritePaths(tool, args) {
|
|
3937
|
-
if (tool === "write" || tool === "edit") {
|
|
3938
|
-
if (typeof args.filePath !== "string")
|
|
3939
|
-
throw new Error(`Scope denied: ${tool} requires filePath.`);
|
|
3940
|
-
return [args.filePath];
|
|
3941
|
-
}
|
|
3942
|
-
if (tool !== "apply_patch")
|
|
3943
|
-
return;
|
|
3944
|
-
const source = args.patchText ?? args.patch;
|
|
3945
|
-
if (typeof source !== "string" || !source.startsWith(`*** Begin Patch
|
|
3946
|
-
`) || !source.trimEnd().endsWith("*** End Patch")) {
|
|
3947
|
-
throw new Error("Scope denied: unsupported apply_patch format.");
|
|
3948
|
-
}
|
|
3949
|
-
const paths = [];
|
|
3950
|
-
let section = false;
|
|
3951
|
-
for (const line of source.split(`
|
|
3952
|
-
`)) {
|
|
3953
|
-
const header = /^\*\*\* (?:Add File|Update File|Delete File): (.+)$/.exec(line);
|
|
3954
|
-
if (header) {
|
|
3955
|
-
paths.push(header[1]);
|
|
3956
|
-
section = true;
|
|
3957
|
-
continue;
|
|
3958
|
-
}
|
|
3959
|
-
const move = /^\*\*\* Move to: (.+)$/.exec(line);
|
|
3960
|
-
if (move) {
|
|
3961
|
-
if (!section)
|
|
3962
|
-
throw new Error("Scope denied: rename outside patch section.");
|
|
3963
|
-
paths.push(move[1]);
|
|
3964
|
-
continue;
|
|
3965
|
-
}
|
|
3966
|
-
if (line.startsWith("*** ") && !["*** Begin Patch", "*** End Patch", "*** End of File"].includes(line)) {
|
|
3967
|
-
throw new Error("Scope denied: unknown patch operation.");
|
|
3968
|
-
}
|
|
3969
|
-
}
|
|
3970
|
-
if (!paths.length)
|
|
3971
|
-
throw new Error("Scope denied: patch has no file operations.");
|
|
3972
|
-
return paths;
|
|
3973
|
-
}
|
|
3974
|
-
function heldGoals(state) {
|
|
3975
|
-
return state.goals.filter((goal) => goal.status === "active" || goal.scopeClosing || state.workspaceCalls?.some((call) => call.goalID === goal.id) || state.workspaceOperation?.goalID === goal.id);
|
|
3976
|
-
}
|
|
3977
|
-
function mayBeAlive(pid) {
|
|
3978
|
-
if (!pid || pid < 1)
|
|
3979
|
-
return true;
|
|
3980
|
-
try {
|
|
3981
|
-
process.kill(pid, 0);
|
|
3982
|
-
return true;
|
|
3983
|
-
} catch (error) {
|
|
3984
|
-
return error?.code !== "ESRCH";
|
|
3985
|
-
}
|
|
3986
|
-
}
|
|
3987
|
-
function assertScopeAvailable(state, goal) {
|
|
3988
|
-
if (goal.scopeClosing || state.workspaceCalls?.some((call) => call.goalID === goal.id)) {
|
|
3989
|
-
throw new Error("Goal still has closing/in-flight writes; ownership cannot be reacquired before quiescence.");
|
|
3990
|
-
}
|
|
3991
|
-
if (state.workspaceOperation)
|
|
3992
|
-
throw new Error(`Workspace operation ${state.workspaceOperation.id} is active.`);
|
|
3993
|
-
if (goal.config.write_scope !== undefined || goal.config.workspaceWrite === false) {
|
|
3994
|
-
const orphan = [...state.orphanedCommandProcesses || [], ...(state.commands || []).filter((command) => command.status === "missing").map((command) => ({ commandID: command.id, pid: command.pid }))].find((process2) => mayBeAlive(process2.pid));
|
|
3995
|
-
if (orphan)
|
|
3996
|
-
throw new Error(`Protected activation denied: command ${orphan.commandID} lost its handle and process exit is unproved. Stop/verify the orphan process externally; removing its log does not release this fence.`);
|
|
3997
|
-
if (state.workspaceCalls?.some((call) => call.uncontrolled))
|
|
3998
|
-
throw new Error("Protected goal activation denied: an uncontrolled tool/shell is still executing. Wait for its exit event, do not poll claims.");
|
|
3999
|
-
const running = state.commands?.find((command) => command.status === "running");
|
|
4000
|
-
if (running)
|
|
4001
|
-
throw new Error(`Protected goal activation denied: uncontrolled command ${running.id} is still running. Terminate it before starting scoped/read-only work.`);
|
|
4002
|
-
}
|
|
4003
|
-
const conflict = findScopeConflict(heldGoals(state).map((owner) => ({ ...owner, status: "active" })), goal);
|
|
4004
|
-
if (conflict) {
|
|
4005
|
-
const owner = state.goals.find((item) => item.id === conflict.goalID);
|
|
4006
|
-
if (goal.config.write_scope === undefined && owner?.config.write_scope === undefined) {
|
|
4007
|
-
throw new Error(`Workspace-writing goal "${conflict.name}" (${conflict.goalID}) is already active (owned by session ${conflict.ownerSessionID}). Pause, block, complete, or clear it before activating another workspace-writing goal. Note: list_background_goals shows only this session's goals.`);
|
|
4008
|
-
}
|
|
4009
|
-
throw new Error(scopeConflictMessage(conflict));
|
|
4010
|
-
}
|
|
4011
|
-
if (goal.config.workspaceWrite !== false && goal.config.write_scope?.length !== 0) {
|
|
4012
|
-
const call = state.workspaceCalls?.find((call) => call.goalID !== goal.id && (goal.config.write_scope === undefined || call.paths.some((file) => goal.config.write_scope.includes(file))));
|
|
4013
|
-
if (call)
|
|
4014
|
-
throw new Error(`Scope conflict with in-flight call ${call.callID}, session ${call.sessionID}, path ${call.paths[0]}.`);
|
|
4015
|
-
}
|
|
4016
|
-
}
|
|
4017
|
-
function retireWorker(state, goal) {
|
|
4018
|
-
if (!goal.workerSessionID)
|
|
4019
|
-
return;
|
|
4020
|
-
state.retiredWorkerSessions = [...new Set([...state.retiredWorkerSessions || [], goal.workerSessionID])];
|
|
4021
|
-
}
|
|
4022
|
-
async function beforeWorkspaceTool(directory, input, args) {
|
|
4023
|
-
if (readTools.has(input.tool))
|
|
4024
|
-
return;
|
|
4025
|
-
await mutateState(directory, `workspace.before:${input.callID}`, async (state) => {
|
|
4026
|
-
const goal = state.goals.find((item) => item.workerSessionID === input.sessionID);
|
|
4027
|
-
if (state.retiredWorkerSessions?.includes(input.sessionID))
|
|
4028
|
-
throw new Error("Scope denied: retired worker session; late executions are fenced.");
|
|
4029
|
-
const held = heldGoals(state);
|
|
4030
|
-
const protectedGoals = held.filter((item) => item.config.write_scope !== undefined || item.config.workspaceWrite === false);
|
|
4031
|
-
if (!goal && ownerTools.has(input.tool) && (!protectedGoals.length || state.goals.some((item) => item.ownerSessionID === input.sessionID)))
|
|
4032
|
-
return state;
|
|
4033
|
-
if (!state.workspaceOperation && !protectedGoals.length && (!goal || goal.config.write_scope === undefined && goal.config.workspaceWrite !== false)) {
|
|
4034
|
-
let paths = [];
|
|
4035
|
-
let uncontrolled = false;
|
|
4036
|
-
try {
|
|
4037
|
-
const writes = structuredWritePaths(input.tool, args);
|
|
4038
|
-
if (writes)
|
|
4039
|
-
paths = await Promise.all(writes.map((file) => canonicalScopePath(directory, file)));
|
|
4040
|
-
else
|
|
4041
|
-
uncontrolled = true;
|
|
4042
|
-
} catch {
|
|
4043
|
-
uncontrolled = true;
|
|
4044
|
-
}
|
|
4045
|
-
state.workspaceCalls ??= [];
|
|
4046
|
-
if (!state.workspaceCalls.some((call) => call.callID === input.callID && call.sessionID === input.sessionID)) {
|
|
4047
|
-
state.workspaceCalls.push({ callID: input.callID, sessionID: input.sessionID, goalID: goal?.id, paths, uncontrolled, at: new Date().toISOString() });
|
|
4048
|
-
}
|
|
4049
|
-
return state;
|
|
4050
|
-
}
|
|
4051
|
-
const writes = structuredWritePaths(input.tool, args);
|
|
4052
|
-
if (!writes)
|
|
4053
|
-
throw new Error(`Scope denied: uncontrolled tool "${input.tool}". Shell, commands, custom tools, batch and subagents are not allowed while protected goals exist. Use structured edits or run_goal_checks.`);
|
|
4054
|
-
if (state.workspaceOperation)
|
|
4055
|
-
throw new Error(`Workspace operation ${state.workspaceOperation.id} is active; source writes are denied until it finishes.`);
|
|
4056
|
-
if (goal && (goal.status !== "active" || goal.scopeClosing))
|
|
4057
|
-
throw new Error(`Scope denied: worker goal is ${goal.scopeClosing ? "closing" : goal.status}.`);
|
|
4058
|
-
if (!goal && !state.goals.some((item) => item.ownerSessionID === input.sessionID)) {
|
|
4059
|
-
throw new Error("Scope denied: unrecognized session/child. Subagent writes are unsupported.");
|
|
4060
|
-
}
|
|
4061
|
-
const paths = await Promise.all(writes.map((file) => canonicalScopePath(directory, file)));
|
|
4062
|
-
const artifact = goal?.config.artifactDir ? path3.relative(directory, goal.config.artifactDir).split(path3.sep).join("/") : undefined;
|
|
4063
|
-
for (const file of paths) {
|
|
4064
|
-
const inArtifact = artifact && file.startsWith(`${artifact}/`);
|
|
4065
|
-
if (goal && !inArtifact && (goal.config.workspaceWrite === false || goal.config.write_scope !== undefined && !goal.config.write_scope.includes(file))) {
|
|
4066
|
-
throw new Error(`Scope denied: "${file}" is outside this goal's write_scope. Claim it before editing.`);
|
|
4067
|
-
}
|
|
4068
|
-
const owner = !inArtifact && held.find((item) => item.id !== goal?.id && item.config.workspaceWrite !== false && (item.config.write_scope === undefined || item.config.write_scope.includes(file)));
|
|
4069
|
-
if (owner)
|
|
4070
|
-
throw new Error(scopeConflictMessage({ goalID: owner.id, ownerSessionID: owner.ownerSessionID, name: owner.name, path: file }));
|
|
4071
|
-
const call = state.workspaceCalls?.find((item) => item.paths.includes(file) && !(item.callID === input.callID && item.sessionID === input.sessionID));
|
|
4072
|
-
if (call)
|
|
4073
|
-
throw new Error(`Scope denied: "${file}" is being written by session ${call.sessionID}, call ${call.callID}.`);
|
|
4074
|
-
}
|
|
4075
|
-
if (!state.workspaceCalls)
|
|
4076
|
-
state.workspaceCalls = [];
|
|
4077
|
-
if (!state.workspaceCalls.some((call) => call.callID === input.callID && call.sessionID === input.sessionID)) {
|
|
4078
|
-
state.workspaceCalls.push({ callID: input.callID, sessionID: input.sessionID, goalID: goal?.id, generation: state.runtimes.find((runtime) => runtime.goalID === goal?.id)?.runGeneration, paths, at: new Date().toISOString() });
|
|
4079
|
-
}
|
|
4080
|
-
return state;
|
|
4081
|
-
});
|
|
4082
|
-
}
|
|
4083
|
-
async function afterWorkspaceTool(directory, sessionID, callID) {
|
|
4084
|
-
await mutateState(directory, `workspace.after:${callID}`, async (state) => {
|
|
4085
|
-
state.workspaceCalls = state.workspaceCalls?.filter((call) => call.sessionID !== sessionID || call.callID !== callID);
|
|
4086
|
-
state.goals = state.goals.filter((goal) => {
|
|
4087
|
-
if (!goal.scopeClearPending || state.workspaceCalls?.some((call) => call.goalID === goal.id) || state.workspaceOperation?.goalID === goal.id)
|
|
4088
|
-
return true;
|
|
4089
|
-
state.runtimes = state.runtimes.filter((runtime) => runtime.goalID !== goal.id);
|
|
4090
|
-
return false;
|
|
4091
|
-
});
|
|
4092
|
-
return state;
|
|
4093
|
-
});
|
|
4094
|
-
}
|
|
4095
|
-
function guardV2ToolEditor(directory, editor) {
|
|
4096
|
-
if (typeof editor.list !== "function" || typeof editor.update !== "function")
|
|
4097
|
-
throw new Error("Scoped execution requires the v2 tool editor list/update execution surface.");
|
|
4098
|
-
for (const entry of editor.list()) {
|
|
4099
|
-
editor.update(entry.id, (definition) => {
|
|
4100
|
-
const execute = definition.execute;
|
|
4101
|
-
definition.execute = async (args, context) => {
|
|
4102
|
-
await beforeWorkspaceTool(directory, { tool: entry.name, sessionID: context.sessionID, callID: context.id }, args);
|
|
4103
|
-
try {
|
|
4104
|
-
return await execute(args, context);
|
|
4105
|
-
} finally {
|
|
4106
|
-
await afterWorkspaceTool(directory, context.sessionID, context.id);
|
|
4107
|
-
}
|
|
4108
|
-
};
|
|
4109
|
-
});
|
|
4333
|
+
if (v.artifactSummary)
|
|
4334
|
+
parts.push(`- artifacts: ${v.artifactSummary}`);
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
parts.push(``, `## COMPLETION REVIEW`, `You are the semantic reviewer. The host is the acceptance authority.`, `Before proposing completion:`, `1. Derive concrete requirements from the objective and any referenced files/plans/specs/issues.`, `2. For each requirement, identify authoritative evidence: files, command output, test results.`, `3. Judge each: proves | contradicts | incomplete | missing.`, `4. Only call complete_goal when you have verified every requirement yourself.`, `5. If objective and checks appear contradictory, call block_goal \u2014 do not silently violate either.`);
|
|
4338
|
+
if (context?.forceFinish) {
|
|
4339
|
+
parts.push(``, `## FINAL REPORT REQUIRED \u2014 STOPPING SOON`, `The system requires you to wrap up now. Do NOT start new work.`, `Call complete_goal NOW with:`, `- summary: a specific semantic summary of what was accomplished (files changed, results, key findings)`, `- evidence: concrete proof (commands run, files created, checks passed)`, `If you cannot complete truthfully, call block_goal with the reason \u2014 do not fabricate evidence.`);
|
|
4340
|
+
} else {
|
|
4341
|
+
parts.push(``, `## INSTRUCTIONS`, `1. Inspect current workspace state \u2014 read files, check what exists. Do NOT redo completed work.`, `2. Continue concrete progress toward the objective.`, `3. After completing a batch, call report_goal_progress with what you did and what's next.`, `4. Use the built-in question tool only for genuinely risky ambiguity.`);
|
|
4342
|
+
}
|
|
4110
4343
|
}
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
throw new Error("Workspace is busy with an operation or in-flight writes; verification denied until quiescent.");
|
|
4117
|
-
state.workspaceOperation = { id, goalID, sessionID };
|
|
4118
|
-
return state;
|
|
4119
|
-
});
|
|
4120
|
-
try {
|
|
4121
|
-
return await run();
|
|
4122
|
-
} finally {
|
|
4123
|
-
await mutateState(directory, `workspace.operation-end:${id}`, async (state) => {
|
|
4124
|
-
if (state.workspaceOperation?.id === id)
|
|
4125
|
-
state.workspaceOperation = undefined;
|
|
4126
|
-
state.goals = state.goals.filter((goal) => {
|
|
4127
|
-
if (!goal.scopeClearPending || state.workspaceCalls?.some((call) => call.goalID === goal.id))
|
|
4128
|
-
return true;
|
|
4129
|
-
state.runtimes = state.runtimes.filter((runtime) => runtime.goalID !== goal.id);
|
|
4130
|
-
return false;
|
|
4131
|
-
});
|
|
4132
|
-
return state;
|
|
4133
|
-
});
|
|
4344
|
+
if (context?.inboxMessages && context.inboxMessages.length > 0) {
|
|
4345
|
+
parts.push(``, `## USER INSTRUCTIONS`);
|
|
4346
|
+
for (const msg of context.inboxMessages) {
|
|
4347
|
+
parts.push(`- ${msg}`);
|
|
4348
|
+
}
|
|
4134
4349
|
}
|
|
4350
|
+
parts.push(...scopeInstructions);
|
|
4351
|
+
return parts.join(`
|
|
4352
|
+
`);
|
|
4135
4353
|
}
|
|
4136
4354
|
|
|
4137
4355
|
// src/application/goal-service.ts
|
|
4356
|
+
init_server_log();
|
|
4357
|
+
init_workspace_execution();
|
|
4358
|
+
|
|
4138
4359
|
class GoalStartError extends Error {
|
|
4139
4360
|
goalID;
|
|
4140
4361
|
workerSessionID;
|
|
@@ -4147,6 +4368,14 @@ class GoalStartError extends Error {
|
|
|
4147
4368
|
this.failedStage = info.failedStage;
|
|
4148
4369
|
}
|
|
4149
4370
|
}
|
|
4371
|
+
|
|
4372
|
+
class GoalDeliveryBlockedError extends Error {
|
|
4373
|
+
result;
|
|
4374
|
+
constructor(result) {
|
|
4375
|
+
super(result.message);
|
|
4376
|
+
this.result = result;
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4150
4379
|
function createGoalService(host) {
|
|
4151
4380
|
const workers = createWorkerManager(host);
|
|
4152
4381
|
const sessions = new Map;
|
|
@@ -4639,17 +4868,22 @@ function createGoalService(host) {
|
|
|
4639
4868
|
await host.compactSession(fresh.workerSessionID, parseModelRef(fresh.config.model));
|
|
4640
4869
|
});
|
|
4641
4870
|
}
|
|
4642
|
-
async function recordPromptFailure(directory, goalID, error, blockImmediately = false) {
|
|
4871
|
+
async function recordPromptFailure(directory, goalID, error, blockImmediately = false, expectedRunID) {
|
|
4643
4872
|
const detail = describeError(error);
|
|
4644
4873
|
let runID = "unknown";
|
|
4645
4874
|
let failureCount = 0;
|
|
4646
4875
|
let blocked = false;
|
|
4876
|
+
let superseded = false;
|
|
4647
4877
|
let blockerNeeded = "Retry after the OpenCode worker/session API is available.";
|
|
4648
4878
|
const state = await mutateState(directory, `turn.prompt-failed:${goalID}`, async (s) => {
|
|
4649
4879
|
const goal = s.goals.find((item) => item.id === goalID);
|
|
4650
4880
|
const rt = s.runtimes.find((item) => item.goalID === goalID);
|
|
4651
4881
|
if (!goal || !rt)
|
|
4652
4882
|
return s;
|
|
4883
|
+
if (expectedRunID !== undefined && rt.activeRunID !== expectedRunID) {
|
|
4884
|
+
superseded = true;
|
|
4885
|
+
return s;
|
|
4886
|
+
}
|
|
4653
4887
|
runID = rt.activeRunID || "unknown";
|
|
4654
4888
|
failureCount = rt.consecutiveFailures + 1;
|
|
4655
4889
|
Object.assign(rt, releaseLease(rt));
|
|
@@ -4674,9 +4908,14 @@ function createGoalService(host) {
|
|
|
4674
4908
|
rt.updatedAt = new Date().toISOString();
|
|
4675
4909
|
return s;
|
|
4676
4910
|
});
|
|
4911
|
+
if (superseded) {
|
|
4912
|
+
await logServerEvent(directory, "turn.prompt-failed-superseded", { goalID, expectedRunID }).catch(() => {});
|
|
4913
|
+
await observeProviderErrorUnlocked(directory, goalID, error, "prompt-delivery");
|
|
4914
|
+
return;
|
|
4915
|
+
}
|
|
4677
4916
|
await appendEvent(directory, {
|
|
4678
4917
|
version: 1,
|
|
4679
|
-
eventID:
|
|
4918
|
+
eventID: randomUUID7(),
|
|
4680
4919
|
goalID,
|
|
4681
4920
|
type: "run.failed",
|
|
4682
4921
|
runID,
|
|
@@ -4688,7 +4927,7 @@ function createGoalService(host) {
|
|
|
4688
4927
|
if (blocked) {
|
|
4689
4928
|
await appendEvent(directory, {
|
|
4690
4929
|
version: 1,
|
|
4691
|
-
eventID:
|
|
4930
|
+
eventID: randomUUID7(),
|
|
4692
4931
|
goalID,
|
|
4693
4932
|
type: "goal.blocked",
|
|
4694
4933
|
reason: `Worker prompt delivery failed: ${detail}`,
|
|
@@ -4699,6 +4938,95 @@ function createGoalService(host) {
|
|
|
4699
4938
|
}
|
|
4700
4939
|
await observeProviderErrorUnlocked(directory, goalID, error, "prompt-delivery");
|
|
4701
4940
|
}
|
|
4941
|
+
function quotaHoldMessage(goal) {
|
|
4942
|
+
const observation = goal.lastProviderLimit;
|
|
4943
|
+
const retryMs = observation?.retryAt ? Date.parse(observation.retryAt) : NaN;
|
|
4944
|
+
if (!observation || !Number.isFinite(retryMs) || retryMs <= Date.now())
|
|
4945
|
+
return;
|
|
4946
|
+
if (observation.model && observation.model !== goal.config.model)
|
|
4947
|
+
return;
|
|
4948
|
+
const kind = observation.kind === "quota" ? "quota exhausted" : "rate-limited";
|
|
4949
|
+
const pending = goal.modelSwitch?.pending?.reason === "quota" ? ` A quota fallback to "${goal.modelSwitch.pending.model}" is prepared and applies on the next idle turn.` : "";
|
|
4950
|
+
return `Model "${goal.config.model ?? "default"}" is provider-${kind} ` + `(observed ${observation.observedAt}; provider says retry after ${observation.retryAt}).` + `${pending} Not re-prompting: nudging a limited model burns quota without helping. ` + `Wait for the retry time, switch model, or use retry_goal afterwards.`;
|
|
4951
|
+
}
|
|
4952
|
+
async function wasPromptAccepted(directory, workerSessionID, promptMessageID) {
|
|
4953
|
+
if (!promptMessageID)
|
|
4954
|
+
return false;
|
|
4955
|
+
try {
|
|
4956
|
+
const tail = await withStageTimeout(host.readMessages(workerSessionID, 10), PROMPT_VERIFY_TIMEOUT_MS, `late prompt-acceptance verify for session "${workerSessionID}"`);
|
|
4957
|
+
const found = tail.some((m) => m.messageID === promptMessageID || m.parentMessageID === promptMessageID);
|
|
4958
|
+
if (found) {
|
|
4959
|
+
await logServerEvent(directory, "worker.prompted-late", { workerSessionID, promptMessageID }).catch(() => {});
|
|
4960
|
+
}
|
|
4961
|
+
return found;
|
|
4962
|
+
} catch {
|
|
4963
|
+
return false;
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
async function dispatchOrRecordFailure(directory, goalID, dispatch, run, workerSessionID, blockImmediately = false) {
|
|
4967
|
+
let blocked;
|
|
4968
|
+
await mutateState(directory, `turn.dispatch-intent:${goalID}`, async (s) => {
|
|
4969
|
+
blocked = continuationGuard(s, goalID);
|
|
4970
|
+
const g = s.goals.find((item) => item.id === goalID);
|
|
4971
|
+
const rt = s.runtimes.find((item) => item.goalID === goalID);
|
|
4972
|
+
const ownsRun = rt && g?.workerSessionID === workerSessionID && rt.activeRunID === run.activeRunID && rt.activePromptMessageID === run.activePromptMessageID && (run.runGeneration === undefined || rt.runGeneration === run.runGeneration);
|
|
4973
|
+
if (!ownsRun)
|
|
4974
|
+
blocked ??= rejected(goalID, "blocked", "Run or worker was superseded before dispatch; no prompt was sent.");
|
|
4975
|
+
if (blocked) {
|
|
4976
|
+
if (ownsRun) {
|
|
4977
|
+
Object.assign(rt, releaseLease(rt));
|
|
4978
|
+
rt.activeRunID = undefined;
|
|
4979
|
+
}
|
|
4980
|
+
} else if (rt) {
|
|
4981
|
+
rt.activePromptDispatchAt = new Date().toISOString();
|
|
4982
|
+
}
|
|
4983
|
+
return s;
|
|
4984
|
+
});
|
|
4985
|
+
if (blocked) {
|
|
4986
|
+
if (blocked.outcome === "not_found")
|
|
4987
|
+
sessions.delete(goalID);
|
|
4988
|
+
throw new GoalDeliveryBlockedError(blocked);
|
|
4989
|
+
}
|
|
4990
|
+
let observed = false;
|
|
4991
|
+
try {
|
|
4992
|
+
await dispatch();
|
|
4993
|
+
observed = await wasPromptAccepted(directory, workerSessionID, run.activePromptMessageID);
|
|
4994
|
+
} catch (error) {
|
|
4995
|
+
const current = await readState(directory);
|
|
4996
|
+
if (!current.goals.some((goal) => goal.id === goalID)) {
|
|
4997
|
+
throw new GoalDeliveryBlockedError(rejected(goalID, "not_found", "Goal cleared while host delivery was pending. Its late result was ignored; list current goals instead of retrying this ID."));
|
|
4998
|
+
}
|
|
4999
|
+
observed = await wasPromptAccepted(directory, workerSessionID, run.activePromptMessageID);
|
|
5000
|
+
if (!observed) {
|
|
5001
|
+
await recordPromptFailure(directory, goalID, error, blockImmediately, run.activeRunID);
|
|
5002
|
+
throw error;
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
let removed = false;
|
|
5006
|
+
let acceptedRun = false;
|
|
5007
|
+
const accepted = await mutateState(directory, `turn.dispatch-accepted:${goalID}`, async (s) => {
|
|
5008
|
+
const g = s.goals.find((item) => item.id === goalID);
|
|
5009
|
+
removed = !g;
|
|
5010
|
+
const rt = s.runtimes.find((item) => item.goalID === goalID);
|
|
5011
|
+
if (rt && g?.status === "active" && g.workerSessionID === workerSessionID && rt.activeRunID === run.activeRunID && rt.activePromptMessageID === run.activePromptMessageID && (run.runGeneration === undefined || rt.runGeneration === run.runGeneration)) {
|
|
5012
|
+
acceptedRun = true;
|
|
5013
|
+
rt.activePromptAcceptedAt = new Date().toISOString();
|
|
5014
|
+
if (run.forceFinish)
|
|
5015
|
+
rt.forceFinishRequested = true;
|
|
5016
|
+
if (observed)
|
|
5017
|
+
rt.activePromptObservedAt ??= rt.activePromptAcceptedAt;
|
|
5018
|
+
}
|
|
5019
|
+
return s;
|
|
5020
|
+
});
|
|
5021
|
+
if (removed)
|
|
5022
|
+
throw new GoalDeliveryBlockedError(rejected(goalID, "not_found", "Goal cleared while host delivery was pending. Its late result was ignored; list current goals instead of retrying this ID."));
|
|
5023
|
+
if (!acceptedRun)
|
|
5024
|
+
throw new GoalDeliveryBlockedError(rejected(goalID, "blocked", "The turn or worker changed while host delivery was pending. Its late result was ignored; inspect the current run before retrying."));
|
|
5025
|
+
const rt = accepted.runtimes.find((item) => item.goalID === goalID);
|
|
5026
|
+
if (rt && rt.activeRunID === run.activeRunID && rt.activePromptAcceptedAt) {
|
|
5027
|
+
await acknowledgeGoalInbox(directory, goalID, rt.activeInboxLines ?? []);
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
4702
5030
|
async function ensureWorkerSession(directory, goal) {
|
|
4703
5031
|
let session = sessions.get(goal.id);
|
|
4704
5032
|
if (session)
|
|
@@ -4713,22 +5041,36 @@ function createGoalService(host) {
|
|
|
4713
5041
|
return session;
|
|
4714
5042
|
}
|
|
4715
5043
|
session = await workers.createWorker(goal);
|
|
4716
|
-
|
|
5044
|
+
let linked = false;
|
|
5045
|
+
let failure;
|
|
4717
5046
|
await mutateState(directory, `goal.set-worker:${goal.id}`, async (s) => {
|
|
4718
5047
|
const persisted = s.goals.find((item) => item.id === goal.id);
|
|
4719
|
-
if (persisted)
|
|
5048
|
+
if (!persisted)
|
|
5049
|
+
failure = rejected(goal.id, "not_found", "Goal cleared during worker creation; the new worker was retired. List your goals instead of retrying this ID.");
|
|
5050
|
+
else if (persisted.ownerSessionID !== goal.ownerSessionID || persisted.workerSessionID !== goal.workerSessionID || persisted.status !== "active") {
|
|
5051
|
+
failure = rejected(goal.id, "blocked", "Goal changed during worker creation; the unused worker was retired. Inspect current state before retrying.");
|
|
5052
|
+
} else {
|
|
4720
5053
|
persisted.workerSessionID = session.workerSessionID;
|
|
5054
|
+
linked = true;
|
|
4721
5055
|
if (session.topology) {
|
|
4722
5056
|
persisted.workerTopology = session.topology;
|
|
4723
5057
|
persisted.nativeParentID = session.nativeParentID;
|
|
4724
5058
|
}
|
|
4725
5059
|
}
|
|
5060
|
+
if (!linked)
|
|
5061
|
+
retireWorker(s, { ...goal, workerSessionID: session.workerSessionID });
|
|
4726
5062
|
return s;
|
|
4727
5063
|
});
|
|
5064
|
+
if (!linked) {
|
|
5065
|
+
sessions.delete(goal.id);
|
|
5066
|
+
await workers.abortWorker(session.workerSessionID).catch(() => {});
|
|
5067
|
+
throw new GoalDeliveryBlockedError(failure);
|
|
5068
|
+
}
|
|
5069
|
+
sessions.set(goal.id, session);
|
|
4728
5070
|
return session;
|
|
4729
5071
|
}
|
|
4730
5072
|
async function start(directory, input) {
|
|
4731
|
-
const id =
|
|
5073
|
+
const id = randomUUID7();
|
|
4732
5074
|
return withGoalOperation(id, () => startUnlocked(directory, input, id));
|
|
4733
5075
|
}
|
|
4734
5076
|
async function startUnlocked(directory, input, id) {
|
|
@@ -4835,7 +5177,7 @@ function createGoalService(host) {
|
|
|
4835
5177
|
});
|
|
4836
5178
|
await appendEvent(directory, {
|
|
4837
5179
|
version: 1,
|
|
4838
|
-
eventID:
|
|
5180
|
+
eventID: randomUUID7(),
|
|
4839
5181
|
goalID: id,
|
|
4840
5182
|
type: "goal.blocked",
|
|
4841
5183
|
reason: detail,
|
|
@@ -4862,7 +5204,7 @@ function createGoalService(host) {
|
|
|
4862
5204
|
const rt = state.runtimes.find((item) => item.goalID === id);
|
|
4863
5205
|
if (rt) {
|
|
4864
5206
|
Object.assign(rt, acquireLease(rt, g.config.timeoutMs || 300000));
|
|
4865
|
-
rt.activeRunID =
|
|
5207
|
+
rt.activeRunID = randomUUID7();
|
|
4866
5208
|
rt.activePromptMessageID = newPromptMessageID();
|
|
4867
5209
|
rt.runCount = 1;
|
|
4868
5210
|
rt.budgetTurnCount = 1;
|
|
@@ -4873,7 +5215,7 @@ function createGoalService(host) {
|
|
|
4873
5215
|
runtime = state2.runtimes.find((r) => r.goalID === id);
|
|
4874
5216
|
await appendEvent(directory, {
|
|
4875
5217
|
version: 1,
|
|
4876
|
-
eventID:
|
|
5218
|
+
eventID: randomUUID7(),
|
|
4877
5219
|
goalID: id,
|
|
4878
5220
|
type: "goal.created",
|
|
4879
5221
|
name: input.name,
|
|
@@ -4885,7 +5227,7 @@ function createGoalService(host) {
|
|
|
4885
5227
|
if (runtime) {
|
|
4886
5228
|
await appendEvent(directory, {
|
|
4887
5229
|
version: 1,
|
|
4888
|
-
eventID:
|
|
5230
|
+
eventID: randomUUID7(),
|
|
4889
5231
|
goalID: id,
|
|
4890
5232
|
type: "run.started",
|
|
4891
5233
|
runID: runtime.activeRunID,
|
|
@@ -4894,9 +5236,8 @@ function createGoalService(host) {
|
|
|
4894
5236
|
revision: state2.revision
|
|
4895
5237
|
});
|
|
4896
5238
|
try {
|
|
4897
|
-
await workers.continueWorker(worker, goal, runtime);
|
|
5239
|
+
await dispatchOrRecordFailure(directory, id, () => workers.continueWorker(worker, goal, runtime), runtime, worker.workerSessionID, true);
|
|
4898
5240
|
} catch (error) {
|
|
4899
|
-
await recordPromptFailure(directory, id, error, true);
|
|
4900
5241
|
throw new GoalStartError(describeError(error), {
|
|
4901
5242
|
goalID: id,
|
|
4902
5243
|
workerSessionID: worker.workerSessionID,
|
|
@@ -4963,20 +5304,48 @@ function createGoalService(host) {
|
|
|
4963
5304
|
}
|
|
4964
5305
|
return accountTailUsage(directory, goalID, runtime, tail);
|
|
4965
5306
|
}
|
|
5307
|
+
function rejected(goalID, outcome, message) {
|
|
5308
|
+
if (outcome === "not_found")
|
|
5309
|
+
sessions.delete(goalID);
|
|
5310
|
+
return { ok: false, outcome, goalID, message };
|
|
5311
|
+
}
|
|
5312
|
+
function continuationGuard(state, goalID) {
|
|
5313
|
+
const goal = state.goals.find((g) => g.id === goalID);
|
|
5314
|
+
if (!goal)
|
|
5315
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5316
|
+
if (goal.status !== "active")
|
|
5317
|
+
return rejected(goalID, "invalid_state", `Goal is ${goal.status}; resume or retry it before requesting a turn.`);
|
|
5318
|
+
if (goal.config.write_scope !== undefined && !host.scopedExecution)
|
|
5319
|
+
return rejected(goalID, "blocked", "Enforced scopes are unsupported on this host.");
|
|
5320
|
+
if (goal.scopeClosing)
|
|
5321
|
+
return rejected(goalID, "blocked", "Goal is closing; wait for in-flight writes to finish before resuming.");
|
|
5322
|
+
if ((state.workspaceCalls ?? []).some((call) => call.goalID === goalID && !isSyncStaleWorkspaceCall(state, call, Date.now()))) {
|
|
5323
|
+
return rejected(goalID, "blocked", "Goal has in-flight write reservations. Nothing was sent; retry after writes finish.");
|
|
5324
|
+
}
|
|
5325
|
+
if (state.workspaceOperation)
|
|
5326
|
+
return rejected(goalID, "blocked", `Workspace operation ${state.workspaceOperation.id} is in progress. Nothing was sent; retry after it finishes.`);
|
|
5327
|
+
const runtime = state.runtimes.find((r) => r.goalID === goalID);
|
|
5328
|
+
if (!runtime)
|
|
5329
|
+
return rejected(goalID, "blocked", "Goal runtime is missing; no turn was sent.");
|
|
5330
|
+
if (runtime.activeToolCallIDs?.length)
|
|
5331
|
+
return rejected(goalID, "blocked", "Worker has outstanding tool calls. Nothing was sent; wait for terminal tool evidence.");
|
|
5332
|
+
}
|
|
4966
5333
|
async function continueTurnUnlocked(directory, goalID, opts) {
|
|
5334
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
4967
5335
|
const preState = await readState(directory);
|
|
4968
5336
|
const goal = preState.goals.find((g) => g.id === goalID);
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
return;
|
|
5337
|
+
const guard = continuationGuard(preState, goalID);
|
|
5338
|
+
if (guard)
|
|
5339
|
+
return guard;
|
|
5340
|
+
if (!goal)
|
|
5341
|
+
return rejected(goalID, "not_found", "Goal not found.");
|
|
4975
5342
|
const runtime = preState.runtimes.find((r) => r.goalID === goalID);
|
|
4976
5343
|
if (!runtime)
|
|
4977
|
-
return;
|
|
5344
|
+
return rejected(goalID, "blocked", "Goal runtime is missing; no turn was sent.");
|
|
5345
|
+
if (runtime.activePromptAcceptedAt)
|
|
5346
|
+
await acknowledgeGoalInbox(directory, goalID, runtime.activeInboxLines ?? []);
|
|
4978
5347
|
if (runtime.phase === "running" && leaseIsValid(runtime))
|
|
4979
|
-
return;
|
|
5348
|
+
return rejected(goalID, "blocked", "An existing turn holds the run lease; no additional prompt was sent.");
|
|
4980
5349
|
let session = sessions.get(goalID);
|
|
4981
5350
|
if (session && session.workerSessionID !== goal.workerSessionID) {
|
|
4982
5351
|
sessions.delete(goalID);
|
|
@@ -4993,18 +5362,22 @@ function createGoalService(host) {
|
|
|
4993
5362
|
if (!session && !goal.workerSessionID)
|
|
4994
5363
|
session = await ensureWorkerSession(directory, goal);
|
|
4995
5364
|
if (!session)
|
|
4996
|
-
return;
|
|
5365
|
+
return rejected(goalID, "blocked", "Worker session is missing; resume or retry the goal.");
|
|
4997
5366
|
if (!opts?.force && !await workers.isIdle(session.workerSessionID))
|
|
4998
|
-
return;
|
|
5367
|
+
return rejected(goalID, "blocked", "Worker is busy or its status is unknown; no prompt was sent. Retry after confirmed idle.");
|
|
4999
5368
|
if (goal.pendingIdentity || goal.modelSwitch?.pending || goal.agentSwitch?.pending) {
|
|
5000
5369
|
if (await host.sessionStatus(session.workerSessionID) !== "idle")
|
|
5001
|
-
return;
|
|
5370
|
+
return rejected(goalID, "blocked", "Pending identity requires an idle worker; no prompt was sent. Wait for idle and retry.");
|
|
5002
5371
|
await applyPendingIdentity(directory, goal);
|
|
5003
5372
|
await applyPendingModel(directory, goal);
|
|
5004
5373
|
await applyPendingAgent(directory, goal);
|
|
5005
5374
|
}
|
|
5006
5375
|
let acquired = false;
|
|
5376
|
+
let acquisitionFailure;
|
|
5007
5377
|
const state = await mutateState(directory, `turn.acquire:${goalID}`, async (s) => {
|
|
5378
|
+
acquisitionFailure = continuationGuard(s, goalID);
|
|
5379
|
+
if (acquisitionFailure)
|
|
5380
|
+
return s;
|
|
5008
5381
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5009
5382
|
if (!g || g.status !== "active")
|
|
5010
5383
|
return s;
|
|
@@ -5015,7 +5388,7 @@ function createGoalService(host) {
|
|
|
5015
5388
|
return s;
|
|
5016
5389
|
const timeoutMs = g.config.timeoutMs || 300000;
|
|
5017
5390
|
Object.assign(rt, acquireLease(rt, timeoutMs));
|
|
5018
|
-
rt.activeRunID =
|
|
5391
|
+
rt.activeRunID = randomUUID7();
|
|
5019
5392
|
rt.activePromptMessageID = newPromptMessageID();
|
|
5020
5393
|
rt.runCount += 1;
|
|
5021
5394
|
if (rt.freeRetryPending) {
|
|
@@ -5030,78 +5403,95 @@ function createGoalService(host) {
|
|
|
5030
5403
|
const freshGoal = state.goals.find((g) => g.id === goalID);
|
|
5031
5404
|
const freshRuntime = state.runtimes.find((r) => r.goalID === goalID);
|
|
5032
5405
|
if (!acquired || !freshGoal || freshGoal.status !== "active" || !freshRuntime?.activeRunID)
|
|
5033
|
-
return;
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5406
|
+
return acquisitionFailure ?? rejected(goalID, "blocked", "Run lease acquisition lost to another turn; no prompt was sent.");
|
|
5407
|
+
const sent = {
|
|
5408
|
+
ok: true,
|
|
5409
|
+
outcome: "sent",
|
|
5037
5410
|
goalID,
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
if (opts?.bare) {
|
|
5045
|
-
const bareWords = await drainGoalInbox(directory, goalID);
|
|
5046
|
-
const bareText = bareWords.join(`
|
|
5047
|
-
`).trim();
|
|
5048
|
-
if (bareText) {
|
|
5049
|
-
try {
|
|
5050
|
-
await workers.sendBare(session, freshGoal, freshRuntime, bareText);
|
|
5051
|
-
} catch (error) {
|
|
5052
|
-
await recordPromptFailure(directory, goalID, error);
|
|
5053
|
-
throw error;
|
|
5054
|
-
}
|
|
5055
|
-
return;
|
|
5056
|
-
}
|
|
5057
|
-
}
|
|
5058
|
-
const inboxMessages = await drainGoalInbox(directory, goalID);
|
|
5059
|
-
const allEvents = await readEvents(directory, 200);
|
|
5060
|
-
const progressHistory = allEvents.filter((e) => e.goalID === goalID && e.type === "goal.progress").map((e) => ({
|
|
5061
|
-
summary: String(e.summary || ""),
|
|
5062
|
-
next: e.next ? String(e.next) : undefined,
|
|
5063
|
-
at: String(e.timestamp || "")
|
|
5064
|
-
}));
|
|
5065
|
-
let transcriptTail;
|
|
5066
|
-
try {
|
|
5067
|
-
transcriptTail = await host.readMessages(freshGoal.workerSessionID, 5);
|
|
5068
|
-
} catch {
|
|
5069
|
-
transcriptTail = [];
|
|
5070
|
-
}
|
|
5071
|
-
await accountTailUsage(directory, goalID, freshRuntime, transcriptTail ?? []);
|
|
5072
|
-
let verification;
|
|
5411
|
+
workerSessionID: session.workerSessionID,
|
|
5412
|
+
runGeneration: freshRuntime.runGeneration,
|
|
5413
|
+
promptMessageID: freshRuntime.activePromptMessageID,
|
|
5414
|
+
message: `Prompt submitted for "${freshGoal.name}"; worker activity is not yet confirmed.`
|
|
5415
|
+
};
|
|
5416
|
+
let dispatchAttempted = false;
|
|
5073
5417
|
try {
|
|
5074
|
-
const
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5418
|
+
const inbox = await snapshotGoalInbox(directory, goalID);
|
|
5419
|
+
const inboxMessages = inbox.messages;
|
|
5420
|
+
await mutateState(directory, `turn.inbox-snapshot:${goalID}`, async (s) => {
|
|
5421
|
+
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
5422
|
+
if (rt && rt.activeRunID === freshRuntime.activeRunID)
|
|
5423
|
+
rt.activeInboxLines = inbox.lines;
|
|
5424
|
+
return s;
|
|
5425
|
+
});
|
|
5426
|
+
await appendEvent(directory, {
|
|
5427
|
+
version: 1,
|
|
5428
|
+
eventID: randomUUID7(),
|
|
5429
|
+
goalID,
|
|
5430
|
+
type: "run.started",
|
|
5431
|
+
runID: freshRuntime.activeRunID,
|
|
5432
|
+
turnCount: freshRuntime.runCount,
|
|
5433
|
+
timestamp: new Date().toISOString(),
|
|
5434
|
+
revision: state.revision
|
|
5435
|
+
});
|
|
5436
|
+
if (opts?.bare) {
|
|
5437
|
+
const bareText = inboxMessages.join(`
|
|
5438
|
+
`).trim();
|
|
5439
|
+
if (bareText) {
|
|
5440
|
+
dispatchAttempted = true;
|
|
5441
|
+
await dispatchOrRecordFailure(directory, goalID, () => workers.sendBare(session, freshGoal, freshRuntime, bareText), freshRuntime, session.workerSessionID);
|
|
5442
|
+
return sent;
|
|
5081
5443
|
}
|
|
5082
5444
|
}
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5445
|
+
const allEvents = await readEvents(directory, 200);
|
|
5446
|
+
const progressHistory = allEvents.filter((e) => e.goalID === goalID && e.type === "goal.progress").map((e) => ({
|
|
5447
|
+
summary: String(e.summary || ""),
|
|
5448
|
+
next: e.next ? String(e.next) : undefined,
|
|
5449
|
+
at: String(e.timestamp || "")
|
|
5450
|
+
}));
|
|
5451
|
+
let transcriptTail;
|
|
5452
|
+
try {
|
|
5453
|
+
transcriptTail = await host.readMessages(freshGoal.workerSessionID, 5);
|
|
5454
|
+
} catch {
|
|
5455
|
+
transcriptTail = [];
|
|
5092
5456
|
}
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5457
|
+
await accountTailUsage(directory, goalID, freshRuntime, transcriptTail ?? []);
|
|
5458
|
+
let verification;
|
|
5459
|
+
try {
|
|
5460
|
+
const artifactDir = freshGoal.config.artifactDir;
|
|
5461
|
+
if (artifactDir) {
|
|
5462
|
+
try {
|
|
5463
|
+
const files = await fs4.readdir(artifactDir);
|
|
5464
|
+
verification = { artifactSummary: files.length ? `${files.length} file(s): ${files.slice(0, 8).join(", ")}` : "no artifacts yet" };
|
|
5465
|
+
} catch {
|
|
5466
|
+
verification = { artifactSummary: "no artifacts yet" };
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
if (freshGoal.config.checks?.length) {
|
|
5470
|
+
const c = `checks configured: ${freshGoal.config.checks.length} \u2014 run them before claiming completion`;
|
|
5471
|
+
verification = { ...verification || {}, failedChecks: [c], checksPassed: undefined };
|
|
5472
|
+
}
|
|
5473
|
+
if (freshRuntime.evaluatorRejectionCount && freshRuntime.evaluatorRejectionCount > 0) {
|
|
5474
|
+
verification = { ...verification || {}, evaluatorRejectionCount: freshRuntime.evaluatorRejectionCount };
|
|
5475
|
+
}
|
|
5476
|
+
if (freshRuntime.lastRejectionDetails) {
|
|
5477
|
+
verification = { ...verification || {}, lastRejectionDetails: freshRuntime.lastRejectionDetails };
|
|
5478
|
+
}
|
|
5479
|
+
} catch {}
|
|
5480
|
+
const context = {
|
|
5481
|
+
inboxMessages: inboxMessages.length > 0 ? inboxMessages : undefined,
|
|
5482
|
+
progressHistory: progressHistory.length > 0 ? progressHistory : undefined,
|
|
5483
|
+
transcriptTail: transcriptTail && transcriptTail.length > 0 ? transcriptTail : undefined,
|
|
5484
|
+
forceFinish: opts?.forceFinish || undefined,
|
|
5485
|
+
verification
|
|
5486
|
+
};
|
|
5487
|
+
dispatchAttempted = true;
|
|
5488
|
+
await dispatchOrRecordFailure(directory, goalID, () => workers.continueWorker(session, freshGoal, freshRuntime, context), { ...freshRuntime, forceFinish: opts?.forceFinish }, session.workerSessionID);
|
|
5489
|
+
return sent;
|
|
5103
5490
|
} catch (error) {
|
|
5104
|
-
|
|
5491
|
+
if (error instanceof GoalDeliveryBlockedError)
|
|
5492
|
+
return error.result;
|
|
5493
|
+
if (!dispatchAttempted)
|
|
5494
|
+
await recordPromptFailure(directory, goalID, error, false, freshRuntime.activeRunID);
|
|
5105
5495
|
throw error;
|
|
5106
5496
|
}
|
|
5107
5497
|
}
|
|
@@ -5122,7 +5512,7 @@ function createGoalService(host) {
|
|
|
5122
5512
|
return { ok: false, message: "Goal not found." };
|
|
5123
5513
|
await appendEvent(directory, {
|
|
5124
5514
|
version: 1,
|
|
5125
|
-
eventID:
|
|
5515
|
+
eventID: randomUUID7(),
|
|
5126
5516
|
goalID,
|
|
5127
5517
|
type: "goal.interactive_changed",
|
|
5128
5518
|
interactive,
|
|
@@ -5183,7 +5573,7 @@ function createGoalService(host) {
|
|
|
5183
5573
|
await cancelAwaitsForGoal(directory, goalID, "goal paused").catch(() => {});
|
|
5184
5574
|
await appendEvent(directory, {
|
|
5185
5575
|
version: 1,
|
|
5186
|
-
eventID:
|
|
5576
|
+
eventID: randomUUID7(),
|
|
5187
5577
|
goalID,
|
|
5188
5578
|
type: "goal.status_changed",
|
|
5189
5579
|
from: "active",
|
|
@@ -5192,7 +5582,37 @@ function createGoalService(host) {
|
|
|
5192
5582
|
revision: state.revision
|
|
5193
5583
|
});
|
|
5194
5584
|
}
|
|
5585
|
+
async function withStageTimeout(promise, ms, stage) {
|
|
5586
|
+
let timer;
|
|
5587
|
+
try {
|
|
5588
|
+
return await Promise.race([
|
|
5589
|
+
promise,
|
|
5590
|
+
new Promise((_, reject) => {
|
|
5591
|
+
timer = setTimeout(() => reject(new Error(`${stage} timed out after ${ms}ms`)), ms);
|
|
5592
|
+
})
|
|
5593
|
+
]);
|
|
5594
|
+
} finally {
|
|
5595
|
+
if (timer)
|
|
5596
|
+
clearTimeout(timer);
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
5599
|
+
const PROBE_TIMEOUT_MS = 15000;
|
|
5600
|
+
const PROMPT_VERIFY_TIMEOUT_MS = 12000;
|
|
5601
|
+
async function probeWorkerLiveness(workerSessionID) {
|
|
5602
|
+
if (!workerSessionID)
|
|
5603
|
+
return "dead";
|
|
5604
|
+
try {
|
|
5605
|
+
const status = await withStageTimeout(host.sessionStatus(workerSessionID), PROBE_TIMEOUT_MS, `worker liveness probe for session "${workerSessionID}"`);
|
|
5606
|
+
return status === "unknown" ? "unknown" : "live";
|
|
5607
|
+
} catch {
|
|
5608
|
+
return "unknown";
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5195
5611
|
async function resumeUnlocked(directory, goalID) {
|
|
5612
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5613
|
+
const preState = await readState(directory);
|
|
5614
|
+
const preGoal = preState.goals.find((g) => g.id === goalID);
|
|
5615
|
+
const workerLiveness = await probeWorkerLiveness(preGoal?.workerSessionID);
|
|
5196
5616
|
let resumed = false;
|
|
5197
5617
|
const state = await mutateState(directory, `goal.resume:${goalID}`, async (state) => {
|
|
5198
5618
|
const goal = state.goals.find((g) => g.id === goalID);
|
|
@@ -5200,23 +5620,44 @@ function createGoalService(host) {
|
|
|
5200
5620
|
return state;
|
|
5201
5621
|
if (!canTransition(goal.status, "active", "user"))
|
|
5202
5622
|
return state;
|
|
5203
|
-
|
|
5623
|
+
const liveness = { isWorkerLive: (_gid, _wsid) => _gid === goalID ? workerLiveness !== "dead" : false, now: Date.now() };
|
|
5624
|
+
sweepStaleWorkspaceState(state, liveness);
|
|
5625
|
+
const rt = state.runtimes.find((r) => r.goalID === goalID);
|
|
5626
|
+
const liveCalls = (state.workspaceCalls ?? []).filter((call) => call.goalID === goalID && !isStaleWorkspaceCall(state, call, liveness));
|
|
5627
|
+
const blocking = liveCalls;
|
|
5628
|
+
if (blocking.length) {
|
|
5629
|
+
if (workerLiveness === "unknown") {
|
|
5630
|
+
throw new Error(`Worker liveness unknown for session "${goal.workerSessionID}" (probe timed out or unreachable); keeping ${blocking.length} write reservation(s) \u2014 nothing reaped, nothing rotated. ` + `The worker may still be running; wait for actual tool completion/quiescence. Neither age nor a missing session releases execution fences.`);
|
|
5631
|
+
}
|
|
5632
|
+
throw new Error("Goal still has closing/in-flight writes; ownership cannot be reacquired before quiescence.");
|
|
5633
|
+
}
|
|
5204
5634
|
if (goal.config.write_scope !== undefined) {
|
|
5205
5635
|
retireWorker(state, goal);
|
|
5206
5636
|
goal.workerSessionID = undefined;
|
|
5207
5637
|
sessions.delete(goalID);
|
|
5638
|
+
if (rt) {
|
|
5639
|
+
rt.runGeneration += 1;
|
|
5640
|
+
rt.updatedAt = new Date().toISOString();
|
|
5641
|
+
}
|
|
5208
5642
|
}
|
|
5643
|
+
goal.scopeClosing = false;
|
|
5644
|
+
goal.scopeClearPending = false;
|
|
5645
|
+
assertWorkspaceWriteAvailable(state, goal, goal.ownerSessionID);
|
|
5209
5646
|
goal.status = "active";
|
|
5210
5647
|
goal.updatedAt = new Date().toISOString();
|
|
5211
5648
|
resumed = true;
|
|
5212
5649
|
return state;
|
|
5213
5650
|
});
|
|
5214
5651
|
const goal = state.goals.find((g) => g.id === goalID);
|
|
5215
|
-
if (!goal
|
|
5216
|
-
return;
|
|
5652
|
+
if (!goal)
|
|
5653
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5654
|
+
if (!resumed)
|
|
5655
|
+
return rejected(goalID, "invalid_state", `Cannot resume a ${goal.status} goal; no turn was sent.`);
|
|
5217
5656
|
try {
|
|
5218
5657
|
await ensureWorkerSession(directory, goal);
|
|
5219
5658
|
} catch (error) {
|
|
5659
|
+
if (error instanceof GoalDeliveryBlockedError)
|
|
5660
|
+
return error.result;
|
|
5220
5661
|
await mutateState(directory, `goal.resume-rollback:${goalID}`, async (s) => {
|
|
5221
5662
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5222
5663
|
if (g?.status === "active")
|
|
@@ -5227,7 +5668,7 @@ function createGoalService(host) {
|
|
|
5227
5668
|
}
|
|
5228
5669
|
await appendEvent(directory, {
|
|
5229
5670
|
version: 1,
|
|
5230
|
-
eventID:
|
|
5671
|
+
eventID: randomUUID7(),
|
|
5231
5672
|
goalID,
|
|
5232
5673
|
type: "goal.status_changed",
|
|
5233
5674
|
from: "paused",
|
|
@@ -5235,20 +5676,41 @@ function createGoalService(host) {
|
|
|
5235
5676
|
timestamp: new Date().toISOString(),
|
|
5236
5677
|
revision: state.revision
|
|
5237
5678
|
});
|
|
5238
|
-
await continueTurnUnlocked(directory, goalID);
|
|
5679
|
+
const result = await continueTurnUnlocked(directory, goalID);
|
|
5680
|
+
return result.ok ? { ...result, message: `Goal "${goal.name}" resumed; ${result.message}` } : result;
|
|
5239
5681
|
}
|
|
5240
5682
|
async function retryUnlocked(directory, goalID) {
|
|
5683
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5684
|
+
const preState = await readState(directory);
|
|
5685
|
+
const workerLiveness = await probeWorkerLiveness(preState.goals.find((g) => g.id === goalID)?.workerSessionID);
|
|
5241
5686
|
let retried = false;
|
|
5242
5687
|
const state = await mutateState(directory, `goal.retry:${goalID}`, async (state) => {
|
|
5243
5688
|
const goal = state.goals.find((g) => g.id === goalID);
|
|
5244
5689
|
if (!goal || goal.status !== "blocked")
|
|
5245
5690
|
return state;
|
|
5246
|
-
|
|
5691
|
+
const liveness = { isWorkerLive: (_gid, _wsid) => _gid === goalID ? workerLiveness !== "dead" : false, now: Date.now() };
|
|
5692
|
+
sweepStaleWorkspaceState(state, liveness);
|
|
5693
|
+
const blocking = (state.workspaceCalls ?? []).filter((call) => call.goalID === goalID && !isStaleWorkspaceCall(state, call, liveness));
|
|
5694
|
+
if (blocking.length) {
|
|
5695
|
+
if (workerLiveness === "unknown") {
|
|
5696
|
+
throw new Error(`Worker liveness unknown for session "${goal.workerSessionID}" (probe timed out or unreachable); keeping ${blocking.length} write reservation(s) \u2014 nothing reaped, nothing rotated. ` + `The worker may still be running; wait for actual tool completion/quiescence. Neither age nor a missing session releases execution fences.`);
|
|
5697
|
+
}
|
|
5698
|
+
throw new Error("Goal still has closing/in-flight writes; ownership cannot be reacquired before quiescence.");
|
|
5699
|
+
}
|
|
5247
5700
|
if (goal.config.write_scope !== undefined) {
|
|
5248
5701
|
retireWorker(state, goal);
|
|
5249
5702
|
goal.workerSessionID = undefined;
|
|
5250
5703
|
sessions.delete(goalID);
|
|
5704
|
+
const rt = state.runtimes.find((r) => r.goalID === goalID);
|
|
5705
|
+
if (rt) {
|
|
5706
|
+
rt.runGeneration += 1;
|
|
5707
|
+
rt.updatedAt = new Date().toISOString();
|
|
5708
|
+
}
|
|
5251
5709
|
}
|
|
5710
|
+
goal.scopeClosing = false;
|
|
5711
|
+
goal.scopeClearPending = false;
|
|
5712
|
+
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => call.goalID !== goalID);
|
|
5713
|
+
assertWorkspaceWriteAvailable(state, goal, goal.ownerSessionID);
|
|
5252
5714
|
goal.status = "active";
|
|
5253
5715
|
goal.updatedAt = new Date().toISOString();
|
|
5254
5716
|
retried = true;
|
|
@@ -5269,11 +5731,15 @@ function createGoalService(host) {
|
|
|
5269
5731
|
return state;
|
|
5270
5732
|
});
|
|
5271
5733
|
const goal = state.goals.find((g) => g.id === goalID);
|
|
5272
|
-
if (!goal
|
|
5273
|
-
return;
|
|
5734
|
+
if (!goal)
|
|
5735
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5736
|
+
if (!retried)
|
|
5737
|
+
return rejected(goalID, "invalid_state", `Cannot retry a ${goal.status} goal; no turn was sent.`);
|
|
5274
5738
|
try {
|
|
5275
5739
|
await ensureWorkerSession(directory, goal);
|
|
5276
5740
|
} catch (error) {
|
|
5741
|
+
if (error instanceof GoalDeliveryBlockedError)
|
|
5742
|
+
return error.result;
|
|
5277
5743
|
await mutateState(directory, `goal.retry-rollback:${goalID}`, async (s) => {
|
|
5278
5744
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5279
5745
|
if (g?.status === "active")
|
|
@@ -5284,7 +5750,7 @@ function createGoalService(host) {
|
|
|
5284
5750
|
}
|
|
5285
5751
|
await appendEvent(directory, {
|
|
5286
5752
|
version: 1,
|
|
5287
|
-
eventID:
|
|
5753
|
+
eventID: randomUUID7(),
|
|
5288
5754
|
goalID,
|
|
5289
5755
|
type: "goal.status_changed",
|
|
5290
5756
|
from: "blocked",
|
|
@@ -5292,53 +5758,37 @@ function createGoalService(host) {
|
|
|
5292
5758
|
timestamp: new Date().toISOString(),
|
|
5293
5759
|
revision: state.revision
|
|
5294
5760
|
});
|
|
5295
|
-
|
|
5761
|
+
return continueTurnUnlocked(directory, goalID);
|
|
5296
5762
|
}
|
|
5297
5763
|
async function clearUnlocked(directory, goalID) {
|
|
5298
|
-
const
|
|
5299
|
-
const
|
|
5300
|
-
|
|
5301
|
-
return;
|
|
5302
|
-
await mutateState(directory, `goal.clear-closing:${goalID}`, async (s) => {
|
|
5764
|
+
const pre = await readState(directory);
|
|
5765
|
+
const workerID = sessions.get(goalID)?.workerSessionID || pre.goals.find((g) => g.id === goalID)?.workerSessionID;
|
|
5766
|
+
const state = await mutateState(directory, `goal.clear:${goalID}`, async (s) => {
|
|
5303
5767
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5304
|
-
if (g)
|
|
5305
|
-
g.scopeClosing = true;
|
|
5768
|
+
if (g)
|
|
5306
5769
|
retireWorker(s, g);
|
|
5307
|
-
}
|
|
5308
|
-
return s;
|
|
5309
|
-
});
|
|
5310
|
-
const session = sessions.get(goalID) || (goal.workerSessionID ? {
|
|
5311
|
-
goalID: goal.id,
|
|
5312
|
-
workerSessionID: goal.workerSessionID,
|
|
5313
|
-
startedAt: goal.createdAt
|
|
5314
|
-
} : undefined);
|
|
5315
|
-
if (session) {
|
|
5316
|
-
await workers.abortWorker(session.workerSessionID);
|
|
5317
|
-
sessions.delete(goalID);
|
|
5318
|
-
}
|
|
5319
|
-
await mutateState(directory, `goal.clear:${goalID}`, async (s) => {
|
|
5320
|
-
if (s.workspaceCalls?.some((call) => call.goalID === goalID) || s.workspaceOperation?.goalID === goalID) {
|
|
5321
|
-
const g = s.goals.find((item) => item.id === goalID);
|
|
5322
|
-
if (g) {
|
|
5323
|
-
g.status = "paused";
|
|
5324
|
-
g.scopeClearPending = true;
|
|
5325
|
-
}
|
|
5326
|
-
s.commandAwaits = (s.commandAwaits ?? []).filter((a) => a.goalID !== goalID);
|
|
5327
|
-
return s;
|
|
5328
|
-
}
|
|
5329
5770
|
s.goals = s.goals.filter((g) => g.id !== goalID);
|
|
5330
5771
|
s.runtimes = s.runtimes.filter((r) => r.goalID !== goalID);
|
|
5331
5772
|
s.commandAwaits = (s.commandAwaits ?? []).filter((a) => a.goalID !== goalID);
|
|
5773
|
+
await deleteGoalInbox(directory, goalID);
|
|
5332
5774
|
return s;
|
|
5333
5775
|
});
|
|
5776
|
+
sessions.delete(goalID);
|
|
5777
|
+
if (workerID) {
|
|
5778
|
+
try {
|
|
5779
|
+
await workers.abortWorker(workerID);
|
|
5780
|
+
} catch {}
|
|
5781
|
+
}
|
|
5782
|
+
await cancelAwaitsForGoal(directory, goalID, "goal cleared").catch(() => {});
|
|
5334
5783
|
await appendEvent(directory, {
|
|
5335
5784
|
version: 1,
|
|
5336
|
-
eventID:
|
|
5785
|
+
eventID: randomUUID7(),
|
|
5337
5786
|
goalID,
|
|
5338
5787
|
type: "goal.cleared",
|
|
5339
5788
|
timestamp: new Date().toISOString(),
|
|
5340
5789
|
revision: state.revision
|
|
5341
5790
|
});
|
|
5791
|
+
await logServerEvent(directory, "goal.cleared-terminal", { goalID }).catch(() => {});
|
|
5342
5792
|
}
|
|
5343
5793
|
function getWorker(goalID) {
|
|
5344
5794
|
return sessions.get(goalID);
|
|
@@ -5347,6 +5797,25 @@ function createGoalService(host) {
|
|
|
5347
5797
|
return new Map(sessions);
|
|
5348
5798
|
}
|
|
5349
5799
|
async function reconcile(directory) {
|
|
5800
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5801
|
+
try {
|
|
5802
|
+
let summary = { reapedCalls: [], resetClosing: [], finalizedClears: [], forceCleared: [] };
|
|
5803
|
+
const pre = await readState(directory);
|
|
5804
|
+
if (pre.workspaceCalls?.length || pre.goals.some((g) => g.scopeClosing || g.scopeClearPending)) {
|
|
5805
|
+
await mutateState(directory, "reconcile.sweep-stale-workspace", async (s) => {
|
|
5806
|
+
summary = sweepStaleWorkspaceState(s);
|
|
5807
|
+
return s;
|
|
5808
|
+
});
|
|
5809
|
+
}
|
|
5810
|
+
if (summary.reapedCalls.length || summary.resetClosing.length || summary.finalizedClears.length || summary.forceCleared.length) {
|
|
5811
|
+
await logServerEvent(directory, "reconcile.sweep-stale-workspace", {
|
|
5812
|
+
reapedCalls: summary.reapedCalls,
|
|
5813
|
+
resetClosing: summary.resetClosing,
|
|
5814
|
+
finalizedClears: summary.finalizedClears,
|
|
5815
|
+
forceCleared: summary.forceCleared
|
|
5816
|
+
}).catch(() => {});
|
|
5817
|
+
}
|
|
5818
|
+
} catch {}
|
|
5350
5819
|
const state = await readState(directory);
|
|
5351
5820
|
for (const goal of state.goals) {
|
|
5352
5821
|
if (isTerminal(goal.status))
|
|
@@ -5437,19 +5906,39 @@ function createGoalService(host) {
|
|
|
5437
5906
|
}
|
|
5438
5907
|
}
|
|
5439
5908
|
async function nudgeUnlocked(directory, goalID) {
|
|
5909
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5440
5910
|
const preState = await readState(directory);
|
|
5441
5911
|
const goal = preState.goals.find((g) => g.id === goalID);
|
|
5442
5912
|
if (!goal)
|
|
5443
|
-
return
|
|
5913
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5444
5914
|
if (goal.status !== "active") {
|
|
5445
|
-
return
|
|
5915
|
+
return rejected(goalID, "invalid_state", `Goal is ${goal.status}; resume or retry it before nudging.`);
|
|
5916
|
+
}
|
|
5917
|
+
const quotaHold = quotaHoldMessage(goal);
|
|
5918
|
+
if (quotaHold)
|
|
5919
|
+
return rejected(goalID, "blocked", quotaHold);
|
|
5920
|
+
const guard = continuationGuard(preState, goalID);
|
|
5921
|
+
if (guard)
|
|
5922
|
+
return guard;
|
|
5923
|
+
if (goal.workerSessionID) {
|
|
5924
|
+
const status = await host.sessionStatus(goal.workerSessionID);
|
|
5925
|
+
if (status === "busy")
|
|
5926
|
+
return rejected(goalID, "blocked", "Worker is busy; no prompt was sent and its active turn was left intact. Retry after idle.");
|
|
5927
|
+
if (status !== "idle" && (goal.pendingIdentity || goal.modelSwitch?.pending || goal.agentSwitch?.pending)) {
|
|
5928
|
+
return rejected(goalID, "blocked", "Pending identity requires confirmed idle; worker status is unknown. No prompt was sent.");
|
|
5929
|
+
}
|
|
5930
|
+
if (status !== "idle")
|
|
5931
|
+
return rejected(goalID, "blocked", "Worker status is unknown or retrying; an outstanding prompt may already be accepted. Nothing was sent or reset. Retry after confirmed idle.");
|
|
5446
5932
|
}
|
|
5447
5933
|
if (goal.config.write_scope !== undefined) {
|
|
5448
5934
|
await pauseUnlocked(directory, goalID);
|
|
5449
|
-
|
|
5450
|
-
return { ok: true, message: `Re-prompted protected worker for "${goal.name}" in a freshly fenced session.` };
|
|
5935
|
+
return resumeUnlocked(directory, goalID);
|
|
5451
5936
|
}
|
|
5937
|
+
let resetFailure;
|
|
5452
5938
|
const cleared = await mutateState(directory, `goal.nudge:${goalID}`, async (s) => {
|
|
5939
|
+
resetFailure = continuationGuard(s, goalID);
|
|
5940
|
+
if (resetFailure)
|
|
5941
|
+
return s;
|
|
5453
5942
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
5454
5943
|
if (!rt)
|
|
5455
5944
|
return s;
|
|
@@ -5461,30 +5950,46 @@ function createGoalService(host) {
|
|
|
5461
5950
|
rt.updatedAt = new Date().toISOString();
|
|
5462
5951
|
return s;
|
|
5463
5952
|
});
|
|
5953
|
+
if (resetFailure)
|
|
5954
|
+
return resetFailure;
|
|
5464
5955
|
const freshGoal = cleared.goals.find((g) => g.id === goalID);
|
|
5465
5956
|
if (!freshGoal || !freshGoal.workerSessionID) {
|
|
5466
|
-
return
|
|
5957
|
+
return rejected(goalID, "blocked", "Goal has no worker session. Use resume_goal or retry_goal.");
|
|
5467
5958
|
}
|
|
5468
|
-
|
|
5469
|
-
return { ok: true, message: `Re-prompted worker for "${freshGoal.name}".` };
|
|
5959
|
+
return continueTurnUnlocked(directory, goalID, { force: true });
|
|
5470
5960
|
}
|
|
5471
5961
|
async function sendUnlocked(directory, goalID, text) {
|
|
5472
5962
|
const trimmed = text.trim();
|
|
5473
5963
|
if (!trimmed)
|
|
5474
|
-
return
|
|
5964
|
+
return rejected(goalID, "invalid_state", "Nothing to send.");
|
|
5965
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5475
5966
|
const preState = await readState(directory);
|
|
5476
5967
|
const goal = preState.goals.find((g) => g.id === goalID);
|
|
5477
5968
|
if (!goal)
|
|
5478
|
-
return
|
|
5479
|
-
await appendGoalInbox(directory, goalID, "user", trimmed)
|
|
5969
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5970
|
+
if (!await appendGoalInbox(directory, goalID, "user", trimmed))
|
|
5971
|
+
return rejected(goalID, "not_found", "Goal cleared before words could be queued. List current goals instead of retrying this ID.");
|
|
5480
5972
|
if (goal.status !== "active") {
|
|
5481
|
-
return { ok: true, message: `Queued for "${goal.name}" (goal is ${goal.status}; delivers on the next active turn).` };
|
|
5973
|
+
return { ok: true, outcome: "queued", goalID, message: `Queued for "${goal.name}" (goal is ${goal.status}; delivers on the next active turn).` };
|
|
5974
|
+
}
|
|
5975
|
+
const guard = continuationGuard(preState, goalID);
|
|
5976
|
+
if (guard)
|
|
5977
|
+
return { ok: true, outcome: "queued", goalID, message: `Saved in inbox for "${goal.name}". ${guard.message}` };
|
|
5978
|
+
const status = goal.workerSessionID ? await host.sessionStatus(goal.workerSessionID) : undefined;
|
|
5979
|
+
if (!(await readState(directory)).goals.some((item) => item.id === goalID))
|
|
5980
|
+
return rejected(goalID, "not_found", "Goal cleared during worker status probe; no prompt was sent.");
|
|
5981
|
+
if (goal.workerSessionID && status !== "idle") {
|
|
5982
|
+
return { ok: true, outcome: "queued", goalID, message: `Saved in inbox for "${goal.name}"; worker is busy or unconfirmed. Its active turn was left intact.` };
|
|
5482
5983
|
}
|
|
5483
5984
|
if (goal.config.write_scope !== undefined) {
|
|
5484
|
-
await continueTurnUnlocked(directory, goalID, { bare: true });
|
|
5485
|
-
return { ok: true, message: `
|
|
5985
|
+
const result = await continueTurnUnlocked(directory, goalID, { bare: true });
|
|
5986
|
+
return result.ok || result.outcome === "not_found" ? result : { ok: true, outcome: "queued", goalID, message: `Saved in inbox for "${goal.name}". ${result.message}` };
|
|
5486
5987
|
}
|
|
5988
|
+
let resetFailure;
|
|
5487
5989
|
const cleared = await mutateState(directory, `goal.send:${goalID}`, async (s) => {
|
|
5990
|
+
resetFailure = continuationGuard(s, goalID);
|
|
5991
|
+
if (resetFailure)
|
|
5992
|
+
return s;
|
|
5488
5993
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
5489
5994
|
if (!rt)
|
|
5490
5995
|
return s;
|
|
@@ -5496,12 +6001,14 @@ function createGoalService(host) {
|
|
|
5496
6001
|
rt.updatedAt = new Date().toISOString();
|
|
5497
6002
|
return s;
|
|
5498
6003
|
});
|
|
6004
|
+
if (resetFailure)
|
|
6005
|
+
return resetFailure.outcome === "not_found" ? resetFailure : { ok: true, outcome: "queued", goalID, message: `Words saved in inbox. ${resetFailure.message}` };
|
|
5499
6006
|
const freshGoal = cleared.goals.find((g) => g.id === goalID);
|
|
5500
6007
|
if (!freshGoal || !freshGoal.workerSessionID) {
|
|
5501
|
-
return { ok: true, message: `Queued for "${goal.name}" (no worker session yet; delivers on the next turn).` };
|
|
6008
|
+
return { ok: true, outcome: "queued", goalID, message: `Queued for "${goal.name}" (no worker session yet; delivers on the next turn).` };
|
|
5502
6009
|
}
|
|
5503
|
-
await continueTurnUnlocked(directory, goalID, { force: true, bare: true });
|
|
5504
|
-
return { ok: true, message: `
|
|
6010
|
+
const result = await continueTurnUnlocked(directory, goalID, { force: true, bare: true });
|
|
6011
|
+
return result.ok || result.outcome === "not_found" ? result : { ok: true, outcome: "queued", goalID, message: `Saved in inbox for "${goal.name}". ${result.message}` };
|
|
5505
6012
|
}
|
|
5506
6013
|
function sendUserMessage(directory, goalID, text) {
|
|
5507
6014
|
return withGoalOperation(goalID, () => sendUnlocked(directory, goalID, text));
|
|
@@ -5575,7 +6082,9 @@ function createGoalService(host) {
|
|
|
5575
6082
|
|
|
5576
6083
|
// src/application/schedule-worker.ts
|
|
5577
6084
|
init_state_repository();
|
|
5578
|
-
|
|
6085
|
+
init_server_log();
|
|
6086
|
+
init_workspace_execution();
|
|
6087
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
5579
6088
|
function createScheduleWorker(options) {
|
|
5580
6089
|
const { directory, goalService } = options;
|
|
5581
6090
|
const intervalMs = options.intervalMs ?? 5000;
|
|
@@ -5666,7 +6175,7 @@ function createScheduleWorker(options) {
|
|
|
5666
6175
|
continue;
|
|
5667
6176
|
await appendEvent(directory, {
|
|
5668
6177
|
version: 1,
|
|
5669
|
-
eventID:
|
|
6178
|
+
eventID: randomUUID8(),
|
|
5670
6179
|
goalID: goal.id,
|
|
5671
6180
|
type: "schedule.tick",
|
|
5672
6181
|
scheduleRunCount: count,
|
|
@@ -5692,7 +6201,7 @@ function createScheduleWorker(options) {
|
|
|
5692
6201
|
}
|
|
5693
6202
|
|
|
5694
6203
|
// src/v2/native-server.ts
|
|
5695
|
-
import { randomUUID as
|
|
6204
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
5696
6205
|
|
|
5697
6206
|
// src/v2/native-bridge.ts
|
|
5698
6207
|
class NativeBridgeError extends Error {
|
|
@@ -5834,7 +6343,7 @@ async function setupNativeServer(context) {
|
|
|
5834
6343
|
bridge,
|
|
5835
6344
|
requestWorker: async (input) => {
|
|
5836
6345
|
const request = {
|
|
5837
|
-
requestID: `req_${
|
|
6346
|
+
requestID: `req_${randomUUID9()}`,
|
|
5838
6347
|
goalID: input.goalID ?? "",
|
|
5839
6348
|
parentSessionID: input.parentSessionID,
|
|
5840
6349
|
title: input.title,
|
|
@@ -6103,7 +6612,7 @@ function createCommandHost(opts) {
|
|
|
6103
6612
|
}
|
|
6104
6613
|
|
|
6105
6614
|
// src/application/command-service.ts
|
|
6106
|
-
import { randomUUID as
|
|
6615
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
6107
6616
|
import { promises as fs5 } from "fs";
|
|
6108
6617
|
import path5 from "path";
|
|
6109
6618
|
|
|
@@ -6359,6 +6868,8 @@ function validateCommandStreamMessage(value) {
|
|
|
6359
6868
|
}
|
|
6360
6869
|
|
|
6361
6870
|
// src/application/command-service.ts
|
|
6871
|
+
init_server_log();
|
|
6872
|
+
init_workspace_execution();
|
|
6362
6873
|
init_command_await2();
|
|
6363
6874
|
init_state_repository();
|
|
6364
6875
|
var ANSI_PATTERN2 = new RegExp("\x1B\\[[0-9;?]*[ -/]*[@-~]|\x1B\\][^\x07]*(?:\x07|\x1B\\\\)|\x1B[()][0-9A-Z]", "g");
|
|
@@ -6467,7 +6978,7 @@ function createCommandService(host, opts) {
|
|
|
6467
6978
|
function watchLedger(directory, commandID, type, extra) {
|
|
6468
6979
|
return appendEvent(directory, {
|
|
6469
6980
|
version: 1,
|
|
6470
|
-
eventID:
|
|
6981
|
+
eventID: randomUUID10(),
|
|
6471
6982
|
commandID,
|
|
6472
6983
|
type,
|
|
6473
6984
|
timestamp: new Date().toISOString(),
|
|
@@ -6944,7 +7455,7 @@ function createCommandService(host, opts) {
|
|
|
6944
7455
|
}).catch(() => {});
|
|
6945
7456
|
await appendEvent(directory, {
|
|
6946
7457
|
version: 1,
|
|
6947
|
-
eventID:
|
|
7458
|
+
eventID: randomUUID10(),
|
|
6948
7459
|
commandID: id,
|
|
6949
7460
|
type: "command.exited",
|
|
6950
7461
|
exitCode: info.exitCode,
|
|
@@ -6989,7 +7500,7 @@ function createCommandService(host, opts) {
|
|
|
6989
7500
|
watchState = initialWatchState();
|
|
6990
7501
|
}
|
|
6991
7502
|
const startedAt = new Date;
|
|
6992
|
-
const id =
|
|
7503
|
+
const id = randomUUID10();
|
|
6993
7504
|
const cwd = input.cwd || directory;
|
|
6994
7505
|
const useShell = input.shell === true;
|
|
6995
7506
|
const spawnOpts = useShell ? { command: "/bin/sh", args: ["-c", shellJoin(command, input.args ?? [])], cwd, cols: input.cols, rows: input.rows, env: input.env } : { command, args: input.args ?? [], cwd, cols: input.cols, rows: input.rows, env: input.env };
|
|
@@ -7055,7 +7566,7 @@ function createCommandService(host, opts) {
|
|
|
7055
7566
|
rememberDir(id, directory);
|
|
7056
7567
|
await appendEvent(directory, {
|
|
7057
7568
|
version: 1,
|
|
7058
|
-
eventID:
|
|
7569
|
+
eventID: randomUUID10(),
|
|
7059
7570
|
...input.goalID ? { goalID: input.goalID } : {},
|
|
7060
7571
|
commandID: id,
|
|
7061
7572
|
type: "command.started",
|
|
@@ -7155,7 +7666,7 @@ function createCommandService(host, opts) {
|
|
|
7155
7666
|
return { session, text: file.text, totalBytes: session.outputBytes, startByte: file.startByte, live: session.status === "running" };
|
|
7156
7667
|
},
|
|
7157
7668
|
async write(directory, id, ownerSessionID, data) {
|
|
7158
|
-
const scopeCallID = `command-write:${
|
|
7669
|
+
const scopeCallID = `command-write:${randomUUID10()}`;
|
|
7159
7670
|
await beforeWorkspaceTool(directory, { tool: "loopd_command_write", sessionID: ownerSessionID, callID: scopeCallID }, {});
|
|
7160
7671
|
try {
|
|
7161
7672
|
const session = await service.get(directory, id, ownerSessionID);
|
|
@@ -7611,7 +8122,7 @@ function createCommandService(host, opts) {
|
|
|
7611
8122
|
}
|
|
7612
8123
|
|
|
7613
8124
|
// src/application/command-event-broker.ts
|
|
7614
|
-
import { randomUUID as
|
|
8125
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
7615
8126
|
function safeDeliver(sink, msg) {
|
|
7616
8127
|
try {
|
|
7617
8128
|
sink(msg);
|
|
@@ -7654,7 +8165,7 @@ function createCommandEventBroker(resolver) {
|
|
|
7654
8165
|
throw new Error("ownerSessionID is required.");
|
|
7655
8166
|
if (typeof sink !== "function")
|
|
7656
8167
|
throw new Error("sink must be a function.");
|
|
7657
|
-
const sinkID =
|
|
8168
|
+
const sinkID = randomUUID11();
|
|
7658
8169
|
const entry = { sinkID, sink, state: "subscribing", buffer: [] };
|
|
7659
8170
|
entriesFor(commandID).set(sinkID, entry);
|
|
7660
8171
|
try {
|
|
@@ -7743,9 +8254,10 @@ function createCommandEventBroker(resolver) {
|
|
|
7743
8254
|
}
|
|
7744
8255
|
|
|
7745
8256
|
// src/server/command-stream-server.ts
|
|
7746
|
-
import { randomBytes, randomUUID as
|
|
8257
|
+
import { randomBytes, randomUUID as randomUUID12, timingSafeEqual } from "crypto";
|
|
7747
8258
|
import { promises as fs6 } from "fs";
|
|
7748
8259
|
import path6 from "path";
|
|
8260
|
+
init_server_log();
|
|
7749
8261
|
function streamEndpointPath(directory) {
|
|
7750
8262
|
return path6.join(directory, ".opencode", "loopd", "commands", ".stream-endpoint.json");
|
|
7751
8263
|
}
|
|
@@ -7777,7 +8289,7 @@ var MAX_BUFFERED_BYTES = 512 * 1024;
|
|
|
7777
8289
|
function createCommandStreamServer(directory, commandService, broker) {
|
|
7778
8290
|
const endpointPath = streamEndpointPath(directory);
|
|
7779
8291
|
const token = randomBytes(32).toString("hex");
|
|
7780
|
-
const generation =
|
|
8292
|
+
const generation = randomUUID12();
|
|
7781
8293
|
const startedAt = new Date().toISOString();
|
|
7782
8294
|
let server;
|
|
7783
8295
|
let serverURL;
|
|
@@ -8097,7 +8609,7 @@ function createCommandStreamServer(directory, commandService, broker) {
|
|
|
8097
8609
|
|
|
8098
8610
|
// src/server/goal-tools.ts
|
|
8099
8611
|
init_state_repository();
|
|
8100
|
-
import { randomUUID as
|
|
8612
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
8101
8613
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
8102
8614
|
// src/domain/verification.ts
|
|
8103
8615
|
var MAX_RECENT_ATTEMPTS = 10;
|
|
@@ -8112,10 +8624,13 @@ function appendVerificationAttempt(recent, attempt) {
|
|
|
8112
8624
|
// src/server/goal-tools.ts
|
|
8113
8625
|
import { exec as execChild } from "child_process";
|
|
8114
8626
|
import { promisify } from "util";
|
|
8627
|
+
init_server_log();
|
|
8115
8628
|
|
|
8116
8629
|
// src/application/goal-scope.ts
|
|
8117
8630
|
init_state_repository();
|
|
8118
8631
|
init_workspace_scope();
|
|
8632
|
+
init_workspace_execution();
|
|
8633
|
+
|
|
8119
8634
|
class GoalScopeConflictError extends Error {
|
|
8120
8635
|
conflict;
|
|
8121
8636
|
kind;
|
|
@@ -8155,6 +8670,7 @@ async function claimGoalScope(directory, goalID, workerSessionID, generation, pa
|
|
|
8155
8670
|
}
|
|
8156
8671
|
|
|
8157
8672
|
// src/server/goal-tools.ts
|
|
8673
|
+
init_workspace_execution();
|
|
8158
8674
|
var execAsync = promisify(execChild);
|
|
8159
8675
|
function goalTools(dir, goalService, hostSessionID, defaults = {}, host) {
|
|
8160
8676
|
return {
|
|
@@ -8422,7 +8938,7 @@ function goalTools(dir, goalService, hostSessionID, defaults = {}, host) {
|
|
|
8422
8938
|
state.revision = (await persistWorkerMutation(dir, goal, runtime, ["active"])).revision;
|
|
8423
8939
|
const event = {
|
|
8424
8940
|
version: 1,
|
|
8425
|
-
eventID:
|
|
8941
|
+
eventID: randomUUID13(),
|
|
8426
8942
|
goalID: goal.id,
|
|
8427
8943
|
type: "goal.progress",
|
|
8428
8944
|
summary: args.summary,
|
|
@@ -8487,7 +9003,7 @@ Exit code: ${f.exitCode}${stdoutSnippet}${stderrSnippet}`;
|
|
|
8487
9003
|
Working directory: ${cwd}
|
|
8488
9004
|
|
|
8489
9005
|
${failureDetails}`;
|
|
8490
|
-
attemptID =
|
|
9006
|
+
attemptID = randomUUID13();
|
|
8491
9007
|
const verificationAttempt = {
|
|
8492
9008
|
id: attemptID,
|
|
8493
9009
|
sequence: runtime.evaluatorRejectionCount,
|
|
@@ -8532,7 +9048,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8532
9048
|
if (attemptID) {
|
|
8533
9049
|
await appendEvent(dir, {
|
|
8534
9050
|
version: 1,
|
|
8535
|
-
eventID:
|
|
9051
|
+
eventID: randomUUID13(),
|
|
8536
9052
|
goalID: goal.id,
|
|
8537
9053
|
type: "goal.completion_rejected",
|
|
8538
9054
|
attemptID,
|
|
@@ -8550,7 +9066,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8550
9066
|
if (blocked && rejectedGoal?.blocker) {
|
|
8551
9067
|
await appendEvent(dir, {
|
|
8552
9068
|
version: 1,
|
|
8553
|
-
eventID:
|
|
9069
|
+
eventID: randomUUID13(),
|
|
8554
9070
|
goalID: goal.id,
|
|
8555
9071
|
type: "goal.blocked",
|
|
8556
9072
|
reason: rejectedGoal.blocker.reason,
|
|
@@ -8610,7 +9126,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8610
9126
|
}
|
|
8611
9127
|
runtime.updatedAt = new Date().toISOString();
|
|
8612
9128
|
}
|
|
8613
|
-
const attemptID =
|
|
9129
|
+
const attemptID = randomUUID13();
|
|
8614
9130
|
const cwd = goal.config.checkCwd || goal.config.artifactDir || dir;
|
|
8615
9131
|
const checks = (goal.config.checks || []).map((cmd) => ({
|
|
8616
9132
|
command: cmd,
|
|
@@ -8634,7 +9150,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8634
9150
|
state.revision = (await persistWorkerMutation(dir, goal, runtime, ["active", "budget_limited"])).revision;
|
|
8635
9151
|
const event = {
|
|
8636
9152
|
version: 1,
|
|
8637
|
-
eventID:
|
|
9153
|
+
eventID: randomUUID13(),
|
|
8638
9154
|
goalID: goal.id,
|
|
8639
9155
|
type: "goal.completed",
|
|
8640
9156
|
summary: args.summary,
|
|
@@ -8694,7 +9210,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8694
9210
|
state.revision = (await persistWorkerMutation(dir, goal, runtime, ["active", "budget_limited"])).revision;
|
|
8695
9211
|
const event = {
|
|
8696
9212
|
version: 1,
|
|
8697
|
-
eventID:
|
|
9213
|
+
eventID: randomUUID13(),
|
|
8698
9214
|
goalID: goal.id,
|
|
8699
9215
|
type: "goal.blocked",
|
|
8700
9216
|
reason: args.reason,
|
|
@@ -8748,6 +9264,9 @@ async function withParentIdentity(defaults, host, ownerSessionID) {
|
|
|
8748
9264
|
function findGoalByWorkerSession(state, sessionID) {
|
|
8749
9265
|
if (!sessionID)
|
|
8750
9266
|
return;
|
|
9267
|
+
if (state.retiredWorkerSessions?.includes(sessionID)) {
|
|
9268
|
+
throw new Error("Goal cleared or worker retired. This session has no goal authority; stop work and ask the owner to list their current goals. Do not resume a remembered ID.");
|
|
9269
|
+
}
|
|
8751
9270
|
return state.goals.find((g) => g.workerSessionID === sessionID && (g.status === "active" || g.status === "blocked"));
|
|
8752
9271
|
}
|
|
8753
9272
|
function formatGoalStructured(goal, runtime, enforcedScopedExecution = false) {
|
|
@@ -8875,7 +9394,7 @@ var GOAL_STATUS_META = {
|
|
|
8875
9394
|
var PHASE_META = {
|
|
8876
9395
|
idle: { short: "Idle", hint: "between turns" },
|
|
8877
9396
|
queued: { short: "Queued", hint: "waiting to start" },
|
|
8878
|
-
running: { short: "
|
|
9397
|
+
running: { short: "Reserved", hint: "turn reserved; activity unconfirmed" },
|
|
8879
9398
|
compacting: { short: "Compacting", hint: "summarizing context" },
|
|
8880
9399
|
waiting_retry: { short: "Retrying", hint: "backing off" },
|
|
8881
9400
|
stopping: { short: "Stopping", hint: "abort in flight" }
|
|
@@ -8886,9 +9405,30 @@ function goalStatusLabel(status) {
|
|
|
8886
9405
|
function phaseLabel(phase) {
|
|
8887
9406
|
return PHASE_META[phase] ?? { short: phase, hint: "" };
|
|
8888
9407
|
}
|
|
8889
|
-
function
|
|
9408
|
+
function workerActivityLabel(runtime, now = Date.now()) {
|
|
9409
|
+
if (runtime.phase !== "running")
|
|
9410
|
+
return { ...phaseLabel(runtime.phase), acting: false };
|
|
9411
|
+
const expires = runtime.leaseExpiresAt ? Date.parse(runtime.leaseExpiresAt) : NaN;
|
|
9412
|
+
if (Number.isFinite(expires) && expires <= now)
|
|
9413
|
+
return { short: "Stale", hint: "lease expired; activity unconfirmed (not proof of worker exit)", acting: false };
|
|
9414
|
+
if (!Number.isFinite(expires) || (runtime.unknownStatusCount ?? 0) > 0)
|
|
9415
|
+
return { short: "Unconfirmed", hint: "worker status unavailable; do not assume the prompt failed", acting: false };
|
|
9416
|
+
if (runtime.activeAssistantCompletedAt)
|
|
9417
|
+
return { short: "Finishing", hint: "response completed; waiting for turn reconciliation", acting: false };
|
|
9418
|
+
if (runtime.activePromptObservedAt && runtime.lastWorkerStatus !== "idle" && runtime.lastWorkerStatus !== "retry" && (runtime.activeAssistantMessageID || runtime.activeToolCallIDs?.length)) {
|
|
9419
|
+
return { short: "Running", hint: "correlated assistant or tool activity observed", acting: true };
|
|
9420
|
+
}
|
|
9421
|
+
if (runtime.activePromptObservedAt)
|
|
9422
|
+
return { short: "Prompt seen", hint: "prompt observed; response activity not confirmed", acting: false };
|
|
9423
|
+
if (runtime.activePromptAcceptedAt)
|
|
9424
|
+
return { short: "Awaiting", hint: "host accepted prompt; transcript activity not confirmed", acting: false };
|
|
9425
|
+
if (runtime.activePromptDispatchAt)
|
|
9426
|
+
return { short: "Sending", hint: "dispatch requested; awaiting host acceptance", acting: false };
|
|
9427
|
+
return { short: "Requested", hint: "run reserved; no dispatch evidence yet", acting: false };
|
|
9428
|
+
}
|
|
9429
|
+
function describeGoalState(status, phase, now = Date.now()) {
|
|
8890
9430
|
const goal = goalStatusLabel(status);
|
|
8891
|
-
const activity = phase ? phaseLabel(phase) : undefined;
|
|
9431
|
+
const activity = phase ? typeof phase === "string" ? phaseLabel(phase) : workerActivityLabel(phase, now) : undefined;
|
|
8892
9432
|
const workerClause = activity ? `worker is ${activity.hint || activity.short.toLowerCase()}` : "worker state unknown";
|
|
8893
9433
|
if (status === "active")
|
|
8894
9434
|
return `Loopd owns this; ${workerClause}.`;
|
|
@@ -9233,18 +9773,21 @@ function ownerTools2(options) {
|
|
|
9233
9773
|
execute: async (args, context) => {
|
|
9234
9774
|
const state = await readState(directory);
|
|
9235
9775
|
const ownerID = context?.sessionID;
|
|
9236
|
-
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id
|
|
9776
|
+
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id) : state.goals.find((g) => g.ownerSessionID === ownerID && g.status !== "complete");
|
|
9237
9777
|
if (!goal) {
|
|
9238
9778
|
return {
|
|
9239
9779
|
title: "No goal found",
|
|
9240
|
-
output: JSON.stringify({ ok: false, message: "No matching active goal for this session." })
|
|
9780
|
+
output: JSON.stringify({ ok: false, outcome: "not_found", message: args.goal_id ? "Goal not found (it may have been cleared). List your goals instead of retrying this ID." : "No matching active goal for this session." })
|
|
9241
9781
|
};
|
|
9242
9782
|
}
|
|
9783
|
+
if (goal.ownerSessionID !== ownerID) {
|
|
9784
|
+
return { title: "Not authorized", output: JSON.stringify({ ok: false, outcome: "unauthorized", message: "This session cannot control that goal." }) };
|
|
9785
|
+
}
|
|
9243
9786
|
const result = await goalService.sendUserMessage(directory, goal.id, args.message);
|
|
9244
9787
|
return {
|
|
9245
|
-
title: result.ok ? "Message
|
|
9788
|
+
title: result.ok ? result.outcome === "queued" ? "Message queued" : "Message submitted" : "Send failed",
|
|
9246
9789
|
output: JSON.stringify({
|
|
9247
|
-
|
|
9790
|
+
...result,
|
|
9248
9791
|
goalID: goal.id,
|
|
9249
9792
|
goalName: goal.name,
|
|
9250
9793
|
message: result.message
|
|
@@ -9304,16 +9847,20 @@ function ownerTools2(options) {
|
|
|
9304
9847
|
execute: async (args, context) => {
|
|
9305
9848
|
const state = await readState(directory);
|
|
9306
9849
|
const ownerID = context?.sessionID;
|
|
9307
|
-
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id
|
|
9850
|
+
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id) : state.goals.find((g) => g.ownerSessionID === ownerID && (g.status === "paused" || g.status === "blocked"));
|
|
9308
9851
|
if (!goal) {
|
|
9309
9852
|
return {
|
|
9310
9853
|
title: "No goal found",
|
|
9311
9854
|
output: JSON.stringify({
|
|
9312
9855
|
ok: false,
|
|
9313
|
-
|
|
9856
|
+
outcome: "not_found",
|
|
9857
|
+
message: args.goal_id ? "Goal not found (it may have been cleared). List your goals instead of retrying this ID." : "No matching paused/blocked goal for this session."
|
|
9314
9858
|
})
|
|
9315
9859
|
};
|
|
9316
9860
|
}
|
|
9861
|
+
if (goal.ownerSessionID !== ownerID) {
|
|
9862
|
+
return { title: "Not authorized", output: JSON.stringify({ ok: false, outcome: "unauthorized", message: "This session cannot control that goal." }) };
|
|
9863
|
+
}
|
|
9317
9864
|
if (goal.status === "active") {
|
|
9318
9865
|
return {
|
|
9319
9866
|
title: "Already active",
|
|
@@ -9321,18 +9868,13 @@ function ownerTools2(options) {
|
|
|
9321
9868
|
};
|
|
9322
9869
|
}
|
|
9323
9870
|
try {
|
|
9324
|
-
|
|
9325
|
-
await goalService.resume(directory, goal.id);
|
|
9326
|
-
} else if (goal.status === "blocked") {
|
|
9327
|
-
await goalService.retry(directory, goal.id);
|
|
9328
|
-
}
|
|
9871
|
+
const result = goal.status === "blocked" ? await goalService.retry(directory, goal.id) : await goalService.resume(directory, goal.id);
|
|
9329
9872
|
return {
|
|
9330
|
-
title: "
|
|
9873
|
+
title: result.ok ? "Resume submitted" : "Resume not delivered",
|
|
9331
9874
|
output: JSON.stringify({
|
|
9332
|
-
|
|
9875
|
+
...result,
|
|
9333
9876
|
goalID: goal.id,
|
|
9334
|
-
goalName: goal.name
|
|
9335
|
-
message: `Goal "${goal.name}" resumed.`
|
|
9877
|
+
goalName: goal.name
|
|
9336
9878
|
})
|
|
9337
9879
|
};
|
|
9338
9880
|
} catch (error) {
|
|
@@ -9348,20 +9890,23 @@ function ownerTools2(options) {
|
|
|
9348
9890
|
}
|
|
9349
9891
|
}),
|
|
9350
9892
|
nudge_goal: tool2({
|
|
9351
|
-
description: "
|
|
9893
|
+
description: "Request a recovery turn for an active goal after confirmed worker idle. Returns the actual delivery outcome or actionable blocker (in-flight writes, workspace operation, busy/unknown worker, pending identity, quota). Never interrupts a live worker or duplicates an unconfirmed prompt. Per-goal mutex.",
|
|
9352
9894
|
args: {
|
|
9353
9895
|
goal_id: tool2.schema.string().optional().describe("Goal ID. Omit to nudge the first active goal.")
|
|
9354
9896
|
},
|
|
9355
9897
|
execute: async (args, context) => {
|
|
9356
9898
|
const state = await readState(directory);
|
|
9357
9899
|
const ownerID = context?.sessionID;
|
|
9358
|
-
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id
|
|
9900
|
+
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id) : state.goals.find((g) => g.ownerSessionID === ownerID && g.status !== "complete");
|
|
9359
9901
|
if (!goal) {
|
|
9360
9902
|
return {
|
|
9361
9903
|
title: "No goal found",
|
|
9362
|
-
output: JSON.stringify({ ok: false, message: "No matching active goal for this session." })
|
|
9904
|
+
output: JSON.stringify({ ok: false, outcome: "not_found", message: args.goal_id ? "Goal not found (it may have been cleared). List your goals instead of retrying this ID." : "No matching active goal for this session." })
|
|
9363
9905
|
};
|
|
9364
9906
|
}
|
|
9907
|
+
if (goal.ownerSessionID !== ownerID) {
|
|
9908
|
+
return { title: "Not authorized", output: JSON.stringify({ ok: false, outcome: "unauthorized", message: "This session cannot control that goal." }) };
|
|
9909
|
+
}
|
|
9365
9910
|
try {
|
|
9366
9911
|
const result = await goalService.nudge(directory, goal.id);
|
|
9367
9912
|
return {
|
|
@@ -9863,11 +10408,13 @@ function commandTools(options) {
|
|
|
9863
10408
|
}
|
|
9864
10409
|
|
|
9865
10410
|
// src/server/plugin.ts
|
|
10411
|
+
init_server_log();
|
|
9866
10412
|
init_state_repository();
|
|
9867
10413
|
// package.json
|
|
9868
|
-
var version = "1.
|
|
10414
|
+
var version = "1.13.1";
|
|
9869
10415
|
|
|
9870
10416
|
// src/server/plugin.ts
|
|
10417
|
+
init_workspace_execution();
|
|
9871
10418
|
var PLUGIN_ID = "opencode-loopd.server";
|
|
9872
10419
|
var server = async ({ client, directory }, pluginOptions) => {
|
|
9873
10420
|
const defaults = parsePluginDefaults(pluginOptions);
|