Phase 14: B-002 pre-fix RPC repro — filter snapshots and readSettings fix
RPC E2E with debug shows registered present at session_compact but planned=[] because kept still contains the skill block; registered=[] still drops skills absent from kept. Sync file readSettings avoids RPC hook deadlock on SettingsManager/isProjectTrusted. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 14 manual repro (B-002 pre-fix): RPC flow with skillReinject.debug.
|
||||
* Captures diag lines from extension stderr (console.error) and extension_ui notify.
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
|
||||
const skillPath = resolve(process.env.B002_SKILL_PATH ?? `${process.env.HOME}/.cursor/skills/fup-blame-commits`);
|
||||
const extensionPath = join(repoRoot, "src/index.ts");
|
||||
const globalSettingsPath = join(process.env.HOME ?? "", ".pi/agent/settings.json");
|
||||
const globalBackupPath = `${globalSettingsPath}.b002-backup`;
|
||||
|
||||
function patchGlobalSettings() {
|
||||
if (!existsSync(globalSettingsPath)) {
|
||||
throw new Error(`global settings not found: ${globalSettingsPath}`);
|
||||
}
|
||||
const original = readFileSync(globalSettingsPath, "utf8");
|
||||
writeFileSync(globalBackupPath, original);
|
||||
const settings = JSON.parse(original);
|
||||
settings.skillReinject = {
|
||||
...settings.skillReinject,
|
||||
enabled: true,
|
||||
debug: true,
|
||||
autoCompactIntegration: "defer",
|
||||
};
|
||||
writeFileSync(globalSettingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
||||
return original;
|
||||
}
|
||||
|
||||
function restoreGlobalSettings(original) {
|
||||
writeFileSync(globalSettingsPath, original);
|
||||
rmSync(globalBackupPath, { force: true });
|
||||
}
|
||||
|
||||
const sessionDir = mkdtempSync(join(tmpdir(), "b002-session-"));
|
||||
let originalSettings;
|
||||
try {
|
||||
originalSettings = patchGlobalSettings();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
const diagNotifies = [];
|
||||
let compactResponse;
|
||||
let statusBeforeCompact;
|
||||
let statusAfterCompact;
|
||||
|
||||
function send(child, cmd) {
|
||||
const id = `req-${nextId++}`;
|
||||
child.stdin.write(`${JSON.stringify({ id, ...cmd })}\n`);
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
pending.delete(id);
|
||||
reject(new Error(`timeout waiting for ${cmd.type} (${id})`));
|
||||
}, Number(process.env.B002_STEP_TIMEOUT_MS ?? 180_000));
|
||||
pending.set(id, {
|
||||
resolve: (value) => {
|
||||
clearTimeout(timer);
|
||||
resolvePromise(value);
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function captureDiagLine(line) {
|
||||
if (line.includes("skill-reinject [session_compact]:") || line.includes("skill-reinject [before_agent_start]:")) {
|
||||
diagNotifies.push(line.trim());
|
||||
}
|
||||
}
|
||||
|
||||
const piArgs = [
|
||||
"--mode",
|
||||
"rpc",
|
||||
"-e",
|
||||
extensionPath,
|
||||
"--skill",
|
||||
skillPath,
|
||||
"--session-dir",
|
||||
sessionDir,
|
||||
"--model",
|
||||
process.env.B002_MODEL ?? "Eltex-Coder-Senior",
|
||||
"--no-session",
|
||||
];
|
||||
|
||||
const child = spawn("pi", piArgs, {
|
||||
cwd: repoRoot,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
let stdoutBuffer = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdoutBuffer += chunk.toString("utf8");
|
||||
let newlineIndex = stdoutBuffer.indexOf("\n");
|
||||
while (newlineIndex >= 0) {
|
||||
const line = stdoutBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
||||
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
||||
if (!line.trim()) {
|
||||
newlineIndex = stdoutBuffer.indexOf("\n");
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const record = JSON.parse(line);
|
||||
if (record.type === "extension_ui_request" && record.method === "notify") {
|
||||
if (typeof record.message === "string") {
|
||||
captureDiagLine(record.message);
|
||||
}
|
||||
}
|
||||
if (record.type === "message" && record.message?.role === "assistant") {
|
||||
const text = JSON.stringify(record.message.content ?? "");
|
||||
if (text.includes("skill-reinject:")) {
|
||||
if (!statusBeforeCompact && text.includes("tracked:")) {
|
||||
statusBeforeCompact = text;
|
||||
} else if (text.includes("tracked:")) {
|
||||
statusAfterCompact = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (record.type === "response" && record.id && pending.has(record.id)) {
|
||||
pending.get(record.id).resolve(record);
|
||||
pending.delete(record.id);
|
||||
}
|
||||
} catch {
|
||||
// ignore non-JSON stdout
|
||||
}
|
||||
newlineIndex = stdoutBuffer.indexOf("\n");
|
||||
}
|
||||
});
|
||||
|
||||
const stderrChunks = [];
|
||||
child.stderr.on("data", (chunk) => {
|
||||
const text = chunk.toString("utf8");
|
||||
stderrChunks.push(chunk);
|
||||
for (const line of text.split("\n")) {
|
||||
captureDiagLine(line);
|
||||
}
|
||||
});
|
||||
|
||||
child.on("close", () => {
|
||||
for (const { reject } of pending.values()) {
|
||||
reject(new Error("pi process exited"));
|
||||
}
|
||||
pending.clear();
|
||||
});
|
||||
|
||||
async function runFlow() {
|
||||
await new Promise((resolvePromise) => setTimeout(resolvePromise, 3000));
|
||||
|
||||
await send(child, { type: "prompt", message: "/skill-reinject on" });
|
||||
await send(child, { type: "prompt", message: "/skill:fup-blame-commits" });
|
||||
await send(child, { type: "prompt", message: "Reply with exactly: ack" });
|
||||
await send(child, { type: "prompt", message: "/skill-reinject" });
|
||||
|
||||
compactResponse = await send(child, { type: "compact" });
|
||||
await send(child, { type: "prompt", message: "/skill-reinject" });
|
||||
await send(child, { type: "prompt", message: "Reply with exactly: after-compact" });
|
||||
}
|
||||
|
||||
let flowError;
|
||||
let exitCode = 1;
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
runFlow().then(() => child.stdin.end()),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(`global timeout after ${process.env.B002_TIMEOUT_MS ?? 360_000}ms`)),
|
||||
Number(process.env.B002_TIMEOUT_MS ?? 360_000),
|
||||
),
|
||||
),
|
||||
]);
|
||||
exitCode = await new Promise((resolvePromise) => {
|
||||
child.on("close", (code) => resolvePromise(code ?? 1));
|
||||
});
|
||||
} catch (error) {
|
||||
flowError = error instanceof Error ? error.message : String(error);
|
||||
child.kill("SIGTERM");
|
||||
} finally {
|
||||
restoreGlobalSettings(originalSettings);
|
||||
rmSync(sessionDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
||||
const parsedDiag = diagNotifies.map((line) => {
|
||||
const match = line.match(/skill-reinject \[(session_compact|before_agent_start)\]: (.+)$/);
|
||||
if (!match) {
|
||||
return { raw: line };
|
||||
}
|
||||
try {
|
||||
return { phase: match[1], snapshot: JSON.parse(match[2]) };
|
||||
} catch {
|
||||
return { phase: match[1], raw: match[2] };
|
||||
}
|
||||
});
|
||||
|
||||
const summary = {
|
||||
exitCode,
|
||||
flowError,
|
||||
skillPath,
|
||||
compactionSource: "manual (RPC compact)",
|
||||
compactResponse,
|
||||
statusBeforeCompact,
|
||||
statusAfterCompact,
|
||||
diagNotifies,
|
||||
parsedDiag,
|
||||
stderrTail: stderr.trim().split("\n").filter((l) => !l.includes("Llama.cpp")).slice(-15).join("\n"),
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
process.exit(diagNotifies.length > 0 && !flowError ? 0 : 1);
|
||||
Reference in New Issue
Block a user