@poncho-ai/cli 0.32.3 → 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 {
@@ -292,6 +303,325 @@ const parseParams = (values: string[]): Record<string, string> => {
292
303
  return params;
293
304
  };
294
305
 
306
+ const normalizeMessageForClient = (message: Message): Message => {
307
+ if (message.role !== "assistant" || typeof message.content !== "string") {
308
+ return message;
309
+ }
310
+ try {
311
+ const parsed = JSON.parse(message.content) as Record<string, unknown>;
312
+ const text = typeof parsed.text === "string" ? parsed.text : undefined;
313
+ const toolCalls = Array.isArray(parsed.tool_calls) ? parsed.tool_calls : undefined;
314
+ if (typeof text === "string" && toolCalls) {
315
+ return {
316
+ ...message,
317
+ content: text,
318
+ };
319
+ }
320
+ } catch {
321
+ // Keep original assistant content when it's plain text or non-JSON.
322
+ }
323
+ return message;
324
+ };
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
+
295
625
  const AGENT_TEMPLATE = (
296
626
  name: string,
297
627
  id: string,
@@ -1621,6 +1951,77 @@ export const createRequestHandler = async (options?: {
1621
1951
  // separate copy, causing last-writer-wins when decisions overlap).
1622
1952
  const approvalDecisionTracker = new Map<string, Map<string, boolean>>();
1623
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
+
1624
2025
  const getSubagentDepth = async (conversationId: string): Promise<number> => {
1625
2026
  let depth = 0;
1626
2027
  let current = await conversationStore.get(conversationId);
@@ -1672,7 +2073,9 @@ export const createRequestHandler = async (options?: {
1672
2073
  const conv = await conversationStore.get(subagentId);
1673
2074
  if (!conv || !conv.parentConversationId) return;
1674
2075
 
1675
- const allApprovals = conv.pendingApprovals ?? [];
2076
+ const allApprovals = (conv.pendingApprovals ?? []).map((approval) =>
2077
+ normalizeApprovalCheckpoint(approval, conv.messages),
2078
+ );
1676
2079
  if (allApprovals.length === 0) return;
1677
2080
  const allDecided = allApprovals.every(a => a.decision != null);
1678
2081
  if (!allDecided) return;
@@ -1764,9 +2167,11 @@ export const createRequestHandler = async (options?: {
1764
2167
  conversation.lastActivityAt = Date.now();
1765
2168
  await conversationStore.update(conversation);
1766
2169
 
1767
- const harnessMessages = conversation._harnessMessages?.length
1768
- ? [...conversation._harnessMessages]
1769
- : [...conversation.messages];
2170
+ const runOutcome = resolveRunRequest(conversation, {
2171
+ conversationId: childConversationId,
2172
+ messages: conversation.messages,
2173
+ });
2174
+ const harnessMessages = [...runOutcome.messages];
1770
2175
 
1771
2176
  for await (const event of childHarness.runWithTelemetry({
1772
2177
  task,
@@ -1829,16 +2234,13 @@ export const createRequestHandler = async (options?: {
1829
2234
  if (event.type === "tool:approval:checkpoint") {
1830
2235
  const cpConv = await conversationStore.get(childConversationId);
1831
2236
  if (cpConv) {
1832
- const allCpData: NonNullable<Conversation["pendingApprovals"]> = event.approvals.map(a => ({
1833
- approvalId: a.approvalId,
2237
+ const allCpData = buildApprovalCheckpoints({
2238
+ approvals: event.approvals,
1834
2239
  runId: latestRunId,
1835
- tool: a.tool,
1836
- toolCallId: a.toolCallId,
1837
- input: a.input,
1838
2240
  checkpointMessages: [...harnessMessages, ...event.checkpointMessages],
1839
2241
  baseMessageCount: 0,
1840
2242
  pendingToolCalls: event.pendingToolCalls,
1841
- }));
2243
+ });
1842
2244
  cpConv.pendingApprovals = allCpData;
1843
2245
  cpConv.updatedAt = Date.now();
1844
2246
  await conversationStore.update(cpConv);
@@ -1855,7 +2257,7 @@ export const createRequestHandler = async (options?: {
1855
2257
  }
1856
2258
  });
1857
2259
 
1858
- const checkpointRef = allCpData[0]!;
2260
+ const checkpointRef = normalizeApprovalCheckpoint(allCpData[0]!, [...harnessMessages]);
1859
2261
  const toolContext = {
1860
2262
  runId: checkpointRef.runId,
1861
2263
  agentId: identity.id,
@@ -1988,6 +2390,8 @@ export const createRequestHandler = async (options?: {
1988
2390
  }
1989
2391
  if (runResult?.continuationMessages) {
1990
2392
  conv._harnessMessages = runResult.continuationMessages;
2393
+ } else if (runOutcome.shouldRebuildCanonical) {
2394
+ conv._harnessMessages = conv.messages;
1991
2395
  }
1992
2396
  conv._toolResultArchive = childHarness.getToolResultArchive(childConversationId);
1993
2397
  conv.lastActivityAt = Date.now();
@@ -2156,6 +2560,9 @@ export const createRequestHandler = async (options?: {
2156
2560
  metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, timestamp: pr.timestamp } as Message["metadata"],
2157
2561
  });
2158
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];
2159
2566
  conversation.updatedAt = Date.now();
2160
2567
  await conversationStore.update(conversation);
2161
2568
 
@@ -2190,120 +2597,74 @@ export const createRequestHandler = async (options?: {
2190
2597
  });
2191
2598
  }
2192
2599
 
2193
- const historyMessages = isContinuationResume && conversation._continuationMessages?.length
2194
- ? [...conversation._continuationMessages]
2195
- : conversation._harnessMessages?.length
2196
- ? [...conversation._harnessMessages]
2197
- : [...conversation.messages];
2198
- let assistantResponse = "";
2199
- let latestRunId = "";
2200
- let runContinuation = false;
2201
- let runContinuationMessages: Message[] | undefined;
2202
- let runHarnessMessages: Message[] | undefined;
2203
- let runContextTokens = conversation.contextTokens ?? 0;
2204
- let runContextWindow = conversation.contextWindow ?? 0;
2205
- const toolTimeline: string[] = [];
2206
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
2207
- let currentTools: string[] = [];
2208
- 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;
2209
2610
 
2210
2611
  try {
2211
- for await (const event of harness.runWithTelemetry({
2212
- task: undefined,
2213
- conversationId,
2214
- parameters: withToolResultArchiveParam({
2215
- __activeConversationId: conversationId,
2216
- __ownerId: conversation.ownerId,
2217
- }, conversation),
2218
- messages: historyMessages,
2219
- abortSignal: abortController.signal,
2220
- })) {
2221
- if (event.type === "run:started") {
2222
- latestRunId = event.runId;
2223
- const active = activeConversationRuns.get(conversationId);
2224
- if (active) active.runId = event.runId;
2225
- }
2226
- if (event.type === "model:chunk") {
2227
- if (currentTools.length > 0) {
2228
- sections.push({ type: "tools", content: currentTools });
2229
- currentTools = [];
2230
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
2231
- assistantResponse += " ";
2232
- }
2233
- }
2234
- assistantResponse += event.content;
2235
- currentText += event.content;
2236
- }
2237
- if (event.type === "tool:started") {
2238
- if (currentText.length > 0) {
2239
- sections.push({ type: "text", content: currentText });
2240
- currentText = "";
2241
- }
2242
- const toolText = `- start \`${event.tool}\``;
2243
- toolTimeline.push(toolText);
2244
- currentTools.push(toolText);
2245
- }
2246
- if (event.type === "tool:completed") {
2247
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
2248
- toolTimeline.push(toolText);
2249
- currentTools.push(toolText);
2250
- }
2251
- if (event.type === "tool:error") {
2252
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
2253
- toolTimeline.push(toolText);
2254
- currentTools.push(toolText);
2255
- }
2256
- if (event.type === "run:completed") {
2257
- if (assistantResponse.length === 0 && event.result.response) {
2258
- assistantResponse = event.result.response;
2259
- }
2260
- runContextTokens = event.result.contextTokens ?? runContextTokens;
2261
- runContextWindow = event.result.contextWindow ?? runContextWindow;
2262
- if (event.result.continuationMessages) {
2263
- runHarnessMessages = event.result.continuationMessages;
2264
- }
2265
- if (event.result.continuation) {
2266
- runContinuation = true;
2267
- if (event.result.continuationMessages) {
2268
- runContinuationMessages = event.result.continuationMessages;
2269
- }
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;
2270
2630
  }
2271
- }
2272
- broadcastEvent(conversationId, event);
2273
- }
2274
-
2275
- if (currentTools.length > 0) sections.push({ type: "tools", content: currentTools });
2276
- if (currentText.length > 0) sections.push({ type: "text", content: currentText });
2631
+ broadcastEvent(conversationId, event);
2632
+ },
2633
+ });
2634
+ flushTurnDraft(execution.draft);
2277
2635
 
2278
- 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) {
2279
2638
  const freshConv = await conversationStore.get(conversationId);
2280
2639
  if (freshConv) {
2281
- if (runContinuationMessages) {
2282
- freshConv._continuationMessages = runContinuationMessages;
2640
+ if (callbackNeedsContinuation) {
2641
+ freshConv._continuationMessages = execution.runContinuationMessages;
2283
2642
  } else {
2284
2643
  freshConv._continuationMessages = undefined;
2285
2644
  freshConv.messages.push({
2286
2645
  role: "assistant",
2287
- content: assistantResponse,
2288
- metadata: toolTimeline.length > 0 || sections.length > 0
2289
- ? { toolActivity: toolTimeline, sections: sections.length > 0 ? sections : undefined } as Message["metadata"]
2290
- : undefined,
2646
+ content: execution.draft.assistantResponse,
2647
+ metadata: buildAssistantMetadata(execution.draft),
2291
2648
  });
2292
2649
  }
2293
- if (runHarnessMessages) {
2294
- 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;
2295
2656
  }
2296
2657
  freshConv._toolResultArchive = harness.getToolResultArchive(conversationId);
2297
- freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
2658
+ freshConv.runtimeRunId = execution.latestRunId || freshConv.runtimeRunId;
2298
2659
  freshConv.runningCallbackSince = undefined;
2299
2660
  freshConv.runStatus = "idle";
2300
- if (runContextTokens > 0) freshConv.contextTokens = runContextTokens;
2301
- if (runContextWindow > 0) freshConv.contextWindow = runContextWindow;
2661
+ if (execution.runContextTokens > 0) freshConv.contextTokens = execution.runContextTokens;
2662
+ if (execution.runContextWindow > 0) freshConv.contextWindow = execution.runContextWindow;
2302
2663
  freshConv.updatedAt = Date.now();
2303
2664
  await conversationStore.update(freshConv);
2304
2665
 
2305
2666
  // Proactive messaging notification if conversation has a messaging channel
2306
- if (freshConv.channelMeta && assistantResponse.length > 0) {
2667
+ if (freshConv.channelMeta && execution.draft.assistantResponse.length > 0) {
2307
2668
  const adapter = messagingAdapters.get(freshConv.channelMeta.platform);
2308
2669
  if (adapter) {
2309
2670
  try {
@@ -2312,7 +2673,7 @@ export const createRequestHandler = async (options?: {
2312
2673
  channelId: freshConv.channelMeta.channelId,
2313
2674
  platformThreadId: freshConv.channelMeta.platformThreadId,
2314
2675
  },
2315
- assistantResponse,
2676
+ execution.draft.assistantResponse,
2316
2677
  );
2317
2678
  } catch (sendErr) {
2318
2679
  console.error(`[poncho][subagent-callback] Messaging notify failed:`, sendErr instanceof Error ? sendErr.message : sendErr);
@@ -2323,7 +2684,7 @@ export const createRequestHandler = async (options?: {
2323
2684
  }
2324
2685
 
2325
2686
  // Handle continuation for the callback run itself
2326
- if (runContinuation) {
2687
+ if (execution.runContinuation) {
2327
2688
  if (isServerless) {
2328
2689
  const work = selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(err =>
2329
2690
  console.error(`[poncho][subagent-callback] Continuation self-fetch failed:`, err instanceof Error ? err.message : err),
@@ -2345,7 +2706,6 @@ export const createRequestHandler = async (options?: {
2345
2706
  }
2346
2707
  } finally {
2347
2708
  activeConversationRuns.delete(conversationId);
2348
- finishConversationStream(conversationId);
2349
2709
 
2350
2710
  // Check both the in-memory flag (always reliable) and the store.
2351
2711
  // We drain the flag first so a concurrent triggerParentCallback that
@@ -2354,6 +2714,16 @@ export const createRequestHandler = async (options?: {
2354
2714
  const hadDeferredTrigger = pendingCallbackNeeded.delete(conversationId);
2355
2715
  const freshConv = await conversationStore.get(conversationId);
2356
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
+ }
2357
2727
 
2358
2728
  if (hadDeferredTrigger || hasPendingInStore) {
2359
2729
  // Re-trigger immediately. Skip the runningCallbackSince lock check
@@ -2531,10 +2901,11 @@ export const createRequestHandler = async (options?: {
2531
2901
  let runContextWindow = conversation.contextWindow ?? 0;
2532
2902
  let resumeHarnessMessages: Message[] | undefined;
2533
2903
 
2534
- const baseMessages = checkpoint.baseMessageCount != null
2535
- ? 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)
2536
2907
  : [];
2537
- const fullCheckpointMessages = [...baseMessages, ...checkpoint.checkpointMessages!];
2908
+ const fullCheckpointMessages = [...baseMessages, ...normalizedCheckpoint.checkpointMessages!];
2538
2909
 
2539
2910
  // Build the tool result message that continueFromToolResult will also
2540
2911
  // construct internally. We need it here so that if the resumed run hits
@@ -2625,25 +2996,27 @@ export const createRequestHandler = async (options?: {
2625
2996
  if (event.type === "tool:approval:checkpoint") {
2626
2997
  const conv = await conversationStore.get(conversationId);
2627
2998
  if (conv) {
2628
- conv.pendingApprovals = event.approvals.map(a => ({
2629
- approvalId: a.approvalId,
2999
+ conv.pendingApprovals = buildApprovalCheckpoints({
3000
+ approvals: event.approvals,
2630
3001
  runId: latestRunId,
2631
- tool: a.tool,
2632
- toolCallId: a.toolCallId,
2633
- input: a.input,
2634
3002
  checkpointMessages: [...fullCheckpointWithResults, ...event.checkpointMessages],
2635
3003
  baseMessageCount: 0,
2636
3004
  pendingToolCalls: event.pendingToolCalls,
2637
- }));
3005
+ });
2638
3006
  conv.updatedAt = Date.now();
2639
3007
  await conversationStore.update(conv);
2640
3008
 
2641
3009
  if (conv.channelMeta?.platform === "telegram") {
2642
3010
  const tgAdapter = messagingAdapters.get("telegram") as TelegramAdapter | undefined;
2643
3011
  if (tgAdapter) {
3012
+ const messageThreadId = parseTelegramMessageThreadIdFromPlatformThreadId(
3013
+ conv.channelMeta.platformThreadId,
3014
+ conv.channelMeta.channelId,
3015
+ );
2644
3016
  void tgAdapter.sendApprovalRequest(
2645
3017
  conv.channelMeta.channelId,
2646
3018
  event.approvals.map(a => ({ approvalId: a.approvalId, tool: a.tool, input: a.input })),
3019
+ { message_thread_id: messageThreadId },
2647
3020
  ).catch(() => {});
2648
3021
  }
2649
3022
  }
@@ -2725,6 +3098,8 @@ export const createRequestHandler = async (options?: {
2725
3098
  }
2726
3099
  if (resumeHarnessMessages) {
2727
3100
  conv._harnessMessages = resumeHarnessMessages;
3101
+ } else {
3102
+ conv._harnessMessages = conv.messages;
2728
3103
  }
2729
3104
  conv.runtimeRunId = latestRunId || conv.runtimeRunId;
2730
3105
  conv.pendingApprovals = [];
@@ -2743,7 +3118,6 @@ export const createRequestHandler = async (options?: {
2743
3118
  }
2744
3119
  }
2745
3120
 
2746
- finishConversationStream(conversationId);
2747
3121
  activeConversationRuns.delete(conversationId);
2748
3122
  if (latestRunId) {
2749
3123
  runOwners.delete(latestRunId);
@@ -2754,7 +3128,14 @@ export const createRequestHandler = async (options?: {
2754
3128
  // Check for pending subagent results that arrived during the run
2755
3129
  const hadDeferred = pendingCallbackNeeded.delete(conversationId);
2756
3130
  const postConv = await conversationStore.get(conversationId);
2757
- 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) {
2758
3139
  processSubagentCallback(conversationId, true).catch(err =>
2759
3140
  console.error(`[poncho][subagent-callback] Post-resume callback failed:`, err instanceof Error ? err.message : err),
2760
3141
  );
@@ -2787,7 +3168,7 @@ export const createRequestHandler = async (options?: {
2787
3168
  };
2788
3169
  await conversationStore.update(existing);
2789
3170
  }
2790
- return { messages: existing._harnessMessages?.length ? existing._harnessMessages : existing.messages };
3171
+ return { messages: loadCanonicalHistory(existing).messages };
2791
3172
  }
2792
3173
  const now = Date.now();
2793
3174
  const channelMeta = meta.channelId
@@ -2811,11 +3192,21 @@ export const createRequestHandler = async (options?: {
2811
3192
  return { messages: [] };
2812
3193
  },
2813
3194
  async run(conversationId, input) {
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
+
2814
3201
  const isContinuation = input.task == null;
2815
- console.log("[messaging-runner] starting run for", conversationId, isContinuation ? "(continuation)" : `task: ${input.task!.slice(0, 80)}`);
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
+ );
2816
3207
 
2817
- const historyMessages = [...input.messages];
2818
- const preRunMessages = [...input.messages];
3208
+ const historyMessages = [...canonicalHistory.messages];
3209
+ const preRunMessages = [...canonicalHistory.messages];
2819
3210
  const userContent = input.task;
2820
3211
 
2821
3212
  // Read-modify-write helper: always fetches the latest version from
@@ -2838,11 +3229,7 @@ export const createRequestHandler = async (options?: {
2838
3229
  });
2839
3230
 
2840
3231
  let latestRunId = "";
2841
- let assistantResponse = "";
2842
- const toolTimeline: string[] = [];
2843
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
2844
- let currentTools: string[] = [];
2845
- let currentText = "";
3232
+ const draft = createTurnDraftState();
2846
3233
  let checkpointedRun = false;
2847
3234
  let runContextTokens = 0;
2848
3235
  let runContextWindow = 0;
@@ -2852,23 +3239,18 @@ export const createRequestHandler = async (options?: {
2852
3239
  let runMaxSteps: number | undefined;
2853
3240
 
2854
3241
  const buildMessages = (): Message[] => {
2855
- const draftSections: Array<{ type: "text" | "tools"; content: string | string[] }> = [
2856
- ...sections.map((s) => ({
2857
- type: s.type,
2858
- content: Array.isArray(s.content) ? [...s.content] : s.content,
2859
- })),
2860
- ];
2861
- if (currentTools.length > 0) {
2862
- 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] });
2863
3245
  }
2864
- if (currentText.length > 0) {
2865
- draftSections.push({ type: "text", content: currentText });
3246
+ if (draft.currentText.length > 0) {
3247
+ draftSections.push({ type: "text", content: draft.currentText });
2866
3248
  }
2867
3249
  const userTurn: Message[] = userContent != null
2868
3250
  ? [{ role: "user" as const, content: userContent }]
2869
3251
  : [];
2870
3252
  const hasDraftContent =
2871
- assistantResponse.length > 0 || toolTimeline.length > 0 || draftSections.length > 0;
3253
+ draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draftSections.length > 0;
2872
3254
  if (!hasDraftContent) {
2873
3255
  return [...historyMessages, ...userTurn];
2874
3256
  }
@@ -2877,20 +3259,14 @@ export const createRequestHandler = async (options?: {
2877
3259
  ...userTurn,
2878
3260
  {
2879
3261
  role: "assistant" as const,
2880
- content: assistantResponse,
2881
- metadata:
2882
- toolTimeline.length > 0 || draftSections.length > 0
2883
- ? ({
2884
- toolActivity: [...toolTimeline],
2885
- sections: draftSections.length > 0 ? draftSections : undefined,
2886
- } as Message["metadata"])
2887
- : undefined,
3262
+ content: draft.assistantResponse,
3263
+ metadata: buildAssistantMetadata(draft, draftSections),
2888
3264
  },
2889
3265
  ];
2890
3266
  };
2891
3267
 
2892
3268
  const persistDraftAssistantTurn = async (): Promise<void> => {
2893
- if (assistantResponse.length === 0 && toolTimeline.length === 0) return;
3269
+ if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
2894
3270
  await updateConversation((c) => {
2895
3271
  c.messages = buildMessages();
2896
3272
  });
@@ -2899,75 +3275,53 @@ export const createRequestHandler = async (options?: {
2899
3275
  const runInput = {
2900
3276
  task: input.task,
2901
3277
  conversationId,
2902
- messages: input.messages,
3278
+ messages: historyMessages,
2903
3279
  files: input.files,
2904
- parameters: input.metadata ? {
2905
- __messaging_platform: input.metadata.platform,
2906
- __messaging_sender_id: input.metadata.sender.id,
2907
- __messaging_sender_name: input.metadata.sender.name ?? "",
2908
- __messaging_thread_id: input.metadata.threadId,
2909
- } : undefined,
3280
+ parameters: {
3281
+ ...(input.metadata ? {
3282
+ __messaging_platform: input.metadata.platform,
3283
+ __messaging_sender_id: input.metadata.sender.id,
3284
+ __messaging_sender_name: input.metadata.sender.name ?? "",
3285
+ __messaging_thread_id: input.metadata.threadId,
3286
+ } : {}),
3287
+ __activeConversationId: conversationId,
3288
+ },
2910
3289
  };
2911
3290
 
2912
3291
  try {
2913
- for await (const event of harness.runWithTelemetry(runInput)) {
2914
- if (event.type === "run:started") {
2915
- latestRunId = event.runId;
2916
- runOwners.set(event.runId, "local-owner");
2917
- runConversations.set(event.runId, conversationId);
2918
- }
2919
- if (event.type === "model:chunk") {
2920
- if (currentTools.length > 0) {
2921
- sections.push({ type: "tools", content: currentTools });
2922
- currentTools = [];
2923
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
2924
- assistantResponse += " ";
2925
- }
2926
- }
2927
- assistantResponse += event.content;
2928
- currentText += event.content;
2929
- }
2930
- if (event.type === "tool:started") {
2931
- if (currentText.length > 0) {
2932
- sections.push({ type: "text", content: currentText });
2933
- 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);
2934
3305
  }
2935
- const toolText = `- start \`${event.tool}\``;
2936
- toolTimeline.push(toolText);
2937
- currentTools.push(toolText);
2938
- }
2939
- if (event.type === "tool:completed") {
2940
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
2941
- toolTimeline.push(toolText);
2942
- currentTools.push(toolText);
2943
- }
2944
- if (event.type === "tool:error") {
2945
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
2946
- toolTimeline.push(toolText);
2947
- currentTools.push(toolText);
2948
- }
2949
3306
  if (event.type === "step:completed") {
2950
3307
  await persistDraftAssistantTurn();
2951
3308
  }
2952
3309
  if (event.type === "tool:approval:required") {
2953
3310
  const toolText = `- approval required \`${event.tool}\``;
2954
- toolTimeline.push(toolText);
2955
- currentTools.push(toolText);
3311
+ draft.toolTimeline.push(toolText);
3312
+ draft.currentTools.push(toolText);
2956
3313
  await persistDraftAssistantTurn();
2957
3314
  }
2958
3315
  if (event.type === "tool:approval:checkpoint") {
2959
3316
  await updateConversation((c) => {
2960
3317
  c.messages = buildMessages();
2961
- c.pendingApprovals = event.approvals.map(a => ({
2962
- approvalId: a.approvalId,
3318
+ c.pendingApprovals = buildApprovalCheckpoints({
3319
+ approvals: event.approvals,
2963
3320
  runId: latestRunId,
2964
- tool: a.tool,
2965
- toolCallId: a.toolCallId,
2966
- input: a.input,
2967
3321
  checkpointMessages: event.checkpointMessages,
2968
3322
  baseMessageCount: historyMessages.length,
2969
3323
  pendingToolCalls: event.pendingToolCalls,
2970
- }));
3324
+ });
2971
3325
  });
2972
3326
  checkpointedRun = true;
2973
3327
 
@@ -2981,9 +3335,14 @@ export const createRequestHandler = async (options?: {
2981
3335
  tool: a.tool,
2982
3336
  input: a.input,
2983
3337
  }));
3338
+ const messageThreadId = parseTelegramMessageThreadIdFromPlatformThreadId(
3339
+ conv.channelMeta.platformThreadId,
3340
+ conv.channelMeta.channelId,
3341
+ );
2984
3342
  void tgAdapter.sendApprovalRequest(
2985
3343
  conv.channelMeta.channelId,
2986
3344
  approvals,
3345
+ { message_thread_id: messageThreadId },
2987
3346
  ).catch((err: unknown) => {
2988
3347
  console.error("[messaging-runner] failed to send Telegram approval request:", err instanceof Error ? err.message : err);
2989
3348
  });
@@ -3006,38 +3365,22 @@ export const createRequestHandler = async (options?: {
3006
3365
  });
3007
3366
  }
3008
3367
  }
3009
- if (event.type === "run:completed") {
3010
- if (assistantResponse.length === 0 && event.result.response) {
3011
- assistantResponse = event.result.response;
3012
- }
3013
- runContinuation = event.result.continuation === true;
3014
- if (event.result.continuationMessages) {
3015
- runContinuationMessages = event.result.continuationMessages;
3016
- }
3017
- runSteps = event.result.steps;
3018
- if (typeof event.result.maxSteps === "number") runMaxSteps = event.result.maxSteps;
3019
- runContextTokens = event.result.contextTokens ?? runContextTokens;
3020
- runContextWindow = event.result.contextWindow ?? runContextWindow;
3021
- }
3022
- if (event.type === "run:error") {
3023
- assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
3024
- }
3025
- broadcastEvent(conversationId, event);
3026
- }
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;
3027
3378
  } catch (err) {
3028
3379
  console.error("[messaging-runner] run failed:", err instanceof Error ? err.message : err);
3029
- assistantResponse = assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
3380
+ draft.assistantResponse = draft.assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
3030
3381
  }
3031
3382
 
3032
- // Finalize sections (clear after pushing so buildMessages doesn't re-add)
3033
- if (currentTools.length > 0) {
3034
- sections.push({ type: "tools", content: currentTools });
3035
- currentTools = [];
3036
- }
3037
- if (currentText.length > 0) {
3038
- sections.push({ type: "text", content: currentText });
3039
- currentText = "";
3040
- }
3383
+ flushTurnDraft(draft);
3041
3384
 
3042
3385
  if (!checkpointedRun) {
3043
3386
  await updateConversation((c) => {
@@ -3049,6 +3392,10 @@ export const createRequestHandler = async (options?: {
3049
3392
  }
3050
3393
  if (runContinuationMessages) {
3051
3394
  c._harnessMessages = runContinuationMessages;
3395
+ } else if (shouldRebuildCanonical) {
3396
+ c._harnessMessages = c.messages;
3397
+ } else {
3398
+ c._harnessMessages = c.messages;
3052
3399
  }
3053
3400
  c.runtimeRunId = latestRunId || c.runtimeRunId;
3054
3401
  c.pendingApprovals = [];
@@ -3058,6 +3405,9 @@ export const createRequestHandler = async (options?: {
3058
3405
  });
3059
3406
  } else {
3060
3407
  await updateConversation((c) => {
3408
+ if (shouldRebuildCanonical && !c._harnessMessages?.length) {
3409
+ c._harnessMessages = c.messages;
3410
+ }
3061
3411
  c.runStatus = "idle";
3062
3412
  });
3063
3413
  }
@@ -3067,8 +3417,8 @@ export const createRequestHandler = async (options?: {
3067
3417
  runConversations.delete(latestRunId);
3068
3418
  }
3069
3419
 
3070
- console.log("[messaging-runner] run complete, response length:", assistantResponse.length, runContinuation ? "(continuation)" : "");
3071
- const response = assistantResponse;
3420
+ console.log("[messaging-runner] run complete, response length:", draft.assistantResponse.length, runContinuation ? "(continuation)" : "");
3421
+ const response = draft.assistantResponse;
3072
3422
 
3073
3423
  return {
3074
3424
  response,
@@ -3225,6 +3575,7 @@ export const createRequestHandler = async (options?: {
3225
3575
  ): AsyncGenerator<AgentEvent> {
3226
3576
  const conversation = await conversationStore.get(conversationId);
3227
3577
  if (!conversation) return;
3578
+ if (Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0) return;
3228
3579
  if (!conversation._continuationMessages?.length) return;
3229
3580
  if (conversation.runStatus === "running") return;
3230
3581
 
@@ -3392,7 +3743,11 @@ export const createRequestHandler = async (options?: {
3392
3743
  freshConv._continuationCount = undefined;
3393
3744
  }
3394
3745
 
3395
- if (nextHarnessMessages) freshConv._harnessMessages = nextHarnessMessages;
3746
+ if (nextHarnessMessages) {
3747
+ freshConv._harnessMessages = nextHarnessMessages;
3748
+ } else {
3749
+ freshConv._harnessMessages = freshConv.messages;
3750
+ }
3396
3751
  freshConv._toolResultArchive = harness.getToolResultArchive(conversationId);
3397
3752
  freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
3398
3753
  freshConv.pendingApprovals = [];
@@ -3529,6 +3884,8 @@ export const createRequestHandler = async (options?: {
3529
3884
  }
3530
3885
  if (runResult?.continuationMessages) {
3531
3886
  conv._harnessMessages = runResult.continuationMessages;
3887
+ } else {
3888
+ conv._harnessMessages = conv.messages;
3532
3889
  }
3533
3890
  conv._toolResultArchive = childHarness.getToolResultArchive(conversationId);
3534
3891
  conv.lastActivityAt = Date.now();
@@ -3728,23 +4085,15 @@ export const createRequestHandler = async (options?: {
3728
4085
  }
3729
4086
 
3730
4087
  // Regular (non-subagent) approval
3731
- const conversations = await conversationStore.list("local-owner");
3732
- let foundConversation: Conversation | undefined;
3733
- let foundApproval: NonNullable<Conversation["pendingApprovals"]>[number] | undefined;
3734
- for (const conv of conversations) {
3735
- if (!Array.isArray(conv.pendingApprovals)) continue;
3736
- const match = conv.pendingApprovals.find(a => a.approvalId === approvalId);
3737
- if (match) {
3738
- foundConversation = conv;
3739
- foundApproval = match;
3740
- break;
3741
- }
3742
- }
4088
+ const found = await findPendingApproval(approvalId, "local-owner");
4089
+ let foundConversation = found?.conversation;
4090
+ let foundApproval = found?.approval;
3743
4091
 
3744
4092
  if (!foundConversation || !foundApproval) {
3745
4093
  console.warn("[telegram-approval] approval not found:", approvalId);
3746
4094
  return;
3747
4095
  }
4096
+ foundApproval = normalizeApprovalCheckpoint(foundApproval, foundConversation.messages);
3748
4097
 
3749
4098
  await adapter.updateApprovalMessage(approvalId, approved ? "approved" : "denied", foundApproval.tool);
3750
4099
 
@@ -3756,7 +4105,9 @@ export const createRequestHandler = async (options?: {
3756
4105
  : { type: "tool:approval:denied", approvalId },
3757
4106
  );
3758
4107
 
3759
- const allApprovals = foundConversation.pendingApprovals ?? [];
4108
+ const allApprovals = (foundConversation.pendingApprovals ?? []).map((approval) =>
4109
+ normalizeApprovalCheckpoint(approval, foundConversation!.messages),
4110
+ );
3760
4111
  const allDecided = allApprovals.length > 0 && allApprovals.every(a => a.decision != null);
3761
4112
 
3762
4113
  if (!allDecided) {
@@ -4174,6 +4525,9 @@ export const createRequestHandler = async (options?: {
4174
4525
  pathname !== "/api/auth/login" &&
4175
4526
  request.headers["x-csrf-token"] !== session?.csrfToken
4176
4527
  ) {
4528
+ console.warn(
4529
+ `[poncho][csrf] blocked request method=${request.method} path="${pathname}" session=${session.sessionId}`,
4530
+ );
4177
4531
  writeJson(response, 403, {
4178
4532
  code: "CSRF_ERROR",
4179
4533
  message: "Invalid CSRF token",
@@ -4397,8 +4751,11 @@ export const createRequestHandler = async (options?: {
4397
4751
  const approvalMatch = pathname.match(/^\/api\/approvals\/([^/]+)$/);
4398
4752
  if (approvalMatch && request.method === "POST") {
4399
4753
  const approvalId = decodeURIComponent(approvalMatch[1] ?? "");
4400
- const body = (await readRequestBody(request)) as { approved?: boolean };
4754
+ const body = (await readRequestBody(request)) as { approved?: boolean; conversationId?: string };
4401
4755
  const approved = body.approved === true;
4756
+ const hintedConversationId = typeof body.conversationId === "string" && body.conversationId.trim().length > 0
4757
+ ? body.conversationId.trim()
4758
+ : undefined;
4402
4759
 
4403
4760
  // Check if this is a pending subagent approval (handled inline by runSubagent)
4404
4761
  const pendingSubagent = pendingSubagentApprovals.get(approvalId);
@@ -4434,18 +4791,23 @@ export const createRequestHandler = async (options?: {
4434
4791
  }
4435
4792
 
4436
4793
  // Find the approval in the conversation store (checkpoint-based flow)
4437
- const conversations = await conversationStore.list(ownerId);
4438
4794
  let foundConversation: Conversation | undefined;
4439
4795
  let foundApproval: NonNullable<Conversation["pendingApprovals"]>[number] | undefined;
4440
- for (const conv of conversations) {
4441
- if (!Array.isArray(conv.pendingApprovals)) continue;
4442
- const match = conv.pendingApprovals.find(a => a.approvalId === approvalId);
4443
- if (match) {
4444
- foundConversation = conv;
4445
- foundApproval = match;
4446
- 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
+ }
4447
4804
  }
4448
4805
  }
4806
+ if (!foundConversation || !foundApproval) {
4807
+ const found = await findPendingApproval(approvalId, ownerId);
4808
+ foundConversation = found?.conversation;
4809
+ foundApproval = found?.approval;
4810
+ }
4449
4811
 
4450
4812
  if (!foundConversation || !foundApproval) {
4451
4813
  writeJson(response, 404, {
@@ -4456,28 +4818,23 @@ export const createRequestHandler = async (options?: {
4456
4818
  }
4457
4819
 
4458
4820
  const conversationId = foundConversation.conversationId;
4821
+ foundApproval = normalizeApprovalCheckpoint(foundApproval, foundConversation.messages);
4459
4822
 
4460
4823
  if (!foundApproval.checkpointMessages || !foundApproval.toolCallId) {
4461
- foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? [])
4462
- .filter(a => a.approvalId !== approvalId);
4463
- await conversationStore.update(foundConversation);
4464
- writeJson(response, 404, {
4465
- code: "APPROVAL_NOT_FOUND",
4466
- 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.",
4467
4827
  });
4468
4828
  return;
4469
4829
  }
4470
4830
 
4471
- // Track decision in memory so parallel batch requests see a consistent
4472
- // view (file-store reads return independent copies, causing lost updates).
4473
- let batchDecisions = approvalDecisionTracker.get(conversationId);
4474
- if (!batchDecisions) {
4475
- batchDecisions = new Map();
4476
- approvalDecisionTracker.set(conversationId, batchDecisions);
4477
- }
4478
- batchDecisions.set(approvalId, approved);
4479
-
4480
- 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);
4481
4838
 
4482
4839
  broadcastEvent(conversationId,
4483
4840
  approved
@@ -4485,25 +4842,18 @@ export const createRequestHandler = async (options?: {
4485
4842
  : { type: "tool:approval:denied", approvalId },
4486
4843
  );
4487
4844
 
4488
- const allApprovals = foundConversation.pendingApprovals ?? [];
4845
+ const refreshedConversation = await conversationStore.get(conversationId);
4846
+ const allApprovals = (refreshedConversation?.pendingApprovals ?? []).map((approval) =>
4847
+ normalizeApprovalCheckpoint(approval, refreshedConversation!.messages),
4848
+ );
4489
4849
  const allDecided = allApprovals.length > 0 &&
4490
- allApprovals.every(a => batchDecisions!.has(a.approvalId));
4850
+ allApprovals.every(a => a.decision != null);
4491
4851
 
4492
4852
  if (!allDecided) {
4493
- // Still waiting for more decisions — persist best-effort and respond.
4494
- // The write may be overwritten by a concurrent request, but that's
4495
- // fine: the in-memory tracker is the source of truth for completion.
4496
- await conversationStore.update(foundConversation);
4497
4853
  writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
4498
4854
  return;
4499
4855
  }
4500
4856
 
4501
- // All approvals in the batch are decided — apply tracked decisions,
4502
- // execute approved tools, and resume the run.
4503
- for (const a of allApprovals) {
4504
- const d = batchDecisions.get(a.approvalId);
4505
- if (d != null) a.decision = d ? "approved" : "denied";
4506
- }
4507
4857
  approvalDecisionTracker.delete(conversationId);
4508
4858
 
4509
4859
  foundConversation.pendingApprovals = [];
@@ -4741,6 +5091,7 @@ export const createRequestHandler = async (options?: {
4741
5091
  runId: a.runId,
4742
5092
  tool: a.tool,
4743
5093
  input: a.input,
5094
+ decision: a.decision,
4744
5095
  }))
4745
5096
  : [];
4746
5097
  // Collect pending approvals from subagent conversations (in-memory map, no disk I/O)
@@ -4759,29 +5110,21 @@ export const createRequestHandler = async (options?: {
4759
5110
  }
4760
5111
  const activeStream = conversationEventStreams.get(conversationId);
4761
5112
  const hasActiveRun = (!!activeStream && !activeStream.finished) || conversation.runStatus === "running";
4762
- let hasRunningSubagents = Array.from(activeSubagentRuns.values()).some(
4763
- r => r.parentConversationId === conversationId,
4764
- );
4765
- // On serverless, in-memory maps may be empty — also check store
4766
- if (!hasRunningSubagents && !conversation.parentConversationId) {
4767
- const summaries = await conversationStore.listSummaries(conversation.ownerId);
4768
- for (const s of summaries) {
4769
- if (s.parentConversationId !== conversationId) continue;
4770
- const c = await conversationStore.get(s.conversationId);
4771
- if (c?.subagentMeta?.status === "running") {
4772
- hasRunningSubagents = true;
4773
- break;
4774
- }
4775
- }
4776
- }
5113
+ const hasRunningSubagents = !conversation.parentConversationId
5114
+ ? await hasRunningSubagentsForParent(conversationId, conversation.ownerId)
5115
+ : false;
4777
5116
  const hasPendingCallbackResults = Array.isArray(conversation.pendingSubagentResults)
4778
5117
  && conversation.pendingSubagentResults.length > 0;
5118
+ const hasPendingApprovals = Array.isArray(conversation.pendingApprovals)
5119
+ && conversation.pendingApprovals.length > 0;
4779
5120
  const needsContinuation = !hasActiveRun
4780
5121
  && Array.isArray(conversation._continuationMessages)
4781
- && conversation._continuationMessages.length > 0;
5122
+ && conversation._continuationMessages.length > 0
5123
+ && !hasPendingApprovals;
4782
5124
  writeJson(response, 200, {
4783
5125
  conversation: {
4784
5126
  ...conversation,
5127
+ messages: conversation.messages.map(normalizeMessageForClient),
4785
5128
  pendingApprovals: storedPending,
4786
5129
  _continuationMessages: undefined,
4787
5130
  _harnessMessages: undefined,
@@ -4989,11 +5332,9 @@ export const createRequestHandler = async (options?: {
4989
5332
  eventCount++;
4990
5333
  let sseEvent: AgentEvent = event;
4991
5334
  if (sseEvent.type === "run:completed") {
4992
- const hasRunningSubagents = Array.from(activeSubagentRuns.values()).some(
4993
- r => r.parentConversationId === conversationId,
4994
- );
5335
+ const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
4995
5336
  const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
4996
- sseEvent = hasRunningSubagents ? { ...stripped, pendingSubagents: true } : stripped;
5337
+ sseEvent = hasPendingSubagents ? { ...stripped, pendingSubagents: true } : stripped;
4997
5338
  }
4998
5339
  try {
4999
5340
  response.write(formatSseEvent(sseEvent));
@@ -5020,7 +5361,10 @@ export const createRequestHandler = async (options?: {
5020
5361
  // fire a delayed safety net in case the client disconnects before
5021
5362
  // POSTing the next /continue.
5022
5363
  const freshConv = await conversationStore.get(conversationId);
5023
- if (freshConv?._continuationMessages?.length) {
5364
+ if (
5365
+ freshConv?._continuationMessages?.length &&
5366
+ (!Array.isArray(freshConv.pendingApprovals) || freshConv.pendingApprovals.length === 0)
5367
+ ) {
5024
5368
  doWaitUntil(
5025
5369
  new Promise(r => setTimeout(r, 3000)).then(() =>
5026
5370
  selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
@@ -5112,11 +5456,17 @@ export const createRequestHandler = async (options?: {
5112
5456
  Connection: "keep-alive",
5113
5457
  "X-Accel-Buffering": "no",
5114
5458
  });
5115
- const harnessMessages = conversation._harnessMessages?.length
5116
- ? [...conversation._harnessMessages]
5117
- : [...conversation.messages];
5459
+ const canonicalHistory = resolveRunRequest(conversation, {
5460
+ conversationId,
5461
+ messages: conversation.messages,
5462
+ });
5463
+ const shouldRebuildCanonical = canonicalHistory.shouldRebuildCanonical;
5464
+ const harnessMessages = [...canonicalHistory.messages];
5118
5465
  const historyMessages = [...conversation.messages];
5119
5466
  const preRunMessages = [...conversation.messages];
5467
+ console.info(
5468
+ `[poncho] conversation="${conversationId}" history_source=${canonicalHistory.source}`,
5469
+ );
5120
5470
  let latestRunId = conversation.runtimeRunId ?? "";
5121
5471
  let assistantResponse = "";
5122
5472
  const toolTimeline: string[] = [];
@@ -5332,6 +5682,26 @@ export const createRequestHandler = async (options?: {
5332
5682
  const toolText = `- approval required \`${event.tool}\``;
5333
5683
  toolTimeline.push(toolText);
5334
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
+ }
5335
5705
  await persistDraftAssistantTurn();
5336
5706
  }
5337
5707
  if (event.type === "tool:approval:checkpoint") {
@@ -5355,16 +5725,13 @@ export const createRequestHandler = async (options?: {
5355
5725
  }]
5356
5726
  : []),
5357
5727
  ];
5358
- conversation.pendingApprovals = event.approvals.map(a => ({
5359
- approvalId: a.approvalId,
5728
+ conversation.pendingApprovals = buildApprovalCheckpoints({
5729
+ approvals: event.approvals,
5360
5730
  runId: latestRunId,
5361
- tool: a.tool,
5362
- toolCallId: a.toolCallId,
5363
- input: a.input,
5364
5731
  checkpointMessages: event.checkpointMessages,
5365
5732
  baseMessageCount: historyMessages.length,
5366
5733
  pendingToolCalls: event.pendingToolCalls,
5367
- }));
5734
+ });
5368
5735
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5369
5736
  conversation.updatedAt = Date.now();
5370
5737
  await conversationStore.update(conversation);
@@ -5400,7 +5767,9 @@ export const createRequestHandler = async (options?: {
5400
5767
  conversation._harnessMessages = runContinuationMessages;
5401
5768
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5402
5769
  conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
5403
- conversation.pendingApprovals = [];
5770
+ if (!checkpointedRun) {
5771
+ conversation.pendingApprovals = [];
5772
+ }
5404
5773
  if (runContextTokens > 0) conversation.contextTokens = runContextTokens;
5405
5774
  if (runContextWindow > 0) conversation.contextWindow = runContextWindow;
5406
5775
  conversation.updatedAt = Date.now();
@@ -5408,11 +5777,13 @@ export const createRequestHandler = async (options?: {
5408
5777
 
5409
5778
  // Delayed safety net: if the client doesn't POST to /continue
5410
5779
  // within 3 seconds (e.g. browser closed), the server picks it up.
5411
- doWaitUntil(
5412
- new Promise(r => setTimeout(r, 3000)).then(() =>
5413
- selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
5414
- ),
5415
- );
5780
+ if (!checkpointedRun) {
5781
+ doWaitUntil(
5782
+ new Promise(r => setTimeout(r, 3000)).then(() =>
5783
+ selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
5784
+ ),
5785
+ );
5786
+ }
5416
5787
  }
5417
5788
  }
5418
5789
  await telemetry.emit(event);
@@ -5420,11 +5791,9 @@ export const createRequestHandler = async (options?: {
5420
5791
  ? { ...event, compactedMessages: undefined }
5421
5792
  : event;
5422
5793
  if (sseEvent.type === "run:completed") {
5423
- const hasRunningSubagents = Array.from(activeSubagentRuns.values()).some(
5424
- r => r.parentConversationId === conversationId,
5425
- );
5794
+ const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
5426
5795
  const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
5427
- if (hasRunningSubagents) {
5796
+ if (hasPendingSubagents) {
5428
5797
  sseEvent = { ...stripped, pendingSubagents: true };
5429
5798
  } else {
5430
5799
  sseEvent = stripped;
@@ -5472,6 +5841,10 @@ export const createRequestHandler = async (options?: {
5472
5841
  conversation._continuationMessages = undefined;
5473
5842
  if (runHarnessMessages) {
5474
5843
  conversation._harnessMessages = runHarnessMessages;
5844
+ } else if (shouldRebuildCanonical) {
5845
+ conversation._harnessMessages = conversation.messages;
5846
+ } else {
5847
+ conversation._harnessMessages = conversation.messages;
5475
5848
  }
5476
5849
  conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
5477
5850
  conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
@@ -5509,7 +5882,9 @@ export const createRequestHandler = async (options?: {
5509
5882
  conversation.updatedAt = Date.now();
5510
5883
  await conversationStore.update(conversation);
5511
5884
  }
5512
- await clearPendingApprovalsForConversation(conversationId);
5885
+ if (!checkpointedRun) {
5886
+ await clearPendingApprovalsForConversation(conversationId);
5887
+ }
5513
5888
  return;
5514
5889
  }
5515
5890
  try {
@@ -5557,20 +5932,32 @@ export const createRequestHandler = async (options?: {
5557
5932
  if (active && active.abortController === abortController) {
5558
5933
  activeConversationRuns.delete(conversationId);
5559
5934
  }
5560
- finishConversationStream(conversationId);
5561
5935
  if (latestRunId) {
5562
5936
  runOwners.delete(latestRunId);
5563
5937
  runConversations.delete(latestRunId);
5564
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
+
5565
5955
  try {
5566
5956
  response.end();
5567
5957
  } catch {
5568
5958
  // Already closed.
5569
5959
  }
5570
- // Check for pending subagent results that arrived during the run
5571
- const hadDeferred = pendingCallbackNeeded.delete(conversationId);
5572
- const freshConv = await conversationStore.get(conversationId);
5573
- if (hadDeferred || freshConv?.pendingSubagentResults?.length) {
5960
+ if (needsCallback) {
5574
5961
  processSubagentCallback(conversationId, true).catch(err =>
5575
5962
  console.error(`[poncho][subagent-callback] Post-run callback failed:`, err instanceof Error ? err.message : err),
5576
5963
  );
@@ -5635,46 +6022,42 @@ export const createRequestHandler = async (options?: {
5635
6022
  if (!conv) continue;
5636
6023
 
5637
6024
  const task = `[Scheduled: ${jobName}]\n${cronJob.task}`;
5638
- const historyMessages = conv._harnessMessages?.length
5639
- ? [...conv._harnessMessages]
5640
- : [...conv.messages];
6025
+ const historySelection = resolveRunRequest(conv, {
6026
+ conversationId: conv.conversationId,
6027
+ messages: conv.messages,
6028
+ });
6029
+ const historyMessages = [...historySelection.messages];
5641
6030
  try {
5642
- let assistantResponse = "";
5643
- let steps = 0;
5644
- let cronHarnessMessages: Message[] | undefined;
5645
- for await (const event of harness.runWithTelemetry({
5646
- task,
5647
- conversationId: conv.conversationId,
5648
- parameters: withToolResultArchiveParam(
5649
- { __activeConversationId: conv.conversationId },
5650
- conv,
5651
- ),
5652
- messages: historyMessages,
5653
- })) {
5654
- if (event.type === "model:chunk") {
5655
- assistantResponse += event.content;
5656
- }
5657
- if (event.type === "run:completed") {
5658
- steps = event.result.steps;
5659
- if (!assistantResponse && event.result.response) {
5660
- assistantResponse = event.result.response;
5661
- }
5662
- if (event.result.continuationMessages) {
5663
- cronHarnessMessages = event.result.continuationMessages;
5664
- }
5665
- }
5666
- await telemetry.emit(event);
5667
- }
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;
5668
6047
 
5669
6048
  conv.messages = [
5670
6049
  ...historyMessages,
5671
6050
  { role: "user" as const, content: task },
5672
6051
  ...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
5673
6052
  ];
5674
- if (cronHarnessMessages) {
5675
- conv._harnessMessages = cronHarnessMessages;
6053
+ if (execution.runHarnessMessages) {
6054
+ conv._harnessMessages = execution.runHarnessMessages;
6055
+ } else if (historySelection.shouldRebuildCanonical) {
6056
+ conv._harnessMessages = conv.messages;
5676
6057
  }
5677
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;
5678
6061
  conv.updatedAt = Date.now();
5679
6062
  await conversationStore.update(conv);
5680
6063
 
@@ -5691,7 +6074,7 @@ export const createRequestHandler = async (options?: {
5691
6074
  console.error(`[cron] ${jobName}: send to ${chatId} failed:`, sendError instanceof Error ? sendError.message : sendError);
5692
6075
  }
5693
6076
  }
5694
- chatResults.push({ chatId, status: "completed", steps });
6077
+ chatResults.push({ chatId, status: "completed", steps: execution.runSteps });
5695
6078
  } catch (runError) {
5696
6079
  chatResults.push({ chatId, status: "error" });
5697
6080
  console.error(`[cron] ${jobName}: run for chat ${chatId} failed:`, runError instanceof Error ? runError.message : runError);
@@ -5991,91 +6374,32 @@ export const startDevServer = async (
5991
6374
  conversationId: string,
5992
6375
  historyMessages: Message[],
5993
6376
  toolResultArchive?: Conversation["_toolResultArchive"],
5994
- onEvent?: (event: AgentEvent) => void,
6377
+ onEvent?: (event: AgentEvent) => void | Promise<void>,
5995
6378
  ): Promise<CronRunResult> => {
5996
- let assistantResponse = "";
5997
- let steps = 0;
5998
- let contextTokens = 0;
5999
- let contextWindow = 0;
6000
- let harnessMessages: Message[] | undefined;
6001
- const toolTimeline: string[] = [];
6002
- const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
6003
- let currentTools: string[] = [];
6004
- let currentText = "";
6005
- for await (const event of harnessRef.runWithTelemetry({
6006
- task,
6007
- conversationId,
6008
- parameters: {
6009
- __activeConversationId: conversationId,
6010
- [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,
6011
6389
  },
6012
- messages: historyMessages,
6013
- })) {
6014
- onEvent?.(event);
6015
- if (event.type === "model:chunk") {
6016
- if (currentTools.length > 0) {
6017
- sections.push({ type: "tools", content: currentTools });
6018
- currentTools = [];
6019
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
6020
- assistantResponse += " ";
6021
- }
6022
- }
6023
- assistantResponse += event.content;
6024
- currentText += event.content;
6025
- }
6026
- if (event.type === "tool:started") {
6027
- if (currentText.length > 0) {
6028
- sections.push({ type: "text", content: currentText });
6029
- currentText = "";
6030
- }
6031
- const toolText = `- start \`${event.tool}\``;
6032
- toolTimeline.push(toolText);
6033
- currentTools.push(toolText);
6034
- }
6035
- if (event.type === "tool:completed") {
6036
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
6037
- toolTimeline.push(toolText);
6038
- currentTools.push(toolText);
6039
- }
6040
- if (event.type === "tool:error") {
6041
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
6042
- toolTimeline.push(toolText);
6043
- currentTools.push(toolText);
6044
- }
6045
- if (event.type === "run:completed") {
6046
- steps = event.result.steps;
6047
- contextTokens = event.result.contextTokens ?? 0;
6048
- contextWindow = event.result.contextWindow ?? 0;
6049
- if (event.result.continuationMessages) {
6050
- harnessMessages = event.result.continuationMessages;
6051
- }
6052
- if (!assistantResponse && event.result.response) {
6053
- assistantResponse = event.result.response;
6054
- }
6055
- }
6056
- }
6057
- if (currentTools.length > 0) {
6058
- sections.push({ type: "tools", content: currentTools });
6059
- }
6060
- if (currentText.length > 0) {
6061
- sections.push({ type: "text", content: currentText });
6062
- }
6063
- const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
6064
- const assistantMetadata =
6065
- toolTimeline.length > 0 || sections.length > 0
6066
- ? ({
6067
- toolActivity: [...toolTimeline],
6068
- sections: sections.length > 0 ? sections : undefined,
6069
- } as Message["metadata"])
6070
- : 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);
6071
6395
  return {
6072
- response: assistantResponse,
6073
- steps,
6396
+ response: execution.draft.assistantResponse,
6397
+ steps: execution.runSteps,
6074
6398
  assistantMetadata,
6075
6399
  hasContent,
6076
- contextTokens,
6077
- contextWindow,
6078
- harnessMessages,
6400
+ contextTokens: execution.runContextTokens,
6401
+ contextWindow: execution.runContextWindow,
6402
+ harnessMessages: execution.runHarnessMessages,
6079
6403
  toolResultArchive: harnessRef.getToolResultArchive(conversationId),
6080
6404
  };
6081
6405
  };
@@ -6147,9 +6471,11 @@ export const startDevServer = async (
6147
6471
  if (!conversation) continue;
6148
6472
 
6149
6473
  const task = `[Scheduled: ${jobName}]\n${config.task}`;
6150
- const historyMessages = conversation._harnessMessages?.length
6151
- ? [...conversation._harnessMessages]
6152
- : [...conversation.messages];
6474
+ const historySelection = resolveRunRequest(conversation, {
6475
+ conversationId: conversation.conversationId,
6476
+ messages: conversation.messages,
6477
+ });
6478
+ const historyMessages = [...historySelection.messages];
6153
6479
  const convId = conversation.conversationId;
6154
6480
 
6155
6481
  activeRuns?.set(convId, {
@@ -6170,6 +6496,8 @@ export const startDevServer = async (
6170
6496
  freshConv.messages = buildCronMessages(task, historyMessages, result);
6171
6497
  if (result.harnessMessages) {
6172
6498
  freshConv._harnessMessages = result.harnessMessages;
6499
+ } else if (historySelection.shouldRebuildCanonical) {
6500
+ freshConv._harnessMessages = freshConv.messages;
6173
6501
  }
6174
6502
  if (result.toolResultArchive) {
6175
6503
  freshConv._toolResultArchive = result.toolResultArchive;