@bojackduy/opencode-loopd 1.12.4 → 1.13.1

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