Rename readonly reviewer runtime remnants
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ store-*/
|
|||||||
data/
|
data/
|
||||||
data-*/
|
data-*/
|
||||||
logs/
|
logs/
|
||||||
|
.ejclaw-reviewer-runtime/
|
||||||
|
|
||||||
# Groups - only track base structure and specific CLAUDE.md files
|
# Groups - only track base structure and specific CLAUDE.md files
|
||||||
groups-*/
|
groups-*/
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ejclaw-agent-runner",
|
"name": "ejclaw-agent-runner",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Container-side agent runner for EJClaw",
|
"description": "Agent runner subprocess for EJClaw",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
} from './reviewer-runtime.js';
|
} from './reviewer-runtime.js';
|
||||||
import { selectCompactMemoriesFromSummary } from './memory-selection.js';
|
import { selectCompactMemoriesFromSummary } from './memory-selection.js';
|
||||||
|
|
||||||
interface ContainerInput {
|
interface RunnerInput {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
groupFolder: string;
|
groupFolder: string;
|
||||||
@@ -47,7 +47,7 @@ interface ContainerInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Mirrors AgentOutput in src/agent-runner.ts (separate package, can't import directly). */
|
/** Mirrors AgentOutput in src/agent-runner.ts (separate package, can't import directly). */
|
||||||
interface ContainerOutput {
|
interface RunnerOutput {
|
||||||
status: 'success' | 'error';
|
status: 'success' | 'error';
|
||||||
phase?: 'progress' | 'final' | 'tool-activity' | 'intermediate';
|
phase?: 'progress' | 'final' | 'tool-activity' | 'intermediate';
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
@@ -199,7 +199,7 @@ async function readStdin(): Promise<string> {
|
|||||||
const OUTPUT_START_MARKER = '---EJCLAW_OUTPUT_START---';
|
const OUTPUT_START_MARKER = '---EJCLAW_OUTPUT_START---';
|
||||||
const OUTPUT_END_MARKER = '---EJCLAW_OUTPUT_END---';
|
const OUTPUT_END_MARKER = '---EJCLAW_OUTPUT_END---';
|
||||||
|
|
||||||
function writeOutput(output: ContainerOutput): void {
|
function writeOutput(output: RunnerOutput): void {
|
||||||
console.log(OUTPUT_START_MARKER);
|
console.log(OUTPUT_START_MARKER);
|
||||||
console.log(JSON.stringify(output));
|
console.log(JSON.stringify(output));
|
||||||
console.log(OUTPUT_END_MARKER);
|
console.log(OUTPUT_END_MARKER);
|
||||||
@@ -207,7 +207,7 @@ function writeOutput(output: ContainerOutput): void {
|
|||||||
|
|
||||||
function normalizeStructuredOutput(result: string | null): {
|
function normalizeStructuredOutput(result: string | null): {
|
||||||
result: string | null;
|
result: string | null;
|
||||||
output?: ContainerOutput['output'];
|
output?: RunnerOutput['output'];
|
||||||
} {
|
} {
|
||||||
if (typeof result !== 'string' || result.length === 0) {
|
if (typeof result !== 'string' || result.length === 0) {
|
||||||
return { result };
|
return { result };
|
||||||
@@ -545,7 +545,7 @@ async function runQuery(
|
|||||||
prompt: string,
|
prompt: string,
|
||||||
sessionId: string | undefined,
|
sessionId: string | undefined,
|
||||||
mcpServerPath: string,
|
mcpServerPath: string,
|
||||||
containerInput: ContainerInput,
|
runnerInput: RunnerInput,
|
||||||
sdkEnv: Record<string, string | undefined>,
|
sdkEnv: Record<string, string | undefined>,
|
||||||
reviewerRuntime: boolean,
|
reviewerRuntime: boolean,
|
||||||
claudeReadonlyReviewerRuntime: boolean,
|
claudeReadonlyReviewerRuntime: boolean,
|
||||||
@@ -698,12 +698,12 @@ async function runQuery(
|
|||||||
command: 'node',
|
command: 'node',
|
||||||
args: [mcpServerPath],
|
args: [mcpServerPath],
|
||||||
env: {
|
env: {
|
||||||
EJCLAW_CHAT_JID: containerInput.chatJid,
|
EJCLAW_CHAT_JID: runnerInput.chatJid,
|
||||||
EJCLAW_GROUP_FOLDER: containerInput.groupFolder,
|
EJCLAW_GROUP_FOLDER: runnerInput.groupFolder,
|
||||||
EJCLAW_IS_MAIN: containerInput.isMain ? '1' : '0',
|
EJCLAW_IS_MAIN: runnerInput.isMain ? '1' : '0',
|
||||||
EJCLAW_AGENT_TYPE:
|
EJCLAW_AGENT_TYPE:
|
||||||
process.env.EJCLAW_AGENT_TYPE || 'claude-code',
|
process.env.EJCLAW_AGENT_TYPE || 'claude-code',
|
||||||
EJCLAW_ROOM_ROLE: containerInput.roomRoleContext?.role || '',
|
EJCLAW_ROOM_ROLE: runnerInput.roomRoleContext?.role || '',
|
||||||
...(process.env.EJCLAW_IPC_DIR && {
|
...(process.env.EJCLAW_IPC_DIR && {
|
||||||
EJCLAW_IPC_DIR: process.env.EJCLAW_IPC_DIR,
|
EJCLAW_IPC_DIR: process.env.EJCLAW_IPC_DIR,
|
||||||
}),
|
}),
|
||||||
@@ -714,7 +714,7 @@ async function runQuery(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
PreCompact: [{ hooks: [createPreCompactHook(containerInput.assistantName)] }],
|
PreCompact: [{ hooks: [createPreCompactHook(runnerInput.assistantName)] }],
|
||||||
PreToolUse: [
|
PreToolUse: [
|
||||||
{
|
{
|
||||||
matcher: 'Bash',
|
matcher: 'Bash',
|
||||||
@@ -958,14 +958,14 @@ async function runQuery(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
let containerInput: ContainerInput;
|
let runnerInput: RunnerInput;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stdinData = await readStdin();
|
const stdinData = await readStdin();
|
||||||
containerInput = JSON.parse(stdinData);
|
runnerInput = JSON.parse(stdinData);
|
||||||
// Delete the temp file the entrypoint wrote — it contains secrets
|
// Delete the temp file the entrypoint wrote — it contains secrets
|
||||||
try { fs.unlinkSync('/tmp/input.json'); } catch { /* may not exist */ }
|
try { fs.unlinkSync('/tmp/input.json'); } catch { /* may not exist */ }
|
||||||
log(`Received input for group: ${containerInput.groupFolder}`);
|
log(`Received input for group: ${runnerInput.groupFolder}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
writeOutput({
|
writeOutput({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
@@ -978,14 +978,14 @@ async function main(): Promise<void> {
|
|||||||
// Build SDK env: merge secrets into process.env for the SDK only.
|
// Build SDK env: merge secrets into process.env for the SDK only.
|
||||||
// Secrets never touch process.env itself, so Bash subprocesses can't see them.
|
// Secrets never touch process.env itself, so Bash subprocesses can't see them.
|
||||||
const sdkEnv: Record<string, string | undefined> = { ...process.env };
|
const sdkEnv: Record<string, string | undefined> = { ...process.env };
|
||||||
for (const [key, value] of Object.entries(containerInput.secrets || {})) {
|
for (const [key, value] of Object.entries(runnerInput.secrets || {})) {
|
||||||
sdkEnv[key] = value;
|
sdkEnv[key] = value;
|
||||||
}
|
}
|
||||||
const reviewerRuntime =
|
const reviewerRuntime =
|
||||||
process.env.EJCLAW_REVIEWER_RUNTIME === '1' ||
|
process.env.EJCLAW_REVIEWER_RUNTIME === '1' ||
|
||||||
isReviewerRuntime(containerInput.roomRoleContext);
|
isReviewerRuntime(runnerInput.roomRoleContext);
|
||||||
const claudeReadonlyReviewerRuntime =
|
const claudeReadonlyReviewerRuntime =
|
||||||
isClaudeReadonlyReviewerRuntime(containerInput.roomRoleContext);
|
isClaudeReadonlyReviewerRuntime(runnerInput.roomRoleContext);
|
||||||
const claudeReadonlySandboxMode = claudeReadonlyReviewerRuntime
|
const claudeReadonlySandboxMode = claudeReadonlyReviewerRuntime
|
||||||
? getClaudeReadonlySandboxMode()
|
? getClaudeReadonlySandboxMode()
|
||||||
: null;
|
: null;
|
||||||
@@ -1002,18 +1002,18 @@ async function main(): Promise<void> {
|
|||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const mcpServerPath = path.join(__dirname, 'ipc-mcp-stdio.js');
|
const mcpServerPath = path.join(__dirname, 'ipc-mcp-stdio.js');
|
||||||
|
|
||||||
let sessionId = containerInput.sessionId;
|
let sessionId = runnerInput.sessionId;
|
||||||
fs.mkdirSync(IPC_INPUT_DIR, { recursive: true });
|
fs.mkdirSync(IPC_INPUT_DIR, { recursive: true });
|
||||||
|
|
||||||
// Clean up stale _close sentinel from previous container runs
|
// Clean up stale _close sentinel from previous runner sessions
|
||||||
try { fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); } catch { /* ignore */ }
|
try { fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); } catch { /* ignore */ }
|
||||||
|
|
||||||
// Effective working directory (WORK_DIR overrides GROUP_DIR)
|
// Effective working directory (WORK_DIR overrides GROUP_DIR)
|
||||||
const mainEffectiveCwd = WORK_DIR || GROUP_DIR;
|
const mainEffectiveCwd = WORK_DIR || GROUP_DIR;
|
||||||
|
|
||||||
// Build initial prompt (drain any pending IPC messages too)
|
// Build initial prompt (drain any pending IPC messages too)
|
||||||
let prompt = containerInput.prompt;
|
let prompt = runnerInput.prompt;
|
||||||
if (containerInput.isScheduledTask) {
|
if (runnerInput.isScheduledTask) {
|
||||||
prompt = `[SCHEDULED TASK - The following message was sent automatically and is not coming directly from the user or group.]\n\n${prompt}`;
|
prompt = `[SCHEDULED TASK - The following message was sent automatically and is not coming directly from the user or group.]\n\n${prompt}`;
|
||||||
}
|
}
|
||||||
const pending = drainIpcInput();
|
const pending = drainIpcInput();
|
||||||
@@ -1024,10 +1024,10 @@ async function main(): Promise<void> {
|
|||||||
// --- Slash command handling ---
|
// --- Slash command handling ---
|
||||||
// Check BEFORE prepending room role header so /compact isn't masked.
|
// Check BEFORE prepending room role header so /compact isn't masked.
|
||||||
const KNOWN_SESSION_COMMANDS = new Set(['/compact']);
|
const KNOWN_SESSION_COMMANDS = new Set(['/compact']);
|
||||||
const isSessionSlashCommand = KNOWN_SESSION_COMMANDS.has(containerInput.prompt.trim());
|
const isSessionSlashCommand = KNOWN_SESSION_COMMANDS.has(runnerInput.prompt.trim());
|
||||||
|
|
||||||
if (!isSessionSlashCommand) {
|
if (!isSessionSlashCommand) {
|
||||||
prompt = prependRoomRoleHeader(prompt, containerInput.roomRoleContext);
|
prompt = prependRoomRoleHeader(prompt, runnerInput.roomRoleContext);
|
||||||
}
|
}
|
||||||
const trimmedPrompt = prompt.trim();
|
const trimmedPrompt = prompt.trim();
|
||||||
|
|
||||||
@@ -1052,7 +1052,7 @@ async function main(): Promise<void> {
|
|||||||
settingSources: ['project', 'user'] as const,
|
settingSources: ['project', 'user'] as const,
|
||||||
abortController: agentAbortController,
|
abortController: agentAbortController,
|
||||||
hooks: {
|
hooks: {
|
||||||
PreCompact: [{ hooks: [createPreCompactHook(containerInput.assistantName)] }],
|
PreCompact: [{ hooks: [createPreCompactHook(runnerInput.assistantName)] }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})) {
|
})) {
|
||||||
@@ -1133,7 +1133,7 @@ async function main(): Promise<void> {
|
|||||||
prompt,
|
prompt,
|
||||||
sessionId,
|
sessionId,
|
||||||
mcpServerPath,
|
mcpServerPath,
|
||||||
containerInput,
|
runnerInput,
|
||||||
guardedSdkEnv,
|
guardedSdkEnv,
|
||||||
reviewerRuntime,
|
reviewerRuntime,
|
||||||
claudeReadonlyReviewerRuntime,
|
claudeReadonlyReviewerRuntime,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* App-server only runtime.
|
* App-server only runtime.
|
||||||
*
|
*
|
||||||
* Input protocol:
|
* Input protocol:
|
||||||
* Stdin: Full ContainerInput JSON (read until EOF)
|
* Stdin: Full RunnerInput JSON (read until EOF)
|
||||||
* IPC: Follow-up messages as JSON files in $EJCLAW_IPC_DIR/input/
|
* IPC: Follow-up messages as JSON files in $EJCLAW_IPC_DIR/input/
|
||||||
* Sentinel: _close — signals session end
|
* Sentinel: _close — signals session end
|
||||||
*
|
*
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
|
|
||||||
// ── Types ──────────────────────────────────────────────────────────
|
// ── Types ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface ContainerInput {
|
interface RunnerInput {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
groupFolder: string;
|
groupFolder: string;
|
||||||
@@ -43,7 +43,7 @@ interface ContainerInput {
|
|||||||
roomRoleContext?: RoomRoleContext;
|
roomRoleContext?: RoomRoleContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContainerOutput {
|
interface RunnerOutput {
|
||||||
status: 'success' | 'error';
|
status: 'success' | 'error';
|
||||||
result: string | null;
|
result: string | null;
|
||||||
output?: {
|
output?: {
|
||||||
@@ -77,7 +77,7 @@ let closeRequested = false;
|
|||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function writeOutput(output: ContainerOutput): void {
|
function writeOutput(output: RunnerOutput): void {
|
||||||
console.log(OUTPUT_START_MARKER);
|
console.log(OUTPUT_START_MARKER);
|
||||||
console.log(JSON.stringify(output));
|
console.log(JSON.stringify(output));
|
||||||
console.log(OUTPUT_END_MARKER);
|
console.log(OUTPUT_END_MARKER);
|
||||||
@@ -85,7 +85,7 @@ function writeOutput(output: ContainerOutput): void {
|
|||||||
|
|
||||||
function normalizeStructuredOutput(result: string | null): {
|
function normalizeStructuredOutput(result: string | null): {
|
||||||
result: string | null;
|
result: string | null;
|
||||||
output?: ContainerOutput['output'];
|
output?: RunnerOutput['output'];
|
||||||
} {
|
} {
|
||||||
if (typeof result !== 'string' || result.length === 0) {
|
if (typeof result !== 'string' || result.length === 0) {
|
||||||
return { result };
|
return { result };
|
||||||
@@ -400,12 +400,12 @@ async function runAppServerCompact(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runAppServerSession(
|
async function runAppServerSession(
|
||||||
containerInput: ContainerInput,
|
runnerInput: RunnerInput,
|
||||||
prompt: string,
|
prompt: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reviewerRuntime =
|
const reviewerRuntime =
|
||||||
process.env.EJCLAW_REVIEWER_RUNTIME === '1' ||
|
process.env.EJCLAW_REVIEWER_RUNTIME === '1' ||
|
||||||
isReviewerRuntime(containerInput.roomRoleContext);
|
isReviewerRuntime(runnerInput.roomRoleContext);
|
||||||
const readonlyRuntime =
|
const readonlyRuntime =
|
||||||
reviewerRuntime || process.env.EJCLAW_ARBITER_RUNTIME === '1';
|
reviewerRuntime || process.env.EJCLAW_ARBITER_RUNTIME === '1';
|
||||||
const clientEnv = buildReviewerGitGuardEnv(process.env, reviewerRuntime);
|
const clientEnv = buildReviewerGitGuardEnv(process.env, reviewerRuntime);
|
||||||
@@ -421,17 +421,17 @@ async function runAppServerSession(
|
|||||||
let threadId: string | undefined;
|
let threadId: string | undefined;
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
threadId = await client.startOrResumeThread(containerInput.sessionId, {
|
threadId = await client.startOrResumeThread(runnerInput.sessionId, {
|
||||||
cwd: EFFECTIVE_CWD,
|
cwd: EFFECTIVE_CWD,
|
||||||
model: CODEX_MODEL || undefined,
|
model: CODEX_MODEL || undefined,
|
||||||
});
|
});
|
||||||
log(
|
log(
|
||||||
containerInput.sessionId
|
runnerInput.sessionId
|
||||||
? `App-server thread resumed (${threadId})`
|
? `App-server thread resumed (${threadId})`
|
||||||
: `App-server thread started (${threadId})`,
|
: `App-server thread started (${threadId})`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!containerInput.sessionId) throw err;
|
if (!runnerInput.sessionId) throw err;
|
||||||
log(
|
log(
|
||||||
`App-server resume failed, retrying with new thread: ${
|
`App-server resume failed, retrying with new thread: ${
|
||||||
err instanceof Error ? err.message : String(err)
|
err instanceof Error ? err.message : String(err)
|
||||||
@@ -483,17 +483,17 @@ async function runAppServerSession(
|
|||||||
// ── Main ──────────────────────────────────────────────────────────
|
// ── Main ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
let containerInput: ContainerInput;
|
let runnerInput: RunnerInput;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stdinData = await readStdin();
|
const stdinData = await readStdin();
|
||||||
containerInput = JSON.parse(stdinData);
|
runnerInput = JSON.parse(stdinData);
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync('/tmp/input.json');
|
fs.unlinkSync('/tmp/input.json');
|
||||||
} catch {
|
} catch {
|
||||||
/* may not exist */
|
/* may not exist */
|
||||||
}
|
}
|
||||||
log(`Received input for group: ${containerInput.groupFolder}`);
|
log(`Received input for group: ${runnerInput.groupFolder}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
writeOutput({
|
writeOutput({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
@@ -513,9 +513,9 @@ async function main(): Promise<void> {
|
|||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawPrompt = containerInput.prompt;
|
const rawPrompt = runnerInput.prompt;
|
||||||
let prompt = rawPrompt;
|
let prompt = rawPrompt;
|
||||||
if (containerInput.isScheduledTask) {
|
if (runnerInput.isScheduledTask) {
|
||||||
prompt = `[SCHEDULED TASK]\n\n${prompt}`;
|
prompt = `[SCHEDULED TASK]\n\n${prompt}`;
|
||||||
}
|
}
|
||||||
const pending = drainIpcInput();
|
const pending = drainIpcInput();
|
||||||
@@ -526,12 +526,12 @@ async function main(): Promise<void> {
|
|||||||
// so /compact is not masked by the header prefix.
|
// so /compact is not masked by the header prefix.
|
||||||
const isSessionCommand = rawPrompt.trim() === '/compact';
|
const isSessionCommand = rawPrompt.trim() === '/compact';
|
||||||
if (!isSessionCommand) {
|
if (!isSessionCommand) {
|
||||||
prompt = prependRoomRoleHeader(prompt, containerInput.roomRoleContext);
|
prompt = prependRoomRoleHeader(prompt, runnerInput.roomRoleContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log('Runtime selected: app-server');
|
log('Runtime selected: app-server');
|
||||||
await runAppServerSession(containerInput, prompt);
|
await runAppServerSession(runnerInput, prompt);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
log(`Runner error: ${errorMessage}`);
|
log(`Runner error: ${errorMessage}`);
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ vi.mock('os', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import {
|
import {
|
||||||
prepareContainerSessionEnvironment,
|
prepareReadonlySessionEnvironment,
|
||||||
prepareGroupEnvironment,
|
prepareGroupEnvironment,
|
||||||
} from './agent-runner-environment.js';
|
} from './agent-runner-environment.js';
|
||||||
import * as config from './config.js';
|
import * as config from './config.js';
|
||||||
@@ -341,12 +341,12 @@ describe('prepareGroupEnvironment codex auth handling', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('prepareContainerSessionEnvironment codex compatibility', () => {
|
describe('prepareReadonlySessionEnvironment codex compatibility', () => {
|
||||||
let tempRoot: string;
|
let tempRoot: string;
|
||||||
let previousCwd: string;
|
let previousCwd: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tempRoot = fs.mkdtempSync(path.join('/tmp', 'ejclaw-container-env-'));
|
tempRoot = fs.mkdtempSync(path.join('/tmp', 'ejclaw-readonly-env-'));
|
||||||
previousCwd = process.cwd();
|
previousCwd = process.cwd();
|
||||||
process.chdir(tempRoot);
|
process.chdir(tempRoot);
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ describe('prepareContainerSessionEnvironment codex compatibility', () => {
|
|||||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('writes matching AGENTS.md and copies host codex auth/config into the container session', () => {
|
it('writes matching AGENTS.md and copies host codex auth/config into the role-scoped session', () => {
|
||||||
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
|
|
||||||
const promptsDir = path.join(tempRoot, 'prompts');
|
const promptsDir = path.join(tempRoot, 'prompts');
|
||||||
@@ -404,8 +404,8 @@ args = ["other.js"]
|
|||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const sessionDir = path.join(tempRoot, 'container-reviewer-session');
|
const sessionDir = path.join(tempRoot, 'readonly-reviewer-session');
|
||||||
prepareContainerSessionEnvironment({
|
prepareReadonlySessionEnvironment({
|
||||||
sessionDir,
|
sessionDir,
|
||||||
chatJid: 'dc:test',
|
chatJid: 'dc:test',
|
||||||
isMain: false,
|
isMain: false,
|
||||||
|
|||||||
@@ -487,7 +487,7 @@ export function prepareGroupEnvironment(
|
|||||||
: undefined;
|
: undefined;
|
||||||
// Owner CLAUDE.md: platform rules + owner paired room rules.
|
// Owner CLAUDE.md: platform rules + owner paired room rules.
|
||||||
// Reviewer paired room rules are NOT included — those belong to the
|
// Reviewer paired room rules are NOT included — those belong to the
|
||||||
// container reviewer only (via prepareContainerSessionEnvironment).
|
// Read-only reviewer/arbiter session only (via prepareReadonlySessionEnvironment).
|
||||||
const sessionClaudeMd = [
|
const sessionClaudeMd = [
|
||||||
ownerCommonPlatformPrompt,
|
ownerCommonPlatformPrompt,
|
||||||
claudePlatformPrompt,
|
claudePlatformPrompt,
|
||||||
@@ -558,14 +558,15 @@ export function prepareGroupEnvironment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare the Claude session directory for a container-based reviewer.
|
* Prepare a role-scoped session directory for a host-based read-only reviewer
|
||||||
|
* or arbiter run.
|
||||||
*
|
*
|
||||||
* Writes CLAUDE.md (platform + paired room prompts + global memory + briefing),
|
* Writes CLAUDE.md (platform + paired room prompts + global memory + briefing),
|
||||||
* syncs skills, and ensures settings.json exist — the same steps that
|
* syncs skills, and ensures settings.json exist — the same steps that
|
||||||
* `prepareGroupEnvironment` does for host-mode agents, but targeted at an
|
* `prepareGroupEnvironment` does for regular host-mode agents, but targeted at
|
||||||
* externally provided session directory (the one mounted into the container).
|
* an externally provided session directory.
|
||||||
*/
|
*/
|
||||||
export function prepareContainerSessionEnvironment(args: {
|
export function prepareReadonlySessionEnvironment(args: {
|
||||||
sessionDir: string;
|
sessionDir: string;
|
||||||
chatJid: string;
|
chatJid: string;
|
||||||
isMain: boolean;
|
isMain: boolean;
|
||||||
@@ -665,7 +666,7 @@ export function prepareContainerSessionEnvironment(args: {
|
|||||||
hasGlobalMemory: !!globalClaudeMemory,
|
hasGlobalMemory: !!globalClaudeMemory,
|
||||||
hasMemoryBriefing: !!memoryBriefing,
|
hasMemoryBriefing: !!memoryBriefing,
|
||||||
},
|
},
|
||||||
'Container session CLAUDE.md written',
|
'Readonly session CLAUDE.md written',
|
||||||
);
|
);
|
||||||
} else if (fs.existsSync(sessionClaudeMdPath)) {
|
} else if (fs.existsSync(sessionClaudeMdPath)) {
|
||||||
fs.unlinkSync(sessionClaudeMdPath);
|
fs.unlinkSync(sessionClaudeMdPath);
|
||||||
|
|||||||
@@ -318,9 +318,9 @@ describe('agent-runner timeout behavior', () => {
|
|||||||
it('uses host IPC and workdir in unsafe host paired mode reviewer sessions', async () => {
|
it('uses host IPC and workdir in unsafe host paired mode reviewer sessions', async () => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
fakeProc = createFakeProcess();
|
fakeProc = createFakeProcess();
|
||||||
const prepareContainerSessionEnvironmentSpy = vi.spyOn(
|
const prepareReadonlySessionEnvironmentSpy = vi.spyOn(
|
||||||
agentRunnerEnvironment,
|
agentRunnerEnvironment,
|
||||||
'prepareContainerSessionEnvironment',
|
'prepareReadonlySessionEnvironment',
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mocked(fs.existsSync).mockImplementation((p: fs.PathLike) => {
|
vi.mocked(fs.existsSync).mockImplementation((p: fs.PathLike) => {
|
||||||
@@ -356,7 +356,7 @@ describe('agent-runner timeout behavior', () => {
|
|||||||
fakeProc.emit('close', 0);
|
fakeProc.emit('close', 0);
|
||||||
const result = await resultPromise;
|
const result = await resultPromise;
|
||||||
expect(result.status).toBe('success');
|
expect(result.status).toBe('success');
|
||||||
expect(prepareContainerSessionEnvironmentSpy).toHaveBeenCalledWith(
|
expect(prepareReadonlySessionEnvironmentSpy).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
sessionDir: '/tmp/host-reviewer-session',
|
sessionDir: '/tmp/host-reviewer-session',
|
||||||
ipcDir: '/tmp/ejclaw-test-data/ipc/test-group',
|
ipcDir: '/tmp/ejclaw-test-data/ipc/test-group',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
IDLE_TIMEOUT,
|
IDLE_TIMEOUT,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import {
|
import {
|
||||||
prepareContainerSessionEnvironment,
|
prepareReadonlySessionEnvironment,
|
||||||
prepareGroupEnvironment,
|
prepareGroupEnvironment,
|
||||||
} from './agent-runner-environment.js';
|
} from './agent-runner-environment.js';
|
||||||
import { getEnv } from './env.js';
|
import { getEnv } from './env.js';
|
||||||
@@ -99,7 +99,7 @@ export async function runAgentProcess(
|
|||||||
(input.roomRoleContext?.role === 'reviewer' ||
|
(input.roomRoleContext?.role === 'reviewer' ||
|
||||||
input.roomRoleContext?.role === 'arbiter')
|
input.roomRoleContext?.role === 'arbiter')
|
||||||
) {
|
) {
|
||||||
prepareContainerSessionEnvironment({
|
prepareReadonlySessionEnvironment({
|
||||||
sessionDir: envOverrides.CLAUDE_CONFIG_DIR,
|
sessionDir: envOverrides.CLAUDE_CONFIG_DIR,
|
||||||
chatJid: input.chatJid,
|
chatJid: input.chatJid,
|
||||||
isMain: input.isMain,
|
isMain: input.isMain,
|
||||||
|
|||||||
@@ -1237,6 +1237,83 @@ describe('runAgentForGroup room memory', () => {
|
|||||||
expect(result).toBe('success');
|
expect(result).toBe('success');
|
||||||
expect(deps.queue.enqueueMessageCheck).not.toHaveBeenCalled();
|
expect(deps.queue.enqueueMessageCheck).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('re-enqueues the group when a reviewer fails and the task remains review_ready', async () => {
|
||||||
|
const group = { ...makeGroup(), folder: 'test-group' };
|
||||||
|
const deps = makeDeps();
|
||||||
|
|
||||||
|
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
||||||
|
chat_jid: 'group@test',
|
||||||
|
owner_agent_type: 'claude-code',
|
||||||
|
reviewer_agent_type: 'claude-code',
|
||||||
|
arbiter_agent_type: null,
|
||||||
|
owner_service_id: 'claude',
|
||||||
|
reviewer_service_id: 'claude',
|
||||||
|
arbiter_service_id: null,
|
||||||
|
activated_at: null,
|
||||||
|
reason: null,
|
||||||
|
explicit: false,
|
||||||
|
});
|
||||||
|
vi.mocked(
|
||||||
|
pairedExecutionContext.preparePairedExecutionContext,
|
||||||
|
).mockReturnValue({
|
||||||
|
task: {
|
||||||
|
id: 'paired-task-review-ready',
|
||||||
|
chat_jid: 'group@test',
|
||||||
|
group_folder: 'test-group',
|
||||||
|
owner_service_id: 'claude',
|
||||||
|
reviewer_service_id: 'claude',
|
||||||
|
title: null,
|
||||||
|
source_ref: 'HEAD',
|
||||||
|
plan_notes: null,
|
||||||
|
round_trip_count: 1,
|
||||||
|
review_requested_at: '2026-03-31T00:00:00.000Z',
|
||||||
|
status: 'in_review',
|
||||||
|
arbiter_verdict: null,
|
||||||
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
|
created_at: '2026-03-31T00:00:00.000Z',
|
||||||
|
updated_at: '2026-03-31T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
workspace: null,
|
||||||
|
envOverrides: {},
|
||||||
|
});
|
||||||
|
vi.mocked(db.getPairedTaskById).mockReturnValue({
|
||||||
|
id: 'paired-task-review-ready',
|
||||||
|
chat_jid: 'group@test',
|
||||||
|
group_folder: 'test-group',
|
||||||
|
owner_service_id: 'claude',
|
||||||
|
reviewer_service_id: 'claude',
|
||||||
|
title: null,
|
||||||
|
source_ref: 'HEAD',
|
||||||
|
plan_notes: null,
|
||||||
|
round_trip_count: 1,
|
||||||
|
review_requested_at: '2026-03-31T00:00:00.000Z',
|
||||||
|
status: 'review_ready',
|
||||||
|
arbiter_verdict: null,
|
||||||
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
|
created_at: '2026-03-31T00:00:00.000Z',
|
||||||
|
updated_at: '2026-03-31T00:00:00.000Z',
|
||||||
|
});
|
||||||
|
vi.mocked(agentRunner.runAgentProcess).mockResolvedValue({
|
||||||
|
status: 'error',
|
||||||
|
result: null,
|
||||||
|
error: 'SDK crashed with exit code 1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await runAgentForGroup(deps, {
|
||||||
|
group,
|
||||||
|
prompt: 'please review',
|
||||||
|
chatJid: 'group@test',
|
||||||
|
runId: 'run-review-ready-requeue',
|
||||||
|
forcedRole: 'reviewer',
|
||||||
|
onOutput: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('error');
|
||||||
|
expect(deps.queue.enqueueMessageCheck).toHaveBeenCalledWith('group@test');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('runAgentForGroup Claude rotation', () => {
|
describe('runAgentForGroup Claude rotation', () => {
|
||||||
|
|||||||
@@ -1165,6 +1165,28 @@ export async function runAgentForGroup(
|
|||||||
} else if (finishedCheck?.status !== 'completed') {
|
} else if (finishedCheck?.status !== 'completed') {
|
||||||
deps.queue.enqueueMessageCheck(chatJid);
|
deps.queue.enqueueMessageCheck(chatJid);
|
||||||
}
|
}
|
||||||
|
} else if (pairedExecutionContext) {
|
||||||
|
const completedRole = roomRoleContext?.role ?? 'owner';
|
||||||
|
const finishedCheck = getPairedTaskById(pairedExecutionContext.task.id);
|
||||||
|
const shouldRequeuePendingPairedTurn =
|
||||||
|
(completedRole === 'reviewer' || completedRole === 'arbiter') &&
|
||||||
|
(finishedCheck?.status === 'review_ready' ||
|
||||||
|
finishedCheck?.status === 'in_review' ||
|
||||||
|
finishedCheck?.status === 'arbiter_requested' ||
|
||||||
|
finishedCheck?.status === 'in_arbitration' ||
|
||||||
|
finishedCheck?.status === 'merge_ready');
|
||||||
|
if (shouldRequeuePendingPairedTurn) {
|
||||||
|
deps.queue.enqueueMessageCheck(chatJid);
|
||||||
|
log.info(
|
||||||
|
{
|
||||||
|
taskId: pairedExecutionContext.task.id,
|
||||||
|
role: completedRole,
|
||||||
|
pairedExecutionStatus,
|
||||||
|
taskStatus: finishedCheck?.status ?? null,
|
||||||
|
},
|
||||||
|
'Queued paired follow-up after failed reviewer/arbiter execution left a pending task state',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user