Merge origin/main into codex/local-memory-v1

This commit is contained in:
Eyejoker
2026-04-04 03:54:40 +09:00
8 changed files with 304 additions and 22 deletions

View File

@@ -102,7 +102,8 @@ export type AgentTriggerReason =
| 'session-failure'
| 'overloaded'
| 'network-error'
| 'success-null-result';
| 'success-null-result'
| 'session-failure';
export type ClaudeRotationReason = Extract<
AgentTriggerReason,

View File

@@ -97,6 +97,26 @@ export async function runAgentProcess(
memoryBriefing: input.memoryBriefing,
role: containerRole,
});
// For codex: also write AGENTS.md to the reviewer session dir, because
// the container's /home/node/.claude always mounts the reviewer session.
// Arbiter and reviewer never run simultaneously, so this is safe.
if (containerRole === 'arbiter') {
const reviewerSessionDir = path.join(
path.dirname(sessionDir),
`${group.folder}-reviewer`,
);
const reviewerCodexDir = path.join(reviewerSessionDir, '.codex');
if (fs.existsSync(reviewerSessionDir)) {
fs.mkdirSync(reviewerCodexDir, { recursive: true });
const arbiterAgentsMd = path.join(sessionDir, '.codex', 'AGENTS.md');
if (fs.existsSync(arbiterAgentsMd)) {
fs.copyFileSync(
arbiterAgentsMd,
path.join(reviewerCodexDir, 'AGENTS.md'),
);
}
}
}
}
return runReviewerContainer({

View File

@@ -321,6 +321,28 @@ export function buildReviewerMounts(
readonly: false,
});
// Owner session directory: read-only so reviewer can verify runtime state
// files (cron state, configs, etc.) that the owner references by absolute path.
const ownerSessionDir = path.join(DATA_DIR, 'sessions', group.folder);
if (fs.existsSync(ownerSessionDir)) {
mounts.push({
hostPath: ownerSessionDir,
containerPath: ownerSessionDir,
readonly: true,
});
}
// Codex OAuth: mount host's ~/.codex (read-only) so codex-runner can authenticate
const hostCodexHome =
process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
if (fs.existsSync(hostCodexHome)) {
mounts.push({
hostPath: hostCodexHome,
containerPath: '/home/node/.codex',
readonly: true,
});
}
return mounts;
}
@@ -367,12 +389,20 @@ function buildCreateArgs(
args.push('-e', `SENTRY_AUTH_TOKEN=${process.env.SENTRY_AUTH_TOKEN}`);
}
// Extra env overrides from paired-execution-context
// Extra env overrides from paired-execution-context.
// Model/effort keys are excluded here — they are injected per-exec so the
// correct agent-type-specific values are used (claude vs codex).
const perExecKeys = new Set([
'CLAUDE_MODEL',
'CLAUDE_EFFORT',
'CODEX_MODEL',
'CODEX_EFFORT',
]);
if (envOverrides) {
for (const [key, value] of Object.entries(envOverrides)) {
// Already set above — skip duplicates
if (key === 'ANTHROPIC_API_KEY' || key === 'CLAUDE_CODE_OAUTH_TOKEN')
continue;
if (perExecKeys.has(key)) continue;
if (key === 'EJCLAW_WORK_DIR') {
args.push('-e', 'EJCLAW_WORK_DIR=/workspace/project');
continue;
@@ -469,22 +499,38 @@ export async function runReviewerContainer(args: {
'';
execArgs.push('-e', `CLAUDE_CODE_OAUTH_TOKEN=${oauthToken}`);
}
// Inject model/effort overrides at exec-time so per-role config
// takes effect even with persistent containers.
const modelEnvKeys = [
'CLAUDE_MODEL',
'CLAUDE_EFFORT',
'CODEX_MODEL',
'CODEX_EFFORT',
];
const isCodexAgent = (group.agentType || 'claude-code') === 'codex';
logger.info(
{
containerName,
groupAgentType: group.agentType,
isCodexAgent,
runnerPath: isCodexAgent
? '/app/codex/dist/index.js'
: '/app/agent/dist/index.js',
},
'Container exec runner selection',
);
// Inject only agent-type-appropriate model/effort overrides per exec.
if (envOverrides) {
const modelEnvKeys = isCodexAgent
? ['CODEX_MODEL', 'CODEX_EFFORT']
: ['CLAUDE_MODEL', 'CLAUDE_EFFORT'];
for (const key of modelEnvKeys) {
if (envOverrides[key]) {
execArgs.push('-e', `${key}=${envOverrides[key]}`);
}
}
}
execArgs.push(containerName, 'bun', '/app/dist/index.js');
if (isCodexAgent) {
// Use session-local .codex dir (contains AGENTS.md with role prompts)
// instead of the host-mounted ~/.codex (which has owner-only config).
execArgs.push('-e', 'CODEX_HOME=/home/node/.claude/.codex');
}
const runnerPath = isCodexAgent
? '/app/codex/dist/index.js'
: '/app/agent/dist/index.js';
execArgs.push(containerName, 'bun', runnerPath);
return new Promise<AgentOutput>((resolve) => {
const proc = spawn(CONTAINER_RUNTIME_BIN, execArgs, {

View File

@@ -591,6 +591,7 @@ function buildModelConfigSection(): string {
},
];
const failover = getGlobalFailoverInfo();
const lines = ['🤖 *모델 구성*'];
for (const role of roleConfigs) {
if (!role.agentType && role.label === 'Arbiter') continue;
@@ -600,7 +601,15 @@ function buildModelConfigSection(): string {
? process.env.CODEX_MODEL || 'codex'
: process.env.CLAUDE_MODEL || 'claude';
const model = role.model || defaultModel;
lines.push(` **${role.label}** — ${type} \`${model}\``);
// Show fallback status for claude-code roles when global failover is active
const isFallback = failover.active && type === 'claude-code';
if (isFallback) {
const fallbackModel = process.env.CODEX_MODEL || 'codex';
lines.push(` **${role.label}** — codex \`${fallbackModel}\` (fallback)`);
} else {
lines.push(` **${role.label}** — ${type} \`${model}\``);
}
}
// MoA status
@@ -612,8 +621,6 @@ function buildModelConfigSection(): string {
lines.push(` **MoA** — ${refs}`);
}
// Global failover
const failover = getGlobalFailoverInfo();
if (failover.active) {
lines.push(` ⚠️ **Failover 활성** — ${failover.reason || '알 수 없음'}`);
}