mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 14:03:49 +00:00
fix: RPC mode session management not saving sessions
Since version 0.9.0, RPC mode (--mode rpc) was not saving messages to session files. The agent.subscribe() call with session management logic was only present in the TUI renderer after it was refactored. RPC mode now properly saves sessions just like interactive mode. Added test for RPC mode session management to prevent regression. Fixes #83 Thanks @kiliman for reporting this issue!
This commit is contained in:
parent
5fa30b8add
commit
d2b60f11eb
14 changed files with 284 additions and 123 deletions
|
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
## [0.11.2] - 2025-12-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- **RPC Mode Session Management**: Fixed session files not being saved in RPC mode (`--mode rpc`). Since version 0.9.0, the `agent.subscribe()` call with session management logic was only present in the TUI renderer, causing RPC mode to skip saving messages to session files. RPC mode now properly saves sessions just like interactive mode. ([#83](https://github.com/badlogic/pi-mono/issues/83))
|
||||
|
||||
## [0.11.1] - 2025-11-29
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"version": "0.11.1",
|
||||
"version": "0.11.2",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
|
@ -22,9 +22,9 @@
|
|||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent-core": "^0.11.1",
|
||||
"@mariozechner/pi-ai": "^0.11.1",
|
||||
"@mariozechner/pi-tui": "^0.11.1",
|
||||
"@mariozechner/pi-agent-core": "^0.11.2",
|
||||
"@mariozechner/pi-ai": "^0.11.2",
|
||||
"@mariozechner/pi-tui": "^0.11.2",
|
||||
"chalk": "^5.5.0",
|
||||
"diff": "^8.0.2",
|
||||
"glob": "^11.0.3"
|
||||
|
|
|
|||
|
|
@ -806,10 +806,24 @@ async function runSingleShotMode(
|
|||
}
|
||||
}
|
||||
|
||||
async function runRpcMode(agent: Agent, _sessionManager: SessionManager): Promise<void> {
|
||||
// Subscribe to all events and output as JSON
|
||||
agent.subscribe((event) => {
|
||||
async function runRpcMode(agent: Agent, sessionManager: SessionManager): Promise<void> {
|
||||
// Subscribe to all events and output as JSON (same pattern as tui-renderer)
|
||||
agent.subscribe(async (event) => {
|
||||
console.log(JSON.stringify(event));
|
||||
|
||||
// Save messages to session
|
||||
if (event.type === "message_end") {
|
||||
sessionManager.saveMessage(event.message);
|
||||
|
||||
// Yield to microtask queue to allow agent state to update
|
||||
// (tui-renderer does this implicitly via await handleEvent)
|
||||
await Promise.resolve();
|
||||
|
||||
// Check if we should initialize session now (after first user+assistant exchange)
|
||||
if (sessionManager.shouldInitializeSession(agent.state.messages)) {
|
||||
sessionManager.startSession(agent.state);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for JSON input on stdin
|
||||
|
|
|
|||
134
packages/coding-agent/test/rpc.test.ts
Normal file
134
packages/coding-agent/test/rpc.test.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import * as readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AgentEvent } from "@mariozechner/pi-agent-core";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* RPC mode tests.
|
||||
* Regression test for issue #83: https://github.com/badlogic/pi-mono/issues/83
|
||||
*/
|
||||
describe("RPC mode", () => {
|
||||
let agent: ChildProcess;
|
||||
let sessionDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a unique temp directory for sessions
|
||||
sessionDir = join(tmpdir(), `pi-rpc-test-${Date.now()}`);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Kill the agent if still running
|
||||
if (agent && !agent.killed) {
|
||||
agent.kill("SIGKILL");
|
||||
}
|
||||
// Clean up session directory
|
||||
if (sessionDir && existsSync(sessionDir)) {
|
||||
rmSync(sessionDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("should save messages to session file", async () => {
|
||||
// Spawn agent in RPC mode with custom session directory
|
||||
agent = spawn("node", ["dist/cli.js", "--mode", "rpc"], {
|
||||
cwd: join(__dirname, ".."),
|
||||
env: {
|
||||
...process.env,
|
||||
PI_CODING_AGENT_DIR: sessionDir,
|
||||
},
|
||||
});
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
|
||||
// Parse agent events
|
||||
const rl = readline.createInterface({ input: agent.stdout!, terminal: false });
|
||||
|
||||
// Collect stderr for debugging
|
||||
let stderr = "";
|
||||
agent.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
// Wait for agent_end which signals the full prompt/response cycle is complete
|
||||
const waitForAgentEnd = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error("Timeout waiting for agent_end")), 60000);
|
||||
|
||||
rl.on("line", (line: string) => {
|
||||
try {
|
||||
const event = JSON.parse(line) as AgentEvent;
|
||||
events.push(event);
|
||||
|
||||
// agent_end means the full prompt cycle completed (user msg + assistant response)
|
||||
if (event.type === "agent_end") {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
} catch {
|
||||
// Ignore non-JSON lines
|
||||
}
|
||||
});
|
||||
|
||||
rl.on("close", () => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error("Agent stdout closed before agent_end"));
|
||||
});
|
||||
});
|
||||
|
||||
// Send a simple prompt - the LLM will respond
|
||||
agent.stdin!.write(JSON.stringify({ type: "prompt", message: "Reply with just the word 'hello'" }) + "\n");
|
||||
|
||||
// Wait for full prompt/response cycle to complete
|
||||
await waitForAgentEnd;
|
||||
|
||||
// Check that message_end events were emitted
|
||||
const messageEndEvents = events.filter((e) => e.type === "message_end");
|
||||
expect(messageEndEvents.length).toBeGreaterThanOrEqual(2); // user + assistant
|
||||
|
||||
// Wait a bit for file writes to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
// Kill the agent gracefully
|
||||
agent.kill("SIGTERM");
|
||||
|
||||
// Find and verify the session file
|
||||
const sessionsPath = join(sessionDir, "sessions");
|
||||
expect(existsSync(sessionsPath), `Sessions path should exist: ${sessionsPath}. Stderr: ${stderr}`).toBe(true);
|
||||
|
||||
// Find the session directory (it's based on cwd)
|
||||
const sessionDirs = readdirSync(sessionsPath);
|
||||
expect(sessionDirs.length, `Should have at least one session dir. Stderr: ${stderr}`).toBeGreaterThan(0);
|
||||
|
||||
const cwdSessionDir = join(sessionsPath, sessionDirs[0]);
|
||||
const allFiles = readdirSync(cwdSessionDir);
|
||||
const sessionFiles = allFiles.filter((f) => f.endsWith(".jsonl"));
|
||||
expect(
|
||||
sessionFiles.length,
|
||||
`Should have exactly one session file. Dir: ${cwdSessionDir}, Files: ${JSON.stringify(allFiles)}, Stderr: ${stderr}`,
|
||||
).toBe(1);
|
||||
|
||||
// Read and verify session content
|
||||
const sessionContent = readFileSync(join(cwdSessionDir, sessionFiles[0]), "utf8");
|
||||
const lines = sessionContent.trim().split("\n");
|
||||
|
||||
// Should have session header and at least 2 messages (user + assistant)
|
||||
expect(lines.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
const entries = lines.map((line) => JSON.parse(line));
|
||||
|
||||
// First entry should be session header
|
||||
expect(entries[0].type).toBe("session");
|
||||
|
||||
// Should have user and assistant messages
|
||||
const messages = entries.filter((e: { type: string }) => e.type === "message");
|
||||
expect(messages.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const roles = messages.map((m: { message: { role: string } }) => m.message.role);
|
||||
expect(roles).toContain("user");
|
||||
expect(roles).toContain("assistant");
|
||||
}, 90000);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue