pikiclaw 0.2.71 → 0.2.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bot-command-ui.js +4 -10
- package/dist/bot-feishu.js +49 -9
- package/dist/bot-handler.js +1 -1
- package/dist/bot-orchestration.js +9 -1
- package/dist/bot-telegram-live-preview.js +3 -0
- package/dist/bot-telegram.js +48 -10
- package/dist/bot.js +69 -20
- package/dist/driver-claude.js +307 -8
- package/dist/driver-codex.js +134 -11
- package/package.json +1 -1
package/dist/bot-command-ui.js
CHANGED
|
@@ -134,7 +134,7 @@ export function buildAgentsCommandView(bot, chatId) {
|
|
|
134
134
|
const actions = data.agents
|
|
135
135
|
.filter(agent => agent.installed)
|
|
136
136
|
.map(agent => ({
|
|
137
|
-
label: agent.agent,
|
|
137
|
+
label: agent.version ? `${agent.agent} ${agent.version}` : agent.agent,
|
|
138
138
|
action: { kind: 'agent.switch', agent: agent.agent },
|
|
139
139
|
state: buttonStateFromFlags({ isCurrent: agent.isCurrent }),
|
|
140
140
|
primary: agent.isCurrent,
|
|
@@ -143,15 +143,9 @@ export function buildAgentsCommandView(bot, chatId) {
|
|
|
143
143
|
kind: 'agents',
|
|
144
144
|
title: 'Agents',
|
|
145
145
|
metaLines: [],
|
|
146
|
-
items:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
? (agent.version ? `Version ${agent.version}` : 'Installed')
|
|
150
|
-
: 'Not installed',
|
|
151
|
-
state: buttonStateFromFlags({ isCurrent: agent.isCurrent, unavailable: !agent.installed }),
|
|
152
|
-
})),
|
|
153
|
-
helperText: 'Use the controls below to switch agents.',
|
|
154
|
-
rows: chunkRows(actions, 3),
|
|
146
|
+
items: [],
|
|
147
|
+
emptyText: actions.length ? undefined : 'No installed agents.',
|
|
148
|
+
rows: actions.map(action => [action]),
|
|
155
149
|
};
|
|
156
150
|
}
|
|
157
151
|
const modelsDrafts = new Map();
|
package/dist/bot-feishu.js
CHANGED
|
@@ -134,7 +134,6 @@ export class FeishuBot extends Bot {
|
|
|
134
134
|
this.log(`menu: ${commands.length} commands (${skillCount} skills)`);
|
|
135
135
|
}
|
|
136
136
|
afterSwitchWorkdir(_oldPath, _newPath) {
|
|
137
|
-
this.sessionMessages.clear();
|
|
138
137
|
if (!this.channel)
|
|
139
138
|
return;
|
|
140
139
|
void this.setupMenu().catch(err => this.log(`menu refresh failed: ${err}`));
|
|
@@ -187,14 +186,17 @@ export class FeishuBot extends Bot {
|
|
|
187
186
|
return buildSessionTaskId(session, this.nextTaskId++);
|
|
188
187
|
}
|
|
189
188
|
registerSessionMessage(chatId, messageId, session) {
|
|
190
|
-
this.sessionMessages.register(chatId, messageId, session,
|
|
189
|
+
this.sessionMessages.register(chatId, messageId, session, session.workdir);
|
|
191
190
|
}
|
|
192
191
|
registerSessionMessages(chatId, messageIds, session) {
|
|
193
|
-
this.sessionMessages.registerMany(chatId, messageIds, session,
|
|
192
|
+
this.sessionMessages.registerMany(chatId, messageIds, session, session.workdir);
|
|
194
193
|
}
|
|
195
194
|
sessionFromMessage(chatId, messageId) {
|
|
196
|
-
const
|
|
197
|
-
|
|
195
|
+
const sessionRef = this.sessionMessages.resolve(chatId, messageId);
|
|
196
|
+
if (!sessionRef)
|
|
197
|
+
return null;
|
|
198
|
+
return this.getSessionRuntimeByKey(sessionRef.key, { allowAnyWorkdir: true })
|
|
199
|
+
|| this.hydrateSessionRuntime(sessionRef);
|
|
198
200
|
}
|
|
199
201
|
ensureSession(chatId, title, files) {
|
|
200
202
|
return this.ensureSessionForChat(chatId, title, files);
|
|
@@ -533,7 +535,7 @@ export class FeishuBot extends Bot {
|
|
|
533
535
|
}
|
|
534
536
|
const staged = stageSessionFiles({
|
|
535
537
|
agent: session.agent,
|
|
536
|
-
workdir:
|
|
538
|
+
workdir: session.workdir,
|
|
537
539
|
files: msg.files,
|
|
538
540
|
sessionId: session.sessionId,
|
|
539
541
|
title: undefined,
|
|
@@ -571,6 +573,7 @@ export class FeishuBot extends Bot {
|
|
|
571
573
|
agent: session.agent,
|
|
572
574
|
sessionKey: session.key,
|
|
573
575
|
prompt,
|
|
576
|
+
attachments: files,
|
|
574
577
|
startedAt: start,
|
|
575
578
|
sourceMessageId: ctx.messageId,
|
|
576
579
|
});
|
|
@@ -588,9 +591,10 @@ export class FeishuBot extends Bot {
|
|
|
588
591
|
this.registerTaskPlaceholders(taskId, [placeholderId]);
|
|
589
592
|
void this.queueSessionTask(session, async () => {
|
|
590
593
|
let livePreview = null;
|
|
594
|
+
let task = null;
|
|
591
595
|
const abortController = new AbortController();
|
|
592
596
|
try {
|
|
593
|
-
|
|
597
|
+
task = this.markTaskRunning(taskId, () => abortController.abort());
|
|
594
598
|
if (!task || task.cancelled) {
|
|
595
599
|
if (placeholderId) {
|
|
596
600
|
try {
|
|
@@ -633,8 +637,19 @@ export class FeishuBot extends Bot {
|
|
|
633
637
|
const mcpSendFile = this.createMcpSendFileCallback(ctx);
|
|
634
638
|
const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
|
|
635
639
|
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
636
|
-
}, undefined, mcpSendFile, abortController.signal, this.createCodexHumanLoopHandler(ctx, taskId))
|
|
640
|
+
}, undefined, mcpSendFile, abortController.signal, this.createCodexHumanLoopHandler(ctx, taskId), (steer) => {
|
|
641
|
+
const currentTask = this.activeTasks.get(taskId);
|
|
642
|
+
if (!currentTask || currentTask.cancelled || currentTask.status !== 'running')
|
|
643
|
+
return;
|
|
644
|
+
currentTask.steer = steer;
|
|
645
|
+
});
|
|
637
646
|
await livePreview?.settle();
|
|
647
|
+
if (task?.freezePreviewOnAbort && result.stopReason === 'interrupted') {
|
|
648
|
+
const frozenMessageIds = await this.freezeSteerHandoffPreview(ctx, placeholderId, livePreview);
|
|
649
|
+
this.registerSessionMessages(ctx.chatId, frozenMessageIds, session);
|
|
650
|
+
this.log(`[handleMessage] steer handoff preserved previous preview chat=${ctx.chatId} task=${taskId}`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
638
653
|
const finalReplyIds = await this.sendFinalReply(ctx, placeholderId, session.agent, result);
|
|
639
654
|
this.registerSessionMessages(ctx.chatId, finalReplyIds, session);
|
|
640
655
|
this.log(`[handleMessage] end chat=${ctx.chatId} agent=${session.agent} ok=${result.ok} session=${result.sessionId || session.sessionId || '(new)'} ` +
|
|
@@ -642,6 +657,12 @@ export class FeishuBot extends Bot {
|
|
|
642
657
|
`tools=${formatToolLog(result.activity)}`);
|
|
643
658
|
}
|
|
644
659
|
catch (e) {
|
|
660
|
+
if (task?.freezePreviewOnAbort && abortController.signal.aborted) {
|
|
661
|
+
const frozenMessageIds = await this.freezeSteerHandoffPreview(ctx, placeholderId, livePreview);
|
|
662
|
+
this.registerSessionMessages(ctx.chatId, frozenMessageIds, session);
|
|
663
|
+
this.log(`[handleMessage] steer handoff preserved preview after abort chat=${ctx.chatId} task=${taskId}`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
645
666
|
const msgText = String(e?.message || e || 'Unknown error');
|
|
646
667
|
this.log(`[handleMessage] end chat=${ctx.chatId} agent=${session.agent} ok=false session=${session.sessionId || '(new)'} ` +
|
|
647
668
|
`elapsed=${((Date.now() - start) / 1000).toFixed(1)}s error="${msgText.slice(0, 240)}" tools=-`);
|
|
@@ -671,6 +692,25 @@ export class FeishuBot extends Bot {
|
|
|
671
692
|
this.finishTask(taskId);
|
|
672
693
|
});
|
|
673
694
|
}
|
|
695
|
+
async freezeSteerHandoffPreview(ctx, placeholderId, livePreview) {
|
|
696
|
+
if (!placeholderId)
|
|
697
|
+
return [];
|
|
698
|
+
const previewMarkdown = livePreview?.getRenderedPreview()?.trim() || '';
|
|
699
|
+
if (!previewMarkdown)
|
|
700
|
+
return [placeholderId];
|
|
701
|
+
try {
|
|
702
|
+
if (this.channel.isStreamingCard(placeholderId)) {
|
|
703
|
+
await this.channel.endStreaming(placeholderId, 'Steered to a new reply.');
|
|
704
|
+
}
|
|
705
|
+
await this.channel.editMessage(ctx.chatId, placeholderId, previewMarkdown, {
|
|
706
|
+
keyboard: { rows: [] },
|
|
707
|
+
});
|
|
708
|
+
return [placeholderId];
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
}
|
|
674
714
|
async sendFinalReply(ctx, placeholderId, agent, result) {
|
|
675
715
|
const rendered = buildFinalReplyRender(agent, result);
|
|
676
716
|
const messageIds = [];
|
|
@@ -917,7 +957,7 @@ export class FeishuBot extends Bot {
|
|
|
917
957
|
if (!data.startsWith('tsk:steer:'))
|
|
918
958
|
return false;
|
|
919
959
|
const actionId = data.slice('tsk:steer:'.length).trim();
|
|
920
|
-
const result = this.steerTaskByActionId(actionId);
|
|
960
|
+
const result = await this.steerTaskByActionId(actionId);
|
|
921
961
|
if (!result.task)
|
|
922
962
|
return true;
|
|
923
963
|
// The queued task will naturally run next after the running task is interrupted
|
package/dist/bot-handler.js
CHANGED
|
@@ -16,7 +16,7 @@ import { stageSessionFiles } from './code-agent.js';
|
|
|
16
16
|
export async function stageFilesIntoSession(bot, session, files) {
|
|
17
17
|
const staged = stageSessionFiles({
|
|
18
18
|
agent: session.agent,
|
|
19
|
-
workdir:
|
|
19
|
+
workdir: session.workdir,
|
|
20
20
|
files,
|
|
21
21
|
sessionId: session.sessionId,
|
|
22
22
|
title: undefined,
|
|
@@ -40,7 +40,15 @@ export class SessionMessageRegistry {
|
|
|
40
40
|
chatMessages = new Map();
|
|
41
41
|
this.messages.set(chatId, chatMessages);
|
|
42
42
|
}
|
|
43
|
-
chatMessages.set(messageId,
|
|
43
|
+
chatMessages.set(messageId, {
|
|
44
|
+
key: session.key,
|
|
45
|
+
workdir: session.workdir,
|
|
46
|
+
agent: session.agent,
|
|
47
|
+
sessionId: session.sessionId,
|
|
48
|
+
workspacePath: session.workspacePath ?? null,
|
|
49
|
+
codexCumulative: session.codexCumulative,
|
|
50
|
+
modelId: session.modelId ?? null,
|
|
51
|
+
});
|
|
44
52
|
while (chatMessages.size > this.maxPerChat) {
|
|
45
53
|
const oldest = chatMessages.keys().next();
|
|
46
54
|
if (oldest.done)
|
package/dist/bot-telegram.js
CHANGED
|
@@ -82,7 +82,6 @@ export class TelegramBot extends Bot {
|
|
|
82
82
|
this.log(`menu: ${commands.length} commands (${skillCount} skills)`);
|
|
83
83
|
}
|
|
84
84
|
afterSwitchWorkdir(_oldPath, _newPath) {
|
|
85
|
-
this.sessionMessages.clear();
|
|
86
85
|
if (!this.channel)
|
|
87
86
|
return;
|
|
88
87
|
void this.setupMenu().catch(err => this.log(`menu refresh failed after workdir switch: ${err}`));
|
|
@@ -159,14 +158,17 @@ export class TelegramBot extends Bot {
|
|
|
159
158
|
return buildSessionTaskId(session, this.nextTaskId++);
|
|
160
159
|
}
|
|
161
160
|
registerSessionMessage(chatId, messageId, session) {
|
|
162
|
-
this.sessionMessages.register(chatId, messageId, session,
|
|
161
|
+
this.sessionMessages.register(chatId, messageId, session, session.workdir);
|
|
163
162
|
}
|
|
164
163
|
registerSessionMessages(chatId, messageIds, session) {
|
|
165
|
-
this.sessionMessages.registerMany(chatId, messageIds, session,
|
|
164
|
+
this.sessionMessages.registerMany(chatId, messageIds, session, session.workdir);
|
|
166
165
|
}
|
|
167
166
|
sessionFromMessage(chatId, messageId) {
|
|
168
|
-
const
|
|
169
|
-
|
|
167
|
+
const sessionRef = this.sessionMessages.resolve(chatId, messageId);
|
|
168
|
+
if (!sessionRef)
|
|
169
|
+
return null;
|
|
170
|
+
return this.getSessionRuntimeByKey(sessionRef.key, { allowAnyWorkdir: true })
|
|
171
|
+
|| this.hydrateSessionRuntime(sessionRef);
|
|
170
172
|
}
|
|
171
173
|
ensureSession(chatId, title, files) {
|
|
172
174
|
return this.ensureSessionForChat(chatId, title, files);
|
|
@@ -468,7 +470,7 @@ export class TelegramBot extends Bot {
|
|
|
468
470
|
}
|
|
469
471
|
const staged = stageSessionFiles({
|
|
470
472
|
agent: session.agent,
|
|
471
|
-
workdir:
|
|
473
|
+
workdir: session.workdir,
|
|
472
474
|
files: msg.files,
|
|
473
475
|
sessionId: session.sessionId,
|
|
474
476
|
title: undefined,
|
|
@@ -507,6 +509,7 @@ export class TelegramBot extends Bot {
|
|
|
507
509
|
agent: session.agent,
|
|
508
510
|
sessionKey: session.key,
|
|
509
511
|
prompt,
|
|
512
|
+
attachments: files,
|
|
510
513
|
startedAt: start,
|
|
511
514
|
sourceMessageId: ctx.messageId,
|
|
512
515
|
});
|
|
@@ -531,9 +534,10 @@ export class TelegramBot extends Bot {
|
|
|
531
534
|
this.registerTaskPlaceholders(taskId, [phId]);
|
|
532
535
|
void this.queueSessionTask(session, async () => {
|
|
533
536
|
let livePreview = null;
|
|
537
|
+
let task = null;
|
|
534
538
|
const abortController = new AbortController();
|
|
535
539
|
try {
|
|
536
|
-
|
|
540
|
+
task = this.markTaskRunning(taskId, () => abortController.abort());
|
|
537
541
|
if (!task || task.cancelled) {
|
|
538
542
|
if (phId != null) {
|
|
539
543
|
try {
|
|
@@ -573,8 +577,19 @@ export class TelegramBot extends Bot {
|
|
|
573
577
|
const mcpSendFile = this.createMcpSendFileCallback(ctx, messageThreadId);
|
|
574
578
|
const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
|
|
575
579
|
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
576
|
-
}, undefined, mcpSendFile, abortController.signal, this.createCodexHumanLoopHandler(ctx, taskId, messageThreadId))
|
|
580
|
+
}, undefined, mcpSendFile, abortController.signal, this.createCodexHumanLoopHandler(ctx, taskId, messageThreadId), (steer) => {
|
|
581
|
+
const currentTask = this.activeTasks.get(taskId);
|
|
582
|
+
if (!currentTask || currentTask.cancelled || currentTask.status !== 'running')
|
|
583
|
+
return;
|
|
584
|
+
currentTask.steer = steer;
|
|
585
|
+
});
|
|
577
586
|
await livePreview?.settle();
|
|
587
|
+
if (task?.freezePreviewOnAbort && result.stopReason === 'interrupted') {
|
|
588
|
+
const frozenMessageIds = await this.freezeSteerHandoffPreview(ctx, phId, livePreview);
|
|
589
|
+
this.registerSessionMessages(ctx.chatId, frozenMessageIds, session);
|
|
590
|
+
this.log(`[handleMessage] steer handoff preserved previous preview chat=${ctx.chatId} task=${taskId}`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
578
593
|
this.log(`[handleMessage] done agent=${session.agent} ok=${result.ok} session=${result.sessionId || '?'} elapsed=${result.elapsedS.toFixed(1)}s edits=${livePreview?.getEditCount() || 0} ` +
|
|
579
594
|
`tokens=in:${fmtTokens(result.inputTokens)}/cached:${fmtTokens(result.cachedInputTokens)}/out:${fmtTokens(result.outputTokens)}`);
|
|
580
595
|
this.log(`[handleMessage] response preview: "${result.message.slice(0, 150)}"`);
|
|
@@ -583,6 +598,12 @@ export class TelegramBot extends Bot {
|
|
|
583
598
|
this.log(`[handleMessage] final reply sent to chat=${ctx.chatId}`);
|
|
584
599
|
}
|
|
585
600
|
catch (e) {
|
|
601
|
+
if (task?.freezePreviewOnAbort && abortController.signal.aborted) {
|
|
602
|
+
const frozenMessageIds = await this.freezeSteerHandoffPreview(ctx, phId, livePreview);
|
|
603
|
+
this.registerSessionMessages(ctx.chatId, frozenMessageIds, session);
|
|
604
|
+
this.log(`[handleMessage] steer handoff preserved preview after abort chat=${ctx.chatId} task=${taskId}`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
586
607
|
const msgText = String(e?.message || e || 'Unknown error');
|
|
587
608
|
this.log(`[handleMessage] task failed chat=${ctx.chatId} session=${session.sessionId} error=${msgText}`);
|
|
588
609
|
const errorHtml = `<b>Error</b>\n\n<code>${escapeHtml(msgText.slice(0, 500))}</code>`;
|
|
@@ -612,6 +633,23 @@ export class TelegramBot extends Bot {
|
|
|
612
633
|
this.finishTask(taskId);
|
|
613
634
|
});
|
|
614
635
|
}
|
|
636
|
+
async freezeSteerHandoffPreview(ctx, phId, livePreview) {
|
|
637
|
+
if (phId == null)
|
|
638
|
+
return [];
|
|
639
|
+
const previewHtml = livePreview?.getRenderedPreview()?.trim() || '';
|
|
640
|
+
if (!previewHtml)
|
|
641
|
+
return [phId];
|
|
642
|
+
try {
|
|
643
|
+
await this.channel.editMessage(ctx.chatId, phId, previewHtml, {
|
|
644
|
+
parseMode: 'HTML',
|
|
645
|
+
keyboard: { inline_keyboard: [] },
|
|
646
|
+
});
|
|
647
|
+
return [phId];
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
return [];
|
|
651
|
+
}
|
|
652
|
+
}
|
|
615
653
|
/** Create an MCP sendFile callback bound to a Telegram chat context. */
|
|
616
654
|
createMcpSendFileCallback(ctx, messageThreadId) {
|
|
617
655
|
return async (filePath, opts) => {
|
|
@@ -780,7 +818,7 @@ export class TelegramBot extends Bot {
|
|
|
780
818
|
if (!data.startsWith('tsk:steer:'))
|
|
781
819
|
return false;
|
|
782
820
|
const actionId = data.slice('tsk:steer:'.length).trim();
|
|
783
|
-
const result = this.steerTaskByActionId(actionId);
|
|
821
|
+
const result = await this.steerTaskByActionId(actionId);
|
|
784
822
|
if (!result.task) {
|
|
785
823
|
await ctx.answerCallback('This task already finished.');
|
|
786
824
|
return true;
|
|
@@ -789,7 +827,7 @@ export class TelegramBot extends Bot {
|
|
|
789
827
|
await ctx.answerCallback('Task is already running.');
|
|
790
828
|
return true;
|
|
791
829
|
}
|
|
792
|
-
await ctx.answerCallback(result.interrupted ? 'Steering —
|
|
830
|
+
await ctx.answerCallback(result.interrupted ? 'Steering — switching to the queued reply...' : 'No running task to interrupt.');
|
|
793
831
|
return true;
|
|
794
832
|
}
|
|
795
833
|
async handleHumanLoopCallback(data, ctx) {
|
package/dist/bot.js
CHANGED
|
@@ -538,7 +538,19 @@ export class Bot {
|
|
|
538
538
|
return runtime;
|
|
539
539
|
}
|
|
540
540
|
getSelectedSession(cs) {
|
|
541
|
-
return this.getSessionRuntimeByKey(cs.activeSessionKey);
|
|
541
|
+
return this.getSessionRuntimeByKey(cs.activeSessionKey, { allowAnyWorkdir: true });
|
|
542
|
+
}
|
|
543
|
+
hydrateSessionRuntime(session) {
|
|
544
|
+
if (!session.sessionId)
|
|
545
|
+
return null;
|
|
546
|
+
return this.upsertSessionRuntime({
|
|
547
|
+
agent: session.agent,
|
|
548
|
+
sessionId: session.sessionId,
|
|
549
|
+
workdir: session.workdir || this.workdir,
|
|
550
|
+
workspacePath: session.workspacePath ?? null,
|
|
551
|
+
codexCumulative: session.codexCumulative,
|
|
552
|
+
modelId: session.modelId ?? null,
|
|
553
|
+
});
|
|
542
554
|
}
|
|
543
555
|
upsertSessionRuntime(session) {
|
|
544
556
|
const workdir = path.resolve(session.workdir || this.workdir);
|
|
@@ -570,6 +582,7 @@ export class Bot {
|
|
|
570
582
|
return runtime;
|
|
571
583
|
}
|
|
572
584
|
applySessionSelection(cs, session) {
|
|
585
|
+
const previousSessionKey = cs.activeSessionKey ?? null;
|
|
573
586
|
cs.activeSessionKey = session?.key ?? null;
|
|
574
587
|
if (session) {
|
|
575
588
|
cs.agent = session.agent;
|
|
@@ -577,12 +590,16 @@ export class Bot {
|
|
|
577
590
|
cs.workspacePath = session.workspacePath;
|
|
578
591
|
cs.codexCumulative = session.codexCumulative;
|
|
579
592
|
cs.modelId = session.modelId ?? null;
|
|
593
|
+
if (previousSessionKey && previousSessionKey !== session.key)
|
|
594
|
+
this.maybeEvictSessionRuntime(previousSessionKey);
|
|
580
595
|
return;
|
|
581
596
|
}
|
|
582
597
|
cs.sessionId = null;
|
|
583
598
|
cs.workspacePath = null;
|
|
584
599
|
cs.codexCumulative = undefined;
|
|
585
600
|
cs.modelId = null;
|
|
601
|
+
if (previousSessionKey)
|
|
602
|
+
this.maybeEvictSessionRuntime(previousSessionKey);
|
|
586
603
|
}
|
|
587
604
|
resetChatConversation(cs) {
|
|
588
605
|
this.applySessionSelection(cs, null);
|
|
@@ -592,12 +609,17 @@ export class Bot {
|
|
|
592
609
|
this.applySessionSelection(cs, null);
|
|
593
610
|
return;
|
|
594
611
|
}
|
|
595
|
-
const runtime = this.
|
|
612
|
+
const runtime = this.hydrateSessionRuntime({
|
|
596
613
|
agent: session.agent,
|
|
597
614
|
sessionId: session.sessionId,
|
|
615
|
+
workdir: 'workdir' in session ? session.workdir : null,
|
|
598
616
|
workspacePath: session.workspacePath ?? null,
|
|
599
617
|
modelId: session.model ?? null,
|
|
600
618
|
});
|
|
619
|
+
if (!runtime) {
|
|
620
|
+
this.applySessionSelection(cs, null);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
601
623
|
this.applySessionSelection(cs, runtime);
|
|
602
624
|
}
|
|
603
625
|
syncSelectedChats(session) {
|
|
@@ -607,6 +629,27 @@ export class Bot {
|
|
|
607
629
|
this.applySessionSelection(cs, session);
|
|
608
630
|
}
|
|
609
631
|
}
|
|
632
|
+
isSessionSelected(sessionKey) {
|
|
633
|
+
if (!sessionKey)
|
|
634
|
+
return false;
|
|
635
|
+
for (const [, cs] of this.chats) {
|
|
636
|
+
if (cs.activeSessionKey === sessionKey)
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
maybeEvictSessionRuntime(sessionKey) {
|
|
642
|
+
const session = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
643
|
+
if (!session)
|
|
644
|
+
return;
|
|
645
|
+
if (session.runningTaskIds.size)
|
|
646
|
+
return;
|
|
647
|
+
if (session.workdir === this.workdir)
|
|
648
|
+
return;
|
|
649
|
+
if (this.isSessionSelected(session.key))
|
|
650
|
+
return;
|
|
651
|
+
this.sessionStates.delete(session.key);
|
|
652
|
+
}
|
|
610
653
|
ensureSessionForChat(chatId, title, files) {
|
|
611
654
|
const cs = this.chat(chatId);
|
|
612
655
|
const selected = this.getSelectedSession(cs);
|
|
@@ -661,9 +704,7 @@ export class Bot {
|
|
|
661
704
|
if (!session)
|
|
662
705
|
return;
|
|
663
706
|
session.runningTaskIds.delete(taskId);
|
|
664
|
-
|
|
665
|
-
this.sessionStates.delete(session.key);
|
|
666
|
-
}
|
|
707
|
+
this.maybeEvictSessionRuntime(session.key);
|
|
667
708
|
}
|
|
668
709
|
runningTaskForSession(sessionKey) {
|
|
669
710
|
const session = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
@@ -687,6 +728,8 @@ export class Bot {
|
|
|
687
728
|
return task;
|
|
688
729
|
task.status = 'running';
|
|
689
730
|
task.abort = abort || null;
|
|
731
|
+
task.steer = null;
|
|
732
|
+
task.freezePreviewOnAbort = false;
|
|
690
733
|
return task;
|
|
691
734
|
}
|
|
692
735
|
registerTaskPlaceholders(taskId, messageIds) {
|
|
@@ -768,25 +811,25 @@ export class Bot {
|
|
|
768
811
|
return { task, interrupted: false, cancelled: false };
|
|
769
812
|
}
|
|
770
813
|
/**
|
|
771
|
-
* Steer
|
|
772
|
-
*
|
|
773
|
-
*
|
|
814
|
+
* Steer hands off to the queued task's own placeholder card. Interrupt the
|
|
815
|
+
* active task so the queued task can run next and the current preview can be
|
|
816
|
+
* frozen in place instead of being rewritten as an error.
|
|
774
817
|
*/
|
|
775
|
-
steerTaskByActionId(actionId) {
|
|
818
|
+
async steerTaskByActionId(actionId) {
|
|
776
819
|
const taskId = this.taskKeysByActionId.get(String(actionId));
|
|
777
820
|
if (!taskId)
|
|
778
|
-
return { task: null, interrupted: false };
|
|
821
|
+
return { task: null, interrupted: false, steered: false };
|
|
779
822
|
const task = this.activeTasks.get(taskId) || null;
|
|
780
823
|
if (!task || task.status !== 'queued')
|
|
781
|
-
return { task, interrupted: false };
|
|
782
|
-
const interrupted = this.interruptRunningTask(task.sessionKey);
|
|
783
|
-
return { task, interrupted };
|
|
824
|
+
return { task, interrupted: false, steered: false };
|
|
825
|
+
const interrupted = this.interruptRunningTask(task.sessionKey, { freezePreview: true });
|
|
826
|
+
return { task, interrupted, steered: false };
|
|
784
827
|
}
|
|
785
828
|
/**
|
|
786
829
|
* Interrupt only the currently running task for a session, leaving queued tasks intact.
|
|
787
830
|
* Used by the "Steer" action to let a queued task run next.
|
|
788
831
|
*/
|
|
789
|
-
interruptRunningTask(sessionKey) {
|
|
832
|
+
interruptRunningTask(sessionKey, opts = {}) {
|
|
790
833
|
const session = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
791
834
|
if (!session)
|
|
792
835
|
return false;
|
|
@@ -794,6 +837,7 @@ export class Bot {
|
|
|
794
837
|
const task = this.activeTasks.get(taskId);
|
|
795
838
|
if (!task || task.status !== 'running')
|
|
796
839
|
continue;
|
|
840
|
+
task.freezePreviewOnAbort = !!opts.freezePreview;
|
|
797
841
|
try {
|
|
798
842
|
task.abort?.();
|
|
799
843
|
}
|
|
@@ -995,8 +1039,8 @@ export class Bot {
|
|
|
995
1039
|
fetchSessions(agent) {
|
|
996
1040
|
return getSessions({ agent, workdir: this.workdir });
|
|
997
1041
|
}
|
|
998
|
-
fetchSessionTail(agent, sessionId, limit) {
|
|
999
|
-
return getSessionTail({ agent, sessionId, workdir
|
|
1042
|
+
fetchSessionTail(agent, sessionId, limit, workdir = this.workdir) {
|
|
1043
|
+
return getSessionTail({ agent, sessionId, workdir, limit });
|
|
1000
1044
|
}
|
|
1001
1045
|
fetchAgents(options = {}) {
|
|
1002
1046
|
return listAgents(options);
|
|
@@ -1048,7 +1092,7 @@ export class Bot {
|
|
|
1048
1092
|
return {
|
|
1049
1093
|
version: VERSION, uptime: Date.now() - this.startedAt,
|
|
1050
1094
|
memRss: mem.rss, memHeap: mem.heapUsed, pid: process.pid,
|
|
1051
|
-
workdir: this.workdir, agent: cs.agent, model, sessionId: cs.sessionId,
|
|
1095
|
+
workdir: selectedSession?.workdir || this.workdir, agent: cs.agent, model, sessionId: cs.sessionId,
|
|
1052
1096
|
workspacePath: cs.workspacePath ?? null,
|
|
1053
1097
|
running: fallbackTask, activeTasksCount: this.activeTasks.size, stats: this.stats,
|
|
1054
1098
|
usage: getUsage({ agent: cs.agent, model }),
|
|
@@ -1143,12 +1187,15 @@ export class Bot {
|
|
|
1143
1187
|
if (!opts.initial)
|
|
1144
1188
|
this.onManagedConfigChange(config, opts);
|
|
1145
1189
|
}
|
|
1146
|
-
async runStream(prompt, cs, attachments, onText, systemPrompt, mcpSendFile, abortSignal, onCodexInteractionRequest) {
|
|
1190
|
+
async runStream(prompt, cs, attachments, onText, systemPrompt, mcpSendFile, abortSignal, onCodexInteractionRequest, onSteerReady, onCodexTurnReady) {
|
|
1147
1191
|
const resolvedModel = cs.modelId || this.modelForAgent(cs.agent);
|
|
1148
1192
|
const agentConfig = this.agentConfigs[cs.agent] || {};
|
|
1149
1193
|
const extraArgs = agentConfig.extraArgs || [];
|
|
1150
1194
|
const browserEnabled = resolveGuiIntegrationConfig(getActiveUserConfig()).browserEnabled;
|
|
1151
|
-
|
|
1195
|
+
const sessionWorkdir = 'workdir' in cs && typeof cs.workdir === 'string' && cs.workdir
|
|
1196
|
+
? path.resolve(cs.workdir)
|
|
1197
|
+
: this.workdir;
|
|
1198
|
+
this.log(`[runStream] agent=${cs.agent} session=${cs.sessionId || '(new)'} workdir=${sessionWorkdir} timeout=${this.runTimeout}s attachments=${attachments.length}`);
|
|
1152
1199
|
this.log(`[runStream] ${cs.agent} config: model=${resolvedModel} extraArgs=[${extraArgs.join(' ')}]`);
|
|
1153
1200
|
const isFirstTurnOfSession = !cs.sessionId || isPendingSessionId(cs.sessionId);
|
|
1154
1201
|
const mcpSystemPrompt = mcpSendFile
|
|
@@ -1158,7 +1205,7 @@ export class Bot {
|
|
|
1158
1205
|
? appendExtraPrompt(systemPrompt, mcpSystemPrompt)
|
|
1159
1206
|
: undefined;
|
|
1160
1207
|
const opts = {
|
|
1161
|
-
agent: cs.agent, prompt, workdir:
|
|
1208
|
+
agent: cs.agent, prompt, workdir: sessionWorkdir, timeout: this.runTimeout,
|
|
1162
1209
|
sessionId: cs.sessionId, model: null,
|
|
1163
1210
|
thinkingEffort: agentConfig.reasoningEffort || 'high', onText,
|
|
1164
1211
|
attachments: attachments.length ? attachments : undefined,
|
|
@@ -1183,6 +1230,8 @@ export class Bot {
|
|
|
1183
1230
|
mcpSendFile,
|
|
1184
1231
|
abortSignal,
|
|
1185
1232
|
onCodexInteractionRequest,
|
|
1233
|
+
onSteerReady,
|
|
1234
|
+
onCodexTurnReady,
|
|
1186
1235
|
};
|
|
1187
1236
|
const result = await doStream(opts);
|
|
1188
1237
|
this.stats.totalTurns++;
|
package/dist/driver-claude.js
CHANGED
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { execSync } from 'node:child_process';
|
|
6
|
+
import { execSync, spawn } from 'node:child_process';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
7
8
|
import { registerDriver } from './agent-driver.js';
|
|
8
9
|
import {
|
|
9
10
|
// shared helpers
|
|
10
|
-
run, agentLog, detectAgentBin, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, IMAGE_EXTS, mimeForExt, listPikiclawSessions, mergeManagedAndNativeSessions, readTailLines, stripInjectedPrompts, roundPercent, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, } from './code-agent.js';
|
|
11
|
-
import { SESSION_RUNNING_THRESHOLD_MS } from './constants.js';
|
|
11
|
+
Q, run, agentLog, detectAgentBin, buildStreamPreviewMeta, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, IMAGE_EXTS, mimeForExt, listPikiclawSessions, mergeManagedAndNativeSessions, readTailLines, stripInjectedPrompts, roundPercent, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, } from './code-agent.js';
|
|
12
|
+
import { AGENT_STREAM_HARD_KILL_GRACE_MS, SESSION_RUNNING_THRESHOLD_MS } from './constants.js';
|
|
13
|
+
import { terminateProcessTree } from './process-control.js';
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
13
15
|
// Multimodal stdin
|
|
14
16
|
// ---------------------------------------------------------------------------
|
|
15
|
-
function
|
|
17
|
+
function buildClaudeUserMessage(prompt, attachments) {
|
|
16
18
|
const content = [];
|
|
17
19
|
for (const filePath of attachments) {
|
|
18
20
|
const ext = path.extname(filePath).toLowerCase();
|
|
@@ -35,6 +37,10 @@ function buildClaudeMultimodalStdin(prompt, attachments) {
|
|
|
35
37
|
content.push({ type: 'text', text: prompt });
|
|
36
38
|
return JSON.stringify({ type: 'user', message: { role: 'user', content } }) + '\n';
|
|
37
39
|
}
|
|
40
|
+
function claudeUsesStreamJsonInput(o) {
|
|
41
|
+
return !!o.attachments?.length || !!o.onSteerReady;
|
|
42
|
+
}
|
|
43
|
+
const CLAUDE_STEER_IDLE_CLOSE_MS = 1200;
|
|
38
44
|
// ---------------------------------------------------------------------------
|
|
39
45
|
// Command & parser
|
|
40
46
|
// ---------------------------------------------------------------------------
|
|
@@ -47,9 +53,12 @@ function claudeCmd(o) {
|
|
|
47
53
|
args.push('--permission-mode', o.claudePermissionMode);
|
|
48
54
|
if (o.sessionId)
|
|
49
55
|
args.push('--resume', o.sessionId);
|
|
50
|
-
if (o
|
|
56
|
+
if (claudeUsesStreamJsonInput(o)) {
|
|
51
57
|
args.push('--input-format', 'stream-json');
|
|
52
|
-
|
|
58
|
+
if (o.onSteerReady)
|
|
59
|
+
args.push('--replay-user-messages');
|
|
60
|
+
if (o.attachments?.length)
|
|
61
|
+
o._stdinOverride = buildClaudeUserMessage(o.prompt, o.attachments);
|
|
53
62
|
}
|
|
54
63
|
if (o.thinkingEffort)
|
|
55
64
|
args.push('--effort', o.thinkingEffort);
|
|
@@ -183,14 +192,304 @@ function claudeParse(ev, s) {
|
|
|
183
192
|
}
|
|
184
193
|
}
|
|
185
194
|
}
|
|
195
|
+
function createClaudeStreamState(opts) {
|
|
196
|
+
return {
|
|
197
|
+
sessionId: opts.sessionId,
|
|
198
|
+
text: '',
|
|
199
|
+
thinking: '',
|
|
200
|
+
msgs: [],
|
|
201
|
+
thinkParts: [],
|
|
202
|
+
model: opts.model,
|
|
203
|
+
thinkingEffort: opts.thinkingEffort,
|
|
204
|
+
errors: null,
|
|
205
|
+
inputTokens: null,
|
|
206
|
+
outputTokens: null,
|
|
207
|
+
cachedInputTokens: null,
|
|
208
|
+
cacheCreationInputTokens: null,
|
|
209
|
+
contextWindow: null,
|
|
210
|
+
contextUsedTokens: null,
|
|
211
|
+
codexCumulative: null,
|
|
212
|
+
stopReason: null,
|
|
213
|
+
activity: '',
|
|
214
|
+
recentActivity: [],
|
|
215
|
+
claudeToolsById: new Map(),
|
|
216
|
+
seenClaudeToolIds: new Set(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function resetClaudeTurnState(s, note) {
|
|
220
|
+
s.text = '';
|
|
221
|
+
s.thinking = '';
|
|
222
|
+
s.msgs = [];
|
|
223
|
+
s.thinkParts = [];
|
|
224
|
+
s.errors = null;
|
|
225
|
+
s.inputTokens = null;
|
|
226
|
+
s.outputTokens = null;
|
|
227
|
+
s.cachedInputTokens = null;
|
|
228
|
+
s.cacheCreationInputTokens = null;
|
|
229
|
+
s.contextUsedTokens = null;
|
|
230
|
+
s.stopReason = null;
|
|
231
|
+
s.activity = '';
|
|
232
|
+
s.recentActivity = [];
|
|
233
|
+
s.claudeToolsById = new Map();
|
|
234
|
+
s.seenClaudeToolIds = new Set();
|
|
235
|
+
if (note) {
|
|
236
|
+
pushRecentActivity(s.recentActivity, note);
|
|
237
|
+
s.activity = s.recentActivity.join('\n');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function doClaudeInteractiveStream(opts) {
|
|
241
|
+
const start = Date.now();
|
|
242
|
+
const deadline = start + opts.timeout * 1000;
|
|
243
|
+
let stderr = '';
|
|
244
|
+
let lineCount = 0;
|
|
245
|
+
let timedOut = false;
|
|
246
|
+
let interrupted = false;
|
|
247
|
+
let stdinClosed = false;
|
|
248
|
+
let steerQueued = false;
|
|
249
|
+
let awaitingSteeredResponseStart = false;
|
|
250
|
+
let idleCloseTimer = null;
|
|
251
|
+
const s = createClaudeStreamState(opts);
|
|
252
|
+
const cmd = claudeCmd(opts);
|
|
253
|
+
const shellCmd = cmd.map(Q).join(' ');
|
|
254
|
+
agentLog(`[spawn] full command: cd ${Q(opts.workdir)} && ${shellCmd}`);
|
|
255
|
+
agentLog(`[spawn] timeout: ${opts.timeout}s session: ${opts.sessionId || '(new)'}`);
|
|
256
|
+
agentLog(`[spawn] prompt (stdin): "${opts.prompt.slice(0, 300)}${opts.prompt.length > 300 ? '…' : ''}"`);
|
|
257
|
+
const spawnEnv = { ...process.env, ...(opts.extraEnv || {}) };
|
|
258
|
+
delete spawnEnv.CLAUDECODE;
|
|
259
|
+
const proc = spawn(shellCmd, {
|
|
260
|
+
cwd: opts.workdir,
|
|
261
|
+
env: spawnEnv,
|
|
262
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
263
|
+
shell: true,
|
|
264
|
+
detached: process.platform !== 'win32',
|
|
265
|
+
});
|
|
266
|
+
agentLog(`[spawn] pid=${proc.pid}`);
|
|
267
|
+
const closeInput = () => {
|
|
268
|
+
if (idleCloseTimer) {
|
|
269
|
+
clearTimeout(idleCloseTimer);
|
|
270
|
+
idleCloseTimer = null;
|
|
271
|
+
}
|
|
272
|
+
if (stdinClosed)
|
|
273
|
+
return;
|
|
274
|
+
stdinClosed = true;
|
|
275
|
+
try {
|
|
276
|
+
proc.stdin?.end();
|
|
277
|
+
}
|
|
278
|
+
catch { }
|
|
279
|
+
};
|
|
280
|
+
const emit = () => {
|
|
281
|
+
opts.onText(s.text, s.thinking, s.activity, buildStreamPreviewMeta(s), null);
|
|
282
|
+
};
|
|
283
|
+
const abortStream = () => {
|
|
284
|
+
if (interrupted || proc.killed)
|
|
285
|
+
return;
|
|
286
|
+
interrupted = true;
|
|
287
|
+
s.stopReason = 'interrupted';
|
|
288
|
+
closeInput();
|
|
289
|
+
agentLog(`[abort] user interrupt, killing process tree pid=${proc.pid}`);
|
|
290
|
+
terminateProcessTree(proc, { signal: 'SIGTERM', forceSignal: 'SIGKILL', forceAfterMs: 5000 });
|
|
291
|
+
};
|
|
292
|
+
if (opts.abortSignal?.aborted)
|
|
293
|
+
abortStream();
|
|
294
|
+
opts.abortSignal?.addEventListener('abort', abortStream, { once: true });
|
|
295
|
+
const scheduleIdleClose = () => {
|
|
296
|
+
if (idleCloseTimer)
|
|
297
|
+
clearTimeout(idleCloseTimer);
|
|
298
|
+
idleCloseTimer = setTimeout(() => {
|
|
299
|
+
idleCloseTimer = null;
|
|
300
|
+
if (stdinClosed || interrupted || timedOut || proc.killed || proc.exitCode != null)
|
|
301
|
+
return;
|
|
302
|
+
agentLog(`[stdin] closing Claude input after ${CLAUDE_STEER_IDLE_CLOSE_MS}ms idle result window`);
|
|
303
|
+
closeInput();
|
|
304
|
+
}, CLAUDE_STEER_IDLE_CLOSE_MS);
|
|
305
|
+
};
|
|
306
|
+
const startsClaudeFollowup = (ev) => {
|
|
307
|
+
const evType = ev?.type || '';
|
|
308
|
+
if (evType === 'assistant')
|
|
309
|
+
return true;
|
|
310
|
+
if (evType !== 'stream_event')
|
|
311
|
+
return false;
|
|
312
|
+
const innerType = ev?.event?.type || '';
|
|
313
|
+
return innerType === 'message_start' || innerType === 'content_block_delta';
|
|
314
|
+
};
|
|
315
|
+
const sendInput = (prompt, attachments = [], note, kind = 'steer') => {
|
|
316
|
+
if (stdinClosed || interrupted || timedOut || proc.killed || proc.exitCode != null)
|
|
317
|
+
return false;
|
|
318
|
+
try {
|
|
319
|
+
proc.stdin?.write(buildClaudeUserMessage(prompt, attachments));
|
|
320
|
+
if (kind === 'steer') {
|
|
321
|
+
steerQueued = true;
|
|
322
|
+
if (idleCloseTimer) {
|
|
323
|
+
clearTimeout(idleCloseTimer);
|
|
324
|
+
idleCloseTimer = null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (note) {
|
|
328
|
+
pushRecentActivity(s.recentActivity, note);
|
|
329
|
+
s.activity = s.recentActivity.join('\n');
|
|
330
|
+
emit();
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
agentLog(`[stdin] failed to write Claude input: ${error?.message || error}`);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
if (!sendInput(opts.prompt, opts.attachments || [], undefined, 'initial')) {
|
|
340
|
+
closeInput();
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
opts.onSteerReady?.(async (prompt, attachments = []) => {
|
|
344
|
+
if (!sendInput(prompt, attachments, 'Queued steer input', 'steer'))
|
|
345
|
+
return false;
|
|
346
|
+
return true;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
agentLog(`[stdin] onSteerReady error: ${error?.message || error}`);
|
|
351
|
+
}
|
|
352
|
+
proc.stderr?.on('data', (c) => {
|
|
353
|
+
const chunk = c.toString();
|
|
354
|
+
stderr += chunk;
|
|
355
|
+
agentLog(`[stderr] ${chunk.trim().slice(0, 200)}`);
|
|
356
|
+
});
|
|
357
|
+
const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
358
|
+
rl.on('line', raw => {
|
|
359
|
+
if (Date.now() > deadline) {
|
|
360
|
+
timedOut = true;
|
|
361
|
+
s.stopReason = 'timeout';
|
|
362
|
+
closeInput();
|
|
363
|
+
agentLog('[timeout] deadline exceeded, killing process tree');
|
|
364
|
+
terminateProcessTree(proc, { signal: 'SIGKILL' });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const line = raw.trim();
|
|
368
|
+
if (!line || line[0] !== '{')
|
|
369
|
+
return;
|
|
370
|
+
lineCount++;
|
|
371
|
+
try {
|
|
372
|
+
const ev = JSON.parse(line);
|
|
373
|
+
const evType = ev.type || '?';
|
|
374
|
+
if (evType !== 'result' && idleCloseTimer) {
|
|
375
|
+
clearTimeout(idleCloseTimer);
|
|
376
|
+
idleCloseTimer = null;
|
|
377
|
+
}
|
|
378
|
+
if (awaitingSteeredResponseStart && startsClaudeFollowup(ev)) {
|
|
379
|
+
awaitingSteeredResponseStart = false;
|
|
380
|
+
steerQueued = false;
|
|
381
|
+
resetClaudeTurnState(s);
|
|
382
|
+
}
|
|
383
|
+
if (evType === 'system' || evType === 'result' || evType === 'assistant') {
|
|
384
|
+
agentLog(`[event] type=${evType} session=${ev.session_id || s.sessionId || '?'} model=${ev.model || s.model || '?'}`);
|
|
385
|
+
}
|
|
386
|
+
if (evType === 'stream_event') {
|
|
387
|
+
const inner = ev.event || {};
|
|
388
|
+
if (inner.type === 'message_start' || inner.type === 'message_delta') {
|
|
389
|
+
agentLog(`[event] stream_event/${inner.type} session=${ev.session_id || s.sessionId || '?'}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
claudeParse(ev, s);
|
|
393
|
+
if (evType === 'result') {
|
|
394
|
+
const hasError = !!ev.is_error || (Array.isArray(ev.errors) && ev.errors.length > 0);
|
|
395
|
+
if (hasError) {
|
|
396
|
+
awaitingSteeredResponseStart = false;
|
|
397
|
+
steerQueued = false;
|
|
398
|
+
closeInput();
|
|
399
|
+
}
|
|
400
|
+
else if (steerQueued) {
|
|
401
|
+
awaitingSteeredResponseStart = true;
|
|
402
|
+
scheduleIdleClose();
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
closeInput();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
emit();
|
|
409
|
+
}
|
|
410
|
+
catch { }
|
|
411
|
+
});
|
|
412
|
+
const hardTimer = setTimeout(() => {
|
|
413
|
+
timedOut = true;
|
|
414
|
+
s.stopReason = 'timeout';
|
|
415
|
+
closeInput();
|
|
416
|
+
agentLog(`[timeout] hard deadline reached (${opts.timeout}s), killing process tree pid=${proc.pid}`);
|
|
417
|
+
terminateProcessTree(proc, { signal: 'SIGTERM', forceSignal: 'SIGKILL', forceAfterMs: 5000 });
|
|
418
|
+
}, opts.timeout * 1000 + AGENT_STREAM_HARD_KILL_GRACE_MS);
|
|
419
|
+
const [procOk, code] = await new Promise(resolve => {
|
|
420
|
+
proc.on('close', code => {
|
|
421
|
+
clearTimeout(hardTimer);
|
|
422
|
+
if (idleCloseTimer) {
|
|
423
|
+
clearTimeout(idleCloseTimer);
|
|
424
|
+
idleCloseTimer = null;
|
|
425
|
+
}
|
|
426
|
+
agentLog(`[exit] code=${code} lines_parsed=${lineCount}`);
|
|
427
|
+
resolve([code === 0, code]);
|
|
428
|
+
});
|
|
429
|
+
proc.on('error', e => {
|
|
430
|
+
clearTimeout(hardTimer);
|
|
431
|
+
if (idleCloseTimer) {
|
|
432
|
+
clearTimeout(idleCloseTimer);
|
|
433
|
+
idleCloseTimer = null;
|
|
434
|
+
}
|
|
435
|
+
agentLog(`[error] ${e.message}`);
|
|
436
|
+
stderr += e.message;
|
|
437
|
+
resolve([false, -1]);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
opts.abortSignal?.removeEventListener('abort', abortStream);
|
|
441
|
+
if (!s.text.trim() && s.msgs.length)
|
|
442
|
+
s.text = s.msgs.join('\n\n');
|
|
443
|
+
if (!s.thinking.trim() && s.thinkParts.length)
|
|
444
|
+
s.thinking = s.thinkParts.join('\n\n');
|
|
445
|
+
const errorText = joinErrorMessages(s.errors);
|
|
446
|
+
const ok = procOk && !s.errors && !timedOut && !interrupted;
|
|
447
|
+
const error = errorText
|
|
448
|
+
|| (interrupted ? 'Interrupted by user.' : null)
|
|
449
|
+
|| (timedOut ? `Timed out after ${opts.timeout}s before the agent reported completion.` : null)
|
|
450
|
+
|| (!procOk ? (stderr.trim() || `Failed (exit=${code}).`) : null);
|
|
451
|
+
const incomplete = !ok || s.stopReason === 'max_tokens' || s.stopReason === 'timeout';
|
|
452
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
453
|
+
agentLog(`[result] ok=${ok && !s.errors} elapsed=${elapsed}s text=${s.text.length}chars thinking=${s.thinking.length}chars session=${s.sessionId || '?'}`);
|
|
454
|
+
if (errorText)
|
|
455
|
+
agentLog(`[result] errors: ${errorText}`);
|
|
456
|
+
if (s.stopReason)
|
|
457
|
+
agentLog(`[result] stop_reason=${s.stopReason}`);
|
|
458
|
+
if (stderr.trim() && !procOk)
|
|
459
|
+
agentLog(`[result] stderr: ${stderr.trim().slice(0, 300)}`);
|
|
460
|
+
return {
|
|
461
|
+
ok,
|
|
462
|
+
sessionId: s.sessionId,
|
|
463
|
+
workspacePath: null,
|
|
464
|
+
model: s.model,
|
|
465
|
+
thinkingEffort: s.thinkingEffort,
|
|
466
|
+
message: s.text.trim() || errorText || (procOk ? '(no textual response)' : `Failed (exit=${code}).\n\n${stderr.trim() || '(no output)'}`),
|
|
467
|
+
thinking: s.thinking.trim() || null,
|
|
468
|
+
elapsedS: (Date.now() - start) / 1000,
|
|
469
|
+
inputTokens: s.inputTokens,
|
|
470
|
+
outputTokens: s.outputTokens,
|
|
471
|
+
cachedInputTokens: s.cachedInputTokens,
|
|
472
|
+
cacheCreationInputTokens: s.cacheCreationInputTokens,
|
|
473
|
+
contextWindow: s.contextWindow,
|
|
474
|
+
contextUsedTokens: s.contextUsedTokens,
|
|
475
|
+
contextPercent: roundPercent((s.contextUsedTokens || 0) / (s.contextWindow || 0)),
|
|
476
|
+
codexCumulative: null,
|
|
477
|
+
error,
|
|
478
|
+
stopReason: s.stopReason,
|
|
479
|
+
incomplete,
|
|
480
|
+
activity: s.activity.trim() || null,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
186
483
|
// ---------------------------------------------------------------------------
|
|
187
484
|
// Stream
|
|
188
485
|
// ---------------------------------------------------------------------------
|
|
189
486
|
export async function doClaudeStream(opts) {
|
|
190
|
-
const result =
|
|
487
|
+
const result = opts.onSteerReady
|
|
488
|
+
? await doClaudeInteractiveStream(opts)
|
|
489
|
+
: await run(claudeCmd(opts), opts, claudeParse);
|
|
191
490
|
const retryText = `${result.error || ''}\n${result.message}`;
|
|
192
491
|
if (!result.ok && opts.sessionId && /no conversation found/i.test(retryText)) {
|
|
193
|
-
return
|
|
492
|
+
return doClaudeStream({ ...opts, sessionId: null });
|
|
194
493
|
}
|
|
195
494
|
return result;
|
|
196
495
|
}
|
package/dist/driver-codex.js
CHANGED
|
@@ -95,7 +95,15 @@ export class CodexAppServer {
|
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
proc.on('error', () => { clearTimeout(timer); this.ready = false; resolve(false); });
|
|
98
|
-
proc.on('close', () => {
|
|
98
|
+
proc.on('close', () => {
|
|
99
|
+
this.ready = false;
|
|
100
|
+
this.proc = null;
|
|
101
|
+
// Resolve any pending RPC calls so callers don't hang forever
|
|
102
|
+
for (const [id, cb] of this.pending) {
|
|
103
|
+
cb({ error: { message: 'process exited before responding' } });
|
|
104
|
+
}
|
|
105
|
+
this.pending.clear();
|
|
106
|
+
});
|
|
99
107
|
this.call('initialize', { clientInfo: { name: 'pikiclaw', version: '0.2.0' } })
|
|
100
108
|
.then(resp => {
|
|
101
109
|
clearTimeout(timer);
|
|
@@ -111,14 +119,24 @@ export class CodexAppServer {
|
|
|
111
119
|
.catch(() => { clearTimeout(timer); resolve(false); });
|
|
112
120
|
});
|
|
113
121
|
}
|
|
114
|
-
call(method, params) {
|
|
122
|
+
call(method, params, timeoutMs) {
|
|
115
123
|
return new Promise((resolve) => {
|
|
116
124
|
if (!this.proc || this.proc.killed) {
|
|
117
125
|
resolve({ error: { message: 'not connected' } });
|
|
118
126
|
return;
|
|
119
127
|
}
|
|
120
128
|
const id = this.nextId++;
|
|
121
|
-
|
|
129
|
+
const wrappedResolve = (result) => {
|
|
130
|
+
if (timer)
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
this.pending.delete(id);
|
|
133
|
+
resolve(result);
|
|
134
|
+
};
|
|
135
|
+
const timer = timeoutMs ? setTimeout(() => {
|
|
136
|
+
this.pending.delete(id);
|
|
137
|
+
resolve({ error: { message: `RPC call '${method}' timed out after ${timeoutMs}ms` } });
|
|
138
|
+
}, timeoutMs) : null;
|
|
139
|
+
this.pending.set(id, wrappedResolve);
|
|
122
140
|
const msg = { jsonrpc: '2.0', id, method };
|
|
123
141
|
if (params !== undefined)
|
|
124
142
|
msg.params = params;
|
|
@@ -126,6 +144,9 @@ export class CodexAppServer {
|
|
|
126
144
|
this.proc.stdin.write(JSON.stringify(msg) + '\n');
|
|
127
145
|
}
|
|
128
146
|
catch {
|
|
147
|
+
if (timer)
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
this.pending.delete(id);
|
|
129
150
|
resolve({ error: { message: 'write failed' } });
|
|
130
151
|
}
|
|
131
152
|
});
|
|
@@ -238,6 +259,34 @@ function summarizeCodexFileChange(item) {
|
|
|
238
259
|
return `Updated ${paths.length} files`;
|
|
239
260
|
return 'Updated files';
|
|
240
261
|
}
|
|
262
|
+
function summarizeCodexRawResponseItem(item) {
|
|
263
|
+
if (!item || typeof item !== 'object')
|
|
264
|
+
return null;
|
|
265
|
+
switch (item.type) {
|
|
266
|
+
case 'web_search_call': {
|
|
267
|
+
const action = item.action || {};
|
|
268
|
+
if (action.type === 'search') {
|
|
269
|
+
const query = shortValue(action.query, 120);
|
|
270
|
+
return query ? `Search web: ${query}` : 'Search web';
|
|
271
|
+
}
|
|
272
|
+
if (action.type === 'open_page') {
|
|
273
|
+
const url = shortValue(action.url, 120);
|
|
274
|
+
return url ? `Open ${url}` : 'Open web page';
|
|
275
|
+
}
|
|
276
|
+
return 'Search web';
|
|
277
|
+
}
|
|
278
|
+
case 'custom_tool_call': {
|
|
279
|
+
const name = shortValue(item.name, 80);
|
|
280
|
+
return name ? `Use ${name}` : 'Use tool';
|
|
281
|
+
}
|
|
282
|
+
case 'local_shell_call': {
|
|
283
|
+
const command = shortValue(item.action?.command || item.action?.cmd, 120);
|
|
284
|
+
return command ? `Run shell command: ${command}` : 'Run shell command';
|
|
285
|
+
}
|
|
286
|
+
default:
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
241
290
|
function buildCodexInteractionRequest(method, params, requestId) {
|
|
242
291
|
if (method === 'item/tool/requestUserInput') {
|
|
243
292
|
const questions = Array.isArray(params?.questions) ? params.questions : [];
|
|
@@ -423,7 +472,7 @@ function codexErrorResult(error, start, sessionId, model, thinkingEffort) {
|
|
|
423
472
|
// ---------------------------------------------------------------------------
|
|
424
473
|
// Stream notification handler (extracted from doCodexStream)
|
|
425
474
|
// ---------------------------------------------------------------------------
|
|
426
|
-
function handleCodexNotification(method, params, s, opts, deadline, emit, hardTimer, settleTurnDone) {
|
|
475
|
+
function handleCodexNotification(method, params, s, opts, deadline, emit, hardTimer, settleTurnDone, publishTurnControl) {
|
|
427
476
|
if (Date.now() > deadline)
|
|
428
477
|
return;
|
|
429
478
|
if (params.threadId !== s.sessionId) {
|
|
@@ -448,6 +497,9 @@ function handleCodexNotification(method, params, s, opts, deadline, emit, hardTi
|
|
|
448
497
|
case 'item/completed':
|
|
449
498
|
handleItemCompleted(params.item || {}, s, emit);
|
|
450
499
|
return;
|
|
500
|
+
case 'rawResponseItem/completed':
|
|
501
|
+
handleRawResponseItemCompleted(params.item || {}, s, emit);
|
|
502
|
+
return;
|
|
451
503
|
case 'thread/tokenUsage/updated':
|
|
452
504
|
applyCodexTokenUsage(s, params.tokenUsage, opts.codexPrevCumulative);
|
|
453
505
|
emit();
|
|
@@ -475,6 +527,7 @@ function handleCodexNotification(method, params, s, opts, deadline, emit, hardTi
|
|
|
475
527
|
}
|
|
476
528
|
case 'turn/started':
|
|
477
529
|
s.turnId = params.turn?.id ?? null;
|
|
530
|
+
publishTurnControl?.();
|
|
478
531
|
return;
|
|
479
532
|
case 'model/rerouted':
|
|
480
533
|
s.model = params.model ?? s.model;
|
|
@@ -537,6 +590,27 @@ function handleItemCompleted(item, s, emit) {
|
|
|
537
590
|
emit();
|
|
538
591
|
}
|
|
539
592
|
}
|
|
593
|
+
function handleRawResponseItemCompleted(item, s, emit) {
|
|
594
|
+
if (item?.type === 'reasoning') {
|
|
595
|
+
const summary = Array.isArray(item.summary)
|
|
596
|
+
? item.summary
|
|
597
|
+
.map((entry) => (typeof entry === 'string' ? entry : entry?.text || ''))
|
|
598
|
+
.filter(Boolean)
|
|
599
|
+
.join('\n')
|
|
600
|
+
.trim()
|
|
601
|
+
: '';
|
|
602
|
+
if (summary) {
|
|
603
|
+
s.thinkParts.push(summary);
|
|
604
|
+
emit();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const summary = summarizeCodexRawResponseItem(item);
|
|
609
|
+
if (!summary)
|
|
610
|
+
return;
|
|
611
|
+
pushRecentActivity(s.recentNarrative, summary);
|
|
612
|
+
emit();
|
|
613
|
+
}
|
|
540
614
|
function handleCompletedAgentMessage(item, s, emit) {
|
|
541
615
|
const phase = item.phase || s.messagePhases.get(item.id) || 'final_answer';
|
|
542
616
|
if (phase === 'final_answer') {
|
|
@@ -620,6 +694,8 @@ export async function doCodexStream(opts) {
|
|
|
620
694
|
let unsubscribeNotifications = () => { };
|
|
621
695
|
let unsubscribeRequests = () => { };
|
|
622
696
|
let settleTurnDone = null;
|
|
697
|
+
let emitPreview = () => { };
|
|
698
|
+
let publishedTurnControl = false;
|
|
623
699
|
try {
|
|
624
700
|
const config = [];
|
|
625
701
|
if (opts.codexExtraArgs?.length) {
|
|
@@ -632,6 +708,45 @@ export async function doCodexStream(opts) {
|
|
|
632
708
|
return codexErrorResult('Failed to start codex app-server.', start, opts.sessionId, opts.model, opts.thinkingEffort);
|
|
633
709
|
}
|
|
634
710
|
const s = createCodexStreamState(opts);
|
|
711
|
+
const publishTurnControl = () => {
|
|
712
|
+
if (publishedTurnControl || !opts.onCodexTurnReady || !s.sessionId || !s.turnId)
|
|
713
|
+
return;
|
|
714
|
+
publishedTurnControl = true;
|
|
715
|
+
try {
|
|
716
|
+
const control = {
|
|
717
|
+
threadId: s.sessionId,
|
|
718
|
+
turnId: s.turnId,
|
|
719
|
+
steer: async (prompt, attachments = []) => {
|
|
720
|
+
if (!s.sessionId || !s.turnId)
|
|
721
|
+
return false;
|
|
722
|
+
const expectedTurnId = s.turnId;
|
|
723
|
+
const clippedPrompt = prompt.slice(0, 200);
|
|
724
|
+
agentLog(`[codex-rpc] turn/steer turn=${expectedTurnId} prompt="${clippedPrompt}${prompt.length > 200 ? '…' : ''}"`);
|
|
725
|
+
const steerResp = await srv.call('turn/steer', {
|
|
726
|
+
threadId: s.sessionId,
|
|
727
|
+
expectedTurnId,
|
|
728
|
+
input: buildCodexTurnInput(prompt, attachments),
|
|
729
|
+
}, 30_000);
|
|
730
|
+
if (steerResp.error) {
|
|
731
|
+
const errMsg = steerResp.error.message || 'turn/steer failed';
|
|
732
|
+
agentLog(`[codex-rpc] turn/steer error: ${errMsg}`);
|
|
733
|
+
pushRecentActivity(s.recentFailures, `Steer failed: ${shortValue(errMsg, 120)}`, 4);
|
|
734
|
+
emitPreview();
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
s.turnId = steerResp.result?.turnId ?? s.turnId;
|
|
738
|
+
pushRecentActivity(s.recentNarrative, 'Applied steer input');
|
|
739
|
+
emitPreview();
|
|
740
|
+
return true;
|
|
741
|
+
},
|
|
742
|
+
};
|
|
743
|
+
opts.onSteerReady?.(control.steer);
|
|
744
|
+
opts.onCodexTurnReady?.(control);
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
agentLog(`[codex-rpc] onCodexTurnReady error: ${error?.message || error}`);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
635
750
|
// thread/start or thread/resume
|
|
636
751
|
let threadResp;
|
|
637
752
|
const threadParams = {
|
|
@@ -643,11 +758,11 @@ export async function doCodexStream(opts) {
|
|
|
643
758
|
};
|
|
644
759
|
if (opts.sessionId) {
|
|
645
760
|
agentLog(`[codex-rpc] thread/resume id=${opts.sessionId}`);
|
|
646
|
-
threadResp = await srv.call('thread/resume', { threadId: opts.sessionId, ...threadParams });
|
|
761
|
+
threadResp = await srv.call('thread/resume', { threadId: opts.sessionId, ...threadParams }, 60_000);
|
|
647
762
|
}
|
|
648
763
|
else {
|
|
649
764
|
agentLog(`[codex-rpc] thread/start cwd=${opts.workdir} model=${opts.codexModel || '(default)'}`);
|
|
650
|
-
threadResp = await srv.call('thread/start', threadParams);
|
|
765
|
+
threadResp = await srv.call('thread/start', threadParams, 60_000);
|
|
651
766
|
}
|
|
652
767
|
if (threadResp.error) {
|
|
653
768
|
const errMsg = threadResp.error.message || 'thread/start failed';
|
|
@@ -689,8 +804,9 @@ export async function doCodexStream(opts) {
|
|
|
689
804
|
s.activity = buildCodexActivityPreview(s);
|
|
690
805
|
opts.onText(s.text, s.thinking, s.activity, buildStreamPreviewMeta(s), s.plan);
|
|
691
806
|
};
|
|
807
|
+
emitPreview = emit;
|
|
692
808
|
unsubscribeNotifications = srv.onNotification((method, params) => {
|
|
693
|
-
handleCodexNotification(method, params, s, opts, deadline, emit, hardTimer, settleTurnDone);
|
|
809
|
+
handleCodexNotification(method, params, s, opts, deadline, emit, hardTimer, settleTurnDone, publishTurnControl);
|
|
694
810
|
});
|
|
695
811
|
unsubscribeRequests = srv.onRequest((method, params, requestId) => {
|
|
696
812
|
return handleCodexRequest(method, params, requestId, s, opts, emit);
|
|
@@ -704,10 +820,16 @@ export async function doCodexStream(opts) {
|
|
|
704
820
|
s.turnError = s.turnError || 'Interrupted by user.';
|
|
705
821
|
agentLog(`[codex-rpc] abort requested thread=${s.sessionId || '?'} turn=${s.turnId || '?'}`);
|
|
706
822
|
if (s.turnId && s.sessionId) {
|
|
707
|
-
|
|
823
|
+
// Send turn/interrupt and wait for Codex to acknowledge before settling.
|
|
824
|
+
// Don't kill the process here — let the finally block handle it after
|
|
825
|
+
// Codex has had time to persist the interrupted session state.
|
|
826
|
+
srv.call('turn/interrupt', { threadId: s.sessionId, turnId: s.turnId }, 5_000)
|
|
827
|
+
.finally(() => settleTurnDone?.());
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
srv.kill();
|
|
831
|
+
settleTurnDone?.();
|
|
708
832
|
}
|
|
709
|
-
srv.kill();
|
|
710
|
-
settleTurnDone?.();
|
|
711
833
|
};
|
|
712
834
|
if (opts.abortSignal?.aborted)
|
|
713
835
|
abortStream();
|
|
@@ -732,7 +854,7 @@ export async function doCodexStream(opts) {
|
|
|
732
854
|
threadId: s.sessionId, input,
|
|
733
855
|
model: opts.codexModel || undefined,
|
|
734
856
|
effort: mapEffort(opts.thinkingEffort),
|
|
735
|
-
});
|
|
857
|
+
}, 60_000);
|
|
736
858
|
if (turnResp.error) {
|
|
737
859
|
opts.abortSignal?.removeEventListener('abort', abortStream);
|
|
738
860
|
unsubscribeNotifications();
|
|
@@ -742,6 +864,7 @@ export async function doCodexStream(opts) {
|
|
|
742
864
|
return codexErrorResult(errMsg, start, s.sessionId, s.model, s.thinkingEffort);
|
|
743
865
|
}
|
|
744
866
|
s.turnId = turnResp.result?.turn?.id ?? null;
|
|
867
|
+
publishTurnControl();
|
|
745
868
|
await turnDone;
|
|
746
869
|
opts.abortSignal?.removeEventListener('abort', abortStream);
|
|
747
870
|
unsubscribeNotifications();
|
package/package.json
CHANGED