@poncho-ai/cli 0.32.4 → 0.32.5

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
@@ -119,6 +119,17 @@ const readRequestBody = async (request: IncomingMessage): Promise<unknown> => {
119
119
  return body.length > 0 ? (JSON.parse(body) as unknown) : {};
120
120
  };
121
121
 
122
+ const parseTelegramMessageThreadIdFromPlatformThreadId = (
123
+ platformThreadId: string | undefined,
124
+ chatId: string | undefined,
125
+ ): number | undefined => {
126
+ if (!platformThreadId || !chatId) return undefined;
127
+ const parts = platformThreadId.split(":");
128
+ if (parts.length !== 3 || parts[0] !== chatId) return undefined;
129
+ const threadId = Number(parts[1]);
130
+ return Number.isInteger(threadId) ? threadId : undefined;
131
+ };
132
+
122
133
  const MAX_UPLOAD_SIZE = 25 * 1024 * 1024; // 25MB per file
123
134
 
124
135
  interface ParsedMultipart {
@@ -312,6 +323,305 @@ const normalizeMessageForClient = (message: Message): Message => {
312
323
  return message;
313
324
  };
314
325
 
326
+ const isMessageArray = (value: unknown): value is Message[] =>
327
+ Array.isArray(value) &&
328
+ value.every((entry) => {
329
+ if (!entry || typeof entry !== "object") return false;
330
+ const row = entry as Record<string, unknown>;
331
+ const role = row.role;
332
+ const content = row.content;
333
+ const roleOk = role === "system" || role === "user" || role === "assistant" || role === "tool";
334
+ const contentOk = typeof content === "string" || Array.isArray(content);
335
+ return roleOk && contentOk;
336
+ });
337
+
338
+ type HistorySource = "harness" | "continuation" | "messages";
339
+
340
+ const loadCanonicalHistory = (
341
+ conversation: Conversation,
342
+ ): { messages: Message[]; source: HistorySource } => {
343
+ if (isMessageArray(conversation._harnessMessages) && conversation._harnessMessages.length > 0) {
344
+ return { messages: [...conversation._harnessMessages], source: "harness" };
345
+ }
346
+ return { messages: [...conversation.messages], source: "messages" };
347
+ };
348
+
349
+ const loadRunHistory = (
350
+ conversation: Conversation,
351
+ options?: { preferContinuation?: boolean },
352
+ ): { messages: Message[]; source: HistorySource; shouldRebuildCanonical: boolean } => {
353
+ if (options?.preferContinuation && isMessageArray(conversation._continuationMessages) && conversation._continuationMessages.length > 0) {
354
+ return {
355
+ messages: [...conversation._continuationMessages],
356
+ source: "continuation",
357
+ shouldRebuildCanonical: !isMessageArray(conversation._harnessMessages) || conversation._harnessMessages.length === 0,
358
+ };
359
+ }
360
+ const canonical = loadCanonicalHistory(conversation);
361
+ return {
362
+ ...canonical,
363
+ shouldRebuildCanonical: canonical.source !== "harness",
364
+ };
365
+ };
366
+
367
+ type StoredApproval = NonNullable<Conversation["pendingApprovals"]>[number];
368
+ type PendingToolCall = { id: string; name: string; input: Record<string, unknown> };
369
+ type ApprovalEventItem = {
370
+ approvalId: string;
371
+ tool: string;
372
+ toolCallId?: string;
373
+ input: Record<string, unknown>;
374
+ };
375
+ type RunRequest = {
376
+ conversationId: string;
377
+ messages: Message[];
378
+ preferContinuation?: boolean;
379
+ };
380
+ type RunOutcome = {
381
+ source: HistorySource;
382
+ shouldRebuildCanonical: boolean;
383
+ messages: Message[];
384
+ };
385
+ type TurnSection = { type: "text" | "tools"; content: string | string[] };
386
+ type TurnDraftState = {
387
+ assistantResponse: string;
388
+ toolTimeline: string[];
389
+ sections: TurnSection[];
390
+ currentTools: string[];
391
+ currentText: string;
392
+ };
393
+ type ExecuteTurnResult = {
394
+ latestRunId: string;
395
+ runCancelled: boolean;
396
+ runContinuation: boolean;
397
+ runContinuationMessages?: Message[];
398
+ runHarnessMessages?: Message[];
399
+ runContextTokens: number;
400
+ runContextWindow: number;
401
+ runSteps: number;
402
+ runMaxSteps?: number;
403
+ draft: TurnDraftState;
404
+ };
405
+
406
+ const createTurnDraftState = (): TurnDraftState => ({
407
+ assistantResponse: "",
408
+ toolTimeline: [],
409
+ sections: [],
410
+ currentTools: [],
411
+ currentText: "",
412
+ });
413
+
414
+ const cloneSections = (sections: TurnSection[]): TurnSection[] =>
415
+ sections.map((section) => ({
416
+ type: section.type,
417
+ content: Array.isArray(section.content) ? [...section.content] : section.content,
418
+ }));
419
+
420
+ const flushTurnDraft = (draft: TurnDraftState): void => {
421
+ if (draft.currentTools.length > 0) {
422
+ draft.sections.push({ type: "tools", content: draft.currentTools });
423
+ draft.currentTools = [];
424
+ }
425
+ if (draft.currentText.length > 0) {
426
+ draft.sections.push({ type: "text", content: draft.currentText });
427
+ draft.currentText = "";
428
+ }
429
+ };
430
+
431
+ const recordStandardTurnEvent = (draft: TurnDraftState, event: AgentEvent): void => {
432
+ if (event.type === "model:chunk") {
433
+ if (draft.currentTools.length > 0) {
434
+ draft.sections.push({ type: "tools", content: draft.currentTools });
435
+ draft.currentTools = [];
436
+ if (draft.assistantResponse.length > 0 && !/\s$/.test(draft.assistantResponse)) {
437
+ draft.assistantResponse += " ";
438
+ }
439
+ }
440
+ draft.assistantResponse += event.content;
441
+ draft.currentText += event.content;
442
+ return;
443
+ }
444
+ if (event.type === "tool:started") {
445
+ if (draft.currentText.length > 0) {
446
+ draft.sections.push({ type: "text", content: draft.currentText });
447
+ draft.currentText = "";
448
+ }
449
+ const toolText = `- start \`${event.tool}\``;
450
+ draft.toolTimeline.push(toolText);
451
+ draft.currentTools.push(toolText);
452
+ return;
453
+ }
454
+ if (event.type === "tool:completed") {
455
+ const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
456
+ draft.toolTimeline.push(toolText);
457
+ draft.currentTools.push(toolText);
458
+ return;
459
+ }
460
+ if (event.type === "tool:error") {
461
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
462
+ draft.toolTimeline.push(toolText);
463
+ draft.currentTools.push(toolText);
464
+ }
465
+ };
466
+
467
+ const buildAssistantMetadata = (
468
+ draft: TurnDraftState,
469
+ sectionsOverride?: TurnSection[],
470
+ ): Message["metadata"] | undefined => {
471
+ const sections = sectionsOverride ?? cloneSections(draft.sections);
472
+ if (draft.toolTimeline.length === 0 && sections.length === 0) return undefined;
473
+ return {
474
+ toolActivity: [...draft.toolTimeline],
475
+ sections: sections.length > 0 ? sections : undefined,
476
+ } as Message["metadata"];
477
+ };
478
+
479
+ const executeConversationTurn = async ({
480
+ harness,
481
+ runInput,
482
+ initialContextTokens = 0,
483
+ initialContextWindow = 0,
484
+ onEvent,
485
+ }: {
486
+ harness: AgentHarness;
487
+ runInput: Parameters<AgentHarness["runWithTelemetry"]>[0];
488
+ initialContextTokens?: number;
489
+ initialContextWindow?: number;
490
+ onEvent?: (event: AgentEvent, draft: TurnDraftState) => void | Promise<void>;
491
+ }): Promise<ExecuteTurnResult> => {
492
+ const draft = createTurnDraftState();
493
+ let latestRunId = "";
494
+ let runCancelled = false;
495
+ let runContinuation = false;
496
+ let runContinuationMessages: Message[] | undefined;
497
+ let runHarnessMessages: Message[] | undefined;
498
+ let runContextTokens = initialContextTokens;
499
+ let runContextWindow = initialContextWindow;
500
+ let runSteps = 0;
501
+ let runMaxSteps: number | undefined;
502
+
503
+ for await (const event of harness.runWithTelemetry(runInput)) {
504
+ recordStandardTurnEvent(draft, event);
505
+ if (event.type === "run:started") {
506
+ latestRunId = event.runId;
507
+ }
508
+ if (event.type === "run:cancelled") {
509
+ runCancelled = true;
510
+ }
511
+ if (event.type === "run:completed") {
512
+ runContinuation = event.result.continuation === true;
513
+ runContinuationMessages = event.result.continuationMessages;
514
+ runHarnessMessages = event.result.continuationMessages;
515
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
516
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
517
+ runSteps = event.result.steps;
518
+ if (typeof event.result.maxSteps === "number") {
519
+ runMaxSteps = event.result.maxSteps;
520
+ }
521
+ if (draft.assistantResponse.length === 0 && event.result.response) {
522
+ draft.assistantResponse = event.result.response;
523
+ }
524
+ }
525
+ if (event.type === "run:error") {
526
+ draft.assistantResponse = draft.assistantResponse || `[Error: ${event.error.message}]`;
527
+ }
528
+ if (onEvent) {
529
+ await onEvent(event, draft);
530
+ }
531
+ }
532
+
533
+ return {
534
+ latestRunId,
535
+ runCancelled,
536
+ runContinuation,
537
+ runContinuationMessages,
538
+ runHarnessMessages,
539
+ runContextTokens,
540
+ runContextWindow,
541
+ runSteps,
542
+ runMaxSteps,
543
+ draft,
544
+ };
545
+ };
546
+
547
+ const normalizePendingToolCalls = (value: unknown): PendingToolCall[] => {
548
+ if (!Array.isArray(value)) return [];
549
+ return value
550
+ .filter((entry): entry is PendingToolCall => {
551
+ if (!entry || typeof entry !== "object") return false;
552
+ const row = entry as Record<string, unknown>;
553
+ return (
554
+ typeof row.id === "string" &&
555
+ typeof row.name === "string" &&
556
+ typeof row.input === "object" &&
557
+ row.input !== null
558
+ );
559
+ })
560
+ .map((entry) => ({ id: entry.id, name: entry.name, input: entry.input }));
561
+ };
562
+
563
+ const normalizeApprovalCheckpoint = (
564
+ approval: StoredApproval,
565
+ fallbackMessages: Message[],
566
+ ): StoredApproval => ({
567
+ ...approval,
568
+ checkpointMessages: isMessageArray(approval.checkpointMessages) ? approval.checkpointMessages : [...fallbackMessages],
569
+ baseMessageCount: typeof approval.baseMessageCount === "number" && approval.baseMessageCount >= 0
570
+ ? approval.baseMessageCount
571
+ : 0,
572
+ pendingToolCalls: normalizePendingToolCalls(approval.pendingToolCalls),
573
+ });
574
+
575
+ const buildApprovalCheckpoints = ({
576
+ approvals,
577
+ runId,
578
+ checkpointMessages,
579
+ baseMessageCount,
580
+ pendingToolCalls,
581
+ }: {
582
+ approvals: ApprovalEventItem[];
583
+ runId: string;
584
+ checkpointMessages: Message[];
585
+ baseMessageCount: number;
586
+ pendingToolCalls: PendingToolCall[];
587
+ }): NonNullable<Conversation["pendingApprovals"]> =>
588
+ approvals.map((approval) => ({
589
+ approvalId: approval.approvalId,
590
+ runId,
591
+ tool: approval.tool,
592
+ toolCallId: approval.toolCallId,
593
+ input: approval.input,
594
+ checkpointMessages,
595
+ baseMessageCount,
596
+ pendingToolCalls,
597
+ }));
598
+
599
+ const resolveRunRequest = (
600
+ conversation: Conversation,
601
+ request: RunRequest,
602
+ ): RunOutcome => {
603
+ const resolved = loadRunHistory(conversation, {
604
+ preferContinuation: request.preferContinuation,
605
+ });
606
+ return {
607
+ source: resolved.source,
608
+ shouldRebuildCanonical: resolved.shouldRebuildCanonical,
609
+ messages: resolved.messages.length > 0 ? resolved.messages : request.messages,
610
+ };
611
+ };
612
+
613
+ export const __internalRunOrchestration = {
614
+ isMessageArray,
615
+ loadCanonicalHistory,
616
+ loadRunHistory,
617
+ normalizeApprovalCheckpoint,
618
+ buildApprovalCheckpoints,
619
+ resolveRunRequest,
620
+ createTurnDraftState,
621
+ recordStandardTurnEvent,
622
+ executeConversationTurn,
623
+ };
624
+
315
625
  const AGENT_TEMPLATE = (
316
626
  name: string,
317
627
  id: string,
@@ -1641,6 +1951,77 @@ export const createRequestHandler = async (options?: {
1641
1951
  // separate copy, causing last-writer-wins when decisions overlap).
1642
1952
  const approvalDecisionTracker = new Map<string, Map<string, boolean>>();
1643
1953
 
1954
+ const findPendingApproval = async (
1955
+ approvalId: string,
1956
+ owner: string,
1957
+ ): Promise<{ conversation: Conversation; approval: StoredApproval } | undefined> => {
1958
+ const searchedConversationIds = new Set<string>();
1959
+ const scan = async (conversations: Conversation[]) => {
1960
+ for (const conv of conversations) {
1961
+ if (searchedConversationIds.has(conv.conversationId)) continue;
1962
+ searchedConversationIds.add(conv.conversationId);
1963
+ if (!Array.isArray(conv.pendingApprovals)) continue;
1964
+ const match = conv.pendingApprovals.find((approval) => approval.approvalId === approvalId);
1965
+ if (match) {
1966
+ return { conversation: conv, approval: match as StoredApproval };
1967
+ }
1968
+ }
1969
+ return undefined;
1970
+ };
1971
+
1972
+ const ownerScoped = await scan(await conversationStore.list(owner));
1973
+ if (ownerScoped) return ownerScoped;
1974
+
1975
+ // In local-owner mode (default dev), historical conversations may exist
1976
+ // with mixed ownership keys; do one global fallback scan.
1977
+ if (owner === "local-owner") {
1978
+ return await scan(await conversationStore.list());
1979
+ }
1980
+ return undefined;
1981
+ };
1982
+
1983
+ const hasRunningSubagentsForParent = async (
1984
+ parentConversationId: string,
1985
+ owner: string,
1986
+ ): Promise<boolean> => {
1987
+ let hasRunning = Array.from(activeSubagentRuns.values()).some(
1988
+ (run) => run.parentConversationId === parentConversationId,
1989
+ );
1990
+ if (hasRunning) return true;
1991
+
1992
+ const summaries = await conversationStore.listSummaries(owner);
1993
+ for (const summary of summaries) {
1994
+ if (summary.parentConversationId !== parentConversationId) continue;
1995
+ const childConversation = await conversationStore.get(summary.conversationId);
1996
+ if (childConversation?.subagentMeta?.status === "running") {
1997
+ hasRunning = true;
1998
+ break;
1999
+ }
2000
+ }
2001
+ return hasRunning;
2002
+ };
2003
+
2004
+ const hasPendingSubagentWorkForParent = async (
2005
+ parentConversationId: string,
2006
+ owner: string,
2007
+ ): Promise<boolean> => {
2008
+ if (await hasRunningSubagentsForParent(parentConversationId, owner)) {
2009
+ return true;
2010
+ }
2011
+ if (pendingCallbackNeeded.has(parentConversationId)) {
2012
+ return true;
2013
+ }
2014
+ const parentConversation = await conversationStore.get(parentConversationId);
2015
+ if (!parentConversation) return false;
2016
+ if (Array.isArray(parentConversation.pendingSubagentResults) && parentConversation.pendingSubagentResults.length > 0) {
2017
+ return true;
2018
+ }
2019
+ if (typeof parentConversation.runningCallbackSince === "number" && parentConversation.runningCallbackSince > 0) {
2020
+ return true;
2021
+ }
2022
+ return false;
2023
+ };
2024
+
1644
2025
  const getSubagentDepth = async (conversationId: string): Promise<number> => {
1645
2026
  let depth = 0;
1646
2027
  let current = await conversationStore.get(conversationId);
@@ -1692,7 +2073,9 @@ export const createRequestHandler = async (options?: {
1692
2073
  const conv = await conversationStore.get(subagentId);
1693
2074
  if (!conv || !conv.parentConversationId) return;
1694
2075
 
1695
- const allApprovals = conv.pendingApprovals ?? [];
2076
+ const allApprovals = (conv.pendingApprovals ?? []).map((approval) =>
2077
+ normalizeApprovalCheckpoint(approval, conv.messages),
2078
+ );
1696
2079
  if (allApprovals.length === 0) return;
1697
2080
  const allDecided = allApprovals.every(a => a.decision != null);
1698
2081
  if (!allDecided) return;
@@ -1784,9 +2167,11 @@ export const createRequestHandler = async (options?: {
1784
2167
  conversation.lastActivityAt = Date.now();
1785
2168
  await conversationStore.update(conversation);
1786
2169
 
1787
- const harnessMessages = conversation._harnessMessages?.length
1788
- ? [...conversation._harnessMessages]
1789
- : [...conversation.messages];
2170
+ const runOutcome = resolveRunRequest(conversation, {
2171
+ conversationId: childConversationId,
2172
+ messages: conversation.messages,
2173
+ });
2174
+ const harnessMessages = [...runOutcome.messages];
1790
2175
 
1791
2176
  for await (const event of childHarness.runWithTelemetry({
1792
2177
  task,
@@ -1849,16 +2234,13 @@ export const createRequestHandler = async (options?: {
1849
2234
  if (event.type === "tool:approval:checkpoint") {
1850
2235
  const cpConv = await conversationStore.get(childConversationId);
1851
2236
  if (cpConv) {
1852
- const allCpData: NonNullable<Conversation["pendingApprovals"]> = event.approvals.map(a => ({
1853
- approvalId: a.approvalId,
2237
+ const allCpData = buildApprovalCheckpoints({
2238
+ approvals: event.approvals,
1854
2239
  runId: latestRunId,
1855
- tool: a.tool,
1856
- toolCallId: a.toolCallId,
1857
- input: a.input,
1858
2240
  checkpointMessages: [...harnessMessages, ...event.checkpointMessages],
1859
2241
  baseMessageCount: 0,
1860
2242
  pendingToolCalls: event.pendingToolCalls,
1861
- }));
2243
+ });
1862
2244
  cpConv.pendingApprovals = allCpData;
1863
2245
  cpConv.updatedAt = Date.now();
1864
2246
  await conversationStore.update(cpConv);
@@ -1875,7 +2257,7 @@ export const createRequestHandler = async (options?: {
1875
2257
  }
1876
2258
  });
1877
2259
 
1878
- const checkpointRef = allCpData[0]!;
2260
+ const checkpointRef = normalizeApprovalCheckpoint(allCpData[0]!, [...harnessMessages]);
1879
2261
  const toolContext = {
1880
2262
  runId: checkpointRef.runId,
1881
2263
  agentId: identity.id,
@@ -2008,6 +2390,8 @@ export const createRequestHandler = async (options?: {
2008
2390
  }
2009
2391
  if (runResult?.continuationMessages) {
2010
2392
  conv._harnessMessages = runResult.continuationMessages;
2393
+ } else if (runOutcome.shouldRebuildCanonical) {
2394
+ conv._harnessMessages = conv.messages;
2011
2395
  }
2012
2396
  conv._toolResultArchive = childHarness.getToolResultArchive(childConversationId);
2013
2397
  conv.lastActivityAt = Date.now();
@@ -2176,6 +2560,9 @@ export const createRequestHandler = async (options?: {
2176
2560
  metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, timestamp: pr.timestamp } as Message["metadata"],
2177
2561
  });
2178
2562
  }
2563
+ // Callback-injected result messages must be visible to the next parent turn.
2564
+ // Keep canonical transcript in sync before resolving callback run history.
2565
+ conversation._harnessMessages = [...conversation.messages];
2179
2566
  conversation.updatedAt = Date.now();
2180
2567
  await conversationStore.update(conversation);
2181
2568
 
@@ -2210,120 +2597,74 @@ export const createRequestHandler = async (options?: {
2210
2597
  });
2211
2598
  }
2212
2599
 
2213
- const historyMessages = isContinuationResume && conversation._continuationMessages?.length
2214
- ? [...conversation._continuationMessages]
2215
- : conversation._harnessMessages?.length
2216
- ? [...conversation._harnessMessages]
2217
- : [...conversation.messages];
2218
- let assistantResponse = "";
2219
- let latestRunId = "";
2220
- let runContinuation = false;
2221
- let runContinuationMessages: Message[] | undefined;
2222
- let runHarnessMessages: Message[] | undefined;
2223
- let runContextTokens = conversation.contextTokens ?? 0;
2224
- let runContextWindow = conversation.contextWindow ?? 0;
2225
- const toolTimeline: string[] = [];
2226
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
2227
- let currentTools: string[] = [];
2228
- let currentText = "";
2600
+ const historySelection = resolveRunRequest(conversation, {
2601
+ conversationId,
2602
+ messages: conversation.messages,
2603
+ preferContinuation: isContinuationResume,
2604
+ });
2605
+ const historyMessages = [...historySelection.messages];
2606
+ console.info(
2607
+ `[poncho][subagent-callback] conversation="${conversationId}" history_source=${historySelection.source}`,
2608
+ );
2609
+ let execution: ExecuteTurnResult | undefined;
2229
2610
 
2230
2611
  try {
2231
- for await (const event of harness.runWithTelemetry({
2232
- task: undefined,
2233
- conversationId,
2234
- parameters: withToolResultArchiveParam({
2235
- __activeConversationId: conversationId,
2236
- __ownerId: conversation.ownerId,
2237
- }, conversation),
2238
- messages: historyMessages,
2239
- abortSignal: abortController.signal,
2240
- })) {
2241
- if (event.type === "run:started") {
2242
- latestRunId = event.runId;
2243
- const active = activeConversationRuns.get(conversationId);
2244
- if (active) active.runId = event.runId;
2245
- }
2246
- if (event.type === "model:chunk") {
2247
- if (currentTools.length > 0) {
2248
- sections.push({ type: "tools", content: currentTools });
2249
- currentTools = [];
2250
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
2251
- assistantResponse += " ";
2252
- }
2253
- }
2254
- assistantResponse += event.content;
2255
- currentText += event.content;
2256
- }
2257
- if (event.type === "tool:started") {
2258
- if (currentText.length > 0) {
2259
- sections.push({ type: "text", content: currentText });
2260
- currentText = "";
2261
- }
2262
- const toolText = `- start \`${event.tool}\``;
2263
- toolTimeline.push(toolText);
2264
- currentTools.push(toolText);
2265
- }
2266
- if (event.type === "tool:completed") {
2267
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
2268
- toolTimeline.push(toolText);
2269
- currentTools.push(toolText);
2270
- }
2271
- if (event.type === "tool:error") {
2272
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
2273
- toolTimeline.push(toolText);
2274
- currentTools.push(toolText);
2275
- }
2276
- if (event.type === "run:completed") {
2277
- if (assistantResponse.length === 0 && event.result.response) {
2278
- assistantResponse = event.result.response;
2279
- }
2280
- runContextTokens = event.result.contextTokens ?? runContextTokens;
2281
- runContextWindow = event.result.contextWindow ?? runContextWindow;
2282
- if (event.result.continuationMessages) {
2283
- runHarnessMessages = event.result.continuationMessages;
2284
- }
2285
- if (event.result.continuation) {
2286
- runContinuation = true;
2287
- if (event.result.continuationMessages) {
2288
- runContinuationMessages = event.result.continuationMessages;
2289
- }
2612
+ execution = await executeConversationTurn({
2613
+ harness,
2614
+ runInput: {
2615
+ task: undefined,
2616
+ conversationId,
2617
+ parameters: withToolResultArchiveParam({
2618
+ __activeConversationId: conversationId,
2619
+ __ownerId: conversation.ownerId,
2620
+ }, conversation),
2621
+ messages: historyMessages,
2622
+ abortSignal: abortController.signal,
2623
+ },
2624
+ initialContextTokens: conversation.contextTokens ?? 0,
2625
+ initialContextWindow: conversation.contextWindow ?? 0,
2626
+ onEvent: (event) => {
2627
+ if (event.type === "run:started") {
2628
+ const active = activeConversationRuns.get(conversationId);
2629
+ if (active) active.runId = event.runId;
2290
2630
  }
2291
- }
2292
- broadcastEvent(conversationId, event);
2293
- }
2294
-
2295
- if (currentTools.length > 0) sections.push({ type: "tools", content: currentTools });
2296
- if (currentText.length > 0) sections.push({ type: "text", content: currentText });
2631
+ broadcastEvent(conversationId, event);
2632
+ },
2633
+ });
2634
+ flushTurnDraft(execution.draft);
2297
2635
 
2298
- if (runContinuationMessages || assistantResponse.length > 0 || toolTimeline.length > 0) {
2636
+ const callbackNeedsContinuation = execution.runContinuation && execution.runContinuationMessages;
2637
+ if (callbackNeedsContinuation || execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0) {
2299
2638
  const freshConv = await conversationStore.get(conversationId);
2300
2639
  if (freshConv) {
2301
- if (runContinuationMessages) {
2302
- freshConv._continuationMessages = runContinuationMessages;
2640
+ if (callbackNeedsContinuation) {
2641
+ freshConv._continuationMessages = execution.runContinuationMessages;
2303
2642
  } else {
2304
2643
  freshConv._continuationMessages = undefined;
2305
2644
  freshConv.messages.push({
2306
2645
  role: "assistant",
2307
- content: assistantResponse,
2308
- metadata: toolTimeline.length > 0 || sections.length > 0
2309
- ? { toolActivity: toolTimeline, sections: sections.length > 0 ? sections : undefined } as Message["metadata"]
2310
- : undefined,
2646
+ content: execution.draft.assistantResponse,
2647
+ metadata: buildAssistantMetadata(execution.draft),
2311
2648
  });
2312
2649
  }
2313
- if (runHarnessMessages) {
2314
- freshConv._harnessMessages = runHarnessMessages;
2650
+ if (callbackNeedsContinuation && execution.runHarnessMessages) {
2651
+ freshConv._harnessMessages = execution.runHarnessMessages;
2652
+ } else if (historySelection.shouldRebuildCanonical) {
2653
+ freshConv._harnessMessages = freshConv.messages;
2654
+ } else {
2655
+ freshConv._harnessMessages = freshConv.messages;
2315
2656
  }
2316
2657
  freshConv._toolResultArchive = harness.getToolResultArchive(conversationId);
2317
- freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
2658
+ freshConv.runtimeRunId = execution.latestRunId || freshConv.runtimeRunId;
2318
2659
  freshConv.runningCallbackSince = undefined;
2319
2660
  freshConv.runStatus = "idle";
2320
- if (runContextTokens > 0) freshConv.contextTokens = runContextTokens;
2321
- if (runContextWindow > 0) freshConv.contextWindow = runContextWindow;
2661
+ if (execution.runContextTokens > 0) freshConv.contextTokens = execution.runContextTokens;
2662
+ if (execution.runContextWindow > 0) freshConv.contextWindow = execution.runContextWindow;
2322
2663
  freshConv.updatedAt = Date.now();
2323
2664
  await conversationStore.update(freshConv);
2324
2665
 
2325
2666
  // Proactive messaging notification if conversation has a messaging channel
2326
- if (freshConv.channelMeta && assistantResponse.length > 0) {
2667
+ if (freshConv.channelMeta && execution.draft.assistantResponse.length > 0) {
2327
2668
  const adapter = messagingAdapters.get(freshConv.channelMeta.platform);
2328
2669
  if (adapter) {
2329
2670
  try {
@@ -2332,7 +2673,7 @@ export const createRequestHandler = async (options?: {
2332
2673
  channelId: freshConv.channelMeta.channelId,
2333
2674
  platformThreadId: freshConv.channelMeta.platformThreadId,
2334
2675
  },
2335
- assistantResponse,
2676
+ execution.draft.assistantResponse,
2336
2677
  );
2337
2678
  } catch (sendErr) {
2338
2679
  console.error(`[poncho][subagent-callback] Messaging notify failed:`, sendErr instanceof Error ? sendErr.message : sendErr);
@@ -2343,7 +2684,7 @@ export const createRequestHandler = async (options?: {
2343
2684
  }
2344
2685
 
2345
2686
  // Handle continuation for the callback run itself
2346
- if (runContinuation) {
2687
+ if (execution.runContinuation) {
2347
2688
  if (isServerless) {
2348
2689
  const work = selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(err =>
2349
2690
  console.error(`[poncho][subagent-callback] Continuation self-fetch failed:`, err instanceof Error ? err.message : err),
@@ -2365,7 +2706,6 @@ export const createRequestHandler = async (options?: {
2365
2706
  }
2366
2707
  } finally {
2367
2708
  activeConversationRuns.delete(conversationId);
2368
- finishConversationStream(conversationId);
2369
2709
 
2370
2710
  // Check both the in-memory flag (always reliable) and the store.
2371
2711
  // We drain the flag first so a concurrent triggerParentCallback that
@@ -2374,6 +2714,16 @@ export const createRequestHandler = async (options?: {
2374
2714
  const hadDeferredTrigger = pendingCallbackNeeded.delete(conversationId);
2375
2715
  const freshConv = await conversationStore.get(conversationId);
2376
2716
  const hasPendingInStore = !!freshConv?.pendingSubagentResults?.length;
2717
+ const hasRunningCallbackChildren = Array.from(activeSubagentRuns.values()).some(
2718
+ (run) => run.parentConversationId === conversationId,
2719
+ );
2720
+
2721
+ // Only close the event stream when no further subagent work is pending.
2722
+ // If more callbacks are about to run, the stream stays alive so clients
2723
+ // receive the next callback's events through the same SSE connection.
2724
+ if (!hadDeferredTrigger && !hasPendingInStore && !hasRunningCallbackChildren) {
2725
+ finishConversationStream(conversationId);
2726
+ }
2377
2727
 
2378
2728
  if (hadDeferredTrigger || hasPendingInStore) {
2379
2729
  // Re-trigger immediately. Skip the runningCallbackSince lock check
@@ -2551,10 +2901,11 @@ export const createRequestHandler = async (options?: {
2551
2901
  let runContextWindow = conversation.contextWindow ?? 0;
2552
2902
  let resumeHarnessMessages: Message[] | undefined;
2553
2903
 
2554
- const baseMessages = checkpoint.baseMessageCount != null
2555
- ? conversation.messages.slice(0, checkpoint.baseMessageCount)
2904
+ const normalizedCheckpoint = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
2905
+ const baseMessages = normalizedCheckpoint.baseMessageCount != null
2906
+ ? conversation.messages.slice(0, normalizedCheckpoint.baseMessageCount)
2556
2907
  : [];
2557
- const fullCheckpointMessages = [...baseMessages, ...checkpoint.checkpointMessages!];
2908
+ const fullCheckpointMessages = [...baseMessages, ...normalizedCheckpoint.checkpointMessages!];
2558
2909
 
2559
2910
  // Build the tool result message that continueFromToolResult will also
2560
2911
  // construct internally. We need it here so that if the resumed run hits
@@ -2645,25 +2996,27 @@ export const createRequestHandler = async (options?: {
2645
2996
  if (event.type === "tool:approval:checkpoint") {
2646
2997
  const conv = await conversationStore.get(conversationId);
2647
2998
  if (conv) {
2648
- conv.pendingApprovals = event.approvals.map(a => ({
2649
- approvalId: a.approvalId,
2999
+ conv.pendingApprovals = buildApprovalCheckpoints({
3000
+ approvals: event.approvals,
2650
3001
  runId: latestRunId,
2651
- tool: a.tool,
2652
- toolCallId: a.toolCallId,
2653
- input: a.input,
2654
3002
  checkpointMessages: [...fullCheckpointWithResults, ...event.checkpointMessages],
2655
3003
  baseMessageCount: 0,
2656
3004
  pendingToolCalls: event.pendingToolCalls,
2657
- }));
3005
+ });
2658
3006
  conv.updatedAt = Date.now();
2659
3007
  await conversationStore.update(conv);
2660
3008
 
2661
3009
  if (conv.channelMeta?.platform === "telegram") {
2662
3010
  const tgAdapter = messagingAdapters.get("telegram") as TelegramAdapter | undefined;
2663
3011
  if (tgAdapter) {
3012
+ const messageThreadId = parseTelegramMessageThreadIdFromPlatformThreadId(
3013
+ conv.channelMeta.platformThreadId,
3014
+ conv.channelMeta.channelId,
3015
+ );
2664
3016
  void tgAdapter.sendApprovalRequest(
2665
3017
  conv.channelMeta.channelId,
2666
3018
  event.approvals.map(a => ({ approvalId: a.approvalId, tool: a.tool, input: a.input })),
3019
+ { message_thread_id: messageThreadId },
2667
3020
  ).catch(() => {});
2668
3021
  }
2669
3022
  }
@@ -2745,6 +3098,8 @@ export const createRequestHandler = async (options?: {
2745
3098
  }
2746
3099
  if (resumeHarnessMessages) {
2747
3100
  conv._harnessMessages = resumeHarnessMessages;
3101
+ } else {
3102
+ conv._harnessMessages = conv.messages;
2748
3103
  }
2749
3104
  conv.runtimeRunId = latestRunId || conv.runtimeRunId;
2750
3105
  conv.pendingApprovals = [];
@@ -2763,7 +3118,6 @@ export const createRequestHandler = async (options?: {
2763
3118
  }
2764
3119
  }
2765
3120
 
2766
- finishConversationStream(conversationId);
2767
3121
  activeConversationRuns.delete(conversationId);
2768
3122
  if (latestRunId) {
2769
3123
  runOwners.delete(latestRunId);
@@ -2774,7 +3128,14 @@ export const createRequestHandler = async (options?: {
2774
3128
  // Check for pending subagent results that arrived during the run
2775
3129
  const hadDeferred = pendingCallbackNeeded.delete(conversationId);
2776
3130
  const postConv = await conversationStore.get(conversationId);
2777
- if (hadDeferred || postConv?.pendingSubagentResults?.length) {
3131
+ const needsResumeCallback = hadDeferred || !!postConv?.pendingSubagentResults?.length;
3132
+ const hasRunningResumeChildren = Array.from(activeSubagentRuns.values()).some(
3133
+ (run) => run.parentConversationId === conversationId,
3134
+ );
3135
+ if (!needsResumeCallback && !hasRunningResumeChildren) {
3136
+ finishConversationStream(conversationId);
3137
+ }
3138
+ if (needsResumeCallback) {
2778
3139
  processSubagentCallback(conversationId, true).catch(err =>
2779
3140
  console.error(`[poncho][subagent-callback] Post-resume callback failed:`, err instanceof Error ? err.message : err),
2780
3141
  );
@@ -2786,7 +3147,6 @@ export const createRequestHandler = async (options?: {
2786
3147
  // adapter handles its own request verification (e.g. Slack signing secret).
2787
3148
  // ---------------------------------------------------------------------------
2788
3149
  const messagingRoutes = new Map<string, Map<string, (req: IncomingMessage, res: ServerResponse) => Promise<void>>>();
2789
- const messagingRunQueues = new Map<string, Promise<void>>();
2790
3150
  const messagingRouteRegistrar: RouteRegistrar = (method, path, routeHandler) => {
2791
3151
  let byMethod = messagingRoutes.get(path);
2792
3152
  if (!byMethod) {
@@ -2808,7 +3168,7 @@ export const createRequestHandler = async (options?: {
2808
3168
  };
2809
3169
  await conversationStore.update(existing);
2810
3170
  }
2811
- return { messages: existing._harnessMessages?.length ? existing._harnessMessages : existing.messages };
3171
+ return { messages: loadCanonicalHistory(existing).messages };
2812
3172
  }
2813
3173
  const now = Date.now();
2814
3174
  const channelMeta = meta.channelId
@@ -2832,28 +3192,22 @@ export const createRequestHandler = async (options?: {
2832
3192
  return { messages: [] };
2833
3193
  },
2834
3194
  async run(conversationId, input) {
2835
- const previous = messagingRunQueues.get(conversationId) ?? Promise.resolve();
2836
- let releaseQueue: (() => void) | undefined;
2837
- const current = new Promise<void>((resolve) => {
2838
- releaseQueue = resolve;
2839
- });
2840
- const chained = previous.then(() => current);
2841
- messagingRunQueues.set(conversationId, chained);
2842
- await previous;
2843
- try {
2844
- const latestConversation = await conversationStore.get(conversationId);
2845
- const latestMessages = latestConversation
2846
- ? (latestConversation._harnessMessages?.length
2847
- ? [...latestConversation._harnessMessages]
2848
- : [...latestConversation.messages])
2849
- : [...input.messages];
2850
-
2851
- const isContinuation = input.task == null;
2852
- console.log("[messaging-runner] starting run for", conversationId, isContinuation ? "(continuation)" : `task: ${input.task!.slice(0, 80)}`);
3195
+ const latestConversation = await conversationStore.get(conversationId);
3196
+ const canonicalHistory = latestConversation
3197
+ ? loadCanonicalHistory(latestConversation)
3198
+ : { messages: [...input.messages], source: "messages" as const };
3199
+ const shouldRebuildCanonical = canonicalHistory.source !== "harness";
3200
+
3201
+ const isContinuation = input.task == null;
3202
+ console.log(
3203
+ `[messaging-runner] starting run for ${conversationId} ` +
3204
+ `${isContinuation ? "(continuation)" : `task: ${input.task!.slice(0, 80)}`} ` +
3205
+ `history_source=${canonicalHistory.source}`,
3206
+ );
2853
3207
 
2854
- const historyMessages = [...latestMessages];
2855
- const preRunMessages = [...latestMessages];
2856
- const userContent = input.task;
3208
+ const historyMessages = [...canonicalHistory.messages];
3209
+ const preRunMessages = [...canonicalHistory.messages];
3210
+ const userContent = input.task;
2857
3211
 
2858
3212
  // Read-modify-write helper: always fetches the latest version from
2859
3213
  // the store before writing, so concurrent writers don't get clobbered.
@@ -2875,11 +3229,7 @@ export const createRequestHandler = async (options?: {
2875
3229
  });
2876
3230
 
2877
3231
  let latestRunId = "";
2878
- let assistantResponse = "";
2879
- const toolTimeline: string[] = [];
2880
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
2881
- let currentTools: string[] = [];
2882
- let currentText = "";
3232
+ const draft = createTurnDraftState();
2883
3233
  let checkpointedRun = false;
2884
3234
  let runContextTokens = 0;
2885
3235
  let runContextWindow = 0;
@@ -2889,23 +3239,18 @@ export const createRequestHandler = async (options?: {
2889
3239
  let runMaxSteps: number | undefined;
2890
3240
 
2891
3241
  const buildMessages = (): Message[] => {
2892
- const draftSections: Array<{ type: "text" | "tools"; content: string | string[] }> = [
2893
- ...sections.map((s) => ({
2894
- type: s.type,
2895
- content: Array.isArray(s.content) ? [...s.content] : s.content,
2896
- })),
2897
- ];
2898
- if (currentTools.length > 0) {
2899
- draftSections.push({ type: "tools", content: [...currentTools] });
3242
+ const draftSections = cloneSections(draft.sections);
3243
+ if (draft.currentTools.length > 0) {
3244
+ draftSections.push({ type: "tools", content: [...draft.currentTools] });
2900
3245
  }
2901
- if (currentText.length > 0) {
2902
- draftSections.push({ type: "text", content: currentText });
3246
+ if (draft.currentText.length > 0) {
3247
+ draftSections.push({ type: "text", content: draft.currentText });
2903
3248
  }
2904
3249
  const userTurn: Message[] = userContent != null
2905
3250
  ? [{ role: "user" as const, content: userContent }]
2906
3251
  : [];
2907
3252
  const hasDraftContent =
2908
- assistantResponse.length > 0 || toolTimeline.length > 0 || draftSections.length > 0;
3253
+ draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draftSections.length > 0;
2909
3254
  if (!hasDraftContent) {
2910
3255
  return [...historyMessages, ...userTurn];
2911
3256
  }
@@ -2914,20 +3259,14 @@ export const createRequestHandler = async (options?: {
2914
3259
  ...userTurn,
2915
3260
  {
2916
3261
  role: "assistant" as const,
2917
- content: assistantResponse,
2918
- metadata:
2919
- toolTimeline.length > 0 || draftSections.length > 0
2920
- ? ({
2921
- toolActivity: [...toolTimeline],
2922
- sections: draftSections.length > 0 ? draftSections : undefined,
2923
- } as Message["metadata"])
2924
- : undefined,
3262
+ content: draft.assistantResponse,
3263
+ metadata: buildAssistantMetadata(draft, draftSections),
2925
3264
  },
2926
3265
  ];
2927
3266
  };
2928
3267
 
2929
3268
  const persistDraftAssistantTurn = async (): Promise<void> => {
2930
- if (assistantResponse.length === 0 && toolTimeline.length === 0) return;
3269
+ if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
2931
3270
  await updateConversation((c) => {
2932
3271
  c.messages = buildMessages();
2933
3272
  });
@@ -2950,64 +3289,39 @@ export const createRequestHandler = async (options?: {
2950
3289
  };
2951
3290
 
2952
3291
  try {
2953
- for await (const event of harness.runWithTelemetry(runInput)) {
2954
- if (event.type === "run:started") {
2955
- latestRunId = event.runId;
2956
- runOwners.set(event.runId, "local-owner");
2957
- runConversations.set(event.runId, conversationId);
2958
- }
2959
- if (event.type === "model:chunk") {
2960
- if (currentTools.length > 0) {
2961
- sections.push({ type: "tools", content: currentTools });
2962
- currentTools = [];
2963
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
2964
- assistantResponse += " ";
2965
- }
2966
- }
2967
- assistantResponse += event.content;
2968
- currentText += event.content;
2969
- }
2970
- if (event.type === "tool:started") {
2971
- if (currentText.length > 0) {
2972
- sections.push({ type: "text", content: currentText });
2973
- currentText = "";
3292
+ const execution = await executeConversationTurn({
3293
+ harness,
3294
+ runInput,
3295
+ onEvent: async (event, eventDraft) => {
3296
+ draft.assistantResponse = eventDraft.assistantResponse;
3297
+ draft.toolTimeline = eventDraft.toolTimeline;
3298
+ draft.sections = eventDraft.sections;
3299
+ draft.currentTools = eventDraft.currentTools;
3300
+ draft.currentText = eventDraft.currentText;
3301
+ if (event.type === "run:started") {
3302
+ latestRunId = event.runId;
3303
+ runOwners.set(event.runId, "local-owner");
3304
+ runConversations.set(event.runId, conversationId);
2974
3305
  }
2975
- const toolText = `- start \`${event.tool}\``;
2976
- toolTimeline.push(toolText);
2977
- currentTools.push(toolText);
2978
- }
2979
- if (event.type === "tool:completed") {
2980
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
2981
- toolTimeline.push(toolText);
2982
- currentTools.push(toolText);
2983
- }
2984
- if (event.type === "tool:error") {
2985
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
2986
- toolTimeline.push(toolText);
2987
- currentTools.push(toolText);
2988
- }
2989
3306
  if (event.type === "step:completed") {
2990
3307
  await persistDraftAssistantTurn();
2991
3308
  }
2992
3309
  if (event.type === "tool:approval:required") {
2993
3310
  const toolText = `- approval required \`${event.tool}\``;
2994
- toolTimeline.push(toolText);
2995
- currentTools.push(toolText);
3311
+ draft.toolTimeline.push(toolText);
3312
+ draft.currentTools.push(toolText);
2996
3313
  await persistDraftAssistantTurn();
2997
3314
  }
2998
3315
  if (event.type === "tool:approval:checkpoint") {
2999
3316
  await updateConversation((c) => {
3000
3317
  c.messages = buildMessages();
3001
- c.pendingApprovals = event.approvals.map(a => ({
3002
- approvalId: a.approvalId,
3318
+ c.pendingApprovals = buildApprovalCheckpoints({
3319
+ approvals: event.approvals,
3003
3320
  runId: latestRunId,
3004
- tool: a.tool,
3005
- toolCallId: a.toolCallId,
3006
- input: a.input,
3007
3321
  checkpointMessages: event.checkpointMessages,
3008
3322
  baseMessageCount: historyMessages.length,
3009
3323
  pendingToolCalls: event.pendingToolCalls,
3010
- }));
3324
+ });
3011
3325
  });
3012
3326
  checkpointedRun = true;
3013
3327
 
@@ -3021,9 +3335,14 @@ export const createRequestHandler = async (options?: {
3021
3335
  tool: a.tool,
3022
3336
  input: a.input,
3023
3337
  }));
3338
+ const messageThreadId = parseTelegramMessageThreadIdFromPlatformThreadId(
3339
+ conv.channelMeta.platformThreadId,
3340
+ conv.channelMeta.channelId,
3341
+ );
3024
3342
  void tgAdapter.sendApprovalRequest(
3025
3343
  conv.channelMeta.channelId,
3026
3344
  approvals,
3345
+ { message_thread_id: messageThreadId },
3027
3346
  ).catch((err: unknown) => {
3028
3347
  console.error("[messaging-runner] failed to send Telegram approval request:", err instanceof Error ? err.message : err);
3029
3348
  });
@@ -3046,38 +3365,22 @@ export const createRequestHandler = async (options?: {
3046
3365
  });
3047
3366
  }
3048
3367
  }
3049
- if (event.type === "run:completed") {
3050
- if (assistantResponse.length === 0 && event.result.response) {
3051
- assistantResponse = event.result.response;
3052
- }
3053
- runContinuation = event.result.continuation === true;
3054
- if (event.result.continuationMessages) {
3055
- runContinuationMessages = event.result.continuationMessages;
3056
- }
3057
- runSteps = event.result.steps;
3058
- if (typeof event.result.maxSteps === "number") runMaxSteps = event.result.maxSteps;
3059
- runContextTokens = event.result.contextTokens ?? runContextTokens;
3060
- runContextWindow = event.result.contextWindow ?? runContextWindow;
3061
- }
3062
- if (event.type === "run:error") {
3063
- assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
3064
- }
3065
- broadcastEvent(conversationId, event);
3066
- }
3368
+ broadcastEvent(conversationId, event);
3369
+ },
3370
+ });
3371
+ runContinuation = execution.runContinuation;
3372
+ runContinuationMessages = execution.runContinuationMessages;
3373
+ runSteps = execution.runSteps;
3374
+ runMaxSteps = execution.runMaxSteps;
3375
+ runContextTokens = execution.runContextTokens;
3376
+ runContextWindow = execution.runContextWindow;
3377
+ latestRunId = execution.latestRunId || latestRunId;
3067
3378
  } catch (err) {
3068
3379
  console.error("[messaging-runner] run failed:", err instanceof Error ? err.message : err);
3069
- assistantResponse = assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
3380
+ draft.assistantResponse = draft.assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
3070
3381
  }
3071
3382
 
3072
- // Finalize sections (clear after pushing so buildMessages doesn't re-add)
3073
- if (currentTools.length > 0) {
3074
- sections.push({ type: "tools", content: currentTools });
3075
- currentTools = [];
3076
- }
3077
- if (currentText.length > 0) {
3078
- sections.push({ type: "text", content: currentText });
3079
- currentText = "";
3080
- }
3383
+ flushTurnDraft(draft);
3081
3384
 
3082
3385
  if (!checkpointedRun) {
3083
3386
  await updateConversation((c) => {
@@ -3089,6 +3392,10 @@ export const createRequestHandler = async (options?: {
3089
3392
  }
3090
3393
  if (runContinuationMessages) {
3091
3394
  c._harnessMessages = runContinuationMessages;
3395
+ } else if (shouldRebuildCanonical) {
3396
+ c._harnessMessages = c.messages;
3397
+ } else {
3398
+ c._harnessMessages = c.messages;
3092
3399
  }
3093
3400
  c.runtimeRunId = latestRunId || c.runtimeRunId;
3094
3401
  c.pendingApprovals = [];
@@ -3098,6 +3405,9 @@ export const createRequestHandler = async (options?: {
3098
3405
  });
3099
3406
  } else {
3100
3407
  await updateConversation((c) => {
3408
+ if (shouldRebuildCanonical && !c._harnessMessages?.length) {
3409
+ c._harnessMessages = c.messages;
3410
+ }
3101
3411
  c.runStatus = "idle";
3102
3412
  });
3103
3413
  }
@@ -3107,21 +3417,15 @@ export const createRequestHandler = async (options?: {
3107
3417
  runConversations.delete(latestRunId);
3108
3418
  }
3109
3419
 
3110
- console.log("[messaging-runner] run complete, response length:", assistantResponse.length, runContinuation ? "(continuation)" : "");
3111
- const response = assistantResponse;
3420
+ console.log("[messaging-runner] run complete, response length:", draft.assistantResponse.length, runContinuation ? "(continuation)" : "");
3421
+ const response = draft.assistantResponse;
3112
3422
 
3113
- return {
3114
- response,
3115
- continuation: runContinuation,
3116
- steps: runSteps,
3117
- maxSteps: runMaxSteps,
3118
- };
3119
- } finally {
3120
- releaseQueue?.();
3121
- if (messagingRunQueues.get(conversationId) === chained) {
3122
- messagingRunQueues.delete(conversationId);
3123
- }
3124
- }
3423
+ return {
3424
+ response,
3425
+ continuation: runContinuation,
3426
+ steps: runSteps,
3427
+ maxSteps: runMaxSteps,
3428
+ };
3125
3429
  },
3126
3430
  };
3127
3431
 
@@ -3271,6 +3575,7 @@ export const createRequestHandler = async (options?: {
3271
3575
  ): AsyncGenerator<AgentEvent> {
3272
3576
  const conversation = await conversationStore.get(conversationId);
3273
3577
  if (!conversation) return;
3578
+ if (Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0) return;
3274
3579
  if (!conversation._continuationMessages?.length) return;
3275
3580
  if (conversation.runStatus === "running") return;
3276
3581
 
@@ -3438,7 +3743,11 @@ export const createRequestHandler = async (options?: {
3438
3743
  freshConv._continuationCount = undefined;
3439
3744
  }
3440
3745
 
3441
- if (nextHarnessMessages) freshConv._harnessMessages = nextHarnessMessages;
3746
+ if (nextHarnessMessages) {
3747
+ freshConv._harnessMessages = nextHarnessMessages;
3748
+ } else {
3749
+ freshConv._harnessMessages = freshConv.messages;
3750
+ }
3442
3751
  freshConv._toolResultArchive = harness.getToolResultArchive(conversationId);
3443
3752
  freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
3444
3753
  freshConv.pendingApprovals = [];
@@ -3575,6 +3884,8 @@ export const createRequestHandler = async (options?: {
3575
3884
  }
3576
3885
  if (runResult?.continuationMessages) {
3577
3886
  conv._harnessMessages = runResult.continuationMessages;
3887
+ } else {
3888
+ conv._harnessMessages = conv.messages;
3578
3889
  }
3579
3890
  conv._toolResultArchive = childHarness.getToolResultArchive(conversationId);
3580
3891
  conv.lastActivityAt = Date.now();
@@ -3774,23 +4085,15 @@ export const createRequestHandler = async (options?: {
3774
4085
  }
3775
4086
 
3776
4087
  // Regular (non-subagent) approval
3777
- const conversations = await conversationStore.list("local-owner");
3778
- let foundConversation: Conversation | undefined;
3779
- let foundApproval: NonNullable<Conversation["pendingApprovals"]>[number] | undefined;
3780
- for (const conv of conversations) {
3781
- if (!Array.isArray(conv.pendingApprovals)) continue;
3782
- const match = conv.pendingApprovals.find(a => a.approvalId === approvalId);
3783
- if (match) {
3784
- foundConversation = conv;
3785
- foundApproval = match;
3786
- break;
3787
- }
3788
- }
4088
+ const found = await findPendingApproval(approvalId, "local-owner");
4089
+ let foundConversation = found?.conversation;
4090
+ let foundApproval = found?.approval;
3789
4091
 
3790
4092
  if (!foundConversation || !foundApproval) {
3791
4093
  console.warn("[telegram-approval] approval not found:", approvalId);
3792
4094
  return;
3793
4095
  }
4096
+ foundApproval = normalizeApprovalCheckpoint(foundApproval, foundConversation.messages);
3794
4097
 
3795
4098
  await adapter.updateApprovalMessage(approvalId, approved ? "approved" : "denied", foundApproval.tool);
3796
4099
 
@@ -3802,7 +4105,9 @@ export const createRequestHandler = async (options?: {
3802
4105
  : { type: "tool:approval:denied", approvalId },
3803
4106
  );
3804
4107
 
3805
- const allApprovals = foundConversation.pendingApprovals ?? [];
4108
+ const allApprovals = (foundConversation.pendingApprovals ?? []).map((approval) =>
4109
+ normalizeApprovalCheckpoint(approval, foundConversation!.messages),
4110
+ );
3806
4111
  const allDecided = allApprovals.length > 0 && allApprovals.every(a => a.decision != null);
3807
4112
 
3808
4113
  if (!allDecided) {
@@ -4220,6 +4525,9 @@ export const createRequestHandler = async (options?: {
4220
4525
  pathname !== "/api/auth/login" &&
4221
4526
  request.headers["x-csrf-token"] !== session?.csrfToken
4222
4527
  ) {
4528
+ console.warn(
4529
+ `[poncho][csrf] blocked request method=${request.method} path="${pathname}" session=${session.sessionId}`,
4530
+ );
4223
4531
  writeJson(response, 403, {
4224
4532
  code: "CSRF_ERROR",
4225
4533
  message: "Invalid CSRF token",
@@ -4443,8 +4751,11 @@ export const createRequestHandler = async (options?: {
4443
4751
  const approvalMatch = pathname.match(/^\/api\/approvals\/([^/]+)$/);
4444
4752
  if (approvalMatch && request.method === "POST") {
4445
4753
  const approvalId = decodeURIComponent(approvalMatch[1] ?? "");
4446
- const body = (await readRequestBody(request)) as { approved?: boolean };
4754
+ const body = (await readRequestBody(request)) as { approved?: boolean; conversationId?: string };
4447
4755
  const approved = body.approved === true;
4756
+ const hintedConversationId = typeof body.conversationId === "string" && body.conversationId.trim().length > 0
4757
+ ? body.conversationId.trim()
4758
+ : undefined;
4448
4759
 
4449
4760
  // Check if this is a pending subagent approval (handled inline by runSubagent)
4450
4761
  const pendingSubagent = pendingSubagentApprovals.get(approvalId);
@@ -4480,18 +4791,23 @@ export const createRequestHandler = async (options?: {
4480
4791
  }
4481
4792
 
4482
4793
  // Find the approval in the conversation store (checkpoint-based flow)
4483
- const conversations = await conversationStore.list(ownerId);
4484
4794
  let foundConversation: Conversation | undefined;
4485
4795
  let foundApproval: NonNullable<Conversation["pendingApprovals"]>[number] | undefined;
4486
- for (const conv of conversations) {
4487
- if (!Array.isArray(conv.pendingApprovals)) continue;
4488
- const match = conv.pendingApprovals.find(a => a.approvalId === approvalId);
4489
- if (match) {
4490
- foundConversation = conv;
4491
- foundApproval = match;
4492
- break;
4796
+ if (hintedConversationId) {
4797
+ const hintedConversation = await conversationStore.get(hintedConversationId);
4798
+ if (hintedConversation && hintedConversation.ownerId === ownerId && Array.isArray(hintedConversation.pendingApprovals)) {
4799
+ const hintedMatch = hintedConversation.pendingApprovals.find((approval) => approval.approvalId === approvalId);
4800
+ if (hintedMatch) {
4801
+ foundConversation = hintedConversation;
4802
+ foundApproval = hintedMatch;
4803
+ }
4493
4804
  }
4494
4805
  }
4806
+ if (!foundConversation || !foundApproval) {
4807
+ const found = await findPendingApproval(approvalId, ownerId);
4808
+ foundConversation = found?.conversation;
4809
+ foundApproval = found?.approval;
4810
+ }
4495
4811
 
4496
4812
  if (!foundConversation || !foundApproval) {
4497
4813
  writeJson(response, 404, {
@@ -4502,28 +4818,23 @@ export const createRequestHandler = async (options?: {
4502
4818
  }
4503
4819
 
4504
4820
  const conversationId = foundConversation.conversationId;
4821
+ foundApproval = normalizeApprovalCheckpoint(foundApproval, foundConversation.messages);
4505
4822
 
4506
4823
  if (!foundApproval.checkpointMessages || !foundApproval.toolCallId) {
4507
- foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? [])
4508
- .filter(a => a.approvalId !== approvalId);
4509
- await conversationStore.update(foundConversation);
4510
- writeJson(response, 404, {
4511
- code: "APPROVAL_NOT_FOUND",
4512
- message: "Approval request is no longer active (no checkpoint data)",
4824
+ writeJson(response, 409, {
4825
+ code: "APPROVAL_NOT_READY",
4826
+ message: "Approval checkpoint is not ready yet. Please retry shortly.",
4513
4827
  });
4514
4828
  return;
4515
4829
  }
4516
4830
 
4517
- // Track decision in memory so parallel batch requests see a consistent
4518
- // view (file-store reads return independent copies, causing lost updates).
4519
- let batchDecisions = approvalDecisionTracker.get(conversationId);
4520
- if (!batchDecisions) {
4521
- batchDecisions = new Map();
4522
- approvalDecisionTracker.set(conversationId, batchDecisions);
4523
- }
4524
- batchDecisions.set(approvalId, approved);
4525
-
4526
- foundApproval.decision = approved ? "approved" : "denied";
4831
+ const approvalDecision = approved ? "approved" : "denied";
4832
+ foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? []).map((approval) =>
4833
+ approval.approvalId === approvalId
4834
+ ? { ...normalizeApprovalCheckpoint(approval, foundConversation!.messages), decision: approvalDecision }
4835
+ : normalizeApprovalCheckpoint(approval, foundConversation!.messages),
4836
+ );
4837
+ await conversationStore.update(foundConversation);
4527
4838
 
4528
4839
  broadcastEvent(conversationId,
4529
4840
  approved
@@ -4531,25 +4842,18 @@ export const createRequestHandler = async (options?: {
4531
4842
  : { type: "tool:approval:denied", approvalId },
4532
4843
  );
4533
4844
 
4534
- const allApprovals = foundConversation.pendingApprovals ?? [];
4845
+ const refreshedConversation = await conversationStore.get(conversationId);
4846
+ const allApprovals = (refreshedConversation?.pendingApprovals ?? []).map((approval) =>
4847
+ normalizeApprovalCheckpoint(approval, refreshedConversation!.messages),
4848
+ );
4535
4849
  const allDecided = allApprovals.length > 0 &&
4536
- allApprovals.every(a => batchDecisions!.has(a.approvalId));
4850
+ allApprovals.every(a => a.decision != null);
4537
4851
 
4538
4852
  if (!allDecided) {
4539
- // Still waiting for more decisions — persist best-effort and respond.
4540
- // The write may be overwritten by a concurrent request, but that's
4541
- // fine: the in-memory tracker is the source of truth for completion.
4542
- await conversationStore.update(foundConversation);
4543
4853
  writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
4544
4854
  return;
4545
4855
  }
4546
4856
 
4547
- // All approvals in the batch are decided — apply tracked decisions,
4548
- // execute approved tools, and resume the run.
4549
- for (const a of allApprovals) {
4550
- const d = batchDecisions.get(a.approvalId);
4551
- if (d != null) a.decision = d ? "approved" : "denied";
4552
- }
4553
4857
  approvalDecisionTracker.delete(conversationId);
4554
4858
 
4555
4859
  foundConversation.pendingApprovals = [];
@@ -4787,6 +5091,7 @@ export const createRequestHandler = async (options?: {
4787
5091
  runId: a.runId,
4788
5092
  tool: a.tool,
4789
5093
  input: a.input,
5094
+ decision: a.decision,
4790
5095
  }))
4791
5096
  : [];
4792
5097
  // Collect pending approvals from subagent conversations (in-memory map, no disk I/O)
@@ -4805,26 +5110,17 @@ export const createRequestHandler = async (options?: {
4805
5110
  }
4806
5111
  const activeStream = conversationEventStreams.get(conversationId);
4807
5112
  const hasActiveRun = (!!activeStream && !activeStream.finished) || conversation.runStatus === "running";
4808
- let hasRunningSubagents = Array.from(activeSubagentRuns.values()).some(
4809
- r => r.parentConversationId === conversationId,
4810
- );
4811
- // On serverless, in-memory maps may be empty — also check store
4812
- if (!hasRunningSubagents && !conversation.parentConversationId) {
4813
- const summaries = await conversationStore.listSummaries(conversation.ownerId);
4814
- for (const s of summaries) {
4815
- if (s.parentConversationId !== conversationId) continue;
4816
- const c = await conversationStore.get(s.conversationId);
4817
- if (c?.subagentMeta?.status === "running") {
4818
- hasRunningSubagents = true;
4819
- break;
4820
- }
4821
- }
4822
- }
5113
+ const hasRunningSubagents = !conversation.parentConversationId
5114
+ ? await hasRunningSubagentsForParent(conversationId, conversation.ownerId)
5115
+ : false;
4823
5116
  const hasPendingCallbackResults = Array.isArray(conversation.pendingSubagentResults)
4824
5117
  && conversation.pendingSubagentResults.length > 0;
5118
+ const hasPendingApprovals = Array.isArray(conversation.pendingApprovals)
5119
+ && conversation.pendingApprovals.length > 0;
4825
5120
  const needsContinuation = !hasActiveRun
4826
5121
  && Array.isArray(conversation._continuationMessages)
4827
- && conversation._continuationMessages.length > 0;
5122
+ && conversation._continuationMessages.length > 0
5123
+ && !hasPendingApprovals;
4828
5124
  writeJson(response, 200, {
4829
5125
  conversation: {
4830
5126
  ...conversation,
@@ -5036,11 +5332,9 @@ export const createRequestHandler = async (options?: {
5036
5332
  eventCount++;
5037
5333
  let sseEvent: AgentEvent = event;
5038
5334
  if (sseEvent.type === "run:completed") {
5039
- const hasRunningSubagents = Array.from(activeSubagentRuns.values()).some(
5040
- r => r.parentConversationId === conversationId,
5041
- );
5335
+ const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
5042
5336
  const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
5043
- sseEvent = hasRunningSubagents ? { ...stripped, pendingSubagents: true } : stripped;
5337
+ sseEvent = hasPendingSubagents ? { ...stripped, pendingSubagents: true } : stripped;
5044
5338
  }
5045
5339
  try {
5046
5340
  response.write(formatSseEvent(sseEvent));
@@ -5067,7 +5361,10 @@ export const createRequestHandler = async (options?: {
5067
5361
  // fire a delayed safety net in case the client disconnects before
5068
5362
  // POSTing the next /continue.
5069
5363
  const freshConv = await conversationStore.get(conversationId);
5070
- if (freshConv?._continuationMessages?.length) {
5364
+ if (
5365
+ freshConv?._continuationMessages?.length &&
5366
+ (!Array.isArray(freshConv.pendingApprovals) || freshConv.pendingApprovals.length === 0)
5367
+ ) {
5071
5368
  doWaitUntil(
5072
5369
  new Promise(r => setTimeout(r, 3000)).then(() =>
5073
5370
  selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
@@ -5159,11 +5456,17 @@ export const createRequestHandler = async (options?: {
5159
5456
  Connection: "keep-alive",
5160
5457
  "X-Accel-Buffering": "no",
5161
5458
  });
5162
- const harnessMessages = conversation._harnessMessages?.length
5163
- ? [...conversation._harnessMessages]
5164
- : [...conversation.messages];
5459
+ const canonicalHistory = resolveRunRequest(conversation, {
5460
+ conversationId,
5461
+ messages: conversation.messages,
5462
+ });
5463
+ const shouldRebuildCanonical = canonicalHistory.shouldRebuildCanonical;
5464
+ const harnessMessages = [...canonicalHistory.messages];
5165
5465
  const historyMessages = [...conversation.messages];
5166
5466
  const preRunMessages = [...conversation.messages];
5467
+ console.info(
5468
+ `[poncho] conversation="${conversationId}" history_source=${canonicalHistory.source}`,
5469
+ );
5167
5470
  let latestRunId = conversation.runtimeRunId ?? "";
5168
5471
  let assistantResponse = "";
5169
5472
  const toolTimeline: string[] = [];
@@ -5379,6 +5682,26 @@ export const createRequestHandler = async (options?: {
5379
5682
  const toolText = `- approval required \`${event.tool}\``;
5380
5683
  toolTimeline.push(toolText);
5381
5684
  currentTools.push(toolText);
5685
+ const existingApprovals = Array.isArray(conversation.pendingApprovals)
5686
+ ? conversation.pendingApprovals
5687
+ : [];
5688
+ if (!existingApprovals.some((approval) => approval.approvalId === event.approvalId)) {
5689
+ conversation.pendingApprovals = [
5690
+ ...existingApprovals,
5691
+ {
5692
+ approvalId: event.approvalId,
5693
+ runId: latestRunId || conversation.runtimeRunId || "",
5694
+ tool: event.tool,
5695
+ toolCallId: undefined,
5696
+ input: (event.input ?? {}) as Record<string, unknown>,
5697
+ checkpointMessages: undefined,
5698
+ baseMessageCount: historyMessages.length,
5699
+ pendingToolCalls: [],
5700
+ },
5701
+ ];
5702
+ conversation.updatedAt = Date.now();
5703
+ await conversationStore.update(conversation);
5704
+ }
5382
5705
  await persistDraftAssistantTurn();
5383
5706
  }
5384
5707
  if (event.type === "tool:approval:checkpoint") {
@@ -5402,16 +5725,13 @@ export const createRequestHandler = async (options?: {
5402
5725
  }]
5403
5726
  : []),
5404
5727
  ];
5405
- conversation.pendingApprovals = event.approvals.map(a => ({
5406
- approvalId: a.approvalId,
5728
+ conversation.pendingApprovals = buildApprovalCheckpoints({
5729
+ approvals: event.approvals,
5407
5730
  runId: latestRunId,
5408
- tool: a.tool,
5409
- toolCallId: a.toolCallId,
5410
- input: a.input,
5411
5731
  checkpointMessages: event.checkpointMessages,
5412
5732
  baseMessageCount: historyMessages.length,
5413
5733
  pendingToolCalls: event.pendingToolCalls,
5414
- }));
5734
+ });
5415
5735
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5416
5736
  conversation.updatedAt = Date.now();
5417
5737
  await conversationStore.update(conversation);
@@ -5447,7 +5767,9 @@ export const createRequestHandler = async (options?: {
5447
5767
  conversation._harnessMessages = runContinuationMessages;
5448
5768
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5449
5769
  conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
5450
- conversation.pendingApprovals = [];
5770
+ if (!checkpointedRun) {
5771
+ conversation.pendingApprovals = [];
5772
+ }
5451
5773
  if (runContextTokens > 0) conversation.contextTokens = runContextTokens;
5452
5774
  if (runContextWindow > 0) conversation.contextWindow = runContextWindow;
5453
5775
  conversation.updatedAt = Date.now();
@@ -5455,11 +5777,13 @@ export const createRequestHandler = async (options?: {
5455
5777
 
5456
5778
  // Delayed safety net: if the client doesn't POST to /continue
5457
5779
  // within 3 seconds (e.g. browser closed), the server picks it up.
5458
- doWaitUntil(
5459
- new Promise(r => setTimeout(r, 3000)).then(() =>
5460
- selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
5461
- ),
5462
- );
5780
+ if (!checkpointedRun) {
5781
+ doWaitUntil(
5782
+ new Promise(r => setTimeout(r, 3000)).then(() =>
5783
+ selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
5784
+ ),
5785
+ );
5786
+ }
5463
5787
  }
5464
5788
  }
5465
5789
  await telemetry.emit(event);
@@ -5467,11 +5791,9 @@ export const createRequestHandler = async (options?: {
5467
5791
  ? { ...event, compactedMessages: undefined }
5468
5792
  : event;
5469
5793
  if (sseEvent.type === "run:completed") {
5470
- const hasRunningSubagents = Array.from(activeSubagentRuns.values()).some(
5471
- r => r.parentConversationId === conversationId,
5472
- );
5794
+ const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
5473
5795
  const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
5474
- if (hasRunningSubagents) {
5796
+ if (hasPendingSubagents) {
5475
5797
  sseEvent = { ...stripped, pendingSubagents: true };
5476
5798
  } else {
5477
5799
  sseEvent = stripped;
@@ -5519,6 +5841,10 @@ export const createRequestHandler = async (options?: {
5519
5841
  conversation._continuationMessages = undefined;
5520
5842
  if (runHarnessMessages) {
5521
5843
  conversation._harnessMessages = runHarnessMessages;
5844
+ } else if (shouldRebuildCanonical) {
5845
+ conversation._harnessMessages = conversation.messages;
5846
+ } else {
5847
+ conversation._harnessMessages = conversation.messages;
5522
5848
  }
5523
5849
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5524
5850
  conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
@@ -5556,7 +5882,9 @@ export const createRequestHandler = async (options?: {
5556
5882
  conversation.updatedAt = Date.now();
5557
5883
  await conversationStore.update(conversation);
5558
5884
  }
5559
- await clearPendingApprovalsForConversation(conversationId);
5885
+ if (!checkpointedRun) {
5886
+ await clearPendingApprovalsForConversation(conversationId);
5887
+ }
5560
5888
  return;
5561
5889
  }
5562
5890
  try {
@@ -5604,20 +5932,32 @@ export const createRequestHandler = async (options?: {
5604
5932
  if (active && active.abortController === abortController) {
5605
5933
  activeConversationRuns.delete(conversationId);
5606
5934
  }
5607
- finishConversationStream(conversationId);
5608
5935
  if (latestRunId) {
5609
5936
  runOwners.delete(latestRunId);
5610
5937
  runConversations.delete(latestRunId);
5611
5938
  }
5939
+
5940
+ // Determine if subagent work is pending before deciding to close the
5941
+ // event stream. When a callback is about to run, the stream stays open
5942
+ // so clients that subscribe to /events receive callback-run events in
5943
+ // real-time — the same delivery path used for every other run.
5944
+ const hadDeferred = pendingCallbackNeeded.delete(conversationId);
5945
+ const freshConv = await conversationStore.get(conversationId);
5946
+ const needsCallback = hadDeferred || !!freshConv?.pendingSubagentResults?.length;
5947
+ const hasRunningChildren = Array.from(activeSubagentRuns.values()).some(
5948
+ (run) => run.parentConversationId === conversationId,
5949
+ );
5950
+
5951
+ if (!needsCallback && !hasRunningChildren) {
5952
+ finishConversationStream(conversationId);
5953
+ }
5954
+
5612
5955
  try {
5613
5956
  response.end();
5614
5957
  } catch {
5615
5958
  // Already closed.
5616
5959
  }
5617
- // Check for pending subagent results that arrived during the run
5618
- const hadDeferred = pendingCallbackNeeded.delete(conversationId);
5619
- const freshConv = await conversationStore.get(conversationId);
5620
- if (hadDeferred || freshConv?.pendingSubagentResults?.length) {
5960
+ if (needsCallback) {
5621
5961
  processSubagentCallback(conversationId, true).catch(err =>
5622
5962
  console.error(`[poncho][subagent-callback] Post-run callback failed:`, err instanceof Error ? err.message : err),
5623
5963
  );
@@ -5682,46 +6022,42 @@ export const createRequestHandler = async (options?: {
5682
6022
  if (!conv) continue;
5683
6023
 
5684
6024
  const task = `[Scheduled: ${jobName}]\n${cronJob.task}`;
5685
- const historyMessages = conv._harnessMessages?.length
5686
- ? [...conv._harnessMessages]
5687
- : [...conv.messages];
6025
+ const historySelection = resolveRunRequest(conv, {
6026
+ conversationId: conv.conversationId,
6027
+ messages: conv.messages,
6028
+ });
6029
+ const historyMessages = [...historySelection.messages];
5688
6030
  try {
5689
- let assistantResponse = "";
5690
- let steps = 0;
5691
- let cronHarnessMessages: Message[] | undefined;
5692
- for await (const event of harness.runWithTelemetry({
5693
- task,
5694
- conversationId: conv.conversationId,
5695
- parameters: withToolResultArchiveParam(
5696
- { __activeConversationId: conv.conversationId },
5697
- conv,
5698
- ),
5699
- messages: historyMessages,
5700
- })) {
5701
- if (event.type === "model:chunk") {
5702
- assistantResponse += event.content;
5703
- }
5704
- if (event.type === "run:completed") {
5705
- steps = event.result.steps;
5706
- if (!assistantResponse && event.result.response) {
5707
- assistantResponse = event.result.response;
5708
- }
5709
- if (event.result.continuationMessages) {
5710
- cronHarnessMessages = event.result.continuationMessages;
5711
- }
5712
- }
5713
- await telemetry.emit(event);
5714
- }
6031
+ const execution = await executeConversationTurn({
6032
+ harness,
6033
+ runInput: {
6034
+ task,
6035
+ conversationId: conv.conversationId,
6036
+ parameters: withToolResultArchiveParam(
6037
+ { __activeConversationId: conv.conversationId },
6038
+ conv,
6039
+ ),
6040
+ messages: historyMessages,
6041
+ },
6042
+ onEvent: async (event) => {
6043
+ await telemetry.emit(event);
6044
+ },
6045
+ });
6046
+ const assistantResponse = execution.draft.assistantResponse;
5715
6047
 
5716
6048
  conv.messages = [
5717
6049
  ...historyMessages,
5718
6050
  { role: "user" as const, content: task },
5719
6051
  ...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
5720
6052
  ];
5721
- if (cronHarnessMessages) {
5722
- conv._harnessMessages = cronHarnessMessages;
6053
+ if (execution.runHarnessMessages) {
6054
+ conv._harnessMessages = execution.runHarnessMessages;
6055
+ } else if (historySelection.shouldRebuildCanonical) {
6056
+ conv._harnessMessages = conv.messages;
5723
6057
  }
5724
6058
  conv._toolResultArchive = harness.getToolResultArchive(conv.conversationId);
6059
+ if (execution.runContextTokens > 0) conv.contextTokens = execution.runContextTokens;
6060
+ if (execution.runContextWindow > 0) conv.contextWindow = execution.runContextWindow;
5725
6061
  conv.updatedAt = Date.now();
5726
6062
  await conversationStore.update(conv);
5727
6063
 
@@ -5738,7 +6074,7 @@ export const createRequestHandler = async (options?: {
5738
6074
  console.error(`[cron] ${jobName}: send to ${chatId} failed:`, sendError instanceof Error ? sendError.message : sendError);
5739
6075
  }
5740
6076
  }
5741
- chatResults.push({ chatId, status: "completed", steps });
6077
+ chatResults.push({ chatId, status: "completed", steps: execution.runSteps });
5742
6078
  } catch (runError) {
5743
6079
  chatResults.push({ chatId, status: "error" });
5744
6080
  console.error(`[cron] ${jobName}: run for chat ${chatId} failed:`, runError instanceof Error ? runError.message : runError);
@@ -6038,91 +6374,32 @@ export const startDevServer = async (
6038
6374
  conversationId: string,
6039
6375
  historyMessages: Message[],
6040
6376
  toolResultArchive?: Conversation["_toolResultArchive"],
6041
- onEvent?: (event: AgentEvent) => void,
6377
+ onEvent?: (event: AgentEvent) => void | Promise<void>,
6042
6378
  ): Promise<CronRunResult> => {
6043
- let assistantResponse = "";
6044
- let steps = 0;
6045
- let contextTokens = 0;
6046
- let contextWindow = 0;
6047
- let harnessMessages: Message[] | undefined;
6048
- const toolTimeline: string[] = [];
6049
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
6050
- let currentTools: string[] = [];
6051
- let currentText = "";
6052
- for await (const event of harnessRef.runWithTelemetry({
6053
- task,
6054
- conversationId,
6055
- parameters: {
6056
- __activeConversationId: conversationId,
6057
- [TOOL_RESULT_ARCHIVE_PARAM]: toolResultArchive ?? {},
6379
+ const execution = await executeConversationTurn({
6380
+ harness: harnessRef,
6381
+ runInput: {
6382
+ task,
6383
+ conversationId,
6384
+ parameters: {
6385
+ __activeConversationId: conversationId,
6386
+ [TOOL_RESULT_ARCHIVE_PARAM]: toolResultArchive ?? {},
6387
+ },
6388
+ messages: historyMessages,
6058
6389
  },
6059
- messages: historyMessages,
6060
- })) {
6061
- onEvent?.(event);
6062
- if (event.type === "model:chunk") {
6063
- if (currentTools.length > 0) {
6064
- sections.push({ type: "tools", content: currentTools });
6065
- currentTools = [];
6066
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
6067
- assistantResponse += " ";
6068
- }
6069
- }
6070
- assistantResponse += event.content;
6071
- currentText += event.content;
6072
- }
6073
- if (event.type === "tool:started") {
6074
- if (currentText.length > 0) {
6075
- sections.push({ type: "text", content: currentText });
6076
- currentText = "";
6077
- }
6078
- const toolText = `- start \`${event.tool}\``;
6079
- toolTimeline.push(toolText);
6080
- currentTools.push(toolText);
6081
- }
6082
- if (event.type === "tool:completed") {
6083
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
6084
- toolTimeline.push(toolText);
6085
- currentTools.push(toolText);
6086
- }
6087
- if (event.type === "tool:error") {
6088
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
6089
- toolTimeline.push(toolText);
6090
- currentTools.push(toolText);
6091
- }
6092
- if (event.type === "run:completed") {
6093
- steps = event.result.steps;
6094
- contextTokens = event.result.contextTokens ?? 0;
6095
- contextWindow = event.result.contextWindow ?? 0;
6096
- if (event.result.continuationMessages) {
6097
- harnessMessages = event.result.continuationMessages;
6098
- }
6099
- if (!assistantResponse && event.result.response) {
6100
- assistantResponse = event.result.response;
6101
- }
6102
- }
6103
- }
6104
- if (currentTools.length > 0) {
6105
- sections.push({ type: "tools", content: currentTools });
6106
- }
6107
- if (currentText.length > 0) {
6108
- sections.push({ type: "text", content: currentText });
6109
- }
6110
- const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
6111
- const assistantMetadata =
6112
- toolTimeline.length > 0 || sections.length > 0
6113
- ? ({
6114
- toolActivity: [...toolTimeline],
6115
- sections: sections.length > 0 ? sections : undefined,
6116
- } as Message["metadata"])
6117
- : undefined;
6390
+ onEvent,
6391
+ });
6392
+ flushTurnDraft(execution.draft);
6393
+ const hasContent = execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0;
6394
+ const assistantMetadata = buildAssistantMetadata(execution.draft);
6118
6395
  return {
6119
- response: assistantResponse,
6120
- steps,
6396
+ response: execution.draft.assistantResponse,
6397
+ steps: execution.runSteps,
6121
6398
  assistantMetadata,
6122
6399
  hasContent,
6123
- contextTokens,
6124
- contextWindow,
6125
- harnessMessages,
6400
+ contextTokens: execution.runContextTokens,
6401
+ contextWindow: execution.runContextWindow,
6402
+ harnessMessages: execution.runHarnessMessages,
6126
6403
  toolResultArchive: harnessRef.getToolResultArchive(conversationId),
6127
6404
  };
6128
6405
  };
@@ -6194,9 +6471,11 @@ export const startDevServer = async (
6194
6471
  if (!conversation) continue;
6195
6472
 
6196
6473
  const task = `[Scheduled: ${jobName}]\n${config.task}`;
6197
- const historyMessages = conversation._harnessMessages?.length
6198
- ? [...conversation._harnessMessages]
6199
- : [...conversation.messages];
6474
+ const historySelection = resolveRunRequest(conversation, {
6475
+ conversationId: conversation.conversationId,
6476
+ messages: conversation.messages,
6477
+ });
6478
+ const historyMessages = [...historySelection.messages];
6200
6479
  const convId = conversation.conversationId;
6201
6480
 
6202
6481
  activeRuns?.set(convId, {
@@ -6217,6 +6496,8 @@ export const startDevServer = async (
6217
6496
  freshConv.messages = buildCronMessages(task, historyMessages, result);
6218
6497
  if (result.harnessMessages) {
6219
6498
  freshConv._harnessMessages = result.harnessMessages;
6499
+ } else if (historySelection.shouldRebuildCanonical) {
6500
+ freshConv._harnessMessages = freshConv.messages;
6220
6501
  }
6221
6502
  if (result.toolResultArchive) {
6222
6503
  freshConv._toolResultArchive = result.toolResultArchive;