pi-context 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.js +443 -0
- package/package.json +2 -7
- package/skills/context-management/SKILL.md +181 -111
- package/src/index.ts +83 -72
package/README.md
CHANGED
|
@@ -4,6 +4,9 @@ A Git-like context management tool that allows AI agents to proactively manage t
|
|
|
4
4
|
|
|
5
5
|
Inspired by kimi-cli d-mail, implementing lossless time travel on the Pi session tree.
|
|
6
6
|
|
|
7
|
+
For the design philosophy, see the [blog post](https://blog.xlab.app/p/51d26495/)
|
|
8
|
+
([中文版本](https://blog.xlab.app/p/6a966aeb/)).
|
|
9
|
+
|
|
7
10
|
## Installation
|
|
8
11
|
|
|
9
12
|
```bash
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { DynamicBorder, } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { Container, Text, Spacer } from "@mariozechner/pi-tui";
|
|
4
|
+
const ContextLogParams = Type.Object({
|
|
5
|
+
limit: Type.Optional(Type.Number({ description: "History limit for visible entries (default: 50)." })),
|
|
6
|
+
verbose: Type.Optional(Type.Boolean({ description: "If true, show ALL messages. If false (default), collapses intermediate AI steps and only shows 'milestones': User messages, Tags, Branch Points, and Summaries." })),
|
|
7
|
+
});
|
|
8
|
+
const ContextCheckoutParams = Type.Object({
|
|
9
|
+
target: Type.String({ description: "Where to jump/squash to. Can be a tag name (e.g., 'task-start'), a commit ID, or 'root'. This is the base for your new branch." }),
|
|
10
|
+
message: Type.String({ description: "The 'Carryover Message' for the new branch. A summary of your *current* progress/lessons that you want to bring with you to the new state. This ensures you don't lose key information when switching contexts. Good summary message: '[Status] + [Reason] + [Important Changes] + [Carryover Data]'" }),
|
|
11
|
+
backupTag: Type.Optional(Type.String({ description: "Optional tag name to apply to the CURRENT state before checking out. Use this to create an automatic backup of the history you are about to leave/squash." })),
|
|
12
|
+
});
|
|
13
|
+
const ContextTagParams = Type.Object({
|
|
14
|
+
name: Type.String({ description: "The tag/milestone name. Use meaningful names." }),
|
|
15
|
+
target: Type.Optional(Type.String({ description: "The commit ID to tag. Defaults to HEAD (current state)." })),
|
|
16
|
+
});
|
|
17
|
+
const isInternal = (name) => ["context_tag", "context_log", "context_checkout"].includes(name);
|
|
18
|
+
const resolveTargetId = (sm, target) => {
|
|
19
|
+
if (target.toLowerCase() === "root") {
|
|
20
|
+
const tree = sm.getTree();
|
|
21
|
+
return tree.length > 0 ? tree[0].entry.id : target;
|
|
22
|
+
}
|
|
23
|
+
if (/^[0-9a-f]{8,}$/i.test(target))
|
|
24
|
+
return target;
|
|
25
|
+
const find = (nodes) => {
|
|
26
|
+
for (const n of nodes) {
|
|
27
|
+
if (sm.getLabel(n.entry.id) === target)
|
|
28
|
+
return n.entry.id;
|
|
29
|
+
const r = find(n.children);
|
|
30
|
+
if (r)
|
|
31
|
+
return r;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
// sm.getTree() returns the SDK's SessionTreeNode[], which is structurally compatible
|
|
36
|
+
return find(sm.getTree()) || target;
|
|
37
|
+
};
|
|
38
|
+
const formatTokens = (n) => {
|
|
39
|
+
if (n >= 1_000_000)
|
|
40
|
+
return (n / 1_000_000).toFixed(1) + "M";
|
|
41
|
+
if (n >= 1_000)
|
|
42
|
+
return Math.round(n / 1_000) + "k";
|
|
43
|
+
return n.toString();
|
|
44
|
+
};
|
|
45
|
+
export default function (pi) {
|
|
46
|
+
pi.registerTool({
|
|
47
|
+
name: "context_tag",
|
|
48
|
+
label: "Context Tag",
|
|
49
|
+
description: "Creates a 'Save Point' (Bookmark) in the history. Use this before trying risky changes or when a feature is stable. 'Untagged progress is risky'.",
|
|
50
|
+
parameters: ContextTagParams,
|
|
51
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
52
|
+
const sm = ctx.sessionManager;
|
|
53
|
+
let id = params.target ? resolveTargetId(sm, params.target) : undefined;
|
|
54
|
+
if (!id) {
|
|
55
|
+
// Auto-resolve: Find the last "interesting" node to tag.
|
|
56
|
+
// We skip ToolResults (which look ugly tagged) and internal-only Assistant messages (which look empty).
|
|
57
|
+
const branch = sm.getBranch();
|
|
58
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
59
|
+
const entry = branch[i];
|
|
60
|
+
// 1. Check ToolResults
|
|
61
|
+
if (entry.type === 'message' && entry.message.role === 'toolResult') {
|
|
62
|
+
const tr = entry.message;
|
|
63
|
+
if (isInternal(tr.toolName))
|
|
64
|
+
continue;
|
|
65
|
+
// Public tool result is a valid target
|
|
66
|
+
id = entry.id;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
// 2. Check Assistant messages for visibility
|
|
70
|
+
if (entry.type === 'message' && entry.message.role === 'assistant') {
|
|
71
|
+
const m = entry.message;
|
|
72
|
+
let hasText = false;
|
|
73
|
+
let hasPublicTools = false;
|
|
74
|
+
const content = m.content;
|
|
75
|
+
if (typeof content === 'string') {
|
|
76
|
+
hasText = content.trim().length > 0;
|
|
77
|
+
}
|
|
78
|
+
else if (Array.isArray(content)) {
|
|
79
|
+
hasText = content.some((c) => c.type === 'text' && typeof c.text === 'string' && c.text.trim().length > 0);
|
|
80
|
+
const toolCalls = content.filter((c) => c.type === 'toolCall');
|
|
81
|
+
hasPublicTools = toolCalls.some((tc) => !isInternal(tc.name));
|
|
82
|
+
}
|
|
83
|
+
// If it has visible text or public tools, it's a valid target
|
|
84
|
+
if (hasText || hasPublicTools) {
|
|
85
|
+
id = entry.id;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
// Otherwise (only internal tools like context_tag), skip it
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// 3. User messages, Summaries, etc. are always valid targets
|
|
92
|
+
id = entry.id;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
// Fallback to leaf if search failed
|
|
96
|
+
if (!id)
|
|
97
|
+
id = sm.getLeafId() ?? "";
|
|
98
|
+
}
|
|
99
|
+
pi.setLabel(id, params.name);
|
|
100
|
+
return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id.slice(0, 7)}` }], details: {} };
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
pi.registerTool({
|
|
104
|
+
name: "context_log",
|
|
105
|
+
label: "Context Log",
|
|
106
|
+
description: "Show the entire history structure (status, message, tags, milestones). Analogous to 'git log --graph --oneline --decorate'",
|
|
107
|
+
parameters: ContextLogParams,
|
|
108
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
109
|
+
const sm = ctx.sessionManager;
|
|
110
|
+
const branch = sm.getBranch();
|
|
111
|
+
const currentLeafId = sm.getLeafId();
|
|
112
|
+
const verbose = params.verbose ?? false;
|
|
113
|
+
const limit = params.limit ?? 50;
|
|
114
|
+
const backboneIds = new Set(branch.map((e) => e.id));
|
|
115
|
+
const sequence = [];
|
|
116
|
+
branch.forEach((entry) => {
|
|
117
|
+
sequence.push(entry);
|
|
118
|
+
// Preserve side-summary logic: Show branch summaries/compactions that are off-path
|
|
119
|
+
const children = sm.getChildren(entry.id);
|
|
120
|
+
children.forEach((child) => {
|
|
121
|
+
if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.has(child.id)) {
|
|
122
|
+
sequence.push(child);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
const getMsgContent = (entry) => {
|
|
127
|
+
if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
128
|
+
const e = entry;
|
|
129
|
+
return e.summary || "[No summary provided]";
|
|
130
|
+
}
|
|
131
|
+
if (entry.type === "label") {
|
|
132
|
+
return `tag: ${entry.label}`;
|
|
133
|
+
}
|
|
134
|
+
if (entry.type === "message") {
|
|
135
|
+
const msg = entry.message;
|
|
136
|
+
if (msg.role === "toolResult") {
|
|
137
|
+
const tr = msg;
|
|
138
|
+
if (!verbose && isInternal(tr.toolName))
|
|
139
|
+
return "";
|
|
140
|
+
const extractText = (content) => {
|
|
141
|
+
return content
|
|
142
|
+
.map((p) => (p.type === "text" ? p.text : ""))
|
|
143
|
+
.join(" ")
|
|
144
|
+
.trim();
|
|
145
|
+
};
|
|
146
|
+
let resText = extractText(tr.content);
|
|
147
|
+
const details = tr.details;
|
|
148
|
+
if ((tr.toolName === "read" || tr.toolName === "edit") && details && "path" in details && typeof details.path === "string") {
|
|
149
|
+
resText = `${details.path}: ${resText}`;
|
|
150
|
+
}
|
|
151
|
+
return `(${tr.toolName}) ${resText}`;
|
|
152
|
+
}
|
|
153
|
+
if (msg.role === "bashExecution") {
|
|
154
|
+
return `[Bash] ${msg.command}`;
|
|
155
|
+
}
|
|
156
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
157
|
+
let text = "";
|
|
158
|
+
if (typeof msg.content === "string") {
|
|
159
|
+
text = msg.content;
|
|
160
|
+
}
|
|
161
|
+
else if (Array.isArray(msg.content)) {
|
|
162
|
+
text = msg.content
|
|
163
|
+
.map((p) => {
|
|
164
|
+
if (typeof p === "object" && p !== null && "text" in p)
|
|
165
|
+
return p.text;
|
|
166
|
+
return "";
|
|
167
|
+
})
|
|
168
|
+
.join(" ")
|
|
169
|
+
.trim();
|
|
170
|
+
}
|
|
171
|
+
let toolCallsText = "";
|
|
172
|
+
if (msg.role === "assistant") {
|
|
173
|
+
const toolCalls = msg.content.filter((c) => c.type === "toolCall");
|
|
174
|
+
toolCallsText = toolCalls
|
|
175
|
+
.filter((tc) => verbose || !isInternal(tc.name))
|
|
176
|
+
.map((tc) => `call: ${tc.name}(${JSON.stringify(tc.arguments)})`)
|
|
177
|
+
.join("; ");
|
|
178
|
+
}
|
|
179
|
+
return [text, toolCallsText].filter(Boolean).join(" ");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return "";
|
|
183
|
+
};
|
|
184
|
+
const isInteresting = (entry) => {
|
|
185
|
+
// 1. HEAD and Root
|
|
186
|
+
if (entry.id === currentLeafId)
|
|
187
|
+
return true;
|
|
188
|
+
if (branch.length > 0 && entry.id === branch[0].id)
|
|
189
|
+
return true;
|
|
190
|
+
// 2. Explicit Tags (Labels) - Only show the TAGGED node, not the label node itself
|
|
191
|
+
if (sm.getLabel(entry.id))
|
|
192
|
+
return true;
|
|
193
|
+
if (entry.type === 'label')
|
|
194
|
+
return false; // Hide label nodes, they are redundant
|
|
195
|
+
// 3. Structural Milestones (Summaries)
|
|
196
|
+
if (entry.type === 'branch_summary' || entry.type === 'compaction')
|
|
197
|
+
return true;
|
|
198
|
+
// 4. Branch Points (Forks)
|
|
199
|
+
if (sm.getChildren(entry.id).length > 1)
|
|
200
|
+
return true;
|
|
201
|
+
// 5. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
|
|
202
|
+
if (entry.type === 'message' && entry.message.role === 'user')
|
|
203
|
+
return true;
|
|
204
|
+
return false;
|
|
205
|
+
};
|
|
206
|
+
const visibleSequenceIds = new Set();
|
|
207
|
+
sequence.forEach(e => {
|
|
208
|
+
if (verbose || isInteresting(e)) {
|
|
209
|
+
visibleSequenceIds.add(e.id);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
let visibleEntries = sequence.filter(e => visibleSequenceIds.has(e.id));
|
|
213
|
+
if (visibleEntries.length > limit) {
|
|
214
|
+
const allowedIds = new Set(visibleEntries.slice(-limit).map(e => e.id));
|
|
215
|
+
visibleSequenceIds.clear();
|
|
216
|
+
allowedIds.forEach(id => visibleSequenceIds.add(id));
|
|
217
|
+
}
|
|
218
|
+
const lines = [];
|
|
219
|
+
let hiddenCount = 0;
|
|
220
|
+
sequence.forEach((entry) => {
|
|
221
|
+
if (!visibleSequenceIds.has(entry.id)) {
|
|
222
|
+
hiddenCount++;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (hiddenCount > 0) {
|
|
226
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
227
|
+
hiddenCount = 0;
|
|
228
|
+
}
|
|
229
|
+
const isHead = entry.id === currentLeafId;
|
|
230
|
+
const label = sm.getLabel(entry.id);
|
|
231
|
+
const content = getMsgContent(entry).replace(/\s+/g, " ");
|
|
232
|
+
let role = entry.type.toUpperCase();
|
|
233
|
+
if (entry.type === "message") {
|
|
234
|
+
const m = entry.message;
|
|
235
|
+
role =
|
|
236
|
+
m.role === "assistant"
|
|
237
|
+
? "AI"
|
|
238
|
+
: m.role === "user"
|
|
239
|
+
? "USER"
|
|
240
|
+
: m.role === "bashExecution"
|
|
241
|
+
? "BASH"
|
|
242
|
+
: "TOOL";
|
|
243
|
+
}
|
|
244
|
+
else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
245
|
+
role = "SUMMARY";
|
|
246
|
+
}
|
|
247
|
+
const id = entry.id;
|
|
248
|
+
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
249
|
+
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
|
|
250
|
+
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
251
|
+
const marker = isHead ? "*" : (role === "USER" ? "•" : "|");
|
|
252
|
+
lines.push(`${marker} ${id}${meta ? ` (${meta})` : ""} [${role}] ${body}`);
|
|
253
|
+
});
|
|
254
|
+
if (hiddenCount > 0) {
|
|
255
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
256
|
+
}
|
|
257
|
+
// --- Context Dashboard (HUD) ---
|
|
258
|
+
const usage = await ctx.getContextUsage();
|
|
259
|
+
let usageStr = "Unknown";
|
|
260
|
+
if (usage) {
|
|
261
|
+
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
262
|
+
}
|
|
263
|
+
// Find the distance to the nearest tag
|
|
264
|
+
let stepsSinceTag = 0;
|
|
265
|
+
let nearestTagName = "None";
|
|
266
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
267
|
+
const id = branch[i].id;
|
|
268
|
+
const label = sm.getLabel(id);
|
|
269
|
+
if (label) {
|
|
270
|
+
nearestTagName = label;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
stepsSinceTag++;
|
|
274
|
+
}
|
|
275
|
+
const hud = [
|
|
276
|
+
`[Context Dashboard]`,
|
|
277
|
+
`• Context Usage: ${usageStr}`,
|
|
278
|
+
`• Segment Size: ${stepsSinceTag} steps since last tag '${nearestTagName}'`,
|
|
279
|
+
`---------------------------------------------------`
|
|
280
|
+
].join("\n");
|
|
281
|
+
return { content: [{ type: "text", text: hud + "\n" + (lines.join("\n") || "(Root Path Only)") }], details: {} };
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
pi.registerTool({
|
|
285
|
+
name: "context_checkout",
|
|
286
|
+
label: "Context Checkout",
|
|
287
|
+
description: "Navigate to ANY point in the conversation history. This checkout only resets *conversation history*, NOT disk files. ALWAYS provide a detailed 'message' to bridge context.",
|
|
288
|
+
parameters: ContextCheckoutParams,
|
|
289
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
290
|
+
const sm = ctx.sessionManager;
|
|
291
|
+
const tid = resolveTargetId(sm, params.target);
|
|
292
|
+
const currentLeaf = sm.getLeafId();
|
|
293
|
+
if (currentLeaf === tid) {
|
|
294
|
+
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
295
|
+
}
|
|
296
|
+
if (params.backupTag && currentLeaf) {
|
|
297
|
+
pi.setLabel(currentLeaf, params.backupTag);
|
|
298
|
+
}
|
|
299
|
+
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
300
|
+
const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf || "unknown");
|
|
301
|
+
const enrichedMessage = `(summary from ${origin})\n${params.message}`;
|
|
302
|
+
await sm.branchWithSummary(tid, enrichedMessage);
|
|
303
|
+
return { content: [{ type: "text", text: `Checked out ${tid}\nBackup tag created: ${params.backupTag || "none"}\nmessage: ${enrichedMessage}` }], details: {} };
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
pi.registerCommand("context", {
|
|
307
|
+
description: "Show context usage visualization",
|
|
308
|
+
handler: async (args, ctx) => {
|
|
309
|
+
const usage = await ctx.getContextUsage();
|
|
310
|
+
if (!usage) {
|
|
311
|
+
ctx.ui.notify("Context usage info not available.", "warning");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const sm = ctx.sessionManager;
|
|
315
|
+
const branch = sm.getBranch();
|
|
316
|
+
const systemPrompt = ctx.getSystemPrompt();
|
|
317
|
+
const tools = pi.getActiveTools();
|
|
318
|
+
const allTools = pi.getAllTools();
|
|
319
|
+
const activeToolDefs = allTools.filter(t => tools.includes(t.name));
|
|
320
|
+
const estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
321
|
+
let msgTokensRaw = 0;
|
|
322
|
+
let toolUseTokensRaw = 0;
|
|
323
|
+
let toolResultTokensRaw = 0;
|
|
324
|
+
for (const entry of branch) {
|
|
325
|
+
if (entry.type === "message") {
|
|
326
|
+
const m = entry.message;
|
|
327
|
+
if (m.role === "user") {
|
|
328
|
+
if (typeof m.content === "string")
|
|
329
|
+
msgTokensRaw += estimateTokens(m.content);
|
|
330
|
+
else if (Array.isArray(m.content)) {
|
|
331
|
+
for (const p of m.content)
|
|
332
|
+
if (p.type === "text")
|
|
333
|
+
msgTokensRaw += estimateTokens(p.text);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else if (m.role === "assistant") {
|
|
337
|
+
if (typeof m.content === "string")
|
|
338
|
+
msgTokensRaw += estimateTokens(m.content);
|
|
339
|
+
else if (Array.isArray(m.content)) {
|
|
340
|
+
for (const p of m.content) {
|
|
341
|
+
if (p.type === "text")
|
|
342
|
+
msgTokensRaw += estimateTokens(p.text);
|
|
343
|
+
if (p.type === "toolCall")
|
|
344
|
+
toolUseTokensRaw += estimateTokens(JSON.stringify(p));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else if (m.role === "toolResult") {
|
|
349
|
+
if (Array.isArray(m.content)) {
|
|
350
|
+
for (const p of m.content)
|
|
351
|
+
if (p.type === "text")
|
|
352
|
+
toolResultTokensRaw += estimateTokens(p.text);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (m.role === "bashExecution") {
|
|
356
|
+
toolUseTokensRaw += estimateTokens(m.command || "");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
360
|
+
msgTokensRaw += estimateTokens(entry.summary || "");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const systemTokensRaw = estimateTokens(systemPrompt);
|
|
364
|
+
const toolDefTokensRaw = estimateTokens(JSON.stringify(activeToolDefs));
|
|
365
|
+
const totalActual = usage.tokens;
|
|
366
|
+
const limit = usage.contextWindow;
|
|
367
|
+
const totalRaw = systemTokensRaw + toolDefTokensRaw + msgTokensRaw + toolUseTokensRaw + toolResultTokensRaw;
|
|
368
|
+
const ratio = totalRaw > 0 ? (totalActual / totalRaw) : 1;
|
|
369
|
+
const systemTokens = Math.round(systemTokensRaw * ratio);
|
|
370
|
+
const toolDefTokens = Math.round(toolDefTokensRaw * ratio);
|
|
371
|
+
const msgTokens = Math.round(msgTokensRaw * ratio);
|
|
372
|
+
const toolUseTokens = Math.round(toolUseTokensRaw * ratio);
|
|
373
|
+
const toolResultTokens = Math.round(toolResultTokensRaw * ratio);
|
|
374
|
+
await ctx.ui.custom((tui, theme, kb, done) => {
|
|
375
|
+
const container = new Container();
|
|
376
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
377
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(" Context Usage")), 1, 0));
|
|
378
|
+
container.addChild(new Spacer(1));
|
|
379
|
+
// Grouped by function and color
|
|
380
|
+
const categories = [
|
|
381
|
+
{ label: "System Prompt", value: systemTokens, color: "muted" },
|
|
382
|
+
{ label: "System Tools", value: toolDefTokens, color: "dim" },
|
|
383
|
+
{ label: "Tool Call", value: toolUseTokens + toolResultTokens, color: "success" },
|
|
384
|
+
{ label: "Messages", value: msgTokens, color: "accent" },
|
|
385
|
+
];
|
|
386
|
+
const otherTokens = Math.max(0, totalActual - (systemTokens + toolDefTokens + msgTokens + toolUseTokens + toolResultTokens));
|
|
387
|
+
if (otherTokens > 10)
|
|
388
|
+
categories.push({ label: "Other", value: otherTokens, color: "dim" });
|
|
389
|
+
categories.push({ label: "Available", value: Math.max(0, limit - totalActual), color: "borderMuted" });
|
|
390
|
+
const gridWidth = 10;
|
|
391
|
+
const gridHeight = 5;
|
|
392
|
+
const totalBlocks = gridWidth * gridHeight;
|
|
393
|
+
const blocks = [];
|
|
394
|
+
categories.forEach((cat) => {
|
|
395
|
+
if (cat.label === "Available")
|
|
396
|
+
return;
|
|
397
|
+
let count = Math.round((cat.value / limit) * totalBlocks);
|
|
398
|
+
if (count === 0 && cat.value > 0)
|
|
399
|
+
count = 1;
|
|
400
|
+
for (let i = 0; i < count && blocks.length < totalBlocks; i++) {
|
|
401
|
+
blocks.push({ color: cat.color, filled: true });
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
while (blocks.length < totalBlocks) {
|
|
405
|
+
blocks.push({ color: "borderMuted", filled: false });
|
|
406
|
+
}
|
|
407
|
+
const gridLines = [];
|
|
408
|
+
for (let r = 0; r < gridHeight; r++) {
|
|
409
|
+
let rowStr = "";
|
|
410
|
+
for (let c = 0; c < gridWidth; c++) {
|
|
411
|
+
const b = blocks[r * gridWidth + c];
|
|
412
|
+
rowStr += theme.fg(b.color, b.filled ? "■ " : "□ ");
|
|
413
|
+
}
|
|
414
|
+
gridLines.push(rowStr.trimEnd());
|
|
415
|
+
}
|
|
416
|
+
const totalUsageTitle = `${theme.fg("text", theme.bold("Total Usage".padEnd(16)))} ${theme.fg("text", theme.bold(formatTokens(totalActual).padStart(7)))} ${theme.fg("text", theme.bold(`(${usage.percent.toFixed(1).padStart(5)}%)`))}`;
|
|
417
|
+
const catDetailLines = categories.map(cat => {
|
|
418
|
+
const labelStr = cat.label.padEnd(14);
|
|
419
|
+
const valStr = formatTokens(cat.value).padStart(7);
|
|
420
|
+
const rowPercent = ((cat.value / limit) * 100).toFixed(1).padStart(5);
|
|
421
|
+
const icon = cat.label === "Available" ? "□" : "■";
|
|
422
|
+
return `${theme.fg(cat.color, icon)} ${theme.fg("text", labelStr)} ${theme.fg("accent", valStr)} (${rowPercent}%)`;
|
|
423
|
+
});
|
|
424
|
+
const allDetailLines = [totalUsageTitle, "", ...catDetailLines];
|
|
425
|
+
const leftSideWidth = 20;
|
|
426
|
+
const maxH = Math.max(gridLines.length, allDetailLines.length);
|
|
427
|
+
for (let i = 0; i < maxH; i++) {
|
|
428
|
+
const left = (gridLines[i] || "").padEnd(leftSideWidth);
|
|
429
|
+
const right = allDetailLines[i] || "";
|
|
430
|
+
container.addChild(new Text(` ${left} ${right}`, 1, 0));
|
|
431
|
+
}
|
|
432
|
+
container.addChild(new Spacer(1));
|
|
433
|
+
container.addChild(new Text(theme.fg("dim", " Press any key to close"), 1, 0));
|
|
434
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
435
|
+
return {
|
|
436
|
+
render: (w) => container.render(w),
|
|
437
|
+
invalidate: () => container.invalidate(),
|
|
438
|
+
handleInput: (data) => done(undefined),
|
|
439
|
+
};
|
|
440
|
+
}, { overlay: true });
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "agent-driven context management tools",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -22,13 +22,8 @@
|
|
|
22
22
|
"type": "module",
|
|
23
23
|
"author": "",
|
|
24
24
|
"license": "MIT",
|
|
25
|
-
"peerDependencies": {
|
|
26
|
-
"@mariozechner/pi-coding-agent": "*"
|
|
27
|
-
},
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"@sinclair/typebox": "^0.34.13"
|
|
30
|
-
},
|
|
31
25
|
"devDependencies": {
|
|
26
|
+
"@sinclair/typebox": "^0.34.13",
|
|
32
27
|
"typescript": "^5.7.2",
|
|
33
28
|
"@mariozechner/pi-coding-agent": "latest"
|
|
34
29
|
},
|
|
@@ -5,176 +5,246 @@ description: Strategies for efficient context management using context_log, cont
|
|
|
5
5
|
|
|
6
6
|
# Context Management
|
|
7
7
|
|
|
8
|
-
**CRITICAL: THIS SKILL MANAGES YOUR MEMORY
|
|
8
|
+
**CRITICAL: THIS SKILL MANAGES YOUR MEMORY. WITHOUT IT, YOU WILL FORGET.**
|
|
9
9
|
|
|
10
|
-
Your context window is
|
|
10
|
+
Your context window is limited. As conversations grow, "pollution" (noise, failed attempts) degrades your reasoning.
|
|
11
11
|
|
|
12
|
-
**
|
|
13
|
-
|
|
12
|
+
**YOU MUST PROACTIVELY MANAGE YOUR HISTORY.**
|
|
13
|
+
Do not wait for the user to tell you.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## The Core Philosophy: Build, Perceive, Navigate
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
3. **Compress (Hygiene)**: Use `context_checkout` to squash history. **Finished tasks are noise.** Summarize them to reclaim memory.
|
|
17
|
+
```
|
|
18
|
+
Context Window = RAM (Expensive, volatile, limited)
|
|
19
|
+
Context Graph = Disk (Cheap, persistent, unlimited)
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
→ Move finished tasks from RAM to the Graph.
|
|
22
|
+
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Manage your context window like a Git repository. You are the maintainer.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
1. **BUILD the Skeleton (`context_tag`)**:
|
|
27
|
+
* Raw conversation is a flat list. **Tags create structure.**
|
|
28
|
+
* Without tags, `context_log` is just a list of IDs. With tags, it is a **Map**.
|
|
29
|
+
2. **PERCEIVE the State (`context_log`)**:
|
|
30
|
+
* Check the HUD: Is "Segment Size" too big? You are drifting.
|
|
31
|
+
* Check the Graph: Where are you? Are you in a deep branch?
|
|
32
|
+
3. **NAVIGATE & MERGE (`context_checkout`)**:
|
|
33
|
+
* **Squash:** Convert a messy "feature branch" (thinking process) into a single "merge commit" (summary).
|
|
34
|
+
* **Jump:** Move between tasks or retry paths without carrying baggage.
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
36
|
+
## Quick Start: The Loop
|
|
37
|
+
|
|
38
|
+
Follow this cycle for every major task:
|
|
39
|
+
|
|
40
|
+
1. **CHECK:** Verify state.
|
|
41
|
+
`context_log`
|
|
42
|
+
2. **START:** Tag the beginning with a semantic name.
|
|
43
|
+
`context_tag({ name: "<task-slug>-start" })` // e.g., `auth-login-start`
|
|
44
|
+
3. **WORK:** Execute steps.
|
|
45
|
+
4. **MILESTONE:** Tag intermediate stable states.
|
|
46
|
+
`context_tag({ name: "<task-slug>-plan" })` // e.g., `auth-login-plan`
|
|
47
|
+
5. **SQUASH (Autonomous):** If history becomes noisy or low-density, **Squash with Backup**.
|
|
48
|
+
* *Action:* `context_checkout({ target: "<task-slug>-start", message: "...", backupTag: "<task-slug>-raw-history" })`
|
|
49
|
+
* *Action (Optional):* `context_tag({ name: "<task-slug>-done" })`
|
|
50
|
+
* *Safety:* If you need the details later, checkout the backup tag.
|
|
51
|
+
|
|
52
|
+
## Tool Reference
|
|
53
|
+
|
|
54
|
+
| Tool | Analog | Purpose | When to Use |
|
|
55
|
+
| :--- | :--- | :--- | :--- |
|
|
56
|
+
| `context_tag` | `git tag` | Bookmark a stable state. | Before risky changes. Before starting a new task. |
|
|
57
|
+
| `context_log` | `git log` | See where you are. | When you feel lost. To find IDs for checkout. |
|
|
58
|
+
| `context_checkout`| `git reset --soft` | **Time Travel / Squash.** | To undo mistakes. To compress history. |
|
|
45
59
|
|
|
46
|
-
##
|
|
60
|
+
## Critical Rules
|
|
47
61
|
|
|
48
|
-
###
|
|
49
|
-
*Check the `context_log` HUD.*
|
|
62
|
+
### Tag Wisely (Build The Skeleton)
|
|
50
63
|
|
|
51
|
-
|
|
64
|
+
Tags are the "Table of Contents". Name them so you can understand the history at a glance.
|
|
65
|
+
|
|
66
|
+
**Naming Formula:** `<task-slug>-<phase>`
|
|
67
|
+
|
|
68
|
+
* **task-slug**: Short, kebab-case identifier for the task (e.g., `auth-login`, `db-migration`)
|
|
69
|
+
* **phase**: The stage of work (`start`, `plan`, `impl`, `done`, `fail`, `backup`)
|
|
70
|
+
|
|
71
|
+
| Bad (Generic) | Good (Semantic) | Why |
|
|
52
72
|
| :--- | :--- | :--- |
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
73
|
+
| `task-start` | `auth-oauth-start` | Describes WHAT task |
|
|
74
|
+
| `pre-research` | `error-log-analysis-start` | Future-you knows the topic |
|
|
75
|
+
| `phase-1-done` | `db-schema-plan-done` | Know which phase of which task |
|
|
76
|
+
| `debug-retry` | `null-pointer-fix-retry` | What bug? |
|
|
56
77
|
|
|
57
|
-
|
|
58
|
-
*Reflect on your current progress.*
|
|
78
|
+
**Tag Categories:**
|
|
59
79
|
|
|
60
|
-
|
|
|
80
|
+
| Category | Pattern | Examples |
|
|
61
81
|
| :--- | :--- | :--- |
|
|
62
|
-
| **
|
|
63
|
-
| **
|
|
64
|
-
| **
|
|
82
|
+
| **Start** | `<task>-start` | `auth-jwt-start`, `docker-setup-start` |
|
|
83
|
+
| **Plan** | `<task>-plan` | `api-v2-plan`, `migration-plan` |
|
|
84
|
+
| **Milestone** | `<task>-<milestone>` | `auth-jwt-impl-done`, `tests-passed` |
|
|
85
|
+
| **Backup** | `<task>-raw-history` | `auth-jwt-raw-history` |
|
|
86
|
+
| **Failure** | `<task>-fail-<reason>` | `auth-jwt-fail-timeout` |
|
|
87
|
+
|
|
88
|
+
**How to generate:** Ask yourself "What is the task?" → Extract 1-3 keywords (e.g., "fix login timeout" → `login-timeout-fix-start`)
|
|
89
|
+
|
|
90
|
+
### Squash Noise, Keep Signal, Focus on Goal (Context Hygiene)
|
|
91
|
+
Think of your conversation as a "Feature Branch" full of messy thoughts.
|
|
92
|
+
**You must distinguish Signal from Noise.**
|
|
93
|
+
|
|
94
|
+
* **Signal (High Value):** Design decisions, user constraints, final working code. -> **KEEP.**
|
|
95
|
+
* **Noise (Low Value):** Failed attempts, long tool outputs, "thinking" steps. -> **SQUASH.**
|
|
96
|
+
* **Focus on Goal:** Ask yourself: "Does this message help me achieve the current goal?" -> **KEEP.**
|
|
97
|
+
|
|
98
|
+
**When to Squash:**
|
|
99
|
+
1. **Task Done:** Convert the messy process into one clean summary.
|
|
100
|
+
2. **Low Density:** You read 2000 lines but only found 1 error.
|
|
65
101
|
|
|
66
|
-
|
|
102
|
+
**Safety:** Squashing is **LOSSLESS**.
|
|
103
|
+
By using `backupTag`, you save the "Messy Branch" forever. You can always checkout the backup tag if the summary isn't enough.
|
|
104
|
+
* **Main Trunk:** Jump back to the summary.
|
|
105
|
+
* **Backup Tag:** Jump back to the raw details.
|
|
67
106
|
|
|
68
|
-
###
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
### Fail Fast, Revert Faster
|
|
108
|
+
If you fail 3 times:
|
|
109
|
+
1. **STOP.** Don't try a 4th time.
|
|
110
|
+
2. `context_checkout` back to the last safe tag.
|
|
111
|
+
3. Summarize the failure in the checkout message ("Tried X, failed because Y").
|
|
112
|
+
4. Try a new approach from the clean state.
|
|
72
113
|
|
|
73
|
-
|
|
74
|
-
Check where you are and how expensive the path is.
|
|
75
|
-
- Use `verbose: false` (default) to see the high-level "ToC" (Milestones).
|
|
114
|
+
## Decision Matrix: When to Act
|
|
76
115
|
|
|
77
|
-
|
|
78
|
-
|
|
116
|
+
| Situation | Action | Reason |
|
|
117
|
+
| :--- | :--- | :--- |
|
|
118
|
+
| **Starting Task** | `context_tag({ name: "<task-slug>-start" })` | Create a rollback point. |
|
|
119
|
+
| **Research / Logs** | `context_checkout` (Squash) | **Process is Noise.** Read 2000 lines -> Keep result. |
|
|
120
|
+
| **Messy Debugging** | **Squash w/ Backup** | **Cleanup.** The error logs are noise once fixed. |
|
|
121
|
+
| **Task Done (Candidate)**| **Squash w/ Backup** | **Assume Success.** Summary is usually enough. Backup exists if not. |
|
|
122
|
+
| **Goal Shift** | `context_checkout` (Squash) | Old context is irrelevant. |
|
|
123
|
+
| **Drift (some steps w/o tag)** | **Tag (Milestone)** | Maintain the skeleton. Don't fly blind. |
|
|
124
|
+
|
|
125
|
+
## The "Context Health" Check
|
|
126
|
+
|
|
127
|
+
If you cannot answer these, run `context_log`:
|
|
128
|
+
|
|
129
|
+
| Question | Answer Source |
|
|
130
|
+
| :--- | :--- |
|
|
131
|
+
| **Where is the skeleton?** | The sequence of `tag`s in the log. |
|
|
132
|
+
| **Is this history useful?** | If "No" -> **SQUASH IT.** |
|
|
133
|
+
| **Am I in a loop?** | Repeated entries in the graph. |
|
|
79
134
|
|
|
80
|
-
> **CRITICAL REMINDER: CONTEXT IS NOT CODEBASE**
|
|
81
|
-
> Checking out a new context branch **DOES NOT DELETE FILES**. It only resets your *conversation memory*. Your code on disk is safe.
|
|
82
135
|
|
|
83
|
-
|
|
136
|
+
## Good Checkout Messages
|
|
84
137
|
|
|
85
138
|
The `message` is your lifeline to your past self.
|
|
86
139
|
A good message preserves critical context that would otherwise be lost.
|
|
87
140
|
|
|
88
|
-
Structure: `[Status] + [Reason] + [Important Changes]
|
|
141
|
+
Structure: `[Key Finding/Status] + [Reason] + [Important Changes]`
|
|
89
142
|
|
|
90
|
-
* **Status**: What did you
|
|
91
|
-
* **Reason**: Why are you branching/moving? (e.g., "
|
|
143
|
+
* **Key Finding/Status**: What did you discover or complete? Include specific numbers, errors, or outcomes.
|
|
144
|
+
* **Reason**: Why are you branching/moving? (e.g., "Task complete", "Approach failed", "Need raw logs")
|
|
92
145
|
* **Important Changes**: What files or logic have been modified? (This checkout only resets *conversation history*, NOT disk files, so you must remember what changed.)
|
|
93
|
-
* **Carryover**: What specific details (IDs, file paths, user constraints) must be remembered?
|
|
94
146
|
|
|
95
147
|
Examples:
|
|
96
148
|
|
|
97
|
-
* *Good (Resetting after failure)*: "
|
|
98
|
-
* *Good (Cleaning up)*: "
|
|
149
|
+
* *Good (Resetting after failure)*: "Recursive parser hit stack overflow at depth 8000. Switching to iterative. **Reason**: Stack limit reached. **Important Changes**: Modified `utils/recursion.ts`."
|
|
150
|
+
* *Good (Cleaning up)*: "Auth module complete: JWT + OAuth2 + RBAC. 23 tests passing. **Reason**: Task done, cleaning context. **Important Changes**: Created `auth/`, modified `routes.ts` and `middleware.ts`."
|
|
99
151
|
* *Bad*: "Switching context." (Too vague - you will forget why)
|
|
100
152
|
* *Bad*: "Done." (What is done? What did we learn?)
|
|
101
153
|
|
|
102
|
-
|
|
103
|
-
`context_checkout` is non-destructive. If you squash too much, use `context_log` to find the old branch ID and checkout back to it.
|
|
154
|
+
## Anti-Patterns
|
|
104
155
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
| Don't | Do Instead |
|
|
157
|
+
| :--- | :--- |
|
|
158
|
+
| **Blind Tagging** (Tagging without looking) | **Check** (`context_log`) to avoid duplicates or tagging noise. |
|
|
159
|
+
| **Over-Tagging** (Tagging every step) | **Tag** only major phase changes (`start`, `milestone`). |
|
|
160
|
+
| **Hoard** (Keep all history "just in case") | **Squash** low-density history (research, logs). |
|
|
161
|
+
| **Panic** (Apologize repeatedly for errors) | **Revert** (`context_checkout`) to before the error. |
|
|
162
|
+
| **Blind Checkout** (Guessing IDs) | **Look** (`context_log`) first to get valid IDs. |
|
|
163
|
+
| **Vague Summaries** ("Done", "Fixed") | **Detailed Summaries** ("Found bug in line 40. Fixed with patch X.") |
|
|
164
|
+
| **Generic Tag Names** (`task-start`, `phase-1`) | **Semantic Names** (`auth-jwt-start`, `db-schema-plan`) |
|
|
110
165
|
|
|
111
|
-
##
|
|
166
|
+
## Recipes (Copy-Paste)
|
|
112
167
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
*
|
|
116
|
-
|
|
168
|
+
### 1. The "Miner" (Immediate Squash)
|
|
169
|
+
**Goal:** Pure information gathering (Reading files, Searching web).
|
|
170
|
+
**Why:** The *process* of searching is irrelevant. Only the *result* matters.
|
|
171
|
+
|
|
172
|
+
**Example Task:** Analyzing error logs to find root cause of timeout
|
|
117
173
|
|
|
118
174
|
```javascript
|
|
119
|
-
// 1. Tag the
|
|
120
|
-
context_tag({ name: "
|
|
175
|
+
// 1. Tag BEFORE starting the noisy work (use descriptive name)
|
|
176
|
+
context_tag({ name: "timeout-analysis-start" });
|
|
177
|
+
|
|
178
|
+
// ... (Read 5 log files, search 3 docs, find DB connection pool exhaustion) ...
|
|
121
179
|
|
|
122
|
-
// 2. Squash
|
|
180
|
+
// 2. Squash IMMEDIATELY. Do not wait for user.
|
|
123
181
|
context_checkout({
|
|
124
|
-
target: "
|
|
125
|
-
message: "
|
|
126
|
-
|
|
182
|
+
target: "timeout-analysis-start",
|
|
183
|
+
message: "Found DB connection pool exhaustion as root cause (pool size: 10, peak load: 1000 req/s). Recommended fix: increase to 50. **Reason**: Context cleanup after research. **Important Changes**: None (read-only).",
|
|
184
|
+
backupTag: "timeout-analysis-raw-history" // Safety backup
|
|
127
185
|
});
|
|
186
|
+
context_tag({ name: "timeout-analysis-done" });
|
|
128
187
|
```
|
|
129
|
-
*Result*: You are now at a clean state with just the summary of Feature A. Cost dropped from 50 msgs to 1 msg.
|
|
130
188
|
|
|
131
|
-
|
|
132
|
-
You
|
|
133
|
-
|
|
189
|
+
### 2. The "Candidate" (Wait for Confirmation)
|
|
190
|
+
**Goal:** You finished a complex task.
|
|
191
|
+
**Why:** The history is noisy. The result is clean.
|
|
192
|
+
**Safety:** We create a backup tag automatically.
|
|
193
|
+
|
|
194
|
+
**Example Task:** Implementing OAuth login flow
|
|
134
195
|
|
|
135
196
|
```javascript
|
|
136
|
-
//
|
|
197
|
+
// Squash to Summary (Optimistic Cleanup)
|
|
137
198
|
context_checkout({
|
|
138
|
-
target: "
|
|
139
|
-
message: "
|
|
140
|
-
|
|
199
|
+
target: "oauth-impl-start", // Squash range: Start -> Now
|
|
200
|
+
message: "OAuth2 flow implemented with PKCE, Google + GitHub providers. All 12 tests passing. **Reason**: Task complete, cleaning up. **Important Changes**: Created `auth/oauth.ts`, modified `routes.ts`, `config.ts`. Backup at 'oauth-impl-raw-history'.",
|
|
201
|
+
backupTag: "oauth-impl-raw-history"
|
|
141
202
|
});
|
|
203
|
+
context_tag({ name: "oauth-impl-candidate" });
|
|
142
204
|
```
|
|
143
205
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
206
|
+
### 3. The "Undo" (Revert Squash)
|
|
207
|
+
**Goal:** User asks about a detail you squashed away.
|
|
208
|
+
**Action:** Jump back to the backup tag.
|
|
209
|
+
|
|
210
|
+
**Example Task:** Reviewing OAuth implementation details
|
|
148
211
|
|
|
149
212
|
```javascript
|
|
150
|
-
//
|
|
151
|
-
|
|
213
|
+
// Jump back to the raw history
|
|
214
|
+
context_checkout({
|
|
215
|
+
target: "oauth-impl-raw-history",
|
|
216
|
+
message: "Reviewing token refresh logic - user reports 401 after 15 min idle. Suspect refresh token not firing. **Reason**: Need raw logs to trace the bug. **Important Changes**: None."
|
|
217
|
+
});
|
|
218
|
+
context_tag({ name: "oauth-review-start" });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 4. Branching (Alternative Approach)
|
|
222
|
+
**Scenario:** Method A failed (and was squashed). You want to try Method B from the clean state.
|
|
223
|
+
**Action:** Checkout the start point.
|
|
152
224
|
|
|
153
|
-
|
|
225
|
+
**Example Task:** Fixing memory leak - trying different approaches
|
|
154
226
|
|
|
155
|
-
|
|
227
|
+
```javascript
|
|
228
|
+
// Method A (weak references) failed, trying Method B (object pooling)
|
|
156
229
|
context_checkout({
|
|
157
|
-
target: "
|
|
158
|
-
message: "
|
|
159
|
-
tagName: "research-complete"
|
|
230
|
+
target: "memory-leak-fix-start",
|
|
231
|
+
message: "WeakRef approach failed: objects GC'd within 30s (expected: 5min). Cache hit rate dropped from 95% to 12%. **Reason**: Switching to object pooling approach. **Important Changes**: `CacheManager.ts` modified (will revert)."
|
|
160
232
|
});
|
|
233
|
+
context_tag({ name: "memory-leak-pool-approach-start" });
|
|
161
234
|
```
|
|
162
|
-
*Result*: Your context now contains the prompt, the tag, and the single message with the key config. The 2000 lines of noise are gone.
|
|
163
235
|
|
|
164
|
-
|
|
165
|
-
You
|
|
166
|
-
|
|
167
|
-
*Solution*: Send the *final working code* back to the moment before you started coding.
|
|
236
|
+
### 5. The "Undo" (Failed Attempt)
|
|
237
|
+
You tried to fix a bug but broke everything.
|
|
238
|
+
**Goal:** Clean up a failed path.
|
|
168
239
|
|
|
169
|
-
|
|
170
|
-
// 1. You realize you are in a messy debug loop.
|
|
171
|
-
// 2. You finally get the code working.
|
|
172
|
-
// 3. Checkout back to BEFORE you started the implementation.
|
|
240
|
+
**Example Task:** Fixing race condition in async handler
|
|
173
241
|
|
|
242
|
+
```javascript
|
|
243
|
+
// Attempted mutex-based fix, but introduced deadlock
|
|
174
244
|
context_checkout({
|
|
175
|
-
target: "
|
|
176
|
-
message: "
|
|
177
|
-
|
|
245
|
+
target: "race-condition-fix-start",
|
|
246
|
+
message: "Mutex caused deadlock: Thread A holds mutex, awaits callback; callback needs mutex held by B; B waits for A. Circular wait detected. **Reason**: Trying lock-free CAS approach next. **Important Changes**: `AsyncQueue.ts` lines 70-90 modified (backup saved).",
|
|
247
|
+
backupTag: "race-condition-mutex-fail" // Save the failure for reference
|
|
178
248
|
});
|
|
249
|
+
context_tag({ name: "race-condition-lockfree-start" });
|
|
179
250
|
```
|
|
180
|
-
*Result*: It looks like you got it right on the first try. Context is clean and focused.
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ const ContextLogParams = Type.Object({
|
|
|
27
27
|
const ContextCheckoutParams = Type.Object({
|
|
28
28
|
target: Type.String({ description: "Where to jump/squash to. Can be a tag name (e.g., 'task-start'), a commit ID, or 'root'. This is the base for your new branch." }),
|
|
29
29
|
message: Type.String({ description: "The 'Carryover Message' for the new branch. A summary of your *current* progress/lessons that you want to bring with you to the new state. This ensures you don't lose key information when switching contexts. Good summary message: '[Status] + [Reason] + [Important Changes] + [Carryover Data]'" }),
|
|
30
|
-
|
|
30
|
+
backupTag: Type.Optional(Type.String({ description: "Optional tag name to apply to the CURRENT state before checking out. Use this to create an automatic backup of the history you are about to leave/squash." })),
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
const ContextTagParams = Type.Object({
|
|
@@ -35,7 +35,7 @@ const ContextTagParams = Type.Object({
|
|
|
35
35
|
target: Type.Optional(Type.String({ description: "The commit ID to tag. Defaults to HEAD (current state)." })),
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
const isInternal = (name: string) => ["
|
|
38
|
+
const isInternal = (name: string) => ["context_tag", "context_log", "context_checkout"].includes(name);
|
|
39
39
|
|
|
40
40
|
const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
41
41
|
if (target.toLowerCase() === "root") {
|
|
@@ -62,6 +62,55 @@ const formatTokens = (n: number) => {
|
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
export default function (pi: ExtensionAPI) {
|
|
65
|
+
pi.registerTool({
|
|
66
|
+
name: "context_tag",
|
|
67
|
+
label: "Context Tag",
|
|
68
|
+
description: "Creates a 'Save Point' (Bookmark) in the history. Use this before trying risky changes or when a feature is stable. 'Untagged progress is risky'.",
|
|
69
|
+
parameters: ContextTagParams,
|
|
70
|
+
async execute(_id, params: Static<typeof ContextTagParams>, _signal, _onUpdate, ctx) {
|
|
71
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
72
|
+
let id = params.target ? resolveTargetId(sm, params.target) : undefined;
|
|
73
|
+
|
|
74
|
+
if (!id) {
|
|
75
|
+
// Auto-resolve: Find the last "interesting" node to tag.
|
|
76
|
+
// We skip ToolResults (which look ugly tagged) and internal-only Assistant messages (which look empty).
|
|
77
|
+
const branch = sm.getBranch();
|
|
78
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
79
|
+
const entry = branch[i];
|
|
80
|
+
|
|
81
|
+
// 1. Check ToolResults
|
|
82
|
+
if (entry.type === 'message' && entry.message.role === 'toolResult') {
|
|
83
|
+
const tr = entry.message as any;
|
|
84
|
+
if (isInternal(tr.toolName)) continue;
|
|
85
|
+
|
|
86
|
+
// Public tool result is a valid target
|
|
87
|
+
id = entry.id;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Check Assistant messages for visibility
|
|
92
|
+
if (entry.type === 'message' && entry.message.role === 'assistant') {
|
|
93
|
+
const m = entry.message;
|
|
94
|
+
const hasInternalTool = m.content.some(c => c.type === 'toolCall' && isInternal(c.name));
|
|
95
|
+
|
|
96
|
+
if (!hasInternalTool) {
|
|
97
|
+
id = entry.id;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
id = entry.id;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
// Fallback to leaf if search failed
|
|
106
|
+
if (!id) id = sm.getLeafId() ?? "";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pi.setLabel(id, params.name);
|
|
110
|
+
return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id}` }], details: {} };
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
65
114
|
pi.registerTool({
|
|
66
115
|
name: "context_log",
|
|
67
116
|
label: "Context Log",
|
|
@@ -69,28 +118,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
69
118
|
parameters: ContextLogParams,
|
|
70
119
|
async execute(_id, params: Static<typeof ContextLogParams>, _signal, _onUpdate, ctx) {
|
|
71
120
|
const sm = ctx.sessionManager as SessionManager;
|
|
72
|
-
const
|
|
121
|
+
const branch = sm.getBranch();
|
|
73
122
|
const currentLeafId = sm.getLeafId();
|
|
74
123
|
const verbose = params.verbose ?? false;
|
|
75
124
|
const limit = params.limit ?? 50;
|
|
76
125
|
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
childrenMap.set(pId, siblings);
|
|
126
|
+
const backboneIds = new Set(branch.map((e) => e.id));
|
|
127
|
+
const sequence: SessionEntry[] = [];
|
|
128
|
+
|
|
129
|
+
branch.forEach((entry) => {
|
|
130
|
+
sequence.push(entry);
|
|
131
|
+
|
|
132
|
+
// Preserve side-summary logic: Show branch summaries/compactions that are off-path
|
|
133
|
+
const children = sm.getChildren(entry.id);
|
|
134
|
+
children.forEach((child) => {
|
|
135
|
+
if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.has(child.id)) {
|
|
136
|
+
sequence.push(child);
|
|
89
137
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
};
|
|
93
|
-
walk(tree);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
94
140
|
|
|
95
141
|
const getMsgContent = (entry: SessionEntry): string => {
|
|
96
142
|
if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
@@ -157,40 +203,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
157
203
|
return "";
|
|
158
204
|
};
|
|
159
205
|
|
|
160
|
-
const backboneIds: string[] = [];
|
|
161
|
-
let currId: string | undefined = currentLeafId ?? undefined;
|
|
162
|
-
while (currId) {
|
|
163
|
-
backboneIds.unshift(currId);
|
|
164
|
-
currId = parents.get(currId);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const sequence: SessionEntry[] = [];
|
|
168
|
-
backboneIds.forEach((id) => {
|
|
169
|
-
const entry = entryMap.get(id);
|
|
170
|
-
if (!entry) return;
|
|
171
|
-
sequence.push(entry);
|
|
172
|
-
const children = childrenMap.get(id) || [];
|
|
173
|
-
children.forEach((child) => {
|
|
174
|
-
if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.includes(child.id)) {
|
|
175
|
-
sequence.push(child);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
206
|
const isInteresting = (entry: SessionEntry): boolean => {
|
|
181
207
|
// 1. HEAD and Root
|
|
182
208
|
if (entry.id === currentLeafId) return true;
|
|
183
|
-
if (
|
|
209
|
+
if (branch.length > 0 && entry.id === branch[0].id) return true;
|
|
184
210
|
|
|
185
211
|
// 2. Explicit Tags (Labels) - Only show the TAGGED node, not the label node itself
|
|
186
212
|
if (sm.getLabel(entry.id)) return true;
|
|
187
213
|
if (entry.type === 'label') return false; // Hide label nodes, they are redundant
|
|
188
214
|
|
|
189
|
-
// 3. Structural Milestones (Summaries
|
|
215
|
+
// 3. Structural Milestones (Summaries)
|
|
190
216
|
if (entry.type === 'branch_summary' || entry.type === 'compaction') return true;
|
|
191
|
-
if ((childrenMap.get(entry.id)?.length ?? 0) > 1) return true;
|
|
192
217
|
|
|
193
|
-
// 4.
|
|
218
|
+
// 4. Branch Points (Forks)
|
|
219
|
+
if (sm.getChildren(entry.id).length > 1) return true;
|
|
220
|
+
|
|
221
|
+
// 5. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
|
|
194
222
|
if (entry.type === 'message' && entry.message.role === 'user') return true;
|
|
195
223
|
|
|
196
224
|
return false;
|
|
@@ -243,8 +271,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
243
271
|
role = "SUMMARY";
|
|
244
272
|
}
|
|
245
273
|
|
|
246
|
-
const id = entry.id
|
|
247
|
-
const isRoot =
|
|
274
|
+
const id = entry.id;
|
|
275
|
+
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
248
276
|
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
|
|
249
277
|
|
|
250
278
|
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
@@ -259,9 +287,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
259
287
|
}
|
|
260
288
|
|
|
261
289
|
// --- Context Dashboard (HUD) ---
|
|
262
|
-
const totalNodes = entryMap.size;
|
|
263
|
-
const currentDepth = backboneIds.length;
|
|
264
|
-
|
|
265
290
|
const usage = await ctx.getContextUsage();
|
|
266
291
|
let usageStr = "Unknown";
|
|
267
292
|
if (usage) {
|
|
@@ -271,8 +296,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
271
296
|
// Find the distance to the nearest tag
|
|
272
297
|
let stepsSinceTag = 0;
|
|
273
298
|
let nearestTagName = "None";
|
|
274
|
-
for (let i =
|
|
275
|
-
const id =
|
|
299
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
300
|
+
const id = branch[i].id;
|
|
276
301
|
const label = sm.getLabel(id);
|
|
277
302
|
if (label) {
|
|
278
303
|
nearestTagName = label;
|
|
@@ -299,40 +324,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
299
324
|
parameters: ContextCheckoutParams,
|
|
300
325
|
async execute(_id, params: Static<typeof ContextCheckoutParams>, _signal, _onUpdate, ctx) {
|
|
301
326
|
const sm = ctx.sessionManager as SessionManager;
|
|
327
|
+
|
|
302
328
|
const tid = resolveTargetId(sm, params.target);
|
|
303
329
|
|
|
304
330
|
const currentLeaf = sm.getLeafId();
|
|
305
331
|
if (currentLeaf === tid) {
|
|
306
|
-
return { content: [{ type: "text", text: `Already at target ${tid
|
|
332
|
+
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
333
|
+
}
|
|
334
|
+
if (params.backupTag && currentLeaf) {
|
|
335
|
+
pi.setLabel(currentLeaf, params.backupTag);
|
|
307
336
|
}
|
|
308
|
-
|
|
309
337
|
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
310
|
-
const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf
|
|
338
|
+
const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf || "unknown");
|
|
311
339
|
|
|
312
|
-
const enrichedMessage =
|
|
340
|
+
const enrichedMessage = `(summary from ${origin})\n${params.message}`;
|
|
313
341
|
await sm.branchWithSummary(tid, enrichedMessage);
|
|
314
342
|
|
|
315
|
-
|
|
316
|
-
// This ensures 'tagName' acts like naming the NEW branch tip.
|
|
317
|
-
const newLeaf = sm.getLeafId();
|
|
318
|
-
if (params.tagName && newLeaf) pi.setLabel(newLeaf, params.tagName);
|
|
319
|
-
|
|
320
|
-
return { content: [{ type: "text", text: `Checked out ${tid.slice(0, 7)}` }], details: {} };
|
|
343
|
+
return { content: [{ type: "text", text: `Checked out ${tid}\nBackup tag created: ${params.backupTag || "none"}\nmessage: ${enrichedMessage}` }], details: {} };
|
|
321
344
|
},
|
|
322
345
|
});
|
|
323
346
|
|
|
324
|
-
pi.registerTool({
|
|
325
|
-
name: "context_tag",
|
|
326
|
-
label: "Context Tag",
|
|
327
|
-
description: "Creates a 'Save Point' (Bookmark) in the history. Use this before trying risky changes or when a feature is stable. 'Untagged progress is risky'.",
|
|
328
|
-
parameters: ContextTagParams,
|
|
329
|
-
async execute(_id, params: Static<typeof ContextTagParams>, _signal, _onUpdate, ctx) {
|
|
330
|
-
const sm = ctx.sessionManager as SessionManager;
|
|
331
|
-
const id = params.target ? resolveTargetId(sm, params.target) : (sm.getLeafId() ?? "");
|
|
332
|
-
pi.setLabel(id, params.name);
|
|
333
|
-
return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id.slice(0, 7)}` }], details: {} };
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
347
|
|
|
337
348
|
pi.registerCommand("context", {
|
|
338
349
|
description: "Show context usage visualization",
|