chainlesschain 0.166.107 → 0.166.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -9
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DCx6L_HW.js → AIOps-B5HWUFaS.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DYm1CN3Z.js → ActionButton-DCIKpxoQ.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D5ItUS_y.js → Analytics-dsgvjCEQ.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-Cb7XeRdH.js → AppLayout-DWDH5Qbm.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-DDDeqCDQ.js → Artifacts-BD8S8kzU.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BRt3x13w.js → Audit-CDXv4Aoj.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C_oNALMd.js → BackgroundAgents-BLAQ5Uy0.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-PSU2p6CR.js → Backup-moPac6tF.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-G3oL4hcX.js → BaseInput-C_6nzsLU.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C2cLI10b.js → Chat-cYuslkFG.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-KX1OITf8.js → ChatBubbleRenderer-Ceh3MG6M.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CHLnEgFD.js → Checkbox-Qjb-XOX_.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DZm9g_Bc.js → Codegen-IYRWXlMA.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D5Z4zSay.js → Col-CsQ9jv8M.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DHw69NSk.js → Community-BbNmfTwe.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-B-vjYNWz.js → Compact-SShDx-nr.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DaIYVZQY.js → Compliance-CLWhKIJp.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CoA9U-gF.js → Cowork-D4tK9_s3.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-DXqmxlRC.js → Cron-D7GnfQ1z.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Cdkc3ue1.js → Crosschain-TG86SH3X.js} +1 -1
- package/src/assets/web-panel/assets/{DID-oKfYyNSR.js → DID-6pue0NCO.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-n_ycvgWu.js → Dashboard-BwkJ29du.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-D38rZ8WY.js → Dropdown-DEvpYDof.js} +1 -1
- package/src/assets/web-panel/assets/EmailListRenderer-CQj7UlKJ.js +1 -0
- package/src/assets/web-panel/assets/{EvolutionSettings-CTxA_SIq.js → EvolutionSettings-tpvPMM9W.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DmECNwV5.js → FamilyGuardDashboard-CWYFW75O.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-B2IaaDmQ.js → Federation-gRIi96rM.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DgcgqUIk.js → FormItemContext-B7Nr3YkV.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B1-du6Qm.js → GenericCardRenderer-b1BgGgLT.js} +1 -1
- package/src/assets/web-panel/assets/{Git-XceM21Il.js → Git-Dis1jjO7.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CV97iz3z.js → Governance-C4Yg99Am.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CI7fj305.js → Inference-D4UQRbGu.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-B5mekWRy.js → KnowledgeGraph-Co_kLMAq.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BlxFA0xY.js → Logs-D4BRp7bg.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-rDE-XyMY.js → MarkdownRenderer-Dgy8arQI.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DL1BVCiX.js → Marketplace-ys0j1vr9.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BPtwrO3c.js → McpTools-B3ZvRZe_.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-DuSNw4E-.js → Memory-BOa0jbr-.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DTgY9ywI.js → MobileBridge-CWJRmUbj.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-7ty8vTpj.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-h4fCLjXw.js → Mtc-CCcOk5m5.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-fqMDY3-w.js → MtcAudit-vlrrScbQ.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Do3FehBL.js → Multisig-C8K59S_8.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CQVY0kQF.js → NLProgramming-oTpFYVme.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-4c3oNvWh.js → Notes-BhqAJWiz.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BKr2JD4k.js → NotificationSettings-DCnV1CJM.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Bq4jgzRt.js → OrderTableRenderer-CZGnsrVW.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-Dy3XidyA.js → Organization-DX1LuQlJ.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DFF0qN5K.js → Overflow-CbfKrFdT.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DAgYYykd.js → P2P-BEwYZuHL.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DgNHa8b1.js → PdhVaultBrowser-YNmWbwd7.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-Cev6uSBc.js → Permissions-DyZ18LiA.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DJzbN06K.js → PersonalDataHub-B2J3aw2g.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-CoJWvbQ5.js → Pipeline-CXl2ZlhQ.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DZxV6etm.js → Privacy-I5SDmgAp.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BFW87JnM.js → ProjectInit-Crl9tXw3.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-LBrRT27p.js → ProjectSettings-DCFtFlSK.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Do5BQq1i.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CnRbjOYk.js → Providers-Cb7OWa3y.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BVFnIS3r.js → QrScannerModal-CJmhAU-s.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DrcHx5f1.js → QuickAsk-Cu6lEWou.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CbRgrSJm.js → Recommend-Crip3jLB.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-B2Uz_ElB.js → RemoteSession-KxYY-2CB.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DS0ljTxL.js → Reputation-D0vN-htI.js} +1 -1
- package/src/assets/web-panel/assets/{Row-ClLobFht.js → Row-wqzd_xCD.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DxxcTD0x.js → RssFeed-8lsPfb0a.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BbHN0Fik.js → Search-C-e8qAeU.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BAVU_EcY.js → Security-CMM9WgFy.js} +3 -3
- package/src/assets/web-panel/assets/{Services-CtqRSVq_.js → Services-CZdxjN2B.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-IJ8ZKdSZ.js → Skeleton-BI3XNk35.js} +1 -1
- package/src/assets/web-panel/assets/Skills-DMlsh9It.js +1 -0
- package/src/assets/web-panel/assets/{Sla-fRkhnA3i.js → Sla-HQruznKY.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Bi8sYJ-I.js → SpeechSettings-Dl-MFq3w.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CjeplUtS.js → SyncSettings-BU224vDm.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-DESg3yCs.js +1 -0
- package/src/assets/web-panel/assets/{Templates-BwVugi8n.js → Templates-Jb3TPz4Q.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CsPuu5xK.js → Tenant-CQ-GTrfa.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DuaeOPjY.js → Terminal-BbMl3Rue.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D-PMrFxO.js → TimelineRenderer-CS_vstKF.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BXZuiQPZ.js → Tokens-BaopVl1f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DgpTaHL3.js → Trigger-DtnD4WUN.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Bn7lEGRY.js → Trust-CsvG4Tmf.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-zz-8JAAQ.js → UkeySign-C1-A-cOV.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DCzfQGTc.js → VideoEditing-CNgJPdSL.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BiRHM_zB.js → Wallet-CdF41501.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-QmPo2JwU.js → WebAuthn-D-JmxD5g.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-gL7SsPnY.js → WorkflowEditor-C0XunR13.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BysvK4hk.js → chat-BBAZpD_G.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CQq20vqN.js → colors-BxSxO5BX.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CAHlOf1X.js → compact-item-BoxL8pg5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BeF3ESj_.js → createContext-W5JAyZmY.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BI5TZ7EX.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CiqnRf-U.js → hasIn-HpiKDPVC.js} +1 -1
- package/src/assets/web-panel/assets/index-9-JLDK5n.js +1 -0
- package/src/assets/web-panel/assets/{index-CCX0daFQ.js → index-9caMEwQ-.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5941vyo.js → index-B05_YRHZ.js} +1 -1
- package/src/assets/web-panel/assets/index-B7m8ioaQ.js +1 -0
- package/src/assets/web-panel/assets/{index-jJhemX1R.js → index-B9VxE1C-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcVmt6u_.js → index-BA3WWQAT.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZBuJ6O6g.js → index-BSJ0cAle.js} +1 -1
- package/src/assets/web-panel/assets/{index-i9aJV9qk.js → index-BW4PP5GN.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cp3IRTDK.js → index-BZAbaY64.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2xbtl_c.js → index-Ba4usRFM.js} +1 -1
- package/src/assets/web-panel/assets/{index-4a0Lmz_y.js → index-BgMViZIY.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTp89vmg.js → index-BtFAEG08.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTPUVm71.js → index-Bwn6k_qw.js} +1 -1
- package/src/assets/web-panel/assets/{index-xwtRrgVY.js → index-C4uI7WCB.js} +1 -1
- package/src/assets/web-panel/assets/{index-CeIL3-nY.js → index-C5kqii-G.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7q5VH2n.js → index-C7GIUWWF.js} +1 -1
- package/src/assets/web-panel/assets/{index-qjaCk1Mj.js → index-CM_gdohF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfJabF_8.js → index-CRg3aOoj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHGtFwwR.js → index-CcyzZloJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXT7MKkD.js → index-CehxfRUR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CowbNBJs.js → index-D6m6B4Wy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBmGQHEz.js → index-DSyfxxQS.js} +3 -3
- package/src/assets/web-panel/assets/{index-yFQWSRoW.js → index-DaJk9U9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKYbru3i.js → index-DcmAiq_Y.js} +1 -1
- package/src/assets/web-panel/assets/{index-CImCEo8S.js → index-DdEcQ1rp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcccefJc.js → index-DinY7IQa.js} +1 -1
- package/src/assets/web-panel/assets/{index-zNkw0IY7.js → index-DkSnlgK_.js} +1 -1
- package/src/assets/web-panel/assets/{index-QALafVsT.js → index-DlPvZgv8.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bvkbj3x-.js → index-DuM9I1If.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgkUkmEJ.js → index-DwidpDEE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Br5tGVbC.js → index-IO2I7nzu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOiyW31p.js → index-JrMBRi62.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHpHGGmB.js → index-QzlEbtRL.js} +1 -1
- package/src/assets/web-panel/assets/{index-TdVEaJfk.js → index-V6VIJe2N.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXsXhR7p.js → index-_38IJqoP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsnLkZjz.js → index-_p4e3KfB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtWchYfW.js → index-gvfIRZxJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2AOvjgR.js → index-ooP1pQCt.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dqkls2_6.js → index-p6rNtYud.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BQR2HnKb.js → initDefaultProps-Dfn6M05J.js} +1 -1
- package/src/assets/web-panel/assets/{motion-gAz6l-nZ.js → motion-BRRwFqGO.js} +1 -1
- package/src/assets/web-panel/assets/{move-Cy5TN856.js → move-DpDPN6zR.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BeT7VrGi.js → mtc-parser-BHlSE87F.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BLN3mlgK.js → omit-CEOri_L5.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DtwZ8u2d.js → pickAttrs-DqICh-ad.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-ViMwQSa-.js → placementArrow-DQizFFiV.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-dEL3u4E7.js → responsiveObserve-DmFHPdvC.js} +1 -1
- package/src/assets/web-panel/assets/{slide-l05us-Gz.js → slide-s2s1XS3T.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Bk-Qn2Og.js → statusUtils-IwEIbBan.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Bn186qeY.js → styleChecker-CIFLlbXH.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-WP-ugR3H.js → useFlexGapSupport-CFkN2Cgh.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-1SvoeSCL.js → useFs-BSY6O48s.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CfNU8N3z.js → usePersonalDataHub-iJl3VnU0.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Bd2Ya101.js → vnode-vu8ReMs7.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Def_MeZv.js → zoom-CeSrYAHp.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/data/changelog.json +16 -3
- package/src/harness/jsonl-session-store.js +128 -5
- package/src/harness/prompt-compressor.js +18 -10
- package/src/harness/structured-handoff.js +151 -13
- package/src/lib/agent-session-export.js +20 -0
- package/src/lib/background-agent-supervisor.js +54 -0
- package/src/lib/coding-task-guidance.js +2 -1
- package/src/lib/context-memory-kernel/live-compaction.js +75 -12
- package/src/lib/read-file-loop-guard.js +124 -8
- package/src/lib/session-host-snapshot.js +3 -0
- package/src/lib/session-message-provenance.js +24 -5
- package/src/lib/session-tool-round.js +39 -0
- package/src/lib/session-transcript-history.js +2 -0
- package/src/lib/session-transcript-page.js +4 -1
- package/src/lib/session-transcript-structure.js +4 -0
- package/src/runtime/__tests__/message-roles.test.js +29 -0
- package/src/runtime/agent-core.js +115 -43
- package/src/runtime/headless-runner.js +191 -34
- package/src/runtime/headless-stream.js +140 -42
- package/src/runtime/message-roles.js +43 -0
- package/src/workers/background-agent-worker.js +146 -47
- package/src/assets/web-panel/assets/EmailListRenderer-BjreeBQ1.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-CTpVHJMa.js +0 -1
- package/src/assets/web-panel/assets/Projects-O3uz76R1.js +0 -1
- package/src/assets/web-panel/assets/Skills-DXxcznnO.js +0 -1
- package/src/assets/web-panel/assets/Tasks-D4KB2Hbx.js +0 -1
- package/src/assets/web-panel/assets/devWarning-D-Iugqxv.js +0 -1
- package/src/assets/web-panel/assets/index-B0Oz2_rQ.js +0 -1
- package/src/assets/web-panel/assets/index-DwoycEQa.js +0 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* parent-to-child agent context. The field order is part of the wire format.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import { projectCanonicalResumeMessages } from "../lib/session-message-provenance.js";
|
|
7
8
|
|
|
8
9
|
export const STRUCTURED_HANDOFF_FIELDS = Object.freeze([
|
|
@@ -154,12 +155,33 @@ function selectBoundedSources(messages, maxChars) {
|
|
|
154
155
|
// being quoted into a new handoff. Only the runtime WeakMap capability is
|
|
155
156
|
// admitted; ordinary user/assistant/tool conversation remains eligible.
|
|
156
157
|
const canonicalSources = projectCanonicalResumeMessages(messages);
|
|
158
|
+
const calls = new Map();
|
|
157
159
|
const entries = canonicalSources
|
|
158
|
-
.map((message, index) =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
.map((message, index) => {
|
|
161
|
+
for (const call of Array.isArray(message.tool_calls)
|
|
162
|
+
? message.tool_calls
|
|
163
|
+
: []) {
|
|
164
|
+
if (message.role === "assistant" && call?.id && call.function)
|
|
165
|
+
calls.set(call.id, call.function);
|
|
166
|
+
}
|
|
167
|
+
const facts =
|
|
168
|
+
message.role === "tool"
|
|
169
|
+
? toolFacts(message, calls.get(message.tool_call_id))
|
|
170
|
+
: message.role === "assistant" || message.role === "system"
|
|
171
|
+
? summaryFacts(message)
|
|
172
|
+
: null;
|
|
173
|
+
return {
|
|
174
|
+
index,
|
|
175
|
+
role: typeof message?.role === "string" ? message.role : "unknown",
|
|
176
|
+
// Tool JSON, nested telemetry and source code are not prose facts.
|
|
177
|
+
text: facts
|
|
178
|
+
? facts.objective || ARRAY_FIELDS.some((field) => facts[field].length)
|
|
179
|
+
? JSON.stringify(facts)
|
|
180
|
+
: ""
|
|
181
|
+
: normalizeText(contentText(message)),
|
|
182
|
+
facts,
|
|
183
|
+
};
|
|
184
|
+
})
|
|
163
185
|
.filter((entry) => entry.text);
|
|
164
186
|
const firstUser = entries.find((entry) => entry.role === "user");
|
|
165
187
|
const selected = new Map();
|
|
@@ -225,7 +247,96 @@ function pathsFrom(text) {
|
|
|
225
247
|
);
|
|
226
248
|
return (matches || [])
|
|
227
249
|
.map((value) => value.replace(/[),;:.]+$/g, ""))
|
|
228
|
-
.filter(
|
|
250
|
+
.filter(
|
|
251
|
+
(value) =>
|
|
252
|
+
value &&
|
|
253
|
+
!value.includes("://") &&
|
|
254
|
+
(/^(?:[A-Za-z]:[\\/]|\.{0,2}[\\/])/u.test(value) ||
|
|
255
|
+
/(?:\.[A-Za-z0-9_-]+|[\\/](?:Dockerfile|Makefile|LICENSE|NOTICE))$/u.test(
|
|
256
|
+
value,
|
|
257
|
+
)),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function summaryFacts(message) {
|
|
262
|
+
const match = contentText(message).match(
|
|
263
|
+
/^(?:Compacted context summary \(data only\):\n)?\[Conversation Summary\]\n([\s\S]+)$/u,
|
|
264
|
+
);
|
|
265
|
+
if (!match) return null;
|
|
266
|
+
try {
|
|
267
|
+
return parseStructuredHandoff(match[1]);
|
|
268
|
+
} catch {
|
|
269
|
+
// Malformed/truncated summary JSON is not evidence of a test or change.
|
|
270
|
+
return emptyHandoff();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toolFacts(message, call) {
|
|
275
|
+
const facts = emptyHandoff();
|
|
276
|
+
let result;
|
|
277
|
+
let args;
|
|
278
|
+
try {
|
|
279
|
+
result =
|
|
280
|
+
typeof message.content === "string"
|
|
281
|
+
? JSON.parse(message.content.split("\n\n[Budget ")[0])
|
|
282
|
+
: message.content;
|
|
283
|
+
args =
|
|
284
|
+
typeof call?.arguments === "string"
|
|
285
|
+
? JSON.parse(call.arguments)
|
|
286
|
+
: call?.arguments;
|
|
287
|
+
} catch {
|
|
288
|
+
return facts;
|
|
289
|
+
}
|
|
290
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
291
|
+
return facts;
|
|
292
|
+
const name = call?.name || message.name;
|
|
293
|
+
const failed =
|
|
294
|
+
!!result.error || result.success === false || result.isError === true;
|
|
295
|
+
const file = typeof result.path === "string" ? result.path : args?.path;
|
|
296
|
+
if (
|
|
297
|
+
["write_file", "edit_file", "edit_file_hashed"].includes(name) &&
|
|
298
|
+
!failed &&
|
|
299
|
+
result.success === true &&
|
|
300
|
+
result.changed !== false &&
|
|
301
|
+
result.alreadyApplied !== true &&
|
|
302
|
+
typeof file === "string" &&
|
|
303
|
+
file
|
|
304
|
+
) {
|
|
305
|
+
facts.changedFiles.push(file);
|
|
306
|
+
const digest =
|
|
307
|
+
name === "write_file" && typeof args?.content === "string"
|
|
308
|
+
? `; written content sha256:${createHash("sha256").update(args.content).digest("hex")}`
|
|
309
|
+
: "";
|
|
310
|
+
facts.checkpoints.push(
|
|
311
|
+
`Observed ${name} success: ${file}${digest}. File contents are omitted from this summary.`,
|
|
312
|
+
);
|
|
313
|
+
facts.nextSteps.push(
|
|
314
|
+
`Read the needed current source ranges in ${file} before replacing existing code; this summary is not a reconstruction of the file.`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (
|
|
318
|
+
["run_shell", "run_code"].includes(name) &&
|
|
319
|
+
Number.isInteger(result.exitCode)
|
|
320
|
+
) {
|
|
321
|
+
const command =
|
|
322
|
+
name === "run_shell" && typeof args?.command === "string"
|
|
323
|
+
? truncateText(args.command, 300)
|
|
324
|
+
: name;
|
|
325
|
+
facts.checkpoints.push(
|
|
326
|
+
`Observed ${name}: ${command}; exitCode=${result.exitCode}.`,
|
|
327
|
+
);
|
|
328
|
+
if (
|
|
329
|
+
name === "run_shell" &&
|
|
330
|
+
/\b(?:vitest|jest|pytest|playwright|npm\s+(?:run\s+)?test|node\s+--test)\b/u.test(
|
|
331
|
+
command,
|
|
332
|
+
)
|
|
333
|
+
) {
|
|
334
|
+
facts.tests.push(
|
|
335
|
+
`Observed command ${command}; exitCode=${result.exitCode}.`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return facts;
|
|
229
340
|
}
|
|
230
341
|
|
|
231
342
|
function emptyHandoff(objective = "") {
|
|
@@ -313,12 +424,40 @@ export function buildExtractiveHandoff(messages, options = {}) {
|
|
|
313
424
|
const sources = selectBoundedSources(messages, maxFallbackSourceChars);
|
|
314
425
|
const firstUser = sources.find((entry) => entry.role === "user");
|
|
315
426
|
const candidates = emptyHandoff(
|
|
316
|
-
truncateText(
|
|
427
|
+
truncateText(
|
|
428
|
+
firstUser?.text ||
|
|
429
|
+
sources.find((entry) => entry.facts?.objective)?.facts.objective ||
|
|
430
|
+
"",
|
|
431
|
+
maxItemChars,
|
|
432
|
+
),
|
|
317
433
|
);
|
|
318
434
|
|
|
319
435
|
for (const source of sources) {
|
|
320
|
-
|
|
321
|
-
|
|
436
|
+
if (source.facts) {
|
|
437
|
+
// Extract typed observations before budgeting, never regex-match a
|
|
438
|
+
// serialized tool payload (which may contain arbitrary source text).
|
|
439
|
+
for (const field of ARRAY_FIELDS)
|
|
440
|
+
candidates[field].push(...source.facts[field]);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
// Quoted implementation examples are source data, not executed outcomes.
|
|
444
|
+
const prose = source.text.replace(
|
|
445
|
+
/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/gu,
|
|
446
|
+
"",
|
|
447
|
+
);
|
|
448
|
+
for (const fragment of fragments(prose)) {
|
|
449
|
+
const statusNarrative =
|
|
450
|
+
source.role !== "user" &&
|
|
451
|
+
!/^(?:must|must not|do not|don't|only|next|todo|remaining|then|we will|will|need to)\b/iu.test(
|
|
452
|
+
fragment,
|
|
453
|
+
);
|
|
454
|
+
if (
|
|
455
|
+
statusNarrative &&
|
|
456
|
+
/^(?:changed|modified|updated|created|deleted|wrote|edited)\b/iu.test(
|
|
457
|
+
fragment,
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
candidates.changedFiles.push(...pathsFrom(fragment));
|
|
322
461
|
if (
|
|
323
462
|
/^(?:constraint|constraints|requirement|requirements|must|must not|do not|don't|only|约束|必须|不要|只能|仅限)\b/iu.test(
|
|
324
463
|
fragment,
|
|
@@ -326,10 +465,7 @@ export function buildExtractiveHandoff(messages, options = {}) {
|
|
|
326
465
|
/\bwithout\s+(?:widening|changing|removing|losing)\b/iu.test(fragment)
|
|
327
466
|
) {
|
|
328
467
|
candidates.constraints.push(
|
|
329
|
-
stripLabel(
|
|
330
|
-
fragment,
|
|
331
|
-
"constraint(?:s)?|requirement(?:s)?|must(?: not)?|do not|don't|only|约束|必须|不要|只能|仅限",
|
|
332
|
-
),
|
|
468
|
+
stripLabel(fragment, "constraint(?:s)?|requirement(?:s)?|约束"),
|
|
333
469
|
);
|
|
334
470
|
}
|
|
335
471
|
if (
|
|
@@ -345,6 +481,7 @@ export function buildExtractiveHandoff(messages, options = {}) {
|
|
|
345
481
|
);
|
|
346
482
|
}
|
|
347
483
|
if (
|
|
484
|
+
statusNarrative &&
|
|
348
485
|
/\b(?:test|tests|vitest|jest|pytest|playwright|lint|typecheck|build)\b/iu.test(
|
|
349
486
|
fragment,
|
|
350
487
|
) &&
|
|
@@ -367,6 +504,7 @@ export function buildExtractiveHandoff(messages, options = {}) {
|
|
|
367
504
|
);
|
|
368
505
|
}
|
|
369
506
|
if (
|
|
507
|
+
statusNarrative &&
|
|
370
508
|
/\b(?:checkpoint|commit\s+[0-9a-f]{7,40}|implemented|completed|done|verified|checkpoint|检查点|已完成|已实现)\b/iu.test(
|
|
371
509
|
fragment,
|
|
372
510
|
)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectToolRoundMessages } from "./session-tool-round.js";
|
|
1
2
|
/**
|
|
2
3
|
* Agent-session Markdown export — Claude-Code `/export` parity for the JSONL
|
|
3
4
|
* headless/agent session store (`~/.../sessions/<id>.jsonl`, the `--resume`
|
|
@@ -63,6 +64,25 @@ export function renderAgentSessionMarkdown(sessionId, events, opts = {}) {
|
|
|
63
64
|
for (const ev of events) {
|
|
64
65
|
if (!ev || typeof ev !== "object") continue;
|
|
65
66
|
switch (ev.type) {
|
|
67
|
+
case "tool_round":
|
|
68
|
+
for (const message of projectToolRoundMessages(ev)) {
|
|
69
|
+
L.push(
|
|
70
|
+
message.role === "assistant"
|
|
71
|
+
? "## Assistant tool calls"
|
|
72
|
+
: `### Tool result ${message.tool_call_id}`,
|
|
73
|
+
);
|
|
74
|
+
L.push(
|
|
75
|
+
"",
|
|
76
|
+
fence(
|
|
77
|
+
message.role === "assistant"
|
|
78
|
+
? message.tool_calls
|
|
79
|
+
: message.content,
|
|
80
|
+
"json",
|
|
81
|
+
),
|
|
82
|
+
"",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
66
86
|
case "user_message":
|
|
67
87
|
users += 1;
|
|
68
88
|
L.push("## 👤 User");
|
|
@@ -1394,6 +1394,60 @@ export function mutateBackgroundAgentState(id, updater, options = {}) {
|
|
|
1394
1394
|
);
|
|
1395
1395
|
}
|
|
1396
1396
|
|
|
1397
|
+
export async function mutateBackgroundAgentStateWithLockRetry(
|
|
1398
|
+
id,
|
|
1399
|
+
updater,
|
|
1400
|
+
options = {},
|
|
1401
|
+
) {
|
|
1402
|
+
const now = options.now || Date.now;
|
|
1403
|
+
const sleep =
|
|
1404
|
+
options.sleep ||
|
|
1405
|
+
((milliseconds) =>
|
|
1406
|
+
new Promise((resolvePromise) =>
|
|
1407
|
+
setTimeout(resolvePromise, milliseconds),
|
|
1408
|
+
));
|
|
1409
|
+
const mutate = options.mutate || mutateBackgroundAgentState;
|
|
1410
|
+
const numericOption = (value, fallback, minimum) => {
|
|
1411
|
+
const number = Number(value);
|
|
1412
|
+
return Number.isFinite(number) ? Math.max(minimum, number) : fallback;
|
|
1413
|
+
};
|
|
1414
|
+
const timeoutMs = numericOption(options.timeoutMs ?? 30_000, 30_000, 0);
|
|
1415
|
+
const lockTimeoutMs = numericOption(
|
|
1416
|
+
options.lockTimeoutMs ?? 10_000,
|
|
1417
|
+
10_000,
|
|
1418
|
+
0,
|
|
1419
|
+
);
|
|
1420
|
+
const initialRetryMs = numericOption(options.initialRetryMs ?? 100, 100, 1);
|
|
1421
|
+
const maxRetryMs = Math.max(
|
|
1422
|
+
initialRetryMs,
|
|
1423
|
+
numericOption(options.maxRetryMs ?? 1_000, 1_000, 1),
|
|
1424
|
+
);
|
|
1425
|
+
const deadline = now() + timeoutMs;
|
|
1426
|
+
let attempts = 0;
|
|
1427
|
+
|
|
1428
|
+
for (;;) {
|
|
1429
|
+
attempts += 1;
|
|
1430
|
+
try {
|
|
1431
|
+
return mutate(id, updater, {
|
|
1432
|
+
timeoutMs: Math.min(lockTimeoutMs, Math.max(0, deadline - now())),
|
|
1433
|
+
});
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
if (error?.code !== "STATE_LOCK_UNAVAILABLE") throw error;
|
|
1436
|
+
const remainingMs = deadline - now();
|
|
1437
|
+
if (remainingMs <= 0) {
|
|
1438
|
+
error.attempts = attempts;
|
|
1439
|
+
throw error;
|
|
1440
|
+
}
|
|
1441
|
+
const retryMs = Math.min(
|
|
1442
|
+
maxRetryMs,
|
|
1443
|
+
initialRetryMs * 2 ** Math.min(attempts - 1, 4),
|
|
1444
|
+
remainingMs,
|
|
1445
|
+
);
|
|
1446
|
+
await sleep(retryMs);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1397
1451
|
export function writeBackgroundAgentState(state, options = {}) {
|
|
1398
1452
|
return mutateBackgroundAgentState(state.id, () => state, {
|
|
1399
1453
|
createIfMissing: options.createIfMissing === true,
|
|
@@ -5,8 +5,9 @@ export const CODING_TASK_QUALITY_GUIDANCE = `Coding task quality:
|
|
|
5
5
|
- Resolve each relative import from the destination file's directory. Read back the completed file and fix newDiagnostics from write/edit results before finishing. A successful write confirms persistence, not syntax, test execution or acceptance.
|
|
6
6
|
- Regression tests need a working positive control and a negative control that exercises the requested behavior through the real production entry point. Assert observable effects; avoid mocks or alternate calls that bypass the behavior under test. Complete the current worker/request when testing cancellation and replacement.
|
|
7
7
|
- For boundary regressions, derive expected defaults, computed values and rejection rules from the implementation or the explicit requested contract. Inject mismatched, tampered or stale input through the real entry point while keeping other prerequisites valid; verify that the fixture reaches the intended check rather than failing during setup. Compare observable results with independent expected values or authoritative state, not a helper that merely recomputes the same result.
|
|
8
|
+
- Trace rejection before asserting its aftermath: a request can be rejected before registration or persistence. Check whether state stays unchanged, a failed record is saved, or an event is emitted; do not assume every rejection creates a failed task. When testing attribution, verify independently recorded identities and provenance as well as message contents; identical text can still be attributed to the wrong source.
|
|
8
9
|
- Use available execution tools for a focused check when permitted. When execution tools are unavailable, inspect imports, API contracts and fixtures using the available file tools, and clearly say that tests were not run. Never describe a read-back or static check as a passing test or end-to-end verification.
|
|
9
|
-
- Finish requested file changes before the final response. Reserve output and remaining requests for writing, read-back and focused validation. For large files, aim for a few thousand characters of new content per call: write a small complete section, then extend it with edit_file using a unique existing anchor and a replacement containing that anchor plus the next complete section. Keep each tool call complete and wait for its result before extending the file again; do not repeatedly rewrite the growing file. State what was delivered, what was checked and what remains unverified.`;
|
|
10
|
+
- Finish requested file changes before the final response. Reserve output and remaining requests for writing, read-back and focused validation. For large files, aim for a few thousand characters of new content per call: write a small complete section, then extend it with edit_file using a unique existing anchor and a replacement containing that anchor plus the next complete section. A list of available tool names is not a limit on the number of tool calls; use further calls within the actual request budget to finish and inspect the file. Keep each tool call complete and wait for its result before extending the file again; do not repeatedly rewrite the growing file. State what was delivered, what was checked and what remains unverified, without repeating that account in multiple drafts.`;
|
|
10
11
|
|
|
11
12
|
export const HERMETIC_CODING_SYSTEM_PROMPT = `You are a hermetic coding assistant. The exposed file tools are the complete tool surface. Inspect and mutate only the exact paths authorized by the user request. Do not invoke hidden tools, commands, language servers or other execution paths.
|
|
12
13
|
|
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
import { JsonlSessionContextPort } from "./jsonl-session-context-port.js";
|
|
14
14
|
import { createCliContextMemoryRuntime } from "./runtime.js";
|
|
15
15
|
import { projectRuntimeTokenUsage } from "../runtime-usage-ledger.js";
|
|
16
|
+
import {
|
|
17
|
+
projectCanonicalResumeMessages,
|
|
18
|
+
getDurableSystemMessageProvenance,
|
|
19
|
+
isRuntimeOnlySessionMessage,
|
|
20
|
+
} from "../session-message-provenance.js";
|
|
16
21
|
|
|
17
22
|
const rejectedCompactionUsage = new WeakMap();
|
|
18
23
|
|
|
@@ -138,7 +143,7 @@ class CallbackSessionContextPort extends InMemorySessionContextPort {
|
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
|
|
141
|
-
function kernelBudget(compressor) {
|
|
146
|
+
function kernelBudget(compressor, ephemeralTokens = 0) {
|
|
142
147
|
const inputBudget = Math.max(
|
|
143
148
|
256,
|
|
144
149
|
Math.trunc(Number(compressor?.maxTokens)) || 16_384,
|
|
@@ -150,7 +155,9 @@ function kernelBudget(compressor) {
|
|
|
150
155
|
return {
|
|
151
156
|
modelWindowTokens: inputBudget + reservedOutputTokens,
|
|
152
157
|
reservedOutputTokens,
|
|
153
|
-
|
|
158
|
+
// Live host policy/retry text still consumes the provider window even
|
|
159
|
+
// though it is deliberately absent from the durable authority partition.
|
|
160
|
+
safetyMarginTokens: ephemeralTokens,
|
|
154
161
|
recoveryReserveTokens: Math.min(
|
|
155
162
|
256,
|
|
156
163
|
Math.max(1, Math.floor(inputBudget * 0.05)),
|
|
@@ -202,7 +209,7 @@ export async function compactLiveMessagesCanonical(
|
|
|
202
209
|
messagesInput,
|
|
203
210
|
options = {},
|
|
204
211
|
) {
|
|
205
|
-
const
|
|
212
|
+
const liveMessages = Array.isArray(messagesInput) ? [...messagesInput] : [];
|
|
206
213
|
const sessionId = String(options.sessionId || `live-${randomUUID()}`);
|
|
207
214
|
const operationId = String(options.operationId || `compact-${randomUUID()}`);
|
|
208
215
|
const trigger = options.trigger || "auto";
|
|
@@ -224,6 +231,24 @@ export async function compactLiveMessagesCanonical(
|
|
|
224
231
|
options.persist !== false &&
|
|
225
232
|
Boolean(options.sessionId) &&
|
|
226
233
|
sessionExists(sessionId);
|
|
234
|
+
// Host policy is live-only. Durable summary/lineage systems retain their
|
|
235
|
+
// private provenance and remain part of the exact authority comparison.
|
|
236
|
+
const messages = persistentSession
|
|
237
|
+
? projectCanonicalResumeMessages(liveMessages, { strict: true })
|
|
238
|
+
: liveMessages;
|
|
239
|
+
const ephemeralMessages = [];
|
|
240
|
+
if (persistentSession) {
|
|
241
|
+
let sequence = 0;
|
|
242
|
+
for (const message of liveMessages) {
|
|
243
|
+
if (
|
|
244
|
+
isRuntimeOnlySessionMessage(message) ||
|
|
245
|
+
(message.role === "system" &&
|
|
246
|
+
!getDurableSystemMessageProvenance(message))
|
|
247
|
+
)
|
|
248
|
+
ephemeralMessages.push({ sequence, message });
|
|
249
|
+
else sequence += 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
227
252
|
if (typeof options.commit === "function") {
|
|
228
253
|
const bootstrapRuntime = createCliContextMemoryRuntime({
|
|
229
254
|
sessionId,
|
|
@@ -323,12 +348,29 @@ export async function compactLiveMessagesCanonical(
|
|
|
323
348
|
const provider = options.provider || "provider.local";
|
|
324
349
|
const model = options.model || "default";
|
|
325
350
|
const memoryRevision = await runtime.memoryPort.getRevision();
|
|
351
|
+
let settledOutputItems = null;
|
|
352
|
+
if (ephemeralMessages.length) {
|
|
353
|
+
const appendCompaction = runtime.sessionPort.appendCompaction.bind(
|
|
354
|
+
runtime.sessionPort,
|
|
355
|
+
);
|
|
356
|
+
runtime.sessionPort.appendCompaction = async (event, expectedHead) => {
|
|
357
|
+
const result = await appendCompaction(event, expectedHead);
|
|
358
|
+
if (result?.ok === true) settledOutputItems = event.outputItems;
|
|
359
|
+
return result;
|
|
360
|
+
};
|
|
361
|
+
}
|
|
326
362
|
let receipt;
|
|
327
363
|
try {
|
|
328
364
|
receipt = await runtime.kernel.compactContext({
|
|
329
365
|
operationId,
|
|
330
366
|
sessionId,
|
|
331
|
-
...kernelBudget(
|
|
367
|
+
...kernelBudget(
|
|
368
|
+
compressor,
|
|
369
|
+
messagesToContextItems(
|
|
370
|
+
ephemeralMessages.map(({ message }) => message),
|
|
371
|
+
{ sessionId },
|
|
372
|
+
).reduce((total, item) => total + item.tokenEstimate, 0),
|
|
373
|
+
),
|
|
332
374
|
sink: provider,
|
|
333
375
|
scopeAdmissions: [{ scope: "session", scopeId: sessionId }],
|
|
334
376
|
policyVersion: "cli-context-policy-v1",
|
|
@@ -421,7 +463,7 @@ export async function compactLiveMessagesCanonical(
|
|
|
421
463
|
}
|
|
422
464
|
if (!["committed", "degraded"].includes(receipt.status)) {
|
|
423
465
|
return {
|
|
424
|
-
messages,
|
|
466
|
+
messages: liveMessages,
|
|
425
467
|
stats: {
|
|
426
468
|
...(summaryStats || {}),
|
|
427
469
|
strategy: "none",
|
|
@@ -433,20 +475,41 @@ export async function compactLiveMessagesCanonical(
|
|
|
433
475
|
};
|
|
434
476
|
}
|
|
435
477
|
const snapshot = await runtime.sessionPort.readSnapshot(sessionId);
|
|
436
|
-
|
|
478
|
+
// Reattach host policy and private retry nudges at their durable sequence
|
|
479
|
+
// boundaries. They remain live, never enter the receipt, and do not move a
|
|
480
|
+
// retry instruction behind tool exchanges that happened after that retry.
|
|
481
|
+
const restoredMessages = [];
|
|
482
|
+
let ephemeralIndex = 0;
|
|
483
|
+
for (const item of [...(settledOutputItems || snapshot.items)].sort(
|
|
484
|
+
(left, right) =>
|
|
485
|
+
(left.sourceRef.eventSequence ?? Number.MAX_SAFE_INTEGER) -
|
|
486
|
+
(right.sourceRef.eventSequence ?? Number.MAX_SAFE_INTEGER) ||
|
|
487
|
+
left.itemId.localeCompare(right.itemId, "en"),
|
|
488
|
+
)) {
|
|
489
|
+
const sequence = item.sourceRef.eventSequence ?? Number.MAX_SAFE_INTEGER;
|
|
490
|
+
while (
|
|
491
|
+
ephemeralIndex < ephemeralMessages.length &&
|
|
492
|
+
ephemeralMessages[ephemeralIndex].sequence <= sequence
|
|
493
|
+
)
|
|
494
|
+
restoredMessages.push(ephemeralMessages[ephemeralIndex++].message);
|
|
495
|
+
restoredMessages.push(...contextItemsToMessages([item]));
|
|
496
|
+
}
|
|
497
|
+
restoredMessages.push(
|
|
498
|
+
...ephemeralMessages.slice(ephemeralIndex).map(({ message }) => message),
|
|
499
|
+
);
|
|
437
500
|
return {
|
|
438
|
-
messages:
|
|
501
|
+
messages: restoredMessages,
|
|
439
502
|
stats: {
|
|
440
503
|
...(summaryStats || {}),
|
|
441
504
|
strategy:
|
|
442
505
|
receipt.status === "degraded" ? "deterministic-fallback" : "canonical",
|
|
443
|
-
originalMessages:
|
|
444
|
-
compressedMessages:
|
|
445
|
-
originalTokens: estimatedTokens(
|
|
446
|
-
compressedTokens: estimatedTokens(
|
|
506
|
+
originalMessages: liveMessages.length,
|
|
507
|
+
compressedMessages: restoredMessages.length,
|
|
508
|
+
originalTokens: estimatedTokens(liveMessages),
|
|
509
|
+
compressedTokens: estimatedTokens(restoredMessages),
|
|
447
510
|
saved: Math.max(
|
|
448
511
|
0,
|
|
449
|
-
estimatedTokens(
|
|
512
|
+
estimatedTokens(liveMessages) - estimatedTokens(restoredMessages),
|
|
450
513
|
),
|
|
451
514
|
canonicalAlreadySettled: true,
|
|
452
515
|
canonicalReceipt: receipt,
|
|
@@ -32,6 +32,79 @@ export class ReadFileLoopGuard {
|
|
|
32
32
|
this.batch = null;
|
|
33
33
|
this.largeOutputs = new Set();
|
|
34
34
|
this.editRecoveryReads = new Map();
|
|
35
|
+
this.visiblePages = null;
|
|
36
|
+
this.visibleRequest = 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Observe only the request actually selected for the provider, not history. */
|
|
40
|
+
observeVisibleMessages(messages) {
|
|
41
|
+
this.visibleRequest++;
|
|
42
|
+
this.visiblePages = [];
|
|
43
|
+
const retain = (value) => {
|
|
44
|
+
if (
|
|
45
|
+
typeof value?.path !== "string" ||
|
|
46
|
+
typeof value.fileVersion !== "string"
|
|
47
|
+
)
|
|
48
|
+
return;
|
|
49
|
+
if (typeof value.content === "string" && !value.contentOmitted) {
|
|
50
|
+
this.visiblePages.push(value);
|
|
51
|
+
}
|
|
52
|
+
for (const excerpt of Array.isArray(value.excerpts)
|
|
53
|
+
? value.excerpts
|
|
54
|
+
: []) {
|
|
55
|
+
if (typeof excerpt?.content === "string")
|
|
56
|
+
this.visiblePages.push({
|
|
57
|
+
...excerpt,
|
|
58
|
+
path: value.path,
|
|
59
|
+
fileVersion: value.fileVersion,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const retainContext = (context) => {
|
|
64
|
+
if (
|
|
65
|
+
typeof context !== "string" ||
|
|
66
|
+
!context.startsWith("[File excerpts retained across compaction")
|
|
67
|
+
)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
const entries = JSON.parse(context.slice(context.indexOf("\n") + 1));
|
|
71
|
+
if (Array.isArray(entries)) entries.forEach(retain);
|
|
72
|
+
} catch {
|
|
73
|
+
/* A truncated hint provides no confirmed source evidence. */
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
for (const message of messages || []) {
|
|
77
|
+
if (message.role === "assistant") retainContext(message.content);
|
|
78
|
+
if (message.role !== "tool") continue;
|
|
79
|
+
let value;
|
|
80
|
+
try {
|
|
81
|
+
value =
|
|
82
|
+
typeof message.content === "string"
|
|
83
|
+
? JSON.parse(message.content.split("\n\n[Budget ")[0])
|
|
84
|
+
: message.content;
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
retain(value);
|
|
89
|
+
for (const context of Array.isArray(value?.retainedReadContext)
|
|
90
|
+
? value.retainedReadContext
|
|
91
|
+
: []) {
|
|
92
|
+
retainContext(context);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
isPageVisible(page) {
|
|
98
|
+
return (
|
|
99
|
+
this.visiblePages?.some(
|
|
100
|
+
(visible) =>
|
|
101
|
+
keyFor(visible.path, {}) === keyFor(page.path, {}) &&
|
|
102
|
+
visible.fileVersion === page.fileVersion &&
|
|
103
|
+
typeof page.content === "string" &&
|
|
104
|
+
page.content.length > 0 &&
|
|
105
|
+
visible.content.includes(page.content),
|
|
106
|
+
) === true
|
|
107
|
+
);
|
|
35
108
|
}
|
|
36
109
|
|
|
37
110
|
recordEditRecovery(result) {
|
|
@@ -94,6 +167,7 @@ export class ReadFileLoopGuard {
|
|
|
94
167
|
rereads: new Set(),
|
|
95
168
|
summary: null,
|
|
96
169
|
excerpts: [],
|
|
170
|
+
contextRestoreOffered: false,
|
|
97
171
|
};
|
|
98
172
|
}
|
|
99
173
|
let page = readPage(args);
|
|
@@ -108,11 +182,11 @@ export class ReadFileLoopGuard {
|
|
|
108
182
|
readProgress: { newContent: false },
|
|
109
183
|
};
|
|
110
184
|
}
|
|
111
|
-
|
|
185
|
+
let span = page.readSpan;
|
|
112
186
|
const covered = entry.spans.some(
|
|
113
187
|
([start, end]) => start <= span.start && end >= span.end,
|
|
114
188
|
);
|
|
115
|
-
|
|
189
|
+
let targeted =
|
|
116
190
|
Number(args.offset) > 0 &&
|
|
117
191
|
Number(args.limit) > 0 &&
|
|
118
192
|
Number(args.limit) <= 200 &&
|
|
@@ -122,20 +196,52 @@ export class ReadFileLoopGuard {
|
|
|
122
196
|
);
|
|
123
197
|
let newContent = !covered;
|
|
124
198
|
const hadOutline = !!entry.summary?.outline;
|
|
199
|
+
// A planner can evict a read without compacting the transcript. Give one
|
|
200
|
+
// bounded landing page for that version, then require explicit ranges.
|
|
201
|
+
// Dropping the same evidence repeatedly must not reopen full-file scans.
|
|
202
|
+
if (
|
|
203
|
+
covered &&
|
|
204
|
+
entry.summary?.reachedEnd === true &&
|
|
205
|
+
!targeted &&
|
|
206
|
+
this.visiblePages !== null &&
|
|
207
|
+
entry.lastReadRequest < this.visibleRequest &&
|
|
208
|
+
!this.isPageVisible(page) &&
|
|
209
|
+
!entry.contextRestoreOffered &&
|
|
210
|
+
entry.rereads.size < 3
|
|
211
|
+
) {
|
|
212
|
+
const offset =
|
|
213
|
+
Number.isSafeInteger(Number(args.offset)) && Number(args.offset) > 0
|
|
214
|
+
? Number(args.offset)
|
|
215
|
+
: 1;
|
|
216
|
+
const bounded = readPage({ ...args, offset, limit: 80 });
|
|
217
|
+
if (!bounded.error && bounded.readSpan && !this.isPageVisible(bounded)) {
|
|
218
|
+
page = bounded;
|
|
219
|
+
span = page.readSpan;
|
|
220
|
+
targeted = true;
|
|
221
|
+
entry.contextRestoreOffered = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
125
224
|
if (covered || (coveredPrefix && !targeted)) {
|
|
126
225
|
// Permit a bounded, explicit reread for editing or lost context. Broad
|
|
127
226
|
// rescans and subsequent repeats recover automatically, including when
|
|
128
227
|
// the model varies offset/limit inside an already covered region.
|
|
129
|
-
const
|
|
130
|
-
.
|
|
131
|
-
.
|
|
228
|
+
const restoring =
|
|
229
|
+
this.visiblePages !== null &&
|
|
230
|
+
entry.lastReadRequest < this.visibleRequest &&
|
|
231
|
+
!this.isPageVisible(page);
|
|
232
|
+
const fingerprint =
|
|
233
|
+
(restoring ? "restore:" : "review:") +
|
|
234
|
+
createHash("sha256").update(JSON.stringify(span)).digest("hex");
|
|
132
235
|
if (
|
|
133
236
|
targeted &&
|
|
134
237
|
!entry.rereads.has(fingerprint) &&
|
|
135
238
|
entry.rereads.size < 3
|
|
136
239
|
) {
|
|
137
240
|
entry.rereads.add(fingerprint);
|
|
138
|
-
page.readRecovery = {
|
|
241
|
+
page.readRecovery = {
|
|
242
|
+
action: restoring ? "context-restore" : "targeted-review",
|
|
243
|
+
};
|
|
244
|
+
if (restoring) page.readRecovery.fileVersion = fileVersion;
|
|
139
245
|
} else {
|
|
140
246
|
const unread = entry.spans[0]?.[0] === 0 ? entry.spans[0][1] : 0;
|
|
141
247
|
if (unread < span.total) {
|
|
@@ -162,12 +268,20 @@ export class ReadFileLoopGuard {
|
|
|
162
268
|
...(entry.summary?.outline
|
|
163
269
|
? { outline: entry.summary.outline }
|
|
164
270
|
: {}),
|
|
165
|
-
|
|
271
|
+
recoveryRead: {
|
|
272
|
+
path: filePath,
|
|
273
|
+
offset: 1,
|
|
274
|
+
limit: 80,
|
|
275
|
+
...(args.hashed === true ? { hashed: true } : {}),
|
|
276
|
+
...(args.raw === true ? { raw: true } : {}),
|
|
277
|
+
},
|
|
278
|
+
hint: "The unchanged file was read earlier, but historical coverage does not mean its source is visible now. Use any source excerpts actually present in this request. For missing details, use search_files or read_file with a specific offset and limit (at most 80 lines); recoveryRead is a bounded starting range, not permission to restart a full scan. Reading the file is not task completion.",
|
|
166
279
|
};
|
|
167
280
|
}
|
|
168
281
|
}
|
|
169
282
|
}
|
|
170
283
|
entry.spans = mergeSpan(entry.spans, page.readSpan);
|
|
284
|
+
entry.lastReadRequest = this.visibleRequest;
|
|
171
285
|
// Coverage alone cannot restore source code after compaction. Keep bounded
|
|
172
286
|
// observed pages for every file type, not only large Markdown outlines.
|
|
173
287
|
// Label truncation explicitly; a retained prefix is not the whole range.
|
|
@@ -232,6 +346,8 @@ export class ReadFileLoopGuard {
|
|
|
232
346
|
if (tool === "read_file") {
|
|
233
347
|
if (result?.readRecovery?.action === "edit-anchor-refresh")
|
|
234
348
|
this.batch.advanced = true;
|
|
349
|
+
if (result?.readRecovery?.action === "context-restore")
|
|
350
|
+
this.batch.advanced = true;
|
|
235
351
|
if (result?.readProgress?.newContent === true) this.batch.advanced = true;
|
|
236
352
|
if (result?.readProgress?.newContent === false) this.batch.duplicates++;
|
|
237
353
|
} else if (
|
|
@@ -308,7 +424,7 @@ export class ReadFileLoopGuard {
|
|
|
308
424
|
|
|
309
425
|
get recoveryHint() {
|
|
310
426
|
return this.repeatedBatches >= 1
|
|
311
|
-
? "Repeated file reads or identical large command outputs detected. Do not switch to run_code/run_shell to dump the same file again. Filter/count locally and print only relevant rows, totals and source line numbers, or use a focused search. The runtime continues unfinished read_file scans automatically and returns use-findings after EOF. Use
|
|
427
|
+
? "Repeated file reads or identical large command outputs detected. Do not switch to run_code/run_shell to dump the same file again. Filter/count locally and print only relevant rows, totals and source line numbers, or use a focused search. The runtime continues unfinished read_file scans automatically and returns use-findings after EOF. Use source evidence actually visible in this request; recover missing details with a specific offset and limit of at most 80 lines. Do not restart a whole-file scan after context compaction; answer once the requested evidence is sufficient."
|
|
312
428
|
: null;
|
|
313
429
|
}
|
|
314
430
|
|