pi-ui-extend 1.0.39 → 1.0.41
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 +1 -1
- package/dist/app/commands/command-registry.js +2 -2
- package/dist/app/commands/command-session-actions.d.ts +0 -1
- package/dist/app/commands/command-session-actions.js +22 -13
- package/dist/app/icons.d.ts +14 -0
- package/dist/app/icons.js +33 -0
- package/dist/app/rendering/conversation-tool-renderer.js +2 -2
- package/dist/app/rendering/dcp-stats.d.ts +6 -1
- package/dist/app/rendering/dcp-stats.js +214 -46
- package/dist/app/rendering/editor-panels.js +8 -5
- package/dist/app/session/lazy-session-manager.js +12 -1
- package/dist/app/session/tabs-controller.d.ts +2 -5
- package/dist/app/session/tabs-controller.js +12 -21
- package/dist/app/subagents/subagents-model.d.ts +14 -1
- package/dist/app/subagents/subagents-model.js +34 -15
- package/dist/app/types.d.ts +2 -0
- package/dist/bundled-extensions/session-title/config.js +1 -1
- package/dist/markdown-format.js +27 -9
- package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
- package/dist/schemas/pi-tools-suite-schema.js +46 -31
- package/external/pi-tools-suite/README.md +392 -52
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
- package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
- package/external/pi-tools-suite/docs/evals.md +684 -0
- package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
- package/external/pi-tools-suite/package.json +10 -3
- package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
- package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
- package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
- package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
- package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
- package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
- package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
- package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
- package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
- package/external/pi-tools-suite/src/config.ts +1 -22
- package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
- package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
- package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
- package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
- package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
- package/external/pi-tools-suite/src/dcp/config.ts +36 -61
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
- package/external/pi-tools-suite/src/dcp/index.ts +617 -203
- package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
- package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
- package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
- package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
- package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
- package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
- package/external/pi-tools-suite/src/dcp/state.ts +158 -580
- package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
- package/external/pi-tools-suite/src/index.ts +9 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
- package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
- package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
- package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
- package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
- package/package.json +6 -6
- package/schemas/pi-tools-suite.json +159 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
|
@@ -8,16 +8,18 @@ import {
|
|
|
8
8
|
createState,
|
|
9
9
|
resetState,
|
|
10
10
|
createInputFingerprint,
|
|
11
|
-
restoreState,
|
|
12
|
-
inheritCompressionBlocks,
|
|
13
11
|
} from "./state.js"
|
|
14
12
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
appendDcpJournalDelta,
|
|
14
|
+
appendDcpJournalOperation,
|
|
15
|
+
buildDcpJournalDelta,
|
|
16
|
+
createDcpJournalInit,
|
|
17
|
+
createDcpJournalMirror,
|
|
18
|
+
DcpJournalError,
|
|
19
|
+
readDcpJournalBranch,
|
|
20
|
+
replayDcpJournal,
|
|
21
|
+
type DcpJournalMirror,
|
|
22
|
+
} from "./journal.js"
|
|
21
23
|
import {
|
|
22
24
|
SYSTEM_PROMPT,
|
|
23
25
|
MANUAL_MODE_SYSTEM_PROMPT,
|
|
@@ -28,9 +30,9 @@ import {
|
|
|
28
30
|
} from "./prompts.js"
|
|
29
31
|
import {
|
|
30
32
|
applyPruning,
|
|
31
|
-
injectNudge,
|
|
32
33
|
getNudgeType,
|
|
33
34
|
detectCompressionCandidate,
|
|
35
|
+
detectEmergencyCompressionCandidate,
|
|
34
36
|
detectMessageCompressionCandidates,
|
|
35
37
|
analyzeEmergencyCurrentTurn,
|
|
36
38
|
emergencyPressureState,
|
|
@@ -39,25 +41,37 @@ import {
|
|
|
39
41
|
appendConcreteNudgeGuidance,
|
|
40
42
|
applyAnchoredNudges,
|
|
41
43
|
clearDcpNudgeAnchors,
|
|
44
|
+
hasCacheSafeNudgeCarrier,
|
|
42
45
|
nudgeTypeLabel,
|
|
43
46
|
upsertNudgeAnchor,
|
|
44
47
|
getActiveSummaryTokenEstimate,
|
|
45
48
|
resolveContextThresholds,
|
|
46
49
|
estimateTokens,
|
|
47
50
|
} from "./pruner.js"
|
|
48
|
-
import { stripStaleDcpMetadataFromMessage } from "./pruner-metadata.js"
|
|
51
|
+
import { estimateMessageTokens, stripStaleDcpMetadataFromMessage } from "./pruner-metadata.js"
|
|
49
52
|
import { summarizeDcpState, writeDcpDebugLog } from "./debug-log.js"
|
|
50
53
|
import type { DcpNudgeType } from "./pruner-types.js"
|
|
51
54
|
import { registerCompressTool } from "./compress-tool.js"
|
|
52
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
AutoCompressionBlockedError,
|
|
57
|
+
decideAutoCompress,
|
|
58
|
+
createAutoCompressionBlock,
|
|
59
|
+
} from "./auto-compress.js"
|
|
53
60
|
import { DCP_STATS_MESSAGE_TYPE, registerCommands } from "./commands.js"
|
|
54
61
|
import { normalizeDcpContextUsage } from "./ui.js"
|
|
55
62
|
import { safeGetContextUsage } from "../context-usage.js"
|
|
56
63
|
import {
|
|
57
64
|
collectProviderToolResultEvidence,
|
|
58
65
|
providerPayloadIncludesToolResult,
|
|
66
|
+
providerPayloadIncludesReminder,
|
|
67
|
+
providerPayloadRevision,
|
|
68
|
+
ProviderEvidenceTracker,
|
|
59
69
|
} from "./provider-tool-results.js"
|
|
60
|
-
import {
|
|
70
|
+
import { rehydrateToolRecordsFromMessages } from "./recovery.js"
|
|
71
|
+
import { inferDcpBlockedReason, planDcpBudget } from "./progress-controller.js"
|
|
72
|
+
import { createBudgetedAutoCompressionBlock } from "./auto-compress-budget.js"
|
|
73
|
+
import { outstandingCompressionTokens, resetCompressionProgress, routineRecoveryTokens, trackCompressionProgress } from "./compression-progress.js"
|
|
74
|
+
import { captureDcpTransactionGuard, cloneDcpTransactionState, runDcpStateTransaction, invalidateDcpStateOwner } from "./state-transaction.js"
|
|
61
75
|
|
|
62
76
|
// ---------------------------------------------------------------------------
|
|
63
77
|
// Helpers
|
|
@@ -103,10 +117,8 @@ function isUserVisibleOnlyMessage(message: any): boolean {
|
|
|
103
117
|
return message.details?.userVisibleOnly === true
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
// the removed inline control-message path.
|
|
109
|
-
const DCP_CONTROL_PLANE_CUSTOM_TYPES = new Set(["dcp-state", "dcp-nudge", "dcp-message-ids"])
|
|
120
|
+
// Diagnostic DCP custom messages are never provider context.
|
|
121
|
+
const DCP_CONTROL_PLANE_CUSTOM_TYPES = new Set(["dcp-nudge"])
|
|
110
122
|
const SUMMARY_BUFFER_MAX_CONTEXT_BONUS = 0.05
|
|
111
123
|
|
|
112
124
|
function isDcpControlPlaneMessage(message: any): boolean {
|
|
@@ -117,9 +129,9 @@ function isDcpControlPlaneMessage(message: any): boolean {
|
|
|
117
129
|
// Module export
|
|
118
130
|
// ---------------------------------------------------------------------------
|
|
119
131
|
|
|
120
|
-
export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
132
|
+
export default async function dcpModule(pi: ExtensionAPI, dependencies: { config?: ReturnType<typeof loadConfig>; state?: ReturnType<typeof createState> } = {}): Promise<void> {
|
|
121
133
|
// ── 1. Load config ────────────────────────────────────────────────────────
|
|
122
|
-
const config = loadConfig()
|
|
134
|
+
const config = dependencies.config ?? loadConfig()
|
|
123
135
|
const configForContext = (ctx: unknown) => resolveModelConfig(config, modelKeysFromContext(ctx))
|
|
124
136
|
const hasEnabledModelOverride = Object.values(config.modelOverrides).some(
|
|
125
137
|
(override) => override.enabled === true,
|
|
@@ -128,9 +140,123 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
128
140
|
if (!config.enabled && !hasEnabledModelOverride) return
|
|
129
141
|
|
|
130
142
|
// ── 2. Create state ───────────────────────────────────────────────────────
|
|
131
|
-
const state = createState()
|
|
132
|
-
let
|
|
133
|
-
|
|
143
|
+
const state = dependencies.state ?? createState()
|
|
144
|
+
let journalMirror: DcpJournalMirror | undefined
|
|
145
|
+
let journalSupported = false
|
|
146
|
+
let journalBlockedReason: string | undefined
|
|
147
|
+
let journalPersistent = false
|
|
148
|
+
const providerEvidenceTracker = new ProviderEvidenceTracker()
|
|
149
|
+
let providerEvidenceCommitQueue = Promise.resolve()
|
|
150
|
+
let latestProviderOpportunityAvailable = false
|
|
151
|
+
let latestProviderOpportunityKind: "routine" | "emergency" | undefined
|
|
152
|
+
let latestProviderReminder: string | undefined
|
|
153
|
+
const warnedProgress = new Set<string>()
|
|
154
|
+
const isJournalSessionSupported = () => journalSupported && journalBlockedReason === undefined
|
|
155
|
+
const persistJournalState = async (
|
|
156
|
+
ctx: ExtensionContext,
|
|
157
|
+
targetState: ReturnType<typeof createState>,
|
|
158
|
+
publication: { beforePublish?: () => void; onPublished?: () => void } = {},
|
|
159
|
+
): Promise<void> => {
|
|
160
|
+
if (!isJournalSessionSupported() || !journalMirror) {
|
|
161
|
+
throw new DcpJournalError(journalBlockedReason ?? "DCP journal is not initialized for this session")
|
|
162
|
+
}
|
|
163
|
+
if (!journalPersistent) {
|
|
164
|
+
const delta = buildDcpJournalDelta(targetState, journalMirror)
|
|
165
|
+
if (!delta) return
|
|
166
|
+
publication.beforePublish?.()
|
|
167
|
+
journalMirror = createDcpJournalMirror(targetState, delta.operationId)
|
|
168
|
+
publication.onPublished?.()
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
journalMirror = appendDcpJournalDelta(pi, ctx, targetState, journalMirror, publication)
|
|
172
|
+
}
|
|
173
|
+
const branchHasConversation = (branch: readonly any[]): boolean => branch.some((entry) =>
|
|
174
|
+
entry?.type === "message" || entry?.type === "custom_message" || entry?.type === "compaction" || entry?.type === "branch_summary",
|
|
175
|
+
)
|
|
176
|
+
const loadJournalState = async (ctx: ExtensionContext, allowInitialize: boolean): Promise<void> => {
|
|
177
|
+
journalMirror = undefined
|
|
178
|
+
journalSupported = false
|
|
179
|
+
journalBlockedReason = undefined
|
|
180
|
+
journalPersistent = false
|
|
181
|
+
const branch = await readDcpJournalBranch(ctx)
|
|
182
|
+
const manager = ctx.sessionManager as any
|
|
183
|
+
const persistentSession = (typeof manager.isPersisted === "function" && manager.isPersisted())
|
|
184
|
+
|| (typeof manager.getSessionFile === "function" && Boolean(manager.getSessionFile()))
|
|
185
|
+
try {
|
|
186
|
+
const replay = replayDcpJournal(branch, state)
|
|
187
|
+
if (replay.initialized && replay.lastOperationId) {
|
|
188
|
+
journalSupported = true
|
|
189
|
+
journalPersistent = persistentSession
|
|
190
|
+
journalMirror = createDcpJournalMirror(state, replay.lastOperationId)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
if (allowInitialize && !branchHasConversation(branch)) {
|
|
194
|
+
const init = createDcpJournalInit()
|
|
195
|
+
if (persistentSession) appendDcpJournalOperation(pi, ctx, init)
|
|
196
|
+
journalSupported = true
|
|
197
|
+
journalPersistent = persistentSession
|
|
198
|
+
journalMirror = createDcpJournalMirror(state, init.operationId)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
// Clean break: a pre-journal conversation is deliberately unsupported.
|
|
202
|
+
// Do not inspect external state, infer prior blocks, or create a
|
|
203
|
+
// compatibility checkpoint from existing history.
|
|
204
|
+
journalSupported = false
|
|
205
|
+
} catch (error) {
|
|
206
|
+
journalBlockedReason = error instanceof Error ? error.message : String(error)
|
|
207
|
+
journalSupported = false
|
|
208
|
+
journalMirror = undefined
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const ensureEphemeralJournal = (ctx: ExtensionContext): void => {
|
|
212
|
+
if (journalSupported || journalBlockedReason) return
|
|
213
|
+
const manager = ctx.sessionManager as any
|
|
214
|
+
const persisted = typeof manager.isPersisted === "function" ? manager.isPersisted() : false
|
|
215
|
+
const sessionFile = typeof manager.getSessionFile === "function" ? manager.getSessionFile() : undefined
|
|
216
|
+
if (persisted || sessionFile) return
|
|
217
|
+
let branch: unknown[] = []
|
|
218
|
+
try {
|
|
219
|
+
const current = manager.getBranch?.()
|
|
220
|
+
branch = Array.isArray(current) ? current : []
|
|
221
|
+
} catch { return }
|
|
222
|
+
if (branch.length > 0) return
|
|
223
|
+
const init = createDcpJournalInit()
|
|
224
|
+
journalSupported = true
|
|
225
|
+
journalPersistent = false
|
|
226
|
+
journalMirror = createDcpJournalMirror(state, init.operationId)
|
|
227
|
+
}
|
|
228
|
+
const warnProgress = (ctx: ExtensionContext, reason: string, message: string) => {
|
|
229
|
+
const key = `${state.sessionEpoch}:${reason}`
|
|
230
|
+
if (warnedProgress.has(key)) return
|
|
231
|
+
warnedProgress.add(key)
|
|
232
|
+
writeDcpDebugLog(configForContext(ctx), "context.progress_warning", { reason, message }, ctx)
|
|
233
|
+
try {
|
|
234
|
+
pi.appendEntry("dcp-nudge", {
|
|
235
|
+
event: "progress-blocked", reason, message,
|
|
236
|
+
ignoredOpportunities: state.consecutiveIgnoredNudges,
|
|
237
|
+
remainingRecoveryTokens: state.compressionProgress?.remainingTokens,
|
|
238
|
+
createdAt: Date.now(),
|
|
239
|
+
})
|
|
240
|
+
} catch { /* Diagnostic only; do not turn an explicit blocked state into an extension failure. */ }
|
|
241
|
+
try { ctx.ui.notify(message, "warning") } catch { /* Headless/notification failure is not a persistence failure. */ }
|
|
242
|
+
}
|
|
243
|
+
const invalidateOwner = () => {
|
|
244
|
+
invalidateDcpStateOwner(state)
|
|
245
|
+
providerEvidenceTracker.reset()
|
|
246
|
+
latestProviderOpportunityAvailable = false
|
|
247
|
+
latestProviderOpportunityKind = undefined
|
|
248
|
+
latestProviderReminder = undefined
|
|
249
|
+
resetCompressionProgress(state)
|
|
250
|
+
warnedProgress.clear()
|
|
251
|
+
}
|
|
252
|
+
pi.on("model_select", invalidateOwner)
|
|
253
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
254
|
+
invalidateOwner()
|
|
255
|
+
resetState(state)
|
|
256
|
+
if (config.manualMode.enabled) state.manualMode = true
|
|
257
|
+
await loadJournalState(ctx, false)
|
|
258
|
+
})
|
|
259
|
+
pi.on("session_compact", invalidateOwner)
|
|
134
260
|
const appendNudgeTelemetry = (
|
|
135
261
|
event: "emitted" | "upgraded" | "reapplied",
|
|
136
262
|
type: DcpNudgeType,
|
|
@@ -164,72 +290,59 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
164
290
|
}
|
|
165
291
|
|
|
166
292
|
// ── 3. Register compress tool ─────────────────────────────────────────────
|
|
167
|
-
registerCompressTool(pi, state, config
|
|
293
|
+
registerCompressTool(pi, state, config, {
|
|
294
|
+
persistState: persistJournalState,
|
|
295
|
+
isSessionSupported: isJournalSessionSupported,
|
|
296
|
+
})
|
|
168
297
|
|
|
169
298
|
// ── 4. Register /dcp commands ─────────────────────────────────────────────
|
|
170
|
-
registerCommands(pi, state, config
|
|
299
|
+
registerCommands(pi, state, config, {
|
|
300
|
+
persistState: persistJournalState,
|
|
301
|
+
isSessionSupported: isJournalSessionSupported,
|
|
302
|
+
})
|
|
171
303
|
|
|
172
304
|
// ── 5. session_start: restore state from session entries ──────────────────
|
|
173
305
|
pi.on("session_start", async (event, ctx) => {
|
|
174
|
-
// Reset to a clean slate first.
|
|
175
306
|
resetState(state)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
191
|
-
restoreState(state, await loadDcpState(ctx))
|
|
192
|
-
pendingInheritedBlockReconciliation = state.compressionBlocks.length > 0
|
|
193
|
-
|
|
194
|
-
// fork/resume/new sessions inherit the source conversation but get a fresh
|
|
195
|
-
// sidecar; inherit the previous session's compression blocks so they are
|
|
196
|
-
// not silently lost (which previously forced re-compressing all history).
|
|
197
|
-
if (state.compressionBlocks.length === 0 && event.previousSessionFile) {
|
|
307
|
+
providerEvidenceTracker.reset()
|
|
308
|
+
latestProviderOpportunityAvailable = false
|
|
309
|
+
latestProviderOpportunityKind = undefined
|
|
310
|
+
latestProviderReminder = undefined
|
|
311
|
+
warnedProgress.clear()
|
|
312
|
+
if (config.manualMode.enabled) state.manualMode = true
|
|
313
|
+
await loadJournalState(ctx, event.reason === "new" || event.reason === "startup")
|
|
314
|
+
writeDcpDebugLog(configForContext(ctx), "session_start.journal", {
|
|
315
|
+
reason: event.reason,
|
|
316
|
+
supported: journalSupported,
|
|
317
|
+
blockedReason: journalBlockedReason,
|
|
318
|
+
operations: journalMirror ? "replayed" : "none",
|
|
319
|
+
}, ctx)
|
|
320
|
+
if (!journalSupported && !journalBlockedReason) {
|
|
198
321
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
reason: event.reason,
|
|
205
|
-
previousSessionFile: event.previousSessionFile,
|
|
206
|
-
added,
|
|
207
|
-
totalBlocks: state.compressionBlocks.length,
|
|
208
|
-
}, ctx)
|
|
209
|
-
// Persist inherited state into this session's own sidecar so a later
|
|
210
|
-
// reload restores it directly.
|
|
211
|
-
await saveDcpState(ctx, state)
|
|
212
|
-
}
|
|
213
|
-
} catch {
|
|
214
|
-
// Inheritance is best-effort; never block session startup.
|
|
215
|
-
}
|
|
322
|
+
ctx.ui.notify(
|
|
323
|
+
"DCP is disabled for this pre-journal session. Start a new session to use the current DCP implementation.",
|
|
324
|
+
"warning",
|
|
325
|
+
)
|
|
326
|
+
} catch { /* Headless mode. */ }
|
|
216
327
|
}
|
|
217
|
-
|
|
218
|
-
// Headless by design: no extension status/footer/widgets are rendered.
|
|
219
328
|
})
|
|
220
329
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
330
|
+
// Journal operations are committed at the mutation boundary; shutdown does
|
|
331
|
+
// not write a full runtime snapshot.
|
|
332
|
+
pi.on("session_shutdown", async () => {
|
|
333
|
+
journalMirror = undefined
|
|
334
|
+
journalSupported = false
|
|
335
|
+
journalBlockedReason = undefined
|
|
336
|
+
journalPersistent = false
|
|
227
337
|
})
|
|
228
338
|
|
|
229
339
|
// ── 7. before_agent_start: inject system prompt ───────────────────────────
|
|
230
340
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
231
341
|
const effectiveConfig = configForContext(_ctx)
|
|
232
342
|
if (!effectiveConfig.enabled) return { systemPrompt: event.systemPrompt }
|
|
343
|
+
ensureEphemeralJournal(_ctx)
|
|
344
|
+
if (journalBlockedReason) throw new DcpJournalError(`DCP journal is blocked: ${journalBlockedReason}`)
|
|
345
|
+
if (!journalSupported) return { systemPrompt: event.systemPrompt }
|
|
233
346
|
|
|
234
347
|
const promptAddition = state.manualMode
|
|
235
348
|
? MANUAL_MODE_SYSTEM_PROMPT
|
|
@@ -295,7 +408,9 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
295
408
|
|
|
296
409
|
// ── 10. context: apply pruning and inject nudges ──────────────────────────
|
|
297
410
|
pi.on("context", async (event, ctx) => {
|
|
411
|
+
const contextEpoch = state.sessionEpoch
|
|
298
412
|
const effectiveConfig = configForContext(ctx)
|
|
413
|
+
ensureEphemeralJournal(ctx)
|
|
299
414
|
const contextMessages = event.messages
|
|
300
415
|
.filter((message: any) => !isUserVisibleOnlyMessage(message) && !isDcpControlPlaneMessage(message))
|
|
301
416
|
.map((message: any) => stripStaleDcpMetadataFromMessage(message))
|
|
@@ -306,7 +421,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
306
421
|
filteredMessages: contextMessages.length,
|
|
307
422
|
outputMessages: messages.length,
|
|
308
423
|
messageIdControl: "distributed-carriers",
|
|
309
|
-
state: summarizeDcpState(state),
|
|
424
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
310
425
|
...details,
|
|
311
426
|
}, ctx)
|
|
312
427
|
return { messages }
|
|
@@ -317,6 +432,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
317
432
|
filteredMessages: contextMessages.length,
|
|
318
433
|
filteredDcpControlPlaneMessages: event.messages.length - contextMessages.length,
|
|
319
434
|
}, ctx)
|
|
435
|
+
latestProviderOpportunityAvailable = false
|
|
320
436
|
if (!effectiveConfig.enabled) {
|
|
321
437
|
writeDcpDebugLog(effectiveConfig, "context.disabled", {
|
|
322
438
|
inputMessages: event.messages.length,
|
|
@@ -324,59 +440,47 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
324
440
|
}, ctx)
|
|
325
441
|
return { messages: contextMessages }
|
|
326
442
|
}
|
|
443
|
+
if (journalBlockedReason) {
|
|
444
|
+
throw new DcpJournalError(`DCP journal is blocked: ${journalBlockedReason}`)
|
|
445
|
+
}
|
|
446
|
+
if (!journalSupported) {
|
|
447
|
+
return { messages: event.messages.filter((message: any) => !isUserVisibleOnlyMessage(message)) }
|
|
448
|
+
}
|
|
449
|
+
latestProviderOpportunityKind = undefined
|
|
450
|
+
latestProviderReminder = undefined
|
|
327
451
|
annotateMessagesWithBranchEntryIds(contextMessages, ctx)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
writeDcpDebugLog(
|
|
332
|
-
effectiveConfig,
|
|
333
|
-
"context.reconciled_inherited_blocks",
|
|
334
|
-
{ ...reconciliation },
|
|
335
|
-
ctx,
|
|
336
|
-
)
|
|
337
|
-
if (
|
|
338
|
-
reconciliation.activatedBlockIds.length > 0 ||
|
|
339
|
-
reconciliation.deactivatedBlockIds.length > 0
|
|
340
|
-
) {
|
|
341
|
-
await saveDcpState(ctx, state)
|
|
342
|
-
}
|
|
452
|
+
const rehydration = rehydrateToolRecordsFromMessages(contextMessages, state)
|
|
453
|
+
if (rehydration.recordsUpdated > 0) {
|
|
454
|
+
writeDcpDebugLog(effectiveConfig, "context.rehydrated_tool_records", { ...rehydration }, ctx)
|
|
343
455
|
}
|
|
344
|
-
const prunedToolCountBeforeCheckpoint = state.prunedToolIds.size
|
|
345
456
|
let prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
await saveDcpState(ctx, state)
|
|
350
|
-
writeDcpDebugLog(effectiveConfig, "prune.tool_checkpoint", {
|
|
351
|
-
committed: automaticPrunesCommitted,
|
|
352
|
-
clearedAnchors,
|
|
353
|
-
turn: state.currentTurn,
|
|
354
|
-
blockId: state.lastAutomaticPruneBlockId,
|
|
355
|
-
state: summarizeDcpState(state),
|
|
356
|
-
}, ctx)
|
|
357
|
-
}
|
|
457
|
+
// Stable IDs and any pruning decisions that affect this provider-visible
|
|
458
|
+
// projection must be committed before the request can use them.
|
|
459
|
+
await persistJournalState(ctx, state)
|
|
358
460
|
let candidate = null as ReturnType<typeof detectCompressionCandidate>
|
|
461
|
+
let emergencyCompressionCandidate = null as ReturnType<typeof detectEmergencyCompressionCandidate>
|
|
359
462
|
let messageCandidates = [] as ReturnType<typeof detectMessageCompressionCandidates>
|
|
360
463
|
let emergencySelection = null as ReturnType<typeof analyzeEmergencyCurrentTurn> | null
|
|
361
464
|
let emergencyPruneResult = null as ReturnType<typeof pruneEmergencyCurrentTurn> | null
|
|
362
465
|
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
const
|
|
466
|
+
// Manual mode skips routine autonomous nudges and automatic summary
|
|
467
|
+
// creation. The bounded emergency safety path remains separate, matching
|
|
468
|
+
// the manual-mode prompt.
|
|
469
|
+
const nativeUsage = normalizeDcpContextUsage(safeGetContextUsage(ctx))
|
|
470
|
+
const ctxModel = (ctx as any).model
|
|
471
|
+
const fallbackContextWindow = nativeUsage?.contextWindow ?? (
|
|
472
|
+
typeof ctxModel?.contextWindow === "number" && Number.isFinite(ctxModel.contextWindow) && ctxModel.contextWindow > 0
|
|
473
|
+
? ctxModel.contextWindow
|
|
474
|
+
: undefined
|
|
475
|
+
)
|
|
476
|
+
const repoProjectedTokens = prunedMessages.reduce(
|
|
477
|
+
(sum, message) => sum + estimateMessageTokens(message),
|
|
478
|
+
0,
|
|
479
|
+
)
|
|
480
|
+
const usage = nativeUsage ?? (fallbackContextWindow
|
|
481
|
+
? { tokens: null, contextWindow: fallbackContextWindow, percent: null }
|
|
482
|
+
: undefined)
|
|
367
483
|
if (usage) {
|
|
368
|
-
const contextPercent = typeof usage.percent === "number" && Number.isFinite(usage.percent)
|
|
369
|
-
? usage.percent / 100
|
|
370
|
-
: typeof usage.tokens === "number"
|
|
371
|
-
? usage.tokens / usage.contextWindow
|
|
372
|
-
: undefined
|
|
373
|
-
|
|
374
|
-
if (contextPercent === undefined) {
|
|
375
|
-
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
376
|
-
if (clearedAnchors > 0) await saveDcpState(ctx, state)
|
|
377
|
-
return finishContext("unknown-context-percent", prunedMessages, { clearedAnchors })
|
|
378
|
-
}
|
|
379
|
-
|
|
380
484
|
// Record the observed context window on EVERY context event (before
|
|
381
485
|
// any early return) so a mid-session model/window downgrade is
|
|
382
486
|
// detectable even when earlier passes were below threshold. We
|
|
@@ -392,34 +496,49 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
392
496
|
state.lastContextWindow = currentContextWindow
|
|
393
497
|
}
|
|
394
498
|
|
|
395
|
-
const ctxModel = (ctx as any).model
|
|
396
499
|
const provider = ctxModel?.provider ?? ctxModel?.providerId ?? ctxModel?.providerID
|
|
397
500
|
const model = ctxModel?.id ?? ctxModel?.model ?? ctxModel?.modelId ?? ctxModel?.modelID
|
|
398
501
|
const thresholds = resolveContextThresholds(effectiveConfig, [
|
|
399
502
|
provider && model ? `${provider}/${model}` : undefined,
|
|
400
503
|
model,
|
|
401
504
|
], usage.contextWindow)
|
|
402
|
-
if (effectiveConfig.compress.summaryBuffer) {
|
|
403
|
-
const summaryBonus = getActiveSummaryTokenEstimate(state) / usage.contextWindow
|
|
404
|
-
thresholds.maxContextPercent += Math.min(summaryBonus, SUMMARY_BUFFER_MAX_CONTEXT_BONUS)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
505
|
const emergencySettings = effectiveConfig.strategies.emergencyCurrentTurnPruning
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
506
|
+
const estimatorMarginTokens = Math.max(256, Math.ceil(usage.contextWindow * 0.0025))
|
|
507
|
+
const budget = planDcpBudget({
|
|
508
|
+
providerUsageTokens: nativeUsage?.tokens,
|
|
509
|
+
repoProjectedTokens,
|
|
510
|
+
contextWindow: usage.contextWindow,
|
|
511
|
+
reservedOutputTokens: typeof ctxModel?.maxTokens === "number" ? ctxModel.maxTokens : 0,
|
|
512
|
+
reservedToolTokens: 0,
|
|
513
|
+
maxContextPercent: thresholds.maxContextPercent,
|
|
514
|
+
hardContextPercent: emergencySettings.hardContextPercent,
|
|
515
|
+
targetContextPercent: Math.min(
|
|
516
|
+
Math.max(0, emergencySettings.targetContextPercent),
|
|
517
|
+
Math.max(0, thresholds.maxContextPercent * 0.9),
|
|
518
|
+
),
|
|
519
|
+
summaryBufferEnabled: effectiveConfig.compress.summaryBuffer,
|
|
520
|
+
activeSummaryTokens: getActiveSummaryTokenEstimate(state),
|
|
521
|
+
summaryBufferMaxBonusRatio: SUMMARY_BUFFER_MAX_CONTEXT_BONUS,
|
|
522
|
+
estimatorMarginTokens,
|
|
523
|
+
})
|
|
524
|
+
thresholds.maxContextPercent = budget.softHeadroomTokens / usage.contextWindow
|
|
525
|
+
const contextPercent = budget.projectedBeforeTokens / usage.contextWindow
|
|
526
|
+
const nativePressure = emergencyPressureState(
|
|
413
527
|
contextPercent,
|
|
414
528
|
thresholds.maxContextPercent,
|
|
415
529
|
emergencySettings.hardContextPercent,
|
|
416
530
|
)
|
|
531
|
+
const hardEmergencyReached = nativePressure.hardEmergencyReached || budget.hardPressure
|
|
532
|
+
const contextLimitReached = nativePressure.contextLimitReached || budget.pressured
|
|
533
|
+
const emergencyPressureReached = nativePressure.emergencyPressureReached || budget.pressured
|
|
417
534
|
const routineNudgesAllowed = contextPercent > thresholds.minContextPercent
|
|
535
|
+
if (!budget.capacityExceeded && state.progressRecovery) state.progressRecovery = undefined
|
|
418
536
|
if (!emergencyPressureReached && !routineNudgesAllowed) {
|
|
419
537
|
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
420
|
-
const resetEmergencyPasses = state.consecutiveIgnoredStrongNudges > 0
|
|
421
|
-
state
|
|
422
|
-
|
|
538
|
+
const resetEmergencyPasses = state.consecutiveIgnoredStrongNudges > 0 || state.consecutiveIgnoredNudges > 0 || !!state.compressionProgress
|
|
539
|
+
resetCompressionProgress(state)
|
|
540
|
+
warnedProgress.clear()
|
|
541
|
+
if (clearedAnchors > 0) await persistJournalState(ctx, state)
|
|
423
542
|
return finishContext("below-threshold", prunedMessages, {
|
|
424
543
|
contextPercent,
|
|
425
544
|
thresholds,
|
|
@@ -455,9 +574,40 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
455
574
|
typeof currentContextWindow === "number" &&
|
|
456
575
|
Number.isFinite(currentContextWindow) &&
|
|
457
576
|
currentContextWindow !== previousContextWindow
|
|
458
|
-
if (contextWindowChanged) state
|
|
577
|
+
if (contextWindowChanged) resetCompressionProgress(state)
|
|
578
|
+
|
|
579
|
+
const requestedRecoveryTokens = emergencyPressureReached
|
|
580
|
+
? budget.requiredSavingsTokens
|
|
581
|
+
: routineRecoveryTokens(budget.projectedBeforeTokens, usage.contextWindow, thresholds.minContextPercent)
|
|
582
|
+
const progressInput = {
|
|
583
|
+
projectedTokens: repoProjectedTokens,
|
|
584
|
+
observedTokens: budget.projectedBeforeTokens,
|
|
585
|
+
contextWindow: usage.contextWindow,
|
|
586
|
+
requiredTokens: requestedRecoveryTokens,
|
|
587
|
+
kind: emergencyPressureReached ? "emergency" as const : "routine" as const,
|
|
588
|
+
targetHeadroomTokens: emergencyPressureReached ? budget.targetHeadroomTokens : undefined,
|
|
589
|
+
}
|
|
590
|
+
const remainingRecoveryTokens = outstandingCompressionTokens(state, progressInput)
|
|
591
|
+
if (remainingRecoveryTokens === 0) {
|
|
592
|
+
resetCompressionProgress(state)
|
|
593
|
+
warnedProgress.clear()
|
|
594
|
+
}
|
|
595
|
+
const planningRecoveryTokens = remainingRecoveryTokens !== undefined && remainingRecoveryTokens > 0
|
|
596
|
+
? state.compressionProgress?.kind === progressInput.kind ? remainingRecoveryTokens : Math.max(remainingRecoveryTokens, requestedRecoveryTokens)
|
|
597
|
+
: requestedRecoveryTokens
|
|
598
|
+
const routineEscalated = !state.manualMode && routineNudgesAllowed &&
|
|
599
|
+
state.consecutiveIgnoredNudges > Math.max(0, Math.floor(effectiveConfig.compress.autoCompress.patience))
|
|
600
|
+
const progressPressureReached = emergencyPressureReached || routineEscalated
|
|
601
|
+
// A marathon's first useful reminder must not depend on already having
|
|
602
|
+
// ignored a reminder. Plan a closed, provider-seen same-turn prefix in
|
|
603
|
+
// the routine zone too; only automatic publication waits for patience.
|
|
604
|
+
const routineSameTurnPlanning = !state.manualMode && routineNudgesAllowed && effectiveConfig.compress.autoCandidates.enabled
|
|
605
|
+
const sameTurnPlanningAllowed = emergencyPressureReached || routineSameTurnPlanning
|
|
606
|
+
const planningThreshold = emergencyPressureReached
|
|
607
|
+
? Math.min(thresholds.maxContextPercent, emergencySettings.hardContextPercent)
|
|
608
|
+
: thresholds.minContextPercent
|
|
459
609
|
|
|
460
|
-
const nudgeType = hardEmergencyReached && !contextLimitReached
|
|
610
|
+
const nudgeType = routineEscalated || (hardEmergencyReached && !contextLimitReached)
|
|
461
611
|
? "context-strong"
|
|
462
612
|
: windowDowngraded
|
|
463
613
|
? "context-strong"
|
|
@@ -479,6 +629,9 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
479
629
|
state,
|
|
480
630
|
effectiveConfig,
|
|
481
631
|
contextPercent,
|
|
632
|
+
progressPressureReached
|
|
633
|
+
? { requiredSavingsTokens: planningRecoveryTokens }
|
|
634
|
+
: undefined,
|
|
482
635
|
)
|
|
483
636
|
messageCandidates = detectMessageCompressionCandidates(
|
|
484
637
|
prunedMessages,
|
|
@@ -489,10 +642,11 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
489
642
|
writeDcpDebugLog(effectiveConfig, "context.candidates", {
|
|
490
643
|
contextPercent,
|
|
491
644
|
thresholds,
|
|
645
|
+
budget,
|
|
492
646
|
nudgeType,
|
|
493
647
|
candidate,
|
|
494
648
|
messageCandidates,
|
|
495
|
-
state: summarizeDcpState(state),
|
|
649
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
496
650
|
}, ctx)
|
|
497
651
|
}
|
|
498
652
|
|
|
@@ -500,7 +654,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
500
654
|
candidate !== null || messageCandidates.length > 0
|
|
501
655
|
if (
|
|
502
656
|
emergencySettings.enabled &&
|
|
503
|
-
|
|
657
|
+
sameTurnPlanningAllowed &&
|
|
504
658
|
!manualEmergencyOnly &&
|
|
505
659
|
!hasNormalCompressionSuggestion
|
|
506
660
|
) {
|
|
@@ -513,79 +667,184 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
513
667
|
emergencySelection,
|
|
514
668
|
effectiveConfig,
|
|
515
669
|
)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (sameTurnPlanningAllowed && !manualEmergencyOnly && candidate === null) {
|
|
673
|
+
emergencyCompressionCandidate = detectEmergencyCompressionCandidate(
|
|
674
|
+
prunedMessages,
|
|
675
|
+
state,
|
|
676
|
+
effectiveConfig,
|
|
677
|
+
contextPercent,
|
|
678
|
+
planningThreshold,
|
|
679
|
+
{ requiredSavingsTokens: planningRecoveryTokens },
|
|
680
|
+
)
|
|
681
|
+
if (emergencyCompressionCandidate) {
|
|
682
|
+
writeDcpDebugLog(effectiveConfig, "context.emergency_compression_candidate", {
|
|
683
|
+
contextPercent,
|
|
684
|
+
thresholds,
|
|
685
|
+
candidate: emergencyCompressionCandidate,
|
|
686
|
+
...(emergencySelection?.stats ?? {}),
|
|
687
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
688
|
+
}, ctx)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (
|
|
693
|
+
emergencySelection &&
|
|
694
|
+
candidate === null &&
|
|
695
|
+
emergencyCompressionCandidate === null
|
|
696
|
+
) {
|
|
516
697
|
writeDcpDebugLog(effectiveConfig, "context.strong_nudge_without_candidate", {
|
|
517
698
|
contextPercent,
|
|
518
699
|
thresholds,
|
|
519
700
|
nudgeType,
|
|
520
701
|
emergencyCandidates: messageCandidates.length,
|
|
521
702
|
...emergencySelection.stats,
|
|
522
|
-
state: summarizeDcpState(state),
|
|
703
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
523
704
|
}, ctx)
|
|
524
705
|
}
|
|
525
706
|
|
|
526
707
|
let hasCompressionSuggestion =
|
|
527
|
-
candidate !== null || messageCandidates.length > 0
|
|
708
|
+
candidate !== null || emergencyCompressionCandidate !== null || messageCandidates.length > 0
|
|
709
|
+
if (!manualEmergencyOnly && (hasCompressionSuggestion || emergencyPressureReached)) {
|
|
710
|
+
trackCompressionProgress(state, progressInput)
|
|
711
|
+
latestProviderOpportunityKind = emergencyPressureReached ? "emergency" : !state.manualMode ? "routine" : undefined
|
|
712
|
+
}
|
|
713
|
+
const blockedReason = inferDcpBlockedReason({
|
|
714
|
+
pressured: progressPressureReached,
|
|
715
|
+
candidateAvailable: candidate !== null || emergencyCompressionCandidate !== null,
|
|
716
|
+
messageCandidateCount: messageCandidates.length,
|
|
717
|
+
requiredSavingsTokens: planningRecoveryTokens,
|
|
718
|
+
capacityExceeded: budget.capacityExceeded,
|
|
719
|
+
emergencyStats: emergencySelection?.stats,
|
|
720
|
+
})
|
|
721
|
+
if (blockedReason) {
|
|
722
|
+
writeDcpDebugLog(effectiveConfig, "context.progress_blocked", {
|
|
723
|
+
phase: "blocked",
|
|
724
|
+
blocked_reason: blockedReason,
|
|
725
|
+
contextPercent,
|
|
726
|
+
thresholds,
|
|
727
|
+
budget,
|
|
728
|
+
...(emergencySelection?.stats ?? {}),
|
|
729
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
730
|
+
}, ctx)
|
|
731
|
+
if (routineEscalated) warnProgress(ctx, blockedReason,
|
|
732
|
+
`DCP cannot safely meet its compression goal: ${blockedReason}. Context has not been recovered.`)
|
|
733
|
+
}
|
|
734
|
+
if (blockedReason && budget.capacityExceeded) {
|
|
735
|
+
state.progressRecovery = {
|
|
736
|
+
blockedReason,
|
|
737
|
+
projectedBeforeTokens: budget.projectedBeforeTokens,
|
|
738
|
+
inputCapacityTokens: budget.inputCapacityTokens,
|
|
739
|
+
requiredSavingsTokens: budget.requiredSavingsTokens,
|
|
740
|
+
contextWindow: budget.contextWindow,
|
|
741
|
+
createdAt: Date.now(),
|
|
742
|
+
}
|
|
743
|
+
clearDcpNudgeAnchors(state)
|
|
744
|
+
await persistJournalState(ctx, state)
|
|
745
|
+
const abortSupported = typeof (ctx as any).abort === "function"
|
|
746
|
+
if (abortSupported) (ctx as any).abort()
|
|
747
|
+
writeDcpDebugLog(effectiveConfig, "context.progress_handoff", {
|
|
748
|
+
phase: "blocked",
|
|
749
|
+
blocked_reason: blockedReason,
|
|
750
|
+
handoff: abortSupported ? "abort-current-agent-operation" : "abort-unavailable",
|
|
751
|
+
budget,
|
|
752
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
753
|
+
}, ctx)
|
|
754
|
+
return finishContext("progress.blocked_handoff", prunedMessages, {
|
|
755
|
+
blocked_reason: blockedReason,
|
|
756
|
+
handoff: abortSupported ? "abort-current-agent-operation" : "abort-unavailable",
|
|
757
|
+
budget,
|
|
758
|
+
})
|
|
759
|
+
}
|
|
528
760
|
if (!manualEmergencyOnly && !emergencyPressureReached && !hasCompressionSuggestion) {
|
|
529
761
|
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
530
|
-
if (clearedAnchors > 0) await
|
|
762
|
+
if (clearedAnchors > 0) await persistJournalState(ctx, state)
|
|
531
763
|
if (nudgeType || clearedAnchors > 0) {
|
|
532
764
|
writeDcpDebugLog(effectiveConfig, "context.no_compression_candidate", {
|
|
533
765
|
contextPercent,
|
|
534
766
|
thresholds,
|
|
535
767
|
nudgeType,
|
|
536
768
|
clearedAnchors,
|
|
537
|
-
state: summarizeDcpState(state),
|
|
769
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
538
770
|
}, ctx)
|
|
539
771
|
}
|
|
540
772
|
}
|
|
541
773
|
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
} else {
|
|
547
|
-
state.consecutiveIgnoredStrongNudges = 0
|
|
548
|
-
}
|
|
774
|
+
// E05 patience advances only after a correlated main-provider response
|
|
775
|
+
// completes. Repeated context transforms merely make the reminder
|
|
776
|
+
// available; they do not consume another model opportunity.
|
|
777
|
+
if (!emergencyPressureReached) state.consecutiveIgnoredStrongNudges = 0
|
|
549
778
|
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
|
|
779
|
+
// Completed actionable routine opportunities also have a bound. Never
|
|
780
|
+
// silently enable automatic summarization when the user disabled it.
|
|
781
|
+
if (routineEscalated && !effectiveConfig.compress.autoCompress.enabled) {
|
|
782
|
+
warnProgress(ctx, "auto-disabled", "DCP compression has not made enough progress. Automatic compression is disabled; use compress or enable compress.autoCompress explicitly.")
|
|
783
|
+
}
|
|
553
784
|
if (!manualEmergencyOnly) {
|
|
785
|
+
const autoCandidate = candidate ?? emergencyCompressionCandidate
|
|
786
|
+
const cacheSafeReminderAvailable = hasCacheSafeNudgeCarrier(prunedMessages)
|
|
554
787
|
const autoDecision = decideAutoCompress(
|
|
555
788
|
state,
|
|
556
789
|
effectiveConfig,
|
|
557
790
|
contextPercent,
|
|
558
791
|
thresholds.maxContextPercent,
|
|
559
|
-
|
|
792
|
+
autoCandidate,
|
|
793
|
+
{
|
|
794
|
+
routinePressure: routineEscalated,
|
|
795
|
+
hardPressure: hardEmergencyReached,
|
|
796
|
+
reminderUnavailable: !cacheSafeReminderAvailable,
|
|
797
|
+
},
|
|
560
798
|
)
|
|
561
|
-
if (contextLimitReached &&
|
|
799
|
+
if (contextLimitReached && autoCandidate === null) {
|
|
562
800
|
writeDcpDebugLog(effectiveConfig, "compress.auto_blocked_no_candidate", {
|
|
563
801
|
autoCompressEnabled: effectiveConfig.compress.autoCompress.enabled,
|
|
564
802
|
decisionReason: autoDecision.reason,
|
|
803
|
+
blocked_reason: blockedReason,
|
|
565
804
|
contextPercent,
|
|
566
805
|
thresholds,
|
|
567
806
|
consecutiveEmergencyPasses: state.consecutiveIgnoredStrongNudges,
|
|
568
807
|
...(emergencySelection?.stats ?? {}),
|
|
569
|
-
state: summarizeDcpState(state),
|
|
808
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
570
809
|
}, ctx)
|
|
571
810
|
}
|
|
572
|
-
if (autoDecision.shouldFire &&
|
|
811
|
+
if (autoDecision.shouldFire && autoCandidate) {
|
|
573
812
|
try {
|
|
574
|
-
const
|
|
575
|
-
|
|
813
|
+
const autoOperationEpoch = state.sessionEpoch
|
|
814
|
+
const largestSafeCandidate = candidate
|
|
815
|
+
? detectCompressionCandidate(prunedMessages, state, effectiveConfig, contextPercent)
|
|
816
|
+
: detectEmergencyCompressionCandidate(prunedMessages, state, effectiveConfig, contextPercent, planningThreshold)
|
|
817
|
+
let preparedProjection: any[] | undefined
|
|
818
|
+
const autoResult = await createBudgetedAutoCompressionBlock({
|
|
819
|
+
candidate: autoCandidate,
|
|
576
820
|
topic: "Auto-compressed slice",
|
|
577
821
|
state,
|
|
578
822
|
config: effectiveConfig,
|
|
579
823
|
messages: prunedMessages,
|
|
580
824
|
modelRegistry: (ctx as any).modelRegistry,
|
|
581
825
|
signal: (ctx as any).signal,
|
|
582
|
-
|
|
826
|
+
cwd: (ctx as any).cwd,
|
|
827
|
+
requiredGainTokens: state.compressionProgress?.remainingTokens ?? planningRecoveryTokens,
|
|
828
|
+
allowPartialGain: true,
|
|
829
|
+
persistState: (preparedState, publication) => persistJournalState(ctx, preparedState, publication),
|
|
830
|
+
prepareProjection: (preparedState) => {
|
|
831
|
+
clearDcpNudgeAnchors(preparedState)
|
|
832
|
+
preparedState.progressRecovery = undefined
|
|
833
|
+
preparedProjection = applyPruning(contextMessages, preparedState, effectiveConfig)
|
|
834
|
+
return preparedProjection
|
|
835
|
+
},
|
|
836
|
+
}, largestSafeCandidate)
|
|
837
|
+
if (autoResult.ownerChangedAfterPublication || state.sessionEpoch !== autoOperationEpoch) {
|
|
838
|
+
// The captured owner was durably committed; do not label it
|
|
839
|
+
// failed or mutate/nudge the replacement session.
|
|
840
|
+
return { messages: contextMessages }
|
|
841
|
+
}
|
|
583
842
|
// Re-apply pruning so the new block takes effect on this
|
|
584
843
|
// same context pass instead of the next one.
|
|
585
|
-
prunedMessages = applyPruning(
|
|
844
|
+
prunedMessages = preparedProjection ?? applyPruning(contextMessages, state, effectiveConfig)
|
|
586
845
|
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
587
|
-
|
|
588
|
-
|
|
846
|
+
warnedProgress.clear()
|
|
847
|
+
state.progressRecovery = undefined
|
|
589
848
|
writeDcpDebugLog(effectiveConfig, "compress.auto", {
|
|
590
849
|
trigger: autoDecision.reason,
|
|
591
850
|
blockId: `b${autoResult.blockId}`,
|
|
@@ -594,25 +853,54 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
594
853
|
summarizerAttempts: autoResult.summarizerAttempts,
|
|
595
854
|
summaryTokens: autoResult.summaryTokens,
|
|
596
855
|
removedTokenEstimate: autoResult.removedTokenEstimate,
|
|
597
|
-
|
|
856
|
+
fullProjectionGain: autoResult.fullProjectionGain,
|
|
857
|
+
pressureRelieved: autoResult.pressureRelieved,
|
|
858
|
+
candidate: autoResult.effectiveCandidate,
|
|
598
859
|
clearedAnchors,
|
|
599
|
-
state: summarizeDcpState(state),
|
|
860
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
600
861
|
}, ctx)
|
|
601
862
|
return finishContext("compress.auto", prunedMessages, {
|
|
602
|
-
candidate,
|
|
863
|
+
candidate: autoResult.effectiveCandidate,
|
|
603
864
|
messageCandidates,
|
|
604
865
|
contextPercent,
|
|
605
866
|
thresholds,
|
|
606
867
|
clearedAnchors,
|
|
607
868
|
})
|
|
608
869
|
} catch (error) {
|
|
870
|
+
if (state.sessionEpoch !== contextEpoch || ctx.signal?.aborted) return { messages: contextMessages }
|
|
871
|
+
const autoBlockedReason = error instanceof AutoCompressionBlockedError
|
|
872
|
+
? error.blockedReason
|
|
873
|
+
: undefined
|
|
609
874
|
writeDcpDebugLog(effectiveConfig, "compress.auto_failed", {
|
|
610
875
|
trigger: autoDecision.reason,
|
|
876
|
+
phase: autoBlockedReason ? "blocked" : "degraded",
|
|
877
|
+
blocked_reason: autoBlockedReason,
|
|
611
878
|
error: error instanceof Error ? error.message : String(error),
|
|
612
|
-
candidate,
|
|
613
|
-
state: summarizeDcpState(state),
|
|
879
|
+
candidate: autoCandidate,
|
|
880
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
614
881
|
}, ctx)
|
|
615
|
-
|
|
882
|
+
if (routineEscalated) warnProgress(ctx, autoBlockedReason ?? "auto-failed",
|
|
883
|
+
`DCP automatic compression did not recover the context: ${autoBlockedReason ?? "preparation failed"}. The existing context was preserved.`)
|
|
884
|
+
if (autoBlockedReason && budget.capacityExceeded) {
|
|
885
|
+
state.progressRecovery = {
|
|
886
|
+
blockedReason: autoBlockedReason,
|
|
887
|
+
projectedBeforeTokens: budget.projectedBeforeTokens,
|
|
888
|
+
inputCapacityTokens: budget.inputCapacityTokens,
|
|
889
|
+
requiredSavingsTokens: budget.requiredSavingsTokens,
|
|
890
|
+
contextWindow: budget.contextWindow,
|
|
891
|
+
createdAt: Date.now(),
|
|
892
|
+
}
|
|
893
|
+
clearDcpNudgeAnchors(state)
|
|
894
|
+
await persistJournalState(ctx, state)
|
|
895
|
+
const abortSupported = typeof (ctx as any).abort === "function"
|
|
896
|
+
if (abortSupported) (ctx as any).abort()
|
|
897
|
+
return finishContext("progress.blocked_handoff", prunedMessages, {
|
|
898
|
+
blocked_reason: autoBlockedReason,
|
|
899
|
+
handoff: abortSupported ? "abort-current-agent-operation" : "abort-unavailable",
|
|
900
|
+
budget,
|
|
901
|
+
})
|
|
902
|
+
}
|
|
903
|
+
// Recoverable failures fall through to normal nudge emission.
|
|
616
904
|
}
|
|
617
905
|
}
|
|
618
906
|
}
|
|
@@ -648,10 +936,20 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
648
936
|
prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
|
|
649
937
|
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
650
938
|
state.consecutiveIgnoredStrongNudges = 0
|
|
939
|
+
state.progressRecovery = undefined
|
|
651
940
|
emergencySelection = analyzeEmergencyCurrentTurn(prunedMessages, state, effectiveConfig)
|
|
652
941
|
messageCandidates = emergencyCurrentTurnMessageCandidates(emergencySelection, effectiveConfig)
|
|
653
|
-
|
|
654
|
-
|
|
942
|
+
emergencyCompressionCandidate = detectEmergencyCompressionCandidate(
|
|
943
|
+
prunedMessages,
|
|
944
|
+
state,
|
|
945
|
+
effectiveConfig,
|
|
946
|
+
contextPercent,
|
|
947
|
+
thresholds.maxContextPercent,
|
|
948
|
+
{ requiredSavingsTokens: budget.requiredSavingsTokens },
|
|
949
|
+
)
|
|
950
|
+
hasCompressionSuggestion =
|
|
951
|
+
candidate !== null || emergencyCompressionCandidate !== null || messageCandidates.length > 0
|
|
952
|
+
await persistJournalState(ctx, state)
|
|
655
953
|
writeDcpDebugLog(effectiveConfig, "prune.emergency_current_turn", {
|
|
656
954
|
trigger: hardEmergencyReached ? "hard-context-percent" : "ignored-emergency-reminders",
|
|
657
955
|
contextPercent,
|
|
@@ -671,15 +969,16 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
671
969
|
eligibleExhausted:
|
|
672
970
|
emergencyPruneResult.prunedToolCallIds.length >= selectionStatsBeforePrune.eligiblePairs,
|
|
673
971
|
...selectionStatsBeforePrune,
|
|
674
|
-
state: summarizeDcpState(state),
|
|
972
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
675
973
|
}, ctx)
|
|
676
974
|
}
|
|
677
975
|
}
|
|
678
976
|
|
|
679
977
|
if (nudgeType && !manualEmergencyOnly && (hasCompressionSuggestion || emergencyPressureReached)) {
|
|
978
|
+
latestProviderOpportunityAvailable = latestProviderOpportunityKind !== undefined
|
|
680
979
|
const nudgeText = appendConcreteNudgeGuidance(
|
|
681
980
|
baseNudgeText(nudgeType),
|
|
682
|
-
candidate,
|
|
981
|
+
candidate ?? emergencyCompressionCandidate,
|
|
683
982
|
messageCandidates,
|
|
684
983
|
state,
|
|
685
984
|
)
|
|
@@ -699,7 +998,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
699
998
|
usage,
|
|
700
999
|
toolCallsSinceLastUser,
|
|
701
1000
|
)
|
|
702
|
-
await
|
|
1001
|
+
await persistJournalState(ctx, state)
|
|
703
1002
|
} else {
|
|
704
1003
|
// Anchor already exists at >= priority; the reminder text is
|
|
705
1004
|
// re-applied below via applyAnchoredNudges on every context
|
|
@@ -717,9 +1016,15 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
717
1016
|
)
|
|
718
1017
|
}
|
|
719
1018
|
} else {
|
|
720
|
-
//
|
|
721
|
-
// synthetic
|
|
722
|
-
|
|
1019
|
+
// Do not rewrite an already-sent user message and do not create an
|
|
1020
|
+
// ephemeral synthetic tail that disappears from the next provider
|
|
1021
|
+
// continuation. The reminder is deferred until a fresh user tail; hard
|
|
1022
|
+
// pressure is handled by the bounded emergency path above.
|
|
1023
|
+
writeDcpDebugLog(effectiveConfig, "nudge.deferred_for_cache", {
|
|
1024
|
+
type: nudgeType,
|
|
1025
|
+
contextPercent,
|
|
1026
|
+
toolCallsSinceLastUser,
|
|
1027
|
+
}, ctx)
|
|
723
1028
|
}
|
|
724
1029
|
state.nudgeCounter = 0
|
|
725
1030
|
state.lastNudgeTurn = state.currentTurn
|
|
@@ -729,7 +1034,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
729
1034
|
|
|
730
1035
|
// Persist patience/window changes even when an existing anchor was only
|
|
731
1036
|
// re-applied (that path intentionally emits telemetry without updating it).
|
|
732
|
-
await
|
|
1037
|
+
await persistJournalState(ctx, state)
|
|
733
1038
|
}
|
|
734
1039
|
|
|
735
1040
|
const anchorsBeforeFinalization = state.nudgeAnchors.length
|
|
@@ -739,10 +1044,19 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
739
1044
|
)
|
|
740
1045
|
}
|
|
741
1046
|
const nudgeApplication = applyAnchoredNudges(prunedMessages, state, (anchor) =>
|
|
742
|
-
appendConcreteNudgeGuidance(
|
|
1047
|
+
appendConcreteNudgeGuidance(
|
|
1048
|
+
baseNudgeText(anchor.type),
|
|
1049
|
+
candidate ?? emergencyCompressionCandidate,
|
|
1050
|
+
messageCandidates,
|
|
1051
|
+
state,
|
|
1052
|
+
),
|
|
743
1053
|
)
|
|
1054
|
+
if (nudgeApplication.rendered && latestProviderOpportunityKind) {
|
|
1055
|
+
latestProviderOpportunityAvailable = true
|
|
1056
|
+
latestProviderReminder = state.nudgeAnchors[0]?.renderedReminder
|
|
1057
|
+
}
|
|
744
1058
|
if (state.nudgeAnchors.length !== anchorsBeforeFinalization || nudgeApplication.stateChanged) {
|
|
745
|
-
await
|
|
1059
|
+
await persistJournalState(ctx, state)
|
|
746
1060
|
}
|
|
747
1061
|
|
|
748
1062
|
return finishContext("complete", prunedMessages, {
|
|
@@ -753,28 +1067,50 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
753
1067
|
})
|
|
754
1068
|
})
|
|
755
1069
|
|
|
756
|
-
// ── 10b.
|
|
1070
|
+
// ── 10b. provider lifecycle evidence ─────────────────────────────────────
|
|
757
1071
|
pi.on("before_provider_request", async (event, ctx) => {
|
|
758
1072
|
const effectiveConfig = configForContext(ctx)
|
|
759
|
-
|
|
760
|
-
|
|
1073
|
+
if (!effectiveConfig.enabled) {
|
|
1074
|
+
providerEvidenceTracker.reset()
|
|
1075
|
+
return undefined
|
|
1076
|
+
}
|
|
761
1077
|
|
|
762
1078
|
const providerEvidence = collectProviderToolResultEvidence(event.payload)
|
|
1079
|
+
const pendingToolIds = new Set<string>()
|
|
763
1080
|
for (const meta of state.messageMetaSnapshot.values()) {
|
|
764
1081
|
if (meta.role !== "toolResult") continue
|
|
765
1082
|
if (!meta.toolCallId || state.prunedToolIds.has(meta.toolCallId)) continue
|
|
766
1083
|
if (state.providerSeenToolIds.has(meta.toolCallId)) continue
|
|
767
1084
|
const record = state.toolCalls.get(meta.toolCallId)
|
|
768
1085
|
if (record && providerPayloadIncludesToolResult(providerEvidence, record)) {
|
|
769
|
-
|
|
1086
|
+
pendingToolIds.add(meta.toolCallId)
|
|
770
1087
|
}
|
|
771
1088
|
}
|
|
772
1089
|
|
|
1090
|
+
const model = (ctx as any)?.model
|
|
1091
|
+
const reminderDelivered = latestProviderOpportunityAvailable &&
|
|
1092
|
+
providerPayloadIncludesReminder(event.payload, latestProviderReminder)
|
|
1093
|
+
const pending = providerEvidenceTracker.begin({
|
|
1094
|
+
sessionEpoch: state.sessionEpoch,
|
|
1095
|
+
provider: typeof model?.provider === "string" ? model.provider : undefined,
|
|
1096
|
+
model: typeof model?.id === "string" ? model.id : undefined,
|
|
1097
|
+
contentRevision: providerPayloadRevision(event.payload),
|
|
1098
|
+
toolIds: pendingToolIds,
|
|
1099
|
+
opportunityAvailable: reminderDelivered,
|
|
1100
|
+
opportunityKind: reminderDelivered ? latestProviderOpportunityKind : undefined,
|
|
1101
|
+
})
|
|
1102
|
+
|
|
773
1103
|
writeDcpDebugLog(effectiveConfig, "provider_payload.message_ids", {
|
|
774
1104
|
injected: false,
|
|
775
1105
|
delivery: "distributed-carriers",
|
|
776
|
-
pendingToolResults:
|
|
777
|
-
|
|
1106
|
+
pendingToolResults: pendingToolIds.size,
|
|
1107
|
+
attempts: pending.attempts,
|
|
1108
|
+
ambiguous: pending.ambiguous,
|
|
1109
|
+
opportunityAvailable: pending.opportunityAvailable,
|
|
1110
|
+
opportunityKind: pending.opportunityKind,
|
|
1111
|
+
provider: pending.provider,
|
|
1112
|
+
model: pending.model,
|
|
1113
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
778
1114
|
}, ctx)
|
|
779
1115
|
// IDs are already attached to deterministic user/tool-result context
|
|
780
1116
|
// carriers. Replacing the payload here would move metadata from the old
|
|
@@ -782,33 +1118,111 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
782
1118
|
return undefined
|
|
783
1119
|
})
|
|
784
1120
|
|
|
785
|
-
//
|
|
786
|
-
//
|
|
1121
|
+
// HTTP acceptance is deliberately diagnostic only. The SDK fires this hook
|
|
1122
|
+
// before consuming the response body, so 2xx cannot prove that the provider
|
|
1123
|
+
// actually completed the assistant stream.
|
|
787
1124
|
pi.on("after_provider_response", async (event, ctx) => {
|
|
788
1125
|
const effectiveConfig = configForContext(ctx)
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
if (effectiveConfig.enabled && accepted) {
|
|
792
|
-
for (const toolCallId of pendingProviderToolIds) {
|
|
793
|
-
if (!state.providerSeenToolIds.has(toolCallId)) {
|
|
794
|
-
state.providerSeenToolIds.add(toolCallId)
|
|
795
|
-
newlySeenToolResults++
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
if (newlySeenToolResults > 0) await saveDcpState(ctx, state)
|
|
799
|
-
}
|
|
800
|
-
writeDcpDebugLog(effectiveConfig, "provider_payload.tool_results_seen", {
|
|
1126
|
+
const pending = providerEvidenceTracker.snapshot()
|
|
1127
|
+
writeDcpDebugLog(effectiveConfig, "provider_payload.http_response", {
|
|
801
1128
|
status: event.status,
|
|
802
|
-
accepted,
|
|
803
|
-
pendingToolResults:
|
|
804
|
-
|
|
805
|
-
|
|
1129
|
+
accepted: event.status >= 200 && event.status < 300,
|
|
1130
|
+
pendingToolResults: pending?.toolIds.size ?? 0,
|
|
1131
|
+
attempts: pending?.attempts ?? 0,
|
|
1132
|
+
ambiguous: pending?.ambiguous ?? false,
|
|
1133
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
806
1134
|
}, ctx)
|
|
807
|
-
pendingProviderToolIds.clear()
|
|
808
1135
|
})
|
|
809
1136
|
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
1137
|
+
// message_end is emitted for the finalized assistant message after the
|
|
1138
|
+
// response stream settles. Without a provider request ID in SDK 0.85.1, only
|
|
1139
|
+
// one unambiguous request shape (or identical retries) is safe to promote.
|
|
1140
|
+
pi.on("message_end", async (event, ctx) => {
|
|
1141
|
+
if (event.message?.role !== "assistant") return
|
|
1142
|
+
const effectiveConfig = configForContext(ctx)
|
|
1143
|
+
if (!effectiveConfig.enabled) {
|
|
1144
|
+
providerEvidenceTracker.reset()
|
|
1145
|
+
return
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const message = event.message as any
|
|
1149
|
+
const completion = providerEvidenceTracker.complete({
|
|
1150
|
+
sessionEpoch: state.sessionEpoch,
|
|
1151
|
+
provider: typeof message.provider === "string" ? message.provider : undefined,
|
|
1152
|
+
model: typeof message.model === "string" ? message.model : undefined,
|
|
1153
|
+
stopReason: typeof message.stopReason === "string" ? message.stopReason : undefined,
|
|
1154
|
+
})
|
|
1155
|
+
latestProviderOpportunityAvailable = false
|
|
1156
|
+
|
|
1157
|
+
if (completion.status !== "promote") {
|
|
1158
|
+
writeDcpDebugLog(effectiveConfig, "provider_payload.tool_results_not_promoted", {
|
|
1159
|
+
reason: completion.reason,
|
|
1160
|
+
attempts: completion.attempts,
|
|
1161
|
+
stopReason: message.stopReason,
|
|
1162
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
1163
|
+
}, ctx)
|
|
1164
|
+
return
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Charge the completed opportunity before tool execution. A failed or
|
|
1168
|
+
// insufficient compress call cannot erase it; only a sufficient durable
|
|
1169
|
+
// commit settles the savings goal and resets patience.
|
|
1170
|
+
const ignoredCompressionOpportunity =
|
|
1171
|
+
completion.opportunityAvailable && message.stopReason !== "deferred"
|
|
1172
|
+
|
|
1173
|
+
const commit = providerEvidenceCommitQueue
|
|
1174
|
+
.catch(() => {
|
|
1175
|
+
// Keep later evidence commits moving after a prior transaction failure.
|
|
1176
|
+
})
|
|
1177
|
+
.then(() => runDcpStateTransaction(state, async () => {
|
|
1178
|
+
if (state.sessionEpoch !== completion.sessionEpoch) {
|
|
1179
|
+
writeDcpDebugLog(effectiveConfig, "provider_payload.tool_results_not_promoted", {
|
|
1180
|
+
reason: "stale-session",
|
|
1181
|
+
attempts: completion.attempts,
|
|
1182
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
1183
|
+
}, ctx)
|
|
1184
|
+
return
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const newlySeen = [...completion.toolIds].filter((toolCallId) => !state.providerSeenToolIds.has(toolCallId))
|
|
1188
|
+
if (newlySeen.length === 0 && !ignoredCompressionOpportunity) return
|
|
1189
|
+
const assertCurrent = captureDcpTransactionGuard(state, effectiveConfig, undefined, ctx)
|
|
1190
|
+
const workingState = cloneDcpTransactionState(state)
|
|
1191
|
+
for (const toolCallId of newlySeen) workingState.providerSeenToolIds.add(toolCallId)
|
|
1192
|
+
if (ignoredCompressionOpportunity) {
|
|
1193
|
+
workingState.consecutiveIgnoredNudges = state.consecutiveIgnoredNudges + 1
|
|
1194
|
+
if (completion.opportunityKind !== "routine") {
|
|
1195
|
+
workingState.consecutiveIgnoredStrongNudges = state.consecutiveIgnoredStrongNudges + 1
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Provider-delivery evidence is intentionally runtime-only. After a
|
|
1200
|
+
// restart absence means unknown, never "seen". Persisting it would add
|
|
1201
|
+
// high-frequency journal churn without changing the durable projection.
|
|
1202
|
+
assertCurrent()
|
|
1203
|
+
|
|
1204
|
+
if (state.sessionEpoch !== completion.sessionEpoch) {
|
|
1205
|
+
writeDcpDebugLog(effectiveConfig, "provider_payload.tool_results_not_promoted", {
|
|
1206
|
+
reason: "stale-session",
|
|
1207
|
+
attempts: completion.attempts,
|
|
1208
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
1209
|
+
}, ctx)
|
|
1210
|
+
return
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
state.providerSeenToolIds = workingState.providerSeenToolIds
|
|
1214
|
+
state.consecutiveIgnoredStrongNudges = workingState.consecutiveIgnoredStrongNudges
|
|
1215
|
+
state.consecutiveIgnoredNudges = workingState.consecutiveIgnoredNudges
|
|
1216
|
+
writeDcpDebugLog(effectiveConfig, "provider_payload.tool_results_seen", {
|
|
1217
|
+
attempts: completion.attempts,
|
|
1218
|
+
newlySeenToolResults: newlySeen.length,
|
|
1219
|
+
ignoredCompressionOpportunity,
|
|
1220
|
+
ignoredOpportunities: state.consecutiveIgnoredStrongNudges,
|
|
1221
|
+
allIgnoredOpportunities: state.consecutiveIgnoredNudges,
|
|
1222
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
1223
|
+
}, ctx)
|
|
1224
|
+
}))
|
|
1225
|
+
providerEvidenceCommitQueue = commit
|
|
1226
|
+
await commit
|
|
813
1227
|
})
|
|
814
1228
|
}
|