@tea-agent/loop-agent 0.39.0-beta.11 → 0.39.0-beta.13

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 (112) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/application/dag/generate-task-dag.js +6 -2
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/client-recovery.js +8 -36
  5. package/dist/executors/dag-pi-executor.js +343 -40
  6. package/dist/executors/pi-executor.js +8 -4
  7. package/dist/executors/pi-sdk-executor.js +33 -5
  8. package/dist/executors/shell-executor.js +102 -32
  9. package/dist/governance/checks.js +1 -0
  10. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  11. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  12. package/dist/shared/pi-context-pressure/env.js +58 -0
  13. package/dist/shared/pi-context-pressure/extension.js +100 -0
  14. package/dist/shared/pi-context-pressure/index.js +7 -0
  15. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  16. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  17. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  18. package/dist/task/frontend-project-capability.js +3 -1
  19. package/dist/task/source-prepare/fragment-inventory.js +4 -1
  20. package/dist/worker/console/chat/pi-runtime.js +146 -5
  21. package/dist/worker/console/chat/provider-error.js +2 -1
  22. package/dist/worker/console/chat/routes.js +3 -0
  23. package/dist/worker/console/chat/sift-bridge.js +1 -0
  24. package/dist/worker/console/dag-execution-receipt.js +20 -2
  25. package/dist/worker/console/operator-actions.js +4 -3
  26. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CXj_GnSb.js → abnfDiagram-N423BO3Z-DC863mud.js} +1 -1
  27. package/dist/worker/console/static/assets/{arc-BZp6JAp7.js → arc-CftC38G9.js} +1 -1
  28. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DfpcEuYU.js → architectureDiagram-T3A2C74G-B3PAPQCw.js} +1 -1
  29. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-_Gf0xadb.js → blockDiagram-VBNYF7ZC-C9UDJNRv.js} +1 -1
  30. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct2QPCmv.js → c4Diagram-5PPSVZJV--BJ76fv_.js} +1 -1
  31. package/dist/worker/console/static/assets/channel-CUz-Bg86.js +1 -0
  32. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BfklkzKl.js → chunk-2GRJ4B5K--hiIqoGp.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Dy25vZJV.js → chunk-2Q5K7J3B-DgTzpIa3.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BFlCRZep.js → chunk-5RXB4S5H-DIwpJziP.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-op3oVxIE.js → chunk-5VM5RSS4-BJzbdUi0.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C0R7rzF2.js → chunk-6Q2QTUOP-BbGouI1Z.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bt44TCGy.js → chunk-GF5L2VYU-B9247w69.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-_uEE_XFx.js → chunk-JWPE2WC7-CXob_wYy.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-C3TOYGZ9.js → chunk-KBJHAD2P-C6FUel1g.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cz60oBPV.js → chunk-RYQCIY6F-DGKRYKHu.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-8Bik0qis.js → chunk-XXDRQBXY-kNBqNPfx.js} +1 -1
  42. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlUacmD.js +1 -0
  43. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlUacmD.js +1 -0
  44. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C2QIOA4a.js → cose-bilkent-JH36ORCC--xmkDwfD.js} +1 -1
  45. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CSH_yUUd.js → cynefin-VYW2F7L2-Cw5FMMuG.js} +1 -1
  46. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BDLw2XFM.js → cynefinDiagram-MW4NZA55-CuLslt2t.js} +1 -1
  47. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CcD9ZtF4.js → dagre-VZM6K2ZE-D9ngqrs2.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DqwtkBsI.js → diagram-7IWD3JNH-RDRSbHQp.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BWVEcqsC.js → diagram-B4RE2ZJO-BgQ1P9aV.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-M-WAbtQi.js → diagram-LBJQPF4R-D3WWyPtn.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DKW7SixP.js → diagram-Q27KOJAE-L2k28wTR.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-UB23O5K3-PHHPPtrj.js → diagram-UB23O5K3-DDNrA5Qw.js} +1 -1
  53. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BQD-B4RX.js → ebnfDiagram-BXEA7PRR-BaRFCOdM.js} +1 -1
  54. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BFvULb51.js → erDiagram-JOGREHBK-CM33DVVM.js} +1 -1
  55. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Bw-aXTCb.js → flowDiagram-UKHOOZJN-CVBSjOxe.js} +1 -1
  56. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGKw04Qx.js → ganttDiagram-PKOTCBZU-BdscewE_.js} +1 -1
  57. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-RqKaPR-I.js → gitGraphDiagram-DS77QQ5N-HRuSZb7g.js} +1 -1
  58. package/dist/worker/console/static/assets/{index-B_D8rbWc.js → index-B9JJQsVK.js} +102 -72
  59. package/dist/worker/console/static/assets/index-rWaGv4jz.css +1 -0
  60. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-YO5dnzrJ.js → infoDiagram-6WML65LV-cYfHvfAR.js} +1 -1
  61. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js → ishikawaDiagram-WSZJBQD7-CZmaoOuy.js} +1 -1
  62. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Bszls1DD.js → journeyDiagram-NVQOT4AX-ntYijh7g.js} +1 -1
  63. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PhzeaZ09.js → kanban-definition-27J2QSJJ-Cz3b0DeD.js} +1 -1
  64. package/dist/worker/console/static/assets/{linear-BsjbDoXi.js → linear-DvGonpsP.js} +1 -1
  65. package/dist/worker/console/static/assets/{mermaid.core-0B7NnWKk.js → mermaid.core-B-3vjfyW.js} +5 -5
  66. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BJr4Fj-q.js → mindmap-definition-FAOFIHXS-5iKxlsX3.js} +1 -1
  67. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-moC4fpGB.js → pegDiagram-VL7TDLO6-C8BUwapW.js} +1 -1
  68. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BEw37-2c.js → pieDiagram-7S7Q4E2Y-B2rPoQQG.js} +1 -1
  69. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq6LyasU.js → quadrantDiagram-CIZ2JOQS-jDeVUAy4.js} +1 -1
  70. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DUCMcK0D.js → railroadDiagram-AXF67PYL-DV140Dwm.js} +1 -1
  71. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3upTZm7.js → requirementDiagram-LRYGKXZP-CnYgHtYC.js} +1 -1
  72. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BI_gMsCW.js → sankeyDiagram-W5VNT64P-SIK3CdWw.js} +1 -1
  73. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-YFOIRzfN.js → sequenceDiagram-SI44F4Z6-DW_vZix7.js} +1 -1
  74. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-dOnB7UDD.js → sizeCapture-X5ZJPWSS-NBAtkg0C.js} +1 -1
  75. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BXUniaIh.js → stateDiagram-OKZ733FA-Bi1bQxpi.js} +1 -1
  76. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DjWKYjQ1.js +1 -0
  77. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-2tA4wTNu.js → swimlanes-SLNWSIFB-8PT_uP_i.js} +2 -2
  78. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B4cdm7bH.js +8 -0
  79. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DO0HkJXC.js → timeline-definition-Z64GVDOM-CD0ZZPk0.js} +1 -1
  80. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DvIOzixv.js → vennDiagram-T6HMQDX7-BbEgWdhK.js} +1 -1
  81. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DXDZ0cTj.js → wardleyDiagram-T6FBY63Y-DCwPKdLl.js} +1 -1
  82. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B32Ark0D.js → xychartDiagram-ELKLHX3M-sfC5PcNg.js} +1 -1
  83. package/dist/worker/console/static/index.html +2 -2
  84. package/dist/worker/observe/static/styles.css +9 -0
  85. package/dist/worker/observe/static/views/dag-inspector.js +40 -0
  86. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  87. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
  88. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  89. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  90. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  91. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  92. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  93. package/dist/workflows/dag/frontend-implementation-contract.js +118 -22
  94. package/dist/workflows/dag/frontend-shadow-dual-write.js +1 -1
  95. package/dist/workflows/dag/frontend-writer-admission.js +13 -0
  96. package/dist/workflows/dag/init-hybrid.js +67 -3
  97. package/dist/workflows/dag/node-execution.js +384 -26
  98. package/dist/workflows/dag/rerun-feedback.js +135 -3
  99. package/dist/workflows/dag/retry-policy.js +13 -122
  100. package/dist/workflows/dag/types.js +6 -1
  101. package/docs/architecture/runtime-boundaries.md +2 -1
  102. package/docs/templates/README.md +1 -0
  103. package/docs/templates/backend-test-dag.json +4 -3
  104. package/docs/templates/frontend-implementation-dag.json +89 -0
  105. package/package.json +4 -3
  106. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  107. package/dist/worker/console/static/assets/channel-3TxJgYaH.js +0 -1
  108. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +0 -1
  109. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +0 -1
  110. package/dist/worker/console/static/assets/index-BdNx6fj0.css +0 -1
  111. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +0 -1
  112. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +0 -8
