@bojackduy/opencode-loopd 1.13.0 → 1.13.1

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