@tea-agent/loop-agent 0.40.0-next.8 → 0.40.0

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 (150) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +21 -20
  3. package/README.md +2 -2
  4. package/dist/application/dag/generate-task-dag.js +6 -2
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/command-definitions.js +9 -5
  7. package/dist/cli/program.js +38 -3
  8. package/dist/commands/client-recovery.js +8 -36
  9. package/dist/commands/init-model-catalog.js +22 -0
  10. package/dist/commands/task-advance.js +23 -1
  11. package/dist/commands/task-source-prepare.js +5 -0
  12. package/dist/commands/task-status.js +1 -0
  13. package/dist/executors/dag-pi-executor.js +3 -0
  14. package/dist/executors/pi-executor.js +4 -2
  15. package/dist/executors/pi-sdk-executor.js +79 -6
  16. package/dist/executors/shell-executor.js +94 -20
  17. package/dist/governance/checks.js +1 -0
  18. package/dist/shared/operator/capabilities.js +9 -1
  19. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  20. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  21. package/dist/shared/pi-context-pressure/env.js +58 -0
  22. package/dist/shared/pi-context-pressure/extension.js +100 -0
  23. package/dist/shared/pi-context-pressure/index.js +7 -0
  24. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  25. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  26. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  27. package/dist/worker/console/chat/assistant-content.js +7 -0
  28. package/dist/worker/console/chat/chat-event-store.js +82 -11
  29. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  30. package/dist/worker/console/chat/goals.js +300 -0
  31. package/dist/worker/console/chat/pi-runtime.js +285 -8
  32. package/dist/worker/console/chat/provider-error.js +2 -1
  33. package/dist/worker/console/chat/repo-browser.js +94 -12
  34. package/dist/worker/console/chat/routes.js +495 -30
  35. package/dist/worker/console/chat/session-catalog.js +48 -0
  36. package/dist/worker/console/chat/session-stats.js +96 -0
  37. package/dist/worker/console/chat/session-store.js +27 -18
  38. package/dist/worker/console/chat/shortcuts.js +7 -0
  39. package/dist/worker/console/chat/sift-bridge.js +1 -0
  40. package/dist/worker/console/chat/turn-process.js +42 -5
  41. package/dist/worker/console/dag-execution-receipt.js +20 -2
  42. package/dist/worker/console/operator-actions.js +20 -5
  43. package/dist/worker/console/operator-selection.js +6 -2
  44. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  45. package/dist/worker/console/pi-plugins.js +179 -0
  46. package/dist/worker/console/routes.js +246 -6
  47. package/dist/worker/console/server.js +173 -42
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BYBmDfPW.js → abnfDiagram-N423BO3Z-Dy71QJp3.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-BKMmxpGF.js → arc-M25fNMTE.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DJpwxr1d.js → architectureDiagram-T3A2C74G-CUWlVl1a.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BaPj_ZlY.js → blockDiagram-VBNYF7ZC-Dd0Whpx2.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8iGmHlm.js → c4Diagram-5PPSVZJV-BdfeCkI5.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-Ba0AKiEZ.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DaxXQayc.js → chunk-2GRJ4B5K-D_1sUkyD.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DqQKGA7q.js → chunk-2Q5K7J3B-Cxnxf36E.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CgMKpJBB.js → chunk-5RXB4S5H-D9RG3G_x.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BpToUlaW.js → chunk-5VM5RSS4-Zz9jgjLX.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BsEWOHxv.js → chunk-6Q2QTUOP-BCggDRCH.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CKGSCe71.js → chunk-GF5L2VYU-CFg-5iAS.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Bm1U5299.js → chunk-JWPE2WC7-DRQTE8x_.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BI0ov870.js → chunk-KBJHAD2P-C_Dw7uZ8.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BmnQo0iJ.js → chunk-RYQCIY6F-BjIiApsk.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BJBqa6Xc.js → chunk-XXDRQBXY-B5XQD4KG.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BU3IjVEe.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BU3IjVEe.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BuN9-7AR.js → cose-bilkent-JH36ORCC-huHqH6WQ.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bw22jc2k.js → cynefin-VYW2F7L2-BSi6yZqP.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BHnSH8Dr.js → cynefinDiagram-MW4NZA55-Bs_Jp31y.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BoXXIegj.js → dagre-VZM6K2ZE-CYxavikT.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CU8qyPhT.js → diagram-7IWD3JNH-B6TErl71.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-lrjhF0S8.js → diagram-B4RE2ZJO-CvH4J2ii.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DL0QDEpo.js → diagram-LBJQPF4R-v1NBAqXS.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DxVMJMBH.js → diagram-Q27KOJAE-CUNHVAn2.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CFg9FzvG.js → diagram-UB23O5K3-Dzz1gUu-.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CQgP9fTG.js → ebnfDiagram-BXEA7PRR-CCF8_LjK.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-A5vQXx9U.js → erDiagram-JOGREHBK-DObdPVLa.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-ytXnLyXD.js → flowDiagram-UKHOOZJN-DlN8Pxrj.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CYuksfnS.js → ganttDiagram-PKOTCBZU-BJKIPTGX.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-f_jW3zgU.js → gitGraphDiagram-DS77QQ5N-XcSSnObi.js} +1 -1
  80. package/dist/worker/console/static/assets/index-CIYzS16i.css +1 -0
  81. package/dist/worker/console/static/assets/index-CS3QOTfl.js +451 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C3HLE-xV.js → infoDiagram-6WML65LV-BKrRKMww.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DwpgejS6.js → ishikawaDiagram-WSZJBQD7-CN5dk-yb.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ll0YWX1y.js → journeyDiagram-NVQOT4AX-B4P4KZ-Y.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Hq3rchvU.js → kanban-definition-27J2QSJJ-BRlteP32.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-BaGjJx8z.js → linear-CG8D9fuL.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-CKGQHWX_.js → mermaid.core-BuB6P2rX.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DM1FA6Dr.js → mindmap-definition-FAOFIHXS-DYXGl_DR.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Xa9fHkE2.js → pegDiagram-VL7TDLO6-BM48yEVG.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-pCd4PDqJ.js → pieDiagram-7S7Q4E2Y-mT8NL-Ow.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-KqjpEBLk.js → quadrantDiagram-CIZ2JOQS-C4pZ6DH_.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DgvR7LZD.js → railroadDiagram-AXF67PYL-BKAcr70m.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Bys1UCWA.js → requirementDiagram-LRYGKXZP-CRxcKpoO.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Ms9npnZt.js → sankeyDiagram-W5VNT64P-Ds4LxQDI.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DEIWrtQZ.js → sequenceDiagram-SI44F4Z6-CPLwPf_p.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DELJ0AhZ.js → sizeCapture-X5ZJPWSS-CL65uij0.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BpHt9wKh.js → stateDiagram-OKZ733FA-CO0Mgd1W.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BUewAVYS.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-GKRsBluT.js → swimlanes-SLNWSIFB-Cxv3TSNw.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bhe67Wjl.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cwsq1DxL.js → timeline-definition-Z64GVDOM-Dw85f9L2.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CfaqtKx0.js → vennDiagram-T6HMQDX7-B4QLCqCy.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BlEUtK_g.js → wardleyDiagram-T6FBY63Y-B_yKCWwn.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CHSRXPvO.js → xychartDiagram-ELKLHX3M-nniBEGv2.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  107. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  108. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  109. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  110. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
  111. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  112. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
  113. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  114. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  115. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  116. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  117. package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
  118. package/dist/worker/console/workspace-context.js +28 -3
  119. package/dist/worker/console/workspace-registry.js +333 -60
  120. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  121. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  122. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  123. package/dist/worker/observe/static/operator-chrome.css +23 -4
  124. package/dist/worker/observe/static/operator-chrome.js +7 -2
  125. package/dist/worker/observe/static/styles.css +87 -25
  126. package/dist/worker/observe/static/views/dag-inspector.js +42 -1
  127. package/dist/worker/observe/static/views/dags.js +7 -5
  128. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  129. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
  130. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  131. package/dist/workflows/dag/backend-test-result-contract.js +5 -1
  132. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  133. package/dist/workflows/dag/backend-test-writer-completeness.js +18 -12
  134. package/dist/workflows/dag/init-hybrid.js +32 -9
  135. package/dist/workflows/dag/types.js +13 -0
  136. package/docs/architecture/runtime-boundaries.md +2 -1
  137. package/docs/templates/agent-dag.schema.json +18 -0
  138. package/docs/templates/backend-test-dag.json +27 -9
  139. package/docs/templates/init-managed-agents.md +1 -1
  140. package/package.json +3 -2
  141. package/skills/loop-agent/references/command-reference.md +15 -1
  142. package/dist/worker/console/static/assets/channel-CrzAa3FA.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-B7QwCCb4.js +0 -1
  144. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-B7QwCCb4.js +0 -1
  145. package/dist/worker/console/static/assets/index-BB6KxwT_.css +0 -1
  146. package/dist/worker/console/static/assets/index-D_wxbadT.js +0 -407
  147. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BEeSswlv.js +0 -1
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B2m_wca8.js +0 -8
  149. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  150. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -1,9 +1,12 @@