@@ -1023,15 +1023,19 @@ export function classifyPiFailure(input) {
1023
1023
  return "context-overflow";
1024
1024
  // Prefer explicit rate-limit / HTTP 429 before generic "limit" quota phrases.
1025
1025
  // Chinese gateways often say "请求频率已达到限制" without English "rate limit".
1026
- if (/rate.?limit|too many requests|\b429\b|请求频率|频率.*限制|rpm|tpm/.test(combined))
1026
+ // Patterns need error-shaped context: the captured stream also contains
1027
+ // serialized session events, where a bare "429" is an output-token counter,
1028
+ // bare "rpm"/"tpm" are base64 substrings of encrypted reasoning, and bare
1029
+ // "network" is a "no-network" UI state name.
1030
+ if (/rate.?limit|too many requests|(?:http|status)[^\d]{0,4}429\b|"code"\s*:\s*"?429\b|(?:^|\n)429[\s:]|请求频率|频率.*限制|\b(?:rpm|tpm)\s*(?:limit|quota|exceeded)/.test(combined))
1027
1031
  return "rate-limit";
1028
- if (/quota|usage limit|reached.*limit|5\s*小时|5小时|insufficient_quota|billing/.test(combined))
1032
+ if (/quota|usage limit|reached[^\n.]{0,60}limit|5\s*小时|5小时|insufficient_quota|billing/.test(combined))
1029
1033
  return "quota";
1030
1034
  if (/\bunauthorized\b|\bhttp\s*40[13]\b|invalid api key|authentication (?:failed|required|error)|auth(?:entication)? failed/.test(combined))
1031
1035
  return "auth";
1032
- if (/unknown provider|unknown model|model.*unavailable|provider.*unavailable|\bunavailable\b|overloaded|capacity|temporarily unavailable/.test(combined))
1036
+ if (/unknown provider|unknown model|(?:provider|model|service|gateway)\s+(?:is\s+)?unavailable|overloaded|at capacity|temporarily unavailable/.test(combined))
1033
1037
  return "unavailable";
1034
- if (/network|econnreset|etimedout|socket hang up|connection reset|connection error|request timed out|dns|fetch failed/.test(combined))
1038
+ if (/network\s*(?:error|failure|issue|timeout|unreachable|reset|abort)|networkerror|connection (?:reset|error|refused|closed|timed\s*out)|request timed out|socket hang up|econn\w*|etimedout|fetch failed|dns (?:error|failure|lookup failed)/.test(combined))
1035
1039
  return "network";
1036
1040
  if (input.exitCode === 0 && !input.assistantText.trim())
1037
1041
  return "empty-output";
@@ -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) {
@@ -42,11 +42,11 @@ import { materializeBackendTestExecutionContract } from "../workflows/dag/backen
42
42
  import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
43
43
  import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
44
44
  import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
45
- import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
45
+ import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, classifyBackendTestCorrespondenceRepair, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
46
46
  import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
47
47
  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";
48
48
  import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
49
- import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
49
+ import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, summarizeBackendTestExclusions, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
50
50
  import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
51
51
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
52
52
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
@@ -880,26 +880,49 @@ async function executeBackendTestPipeline(input, meta) {
880
880
  const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
881
881
  await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
882
882
  outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
883
- if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
884
- const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
885
- .filter((entry) => entry.status !== "EXACT_1_TO_1")
886
- .flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
887
- .filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
888
- if (correspondenceRepairPaths.length > 0) {
889
- facts.status = "REPAIRABLE";
890
- facts.repairEligible = true;
891
- facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
883
+ if (correspondence.facts.status === "FAIL") {
884
+ const repair = classifyBackendTestCorrespondenceRepair(correspondence.facts, backendLayout);
885
+ if (repair.markdownBlockingEntries.length > 0) {
886
+ facts.status = "BLOCKED";
887
+ facts.repairEligible = false;
888
+ facts.repairPaths = [];
892
889
  facts.findings.push({
893
- kind: "markdown-pytest-correspondence",
890
+ kind: "markdown-correspondence-contract",
894
891
  classification: "test-asset-defect",
895
- repairability: "repairable",
896
- detail: correspondence.facts.entries
897
- .filter((entry) => entry.status !== "EXACT_1_TO_1")
898
- .flatMap((entry) => entry.findings)
892
+ repairability: "blocked",
893
+ detail: repair.markdownBlockingEntries
894
+ .map((entry) => `${entry.caseId ?? "unknown Case"}: ${entry.reasonCodes.join(", ")}`)
899
895
  .join("; ")
900
- .slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
896
+ .slice(0, 12_000),
901
897
  });
902
898
  }
899
+ else if (repair.pytestRepairPaths.length > 0 &&
900
+ (facts.status === "PASS" || facts.status === "REPAIRABLE")) {
901
+ facts.status = "REPAIRABLE";
902
+ facts.repairEligible = true;
903
+ facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...repair.pytestRepairPaths])).sort();
904
+ const repairEntries = correspondence.facts.entries.filter((entry) => repair.pytestRepairEntries.some((candidate) => candidate.caseId === entry.caseId));
905
+ for (const entry of repairEntries.slice(0, 40)) {
906
+ const classified = repair.pytestRepairEntries.find((candidate) => candidate.caseId === entry.caseId);
907
+ facts.findings.push({
908
+ kind: "markdown-pytest-correspondence",
909
+ classification: "test-asset-defect",
910
+ repairability: "repairable",
911
+ detail: entry.findings.join("; ").slice(0, 2_000) || "Markdown-to-pytest correspondence is incomplete",
912
+ ...(entry.caseId ? { caseId: entry.caseId } : {}),
913
+ reasonCodes: classified?.reasonCodes ?? [],
914
+ repairPaths: classified?.paths ?? [],
915
+ payloadExpectedPaths: Array.from(new Set([
916
+ ...entry.payloadAssessment.requiredPaths,
917
+ ...entry.payloadAssessment.allowedPaths,
918
+ ])).sort(),
919
+ payloadObservedPaths: entry.payloadAssessment.observedPaths,
920
+ payloadMissingPaths: entry.payloadAssessment.missingPaths,
921
+ payloadUnexpectedPaths: entry.payloadAssessment.unexpectedPaths,
922
+ payloadEnumMismatches: entry.payloadAssessment.enumMismatches,
923
+ });
924
+ }
925
+ }
903
926
  }
904
927
  }
