// @generated by scripts/build-runtime.mjs; do not edit. // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader. import { terminalText } from "./chunks/chunk-K6TRB5OJ.ts"; // src/context-management.ts import { randomUUID as randomUUID2 } from "node:crypto"; import { realpathSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { sessionEntryToContextMessages as sessionEntryToContextMessages3 } from "@earendil-works/pi-coding-agent"; // src/context-tools.ts import { StringEnum } from "@earendil-works/pi-ai"; import { Type } from "typebox"; // src/context-window.ts import { randomUUID } from "node:crypto"; import { buildContextEntries, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent"; // src/fingerprint.ts import { createHash } from "node:crypto"; var MAX_FINGERPRINT_DEPTH = 512; var MAX_FINGERPRINT_OPERATION_UNITS = 4 * 1024 * 1024; var MAX_FINGERPRINT_OPERATION_BYTES = 8 * 1024 * 1024; function createFingerprintBudget() { return { remainingUnits: MAX_FINGERPRINT_OPERATION_UNITS, remainingBytes: MAX_FINGERPRINT_OPERATION_BYTES }; } function failLimit() { throw new Error("Context message fingerprint exceeded its traversal limit"); } function assign(parent, key, value) { if (Array.isArray(parent)) parent[key] = value; else parent[String(key)] = value; } function stableValue(value, operationBudget) { const root = {}; const tasks = [{ source: value, parent: root, key: "value", depth: 0 }]; let remainingUnits = MAX_FINGERPRINT_OPERATION_UNITS; const consume = (units) => { if (!Number.isSafeInteger(units) || units < 0 || units > remainingUnits || units > operationBudget.remainingUnits) { failLimit(); } remainingUnits -= units; operationBudget.remainingUnits -= units; }; while (tasks.length > 0) { const task = tasks.pop(); if (!task) break; consume(1); const source = task.source; if (typeof source === "string") consume(source.length); if (typeof source !== "object" || source === null) { if (typeof source === "bigint") throw new Error("Context message fingerprint cannot serialize bigint values"); assign(task.parent, task.key, source); continue; } if (task.depth >= MAX_FINGERPRINT_DEPTH) failLimit(); if (Array.isArray(source)) { consume(source.length); const target2 = new Array(source.length); assign(task.parent, task.key, target2); for (let index = source.length - 1; index >= 0; index -= 1) { tasks.push({ source: source[index], parent: target2, key: index, depth: task.depth + 1 }); } continue; } const keys = []; for (const key in source) { if (!Object.hasOwn(source, key)) continue; consume(1 + key.length); keys.push(key); } keys.sort((left, right) => left < right ? -1 : left > right ? 1 : 0); const target = {}; assign(task.parent, task.key, target); for (let index = keys.length - 1; index >= 0; index -= 1) { const key = keys[index]; tasks.push({ source: source[key], parent: target, key, depth: task.depth + 1 }); } } return root.value; } function fingerprintMessage(message, operationBudget = createFingerprintBudget()) { const serialized = JSON.stringify(stableValue(message, operationBudget)); if (serialized === void 0) failLimit(); const bytes2 = Buffer.byteLength(serialized, "utf8"); if (bytes2 > MAX_FINGERPRINT_OPERATION_BYTES || bytes2 > operationBudget.remainingBytes) failLimit(); operationBudget.remainingBytes -= bytes2; return createHash("sha256").update(serialized).digest("hex"); } // src/context-window.ts var CONTEXT_STATE_ENTRY_TYPE = "pi-context-management-state"; var CONTEXT_CONTRACT_MESSAGE_TYPE = "pi-context-management-contract"; var CONTEXT_DEACTIVATION_MESSAGE_TYPE = "pi-context-management-deactivation"; var CONTEXT_DETAILS_KIND = "pi-context-management-window"; var CONTEXT_VERSION = 1; var MAX_DETAILS_BYTES = 8 * 1024 * 1024; var MAX_FINGERPRINTS = 1e5; var MAX_CONTEXT_BRANCH_ENTRY_VISITS = 1e5; var MAX_CONTEXT_BRANCH_SCAN_UNITS = 8 * 1024 * 1024; function createContextBranchScanBudget() { return { remainingVisits: MAX_CONTEXT_BRANCH_ENTRY_VISITS, remainingScanUnits: MAX_CONTEXT_BRANCH_SCAN_UNITS }; } function visitContextBranchEntry(budget) { if (budget.remainingVisits <= 0) { throw new Error("context_management context branch traversal exceeded its entry limit"); } budget.remainingVisits -= 1; } function consumeContextBranchScan(budget, units) { if (!Number.isSafeInteger(units) || units < 0 || units > budget.remainingScanUnits) { throw new Error("context_management context branch traversal exceeded its scan limit"); } budget.remainingScanUnits -= units; } function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function isIdentifier(value) { return typeof value === "string" && value.length >= 8 && value.length <= 128; } function parseLineage(value) { if (!isIdentifier(value.firstWindowId) || !isIdentifier(value.currentWindowId)) return void 0; if (value.previousWindowId !== void 0 && !isIdentifier(value.previousWindowId)) return void 0; return { firstWindowId: value.firstWindowId, ...typeof value.previousWindowId === "string" ? { previousWindowId: value.previousWindowId } : {}, currentWindowId: value.currentWindowId }; } function parseContextState(value) { if (!isRecord(value)) return void 0; const lineage = parseLineage(value); if (!lineage || value.kind !== CONTEXT_DETAILS_KIND || value.version !== CONTEXT_VERSION || value.previousWindowId !== void 0 || lineage.firstWindowId !== lineage.currentWindowId) { return void 0; } return { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, ...lineage }; } function parseContextManagementDetails(value, budget) { if (!isRecord(value) || value.kind !== CONTEXT_DETAILS_KIND || value.version !== CONTEXT_VERSION) return void 0; const lineage = parseLineage(value); if (!lineage?.previousWindowId || lineage.currentWindowId === lineage.previousWindowId || value.reason !== "manual" && value.reason !== "threshold" && value.reason !== "overflow" || value.requestId !== void 0 && !isIdentifier(value.requestId) || !Array.isArray(value.keptMessageFingerprints) || value.keptMessageFingerprints.length > MAX_FINGERPRINTS || value.retryResponseFingerprint !== void 0 && (typeof value.retryResponseFingerprint !== "string" || value.retryResponseFingerprint.length !== 64) || typeof value.createdAt !== "string" || value.createdAt.length > 64) { return void 0; } const scanUnits = 1 + lineage.firstWindowId.length + lineage.previousWindowId.length + lineage.currentWindowId.length + value.createdAt.length + (typeof value.requestId === "string" ? value.requestId.length : 0) + (typeof value.retryResponseFingerprint === "string" ? value.retryResponseFingerprint.length : 0) + value.keptMessageFingerprints.length + value.keptMessageFingerprints.reduce( (total, fingerprint) => total + (typeof fingerprint === "string" ? fingerprint.length : 1), 0 ); const scanBudget = budget ?? createContextBranchScanBudget(); try { consumeContextBranchScan(scanBudget, scanUnits); } catch (error) { if (budget) throw error; return void 0; } if (!value.keptMessageFingerprints.every( (fingerprint) => typeof fingerprint === "string" && /^[a-f0-9]{64}$/.test(fingerprint) ) || typeof value.retryResponseFingerprint === "string" && !/^[a-f0-9]{64}$/.test(value.retryResponseFingerprint)) { return void 0; } const details = { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, ...lineage, reason: value.reason, ...typeof value.requestId === "string" ? { requestId: value.requestId } : {}, keptMessageFingerprints: [...value.keptMessageFingerprints], ...typeof value.retryResponseFingerprint === "string" ? { retryResponseFingerprint: value.retryResponseFingerprint } : {}, createdAt: value.createdAt }; return Buffer.byteLength(JSON.stringify(details), "utf8") <= MAX_DETAILS_BYTES ? details : void 0; } function parseContextManagementCompaction(entry, budget) { const details = parseContextManagementDetails(entry.details, budget); return details && entry.summary === contextContract(details) ? details : void 0; } function createInitialContextState(windowId = randomUUID()) { return { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, firstWindowId: windowId, currentWindowId: windowId }; } function loadContextLineage(entries, budget = createContextBranchScanBudget()) { for (let index = entries.length - 1; index >= 0; index -= 1) { visitContextBranchEntry(budget); const entry = entries[index]; if (entry.type === "compaction") { const details = parseContextManagementCompaction(entry, budget); if (details) return details; continue; } if (entry.type === "custom" && entry.customType === CONTEXT_STATE_ENTRY_TYPE) { const state = parseContextState(entry.data); if (state) return state; } } return void 0; } function activeContextManagementCompaction(entries, budget = createContextBranchScanBudget()) { for (let index = entries.length - 1; index >= 0; index -= 1) { visitContextBranchEntry(budget); const entry = entries[index]; if (entry.type !== "compaction") continue; const details = parseContextManagementCompaction(entry, budget); return details ? { entry, details } : void 0; } return void 0; } function contextContract(lineage) { return [ `[PI_CONTEXT_MANAGEMENT_WINDOW:${lineage.currentWindowId}]`, "Experimental Pi-native context management is active.", lineage.previousWindowId ? `The previous context window was ${lineage.previousWindowId}.` : "This is the first context window in this session.", "Use context_management_get_context_remaining to inspect capacity, context_management_recall_context to retrieve older history or notes, and context_management_update_notes to preserve durable working memory.", "Call context_management_start_new_context when a fresh context is needed. Important information is not summarized automatically." ].join("\n"); } function contextDeactivation() { return [ "Experimental Pi-native context management is no longer active.", "Its context tools are unavailable. Continue with Pi's active compaction strategy and do not call context_management_start_new_context, context_management_get_context_remaining, context_management_recall_context, or context_management_update_notes." ].join("\n"); } function latestContextMode(entries, budget = createContextBranchScanBudget()) { let mode; for (const _entry of entries) visitContextBranchEntry(budget); const leafId = entries.at(-1)?.id ?? null; for (const entry of buildContextEntries([...entries], leafId)) { if (entry.type === "compaction" && parseContextManagementCompaction(entry, budget)) mode = "active"; for (const message of sessionEntryToContextMessages(entry)) { if (message.role !== "custom") continue; if (message.customType === CONTEXT_CONTRACT_MESSAGE_TYPE) mode = "active"; if (message.customType === CONTEXT_DEACTIVATION_MESSAGE_TYPE && message.content === contextDeactivation()) { mode = "inactive"; } } } return mode; } function createContextContractMessage(lineage) { return { role: "custom", customType: CONTEXT_CONTRACT_MESSAGE_TYPE, content: contextContract(lineage), display: false, details: { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, currentWindowId: lineage.currentWindowId }, timestamp: 0 }; } function hasContextContract(messages, lineage) { const expected = contextContract(lineage); return messages.some( (message) => message.role === "custom" && message.customType === CONTEXT_CONTRACT_MESSAGE_TYPE && message.content === expected || message.role === "compactionSummary" && message.summary === expected ); } function reconcileContextContract(messages, lineage) { if (hasContextContract(messages, lineage)) return [...messages]; return [...messages, createContextContractMessage(lineage)]; } function compactionRetainedContext(event, fingerprintBudget = createFingerprintBudget()) { const leafId = event.branchEntries.at(-1)?.id ?? null; const contextEntries = buildContextEntries(event.branchEntries, leafId); const keptIndex = contextEntries.findIndex((entry) => entry.id === event.preparation.firstKeptEntryId); if (keptIndex < 0) { throw new Error("Pi compaction cut point is not present in the active context"); } const keptMessages = contextEntries.slice(keptIndex).flatMap(sessionEntryToContextMessages); const lastMessage = keptMessages.at(-1); if (event.willRetry && lastMessage?.role === "assistant" && (lastMessage.stopReason === "error" || lastMessage.stopReason === "length")) { return { keptMessages: keptMessages.slice(0, -1), retryResponseFingerprint: fingerprintMessage(lastMessage, fingerprintBudget) }; } return { keptMessages }; } function createContextManagementDetails(input, fingerprintBudget = createFingerprintBudget()) { const currentWindowId = input.windowId ?? randomUUID(); const details = { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, firstWindowId: input.lineage.firstWindowId, previousWindowId: input.lineage.currentWindowId, currentWindowId, reason: input.reason, ...input.requestId ? { requestId: input.requestId } : {}, keptMessageFingerprints: input.keptMessages.map((message) => fingerprintMessage(message, fingerprintBudget)), ...input.retryResponseFingerprint ? { retryResponseFingerprint: input.retryResponseFingerprint } : {}, createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString() }; const parsed = parseContextManagementDetails(details); if (!parsed) throw new Error("Created invalid context-management details"); return parsed; } function isOlderCompactionSummary(message, timestamp) { return message.role === "compactionSummary" && Number.isFinite(message.timestamp) && Number.isFinite(timestamp) && message.timestamp < timestamp; } function projectContextManagementContext(messages, entry, details) { const fingerprintBudget = createFingerprintBudget(); const expectedSummary = contextContract(details); if (entry.summary !== expectedSummary) return void 0; const summaryIndex = messages.findIndex( (message) => message.role === "compactionSummary" && message.summary === expectedSummary ); if (summaryIndex < 0) return void 0; const timestamp = messages[summaryIndex].timestamp; let messageIndex = summaryIndex + 1; let fingerprintIndex = 0; while (fingerprintIndex < details.keptMessageFingerprints.length) { if (messageIndex >= messages.length) return void 0; const message = messages[messageIndex]; if (fingerprintMessage(message, fingerprintBudget) === details.keptMessageFingerprints[fingerprintIndex]) { messageIndex += 1; fingerprintIndex += 1; continue; } if (isOlderCompactionSummary(message, timestamp)) { messageIndex += 1; continue; } return void 0; } while (messageIndex < messages.length && isOlderCompactionSummary(messages[messageIndex], timestamp)) { messageIndex += 1; } if (details.retryResponseFingerprint && messageIndex < messages.length && fingerprintMessage(messages[messageIndex], fingerprintBudget) === details.retryResponseFingerprint) { messageIndex += 1; } return [...messages.slice(0, summaryIndex + 1), ...messages.slice(messageIndex)]; } // src/notes-state.ts import { stripVTControlCharacters } from "node:util"; var NOTES_ENTRY_TYPE = "pi-context-management-note"; var NOTES_VERSION = 1; var MAX_NOTE_NAME_LENGTH = 128; var MAX_NOTE_MUTATION_BYTES = 16 * 1024; var MAX_NOTE_COUNT = 64; var MAX_NOTES_BYTES = 256 * 1024; var MAX_NOTE_BRANCH_ENTRY_VISITS = 1e5; var MAX_NOTE_REPLAY_SCAN_UNITS = 4 * 1024 * 1024; function isRecord2(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function bytes(value) { return Buffer.byteLength(value, "utf8"); } function validNoteName(value) { return typeof value === "string" && value.length > 0 && value.length <= MAX_NOTE_NAME_LENGTH && value.trim() === value && stripVTControlCharacters(value) === value && !Array.from(value).some((character) => { const codePoint = character.codePointAt(0) ?? 0; return codePoint < 32 || codePoint >= 127 && codePoint <= 159; }); } function parseNoteMutation(value) { if (!isRecord2(value)) return void 0; if (value.version !== NOTES_VERSION || value.action !== "write" && value.action !== "append" || !validNoteName(value.note) || typeof value.content !== "string" || value.content.length === 0 || value.content.length > MAX_NOTE_MUTATION_BYTES || bytes(value.content) > MAX_NOTE_MUTATION_BYTES) { return void 0; } return { version: NOTES_VERSION, action: value.action, note: value.note, content: value.content }; } function loadNotes(entries) { const notes = /* @__PURE__ */ new Map(); const noteContentBytes = /* @__PURE__ */ new Map(); let totalBytes = 0; let remainingEntries = MAX_NOTE_BRANCH_ENTRY_VISITS; let remainingScanUnits = MAX_NOTE_REPLAY_SCAN_UNITS; for (const entry of entries) { if (remainingEntries <= 0) { throw new Error("context_management notes branch traversal exceeded its entry limit"); } remainingEntries -= 1; if (entry.type !== "custom" || entry.customType !== NOTES_ENTRY_TYPE) continue; const data = entry.data; const scanUnits = 1 + (isRecord2(data) && typeof data.note === "string" ? data.note.length : 0) + (isRecord2(data) && typeof data.content === "string" ? data.content.length : 0); if (scanUnits > remainingScanUnits) { throw new Error("context_management notes replay exceeded its scan limit"); } remainingScanUnits -= scanUnits; const mutation = parseNoteMutation(data); if (!mutation) continue; const previous = notes.get(mutation.note); const previousContentBytes = noteContentBytes.get(mutation.note) ?? 0; const nameBytes = bytes(mutation.note); const mutationContentBytes = bytes(mutation.content); const nextContentBytes = mutation.action === "append" ? previousContentBytes + mutationContentBytes : mutationContentBytes; const nextTotalBytes = totalBytes - (previous === void 0 ? 0 : nameBytes + previousContentBytes) + nameBytes + nextContentBytes; if (previous === void 0 && notes.size >= MAX_NOTE_COUNT || nextTotalBytes > MAX_NOTES_BYTES) continue; notes.set(mutation.note, mutation.action === "append" ? `${previous ?? ""}${mutation.content}` : mutation.content); noteContentBytes.set(mutation.note, nextContentBytes); totalBytes = nextTotalBytes; } return notes; } function totalNotesBytes(notes) { let total = 0; for (const [name, content] of notes) total += bytes(name) + bytes(content); return total; } function createNoteMutation(entries, input) { const mutation = parseNoteMutation({ version: NOTES_VERSION, ...input }); if (!mutation) { throw new Error( `Invalid note mutation; names must be 1-${MAX_NOTE_NAME_LENGTH} characters without terminal controls and content must be 1-${MAX_NOTE_MUTATION_BYTES} UTF-8 bytes` ); } const notes = loadNotes(entries); const exists = notes.has(mutation.note); const next = mutation.action === "append" ? `${notes.get(mutation.note) ?? ""}${mutation.content}` : mutation.content; if (!exists && notes.size >= MAX_NOTE_COUNT) { throw new Error(`Context notes are limited to ${MAX_NOTE_COUNT} names`); } notes.set(mutation.note, next); if (totalNotesBytes(notes) > MAX_NOTES_BYTES) { throw new Error(`Context notes exceed the ${MAX_NOTES_BYTES}-byte active-state limit`); } return { mutation, notes }; } function sortedNotes(entries) { return [...loadNotes(entries)].map(([name, content]) => ({ name, content })).sort((left, right) => left.name.localeCompare(right.name)); } // src/recall-context.ts import { stripVTControlCharacters as stripVTControlCharacters2 } from "node:util"; import { sessionEntryToContextMessages as sessionEntryToContextMessages2 } from "@earendil-works/pi-coding-agent"; var MAX_RECALL_QUERY_LENGTH = 512; var MAX_RECALL_RESULT_BYTES = 32 * 1024; var MAX_RECALL_MATCHES = 20; var MAX_HISTORY_BRANCH_ENTRY_VISITS = 1e5; var MAX_INDEXED_MESSAGE_CHARS = 256 * 1024; var MAX_SCANNED_MESSAGE_UNITS = 4 * MAX_INDEXED_MESSAGE_CHARS; var MAX_HISTORY_SEARCH_SCAN_UNITS = 4 * MAX_SCANNED_MESSAGE_UNITS; var MAX_HISTORY_READ_SCAN_UNITS = MAX_HISTORY_SEARCH_SCAN_UNITS; var MAX_HISTORY_READ_DEPTH = 512; var READ_CHUNK_BYTES = 12 * 1024; function visitHistoryEntry(budget) { if (budget.remainingVisits <= 0) { throw new Error("context_management_recall_context history branch traversal exceeded its entry limit"); } budget.remainingVisits -= 1; } function parseCursor(cursor) { if (cursor === void 0) return 0; if (!/^\d{1,12}$/.test(cursor)) throw new Error("context_management_recall_context cursor is invalid"); return Number.parseInt(cursor, 10); } function messagePayload(message) { switch (message.role) { case "compactionSummary": return { summary: message.summary }; case "branchSummary": return { summary: message.summary }; case "custom": return { customType: message.customType, content: message.content }; case "toolResult": return { toolName: message.toolName, toolCallId: message.toolCallId, content: message.content, isError: message.isError }; default: { const candidate = message; return Object.hasOwn(candidate, "content") ? { content: candidate.content } : { command: candidate.command, output: candidate.output, exitCode: candidate.exitCode }; } } } function serializeMessage(message) { let remainingUnits = MAX_HISTORY_READ_SCAN_UNITS; const depths = /* @__PURE__ */ new WeakMap(); const serialized = JSON.stringify( messagePayload(message), function boundedReplacer(key, value) { const holderDepth = typeof this === "object" && this !== null ? depths.get(this) : void 0; const depth = key === "" && holderDepth === void 0 ? 0 : (holderDepth ?? -1) + 1; const units = 1 + key.length + (typeof value === "string" ? value.length : 0); if (depth > MAX_HISTORY_READ_DEPTH || units > remainingUnits) { throw new Error("context_management_recall_context history read exceeded its scan limit"); } remainingUnits -= units; if (typeof value === "object" && value !== null) depths.set(value, depth); return value; } ); if (serialized === void 0) { throw new Error("context_management_recall_context selected history payload is not serializable"); } return JSON.stringify(sanitizeJsonValue(JSON.parse(serialized))); } function isRecord3(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } var INDEXED_CONTENT = /* @__PURE__ */ Symbol("pi-context-management-indexed-content"); function indexedContent(value) { return { [INDEXED_CONTENT]: true, value }; } function isIndexedContent(value) { return typeof value === "object" && value !== null && value[INDEXED_CONTENT] === true; } function indexedBlock(block) { if (!isRecord3(block)) return block; switch (block.type) { case "text": return { type: block.type, text: block.text }; case "image": return { type: block.type, mimeType: block.mimeType }; case "thinking": return { type: block.type, thinking: block.thinking, ...typeof block.redacted === "boolean" ? { redacted: block.redacted } : {} }; case "toolCall": return { type: block.type, id: block.id, name: block.name, ...block.name === "context_management_recall_context" ? {} : { arguments: block.arguments }, ...typeof block.namespace === "string" ? { namespace: block.namespace } : {} }; default: return block; } } function messageIndexPayload(message) { if (message.role === "toolResult" && message.toolName === "context_management_recall_context") { return { toolName: message.toolName, toolCallId: message.toolCallId, isError: message.isError }; } const payload = messagePayload(message); if (!isRecord3(payload) || !Object.hasOwn(payload, "content")) return payload; return { ...payload, content: indexedContent(payload.content) }; } function isExcludedFromModelContext(message) { return message.role === "bashExecution" && message.excludeFromContext === true; } function* historyMessageItems(entries, budget, firstWindowId, detailScanBudget) { let windowId = firstWindowId; for (const entry of entries) { visitHistoryEntry(budget); if (entry.type === "compaction") { const details = parseContextManagementCompaction(entry, detailScanBudget); if (details) windowId = details.currentWindowId; } const messages = sessionEntryToContextMessages2(entry); for (let index = 0; index < messages.length; index += 1) { const message = messages[index]; if (isExcludedFromModelContext(message)) continue; yield { id: messages.length === 1 ? entry.id : `${entry.id}:${index}`, ...windowId ? { windowId } : {}, role: message.role, message }; } } } function isActiveToolCallMessage(message, toolCallId) { return toolCallId !== void 0 && message.role === "assistant" && message.content.some((block) => block.type === "toolCall" && block.id === toolCallId); } function displayText(value) { return Array.from(stripVTControlCharacters2(value), (character) => { const codePoint = character.codePointAt(0) ?? 0; if (codePoint === 9 || codePoint === 10 || codePoint === 13) return character; return codePoint < 32 || codePoint >= 127 && codePoint <= 159 ? " " : character; }).join(""); } function normalizedSearchQuery(input) { if (!input.query || input.query.length > MAX_RECALL_QUERY_LENGTH || input.id !== void 0) { throw new Error( `context_management_recall_context search requires a query of 1-${MAX_RECALL_QUERY_LENGTH} characters and does not accept id` ); } const query = displayText(input.query).trim().toLowerCase(); if (!query) throw new Error("context_management_recall_context search query contains no visible text"); return query; } function preview(value) { const compact = displayText(value).replace(/\s+/g, " ").trim(); const characters = Array.from(compact); return characters.length > 240 ? `${characters.slice(0, 239).join("")}\u2026` : compact; } function* ownEntries(value) { for (const key in value) { if (Object.hasOwn(value, key)) yield [key, value[key]]; } } function boundedPayloadText(value, searchBudget) { let scannedUnits = 0; let text = ""; const consume = (requestedUnits) => { const allowedByMessage = Math.min(requestedUnits, MAX_SCANNED_MESSAGE_UNITS - scannedUnits); const consumedUnits = Math.min(allowedByMessage, searchBudget?.remainingUnits ?? allowedByMessage); scannedUnits += consumedUnits; if (searchBudget) { searchBudget.remainingUnits -= consumedUnits; if (consumedUnits < allowedByMessage) searchBudget.exceeded = true; } return consumedUnits; }; const append = (value2) => { const part = value2.slice(0, consume(value2.length)); text += displayText(part).slice(0, MAX_INDEXED_MESSAGE_CHARS - text.length); }; const frames = [{ kind: "value", value }]; while (frames.length > 0 && scannedUnits < MAX_SCANNED_MESSAGE_UNITS && text.length < MAX_INDEXED_MESSAGE_CHARS && !searchBudget?.exceeded) { const frame = frames.pop(); if (!frame) break; if (frame.kind === "text") { append(frame.value); continue; } if (frame.kind === "array") { if (frame.index < frame.value.length) { frames.push( { kind: "array", value: frame.value, index: frame.index + 1 }, { kind: "value", value: frame.value[frame.index] } ); } continue; } if (frame.kind === "indexed-content") { if (frame.index < frame.value.length && consume(1) === 1) { frames.push( { kind: "indexed-content", value: frame.value, index: frame.index + 1 }, { kind: "value", value: indexedBlock(frame.value[frame.index]) } ); } continue; } if (frame.kind === "object") { const next = frame.entries.next(); if (!next.done) { const [key, item2] = next.value; frames.push( frame, { kind: "text", value: "\n" }, { kind: "value", value: item2 }, { kind: "text", value: `${key} ` } ); } continue; } if (consume(1) < 1) continue; const item = frame.value; if (typeof item === "string") { append(item); } else if (typeof item !== "object" || item === null) { if (item !== void 0 && typeof item !== "function" && typeof item !== "symbol") { append(String(item)); } } else if (isIndexedContent(item)) { if (Array.isArray(item.value)) frames.push({ kind: "indexed-content", value: item.value, index: 0 }); else frames.push({ kind: "value", value: item.value }); } else if (Array.isArray(item)) { frames.push({ kind: "array", value: item, index: 0 }); } else { frames.push({ kind: "object", entries: ownEntries(item) }); } } return text; } function boundedMessageText(message, searchBudget) { return boundedPayloadText(messageIndexPayload(message), searchBudget); } function paged(values, offset) { const items = values.slice(offset, offset + MAX_RECALL_MATCHES); const next = offset + items.length; return { items, ...next < values.length ? { nextCursor: String(next) } : {} }; } function searchPage(values, offset, matches) { const items = []; let matchIndex = 0; for (const value of values) { if (!matches(value)) continue; if (matchIndex >= offset) items.push(value); matchIndex += 1; if (items.length > MAX_RECALL_MATCHES) break; } const hasMore = items.length > MAX_RECALL_MATCHES; return { items: items.slice(0, MAX_RECALL_MATCHES), ...hasMore ? { nextCursor: String(offset + MAX_RECALL_MATCHES) } : {} }; } function readChunk(value, offset) { if (offset > value.length) throw new Error("context_management_recall_context cursor exceeds the selected item"); if (offset > 0 && offset < value.length && /[\uDC00-\uDFFF]/.test(value[offset]) && /[\uD800-\uDBFF]/.test(value[offset - 1])) { throw new Error("context_management_recall_context cursor splits a Unicode code point"); } let bytes2 = 0; let next = offset; for (const character of value.slice(offset)) { const characterBytes = Buffer.byteLength(character, "utf8"); if (bytes2 + characterBytes > READ_CHUNK_BYTES) break; bytes2 += characterBytes; next += character.length; } const chunk = value.slice(offset, next); return { chunk, ...next < value.length ? { nextCursor: String(next) } : {} }; } function sanitizeJsonValue(value) { if (typeof value === "string") return displayText(value); if (Array.isArray(value)) return value.map(sanitizeJsonValue); if (typeof value !== "object" || value === null) return value; return Object.fromEntries(Object.entries(value).map(([key, item]) => [displayText(key), sanitizeJsonValue(item)])); } function safeJson(value) { const text = JSON.stringify(sanitizeJsonValue(value), null, 2); if (Buffer.byteLength(text, "utf8") > MAX_RECALL_RESULT_BYTES) { throw new Error("context_management_recall_context result exceeded its output limit"); } if (text.split("\n").length > 1e3) { throw new Error("context_management_recall_context result exceeded its line limit"); } return text; } function recallContext(entries, input, activeToolCallId, limits = {}) { if (input.source !== "history" && input.source !== "notes") { throw new Error("context_management_recall_context source must be history or notes"); } if (input.action !== "list" && input.action !== "read" && input.action !== "search") { throw new Error("context_management_recall_context action must be list, read, or search"); } const offset = parseCursor(input.cursor); if (input.action === "read" && (!input.id || input.query !== void 0)) { throw new Error("context_management_recall_context read requires id and does not accept query"); } const searchQuery = input.action === "search" ? normalizedSearchQuery(input) : ""; if (input.action === "list" && (input.id !== void 0 || input.query !== void 0)) { throw new Error("context_management_recall_context list does not accept id or query"); } if (input.source === "notes") { const notes = sortedNotes(entries); if (input.action === "list") { const page3 = paged( notes.map((note) => ({ id: note.name, bytes: Buffer.byteLength(note.content, "utf8") })), offset ); return { text: safeJson({ source: "notes", action: "list", ...page3 }), details: page3 }; } if (input.action === "read") { const note = notes.find((candidate) => candidate.name === input.id); if (!note) throw new Error(`Context note ${JSON.stringify(displayText(input.id ?? ""))} was not found`); const page3 = readChunk(displayText(note.content), offset); return { text: safeJson({ source: "notes", action: "read", id: note.name, ...page3 }), details: { source: "notes", id: note.name, ...page3 } }; } const matches2 = searchPage( notes, offset, (note) => displayText(`${note.name} ${note.content.slice(0, MAX_INDEXED_MESSAGE_CHARS)}`).toLowerCase().includes(searchQuery) ); const page2 = { ...matches2, items: matches2.items.map((note) => ({ id: note.name, preview: preview(note.content) })) }; return { text: safeJson({ source: "notes", action: "search", ...page2 }), details: page2 }; } const detailScanBudget = createContextBranchScanBudget(); if (limits.historyDetailScanUnits !== void 0) { detailScanBudget.remainingScanUnits = limits.historyDetailScanUnits; } const history = historyMessageItems( entries, { remainingVisits: MAX_HISTORY_BRANCH_ENTRY_VISITS }, limits.firstWindowId, detailScanBudget ); if (input.action === "list") { const selected = searchPage(history, offset, () => true); const page2 = { ...selected, items: selected.items.map((item) => ({ id: item.id, ...item.windowId ? { windowId: item.windowId } : {}, role: item.role, preview: preview(boundedMessageText(item.message)) })) }; return { text: safeJson({ source: "history", action: "list", ...page2 }), details: page2 }; } if (input.action === "read") { let item; for (const candidate of history) { if (candidate.id !== input.id) continue; item = candidate; break; } if (!item) throw new Error(`History item ${JSON.stringify(displayText(input.id ?? ""))} was not found`); const content = serializeMessage(item.message); const page2 = readChunk(content, offset); return { text: safeJson({ source: "history", action: "read", id: item.id, ...item.windowId ? { windowId: item.windowId } : {}, role: item.role, ...page2 }), details: { source: "history", id: item.id, ...page2 } }; } const searchBudget = { remainingUnits: limits.historySearchScanUnits ?? MAX_HISTORY_SEARCH_SCAN_UNITS, exceeded: false }; const indexedText = /* @__PURE__ */ new Map(); const matches = searchPage(history, offset, (item) => { if (isActiveToolCallMessage(item.message, activeToolCallId)) return false; const text = boundedMessageText(item.message, searchBudget); if (searchBudget.exceeded) { throw new Error("context_management_recall_context history search exceeded its scan limit"); } indexedText.set(item.id, text); return text.toLowerCase().includes(searchQuery); }); const page = { ...matches, items: matches.items.map((item) => ({ id: item.id, ...item.windowId ? { windowId: item.windowId } : {}, role: item.role, preview: preview(indexedText.get(item.id) ?? "") })) }; return { text: safeJson({ source: "history", action: "search", ...page }), details: page }; } // src/context-tools.ts var CONTEXT_MANAGEMENT_TOOL_NAMES = [ "context_management_start_new_context", "context_management_get_context_remaining", "context_management_recall_context", "context_management_update_notes" ]; var CONTEXT_MANAGEMENT_TOOL_DESCRIPTIONS = { context_management_start_new_context: "Request a fresh summary-free context window after the current agent run settles. Save important durable information with context_management_update_notes first. This tool changes context state but does not read or write memory itself.", context_management_get_context_remaining: "Inspect the active model context window and estimated remaining tokens without changing context state.", context_management_recall_context: "Read-only access to branch-local conversation history or context notes. List items, read one stable item ID, or search text. Use the returned cursor to continue bounded results.", context_management_update_notes: "Write or append one branch-local context note. This tool changes notes only; use context_management_recall_context with source notes to read them." }; var RECALL_SOURCES = ["history", "notes"]; var RECALL_ACTIONS = ["list", "read", "search"]; var NOTE_ACTIONS = ["write", "append"]; function requireEnabled(runtime, ctx) { if (!runtime.isEnabled(ctx)) { throw new Error("Experimental context management is disabled; enable it in pi-context-management settings first"); } } function throwIfAborted(signal) { if (signal?.aborted) throw new DOMException("Context tool operation aborted", "AbortError"); } function result(value, details) { return { content: [{ type: "text", text: terminalText(JSON.stringify(value, null, 2)) }], details }; } function registerContextManagementTools(pi, runtime) { pi.registerTool({ name: "context_management_start_new_context", label: "Start New Context", description: CONTEXT_MANAGEMENT_TOOL_DESCRIPTIONS.context_management_start_new_context, parameters: Type.Object( { reason: Type.Optional( Type.String({ description: "Short reason for starting a fresh context window", maxLength: 512 }) ) }, { additionalProperties: false } ), executionMode: "sequential", async execute(toolCallId, params, signal, _onUpdate, ctx) { requireEnabled(runtime, ctx); throwIfAborted(signal); const requested = runtime.requestNewContext(ctx, { toolCallId, ...params.reason ? { reason: params.reason } : {} }); throwIfAborted(signal); return { ...result( { status: "scheduled", requestId: requested.requestId, currentWindowId: requested.currentWindowId, message: "The current run will end before rollover. A hidden continuation follows the compaction attempt unless a later successful turn already continued the work." }, { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, ...requested, status: "scheduled" } ), terminate: true }; } }); pi.registerTool({ name: "context_management_get_context_remaining", label: "Get Context Remaining", description: CONTEXT_MANAGEMENT_TOOL_DESCRIPTIONS.context_management_get_context_remaining, parameters: Type.Object({}, { additionalProperties: false }), async execute(_toolCallId, _params, signal, _onUpdate, ctx) { requireEnabled(runtime, ctx); throwIfAborted(signal); const usage = ctx.getContextUsage(); const lineage = loadContextLineage(ctx.sessionManager.getBranch()); if (!usage) { return result({ available: false, ...lineage ? { currentWindowId: lineage.currentWindowId } : {} }); } const remainingTokens = usage.tokens === null ? null : Math.max(0, usage.contextWindow - usage.tokens); return result({ available: usage.tokens !== null, ...lineage ? { currentWindowId: lineage.currentWindowId } : {}, contextWindow: usage.contextWindow, usedTokens: usage.tokens, remainingTokens, remainingPercent: usage.percent === null ? null : Math.max(0, 100 - usage.percent) }); } }); pi.registerTool({ name: "context_management_recall_context", label: "Recall Context", description: CONTEXT_MANAGEMENT_TOOL_DESCRIPTIONS.context_management_recall_context, parameters: Type.Object( { source: StringEnum(RECALL_SOURCES, { description: "Read conversation history or context notes" }), action: StringEnum(RECALL_ACTIONS, { description: "List items, read one ID, or search text" }), id: Type.Optional(Type.String({ description: "Stable history item ID or note name", maxLength: 256 })), query: Type.Optional(Type.String({ description: "Case-insensitive search text", maxLength: 512 })), cursor: Type.Optional(Type.String({ description: "Cursor returned by an earlier result", maxLength: 12 })) }, { additionalProperties: false } ), async execute(toolCallId, params, signal, _onUpdate, ctx) { requireEnabled(runtime, ctx); throwIfAborted(signal); const recalled = recallContext(ctx.sessionManager.getBranch(), params, toolCallId, { firstWindowId: runtime.firstWindowId(ctx) }); throwIfAborted(signal); return { content: [{ type: "text", text: recalled.text }], details: recalled.details }; } }); pi.registerTool({ name: "context_management_update_notes", label: "Update Context Notes", description: CONTEXT_MANAGEMENT_TOOL_DESCRIPTIONS.context_management_update_notes, parameters: Type.Object( { action: StringEnum(NOTE_ACTIONS, { description: "Replace a note or append content exactly as supplied" }), note: Type.String({ description: "Note name", minLength: 1, maxLength: 128 }), content: Type.String({ description: "Note content written or appended exactly as supplied", minLength: 1, maxLength: 16384 }) }, { additionalProperties: false } ), executionMode: "sequential", async execute(_toolCallId, params, signal, _onUpdate, ctx) { requireEnabled(runtime, ctx); throwIfAborted(signal); const updated = createNoteMutation(ctx.sessionManager.getBranch(), params); throwIfAborted(signal); pi.appendEntry(NOTES_ENTRY_TYPE, updated.mutation); return result( { status: "updated", action: updated.mutation.action, note: updated.mutation.note, bytes: Buffer.byteLength(updated.notes.get(updated.mutation.note) ?? "", "utf8") }, { version: updated.mutation.version, action: updated.mutation.action, note: updated.mutation.note } ); } }); } // src/context-management.ts var CONTINUATION_MESSAGE_TYPE = "pi-context-management-continuation"; var ROLLOVER_STATE_ENTRY_TYPE = "pi-context-management-rollover"; var START_NEW_CONTEXT_TOOL_NAME = "context_management_start_new_context"; var EXTENSION_ENTRY_PATH = realpathSync(join(fileURLToPath(new URL(".", import.meta.url)), "index.ts")); function sameNames(left, right) { return left.length === right.length && left.every((name, index) => name === right[index]); } function isOwnedToolSource(tool) { if (!tool || tool.sourceInfo.path.startsWith("<")) return false; try { return realpathSync(tool.sourceInfo.path) === EXTENSION_ENTRY_PATH; } catch { return false; } } function isRecord4(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function isIdentifier2(value) { return typeof value === "string" && value.length >= 8 && value.length <= 128; } function contractMessage(lineage) { return { customType: CONTEXT_CONTRACT_MESSAGE_TYPE, content: contextContract(lineage), display: false, details: { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, currentWindowId: lineage.currentWindowId } }; } function deactivationMessage() { return { customType: CONTEXT_DEACTIVATION_MESSAGE_TYPE, content: contextDeactivation(), display: false, details: { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION } }; } function deactivationAgentMessage() { return { role: "custom", ...deactivationMessage(), timestamp: 0 }; } function latestAssistantStopReason(messages) { for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (message.role === "assistant") return message.stopReason; } return void 0; } function terminalRolloverRequestId(entry) { if (entry.type === "custom" && entry.customType === ROLLOVER_STATE_ENTRY_TYPE) { const data = entry.data; return isRecord4(data) && data.kind === CONTEXT_DETAILS_KIND && data.version === CONTEXT_VERSION && (data.status === "cancelled" || data.status === "suppressed") && isIdentifier2(data.requestId) ? data.requestId : void 0; } const message = entry.type === "message" && entry.message.role === "custom" ? entry.message : void 0; const customMessage = entry.type === "custom_message" ? entry : message; if (customMessage?.customType === CONTINUATION_MESSAGE_TYPE && isRecord4(customMessage.details) && customMessage.details.kind === CONTEXT_DETAILS_KIND && customMessage.details.version === CONTEXT_VERSION && isIdentifier2(customMessage.details.requestId)) { return customMessage.details.requestId; } return void 0; } function restoredRollover(entries, sessionId, generation, budget = createContextBranchScanBudget()) { const terminalRequests = /* @__PURE__ */ new Set(); const completedRequests = /* @__PURE__ */ new Map(); for (let index = entries.length - 1; index >= 0; index -= 1) { visitContextBranchEntry(budget); const entry = entries[index]; const terminalRequestId = terminalRolloverRequestId(entry); if (terminalRequestId) terminalRequests.add(terminalRequestId); if (entry.type === "compaction") { const details2 = parseContextManagementCompaction(entry, budget); if (details2?.requestId) completedRequests.set(details2.requestId, details2.currentWindowId); continue; } if (entry.type !== "message") continue; const message = entry.message; if (message.role !== "toolResult" || message.toolName !== START_NEW_CONTEXT_TOOL_NAME) continue; const details = message.details; if (!isRecord4(details) || details.kind !== CONTEXT_DETAILS_KIND || details.version !== CONTEXT_VERSION || details.status !== "scheduled" || !isIdentifier2(details.requestId) || !isIdentifier2(details.currentWindowId) || !isIdentifier2(details.nextWindowId) || details.reason !== void 0 && (typeof details.reason !== "string" || details.reason.length > 512)) { continue; } if (terminalRequests.has(details.requestId)) return void 0; const completedWindowId = completedRequests.get(details.requestId); return { requestId: details.requestId, nextWindowId: completedWindowId ?? details.nextWindowId, sessionId, generation, status: completedWindowId ? "completed" : "requested", turnStartedAfterRequest: false, successfulTurnAfterRequest: false, ...typeof details.reason === "string" ? { reason: details.reason } : {} }; } return void 0; } function boundedContextMessages(entries) { const budget = createContextBranchScanBudget(); const messages = []; for (const entry of entries) { visitContextBranchEntry(budget); messages.push(...sessionEntryToContextMessages3(entry)); } return messages; } function branchContainsEntry(entries, entryId) { const budget = createContextBranchScanBudget(); for (let index = entries.length - 1; index >= 0; index -= 1) { visitContextBranchEntry(budget); if (entries[index].id === entryId) return true; } return false; } function createContextManager(pi, settingsRuntime) { const states = /* @__PURE__ */ new Map(); let nextGeneration = 0; const isConfigured = () => settingsRuntime.get().settings.enabled; const stateFor = (ctx) => { const state = states.get(ctx.sessionManager); return state && !state.controller.signal.aborted && state.sessionId === ctx.sessionManager.getSessionId() ? state : void 0; }; const inspectToolUnit = () => { const available = new Map(pi.getAllTools().map((tool) => [tool.name, tool])); const ownedNames = new Set( CONTEXT_MANAGEMENT_TOOL_NAMES.filter((name) => isOwnedToolSource(available.get(name))) ); const activeNames = new Set(pi.getActiveTools()); const unavailableNames = CONTEXT_MANAGEMENT_TOOL_NAMES.filter((name) => !ownedNames.has(name)); const inactiveNames = CONTEXT_MANAGEMENT_TOOL_NAMES.filter( (name) => ownedNames.has(name) && !activeNames.has(name) ); return { ownedNames, unavailableNames, inactiveNames, complete: unavailableNames.length === 0 && inactiveNames.length === 0 }; }; const enabledFor = (state) => (isConfigured() || state.removeToolsAtSettlement) && state.toolsAvailable && inspectToolUnit().complete; const isOwned = (state, ctx, request) => stateFor(ctx) === state && (!request || request.sessionId === state.sessionId && request.generation === state.generation && state.pending?.requestId === request.requestId); const removeOwnedTools = (ownedNames) => { const current = pi.getActiveTools(); const next = current.filter((name) => !ownedNames.has(name)); if (!sameNames(current, next)) pi.setActiveTools(next); for (const state of states.values()) state.toolsAvailable = false; }; const warnToolUnitUnavailable = (state, ctx, unavailableNames, inactiveNames) => { if (state.warnedUnavailableTools || !ctx.hasUI) return; state.warnedUnavailableTools = true; const names = [...unavailableNames, ...inactiveNames]; ctx.ui.notify( `Experimental context management could not activate because these tool names are unavailable, inactive, or owned by another extension: ${names.join(", ")}. Pi-native compaction remains active.`, "warning" ); }; const hasDeferredToolRemoval = () => [...states.values()].some((state) => state.removeToolsAtSettlement); const reconcileTools = (state, activate, ctx) => { const inspection = inspectToolUnit(); const available = activate && inspection.unavailableNames.length === 0; const current = pi.getActiveTools(); const next = available ? [...current, ...inspection.inactiveNames] : current.filter((name) => !inspection.ownedNames.has(name)); if (!sameNames(current, next)) pi.setActiveTools(next); for (const candidate of states.values()) { candidate.toolsAvailable = available && !candidate.lineageFailed && (candidate === state || candidate.lineage !== void 0); } if (activate && !available) { warnToolUnitUnavailable(state, ctx, inspection.unavailableNames, inspection.inactiveNames); } return available; }; const deactivateIncompleteToolUnit = (state, ctx, publishCurrentDeactivation = true) => { const inspection = inspectToolUnit(); if (inspection.complete) return false; let branchIsActive = false; for (const candidate of states.values()) { if (candidate.controller.signal.aborted || candidate.sessionId !== candidate.key.getSessionId()) continue; const candidateBranchIsActive = latestContextMode(candidate.key.getBranch()) === "active"; if (candidate === state) branchIsActive = candidateBranchIsActive; candidate.fallbackDeactivationPending = candidateBranchIsActive; candidate.fallbackActivationPending = false; candidate.removeToolsAtSettlement = false; if (candidate.pending?.status === "requested" || candidate.pending?.status === "compacting") { candidate.pending.status = "failed"; candidate.pending.errorMessage = "Experimental context management tool unit became incomplete during rollover."; } } if (branchIsActive && publishCurrentDeactivation) { pi.sendMessage(deactivationMessage(), { triggerTurn: false }); } removeOwnedTools(inspection.ownedNames); warnToolUnitUnavailable(state, ctx, inspection.unavailableNames, inspection.inactiveNames); return branchIsActive; }; const ensureLineage = (state, ctx) => { let lineage; try { const persisted = state.lineage ?? loadContextLineage(ctx.sessionManager.getBranch()); if (persisted) { lineage = persisted; } else { const initial = createInitialContextState(); pi.appendEntry(CONTEXT_STATE_ENTRY_TYPE, initial); lineage = initial; } } catch (error) { state.lineageFailed = true; state.toolsAvailable = false; throw error; } state.lineage = lineage; state.lineageFailed = false; state.toolsAvailable = (isConfigured() || state.removeToolsAtSettlement) && inspectToolUnit().complete; return lineage; }; const warnEnabled = (state, ctx) => { if (state.warned || !ctx.hasUI) return; state.warned = true; ctx.ui.notify( "Experimental context management is active. Context rollover does not create a summary; preserve important information with context_management_update_notes.", "warning" ); }; const applySettings = (ctx) => { const state = stateFor(ctx); if (!state) return; const branch = ctx.sessionManager.getBranch(); const runIsActive = state.agentRunActive || ctx.signal !== void 0; state.agentRunActive = runIsActive; if (!isConfigured()) { const inspection = inspectToolUnit(); for (const candidate of states.values()) { candidate.fallbackActivationPending = false; const candidateRunIsActive = candidate === state ? runIsActive : candidate.agentRunActive; if (candidateRunIsActive && candidate.toolsAvailable && inspection.complete) { candidate.removeToolsAtSettlement = true; candidate.fallbackDeactivationPending = false; continue; } candidate.removeToolsAtSettlement = false; const branchIsActive = !candidate.controller.signal.aborted && candidate.sessionId === candidate.key.getSessionId() && latestContextMode(candidate.key.getBranch()) === "active"; candidate.fallbackDeactivationPending = branchIsActive && (candidate !== state || candidateRunIsActive); if (branchIsActive && candidate === state && !candidateRunIsActive) { pi.sendMessage(deactivationMessage(), { triggerTurn: false }); } } reconcileTools(state, hasDeferredToolRemoval(), ctx); return; } const contractAlreadyActiveOrQueued = state.removeToolsAtSettlement && !state.fallbackDeactivationPending; const deactivationAlreadyPending = state.fallbackDeactivationPending; const toolsWereAvailable = state.toolsAvailable; for (const candidate of states.values()) { candidate.removeToolsAtSettlement = false; candidate.fallbackDeactivationPending = false; candidate.fallbackActivationPending = false; } if (!reconcileTools(state, true, ctx)) { deactivateIncompleteToolUnit(state, ctx); if (runIsActive && (toolsWereAvailable || contractAlreadyActiveOrQueued || deactivationAlreadyPending)) { state.fallbackDeactivationPending = true; } return; } let activeLineage; try { activeLineage = ensureLineage(state, ctx); } catch (error) { state.toolsAvailable = false; const healthySessionRemains = [...states.values()].some( (candidate) => candidate !== state && candidate.toolsAvailable && !candidate.controller.signal.aborted && candidate.sessionId === candidate.key.getSessionId() ); if (!healthySessionRemains) reconcileTools(state, false, ctx); throw error; } for (const candidate of states.values()) { if (candidate !== state && candidate.lineage !== void 0 && !candidate.controller.signal.aborted && candidate.sessionId === candidate.key.getSessionId() && latestContextMode(candidate.key.getBranch()) !== "active") { candidate.fallbackActivationPending = true; } } const messages = boundedContextMessages(branch); if (deactivationAlreadyPending || !contractAlreadyActiveOrQueued && (latestContextMode(branch) !== "active" || !hasContextContract(messages, activeLineage))) { pi.sendMessage(contractMessage(activeLineage), { triggerTurn: false }); } state.fallbackActivationPending = false; warnEnabled(state, ctx); }; const requestNewContext = (ctx, input) => { const state = stateFor(ctx); if (!state) throw new Error("The context session was replaced; retry in the active session"); if (!isConfigured()) { throw new Error("Experimental context management is deactivating; retry after enabling it"); } if (state.pending) throw new Error("A context rollover is already pending"); const activeLineage = ensureLineage(state, ctx); state.pending = { requestId: randomUUID2(), nextWindowId: randomUUID2(), sessionId: state.sessionId, generation: state.generation, status: "requested", turnStartedAfterRequest: false, successfulTurnAfterRequest: false, ...input.reason ? { reason: input.reason } : {} }; return { requestId: state.pending.requestId, currentWindowId: activeLineage.currentWindowId, nextWindowId: state.pending.nextWindowId, ...state.pending.reason ? { reason: state.pending.reason } : {} }; }; registerContextManagementTools(pi, { isEnabled(ctx) { const state = stateFor(ctx); return state ? enabledFor(state) : false; }, firstWindowId(ctx) { return stateFor(ctx)?.lineage?.firstWindowId; }, requestNewContext }); const continueAfterRollover = (state, ctx, request) => { if (!isOwned(state, ctx, request) || request.status !== "completed") return; const current = state.lineage; const contextToolsAvailable = enabledFor(state); state.pending = void 0; if (!current) return; pi.sendMessage( { customType: CONTINUATION_MESSAGE_TYPE, content: [ `Context window ${current.currentWindowId} is now active.`, request.reason ? `Rollover reason: ${request.reason}` : void 0, contextToolsAvailable ? "Continue the interrupted task. Use context_management_recall_context for older details and do not assume an automatic summary exists." : "Continue the interrupted task. The experimental context tools became unavailable; do not assume an automatic summary or local recall is available." ].filter((line) => Boolean(line)).join("\n"), display: false, details: { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, requestId: request.requestId, currentWindowId: current.currentWindowId } }, { triggerTurn: true } ); }; const failRollover = (state, ctx, request, message) => { if (!isOwned(state, ctx, request)) return; state.pending = void 0; const safeMessage = terminalText(message).slice(0, 2e3); if (ctx.hasUI) ctx.ui.notify(safeMessage, "warning"); pi.sendMessage( { customType: CONTINUATION_MESSAGE_TYPE, content: `The requested experimental context rollover failed. Continue with Pi's active fallback context. ${safeMessage}`, display: false, details: { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, requestId: request.requestId, failed: true } }, request.successfulTurnAfterRequest ? { triggerTurn: false } : ctx.isIdle() ? { triggerTurn: true } : { triggerTurn: true, deliverAs: "followUp" } ); }; const finishRolloverWithoutContinuation = (state, ctx, request, status) => { if (!isOwned(state, ctx, request)) return; pi.appendEntry(ROLLOVER_STATE_ENTRY_TYPE, { kind: CONTEXT_DETAILS_KIND, version: CONTEXT_VERSION, requestId: request.requestId, status }); if (isOwned(state, ctx, request)) state.pending = void 0; }; const settle = (state, ctx) => { if (!isOwned(state, ctx)) return; state.agentRunActive = ctx.signal !== void 0; if (state.agentRunActive) return; if (state.removeToolsAtSettlement || state.fallbackDeactivationPending) { state.removeToolsAtSettlement = false; state.fallbackDeactivationPending = false; if (!isConfigured()) { if (latestContextMode(ctx.sessionManager.getBranch()) !== "inactive") { pi.sendMessage(deactivationMessage(), { triggerTurn: false }); } reconcileTools(state, hasDeferredToolRemoval(), ctx); } } const request = state.pending; if (!request || !isOwned(state, ctx, request)) return; if (!isConfigured() && (request.status === "requested" || request.status === "compacting")) { request.status = "failed"; request.errorMessage = "Experimental context management was disabled before rollover completed."; } if (request.status === "completed") { if (request.successfulTurnAfterRequest) { finishRolloverWithoutContinuation(state, ctx, request, "suppressed"); } else { continueAfterRollover(state, ctx, request); } return; } if (request.status === "failed") { failRollover(state, ctx, request, request.errorMessage ?? "Compaction failed."); return; } if (request.status !== "requested") return; request.status = "compacting"; ctx.compact({ onComplete: (result2) => { if (!isOwned(state, ctx, request)) return; if (request.status === "failed") { failRollover( state, ctx, request, request.errorMessage ?? "Compaction completed without the requested context marker." ); return; } const details = parseContextManagementCompaction(result2); if (request.status !== "completed" || !details || details.requestId !== request.requestId) { failRollover(state, ctx, request, "Compaction completed without the requested context marker."); return; } state.lineage = details; if (request.successfulTurnAfterRequest) { finishRolloverWithoutContinuation(state, ctx, request, "suppressed"); } else { continueAfterRollover(state, ctx, request); } }, onError: (error) => failRollover(state, ctx, request, error.message) }); }; return { isEnabled(ctx) { const state = stateFor(ctx); return state ? enabledFor(state) : false; }, startSession(ctx) { const previous = states.get(ctx.sessionManager); previous?.controller.abort(); const generation = ++nextGeneration; const branch = ctx.sessionManager.getBranch(); const sessionId = ctx.sessionManager.getSessionId(); const recoveryBudget = createContextBranchScanBudget(); const state = { key: ctx.sessionManager, generation, sessionId, lineage: loadContextLineage(branch, recoveryBudget), lineageFailed: false, pending: restoredRollover(branch, sessionId, generation, recoveryBudget), warned: false, warnedUnavailableTools: false, warnedProjectionFailure: false, toolsAvailable: false, removeToolsAtSettlement: false, fallbackDeactivationPending: false, fallbackActivationPending: false, agentRunActive: false, controller: new AbortController() }; states.set(ctx.sessionManager, state); applySettings(ctx); if (state.pending && ctx.isIdle()) settle(state, ctx); }, onSessionTree(ctx) { const previous = stateFor(ctx); if (!previous) return; previous.controller.abort(); const generation = ++nextGeneration; const branch = ctx.sessionManager.getBranch(); const recoveryBudget = createContextBranchScanBudget(); const state = { ...previous, generation, lineage: loadContextLineage(branch, recoveryBudget), lineageFailed: false, pending: restoredRollover(branch, previous.sessionId, generation, recoveryBudget), removeToolsAtSettlement: false, fallbackDeactivationPending: false, fallbackActivationPending: false, agentRunActive: false, controller: new AbortController() }; states.set(ctx.sessionManager, state); applySettings(ctx); if (state.pending && ctx.isIdle()) settle(state, ctx); }, applySettings, onInput(ctx) { const state = stateFor(ctx); if (!state || !ctx.isIdle()) return; if (isConfigured() && state.fallbackActivationPending && enabledFor(state)) { const activeLineage = ensureLineage(state, ctx); if (latestContextMode(ctx.sessionManager.getBranch()) !== "active") { pi.sendMessage(contractMessage(activeLineage), { triggerTurn: false }); } state.fallbackActivationPending = false; return; } if (isConfigured() || !state.fallbackDeactivationPending) return; if (latestContextMode(ctx.sessionManager.getBranch()) !== "inactive") { pi.sendMessage(deactivationMessage(), { triggerTurn: false }); } state.fallbackDeactivationPending = false; }, beforeCompact(event, ctx) { const state = stateFor(ctx); if (!state || event.signal.aborted) return void 0; if (state.toolsAvailable && !inspectToolUnit().complete) deactivateIncompleteToolUnit(state, ctx); if (!enabledFor(state)) return void 0; const request = state.pending?.status === "requested" || state.pending?.status === "compacting" ? state.pending : void 0; try { const activeLineage = ensureLineage(state, ctx); const fingerprintBudget = createFingerprintBudget(); const details = createContextManagementDetails( { lineage: activeLineage, ...compactionRetainedContext(event, fingerprintBudget), reason: event.reason, ...request ? { requestId: request.requestId, windowId: request.nextWindowId } : {} }, fingerprintBudget ); if (request) request.status = "compacting"; return { compaction: { summary: contextContract(details), firstKeptEntryId: event.preparation.firstKeptEntryId, tokensBefore: event.preparation.tokensBefore, details } }; } catch (error) { const message = terminalText(error instanceof Error ? error.message : String(error)).slice(0, 2e3); if (request && isOwned(state, ctx, request)) { request.status = "failed"; request.errorMessage = message; } if (ctx.hasUI) ctx.ui.notify(`Experimental context compaction was cancelled. ${message}`, "warning"); return { cancel: true }; } }, projectContext(messages, ctx) { const state = stateFor(ctx); if (!state) return void 0; if (state.toolsAvailable && !inspectToolUnit().complete) { deactivateIncompleteToolUnit(state, ctx, false); } if (!enabledFor(state)) { if (state.fallbackDeactivationPending) { if (latestContextMode(ctx.sessionManager.getBranch()) !== "inactive") { pi.sendMessage(deactivationMessage(), { triggerTurn: false }); state.fallbackDeactivationPending = false; return [...messages, deactivationAgentMessage()]; } state.fallbackDeactivationPending = false; } return void 0; } const branch = ctx.sessionManager.getBranch(); try { const activationPending = state.fallbackActivationPending && latestContextMode(branch) !== "active"; const activeLineage = state.lineage ?? loadContextLineage(branch); if (!activeLineage) return void 0; const compaction = activeContextManagementCompaction(branch); if (compaction) { const projected = projectContextManagementContext(messages, compaction.entry, compaction.details); if (!projected) return void 0; state.fallbackActivationPending = false; if (activationPending) { pi.sendMessage(contractMessage(compaction.details), { triggerTurn: false }); return [...projected, createContextContractMessage(compaction.details)]; } return reconcileContextContract(projected, compaction.details); } state.fallbackActivationPending = false; if (activationPending) { pi.sendMessage(contractMessage(activeLineage), { triggerTurn: false }); return [...messages, createContextContractMessage(activeLineage)]; } return hasContextContract(messages, activeLineage) ? void 0 : reconcileContextContract(messages, activeLineage); } catch (error) { if (!state.warnedProjectionFailure && ctx.hasUI) { state.warnedProjectionFailure = true; ctx.ui.notify( `Experimental context projection kept Pi's persisted context unchanged. ${terminalText(error instanceof Error ? error.message : String(error))}`, "warning" ); } return void 0; } }, onCompact(event, ctx) { const state = stateFor(ctx); if (!state || !branchContainsEntry(ctx.sessionManager.getBranch(), event.compactionEntry.id)) { return; } const details = parseContextManagementCompaction(event.compactionEntry); const request = state.pending; if (request?.status !== "compacting" || !isOwned(state, ctx, request)) { if (details) state.lineage = details; return; } if (!details || details.requestId !== request.requestId) { request.status = "failed"; request.errorMessage = "Compaction completed without the requested context marker."; return; } state.lineage = details; request.status = "completed"; }, onCompactFailed(event, ctx) { const state = stateFor(ctx); const request = state?.pending; if (!state || request?.status !== "compacting" || !isOwned(state, ctx, request)) return; if (event.aborted) { finishRolloverWithoutContinuation(state, ctx, request, "cancelled"); return; } request.status = "failed"; request.errorMessage = event.errorMessage ?? "Compaction failed."; }, onAgentStart(ctx) { const state = stateFor(ctx); if (state) state.agentRunActive = true; }, onAgentEnd(event, ctx) { const state = stateFor(ctx); const request = state?.pending; if (!state || !request || !isOwned(state, ctx, request)) return; const stopReason = latestAssistantStopReason(event.messages); if (ctx.signal?.aborted || stopReason === "aborted") { finishRolloverWithoutContinuation(state, ctx, request, "cancelled"); return; } if (request.turnStartedAfterRequest && (stopReason === "stop" || stopReason === "toolUse")) { request.successfulTurnAfterRequest = true; } }, onTurnStart(ctx) { const state = stateFor(ctx); const request = state?.pending; if (state && request && isOwned(state, ctx, request)) request.turnStartedAfterRequest = true; }, onAgentSettled(ctx) { const state = stateFor(ctx); if (state) settle(state, ctx); }, shutdown(ctx) { const state = states.get(ctx.sessionManager); if (!state) return; state.controller.abort(); states.delete(ctx.sessionManager); if (!isConfigured()) reconcileTools(state, hasDeferredToolRemoval(), ctx); } }; } // src/settings.ts import { randomUUID as randomUUID3 } from "node:crypto"; import { constants } from "node:fs"; import { mkdir, open, rename, rm, writeFile } from "node:fs/promises"; import { basename, dirname, join as join2 } from "node:path"; import { getAgentDir } from "@earendil-works/pi-coding-agent"; var CONTEXT_MANAGEMENT_SETTINGS_FILE = "pi-context-management.json"; var MAX_SETTINGS_BYTES = 64 * 1024; var DEFAULT_CONTEXT_MANAGEMENT_SETTINGS = Object.freeze({ enabled: false }); function isRecord5(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function normalizeContextManagementSettings(value) { if (!isRecord5(value)) return void 0; if (Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean") return void 0; return { enabled: typeof value.enabled === "boolean" ? value.enabled : DEFAULT_CONTEXT_MANAGEMENT_SETTINGS.enabled }; } function contextManagementSettingsPath() { return join2(getAgentDir(), CONTEXT_MANAGEMENT_SETTINGS_FILE); } function aborted(signal) { if (signal?.aborted) throw new DOMException("Settings operation aborted", "AbortError"); } async function loadContextManagementSettings(path = contextManagementSettingsPath(), signal) { aborted(signal); try { const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW); let text; try { const stats = await handle.stat(); aborted(signal); if (!stats.isFile()) throw new Error("settings path is not a regular file"); if (stats.size > MAX_SETTINGS_BYTES) throw new Error("settings file exceeds 64 KiB"); text = await handle.readFile("utf8"); } finally { await handle.close(); } aborted(signal); const document = JSON.parse(text); const settings = normalizeContextManagementSettings(document); if (!settings || !isRecord5(document)) throw new Error("invalid settings shape"); return { kind: "loaded", path, settings, document }; } catch (error) { if (signal?.aborted) throw error; if (isNodeError(error) && error.code === "ENOENT") { return { kind: "missing", path, settings: { ...DEFAULT_CONTEXT_MANAGEMENT_SETTINGS }, document: {} }; } return { kind: "invalid", path, settings: { ...DEFAULT_CONTEXT_MANAGEMENT_SETTINGS }, issue: isNodeError(error) && error.code === "ELOOP" ? "symbolic links are not accepted" : error instanceof Error ? error.message : String(error) }; } } async function savePatch(path, patch, signal) { const latest = await loadContextManagementSettings(path, signal); if (latest.kind === "invalid") { throw new Error("Cannot overwrite an invalid pi-context-management.json; repair it and reload first"); } const document = { ...latest.document, ...patch }; const settings = normalizeContextManagementSettings(document); if (!settings) throw new Error("Refusing to save invalid context management settings"); const temporaryPath = join2(dirname(path), `.${basename(path)}.${randomUUID3()}.tmp`); await mkdir(dirname(path), { recursive: true }); aborted(signal); try { await writeFile(temporaryPath, `${JSON.stringify(document, null, 2)} `, { encoding: "utf8", flag: "wx", mode: 384 }); aborted(signal); const current = await loadContextManagementSettings(path, signal); if (current.kind === "invalid" || current.kind !== latest.kind || JSON.stringify(current.document) !== JSON.stringify(latest.document)) { throw new Error("pi-context-management.json changed while saving; reopen settings and retry"); } await rename(temporaryPath, path); } finally { await rm(temporaryPath, { force: true }).catch(() => void 0); } return { kind: "loaded", path, settings, document }; } function createContextManagementSettingsRuntime(path = contextManagementSettingsPath()) { let state = { kind: "missing", path, settings: { ...DEFAULT_CONTEXT_MANAGEMENT_SETTINGS }, document: {} }; let queue = Promise.resolve(); const enqueue = (operation) => { const result2 = queue.then(operation, operation); queue = result2.then( () => void 0, () => void 0 ); return result2; }; return { get: () => structuredClone(state), reload: (signal) => enqueue(async () => { state = await loadContextManagementSettings(path, signal); return structuredClone(state); }), update: (patch, signal) => enqueue(async () => { state = await savePatch(path, patch, signal); return structuredClone(state); }), flush: () => queue }; } function isNodeError(error) { return error instanceof Error && "code" in error; } // src/pi-context-management.ts function createContextManagementExtension(options = {}) { return (pi) => { const settingsRuntime = options.settingsRuntime ?? createContextManagementSettingsRuntime(); const manager = createContextManager(pi, settingsRuntime); const sessions = /* @__PURE__ */ new Map(); let nextGeneration = 0; pi.registerCommand("context-management", { description: "Configure experimental summary-free context management", handler: async (args, ctx) => { if (args.trim()) throw new Error("Usage: /context-management"); const owner = sessions.get(ctx.sessionManager); const controller = owner?.controller ?? new AbortController(); const { showContextManagementMenu } = await import("./chunks/settings-menu-WBYIMN23.ts"); if (owner && (sessions.get(ctx.sessionManager) !== owner || controller.signal.aborted)) return; await showContextManagementMenu(settingsRuntime, ctx, { signal: controller.signal, isCurrent: () => owner ? sessions.get(ctx.sessionManager) === owner && !controller.signal.aborted : !controller.signal.aborted, isActive: () => manager.isEnabled(ctx), onSettingsChanged: () => manager.applySettings(ctx) }); } }); pi.on("session_start", async (_event, ctx) => { sessions.get(ctx.sessionManager)?.controller.abort(); const owner = { controller: new AbortController(), generation: ++nextGeneration }; sessions.set(ctx.sessionManager, owner); const sessionId = ctx.sessionManager.getSessionId(); let state; try { state = await settingsRuntime.reload(owner.controller.signal); } catch (error) { if (owner.controller.signal.aborted || sessions.get(ctx.sessionManager) !== owner) return; state = settingsRuntime.get(); if (ctx.hasUI) { ctx.ui.notify( `Could not load pi-context-management.json; using defaults. ${terminalText(error instanceof Error ? error.message : String(error))}`, "warning" ); } } if (owner.controller.signal.aborted || sessions.get(ctx.sessionManager) !== owner || ctx.sessionManager.getSessionId() !== sessionId) { return; } if (ctx.hasUI && state.kind === "invalid") { ctx.ui.notify( `Invalid pi-context-management.json; using defaults without overwriting it. ${terminalText(state.issue ?? "unknown validation error")}`, "warning" ); } manager.startSession(ctx); }); pi.on("input", (_event, ctx) => manager.onInput(ctx)); pi.on("session_before_compact", (event, ctx) => manager.beforeCompact(event, ctx)); pi.on("context", (event, ctx) => { const messages = manager.projectContext(event.messages, ctx); return messages ? { messages } : void 0; }); pi.on("session_tree", (_event, ctx) => manager.onSessionTree(ctx)); pi.on("session_compact", (event, ctx) => manager.onCompact(event, ctx)); pi.on("session_compact_failed", (event, ctx) => manager.onCompactFailed(event, ctx)); pi.on("agent_start", (_event, ctx) => manager.onAgentStart(ctx)); pi.on("agent_end", (event, ctx) => manager.onAgentEnd(event, ctx)); pi.on("turn_start", (_event, ctx) => manager.onTurnStart(ctx)); pi.on("agent_settled", (_event, ctx) => manager.onAgentSettled(ctx)); pi.on("session_shutdown", async (_event, ctx) => { const owner = sessions.get(ctx.sessionManager); owner?.controller.abort(); if (sessions.get(ctx.sessionManager) === owner) sessions.delete(ctx.sessionManager); manager.shutdown(ctx); await settingsRuntime.flush(); }); }; } var pi_context_management_default = createContextManagementExtension(); export { pi_context_management_default as default }; //# sourceMappingURL=index.ts.map