feat: auto-suspend tasks on quota errors, add Claude progress tracking

- Auto-suspend tasks after 3 consecutive quota/auth errors with
  retry-after date parsing and Discord notification
- Add tool_progress and tool_use_summary streaming from Claude Agent SDK
- Prefix progress messages with invisible marker so bots ignore them
- Fix misleading provider label in codex service logs
This commit is contained in:
Eyejoker
2026-03-23 17:03:17 +09:00
parent 42d721a9ec
commit 77e79505eb
8 changed files with 320 additions and 32 deletions

View File

@@ -32,6 +32,7 @@ interface ContainerInput {
interface ContainerOutput {
status: 'success' | 'error';
phase?: 'progress' | 'final';
result: string | null;
newSessionId?: string;
error?: string;
@@ -59,6 +60,11 @@ interface SDKUserMessage {
session_id: string;
}
interface AssistantContentBlock {
type?: string;
text?: string;
}
// Paths configurable via env vars.
const GROUP_DIR = process.env.EJCLAW_GROUP_DIR || '/workspace/group';
const IPC_DIR = process.env.EJCLAW_IPC_DIR || '/workspace/ipc';
@@ -174,6 +180,25 @@ function log(message: string): void {
console.error(`[agent-runner] ${message}`);
}
function extractAssistantText(message: unknown): string | null {
const assistant = message as {
message?: {
content?: AssistantContentBlock[];
};
};
const blocks = assistant.message?.content;
if (!Array.isArray(blocks)) return null;
const text = blocks
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
.map((block) => block.text!.trim())
.filter(Boolean)
.join('\n\n')
.trim();
return text || null;
}
function getSessionSummary(sessionId: string, transcriptPath: string): string | null {
const projectDir = path.dirname(transcriptPath);
const indexPath = path.join(projectDir, 'sessions-index.json');
@@ -525,6 +550,32 @@ async function runQuery(
log(`Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`);
}
if (message.type === 'tool_progress') {
const tp = message as {
tool_name: string;
elapsed_time_seconds: number;
};
const label = `${tp.tool_name} (${Math.round(tp.elapsed_time_seconds)}s)`;
log(`Tool progress: ${label}`);
writeOutput({
status: 'success',
phase: 'progress',
result: label,
newSessionId,
});
}
if (message.type === 'tool_use_summary') {
const ts = message as { summary: string };
log(`Tool use summary: ${ts.summary.slice(0, 200)}`);
writeOutput({
status: 'success',
phase: 'progress',
result: ts.summary,
newSessionId,
});
}
if (message.type === 'result') {
resultCount++;
const textResult = 'result' in message ? (message as { result?: string }).result : null;
@@ -569,6 +620,26 @@ async function runQuery(
log('Terminal result observed, ending query stream');
break;
}
if (message.type === 'assistant') {
const stopReason = (message as { stop_reason?: string }).stop_reason;
const textResult = extractAssistantText(message);
if (stopReason === 'end_turn' && textResult) {
resultCount++;
log(
`Terminal assistant turn observed without result event (${textResult.length} chars), ending query stream`,
);
writeOutput({
status: 'success',
result: textResult,
newSessionId,
});
terminalResultObserved = true;
ipcPolling = false;
stream.end();
break;
}
}
}
ipcPolling = false;