@bojackduy/opencode-loopd 1.13.0 → 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 +688 -423
- package/dist/tui.js +57 -31
- 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();
|
|
@@ -1351,7 +1421,7 @@ function scopeConflictMessage(conflict) {
|
|
|
1351
1421
|
var init_workspace_scope = () => {};
|
|
1352
1422
|
|
|
1353
1423
|
// src/application/workspace-execution.ts
|
|
1354
|
-
import { randomUUID as
|
|
1424
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1355
1425
|
import path3 from "path";
|
|
1356
1426
|
function structuredWritePaths(tool, args) {
|
|
1357
1427
|
if (tool === "write" || tool === "edit") {
|
|
@@ -1391,107 +1461,31 @@ function structuredWritePaths(tool, args) {
|
|
|
1391
1461
|
throw new Error("Scope denied: patch has no file operations.");
|
|
1392
1462
|
return paths;
|
|
1393
1463
|
}
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1396
|
-
return Number.POSITIVE_INFINITY;
|
|
1397
|
-
const age = now - Date.parse(call.at);
|
|
1398
|
-
return Number.isFinite(age) ? age : Number.POSITIVE_INFINITY;
|
|
1399
|
-
}
|
|
1400
|
-
function runtimeActivityFresh(state, goalID, now) {
|
|
1401
|
-
if (!goalID)
|
|
1402
|
-
return false;
|
|
1403
|
-
const rt = state.runtimes.find((r) => r.goalID === goalID);
|
|
1404
|
-
if (!rt?.lastActivityAt)
|
|
1405
|
-
return false;
|
|
1406
|
-
const age = now - Date.parse(rt.lastActivityAt);
|
|
1407
|
-
return Number.isFinite(age) && age < STALE_WORKSPACE_CALL_TTL_MS;
|
|
1408
|
-
}
|
|
1409
|
-
function isGenerationSuperseded(state, call) {
|
|
1410
|
-
if (call.generation === undefined || !call.goalID)
|
|
1411
|
-
return false;
|
|
1412
|
-
const rt = state.runtimes.find((r) => r.goalID === call.goalID);
|
|
1413
|
-
return !!rt && call.generation < rt.runGeneration;
|
|
1414
|
-
}
|
|
1415
|
-
function isSyncStaleWorkspaceCall(state, call, now = Date.now()) {
|
|
1416
|
-
if (isGenerationSuperseded(state, call))
|
|
1417
|
-
return true;
|
|
1418
|
-
if (call.goalID && !state.goals.some((g) => g.id === call.goalID))
|
|
1419
|
-
return true;
|
|
1420
|
-
return callAgeMs(call, now) > CLEAR_PENDING_FORCE_TTL_MS;
|
|
1464
|
+
function isSyncStaleWorkspaceCall(_state, _call, _now = Date.now()) {
|
|
1465
|
+
return false;
|
|
1421
1466
|
}
|
|
1422
1467
|
function isStaleWorkspaceCall(state, call, liveness = {}) {
|
|
1423
|
-
|
|
1424
|
-
return true;
|
|
1425
|
-
const now = liveness.now ?? Date.now();
|
|
1426
|
-
const age = callAgeMs(call, now);
|
|
1427
|
-
if (!call.goalID)
|
|
1428
|
-
return age > OWNER_CALL_TTL_MS;
|
|
1429
|
-
const goal = state.goals.find((g) => g.id === call.goalID);
|
|
1430
|
-
if (!goal)
|
|
1431
|
-
return true;
|
|
1432
|
-
if (age <= STALE_WORKSPACE_CALL_TTL_MS)
|
|
1433
|
-
return false;
|
|
1434
|
-
const live = liveness.isWorkerLive?.(goal.id, goal.workerSessionID) ?? false;
|
|
1435
|
-
if (live && runtimeActivityFresh(state, goal.id, now))
|
|
1436
|
-
return false;
|
|
1437
|
-
if (live) {
|
|
1438
|
-
return age > CLEAR_PENDING_FORCE_TTL_MS;
|
|
1439
|
-
}
|
|
1440
|
-
return true;
|
|
1468
|
+
return isSyncStaleWorkspaceCall(state, call, liveness.now ?? Date.now());
|
|
1441
1469
|
}
|
|
1442
1470
|
function sweepStaleWorkspaceState(state, liveness = {}) {
|
|
1443
1471
|
const now = liveness.now ?? Date.now();
|
|
1444
1472
|
const summary = { reapedCalls: [], resetClosing: [], finalizedClears: [], forceCleared: [] };
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1447
|
-
const kept = before.filter((call) => {
|
|
1448
|
-
if (call.goalID && clearPendingGoals.has(call.goalID) && !isGenerationSuperseded(state, call) && state.goals.some((g) => g.id === call.goalID)) {
|
|
1449
|
-
return true;
|
|
1450
|
-
}
|
|
1451
|
-
if (!isStaleWorkspaceCall(state, call, { ...liveness, now }))
|
|
1452
|
-
return true;
|
|
1453
|
-
summary.reapedCalls.push({
|
|
1454
|
-
callID: call.callID,
|
|
1455
|
-
sessionID: call.sessionID,
|
|
1456
|
-
goalID: call.goalID,
|
|
1457
|
-
reason: isGenerationSuperseded(state, call) ? "generation-superseded" : call.goalID && !state.goals.some((g) => g.id === call.goalID) ? "goal-gone" : "age-ttl-no-live-backing"
|
|
1458
|
-
});
|
|
1459
|
-
return false;
|
|
1460
|
-
});
|
|
1461
|
-
state.workspaceCalls = kept;
|
|
1462
|
-
for (const goal of state.goals) {
|
|
1473
|
+
const kept = state.workspaceCalls ?? [];
|
|
1474
|
+
for (const goal of [...state.goals]) {
|
|
1463
1475
|
const referenced = kept.some((call) => call.goalID === goal.id) || state.workspaceOperation?.goalID === goal.id;
|
|
1464
1476
|
if (goal.scopeClosing && !referenced && !(liveness.isWorkerLive?.(goal.id, goal.workerSessionID) ?? false)) {
|
|
1465
1477
|
goal.scopeClosing = false;
|
|
1466
1478
|
goal.updatedAt = new Date(now).toISOString();
|
|
1467
1479
|
summary.resetClosing.push(goal.id);
|
|
1468
1480
|
}
|
|
1469
|
-
if (goal.scopeClearPending
|
|
1481
|
+
if (goal.scopeClearPending) {
|
|
1482
|
+
retireWorker(state, goal);
|
|
1470
1483
|
state.goals = state.goals.filter((g) => g.id !== goal.id);
|
|
1471
1484
|
state.runtimes = state.runtimes.filter((r) => r.goalID !== goal.id);
|
|
1472
1485
|
state.commandAwaits = (state.commandAwaits ?? []).filter((a) => a.goalID !== goal.id);
|
|
1473
1486
|
summary.finalizedClears.push(goal.id);
|
|
1474
1487
|
}
|
|
1475
1488
|
}
|
|
1476
|
-
for (const goal of [...state.goals]) {
|
|
1477
|
-
if (!goal.scopeClearPending)
|
|
1478
|
-
continue;
|
|
1479
|
-
const refs = (state.workspaceCalls ?? []).filter((call) => call.goalID === goal.id);
|
|
1480
|
-
if (!refs.length)
|
|
1481
|
-
continue;
|
|
1482
|
-
if (refs.every((call) => callAgeMs(call, now) > CLEAR_PENDING_FORCE_TTL_MS)) {
|
|
1483
|
-
for (const call of refs) {
|
|
1484
|
-
summary.reapedCalls.push({ callID: call.callID, sessionID: call.sessionID, goalID: call.goalID, reason: "clear-pending-force" });
|
|
1485
|
-
}
|
|
1486
|
-
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => call.goalID !== goal.id);
|
|
1487
|
-
if (!(state.workspaceCalls ?? []).some((call) => call.goalID === goal.id) && state.workspaceOperation?.goalID !== goal.id) {
|
|
1488
|
-
state.goals = state.goals.filter((g) => g.id !== goal.id);
|
|
1489
|
-
state.runtimes = state.runtimes.filter((r) => r.goalID !== goal.id);
|
|
1490
|
-
state.commandAwaits = (state.commandAwaits ?? []).filter((a) => a.goalID !== goal.id);
|
|
1491
|
-
summary.forceCleared.push(goal.id);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
1489
|
return summary;
|
|
1496
1490
|
}
|
|
1497
1491
|
function stampCallGeneration(state, goalID) {
|
|
@@ -1557,7 +1551,7 @@ async function beforeWorkspaceTool(directory, input, args) {
|
|
|
1557
1551
|
await mutateState(directory, `workspace.before:${input.callID}`, async (state) => {
|
|
1558
1552
|
const goal = state.goals.find((item) => item.workerSessionID === input.sessionID);
|
|
1559
1553
|
if (state.retiredWorkerSessions?.includes(input.sessionID))
|
|
1560
|
-
throw new Error("Scope denied: retired worker session; late executions are fenced.");
|
|
1554
|
+
throw new Error("Scope denied: retired worker session; goal cleared or worker rotated. Stop work; late executions are fenced.");
|
|
1561
1555
|
const held = heldGoals(state);
|
|
1562
1556
|
const protectedGoals = held.filter((item) => item.config.write_scope !== undefined || item.config.workspaceWrite === false);
|
|
1563
1557
|
if (!goal && ownerTools.has(input.tool) && (!protectedGoals.length || state.goals.some((item) => item.ownerSessionID === input.sessionID)))
|
|
@@ -1621,6 +1615,7 @@ async function afterWorkspaceTool(directory, sessionID, callID) {
|
|
|
1621
1615
|
const live = (state.workspaceCalls ?? []).some((call) => call.goalID === goal.id && !isSyncStaleWorkspaceCall(state, call, now));
|
|
1622
1616
|
if (!goal.scopeClearPending || live || state.workspaceOperation?.goalID === goal.id)
|
|
1623
1617
|
return true;
|
|
1618
|
+
retireWorker(state, goal);
|
|
1624
1619
|
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => !(call.goalID === goal.id && isSyncStaleWorkspaceCall(state, call, now)));
|
|
1625
1620
|
state.runtimes = state.runtimes.filter((runtime) => runtime.goalID !== goal.id);
|
|
1626
1621
|
return false;
|
|
@@ -1628,6 +1623,63 @@ async function afterWorkspaceTool(directory, sessionID, callID) {
|
|
|
1628
1623
|
return state;
|
|
1629
1624
|
});
|
|
1630
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
|
+
}
|
|
1631
1683
|
function guardV2ToolEditor(directory, editor) {
|
|
1632
1684
|
if (typeof editor.list !== "function" || typeof editor.update !== "function")
|
|
1633
1685
|
throw new Error("Scoped execution requires the v2 tool editor list/update execution surface.");
|
|
@@ -1646,7 +1698,7 @@ function guardV2ToolEditor(directory, editor) {
|
|
|
1646
1698
|
}
|
|
1647
1699
|
}
|
|
1648
1700
|
async function withWorkspaceOperation(directory, goalID, sessionID, run) {
|
|
1649
|
-
const id =
|
|
1701
|
+
const id = randomUUID4();
|
|
1650
1702
|
await mutateState(directory, `workspace.operation:${id}`, async (state) => {
|
|
1651
1703
|
const now = Date.now();
|
|
1652
1704
|
const live = (state.workspaceCalls ?? []).filter((call) => !isSyncStaleWorkspaceCall(state, call, now));
|
|
@@ -1664,8 +1716,9 @@ async function withWorkspaceOperation(directory, goalID, sessionID, run) {
|
|
|
1664
1716
|
const now = Date.now();
|
|
1665
1717
|
state.goals = state.goals.filter((goal) => {
|
|
1666
1718
|
const live = (state.workspaceCalls ?? []).some((call) => call.goalID === goal.id && !isSyncStaleWorkspaceCall(state, call, now));
|
|
1667
|
-
if (!goal.scopeClearPending || live)
|
|
1719
|
+
if (!goal.scopeClearPending || live || state.workspaceOperation?.goalID === goal.id)
|
|
1668
1720
|
return true;
|
|
1721
|
+
retireWorker(state, goal);
|
|
1669
1722
|
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => !(call.goalID === goal.id && isSyncStaleWorkspaceCall(state, call, now)));
|
|
1670
1723
|
state.runtimes = state.runtimes.filter((runtime) => runtime.goalID !== goal.id);
|
|
1671
1724
|
return false;
|
|
@@ -1677,6 +1730,7 @@ async function withWorkspaceOperation(directory, goalID, sessionID, run) {
|
|
|
1677
1730
|
var readTools, ownerTools, STALE_WORKSPACE_CALL_TTL_MS, CLEAR_PENDING_FORCE_TTL_MS, OWNER_CALL_TTL_MS;
|
|
1678
1731
|
var init_workspace_execution = __esm(() => {
|
|
1679
1732
|
init_state_repository();
|
|
1733
|
+
init_server_log();
|
|
1680
1734
|
init_workspace_scope();
|
|
1681
1735
|
readTools = new Set([
|
|
1682
1736
|
"read",
|
|
@@ -1726,7 +1780,7 @@ import { tool as v1Tool } from "@opencode-ai/plugin/tool";
|
|
|
1726
1780
|
|
|
1727
1781
|
// src/application/control-worker.ts
|
|
1728
1782
|
init_state_repository();
|
|
1729
|
-
import { randomUUID as
|
|
1783
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1730
1784
|
|
|
1731
1785
|
// src/application/goal-policy.ts
|
|
1732
1786
|
function resolveGoalCreationConfig(input) {
|
|
@@ -1799,42 +1853,8 @@ function cleanList(value) {
|
|
|
1799
1853
|
return value.map(cleanText).filter((item) => Boolean(item));
|
|
1800
1854
|
}
|
|
1801
1855
|
|
|
1802
|
-
// src/infrastructure/server-log.ts
|
|
1803
|
-
import { appendFile } from "fs/promises";
|
|
1804
|
-
var SERVER_LOG_FILE = "/tmp/loopd-server.log";
|
|
1805
|
-
async function logServerEvent(directory, event, details = {}) {
|
|
1806
|
-
try {
|
|
1807
|
-
await appendFile(SERVER_LOG_FILE, `${JSON.stringify({
|
|
1808
|
-
timestamp: new Date().toISOString(),
|
|
1809
|
-
directory,
|
|
1810
|
-
event,
|
|
1811
|
-
...details
|
|
1812
|
-
}, errorReplacer)}
|
|
1813
|
-
`);
|
|
1814
|
-
} catch {}
|
|
1815
|
-
}
|
|
1816
|
-
function describeError(value) {
|
|
1817
|
-
if (value instanceof Error)
|
|
1818
|
-
return value.message;
|
|
1819
|
-
if (typeof value === "string")
|
|
1820
|
-
return value;
|
|
1821
|
-
if (typeof value === "object" && value !== null && "message" in value) {
|
|
1822
|
-
return String(value.message);
|
|
1823
|
-
}
|
|
1824
|
-
try {
|
|
1825
|
-
return JSON.stringify(value, errorReplacer);
|
|
1826
|
-
} catch {
|
|
1827
|
-
return String(value);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
function errorReplacer(_key, value) {
|
|
1831
|
-
if (value instanceof Error) {
|
|
1832
|
-
return { name: value.name, message: value.message, stack: value.stack };
|
|
1833
|
-
}
|
|
1834
|
-
return value;
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
1856
|
// src/application/control-worker.ts
|
|
1857
|
+
init_server_log();
|
|
1838
1858
|
init_command_await2();
|
|
1839
1859
|
var MAX_LEDGER_SIZE = 100;
|
|
1840
1860
|
var RESPONSE_CLEANUP_AGE_MS = 60 * 60 * 1000;
|
|
@@ -1998,23 +2018,25 @@ function createControlWorker(options) {
|
|
|
1998
2018
|
break;
|
|
1999
2019
|
}
|
|
2000
2020
|
case "resume": {
|
|
2001
|
-
await goalSvc.resume(directory, request.goalID);
|
|
2021
|
+
const result = await goalSvc.resume(directory, request.goalID);
|
|
2002
2022
|
const state = await readState(directory);
|
|
2003
|
-
const goal = state.goals.find((g) => g.id === request.goalID);
|
|
2004
2023
|
response = {
|
|
2005
2024
|
...base,
|
|
2006
|
-
|
|
2025
|
+
ok: result.ok,
|
|
2026
|
+
message: result.message,
|
|
2027
|
+
errorCode: result.ok ? undefined : result.outcome,
|
|
2007
2028
|
stateRevision: state.revision
|
|
2008
2029
|
};
|
|
2009
2030
|
break;
|
|
2010
2031
|
}
|
|
2011
2032
|
case "retry": {
|
|
2012
|
-
await goalSvc.retry(directory, request.goalID);
|
|
2033
|
+
const result = await goalSvc.retry(directory, request.goalID);
|
|
2013
2034
|
const state = await readState(directory);
|
|
2014
|
-
const goal = state.goals.find((g) => g.id === request.goalID);
|
|
2015
2035
|
response = {
|
|
2016
2036
|
...base,
|
|
2017
|
-
|
|
2037
|
+
ok: result.ok,
|
|
2038
|
+
message: result.message,
|
|
2039
|
+
errorCode: result.ok ? undefined : result.outcome,
|
|
2018
2040
|
stateRevision: state.revision
|
|
2019
2041
|
};
|
|
2020
2042
|
break;
|
|
@@ -2030,6 +2052,7 @@ function createControlWorker(options) {
|
|
|
2030
2052
|
...base,
|
|
2031
2053
|
ok: result.ok,
|
|
2032
2054
|
message: result.message,
|
|
2055
|
+
errorCode: result.ok ? undefined : result.outcome,
|
|
2033
2056
|
stateRevision: state.revision
|
|
2034
2057
|
};
|
|
2035
2058
|
break;
|
|
@@ -2124,6 +2147,7 @@ function createControlWorker(options) {
|
|
|
2124
2147
|
...base,
|
|
2125
2148
|
ok: sent.ok,
|
|
2126
2149
|
message: sent.message,
|
|
2150
|
+
errorCode: sent.ok ? undefined : sent.outcome,
|
|
2127
2151
|
stateRevision: state.revision
|
|
2128
2152
|
};
|
|
2129
2153
|
break;
|
|
@@ -2186,7 +2210,7 @@ function createControlWorker(options) {
|
|
|
2186
2210
|
});
|
|
2187
2211
|
await appendEvent(directory, {
|
|
2188
2212
|
version: 1,
|
|
2189
|
-
eventID:
|
|
2213
|
+
eventID: randomUUID3(),
|
|
2190
2214
|
goalID: goal.id,
|
|
2191
2215
|
type: "goal.completed",
|
|
2192
2216
|
summary: goal.completionEvidence.summary,
|
|
@@ -2241,7 +2265,7 @@ function createControlWorker(options) {
|
|
|
2241
2265
|
});
|
|
2242
2266
|
await appendEvent(directory, {
|
|
2243
2267
|
version: 1,
|
|
2244
|
-
eventID:
|
|
2268
|
+
eventID: randomUUID3(),
|
|
2245
2269
|
goalID: goal.id,
|
|
2246
2270
|
type: "goal.blocked",
|
|
2247
2271
|
reason: goal.blocker.reason,
|
|
@@ -2409,45 +2433,18 @@ function createControlWorker(options) {
|
|
|
2409
2433
|
|
|
2410
2434
|
// src/application/loop-engine.ts
|
|
2411
2435
|
init_state_repository();
|
|
2412
|
-
|
|
2436
|
+
init_workspace_execution();
|
|
2437
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2413
2438
|
|
|
2414
|
-
// src/domain/
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
budget_limited: ["complete", "blocked"],
|
|
2420
|
-
usage_limited: [],
|
|
2421
|
-
complete: []
|
|
2422
|
-
};
|
|
2423
|
-
var USER_TRANSITIONS = {
|
|
2424
|
-
active: ["paused"],
|
|
2425
|
-
paused: ["active"],
|
|
2426
|
-
blocked: ["active"],
|
|
2427
|
-
budget_limited: ["active"],
|
|
2428
|
-
usage_limited: ["active"],
|
|
2429
|
-
complete: ["active"]
|
|
2430
|
-
};
|
|
2431
|
-
var SYSTEM_TRANSITIONS = {
|
|
2432
|
-
active: ["budget_limited", "usage_limited"],
|
|
2433
|
-
paused: [],
|
|
2434
|
-
blocked: [],
|
|
2435
|
-
budget_limited: [],
|
|
2436
|
-
usage_limited: [],
|
|
2437
|
-
complete: []
|
|
2438
|
-
};
|
|
2439
|
-
function canTransition(current, target, caller) {
|
|
2440
|
-
const table = caller === "model" ? MODEL_TRANSITIONS : caller === "user" ? USER_TRANSITIONS : SYSTEM_TRANSITIONS;
|
|
2441
|
-
return table[current]?.includes(target) ?? false;
|
|
2442
|
-
}
|
|
2443
|
-
function isTerminal(status) {
|
|
2444
|
-
return status === "complete";
|
|
2445
|
-
}
|
|
2446
|
-
function createGoal(input) {
|
|
2447
|
-
const now = new Date().toISOString();
|
|
2448
|
-
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 };
|
|
2449
2444
|
}
|
|
2445
|
+
|
|
2450
2446
|
// src/application/loop-engine.ts
|
|
2447
|
+
init_server_log();
|
|
2451
2448
|
var CONFIRM_IDLE_DURATION_MS = 2000;
|
|
2452
2449
|
var HANDLED_EVENT_TYPES = new Set([
|
|
2453
2450
|
"session.idle",
|
|
@@ -2468,6 +2465,7 @@ function createLoopEngine(options) {
|
|
|
2468
2465
|
const idleRecoverMs = options.idleRecoverMs ?? 3 * 60000;
|
|
2469
2466
|
let running = false;
|
|
2470
2467
|
let maintenanceTimer;
|
|
2468
|
+
let maintaining = false;
|
|
2471
2469
|
let knownWorkerSessions = new Set;
|
|
2472
2470
|
let knownWorkerSessionsLoaded = false;
|
|
2473
2471
|
const inflightContinuations = new Set;
|
|
@@ -2499,8 +2497,12 @@ function createLoopEngine(options) {
|
|
|
2499
2497
|
running = true;
|
|
2500
2498
|
loadWorkerSessionsIfneeded().catch(() => {});
|
|
2501
2499
|
maintenanceTimer = setInterval(() => {
|
|
2502
|
-
if (running)
|
|
2503
|
-
|
|
2500
|
+
if (running && !maintaining) {
|
|
2501
|
+
maintaining = true;
|
|
2502
|
+
maintenance().catch(() => {}).finally(() => {
|
|
2503
|
+
maintaining = false;
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2504
2506
|
}, maintenanceMs);
|
|
2505
2507
|
}
|
|
2506
2508
|
function stop() {
|
|
@@ -2513,13 +2515,21 @@ function createLoopEngine(options) {
|
|
|
2513
2515
|
function isRunning() {
|
|
2514
2516
|
return running;
|
|
2515
2517
|
}
|
|
2516
|
-
async function continueGoal(goalID) {
|
|
2518
|
+
async function continueGoal(goalID, opts) {
|
|
2517
2519
|
if (inflightContinuations.has(goalID))
|
|
2518
2520
|
return false;
|
|
2519
2521
|
inflightContinuations.add(goalID);
|
|
2520
2522
|
try {
|
|
2521
|
-
await
|
|
2522
|
-
|
|
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";
|
|
2523
2533
|
} finally {
|
|
2524
2534
|
inflightContinuations.delete(goalID);
|
|
2525
2535
|
}
|
|
@@ -2530,6 +2540,11 @@ function createLoopEngine(options) {
|
|
|
2530
2540
|
const type = event.type;
|
|
2531
2541
|
if (!type || !HANDLED_EVENT_TYPES.has(type))
|
|
2532
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
|
+
}
|
|
2533
2548
|
const sessionID = eventSessionID(event);
|
|
2534
2549
|
if (!sessionID)
|
|
2535
2550
|
return false;
|
|
@@ -2573,8 +2588,9 @@ function createLoopEngine(options) {
|
|
|
2573
2588
|
async function handleMessageActivity(goal, event) {
|
|
2574
2589
|
let matched = false;
|
|
2575
2590
|
await mutateState(directory, `message-activity:${goal.id}`, async (s) => {
|
|
2591
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
2576
2592
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2577
|
-
if (!rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2593
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2578
2594
|
return s;
|
|
2579
2595
|
if (event.type === "message.updated") {
|
|
2580
2596
|
const info = event.properties?.info;
|
|
@@ -2584,6 +2600,7 @@ function createLoopEngine(options) {
|
|
|
2584
2600
|
matched = true;
|
|
2585
2601
|
} else if (info?.role === "assistant" && info.parentID === rt.activePromptMessageID) {
|
|
2586
2602
|
Object.assign(rt, recordActivity(rt));
|
|
2603
|
+
rt.activePromptObservedAt ??= new Date().toISOString();
|
|
2587
2604
|
rt.activeAssistantMessageID = info.id;
|
|
2588
2605
|
if (info.time?.completed) {
|
|
2589
2606
|
rt.activeAssistantCompletedAt = new Date(info.time.completed).toISOString();
|
|
@@ -2606,8 +2623,9 @@ function createLoopEngine(options) {
|
|
|
2606
2623
|
async function handleExecutionSucceeded(goal) {
|
|
2607
2624
|
let matched = false;
|
|
2608
2625
|
await mutateState(directory, `execution-succeeded:${goal.id}`, async (s) => {
|
|
2626
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
2609
2627
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2610
|
-
if (!rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2628
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2611
2629
|
return s;
|
|
2612
2630
|
Object.assign(rt, recordActivity(rt));
|
|
2613
2631
|
rt.activeAssistantCompletedAt = new Date().toISOString();
|
|
@@ -2626,7 +2644,7 @@ function createLoopEngine(options) {
|
|
|
2626
2644
|
const g = s.goals.find((item) => item.id === goalID);
|
|
2627
2645
|
if (!g)
|
|
2628
2646
|
return s;
|
|
2629
|
-
if (
|
|
2647
|
+
if (g.status !== "active" || g.interactive || g.workerSessionID !== goal.workerSessionID)
|
|
2630
2648
|
return s;
|
|
2631
2649
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2632
2650
|
if (!rt)
|
|
@@ -2691,7 +2709,7 @@ function createLoopEngine(options) {
|
|
|
2691
2709
|
if (newlyStamped) {
|
|
2692
2710
|
await appendEvent(directory, {
|
|
2693
2711
|
version: 1,
|
|
2694
|
-
eventID:
|
|
2712
|
+
eventID: randomUUID5(),
|
|
2695
2713
|
goalID,
|
|
2696
2714
|
type: "idle.confirm-failed",
|
|
2697
2715
|
reason: failureReason,
|
|
@@ -2705,7 +2723,7 @@ function createLoopEngine(options) {
|
|
|
2705
2723
|
afterIdle = await mutateState(directory, `idle.confirm:${goalID}`, async (s) => {
|
|
2706
2724
|
const g = s.goals.find((item) => item.id === goalID);
|
|
2707
2725
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2708
|
-
if (!g || !rt ||
|
|
2726
|
+
if (!g || !rt || g.status !== "active" || g.interactive || g.workerSessionID !== goal.workerSessionID)
|
|
2709
2727
|
return s;
|
|
2710
2728
|
if (rt.phase !== "running")
|
|
2711
2729
|
return s;
|
|
@@ -2736,7 +2754,7 @@ function createLoopEngine(options) {
|
|
|
2736
2754
|
if (completedRunID) {
|
|
2737
2755
|
await appendEvent(directory, {
|
|
2738
2756
|
version: 1,
|
|
2739
|
-
eventID:
|
|
2757
|
+
eventID: randomUUID5(),
|
|
2740
2758
|
goalID,
|
|
2741
2759
|
type: "run.completed",
|
|
2742
2760
|
runID: completedRunID,
|
|
@@ -2756,13 +2774,7 @@ function createLoopEngine(options) {
|
|
|
2756
2774
|
const limitResult = enforceLimits(freshGoal, freshRuntime);
|
|
2757
2775
|
if (limitResult.stop === "force_finish") {
|
|
2758
2776
|
if (!freshRuntime.forceFinishRequested) {
|
|
2759
|
-
await
|
|
2760
|
-
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2761
|
-
if (rt)
|
|
2762
|
-
rt.forceFinishRequested = true;
|
|
2763
|
-
return s;
|
|
2764
|
-
});
|
|
2765
|
-
await goalService.continueTurn(directory, goalID, { forceFinish: true });
|
|
2777
|
+
await continueGoal(goalID, { forceFinish: true });
|
|
2766
2778
|
return true;
|
|
2767
2779
|
}
|
|
2768
2780
|
const blockedKey = goalID;
|
|
@@ -2801,7 +2813,7 @@ function createLoopEngine(options) {
|
|
|
2801
2813
|
});
|
|
2802
2814
|
await appendEvent(directory, {
|
|
2803
2815
|
version: 1,
|
|
2804
|
-
eventID:
|
|
2816
|
+
eventID: randomUUID5(),
|
|
2805
2817
|
goalID,
|
|
2806
2818
|
type: "goal.blocked",
|
|
2807
2819
|
reason: limitResult.reason + " (force-finish ignored)",
|
|
@@ -2825,7 +2837,7 @@ function createLoopEngine(options) {
|
|
|
2825
2837
|
});
|
|
2826
2838
|
await appendEvent(directory, {
|
|
2827
2839
|
version: 1,
|
|
2828
|
-
eventID:
|
|
2840
|
+
eventID: randomUUID5(),
|
|
2829
2841
|
goalID,
|
|
2830
2842
|
type: "goal.status_changed",
|
|
2831
2843
|
from: "active",
|
|
@@ -2873,8 +2885,9 @@ function createLoopEngine(options) {
|
|
|
2873
2885
|
if (statusType === "idle")
|
|
2874
2886
|
return handleSessionIdle(state, goal);
|
|
2875
2887
|
await mutateState(directory, `status:${goal.id}`, async (s) => {
|
|
2888
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
2876
2889
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2877
|
-
if (!rt)
|
|
2890
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.runGeneration !== runtime.runGeneration)
|
|
2878
2891
|
return s;
|
|
2879
2892
|
if (statusType === "busy" || statusType === "retry") {
|
|
2880
2893
|
Object.assign(rt, recordActivity(rt));
|
|
@@ -2895,7 +2908,7 @@ function createLoopEngine(options) {
|
|
|
2895
2908
|
const newState = await mutateState(directory, `error:${goal.id}`, async (s) => {
|
|
2896
2909
|
const g = s.goals.find((item) => item.id === goal.id);
|
|
2897
2910
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2898
|
-
if (!rt)
|
|
2911
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.runGeneration !== runtime.runGeneration)
|
|
2899
2912
|
return s;
|
|
2900
2913
|
rt.consecutiveFailures += 1;
|
|
2901
2914
|
rt.lastError = message;
|
|
@@ -2927,7 +2940,7 @@ function createLoopEngine(options) {
|
|
|
2927
2940
|
});
|
|
2928
2941
|
await appendEvent(directory, {
|
|
2929
2942
|
version: 1,
|
|
2930
|
-
eventID:
|
|
2943
|
+
eventID: randomUUID5(),
|
|
2931
2944
|
goalID: goal.id,
|
|
2932
2945
|
type: "run.failed",
|
|
2933
2946
|
runID: runtime.activeRunID || "unknown",
|
|
@@ -2941,7 +2954,7 @@ function createLoopEngine(options) {
|
|
|
2941
2954
|
if (updatedGoal?.status === "blocked") {
|
|
2942
2955
|
await appendEvent(directory, {
|
|
2943
2956
|
version: 1,
|
|
2944
|
-
eventID:
|
|
2957
|
+
eventID: randomUUID5(),
|
|
2945
2958
|
goalID: goal.id,
|
|
2946
2959
|
type: "goal.blocked",
|
|
2947
2960
|
reason: `Failed ${runtime.consecutiveFailures + 1} times`,
|
|
@@ -2969,7 +2982,7 @@ function createLoopEngine(options) {
|
|
|
2969
2982
|
});
|
|
2970
2983
|
await appendEvent(directory, {
|
|
2971
2984
|
version: 1,
|
|
2972
|
-
eventID:
|
|
2985
|
+
eventID: randomUUID5(),
|
|
2973
2986
|
goalID: goal.id,
|
|
2974
2987
|
type: "compaction.completed",
|
|
2975
2988
|
timestamp: new Date().toISOString(),
|
|
@@ -3039,7 +3052,7 @@ function createLoopEngine(options) {
|
|
|
3039
3052
|
});
|
|
3040
3053
|
await appendEvent(directory, {
|
|
3041
3054
|
version: 1,
|
|
3042
|
-
eventID:
|
|
3055
|
+
eventID: randomUUID5(),
|
|
3043
3056
|
goalID: goal.id,
|
|
3044
3057
|
type: "compaction.started",
|
|
3045
3058
|
timestamp: new Date().toISOString(),
|
|
@@ -3097,7 +3110,7 @@ function createLoopEngine(options) {
|
|
|
3097
3110
|
return false;
|
|
3098
3111
|
await appendEvent(directory, {
|
|
3099
3112
|
version: 1,
|
|
3100
|
-
eventID:
|
|
3113
|
+
eventID: randomUUID5(),
|
|
3101
3114
|
goalID: goal.id,
|
|
3102
3115
|
type: "goal.status_changed",
|
|
3103
3116
|
from: "active",
|
|
@@ -3121,6 +3134,7 @@ function createLoopEngine(options) {
|
|
|
3121
3134
|
}
|
|
3122
3135
|
async function sweepStaleWorkspaceOwnership() {
|
|
3123
3136
|
try {
|
|
3137
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
3124
3138
|
const snapshot = await readState(directory);
|
|
3125
3139
|
if (!snapshot.workspaceCalls?.length && !snapshot.goals.some((g) => g.scopeClosing || g.scopeClearPending)) {
|
|
3126
3140
|
return;
|
|
@@ -3198,7 +3212,7 @@ function createLoopEngine(options) {
|
|
|
3198
3212
|
}
|
|
3199
3213
|
return s;
|
|
3200
3214
|
});
|
|
3201
|
-
|
|
3215
|
+
await continueGoal(goal.id);
|
|
3202
3216
|
}
|
|
3203
3217
|
}
|
|
3204
3218
|
if (!manual && (runtime.phase === "running" || runtime.phase === "idle") && goal.workerSessionID) {
|
|
@@ -3214,22 +3228,6 @@ function createLoopEngine(options) {
|
|
|
3214
3228
|
});
|
|
3215
3229
|
await logServerEvent(directory, "maintenance.stale-run-cleared", { goalID: goal.id });
|
|
3216
3230
|
}
|
|
3217
|
-
if ((runtime.activeToolCallIDs?.length ?? 0) > 0 && runtime.lastActivityAt) {
|
|
3218
|
-
const age = Date.now() - Date.parse(runtime.lastActivityAt);
|
|
3219
|
-
if (age > 30000) {
|
|
3220
|
-
await mutateState(directory, `maintenance.toolcall-ttl:${goal.id}`, async (s) => {
|
|
3221
|
-
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
3222
|
-
if (rt && (rt.activeToolCallIDs?.length ?? 0) > 0) {
|
|
3223
|
-
rt.activeToolCallIDs = [];
|
|
3224
|
-
rt.idleCandidateAt = undefined;
|
|
3225
|
-
rt.idleCandidateGeneration = undefined;
|
|
3226
|
-
rt.updatedAt = new Date().toISOString();
|
|
3227
|
-
}
|
|
3228
|
-
return s;
|
|
3229
|
-
});
|
|
3230
|
-
await logServerEvent(directory, "maintenance.toolcall-ttl-cleared", { goalID: goal.id, age });
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
3231
|
const status = await host.sessionStatus(goal.workerSessionID);
|
|
3234
3232
|
if (status === "unknown") {
|
|
3235
3233
|
let shouldNotify = false;
|
|
@@ -3283,10 +3281,14 @@ function createLoopEngine(options) {
|
|
|
3283
3281
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
3284
3282
|
if (!g || !rt || g.status !== "active")
|
|
3285
3283
|
return s;
|
|
3284
|
+
if (g.interactive || g.scopeClosing || g.workerSessionID !== goal.workerSessionID)
|
|
3285
|
+
return s;
|
|
3286
3286
|
if (rt.phase !== "running")
|
|
3287
3287
|
return s;
|
|
3288
3288
|
if (rt.runGeneration !== generation)
|
|
3289
3289
|
return s;
|
|
3290
|
+
if (s.workspaceOperation || (s.workspaceCalls ?? []).some((call) => call.goalID === goal.id) || rt.activeToolCallIDs?.length)
|
|
3291
|
+
return s;
|
|
3290
3292
|
clearedToolCalls = rt.activeToolCallIDs?.length ?? 0;
|
|
3291
3293
|
Object.assign(rt, releaseLease(rt));
|
|
3292
3294
|
rt.activeRunID = undefined;
|
|
@@ -3300,7 +3302,7 @@ function createLoopEngine(options) {
|
|
|
3300
3302
|
if (recovered) {
|
|
3301
3303
|
await appendEvent(directory, {
|
|
3302
3304
|
version: 1,
|
|
3303
|
-
eventID:
|
|
3305
|
+
eventID: randomUUID5(),
|
|
3304
3306
|
goalID: goal.id,
|
|
3305
3307
|
type: "run.recovered",
|
|
3306
3308
|
runID: stalledRunID ?? "unknown",
|
|
@@ -3334,7 +3336,7 @@ function createLoopEngine(options) {
|
|
|
3334
3336
|
const stuckSeconds = Math.floor((Date.now() - Date.parse(failedAt)) / 1000);
|
|
3335
3337
|
await appendEvent(directory, {
|
|
3336
3338
|
version: 1,
|
|
3337
|
-
eventID:
|
|
3339
|
+
eventID: randomUUID5(),
|
|
3338
3340
|
goalID: goal.id,
|
|
3339
3341
|
type: "run.stuck",
|
|
3340
3342
|
runID: runtime.activeRunID ?? "unknown",
|
|
@@ -3374,7 +3376,7 @@ function createLoopEngine(options) {
|
|
|
3374
3376
|
});
|
|
3375
3377
|
await appendEvent(directory, {
|
|
3376
3378
|
version: 1,
|
|
3377
|
-
eventID:
|
|
3379
|
+
eventID: randomUUID5(),
|
|
3378
3380
|
goalID: goal.id,
|
|
3379
3381
|
type: "run.stuck",
|
|
3380
3382
|
runID: runtime.activeRunID,
|
|
@@ -3397,14 +3399,54 @@ function createLoopEngine(options) {
|
|
|
3397
3399
|
}
|
|
3398
3400
|
|
|
3399
3401
|
// src/application/goal-service.ts
|
|
3400
|
-
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
|
|
3401
3442
|
init_state_repository();
|
|
3402
3443
|
init_command_await2();
|
|
3403
3444
|
import * as path4 from "path";
|
|
3404
3445
|
import { promises as fs4 } from "fs";
|
|
3405
3446
|
|
|
3406
3447
|
// src/server/host-adapter.ts
|
|
3407
|
-
|
|
3448
|
+
init_server_log();
|
|
3449
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3408
3450
|
|
|
3409
3451
|
// src/v2/native-rpc.ts
|
|
3410
3452
|
var NATIVE_RPC_ID = "loopd.native";
|
|
@@ -3630,7 +3672,7 @@ function parseModelRef(value) {
|
|
|
3630
3672
|
return { providerID, modelID };
|
|
3631
3673
|
}
|
|
3632
3674
|
function newPromptMessageID() {
|
|
3633
|
-
return `msg_${
|
|
3675
|
+
return `msg_${randomUUID6()}`;
|
|
3634
3676
|
}
|
|
3635
3677
|
function isV2PromptMessageID(messageID) {
|
|
3636
3678
|
return messageID.startsWith("msg_");
|
|
@@ -3825,6 +3867,19 @@ function createRealHost(client, directory) {
|
|
|
3825
3867
|
return [];
|
|
3826
3868
|
}
|
|
3827
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
|
+
},
|
|
3828
3883
|
async compactSession(sessionID, model) {
|
|
3829
3884
|
try {
|
|
3830
3885
|
const identity = model ?? (await this.readSession(sessionID))?.model;
|
|
@@ -4298,6 +4353,7 @@ function buildContinuationSteering(goal, runtime, context) {
|
|
|
4298
4353
|
}
|
|
4299
4354
|
|
|
4300
4355
|
// src/application/goal-service.ts
|
|
4356
|
+
init_server_log();
|
|
4301
4357
|
init_workspace_execution();
|
|
4302
4358
|
|
|
4303
4359
|
class GoalStartError extends Error {
|
|
@@ -4312,6 +4368,14 @@ class GoalStartError extends Error {
|
|
|
4312
4368
|
this.failedStage = info.failedStage;
|
|
4313
4369
|
}
|
|
4314
4370
|
}
|
|
4371
|
+
|
|
4372
|
+
class GoalDeliveryBlockedError extends Error {
|
|
4373
|
+
result;
|
|
4374
|
+
constructor(result) {
|
|
4375
|
+
super(result.message);
|
|
4376
|
+
this.result = result;
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4315
4379
|
function createGoalService(host) {
|
|
4316
4380
|
const workers = createWorkerManager(host);
|
|
4317
4381
|
const sessions = new Map;
|
|
@@ -4851,7 +4915,7 @@ function createGoalService(host) {
|
|
|
4851
4915
|
}
|
|
4852
4916
|
await appendEvent(directory, {
|
|
4853
4917
|
version: 1,
|
|
4854
|
-
eventID:
|
|
4918
|
+
eventID: randomUUID7(),
|
|
4855
4919
|
goalID,
|
|
4856
4920
|
type: "run.failed",
|
|
4857
4921
|
runID,
|
|
@@ -4863,7 +4927,7 @@ function createGoalService(host) {
|
|
|
4863
4927
|
if (blocked) {
|
|
4864
4928
|
await appendEvent(directory, {
|
|
4865
4929
|
version: 1,
|
|
4866
|
-
eventID:
|
|
4930
|
+
eventID: randomUUID7(),
|
|
4867
4931
|
goalID,
|
|
4868
4932
|
type: "goal.blocked",
|
|
4869
4933
|
reason: `Worker prompt delivery failed: ${detail}`,
|
|
@@ -4900,13 +4964,67 @@ function createGoalService(host) {
|
|
|
4900
4964
|
}
|
|
4901
4965
|
}
|
|
4902
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;
|
|
4903
4991
|
try {
|
|
4904
4992
|
await dispatch();
|
|
4993
|
+
observed = await wasPromptAccepted(directory, workerSessionID, run.activePromptMessageID);
|
|
4905
4994
|
} catch (error) {
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
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 ?? []);
|
|
4910
5028
|
}
|
|
4911
5029
|
}
|
|
4912
5030
|
async function ensureWorkerSession(directory, goal) {
|
|
@@ -4923,22 +5041,36 @@ function createGoalService(host) {
|
|
|
4923
5041
|
return session;
|
|
4924
5042
|
}
|
|
4925
5043
|
session = await workers.createWorker(goal);
|
|
4926
|
-
|
|
5044
|
+
let linked = false;
|
|
5045
|
+
let failure;
|
|
4927
5046
|
await mutateState(directory, `goal.set-worker:${goal.id}`, async (s) => {
|
|
4928
5047
|
const persisted = s.goals.find((item) => item.id === goal.id);
|
|
4929
|
-
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 {
|
|
4930
5053
|
persisted.workerSessionID = session.workerSessionID;
|
|
5054
|
+
linked = true;
|
|
4931
5055
|
if (session.topology) {
|
|
4932
5056
|
persisted.workerTopology = session.topology;
|
|
4933
5057
|
persisted.nativeParentID = session.nativeParentID;
|
|
4934
5058
|
}
|
|
4935
5059
|
}
|
|
5060
|
+
if (!linked)
|
|
5061
|
+
retireWorker(s, { ...goal, workerSessionID: session.workerSessionID });
|
|
4936
5062
|
return s;
|
|
4937
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);
|
|
4938
5070
|
return session;
|
|
4939
5071
|
}
|
|
4940
5072
|
async function start(directory, input) {
|
|
4941
|
-
const id =
|
|
5073
|
+
const id = randomUUID7();
|
|
4942
5074
|
return withGoalOperation(id, () => startUnlocked(directory, input, id));
|
|
4943
5075
|
}
|
|
4944
5076
|
async function startUnlocked(directory, input, id) {
|
|
@@ -5045,7 +5177,7 @@ function createGoalService(host) {
|
|
|
5045
5177
|
});
|
|
5046
5178
|
await appendEvent(directory, {
|
|
5047
5179
|
version: 1,
|
|
5048
|
-
eventID:
|
|
5180
|
+
eventID: randomUUID7(),
|
|
5049
5181
|
goalID: id,
|
|
5050
5182
|
type: "goal.blocked",
|
|
5051
5183
|
reason: detail,
|
|
@@ -5072,7 +5204,7 @@ function createGoalService(host) {
|
|
|
5072
5204
|
const rt = state.runtimes.find((item) => item.goalID === id);
|
|
5073
5205
|
if (rt) {
|
|
5074
5206
|
Object.assign(rt, acquireLease(rt, g.config.timeoutMs || 300000));
|
|
5075
|
-
rt.activeRunID =
|
|
5207
|
+
rt.activeRunID = randomUUID7();
|
|
5076
5208
|
rt.activePromptMessageID = newPromptMessageID();
|
|
5077
5209
|
rt.runCount = 1;
|
|
5078
5210
|
rt.budgetTurnCount = 1;
|
|
@@ -5083,7 +5215,7 @@ function createGoalService(host) {
|
|
|
5083
5215
|
runtime = state2.runtimes.find((r) => r.goalID === id);
|
|
5084
5216
|
await appendEvent(directory, {
|
|
5085
5217
|
version: 1,
|
|
5086
|
-
eventID:
|
|
5218
|
+
eventID: randomUUID7(),
|
|
5087
5219
|
goalID: id,
|
|
5088
5220
|
type: "goal.created",
|
|
5089
5221
|
name: input.name,
|
|
@@ -5095,7 +5227,7 @@ function createGoalService(host) {
|
|
|
5095
5227
|
if (runtime) {
|
|
5096
5228
|
await appendEvent(directory, {
|
|
5097
5229
|
version: 1,
|
|
5098
|
-
eventID:
|
|
5230
|
+
eventID: randomUUID7(),
|
|
5099
5231
|
goalID: id,
|
|
5100
5232
|
type: "run.started",
|
|
5101
5233
|
runID: runtime.activeRunID,
|
|
@@ -5172,23 +5304,48 @@ function createGoalService(host) {
|
|
|
5172
5304
|
}
|
|
5173
5305
|
return accountTailUsage(directory, goalID, runtime, tail);
|
|
5174
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
|
+
}
|
|
5175
5333
|
async function continueTurnUnlocked(directory, goalID, opts) {
|
|
5334
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5176
5335
|
const preState = await readState(directory);
|
|
5177
5336
|
const goal = preState.goals.find((g) => g.id === goalID);
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
const now = Date.now();
|
|
5184
|
-
if (goal.scopeClosing || (preState.workspaceCalls ?? []).some((call) => call.goalID === goalID && !isSyncStaleWorkspaceCall(preState, call, now)) || preState.workspaceOperation)
|
|
5185
|
-
return;
|
|
5186
|
-
}
|
|
5337
|
+
const guard = continuationGuard(preState, goalID);
|
|
5338
|
+
if (guard)
|
|
5339
|
+
return guard;
|
|
5340
|
+
if (!goal)
|
|
5341
|
+
return rejected(goalID, "not_found", "Goal not found.");
|
|
5187
5342
|
const runtime = preState.runtimes.find((r) => r.goalID === goalID);
|
|
5188
5343
|
if (!runtime)
|
|
5189
|
-
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 ?? []);
|
|
5190
5347
|
if (runtime.phase === "running" && leaseIsValid(runtime))
|
|
5191
|
-
return;
|
|
5348
|
+
return rejected(goalID, "blocked", "An existing turn holds the run lease; no additional prompt was sent.");
|
|
5192
5349
|
let session = sessions.get(goalID);
|
|
5193
5350
|
if (session && session.workerSessionID !== goal.workerSessionID) {
|
|
5194
5351
|
sessions.delete(goalID);
|
|
@@ -5205,18 +5362,22 @@ function createGoalService(host) {
|
|
|
5205
5362
|
if (!session && !goal.workerSessionID)
|
|
5206
5363
|
session = await ensureWorkerSession(directory, goal);
|
|
5207
5364
|
if (!session)
|
|
5208
|
-
return;
|
|
5365
|
+
return rejected(goalID, "blocked", "Worker session is missing; resume or retry the goal.");
|
|
5209
5366
|
if (!opts?.force && !await workers.isIdle(session.workerSessionID))
|
|
5210
|
-
return;
|
|
5367
|
+
return rejected(goalID, "blocked", "Worker is busy or its status is unknown; no prompt was sent. Retry after confirmed idle.");
|
|
5211
5368
|
if (goal.pendingIdentity || goal.modelSwitch?.pending || goal.agentSwitch?.pending) {
|
|
5212
5369
|
if (await host.sessionStatus(session.workerSessionID) !== "idle")
|
|
5213
|
-
return;
|
|
5370
|
+
return rejected(goalID, "blocked", "Pending identity requires an idle worker; no prompt was sent. Wait for idle and retry.");
|
|
5214
5371
|
await applyPendingIdentity(directory, goal);
|
|
5215
5372
|
await applyPendingModel(directory, goal);
|
|
5216
5373
|
await applyPendingAgent(directory, goal);
|
|
5217
5374
|
}
|
|
5218
5375
|
let acquired = false;
|
|
5376
|
+
let acquisitionFailure;
|
|
5219
5377
|
const state = await mutateState(directory, `turn.acquire:${goalID}`, async (s) => {
|
|
5378
|
+
acquisitionFailure = continuationGuard(s, goalID);
|
|
5379
|
+
if (acquisitionFailure)
|
|
5380
|
+
return s;
|
|
5220
5381
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5221
5382
|
if (!g || g.status !== "active")
|
|
5222
5383
|
return s;
|
|
@@ -5227,7 +5388,7 @@ function createGoalService(host) {
|
|
|
5227
5388
|
return s;
|
|
5228
5389
|
const timeoutMs = g.config.timeoutMs || 300000;
|
|
5229
5390
|
Object.assign(rt, acquireLease(rt, timeoutMs));
|
|
5230
|
-
rt.activeRunID =
|
|
5391
|
+
rt.activeRunID = randomUUID7();
|
|
5231
5392
|
rt.activePromptMessageID = newPromptMessageID();
|
|
5232
5393
|
rt.runCount += 1;
|
|
5233
5394
|
if (rt.freeRetryPending) {
|
|
@@ -5242,70 +5403,97 @@ function createGoalService(host) {
|
|
|
5242
5403
|
const freshGoal = state.goals.find((g) => g.id === goalID);
|
|
5243
5404
|
const freshRuntime = state.runtimes.find((r) => r.goalID === goalID);
|
|
5244
5405
|
if (!acquired || !freshGoal || freshGoal.status !== "active" || !freshRuntime?.activeRunID)
|
|
5245
|
-
return;
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
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",
|
|
5249
5410
|
goalID,
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
if (opts?.bare) {
|
|
5257
|
-
const bareWords = await drainGoalInbox(directory, goalID);
|
|
5258
|
-
const bareText = bareWords.join(`
|
|
5259
|
-
`).trim();
|
|
5260
|
-
if (bareText) {
|
|
5261
|
-
await dispatchOrRecordFailure(directory, goalID, () => workers.sendBare(session, freshGoal, freshRuntime, bareText), freshRuntime, session.workerSessionID);
|
|
5262
|
-
return;
|
|
5263
|
-
}
|
|
5264
|
-
}
|
|
5265
|
-
const inboxMessages = await drainGoalInbox(directory, goalID);
|
|
5266
|
-
const allEvents = await readEvents(directory, 200);
|
|
5267
|
-
const progressHistory = allEvents.filter((e) => e.goalID === goalID && e.type === "goal.progress").map((e) => ({
|
|
5268
|
-
summary: String(e.summary || ""),
|
|
5269
|
-
next: e.next ? String(e.next) : undefined,
|
|
5270
|
-
at: String(e.timestamp || "")
|
|
5271
|
-
}));
|
|
5272
|
-
let transcriptTail;
|
|
5273
|
-
try {
|
|
5274
|
-
transcriptTail = await host.readMessages(freshGoal.workerSessionID, 5);
|
|
5275
|
-
} catch {
|
|
5276
|
-
transcriptTail = [];
|
|
5277
|
-
}
|
|
5278
|
-
await accountTailUsage(directory, goalID, freshRuntime, transcriptTail ?? []);
|
|
5279
|
-
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;
|
|
5280
5417
|
try {
|
|
5281
|
-
const
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
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;
|
|
5288
5443
|
}
|
|
5289
5444
|
}
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
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 = [];
|
|
5299
5456
|
}
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
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;
|
|
5490
|
+
} catch (error) {
|
|
5491
|
+
if (error instanceof GoalDeliveryBlockedError)
|
|
5492
|
+
return error.result;
|
|
5493
|
+
if (!dispatchAttempted)
|
|
5494
|
+
await recordPromptFailure(directory, goalID, error, false, freshRuntime.activeRunID);
|
|
5495
|
+
throw error;
|
|
5496
|
+
}
|
|
5309
5497
|
}
|
|
5310
5498
|
async function setInteractiveUnlocked(directory, goalID, interactive) {
|
|
5311
5499
|
const state = await mutateState(directory, `goal.interactive:${goalID}`, async (s) => {
|
|
@@ -5324,7 +5512,7 @@ function createGoalService(host) {
|
|
|
5324
5512
|
return { ok: false, message: "Goal not found." };
|
|
5325
5513
|
await appendEvent(directory, {
|
|
5326
5514
|
version: 1,
|
|
5327
|
-
eventID:
|
|
5515
|
+
eventID: randomUUID7(),
|
|
5328
5516
|
goalID,
|
|
5329
5517
|
type: "goal.interactive_changed",
|
|
5330
5518
|
interactive,
|
|
@@ -5385,7 +5573,7 @@ function createGoalService(host) {
|
|
|
5385
5573
|
await cancelAwaitsForGoal(directory, goalID, "goal paused").catch(() => {});
|
|
5386
5574
|
await appendEvent(directory, {
|
|
5387
5575
|
version: 1,
|
|
5388
|
-
eventID:
|
|
5576
|
+
eventID: randomUUID7(),
|
|
5389
5577
|
goalID,
|
|
5390
5578
|
type: "goal.status_changed",
|
|
5391
5579
|
from: "active",
|
|
@@ -5421,6 +5609,7 @@ function createGoalService(host) {
|
|
|
5421
5609
|
}
|
|
5422
5610
|
}
|
|
5423
5611
|
async function resumeUnlocked(directory, goalID) {
|
|
5612
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5424
5613
|
const preState = await readState(directory);
|
|
5425
5614
|
const preGoal = preState.goals.find((g) => g.id === goalID);
|
|
5426
5615
|
const workerLiveness = await probeWorkerLiveness(preGoal?.workerSessionID);
|
|
@@ -5435,10 +5624,10 @@ function createGoalService(host) {
|
|
|
5435
5624
|
sweepStaleWorkspaceState(state, liveness);
|
|
5436
5625
|
const rt = state.runtimes.find((r) => r.goalID === goalID);
|
|
5437
5626
|
const liveCalls = (state.workspaceCalls ?? []).filter((call) => call.goalID === goalID && !isStaleWorkspaceCall(state, call, liveness));
|
|
5438
|
-
const blocking = liveCalls
|
|
5627
|
+
const blocking = liveCalls;
|
|
5439
5628
|
if (blocking.length) {
|
|
5440
5629
|
if (workerLiveness === "unknown") {
|
|
5441
|
-
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
|
|
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.`);
|
|
5442
5631
|
}
|
|
5443
5632
|
throw new Error("Goal still has closing/in-flight writes; ownership cannot be reacquired before quiescence.");
|
|
5444
5633
|
}
|
|
@@ -5453,7 +5642,6 @@ function createGoalService(host) {
|
|
|
5453
5642
|
}
|
|
5454
5643
|
goal.scopeClosing = false;
|
|
5455
5644
|
goal.scopeClearPending = false;
|
|
5456
|
-
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => call.goalID !== goalID);
|
|
5457
5645
|
assertWorkspaceWriteAvailable(state, goal, goal.ownerSessionID);
|
|
5458
5646
|
goal.status = "active";
|
|
5459
5647
|
goal.updatedAt = new Date().toISOString();
|
|
@@ -5461,11 +5649,15 @@ function createGoalService(host) {
|
|
|
5461
5649
|
return state;
|
|
5462
5650
|
});
|
|
5463
5651
|
const goal = state.goals.find((g) => g.id === goalID);
|
|
5464
|
-
if (!goal
|
|
5465
|
-
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.`);
|
|
5466
5656
|
try {
|
|
5467
5657
|
await ensureWorkerSession(directory, goal);
|
|
5468
5658
|
} catch (error) {
|
|
5659
|
+
if (error instanceof GoalDeliveryBlockedError)
|
|
5660
|
+
return error.result;
|
|
5469
5661
|
await mutateState(directory, `goal.resume-rollback:${goalID}`, async (s) => {
|
|
5470
5662
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5471
5663
|
if (g?.status === "active")
|
|
@@ -5476,7 +5668,7 @@ function createGoalService(host) {
|
|
|
5476
5668
|
}
|
|
5477
5669
|
await appendEvent(directory, {
|
|
5478
5670
|
version: 1,
|
|
5479
|
-
eventID:
|
|
5671
|
+
eventID: randomUUID7(),
|
|
5480
5672
|
goalID,
|
|
5481
5673
|
type: "goal.status_changed",
|
|
5482
5674
|
from: "paused",
|
|
@@ -5484,9 +5676,11 @@ function createGoalService(host) {
|
|
|
5484
5676
|
timestamp: new Date().toISOString(),
|
|
5485
5677
|
revision: state.revision
|
|
5486
5678
|
});
|
|
5487
|
-
await continueTurnUnlocked(directory, goalID);
|
|
5679
|
+
const result = await continueTurnUnlocked(directory, goalID);
|
|
5680
|
+
return result.ok ? { ...result, message: `Goal "${goal.name}" resumed; ${result.message}` } : result;
|
|
5488
5681
|
}
|
|
5489
5682
|
async function retryUnlocked(directory, goalID) {
|
|
5683
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5490
5684
|
const preState = await readState(directory);
|
|
5491
5685
|
const workerLiveness = await probeWorkerLiveness(preState.goals.find((g) => g.id === goalID)?.workerSessionID);
|
|
5492
5686
|
let retried = false;
|
|
@@ -5496,11 +5690,10 @@ function createGoalService(host) {
|
|
|
5496
5690
|
return state;
|
|
5497
5691
|
const liveness = { isWorkerLive: (_gid, _wsid) => _gid === goalID ? workerLiveness !== "dead" : false, now: Date.now() };
|
|
5498
5692
|
sweepStaleWorkspaceState(state, liveness);
|
|
5499
|
-
const
|
|
5500
|
-
const blocking = (state.workspaceCalls ?? []).filter((call) => call.goalID === goalID && !isStaleWorkspaceCall(state, call, liveness) && (call.generation === undefined || !rt0 || call.generation >= rt0.runGeneration));
|
|
5693
|
+
const blocking = (state.workspaceCalls ?? []).filter((call) => call.goalID === goalID && !isStaleWorkspaceCall(state, call, liveness));
|
|
5501
5694
|
if (blocking.length) {
|
|
5502
5695
|
if (workerLiveness === "unknown") {
|
|
5503
|
-
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
|
|
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.`);
|
|
5504
5697
|
}
|
|
5505
5698
|
throw new Error("Goal still has closing/in-flight writes; ownership cannot be reacquired before quiescence.");
|
|
5506
5699
|
}
|
|
@@ -5538,11 +5731,15 @@ function createGoalService(host) {
|
|
|
5538
5731
|
return state;
|
|
5539
5732
|
});
|
|
5540
5733
|
const goal = state.goals.find((g) => g.id === goalID);
|
|
5541
|
-
if (!goal
|
|
5542
|
-
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.`);
|
|
5543
5738
|
try {
|
|
5544
5739
|
await ensureWorkerSession(directory, goal);
|
|
5545
5740
|
} catch (error) {
|
|
5741
|
+
if (error instanceof GoalDeliveryBlockedError)
|
|
5742
|
+
return error.result;
|
|
5546
5743
|
await mutateState(directory, `goal.retry-rollback:${goalID}`, async (s) => {
|
|
5547
5744
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5548
5745
|
if (g?.status === "active")
|
|
@@ -5553,7 +5750,7 @@ function createGoalService(host) {
|
|
|
5553
5750
|
}
|
|
5554
5751
|
await appendEvent(directory, {
|
|
5555
5752
|
version: 1,
|
|
5556
|
-
eventID:
|
|
5753
|
+
eventID: randomUUID7(),
|
|
5557
5754
|
goalID,
|
|
5558
5755
|
type: "goal.status_changed",
|
|
5559
5756
|
from: "blocked",
|
|
@@ -5561,7 +5758,7 @@ function createGoalService(host) {
|
|
|
5561
5758
|
timestamp: new Date().toISOString(),
|
|
5562
5759
|
revision: state.revision
|
|
5563
5760
|
});
|
|
5564
|
-
|
|
5761
|
+
return continueTurnUnlocked(directory, goalID);
|
|
5565
5762
|
}
|
|
5566
5763
|
async function clearUnlocked(directory, goalID) {
|
|
5567
5764
|
const pre = await readState(directory);
|
|
@@ -5570,12 +5767,10 @@ function createGoalService(host) {
|
|
|
5570
5767
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5571
5768
|
if (g)
|
|
5572
5769
|
retireWorker(s, g);
|
|
5573
|
-
s.workspaceCalls = (s.workspaceCalls ?? []).filter((call) => call.goalID !== goalID);
|
|
5574
|
-
if (s.workspaceOperation?.goalID === goalID)
|
|
5575
|
-
s.workspaceOperation = undefined;
|
|
5576
5770
|
s.goals = s.goals.filter((g) => g.id !== goalID);
|
|
5577
5771
|
s.runtimes = s.runtimes.filter((r) => r.goalID !== goalID);
|
|
5578
5772
|
s.commandAwaits = (s.commandAwaits ?? []).filter((a) => a.goalID !== goalID);
|
|
5773
|
+
await deleteGoalInbox(directory, goalID);
|
|
5579
5774
|
return s;
|
|
5580
5775
|
});
|
|
5581
5776
|
sessions.delete(goalID);
|
|
@@ -5587,7 +5782,7 @@ function createGoalService(host) {
|
|
|
5587
5782
|
await cancelAwaitsForGoal(directory, goalID, "goal cleared").catch(() => {});
|
|
5588
5783
|
await appendEvent(directory, {
|
|
5589
5784
|
version: 1,
|
|
5590
|
-
eventID:
|
|
5785
|
+
eventID: randomUUID7(),
|
|
5591
5786
|
goalID,
|
|
5592
5787
|
type: "goal.cleared",
|
|
5593
5788
|
timestamp: new Date().toISOString(),
|
|
@@ -5602,6 +5797,7 @@ function createGoalService(host) {
|
|
|
5602
5797
|
return new Map(sessions);
|
|
5603
5798
|
}
|
|
5604
5799
|
async function reconcile(directory) {
|
|
5800
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5605
5801
|
try {
|
|
5606
5802
|
let summary = { reapedCalls: [], resetClosing: [], finalizedClears: [], forceCleared: [] };
|
|
5607
5803
|
const pre = await readState(directory);
|
|
@@ -5710,22 +5906,39 @@ function createGoalService(host) {
|
|
|
5710
5906
|
}
|
|
5711
5907
|
}
|
|
5712
5908
|
async function nudgeUnlocked(directory, goalID) {
|
|
5909
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5713
5910
|
const preState = await readState(directory);
|
|
5714
5911
|
const goal = preState.goals.find((g) => g.id === goalID);
|
|
5715
5912
|
if (!goal)
|
|
5716
|
-
return
|
|
5913
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5717
5914
|
if (goal.status !== "active") {
|
|
5718
|
-
return
|
|
5915
|
+
return rejected(goalID, "invalid_state", `Goal is ${goal.status}; resume or retry it before nudging.`);
|
|
5719
5916
|
}
|
|
5720
5917
|
const quotaHold = quotaHoldMessage(goal);
|
|
5721
5918
|
if (quotaHold)
|
|
5722
|
-
return
|
|
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.");
|
|
5932
|
+
}
|
|
5723
5933
|
if (goal.config.write_scope !== undefined) {
|
|
5724
5934
|
await pauseUnlocked(directory, goalID);
|
|
5725
|
-
|
|
5726
|
-
return { ok: true, message: `Re-prompted protected worker for "${goal.name}" in a freshly fenced session.` };
|
|
5935
|
+
return resumeUnlocked(directory, goalID);
|
|
5727
5936
|
}
|
|
5937
|
+
let resetFailure;
|
|
5728
5938
|
const cleared = await mutateState(directory, `goal.nudge:${goalID}`, async (s) => {
|
|
5939
|
+
resetFailure = continuationGuard(s, goalID);
|
|
5940
|
+
if (resetFailure)
|
|
5941
|
+
return s;
|
|
5729
5942
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
5730
5943
|
if (!rt)
|
|
5731
5944
|
return s;
|
|
@@ -5737,30 +5950,46 @@ function createGoalService(host) {
|
|
|
5737
5950
|
rt.updatedAt = new Date().toISOString();
|
|
5738
5951
|
return s;
|
|
5739
5952
|
});
|
|
5953
|
+
if (resetFailure)
|
|
5954
|
+
return resetFailure;
|
|
5740
5955
|
const freshGoal = cleared.goals.find((g) => g.id === goalID);
|
|
5741
5956
|
if (!freshGoal || !freshGoal.workerSessionID) {
|
|
5742
|
-
return
|
|
5957
|
+
return rejected(goalID, "blocked", "Goal has no worker session. Use resume_goal or retry_goal.");
|
|
5743
5958
|
}
|
|
5744
|
-
|
|
5745
|
-
return { ok: true, message: `Re-prompted worker for "${freshGoal.name}".` };
|
|
5959
|
+
return continueTurnUnlocked(directory, goalID, { force: true });
|
|
5746
5960
|
}
|
|
5747
5961
|
async function sendUnlocked(directory, goalID, text) {
|
|
5748
5962
|
const trimmed = text.trim();
|
|
5749
5963
|
if (!trimmed)
|
|
5750
|
-
return
|
|
5964
|
+
return rejected(goalID, "invalid_state", "Nothing to send.");
|
|
5965
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5751
5966
|
const preState = await readState(directory);
|
|
5752
5967
|
const goal = preState.goals.find((g) => g.id === goalID);
|
|
5753
5968
|
if (!goal)
|
|
5754
|
-
return
|
|
5755
|
-
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.");
|
|
5756
5972
|
if (goal.status !== "active") {
|
|
5757
|
-
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.` };
|
|
5758
5983
|
}
|
|
5759
5984
|
if (goal.config.write_scope !== undefined) {
|
|
5760
|
-
await continueTurnUnlocked(directory, goalID, { bare: true });
|
|
5761
|
-
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}` };
|
|
5762
5987
|
}
|
|
5988
|
+
let resetFailure;
|
|
5763
5989
|
const cleared = await mutateState(directory, `goal.send:${goalID}`, async (s) => {
|
|
5990
|
+
resetFailure = continuationGuard(s, goalID);
|
|
5991
|
+
if (resetFailure)
|
|
5992
|
+
return s;
|
|
5764
5993
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
5765
5994
|
if (!rt)
|
|
5766
5995
|
return s;
|
|
@@ -5772,12 +6001,14 @@ function createGoalService(host) {
|
|
|
5772
6001
|
rt.updatedAt = new Date().toISOString();
|
|
5773
6002
|
return s;
|
|
5774
6003
|
});
|
|
6004
|
+
if (resetFailure)
|
|
6005
|
+
return resetFailure.outcome === "not_found" ? resetFailure : { ok: true, outcome: "queued", goalID, message: `Words saved in inbox. ${resetFailure.message}` };
|
|
5775
6006
|
const freshGoal = cleared.goals.find((g) => g.id === goalID);
|
|
5776
6007
|
if (!freshGoal || !freshGoal.workerSessionID) {
|
|
5777
|
-
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).` };
|
|
5778
6009
|
}
|
|
5779
|
-
await continueTurnUnlocked(directory, goalID, { force: true, bare: true });
|
|
5780
|
-
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}` };
|
|
5781
6012
|
}
|
|
5782
6013
|
function sendUserMessage(directory, goalID, text) {
|
|
5783
6014
|
return withGoalOperation(goalID, () => sendUnlocked(directory, goalID, text));
|
|
@@ -5851,8 +6082,9 @@ function createGoalService(host) {
|
|
|
5851
6082
|
|
|
5852
6083
|
// src/application/schedule-worker.ts
|
|
5853
6084
|
init_state_repository();
|
|
5854
|
-
|
|
6085
|
+
init_server_log();
|
|
5855
6086
|
init_workspace_execution();
|
|
6087
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
5856
6088
|
function createScheduleWorker(options) {
|
|
5857
6089
|
const { directory, goalService } = options;
|
|
5858
6090
|
const intervalMs = options.intervalMs ?? 5000;
|
|
@@ -5943,7 +6175,7 @@ function createScheduleWorker(options) {
|
|
|
5943
6175
|
continue;
|
|
5944
6176
|
await appendEvent(directory, {
|
|
5945
6177
|
version: 1,
|
|
5946
|
-
eventID:
|
|
6178
|
+
eventID: randomUUID8(),
|
|
5947
6179
|
goalID: goal.id,
|
|
5948
6180
|
type: "schedule.tick",
|
|
5949
6181
|
scheduleRunCount: count,
|
|
@@ -5969,7 +6201,7 @@ function createScheduleWorker(options) {
|
|
|
5969
6201
|
}
|
|
5970
6202
|
|
|
5971
6203
|
// src/v2/native-server.ts
|
|
5972
|
-
import { randomUUID as
|
|
6204
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
5973
6205
|
|
|
5974
6206
|
// src/v2/native-bridge.ts
|
|
5975
6207
|
class NativeBridgeError extends Error {
|
|
@@ -6111,7 +6343,7 @@ async function setupNativeServer(context) {
|
|
|
6111
6343
|
bridge,
|
|
6112
6344
|
requestWorker: async (input) => {
|
|
6113
6345
|
const request = {
|
|
6114
|
-
requestID: `req_${
|
|
6346
|
+
requestID: `req_${randomUUID9()}`,
|
|
6115
6347
|
goalID: input.goalID ?? "",
|
|
6116
6348
|
parentSessionID: input.parentSessionID,
|
|
6117
6349
|
title: input.title,
|
|
@@ -6380,7 +6612,7 @@ function createCommandHost(opts) {
|
|
|
6380
6612
|
}
|
|
6381
6613
|
|
|
6382
6614
|
// src/application/command-service.ts
|
|
6383
|
-
import { randomUUID as
|
|
6615
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
6384
6616
|
import { promises as fs5 } from "fs";
|
|
6385
6617
|
import path5 from "path";
|
|
6386
6618
|
|
|
@@ -6636,6 +6868,7 @@ function validateCommandStreamMessage(value) {
|
|
|
6636
6868
|
}
|
|
6637
6869
|
|
|
6638
6870
|
// src/application/command-service.ts
|
|
6871
|
+
init_server_log();
|
|
6639
6872
|
init_workspace_execution();
|
|
6640
6873
|
init_command_await2();
|
|
6641
6874
|
init_state_repository();
|
|
@@ -6745,7 +6978,7 @@ function createCommandService(host, opts) {
|
|
|
6745
6978
|
function watchLedger(directory, commandID, type, extra) {
|
|
6746
6979
|
return appendEvent(directory, {
|
|
6747
6980
|
version: 1,
|
|
6748
|
-
eventID:
|
|
6981
|
+
eventID: randomUUID10(),
|
|
6749
6982
|
commandID,
|
|
6750
6983
|
type,
|
|
6751
6984
|
timestamp: new Date().toISOString(),
|
|
@@ -7222,7 +7455,7 @@ function createCommandService(host, opts) {
|
|
|
7222
7455
|
}).catch(() => {});
|
|
7223
7456
|
await appendEvent(directory, {
|
|
7224
7457
|
version: 1,
|
|
7225
|
-
eventID:
|
|
7458
|
+
eventID: randomUUID10(),
|
|
7226
7459
|
commandID: id,
|
|
7227
7460
|
type: "command.exited",
|
|
7228
7461
|
exitCode: info.exitCode,
|
|
@@ -7267,7 +7500,7 @@ function createCommandService(host, opts) {
|
|
|
7267
7500
|
watchState = initialWatchState();
|
|
7268
7501
|
}
|
|
7269
7502
|
const startedAt = new Date;
|
|
7270
|
-
const id =
|
|
7503
|
+
const id = randomUUID10();
|
|
7271
7504
|
const cwd = input.cwd || directory;
|
|
7272
7505
|
const useShell = input.shell === true;
|
|
7273
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 };
|
|
@@ -7333,7 +7566,7 @@ function createCommandService(host, opts) {
|
|
|
7333
7566
|
rememberDir(id, directory);
|
|
7334
7567
|
await appendEvent(directory, {
|
|
7335
7568
|
version: 1,
|
|
7336
|
-
eventID:
|
|
7569
|
+
eventID: randomUUID10(),
|
|
7337
7570
|
...input.goalID ? { goalID: input.goalID } : {},
|
|
7338
7571
|
commandID: id,
|
|
7339
7572
|
type: "command.started",
|
|
@@ -7433,7 +7666,7 @@ function createCommandService(host, opts) {
|
|
|
7433
7666
|
return { session, text: file.text, totalBytes: session.outputBytes, startByte: file.startByte, live: session.status === "running" };
|
|
7434
7667
|
},
|
|
7435
7668
|
async write(directory, id, ownerSessionID, data) {
|
|
7436
|
-
const scopeCallID = `command-write:${
|
|
7669
|
+
const scopeCallID = `command-write:${randomUUID10()}`;
|
|
7437
7670
|
await beforeWorkspaceTool(directory, { tool: "loopd_command_write", sessionID: ownerSessionID, callID: scopeCallID }, {});
|
|
7438
7671
|
try {
|
|
7439
7672
|
const session = await service.get(directory, id, ownerSessionID);
|
|
@@ -7889,7 +8122,7 @@ function createCommandService(host, opts) {
|
|
|
7889
8122
|
}
|
|
7890
8123
|
|
|
7891
8124
|
// src/application/command-event-broker.ts
|
|
7892
|
-
import { randomUUID as
|
|
8125
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
7893
8126
|
function safeDeliver(sink, msg) {
|
|
7894
8127
|
try {
|
|
7895
8128
|
sink(msg);
|
|
@@ -7932,7 +8165,7 @@ function createCommandEventBroker(resolver) {
|
|
|
7932
8165
|
throw new Error("ownerSessionID is required.");
|
|
7933
8166
|
if (typeof sink !== "function")
|
|
7934
8167
|
throw new Error("sink must be a function.");
|
|
7935
|
-
const sinkID =
|
|
8168
|
+
const sinkID = randomUUID11();
|
|
7936
8169
|
const entry = { sinkID, sink, state: "subscribing", buffer: [] };
|
|
7937
8170
|
entriesFor(commandID).set(sinkID, entry);
|
|
7938
8171
|
try {
|
|
@@ -8021,9 +8254,10 @@ function createCommandEventBroker(resolver) {
|
|
|
8021
8254
|
}
|
|
8022
8255
|
|
|
8023
8256
|
// src/server/command-stream-server.ts
|
|
8024
|
-
import { randomBytes, randomUUID as
|
|
8257
|
+
import { randomBytes, randomUUID as randomUUID12, timingSafeEqual } from "crypto";
|
|
8025
8258
|
import { promises as fs6 } from "fs";
|
|
8026
8259
|
import path6 from "path";
|
|
8260
|
+
init_server_log();
|
|
8027
8261
|
function streamEndpointPath(directory) {
|
|
8028
8262
|
return path6.join(directory, ".opencode", "loopd", "commands", ".stream-endpoint.json");
|
|
8029
8263
|
}
|
|
@@ -8055,7 +8289,7 @@ var MAX_BUFFERED_BYTES = 512 * 1024;
|
|
|
8055
8289
|
function createCommandStreamServer(directory, commandService, broker) {
|
|
8056
8290
|
const endpointPath = streamEndpointPath(directory);
|
|
8057
8291
|
const token = randomBytes(32).toString("hex");
|
|
8058
|
-
const generation =
|
|
8292
|
+
const generation = randomUUID12();
|
|
8059
8293
|
const startedAt = new Date().toISOString();
|
|
8060
8294
|
let server;
|
|
8061
8295
|
let serverURL;
|
|
@@ -8375,7 +8609,7 @@ function createCommandStreamServer(directory, commandService, broker) {
|
|
|
8375
8609
|
|
|
8376
8610
|
// src/server/goal-tools.ts
|
|
8377
8611
|
init_state_repository();
|
|
8378
|
-
import { randomUUID as
|
|
8612
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
8379
8613
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
8380
8614
|
// src/domain/verification.ts
|
|
8381
8615
|
var MAX_RECENT_ATTEMPTS = 10;
|
|
@@ -8390,6 +8624,7 @@ function appendVerificationAttempt(recent, attempt) {
|
|
|
8390
8624
|
// src/server/goal-tools.ts
|
|
8391
8625
|
import { exec as execChild } from "child_process";
|
|
8392
8626
|
import { promisify } from "util";
|
|
8627
|
+
init_server_log();
|
|
8393
8628
|
|
|
8394
8629
|
// src/application/goal-scope.ts
|
|
8395
8630
|
init_state_repository();
|
|
@@ -8703,7 +8938,7 @@ function goalTools(dir, goalService, hostSessionID, defaults = {}, host) {
|
|
|
8703
8938
|
state.revision = (await persistWorkerMutation(dir, goal, runtime, ["active"])).revision;
|
|
8704
8939
|
const event = {
|
|
8705
8940
|
version: 1,
|
|
8706
|
-
eventID:
|
|
8941
|
+
eventID: randomUUID13(),
|
|
8707
8942
|
goalID: goal.id,
|
|
8708
8943
|
type: "goal.progress",
|
|
8709
8944
|
summary: args.summary,
|
|
@@ -8768,7 +9003,7 @@ Exit code: ${f.exitCode}${stdoutSnippet}${stderrSnippet}`;
|
|
|
8768
9003
|
Working directory: ${cwd}
|
|
8769
9004
|
|
|
8770
9005
|
${failureDetails}`;
|
|
8771
|
-
attemptID =
|
|
9006
|
+
attemptID = randomUUID13();
|
|
8772
9007
|
const verificationAttempt = {
|
|
8773
9008
|
id: attemptID,
|
|
8774
9009
|
sequence: runtime.evaluatorRejectionCount,
|
|
@@ -8813,7 +9048,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8813
9048
|
if (attemptID) {
|
|
8814
9049
|
await appendEvent(dir, {
|
|
8815
9050
|
version: 1,
|
|
8816
|
-
eventID:
|
|
9051
|
+
eventID: randomUUID13(),
|
|
8817
9052
|
goalID: goal.id,
|
|
8818
9053
|
type: "goal.completion_rejected",
|
|
8819
9054
|
attemptID,
|
|
@@ -8831,7 +9066,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8831
9066
|
if (blocked && rejectedGoal?.blocker) {
|
|
8832
9067
|
await appendEvent(dir, {
|
|
8833
9068
|
version: 1,
|
|
8834
|
-
eventID:
|
|
9069
|
+
eventID: randomUUID13(),
|
|
8835
9070
|
goalID: goal.id,
|
|
8836
9071
|
type: "goal.blocked",
|
|
8837
9072
|
reason: rejectedGoal.blocker.reason,
|
|
@@ -8891,7 +9126,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8891
9126
|
}
|
|
8892
9127
|
runtime.updatedAt = new Date().toISOString();
|
|
8893
9128
|
}
|
|
8894
|
-
const attemptID =
|
|
9129
|
+
const attemptID = randomUUID13();
|
|
8895
9130
|
const cwd = goal.config.checkCwd || goal.config.artifactDir || dir;
|
|
8896
9131
|
const checks = (goal.config.checks || []).map((cmd) => ({
|
|
8897
9132
|
command: cmd,
|
|
@@ -8915,7 +9150,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8915
9150
|
state.revision = (await persistWorkerMutation(dir, goal, runtime, ["active", "budget_limited"])).revision;
|
|
8916
9151
|
const event = {
|
|
8917
9152
|
version: 1,
|
|
8918
|
-
eventID:
|
|
9153
|
+
eventID: randomUUID13(),
|
|
8919
9154
|
goalID: goal.id,
|
|
8920
9155
|
type: "goal.completed",
|
|
8921
9156
|
summary: args.summary,
|
|
@@ -8975,7 +9210,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8975
9210
|
state.revision = (await persistWorkerMutation(dir, goal, runtime, ["active", "budget_limited"])).revision;
|
|
8976
9211
|
const event = {
|
|
8977
9212
|
version: 1,
|
|
8978
|
-
eventID:
|
|
9213
|
+
eventID: randomUUID13(),
|
|
8979
9214
|
goalID: goal.id,
|
|
8980
9215
|
type: "goal.blocked",
|
|
8981
9216
|
reason: args.reason,
|
|
@@ -9029,6 +9264,9 @@ async function withParentIdentity(defaults, host, ownerSessionID) {
|
|
|
9029
9264
|
function findGoalByWorkerSession(state, sessionID) {
|
|
9030
9265
|
if (!sessionID)
|
|
9031
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
|
+
}
|
|
9032
9270
|
return state.goals.find((g) => g.workerSessionID === sessionID && (g.status === "active" || g.status === "blocked"));
|
|
9033
9271
|
}
|
|
9034
9272
|
function formatGoalStructured(goal, runtime, enforcedScopedExecution = false) {
|
|
@@ -9156,7 +9394,7 @@ var GOAL_STATUS_META = {
|
|
|
9156
9394
|
var PHASE_META = {
|
|
9157
9395
|
idle: { short: "Idle", hint: "between turns" },
|
|
9158
9396
|
queued: { short: "Queued", hint: "waiting to start" },
|
|
9159
|
-
running: { short: "
|
|
9397
|
+
running: { short: "Reserved", hint: "turn reserved; activity unconfirmed" },
|
|
9160
9398
|
compacting: { short: "Compacting", hint: "summarizing context" },
|
|
9161
9399
|
waiting_retry: { short: "Retrying", hint: "backing off" },
|
|
9162
9400
|
stopping: { short: "Stopping", hint: "abort in flight" }
|
|
@@ -9167,9 +9405,30 @@ function goalStatusLabel(status) {
|
|
|
9167
9405
|
function phaseLabel(phase) {
|
|
9168
9406
|
return PHASE_META[phase] ?? { short: phase, hint: "" };
|
|
9169
9407
|
}
|
|
9170
|
-
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()) {
|
|
9171
9430
|
const goal = goalStatusLabel(status);
|
|
9172
|
-
const activity = phase ? phaseLabel(phase) : undefined;
|
|
9431
|
+
const activity = phase ? typeof phase === "string" ? phaseLabel(phase) : workerActivityLabel(phase, now) : undefined;
|
|
9173
9432
|
const workerClause = activity ? `worker is ${activity.hint || activity.short.toLowerCase()}` : "worker state unknown";
|
|
9174
9433
|
if (status === "active")
|
|
9175
9434
|
return `Loopd owns this; ${workerClause}.`;
|
|
@@ -9514,18 +9773,21 @@ function ownerTools2(options) {
|
|
|
9514
9773
|
execute: async (args, context) => {
|
|
9515
9774
|
const state = await readState(directory);
|
|
9516
9775
|
const ownerID = context?.sessionID;
|
|
9517
|
-
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");
|
|
9518
9777
|
if (!goal) {
|
|
9519
9778
|
return {
|
|
9520
9779
|
title: "No goal found",
|
|
9521
|
-
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." })
|
|
9522
9781
|
};
|
|
9523
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
|
+
}
|
|
9524
9786
|
const result = await goalService.sendUserMessage(directory, goal.id, args.message);
|
|
9525
9787
|
return {
|
|
9526
|
-
title: result.ok ? "Message
|
|
9788
|
+
title: result.ok ? result.outcome === "queued" ? "Message queued" : "Message submitted" : "Send failed",
|
|
9527
9789
|
output: JSON.stringify({
|
|
9528
|
-
|
|
9790
|
+
...result,
|
|
9529
9791
|
goalID: goal.id,
|
|
9530
9792
|
goalName: goal.name,
|
|
9531
9793
|
message: result.message
|
|
@@ -9585,16 +9847,20 @@ function ownerTools2(options) {
|
|
|
9585
9847
|
execute: async (args, context) => {
|
|
9586
9848
|
const state = await readState(directory);
|
|
9587
9849
|
const ownerID = context?.sessionID;
|
|
9588
|
-
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"));
|
|
9589
9851
|
if (!goal) {
|
|
9590
9852
|
return {
|
|
9591
9853
|
title: "No goal found",
|
|
9592
9854
|
output: JSON.stringify({
|
|
9593
9855
|
ok: false,
|
|
9594
|
-
|
|
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."
|
|
9595
9858
|
})
|
|
9596
9859
|
};
|
|
9597
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
|
+
}
|
|
9598
9864
|
if (goal.status === "active") {
|
|
9599
9865
|
return {
|
|
9600
9866
|
title: "Already active",
|
|
@@ -9602,18 +9868,13 @@ function ownerTools2(options) {
|
|
|
9602
9868
|
};
|
|
9603
9869
|
}
|
|
9604
9870
|
try {
|
|
9605
|
-
|
|
9606
|
-
await goalService.resume(directory, goal.id);
|
|
9607
|
-
} else if (goal.status === "blocked") {
|
|
9608
|
-
await goalService.retry(directory, goal.id);
|
|
9609
|
-
}
|
|
9871
|
+
const result = goal.status === "blocked" ? await goalService.retry(directory, goal.id) : await goalService.resume(directory, goal.id);
|
|
9610
9872
|
return {
|
|
9611
|
-
title: "
|
|
9873
|
+
title: result.ok ? "Resume submitted" : "Resume not delivered",
|
|
9612
9874
|
output: JSON.stringify({
|
|
9613
|
-
|
|
9875
|
+
...result,
|
|
9614
9876
|
goalID: goal.id,
|
|
9615
|
-
goalName: goal.name
|
|
9616
|
-
message: `Goal "${goal.name}" resumed.`
|
|
9877
|
+
goalName: goal.name
|
|
9617
9878
|
})
|
|
9618
9879
|
};
|
|
9619
9880
|
} catch (error) {
|
|
@@ -9629,20 +9890,23 @@ function ownerTools2(options) {
|
|
|
9629
9890
|
}
|
|
9630
9891
|
}),
|
|
9631
9892
|
nudge_goal: tool2({
|
|
9632
|
-
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.",
|
|
9633
9894
|
args: {
|
|
9634
9895
|
goal_id: tool2.schema.string().optional().describe("Goal ID. Omit to nudge the first active goal.")
|
|
9635
9896
|
},
|
|
9636
9897
|
execute: async (args, context) => {
|
|
9637
9898
|
const state = await readState(directory);
|
|
9638
9899
|
const ownerID = context?.sessionID;
|
|
9639
|
-
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");
|
|
9640
9901
|
if (!goal) {
|
|
9641
9902
|
return {
|
|
9642
9903
|
title: "No goal found",
|
|
9643
|
-
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." })
|
|
9644
9905
|
};
|
|
9645
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
|
+
}
|
|
9646
9910
|
try {
|
|
9647
9911
|
const result = await goalService.nudge(directory, goal.id);
|
|
9648
9912
|
return {
|
|
@@ -10144,9 +10408,10 @@ function commandTools(options) {
|
|
|
10144
10408
|
}
|
|
10145
10409
|
|
|
10146
10410
|
// src/server/plugin.ts
|
|
10411
|
+
init_server_log();
|
|
10147
10412
|
init_state_repository();
|
|
10148
10413
|
// package.json
|
|
10149
|
-
var version = "1.13.
|
|
10414
|
+
var version = "1.13.1";
|
|
10150
10415
|
|
|
10151
10416
|
// src/server/plugin.ts
|
|
10152
10417
|
init_workspace_execution();
|