905
928
  catch (error) {
@@ -922,6 +945,19 @@ async function executeBackendTestPipeline(input, meta) {
922
945
  collectedItemCount: facts.collectedItemCount,
923
946
  missingMappedScripts: facts.missingMappedScripts,
924
947
  repairPaths: facts.repairPaths,
948
+ repairFindings: facts.findings.map((finding) => ({
949
+ kind: finding.kind,
950
+ repairability: finding.repairability,
951
+ detail: finding.detail,
952
+ ...(finding.caseId ? { caseId: finding.caseId } : {}),
953
+ ...(finding.reasonCodes ? { reasonCodes: finding.reasonCodes } : {}),
954
+ ...(finding.repairPaths ? { repairPaths: finding.repairPaths } : {}),
955
+ ...(finding.payloadExpectedPaths ? { payloadExpectedPaths: finding.payloadExpectedPaths } : {}),
956
+ ...(finding.payloadObservedPaths ? { payloadObservedPaths: finding.payloadObservedPaths } : {}),
957
+ ...(finding.payloadMissingPaths ? { payloadMissingPaths: finding.payloadMissingPaths } : {}),
958
+ ...(finding.payloadUnexpectedPaths ? { payloadUnexpectedPaths: finding.payloadUnexpectedPaths } : {}),
959
+ ...(finding.payloadEnumMismatches ? { payloadEnumMismatches: finding.payloadEnumMismatches } : {}),
960
+ })),
925
961
  factsPath: "contracts/backend-test-pytest-collection-initial.json",
926
962
  }),
