@poncho-ai/cli 0.32.4 → 0.32.6

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,28 @@ 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
+ };
3429
+ },
3430
+ async resetConversation(conversationId) {
3431
+ const existing = await conversationStore.get(conversationId);
3432
+ if (!existing) return;
3433
+ existing.messages = [];
3434
+ existing._harnessMessages = undefined;
3435
+ existing._continuationMessages = undefined;
3436
+ existing._toolResultArchive = undefined;
3437
+ existing.pendingApprovals = undefined;
3438
+ existing.runStatus = undefined;
3439
+ existing.updatedAt = Date.now();
3440
+ await conversationStore.update(existing);
3441
+ console.log(`[messaging-runner] conversation reset: ${conversationId}`);
3125
3442
  },
3126
3443
  };
3127
3444
 
@@ -3271,6 +3588,7 @@ export const createRequestHandler = async (options?: {
3271
3588
  ): AsyncGenerator<AgentEvent> {
3272
3589
  const conversation = await conversationStore.get(conversationId);
3273
3590
  if (!conversation) return;
3591
+ if (Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0) return;
3274
3592
  if (!conversation._continuationMessages?.length) return;
3275
3593
  if (conversation.runStatus === "running") return;
3276
3594
 
@@ -3438,7 +3756,11 @@ export const createRequestHandler = async (options?: {
3438
3756
  freshConv._continuationCount = undefined;
3439
3757
  }
3440
3758
 
3441
- if (nextHarnessMessages) freshConv._harnessMessages = nextHarnessMessages;
3759
+ if (nextHarnessMessages) {
3760
+ freshConv._harnessMessages = nextHarnessMessages;
3761
+ } else {
3762
+ freshConv._harnessMessages = freshConv.messages;
3763
+ }
3442
3764
  freshConv._toolResultArchive = harness.getToolResultArchive(conversationId);
3443
3765
  freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
3444
3766
  freshConv.pendingApprovals = [];
@@ -3575,6 +3897,8 @@ export const createRequestHandler = async (options?: {
3575
3897
  }
3576
3898
  if (runResult?.continuationMessages) {
3577
3899
  conv._harnessMessages = runResult.continuationMessages;
3900
+ } else {
3901
+ conv._harnessMessages = conv.messages;
3578
3902
  }
3579
3903
  conv._toolResultArchive = childHarness.getToolResultArchive(conversationId);
3580
3904
  conv.lastActivityAt = Date.now();
@@ -3774,23 +4098,15 @@ export const createRequestHandler = async (options?: {
3774
4098
  }
3775
4099
 
3776
4100
  // 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
- }
4101
+ const found = await findPendingApproval(approvalId, "local-owner");
4102
+ let foundConversation = found?.conversation;
4103
+ let foundApproval = found?.approval;
3789
4104
 
3790
4105
  if (!foundConversation || !foundApproval) {
3791
4106
  console.warn("[telegram-approval] approval not found:", approvalId);
3792
4107
  return;
3793
4108
  }
4109
+ foundApproval = normalizeApprovalCheckpoint(foundApproval, foundConversation.messages);
3794
4110
 
3795
4111
  await adapter.updateApprovalMessage(approvalId, approved ? "approved" : "denied", foundApproval.tool);
3796
4112
 
@@ -3802,7 +4118,9 @@ export const createRequestHandler = async (options?: {
3802
4118
  : { type: "tool:approval:denied", approvalId },
3803
4119
  );
3804
4120
 
3805
- const allApprovals = foundConversation.pendingApprovals ?? [];
4121
+ const allApprovals = (foundConversation.pendingApprovals ?? []).map((approval) =>
4122
+ normalizeApprovalCheckpoint(approval, foundConversation!.messages),
4123
+ );
3806
4124
  const allDecided = allApprovals.length > 0 && allApprovals.every(a => a.decision != null);
3807
4125
 
3808
4126
  if (!allDecided) {
@@ -4220,6 +4538,9 @@ export const createRequestHandler = async (options?: {
4220
4538
  pathname !== "/api/auth/login" &&
4221
4539
  request.headers["x-csrf-token"] !== session?.csrfToken
4222
4540
  ) {
4541
+ console.warn(
4542
+ `[poncho][csrf] blocked request method=${request.method} path="${pathname}" session=${session.sessionId}`,
4543
+ );
4223
4544
  writeJson(response, 403, {
4224
4545
  code: "CSRF_ERROR",
4225
4546
  message: "Invalid CSRF token",
@@ -4443,8 +4764,11 @@ export const createRequestHandler = async (options?: {
4443
4764
  const approvalMatch = pathname.match(/^\/api\/approvals\/([^/]+)$/);
4444
4765
  if (approvalMatch && request.method === "POST") {
4445
4766
  const approvalId = decodeURIComponent(approvalMatch[1] ?? "");
4446
- const body = (await readRequestBody(request)) as { approved?: boolean };
4767
+ const body = (await readRequestBody(request)) as { approved?: boolean; conversationId?: string };
4447
4768
  const approved = body.approved === true;
4769
+ const hintedConversationId = typeof body.conversationId === "string" && body.conversationId.trim().length > 0
4770
+ ? body.conversationId.trim()
4771
+ : undefined;
4448
4772
 
4449
4773
  // Check if this is a pending subagent approval (handled inline by runSubagent)
4450
4774
  const pendingSubagent = pendingSubagentApprovals.get(approvalId);
@@ -4480,18 +4804,23 @@ export const createRequestHandler = async (options?: {
4480
4804
  }
4481
4805
 
4482
4806
  // Find the approval in the conversation store (checkpoint-based flow)
4483
- const conversations = await conversationStore.list(ownerId);
4484
4807
  let foundConversation: Conversation | undefined;
4485
4808
  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;
4809
+ if (hintedConversationId) {
4810
+ const hintedConversation = await conversationStore.get(hintedConversationId);
4811
+ if (hintedConversation && hintedConversation.ownerId === ownerId && Array.isArray(hintedConversation.pendingApprovals)) {
4812
+ const hintedMatch = hintedConversation.pendingApprovals.find((approval) => approval.approvalId === approvalId);
4813
+ if (hintedMatch) {
4814
+ foundConversation = hintedConversation;
4815
+ foundApproval = hintedMatch;
4816
+ }
4493
4817
  }
4494
4818
  }
4819
+ if (!foundConversation || !foundApproval) {
4820
+ const found = await findPendingApproval(approvalId, ownerId);
4821
+ foundConversation = found?.conversation;
4822
+ foundApproval = found?.approval;
4823
+ }
4495
4824
 
4496
4825
  if (!foundConversation || !foundApproval) {
4497
4826
  writeJson(response, 404, {
@@ -4502,28 +4831,23 @@ export const createRequestHandler = async (options?: {
4502
4831
  }
4503
4832
 
4504
4833
  const conversationId = foundConversation.conversationId;
4834
+ foundApproval = normalizeApprovalCheckpoint(foundApproval, foundConversation.messages);
4505
4835
 
4506
4836
  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)",
4837
+ writeJson(response, 409, {
4838
+ code: "APPROVAL_NOT_READY",
4839
+ message: "Approval checkpoint is not ready yet. Please retry shortly.",
4513
4840
  });
4514
4841
  return;
4515
4842
  }
4516
4843
 
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";
4844
+ const approvalDecision = approved ? "approved" : "denied";
4845
+ foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? []).map((approval) =>
4846
+ approval.approvalId === approvalId
4847
+ ? { ...normalizeApprovalCheckpoint(approval, foundConversation!.messages), decision: approvalDecision }
4848
+ : normalizeApprovalCheckpoint(approval, foundConversation!.messages),
4849
+ );
4850
+ await conversationStore.update(foundConversation);
4527
4851
 
4528
4852
  broadcastEvent(conversationId,
4529
4853
  approved
@@ -4531,25 +4855,18 @@ export const createRequestHandler = async (options?: {
4531
4855
  : { type: "tool:approval:denied", approvalId },
4532
4856
  );
4533
4857
 
4534
- const allApprovals = foundConversation.pendingApprovals ?? [];
4858
+ const refreshedConversation = await conversationStore.get(conversationId);
4859
+ const allApprovals = (refreshedConversation?.pendingApprovals ?? []).map((approval) =>
4860
+ normalizeApprovalCheckpoint(approval, refreshedConversation!.messages),
4861
+ );
4535
4862
  const allDecided = allApprovals.length > 0 &&
4536
- allApprovals.every(a => batchDecisions!.has(a.approvalId));
4863
+ allApprovals.every(a => a.decision != null);
4537
4864
 
4538
4865
  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
4866
  writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
4544
4867
  return;
4545
4868
  }
4546
4869
 
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
4870
  approvalDecisionTracker.delete(conversationId);
4554
4871
 
4555
4872
  foundConversation.pendingApprovals = [];
@@ -4787,6 +5104,7 @@ export const createRequestHandler = async (options?: {
4787
5104
  runId: a.runId,
4788
5105
  tool: a.tool,
4789
5106
  input: a.input,
5107
+ decision: a.decision,
4790
5108
  }))
4791
5109
  : [];
4792
5110
  // Collect pending approvals from subagent conversations (in-memory map, no disk I/O)
@@ -4805,26 +5123,17 @@ export const createRequestHandler = async (options?: {
4805
5123
  }
4806
5124
  const activeStream = conversationEventStreams.get(conversationId);
4807
5125
  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
- }
5126
+ const hasRunningSubagents = !conversation.parentConversationId
5127
+ ? await hasRunningSubagentsForParent(conversationId, conversation.ownerId)
5128
+ : false;
4823
5129
  const hasPendingCallbackResults = Array.isArray(conversation.pendingSubagentResults)
4824
5130
  && conversation.pendingSubagentResults.length > 0;
5131
+ const hasPendingApprovals = Array.isArray(conversation.pendingApprovals)
5132
+ && conversation.pendingApprovals.length > 0;
4825
5133
  const needsContinuation = !hasActiveRun
4826
5134
  && Array.isArray(conversation._continuationMessages)
4827
- && conversation._continuationMessages.length > 0;
5135
+ && conversation._continuationMessages.length > 0
5136
+ && !hasPendingApprovals;
4828
5137
  writeJson(response, 200, {
4829
5138
  conversation: {
4830
5139
  ...conversation,
@@ -5036,11 +5345,9 @@ export const createRequestHandler = async (options?: {
5036
5345
  eventCount++;
5037
5346
  let sseEvent: AgentEvent = event;
5038
5347
  if (sseEvent.type === "run:completed") {
5039
- const hasRunningSubagents = Array.from(activeSubagentRuns.values()).some(
5040
- r => r.parentConversationId === conversationId,
5041
- );
5348
+ const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
5042
5349
  const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
5043
- sseEvent = hasRunningSubagents ? { ...stripped, pendingSubagents: true } : stripped;
5350
+ sseEvent = hasPendingSubagents ? { ...stripped, pendingSubagents: true } : stripped;
5044
5351
  }
5045
5352
  try {
5046
5353
  response.write(formatSseEvent(sseEvent));
@@ -5067,7 +5374,10 @@ export const createRequestHandler = async (options?: {
5067
5374
  // fire a delayed safety net in case the client disconnects before
5068
5375
  // POSTing the next /continue.
5069
5376
  const freshConv = await conversationStore.get(conversationId);
5070
- if (freshConv?._continuationMessages?.length) {
5377
+ if (
5378
+ freshConv?._continuationMessages?.length &&
5379
+ (!Array.isArray(freshConv.pendingApprovals) || freshConv.pendingApprovals.length === 0)
5380
+ ) {
5071
5381
  doWaitUntil(
5072
5382
  new Promise(r => setTimeout(r, 3000)).then(() =>
5073
5383
  selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
@@ -5159,11 +5469,17 @@ export const createRequestHandler = async (options?: {
5159
5469
  Connection: "keep-alive",
5160
5470
  "X-Accel-Buffering": "no",
5161
5471
  });
5162
- const harnessMessages = conversation._harnessMessages?.length
5163
- ? [...conversation._harnessMessages]
5164
- : [...conversation.messages];
5472
+ const canonicalHistory = resolveRunRequest(conversation, {
5473
+ conversationId,
5474
+ messages: conversation.messages,
5475
+ });
5476
+ const shouldRebuildCanonical = canonicalHistory.shouldRebuildCanonical;
5477
+ const harnessMessages = [...canonicalHistory.messages];
5165
5478
  const historyMessages = [...conversation.messages];
5166
5479
  const preRunMessages = [...conversation.messages];
5480
+ console.info(
5481
+ `[poncho] conversation="${conversationId}" history_source=${canonicalHistory.source}`,
5482
+ );
5167
5483
  let latestRunId = conversation.runtimeRunId ?? "";
5168
5484
  let assistantResponse = "";
5169
5485
  const toolTimeline: string[] = [];
@@ -5379,6 +5695,26 @@ export const createRequestHandler = async (options?: {
5379
5695
  const toolText = `- approval required \`${event.tool}\``;
5380
5696
  toolTimeline.push(toolText);
5381
5697
  currentTools.push(toolText);
5698
+ const existingApprovals = Array.isArray(conversation.pendingApprovals)
5699
+ ? conversation.pendingApprovals
5700
+ : [];
5701
+ if (!existingApprovals.some((approval) => approval.approvalId === event.approvalId)) {
5702
+ conversation.pendingApprovals = [
5703
+ ...existingApprovals,
5704
+ {
5705
+ approvalId: event.approvalId,
5706
+ runId: latestRunId || conversation.runtimeRunId || "",
5707
+ tool: event.tool,
5708
+ toolCallId: undefined,
5709
+ input: (event.input ?? {}) as Record<string, unknown>,
5710
+ checkpointMessages: undefined,
5711
+ baseMessageCount: historyMessages.length,
5712
+ pendingToolCalls: [],
5713
+ },
5714
+ ];
5715
+ conversation.updatedAt = Date.now();
5716
+ await conversationStore.update(conversation);
5717
+ }
5382
5718
  await persistDraftAssistantTurn();
5383
5719
  }
5384
5720
  if (event.type === "tool:approval:checkpoint") {
@@ -5402,16 +5738,13 @@ export const createRequestHandler = async (options?: {
5402
5738
  }]
5403
5739
  : []),
5404
5740
  ];
5405
- conversation.pendingApprovals = event.approvals.map(a => ({
5406
- approvalId: a.approvalId,
5741
+ conversation.pendingApprovals = buildApprovalCheckpoints({
5742
+ approvals: event.approvals,
5407
5743
  runId: latestRunId,
5408
- tool: a.tool,
5409
- toolCallId: a.toolCallId,
5410
- input: a.input,
5411
5744
  checkpointMessages: event.checkpointMessages,
5412
5745
  baseMessageCount: historyMessages.length,
5413
5746
  pendingToolCalls: event.pendingToolCalls,
5414
- }));
5747
+ });
5415
5748
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5416
5749
  conversation.updatedAt = Date.now();
5417
5750
  await conversationStore.update(conversation);
@@ -5447,7 +5780,9 @@ export const createRequestHandler = async (options?: {
5447
5780
  conversation._harnessMessages = runContinuationMessages;
5448
5781
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5449
5782
  conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
5450
- conversation.pendingApprovals = [];
5783
+ if (!checkpointedRun) {
5784
+ conversation.pendingApprovals = [];
5785
+ }
5451
5786
  if (runContextTokens > 0) conversation.contextTokens = runContextTokens;
5452
5787
  if (runContextWindow > 0) conversation.contextWindow = runContextWindow;
5453
5788
  conversation.updatedAt = Date.now();
@@ -5455,11 +5790,13 @@ export const createRequestHandler = async (options?: {
5455
5790
 
5456
5791
  // Delayed safety net: if the client doesn't POST to /continue
5457
5792
  // 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
- );
5793
+ if (!checkpointedRun) {
5794
+ doWaitUntil(
5795
+ new Promise(r => setTimeout(r, 3000)).then(() =>
5796
+ selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
5797
+ ),
5798
+ );
5799
+ }
5463
5800
  }
5464
5801
  }
5465
5802
  await telemetry.emit(event);
@@ -5467,11 +5804,9 @@ export const createRequestHandler = async (options?: {
5467
5804
  ? { ...event, compactedMessages: undefined }
5468
5805
  : event;
5469
5806
  if (sseEvent.type === "run:completed") {
5470
- const hasRunningSubagents = Array.from(activeSubagentRuns.values()).some(
5471
- r => r.parentConversationId === conversationId,
5472
- );
5807
+ const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
5473
5808
  const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
5474
- if (hasRunningSubagents) {
5809
+ if (hasPendingSubagents) {
5475
5810
  sseEvent = { ...stripped, pendingSubagents: true };
5476
5811
  } else {
5477
5812
  sseEvent = stripped;
@@ -5519,6 +5854,10 @@ export const createRequestHandler = async (options?: {
5519
5854
  conversation._continuationMessages = undefined;
5520
5855
  if (runHarnessMessages) {
5521
5856
  conversation._harnessMessages = runHarnessMessages;
5857
+ } else if (shouldRebuildCanonical) {
5858
+ conversation._harnessMessages = conversation.messages;
5859
+ } else {
5860
+ conversation._harnessMessages = conversation.messages;
5522
5861
  }
5523
5862
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5524
5863
  conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
@@ -5556,7 +5895,9 @@ export const createRequestHandler = async (options?: {
5556
5895
  conversation.updatedAt = Date.now();
5557
5896
  await conversationStore.update(conversation);
5558
5897
  }
5559
- await clearPendingApprovalsForConversation(conversationId);
5898
+ if (!checkpointedRun) {
5899
+ await clearPendingApprovalsForConversation(conversationId);
5900
+ }
5560
5901
  return;
5561
5902
  }
5562
5903
  try {
@@ -5604,20 +5945,32 @@ export const createRequestHandler = async (options?: {
5604
5945
  if (active && active.abortController === abortController) {
5605
5946
  activeConversationRuns.delete(conversationId);
5606
5947
  }
5607
- finishConversationStream(conversationId);
5608
5948
  if (latestRunId) {
5609
5949
  runOwners.delete(latestRunId);
5610
5950
  runConversations.delete(latestRunId);
5611
5951
  }
5952
+
5953
+ // Determine if subagent work is pending before deciding to close the
5954
+ // event stream. When a callback is about to run, the stream stays open
5955
+ // so clients that subscribe to /events receive callback-run events in
5956
+ // real-time — the same delivery path used for every other run.
5957
+ const hadDeferred = pendingCallbackNeeded.delete(conversationId);
5958
+ const freshConv = await conversationStore.get(conversationId);
5959
+ const needsCallback = hadDeferred || !!freshConv?.pendingSubagentResults?.length;
5960
+ const hasRunningChildren = Array.from(activeSubagentRuns.values()).some(
5961
+ (run) => run.parentConversationId === conversationId,
5962
+ );
5963
+
5964
+ if (!needsCallback && !hasRunningChildren) {
5965
+ finishConversationStream(conversationId);
5966
+ }
5967
+
5612
5968
  try {
5613
5969
  response.end();
5614
5970
  } catch {
5615
5971
  // Already closed.
5616
5972
  }
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) {
5973
+ if (needsCallback) {
5621
5974
  processSubagentCallback(conversationId, true).catch(err =>
5622
5975
  console.error(`[poncho][subagent-callback] Post-run callback failed:`, err instanceof Error ? err.message : err),
5623
5976
  );
@@ -5682,46 +6035,42 @@ export const createRequestHandler = async (options?: {
5682
6035
  if (!conv) continue;
5683
6036
 
5684
6037
  const task = `[Scheduled: ${jobName}]\n${cronJob.task}`;
5685
- const historyMessages = conv._harnessMessages?.length
5686
- ? [...conv._harnessMessages]
5687
- : [...conv.messages];
6038
+ const historySelection = resolveRunRequest(conv, {
6039
+ conversationId: conv.conversationId,
6040
+ messages: conv.messages,
6041
+ });
6042
+ const historyMessages = [...historySelection.messages];
5688
6043
  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
- }
6044
+ const execution = await executeConversationTurn({
6045
+ harness,
6046
+ runInput: {
6047
+ task,
6048
+ conversationId: conv.conversationId,
6049
+ parameters: withToolResultArchiveParam(
6050
+ { __activeConversationId: conv.conversationId },
6051
+ conv,
6052
+ ),
6053
+ messages: historyMessages,
6054
+ },
6055
+ onEvent: async (event) => {
6056
+ await telemetry.emit(event);
6057
+ },
6058
+ });
6059
+ const assistantResponse = execution.draft.assistantResponse;
5715
6060
 
5716
6061
  conv.messages = [
5717
6062
  ...historyMessages,
5718
6063
  { role: "user" as const, content: task },
5719
6064
  ...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
5720
6065
  ];
5721
- if (cronHarnessMessages) {
5722
- conv._harnessMessages = cronHarnessMessages;
6066
+ if (execution.runHarnessMessages) {
6067
+ conv._harnessMessages = execution.runHarnessMessages;
6068
+ } else if (historySelection.shouldRebuildCanonical) {
6069
+ conv._harnessMessages = conv.messages;
5723
6070
  }
5724
6071
  conv._toolResultArchive = harness.getToolResultArchive(conv.conversationId);
6072
+ if (execution.runContextTokens > 0) conv.contextTokens = execution.runContextTokens;
6073
+ if (execution.runContextWindow > 0) conv.contextWindow = execution.runContextWindow;
5725
6074
  conv.updatedAt = Date.now();
5726
6075
  await conversationStore.update(conv);
5727
6076
 
@@ -5738,7 +6087,7 @@ export const createRequestHandler = async (options?: {
5738
6087
  console.error(`[cron] ${jobName}: send to ${chatId} failed:`, sendError instanceof Error ? sendError.message : sendError);
5739
6088
  }
5740
6089
  }
5741
- chatResults.push({ chatId, status: "completed", steps });
6090
+ chatResults.push({ chatId, status: "completed", steps: execution.runSteps });
5742
6091
  } catch (runError) {
5743
6092
  chatResults.push({ chatId, status: "error" });
5744
6093
  console.error(`[cron] ${jobName}: run for chat ${chatId} failed:`, runError instanceof Error ? runError.message : runError);
@@ -6038,91 +6387,32 @@ export const startDevServer = async (
6038
6387
  conversationId: string,
6039
6388
  historyMessages: Message[],
6040
6389
  toolResultArchive?: Conversation["_toolResultArchive"],
6041
- onEvent?: (event: AgentEvent) => void,
6390
+ onEvent?: (event: AgentEvent) => void | Promise<void>,
6042
6391
  ): 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 ?? {},
6392
+ const execution = await executeConversationTurn({
6393
+ harness: harnessRef,
6394
+ runInput: {
6395
+ task,
6396
+ conversationId,
6397
+ parameters: {
6398
+ __activeConversationId: conversationId,
6399
+ [TOOL_RESULT_ARCHIVE_PARAM]: toolResultArchive ?? {},
6400
+ },
6401
+ messages: historyMessages,
6058
6402
  },
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;
6403
+ onEvent,
6404
+ });
6405
+ flushTurnDraft(execution.draft);
6406
+ const hasContent = execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0;
6407
+ const assistantMetadata = buildAssistantMetadata(execution.draft);
6118
6408
  return {
6119
- response: assistantResponse,
6120
- steps,
6409
+ response: execution.draft.assistantResponse,
6410
+ steps: execution.runSteps,
6121
6411
  assistantMetadata,
6122
6412
  hasContent,
6123
- contextTokens,
6124
- contextWindow,
6125
- harnessMessages,
6413
+ contextTokens: execution.runContextTokens,
6414
+ contextWindow: execution.runContextWindow,
6415
+ harnessMessages: execution.runHarnessMessages,
6126
6416
  toolResultArchive: harnessRef.getToolResultArchive(conversationId),
6127
6417
  };
6128
6418
  };
@@ -6194,9 +6484,11 @@ export const startDevServer = async (
6194
6484
  if (!conversation) continue;
6195
6485
 
6196
6486
  const task = `[Scheduled: ${jobName}]\n${config.task}`;
6197
- const historyMessages = conversation._harnessMessages?.length
6198
- ? [...conversation._harnessMessages]
6199
- : [...conversation.messages];
6487
+ const historySelection = resolveRunRequest(conversation, {
6488
+ conversationId: conversation.conversationId,
6489
+ messages: conversation.messages,
6490
+ });
6491
+ const historyMessages = [...historySelection.messages];
6200
6492
  const convId = conversation.conversationId;
6201
6493
 
6202
6494
  activeRuns?.set(convId, {
@@ -6217,6 +6509,8 @@ export const startDevServer = async (
6217
6509
  freshConv.messages = buildCronMessages(task, historyMessages, result);
6218
6510
  if (result.harnessMessages) {
6219
6511
  freshConv._harnessMessages = result.harnessMessages;
6512
+ } else if (historySelection.shouldRebuildCanonical) {
6513
+ freshConv._harnessMessages = freshConv.messages;
6220
6514
  }
6221
6515
  if (result.toolResultArchive) {
6222
6516
  freshConv._toolResultArchive = result.toolResultArchive;