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.
Files changed (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -0,0 +1,244 @@
1
+ export type DcpProgressPhase =
2
+ | "normal"
3
+ | "pressure"
4
+ | "awaiting_opportunity"
5
+ | "preparing"
6
+ | "committed"
7
+ | "cooldown"
8
+ | "blocked"
9
+ | "degraded";
10
+
11
+ export type DcpBlockedReason =
12
+ | "live-head-only"
13
+ | "protected-budget-exceeded"
14
+ | "evidence-unknown"
15
+ | "summarizer-unavailable"
16
+ | "non-positive-gain"
17
+ | "missing-source"
18
+ | "budget-exhausted";
19
+
20
+ export interface DcpBlockedReasonInput {
21
+ pressured: boolean;
22
+ candidateAvailable: boolean;
23
+ messageCandidateCount?: number;
24
+ requiredSavingsTokens?: number;
25
+ capacityExceeded?: boolean;
26
+ emergencyStats?: {
27
+ totalPairs: number;
28
+ eligiblePairs: number;
29
+ eligibleRecoverableTokens: number;
30
+ preservedRecentPairs: number;
31
+ preservedUnseenPairs: number;
32
+ preservedProtectedPairs: number;
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Classify an exhausted E05 planning pass without guessing beyond the facts
38
+ * available from the emergency eligibility analysis. This is deliberately
39
+ * conservative: when a safe body candidate still exists, the controller is
40
+ * waiting for an opportunity rather than terminally blocked.
41
+ */
42
+ export function inferDcpBlockedReason(input: DcpBlockedReasonInput): DcpBlockedReason | undefined {
43
+ if (!input.pressured || input.candidateAvailable || (input.messageCandidateCount ?? 0) > 0) return undefined;
44
+
45
+ const stats = input.emergencyStats;
46
+ if (!stats || stats.totalPairs === 0) return "live-head-only";
47
+
48
+ const requiredSavings = Math.max(0, Math.floor(input.requiredSavingsTokens ?? 0));
49
+ if (
50
+ stats.eligiblePairs > 0 &&
51
+ requiredSavings > 0 &&
52
+ stats.eligibleRecoverableTokens < requiredSavings
53
+ ) {
54
+ return "budget-exhausted";
55
+ }
56
+
57
+ if (stats.preservedUnseenPairs > 0 && stats.eligiblePairs === 0) return "evidence-unknown";
58
+ if (
59
+ stats.preservedProtectedPairs > 0 &&
60
+ (stats.eligiblePairs === 0 || input.capacityExceeded === true)
61
+ ) {
62
+ return "protected-budget-exceeded";
63
+ }
64
+ if (stats.preservedRecentPairs >= stats.totalPairs && stats.eligiblePairs === 0) return "live-head-only";
65
+ return "budget-exhausted";
66
+ }
67
+
68
+ export interface DcpProgressInput {
69
+ enabled: boolean;
70
+ autoEnabled: boolean;
71
+ pressure: boolean;
72
+ candidateAvailable: boolean;
73
+ ignoredOpportunities: number;
74
+ patience: number;
75
+ preparing?: boolean;
76
+ justCommitted?: boolean;
77
+ cooldown?: boolean;
78
+ blockedReason?: DcpBlockedReason;
79
+ degradedReason?: string;
80
+ }
81
+
82
+ export interface DcpProgressDecision {
83
+ phase: DcpProgressPhase;
84
+ shouldPrepare: boolean;
85
+ reason: string;
86
+ blockedReason?: DcpBlockedReason;
87
+ }
88
+
89
+ /**
90
+ * Pure E05 progress policy. Runtime hooks own opportunity accounting; this
91
+ * function only maps the current pressure/candidate/budget state to the next
92
+ * observable phase. In particular, context callback count is not an input.
93
+ */
94
+ export function decideDcpProgress(input: DcpProgressInput): DcpProgressDecision {
95
+ if (!input.enabled) return { phase: "blocked", shouldPrepare: false, reason: "disabled" };
96
+ if (input.justCommitted) return { phase: "committed", shouldPrepare: false, reason: "committed" };
97
+ if (input.cooldown) return { phase: "cooldown", shouldPrepare: false, reason: "cooldown" };
98
+ if (input.degradedReason) return { phase: "degraded", shouldPrepare: false, reason: input.degradedReason };
99
+ if (input.blockedReason) {
100
+ return {
101
+ phase: "blocked",
102
+ shouldPrepare: false,
103
+ reason: input.blockedReason,
104
+ blockedReason: input.blockedReason,
105
+ };
106
+ }
107
+ if (!input.pressure) return { phase: "normal", shouldPrepare: false, reason: "below-pressure" };
108
+ if (input.preparing) return { phase: "preparing", shouldPrepare: false, reason: "already-preparing" };
109
+ if (!input.autoEnabled) return { phase: "pressure", shouldPrepare: false, reason: "auto-disabled" };
110
+ if (!input.candidateAvailable) {
111
+ return { phase: "awaiting_opportunity", shouldPrepare: false, reason: "no-candidate" };
112
+ }
113
+
114
+ const ignored = Math.max(0, Math.floor(input.ignoredOpportunities));
115
+ const patience = Math.max(0, Math.floor(input.patience));
116
+ if (ignored <= patience) {
117
+ return { phase: "awaiting_opportunity", shouldPrepare: false, reason: "below-patience" };
118
+ }
119
+ return { phase: "preparing", shouldPrepare: true, reason: "ignored-opportunities" };
120
+ }
121
+
122
+ export type DcpBudgetProjectionOrigin = "repo-fallback" | "provider-native" | "repo-over-provider";
123
+
124
+ export interface DcpBudgetInput {
125
+ providerUsageTokens?: number | null;
126
+ repoProjectedTokens: number;
127
+ contextWindow: number;
128
+ reservedOutputTokens?: number;
129
+ reservedToolTokens?: number;
130
+ maxContextPercent: number;
131
+ hardContextPercent: number;
132
+ targetContextPercent?: number;
133
+ summaryBufferEnabled?: boolean;
134
+ activeSummaryTokens?: number;
135
+ summaryBufferMaxBonusRatio?: number;
136
+ estimatorMarginTokens?: number;
137
+ }
138
+
139
+ export interface DcpBudgetPlan {
140
+ projectedBeforeTokens: number;
141
+ projectionOrigin: DcpBudgetProjectionOrigin;
142
+ contextWindow: number;
143
+ reservedOutputTokens: number;
144
+ reservedToolTokens: number;
145
+ inputCapacityTokens: number;
146
+ summaryBufferTokensApplied: number;
147
+ softHeadroomTokens: number;
148
+ hardHeadroomTokens: number;
149
+ targetHeadroomTokens: number;
150
+ requiredSavingsTokens: number;
151
+ pressured: boolean;
152
+ hardPressure: boolean;
153
+ capacityExceeded: boolean;
154
+ }
155
+
156
+ function finiteNonNegative(value: unknown): number | undefined {
157
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
158
+ }
159
+
160
+ function clampFraction(value: number): number {
161
+ return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0));
162
+ }
163
+
164
+ /**
165
+ * Pure E05 budget planner. Provider-native usage is useful evidence but may be
166
+ * stale between completed requests, so the fresh repo projection is always a
167
+ * lower bound. Capacity is distinct from policy thresholds and explicitly
168
+ * reserves output/tool budget. `summaryBuffer` may relax the soft policy only
169
+ * inside that hard capacity.
170
+ */
171
+ export function planDcpBudget(input: DcpBudgetInput): DcpBudgetPlan {
172
+ const contextWindow = Math.max(1, Math.floor(finiteNonNegative(input.contextWindow) ?? 1));
173
+ const repoProjectedTokens = Math.floor(finiteNonNegative(input.repoProjectedTokens) ?? 0);
174
+ const providerUsageTokens = finiteNonNegative(input.providerUsageTokens);
175
+ const projectedBeforeTokens = Math.max(repoProjectedTokens, Math.floor(providerUsageTokens ?? 0));
176
+ const projectionOrigin: DcpBudgetProjectionOrigin = providerUsageTokens === undefined
177
+ ? "repo-fallback"
178
+ : repoProjectedTokens > providerUsageTokens
179
+ ? "repo-over-provider"
180
+ : "provider-native";
181
+
182
+ const reservedOutputTokens = Math.min(
183
+ contextWindow,
184
+ Math.floor(finiteNonNegative(input.reservedOutputTokens) ?? 0),
185
+ );
186
+ const reservedToolTokens = Math.min(
187
+ Math.max(0, contextWindow - reservedOutputTokens),
188
+ Math.floor(finiteNonNegative(input.reservedToolTokens) ?? 0),
189
+ );
190
+ const inputCapacityTokens = Math.max(0, contextWindow - reservedOutputTokens - reservedToolTokens);
191
+
192
+ const baseSoftHeadroom = Math.min(
193
+ inputCapacityTokens,
194
+ Math.floor(clampFraction(input.maxContextPercent) * contextWindow),
195
+ );
196
+ const summaryBufferCap = Math.floor(
197
+ clampFraction(input.summaryBufferMaxBonusRatio ?? 0.05) * contextWindow,
198
+ );
199
+ const requestedSummaryBuffer = input.summaryBufferEnabled
200
+ ? Math.floor(finiteNonNegative(input.activeSummaryTokens) ?? 0)
201
+ : 0;
202
+ const summaryBufferTokensApplied = Math.min(
203
+ requestedSummaryBuffer,
204
+ summaryBufferCap,
205
+ Math.max(0, inputCapacityTokens - baseSoftHeadroom),
206
+ );
207
+ const softHeadroomTokens = baseSoftHeadroom + summaryBufferTokensApplied;
208
+ const hardHeadroomTokens = Math.min(
209
+ inputCapacityTokens,
210
+ Math.floor(clampFraction(input.hardContextPercent) * contextWindow),
211
+ );
212
+ const capacityExceeded = projectedBeforeTokens > inputCapacityTokens;
213
+ const hardPressure = capacityExceeded || projectedBeforeTokens > hardHeadroomTokens;
214
+ const pressured = hardPressure || projectedBeforeTokens > softHeadroomTokens;
215
+
216
+ const configuredTarget = clampFraction(input.targetContextPercent ?? input.maxContextPercent);
217
+ const recoveryHeadroom = Math.min(
218
+ inputCapacityTokens,
219
+ softHeadroomTokens,
220
+ Math.floor(configuredTarget * contextWindow),
221
+ );
222
+ const targetHeadroomTokens = hardPressure ? recoveryHeadroom : softHeadroomTokens;
223
+ const estimatorMarginTokens = Math.floor(finiteNonNegative(input.estimatorMarginTokens) ?? 0);
224
+ const requiredSavingsTokens = pressured
225
+ ? Math.max(0, projectedBeforeTokens - targetHeadroomTokens + estimatorMarginTokens)
226
+ : 0;
227
+
228
+ return {
229
+ projectedBeforeTokens,
230
+ projectionOrigin,
231
+ contextWindow,
232
+ reservedOutputTokens,
233
+ reservedToolTokens,
234
+ inputCapacityTokens,
235
+ summaryBufferTokensApplied,
236
+ softHeadroomTokens,
237
+ hardHeadroomTokens,
238
+ targetHeadroomTokens,
239
+ requiredSavingsTokens,
240
+ pressured,
241
+ hardPressure,
242
+ capacityExceeded,
243
+ };
244
+ }
@@ -52,6 +52,9 @@ Passing check/test/lint/tsc logs are summary-only after you know the result. Pre
52
52
  DCP REMINDERS
53
53
  If a \`<dcp-system-reminder>\` is present in context, treat it as a signal to evaluate compression. Critical or high-context reminders should be handled promptly. Routine reminders should lead to compression only when a safe, closed, useful-to-summarize range exists; otherwise continue the next atomic step and re-check later.
54
54
 
55
+ COMMITTED IS NOT NECESSARILY RECOVERED
56
+ The tool rejects non-positive full-projection gain, including wrappers and preserved content. An unsuccessful call does not satisfy the reminder. Inspect \`netGain\`, \`pressureRelieved\`, and \`remainingRecoveryTokens\` in its result: a partial positive commit may still leave context pressure. Use freshly visible IDs for subsequent work; do not retry an unchanged rejected summary or discard required facts just to meet a token goal.
57
+
55
58
  THE SUMMARY
56
59
  Your summary must be COMPLETE FOR CONTINUATION, not a transcript rewrite. Preserve only information that will plausibly matter later: user intent, accepted constraints, decisions, files/symbols changed or inspected, exact errors that are still actionable, verification status, and next steps.
57
60
 
@@ -84,7 +87,9 @@ PROTECTED PROMPT CONTENT
84
87
  If selected user text contains \`<protect>...</protect>\` content, preserve that protected content verbatim in your summary. The tool may append protected text automatically, but you should still account for it semantically.
85
88
 
86
89
  COMPRESSED BLOCK PLACEHOLDERS
87
- When the selected range includes previously compressed blocks, use this exact placeholder format when referencing one:
90
+ When rolling up compressed blocks, summarize their continuation-relevant meaning instead of copying the full old summaries. Every block carries a protected-fragment ledger, and those fragments are preserved separately. A block missing that ledger is invalid and must not be rolled up.
91
+
92
+ Only when the full previous summary is genuinely needed verbatim, use this exact placeholder format:
88
93
 
89
94
  - \`(bN)\`
90
95
 
@@ -96,11 +101,11 @@ Compressed block IDs always use the \`bN\` form (never \`mNNN\`) and are represe
96
101
 
97
102
  Rules:
98
103
 
99
- - Include every required block placeholder exactly once when you intentionally roll up older compressed blocks. If you omit one or duplicate one, the tool will try to recover by preserving the missing block summaries automatically, but do not rely on that recovery path.
104
+ - Placeholders are optional, not required for every covered modern block. Each explicit placeholder expands the full old summary and may eliminate the intended savings. Include an intentionally preserved block at most once.
100
105
  - Do not invent placeholders for blocks outside the selected range.
101
106
  - Treat \`(bN)\` placeholders as RESERVED TOKENS. Do not emit \`(bN)\` text anywhere except intentional placeholders.
102
107
  - If you need to mention a block in prose, use plain text like \`compressed bN\` (not as a placeholder).
103
- - Preflight check before finalizing: the set of \`(bN)\` placeholders in your summary should match the required set, with no duplicates.
108
+ - Preflight check before finalizing: each placeholder must refer to a known selected block whose full text you deliberately need to preserve. Otherwise write a concise factual summary in your own words.
104
109
 
105
110
  These placeholders are semantic references. They will be replaced with the full stored compressed block content when the tool processes your output.
106
111
 
@@ -125,10 +130,8 @@ Treat these reference lines as boundary metadata only, not as tool result conten
125
130
 
126
131
  Rules:
127
132
 
128
- - Pick \`startId\` and \`endId\` directly from injected IDs in context.
129
- - IDs must exist in the current conversation context.
130
- - \`startId\` must appear before \`endId\`.
131
- - Do not invent IDs. Use only IDs that are present in context.
133
+ - Use only injected IDs in current context; \`startId\` must precede \`endId\`. Do not invent IDs.
134
+ - For \`ranges\`, never split a tool group: include the calling assistant and all its tool results, including parallel calls.
132
135
  - If \`compress\` fails with \`Unknown message ID\`, treat those IDs as stale; retry at most once using only the error's listed current raw IDs or eligible \`bN\` blocks, or skip if no closed range is safe.
133
136
 
134
137
  BATCHING
@@ -84,3 +84,192 @@ export function providerPayloadIncludesToolResult(
84
84
  return typeof record.outputText === "string" &&
85
85
  evidence.anonymousSignatures.has(signature(record.toolName, record.outputText));
86
86
  }
87
+
88
+ export function providerPayloadRevision(payload: unknown): string | undefined {
89
+ try {
90
+ const serialized = JSON.stringify(payload);
91
+ if (serialized === undefined) return undefined;
92
+ return createHash("sha256").update(serialized).digest("hex");
93
+ } catch {
94
+ return undefined;
95
+ }
96
+ }
97
+
98
+ /** Verify delivery of the exact trusted reminder, not just a context callback. */
99
+ export function providerPayloadIncludesReminder(payload: unknown, reminder: string | undefined): boolean {
100
+ if (!reminder) return false;
101
+ try {
102
+ const serialized = JSON.stringify(payload);
103
+ return typeof serialized === "string" && serialized.includes(JSON.stringify(reminder).slice(1, -1));
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ export interface ProviderEvidenceAttempt {
110
+ sessionEpoch: number;
111
+ provider?: string;
112
+ model?: string;
113
+ contentRevision?: string;
114
+ statePath?: string;
115
+ sessionId?: string;
116
+ toolIds: ReadonlySet<string>;
117
+ opportunityAvailable?: boolean;
118
+ opportunityKind?: "routine" | "emergency";
119
+ }
120
+
121
+ export interface ProviderEvidenceFinalMessage {
122
+ sessionEpoch: number;
123
+ provider?: string;
124
+ model?: string;
125
+ stopReason?: string;
126
+ }
127
+
128
+ export interface ProviderEvidencePendingSnapshot {
129
+ firstAttemptId: number;
130
+ lastAttemptId: number;
131
+ attempts: number;
132
+ sessionEpoch: number;
133
+ provider?: string;
134
+ model?: string;
135
+ contentRevision?: string;
136
+ statePath?: string;
137
+ sessionId?: string;
138
+ toolIds: Set<string>;
139
+ opportunityAvailable: boolean;
140
+ opportunityKind?: "routine" | "emergency";
141
+ ambiguous: boolean;
142
+ }
143
+
144
+ export type ProviderEvidenceCompletion =
145
+ | {
146
+ status: "promote";
147
+ firstAttemptId: number;
148
+ lastAttemptId: number;
149
+ attempts: number;
150
+ sessionEpoch: number;
151
+ statePath?: string;
152
+ sessionId?: string;
153
+ toolIds: Set<string>;
154
+ opportunityAvailable: boolean;
155
+ opportunityKind?: "routine" | "emergency";
156
+ }
157
+ | {
158
+ status: "refused";
159
+ reason:
160
+ | "no-pending"
161
+ | "terminal-failure"
162
+ | "ambiguous"
163
+ | "missing-identity"
164
+ | "stale-session"
165
+ | "identity-mismatch";
166
+ attempts: number;
167
+ };
168
+
169
+ function sameStringSet(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
170
+ if (left.size !== right.size) return false;
171
+ for (const value of left) {
172
+ if (!right.has(value)) return false;
173
+ }
174
+ return true;
175
+ }
176
+
177
+ function attemptHasIdentity(attempt: ProviderEvidenceAttempt): boolean {
178
+ return Boolean(attempt.provider && attempt.model && attempt.contentRevision);
179
+ }
180
+
181
+ /**
182
+ * Correlates provider-input evidence with one finalized assistant message without
183
+ * inventing a request ID the extension API does not expose. Repeated identical
184
+ * attempts are treated as retries; any changed model/content/owner while an
185
+ * attempt is pending makes the evidence ambiguous and therefore non-promotable.
186
+ */
187
+ export class ProviderEvidenceTracker {
188
+ private pending: ProviderEvidencePendingSnapshot | undefined;
189
+ private nextAttemptId = 1;
190
+
191
+ begin(attempt: ProviderEvidenceAttempt): ProviderEvidencePendingSnapshot {
192
+ const attemptId = this.nextAttemptId++;
193
+ const toolIds = new Set(attempt.toolIds);
194
+ if (!this.pending) {
195
+ this.pending = {
196
+ firstAttemptId: attemptId,
197
+ lastAttemptId: attemptId,
198
+ attempts: 1,
199
+ sessionEpoch: attempt.sessionEpoch,
200
+ provider: attempt.provider,
201
+ model: attempt.model,
202
+ contentRevision: attempt.contentRevision,
203
+ statePath: attempt.statePath,
204
+ sessionId: attempt.sessionId,
205
+ toolIds,
206
+ opportunityAvailable: Boolean(attempt.opportunityAvailable),
207
+ opportunityKind: attempt.opportunityKind,
208
+ ambiguous: !attemptHasIdentity(attempt),
209
+ };
210
+ return this.snapshot()!;
211
+ }
212
+
213
+ const pending = this.pending;
214
+ const sameAttemptShape =
215
+ pending.sessionEpoch === attempt.sessionEpoch &&
216
+ pending.provider === attempt.provider &&
217
+ pending.model === attempt.model &&
218
+ pending.contentRevision === attempt.contentRevision &&
219
+ pending.statePath === attempt.statePath &&
220
+ pending.sessionId === attempt.sessionId &&
221
+ pending.opportunityAvailable === Boolean(attempt.opportunityAvailable) &&
222
+ pending.opportunityKind === attempt.opportunityKind &&
223
+ sameStringSet(pending.toolIds, toolIds);
224
+
225
+ pending.lastAttemptId = attemptId;
226
+ pending.attempts++;
227
+ if (!sameAttemptShape || !attemptHasIdentity(attempt)) pending.ambiguous = true;
228
+ return this.snapshot()!;
229
+ }
230
+
231
+ complete(finalMessage: ProviderEvidenceFinalMessage): ProviderEvidenceCompletion {
232
+ const pending = this.pending;
233
+ this.pending = undefined;
234
+ if (!pending) return { status: "refused", reason: "no-pending", attempts: 0 };
235
+
236
+ const successfulStopReasons = new Set(["stop", "length", "toolUse", "deferred"]);
237
+ if (!finalMessage.stopReason || !successfulStopReasons.has(finalMessage.stopReason)) {
238
+ return { status: "refused", reason: "terminal-failure", attempts: pending.attempts };
239
+ }
240
+ if (pending.ambiguous) {
241
+ return { status: "refused", reason: "ambiguous", attempts: pending.attempts };
242
+ }
243
+ if (!pending.provider || !pending.model || !pending.contentRevision || !finalMessage.provider || !finalMessage.model) {
244
+ return { status: "refused", reason: "missing-identity", attempts: pending.attempts };
245
+ }
246
+ if (pending.sessionEpoch !== finalMessage.sessionEpoch) {
247
+ return { status: "refused", reason: "stale-session", attempts: pending.attempts };
248
+ }
249
+ if (pending.provider !== finalMessage.provider || pending.model !== finalMessage.model) {
250
+ return { status: "refused", reason: "identity-mismatch", attempts: pending.attempts };
251
+ }
252
+
253
+ return {
254
+ status: "promote",
255
+ firstAttemptId: pending.firstAttemptId,
256
+ lastAttemptId: pending.lastAttemptId,
257
+ attempts: pending.attempts,
258
+ sessionEpoch: pending.sessionEpoch,
259
+ statePath: pending.statePath,
260
+ sessionId: pending.sessionId,
261
+ toolIds: new Set(pending.toolIds),
262
+ opportunityAvailable: pending.opportunityAvailable,
263
+ opportunityKind: pending.opportunityKind,
264
+ };
265
+ }
266
+
267
+ snapshot(): ProviderEvidencePendingSnapshot | undefined {
268
+ if (!this.pending) return undefined;
269
+ return { ...this.pending, toolIds: new Set(this.pending.toolIds) };
270
+ }
271
+
272
+ reset(): void {
273
+ this.pending = undefined;
274
+ }
275
+ }