927
963
  stderr: "",
@@ -1021,6 +1057,7 @@ async function executeBackendTestPipeline(input, meta) {
1021
1057
  })
1022
1058
  : { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
1023
1059
  const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
1060
+ const exclusionSummary = summarizeBackendTestExclusions(eligibility.excludedItems);
1024
1061
  const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
1025
1062
  await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
1026
1063
  await writeFile(eligibilityFactsPath, `${JSON.stringify({
@@ -1031,6 +1068,9 @@ async function executeBackendTestPipeline(input, meta) {
1031
1068
  excludedItemCount: eligibility.excludedItems.length,
1032
1069
  eligibleItemIds: eligibility.eligibleItemIds,
1033
1070
  excludedItems: eligibility.excludedItems,
1071
+ exclusionSummary,
1072
+ pytestSpawned: false,
1073
+ businessTestBodyExecutedCount: 0,
1034
1074
  inputHashes: eligibilityInputHashes,
1035
1075
  }, null, 2)}\n`, "utf8");
1036
1076
  const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
@@ -1043,6 +1083,10 @@ async function executeBackendTestPipeline(input, meta) {
1043
1083
  `- Collected items: ${effective.collectedItemIds.length}`,
1044
1084
  `- Eligible items: ${eligibility.eligibleItemIds.length}`,
1045
1085
  `- Excluded items: ${eligibility.excludedItems.length}`,
1086
+ `- Excluded Cases: ${exclusionSummary.excludedCaseCount}`,
1087
+ `- Exclusion categories: ${Object.entries(exclusionSummary.excludedByReason).map(([category, count]) => `${category}=${count}`).join("; ") || "none"}`,
1088
+ "- pytest spawned: false",
1089
+ "- Business test bodies executed: 0",
1046
1090
  "",
1047
1091
  "## Excluded Items",
1048
1092
  "",
@@ -1071,6 +1115,9 @@ async function executeBackendTestPipeline(input, meta) {
1071
1115
  executionReadinessStatus: readiness.status,
1072
1116
  eligibleItemCount: readiness.eligibleItemIds.length,
1073
1117
  excludedItemCount: readiness.excludedItems.length,
1118
+ exclusionSummary: readiness.exclusionSummary,
1119
+ pytestSpawned: readiness.pytestSpawned,
1120
+ businessTestBodyExecutedCount: readiness.businessTestBodyExecutedCount,
1074
1121
  factsPath: "contracts/backend-test-pytest-collection-effective.json",
1075
1122
  readinessPath: "contracts/backend-test-execution-readiness.json",
1076
1123
  reportPath: artifacts.reportPath,
@@ -2224,6 +2271,21 @@ async function collectReviewRestartPhase(runDir, reviewNodeId) {
2224
2271
  }
2225
2272
  /** A+B (AC-009): deterministic §4.10 restart phase for a committed
2226
2273
  * `request_design_changes` fact. Mirrors the review collector. */
2274
+ /** Number of findings attached to the committed request_design_changes
2275
+ * terminal fact (0 when absent) — used for the admission advisory. */
2276
+ async function collectDesignReviewFindingCount(runDir, designNodeId) {
2277
+ try {
2278
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2279
+ const request = records.find((record) => record.phase === "committed" &&
2280
+ record.fact.kind ===
2281
+ "request_design_changes");
2282
+ const findings = (request?.fact).findings;
2283
+ return Array.isArray(findings) ? findings.length : 0;
2284
+ }
2285
+ catch {
2286
+ return 0;
2287
+ }
2288
+ }
2227
2289
  async function collectDesignRestartPhase(runDir, designNodeId) {
2228
2290
  try {
2229
2291
  const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
@@ -2540,21 +2602,30 @@ async function executeFrontendWriterAdmission(input, meta) {
2540
2602
  const shell = input.task.shell;
2541
2603
  const config = shell.frontendWriterAdmission;
2542
2604
  try {
2543
- // 1. Design verdict pass is a hard admission condition (M8 D2). In small
2544
- // topology the design review node is absent and the check is vacuous.
2545
- // The verdict is read from the committed typed design terminal fact, not
2605
+ // 1. Design verdict handling (r12 policy change). The typed terminal
2606
+ // verdict is read from the committed typed design terminal fact, not
2546
2607
  // from the legacy first-line VERDICT text protocol (AC-003/AC-004).
2608
+ // A request_design_changes verdict no longer blocks writer admission:
2609
+ // it is downgraded to an advisory that rides along in the admission
2610
+ // receipt (and reaches implement via the review node's upstream
2611
+ // context). Blocking is owned by the deterministic gates (prewrite,
2612
+ // write guard) and the final code review. A missing typed terminal
2613
+ // verdict still fails closed. In small topology the design review node
2614
+ // is absent and the check is vacuous.
2547
2615
  let verdictPass = true;
2548
- let designRestartPhase;
2616
+ let designAdvisory;
2549
2617
  if (config.designReviewFromNodeId) {
2550
2618
  const designKinds = await collectDesignTerminalKinds(meta.runDir, config.designReviewFromNodeId);
2551
- verdictPass =
2552
- designKinds.length === 1 && designKinds[0] === "approve_design";
2553
- // A+B (AC-009): request_design_changes issueCategory maps to the
2554
- // deterministic §4.10 restart phase; unknown fails closed to undefined.
2555
- designRestartPhase = verdictPass
2556
- ? undefined
2557
- : await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId);
2619
+ if (designKinds.length !== 1) {
2620
+ verdictPass = false;
2621
+ }
2622
+ else if (designKinds[0] !== "approve_design") {
2623
+ designAdvisory = {
2624
+ verdict: designKinds[0],
2625
+ restartPhase: await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId),
2626
+ findingCount: await collectDesignReviewFindingCount(meta.runDir, config.designReviewFromNodeId),
2627
+ };
2628
+ }
2558
2629
  }
2559
2630
  // 2. Read the canonical contract + design-policy result.
2560
2631
  const contractRaw = await readRunDirJson(meta.runDir, "contracts/frontend-implementation-contract.json");
@@ -2630,10 +2701,8 @@ async function executeFrontendWriterAdmission(input, meta) {
2630
2701
  : [
2631
2702
  ...admission.findings,
2632
2703
  {
2633
- code: "design-approval-request-revision",
2634
- message: designRestartPhase !== undefined
2635
- ? `design review typed terminal verdict is not approve_design (restartPhase=${designRestartPhase})`
2636
- : "design review typed terminal verdict is not approve_design",
2704
+ code: "design-approval-missing",
2705
+ message: "design review typed terminal verdict missing or ambiguous (fail closed)",
2637
2706
  },
2638
2707
  ];
2639
2708
  const classification = findings.length === 0 && admission.classification === "accepted"
@@ -2654,6 +2723,7 @@ async function executeFrontendWriterAdmission(input, meta) {
2654
2723
  ? { baselineDebtPolicy: admission.baselineDebtPolicy }
2655
2724
  : {}),
2656
2725
  findings,
2726
+ ...(designAdvisory ? { designReviewAdvisory: designAdvisory } : {}),
2657
2727
  failureSource,
2658
2728
  });
2659
2729
  await writeDagRunJsonArtifact(meta.runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, result);
@@ -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',
@@ -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
+ }
@@ -0,0 +1,151 @@
1
+ import { parseBoundedNumber, readContextPressureMode, } from "./env.js";
2
+ export const PI_DEFAULT_RESERVE_TOKENS = 16_384;
3
+ export const DEFAULT_TRAILING_BUDGET_TOKENS = 8_192;
4
+ export const DEFAULT_SAFETY_MARGIN_TOKENS = 4_096;
5
+ export const DEFAULT_MINIMUM_WORKING_SET = 32_768;
6
+ export const DEFAULT_KEEP_RECENT_TOKENS = 20_000;
7
+ export function parseCompactionPolicy(env = process.env) {
8
+ const diagnostics = [];
9
+ const modeResult = readContextPressureMode(env);
10
+ if (modeResult.diagnostic)
11
+ diagnostics.push(modeResult.diagnostic);
12
+ const k = parseBoundedNumber(env.HARNESS_CONTEXT_GATEWAY_K, 1, 1, 8);
13
+ if (k.diagnostic)
14
+ diagnostics.push(k.diagnostic);
15
+ const trailing = parseBoundedNumber(env.HARNESS_CONTEXT_TRAILING_BUDGET_TOKENS, DEFAULT_TRAILING_BUDGET_TOKENS, 256, 128_000);
16
+ if (trailing.diagnostic)
17
+ diagnostics.push(trailing.diagnostic);
18
+ const safety = parseBoundedNumber(env.HARNESS_CONTEXT_SAFETY_MARGIN_TOKENS, DEFAULT_SAFETY_MARGIN_TOKENS, 0, 64_000);
19
+ if (safety.diagnostic)
20
+ diagnostics.push(safety.diagnostic);
21
+ const explicit = env.HARNESS_COMPACTION_RESERVE_TOKENS;
22
+ const explicitReserve = explicit === undefined || explicit.trim() === ""
23
+ ? undefined
24
+ : parseBoundedNumber(explicit, PI_DEFAULT_RESERVE_TOKENS, 1_024, 1_000_000);
25
+ if (explicitReserve?.diagnostic)
26
+ diagnostics.push(explicitReserve.diagnostic);
27
+ return {
28
+ mode: modeResult.mode,
29
+ k: k.value,
30
+ trailingBudgetTokens: trailing.value,
31
+ safetyMarginTokens: safety.value,
32
+ minimumWorkingSet: DEFAULT_MINIMUM_WORKING_SET,
33
+ ...(explicitReserve ? { explicitReserveTokens: explicitReserve.value } : {}),
34
+ diagnostics,
35
+ };
36
+ }
37
+ export function deriveEffectiveWindow(contextWindow, policy) {
38
+ const W = Math.floor(contextWindow);
39
+ if (!Number.isFinite(W) || W <= 0) {
40
+ return {
41
+ recommendedReserve: PI_DEFAULT_RESERVE_TOKENS,
42
+ triggerTokens: 0,
43
+ fallback: true,
44
+ };
45
+ }
46
+ if (policy.explicitReserveTokens !== undefined) {
47
+ const recommendedReserve = Math.min(Math.max(policy.explicitReserveTokens, 1), Math.max(1, W - 1));
48
+ return {
49
+ recommendedReserve,
50
+ triggerTokens: Math.max(0, W - recommendedReserve),
51
+ fallback: false,
52
+ };
53
+ }
54
+ const rawTrigger = Math.floor(W / policy.k) -
55
+ policy.trailingBudgetTokens -
56
+ policy.safetyMarginTokens;
57
+ const rawReserve = W - rawTrigger;
58
+ const maxReserve = Math.max(1, W - policy.minimumWorkingSet);
59
+ if (!Number.isFinite(rawTrigger) ||
60
+ !Number.isFinite(rawReserve) ||
61
+ maxReserve < PI_DEFAULT_RESERVE_TOKENS) {
62
+ const recommendedReserve = Math.min(PI_DEFAULT_RESERVE_TOKENS, Math.max(1, W - 1));
63
+ return {
64
+ recommendedReserve,
65
+ triggerTokens: Math.max(0, W - recommendedReserve),
66
+ fallback: true,
67
+ };
68
+ }
69
+ const recommendedReserve = Math.min(Math.max(PI_DEFAULT_RESERVE_TOKENS, rawReserve), maxReserve);
70
+ return {
71
+ recommendedReserve,
72
+ triggerTokens: Math.max(0, W - recommendedReserve),
73
+ fallback: false,
74
+ };
75
+ }
76
+ export function conservativeTokenEstimate(text, imageCount = 0) {
77
+ const chars = Array.from(text);
78
+ let cjk = 0;
79
+ let other = 0;
80
+ for (const char of chars) {
81
+ if (/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/u.test(char)) {
82
+ cjk += 1;
83
+ }
84
+ else {
85
+ other += 1;
86
+ }
87
+ }
88
+ const utf8 = Buffer.byteLength(text, "utf8");
89
+ const fromChars = cjk + Math.ceil(other / 2);
90
+ const fromBytes = Math.ceil(utf8 / 2);
91
+ return (Math.max(fromChars, fromBytes, Math.ceil(chars.length / 2)) +
92
+ Math.max(0, imageCount) * 1024);
93
+ }
94
+ export function projectPromptPressure(input) {
95
+ if (input.currentContextTokens === null ||
96
+ input.currentContextTokens === undefined ||
97
+ !Number.isFinite(input.currentContextTokens)) {
98
+ return { projected: 0, usageUnavailable: true };
99
+ }
100
+ return {
101
+ projected: Math.max(0, input.currentContextTokens) +
102
+ conservativeTokenEstimate(input.promptText, input.imageCount ?? 0),
103
+ usageUnavailable: false,
104
+ };
105
+ }
106
+ function isExplicitlyDisabled(manager) {
107
+ const project = manager.getProjectSettings?.()?.compaction?.enabled;
108
+ const global = manager.getGlobalSettings?.()?.compaction?.enabled;
109
+ return project === false || (project === undefined && global === false);
110
+ }
111
+ export function applyProcessCompactionOverride(manager, input) {
112
+ if (!manager?.applyOverrides || input.policy.mode === "off") {
113
+ return { applied: false };
114
+ }
115
+ const explicitOff = input.policy.mode === "safe" && isExplicitlyDisabled(manager);
116
+ const enabled = input.policy.mode === "strict" ? true : explicitOff ? false : true;
117
+ const window = typeof input.contextWindow === "number" && input.contextWindow > 0
118
+ ? deriveEffectiveWindow(input.contextWindow, input.policy)
119
+ : undefined;
120
+ const compaction = { enabled };
121
+ if (window && enabled) {
122
+ compaction.reserveTokens = window.recommendedReserve;
123
+ compaction.keepRecentTokens = DEFAULT_KEEP_RECENT_TOKENS;
124
+ }
125
+ manager.applyOverrides({ compaction });
126
+ return {
127
+ applied: true,
128
+ enabled,
129
+ ...(typeof compaction.reserveTokens === "number"
130
+ ? { reserveTokens: compaction.reserveTokens }
131
+ : {}),
132
+ ...(typeof compaction.keepRecentTokens === "number"
133
+ ? { keepRecentTokens: compaction.keepRecentTokens }
134
+ : {}),
135
+ ...(window?.fallback
136
+ ? { diagnostic: "small-or-invalid-window-fell-back-to-pi-default-reserve" }
137
+ : {}),
138
+ };
139
+ }
140
+ export function inMemoryCompactionSettings(policy) {
141
+ if (policy.mode === "off") {
142
+ return { enabled: true };
143
+ }
144
+ return {
145
+ enabled: true,
146
+ keepRecentTokens: DEFAULT_KEEP_RECENT_TOKENS,
147
+ ...(policy.explicitReserveTokens
148
+ ? { reserveTokens: policy.explicitReserveTokens }
149
+ : {}),
150
+ };
151
+ }