@junctionpanel/server 0.1.83 → 0.1.85

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.
Files changed (67) hide show
  1. package/dist/server/client/daemon-client.d.ts +10 -0
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +58 -0
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-checkpoint-storage.d.ts +126 -0
  6. package/dist/server/server/agent/agent-checkpoint-storage.d.ts.map +1 -0
  7. package/dist/server/server/agent/agent-checkpoint-storage.js +203 -0
  8. package/dist/server/server/agent/agent-checkpoint-storage.js.map +1 -0
  9. package/dist/server/server/agent/agent-manager.d.ts +16 -0
  10. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  11. package/dist/server/server/agent/agent-manager.js +584 -12
  12. package/dist/server/server/agent/agent-manager.js.map +1 -1
  13. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  14. package/dist/server/server/agent/agent-projections.js +5 -0
  15. package/dist/server/server/agent/agent-projections.js.map +1 -1
  16. package/dist/server/server/agent/agent-sdk-types.d.ts +2 -2
  17. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  18. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  19. package/dist/server/server/agent/agent-storage.d.ts +37 -37
  20. package/dist/server/server/agent/harness/context.d.ts +2 -1
  21. package/dist/server/server/agent/harness/context.d.ts.map +1 -1
  22. package/dist/server/server/agent/harness/context.js +106 -0
  23. package/dist/server/server/agent/harness/context.js.map +1 -1
  24. package/dist/server/server/agent/harness/memory.d.ts.map +1 -1
  25. package/dist/server/server/agent/harness/memory.js +38 -8
  26. package/dist/server/server/agent/harness/memory.js.map +1 -1
  27. package/dist/server/server/agent/harness/permission-policy.d.ts +10 -0
  28. package/dist/server/server/agent/harness/permission-policy.d.ts.map +1 -0
  29. package/dist/server/server/agent/harness/permission-policy.js +86 -0
  30. package/dist/server/server/agent/harness/permission-policy.js.map +1 -0
  31. package/dist/server/server/agent/harness/risk-classifier.d.ts +8 -0
  32. package/dist/server/server/agent/harness/risk-classifier.d.ts.map +1 -0
  33. package/dist/server/server/agent/harness/risk-classifier.js +73 -0
  34. package/dist/server/server/agent/harness/risk-classifier.js.map +1 -0
  35. package/dist/server/server/agent/harness/run-ledger.d.ts +21 -0
  36. package/dist/server/server/agent/harness/run-ledger.d.ts.map +1 -0
  37. package/dist/server/server/agent/harness/run-ledger.js +79 -0
  38. package/dist/server/server/agent/harness/run-ledger.js.map +1 -0
  39. package/dist/server/server/agent/harness/session-bundle.d.ts +13 -0
  40. package/dist/server/server/agent/harness/session-bundle.d.ts.map +1 -0
  41. package/dist/server/server/agent/harness/session-bundle.js +50 -0
  42. package/dist/server/server/agent/harness/session-bundle.js.map +1 -0
  43. package/dist/server/server/agent/harness/types.d.ts +150 -2
  44. package/dist/server/server/agent/harness/types.d.ts.map +1 -1
  45. package/dist/server/server/agent/harness/types.js +1 -1
  46. package/dist/server/server/agent/harness/types.js.map +1 -1
  47. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +2 -2
  48. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  49. package/dist/server/server/bootstrap.d.ts +1 -0
  50. package/dist/server/server/bootstrap.d.ts.map +1 -1
  51. package/dist/server/server/bootstrap.js +4 -0
  52. package/dist/server/server/bootstrap.js.map +1 -1
  53. package/dist/server/server/config.d.ts.map +1 -1
  54. package/dist/server/server/config.js +1 -0
  55. package/dist/server/server/config.js.map +1 -1
  56. package/dist/server/server/persisted-config.d.ts +6 -6
  57. package/dist/server/server/session.d.ts +2 -0
  58. package/dist/server/server/session.d.ts.map +1 -1
  59. package/dist/server/server/session.js +94 -0
  60. package/dist/server/server/session.js.map +1 -1
  61. package/dist/server/shared/messages.d.ts +9992 -7147
  62. package/dist/server/shared/messages.d.ts.map +1 -1
  63. package/dist/server/shared/messages.js +230 -0
  64. package/dist/server/shared/messages.js.map +1 -1
  65. package/dist/server/shared/switchboard.d.ts +1 -1
  66. package/dist/server/utils/worktree-metadata.d.ts +8 -8
  67. package/package.json +2 -2
@@ -9,6 +9,9 @@ import { buildPermissionRecoveryFingerprint } from "./agent-permission-fingerpri
9
9
  import { isCodexPlanModeEnabled, normalizeCodexModeId, setCodexPlanModeEnabled, } from "./codex-config.js";
10
10
  import { applyHarnessSystemPrompt, buildHarnessContext, compileHarnessSystemPrompt, getHarnessSessionExtra, mergeRuntimeInfoWithHarness, setHarnessSessionExtra, } from "./harness/context.js";
11
11
  import { summarizeTurnFromTimeline, updateSessionState } from "./harness/memory.js";
12
+ import { createHarnessPermissionLedgerEntry, resolveHarnessPermissionLedger, } from "./harness/permission-policy.js";
13
+ import { getActiveHarnessRun, touchHarnessRunProgress, transitionHarnessRunLedger, } from "./harness/run-ledger.js";
14
+ import { accumulateHarnessCostState, createHarnessResumeState, ensureHarnessScratchpadDir, touchHarnessResumeState, } from "./harness/session-bundle.js";
12
15
  import { JUNCTION_HARNESS_VERSION } from "./harness/types.js";
