pikiclaw 0.2.70 → 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/README.md +1 -0
- package/dist/bot-command-ui.js +4 -10
- package/dist/bot-commands.js +1 -1
- package/dist/bot-feishu-render.js +7 -3
- package/dist/bot-feishu.js +101 -15
- package/dist/bot-handler.js +2 -2
- package/dist/bot-orchestration.js +9 -1
- package/dist/bot-telegram-live-preview.js +3 -0
- package/dist/bot-telegram-render.js +8 -4
- package/dist/bot-telegram.js +87 -13
- package/dist/bot.js +116 -12
- package/dist/channel-feishu.js +4 -2
- package/dist/code-agent.js +2 -3
- package/dist/driver-claude.js +307 -8
- package/dist/driver-codex.js +134 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ npx pikiclaw@latest --doctor # check environment only
|
|
|
142
142
|
|
|
143
143
|
- Streaming preview with continuous message updates
|
|
144
144
|
- Session switching, resume, and multi-turn conversations
|
|
145
|
+
- Task queue with **Steer** — interrupt the running task and let a queued message jump ahead
|
|
145
146
|
- Working directory browsing and switching
|
|
146
147
|
- File attachments automatically enter the session workspace
|
|
147
148
|
- Long-task sleep prevention, watchdog, and auto-restart
|
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-commands.js
CHANGED
|
@@ -78,7 +78,7 @@ export async function getSessionsPageData(bot, chatId, page, pageSize = 5) {
|
|
|
78
78
|
runState: status.isRunning ? 'running' : s.runState,
|
|
79
79
|
runDetail: s.runDetail,
|
|
80
80
|
});
|
|
81
|
-
const title = s.title ? s.title.replace(/\n/g, ' ').slice(0,
|
|
81
|
+
const title = s.title ? s.title.replace(/\n/g, ' ').slice(0, 20) : sessionKey.slice(0, 20);
|
|
82
82
|
const time = s.createdAt
|
|
83
83
|
? new Date(s.createdAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
84
84
|
: '?';
|
|
@@ -179,10 +179,14 @@ export function buildHumanLoopPromptMarkdown(prompt) {
|
|
|
179
179
|
// ---------------------------------------------------------------------------
|
|
180
180
|
// LivePreview renderer — produces Markdown for Feishu card elements
|
|
181
181
|
// ---------------------------------------------------------------------------
|
|
182
|
-
export function buildInitialPreviewMarkdown(agent, model, effort, waiting = false) {
|
|
182
|
+
export function buildInitialPreviewMarkdown(agent, model, effort, waiting = false, queuePosition = 0) {
|
|
183
183
|
const parts = [];
|
|
184
|
-
if (waiting)
|
|
185
|
-
|
|
184
|
+
if (waiting) {
|
|
185
|
+
const queueLabel = queuePosition > 0
|
|
186
|
+
? `Queued · ${queuePosition} ${queuePosition === 1 ? 'task' : 'tasks'} ahead`
|
|
187
|
+
: 'Waiting in queue...';
|
|
188
|
+
parts.push(queueLabel);
|
|
189
|
+
}
|
|
186
190
|
if (model)
|
|
187
191
|
parts.push(model);
|
|
188
192
|
else
|
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,21 +186,30 @@ 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);
|
|
201
203
|
}
|
|
202
204
|
resolveIncomingSession(ctx, text, files) {
|
|
203
205
|
const cs = this.chat(ctx.chatId);
|
|
204
|
-
|
|
206
|
+
const replyMessageId = ctx.replyToMessageId || null;
|
|
207
|
+
const repliedSession = this.sessionFromMessage(ctx.chatId, replyMessageId);
|
|
208
|
+
if (repliedSession) {
|
|
209
|
+
this.log(`[resolveSession] reply matched session=${repliedSession.sessionId} chat=${ctx.chatId}`);
|
|
210
|
+
this.applySessionSelection(cs, repliedSession);
|
|
211
|
+
return repliedSession;
|
|
212
|
+
}
|
|
205
213
|
const selected = this.getSelectedSession(cs);
|
|
206
214
|
if (selected)
|
|
207
215
|
return selected;
|
|
@@ -257,9 +265,27 @@ export class FeishuBot extends Bot {
|
|
|
257
265
|
}
|
|
258
266
|
}
|
|
259
267
|
sessionsPageSize = 5;
|
|
260
|
-
buildStopKeyboard(actionId) {
|
|
268
|
+
buildStopKeyboard(actionId, opts) {
|
|
261
269
|
if (!actionId)
|
|
262
270
|
return undefined;
|
|
271
|
+
if (opts?.queued) {
|
|
272
|
+
return {
|
|
273
|
+
rows: [{
|
|
274
|
+
actions: [
|
|
275
|
+
{
|
|
276
|
+
tag: 'button',
|
|
277
|
+
text: { tag: 'plain_text', content: 'Recall' },
|
|
278
|
+
value: { action: `tsk:stop:${actionId}` },
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
tag: 'button',
|
|
282
|
+
text: { tag: 'plain_text', content: 'Steer' },
|
|
283
|
+
value: { action: `tsk:steer:${actionId}` },
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
}],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
263
289
|
return {
|
|
264
290
|
rows: [{
|
|
265
291
|
actions: [{
|
|
@@ -509,10 +535,10 @@ export class FeishuBot extends Bot {
|
|
|
509
535
|
}
|
|
510
536
|
const staged = stageSessionFiles({
|
|
511
537
|
agent: session.agent,
|
|
512
|
-
workdir:
|
|
538
|
+
workdir: session.workdir,
|
|
513
539
|
files: msg.files,
|
|
514
540
|
sessionId: session.sessionId,
|
|
515
|
-
title:
|
|
541
|
+
title: undefined,
|
|
516
542
|
});
|
|
517
543
|
session.workspacePath = staged.workspacePath;
|
|
518
544
|
this.syncSelectedChats(session);
|
|
@@ -547,15 +573,17 @@ export class FeishuBot extends Bot {
|
|
|
547
573
|
agent: session.agent,
|
|
548
574
|
sessionKey: session.key,
|
|
549
575
|
prompt,
|
|
576
|
+
attachments: files,
|
|
550
577
|
startedAt: start,
|
|
551
578
|
sourceMessageId: ctx.messageId,
|
|
552
579
|
});
|
|
553
|
-
const
|
|
580
|
+
const queuePosition = waiting ? this.getQueuePosition(session.key, taskId) : 0;
|
|
581
|
+
const placeholderKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId), { queued: waiting });
|
|
554
582
|
const model = session.modelId || this.modelForAgent(session.agent);
|
|
555
583
|
const effort = this.effortForAgent(session.agent);
|
|
556
|
-
const placeholderId = await this.channel.sendStreamingCard(ctx.chatId, buildInitialPreviewMarkdown(session.agent, model, effort, waiting), {
|
|
584
|
+
const placeholderId = await this.channel.sendStreamingCard(ctx.chatId, buildInitialPreviewMarkdown(session.agent, model, effort, waiting, queuePosition), {
|
|
557
585
|
replyTo: ctx.messageId || undefined,
|
|
558
|
-
keyboard:
|
|
586
|
+
keyboard: placeholderKeyboard,
|
|
559
587
|
});
|
|
560
588
|
if (placeholderId) {
|
|
561
589
|
this.registerSessionMessage(ctx.chatId, placeholderId, session);
|
|
@@ -563,9 +591,10 @@ export class FeishuBot extends Bot {
|
|
|
563
591
|
this.registerTaskPlaceholders(taskId, [placeholderId]);
|
|
564
592
|
void this.queueSessionTask(session, async () => {
|
|
565
593
|
let livePreview = null;
|
|
594
|
+
let task = null;
|
|
566
595
|
const abortController = new AbortController();
|
|
567
596
|
try {
|
|
568
|
-
|
|
597
|
+
task = this.markTaskRunning(taskId, () => abortController.abort());
|
|
569
598
|
if (!task || task.cancelled) {
|
|
570
599
|
if (placeholderId) {
|
|
571
600
|
try {
|
|
@@ -576,6 +605,14 @@ export class FeishuBot extends Bot {
|
|
|
576
605
|
this.log(`[handleMessage] skipped cancelled queued task chat=${ctx.chatId} msg=${ctx.messageId}`);
|
|
577
606
|
return;
|
|
578
607
|
}
|
|
608
|
+
// Task is now running — update keyboard from Recall/Steer to Stop
|
|
609
|
+
const runningKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId));
|
|
610
|
+
if (placeholderId && waiting) {
|
|
611
|
+
try {
|
|
612
|
+
await this.channel.editMessage(ctx.chatId, placeholderId, buildInitialPreviewMarkdown(session.agent, model, effort, false), { keyboard: runningKeyboard });
|
|
613
|
+
}
|
|
614
|
+
catch { }
|
|
615
|
+
}
|
|
579
616
|
if (placeholderId) {
|
|
580
617
|
const renderer = this.channel.isStreamingCard(placeholderId)
|
|
581
618
|
? feishuStreamingPreviewRenderer
|
|
@@ -591,7 +628,7 @@ export class FeishuBot extends Bot {
|
|
|
591
628
|
canEditMessages: supportsChannelCapability(this.channel, 'editMessages'),
|
|
592
629
|
canSendTyping: false,
|
|
593
630
|
parseMode: 'Markdown',
|
|
594
|
-
keyboard:
|
|
631
|
+
keyboard: runningKeyboard,
|
|
595
632
|
log: (message) => this.log(message),
|
|
596
633
|
});
|
|
597
634
|
livePreview.start();
|
|
@@ -600,8 +637,19 @@ export class FeishuBot extends Bot {
|
|
|
600
637
|
const mcpSendFile = this.createMcpSendFileCallback(ctx);
|
|
601
638
|
const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
|
|
602
639
|
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
603
|
-
}, 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
|
+
});
|
|
604
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
|
+
}
|
|
605
653
|
const finalReplyIds = await this.sendFinalReply(ctx, placeholderId, session.agent, result);
|
|
606
654
|
this.registerSessionMessages(ctx.chatId, finalReplyIds, session);
|
|
607
655
|
this.log(`[handleMessage] end chat=${ctx.chatId} agent=${session.agent} ok=${result.ok} session=${result.sessionId || session.sessionId || '(new)'} ` +
|
|
@@ -609,6 +657,12 @@ export class FeishuBot extends Bot {
|
|
|
609
657
|
`tools=${formatToolLog(result.activity)}`);
|
|
610
658
|
}
|
|
611
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
|
+
}
|
|
612
666
|
const msgText = String(e?.message || e || 'Unknown error');
|
|
613
667
|
this.log(`[handleMessage] end chat=${ctx.chatId} agent=${session.agent} ok=false session=${session.sessionId || '(new)'} ` +
|
|
614
668
|
`elapsed=${((Date.now() - start) / 1000).toFixed(1)}s error="${msgText.slice(0, 240)}" tools=-`);
|
|
@@ -638,6 +692,25 @@ export class FeishuBot extends Bot {
|
|
|
638
692
|
this.finishTask(taskId);
|
|
639
693
|
});
|
|
640
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
|
+
}
|
|
641
714
|
async sendFinalReply(ctx, placeholderId, agent, result) {
|
|
642
715
|
const rendered = buildFinalReplyRender(agent, result);
|
|
643
716
|
const messageIds = [];
|
|
@@ -787,6 +860,7 @@ export class FeishuBot extends Bot {
|
|
|
787
860
|
messageId: ctx.messageId,
|
|
788
861
|
from: ctx.from,
|
|
789
862
|
chatType: 'p2p',
|
|
863
|
+
replyToMessageId: null,
|
|
790
864
|
reply: (text, opts) => ctx.channel.send(ctx.chatId, text, opts),
|
|
791
865
|
editReply: (msgId, text, opts) => ctx.channel.editMessage(ctx.chatId, msgId, text, opts),
|
|
792
866
|
channel: ctx.channel,
|
|
@@ -800,6 +874,8 @@ export class FeishuBot extends Bot {
|
|
|
800
874
|
return;
|
|
801
875
|
if (await this.handleTaskStopCallback(data, ctx))
|
|
802
876
|
return;
|
|
877
|
+
if (await this.handleTaskSteerCallback(data, ctx))
|
|
878
|
+
return;
|
|
803
879
|
if (await this.handleSwitchNavigateCallback(data, ctx))
|
|
804
880
|
return;
|
|
805
881
|
if (await this.handleSwitchSelectCallback(data, ctx))
|
|
@@ -877,6 +953,16 @@ export class FeishuBot extends Bot {
|
|
|
877
953
|
}
|
|
878
954
|
return true;
|
|
879
955
|
}
|
|
956
|
+
async handleTaskSteerCallback(data, ctx) {
|
|
957
|
+
if (!data.startsWith('tsk:steer:'))
|
|
958
|
+
return false;
|
|
959
|
+
const actionId = data.slice('tsk:steer:'.length).trim();
|
|
960
|
+
const result = await this.steerTaskByActionId(actionId);
|
|
961
|
+
if (!result.task)
|
|
962
|
+
return true;
|
|
963
|
+
// The queued task will naturally run next after the running task is interrupted
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
880
966
|
async handleSwitchNavigateCallback(data, ctx) {
|
|
881
967
|
if (!data.startsWith('sw:n:'))
|
|
882
968
|
return false;
|
package/dist/bot-handler.js
CHANGED
|
@@ -16,10 +16,10 @@ 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
|
-
title:
|
|
22
|
+
title: undefined,
|
|
23
23
|
});
|
|
24
24
|
session.workspacePath = staged.workspacePath;
|
|
25
25
|
return {
|
|
@@ -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)
|
|
@@ -258,10 +258,14 @@ function formatFinalFooterHtml(status, agent, elapsedMs, contextPercent) {
|
|
|
258
258
|
export function formatProviderUsageLines(usage) {
|
|
259
259
|
return buildProviderUsageLines(usage).map(line => line.bold ? `<b>${escapeHtml(line.text)}</b>` : escapeHtml(line.text));
|
|
260
260
|
}
|
|
261
|
-
export function buildInitialPreviewHtml(agent, waiting = false) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
export function buildInitialPreviewHtml(agent, waiting = false, queuePosition = 0) {
|
|
262
|
+
if (waiting) {
|
|
263
|
+
const queueLabel = queuePosition > 0
|
|
264
|
+
? `Queued · ${queuePosition} ${queuePosition === 1 ? 'task' : 'tasks'} ahead`
|
|
265
|
+
: 'Waiting in queue...';
|
|
266
|
+
return `<i>${escapeHtml(queueLabel)}</i>\n\n${formatPreviewFooterHtml(agent, 0)}`;
|
|
267
|
+
}
|
|
268
|
+
return formatPreviewFooterHtml(agent, 0);
|
|
265
269
|
}
|
|
266
270
|
export function buildStreamPreviewHtml(input) {
|
|
267
271
|
const data = extractStreamPreviewData(input);
|
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);
|
|
@@ -178,6 +180,7 @@ export class TelegramBot extends Bot {
|
|
|
178
180
|
: null;
|
|
179
181
|
const repliedSession = this.sessionFromMessage(ctx.chatId, replyMessageId);
|
|
180
182
|
if (repliedSession) {
|
|
183
|
+
this.log(`[resolveSession] reply matched session=${repliedSession.sessionId} chat=${ctx.chatId}`);
|
|
181
184
|
this.applySessionSelection(cs, repliedSession);
|
|
182
185
|
return repliedSession;
|
|
183
186
|
}
|
|
@@ -262,9 +265,17 @@ export class TelegramBot extends Bot {
|
|
|
262
265
|
}
|
|
263
266
|
}
|
|
264
267
|
sessionsPageSize = 5;
|
|
265
|
-
buildStopKeyboard(actionId) {
|
|
268
|
+
buildStopKeyboard(actionId, opts) {
|
|
266
269
|
if (!actionId)
|
|
267
270
|
return undefined;
|
|
271
|
+
if (opts?.queued) {
|
|
272
|
+
return {
|
|
273
|
+
inline_keyboard: [[
|
|
274
|
+
{ text: 'Recall', callback_data: `tsk:stop:${actionId}` },
|
|
275
|
+
{ text: 'Steer', callback_data: `tsk:steer:${actionId}` },
|
|
276
|
+
]],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
268
279
|
return {
|
|
269
280
|
inline_keyboard: [[
|
|
270
281
|
{ text: 'Stop', callback_data: `tsk:stop:${actionId}` },
|
|
@@ -459,10 +470,10 @@ export class TelegramBot extends Bot {
|
|
|
459
470
|
}
|
|
460
471
|
const staged = stageSessionFiles({
|
|
461
472
|
agent: session.agent,
|
|
462
|
-
workdir:
|
|
473
|
+
workdir: session.workdir,
|
|
463
474
|
files: msg.files,
|
|
464
475
|
sessionId: session.sessionId,
|
|
465
|
-
title:
|
|
476
|
+
title: undefined,
|
|
466
477
|
});
|
|
467
478
|
session.workspacePath = staged.workspacePath;
|
|
468
479
|
this.syncSelectedChats(session);
|
|
@@ -498,14 +509,16 @@ export class TelegramBot extends Bot {
|
|
|
498
509
|
agent: session.agent,
|
|
499
510
|
sessionKey: session.key,
|
|
500
511
|
prompt,
|
|
512
|
+
attachments: files,
|
|
501
513
|
startedAt: start,
|
|
502
514
|
sourceMessageId: ctx.messageId,
|
|
503
515
|
});
|
|
504
|
-
const stopKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId));
|
|
505
516
|
const waiting = this.sessionHasPendingWork(session);
|
|
517
|
+
const queuePosition = waiting ? this.getQueuePosition(session.key, taskId) : 0;
|
|
518
|
+
const placeholderKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId), { queued: waiting });
|
|
506
519
|
let phId = null;
|
|
507
520
|
if (canEditMessages) {
|
|
508
|
-
const placeholderId = await ctx.reply(buildInitialPreviewHtml(session.agent, waiting), { parseMode: 'HTML', messageThreadId, keyboard:
|
|
521
|
+
const placeholderId = await ctx.reply(buildInitialPreviewHtml(session.agent, waiting, queuePosition), { parseMode: 'HTML', messageThreadId, keyboard: placeholderKeyboard });
|
|
509
522
|
phId = typeof placeholderId === 'number' ? placeholderId : null;
|
|
510
523
|
if (phId != null) {
|
|
511
524
|
this.registerSessionMessage(ctx.chatId, phId, session);
|
|
@@ -521,9 +534,10 @@ export class TelegramBot extends Bot {
|
|
|
521
534
|
this.registerTaskPlaceholders(taskId, [phId]);
|
|
522
535
|
void this.queueSessionTask(session, async () => {
|
|
523
536
|
let livePreview = null;
|
|
537
|
+
let task = null;
|
|
524
538
|
const abortController = new AbortController();
|
|
525
539
|
try {
|
|
526
|
-
|
|
540
|
+
task = this.markTaskRunning(taskId, () => abortController.abort());
|
|
527
541
|
if (!task || task.cancelled) {
|
|
528
542
|
if (phId != null) {
|
|
529
543
|
try {
|
|
@@ -534,6 +548,14 @@ export class TelegramBot extends Bot {
|
|
|
534
548
|
this.log(`[handleMessage] skipped cancelled queued task chat=${ctx.chatId} msg=${ctx.messageId}`);
|
|
535
549
|
return;
|
|
536
550
|
}
|
|
551
|
+
// Task is now running — update keyboard from Recall/Steer to Stop
|
|
552
|
+
const runningKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId));
|
|
553
|
+
if (phId != null && waiting) {
|
|
554
|
+
try {
|
|
555
|
+
await this.channel.editMessage(ctx.chatId, phId, buildInitialPreviewHtml(session.agent, false), { parseMode: 'HTML', keyboard: runningKeyboard });
|
|
556
|
+
}
|
|
557
|
+
catch { }
|
|
558
|
+
}
|
|
537
559
|
if (phId != null || canSendTyping) {
|
|
538
560
|
livePreview = new LivePreview({
|
|
539
561
|
agent: session.agent,
|
|
@@ -546,7 +568,7 @@ export class TelegramBot extends Bot {
|
|
|
546
568
|
canEditMessages,
|
|
547
569
|
canSendTyping,
|
|
548
570
|
messageThreadId,
|
|
549
|
-
keyboard:
|
|
571
|
+
keyboard: runningKeyboard,
|
|
550
572
|
log: (message) => this.log(message),
|
|
551
573
|
});
|
|
552
574
|
livePreview.start();
|
|
@@ -555,8 +577,19 @@ export class TelegramBot extends Bot {
|
|
|
555
577
|
const mcpSendFile = this.createMcpSendFileCallback(ctx, messageThreadId);
|
|
556
578
|
const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
|
|
557
579
|
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
558
|
-
}, 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
|
+
});
|
|
559
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
|
+
}
|
|
560
593
|
this.log(`[handleMessage] done agent=${session.agent} ok=${result.ok} session=${result.sessionId || '?'} elapsed=${result.elapsedS.toFixed(1)}s edits=${livePreview?.getEditCount() || 0} ` +
|
|
561
594
|
`tokens=in:${fmtTokens(result.inputTokens)}/cached:${fmtTokens(result.cachedInputTokens)}/out:${fmtTokens(result.outputTokens)}`);
|
|
562
595
|
this.log(`[handleMessage] response preview: "${result.message.slice(0, 150)}"`);
|
|
@@ -565,6 +598,12 @@ export class TelegramBot extends Bot {
|
|
|
565
598
|
this.log(`[handleMessage] final reply sent to chat=${ctx.chatId}`);
|
|
566
599
|
}
|
|
567
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
|
+
}
|
|
568
607
|
const msgText = String(e?.message || e || 'Unknown error');
|
|
569
608
|
this.log(`[handleMessage] task failed chat=${ctx.chatId} session=${session.sessionId} error=${msgText}`);
|
|
570
609
|
const errorHtml = `<b>Error</b>\n\n<code>${escapeHtml(msgText.slice(0, 500))}</code>`;
|
|
@@ -594,6 +633,23 @@ export class TelegramBot extends Bot {
|
|
|
594
633
|
this.finishTask(taskId);
|
|
595
634
|
});
|
|
596
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
|
+
}
|
|
597
653
|
/** Create an MCP sendFile callback bound to a Telegram chat context. */
|
|
598
654
|
createMcpSendFileCallback(ctx, messageThreadId) {
|
|
599
655
|
return async (filePath, opts) => {
|
|
@@ -758,6 +814,22 @@ export class TelegramBot extends Bot {
|
|
|
758
814
|
await ctx.answerCallback('Nothing to stop.');
|
|
759
815
|
return true;
|
|
760
816
|
}
|
|
817
|
+
async handleTaskSteerCallback(data, ctx) {
|
|
818
|
+
if (!data.startsWith('tsk:steer:'))
|
|
819
|
+
return false;
|
|
820
|
+
const actionId = data.slice('tsk:steer:'.length).trim();
|
|
821
|
+
const result = await this.steerTaskByActionId(actionId);
|
|
822
|
+
if (!result.task) {
|
|
823
|
+
await ctx.answerCallback('This task already finished.');
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
if (result.task.status !== 'queued') {
|
|
827
|
+
await ctx.answerCallback('Task is already running.');
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
await ctx.answerCallback(result.interrupted ? 'Steering — switching to the queued reply...' : 'No running task to interrupt.');
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
761
833
|
async handleHumanLoopCallback(data, ctx) {
|
|
762
834
|
if (!data.startsWith('hl:'))
|
|
763
835
|
return false;
|
|
@@ -824,6 +896,8 @@ export class TelegramBot extends Bot {
|
|
|
824
896
|
return;
|
|
825
897
|
if (await this.handleTaskStopCallback(data, ctx))
|
|
826
898
|
return;
|
|
899
|
+
if (await this.handleTaskSteerCallback(data, ctx))
|
|
900
|
+
return;
|
|
827
901
|
if (await this.handleSwitchNavigateCallback(data, ctx))
|
|
828
902
|
return;
|
|
829
903
|
if (await this.handleSwitchSelectCallback(data, ctx))
|