@jetcrab/pi-desk-ctx 0.0.0-stage → 1.0.0

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.
@@ -0,0 +1,198 @@
1
+ import { getSkillBaseDirs, isSkillContentRead } from './context-skill-path.js';
2
+ export const COMPRESSED_TOOL_CONTEXT_TAG = 'compressed_tool_context';
3
+ export const COMPRESSED_TOOL_CONTEXT_CUSTOM_TYPE = 'context-ignore-compressed-tools';
4
+ function compressedToolStatus(result) {
5
+ if (result === undefined)
6
+ return 'missing';
7
+ return result.isError ? 'error' : 'ok';
8
+ }
9
+ export function createCompressedToolRecord(ref, toolCall, result) {
10
+ return {
11
+ ref,
12
+ name: toolCall.name,
13
+ arguments: toolCall.arguments,
14
+ status: compressedToolStatus(result)
15
+ };
16
+ }
17
+ function serializeCompressedJson(value) {
18
+ return JSON.stringify(value)
19
+ .replace(/</g, '\\u003c')
20
+ .replace(/>/g, '\\u003e')
21
+ .replace(/\u2028/g, '\\u2028')
22
+ .replace(/\u2029/g, '\\u2029');
23
+ }
24
+ export function createToolSummaryText(records) {
25
+ return `<${COMPRESSED_TOOL_CONTEXT_TAG}>${serializeCompressedJson(records)}</${COMPRESSED_TOOL_CONTEXT_TAG}>`;
26
+ }
27
+ function findAssistantCutoffIndex(messages, cutoffTimestamp) {
28
+ const cutoffIndex = messages.findIndex((message) => message.role === 'assistant' && message.timestamp >= cutoffTimestamp);
29
+ return cutoffIndex >= 0 ? cutoffIndex : messages.length;
30
+ }
31
+ function findScopedUserIndex(messages, userTimestamp) {
32
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
33
+ const message = messages[index];
34
+ if (message?.role === 'user' && message.timestamp === userTimestamp)
35
+ return index;
36
+ }
37
+ return undefined;
38
+ }
39
+ /** Returns message indexes whose process details are replaced by the current ignore boundaries. */
40
+ export function getIgnoredMessageIndexes(messages, state, ignoredMessages) {
41
+ const ignoredIndexes = new Set();
42
+ if (!state.enabled)
43
+ return ignoredIndexes;
44
+ if (ignoredMessages !== undefined) {
45
+ for (const [index, message] of messages.entries()) {
46
+ if (ignoredMessages.has(message))
47
+ ignoredIndexes.add(index);
48
+ }
49
+ return ignoredIndexes;
50
+ }
51
+ if (state.cutoffTimestamp !== undefined) {
52
+ for (const [index, message] of messages.entries()) {
53
+ if (message.timestamp < state.cutoffTimestamp)
54
+ ignoredIndexes.add(index);
55
+ }
56
+ }
57
+ for (const internalTurnCutoff of state.internalTurnCutoffs ?? []) {
58
+ const cutoffIndex = findAssistantCutoffIndex(messages, internalTurnCutoff.cutoffTimestamp);
59
+ const startIndex = internalTurnCutoff.userTimestamp === undefined
60
+ ? 0
61
+ : findScopedUserIndex(messages, internalTurnCutoff.userTimestamp);
62
+ if (startIndex === undefined)
63
+ continue;
64
+ for (let index = startIndex; index < cutoffIndex; index += 1) {
65
+ ignoredIndexes.add(index);
66
+ }
67
+ }
68
+ return ignoredIndexes;
69
+ }
70
+ /** Captures full-session ignore decisions for strict projection onto compaction slices. */
71
+ export function getIgnoredMessageReferences(messages, state) {
72
+ const ignoredMessages = new WeakSet();
73
+ for (const index of getIgnoredMessageIndexes(messages, state)) {
74
+ ignoredMessages.add(messages[index]);
75
+ }
76
+ return ignoredMessages;
77
+ }
78
+ export function transformContextMessages(messages, state, options = {}) {
79
+ const ignoredIndexes = getIgnoredMessageIndexes(messages, state, options.ignoredMessages);
80
+ if (ignoredIndexes.size === 0) {
81
+ return {
82
+ messages,
83
+ toolReferences: new Map()
84
+ };
85
+ }
86
+ const skillBaseDirs = getSkillBaseDirs(messages, options);
87
+ const compressedToolCallIds = new Set();
88
+ const toolResults = new Map();
89
+ for (const [messageIndex, message] of messages.entries()) {
90
+ if (message.role === 'toolResult') {
91
+ toolResults.set(message.toolCallId, message);
92
+ continue;
93
+ }
94
+ if (message.role !== 'assistant' || !ignoredIndexes.has(messageIndex))
95
+ continue;
96
+ for (const block of message.content) {
97
+ if (block.type === 'toolCall' && !isSkillContentRead(block, skillBaseDirs, options)) {
98
+ compressedToolCallIds.add(block.id);
99
+ }
100
+ }
101
+ }
102
+ let latestAssistantIndex = -1;
103
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
104
+ if (messages[index]?.role !== 'assistant')
105
+ continue;
106
+ latestAssistantIndex = index;
107
+ break;
108
+ }
109
+ const toolReferences = new Map();
110
+ const transformed = [];
111
+ const pendingPreservedToolResults = new Set();
112
+ let nextToolReference = 1;
113
+ let pendingTools = [];
114
+ const flushPendingTools = () => {
115
+ if (pendingTools.length === 0)
116
+ return;
117
+ const records = pendingTools.map((item) => item.record);
118
+ const content = createToolSummaryText(records);
119
+ transformed.push({
120
+ role: 'custom',
121
+ customType: COMPRESSED_TOOL_CONTEXT_CUSTOM_TYPE,
122
+ content,
123
+ display: false,
124
+ timestamp: Math.max(...pendingTools.map((item) => item.timestamp))
125
+ });
126
+ pendingTools = [];
127
+ };
128
+ for (const [messageIndex, message] of messages.entries()) {
129
+ if (message.role === 'toolResult') {
130
+ if (compressedToolCallIds.has(message.toolCallId)) {
131
+ continue;
132
+ }
133
+ if (pendingPreservedToolResults.has(message.toolCallId)) {
134
+ transformed.push(message);
135
+ pendingPreservedToolResults.delete(message.toolCallId);
136
+ if (pendingPreservedToolResults.size === 0)
137
+ flushPendingTools();
138
+ continue;
139
+ }
140
+ flushPendingTools();
141
+ transformed.push(message);
142
+ continue;
143
+ }
144
+ if (!ignoredIndexes.has(messageIndex) || message.role !== 'assistant') {
145
+ flushPendingTools();
146
+ transformed.push(message);
147
+ continue;
148
+ }
149
+ if (messageIndex === latestAssistantIndex &&
150
+ message.content.length > 0 &&
151
+ message.content.every((block) => block.type === 'thinking')) {
152
+ flushPendingTools();
153
+ transformed.push(message);
154
+ continue;
155
+ }
156
+ const retainedContent = [];
157
+ const compressedBlocks = [];
158
+ const preservedToolCallIds = [];
159
+ for (const block of message.content) {
160
+ if (block.type === 'thinking') {
161
+ continue;
162
+ }
163
+ if (block.type === 'text') {
164
+ retainedContent.push(block);
165
+ continue;
166
+ }
167
+ if (isSkillContentRead(block, skillBaseDirs, options)) {
168
+ retainedContent.push(block);
169
+ preservedToolCallIds.push(block.id);
170
+ continue;
171
+ }
172
+ compressedBlocks.push(block);
173
+ }
174
+ if (retainedContent.length > 0) {
175
+ flushPendingTools();
176
+ transformed.push({ ...message, content: retainedContent });
177
+ for (const toolCallId of preservedToolCallIds) {
178
+ pendingPreservedToolResults.add(toolCallId);
179
+ }
180
+ }
181
+ for (const block of compressedBlocks) {
182
+ const result = toolResults.get(block.id);
183
+ const ref = nextToolReference;
184
+ nextToolReference += 1;
185
+ toolReferences.set(ref, block.id);
186
+ const record = createCompressedToolRecord(ref, block, result);
187
+ pendingTools.push({
188
+ record,
189
+ timestamp: Math.max(message.timestamp, result?.timestamp ?? message.timestamp)
190
+ });
191
+ }
192
+ }
193
+ flushPendingTools();
194
+ return {
195
+ messages: transformed,
196
+ toolReferences
197
+ };
198
+ }
@@ -0,0 +1,7 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import { type ContextIgnoreSettings } from './context-settings.js';
3
+ export interface ContextIgnoreRuntimeOptions {
4
+ loadSettings?: () => Promise<ContextIgnoreSettings>;
5
+ }
6
+ export declare function registerContextIgnore(pi: ExtensionAPI, options?: ContextIgnoreRuntimeOptions): void;
7
+ export default function contextIgnore(pi: ExtensionAPI): void;