Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kcode",
"version": "2.10.40",
"version": "2.10.41",
"description": "AI-powered coding assistant for the terminal - by Astrolexis",
"author": "Astrolexis",
"module": "src/index.ts",
Expand Down
11 changes: 6 additions & 5 deletions src/core/bash-spawn-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,15 @@ describe("runSpawnPreflight", () => {
expect(r!.refused).toBe(true);
expect(r!.report).toContain("already in use");
expect(r!.report).toContain(String(server.port));
expect(r!.report).toContain("Options:");
});

test("refusal report mentions kill and port-change options", () => {
test("port refusal includes AUTHORIZED RECOVERY block (phase 6)", () => {
server = Bun.serve({ port: 0, fetch: () => new Response("hi") });
const r = runSpawnPreflight(`next dev --port ${server.port}`, process.cwd());
expect(r!.report).toMatch(/kill/i);
expect(r!.report).toMatch(/different port/i);
expect(r!.report).toMatch(/reuse/i);
expect(r!.report).toContain("AUTHORIZED RECOVERY");
expect(r!.report).toContain("WITHOUT asking the user");
expect(r!.report).toMatch(/Step 1[\s\S]*kill/);
expect(r!.report).toMatch(/Step \d[\s\S]*retry/);
expect(r!.report).toContain("ALTERNATIVE");
});
});
44 changes: 33 additions & 11 deletions src/core/bash-spawn-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,22 @@ export function runSpawnPreflight(
lines.push(` occupant: detected by ss but PID hidden (insufficient privileges)`);
}
lines.push(` Spawning ${detection.framework} on this port would race and fail.`);
lines.push(` Options:`);
lines.push(` - reuse the existing server (it may already serve what you need)`);
lines.push(` - kill the occupant first (e.g. 'kill ${occupant > 0 ? occupant : "<pid>"}')`);
lines.push(` - pick a different port (PORT=N or --port N)`);
lines.push(``);
lines.push(` AUTHORIZED RECOVERY (you may run these as your next tool calls`);
lines.push(` WITHOUT asking the user — they are reversible system maintenance):`);
if (occupant > 0) {
lines.push(` Step 1 — kill the occupant:`);
lines.push(` kill ${occupant}`);
lines.push(` Step 2 — wait for the port to release:`);
lines.push(` sleep 1`);
lines.push(` Step 3 — retry the original command.`);
lines.push(``);
lines.push(` ALTERNATIVE: pick a different port (PORT=N or --port N) if you suspect`);
lines.push(` the occupant is a dev server the user is actively using elsewhere.`);
} else {
lines.push(` Step 1 — pick a different port: change PORT=N or --port N in the command.`);
lines.push(` Step 2 — retry the spawn with the new port.`);
}
return { refused: true, report: lines.join("\n") };
}
}
Expand All @@ -172,14 +184,24 @@ export function runSpawnPreflight(
lines.push(`✗ inotify is saturated: ${ino.used}/${ino.limit} instances used (${Math.round(ino.ratio * 100)}%).`);
lines.push(` Spawning a watch-mode dev server right now would EMFILE on boot`);
lines.push(` and you'd see "Watchpack Error (watcher): EMFILE: too many open files".`);
lines.push(` The previous server would also lose hot-reload but appear to keep running.`);
lines.push(` Options:`);
lines.push(` - kill leaked dev servers from this UID:`);
lines.push(` pgrep -af 'next-server|bun --watch|nodemon' && pkill -9 -u $USER -f 'next-server'`);
lines.push(` - raise the limit (one-shot, requires sudo):`);
lines.push(``);
lines.push(` AUTHORIZED RECOVERY (you may run these as your next tool calls`);
lines.push(` WITHOUT asking the user — they are reversible system maintenance):`);
lines.push(` Step 1 — reclaim leaked watchers from previous KCode sessions:`);
lines.push(` pkill -9 -u $USER -f 'next-server|bun --watch|nodemon|vite' || true`);
lines.push(` Step 2 — wait briefly for the kernel to release inotify slots:`);
lines.push(` sleep 1`);
lines.push(` Step 3 — retry the original spawn.`);
lines.push(``);
lines.push(` After Step 3 the spawn-verifier will probe the server and report success`);
lines.push(` or a different failure. If usage is still ≥85% after Step 1, the leaks are`);
lines.push(` owned by another UID and you must instead ask the user to run:`);
lines.push(` sudo sysctl -w fs.inotify.max_user_instances=1024`);
lines.push(` - persist the limit:`);
lines.push(` echo 'fs.inotify.max_user_instances=1024' | sudo tee /etc/sysctl.d/99-inotify.conf`);
lines.push(``);
lines.push(` These pkill targets (next-server, bun --watch, nodemon, vite) are dev-mode`);
lines.push(` watchers that should be ephemeral. They do NOT touch user code, files, git,`);
lines.push(` or any production process. The pkill is not destructive — it is the same`);
lines.push(` cleanup the user would do manually.`);
void cwd; // keep param for future use
return { refused: true, report: lines.join("\n") };
}
Expand Down
54 changes: 54 additions & 0 deletions src/core/operator-recovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Tests for phase 6 — AUTHORIZED RECOVERY language in operator-mind refusals
// and in the system prompt.

import { describe, expect, test } from "bun:test";
import { runSpawnPreflight } from "./bash-spawn-preflight";
import { buildOperatorRecoveryGuidance } from "./system-prompt-layers";

