@poncho-ai/cli 0.33.1 → 0.33.2

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
 
@@ -4471,9 +4494,7 @@ export const createRequestHandler = async (options?: {
4471
4494
  writeJson(response, 202, { ok: true });
4472
4495
  const work = (async () => {
4473
4496
  try {
4474
- for await (const _event of runContinuation(conversationId)) {
4475
- // Events are already broadcast inside runContinuation
4476
- }
4497
+ await runContinuation(conversationId);
4477
4498
  // Chain: if another continuation is needed, fire next self-fetch
4478
4499
  const conv = await conversationStore.get(conversationId);
4479
4500
  if (conv?._continuationMessages?.length) {
@@ -5202,7 +5223,7 @@ export const createRequestHandler = async (options?: {
5202
5223
  writeJson(response, 200, {
5203
5224
  conversation: {
5204
5225
  ...conversation,
5205
- messages: conversation.messages.map(normalizeMessageForClient),
5226
+ messages: conversation.messages.map(normalizeMessageForClient).filter((m): m is Message => m !== null),
5206
5227
  pendingApprovals: storedPending,
5207
5228
  _continuationMessages: undefined,
5208
5229
  _harnessMessages: undefined,
@@ -5406,7 +5427,7 @@ export const createRequestHandler = async (options?: {
5406
5427
 
5407
5428
  let eventCount = 0;
5408
5429
  try {
5409
- for await (const event of runContinuation(conversationId)) {
5430
+ await runContinuation(conversationId, async (event) => {
5410
5431
  eventCount++;
5411
5432
  let sseEvent: AgentEvent = event;
5412
5433
  if (sseEvent.type === "run:completed") {
@@ -5420,7 +5441,7 @@ export const createRequestHandler = async (options?: {
5420
5441
  // Client disconnected — continue processing so the run completes
5421
5442
  }
5422
5443
  emitBrowserStatusIfActive(conversationId, event, response);
5423
- }
5444
+ });
5424
5445
  } catch (err) {
5425
5446
  const errorEvent: AgentEvent = {
5426
5447
  type: "run:error",
@@ -5546,18 +5567,6 @@ export const createRequestHandler = async (options?: {
5546
5567
  `[poncho] conversation="${conversationId}" history_source=${canonicalHistory.source}`,
5547
5568
  );
5548
5569
  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
5570
  let userContent: Message["content"] | undefined = messageText;
5562
5571
  if (files.length > 0) {
5563
5572
  try {
@@ -5593,14 +5602,51 @@ export const createRequestHandler = async (options?: {
5593
5602
  return;
5594
5603
  }
5595
5604
  }
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
5605
  const unsubSubagentEvents = onConversationEvent(conversationId, (evt) => {
5599
5606
  if (evt.type.startsWith("subagent:")) {
5600
5607
  try { response.write(formatSseEvent(evt)); } catch {}
5601
5608
  }
5602
5609
  });
5603
5610
 
5611
+ const draft = createTurnDraftState();
5612
+ let checkpointedRun = false;
5613
+ let runCancelled = false;
5614
+ let runContinuationMessages: Message[] | undefined;
5615
+
5616
+ const buildMessages = (): Message[] => {
5617
+ const draftSections = cloneSections(draft.sections);
5618
+ if (draft.currentTools.length > 0) {
5619
+ draftSections.push({ type: "tools", content: [...draft.currentTools] });
5620
+ }
5621
+ if (draft.currentText.length > 0) {
5622
+ draftSections.push({ type: "text", content: draft.currentText });
5623
+ }
5624
+ const userTurn: Message[] = userContent != null
5625
+ ? [{ role: "user" as const, content: userContent }]
5626
+ : [];
5627
+ const hasDraftContent =
5628
+ draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draftSections.length > 0;
5629
+ if (!hasDraftContent) {
5630
+ return [...historyMessages, ...userTurn];
5631
+ }
5632
+ return [
5633
+ ...historyMessages,
5634
+ ...userTurn,
5635
+ {
5636
+ role: "assistant" as const,
5637
+ content: draft.assistantResponse,
5638
+ metadata: buildAssistantMetadata(draft, draftSections),
5639
+ },
5640
+ ];
5641
+ };
5642
+
5643
+ const persistDraftAssistantTurn = async (): Promise<void> => {
5644
+ if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
5645
+ conversation.messages = buildMessages();
5646
+ conversation.updatedAt = Date.now();
5647
+ await conversationStore.update(conversation);
5648
+ };
5649
+
5604
5650
  try {
5605
5651
  {
5606
5652
  conversation.messages = [...historyMessages, { role: "user", content: userContent! }];
@@ -5612,43 +5658,6 @@ export const createRequestHandler = async (options?: {
5612
5658
  });
5613
5659
  }
5614
5660
 
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
5661
  let cachedRecallCorpus: unknown[] | undefined;
5653
5662
  const lazyRecallCorpus = async () => {
5654
5663
  if (cachedRecallCorpus) return cachedRecallCorpus;
@@ -5682,281 +5691,168 @@ export const createRequestHandler = async (options?: {
5682
5691
  return cachedRecallCorpus;
5683
5692
  };
5684
5693
 
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);
5694
+ const execution = await executeConversationTurn({
5695
+ harness,
5696
+ runInput: {
5697
+ task: messageText,
5698
+ conversationId,
5699
+ parameters: withToolResultArchiveParam({
5700
+ ...(bodyParameters ?? {}),
5701
+ __conversationRecallCorpus: lazyRecallCorpus,
5702
+ __activeConversationId: conversationId,
5703
+ __ownerId: ownerId,
5704
+ }, conversation),
5705
+ messages: harnessMessages,
5706
+ files: files.length > 0 ? files : undefined,
5707
+ abortSignal: abortController.signal,
5708
+ },
5709
+ initialContextTokens: conversation.contextTokens ?? 0,
5710
+ initialContextWindow: conversation.contextWindow ?? 0,
5711
+ onEvent: async (event, eventDraft) => {
5712
+ draft.assistantResponse = eventDraft.assistantResponse;
5713
+ draft.toolTimeline = eventDraft.toolTimeline;
5714
+ draft.sections = eventDraft.sections;
5715
+ draft.currentTools = eventDraft.currentTools;
5716
+ draft.currentText = eventDraft.currentText;
5746
5717
 
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);
5718
+ if (event.type === "run:started") {
5719
+ latestRunId = event.runId;
5720
+ runOwners.set(event.runId, ownerId);
5721
+ runConversations.set(event.runId, conversationId);
5722
+ const active = activeConversationRuns.get(conversationId);
5723
+ if (active && active.abortController === abortController) {
5724
+ active.runId = event.runId;
5725
+ }
5782
5726
  }
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] });
5727
+ if (event.type === "run:cancelled") {
5728
+ runCancelled = true;
5789
5729
  }
5790
- if (currentText.length > 0) {
5791
- checkpointSections.push({ type: "text", content: currentText });
5730
+ if (event.type === "compaction:completed") {
5731
+ if (event.compactedMessages) {
5732
+ historyMessages.length = 0;
5733
+ historyMessages.push(...event.compactedMessages);
5734
+
5735
+ const preservedFromHistory = historyMessages.length - 1;
5736
+ const removedCount = preRunMessages.length - Math.max(0, preservedFromHistory);
5737
+ const existingHistory = conversation.compactedHistory ?? [];
5738
+ conversation.compactedHistory = [
5739
+ ...existingHistory,
5740
+ ...preRunMessages.slice(0, removedCount),
5741
+ ];
5742
+ }
5792
5743
  }
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;
5744
+ if (event.type === "step:completed") {
5745
+ await persistDraftAssistantTurn();
5821
5746
  }
5822
- runContextTokens = event.result.contextTokens ?? runContextTokens;
5823
- runContextWindow = event.result.contextWindow ?? runContextWindow;
5824
- if (event.result.continuationMessages) {
5825
- runHarnessMessages = event.result.continuationMessages;
5747
+ if (event.type === "tool:approval:required") {
5748
+ const toolText = `- approval required \`${event.tool}\``;
5749
+ draft.toolTimeline.push(toolText);
5750
+ draft.currentTools.push(toolText);
5751
+ const existingApprovals = Array.isArray(conversation.pendingApprovals)
5752
+ ? conversation.pendingApprovals
5753
+ : [];
5754
+ if (!existingApprovals.some((approval) => approval.approvalId === event.approvalId)) {
5755
+ conversation.pendingApprovals = [
5756
+ ...existingApprovals,
5757
+ {
5758
+ approvalId: event.approvalId,
5759
+ runId: latestRunId || conversation.runtimeRunId || "",
5760
+ tool: event.tool,
5761
+ toolCallId: undefined,
5762
+ input: (event.input ?? {}) as Record<string, unknown>,
5763
+ checkpointMessages: undefined,
5764
+ baseMessageCount: historyMessages.length,
5765
+ pendingToolCalls: [],
5766
+ },
5767
+ ];
5768
+ conversation.updatedAt = Date.now();
5769
+ await conversationStore.update(conversation);
5770
+ }
5771
+ await persistDraftAssistantTurn();
5826
5772
  }
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;
5773
+ if (event.type === "tool:approval:checkpoint") {
5774
+ conversation.messages = buildMessages();
5775
+ conversation.pendingApprovals = buildApprovalCheckpoints({
5776
+ approvals: event.approvals,
5777
+ runId: latestRunId,
5778
+ checkpointMessages: event.checkpointMessages,
5779
+ baseMessageCount: historyMessages.length,
5780
+ pendingToolCalls: event.pendingToolCalls,
5781
+ });
5846
5782
  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
5783
  conversation.updatedAt = Date.now();
5854
5784
  await conversationStore.update(conversation);
5785
+ checkpointedRun = true;
5786
+ }
5787
+ if (event.type === "run:completed") {
5788
+ if (event.result.continuation && event.result.continuationMessages) {
5789
+ runContinuationMessages = event.result.continuationMessages;
5790
+
5791
+ conversation.messages = buildMessages();
5792
+ conversation._continuationMessages = runContinuationMessages;
5793
+ conversation._harnessMessages = runContinuationMessages;
5794
+ conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5795
+ conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
5796
+ if (!checkpointedRun) {
5797
+ conversation.pendingApprovals = [];
5798
+ }
5799
+ if ((event.result.contextTokens ?? 0) > 0) conversation.contextTokens = event.result.contextTokens!;
5800
+ if ((event.result.contextWindow ?? 0) > 0) conversation.contextWindow = event.result.contextWindow!;
5801
+ conversation.updatedAt = Date.now();
5802
+ await conversationStore.update(conversation);
5803
+
5804
+ if (!checkpointedRun) {
5805
+ doWaitUntil(
5806
+ new Promise(r => setTimeout(r, 3000)).then(() =>
5807
+ selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
5808
+ ),
5809
+ );
5810
+ }
5811
+ }
5812
+ }
5855
5813
 
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
- );
5814
+ await telemetry.emit(event);
5815
+ let sseEvent: AgentEvent = event.type === "compaction:completed" && event.compactedMessages
5816
+ ? { ...event, compactedMessages: undefined }
5817
+ : event;
5818
+ if (sseEvent.type === "run:completed") {
5819
+ const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
5820
+ const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
5821
+ if (hasPendingSubagents) {
5822
+ sseEvent = { ...stripped, pendingSubagents: true };
5823
+ } else {
5824
+ sseEvent = stripped;
5864
5825
  }
5865
5826
  }
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;
5827
+ broadcastEvent(conversationId, sseEvent);
5828
+ try {
5829
+ response.write(formatSseEvent(sseEvent));
5830
+ } catch {
5831
+ // Client disconnected — continue processing so the run completes.
5878
5832
  }
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
- }
5833
+ emitBrowserStatusIfActive(conversationId, event, response);
5834
+ },
5835
+ });
5836
+
5837
+ flushTurnDraft(draft);
5838
+ latestRunId = execution.latestRunId || latestRunId;
5839
+
5896
5840
  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();
5841
+ conversation.messages = buildMessages();
5842
+ applyTurnMetadata(conversation, {
5843
+ latestRunId,
5844
+ contextTokens: execution.runContextTokens,
5845
+ contextWindow: execution.runContextWindow,
5846
+ harnessMessages: execution.runHarnessMessages,
5847
+ toolResultArchive: harness.getToolResultArchive(conversationId),
5848
+ }, { shouldRebuildCanonical });
5933
5849
  await conversationStore.update(conversation);
5934
5850
  }
5935
5851
  } catch (error) {
5852
+ flushTurnDraft(draft);
5936
5853
  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
- ];
5854
+ if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
5855
+ conversation.messages = buildMessages();
5960
5856
  conversation.updatedAt = Date.now();
5961
5857
  await conversationStore.update(conversation);
5962
5858
  }
@@ -5977,29 +5873,8 @@ export const createRequestHandler = async (options?: {
5977
5873
  }),
5978
5874
  );
5979
5875
  } 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
- ];
5876
+ if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
5877
+ conversation.messages = buildMessages();
6003
5878
  conversation.updatedAt = Date.now();
6004
5879
  await conversationStore.update(conversation);
6005
5880
  }
@@ -6015,10 +5890,6 @@ export const createRequestHandler = async (options?: {
6015
5890
  runConversations.delete(latestRunId);
6016
5891
  }
6017
5892
 
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
5893
  const hadDeferred = pendingCallbackNeeded.delete(conversationId);
6023
5894
  const freshConv = await conversationStore.get(conversationId);
6024
5895
  const needsCallback = hadDeferred || !!freshConv?.pendingSubagentResults?.length;
@@ -6106,53 +5977,37 @@ export const createRequestHandler = async (options?: {
6106
5977
  });
6107
5978
  const historyMessages = [...historySelection.messages];
6108
5979
  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;
5980
+ const result = await runCronAgent(harness, task, conv.conversationId, historyMessages,
5981
+ conv._toolResultArchive,
5982
+ async (event) => { await telemetry.emit(event); },
5983
+ );
5984
+
5985
+ const freshConv = await conversationStore.get(conv.conversationId);
5986
+ if (freshConv) {
5987
+ appendCronTurn(freshConv, task, result);
5988
+ applyTurnMetadata(freshConv, result, {
5989
+ clearContinuation: false,
5990
+ clearApprovals: false,
5991
+ setIdle: false,
5992
+ shouldRebuildCanonical: historySelection.shouldRebuildCanonical,
5993
+ });
5994
+ await conversationStore.update(freshConv);
6135
5995
  }
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
5996
 
6142
- if (assistantResponse) {
5997
+ if (result.response) {
6143
5998
  try {
6144
5999
  await adapter.sendReply(
6145
6000
  {
6146
6001
  channelId: chatId,
6147
- platformThreadId: conv.channelMeta?.platformThreadId ?? chatId,
6002
+ platformThreadId: (freshConv ?? conv).channelMeta?.platformThreadId ?? chatId,
6148
6003
  },
6149
- assistantResponse,
6004
+ result.response,
6150
6005
  );
6151
6006
  } catch (sendError) {
6152
6007
  console.error(`[cron] ${jobName}: send to ${chatId} failed:`, sendError instanceof Error ? sendError.message : sendError);
6153
6008
  }
6154
6009
  }
6155
- chatResults.push({ chatId, status: "completed", steps: execution.runSteps });
6010
+ chatResults.push({ chatId, status: "completed", steps: result.steps });
6156
6011
  } catch (runError) {
6157
6012
  chatResults.push({ chatId, status: "error" });
6158
6013
  console.error(`[cron] ${jobName}: run for chat ${chatId} failed:`, runError instanceof Error ? runError.message : runError);
@@ -6180,7 +6035,6 @@ export const createRequestHandler = async (options?: {
6180
6035
  cronOwnerId,
6181
6036
  `[cron] ${jobName} ${timestamp}`,
6182
6037
  );
6183
- const historyMessages: Message[] = [];
6184
6038
 
6185
6039
  const convId = conversation.conversationId;
6186
6040
  activeConversationRuns.set(convId, {
@@ -6190,152 +6044,45 @@ export const createRequestHandler = async (options?: {
6190
6044
  });
6191
6045
 
6192
6046
  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
- }
6047
+ const result = await runCronAgent(harness, cronJob.task, convId, [],
6048
+ conversation._toolResultArchive,
6049
+ async (event) => {
6050
+ broadcastEvent(convId, event);
6051
+ await telemetry.emit(event);
6052
+ },
6053
+ );
6054
+ finishConversationStream(convId);
6055
+
6056
+ const freshConv = await conversationStore.get(convId);
6057
+ if (freshConv) {
6058
+ freshConv.messages = buildCronMessages(cronJob.task, [], result);
6059
+ applyTurnMetadata(freshConv, result, {
6060
+ clearApprovals: false,
6061
+ setIdle: false,
6062
+ });
6063
+ await conversationStore.update(freshConv);
6266
6064
  }
6267
- broadcastEvent(convId, event);
6268
- await telemetry.emit(event);
6269
- }
6270
6065
 
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;
6066
+ if (result.continuation) {
6067
+ const work = selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(convId)}`).catch(err =>
6068
+ console.error(`[poncho][cron] Continuation self-fetch failed:`, err instanceof Error ? err.message : err),
6069
+ );
6070
+ doWaitUntil(work);
6071
+ writeJson(response, 200, {
6072
+ conversationId: convId,
6073
+ status: "continued",
6074
+ duration: Date.now() - start,
6075
+ });
6076
+ return;
6310
6077
  }
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
6078
 
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
6079
  writeJson(response, 200, {
6325
6080
  conversationId: convId,
6326
- status: "continued",
6081
+ status: "completed",
6082
+ response: result.response.slice(0, 500),
6327
6083
  duration: Date.now() - start,
6084
+ steps: result.steps,
6328
6085
  });
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
6086
  } finally {
6340
6087
  activeConversationRuns.delete(convId);
6341
6088
  const hadDeferred = pendingCallbackNeeded.delete(convId);
@@ -6417,26 +6164,19 @@ export const createRequestHandler = async (options?: {
6417
6164
  if (channelMeta) {
6418
6165
  const adapter = messagingAdapters.get(channelMeta.platform);
6419
6166
  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) {
6167
+ const result = await runCronAgent(
6168
+ harness, framedMessage, originConv.conversationId,
6169
+ originConv.messages ?? [],
6170
+ originConv._toolResultArchive,
6171
+ );
6172
+ if (result.response) {
6433
6173
  try {
6434
6174
  await adapter.sendReply(
6435
6175
  {
6436
6176
  channelId: channelMeta.channelId,
6437
6177
  platformThreadId: channelMeta.platformThreadId ?? channelMeta.channelId,
6438
6178
  },
6439
- assistantResponse,
6179
+ result.response,
6440
6180
  );
6441
6181
  } catch (sendError) {
6442
6182
  console.error(`[reminder] Send to ${channelMeta.platform} failed:`, sendError instanceof Error ? sendError.message : sendError);
@@ -6444,12 +6184,12 @@ export const createRequestHandler = async (options?: {
6444
6184
  }
6445
6185
  const freshConv = await conversationStore.get(originConv.conversationId);
6446
6186
  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();
6187
+ appendCronTurn(freshConv, framedMessage, result);
6188
+ applyTurnMetadata(freshConv, result, {
6189
+ clearContinuation: false,
6190
+ clearApprovals: false,
6191
+ setIdle: false,
6192
+ });
6453
6193
  await conversationStore.update(freshConv);
6454
6194
  }
6455
6195
  }
@@ -6460,24 +6200,15 @@ export const createRequestHandler = async (options?: {
6460
6200
  `[reminder] ${reminder.task.slice(0, 80)} ${timestamp}`,
6461
6201
  );
6462
6202
  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
- }
6203
+ const result = await runCronAgent(harness, framedMessage, convId, []);
6474
6204
  const freshConv = await conversationStore.get(convId);
6475
6205
  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();
6206
+ freshConv.messages = buildCronMessages(framedMessage, [], result);
6207
+ applyTurnMetadata(freshConv, result, {
6208
+ clearContinuation: false,
6209
+ clearApprovals: false,
6210
+ setIdle: false,
6211
+ });
6481
6212
  await conversationStore.update(freshConv);
6482
6213
  }
6483
6214
  }
@@ -6568,65 +6299,6 @@ export const startDevServer = async (
6568
6299
  type CronJob = InstanceType<typeof Cron>;
6569
6300
  let activeJobs: CronJob[] = [];
6570
6301
 
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
6302
  const scheduleCronJobs = (jobs: Record<string, CronJobConfig>): void => {
6631
6303
  for (const job of activeJobs) {
6632
6304
  job.stop();
@@ -6704,18 +6376,13 @@ export const startDevServer = async (
6704
6376
 
6705
6377
  const freshConv = await store.get(convId);
6706
6378
  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();
6379
+ appendCronTurn(freshConv, task, result);
6380
+ applyTurnMetadata(freshConv, result, {
6381
+ clearContinuation: false,
6382
+ clearApprovals: false,
6383
+ setIdle: false,
6384
+ shouldRebuildCanonical: historySelection.shouldRebuildCanonical,
6385
+ });
6719
6386
  await store.update(freshConv);
6720
6387
 
6721
6388
  if (result.response) {
@@ -6780,15 +6447,11 @@ export const startDevServer = async (
6780
6447
  const freshConv = await store.get(cronConvId);
6781
6448
  if (freshConv) {
6782
6449
  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();
6450
+ applyTurnMetadata(freshConv, result, {
6451
+ clearContinuation: false,
6452
+ clearApprovals: false,
6453
+ setIdle: false,
6454
+ });
6792
6455
  await store.update(freshConv);
6793
6456
  }
6794
6457
  const elapsed = ((Date.now() - start) / 1000).toFixed(1);