feat: capture AI text responses on RUNNING->IDLE for Discord relay

This commit is contained in:
2026-03-10 08:43:57 +09:00
parent 8c6d25c6d4
commit e586bb6d41
3 changed files with 109 additions and 1 deletions

View File

@@ -1416,6 +1416,9 @@ function setupMonitor() {
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
let stallProbed = false; // prevent repeated step probes during same stall
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
let wasRunning = false; // track RUNNING→IDLE transition for response capture
let lastUserInputStepIdx = -1; // track user input for response matching
let lastResponseCaptureStep = -1; // dedup: don't capture same response twice
setInterval(async () => {
pollCount++;
if (pollCount <= 3 || pollCount % 12 === 0) {
@@ -1782,6 +1785,59 @@ function setupMonitor() {
else if (pollCount <= 5) {
logToFile(`[TASK-STEP] null (no task step in session)`);
}
// ── RUNNING → IDLE transition: capture AI response for Discord ──
const userInputIdx = bestSession.lastUserInputStepIndex ?? -1;
if (userInputIdx > lastUserInputStepIdx) {
lastUserInputStepIdx = userInputIdx;
logToFile(`[RESPONSE-CAPTURE] user input detected at step ${userInputIdx}`);
}
if (wasRunning && !isRunning && currentCount > lastResponseCaptureStep) {
logToFile(`[RESPONSE-CAPTURE] RUNNING→IDLE, steps=${currentCount}, capturing response...`);
lastResponseCaptureStep = currentCount;
try {
const offset = Math.max(0, currentCount - 5);
const latestResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: bestSessionId,
stepOffset: offset,
});
if (latestResp?.steps?.length > 0) {
const steps = latestResp.steps;
for (let ri = steps.length - 1; ri >= 0; ri--) {
const s = steps[ri];
const sType = s?.type || '';
if (sType.includes('PLANNER_RESPONSE') || sType.includes('MESSAGE')) {
const parts = s?.content?.parts || s?.parts || [];
let textContent = '';
for (const p of parts) {
if (p?.text)
textContent += p.text;
else if (typeof p === 'string')
textContent += p;
}
if (!textContent && s?.metadata?.text)
textContent = s.metadata.text;
if (!textContent && s?.rawOutput)
textContent = typeof s.rawOutput === 'string' ? s.rawOutput : JSON.stringify(s.rawOutput);
if (textContent.length > 10) {
logToFile(`[RESPONSE-CAPTURE] found ${sType} (${textContent.length} chars) at step ${offset + ri}`);
const truncated = textContent.length > 1800
? textContent.substring(0, 1800) + '\n\n_(이하 생략)_'
: textContent;
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
break;
}
else {
logToFile(`[RESPONSE-CAPTURE] ${sType} too short (${textContent.length}), keys=[${Object.keys(s).join(',')}]`);
}
}
}
}
}
catch (re) {
logToFile(`[RESPONSE-CAPTURE] error: ${re.message.substring(0, 100)}`);
}
}
wasRunning = isRunning;
}
catch (e) {
if (pollCount <= 5 || pollCount % 20 === 0) {

File diff suppressed because one or more lines are too long

View File

@@ -1391,6 +1391,9 @@ function setupMonitor() {
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
let stallProbed = false; // prevent repeated step probes during same stall
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
let wasRunning = false; // track RUNNING→IDLE transition for response capture
let lastUserInputStepIdx = -1; // track user input for response matching
let lastResponseCaptureStep = -1; // dedup: don't capture same response twice
setInterval(async () => {
pollCount++;
@@ -1748,6 +1751,55 @@ function setupMonitor() {
} else if (pollCount <= 5) {
logToFile(`[TASK-STEP] null (no task step in session)`);
}
// ── RUNNING → IDLE transition: capture AI response for Discord ──
const userInputIdx = bestSession.lastUserInputStepIndex ?? -1;
if (userInputIdx > lastUserInputStepIdx) {
lastUserInputStepIdx = userInputIdx;
logToFile(`[RESPONSE-CAPTURE] user input detected at step ${userInputIdx}`);
}
if (wasRunning && !isRunning && currentCount > lastResponseCaptureStep) {
logToFile(`[RESPONSE-CAPTURE] RUNNING→IDLE, steps=${currentCount}, capturing response...`);
lastResponseCaptureStep = currentCount;
try {
const offset = Math.max(0, currentCount - 5);
const latestResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: bestSessionId,
stepOffset: offset,
});
if (latestResp?.steps?.length > 0) {
const steps = latestResp.steps;
for (let ri = steps.length - 1; ri >= 0; ri--) {
const s = steps[ri];
const sType = s?.type || '';
if (sType.includes('PLANNER_RESPONSE') || sType.includes('MESSAGE')) {
const parts = s?.content?.parts || s?.parts || [];
let textContent = '';
for (const p of parts) {
if (p?.text) textContent += p.text;
else if (typeof p === 'string') textContent += p;
}
if (!textContent && s?.metadata?.text) textContent = s.metadata.text;
if (!textContent && s?.rawOutput) textContent = typeof s.rawOutput === 'string' ? s.rawOutput : JSON.stringify(s.rawOutput);
if (textContent.length > 10) {
logToFile(`[RESPONSE-CAPTURE] found ${sType} (${textContent.length} chars) at step ${offset + ri}`);
const truncated = textContent.length > 1800
? textContent.substring(0, 1800) + '\n\n_(이하 생략)_'
: textContent;
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
break;
} else {
logToFile(`[RESPONSE-CAPTURE] ${sType} too short (${textContent.length}), keys=[${Object.keys(s).join(',')}]`);
}
}
}
}
} catch (re: any) {
logToFile(`[RESPONSE-CAPTURE] error: ${re.message.substring(0, 100)}`);
}
}
wasRunning = isRunning;
} catch (e: any) {
if (pollCount <= 5 || pollCount % 20 === 0) {
console.log(`Gravity Bridge: [POLL#${pollCount}] error: ${e.message}`);