pi-ui-extend 0.1.35 → 0.1.37
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/dist/app/app.d.ts +8 -0
- package/dist/app/app.js +48 -5
- package/dist/app/commands/command-controller.js +1 -0
- package/dist/app/commands/command-host.d.ts +1 -0
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +32 -0
- package/dist/app/commands/command-navigation-actions.js +3 -0
- package/dist/app/commands/command-registry.d.ts +1 -0
- package/dist/app/commands/command-registry.js +8 -0
- package/dist/app/commands/command-session-actions.d.ts +2 -0
- package/dist/app/commands/command-session-actions.js +81 -1
- package/dist/app/extensions/extension-actions-controller.d.ts +5 -1
- package/dist/app/extensions/extension-actions-controller.js +35 -2
- package/dist/app/input/input-controller.d.ts +2 -0
- package/dist/app/input/input-controller.js +50 -2
- package/dist/app/input/terminal-edit-shortcuts.d.ts +2 -0
- package/dist/app/input/terminal-edit-shortcuts.js +49 -0
- package/dist/app/input/voice-controller.js +1 -1
- package/dist/app/popup/popup-action-controller.d.ts +2 -3
- package/dist/app/popup/popup-action-controller.js +2 -5
- package/dist/app/rendering/message-content.js +4 -3
- package/dist/app/rendering/render-controller.js +21 -38
- package/dist/app/rendering/status-line-renderer.d.ts +1 -0
- package/dist/app/rendering/status-line-renderer.js +14 -2
- package/dist/app/runtime.js +12 -2
- package/dist/app/screen/mouse-controller.js +2 -0
- package/dist/app/session/session-event-controller.d.ts +7 -0
- package/dist/app/session/session-event-controller.js +10 -13
- package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
- package/dist/app/session/session-lifecycle-controller.js +7 -0
- package/dist/app/session/tabs-controller.d.ts +1 -0
- package/dist/app/session/tabs-controller.js +1 -0
- package/dist/app/terminal/terminal-controller.js +1 -0
- package/dist/app/terminal/terminal-output-buffer.d.ts +8 -6
- package/dist/app/terminal/terminal-output-buffer.js +24 -16
- package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
- package/dist/app/workspace/workspace-actions-controller.js +1 -0
- package/dist/bundled-extensions/terminal-bell/index.js +118 -33
- package/dist/markdown-format.d.ts +1 -0
- package/dist/markdown-format.js +30 -16
- package/dist/schemas/pi-tools-suite-schema.d.ts +5 -0
- package/dist/schemas/pi-tools-suite-schema.js +5 -0
- package/dist/tool-renderers/apply-patch.js +6 -1
- package/dist/tool-renderers/patch-normalize.d.ts +24 -0
- package/dist/tool-renderers/patch-normalize.js +163 -0
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/package.json +5 -5
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +15 -2
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +36 -19
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +5 -2
- package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +8 -3
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +63 -28
- package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +9 -4
- package/external/pi-tools-suite/src/comment-checker/config.ts +98 -0
- package/external/pi-tools-suite/src/comment-checker/detect.ts +215 -0
- package/external/pi-tools-suite/src/comment-checker/index.ts +294 -0
- package/external/pi-tools-suite/src/dcp/commands.ts +29 -15
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +111 -60
- package/external/pi-tools-suite/src/dcp/config.ts +10 -6
- package/external/pi-tools-suite/src/dcp/debug-log.ts +235 -0
- package/external/pi-tools-suite/src/dcp/index.ts +204 -27
- package/external/pi-tools-suite/src/dcp/prompts.ts +25 -28
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +19 -1
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +36 -58
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +18 -0
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +4 -2
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +31 -2
- package/external/pi-tools-suite/src/dcp/state.ts +62 -4
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +18 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +11 -3
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
- package/external/pi-tools-suite/src/todo/index.ts +24 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +3 -3
- package/external/pi-tools-suite/src/usage/index.ts +18 -4
- package/package.json +4 -4
- package/schemas/pi-tools-suite.json +24 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises"
|
|
2
|
+
import * as os from "node:os"
|
|
3
|
+
import * as path from "node:path"
|
|
4
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
5
|
+
import type { DcpConfig } from "./config.js"
|
|
6
|
+
import type { DcpState } from "./state.js"
|
|
7
|
+
|
|
8
|
+
const TRUE_ENV_RE = /^(1|true|yes|on)$/i
|
|
9
|
+
const FALSE_ENV_RE = /^(0|false|no|off)$/i
|
|
10
|
+
const MAX_IDS = 16
|
|
11
|
+
const DEFAULT_DEBUG_LOG_MAX_BYTES = 5 * 1024 * 1024 // 5 MB
|
|
12
|
+
const DEFAULT_DEBUG_LOG_MAX_BACKUPS = 3
|
|
13
|
+
const MIN_DEBUG_LOG_MAX_BACKUPS = 1
|
|
14
|
+
|
|
15
|
+
function truthyEnv(value: string | undefined): boolean | undefined {
|
|
16
|
+
if (value === undefined) return undefined
|
|
17
|
+
const trimmed = value.trim()
|
|
18
|
+
if (TRUE_ENV_RE.test(trimmed)) return true
|
|
19
|
+
if (FALSE_ENV_RE.test(trimmed)) return false
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function dcpDebugEnabled(config: DcpConfig): boolean {
|
|
24
|
+
return truthyEnv(process.env.PI_DCP_DEBUG)
|
|
25
|
+
?? truthyEnv(process.env.PI_TOOLS_SUITE_DCP_DEBUG)
|
|
26
|
+
?? config.debug
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultLogPath(): string {
|
|
30
|
+
const agentDir = process.env.PI_AGENT_DIR || path.join(os.homedir(), ".pi", "agent")
|
|
31
|
+
return path.join(agentDir, "dcp-debug.jsonl")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dcpDebugLogPath(): string {
|
|
35
|
+
const explicit = process.env.PI_DCP_DEBUG_LOG?.trim()
|
|
36
|
+
return explicit || defaultLogPath()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function positiveIntEnv(value: string | undefined): number | undefined {
|
|
40
|
+
if (value === undefined) return undefined
|
|
41
|
+
const parsed = Number.parseInt(value.trim(), 10)
|
|
42
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Maximum size the active debug log is allowed to reach before it is rotated. */
|
|
46
|
+
export function dcpDebugLogMaxBytes(config: DcpConfig): number {
|
|
47
|
+
return positiveIntEnv(process.env.PI_DCP_DEBUG_MAX_BYTES)
|
|
48
|
+
?? config.debugLog?.maxBytes
|
|
49
|
+
?? DEFAULT_DEBUG_LOG_MAX_BYTES
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Number of rotated backups to keep (e.g. `.1`, `.2`, `.3`). */
|
|
53
|
+
export function dcpDebugLogMaxBackups(config: DcpConfig): number {
|
|
54
|
+
const value = positiveIntEnv(process.env.PI_DCP_DEBUG_MAX_BACKUPS)
|
|
55
|
+
?? config.debugLog?.maxBackups
|
|
56
|
+
?? DEFAULT_DEBUG_LOG_MAX_BACKUPS
|
|
57
|
+
return Math.max(MIN_DEBUG_LOG_MAX_BACKUPS, Math.floor(value))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compactIds(ids: string[]): { count: number; head: string[]; tail: string[] } {
|
|
61
|
+
return {
|
|
62
|
+
count: ids.length,
|
|
63
|
+
head: ids.slice(0, MAX_IDS),
|
|
64
|
+
tail: ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : [],
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function safeError(error: unknown): string {
|
|
69
|
+
return error instanceof Error ? error.message : String(error)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sessionInfo(ctx: ExtensionContext | undefined): Record<string, unknown> {
|
|
73
|
+
if (!ctx) return {}
|
|
74
|
+
const info: Record<string, unknown> = {}
|
|
75
|
+
try {
|
|
76
|
+
const header = (ctx as any).sessionManager?.getHeader?.()
|
|
77
|
+
if (header?.id) info.sessionId = header.id
|
|
78
|
+
if (header?.cwd) info.cwd = header.cwd
|
|
79
|
+
} catch (error) {
|
|
80
|
+
info.sessionInfoError = safeError(error)
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const name = (ctx as any).sessionManager?.getSessionName?.()
|
|
84
|
+
if (name) info.sessionName = name
|
|
85
|
+
} catch {
|
|
86
|
+
// Optional diagnostic only.
|
|
87
|
+
}
|
|
88
|
+
return info
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function summarizeDcpState(state: DcpState): Record<string, unknown> {
|
|
92
|
+
const rawIds = [...new Set([
|
|
93
|
+
...state.messageIdSnapshot.keys(),
|
|
94
|
+
...state.messageMetaSnapshot.keys(),
|
|
95
|
+
])]
|
|
96
|
+
const activeBlocks = state.compressionBlocks
|
|
97
|
+
.filter((block) => block.active)
|
|
98
|
+
.sort((a, b) => a.id - b.id)
|
|
99
|
+
.map((block) => ({
|
|
100
|
+
id: `b${block.id}`,
|
|
101
|
+
topic: block.topic,
|
|
102
|
+
mode: block.mode,
|
|
103
|
+
startMessageId: block.startMessageId,
|
|
104
|
+
endMessageId: block.endMessageId,
|
|
105
|
+
anchorMessageId: block.anchorMessageId,
|
|
106
|
+
coveredBlockIds: block.coveredBlockIds ?? [],
|
|
107
|
+
summaryTokens: block.summaryTokenEstimate,
|
|
108
|
+
}))
|
|
109
|
+
const inactiveBlocks = state.compressionBlocks
|
|
110
|
+
.filter((block) => !block.active)
|
|
111
|
+
.sort((a, b) => a.id - b.id)
|
|
112
|
+
.slice(-MAX_IDS)
|
|
113
|
+
.map((block) => ({
|
|
114
|
+
id: `b${block.id}`,
|
|
115
|
+
topic: block.topic,
|
|
116
|
+
reason: block.deactivatedReason,
|
|
117
|
+
coveredBlockIds: block.coveredBlockIds ?? [],
|
|
118
|
+
}))
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
rawIds: compactIds(rawIds),
|
|
122
|
+
activeBlocks,
|
|
123
|
+
blockCounts: {
|
|
124
|
+
active: activeBlocks.length,
|
|
125
|
+
inactive: state.compressionBlocks.length - activeBlocks.length,
|
|
126
|
+
total: state.compressionBlocks.length,
|
|
127
|
+
nextBlockId: state.nextBlockId,
|
|
128
|
+
},
|
|
129
|
+
inactiveBlocksTail: inactiveBlocks,
|
|
130
|
+
prunedTools: state.prunedToolIds.size,
|
|
131
|
+
nudgeAnchors: state.nudgeAnchors.map((anchor) => ({
|
|
132
|
+
id: anchor.id,
|
|
133
|
+
type: anchor.type,
|
|
134
|
+
anchorStableId: anchor.anchorStableId,
|
|
135
|
+
anchorTimestamp: anchor.anchorTimestamp,
|
|
136
|
+
})),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Serializes all debug-log writes so rotation and appends never race.
|
|
141
|
+
let logWriteChain: Promise<void> = Promise.resolve()
|
|
142
|
+
const ensuredLogDirs = new Set<string>()
|
|
143
|
+
|
|
144
|
+
async function ensureLogDir(logPath: string): Promise<void> {
|
|
145
|
+
const dir = path.dirname(logPath)
|
|
146
|
+
if (ensuredLogDirs.has(dir)) return
|
|
147
|
+
await fs.mkdir(dir, { recursive: true })
|
|
148
|
+
ensuredLogDirs.add(dir)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* When the active log has reached `maxBytes`, rotate numbered backups:
|
|
153
|
+
* drop `.N`, shift `.(N-1)`→`.N`, …, `.1`→`.2`, and rename the active file to
|
|
154
|
+
* `.1` so the next append starts a fresh file. Best-effort: fs errors are
|
|
155
|
+
* swallowed because debug logging must never affect the session.
|
|
156
|
+
*/
|
|
157
|
+
async function rotateDebugLogIfNeeded(
|
|
158
|
+
logPath: string,
|
|
159
|
+
maxBytes: number,
|
|
160
|
+
maxBackups: number,
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
let stat: Awaited<ReturnType<typeof fs.stat>>
|
|
163
|
+
try {
|
|
164
|
+
stat = await fs.stat(logPath)
|
|
165
|
+
} catch {
|
|
166
|
+
return // active file does not exist yet; nothing to rotate
|
|
167
|
+
}
|
|
168
|
+
if (stat.size < maxBytes) return
|
|
169
|
+
|
|
170
|
+
const dir = path.dirname(logPath)
|
|
171
|
+
const base = path.basename(logPath)
|
|
172
|
+
|
|
173
|
+
// Drop the oldest backup (`base.N`) so the chain can shift up by one.
|
|
174
|
+
await fs.rm(path.join(dir, `${base}.${maxBackups}`), { force: true })
|
|
175
|
+
// Shift existing backups `base.i` → `base.(i+1)` from highest to lowest.
|
|
176
|
+
for (let i = maxBackups - 1; i >= 1; i--) {
|
|
177
|
+
await fs
|
|
178
|
+
.rename(path.join(dir, `${base}.${i}`), path.join(dir, `${base}.${i + 1}`))
|
|
179
|
+
.catch(() => {
|
|
180
|
+
// A missing intermediate backup is expected; ignore.
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
// Rotate the active file into `base.1`.
|
|
184
|
+
await fs.rename(logPath, path.join(dir, `${base}.1`)).catch(() => {
|
|
185
|
+
// If we cannot move the active file, truncate it so growth is bounded.
|
|
186
|
+
void fs.truncate(logPath, 0).catch(() => {})
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function appendDebugLogRecord(
|
|
191
|
+
logPath: string,
|
|
192
|
+
record: Record<string, unknown>,
|
|
193
|
+
maxBytes: number,
|
|
194
|
+
maxBackups: number,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
await ensureLogDir(logPath)
|
|
197
|
+
await rotateDebugLogIfNeeded(logPath, maxBytes, maxBackups)
|
|
198
|
+
await fs.appendFile(logPath, `${JSON.stringify(record)}\n`, "utf8")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function writeDcpDebugLog(
|
|
202
|
+
config: DcpConfig,
|
|
203
|
+
event: string,
|
|
204
|
+
details: Record<string, unknown> = {},
|
|
205
|
+
ctx?: ExtensionContext,
|
|
206
|
+
): void {
|
|
207
|
+
if (!dcpDebugEnabled(config)) return
|
|
208
|
+
|
|
209
|
+
const record = {
|
|
210
|
+
ts: new Date().toISOString(),
|
|
211
|
+
event,
|
|
212
|
+
...sessionInfo(ctx),
|
|
213
|
+
...details,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const logPath = dcpDebugLogPath()
|
|
217
|
+
const maxBytes = dcpDebugLogMaxBytes(config)
|
|
218
|
+
const maxBackups = dcpDebugLogMaxBackups(config)
|
|
219
|
+
|
|
220
|
+
// Serialize writes so concurrent records append in order and rotation is safe.
|
|
221
|
+
logWriteChain = logWriteChain
|
|
222
|
+
.then(() => appendDebugLogRecord(logPath, record, maxBytes, maxBackups))
|
|
223
|
+
.catch(() => {
|
|
224
|
+
// Debug logging must never affect the session or tool outcome.
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolves when all queued debug-log writes (and rotations) have settled. Useful
|
|
230
|
+
* for flushing before shutdown and for deterministic test assertions.
|
|
231
|
+
*/
|
|
232
|
+
export function dcpDebugLogDrain(): Promise<void> {
|
|
233
|
+
return logWriteChain
|
|
234
|
+
}
|
|
235
|
+
|
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
resetState,
|
|
10
10
|
createInputFingerprint,
|
|
11
11
|
restoreState,
|
|
12
|
+
inheritCompressionBlocks,
|
|
12
13
|
} from "./state.js"
|
|
13
14
|
import {
|
|
14
15
|
cleanupStaleDcpStateFiles,
|
|
15
16
|
loadDcpState,
|
|
17
|
+
loadDcpStateFromSessionFile,
|
|
16
18
|
resetDcpPersistenceDedup,
|
|
17
19
|
saveDcpState,
|
|
18
20
|
} from "./state-persistence.js"
|
|
@@ -32,12 +34,21 @@ import {
|
|
|
32
34
|
detectMessageCompressionCandidates,
|
|
33
35
|
appendConcreteNudgeGuidance,
|
|
34
36
|
applyAnchoredNudges,
|
|
37
|
+
clearDcpNudgeAnchors,
|
|
35
38
|
nudgeTypeLabel,
|
|
36
39
|
upsertNudgeAnchor,
|
|
37
40
|
getActiveSummaryTokenEstimate,
|
|
38
41
|
resolveContextThresholds,
|
|
39
42
|
estimateTokens,
|
|
40
43
|
} from "./pruner.js"
|
|
44
|
+
import {
|
|
45
|
+
stripStaleDcpMetadataFromAssistantMessage,
|
|
46
|
+
stripStaleDcpMetadataFromMessage,
|
|
47
|
+
} from "./pruner-metadata.js"
|
|
48
|
+
import {
|
|
49
|
+
buildMessageIdControlText,
|
|
50
|
+
} from "./pruner-message-ids.js"
|
|
51
|
+
import { summarizeDcpState, writeDcpDebugLog } from "./debug-log.js"
|
|
41
52
|
import type { DcpNudgeType } from "./pruner-types.js"
|
|
42
53
|
import { registerCompressTool } from "./compress-tool.js"
|
|
43
54
|
import { DCP_STATS_MESSAGE_TYPE, registerCommands } from "./commands.js"
|
|
@@ -88,13 +99,87 @@ function isUserVisibleOnlyMessage(message: any): boolean {
|
|
|
88
99
|
return message.details?.userVisibleOnly === true
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
|
|
102
|
+
// Control-plane custom message types filtered out of the transcript.
|
|
103
|
+
// `dcp-message-ids` is retained only for backward-compat with logs written by
|
|
104
|
+
// the removed inline control-message path.
|
|
105
|
+
const DCP_CONTROL_PLANE_CUSTOM_TYPES = new Set(["dcp-state", "dcp-nudge", "dcp-message-ids"])
|
|
92
106
|
const SUMMARY_BUFFER_MAX_CONTEXT_BONUS = 0.05
|
|
93
107
|
|
|
94
108
|
function isDcpControlPlaneMessage(message: any): boolean {
|
|
95
109
|
return message?.role === "custom" && DCP_CONTROL_PLANE_CUSTOM_TYPES.has(message.customType)
|
|
96
110
|
}
|
|
97
111
|
|
|
112
|
+
const DCP_PROVIDER_CONTROL_HEADER = "DCP message ID control data (do not quote or output):"
|
|
113
|
+
|
|
114
|
+
function appendTextToContent(content: unknown, text: string): unknown {
|
|
115
|
+
if (typeof content === "string") return `${content}\n\n${text}`
|
|
116
|
+
if (Array.isArray(content)) {
|
|
117
|
+
const textType = content.some((part: any) => part?.type === "input_text") ? "input_text" : "text"
|
|
118
|
+
return [...content, { type: textType, text }]
|
|
119
|
+
}
|
|
120
|
+
return text
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function appendDcpControlToMessages(messages: unknown, text: string): unknown {
|
|
124
|
+
if (!Array.isArray(messages)) return messages
|
|
125
|
+
const existingIndex = messages.findIndex((message: any) =>
|
|
126
|
+
message?.role === "system" || message?.role === "developer"
|
|
127
|
+
)
|
|
128
|
+
const block = `${DCP_PROVIDER_CONTROL_HEADER}\n${text}`
|
|
129
|
+
if (existingIndex >= 0) {
|
|
130
|
+
return messages.map((message: any, index) => index === existingIndex
|
|
131
|
+
? { ...message, content: appendTextToContent(message.content, block) }
|
|
132
|
+
: message)
|
|
133
|
+
}
|
|
134
|
+
return [{ role: "system", content: block }, ...messages]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function appendDcpControlToAnthropicSystem(system: unknown, text: string): unknown {
|
|
138
|
+
const block = `${DCP_PROVIDER_CONTROL_HEADER}\n${text}`
|
|
139
|
+
if (typeof system === "string") return `${system}\n\n${block}`
|
|
140
|
+
if (Array.isArray(system)) return [...system, { type: "text", text: block }]
|
|
141
|
+
if (system === undefined || system === null) return [{ type: "text", text: block }]
|
|
142
|
+
return system
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function appendDcpControlToGoogleSystemInstruction(systemInstruction: unknown, text: string): unknown {
|
|
146
|
+
const block = `${DCP_PROVIDER_CONTROL_HEADER}\n${text}`
|
|
147
|
+
if (typeof systemInstruction === "string") return `${systemInstruction}\n\n${block}`
|
|
148
|
+
if (systemInstruction === undefined || systemInstruction === null) return block
|
|
149
|
+
return systemInstruction
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function appendDcpControlToProviderPayload(payload: unknown, text: string): unknown {
|
|
153
|
+
if (Array.isArray(payload)) return appendDcpControlToMessages(payload, text)
|
|
154
|
+
if (!payload || typeof payload !== "object") return payload
|
|
155
|
+
const record = payload as Record<string, unknown>
|
|
156
|
+
|
|
157
|
+
if ("system" in record) {
|
|
158
|
+
return { ...record, system: appendDcpControlToAnthropicSystem(record.system, text) }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (Array.isArray(record.input)) {
|
|
162
|
+
return { ...record, input: appendDcpControlToMessages(record.input, text) }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (Array.isArray(record.messages)) {
|
|
166
|
+
return { ...record, messages: appendDcpControlToMessages(record.messages, text) }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (record.config && typeof record.config === "object") {
|
|
170
|
+
const config = record.config as Record<string, unknown>
|
|
171
|
+
return {
|
|
172
|
+
...record,
|
|
173
|
+
config: {
|
|
174
|
+
...config,
|
|
175
|
+
systemInstruction: appendDcpControlToGoogleSystemInstruction(config.systemInstruction, text),
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return payload
|
|
181
|
+
}
|
|
182
|
+
|
|
98
183
|
// ---------------------------------------------------------------------------
|
|
99
184
|
// Module export
|
|
100
185
|
// ---------------------------------------------------------------------------
|
|
@@ -150,7 +235,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
150
235
|
registerCommands(pi, state, config)
|
|
151
236
|
|
|
152
237
|
// ── 5. session_start: restore state from session entries ──────────────────
|
|
153
|
-
pi.on("session_start", async (
|
|
238
|
+
pi.on("session_start", async (event, ctx) => {
|
|
154
239
|
// Reset to a clean slate first.
|
|
155
240
|
resetState(state)
|
|
156
241
|
|
|
@@ -169,11 +254,37 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
169
254
|
})
|
|
170
255
|
restoreState(state, await loadDcpState(ctx))
|
|
171
256
|
|
|
257
|
+
// fork/resume/new sessions inherit the source conversation but get a fresh
|
|
258
|
+
// sidecar; inherit the previous session's compression blocks so they are
|
|
259
|
+
// not silently lost (which previously forced re-compressing all history).
|
|
260
|
+
if (state.compressionBlocks.length === 0 && event.previousSessionFile) {
|
|
261
|
+
try {
|
|
262
|
+
const inherited = await loadDcpStateFromSessionFile(event.previousSessionFile)
|
|
263
|
+
const added = inheritCompressionBlocks(state, inherited)
|
|
264
|
+
if (added > 0) {
|
|
265
|
+
writeDcpDebugLog(configForContext(ctx), "session_start.inherited_blocks", {
|
|
266
|
+
reason: event.reason,
|
|
267
|
+
previousSessionFile: event.previousSessionFile,
|
|
268
|
+
added,
|
|
269
|
+
totalBlocks: state.compressionBlocks.length,
|
|
270
|
+
}, ctx)
|
|
271
|
+
// Persist inherited state into this session's own sidecar so a later
|
|
272
|
+
// reload restores it directly.
|
|
273
|
+
await saveDcpState(ctx, state)
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// Inheritance is best-effort; never block session startup.
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
172
280
|
// Headless by design: no extension status/footer/widgets are rendered.
|
|
173
281
|
})
|
|
174
282
|
|
|
175
283
|
// ── 6. session_shutdown: save state ───────────────────────────────────────
|
|
176
284
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
285
|
+
// Force-flush: bypass the dedup hash so the final snapshot is always
|
|
286
|
+
// written, guaranteeing the next session_start can restore it.
|
|
287
|
+
resetDcpPersistenceDedup()
|
|
177
288
|
await saveDcpState(ctx, state)
|
|
178
289
|
})
|
|
179
290
|
|
|
@@ -191,6 +302,15 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
191
302
|
}
|
|
192
303
|
})
|
|
193
304
|
|
|
305
|
+
// ── 7b. message_end: never persist provider-echoed DCP control markers ─────
|
|
306
|
+
pi.on("message_end", async (event, ctx) => {
|
|
307
|
+
const effectiveConfig = configForContext(ctx)
|
|
308
|
+
if (!effectiveConfig.enabled || event.message?.role !== "assistant") return undefined
|
|
309
|
+
|
|
310
|
+
const sanitized = stripStaleDcpMetadataFromAssistantMessage(event.message)
|
|
311
|
+
return { message: sanitized }
|
|
312
|
+
})
|
|
313
|
+
|
|
194
314
|
// ── 8. tool_call: record input args for dedup / purge fingerprinting ───────
|
|
195
315
|
pi.on("tool_call", async (event, _ctx) => {
|
|
196
316
|
if (!state.toolCalls.has(event.toolCallId)) {
|
|
@@ -247,10 +367,32 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
247
367
|
// ── 10. context: apply pruning and inject nudges ──────────────────────────
|
|
248
368
|
pi.on("context", async (event, ctx) => {
|
|
249
369
|
const effectiveConfig = configForContext(ctx)
|
|
250
|
-
const contextMessages = event.messages
|
|
251
|
-
!isUserVisibleOnlyMessage(message) && !isDcpControlPlaneMessage(message)
|
|
252
|
-
|
|
370
|
+
const contextMessages = event.messages
|
|
371
|
+
.filter((message: any) => !isUserVisibleOnlyMessage(message) && !isDcpControlPlaneMessage(message))
|
|
372
|
+
.map((message: any) => stripStaleDcpMetadataFromMessage(message))
|
|
373
|
+
const finishContext = (reason: string, messages: any[], details: Record<string, unknown> = {}) => {
|
|
374
|
+
writeDcpDebugLog(effectiveConfig, "context.result", {
|
|
375
|
+
reason,
|
|
376
|
+
inputMessages: event.messages.length,
|
|
377
|
+
filteredMessages: contextMessages.length,
|
|
378
|
+
outputMessages: messages.length,
|
|
379
|
+
messageIdControl: "provider-payload",
|
|
380
|
+
state: summarizeDcpState(state),
|
|
381
|
+
...details,
|
|
382
|
+
}, ctx)
|
|
383
|
+
return { messages }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
writeDcpDebugLog(effectiveConfig, "context.start", {
|
|
387
|
+
inputMessages: event.messages.length,
|
|
388
|
+
filteredMessages: contextMessages.length,
|
|
389
|
+
filteredDcpControlPlaneMessages: event.messages.length - contextMessages.length,
|
|
390
|
+
}, ctx)
|
|
253
391
|
if (!effectiveConfig.enabled) {
|
|
392
|
+
writeDcpDebugLog(effectiveConfig, "context.disabled", {
|
|
393
|
+
inputMessages: event.messages.length,
|
|
394
|
+
filteredMessages: contextMessages.length,
|
|
395
|
+
}, ctx)
|
|
254
396
|
return { messages: contextMessages }
|
|
255
397
|
}
|
|
256
398
|
annotateMessagesWithBranchEntryIds(contextMessages, ctx)
|
|
@@ -270,15 +412,9 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
270
412
|
: undefined
|
|
271
413
|
|
|
272
414
|
if (contextPercent === undefined) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
)
|
|
277
|
-
}
|
|
278
|
-
applyAnchoredNudges(prunedMessages, state, (anchor) =>
|
|
279
|
-
appendConcreteNudgeGuidance(baseNudgeText(anchor.type), candidate, messageCandidates, state),
|
|
280
|
-
)
|
|
281
|
-
return { messages: prunedMessages }
|
|
415
|
+
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
416
|
+
if (clearedAnchors > 0) await saveDcpState(ctx, state)
|
|
417
|
+
return finishContext("unknown-context-percent", prunedMessages, { clearedAnchors })
|
|
282
418
|
}
|
|
283
419
|
|
|
284
420
|
const ctxModel = (ctx as any).model
|
|
@@ -293,6 +429,18 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
293
429
|
thresholds.maxContextPercent += Math.min(summaryBonus, SUMMARY_BUFFER_MAX_CONTEXT_BONUS)
|
|
294
430
|
}
|
|
295
431
|
|
|
432
|
+
const contextLimitReached = contextPercent > thresholds.maxContextPercent
|
|
433
|
+
const routineNudgesAllowed = contextPercent > thresholds.minContextPercent
|
|
434
|
+
if (!contextLimitReached && !routineNudgesAllowed) {
|
|
435
|
+
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
436
|
+
if (clearedAnchors > 0) await saveDcpState(ctx, state)
|
|
437
|
+
return finishContext("below-threshold", prunedMessages, {
|
|
438
|
+
contextPercent,
|
|
439
|
+
thresholds,
|
|
440
|
+
clearedAnchors,
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
296
444
|
let toolCallsSinceLastUser = 0
|
|
297
445
|
for (let i = prunedMessages.length - 1; i >= 0; i--) {
|
|
298
446
|
const msg = prunedMessages[i] as any
|
|
@@ -312,18 +460,28 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
312
460
|
state.manualMode &&
|
|
313
461
|
(nudgeType !== "context-strong" && nudgeType !== "context-soft")
|
|
314
462
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
463
|
+
if (!manualEmergencyOnly) {
|
|
464
|
+
candidate = detectCompressionCandidate(
|
|
465
|
+
prunedMessages,
|
|
466
|
+
state,
|
|
467
|
+
effectiveConfig,
|
|
468
|
+
contextPercent,
|
|
469
|
+
)
|
|
470
|
+
messageCandidates = detectMessageCompressionCandidates(
|
|
471
|
+
prunedMessages,
|
|
472
|
+
state,
|
|
473
|
+
effectiveConfig,
|
|
474
|
+
contextPercent,
|
|
475
|
+
)
|
|
476
|
+
writeDcpDebugLog(effectiveConfig, "context.candidates", {
|
|
477
|
+
contextPercent,
|
|
478
|
+
thresholds,
|
|
479
|
+
nudgeType,
|
|
480
|
+
candidate,
|
|
481
|
+
messageCandidates,
|
|
482
|
+
state: summarizeDcpState(state),
|
|
483
|
+
}, ctx)
|
|
484
|
+
}
|
|
327
485
|
|
|
328
486
|
if (nudgeType && !manualEmergencyOnly) {
|
|
329
487
|
const nudgeText = appendConcreteNudgeGuidance(
|
|
@@ -386,7 +544,26 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
386
544
|
appendConcreteNudgeGuidance(baseNudgeText(anchor.type), candidate, messageCandidates, state),
|
|
387
545
|
)
|
|
388
546
|
|
|
389
|
-
return
|
|
547
|
+
return finishContext("complete", prunedMessages, {
|
|
548
|
+
candidate,
|
|
549
|
+
messageCandidates,
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
// ── 10b. before_provider_request: inject DCP IDs outside transcript ────────
|
|
554
|
+
pi.on("before_provider_request", async (event, ctx) => {
|
|
555
|
+
const effectiveConfig = configForContext(ctx)
|
|
556
|
+
if (!effectiveConfig.enabled) return undefined
|
|
557
|
+
|
|
558
|
+
const controlText = buildMessageIdControlText(state)
|
|
559
|
+
if (!controlText) return undefined
|
|
560
|
+
|
|
561
|
+
const payload = appendDcpControlToProviderPayload(event.payload, controlText)
|
|
562
|
+
writeDcpDebugLog(effectiveConfig, "provider_payload.message_ids", {
|
|
563
|
+
injected: payload !== event.payload,
|
|
564
|
+
state: summarizeDcpState(state),
|
|
565
|
+
}, ctx)
|
|
566
|
+
return payload === event.payload ? undefined : payload
|
|
390
567
|
})
|
|
391
568
|
|
|
392
569
|
// ── 11. agent_end: persist state after each agent run ────────────────────
|