describe("buildOperatorRecoveryGuidance", () => {
test("explains how to read AUTHORIZED RECOVERY blocks", () => {
const out = buildOperatorRecoveryGuidance();
expect(out).toContain("AUTHORIZED RECOVERY");
expect(out).toMatch(/execute the steps yourself/i);
expect(out).toMatch(/do not paste them into[\s\S]*message for the user/i);
});

test("tells the model to use the exact command, not a variant", () => {
const out = buildOperatorRecoveryGuidance();
expect(out).toMatch(/do not invent your own variations/i);
});

test("requires the retry step after recovery", () => {
const out = buildOperatorRecoveryGuidance();
expect(out).toMatch(/recovery without retry is incomplete/i);
});

test("makes the failure mode it prevents explicit", () => {
const out = buildOperatorRecoveryGuidance();
expect(out).toMatch(/options.*user actions/i);
});

test("ends with an unambiguous directive", () => {
const out = buildOperatorRecoveryGuidance();
expect(out).toContain("You are the operator. Operate.");
});
});

describe("inotify refusal AUTHORIZED RECOVERY language", () => {
test("inotify saturation refusal is action-oriented (when system saturated)", () => {
// This test only checks the SHAPE of the report when the refusal
// fires. On a healthy host inotify is below threshold and the
// function returns null — that's fine, we skip the assertion.
const r = runSpawnPreflight("next dev --port 59123", process.cwd());
if (!r) return; // host healthy — nothing to assert
if (!r.report.includes("inotify")) return; // refusal was port not inotify

expect(r.report).toContain("AUTHORIZED RECOVERY");
expect(r.report).toContain("WITHOUT asking the user");
expect(r.report).toMatch(/Step 1[\s\S]*pkill/);
expect(r.report).toMatch(/Step 2[\s\S]*sleep/);
expect(r.report).toMatch(/Step 3[\s\S]*retry/);
expect(r.report).toMatch(/sudo sysctl/);
expect(r.report).toContain("not destructive");
});
});
68 changes: 68 additions & 0 deletions src/core/system-prompt-layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,74 @@ These rules are NON-NEGOTIABLE. Violating them is a failure.
9. **NEVER create formatted sections with headers** (## / ### / ####) in conversational responses. Headers are for code comments and documentation files only.`;
}

/**
* Build the operator-recovery guidance block.
*
* Tells the model how to read and act on `AUTHORIZED RECOVERY` blocks
* that come back from KCode's operator-mind preflight refusals. Without
* this, models tend to interpret recovery options as suggestions for
* the user and politely give up instead of executing the cleanup
* themselves — which leaks orphaned dev servers and aborts the task.
*/
export function buildOperatorRecoveryGuidance(): string {
return `# Operator Recovery — How to Read AUTHORIZED RECOVERY Blocks

When a Bash tool call (or any other tool) returns an \`is_error: true\`
result whose body contains an \`AUTHORIZED RECOVERY\` block, that block
is NOT a list of suggestions for the user. It is an instruction set
KCode has pre-approved for YOU to execute as your next tool calls,
without asking the user for confirmation.

The block follows this shape:
AUTHORIZED RECOVERY (you may run these as your next tool calls
WITHOUT asking the user — they are reversible system maintenance):
Step 1 — <action>:
<command>
Step 2 — <wait/verify>:
<command>
Step 3 — retry the original command.

Your obligation when you see one:

1. **Execute the steps yourself, in order.** Do not paste them into a
message for the user. Do not ask "should I run this?" The block
says "you may run" because KCode has already evaluated the safety
of these specific commands and judged them safe — they touch only
ephemeral system state (leaked dev servers, locked ports, stale
sockets), never user code, files, git, or production processes.

2. **Do not invent your own variations.** If the block says
\`pkill -9 -u $USER -f 'next-server|bun --watch|nodemon|vite'\`,
run exactly that. Do not narrow the pattern, do not add \`sudo\`,
do not target by PID instead. The exact command was chosen for
safety.

3. **Run the retry step (the original failing command) after recovery.**
Recovery without retry is incomplete. After Step 3, the spawn-verifier
will probe again and either confirm success or report a different
problem — at which point you continue diagnosing from there.

4. **If the block has an ALTERNATIVE clause**, prefer the AUTHORIZED
RECOVERY path first. Use the alternative only when there is concrete
reason to believe the recovery would harm something the user wants
alive (e.g. they told you in this conversation that they have a
dev server running on the colliding port for another project).

5. **If the recovery itself fails** (e.g. pkill returns non-zero, or
the resource is still saturated), THAT is the moment to ask the
user — not before. Tell them the recovery you tried, what happened,
and the specific manual step they need to take (usually a sudo
command).

The thing this guidance prevents: in earlier KCode versions the model
would see "inotify saturated, options: kill leaked watchers", interpret
"options" as "user actions", and respond with "the spawn was prepared
but blocked. You can run pkill ... to clean up." That left the user
with a task that KCode itself was supposed to finish. The
AUTHORIZED RECOVERY block exists specifically to remove that ambiguity.
You are the operator. Operate.`;
}

/**
* Build auto-memory instructions section.
*/
Expand Down
6 changes: 6 additions & 0 deletions src/core/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
buildGitInstructions,
buildIdentity,
buildMetacognition,
buildOperatorRecoveryGuidance,
buildToneAndOutput,
buildToolInstructions,
} from "./system-prompt-layers";
Expand Down Expand Up @@ -156,6 +157,11 @@ RULES:
priority: SectionPriority.HIGH,
label: "tone",
});
sections.push({
content: buildOperatorRecoveryGuidance(),
priority: SectionPriority.HIGH,
label: "operator-recovery",
});
if (config.thinking) {
sections.push({
content: `## Extended Reasoning
Expand Down
Loading