1
1
  import { appendFile, mkdir } from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
4
+ import { randomUUID } from "node:crypto";
3
5
  import { computeExtensionToolAllowlistExtras } from "./pi-extension-resolver.js";
4
6
  import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractPiEventStopReason, extractAssistantTextFromPiJson, } from "./pi-executor.js";
5
7
  import { serializeSessionEvent } from "./pi-event-serializer.js";
6
8
  import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
9
+ import { createCompactionController, createContextPressureExtension, createSiftBridgeState, inMemoryCompactionSettings, isDagSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, SIFT_RETRIEVE_TOOL_NAME, } from "../shared/pi-context-pressure/index.js";
7
10
  /** Same-session recovery after a transport stall. Exactly one continue per SDK attempt. */
8
11
  export const PI_SDK_STALL_CONTINUE_MAX = 1;
9
12
  /**
@@ -215,15 +218,37 @@ async function createSdkSession(sdk, input, shared) {
215
218
  // Inject rate-limit-tolerant auto-retry settings for the session lifecycle.
216
219
  // Prefer SettingsManager.inMemory so harness runs do not depend on ~/.pi or
217
220
  // project .pi/settings.json being present/trusted.
221
+ const policy = parseCompactionPolicy();
222
+ const compaction = {
223
+ ...PI_RECOMMENDED_COMPACTION,
224
+ ...inMemoryCompactionSettings(policy),
225
+ };
218
226
  const settingsManager = typeof SettingsManager?.inMemory === "function"
219
227
  ? SettingsManager.inMemory({
220
228
  retry: {
221
229
  ...PI_RECOMMENDED_RETRY,
222
230
  provider: { ...PI_RECOMMENDED_RETRY.provider },
223
231
  },
224
- compaction: { ...PI_RECOMMENDED_COMPACTION },
232
+ compaction,
225
233
  }, { projectTrusted: true })
226
234
  : undefined;
235
+ const nodeSessionId = `dag-${randomUUID()}`;
236
+ const controller = createCompactionController();
237
+ const siftEnabled = isDagSiftEnabled();
238
+ const sift = siftEnabled.enabled
239
+ ? createSiftBridgeState(parseSiftBridgeConfig({
240
+ sessionId: nodeSessionId,
241
+ defaultStashRoot: path.join(os.tmpdir(), "loop-agent-dag-sift"),
242
+ }))
243
+ : undefined;
244
+ if (sift)
245
+ sift.counters = controller.counters;
246
+ const pressureExtension = createContextPressureExtension({
247
+ name: "dag-pi-context-pressure",
248
+ policy,
249
+ controller,
250
+ sift,
251
+ });
227
252
  const loader = new DefaultResourceLoader({
228
253
  cwd: input.cwd,
229
254
  agentDir,
@@ -233,6 +258,7 @@ async function createSdkSession(sdk, input, shared) {
233
258
  ...(input.additionalExtensionPaths && input.additionalExtensionPaths.length > 0
234
259
  ? { additionalExtensionPaths: input.additionalExtensionPaths }
235
260
  : {}),
261
+ ...(pressureExtension ? { extensionFactories: [pressureExtension] } : {}),
236
262
  ...(settingsManager ? { settingsManager } : {}),
237
263
  appendSystemPromptOverride: (base) => [
238
264
  ...base,
@@ -247,10 +273,12 @@ async function createSdkSession(sdk, input, shared) {
247
273
  // extras into the create-time allowlist; protected builtins, the deny
248
274
  // surface, and write-capable extension tools never join.
249
275
  const extensionToolExtras = [];
250
- if (input.additionalExtensionPaths &&
251
- input.additionalExtensionPaths.length > 0) {
252
- const registered = collectLoaderExtensionToolNames(loader);
253
- extensionToolExtras.push(...computeExtensionToolAllowlistExtras(registered.map((name) => ({ name })), input.toolNames));
276
+ const registered = collectLoaderExtensionToolNames(loader);
277
+ extensionToolExtras.push(...computeExtensionToolAllowlistExtras(registered.map((name) => ({ name })), input.toolNames));
278
+ if (siftEnabled.enabled &&
279
+ !extensionToolExtras.includes(SIFT_RETRIEVE_TOOL_NAME) &&
280
+ !input.toolNames.includes(SIFT_RETRIEVE_TOOL_NAME)) {
281
+ extensionToolExtras.push(...computeExtensionToolAllowlistExtras([{ name: SIFT_RETRIEVE_TOOL_NAME }], input.toolNames));
254
282
  }
255
283
  const sessionToolNames = [...input.toolNames, ...extensionToolExtras];
256
284
  if (input.requireWriterCustomTools) {
@@ -485,6 +513,9 @@ function extractSdkUsageSample(event) {
485
513
  const responseKey = responseKeyCandidates.find((value) => typeof value === "string" && value.length > 0);
486
514
  return responseKey ? { responseKey, tokens } : { tokens };
487
515
  }
516
+ function maxSdkContextTokens(samples) {
517
+ return samples.reduce((maximum, sample) => Math.max(maximum, sample.tokens), 0);
518
+ }
488
519
  function aggregateSdkTokenUsage(samples) {
489
520
  const identified = new Map();
490
521
  let anonymousMaximum = 0;
@@ -537,6 +568,9 @@ export async function executeSingleSdkAttempt(options) {
537
568
  const stdoutPreview = new BoundedTextPreview("stdout");
538
569
  const stdoutCollector = createPiJsonlStreamCollector();
539
570
  const usageSamples = [];
571
+ let observedTurns = 0;
572
+ let contextBudgetExhausted = false;
573
+ let contextBudgetDetail = "";
540
574
  // Writer-thinking-exhausted diagnosis evidence (AC-002). Collected in the
541
575
  // subscribe callback regardless of shouldPersistSessionEvent filtering, so a
542
576
  // length-stopped thinking-only attempt with no persisted write tool calls is
@@ -625,6 +659,7 @@ export async function executeSingleSdkAttempt(options) {
625
659
  });
626
660
  let resolveStall;
627
661
  let resolveSettlement;
662
+ let resolveContextBudget;
628
663
  let settled = false;
629
664
  let abortConfirmed = true;
630
665
  const persistRuntimeNote = (event) => {
@@ -655,6 +690,30 @@ export async function executeSingleSdkAttempt(options) {
655
690
  clearTimeout(stallHandle);
656
691
  stallHandle = setTimeout(() => resolveStall?.("stall"), stallTimeoutMs);
657
692
  };
693
+ const checkContextBudget = () => {
694
+ if (contextBudgetExhausted || !options.contextBudget)
695
+ return;
696
+ // Context protection follows the largest provider-reported response
697
+ // snapshot, not cumulative billing usage across turns. Summing repeated
698
+ // input contexts would abort healthy multi-turn sessions far below the
699
+ // model's actual context window.
700
+ const observedTokens = maxSdkContextTokens(usageSamples);
701
+ const turnExceeded = options.contextBudget.maxTurns !== undefined &&
702
+ observedTurns >= options.contextBudget.maxTurns;
703
+ const tokenExceeded = options.contextBudget.maxTokens !== undefined &&
704
+ observedTokens >= options.contextBudget.maxTokens;
705
+ if (!turnExceeded && !tokenExceeded)
706
+ return;
707
+ contextBudgetExhausted = true;
708
+ contextBudgetDetail = [
709
+ "context-budget-exhausted:",
710
+ `observedTurns=${observedTurns}`,
711
+ `observedTokens=${observedTokens}`,
712
+ `maxTurns=${options.contextBudget.maxTurns ?? "unbounded"}`,
713
+ `maxTokens=${options.contextBudget.maxTokens ?? "unbounded"}`,
714
+ ].join(" ");
715
+ resolveContextBudget?.("context-budget");
716
+ };
658
717
  unsubscribe = session.subscribe((event) => {
659
718
  if (event.type === "agent_settled" && !settled) {
660
719
  settled = true;
@@ -678,6 +737,9 @@ export async function executeSingleSdkAttempt(options) {
678
737
  const usageSample = extractSdkUsageSample(event);
679
738
  if (usageSample)
680
739
  usageSamples.push(usageSample);
740
+ if (event.type === "turn_end")
741
+ observedTurns += 1;
742
+ checkContextBudget();
681
743
  // Capture writer-thinking-exhausted evidence BEFORE the
682
744
  // shouldPersistSessionEvent filter: thinking_delta is intentionally
683
745
  // excluded from persisted JSONL, but must still count toward the
@@ -700,6 +762,11 @@ export async function executeSingleSdkAttempt(options) {
700
762
  const runTurn = async (userMessage) => {
701
763
  const settlementPromise = resetSettlement();
702
764
  const stallPromise = resetStallPromise();
765
+ const contextBudgetPromise = options.contextBudget
766
+ ? new Promise((resolve) => {
767
+ resolveContextBudget = resolve;
768
+ })
769
+ : null;
703
770
  const promptPromise = session.prompt(userMessage);
704
771
  // A settlement event is permitted to win the race while prompt() remains
705
772
  // pending. Observe a later rejection so it never becomes unhandled.
@@ -710,6 +777,7 @@ export async function executeSingleSdkAttempt(options) {
710
777
  settlementPromise,
711
778
  ...(timeoutPromise ? [timeoutPromise] : []),
712
779
  ...(stallPromise ? [stallPromise] : []),
780
+ ...(contextBudgetPromise ? [contextBudgetPromise] : []),
713
781
  ]);
714
782
  };
715
783
  const disposeSession = async () => {
@@ -737,7 +805,12 @@ export async function executeSingleSdkAttempt(options) {
737
805
  : options.userMessage;
738
806
  let continuesSent = 0;
739
807
  let raced = await runTurn(promptMessage);
740
- if (raced === "stall") {
808
+ if (raced === "context-budget") {
809
+ appendStderr(contextBudgetDetail);
810
+ abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
811
+ await disposeSession();
812
+ }
813
+ else if (raced === "stall") {
741
814
  abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
742
815
  const withinAbsoluteTimeout = timeoutMs <= 0 || Date.now() - startedAt < timeoutMs;
743
816
  const canContinue = abortConfirmed &&
@@ -32,11 +32,11 @@ import { materializeBackendTestExecutionContract } from "../workflows/dag/backen
32
32
  import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
33
33
  import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
34
34
  import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
35
- import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
35
+ import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, classifyBackendTestCorrespondenceRepair, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
36
36
  import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
37
37
  import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, markdownFiles, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
38
38
  import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
39
- import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
39
+ import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, summarizeBackendTestExclusions, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
40
40
  import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
41
41
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
42
42
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
@@ -870,26 +870,49 @@ async function executeBackendTestPipeline(input, meta) {
870
870
  const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
871
871
  await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
872
872
  outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
873
- if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
874
- const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
875
- .filter((entry) => entry.status !== "EXACT_1_TO_1")
876
- .flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
877
- .filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
878
- if (correspondenceRepairPaths.length > 0) {
879
- facts.status = "REPAIRABLE";
880
- facts.repairEligible = true;
881
- facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
873
+ if (correspondence.facts.status === "FAIL") {
874
+ const repair = classifyBackendTestCorrespondenceRepair(correspondence.facts, backendLayout);
875
+ if (repair.markdownBlockingEntries.length > 0) {
876
+ facts.status = "BLOCKED";
877
+ facts.repairEligible = false;
878
+ facts.repairPaths = [];
882
879
  facts.findings.push({
883
- kind: "markdown-pytest-correspondence",
880
+ kind: "markdown-correspondence-contract",
884
881
  classification: "test-asset-defect",
885
- repairability: "repairable",
886
- detail: correspondence.facts.entries
887
- .filter((entry) => entry.status !== "EXACT_1_TO_1")
888
- .flatMap((entry) => entry.findings)
882
+ repairability: "blocked",
883
+ detail: repair.markdownBlockingEntries
884
+ .map((entry) => `${entry.caseId ?? "unknown Case"}: ${entry.reasonCodes.join(", ")}`)
889
885
  .join("; ")
890
- .slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
886
+ .slice(0, 12_000),
891
887
  });
892
888
  }
889
+ else if (repair.pytestRepairPaths.length > 0 &&
890
+ (facts.status === "PASS" || facts.status === "REPAIRABLE")) {
891
+ facts.status = "REPAIRABLE";
892
+ facts.repairEligible = true;
893
+ facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...repair.pytestRepairPaths])).sort();
894
+ const repairEntries = correspondence.facts.entries.filter((entry) => repair.pytestRepairEntries.some((candidate) => candidate.caseId === entry.caseId));
895
+ for (const entry of repairEntries.slice(0, 40)) {
896
+ const classified = repair.pytestRepairEntries.find((candidate) => candidate.caseId === entry.caseId);
897
+ facts.findings.push({
898
+ kind: "markdown-pytest-correspondence",
899
+ classification: "test-asset-defect",
900
+ repairability: "repairable",
901
+ detail: entry.findings.join("; ").slice(0, 2_000) || "Markdown-to-pytest correspondence is incomplete",
902
+ ...(entry.caseId ? { caseId: entry.caseId } : {}),
903
+ reasonCodes: classified?.reasonCodes ?? [],
904
+ repairPaths: classified?.paths ?? [],
905
+ payloadExpectedPaths: Array.from(new Set([
906
+ ...entry.payloadAssessment.requiredPaths,
907
+ ...entry.payloadAssessment.allowedPaths,
908
+ ])).sort(),
909
+ payloadObservedPaths: entry.payloadAssessment.observedPaths,
910
+ payloadMissingPaths: entry.payloadAssessment.missingPaths,
911
+ payloadUnexpectedPaths: entry.payloadAssessment.unexpectedPaths,
912
+ payloadEnumMismatches: entry.payloadAssessment.enumMismatches,
913
+ });
914
+ }
915
+ }
893
916
  }
894
917
  }
895
918
  catch (error) {
@@ -912,6 +935,19 @@ async function executeBackendTestPipeline(input, meta) {
912
935
  collectedItemCount: facts.collectedItemCount,
913
936
  missingMappedScripts: facts.missingMappedScripts,
914
937
  repairPaths: facts.repairPaths,
938
+ repairFindings: facts.findings.map((finding) => ({
939
+ kind: finding.kind,
940
+ repairability: finding.repairability,
941
+ detail: finding.detail,
942
+ ...(finding.caseId ? { caseId: finding.caseId } : {}),
943
+ ...(finding.reasonCodes ? { reasonCodes: finding.reasonCodes } : {}),
944
+ ...(finding.repairPaths ? { repairPaths: finding.repairPaths } : {}),
945
+ ...(finding.payloadExpectedPaths ? { payloadExpectedPaths: finding.payloadExpectedPaths } : {}),
946
+ ...(finding.payloadObservedPaths ? { payloadObservedPaths: finding.payloadObservedPaths } : {}),
947
+ ...(finding.payloadMissingPaths ? { payloadMissingPaths: finding.payloadMissingPaths } : {}),
948
+ ...(finding.payloadUnexpectedPaths ? { payloadUnexpectedPaths: finding.payloadUnexpectedPaths } : {}),
949
+ ...(finding.payloadEnumMismatches ? { payloadEnumMismatches: finding.payloadEnumMismatches } : {}),
950
+ })),
915
951
  factsPath: "contracts/backend-test-pytest-collection-initial.json",
916
952
  }),
917
953
  stderr: "",
@@ -1011,6 +1047,7 @@ async function executeBackendTestPipeline(input, meta) {
1011
1047
  })
1012
1048
  : { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
1013
1049
  const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
1050
+ const exclusionSummary = summarizeBackendTestExclusions(eligibility.excludedItems);
1014
1051
  const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
1015
1052
  await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
1016
1053
  await writeFile(eligibilityFactsPath, `${JSON.stringify({
@@ -1021,6 +1058,9 @@ async function executeBackendTestPipeline(input, meta) {
1021
1058
  excludedItemCount: eligibility.excludedItems.length,
1022
1059
  eligibleItemIds: eligibility.eligibleItemIds,
1023
1060
  excludedItems: eligibility.excludedItems,
1061
+ exclusionSummary,
1062
+ pytestSpawned: false,
1063
+ businessTestBodyExecutedCount: 0,
1024
1064
  inputHashes: eligibilityInputHashes,
1025
1065
  }, null, 2)}\n`, "utf8");
1026
1066
  const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
@@ -1033,6 +1073,10 @@ async function executeBackendTestPipeline(input, meta) {
1033
1073
  `- Collected items: ${effective.collectedItemIds.length}`,
1034
1074
  `- Eligible items: ${eligibility.eligibleItemIds.length}`,
1035
1075
  `- Excluded items: ${eligibility.excludedItems.length}`,
1076
+ `- Excluded Cases: ${exclusionSummary.excludedCaseCount}`,
1077
+ `- Exclusion categories: ${Object.entries(exclusionSummary.excludedByReason).map(([category, count]) => `${category}=${count}`).join("; ") || "none"}`,
1078
+ "- pytest spawned: false",
1079
+ "- Business test bodies executed: 0",
1036
1080
  "",
1037
1081
  "## Excluded Items",
1038
1082
  "",
@@ -1061,6 +1105,9 @@ async function executeBackendTestPipeline(input, meta) {
1061
1105
  executionReadinessStatus: readiness.status,
1062
1106
  eligibleItemCount: readiness.eligibleItemIds.length,
1063
1107
  excludedItemCount: readiness.excludedItems.length,
1108
+ exclusionSummary: readiness.exclusionSummary,
1109
+ pytestSpawned: readiness.pytestSpawned,
1110
+ businessTestBodyExecutedCount: readiness.businessTestBodyExecutedCount,
1064
1111
  factsPath: "contracts/backend-test-pytest-collection-effective.json",
1065
1112
  readinessPath: "contracts/backend-test-execution-readiness.json",
1066
1113
  reportPath: artifacts.reportPath,
@@ -1183,7 +1230,33 @@ async function executeBackendTestPipeline(input, meta) {
1183
1230
  const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
1184
1231
  const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
1185
1232
  await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
1186
- const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
1233
+ const pytestLauncherRelative = "reports/backend-test-pytest-launcher.py";
1234
+ const pytestLauncherAbsolute = path.join(meta.runDir, pytestLauncherRelative);
1235
+ await writeFile(pytestLauncherAbsolute, [
1236
+ "import os",
1237
+ "import pathlib",
1238
+ "import sys",
1239
+ "",
1240
+ "sys.path[0] = os.getcwd()",
1241
+ "import pytest",
1242
+ "",
1243
+ "if len(sys.argv) != 3:",
1244
+ " print('usage: backend-test-pytest-launcher.py <targets-file> <html-file>', file=sys.stderr)",
1245
+ " raise SystemExit(4)",
1246
+ "targets_path = pathlib.Path(sys.argv[1])",
1247
+ "html_path = pathlib.Path(sys.argv[2])",
1248
+ "try:",
1249
+ " targets = targets_path.read_text(encoding='utf-8').splitlines()",
1250
+ "except (OSError, UnicodeError) as error:",
1251
+ " print(f'backend-test targets unreadable: {error}', file=sys.stderr)",
1252
+ " raise SystemExit(4)",
1253
+ "if not targets or any(not target or target != target.strip() for target in targets):",
1254
+ " print('backend-test targets must contain non-empty exact pytest node IDs', file=sys.stderr)",
1255
+ " raise SystemExit(4)",
1256
+ "pytest_args = [*targets, '-v', '-p', 'no:cacheprovider', f'--html={html_path}', '--self-contained-html']",
1257
+ "raise SystemExit(pytest.main(pytest_args))",
1258
+ "",
1259
+ ].join("\n"), "utf8");
1187
1260
  // Split into short bash -c commands (aligned with markdown-environment).
1188
1261
  // A single ultra-long compound command has been observed on Windows to
1189
1262
  // exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
@@ -1260,10 +1333,11 @@ async function executeBackendTestPipeline(input, meta) {
1260
1333
  'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
1261
1334
  'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
1262
1335
  'echo "STEP=pytest-run"',
1263
- `echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1336
+ `echo "- pytestCommand: controller launcher -> pytest.main(<${eligibleItemIds.length} eligibility-authorized-items>, -v, -p no:cacheprovider, --html=reports/backend-test.html, --self-contained-html)" >> "\${DIAG_FILE}"`,
1264
1337
  `echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
1265
1338
  `echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
1266
- `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargetsArg} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
1339
+ 'rm -f "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-native.html"',
1340
+ `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" "\${HARNESS_DAG_RUN_DIR}/${pytestLauncherRelative}" "\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}" "\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html"`,
1267
1341
  "STATUS=$?",
1268
1342
  'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
1269
1343
  'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
@@ -8,6 +8,7 @@ import { buildTaskReadModel } from '../task/read-model.js';
8
8
  const SKIP_DIRS = new Set([
9
9
  '.git',
10
10
  '.harness',
11
+ '.scratch',
11
12
  'node_modules',
12
13
  'dist',
13
14
  'coverage',
@@ -51,6 +51,8 @@ const OFFICIAL_ACTION_DESCRIPTIONS = {
51
51
  workerSchedulerStatus: "Night-scheduler status overview (agent-worker). Use to check whether overnight jobs are healthy.",
52
52
  workerSchedulerLedger: "Night-scheduler harvest ledger (agent-worker). Use to audit what overnight runs produced.",
53
53
  workerSchedulerDoctor: "Diagnose night-scheduler health (agent-worker). Use when overnight jobs fail to fire or harvest.",
54
+ dagResume: "Unpause a run paused at a human decision checkpoint. Orphaned runs (dead runner, liveness=orphaned) are NOT continuable — do not resume them: use dagReconcileRun --action supersede to archive and then advance/rerun the task fresh.",
55
+ dagReconcileRun: "Reconcile an orphaned run (dead runner, liveness=orphaned). These are NOT continuable: supersede archives the run for a fresh same-task advance/rerun; abandon discards it. resume only unpauses human-decision checkpoints — never for orphaned runs.",
54
56
  };
55
57
  const officialAction = (action, cli, kind, coverage) => ({
56
58
  action,
@@ -817,7 +819,7 @@ export function buildOperatorCapabilitiesDocument() {
817
819
  "BINDING_DRIFT",
818
820
  "HUMAN_CONFIRMATION_REQUIRED",
819
821
  ],
820
- description: "Issue a session-bound single-use execution receipt after server-side G2 assessment (bounded writeSet ⊆ allowedPaths, no forbidden overlap, no broad/destructive risk, structured verification). The model never passes a raw DAG path.",
822
+ description: "Returns SYNCHRONOUSLY (not an operation): it immediately yields a session-bound single-use executionId + receipt, which you then pass to runDag to actually execute. It is a server-side G2 assessment (bounded writeSet ⊆ allowedPaths, no forbidden overlap, no broad/destructive risk, structured verification). The model never passes a raw DAG path.",
821
823
  inputParams: [
822
824
  {
823
825
  name: "taskId",
@@ -1067,6 +1069,12 @@ export function buildOperatorCapabilitiesDocument() {
1067
1069
  required: false,
1068
1070
  description: "verification entries as label:command strings",
1069
1071
  },
1072
+ {
1073
+ name: "approveGate",
1074
+ type: "string",
1075
+ required: false,
1076
+ description: "optional write-set gate approval token `<gate-id>:sha256:<64-hex>` exactly as reported by task status gate.approveCommand; forwards as --approve-gate and executes the approved DAG as this background operation (preferred over a foreground terminal)",
1077
+ },
1070
1078
  ],
1071
1079
  modelCallable: "always",
1072
1080
  humanConfirmation: "none",
@@ -0,0 +1,116 @@
1
+ const CHECKPOINT_MAX_BYTES = 12 * 1024;
2
+ function boundText(value, maxChars) {
3
+ if (typeof value !== "string" || !value.trim())
4
+ return "";
5
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars - 1)}…`;
6
+ }
7
+ function asRecord(value) {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value)
9
+ ? value
10
+ : undefined;
11
+ }
12
+ function extractLastUserGoal(messages) {
13
+ if (!Array.isArray(messages))
14
+ return "";
15
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
16
+ const message = asRecord(messages[index]);
17
+ if (!message || message.role !== "user")
18
+ continue;
19
+ const content = message.content;
20
+ if (typeof content === "string")
21
+ return boundText(content, 800);
22
+ if (Array.isArray(content)) {
23
+ const texts = content
24
+ .map((block) => {
25
+ const rec = asRecord(block);
26
+ return typeof rec?.text === "string" ? rec.text : "";
27
+ })
28
+ .filter(Boolean);
29
+ if (texts.length > 0)
30
+ return boundText(texts.join("\n"), 800);
31
+ }
32
+ }
33
+ return "";
34
+ }
35
+ function extractTodos(details) {
36
+ const record = asRecord(details);
37
+ const todos = record?.todos ?? record?.items;
38
+ if (!Array.isArray(todos))
39
+ return "";
40
+ const lines = todos
41
+ .slice(0, 24)
42
+ .map((item) => {
43
+ const rec = asRecord(item);
44
+ const status = typeof rec?.status === "string" ? rec.status : "pending";
45
+ const content = typeof rec?.content === "string"
46
+ ? rec.content
47
+ : typeof rec?.title === "string"
48
+ ? rec.title
49
+ : "";
50
+ return content ? `- [${status}] ${content}` : "";
51
+ })
52
+ .filter(Boolean);
53
+ return boundText(lines.join("\n"), 1200);
54
+ }
55
+ function extractFileOps(fileOps) {
56
+ const record = asRecord(fileOps);
57
+ if (!record)
58
+ return "";
59
+ const parts = [];
60
+ for (const key of ["edited", "written", "read", "deleted", "ops"]) {
61
+ const value = record[key];
62
+ if (Array.isArray(value)) {
63
+ parts.push(`${key}: ${value
64
+ .slice(0, 20)
65
+ .map((item) => (typeof item === "string" ? item : JSON.stringify(item)))
66
+ .join(", ")}`);
67
+ }
68
+ else if (typeof value === "string" && value) {
69
+ parts.push(`${key}: ${value}`);
70
+ }
71
+ }
72
+ if (parts.length === 0) {
73
+ try {
74
+ return boundText(JSON.stringify(fileOps), 800);
75
+ }
76
+ catch {
77
+ return "";
78
+ }
79
+ }
80
+ return boundText(parts.join("\n"), 800);
81
+ }
82
+ export function buildOverflowCheckpoint(input) {
83
+ const preparation = input.preparation;
84
+ const firstKeptEntryId = typeof preparation?.firstKeptEntryId === "string" &&
85
+ preparation.firstKeptEntryId
86
+ ? preparation.firstKeptEntryId
87
+ : undefined;
88
+ const tokensBefore = typeof preparation?.tokensBefore === "number" &&
89
+ Number.isFinite(preparation.tokensBefore)
90
+ ? preparation.tokensBefore
91
+ : undefined;
92
+ if (!firstKeptEntryId || tokensBefore === undefined)
93
+ return undefined;
94
+ const sections = [
95
+ "Deterministic overflow checkpoint (no extra summary request).",
96
+ boundText(preparation?.previousSummary, 2000)
97
+ ? `Previous summary:\n${boundText(preparation?.previousSummary, 2000)}`
98
+ : "",
99
+ extractLastUserGoal(preparation?.messagesToSummarize)
100
+ ? `Latest user goal:\n${extractLastUserGoal(preparation?.messagesToSummarize)}`
101
+ : "",
102
+ extractTodos(input.todos) ? `Open todos:\n${extractTodos(input.todos)}` : "",
103
+ extractFileOps(preparation?.fileOps)
104
+ ? `File ops:\n${extractFileOps(preparation?.fileOps)}`
105
+ : "",
106
+ typeof input.lastError === "string" && input.lastError
107
+ ? `Latest error:\n${boundText(input.lastError, 400)}`
108
+ : "",
109
+ "Next step: continue the interrupted turn from the kept recent messages. Do not repeat completed file writes.",
110
+ ].filter(Boolean);
111
+ let summary = sections.join("\n\n");
112
+ if (Buffer.byteLength(summary, "utf8") > CHECKPOINT_MAX_BYTES) {
113
+ summary = `${summary.slice(0, CHECKPOINT_MAX_BYTES - 1)}…`;
114
+ }
115
+ return { summary, firstKeptEntryId, tokensBefore };
116
+ }