feat: codex app-server integration, token sync fix, typing fix
- Rewrite codex-runner to use `codex app-server` JSON-RPC instead of `codex exec`. Supports streaming (item/agentMessage/delta), session persistence (thread/start, thread/resume), and mid-turn message injection (turn/steer with IPC polling during execution). - Fix token refresh not syncing to per-group session directories, causing 401 errors on running agents. - Fix typing indicator staying on when codex returns null result by always clearing typing on any streaming callback. - Update README and CLAUDE.md to reflect dual-service architecture, host process mode, and codex app-server integration.
This commit is contained in:
@@ -275,10 +275,7 @@ function prepareGroupEnvironment(
|
||||
? fs.readFileSync(configTomlPath, 'utf-8')
|
||||
: '';
|
||||
// Remove existing nanoclaw MCP section if present (to refresh env vars)
|
||||
toml = toml.replace(
|
||||
/\n?\[mcp_servers\.nanoclaw\][\s\S]*?(?=\n\[|$)/,
|
||||
'',
|
||||
);
|
||||
toml = toml.replace(/\n?\[mcp_servers\.nanoclaw\][\s\S]*?(?=\n\[|$)/, '');
|
||||
const mcpSection = `
|
||||
[mcp_servers.nanoclaw]
|
||||
command = "node"
|
||||
|
||||
11
src/index.ts
11
src/index.ts
@@ -52,7 +52,10 @@ import {
|
||||
isSessionCommandAllowed,
|
||||
} from './session-commands.js';
|
||||
import { startSchedulerLoop } from './task-scheduler.js';
|
||||
import { startTokenRefreshLoop, stopTokenRefreshLoop } from './token-refresh.js';
|
||||
import {
|
||||
startTokenRefreshLoop,
|
||||
stopTokenRefreshLoop,
|
||||
} from './token-refresh.js';
|
||||
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
@@ -257,10 +260,12 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
||||
await channel.sendMessage(chatJid, text);
|
||||
outputSentToUser = true;
|
||||
}
|
||||
await channel.setTyping?.(chatJid, false);
|
||||
resetIdleTimer();
|
||||
}
|
||||
|
||||
// Always clear typing and reset idle timer on any output (including null results)
|
||||
await channel.setTyping?.(chatJid, false);
|
||||
resetIdleTimer();
|
||||
|
||||
if (result.status === 'success') {
|
||||
queue.notifyIdle(chatJid);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { logger } from './logger.js';
|
||||
import { DATA_DIR } from './config.js';
|
||||
|
||||
const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
||||
const FALLBACK_TOKEN_URL = 'https://api.anthropic.com/v1/oauth/token';
|
||||
@@ -68,6 +69,30 @@ function writeCredentials(creds: CredentialsFile): void {
|
||||
const tempPath = `${credsPath}.tmp`;
|
||||
fs.writeFileSync(tempPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
||||
fs.renameSync(tempPath, credsPath);
|
||||
|
||||
// Sync to all per-group session directories so running agents pick up the new token
|
||||
syncToSessionDirs(credsPath);
|
||||
}
|
||||
|
||||
function syncToSessionDirs(credsPath: string): void {
|
||||
const sessionsDir = path.join(DATA_DIR, 'sessions');
|
||||
try {
|
||||
if (!fs.existsSync(sessionsDir)) return;
|
||||
const groups = fs.readdirSync(sessionsDir);
|
||||
let synced = 0;
|
||||
for (const group of groups) {
|
||||
const dest = path.join(sessionsDir, group, '.claude', '.credentials.json');
|
||||
if (fs.existsSync(path.dirname(dest))) {
|
||||
fs.copyFileSync(credsPath, dest);
|
||||
synced++;
|
||||
}
|
||||
}
|
||||
if (synced > 0) {
|
||||
logger.info({ count: synced }, 'Synced credentials to session directories');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn({ err: err instanceof Error ? err.message : String(err) }, 'Failed to sync credentials to sessions');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshToken(
|
||||
@@ -87,10 +112,7 @@ async function refreshToken(
|
||||
for (const url of [TOKEN_URL, FALLBACK_TOKEN_URL]) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user