13
16
  import { getPendingPlanReviewFingerprint, hasPendingPlanReview, } from "./pending-plan-review.js";
14
17
  import { extractPermissionQuestionAnswersFromResponse, formatPermissionQuestionAnswers, isInteractiveQuestionPermission, } from "../../shared/permission-questions.js";
@@ -43,6 +46,36 @@ function extractGeminiFallbackPermissionUpdate(response) {
43
46
  const persistedSummary = typeof fallback.persistedSummary === "string" ? fallback.persistedSummary.trim() : "";
44
47
  return persistedSummary.length > 0 ? { kind, persistedSummary } : { kind };
45
48
  }
49
+ function inferHarnessWorkerRole(detail) {
50
+ const haystack = `${detail.subAgentType ?? ""} ${detail.description ?? ""}`.toLowerCase();
51
+ if (haystack.includes("explore") || haystack.includes("research")) {
52
+ return "explore";
53
+ }
54
+ if (haystack.includes("plan")) {
55
+ return "plan";
56
+ }
57
+ if (haystack.includes("verify") || haystack.includes("test")) {
58
+ return "verify";
59
+ }
60
+ if (haystack.includes("implement") || haystack.includes("fix")) {
61
+ return "implement";
62
+ }
63
+ return "generic";
64
+ }
65
+ function mapTurnOutcomeToRunLifecycle(outcome) {
66
+ switch (outcome) {
67
+ case "provider_error":
68
+ return "error";
69
+ case "canceled":
70
+ return "interrupted";
71
+ case "completed":
72
+ case "needs_verification":
73
+ return "completed";
74
+ case "blocked_on_permission":
75
+ case "blocked_on_feedback":
76
+ return "waiting_permission";
77
+ }
78
+ }
46
79
  function attachPersistenceCwd(handle, cwd) {
47
80
  if (!handle) {
48
81
  return null;
@@ -225,6 +258,7 @@ export class AgentManager {
225
258
  : null;
226
259
  this.idFactory = options?.idFactory ?? (() => randomUUID());
227
260
  this.registry = options?.registry;
261
+ this.checkpointStorage = options?.checkpointStorage;
228
262
  this.onAgentAttention = options?.onAgentAttention;
229
263
  this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
230
264
  if (options?.clients) {
@@ -531,6 +565,7 @@ export class AgentManager {
531
565
  return this.registerSession(session, normalizedConfig, resolvedAgentId, {
532
566
  labels: options?.labels,
533
567
  parentAgentId: options?.parentAgentId ?? null,
568
+ resumeSource: "new_session",
534
569
  });
535
570
  }
536
571
  // Reconstruct an agent from provider persistence. Callers should explicitly
@@ -549,7 +584,7 @@ export class AgentManager {
549
584
  : overrides;
550
585
  const client = this.requireClient(handle.provider);
551
586
  const session = await client.resumeSession(handle, resumeOverrides);
552
- return this.registerSession(session, normalizedConfig, resolvedAgentId, options);
587
+ return this.registerSession(session, normalizedConfig, resolvedAgentId, { ...options, resumeSource: "provider_resume" });
553
588
  }
554
589
  // Hot-reload an active agent session with config overrides while preserving
555
590
  // in-memory timeline state.
@@ -613,6 +648,7 @@ export class AgentManager {
613
648
  activeTurnStartedAt: preservedActiveTurnStartedAt,
614
649
  lastError: preservedLastError,
615
650
  attention: preservedAttention,
651
+ resumeSource: handle ? "provider_resume" : "new_session",
616
652
  });
617
653
  }
618
654
  async closeAgent(agentId) {
@@ -1044,13 +1080,15 @@ export class AgentManager {
1044
1080
  }
1045
1081
  }
1046
1082
  await agent.session.respondToPermission(requestId, response);
1047
- agent.pendingPermissions.delete(requestId);
1048
- this.clearAttentionForUserEngagement(agent);
1049
- this.recordPermissionResolutionTimelineItem(agent, requestId, response, {
1083
+ this.resolvePendingPermission(agent, requestId, response, {
1050
1084
  provider: agent.provider,
1051
- pendingRequest,
1085
+ dispatchEvent: false,
1052
1086
  dispatchTimeline: true,
1087
+ syncHarness: true,
1088
+ dispatchHarnessEvent: false,
1089
+ transitionRunToStreaming: getActiveHarnessRun(getHarnessSessionExtra(agent.config)?.runLedger) !== null,
1053
1090
  });
1091
+ this.clearAttentionForUserEngagement(agent);
1054
1092
  // Update currentModeId - the session may have changed mode internally
1055
1093
  // (e.g., plan approval changes mode from "plan" to "acceptEdits")
1056
1094
  try {
@@ -1122,6 +1160,9 @@ export class AgentManager {
1122
1160
  provider: agent.provider,
1123
1161
  dispatchEvent: true,
1124
1162
  dispatchTimeline: true,
1163
+ syncHarness: true,
1164
+ dispatchHarnessEvent: true,
1165
+ transitionRunToStreaming: false,
1125
1166
  });
1126
1167
  }
1127
1168
  this.emitState(agent);
@@ -1170,7 +1211,14 @@ export class AgentManager {
1170
1211
  for (const [requestId, request] of agent.pendingPermissions) {
1171
1212
  const fingerprint = buildPermissionRecoveryFingerprint(request);
1172
1213
  if (requestIds.has(request.id) || requestFingerprints.has(fingerprint)) {
1173
- agent.pendingPermissions.delete(requestId);
1214
+ this.resolvePendingPermission(agent, requestId, { behavior: "deny", message: "Approval request expired while the daemon was offline." }, {
1215
+ provider: agent.provider,
1216
+ dispatchEvent: false,
1217
+ dispatchTimeline: false,
1218
+ syncHarness: true,
1219
+ dispatchHarnessEvent: false,
1220
+ transitionRunToStreaming: false,
1221
+ });
1174
1222
  removed += 1;
1175
1223
  }
1176
1224
  }
@@ -1407,6 +1455,18 @@ export class AgentManager {
1407
1455
  internal: config.internal ?? false,
1408
1456
  labels: options?.labels ?? {},
1409
1457
  };
1458
+ const registeredAt = (options?.updatedAt ?? options?.createdAt ?? now).toISOString();
1459
+ const existingHarnessExtra = getHarnessSessionExtra(config);
1460
+ setHarnessSessionExtra(config, {
1461
+ scratchpadDir: existingHarnessExtra?.scratchpadDir ?? ensureHarnessScratchpadDir(resolvedAgentId),
1462
+ resumeState: existingHarnessExtra?.resumeState
1463
+ ? touchHarnessResumeState(existingHarnessExtra.resumeState, {
1464
+ status: existingHarnessExtra.resumeState.status === "checkpoint_restored" ? "checkpoint_restored" : "live",
1465
+ source: existingHarnessExtra.resumeState.source,
1466
+ timestamp: registeredAt,
1467
+ })
1468
+ : createHarnessResumeState(options?.resumeSource ?? "new_session", registeredAt),
1469
+ });
1410
1470
  this.agents.set(resolvedAgentId, managed);
1411
1471
  // Initialize previousStatus to track transitions
1412
1472
  this.previousStatuses.set(resolvedAgentId, managed.lifecycle);
@@ -1606,6 +1666,11 @@ export class AgentManager {
1606
1666
  agent.lastUserMessageAt = new Date();
1607
1667
  this.emitState(agent);
1608
1668
  }
1669
+ if (!options?.fromHistory &&
1670
+ event.item.type !== "permission_request" &&
1671
+ event.item.type !== "permission_resolution") {
1672
+ this.touchActiveHarnessRunProgress(agent);
1673
+ }
1609
1674
  break;
1610
1675
  case "turn_completed":
1611
1676
  this.pendingUserInterruptAgents.delete(agent.id);
@@ -1615,7 +1680,20 @@ export class AgentManager {
1615
1680
  let shouldEmitState = pendingRefresh.changed;
1616
1681
  agent.lastUsage = event.usage;
1617
1682
  agent.lastError = undefined;
1618
- this.finalizeHarnessTurn(agent, "completed");
1683
+ if (!options?.fromHistory) {
1684
+ this.finalizeHarnessTurn(agent, "completed");
1685
+ const timestamp = new Date().toISOString();
1686
+ const harnessExtra = getHarnessSessionExtra(agent.config);
1687
+ setHarnessSessionExtra(agent.config, {
1688
+ costState: accumulateHarnessCostState(harnessExtra?.costState, event.usage, timestamp),
1689
+ resumeState: touchHarnessResumeState(harnessExtra?.resumeState, {
1690
+ status: "live",
1691
+ source: harnessExtra?.resumeState?.source ?? "snapshot",
1692
+ timestamp,
1693
+ recoveryHint: null,
1694
+ }),
1695
+ });
1696
+ }
1619
1697
  if (!options?.fromHistory) {
1620
1698
  const turnSummaryItem = this.buildTurnSummaryTimelineItem(agent, event.usage, event.modelId, agent.activeTurnStartedAt);
1621
1699
  if (turnSummaryItem) {
@@ -1647,12 +1725,24 @@ export class AgentManager {
1647
1725
  }
1648
1726
  agent.activeTurnStartedAt = null;
1649
1727
  agent.lastError = event.error;
1650
- this.finalizeHarnessTurn(agent, "provider_error");
1728
+ if (!options?.fromHistory) {
1729
+ this.finalizeHarnessTurn(agent, "provider_error");
1730
+ this.recordHarnessEvent(agent, {
1731
+ kind: "recovery_hint_emitted",
1732
+ summary: "The provider run failed. Review the error and retry or resume the task.",
1733
+ details: {
1734
+ error: event.error,
1735
+ },
1736
+ });
1737
+ }
1651
1738
  for (const [requestId] of agent.pendingPermissions) {
1652
1739
  this.resolvePendingPermission(agent, requestId, { behavior: "deny", message: "Turn failed" }, {
1653
1740
  provider: event.provider,
1654
1741
  dispatchEvent: !options?.fromHistory,
1655
1742
  dispatchTimeline: !options?.fromHistory,
1743
+ syncHarness: !options?.fromHistory,
1744
+ dispatchHarnessEvent: !options?.fromHistory,
1745
+ transitionRunToStreaming: false,
1656
1746
  });
1657
1747
  }
1658
1748
  this.emitState(agent);
@@ -1670,13 +1760,28 @@ export class AgentManager {
1670
1760
  }
1671
1761
  agent.activeTurnStartedAt = null;
1672
1762
  agent.lastError = undefined;
1673
- this.finalizeHarnessTurn(agent, "canceled");
1763
+ if (!options?.fromHistory) {
1764
+ this.finalizeHarnessTurn(agent, "canceled");
1765
+ const timestamp = new Date().toISOString();
1766
+ const harnessExtra = getHarnessSessionExtra(agent.config);
1767
+ setHarnessSessionExtra(agent.config, {
1768
+ resumeState: touchHarnessResumeState(harnessExtra?.resumeState, {
1769
+ status: "live",
1770
+ source: harnessExtra?.resumeState?.source ?? "snapshot",
1771
+ timestamp,
1772
+ recoveryHint: "The run was interrupted before completion.",
1773
+ }),
1774
+ });
1775
+ }
1674
1776
  if (!pendingRefresh.hasPendingPermissions) {
1675
1777
  for (const requestId of pendingRequestIds) {
1676
1778
  this.resolvePendingPermission(agent, requestId, { behavior: "deny", message: "Interrupted" }, {
1677
1779
  provider: event.provider,
1678
1780
  dispatchEvent: !options?.fromHistory,
1679
1781
  dispatchTimeline: !options?.fromHistory,
1782
+ syncHarness: !options?.fromHistory,
1783
+ dispatchHarnessEvent: !options?.fromHistory,
1784
+ transitionRunToStreaming: false,
1680
1785
  });
1681
1786
  }
1682
1787
  }
@@ -1700,12 +1805,22 @@ export class AgentManager {
1700
1805
  if (!agent.pendingRun) {
1701
1806
  agent.lifecycle = "running";
1702
1807
  }
1808
+ if (!options?.fromHistory) {
1809
+ this.transitionHarnessRunState(agent, {
1810
+ lifecycle: "streaming",
1811
+ summary: "Run is streaming.",
1812
+ turnStartSeq: getHarnessSessionExtra(agent.config)?.currentTurn?.turnStartSeq ?? null,
1813
+ });
1814
+ }
1703
1815
  this.emitState(agent);
1704
1816
  shouldPersistSnapshot = true;
1705
1817
  break;
1706
1818
  case "permission_requested":
1707
1819
  {
1708
1820
  agent.pendingPermissions.set(event.request.id, event.request);
1821
+ if (!options?.fromHistory) {
1822
+ this.recordHarnessPermissionRequest(agent, event.request, true);
1823
+ }
1709
1824
  this.recordPermissionRequestTimelineItem(agent, event.request, {
1710
1825
  provider: event.provider,
1711
1826
  dispatchTimeline: !options?.fromHistory,
@@ -1737,6 +1852,9 @@ export class AgentManager {
1737
1852
  provider: event.provider,
1738
1853
  dispatchEvent: false,
1739
1854
  dispatchTimeline: !options?.fromHistory,
1855
+ syncHarness: !options?.fromHistory,
1856
+ dispatchHarnessEvent: !options?.fromHistory,
1857
+ transitionRunToStreaming: !options?.fromHistory,
1740
1858
  });
1741
1859
  this.emitState(agent);
1742
1860
  shouldPersistSnapshot = true;
@@ -1863,6 +1981,12 @@ export class AgentManager {
1863
1981
  }
1864
1982
  resolvePendingPermission(agent, requestId, resolution, options) {
1865
1983
  const pendingRequest = agent.pendingPermissions.get(requestId) ?? null;
1984
+ if (options.syncHarness) {
1985
+ this.recordHarnessPermissionResolution(agent, requestId, resolution, {
1986
+ dispatchEvent: options.dispatchHarnessEvent ?? false,
1987
+ transitionRunToStreaming: options.transitionRunToStreaming ?? false,
1988
+ });
1989
+ }
1866
1990
  agent.pendingPermissions.delete(requestId);
1867
1991
  const row = this.recordPermissionResolutionTimelineItem(agent, requestId, resolution, {
1868
1992
  provider: options.provider,
@@ -2174,6 +2298,261 @@ export class AgentManager {
2174
2298
  },
2175
2299
  };
2176
2300
  }
2301
+ recordHarnessEvent(agent, input) {
2302
+ const item = {
2303
+ type: "harness_event",
2304
+ kind: input.kind,
2305
+ summary: input.summary,
2306
+ ...(input.checkpointId !== undefined ? { checkpointId: input.checkpointId } : {}),
2307
+ ...(input.verificationState !== undefined
2308
+ ? { verificationState: input.verificationState }
2309
+ : {}),
2310
+ ...(input.trustState !== undefined ? { trustState: input.trustState } : {}),
2311
+ ...(input.workerId !== undefined ? { workerId: input.workerId } : {}),
2312
+ ...(input.details ? { details: input.details } : {}),
2313
+ };
2314
+ const row = this.recordTimeline(agent, item);
2315
+ this.dispatchStream(agent.id, { type: "timeline", provider: agent.provider, item }, {
2316
+ seq: row.seq,
2317
+ epoch: this.ensureTimelineState(agent).epoch,
2318
+ });
2319
+ }
2320
+ transitionHarnessRunState(agent, input) {
2321
+ const timestamp = new Date().toISOString();
2322
+ const current = getHarnessSessionExtra(agent.config);
2323
+ const previousRun = getActiveHarnessRun(current?.runLedger);
2324
+ const { ledger, run } = transitionHarnessRunLedger(current?.runLedger, {
2325
+ lifecycle: input.lifecycle,
2326
+ timestamp,
2327
+ idFactory: this.idFactory,
2328
+ owner: agent.internal ? "background" : "foreground",
2329
+ summary: input.summary,
2330
+ turnStartSeq: input.turnStartSeq,
2331
+ turnEndSeq: input.turnEndSeq,
2332
+ permissionRequestId: input.permissionRequestId,
2333
+ correlationId: agent.persistence?.sessionId ?? agent.runtimeInfo?.sessionId ?? null,
2334
+ modelId: agent.runtimeInfo?.model ?? agent.config.model ?? null,
2335
+ progress: input.lifecycle === "streaming",
2336
+ });
2337
+ setHarnessSessionExtra(agent.config, {
2338
+ runLedger: ledger,
2339
+ });
2340
+ if (input.dispatchEvent !== false &&
2341
+ (previousRun?.lifecycle !== run.lifecycle ||
2342
+ previousRun?.permissionRequestId !== run.permissionRequestId)) {
2343
+ this.recordHarnessEvent(agent, {
2344
+ kind: "run_state_changed",
2345
+ summary: input.summary ?? `Run is now ${run.lifecycle}.`,
2346
+ details: {
2347
+ runId: run.id,
2348
+ lifecycle: run.lifecycle,
2349
+ permissionRequestId: run.permissionRequestId,
2350
+ },
2351
+ });
2352
+ }
2353
+ }
2354
+ touchActiveHarnessRunProgress(agent) {
2355
+ const current = getHarnessSessionExtra(agent.config);
2356
+ const nextLedger = touchHarnessRunProgress(current?.runLedger, new Date().toISOString());
2357
+ if (!nextLedger || nextLedger === current?.runLedger) {
2358
+ return;
2359
+ }
2360
+ setHarnessSessionExtra(agent.config, {
2361
+ runLedger: nextLedger,
2362
+ });
2363
+ }
2364
+ recordHarnessPermissionRequest(agent, request, dispatchEvent) {
2365
+ const timestamp = new Date().toISOString();
2366
+ const current = getHarnessSessionExtra(agent.config);
2367
+ const existing = (current?.permissionLedger ?? []).filter((entry) => entry.requestId !== request.id);
2368
+ const nextEntry = createHarnessPermissionLedgerEntry(request, timestamp);
2369
+ setHarnessSessionExtra(agent.config, {
2370
+ permissionLedger: [nextEntry, ...existing].slice(0, 50),
2371
+ });
2372
+ this.transitionHarnessRunState(agent, {
2373
+ lifecycle: "waiting_permission",
2374
+ summary: `Waiting for permission: ${request.name}.`,
2375
+ permissionRequestId: request.id,
2376
+ dispatchEvent,
2377
+ });
2378
+ if (dispatchEvent) {
2379
+ this.recordHarnessEvent(agent, {
2380
+ kind: "permission_enqueued",
2381
+ summary: `Queued ${nextEntry.risk} permission request for ${request.name}.`,
2382
+ details: {
2383
+ requestId: request.id,
2384
+ risk: nextEntry.risk,
2385
+ fingerprint: nextEntry.fingerprint,
2386
+ kind: request.kind,
2387
+ },
2388
+ });
2389
+ }
2390
+ }
2391
+ recordHarnessPermissionResolution(agent, requestId, resolution, options) {
2392
+ const timestamp = new Date().toISOString();
2393
+ const current = getHarnessSessionExtra(agent.config);
2394
+ const nextLedger = resolveHarnessPermissionLedger(current?.permissionLedger, requestId, resolution, timestamp);
2395
+ const resolvedEntry = nextLedger.find((entry) => entry.requestId === requestId) ?? null;
2396
+ setHarnessSessionExtra(agent.config, {
2397
+ permissionLedger: nextLedger,
2398
+ });
2399
+ if (options.dispatchEvent && resolvedEntry && agent.lifecycle !== "closed") {
2400
+ this.recordHarnessEvent(agent, {
2401
+ kind: "permission_resolved",
2402
+ summary: `Permission ${resolvedEntry.status} for ${resolvedEntry.toolName}.`,
2403
+ details: {
2404
+ requestId,
2405
+ risk: resolvedEntry.risk,
2406
+ status: resolvedEntry.status,
2407
+ },
2408
+ });
2409
+ if (resolvedEntry.recoveryHint && resolvedEntry.status !== "allowed") {
2410
+ this.recordHarnessEvent(agent, {
2411
+ kind: "recovery_hint_emitted",
2412
+ summary: resolvedEntry.recoveryHint,
2413
+ details: {
2414
+ requestId,
2415
+ status: resolvedEntry.status,
2416
+ },
2417
+ });
2418
+ }
2419
+ }
2420
+ if (options.transitionRunToStreaming && agent.lifecycle !== "closed") {
2421
+ this.transitionHarnessRunState(agent, {
2422
+ lifecycle: "streaming",
2423
+ summary: resolvedEntry != null
2424
+ ? `Permission ${resolvedEntry.status} for ${resolvedEntry.toolName}.`
2425
+ : "Permission resolved.",
2426
+ permissionRequestId: null,
2427
+ dispatchEvent: false,
2428
+ });
2429
+ }
2430
+ }
2431
+ upsertCheckpointSummary(existing, summary) {
2432
+ return [summary, ...(existing ?? []).filter((entry) => entry.id !== summary.id)].slice(0, 50);
2433
+ }
2434
+ deriveHarnessWorkers(agent, turnRows, objective, timestamp) {
2435
+ const workers = {
2436
+ ...(getHarnessSessionExtra(agent.config)?.workers ?? {}),
2437
+ };
2438
+ for (const row of turnRows) {
2439
+ const item = row.item;
2440
+ if (item.type !== "tool_call" || item.detail.type !== "sub_agent") {
2441
+ continue;
2442
+ }
2443
+ const existing = workers[item.callId];
2444
+ const nextStatus = item.status === "running"
2445
+ ? "running"
2446
+ : item.status === "completed"
2447
+ ? existing?.status === "integrated"
2448
+ ? "integrated"
2449
+ : "completed"
2450
+ : "failed";
2451
+ workers[item.callId] = {
2452
+ id: item.callId,
2453
+ provider: agent.provider,
2454
+ threadId: null,
2455
+ parentAgentId: agent.id,
2456
+ role: inferHarnessWorkerRole(item.detail),
2457
+ kind: "provider_subagent",
2458
+ purpose: item.detail.description?.trim() ||
2459
+ item.detail.subAgentType?.trim() ||
2460
+ "Sub-agent task",
2461
+ owner: objective,
2462
+ status: nextStatus,
2463
+ queueState: nextStatus === "running" ? "active" : "completed",
2464
+ outputSummary: item.detail.actions[item.detail.actions.length - 1]?.summary ??
2465
+ item.detail.description ??
2466
+ existing?.outputSummary ??
2467
+ null,
2468
+ changedFiles: existing?.changedFiles ?? [],
2469
+ verificationState: existing?.verificationState ?? "not_needed",
2470
+ startedAt: existing?.startedAt ?? row.timestamp,
2471
+ integratedAt: existing?.integratedAt ?? null,
2472
+ completedAt: nextStatus === "completed" || nextStatus === "integrated" || nextStatus === "failed"
2473
+ ? timestamp
2474
+ : existing?.completedAt ?? null,
2475
+ sessionId: existing?.sessionId ?? null,
2476
+ transcriptPath: existing?.transcriptPath ?? null,
2477
+ outputPath: existing?.outputPath ?? null,
2478
+ rolloutPath: existing?.rolloutPath ?? null,
2479
+ recentActions: item.detail.actions.map((action) => ({ ...action })),
2480
+ permissionState: existing?.permissionState ?? "none",
2481
+ lastPermissionRequestId: existing?.lastPermissionRequestId ?? null,
2482
+ lastPermissionToolName: existing?.lastPermissionToolName ?? null,
2483
+ error: item.status === "failed"
2484
+ ? typeof item.error === "object" && item.error !== null && "message" in item.error
2485
+ ? String(item.error.message ?? "Worker failed")
2486
+ : String(item.error)
2487
+ : null,
2488
+ progress: {
2489
+ toolCallCount: item.detail.actions.length,
2490
+ lastActivityAt: row.timestamp,
2491
+ latestSummary: item.detail.actions[item.detail.actions.length - 1]?.summary ??
2492
+ item.detail.description ??
2493
+ existing?.progress?.latestSummary ??
2494
+ null,
2495
+ },
2496
+ lastUpdatedAt: timestamp,
2497
+ };
2498
+ }
2499
+ return workers;
2500
+ }
2501
+ async captureCheckpoint(agent, input) {
2502
+ if (!this.checkpointStorage) {
2503
+ return null;
2504
+ }
2505
+ const harness = input.harnessSnapshot ?? getHarnessSessionExtra(agent.config);
2506
+ if (!harness) {
2507
+ return null;
2508
+ }
2509
+ try {
2510
+ return await this.checkpointStorage.capture({
2511
+ agentId: agent.id,
2512
+ provider: agent.provider,
2513
+ cwd: agent.cwd,
2514
+ id: input.id,
2515
+ kind: input.kind,
2516
+ objective: input.objective,
2517
+ summary: input.summary,
2518
+ changedFiles: input.changedFiles,
2519
+ turnStartSeq: input.turnStartSeq,
2520
+ turnEndSeq: input.turnEndSeq,
2521
+ harness,
2522
+ });
2523
+ }
2524
+ catch (error) {
2525
+ this.logger.warn({ err: error, agentId: agent.id, checkpointId: input.id }, "Failed to capture Junction harness checkpoint");
2526
+ return null;
2527
+ }
2528
+ }
2529
+ publishCapturedCheckpoint(agent, summary, input) {
2530
+ const current = getHarnessSessionExtra(agent.config);
2531
+ if (!current) {
2532
+ return;
2533
+ }
2534
+ const extraPatch = input.onSetExtra?.(current);
2535
+ setHarnessSessionExtra(agent.config, {
2536
+ ...extraPatch,
2537
+ checkpoints: this.upsertCheckpointSummary(current.checkpoints, summary),
2538
+ lastCheckpointId: summary.id,
2539
+ });
2540
+ this.recordHarnessEvent(agent, {
2541
+ kind: "checkpoint_created",
2542
+ summary: input.eventSummary,
2543
+ checkpointId: summary.id,
2544
+ details: input.details,
2545
+ });
2546
+ this.touchUpdatedAt(agent);
2547
+ this.emitState(agent);
2548
+ const persistTask = this.persistSnapshot(agent).catch((error) => {
2549
+ this.logger.warn({ err: error, agentId: agent.id, checkpointId: summary.id }, "Failed to persist snapshot after publishing Junction harness checkpoint");
2550
+ });
2551
+ this.trackBackgroundTask(persistTask);
2552
+ }
2553
+ isLiveAgent(agent) {
2554
+ return this.agents.get(agent.id) === agent;
2555
+ }
2177
2556
  async prepareHarnessForRun(agent, prompt) {
2178
2557
  const providerInstructionStrategy = resolveProviderInstructionStrategy(agent.provider);
2179
2558
  try {
@@ -2182,21 +2561,79 @@ export class AgentManager {
2182
2561
  prompt,
2183
2562
  providerInstructionStrategy,
2184
2563
  });
2564
+ const previousHarnessExtra = getHarnessSessionExtra(agent.config);
2565
+ const turnStartSeq = this.ensureTimelineState(agent).nextSeq;
2185
2566
  const compiledSystemPrompt = compileHarnessSystemPrompt(context);
2186
2567
  applyHarnessSystemPrompt(agent.config, compiledSystemPrompt);
2568
+ const startedAt = new Date().toISOString();
2569
+ const scratchpadDir = previousHarnessExtra?.scratchpadDir ?? ensureHarnessScratchpadDir(agent.id);
2187
2570
  setHarnessSessionExtra(agent.config, {
2188
2571
  workspaceSynopsis: context.workspaceSynopsis,
2189
2572
  trustState: context.trustState,
2190
2573
  contextLayers: context.promptLayers.map((layer) => layer.id),
2191
2574
  providerInstructionStrategy,
2192
2575
  harnessVersion: JUNCTION_HARNESS_VERSION,
2576
+ scratchpadDir,
2577
+ resumeState: previousHarnessExtra?.resumeState
2578
+ ? touchHarnessResumeState(previousHarnessExtra.resumeState, {
2579
+ status: "live",
2580
+ source: previousHarnessExtra.resumeState.source,
2581
+ timestamp: startedAt,
2582
+ recoveryHint: null,
2583
+ })
2584
+ : createHarnessResumeState("new_session", startedAt),
2193
2585
  currentTurn: {
2194
2586
  classification: context.classification,
2195
2587
  objective: context.objective,
2196
- turnStartSeq: this.ensureTimelineState(agent).nextSeq,
2197
- startedAt: new Date().toISOString(),
2588
+ turnStartSeq,
2589
+ startedAt,
2198
2590
  },
2199
2591
  });
2592
+ if (previousHarnessExtra?.trustState !== context.trustState) {
2593
+ this.recordHarnessEvent(agent, {
2594
+ kind: "trust_changed",
2595
+ summary: `Trust state is now ${context.trustState}.`,
2596
+ trustState: context.trustState,
2597
+ });
2598
+ }
2599
+ if (this.checkpointStorage &&
2600
+ (context.classification === "implement" ||
2601
+ context.classification === "debug" ||
2602
+ context.classification === "review")) {
2603
+ const checkpointId = this.idFactory();
2604
+ const record = await this.captureCheckpoint(agent, {
2605
+ id: checkpointId,
2606
+ kind: "pre_mutation",
2607
+ objective: context.objective,
2608
+ summary: "Pre-mutation checkpoint",
2609
+ changedFiles: [],
2610
+ turnStartSeq,
2611
+ turnEndSeq: turnStartSeq,
2612
+ });
2613
+ if (record) {
2614
+ this.publishCapturedCheckpoint(agent, this.checkpointStorage.toSummary(record), {
2615
+ eventSummary: "Created a pre-mutation checkpoint.",
2616
+ details: {
2617
+ checkpointKind: "pre_mutation",
2618
+ turnStartSeq,
2619
+ },
2620
+ onSetExtra: (current) => ({
2621
+ currentTurn: current?.currentTurn
2622
+ ? {
2623
+ ...current.currentTurn,
2624
+ preMutationCheckpointId: checkpointId,
2625
+ }
2626
+ : {
2627
+ classification: context.classification,
2628
+ objective: context.objective,
2629
+ turnStartSeq,
2630
+ startedAt,
2631
+ preMutationCheckpointId: checkpointId,
2632
+ },
2633
+ }),
2634
+ });
2635
+ }
2636
+ }
2200
2637
  }
2201
2638
  catch (error) {
2202
2639
  this.logger.warn({ err: error, agentId: agent.id, provider: agent.provider }, "Failed to prepare Junction harness context; continuing with provider defaults");
@@ -2239,21 +2676,156 @@ export class AgentManager {
2239
2676
  turnEndSeq: turnRows.length > 0 ? turnRows[turnRows.length - 1].seq : currentTurn.turnStartSeq,
2240
2677
  timestamp,
2241
2678
  });
2679
+ const nextWorkers = this.deriveHarnessWorkers(agent, turnRows, currentTurn.objective, timestamp);
2242
2680
  const sessionState = updateSessionState(harnessExtra?.sessionState ?? null, turnSummary, {
2243
2681
  modeId: agent.currentModeId ?? null,
2244
2682
  modelId: agent.runtimeInfo?.model ?? agent.config.model ?? null,
2245
2683
  thinkingOptionId: agent.runtimeInfo?.thinkingOptionId ?? agent.config.thinkingOptionId ?? null,
2246
2684
  });
2247
- setHarnessSessionExtra(agent.config, {
2685
+ const nextExtra = {
2248
2686
  ...harnessExtra,
2249
2687
  sessionState,
2688
+ workers: nextWorkers,
2250
2689
  lastTurnSummary: turnSummary,
2251
2690
  lastValidatedAt: turnSummary.lastSuccessfulValidation?.timestamp ?? harnessExtra?.lastValidatedAt ?? null,
2252
2691
  currentTurn: undefined,
2253
2692
  harnessVersion: JUNCTION_HARNESS_VERSION,
2693
+ runLedger: transitionHarnessRunLedger(harnessExtra?.runLedger, {
2694
+ lifecycle: mapTurnOutcomeToRunLifecycle(resolvedOutcome),
2695
+ timestamp,
2696
+ idFactory: this.idFactory,
2697
+ owner: agent.internal ? "background" : "foreground",
2698
+ summary: turnSummary.uiSummary ?? turnSummary.nextRequiredAction,
2699
+ turnStartSeq: turnSummary.turnStartSeq,
2700
+ turnEndSeq: turnSummary.turnEndSeq,
2701
+ correlationId: agent.persistence?.sessionId ?? agent.runtimeInfo?.sessionId ?? null,
2702
+ modelId: agent.runtimeInfo?.model ?? agent.config.model ?? null,
2703
+ }).ledger,
2704
+ resumeState: touchHarnessResumeState(harnessExtra?.resumeState, {
2705
+ status: "live",
2706
+ source: harnessExtra?.resumeState?.source ?? "snapshot",
2707
+ timestamp,
2708
+ recoveryHint: resolvedOutcome === "provider_error"
2709
+ ? "The previous run failed before completion."
2710
+ : resolvedOutcome === "canceled"
2711
+ ? "The previous run was interrupted."
2712
+ : null,
2713
+ }),
2254
2714
  providerInstructionStrategy: harnessExtra?.providerInstructionStrategy ??
2255
2715
  resolveProviderInstructionStrategy(agent.provider),
2716
+ };
2717
+ if (harnessExtra?.sessionState?.verificationState !== sessionState.verificationState) {
2718
+ this.recordHarnessEvent(agent, {
2719
+ kind: "verification_changed",
2720
+ summary: `Verification is now ${sessionState.verificationState}.`,
2721
+ verificationState: sessionState.verificationState,
2722
+ details: {
2723
+ nextRequiredAction: sessionState.nextRequiredAction,
2724
+ },
2725
+ });
2726
+ }
2727
+ const previousWorkerKeys = Object.keys(harnessExtra?.workers ?? {}).sort();
2728
+ const nextWorkerKeys = Object.keys(nextWorkers).sort();
2729
+ if (!isDeepStrictEqual(previousWorkerKeys, nextWorkerKeys)) {
2730
+ for (const workerId of nextWorkerKeys.filter((workerId) => !previousWorkerKeys.includes(workerId))) {
2731
+ this.recordHarnessEvent(agent, {
2732
+ kind: "worker_updated",
2733
+ summary: `Tracked worker ${nextWorkers[workerId]?.purpose ?? workerId}.`,
2734
+ workerId,
2735
+ details: {
2736
+ status: nextWorkers[workerId]?.status ?? null,
2737
+ },
2738
+ });
2739
+ }
2740
+ }
2741
+ if (this.checkpointStorage && turnSummary.changedFiles.length > 0) {
2742
+ const checkpointId = this.idFactory();
2743
+ const task = this.captureCheckpoint(agent, {
2744
+ id: checkpointId,
2745
+ kind: "turn_final",
2746
+ objective: currentTurn.objective,
2747
+ summary: turnSummary.uiSummary,
2748
+ changedFiles: turnSummary.changedFiles,
2749
+ turnStartSeq: turnSummary.turnStartSeq,
2750
+ turnEndSeq: turnSummary.turnEndSeq,
2751
+ harnessSnapshot: nextExtra,
2752
+ }).then((record) => {
2753
+ if (!record) {
2754
+ return;
2755
+ }
2756
+ if (!this.isLiveAgent(agent)) {
2757
+ return;
2758
+ }
2759
+ this.publishCapturedCheckpoint(agent, this.checkpointStorage.toSummary(record), {
2760
+ eventSummary: "Created a restorable checkpoint for this completed turn.",
2761
+ details: {
2762
+ checkpointKind: "turn_final",
2763
+ turnEndSeq: turnSummary.turnEndSeq,
2764
+ changedFiles: turnSummary.changedFiles,
2765
+ },
2766
+ });
2767
+ });
2768
+ this.trackBackgroundTask(task);
2769
+ }
2770
+ setHarnessSessionExtra(agent.config, nextExtra);
2771
+ }
2772
+ async listAgentCheckpoints(agentId) {
2773
+ if (!this.checkpointStorage) {
2774
+ return [];
2775
+ }
2776
+ return (await this.checkpointStorage.list(agentId)).map((record) => this.checkpointStorage.toSummary(record));
2777
+ }
2778
+ async restoreAgentCheckpoint(agentId, checkpointId) {
2779
+ if (!this.checkpointStorage) {
2780
+ throw new Error("Checkpoint storage is not configured.");
2781
+ }
2782
+ const agent = this.requireAgent(agentId);
2783
+ if (agent.lifecycle === "running" || agent.pendingRun) {
2784
+ await this.cancelAgentRun(agentId);
2785
+ }
2786
+ const record = await this.checkpointStorage.get(agentId, checkpointId);
2787
+ if (!record) {
2788
+ throw new Error(`Checkpoint not found: ${checkpointId}`);
2789
+ }
2790
+ await this.checkpointStorage.restore(record);
2791
+ agent.config.systemPrompt = record.harness.baseSystemPrompt?.trim() || undefined;
2792
+ setHarnessSessionExtra(agent.config, {
2793
+ ...record.harness,
2794
+ currentTurn: undefined,
2795
+ checkpoints: this.upsertCheckpointSummary(record.harness.checkpoints, {
2796
+ id: record.id,
2797
+ kind: record.kind,
2798
+ createdAt: record.createdAt,
2799
+ objective: record.objective,
2800
+ summary: record.summary,
2801
+ changedFiles: record.changedFiles,
2802
+ restorable: record.restorable,
2803
+ turnStartSeq: record.turnStartSeq,
2804
+ turnEndSeq: record.turnEndSeq,
2805
+ }),
2806
+ lastCheckpointId: record.id,
2807
+ harnessVersion: JUNCTION_HARNESS_VERSION,
2808
+ resumeState: touchHarnessResumeState(record.harness.resumeState, {
2809
+ status: "checkpoint_restored",
2810
+ source: "checkpoint",
2811
+ timestamp: new Date().toISOString(),
2812
+ recoveryHint: "Workspace restored from a saved checkpoint.",
2813
+ }),
2814
+ });
2815
+ this.recordHarnessEvent(agent, {
2816
+ kind: "checkpoint_restored",
2817
+ summary: "Restored the workspace to the selected checkpoint.",
2818
+ checkpointId: record.id,
2819
+ details: {
2820
+ changedFiles: record.changedFiles,
2821
+ turnEndSeq: record.turnEndSeq,
2822
+ },
2256
2823
  });
2824
+ this.touchUpdatedAt(agent);
2825
+ this.emitState(agent);
2826
+ await this.persistSnapshot(agent);
2827
+ await this.refreshRuntimeInfo(agent);
2828
+ return this.agents.get(agentId) ?? agent;
2257
2829
  }
2258
2830
  requireClient(provider) {
2259
2831
  const client = this.clients.get(provider);