@poncho-ai/cli 0.33.1 → 0.33.3

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/src/index.ts CHANGED
@@ -303,15 +303,21 @@ const parseParams = (values: string[]): Record<string, string> => {
303
303
  return params;
304
304
  };
305
305
 
306
- const normalizeMessageForClient = (message: Message): Message => {
306
+ const normalizeMessageForClient = (message: Message): Message | null => {
307
+ // Hide tool-role and system-role messages from the web UI — they are
308
+ // internal harness bookkeeping that leaks into conv.messages when
309
+ // _harnessMessages are used as canonical history.
310
+ if (message.role === "tool" || message.role === "system") {
311
+ return null;
312
+ }
307
313
  if (message.role !== "assistant" || typeof message.content !== "string") {
308
314
  return message;
309
315
  }
310
316
  try {
311
317
  const parsed = JSON.parse(message.content) as Record<string, unknown>;
312
- const text = typeof parsed.text === "string" ? parsed.text : undefined;
313
318
  const toolCalls = Array.isArray(parsed.tool_calls) ? parsed.tool_calls : undefined;
314
- if (typeof text === "string" && toolCalls) {
319
+ if (toolCalls) {
320
+ const text = typeof parsed.text === "string" ? parsed.text : "";
315
321
  const meta = { ...(message.metadata ?? {}) } as Record<string, unknown>;
316
322
  if (!meta.sections && toolCalls.length > 0) {
317
323
  const toolLabels = toolCalls.map((tc: Record<string, unknown>) => {
@@ -491,12 +497,14 @@ const buildAssistantMetadata = (
491
497
  const executeConversationTurn = async ({
492
498
  harness,
493
499
  runInput,
500
+ events,
494
501
  initialContextTokens = 0,
495
502
  initialContextWindow = 0,
496
503
  onEvent,
497
504
  }: {
498
505
  harness: AgentHarness;
499
- runInput: Parameters<AgentHarness["runWithTelemetry"]>[0];
506
+ runInput?: Parameters<AgentHarness["runWithTelemetry"]>[0];
507
+ events?: AsyncIterable<AgentEvent>;
500
508
  initialContextTokens?: number;
501
509
  initialContextWindow?: number;
502
510
  onEvent?: (event: AgentEvent, draft: TurnDraftState) => void | Promise<void>;
@@ -512,7 +520,8 @@ const executeConversationTurn = async ({
512
520
  let runSteps = 0;
513
521
  let runMaxSteps: number | undefined;
514
522
 
515
- for await (const event of harness.runWithTelemetry(runInput)) {
523
+ const source = events ?? harness.runWithTelemetry(runInput!);
524
+ for await (const event of source) {
516
525
  recordStandardTurnEvent(draft, event);
517
526
  if (event.type === "run:started") {
518
527
  latestRunId = event.runId;
@@ -634,6 +643,142 @@ export const __internalRunOrchestration = {
634
643
  executeConversationTurn,
635
644
  };
636
645
 
646
+ // ── Shared turn metadata helper ──────────────────────────────────
647
+ // Standardises post-run metadata persistence across all execution paths.
648
+
649
+ type TurnResultMetadata = {
650
+ latestRunId: string;
651
+ contextTokens: number;
652
+ contextWindow: number;
653
+ continuation?: boolean;
654
+ continuationMessages?: Message[];
655
+ harnessMessages?: Message[];
656
+ toolResultArchive?: Conversation["_toolResultArchive"];
657
+ };
658
+
659
+ const applyTurnMetadata = (
660
+ conv: Conversation,
661
+ meta: TurnResultMetadata,
662
+ opts: {
663
+ clearContinuation?: boolean;
664
+ clearApprovals?: boolean;
665
+ setIdle?: boolean;
666
+ shouldRebuildCanonical?: boolean;
667
+ } = {},
668
+ ): void => {
669
+ const {
670
+ clearContinuation = true,
671
+ clearApprovals = true,
672
+ setIdle = true,
673
+ shouldRebuildCanonical = false,
674
+ } = opts;
675
+
676
+ if (meta.continuation && meta.continuationMessages) {
677
+ conv._continuationMessages = meta.continuationMessages;
678
+ } else if (clearContinuation) {
679
+ conv._continuationMessages = undefined;
680
+ conv._continuationCount = undefined;
681
+ }
682
+
683
+ if (meta.harnessMessages) {
684
+ conv._harnessMessages = meta.harnessMessages;
685
+ } else if (shouldRebuildCanonical) {
686
+ conv._harnessMessages = conv.messages;
687
+ }
688
+
689
+ if (meta.toolResultArchive !== undefined) {
690
+ conv._toolResultArchive = meta.toolResultArchive;
691
+ }
692
+
693
+ conv.runtimeRunId = meta.latestRunId || conv.runtimeRunId;
694
+
695
+ if (clearApprovals) conv.pendingApprovals = [];
696
+ if (setIdle) conv.runStatus = "idle";
697
+
698
+ if (meta.contextTokens > 0) conv.contextTokens = meta.contextTokens;
699
+ if (meta.contextWindow > 0) conv.contextWindow = meta.contextWindow;
700
+
701
+ conv.updatedAt = Date.now();
702
+ };
703
+
704
+ // ── Shared cron helpers ──────────────────────────────────────────
705
+ // Used by both the HTTP /api/cron endpoint and the local-dev scheduler.
706
+
707
+ type CronRunResult = {
708
+ response: string;
709
+ steps: number;
710
+ assistantMetadata?: Message["metadata"];
711
+ hasContent: boolean;
712
+ contextTokens: number;
713
+ contextWindow: number;
714
+ harnessMessages?: Message[];
715
+ toolResultArchive?: Conversation["_toolResultArchive"];
716
+ latestRunId: string;
717
+ continuation: boolean;
718
+ continuationMessages?: Message[];
719
+ };
720
+
721
+ const runCronAgent = async (
722
+ harnessRef: AgentHarness,
723
+ task: string,
724
+ conversationId: string,
725
+ historyMessages: Message[],
726
+ toolResultArchive?: Conversation["_toolResultArchive"],
727
+ onEvent?: (event: AgentEvent) => void | Promise<void>,
728
+ ): Promise<CronRunResult> => {
729
+ const execution = await executeConversationTurn({
730
+ harness: harnessRef,
731
+ runInput: {
732
+ task,
733
+ conversationId,
734
+ parameters: {
735
+ __activeConversationId: conversationId,
736
+ [TOOL_RESULT_ARCHIVE_PARAM]: toolResultArchive ?? {},
737
+ },
738
+ messages: historyMessages,
739
+ },
740
+ onEvent,
741
+ });
742
+ flushTurnDraft(execution.draft);
743
+ const hasContent = execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0;
744
+ const assistantMetadata = buildAssistantMetadata(execution.draft);
745
+ return {
746
+ response: execution.draft.assistantResponse,
747
+ steps: execution.runSteps,
748
+ assistantMetadata,
749
+ hasContent,
750
+ contextTokens: execution.runContextTokens,
751
+ contextWindow: execution.runContextWindow,
752
+ harnessMessages: execution.runHarnessMessages,
753
+ toolResultArchive: harnessRef.getToolResultArchive(conversationId),
754
+ latestRunId: execution.latestRunId,
755
+ continuation: execution.runContinuation,
756
+ continuationMessages: execution.runContinuationMessages,
757
+ };
758
+ };
759
+
760
+ const buildCronMessages = (
761
+ task: string,
762
+ historyMessages: Message[],
763
+ result: CronRunResult,
764
+ ): Message[] => [
765
+ ...historyMessages,
766
+ { role: "user" as const, content: task },
767
+ ...(result.hasContent
768
+ ? [{ role: "assistant" as const, content: result.response, metadata: result.assistantMetadata }]
769
+ : []),
770
+ ];
771
+
772
+ /** Append a cron turn to a freshly-fetched conversation (avoids overwriting concurrent writes). */
773
+ const appendCronTurn = (conv: Conversation, task: string, result: CronRunResult): void => {
774
+ conv.messages.push(
775
+ { role: "user" as const, content: task },
776
+ ...(result.hasContent
777
+ ? [{ role: "assistant" as const, content: result.response, metadata: result.assistantMetadata }]
778
+ : []),
779
+ );
780
+ };
781
+
637
782
  const AGENT_TEMPLATE = (
638
783
  name: string,
639
784
  id: string,
@@ -2701,30 +2846,23 @@ export const createRequestHandler = async (options?: {
2701
2846
  if (callbackNeedsContinuation || execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0) {
2702
2847
  const freshConv = await conversationStore.get(conversationId);
2703
2848
  if (freshConv) {
2704
- if (callbackNeedsContinuation) {
2705
- freshConv._continuationMessages = execution.runContinuationMessages;
2706
- } else {
2707
- freshConv._continuationMessages = undefined;
2849
+ if (!callbackNeedsContinuation) {
2708
2850
  freshConv.messages.push({
2709
2851
  role: "assistant",
2710
2852
  content: execution.draft.assistantResponse,
2711
2853
  metadata: buildAssistantMetadata(execution.draft),
2712
2854
  });
2713
2855
  }
2714
- if (callbackNeedsContinuation && execution.runHarnessMessages) {
2715
- freshConv._harnessMessages = execution.runHarnessMessages;
2716
- } else if (historySelection.shouldRebuildCanonical) {
2717
- freshConv._harnessMessages = freshConv.messages;
2718
- } else {
2719
- freshConv._harnessMessages = freshConv.messages;
2720
- }
2721
- freshConv._toolResultArchive = harness.getToolResultArchive(conversationId);
2722
- freshConv.runtimeRunId = execution.latestRunId || freshConv.runtimeRunId;
2856
+ applyTurnMetadata(freshConv, {
2857
+ latestRunId: execution.latestRunId,
2858
+ contextTokens: execution.runContextTokens,
2859
+ contextWindow: execution.runContextWindow,
2860
+ continuation: !!callbackNeedsContinuation,
2861
+ continuationMessages: execution.runContinuationMessages,
2862
+ harnessMessages: callbackNeedsContinuation ? execution.runHarnessMessages : undefined,
2863
+ toolResultArchive: harness.getToolResultArchive(conversationId),
2864
+ }, { shouldRebuildCanonical: true, clearApprovals: false });
2723
2865
  freshConv.runningCallbackSince = undefined;
2724
- freshConv.runStatus = "idle";
2725
- if (execution.runContextTokens > 0) freshConv.contextTokens = execution.runContextTokens;
2726
- if (execution.runContextWindow > 0) freshConv.contextWindow = execution.runContextWindow;
2727
- freshConv.updatedAt = Date.now();
2728
2866
  await conversationStore.update(freshConv);
2729
2867
 
2730
2868
  // Proactive messaging notification if conversation has a messaging channel
@@ -2955,15 +3093,7 @@ export const createRequestHandler = async (options?: {
2955
3093
  runId: null,
2956
3094
  });
2957
3095
  let latestRunId = conversation.runtimeRunId ?? "";
2958
- let assistantResponse = "";
2959
- const toolTimeline: string[] = [];
2960
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
2961
- let currentText = "";
2962
- let currentTools: string[] = [];
2963
3096
  let checkpointedRun = false;
2964
- let runContextTokens = conversation.contextTokens ?? 0;
2965
- let runContextWindow = conversation.contextWindow ?? 0;
2966
- let resumeHarnessMessages: Message[] | undefined;
2967
3097
 
2968
3098
  const normalizedCheckpoint = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
2969
3099
  const baseMessages = normalizedCheckpoint.baseMessageCount != null
@@ -3006,136 +3136,103 @@ export const createRequestHandler = async (options?: {
3006
3136
  ? [...fullCheckpointMessages, resumeToolResultMsg]
3007
3137
  : fullCheckpointMessages;
3008
3138
 
3139
+ let draftRef: TurnDraftState | undefined;
3140
+ let execution: ExecuteTurnResult | undefined;
3141
+
3009
3142
  try {
3010
- for await (const event of harness.continueFromToolResult({
3011
- messages: fullCheckpointMessages,
3012
- toolResults,
3013
- conversationId,
3014
- abortSignal: abortController.signal,
3015
- })) {
3016
- if (event.type === "run:started") {
3017
- latestRunId = event.runId;
3018
- runOwners.set(event.runId, conversation.ownerId);
3019
- runConversations.set(event.runId, conversationId);
3020
- const active = activeConversationRuns.get(conversationId);
3021
- if (active && active.abortController === abortController) {
3022
- active.runId = event.runId;
3023
- }
3024
- }
3025
- if (event.type === "model:chunk") {
3026
- if (currentTools.length > 0) {
3027
- sections.push({ type: "tools", content: currentTools });
3028
- currentTools = [];
3029
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
3030
- assistantResponse += " ";
3143
+ execution = await executeConversationTurn({
3144
+ harness,
3145
+ events: harness.continueFromToolResult({
3146
+ messages: fullCheckpointMessages,
3147
+ toolResults,
3148
+ conversationId,
3149
+ abortSignal: abortController.signal,
3150
+ }),
3151
+ initialContextTokens: conversation.contextTokens ?? 0,
3152
+ initialContextWindow: conversation.contextWindow ?? 0,
3153
+ onEvent: async (event, draft) => {
3154
+ draftRef = draft;
3155
+ if (event.type === "run:started") {
3156
+ latestRunId = event.runId;
3157
+ runOwners.set(event.runId, conversation.ownerId);
3158
+ runConversations.set(event.runId, conversationId);
3159
+ const active = activeConversationRuns.get(conversationId);
3160
+ if (active && active.abortController === abortController) {
3161
+ active.runId = event.runId;
3031
3162
  }
3032
3163
  }
3033
- assistantResponse += event.content;
3034
- currentText += event.content;
3035
- }
3036
- if (event.type === "tool:started") {
3037
- if (currentText.length > 0) {
3038
- sections.push({ type: "text", content: currentText });
3039
- currentText = "";
3164
+ if (event.type === "tool:approval:required") {
3165
+ const toolText = `- approval required \`${event.tool}\``;
3166
+ draft.toolTimeline.push(toolText);
3167
+ draft.currentTools.push(toolText);
3040
3168
  }
3041
- const toolText = `- start \`${event.tool}\``;
3042
- toolTimeline.push(toolText);
3043
- currentTools.push(toolText);
3044
- }
3045
- if (event.type === "tool:completed") {
3046
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
3047
- toolTimeline.push(toolText);
3048
- currentTools.push(toolText);
3049
- }
3050
- if (event.type === "tool:error") {
3051
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
3052
- toolTimeline.push(toolText);
3053
- currentTools.push(toolText);
3054
- }
3055
- if (event.type === "tool:approval:required") {
3056
- const toolText = `- approval required \`${event.tool}\``;
3057
- toolTimeline.push(toolText);
3058
- currentTools.push(toolText);
3059
- }
3060
- if (event.type === "tool:approval:checkpoint") {
3061
- const conv = await conversationStore.get(conversationId);
3062
- if (conv) {
3063
- conv.pendingApprovals = buildApprovalCheckpoints({
3064
- approvals: event.approvals,
3065
- runId: latestRunId,
3066
- checkpointMessages: [...fullCheckpointWithResults, ...event.checkpointMessages],
3067
- baseMessageCount: 0,
3068
- pendingToolCalls: event.pendingToolCalls,
3069
- });
3070
- conv.updatedAt = Date.now();
3071
- await conversationStore.update(conv);
3169
+ if (event.type === "tool:approval:checkpoint") {
3170
+ const conv = await conversationStore.get(conversationId);
3171
+ if (conv) {
3172
+ conv.pendingApprovals = buildApprovalCheckpoints({
3173
+ approvals: event.approvals,
3174
+ runId: latestRunId,
3175
+ checkpointMessages: [...fullCheckpointWithResults, ...event.checkpointMessages],
3176
+ baseMessageCount: 0,
3177
+ pendingToolCalls: event.pendingToolCalls,
3178
+ });
3179
+ conv.updatedAt = Date.now();
3180
+ await conversationStore.update(conv);
3072
3181
 
3073
- if (conv.channelMeta?.platform === "telegram") {
3074
- const tgAdapter = messagingAdapters.get("telegram") as TelegramAdapter | undefined;
3075
- if (tgAdapter) {
3076
- const messageThreadId = parseTelegramMessageThreadIdFromPlatformThreadId(
3077
- conv.channelMeta.platformThreadId,
3078
- conv.channelMeta.channelId,
3079
- );
3080
- void tgAdapter.sendApprovalRequest(
3081
- conv.channelMeta.channelId,
3082
- event.approvals.map(a => ({ approvalId: a.approvalId, tool: a.tool, input: a.input })),
3083
- { message_thread_id: messageThreadId },
3084
- ).catch(() => {});
3182
+ if (conv.channelMeta?.platform === "telegram") {
3183
+ const tgAdapter = messagingAdapters.get("telegram") as TelegramAdapter | undefined;
3184
+ if (tgAdapter) {
3185
+ const messageThreadId = parseTelegramMessageThreadIdFromPlatformThreadId(
3186
+ conv.channelMeta.platformThreadId,
3187
+ conv.channelMeta.channelId,
3188
+ );
3189
+ void tgAdapter.sendApprovalRequest(
3190
+ conv.channelMeta.channelId,
3191
+ event.approvals.map(a => ({ approvalId: a.approvalId, tool: a.tool, input: a.input })),
3192
+ { message_thread_id: messageThreadId },
3193
+ ).catch(() => {});
3194
+ }
3085
3195
  }
3086
3196
  }
3197
+ checkpointedRun = true;
3087
3198
  }
3088
- checkpointedRun = true;
3089
- }
3090
- if (event.type === "run:completed") {
3091
- if (assistantResponse.length === 0 && event.result.response) {
3092
- assistantResponse = event.result.response;
3093
- }
3094
- runContextTokens = event.result.contextTokens ?? runContextTokens;
3095
- runContextWindow = event.result.contextWindow ?? runContextWindow;
3096
- if (event.result.continuationMessages) {
3097
- resumeHarnessMessages = event.result.continuationMessages;
3098
- }
3099
- }
3100
- if (event.type === "run:error") {
3101
- assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
3102
- }
3103
- await telemetry.emit(event);
3104
- broadcastEvent(conversationId, event);
3105
- emitBrowserStatusIfActive(conversationId, event);
3106
- }
3199
+ await telemetry.emit(event);
3200
+ broadcastEvent(conversationId, event);
3201
+ emitBrowserStatusIfActive(conversationId, event);
3202
+ },
3203
+ });
3204
+ flushTurnDraft(execution.draft);
3205
+ latestRunId = execution.latestRunId || latestRunId;
3107
3206
  } catch (err) {
3108
3207
  console.error("[resume-run] error:", err instanceof Error ? err.message : err);
3109
- assistantResponse = assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
3208
+ if (draftRef) {
3209
+ draftRef.assistantResponse = draftRef.assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
3210
+ flushTurnDraft(draftRef);
3211
+ }
3110
3212
  }
3111
3213
 
3112
- if (currentTools.length > 0) {
3113
- sections.push({ type: "tools", content: currentTools });
3114
- }
3115
- if (currentText.length > 0) {
3116
- sections.push({ type: "text", content: currentText });
3117
- }
3214
+ const draft = execution?.draft ?? draftRef ?? createTurnDraftState();
3118
3215
 
3119
3216
  if (!checkpointedRun) {
3120
3217
  const conv = await conversationStore.get(conversationId);
3121
3218
  if (conv) {
3122
- const prevMessages = conv.messages;
3123
3219
  const hasAssistantContent =
3124
- assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
3220
+ draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0;
3125
3221
  if (hasAssistantContent) {
3222
+ const prevMessages = conv.messages;
3126
3223
  const lastMsg = prevMessages[prevMessages.length - 1];
3127
3224
  if (lastMsg && lastMsg.role === "assistant" && lastMsg.metadata) {
3128
3225
  const existingToolActivity = (lastMsg.metadata as Record<string, unknown>).toolActivity;
3129
3226
  const existingSections = (lastMsg.metadata as Record<string, unknown>).sections;
3130
3227
  const mergedTimeline = [
3131
3228
  ...(Array.isArray(existingToolActivity) ? existingToolActivity as string[] : []),
3132
- ...toolTimeline,
3229
+ ...draft.toolTimeline,
3133
3230
  ];
3134
3231
  const mergedSections = [
3135
3232
  ...(Array.isArray(existingSections) ? existingSections as Array<{ type: "text" | "tools"; content: string | string[] }> : []),
3136
- ...sections,
3233
+ ...draft.sections,
3137
3234
  ];
3138
- const mergedText = (typeof lastMsg.content === "string" ? lastMsg.content : "") + assistantResponse;
3235
+ const mergedText = (typeof lastMsg.content === "string" ? lastMsg.content : "") + draft.assistantResponse;
3139
3236
  conv.messages = [
3140
3237
  ...prevMessages.slice(0, -1),
3141
3238
  {
@@ -3152,25 +3249,18 @@ export const createRequestHandler = async (options?: {
3152
3249
  ...prevMessages,
3153
3250
  {
3154
3251
  role: "assistant" as const,
3155
- content: assistantResponse,
3156
- metadata: (toolTimeline.length > 0 || sections.length > 0
3157
- ? { toolActivity: toolTimeline, sections: sections.length > 0 ? sections : undefined }
3158
- : undefined) as Message["metadata"],
3252
+ content: draft.assistantResponse,
3253
+ metadata: buildAssistantMetadata(draft),
3159
3254
  },
3160
3255
  ];
3161
3256
  }
3162
3257
  }
3163
- if (resumeHarnessMessages) {
3164
- conv._harnessMessages = resumeHarnessMessages;
3165
- } else {
3166
- conv._harnessMessages = conv.messages;
3167
- }
3168
- conv.runtimeRunId = latestRunId || conv.runtimeRunId;
3169
- conv.pendingApprovals = [];
3170
- conv.runStatus = "idle";
3171
- if (runContextTokens > 0) conv.contextTokens = runContextTokens;
3172
- if (runContextWindow > 0) conv.contextWindow = runContextWindow;
3173
- conv.updatedAt = Date.now();
3258
+ applyTurnMetadata(conv, {
3259
+ latestRunId,
3260
+ contextTokens: execution?.runContextTokens ?? 0,
3261
+ contextWindow: execution?.runContextWindow ?? 0,
3262
+ harnessMessages: execution?.runHarnessMessages,
3263
+ }, { shouldRebuildCanonical: true });
3174
3264
  await conversationStore.update(conv);
3175
3265
  }
3176
3266
  } else {
@@ -3341,7 +3431,7 @@ export const createRequestHandler = async (options?: {
3341
3431
  conversationId,
3342
3432
  messages: historyMessages,
3343
3433
  files: input.files,
3344
- parameters: {
3434
+ parameters: withToolResultArchiveParam({
3345
3435
  ...(input.metadata ? {
3346
3436
  __messaging_platform: input.metadata.platform,
3347
3437
  __messaging_sender_id: input.metadata.sender.id,
@@ -3349,7 +3439,7 @@ export const createRequestHandler = async (options?: {
3349
3439
  __messaging_thread_id: input.metadata.threadId,
3350
3440
  } : {}),
3351
3441
  __activeConversationId: conversationId,
3352
- },
3442
+ }, latestConversation ?? { _toolResultArchive: {} } as Conversation),
3353
3443
  };
3354
3444
 
3355
3445
  try {
@@ -3448,31 +3538,31 @@ export const createRequestHandler = async (options?: {
3448
3538
 
3449
3539
  if (!checkpointedRun) {
3450
3540
  await updateConversation((c) => {
3451
- if (runContinuation && runContinuationMessages) {
3452
- c._continuationMessages = runContinuationMessages;
3453
- } else {
3454
- c._continuationMessages = undefined;
3541
+ if (!(runContinuation && runContinuationMessages)) {
3455
3542
  c.messages = buildMessages();
3456
3543
  }
3457
- if (runContinuationMessages) {
3458
- c._harnessMessages = runContinuationMessages;
3459
- } else if (shouldRebuildCanonical) {
3460
- c._harnessMessages = c.messages;
3461
- } else {
3462
- c._harnessMessages = c.messages;
3463
- }
3464
- c.runtimeRunId = latestRunId || c.runtimeRunId;
3465
- c.pendingApprovals = [];
3466
- c.runStatus = "idle";
3467
- if (runContextTokens > 0) c.contextTokens = runContextTokens;
3468
- if (runContextWindow > 0) c.contextWindow = runContextWindow;
3544
+ applyTurnMetadata(c, {
3545
+ latestRunId,
3546
+ contextTokens: runContextTokens,
3547
+ contextWindow: runContextWindow,
3548
+ continuation: runContinuation,
3549
+ continuationMessages: runContinuationMessages,
3550
+ harnessMessages: runContinuationMessages,
3551
+ toolResultArchive: harness.getToolResultArchive(conversationId),
3552
+ }, { shouldRebuildCanonical: true });
3469
3553
  });
3470
3554
  } else {
3471
3555
  await updateConversation((c) => {
3472
- if (shouldRebuildCanonical && !c._harnessMessages?.length) {
3473
- c._harnessMessages = c.messages;
3474
- }
3475
- c.runStatus = "idle";
3556
+ applyTurnMetadata(c, {
3557
+ latestRunId: "",
3558
+ contextTokens: 0,
3559
+ contextWindow: 0,
3560
+ toolResultArchive: harness.getToolResultArchive(conversationId),
3561
+ }, {
3562
+ clearContinuation: false,
3563
+ clearApprovals: false,
3564
+ shouldRebuildCanonical: shouldRebuildCanonical && !c._harnessMessages?.length,
3565
+ });
3476
3566
  });
3477
3567
  }
3478
3568
  finishConversationStream(conversationId);
@@ -3648,9 +3738,10 @@ export const createRequestHandler = async (options?: {
3648
3738
  // ── Unified continuation ──────────────────────────────────────────────
3649
3739
  const MAX_CONTINUATION_COUNT = 20;
3650
3740
 
3651
- async function* runContinuation(
3741
+ async function runContinuation(
3652
3742
  conversationId: string,
3653
- ): AsyncGenerator<AgentEvent> {
3743
+ onYield?: (event: AgentEvent) => void | Promise<void>,
3744
+ ): Promise<void> {
3654
3745
  const conversation = await conversationStore.get(conversationId);
3655
3746
  if (!conversation) return;
3656
3747
  if (Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0) return;
@@ -3693,9 +3784,11 @@ export const createRequestHandler = async (options?: {
3693
3784
 
3694
3785
  try {
3695
3786
  if (conversation.parentConversationId) {
3696
- yield* runSubagentContinuation(conversationId, conversation, continuationMessages);
3787
+ for await (const event of runSubagentContinuation(conversationId, conversation, continuationMessages)) {
3788
+ if (onYield) await onYield(event);
3789
+ }
3697
3790
  } else {
3698
- yield* runChatContinuation(conversationId, conversation, continuationMessages);
3791
+ await runChatContinuation(conversationId, conversation, continuationMessages, onYield);
3699
3792
  }
3700
3793
  } finally {
3701
3794
  activeConversationRuns.delete(conversationId);
@@ -3703,136 +3796,66 @@ export const createRequestHandler = async (options?: {
3703
3796
  }
3704
3797
  }
3705
3798
 
3706
- async function* runChatContinuation(
3799
+ async function runChatContinuation(
3707
3800
  conversationId: string,
3708
3801
  conversation: Conversation,
3709
3802
  continuationMessages: Message[],
3710
- ): AsyncGenerator<AgentEvent> {
3711
- let assistantResponse = "";
3712
- let latestRunId = conversation.runtimeRunId ?? "";
3713
- const toolTimeline: string[] = [];
3714
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
3715
- let currentTools: string[] = [];
3716
- let currentText = "";
3717
- let runContextTokens = conversation.contextTokens ?? 0;
3718
- let runContextWindow = conversation.contextWindow ?? 0;
3719
- let nextContinuationMessages: Message[] | undefined;
3720
- let nextHarnessMessages: Message[] | undefined;
3721
-
3722
- for await (const event of harness.runWithTelemetry({
3723
- conversationId,
3724
- parameters: withToolResultArchiveParam({
3725
- __activeConversationId: conversationId,
3726
- __ownerId: conversation.ownerId,
3727
- }, conversation),
3728
- messages: continuationMessages,
3729
- abortSignal: activeConversationRuns.get(conversationId)?.abortController.signal,
3730
- })) {
3731
- if (event.type === "run:started") {
3732
- latestRunId = event.runId;
3733
- runOwners.set(event.runId, conversation.ownerId);
3734
- runConversations.set(event.runId, conversationId);
3735
- const active = activeConversationRuns.get(conversationId);
3736
- if (active) active.runId = event.runId;
3737
- }
3738
- if (event.type === "model:chunk") {
3739
- if (currentTools.length > 0) {
3740
- sections.push({ type: "tools", content: currentTools });
3741
- currentTools = [];
3742
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
3743
- assistantResponse += " ";
3744
- }
3745
- }
3746
- assistantResponse += event.content;
3747
- currentText += event.content;
3748
- }
3749
- if (event.type === "tool:started") {
3750
- if (currentText.length > 0) {
3751
- sections.push({ type: "text", content: currentText });
3752
- currentText = "";
3753
- }
3754
- const toolText = `- start \`${event.tool}\``;
3755
- toolTimeline.push(toolText);
3756
- currentTools.push(toolText);
3757
- }
3758
- if (event.type === "tool:completed") {
3759
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
3760
- toolTimeline.push(toolText);
3761
- currentTools.push(toolText);
3762
- }
3763
- if (event.type === "tool:error") {
3764
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
3765
- toolTimeline.push(toolText);
3766
- currentTools.push(toolText);
3767
- }
3768
- if (event.type === "run:completed") {
3769
- runContextTokens = event.result.contextTokens ?? runContextTokens;
3770
- runContextWindow = event.result.contextWindow ?? runContextWindow;
3771
- if (event.result.continuation && event.result.continuationMessages) {
3772
- nextContinuationMessages = event.result.continuationMessages;
3773
- }
3774
- if (event.result.continuationMessages) {
3775
- nextHarnessMessages = event.result.continuationMessages;
3776
- }
3777
- if (!assistantResponse && event.result.response) {
3778
- assistantResponse = event.result.response;
3803
+ onYield?: (event: AgentEvent) => void | Promise<void>,
3804
+ ): Promise<void> {
3805
+ const execution = await executeConversationTurn({
3806
+ harness,
3807
+ runInput: {
3808
+ conversationId,
3809
+ parameters: withToolResultArchiveParam({
3810
+ __activeConversationId: conversationId,
3811
+ __ownerId: conversation.ownerId,
3812
+ }, conversation),
3813
+ messages: continuationMessages,
3814
+ abortSignal: activeConversationRuns.get(conversationId)?.abortController.signal,
3815
+ },
3816
+ initialContextTokens: conversation.contextTokens ?? 0,
3817
+ initialContextWindow: conversation.contextWindow ?? 0,
3818
+ onEvent: async (event) => {
3819
+ if (event.type === "run:started") {
3820
+ runOwners.set(event.runId, conversation.ownerId);
3821
+ runConversations.set(event.runId, conversationId);
3822
+ const active = activeConversationRuns.get(conversationId);
3823
+ if (active) active.runId = event.runId;
3779
3824
  }
3780
- }
3781
- if (event.type === "run:error") {
3782
- assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
3783
- }
3784
- await telemetry.emit(event);
3785
- broadcastEvent(conversationId, event);
3786
- yield event;
3787
- }
3788
-
3789
- if (currentTools.length > 0) sections.push({ type: "tools", content: currentTools });
3790
- if (currentText.length > 0) sections.push({ type: "text", content: currentText });
3825
+ await telemetry.emit(event);
3826
+ broadcastEvent(conversationId, event);
3827
+ if (onYield) await onYield(event);
3828
+ },
3829
+ });
3830
+ flushTurnDraft(execution.draft);
3791
3831
 
3792
3832
  const freshConv = await conversationStore.get(conversationId);
3793
3833
  if (!freshConv) return;
3794
3834
 
3795
- const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
3796
- const assistantMetadata =
3797
- toolTimeline.length > 0 || sections.length > 0
3798
- ? ({
3799
- toolActivity: [...toolTimeline],
3800
- sections: sections.length > 0 ? sections : undefined,
3801
- } as Message["metadata"])
3802
- : undefined;
3835
+ const hasContent = execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0;
3836
+ if (hasContent) {
3837
+ freshConv.messages = [
3838
+ ...freshConv.messages,
3839
+ {
3840
+ role: "assistant" as const,
3841
+ content: execution.draft.assistantResponse,
3842
+ metadata: buildAssistantMetadata(execution.draft),
3843
+ },
3844
+ ];
3845
+ }
3803
3846
 
3804
- if (nextContinuationMessages) {
3805
- if (hasContent) {
3806
- freshConv.messages = [
3807
- ...freshConv.messages,
3808
- { role: "assistant", content: assistantResponse, metadata: assistantMetadata },
3809
- ];
3810
- }
3811
- freshConv._continuationMessages = nextContinuationMessages;
3847
+ applyTurnMetadata(freshConv, {
3848
+ latestRunId: execution.latestRunId,
3849
+ contextTokens: execution.runContextTokens,
3850
+ contextWindow: execution.runContextWindow,
3851
+ continuation: execution.runContinuation,
3852
+ continuationMessages: execution.runContinuationMessages,
3853
+ harnessMessages: execution.runHarnessMessages,
3854
+ toolResultArchive: harness.getToolResultArchive(conversationId),
3855
+ }, { shouldRebuildCanonical: true });
3856
+ if (execution.runContinuation) {
3812
3857
  freshConv._continuationCount = conversation._continuationCount;
3813
- } else {
3814
- if (hasContent) {
3815
- freshConv.messages = [
3816
- ...freshConv.messages,
3817
- { role: "assistant", content: assistantResponse, metadata: assistantMetadata },
3818
- ];
3819
- }
3820
- freshConv._continuationMessages = undefined;
3821
- freshConv._continuationCount = undefined;
3822
3858
  }
3823
-
3824
- if (nextHarnessMessages) {
3825
- freshConv._harnessMessages = nextHarnessMessages;
3826
- } else {
3827
- freshConv._harnessMessages = freshConv.messages;
3828
- }
3829
- freshConv._toolResultArchive = harness.getToolResultArchive(conversationId);
3830
- freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
3831
- freshConv.pendingApprovals = [];
3832
- if (runContextTokens > 0) freshConv.contextTokens = runContextTokens;
3833
- if (runContextWindow > 0) freshConv.contextWindow = runContextWindow;
3834
- freshConv.runStatus = "idle";
3835
- freshConv.updatedAt = Date.now();
3836
3859
  await conversationStore.update(freshConv);
3837
3860
  }
3838
3861
 
@@ -4165,17 +4188,22 @@ export const createRequestHandler = async (options?: {
4165
4188
  // Regular (non-subagent) approval
4166
4189
  const found = await findPendingApproval(approvalId, "local-owner");
4167
4190
  let foundConversation = found?.conversation;
4168
- let foundApproval = found?.approval;
4191
+ const foundApproval = found?.approval;
4169
4192
 
4170
4193
  if (!foundConversation || !foundApproval) {
4171
4194
  console.warn("[telegram-approval] approval not found:", approvalId);
4172
4195
  return;
4173
4196
  }
4174
- foundApproval = normalizeApprovalCheckpoint(foundApproval, foundConversation.messages);
4175
4197
 
4176
- await adapter.updateApprovalMessage(approvalId, approved ? "approved" : "denied", foundApproval.tool);
4198
+ const approvalDecision = approved ? "approved" as const : "denied" as const;
4199
+ await adapter.updateApprovalMessage(approvalId, approvalDecision, foundApproval.tool);
4177
4200
 
4178
- foundApproval.decision = approved ? "approved" : "denied";
4201
+ foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? []).map((approval) =>
4202
+ approval.approvalId === approvalId
4203
+ ? { ...normalizeApprovalCheckpoint(approval, foundConversation!.messages), decision: approvalDecision }
4204
+ : normalizeApprovalCheckpoint(approval, foundConversation!.messages),
4205
+ );
4206
+ await conversationStore.update(foundConversation);
4179
4207
 
4180
4208
  broadcastEvent(foundConversation.conversationId,
4181
4209
  approved
@@ -4183,15 +4211,16 @@ export const createRequestHandler = async (options?: {
4183
4211
  : { type: "tool:approval:denied", approvalId },
4184
4212
  );
4185
4213
 
4186
- const allApprovals = (foundConversation.pendingApprovals ?? []).map((approval) =>
4187
- normalizeApprovalCheckpoint(approval, foundConversation!.messages),
4214
+ const refreshedConversation = await conversationStore.get(foundConversation.conversationId);
4215
+ const allApprovals = (refreshedConversation?.pendingApprovals ?? []).map((approval) =>
4216
+ normalizeApprovalCheckpoint(approval, refreshedConversation!.messages),
4188
4217
  );
4189
4218
  const allDecided = allApprovals.length > 0 && allApprovals.every(a => a.decision != null);
4190
4219
 
4191
4220
  if (!allDecided) {
4192
- await conversationStore.update(foundConversation);
4193
4221
  return;
4194
4222
  }
4223
+ foundConversation = refreshedConversation!;
4195
4224
 
4196
4225
  // All decided — resume the run
4197
4226
  const conversationId = foundConversation.conversationId;
@@ -4471,9 +4500,7 @@ export const createRequestHandler = async (options?: {
4471
4500
  writeJson(response, 202, { ok: true });
4472
4501
  const work = (async () => {
4473
4502
  try {
4474
- for await (const _event of runContinuation(conversationId)) {
4475
- // Events are already broadcast inside runContinuation
4476
- }
4503
+ await runContinuation(conversationId);
4477
4504
  // Chain: if another continuation is needed, fire next self-fetch
4478
4505
  const conv = await conversationStore.get(conversationId);
4479
4506
  if (conv?._continuationMessages?.length) {
@@ -5202,7 +5229,7 @@ export const createRequestHandler = async (options?: {
5202
5229
  writeJson(response, 200, {
5203
5230
  conversation: {
5204
5231
  ...conversation,
5205
- messages: conversation.messages.map(normalizeMessageForClient),
5232
+ messages: conversation.messages.map(normalizeMessageForClient).filter((m): m is Message => m !== null),
5206
5233
  pendingApprovals: storedPending,
5207
5234
  _continuationMessages: undefined,
5208
5235
  _harnessMessages: undefined,
@@ -5406,7 +5433,7 @@ export const createRequestHandler = async (options?: {
5406
5433
 
5407
5434
  let eventCount = 0;
5408
5435
  try {
5409
- for await (const event of runContinuation(conversationId)) {
5436
+ await runContinuation(conversationId, async (event) => {
5410
5437
  eventCount++;
5411
5438
  let sseEvent: AgentEvent = event;
5412
5439
  if (sseEvent.type === "run:completed") {
@@ -5420,7 +5447,7 @@ export const createRequestHandler = async (options?: {
5420
5447
  // Client disconnected — continue processing so the run completes
5421
5448
  }
5422
5449
  emitBrowserStatusIfActive(conversationId, event, response);
5423
- }
5450
+ });
5424
5451
  } catch (err) {
5425
5452
  const errorEvent: AgentEvent = {
5426
5453
  type: "run:error",
@@ -5546,18 +5573,6 @@ export const createRequestHandler = async (options?: {
5546
5573
  `[poncho] conversation="${conversationId}" history_source=${canonicalHistory.source}`,
5547
5574
  );
5548
5575
  let latestRunId = conversation.runtimeRunId ?? "";
5549
- let assistantResponse = "";
5550
- const toolTimeline: string[] = [];
5551
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
5552
- let currentText = "";
5553
- let currentTools: string[] = [];
5554
- let runCancelled = false;
5555
- let checkpointedRun = false;
5556
- let didCompact = false;
5557
- let runContextTokens = conversation.contextTokens ?? 0;
5558
- let runContextWindow = conversation.contextWindow ?? 0;
5559
- let runContinuationMessages: Message[] | undefined;
5560
- let runHarnessMessages: Message[] | undefined;
5561
5576
  let userContent: Message["content"] | undefined = messageText;
5562
5577
  if (files.length > 0) {
5563
5578
  try {
@@ -5593,14 +5608,51 @@ export const createRequestHandler = async (options?: {
5593
5608
  return;
5594
5609
  }
5595
5610
  }
5596
- // Forward subagent lifecycle events to the response so the client can update the sidebar.
5597
- // These events are broadcast via broadcastEvent but not emitted by the harness run loop.
5598
5611
  const unsubSubagentEvents = onConversationEvent(conversationId, (evt) => {
5599
5612
  if (evt.type.startsWith("subagent:")) {
5600
5613
  try { response.write(formatSseEvent(evt)); } catch {}
5601
5614
  }
5602
5615
  });
5603
5616
 
5617
+ const draft = createTurnDraftState();
5618
+ let checkpointedRun = false;
5619
+ let runCancelled = false;
5620
+ let runContinuationMessages: Message[] | undefined;
5621
+
5622
+ const buildMessages = (): Message[] => {
5623
+ const draftSections = cloneSections(draft.sections);
5624
+ if (draft.currentTools.length > 0) {
5625
+ draftSections.push({ type: "tools", content: [...draft.currentTools] });
5626
+ }
5627
+ if (draft.currentText.length > 0) {
5628
+ draftSections.push({ type: "text", content: draft.currentText });
5629
+ }
5630
+ const userTurn: Message[] = userContent != null
5631
+ ? [{ role: "user" as const, content: userContent }]
5632
+ : [];
5633
+ const hasDraftContent =
5634
+ draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draftSections.length > 0;
5635
+ if (!hasDraftContent) {
5636
+ return [...historyMessages, ...userTurn];
5637
+ }
5638
+ return [
5639
+ ...historyMessages,
5640
+ ...userTurn,
5641
+ {
5642
+ role: "assistant" as const,
5643
+ content: draft.assistantResponse,
5644
+ metadata: buildAssistantMetadata(draft, draftSections),
5645
+ },
5646
+ ];
5647
+ };
5648
+
5649
+ const persistDraftAssistantTurn = async (): Promise<void> => {
5650
+ if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
5651
+ conversation.messages = buildMessages();
5652
+ conversation.updatedAt = Date.now();
5653
+ await conversationStore.update(conversation);
5654
+ };
5655
+
5604
5656
  try {
5605
5657
  {
5606
5658
  conversation.messages = [...historyMessages, { role: "user", content: userContent! }];
@@ -5612,43 +5664,6 @@ export const createRequestHandler = async (options?: {
5612
5664
  });
5613
5665
  }
5614
5666
 
5615
- const persistDraftAssistantTurn = async (): Promise<void> => {
5616
- const draftSections: Array<{ type: "text" | "tools"; content: string | string[] }> = [
5617
- ...sections.map((section) => ({
5618
- type: section.type,
5619
- content: Array.isArray(section.content) ? [...section.content] : section.content,
5620
- })),
5621
- ];
5622
- if (currentTools.length > 0) {
5623
- draftSections.push({ type: "tools", content: [...currentTools] });
5624
- }
5625
- if (currentText.length > 0) {
5626
- draftSections.push({ type: "text", content: currentText });
5627
- }
5628
- const hasDraftContent =
5629
- assistantResponse.length > 0 || toolTimeline.length > 0 || draftSections.length > 0;
5630
- if (!hasDraftContent) {
5631
- return;
5632
- }
5633
- conversation.messages = [
5634
- ...historyMessages,
5635
- ...(userContent != null ? [{ role: "user" as const, content: userContent }] : []),
5636
- {
5637
- role: "assistant",
5638
- content: assistantResponse,
5639
- metadata:
5640
- toolTimeline.length > 0 || draftSections.length > 0
5641
- ? ({
5642
- toolActivity: [...toolTimeline],
5643
- sections: draftSections.length > 0 ? draftSections : undefined,
5644
- } as Message["metadata"])
5645
- : undefined,
5646
- },
5647
- ];
5648
- conversation.updatedAt = Date.now();
5649
- await conversationStore.update(conversation);
5650
- };
5651
-
5652
5667
  let cachedRecallCorpus: unknown[] | undefined;
5653
5668
  const lazyRecallCorpus = async () => {
5654
5669
  if (cachedRecallCorpus) return cachedRecallCorpus;
@@ -5682,281 +5697,168 @@ export const createRequestHandler = async (options?: {
5682
5697
  return cachedRecallCorpus;
5683
5698
  };
5684
5699
 
5685
- for await (const event of harness.runWithTelemetry({
5686
- task: messageText,
5687
- conversationId,
5688
- parameters: withToolResultArchiveParam({
5689
- ...(bodyParameters ?? {}),
5690
- __conversationRecallCorpus: lazyRecallCorpus,
5691
- __activeConversationId: conversationId,
5692
- __ownerId: ownerId,
5693
- }, conversation),
5694
- messages: harnessMessages,
5695
- files: files.length > 0 ? files : undefined,
5696
- abortSignal: abortController.signal,
5697
- })) {
5698
- if (event.type === "run:started") {
5699
- latestRunId = event.runId;
5700
- runOwners.set(event.runId, ownerId);
5701
- runConversations.set(event.runId, conversationId);
5702
- const active = activeConversationRuns.get(conversationId);
5703
- if (active && active.abortController === abortController) {
5704
- active.runId = event.runId;
5705
- }
5706
- }
5707
- if (event.type === "run:cancelled") {
5708
- runCancelled = true;
5709
- }
5710
- if (event.type === "model:chunk") {
5711
- if (currentTools.length > 0) {
5712
- sections.push({ type: "tools", content: currentTools });
5713
- currentTools = [];
5714
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
5715
- assistantResponse += " ";
5716
- }
5717
- }
5718
- assistantResponse += event.content;
5719
- currentText += event.content;
5720
- }
5721
- if (event.type === "tool:started") {
5722
- // If we have text accumulated, push it as a text section
5723
- if (currentText.length > 0) {
5724
- sections.push({ type: "text", content: currentText });
5725
- currentText = "";
5726
- }
5727
- const toolText = `- start \`${event.tool}\``;
5728
- toolTimeline.push(toolText);
5729
- currentTools.push(toolText);
5730
- }
5731
- if (event.type === "tool:completed") {
5732
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
5733
- toolTimeline.push(toolText);
5734
- currentTools.push(toolText);
5735
- }
5736
- if (event.type === "tool:error") {
5737
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
5738
- toolTimeline.push(toolText);
5739
- currentTools.push(toolText);
5740
- }
5741
- if (event.type === "compaction:completed") {
5742
- didCompact = true;
5743
- if (event.compactedMessages) {
5744
- historyMessages.length = 0;
5745
- historyMessages.push(...event.compactedMessages);
5700
+ const execution = await executeConversationTurn({
5701
+ harness,
5702
+ runInput: {
5703
+ task: messageText,
5704
+ conversationId,
5705
+ parameters: withToolResultArchiveParam({
5706
+ ...(bodyParameters ?? {}),
5707
+ __conversationRecallCorpus: lazyRecallCorpus,
5708
+ __activeConversationId: conversationId,
5709
+ __ownerId: ownerId,
5710
+ }, conversation),
5711
+ messages: harnessMessages,
5712
+ files: files.length > 0 ? files : undefined,
5713
+ abortSignal: abortController.signal,
5714
+ },
5715
+ initialContextTokens: conversation.contextTokens ?? 0,
5716
+ initialContextWindow: conversation.contextWindow ?? 0,
5717
+ onEvent: async (event, eventDraft) => {
5718
+ draft.assistantResponse = eventDraft.assistantResponse;
5719
+ draft.toolTimeline = eventDraft.toolTimeline;
5720
+ draft.sections = eventDraft.sections;
5721
+ draft.currentTools = eventDraft.currentTools;
5722
+ draft.currentText = eventDraft.currentText;
5746
5723
 
5747
- const preservedFromHistory = historyMessages.length - 1; // exclude summary
5748
- const removedCount = preRunMessages.length - Math.max(0, preservedFromHistory);
5749
- const existingHistory = conversation.compactedHistory ?? [];
5750
- conversation.compactedHistory = [
5751
- ...existingHistory,
5752
- ...preRunMessages.slice(0, removedCount),
5753
- ];
5754
- }
5755
- }
5756
- if (event.type === "step:completed") {
5757
- await persistDraftAssistantTurn();
5758
- }
5759
- if (event.type === "tool:approval:required") {
5760
- const toolText = `- approval required \`${event.tool}\``;
5761
- toolTimeline.push(toolText);
5762
- currentTools.push(toolText);
5763
- const existingApprovals = Array.isArray(conversation.pendingApprovals)
5764
- ? conversation.pendingApprovals
5765
- : [];
5766
- if (!existingApprovals.some((approval) => approval.approvalId === event.approvalId)) {
5767
- conversation.pendingApprovals = [
5768
- ...existingApprovals,
5769
- {
5770
- approvalId: event.approvalId,
5771
- runId: latestRunId || conversation.runtimeRunId || "",
5772
- tool: event.tool,
5773
- toolCallId: undefined,
5774
- input: (event.input ?? {}) as Record<string, unknown>,
5775
- checkpointMessages: undefined,
5776
- baseMessageCount: historyMessages.length,
5777
- pendingToolCalls: [],
5778
- },
5779
- ];
5780
- conversation.updatedAt = Date.now();
5781
- await conversationStore.update(conversation);
5724
+ if (event.type === "run:started") {
5725
+ latestRunId = event.runId;
5726
+ runOwners.set(event.runId, ownerId);
5727
+ runConversations.set(event.runId, conversationId);
5728
+ const active = activeConversationRuns.get(conversationId);
5729
+ if (active && active.abortController === abortController) {
5730
+ active.runId = event.runId;
5731
+ }
5782
5732
  }
5783
- await persistDraftAssistantTurn();
5784
- }
5785
- if (event.type === "tool:approval:checkpoint") {
5786
- const checkpointSections = [...sections];
5787
- if (currentTools.length > 0) {
5788
- checkpointSections.push({ type: "tools", content: [...currentTools] });
5733
+ if (event.type === "run:cancelled") {
5734
+ runCancelled = true;
5789
5735
  }
5790
- if (currentText.length > 0) {
5791
- checkpointSections.push({ type: "text", content: currentText });
5736
+ if (event.type === "compaction:completed") {
5737
+ if (event.compactedMessages) {
5738
+ historyMessages.length = 0;
5739
+ historyMessages.push(...event.compactedMessages);
5740
+
5741
+ const preservedFromHistory = historyMessages.length - 1;
5742
+ const removedCount = preRunMessages.length - Math.max(0, preservedFromHistory);
5743
+ const existingHistory = conversation.compactedHistory ?? [];
5744
+ conversation.compactedHistory = [
5745
+ ...existingHistory,
5746
+ ...preRunMessages.slice(0, removedCount),
5747
+ ];
5748
+ }
5792
5749
  }
5793
- conversation.messages = [
5794
- ...historyMessages,
5795
- ...(userContent != null ? [{ role: "user" as const, content: userContent }] : []),
5796
- ...(assistantResponse.length > 0 || toolTimeline.length > 0 || checkpointSections.length > 0
5797
- ? [{
5798
- role: "assistant" as const,
5799
- content: assistantResponse,
5800
- metadata: (toolTimeline.length > 0 || checkpointSections.length > 0
5801
- ? { toolActivity: [...toolTimeline], sections: checkpointSections.length > 0 ? checkpointSections : undefined }
5802
- : undefined) as Message["metadata"],
5803
- }]
5804
- : []),
5805
- ];
5806
- conversation.pendingApprovals = buildApprovalCheckpoints({
5807
- approvals: event.approvals,
5808
- runId: latestRunId,
5809
- checkpointMessages: event.checkpointMessages,
5810
- baseMessageCount: historyMessages.length,
5811
- pendingToolCalls: event.pendingToolCalls,
5812
- });
5813
- conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5814
- conversation.updatedAt = Date.now();
5815
- await conversationStore.update(conversation);
5816
- checkpointedRun = true;
5817
- }
5818
- if (event.type === "run:completed") {
5819
- if (assistantResponse.length === 0 && event.result.response) {
5820
- assistantResponse = event.result.response;
5750
+ if (event.type === "step:completed") {
5751
+ await persistDraftAssistantTurn();
5821
5752
  }
5822
- runContextTokens = event.result.contextTokens ?? runContextTokens;
5823
- runContextWindow = event.result.contextWindow ?? runContextWindow;
5824
- if (event.result.continuationMessages) {
5825
- runHarnessMessages = event.result.continuationMessages;
5753
+ if (event.type === "tool:approval:required") {
5754
+ const toolText = `- approval required \`${event.tool}\``;
5755
+ draft.toolTimeline.push(toolText);
5756
+ draft.currentTools.push(toolText);
5757
+ const existingApprovals = Array.isArray(conversation.pendingApprovals)
5758
+ ? conversation.pendingApprovals
5759
+ : [];
5760
+ if (!existingApprovals.some((approval) => approval.approvalId === event.approvalId)) {
5761
+ conversation.pendingApprovals = [
5762
+ ...existingApprovals,
5763
+ {
5764
+ approvalId: event.approvalId,
5765
+ runId: latestRunId || conversation.runtimeRunId || "",
5766
+ tool: event.tool,
5767
+ toolCallId: undefined,
5768
+ input: (event.input ?? {}) as Record<string, unknown>,
5769
+ checkpointMessages: undefined,
5770
+ baseMessageCount: historyMessages.length,
5771
+ pendingToolCalls: [],
5772
+ },
5773
+ ];
5774
+ conversation.updatedAt = Date.now();
5775
+ await conversationStore.update(conversation);
5776
+ }
5777
+ await persistDraftAssistantTurn();
5826
5778
  }
5827
- if (event.result.continuation && event.result.continuationMessages) {
5828
- runContinuationMessages = event.result.continuationMessages;
5829
-
5830
- // Persist intermediate messages so clients connecting later
5831
- // see progress, plus _continuationMessages for the next step.
5832
- const intSections = [...sections];
5833
- if (currentTools.length > 0) intSections.push({ type: "tools", content: [...currentTools] });
5834
- if (currentText.length > 0) intSections.push({ type: "text", content: currentText });
5835
- const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0 || intSections.length > 0;
5836
- const intMetadata = toolTimeline.length > 0 || intSections.length > 0
5837
- ? ({ toolActivity: [...toolTimeline], sections: intSections.length > 0 ? intSections : undefined } as Message["metadata"])
5838
- : undefined;
5839
- conversation.messages = [
5840
- ...historyMessages,
5841
- ...(userContent != null ? [{ role: "user" as const, content: userContent }] : []),
5842
- ...(hasContent ? [{ role: "assistant" as const, content: assistantResponse, metadata: intMetadata }] : []),
5843
- ];
5844
- conversation._continuationMessages = runContinuationMessages;
5845
- conversation._harnessMessages = runContinuationMessages;
5779
+ if (event.type === "tool:approval:checkpoint") {
5780
+ conversation.messages = buildMessages();
5781
+ conversation.pendingApprovals = buildApprovalCheckpoints({
5782
+ approvals: event.approvals,
5783
+ runId: latestRunId,
5784
+ checkpointMessages: event.checkpointMessages,
5785
+ baseMessageCount: historyMessages.length,
5786
+ pendingToolCalls: event.pendingToolCalls,
5787
+ });
5846
5788
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5847
- conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
5848
- if (!checkpointedRun) {
5849
- conversation.pendingApprovals = [];
5850
- }
5851
- if (runContextTokens > 0) conversation.contextTokens = runContextTokens;
5852
- if (runContextWindow > 0) conversation.contextWindow = runContextWindow;
5853
5789
  conversation.updatedAt = Date.now();
5854
5790
  await conversationStore.update(conversation);
5791
+ checkpointedRun = true;
5792
+ }
5793
+ if (event.type === "run:completed") {
5794
+ if (event.result.continuation && event.result.continuationMessages) {
5795
+ runContinuationMessages = event.result.continuationMessages;
5796
+
5797
+ conversation.messages = buildMessages();
5798
+ conversation._continuationMessages = runContinuationMessages;
5799
+ conversation._harnessMessages = runContinuationMessages;
5800
+ conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5801
+ conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
5802
+ if (!checkpointedRun) {
5803
+ conversation.pendingApprovals = [];
5804
+ }
5805
+ if ((event.result.contextTokens ?? 0) > 0) conversation.contextTokens = event.result.contextTokens!;
5806
+ if ((event.result.contextWindow ?? 0) > 0) conversation.contextWindow = event.result.contextWindow!;
5807
+ conversation.updatedAt = Date.now();
5808
+ await conversationStore.update(conversation);
5809
+
5810
+ if (!checkpointedRun) {
5811
+ doWaitUntil(
5812
+ new Promise(r => setTimeout(r, 3000)).then(() =>
5813
+ selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
5814
+ ),
5815
+ );
5816
+ }
5817
+ }
5818
+ }
5855
5819
 
5856
- // Delayed safety net: if the client doesn't POST to /continue
5857
- // within 3 seconds (e.g. browser closed), the server picks it up.
5858
- if (!checkpointedRun) {
5859
- doWaitUntil(
5860
- new Promise(r => setTimeout(r, 3000)).then(() =>
5861
- selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
5862
- ),
5863
- );
5820
+ await telemetry.emit(event);
5821
+ let sseEvent: AgentEvent = event.type === "compaction:completed" && event.compactedMessages
5822
+ ? { ...event, compactedMessages: undefined }
5823
+ : event;
5824
+ if (sseEvent.type === "run:completed") {
5825
+ const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
5826
+ const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
5827
+ if (hasPendingSubagents) {
5828
+ sseEvent = { ...stripped, pendingSubagents: true };
5829
+ } else {
5830
+ sseEvent = stripped;
5864
5831
  }
5865
5832
  }
5866
- }
5867
- await telemetry.emit(event);
5868
- let sseEvent: AgentEvent = event.type === "compaction:completed" && event.compactedMessages
5869
- ? { ...event, compactedMessages: undefined }
5870
- : event;
5871
- if (sseEvent.type === "run:completed") {
5872
- const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
5873
- const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
5874
- if (hasPendingSubagents) {
5875
- sseEvent = { ...stripped, pendingSubagents: true };
5876
- } else {
5877
- sseEvent = stripped;
5833
+ broadcastEvent(conversationId, sseEvent);
5834
+ try {
5835
+ response.write(formatSseEvent(sseEvent));
5836
+ } catch {
5837
+ // Client disconnected — continue processing so the run completes.
5878
5838
  }
5879
- }
5880
- broadcastEvent(conversationId, sseEvent);
5881
- try {
5882
- response.write(formatSseEvent(sseEvent));
5883
- } catch {
5884
- // Client disconnected (e.g. browser refresh). Continue processing
5885
- // so the run completes and conversation is persisted.
5886
- }
5887
- emitBrowserStatusIfActive(conversationId, event, response);
5888
- }
5889
- // Finalize sections
5890
- if (currentTools.length > 0) {
5891
- sections.push({ type: "tools", content: currentTools });
5892
- }
5893
- if (currentText.length > 0) {
5894
- sections.push({ type: "text", content: currentText });
5895
- }
5839
+ emitBrowserStatusIfActive(conversationId, event, response);
5840
+ },
5841
+ });
5842
+
5843
+ flushTurnDraft(draft);
5844
+ latestRunId = execution.latestRunId || latestRunId;
5845
+
5896
5846
  if (!checkpointedRun && !runContinuationMessages) {
5897
- const hasAssistantContent =
5898
- assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
5899
- const userTurn: Message[] = userContent != null
5900
- ? [{ role: "user", content: userContent }]
5901
- : [];
5902
- conversation.messages = hasAssistantContent
5903
- ? [
5904
- ...historyMessages,
5905
- ...userTurn,
5906
- {
5907
- role: "assistant",
5908
- content: assistantResponse,
5909
- metadata:
5910
- toolTimeline.length > 0 || sections.length > 0
5911
- ? ({
5912
- toolActivity: toolTimeline,
5913
- sections: sections.length > 0 ? sections : undefined,
5914
- } as Message["metadata"])
5915
- : undefined,
5916
- },
5917
- ]
5918
- : [...historyMessages, ...userTurn];
5919
- conversation._continuationMessages = undefined;
5920
- if (runHarnessMessages) {
5921
- conversation._harnessMessages = runHarnessMessages;
5922
- } else if (shouldRebuildCanonical) {
5923
- conversation._harnessMessages = conversation.messages;
5924
- } else {
5925
- conversation._harnessMessages = conversation.messages;
5926
- }
5927
- conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5928
- conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
5929
- conversation.pendingApprovals = [];
5930
- if (runContextTokens > 0) conversation.contextTokens = runContextTokens;
5931
- if (runContextWindow > 0) conversation.contextWindow = runContextWindow;
5932
- conversation.updatedAt = Date.now();
5847
+ conversation.messages = buildMessages();
5848
+ applyTurnMetadata(conversation, {
5849
+ latestRunId,
5850
+ contextTokens: execution.runContextTokens,
5851
+ contextWindow: execution.runContextWindow,
5852
+ harnessMessages: execution.runHarnessMessages,
5853
+ toolResultArchive: harness.getToolResultArchive(conversationId),
5854
+ }, { shouldRebuildCanonical });
5933
5855
  await conversationStore.update(conversation);
5934
5856
  }
5935
5857
  } catch (error) {
5858
+ flushTurnDraft(draft);
5936
5859
  if (abortController.signal.aborted || runCancelled) {
5937
- const fallbackSections = [...sections];
5938
- if (currentTools.length > 0) {
5939
- fallbackSections.push({ type: "tools", content: [...currentTools] });
5940
- }
5941
- if (currentText.length > 0) {
5942
- fallbackSections.push({ type: "text", content: currentText });
5943
- }
5944
- if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
5945
- conversation.messages = [
5946
- ...historyMessages,
5947
- ...(userContent != null ? [{ role: "user" as const, content: userContent }] : []),
5948
- {
5949
- role: "assistant",
5950
- content: assistantResponse,
5951
- metadata:
5952
- toolTimeline.length > 0 || fallbackSections.length > 0
5953
- ? ({
5954
- toolActivity: [...toolTimeline],
5955
- sections: fallbackSections.length > 0 ? fallbackSections : undefined,
5956
- } as Message["metadata"])
5957
- : undefined,
5958
- },
5959
- ];
5860
+ if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
5861
+ conversation.messages = buildMessages();
5960
5862
  conversation.updatedAt = Date.now();
5961
5863
  await conversationStore.update(conversation);
5962
5864
  }
@@ -5977,29 +5879,8 @@ export const createRequestHandler = async (options?: {
5977
5879
  }),
5978
5880
  );
5979
5881
  } catch {
5980
- const fallbackSections = [...sections];
5981
- if (currentTools.length > 0) {
5982
- fallbackSections.push({ type: "tools", content: [...currentTools] });
5983
- }
5984
- if (currentText.length > 0) {
5985
- fallbackSections.push({ type: "text", content: currentText });
5986
- }
5987
- if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
5988
- conversation.messages = [
5989
- ...historyMessages,
5990
- ...(userContent != null ? [{ role: "user" as const, content: userContent }] : []),
5991
- {
5992
- role: "assistant",
5993
- content: assistantResponse,
5994
- metadata:
5995
- toolTimeline.length > 0 || fallbackSections.length > 0
5996
- ? ({
5997
- toolActivity: [...toolTimeline],
5998
- sections: fallbackSections.length > 0 ? fallbackSections : undefined,
5999
- } as Message["metadata"])
6000
- : undefined,
6001
- },
6002
- ];
5882
+ if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
5883
+ conversation.messages = buildMessages();
6003
5884
  conversation.updatedAt = Date.now();
6004
5885
  await conversationStore.update(conversation);
6005
5886
  }
@@ -6015,10 +5896,6 @@ export const createRequestHandler = async (options?: {
6015
5896
  runConversations.delete(latestRunId);
6016
5897
  }
6017
5898
 
6018
- // Determine if subagent work is pending before deciding to close the
6019
- // event stream. When a callback is about to run, the stream stays open
6020
- // so clients that subscribe to /events receive callback-run events in
6021
- // real-time — the same delivery path used for every other run.
6022
5899
  const hadDeferred = pendingCallbackNeeded.delete(conversationId);
6023
5900
  const freshConv = await conversationStore.get(conversationId);
6024
5901
  const needsCallback = hadDeferred || !!freshConv?.pendingSubagentResults?.length;
@@ -6106,53 +5983,37 @@ export const createRequestHandler = async (options?: {
6106
5983
  });
6107
5984
  const historyMessages = [...historySelection.messages];
6108
5985
  try {
6109
- const execution = await executeConversationTurn({
6110
- harness,
6111
- runInput: {
6112
- task,
6113
- conversationId: conv.conversationId,
6114
- parameters: withToolResultArchiveParam(
6115
- { __activeConversationId: conv.conversationId },
6116
- conv,
6117
- ),
6118
- messages: historyMessages,
6119
- },
6120
- onEvent: async (event) => {
6121
- await telemetry.emit(event);
6122
- },
6123
- });
6124
- const assistantResponse = execution.draft.assistantResponse;
6125
-
6126
- conv.messages = [
6127
- ...historyMessages,
6128
- { role: "user" as const, content: task },
6129
- ...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
6130
- ];
6131
- if (execution.runHarnessMessages) {
6132
- conv._harnessMessages = execution.runHarnessMessages;
6133
- } else if (historySelection.shouldRebuildCanonical) {
6134
- conv._harnessMessages = conv.messages;
5986
+ const result = await runCronAgent(harness, task, conv.conversationId, historyMessages,
5987
+ conv._toolResultArchive,
5988
+ async (event) => { await telemetry.emit(event); },
5989
+ );
5990
+
5991
+ const freshConv = await conversationStore.get(conv.conversationId);
5992
+ if (freshConv) {
5993
+ appendCronTurn(freshConv, task, result);
5994
+ applyTurnMetadata(freshConv, result, {
5995
+ clearContinuation: false,
5996
+ clearApprovals: false,
5997
+ setIdle: false,
5998
+ shouldRebuildCanonical: historySelection.shouldRebuildCanonical,
5999
+ });
6000
+ await conversationStore.update(freshConv);
6135
6001
  }
6136
- conv._toolResultArchive = harness.getToolResultArchive(conv.conversationId);
6137
- if (execution.runContextTokens > 0) conv.contextTokens = execution.runContextTokens;
6138
- if (execution.runContextWindow > 0) conv.contextWindow = execution.runContextWindow;
6139
- conv.updatedAt = Date.now();
6140
- await conversationStore.update(conv);
6141
6002
 
6142
- if (assistantResponse) {
6003
+ if (result.response) {
6143
6004
  try {
6144
6005
  await adapter.sendReply(
6145
6006
  {
6146
6007
  channelId: chatId,
6147
- platformThreadId: conv.channelMeta?.platformThreadId ?? chatId,
6008
+ platformThreadId: (freshConv ?? conv).channelMeta?.platformThreadId ?? chatId,
6148
6009
  },
6149
- assistantResponse,
6010
+ result.response,
6150
6011
  );
6151
6012
  } catch (sendError) {
6152
6013
  console.error(`[cron] ${jobName}: send to ${chatId} failed:`, sendError instanceof Error ? sendError.message : sendError);
6153
6014
  }
6154
6015
  }
6155
- chatResults.push({ chatId, status: "completed", steps: execution.runSteps });
6016
+ chatResults.push({ chatId, status: "completed", steps: result.steps });
6156
6017
  } catch (runError) {
6157
6018
  chatResults.push({ chatId, status: "error" });
6158
6019
  console.error(`[cron] ${jobName}: run for chat ${chatId} failed:`, runError instanceof Error ? runError.message : runError);
@@ -6180,7 +6041,6 @@ export const createRequestHandler = async (options?: {
6180
6041
  cronOwnerId,
6181
6042
  `[cron] ${jobName} ${timestamp}`,
6182
6043
  );
6183
- const historyMessages: Message[] = [];
6184
6044
 
6185
6045
  const convId = conversation.conversationId;
6186
6046
  activeConversationRuns.set(convId, {
@@ -6190,152 +6050,45 @@ export const createRequestHandler = async (options?: {
6190
6050
  });
6191
6051
 
6192
6052
  try {
6193
- const abortController = new AbortController();
6194
- let assistantResponse = "";
6195
- let latestRunId = "";
6196
- let runContinuationMessages: Message[] | undefined;
6197
- const toolTimeline: string[] = [];
6198
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
6199
- let currentTools: string[] = [];
6200
- let currentText = "";
6201
- let runResult: { status: string; steps: number; continuation?: boolean; contextTokens?: number; contextWindow?: number; harnessMessages?: Message[] } = {
6202
- status: "completed",
6203
- steps: 0,
6204
- };
6205
-
6206
- const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
6207
- const softDeadlineMs = platformMaxDurationSec > 0
6208
- ? platformMaxDurationSec * 800
6209
- : 0;
6210
-
6211
- for await (const event of harness.runWithTelemetry({
6212
- task: cronJob.task,
6213
- conversationId: convId,
6214
- parameters: withToolResultArchiveParam({ __activeConversationId: convId }, conversation),
6215
- messages: historyMessages,
6216
- abortSignal: abortController.signal,
6217
- })) {
6218
- if (event.type === "run:started") {
6219
- latestRunId = event.runId;
6220
- }
6221
- if (event.type === "model:chunk") {
6222
- if (currentTools.length > 0) {
6223
- sections.push({ type: "tools", content: currentTools });
6224
- currentTools = [];
6225
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
6226
- assistantResponse += " ";
6227
- }
6228
- }
6229
- assistantResponse += event.content;
6230
- currentText += event.content;
6231
- }
6232
- if (event.type === "tool:started") {
6233
- if (currentText.length > 0) {
6234
- sections.push({ type: "text", content: currentText });
6235
- currentText = "";
6236
- }
6237
- const toolText = `- start \`${event.tool}\``;
6238
- toolTimeline.push(toolText);
6239
- currentTools.push(toolText);
6240
- }
6241
- if (event.type === "tool:completed") {
6242
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
6243
- toolTimeline.push(toolText);
6244
- currentTools.push(toolText);
6245
- }
6246
- if (event.type === "tool:error") {
6247
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
6248
- toolTimeline.push(toolText);
6249
- currentTools.push(toolText);
6250
- }
6251
- if (event.type === "run:completed") {
6252
- runResult = {
6253
- status: event.result.status,
6254
- steps: event.result.steps,
6255
- continuation: event.result.continuation,
6256
- contextTokens: event.result.contextTokens,
6257
- contextWindow: event.result.contextWindow,
6258
- harnessMessages: event.result.continuationMessages,
6259
- };
6260
- if (event.result.continuation && event.result.continuationMessages) {
6261
- runContinuationMessages = event.result.continuationMessages;
6262
- }
6263
- if (!assistantResponse && event.result.response) {
6264
- assistantResponse = event.result.response;
6265
- }
6053
+ const result = await runCronAgent(harness, cronJob.task, convId, [],
6054
+ conversation._toolResultArchive,
6055
+ async (event) => {
6056
+ broadcastEvent(convId, event);
6057
+ await telemetry.emit(event);
6058
+ },
6059
+ );
6060
+ finishConversationStream(convId);
6061
+
6062
+ const freshConv = await conversationStore.get(convId);
6063
+ if (freshConv) {
6064
+ freshConv.messages = buildCronMessages(cronJob.task, [], result);
6065
+ applyTurnMetadata(freshConv, result, {
6066
+ clearApprovals: false,
6067
+ setIdle: false,
6068
+ });
6069
+ await conversationStore.update(freshConv);
6266
6070
  }
6267
- broadcastEvent(convId, event);
6268
- await telemetry.emit(event);
6269
- }
6270
6071
 
6271
- finishConversationStream(convId);
6272
-
6273
- if (currentTools.length > 0) {
6274
- sections.push({ type: "tools", content: currentTools });
6275
- }
6276
- if (currentText.length > 0) {
6277
- sections.push({ type: "text", content: currentText });
6278
- currentText = "";
6279
- }
6280
-
6281
- // Persist the conversation — read fresh state to avoid clobbering
6282
- // pendingSubagentResults appended during the run.
6283
- const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
6284
- const assistantMetadata =
6285
- toolTimeline.length > 0 || sections.length > 0
6286
- ? ({
6287
- toolActivity: [...toolTimeline],
6288
- sections: sections.length > 0 ? sections : undefined,
6289
- } as Message["metadata"])
6290
- : undefined;
6291
- const messages: Message[] = [
6292
- ...historyMessages,
6293
- { role: "user" as const, content: cronJob.task },
6294
- ...(hasContent
6295
- ? [{ role: "assistant" as const, content: assistantResponse, metadata: assistantMetadata }]
6296
- : []),
6297
- ];
6298
- const freshConv = await conversationStore.get(convId);
6299
- if (freshConv) {
6300
- // Always persist intermediate messages so clients see progress
6301
- freshConv.messages = messages;
6302
- if (runContinuationMessages) {
6303
- freshConv._continuationMessages = runContinuationMessages;
6304
- } else {
6305
- freshConv._continuationMessages = undefined;
6306
- freshConv._continuationCount = undefined;
6307
- }
6308
- if (runResult.harnessMessages) {
6309
- freshConv._harnessMessages = runResult.harnessMessages;
6072
+ if (result.continuation) {
6073
+ const work = selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(convId)}`).catch(err =>
6074
+ console.error(`[poncho][cron] Continuation self-fetch failed:`, err instanceof Error ? err.message : err),
6075
+ );
6076
+ doWaitUntil(work);
6077
+ writeJson(response, 200, {
6078
+ conversationId: convId,
6079
+ status: "continued",
6080
+ duration: Date.now() - start,
6081
+ });
6082
+ return;
6310
6083
  }
6311
- freshConv._toolResultArchive = harness.getToolResultArchive(convId);
6312
- freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
6313
- if (runResult.contextTokens) freshConv.contextTokens = runResult.contextTokens;
6314
- if (runResult.contextWindow) freshConv.contextWindow = runResult.contextWindow;
6315
- freshConv.updatedAt = Date.now();
6316
- await conversationStore.update(freshConv);
6317
- }
6318
6084
 
6319
- if (runResult.continuation) {
6320
- const work = selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(convId)}`).catch(err =>
6321
- console.error(`[poncho][cron] Continuation self-fetch failed:`, err instanceof Error ? err.message : err),
6322
- );
6323
- doWaitUntil(work);
6324
6085
  writeJson(response, 200, {
6325
6086
  conversationId: convId,
6326
- status: "continued",
6087
+ status: "completed",
6088
+ response: result.response.slice(0, 500),
6327
6089
  duration: Date.now() - start,
6090
+ steps: result.steps,
6328
6091
  });
6329
- return;
6330
- }
6331
-
6332
- writeJson(response, 200, {
6333
- conversationId: convId,
6334
- status: runResult.status,
6335
- response: assistantResponse.slice(0, 500),
6336
- duration: Date.now() - start,
6337
- steps: runResult.steps,
6338
- });
6339
6092
  } finally {
6340
6093
  activeConversationRuns.delete(convId);
6341
6094
  const hadDeferred = pendingCallbackNeeded.delete(convId);
@@ -6417,26 +6170,19 @@ export const createRequestHandler = async (options?: {
6417
6170
  if (channelMeta) {
6418
6171
  const adapter = messagingAdapters.get(channelMeta.platform);
6419
6172
  if (adapter && originConv) {
6420
- const historyMessages = originConv.messages ?? [];
6421
- let assistantResponse = "";
6422
- for await (const event of harness.runWithTelemetry({
6423
- task: framedMessage,
6424
- conversationId: originConv.conversationId,
6425
- parameters: { __activeConversationId: originConv.conversationId },
6426
- messages: historyMessages,
6427
- })) {
6428
- if (event.type === "model:chunk") {
6429
- assistantResponse += event.content;
6430
- }
6431
- }
6432
- if (assistantResponse) {
6173
+ const result = await runCronAgent(
6174
+ harness, framedMessage, originConv.conversationId,
6175
+ originConv.messages ?? [],
6176
+ originConv._toolResultArchive,
6177
+ );
6178
+ if (result.response) {
6433
6179
  try {
6434
6180
  await adapter.sendReply(
6435
6181
  {
6436
6182
  channelId: channelMeta.channelId,
6437
6183
  platformThreadId: channelMeta.platformThreadId ?? channelMeta.channelId,
6438
6184
  },
6439
- assistantResponse,
6185
+ result.response,
6440
6186
  );
6441
6187
  } catch (sendError) {
6442
6188
  console.error(`[reminder] Send to ${channelMeta.platform} failed:`, sendError instanceof Error ? sendError.message : sendError);
@@ -6444,12 +6190,12 @@ export const createRequestHandler = async (options?: {
6444
6190
  }
6445
6191
  const freshConv = await conversationStore.get(originConv.conversationId);
6446
6192
  if (freshConv) {
6447
- freshConv.messages = [
6448
- ...historyMessages,
6449
- { role: "user" as const, content: framedMessage },
6450
- ...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
6451
- ];
6452
- freshConv.updatedAt = Date.now();
6193
+ appendCronTurn(freshConv, framedMessage, result);
6194
+ applyTurnMetadata(freshConv, result, {
6195
+ clearContinuation: false,
6196
+ clearApprovals: false,
6197
+ setIdle: false,
6198
+ });
6453
6199
  await conversationStore.update(freshConv);
6454
6200
  }
6455
6201
  }
@@ -6460,24 +6206,15 @@ export const createRequestHandler = async (options?: {
6460
6206
  `[reminder] ${reminder.task.slice(0, 80)} ${timestamp}`,
6461
6207
  );
6462
6208
  const convId = conversation.conversationId;
6463
- let assistantResponse = "";
6464
- for await (const event of harness.runWithTelemetry({
6465
- task: framedMessage,
6466
- conversationId: convId,
6467
- parameters: { __activeConversationId: convId },
6468
- messages: [],
6469
- })) {
6470
- if (event.type === "model:chunk") {
6471
- assistantResponse += event.content;
6472
- }
6473
- }
6209
+ const result = await runCronAgent(harness, framedMessage, convId, []);
6474
6210
  const freshConv = await conversationStore.get(convId);
6475
6211
  if (freshConv) {
6476
- freshConv.messages = [
6477
- { role: "user" as const, content: framedMessage },
6478
- ...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
6479
- ];
6480
- freshConv.updatedAt = Date.now();
6212
+ freshConv.messages = buildCronMessages(framedMessage, [], result);
6213
+ applyTurnMetadata(freshConv, result, {
6214
+ clearContinuation: false,
6215
+ clearApprovals: false,
6216
+ setIdle: false,
6217
+ });
6481
6218
  await conversationStore.update(freshConv);
6482
6219
  }
6483
6220
  }
@@ -6568,65 +6305,6 @@ export const startDevServer = async (
6568
6305
  type CronJob = InstanceType<typeof Cron>;
6569
6306
  let activeJobs: CronJob[] = [];
6570
6307
 
6571
- type CronRunResult = {
6572
- response: string;
6573
- steps: number;
6574
- assistantMetadata?: Message["metadata"];
6575
- hasContent: boolean;
6576
- contextTokens: number;
6577
- contextWindow: number;
6578
- harnessMessages?: Message[];
6579
- toolResultArchive?: Conversation["_toolResultArchive"];
6580
- };
6581
-
6582
- const runCronAgent = async (
6583
- harnessRef: AgentHarness,
6584
- task: string,
6585
- conversationId: string,
6586
- historyMessages: Message[],
6587
- toolResultArchive?: Conversation["_toolResultArchive"],
6588
- onEvent?: (event: AgentEvent) => void | Promise<void>,
6589
- ): Promise<CronRunResult> => {
6590
- const execution = await executeConversationTurn({
6591
- harness: harnessRef,
6592
- runInput: {
6593
- task,
6594
- conversationId,
6595
- parameters: {
6596
- __activeConversationId: conversationId,
6597
- [TOOL_RESULT_ARCHIVE_PARAM]: toolResultArchive ?? {},
6598
- },
6599
- messages: historyMessages,
6600
- },
6601
- onEvent,
6602
- });
6603
- flushTurnDraft(execution.draft);
6604
- const hasContent = execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0;
6605
- const assistantMetadata = buildAssistantMetadata(execution.draft);
6606
- return {
6607
- response: execution.draft.assistantResponse,
6608
- steps: execution.runSteps,
6609
- assistantMetadata,
6610
- hasContent,
6611
- contextTokens: execution.runContextTokens,
6612
- contextWindow: execution.runContextWindow,
6613
- harnessMessages: execution.runHarnessMessages,
6614
- toolResultArchive: harnessRef.getToolResultArchive(conversationId),
6615
- };
6616
- };
6617
-
6618
- const buildCronMessages = (
6619
- task: string,
6620
- historyMessages: Message[],
6621
- result: CronRunResult,
6622
- ): Message[] => [
6623
- ...historyMessages,
6624
- { role: "user" as const, content: task },
6625
- ...(result.hasContent
6626
- ? [{ role: "assistant" as const, content: result.response, metadata: result.assistantMetadata }]
6627
- : []),
6628
- ];
6629
-
6630
6308
  const scheduleCronJobs = (jobs: Record<string, CronJobConfig>): void => {
6631
6309
  for (const job of activeJobs) {
6632
6310
  job.stop();
@@ -6704,18 +6382,13 @@ export const startDevServer = async (
6704
6382
 
6705
6383
  const freshConv = await store.get(convId);
6706
6384
  if (freshConv) {
6707
- freshConv.messages = buildCronMessages(task, historyMessages, result);
6708
- if (result.harnessMessages) {
6709
- freshConv._harnessMessages = result.harnessMessages;
6710
- } else if (historySelection.shouldRebuildCanonical) {
6711
- freshConv._harnessMessages = freshConv.messages;
6712
- }
6713
- if (result.toolResultArchive) {
6714
- freshConv._toolResultArchive = result.toolResultArchive;
6715
- }
6716
- if (result.contextTokens > 0) freshConv.contextTokens = result.contextTokens;
6717
- if (result.contextWindow > 0) freshConv.contextWindow = result.contextWindow;
6718
- freshConv.updatedAt = Date.now();
6385
+ appendCronTurn(freshConv, task, result);
6386
+ applyTurnMetadata(freshConv, result, {
6387
+ clearContinuation: false,
6388
+ clearApprovals: false,
6389
+ setIdle: false,
6390
+ shouldRebuildCanonical: historySelection.shouldRebuildCanonical,
6391
+ });
6719
6392
  await store.update(freshConv);
6720
6393
 
6721
6394
  if (result.response) {
@@ -6780,15 +6453,11 @@ export const startDevServer = async (
6780
6453
  const freshConv = await store.get(cronConvId);
6781
6454
  if (freshConv) {
6782
6455
  freshConv.messages = buildCronMessages(config.task, [], result);
6783
- if (result.harnessMessages) {
6784
- freshConv._harnessMessages = result.harnessMessages;
6785
- }
6786
- if (result.toolResultArchive) {
6787
- freshConv._toolResultArchive = result.toolResultArchive;
6788
- }
6789
- if (result.contextTokens > 0) freshConv.contextTokens = result.contextTokens;
6790
- if (result.contextWindow > 0) freshConv.contextWindow = result.contextWindow;
6791
- freshConv.updatedAt = Date.now();
6456
+ applyTurnMetadata(freshConv, result, {
6457
+ clearContinuation: false,
6458
+ clearApprovals: false,
6459
+ setIdle: false,
6460
+ });
6792
6461
  await store.update(freshConv);
6793
6462
  }
6794
6463
  const elapsed = ((Date.now() - start) / 1000).toFixed(1);