context-mode 1.0.106 → 1.0.107
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/adapters/copilot-base.d.ts +3 -3
- package/build/adapters/cursor/hooks.js +8 -0
- package/build/adapters/cursor/index.js +4 -1
- package/build/adapters/gemini-cli/hooks.d.ts +6 -1
- package/build/adapters/gemini-cli/hooks.js +7 -1
- package/build/adapters/gemini-cli/index.js +12 -0
- package/build/adapters/kiro/hooks.js +4 -0
- package/build/adapters/kiro/index.d.ts +9 -2
- package/build/adapters/kiro/index.js +49 -27
- package/build/adapters/opencode/index.js +6 -0
- package/build/adapters/qwen-code/index.js +18 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +0 -4
- package/build/adapters/vscode-copilot/hooks.js +6 -6
- package/build/cli.js +1 -0
- package/build/openclaw/mcp-tools.d.ts +54 -0
- package/build/openclaw/mcp-tools.js +198 -0
- package/build/openclaw-plugin.d.ts +9 -0
- package/build/openclaw-plugin.js +132 -16
- package/build/opencode-plugin.d.ts +29 -4
- package/build/opencode-plugin.js +154 -7
- package/build/pi-extension.js +123 -29
- package/build/server.d.ts +1 -0
- package/build/server.js +19 -1
- package/build/session/extract.d.ts +1 -1
- package/build/session/extract.js +46 -1
- package/cli.bundle.mjs +125 -125
- package/hooks/core/platform-detect.mjs +49 -0
- package/hooks/core/routing.mjs +13 -1
- package/hooks/cursor/afteragentresponse.mjs +74 -0
- package/hooks/gemini-cli/beforeagent.mjs +99 -0
- package/hooks/kiro/agentspawn.mjs +97 -0
- package/hooks/kiro/userpromptsubmit.mjs +88 -0
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/sessionstart.mjs +3 -1
- package/hooks/vscode-copilot/sessionstart.mjs +13 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +68 -68
package/build/opencode-plugin.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenCode / KiloCode TypeScript plugin entry point for context-mode.
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
4
|
+
* Provides five hooks (v1.0.107 — Mickey OC-1..OC-4 follow-up):
|
|
5
5
|
* - tool.execute.before — Routing enforcement (deny/modify/passthrough)
|
|
6
|
-
* - tool.execute.after — Session event capture
|
|
7
|
-
* - experimental.session.compacting — Compaction snapshot
|
|
6
|
+
* - tool.execute.after — Session event capture + first-fire AGENTS.md scan (OC-4)
|
|
7
|
+
* - experimental.session.compacting — Compaction snapshot + budget-capped auto-injection (OC-3)
|
|
8
|
+
* - experimental.chat.system.transform — ROUTING_BLOCK + resume snapshot injection (OC-1)
|
|
9
|
+
* - chat.message — User-prompt capture w/ CCv2 inline filter (OC-2)
|
|
8
10
|
*
|
|
9
11
|
* KiloCode loads this via: import("context-mode") → expects default export
|
|
10
12
|
* with shape { server: (input) => Promise<Hooks> } (PluginModule).
|
|
@@ -14,15 +16,15 @@
|
|
|
14
16
|
*
|
|
15
17
|
* Constraints:
|
|
16
18
|
* - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
|
|
17
|
-
* -
|
|
19
|
+
* - context injection now via chat.system.transform surrogate (OC-1)
|
|
18
20
|
* - No routing file auto-write (avoid dirtying project trees)
|
|
19
21
|
* - Session cleanup happens at plugin init (no SessionStart)
|
|
20
22
|
*/
|
|
21
|
-
import { dirname, resolve } from "node:path";
|
|
23
|
+
import { dirname, resolve, join } from "node:path";
|
|
22
24
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
23
25
|
import { existsSync, readFileSync } from "node:fs";
|
|
24
26
|
import { SessionDB } from "./session/db.js";
|
|
25
|
-
import { extractEvents } from "./session/extract.js";
|
|
27
|
+
import { extractEvents, extractUserEvents } from "./session/extract.js";
|
|
26
28
|
import { buildResumeSnapshot } from "./session/snapshot.js";
|
|
27
29
|
import { OpenCodeAdapter } from "./adapters/opencode/index.js";
|
|
28
30
|
import { PLATFORM_ENV_VARS } from "./adapters/detect.js";
|
|
@@ -41,6 +43,19 @@ const VERSION = (() => {
|
|
|
41
43
|
catch { /* fall through */ }
|
|
42
44
|
return "unknown";
|
|
43
45
|
})();
|
|
46
|
+
// Synthetic message tags emitted by harnesses (CCv2 inline filter). When the
|
|
47
|
+
// user "message" is actually a system-generated nudge (e.g. tool-result, system
|
|
48
|
+
// reminder), capturing it as user_prompt would flood the DB with noise.
|
|
49
|
+
const SYNTHETIC_MESSAGE_PREFIXES = [
|
|
50
|
+
"<task-notification>",
|
|
51
|
+
"<system-reminder>",
|
|
52
|
+
"<context_guidance>",
|
|
53
|
+
"<tool-result>",
|
|
54
|
+
];
|
|
55
|
+
function isSyntheticMessage(text) {
|
|
56
|
+
const trimmed = text.trim();
|
|
57
|
+
return SYNTHETIC_MESSAGE_PREFIXES.some((p) => trimmed.startsWith(p));
|
|
58
|
+
}
|
|
44
59
|
// ── Helpers ───────────────────────────────────────────────
|
|
45
60
|
/**
|
|
46
61
|
* Detect whether the plugin is running under KiloCode or OpenCode.
|
|
@@ -79,12 +94,27 @@ function getPlatform() {
|
|
|
79
94
|
*/
|
|
80
95
|
async function createContextModePlugin(ctx) {
|
|
81
96
|
// Resolve build dir from compiled JS location
|
|
82
|
-
const
|
|
97
|
+
const platform = getPlatform();
|
|
98
|
+
const adapter = new OpenCodeAdapter(platform);
|
|
83
99
|
const buildDir = dirname(fileURLToPath(import.meta.url));
|
|
84
100
|
// Load routing module (ESM .mjs, lives outside build/ in hooks/)
|
|
85
101
|
const routingPath = resolve(buildDir, "..", "hooks", "core", "routing.mjs");
|
|
86
102
|
const routing = await import(pathToFileURL(routingPath).href);
|
|
87
103
|
await routing.initSecurity(buildDir);
|
|
104
|
+
// OC-1 / OC-3: Load hook helpers once at plugin init. Dynamic import keeps
|
|
105
|
+
// the .mjs ESM islands isolated from the .ts compile graph.
|
|
106
|
+
const routingBlockPath = resolve(buildDir, "..", "hooks", "routing-block.mjs");
|
|
107
|
+
const routingBlockMod = await import(pathToFileURL(routingBlockPath).href);
|
|
108
|
+
const toolNamingPath = resolve(buildDir, "..", "hooks", "core", "tool-naming.mjs");
|
|
109
|
+
const toolNamingMod = await import(pathToFileURL(toolNamingPath).href);
|
|
110
|
+
const autoInjectionPath = resolve(buildDir, "..", "hooks", "auto-injection.mjs");
|
|
111
|
+
const autoInjectionMod = await import(pathToFileURL(autoInjectionPath).href);
|
|
112
|
+
// Pre-build the routing block once per process — it is platform-specific
|
|
113
|
+
// (tool naming differs between opencode and kilo) but does NOT depend on
|
|
114
|
+
// sessionID, so we cache it. createToolNamer accepts both "opencode" and
|
|
115
|
+
// "kilo" per hooks/core/tool-naming.mjs:25-26.
|
|
116
|
+
const toolNamer = toolNamingMod.createToolNamer(platform);
|
|
117
|
+
const routingBlock = routingBlockMod.createRoutingBlock(toolNamer);
|
|
88
118
|
// Initialize per-process state. We do NOT fabricate a sessionId here —
|
|
89
119
|
// OpenCode/Kilo provide the real `input.sessionID` on every hook, and a
|
|
90
120
|
// process-global UUID would (a) never match prior-session resume rows and
|
|
@@ -97,6 +127,51 @@ async function createContextModePlugin(ctx) {
|
|
|
97
127
|
// many sessions, so the gate must be keyed by sessionID — NOT a single
|
|
98
128
|
// boolean closure flag (Mickey #2 root cause).
|
|
99
129
|
const resumeInjected = new Set();
|
|
130
|
+
// OC-1: Routing block first-fire gate per session. Distinct from
|
|
131
|
+
// resumeInjected because routing block must always inject (regardless of
|
|
132
|
+
// whether a resume row exists), but resume only on rows present.
|
|
133
|
+
const routingInjected = new Set();
|
|
134
|
+
// OC-4: AGENTS.md/CLAUDE.md captured-once-per-projectDir gate. Idempotent
|
|
135
|
+
// across many sessions reusing the same plugin process + project tree.
|
|
136
|
+
const agentsCaptured = new Set();
|
|
137
|
+
/**
|
|
138
|
+
* OC-4: Read AGENTS.md (and CLAUDE.md fallback if both exist) from the
|
|
139
|
+
* project directory and persist as `rule` + `rule_content` events. Mirrors
|
|
140
|
+
* the CC SessionStart pattern at hooks/sessionstart.mjs:121-132. Idempotent
|
|
141
|
+
* via `agentsCaptured` Set keyed by projectDir.
|
|
142
|
+
*/
|
|
143
|
+
function captureAgentsMd(sessionId) {
|
|
144
|
+
if (agentsCaptured.has(projectDir))
|
|
145
|
+
return;
|
|
146
|
+
agentsCaptured.add(projectDir);
|
|
147
|
+
// Mirror OpenCode's instruction.ts FILES order: AGENTS.md, CLAUDE.md, CONTEXT.md.
|
|
148
|
+
const candidates = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"];
|
|
149
|
+
for (const name of candidates) {
|
|
150
|
+
try {
|
|
151
|
+
const p = join(projectDir, name);
|
|
152
|
+
if (!existsSync(p))
|
|
153
|
+
continue;
|
|
154
|
+
const content = readFileSync(p, "utf-8");
|
|
155
|
+
if (!content.trim())
|
|
156
|
+
continue;
|
|
157
|
+
db.insertEvent(sessionId, {
|
|
158
|
+
type: "rule",
|
|
159
|
+
category: "rule",
|
|
160
|
+
data: p,
|
|
161
|
+
priority: 1,
|
|
162
|
+
}, "PluginInit");
|
|
163
|
+
db.insertEvent(sessionId, {
|
|
164
|
+
type: "rule_content",
|
|
165
|
+
category: "rule",
|
|
166
|
+
data: content,
|
|
167
|
+
priority: 1,
|
|
168
|
+
}, "PluginInit");
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// file missing or unreadable — skip silently
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
100
175
|
return {
|
|
101
176
|
// ── PreToolUse: Routing enforcement ─────────────────
|
|
102
177
|
"tool.execute.before": async (input, output) => {
|
|
@@ -128,6 +203,9 @@ async function createContextModePlugin(ctx) {
|
|
|
128
203
|
return;
|
|
129
204
|
try {
|
|
130
205
|
db.ensureSession(sessionId, projectDir);
|
|
206
|
+
// OC-4: Capture AGENTS.md/CLAUDE.md as rule events on first hook
|
|
207
|
+
// fire per projectDir. Idempotent via `agentsCaptured` Set.
|
|
208
|
+
captureAgentsMd(sessionId);
|
|
131
209
|
const hookInput = {
|
|
132
210
|
tool_name: input.tool ?? "",
|
|
133
211
|
tool_input: input.args ?? {},
|
|
@@ -144,6 +222,43 @@ async function createContextModePlugin(ctx) {
|
|
|
144
222
|
// Silent — session capture must never break the tool call
|
|
145
223
|
}
|
|
146
224
|
},
|
|
225
|
+
// ── chat.message: User-prompt capture (OC-2 / Z2) ───
|
|
226
|
+
// SDK signature verified at refs/platforms/opencode/packages/plugin/src/
|
|
227
|
+
// index.ts:233. Orchestrator reference at refs/plugin-examples/opencode/
|
|
228
|
+
// opencode-orchestrator/src/plugin-handlers/chat-message-handler.ts:41-65.
|
|
229
|
+
// CCv2 inline filter: skip synthetic harness messages (system reminders,
|
|
230
|
+
// tool results, etc.) so we don't pollute the user-prompt event stream.
|
|
231
|
+
"chat.message": async (input, output) => {
|
|
232
|
+
const sessionId = input?.sessionID;
|
|
233
|
+
if (!sessionId)
|
|
234
|
+
return;
|
|
235
|
+
try {
|
|
236
|
+
const parts = Array.isArray(output?.parts) ? output.parts : [];
|
|
237
|
+
const textPart = parts.find((p) => p && p.type === "text" && typeof p.text === "string" && p.text.length > 0);
|
|
238
|
+
if (!textPart || !textPart.text)
|
|
239
|
+
return;
|
|
240
|
+
const message = textPart.text;
|
|
241
|
+
if (isSyntheticMessage(message))
|
|
242
|
+
return;
|
|
243
|
+
db.ensureSession(sessionId, projectDir);
|
|
244
|
+
captureAgentsMd(sessionId);
|
|
245
|
+
// 1. Always save the raw prompt
|
|
246
|
+
db.insertEvent(sessionId, {
|
|
247
|
+
type: "user_prompt",
|
|
248
|
+
category: "user-prompt",
|
|
249
|
+
data: message,
|
|
250
|
+
priority: 1,
|
|
251
|
+
}, "UserPromptSubmit");
|
|
252
|
+
// 2. Extract role/decision/intent/skill events from the prompt body
|
|
253
|
+
const userEvents = extractUserEvents(message);
|
|
254
|
+
for (const ev of userEvents) {
|
|
255
|
+
db.insertEvent(sessionId, ev, "UserPromptSubmit");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Silent — chat.message must never break the turn
|
|
260
|
+
}
|
|
261
|
+
},
|
|
147
262
|
// ── PreCompact: Snapshot generation ─────────────────
|
|
148
263
|
"experimental.session.compacting": async (input, output) => {
|
|
149
264
|
const sessionId = input.sessionID;
|
|
@@ -162,6 +277,19 @@ async function createContextModePlugin(ctx) {
|
|
|
162
277
|
db.incrementCompactCount(sessionId);
|
|
163
278
|
// Mutate output.context to inject the snapshot
|
|
164
279
|
output.context.push(snapshot);
|
|
280
|
+
// OC-3 / Z3: Add budget-capped auto-injection (P1 role / P2 rules /
|
|
281
|
+
// P3 skills / P4 intent — ≤500 tokens / ~2000 chars per
|
|
282
|
+
// hooks/auto-injection.mjs). Pushed as a separate context entry so
|
|
283
|
+
// OpenCode can fold it independently from the verbose snapshot.
|
|
284
|
+
try {
|
|
285
|
+
const autoBlock = autoInjectionMod.buildAutoInjection(events);
|
|
286
|
+
if (autoBlock && autoBlock.length > 0) {
|
|
287
|
+
output.context.push(autoBlock);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// Auto-injection failure must NOT break the snapshot path.
|
|
292
|
+
}
|
|
165
293
|
return snapshot;
|
|
166
294
|
}
|
|
167
295
|
catch {
|
|
@@ -180,6 +308,25 @@ async function createContextModePlugin(ctx) {
|
|
|
180
308
|
const sessionId = input?.sessionID;
|
|
181
309
|
if (!sessionId)
|
|
182
310
|
return;
|
|
311
|
+
// ── OC-1 / CCv1: ROUTING_BLOCK injection ──────────────
|
|
312
|
+
// Inject the <context_window_protection> XML block on the first
|
|
313
|
+
// chat.system.transform per session. This is INDEPENDENT of the
|
|
314
|
+
// resume snapshot path below — routing block must fire even when
|
|
315
|
+
// no prior session row exists. Splice at index 1 (NOT unshift) for
|
|
316
|
+
// the same OpenCode llm.ts:117-128 cache-fold reason as resume.
|
|
317
|
+
if (!routingInjected.has(sessionId) && Array.isArray(output?.system)) {
|
|
318
|
+
try {
|
|
319
|
+
// Visible marker — mirror the resume-snapshot pattern below so
|
|
320
|
+
// users can grep OPENCODE_DEBUG logs to confirm the routing block
|
|
321
|
+
// reached the model (Mickey-class verification path).
|
|
322
|
+
const marker = `<!-- context-mode v${VERSION}: routing block injected (sessionID=${sessionId.slice(0, 8)}) -->\n`;
|
|
323
|
+
output.system.splice(1, 0, marker + routingBlock);
|
|
324
|
+
routingInjected.add(sessionId);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Never break the chat turn on routing-block injection failure.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
183
330
|
if (resumeInjected.has(sessionId))
|
|
184
331
|
return;
|
|
185
332
|
try {
|
package/build/pi-extension.js
CHANGED
|
@@ -14,7 +14,7 @@ import { createHash } from "node:crypto";
|
|
|
14
14
|
import { existsSync, mkdirSync } from "node:fs";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { join, resolve, dirname } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
18
|
import { SessionDB } from "./session/db.js";
|
|
19
19
|
import { extractEvents, extractUserEvents } from "./session/extract.js";
|
|
20
20
|
import { buildResumeSnapshot } from "./session/snapshot.js";
|
|
@@ -45,6 +45,38 @@ const BLOCKED_BASH_PATTERNS = [
|
|
|
45
45
|
// ── Module-level DB singleton ────────────────────────────
|
|
46
46
|
let _db = null;
|
|
47
47
|
let _sessionId = "";
|
|
48
|
+
// Per-session gate: routing block injected at most once per session_id.
|
|
49
|
+
const _routingInjected = new Set();
|
|
50
|
+
// Cached routing-block string (built once per process from hooks/routing-block.mjs).
|
|
51
|
+
let _routingBlock = null;
|
|
52
|
+
async function getRoutingBlock(pluginRoot) {
|
|
53
|
+
if (_routingBlock !== null)
|
|
54
|
+
return _routingBlock;
|
|
55
|
+
try {
|
|
56
|
+
const routingMod = await import(pathToFileURL(join(pluginRoot, "hooks", "routing-block.mjs")).href);
|
|
57
|
+
const namingMod = await import(pathToFileURL(join(pluginRoot, "hooks", "core", "tool-naming.mjs")).href);
|
|
58
|
+
const t = namingMod.createToolNamer("pi");
|
|
59
|
+
_routingBlock = String(routingMod.createRoutingBlock(t));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
_routingBlock = "";
|
|
63
|
+
}
|
|
64
|
+
return _routingBlock;
|
|
65
|
+
}
|
|
66
|
+
// Cached buildAutoInjection (500-token cap, prioritized).
|
|
67
|
+
let _buildAutoInjection = undefined;
|
|
68
|
+
async function getAutoInjection(pluginRoot) {
|
|
69
|
+
if (_buildAutoInjection !== undefined)
|
|
70
|
+
return _buildAutoInjection;
|
|
71
|
+
try {
|
|
72
|
+
const mod = await import(pathToFileURL(join(pluginRoot, "hooks", "auto-injection.mjs")).href);
|
|
73
|
+
_buildAutoInjection = mod.buildAutoInjection;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
_buildAutoInjection = null;
|
|
77
|
+
}
|
|
78
|
+
return _buildAutoInjection ?? null;
|
|
79
|
+
}
|
|
48
80
|
// ── Helpers ──────────────────────────────────────────────
|
|
49
81
|
function getSessionDir() {
|
|
50
82
|
const dir = join(homedir(), ".pi", "context-mode", "sessions");
|
|
@@ -218,8 +250,8 @@ export default function piExtension(pi) {
|
|
|
218
250
|
// Silent — session capture must never break the tool call
|
|
219
251
|
}
|
|
220
252
|
});
|
|
221
|
-
// ── 4. before_agent_start —
|
|
222
|
-
pi.on("before_agent_start", (event) => {
|
|
253
|
+
// ── 4. before_agent_start — Routing + active_memory + resume injection ─
|
|
254
|
+
pi.on("before_agent_start", async (event) => {
|
|
223
255
|
try {
|
|
224
256
|
if (!_sessionId)
|
|
225
257
|
return;
|
|
@@ -231,37 +263,64 @@ export default function piExtension(pi) {
|
|
|
231
263
|
db.insertEvent(_sessionId, ev, "UserPromptSubmit");
|
|
232
264
|
}
|
|
233
265
|
}
|
|
234
|
-
// Check for unconsumed resume snapshot
|
|
235
|
-
const resume = db.getResume(_sessionId);
|
|
236
|
-
if (!resume || resume.consumed)
|
|
237
|
-
return;
|
|
238
|
-
// Build FTS5 active memory from the current prompt
|
|
239
|
-
const stats = db.getSessionStats(_sessionId);
|
|
240
|
-
if ((stats?.compact_count ?? 0) === 0)
|
|
241
|
-
return;
|
|
242
|
-
// Mark resume as consumed so it is not re-injected
|
|
243
|
-
db.markResumeConsumed(_sessionId);
|
|
244
|
-
// Build memory context from recent high-priority events
|
|
245
|
-
const allEvents = db.getEvents(_sessionId, { minPriority: 3, limit: 50 });
|
|
246
|
-
let memoryContext = "";
|
|
247
|
-
if (allEvents.length > 0) {
|
|
248
|
-
const memoryLines = ["<active_memory>"];
|
|
249
|
-
for (const ev of allEvents) {
|
|
250
|
-
memoryLines.push(` <event type="${ev.type}" category="${ev.category}">${ev.data}</event>`);
|
|
251
|
-
}
|
|
252
|
-
memoryLines.push("</active_memory>");
|
|
253
|
-
memoryContext = memoryLines.join("\n");
|
|
254
|
-
}
|
|
255
|
-
// Compose the augmented system prompt
|
|
256
266
|
const existingPrompt = String(event?.systemPrompt ?? "");
|
|
257
267
|
const parts = [];
|
|
258
268
|
if (existingPrompt)
|
|
259
269
|
parts.push(existingPrompt);
|
|
260
|
-
|
|
270
|
+
// Pi-1: Inject routing block once per session (gated by _routingInjected).
|
|
271
|
+
// v1.0.107 — visible marker so Pi users can verify the routing block
|
|
272
|
+
// reached the model (Mickey-class verification path; mirrors OpenCode).
|
|
273
|
+
if (!_routingInjected.has(_sessionId)) {
|
|
274
|
+
const routingBlock = await getRoutingBlock(pluginRoot);
|
|
275
|
+
if (routingBlock) {
|
|
276
|
+
const marker = `<!-- context-mode: routing block injected (sessionID=${String(_sessionId).slice(0, 8)}) -->`;
|
|
277
|
+
parts.push(marker + "\n" + routingBlock);
|
|
278
|
+
_routingInjected.add(_sessionId);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Pi-3 + Pi-4: Always build active_memory (not just post-compact),
|
|
282
|
+
// capped at 500 tokens via buildAutoInjection. Falls back to inline
|
|
283
|
+
// budget loop if the helper is unavailable.
|
|
284
|
+
const activeEvents = db.getEvents(_sessionId, {
|
|
285
|
+
minPriority: 3,
|
|
286
|
+
limit: 50,
|
|
287
|
+
});
|
|
288
|
+
if (activeEvents.length > 0) {
|
|
289
|
+
const buildAuto = await getAutoInjection(pluginRoot);
|
|
290
|
+
let memoryContext = "";
|
|
291
|
+
if (buildAuto) {
|
|
292
|
+
memoryContext = buildAuto(activeEvents.map((e) => ({
|
|
293
|
+
category: String(e.category ?? ""),
|
|
294
|
+
data: String(e.data ?? ""),
|
|
295
|
+
})));
|
|
296
|
+
}
|
|
297
|
+
// Fallback (or if helper produced empty output): inline 500-token cap.
|
|
298
|
+
if (!memoryContext) {
|
|
299
|
+
const memoryLines = ["<active_memory>"];
|
|
300
|
+
let budget = 2000; // ~500 tokens at 4 chars/token
|
|
301
|
+
for (const ev of activeEvents) {
|
|
302
|
+
const line = ` <event type="${ev.type}" category="${ev.category}">${ev.data}</event>`;
|
|
303
|
+
if (line.length > budget)
|
|
304
|
+
break;
|
|
305
|
+
memoryLines.push(line);
|
|
306
|
+
budget -= line.length;
|
|
307
|
+
}
|
|
308
|
+
memoryLines.push("</active_memory>");
|
|
309
|
+
if (memoryLines.length > 2)
|
|
310
|
+
memoryContext = memoryLines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
if (memoryContext)
|
|
313
|
+
parts.push(memoryContext);
|
|
314
|
+
}
|
|
315
|
+
// Resume snapshot (only when present and unconsumed).
|
|
316
|
+
const resume = db.getResume(_sessionId);
|
|
317
|
+
if (resume && !resume.consumed && resume.snapshot) {
|
|
261
318
|
parts.push(resume.snapshot);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
319
|
+
db.markResumeConsumed(_sessionId);
|
|
320
|
+
}
|
|
321
|
+
// Return modified systemPrompt only if we added something beyond existing.
|
|
322
|
+
const baseLen = existingPrompt ? 1 : 0;
|
|
323
|
+
if (parts.length > baseLen) {
|
|
265
324
|
return { systemPrompt: parts.join("\n\n") };
|
|
266
325
|
}
|
|
267
326
|
}
|
|
@@ -269,6 +328,40 @@ export default function piExtension(pi) {
|
|
|
269
328
|
// best effort — never break agent start
|
|
270
329
|
}
|
|
271
330
|
});
|
|
331
|
+
// ── 4b. before_provider_response — capture response metadata ───
|
|
332
|
+
// Pi-2: Register the missing event so providers can record latency,
|
|
333
|
+
// model, and token usage when Pi exposes them. Best-effort only;
|
|
334
|
+
// the handler must never throw or modify the response.
|
|
335
|
+
pi.on("before_provider_response", (event) => {
|
|
336
|
+
try {
|
|
337
|
+
if (!_sessionId)
|
|
338
|
+
return;
|
|
339
|
+
const meta = {
|
|
340
|
+
model: event?.model ?? event?.providerModel,
|
|
341
|
+
provider: event?.provider,
|
|
342
|
+
latencyMs: event?.latencyMs ?? event?.latency,
|
|
343
|
+
tokens: event?.usage ?? event?.tokens,
|
|
344
|
+
};
|
|
345
|
+
// Skip when Pi gives us nothing useful — avoids noise in the DB.
|
|
346
|
+
if (meta.model == null &&
|
|
347
|
+
meta.provider == null &&
|
|
348
|
+
meta.latencyMs == null &&
|
|
349
|
+
meta.tokens == null) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const data = JSON.stringify(meta);
|
|
353
|
+
db.insertEvent(_sessionId, {
|
|
354
|
+
type: "provider_response",
|
|
355
|
+
category: "pi",
|
|
356
|
+
data,
|
|
357
|
+
priority: 1,
|
|
358
|
+
data_hash: createHash("sha256").update(data).digest("hex").slice(0, 16),
|
|
359
|
+
}, "PostToolUse");
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// best effort — never break provider response
|
|
363
|
+
}
|
|
364
|
+
});
|
|
272
365
|
// ── 5. session_before_compact — Build resume snapshot ──
|
|
273
366
|
pi.on("session_before_compact", () => {
|
|
274
367
|
try {
|
|
@@ -305,6 +398,7 @@ export default function piExtension(pi) {
|
|
|
305
398
|
_db.cleanupOldSessions(7);
|
|
306
399
|
}
|
|
307
400
|
_db = null;
|
|
401
|
+
_routingInjected.clear();
|
|
308
402
|
_sessionId = "";
|
|
309
403
|
}
|
|
310
404
|
catch {
|
package/build/server.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ interface BatchExecutor {
|
|
|
37
37
|
timedOut?: boolean;
|
|
38
38
|
}>;
|
|
39
39
|
}
|
|
40
|
+
export declare function buildBatchNodeOptionsPrefix(shellPath: string, preloadPath: string): string;
|
|
40
41
|
/**
|
|
41
42
|
* Execute batch commands. concurrency=1 preserves the legacy serial path
|
|
42
43
|
* (shared timeout budget + cascading skip-on-timeout). concurrency>1 runs
|
package/build/server.js
CHANGED
|
@@ -675,6 +675,24 @@ export function formatBatchQueryResults(store, queries, source, maxOutput = 80 *
|
|
|
675
675
|
sections.push(`\n> **Tip:** Results are scoped to this batch only. To search across all indexed sources, use \`ctx_search(queries: [...])\`.`);
|
|
676
676
|
return sections;
|
|
677
677
|
}
|
|
678
|
+
function quotePosixSingle(value) {
|
|
679
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
680
|
+
}
|
|
681
|
+
function quotePowerShellSingle(value) {
|
|
682
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
683
|
+
}
|
|
684
|
+
export function buildBatchNodeOptionsPrefix(shellPath, preloadPath) {
|
|
685
|
+
const option = `--require ${preloadPath}`;
|
|
686
|
+
const shell = shellPath.toLowerCase();
|
|
687
|
+
const base = shell.split(/[\\/]/).pop() ?? shell;
|
|
688
|
+
if (shell.includes("powershell") || shell.includes("pwsh")) {
|
|
689
|
+
return `$env:NODE_OPTIONS=${quotePowerShellSingle(option)}; `;
|
|
690
|
+
}
|
|
691
|
+
if (base === "cmd" || base === "cmd.exe") {
|
|
692
|
+
return `set "NODE_OPTIONS=${option.replace(/"/g, '""')}" && `;
|
|
693
|
+
}
|
|
694
|
+
return `NODE_OPTIONS=${quotePosixSingle(option)} `;
|
|
695
|
+
}
|
|
678
696
|
function formatCommandOutput(label, raw, onFsBytes) {
|
|
679
697
|
let output = raw || "(no output)";
|
|
680
698
|
const fsMatches = output.matchAll(/__CM_FS__:(\d+)/g);
|
|
@@ -2070,7 +2088,7 @@ server.registerTool("ctx_batch_execute", {
|
|
|
2070
2088
|
// Inject NODE_OPTIONS for FS read tracking in spawned Node processes.
|
|
2071
2089
|
// The executor denies NODE_OPTIONS in its env (security), so we set it
|
|
2072
2090
|
// as an inline shell prefix. This only affects child `node` invocations.
|
|
2073
|
-
const nodeOptsPrefix =
|
|
2091
|
+
const nodeOptsPrefix = buildBatchNodeOptionsPrefix(runtimes.shell, CM_FS_PRELOAD);
|
|
2074
2092
|
// Full stdout is preserved per-command and indexed into FTS5 (Issue #61, #197).
|
|
2075
2093
|
// Concurrency>1 switches to a worker pool with per-command timeouts.
|
|
2076
2094
|
const { outputs: perCommandOutputs, timedOut } = await runBatchCommands(commands, {
|
|
@@ -45,7 +45,7 @@ export declare function resetIterationLoopState(): void;
|
|
|
45
45
|
* Accepts the raw hook JSON shape (snake_case keys) as received from stdin.
|
|
46
46
|
* Returns an array of zero or more SessionEvents. Never throws.
|
|
47
47
|
*/
|
|
48
|
-
export declare function extractEvents(
|
|
48
|
+
export declare function extractEvents(rawInput: HookInput): SessionEvent[];
|
|
49
49
|
/**
|
|
50
50
|
* Extract session events from a UserPromptSubmit hook input (user message text).
|
|
51
51
|
*
|
package/build/session/extract.js
CHANGED
|
@@ -850,14 +850,59 @@ export function resetIterationLoopState() {
|
|
|
850
850
|
callHistory.length = 0;
|
|
851
851
|
}
|
|
852
852
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
853
|
+
/**
|
|
854
|
+
* Map platform-native tool names (Qwen Code, Gemini CLI, OpenCode, etc.) to the
|
|
855
|
+
* canonical Claude Code names this extractor branches on. Without this, Qwen's
|
|
856
|
+
* `run_shell_command` events would silently produce zero git/cwd/env extractions.
|
|
857
|
+
*
|
|
858
|
+
* Evidence: refs/platforms/qwen-code/packages/core/src/tools/tool-names.ts
|
|
859
|
+
*/
|
|
860
|
+
const TOOL_NAME_NORMALIZE = {
|
|
861
|
+
// Qwen Code / Gemini CLI native names
|
|
862
|
+
run_shell_command: "Bash",
|
|
863
|
+
read_file: "Read",
|
|
864
|
+
read_many_files: "Read",
|
|
865
|
+
grep_search: "Grep",
|
|
866
|
+
search_file_content: "Grep",
|
|
867
|
+
web_fetch: "WebFetch",
|
|
868
|
+
write_file: "Write",
|
|
869
|
+
edit: "Edit",
|
|
870
|
+
glob: "Glob",
|
|
871
|
+
todo_write: "TodoWrite",
|
|
872
|
+
ask_user_question: "AskUserQuestion",
|
|
873
|
+
list_directory: "LS",
|
|
874
|
+
save_memory: "Memory",
|
|
875
|
+
skill: "Skill",
|
|
876
|
+
exit_plan_mode: "ExitPlanMode",
|
|
877
|
+
agent: "Agent",
|
|
878
|
+
// OpenCode native names
|
|
879
|
+
bash: "Bash",
|
|
880
|
+
view: "Read",
|
|
881
|
+
grep: "Grep",
|
|
882
|
+
fetch: "WebFetch",
|
|
883
|
+
// Codex CLI
|
|
884
|
+
shell: "Bash",
|
|
885
|
+
shell_command: "Bash",
|
|
886
|
+
exec_command: "Bash",
|
|
887
|
+
"container.exec": "Bash",
|
|
888
|
+
local_shell: "Bash",
|
|
889
|
+
grep_files: "Grep",
|
|
890
|
+
};
|
|
891
|
+
function normalizeHookInput(input) {
|
|
892
|
+
const normalized = TOOL_NAME_NORMALIZE[input.tool_name];
|
|
893
|
+
if (!normalized || normalized === input.tool_name)
|
|
894
|
+
return input;
|
|
895
|
+
return { ...input, tool_name: normalized };
|
|
896
|
+
}
|
|
853
897
|
/**
|
|
854
898
|
* Extract session events from a PostToolUse hook input.
|
|
855
899
|
*
|
|
856
900
|
* Accepts the raw hook JSON shape (snake_case keys) as received from stdin.
|
|
857
901
|
* Returns an array of zero or more SessionEvents. Never throws.
|
|
858
902
|
*/
|
|
859
|
-
export function extractEvents(
|
|
903
|
+
export function extractEvents(rawInput) {
|
|
860
904
|
try {
|
|
905
|
+
const input = normalizeHookInput(rawInput);
|
|
861
906
|
const events = [];
|
|
862
907
|
// File + Rule (handles Read/Edit/Write)
|
|
863
908
|
events.push(...extractFileAndRule(input));
|