@poncho-ai/cli 0.14.0 → 0.14.1

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
@@ -1315,16 +1315,6 @@ export const createRequestHandler = async (options?: {
1315
1315
  runId: string | null;
1316
1316
  };
1317
1317
  const activeConversationRuns = new Map<string, ActiveConversationRun>();
1318
- type PendingApproval = {
1319
- ownerId: string;
1320
- runId: string;
1321
- conversationId: string | null;
1322
- tool: string;
1323
- input: Record<string, unknown>;
1324
- resolve: (approved: boolean) => void;
1325
- };
1326
- const pendingApprovals = new Map<string, PendingApproval>();
1327
-
1328
1318
  // Per-conversation event streaming: buffer events and allow SSE subscribers
1329
1319
  type ConversationEventStream = {
1330
1320
  buffer: AgentEvent[];
@@ -1364,55 +1354,19 @@ export const createRequestHandler = async (options?: {
1364
1354
  setTimeout(() => conversationEventStreams.delete(conversationId), 30_000);
1365
1355
  }
1366
1356
  };
1367
- const persistConversationPendingApprovals = async (conversationId: string): Promise<void> => {
1368
- const conversation = await conversationStore.get(conversationId);
1369
- if (!conversation) {
1370
- return;
1371
- }
1372
- conversation.pendingApprovals = Array.from(pendingApprovals.entries())
1373
- .filter(
1374
- ([, pending]) =>
1375
- pending.ownerId === conversation.ownerId && pending.conversationId === conversationId,
1376
- )
1377
- .map(([approvalId, pending]) => ({
1378
- approvalId,
1379
- runId: pending.runId,
1380
- tool: pending.tool,
1381
- input: pending.input,
1382
- }));
1383
- await conversationStore.update(conversation);
1384
- };
1385
1357
  const clearPendingApprovalsForConversation = async (conversationId: string): Promise<void> => {
1386
- for (const [approvalId, pending] of pendingApprovals.entries()) {
1387
- if (pending.conversationId !== conversationId) {
1388
- continue;
1389
- }
1390
- pendingApprovals.delete(approvalId);
1391
- pending.resolve(false);
1358
+ const conversation = await conversationStore.get(conversationId);
1359
+ if (!conversation) return;
1360
+ if (Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0) {
1361
+ conversation.pendingApprovals = [];
1362
+ await conversationStore.update(conversation);
1392
1363
  }
1393
- await persistConversationPendingApprovals(conversationId);
1394
1364
  };
1395
1365
  const uploadStore = await createUploadStore(config?.uploads, workingDir);
1396
1366
  const harness = new AgentHarness({
1397
1367
  workingDir,
1398
1368
  environment: resolveHarnessEnvironment(),
1399
1369
  uploadStore,
1400
- approvalHandler: async (request) =>
1401
- new Promise<boolean>((resolveApproval) => {
1402
- const ownerIdForRun = runOwners.get(request.runId) ?? "local-owner";
1403
- const conversationIdForRun = runConversations.get(request.runId) ?? null;
1404
- pendingApprovals.set(request.approvalId, {
1405
- ownerId: ownerIdForRun,
1406
- runId: request.runId,
1407
- conversationId: conversationIdForRun,
1408
- tool: request.tool,
1409
- input: request.input,
1410
- resolve: resolveApproval,
1411
- });
1412
- if (conversationIdForRun) {
1413
- void persistConversationPendingApprovals(conversationIdForRun);
1414
- }
1415
- }),
1416
1370
  });
1417
1371
  await harness.initialize();
1418
1372
  const telemetry = new TelemetryEmitter(config?.telemetry);
@@ -1421,6 +1375,186 @@ export const createRequestHandler = async (options?: {
1421
1375
  workingDir,
1422
1376
  agentId: identity.id,
1423
1377
  });
1378
+ // ---------------------------------------------------------------------------
1379
+ // Resume a run from a persisted checkpoint after approval.
1380
+ // Processes events the same way the interactive/messaging runners do.
1381
+ // ---------------------------------------------------------------------------
1382
+ const resumeRunFromCheckpoint = async (
1383
+ conversationId: string,
1384
+ conversation: Conversation,
1385
+ checkpoint: NonNullable<Conversation["pendingApprovals"]>[number],
1386
+ toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }>,
1387
+ ): Promise<void> => {
1388
+ const abortController = new AbortController();
1389
+ activeConversationRuns.set(conversationId, {
1390
+ ownerId: conversation.ownerId,
1391
+ abortController,
1392
+ runId: null,
1393
+ });
1394
+ let latestRunId = conversation.runtimeRunId ?? "";
1395
+ let assistantResponse = "";
1396
+ const toolTimeline: string[] = [];
1397
+ const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
1398
+ let currentText = "";
1399
+ let currentTools: string[] = [];
1400
+ let checkpointedRun = false;
1401
+
1402
+ const baseMessages = checkpoint.baseMessageCount != null
1403
+ ? conversation.messages.slice(0, checkpoint.baseMessageCount)
1404
+ : [];
1405
+ const fullCheckpointMessages = [...baseMessages, ...checkpoint.checkpointMessages!];
1406
+
1407
+ try {
1408
+ for await (const event of harness.continueFromToolResult({
1409
+ messages: fullCheckpointMessages,
1410
+ toolResults,
1411
+ conversationId,
1412
+ abortSignal: abortController.signal,
1413
+ })) {
1414
+ if (event.type === "run:started") {
1415
+ latestRunId = event.runId;
1416
+ runOwners.set(event.runId, conversation.ownerId);
1417
+ runConversations.set(event.runId, conversationId);
1418
+ const active = activeConversationRuns.get(conversationId);
1419
+ if (active && active.abortController === abortController) {
1420
+ active.runId = event.runId;
1421
+ }
1422
+ }
1423
+ if (event.type === "model:chunk") {
1424
+ if (currentTools.length > 0) {
1425
+ sections.push({ type: "tools", content: currentTools });
1426
+ currentTools = [];
1427
+ }
1428
+ assistantResponse += event.content;
1429
+ currentText += event.content;
1430
+ }
1431
+ if (event.type === "tool:started") {
1432
+ if (currentText.length > 0) {
1433
+ sections.push({ type: "text", content: currentText });
1434
+ currentText = "";
1435
+ }
1436
+ const toolText = `- start \`${event.tool}\``;
1437
+ toolTimeline.push(toolText);
1438
+ currentTools.push(toolText);
1439
+ }
1440
+ if (event.type === "tool:completed") {
1441
+ const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
1442
+ toolTimeline.push(toolText);
1443
+ currentTools.push(toolText);
1444
+ }
1445
+ if (event.type === "tool:error") {
1446
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
1447
+ toolTimeline.push(toolText);
1448
+ currentTools.push(toolText);
1449
+ }
1450
+ if (event.type === "tool:approval:required") {
1451
+ const toolText = `- approval required \`${event.tool}\``;
1452
+ toolTimeline.push(toolText);
1453
+ currentTools.push(toolText);
1454
+ }
1455
+ if (event.type === "tool:approval:checkpoint") {
1456
+ const conv = await conversationStore.get(conversationId);
1457
+ if (conv) {
1458
+ conv.pendingApprovals = [{
1459
+ approvalId: event.approvalId,
1460
+ runId: latestRunId,
1461
+ tool: event.tool,
1462
+ toolCallId: event.toolCallId,
1463
+ input: event.input,
1464
+ checkpointMessages: [...fullCheckpointMessages, ...event.checkpointMessages],
1465
+ baseMessageCount: 0,
1466
+ pendingToolCalls: event.pendingToolCalls,
1467
+ }];
1468
+ conv.updatedAt = Date.now();
1469
+ await conversationStore.update(conv);
1470
+ }
1471
+ checkpointedRun = true;
1472
+ }
1473
+ if (
1474
+ event.type === "run:completed" &&
1475
+ assistantResponse.length === 0 &&
1476
+ event.result.response
1477
+ ) {
1478
+ assistantResponse = event.result.response;
1479
+ }
1480
+ if (event.type === "run:error") {
1481
+ assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
1482
+ }
1483
+ await telemetry.emit(event);
1484
+ broadcastEvent(conversationId, event);
1485
+ }
1486
+ } catch (err) {
1487
+ console.error("[resume-run] error:", err instanceof Error ? err.message : err);
1488
+ assistantResponse = assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
1489
+ }
1490
+
1491
+ if (currentTools.length > 0) {
1492
+ sections.push({ type: "tools", content: currentTools });
1493
+ }
1494
+ if (currentText.length > 0) {
1495
+ sections.push({ type: "text", content: currentText });
1496
+ }
1497
+
1498
+ if (!checkpointedRun) {
1499
+ const conv = await conversationStore.get(conversationId);
1500
+ if (conv) {
1501
+ const prevMessages = conv.messages;
1502
+ const hasAssistantContent =
1503
+ assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
1504
+ if (hasAssistantContent) {
1505
+ const lastMsg = prevMessages[prevMessages.length - 1];
1506
+ if (lastMsg && lastMsg.role === "assistant" && lastMsg.metadata) {
1507
+ const existingToolActivity = (lastMsg.metadata as Record<string, unknown>).toolActivity;
1508
+ const existingSections = (lastMsg.metadata as Record<string, unknown>).sections;
1509
+ const mergedTimeline = [
1510
+ ...(Array.isArray(existingToolActivity) ? existingToolActivity as string[] : []),
1511
+ ...toolTimeline,
1512
+ ];
1513
+ const mergedSections = [
1514
+ ...(Array.isArray(existingSections) ? existingSections as Array<{ type: "text" | "tools"; content: string | string[] }> : []),
1515
+ ...sections,
1516
+ ];
1517
+ const mergedText = (typeof lastMsg.content === "string" ? lastMsg.content : "") + assistantResponse;
1518
+ conv.messages = [
1519
+ ...prevMessages.slice(0, -1),
1520
+ {
1521
+ role: "assistant" as const,
1522
+ content: mergedText,
1523
+ metadata: {
1524
+ toolActivity: mergedTimeline,
1525
+ sections: mergedSections.length > 0 ? mergedSections : undefined,
1526
+ } as Message["metadata"],
1527
+ },
1528
+ ];
1529
+ } else {
1530
+ conv.messages = [
1531
+ ...prevMessages,
1532
+ {
1533
+ role: "assistant" as const,
1534
+ content: assistantResponse,
1535
+ metadata: (toolTimeline.length > 0 || sections.length > 0
1536
+ ? { toolActivity: toolTimeline, sections: sections.length > 0 ? sections : undefined }
1537
+ : undefined) as Message["metadata"],
1538
+ },
1539
+ ];
1540
+ }
1541
+ }
1542
+ conv.runtimeRunId = latestRunId || conv.runtimeRunId;
1543
+ conv.pendingApprovals = [];
1544
+ conv.updatedAt = Date.now();
1545
+ await conversationStore.update(conv);
1546
+ }
1547
+ }
1548
+
1549
+ finishConversationStream(conversationId);
1550
+ activeConversationRuns.delete(conversationId);
1551
+ if (latestRunId) {
1552
+ runOwners.delete(latestRunId);
1553
+ runConversations.delete(latestRunId);
1554
+ }
1555
+ console.log("[resume-run] complete for", conversationId);
1556
+ };
1557
+
1424
1558
  // ---------------------------------------------------------------------------
1425
1559
  // Messaging adapters (Slack, etc.) — routes bypass Poncho auth; each
1426
1560
  // adapter handles its own request verification (e.g. Slack signing secret).
@@ -1461,8 +1595,7 @@ export const createRequestHandler = async (options?: {
1461
1595
  const userContent = input.task;
1462
1596
 
1463
1597
  // Read-modify-write helper: always fetches the latest version from
1464
- // the store before writing, so concurrent writers (e.g. the approval
1465
- // handler's persistConversationPendingApprovals) don't get clobbered.
1598
+ // the store before writing, so concurrent writers don't get clobbered.
1466
1599
  const updateConversation = async (
1467
1600
  patch: (conv: Conversation) => void,
1468
1601
  ): Promise<void> => {
@@ -1484,6 +1617,7 @@ export const createRequestHandler = async (options?: {
1484
1617
  const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
1485
1618
  let currentTools: string[] = [];
1486
1619
  let currentText = "";
1620
+ let checkpointedRun = false;
1487
1621
 
1488
1622
  const buildMessages = (): Message[] => {
1489
1623
  const draftSections: Array<{ type: "text" | "tools"; content: string | string[] }> = [
@@ -1582,19 +1716,22 @@ export const createRequestHandler = async (options?: {
1582
1716
  toolTimeline.push(toolText);
1583
1717
  currentTools.push(toolText);
1584
1718
  await persistDraftAssistantTurn();
1585
- await persistConversationPendingApprovals(conversationId);
1586
- }
1587
- if (event.type === "tool:approval:granted") {
1588
- const toolText = `- approval granted (${event.approvalId})`;
1589
- toolTimeline.push(toolText);
1590
- currentTools.push(toolText);
1591
- await persistDraftAssistantTurn();
1592
1719
  }
1593
- if (event.type === "tool:approval:denied") {
1594
- const toolText = `- approval denied (${event.approvalId})`;
1595
- toolTimeline.push(toolText);
1596
- currentTools.push(toolText);
1597
- await persistDraftAssistantTurn();
1720
+ if (event.type === "tool:approval:checkpoint") {
1721
+ await updateConversation((c) => {
1722
+ c.messages = buildMessages();
1723
+ c.pendingApprovals = [{
1724
+ approvalId: event.approvalId,
1725
+ runId: latestRunId,
1726
+ tool: event.tool,
1727
+ toolCallId: event.toolCallId,
1728
+ input: event.input,
1729
+ checkpointMessages: event.checkpointMessages,
1730
+ baseMessageCount: historyMessages.length,
1731
+ pendingToolCalls: event.pendingToolCalls,
1732
+ }];
1733
+ });
1734
+ checkpointedRun = true;
1598
1735
  }
1599
1736
  if (
1600
1737
  event.type === "run:completed" &&
@@ -1623,13 +1760,14 @@ export const createRequestHandler = async (options?: {
1623
1760
  currentText = "";
1624
1761
  }
1625
1762
 
1626
- await updateConversation((c) => {
1627
- c.messages = buildMessages();
1628
- c.runtimeRunId = latestRunId || c.runtimeRunId;
1629
- c.pendingApprovals = [];
1630
- });
1763
+ if (!checkpointedRun) {
1764
+ await updateConversation((c) => {
1765
+ c.messages = buildMessages();
1766
+ c.runtimeRunId = latestRunId || c.runtimeRunId;
1767
+ c.pendingApprovals = [];
1768
+ });
1769
+ }
1631
1770
  finishConversationStream(conversationId);
1632
- await persistConversationPendingApprovals(conversationId);
1633
1771
  if (latestRunId) {
1634
1772
  runOwners.delete(latestRunId);
1635
1773
  runConversations.delete(latestRunId);
@@ -1932,6 +2070,7 @@ export const createRequestHandler = async (options?: {
1932
2070
  createdAt: conversation.createdAt,
1933
2071
  updatedAt: conversation.updatedAt,
1934
2072
  messageCount: conversation.messages.length,
2073
+ hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
1935
2074
  })),
1936
2075
  });
1937
2076
  return;
@@ -1957,40 +2096,93 @@ export const createRequestHandler = async (options?: {
1957
2096
  const approvalMatch = pathname.match(/^\/api\/approvals\/([^/]+)$/);
1958
2097
  if (approvalMatch && request.method === "POST") {
1959
2098
  const approvalId = decodeURIComponent(approvalMatch[1] ?? "");
1960
- const pending = pendingApprovals.get(approvalId);
1961
- if (!pending || pending.ownerId !== ownerId) {
1962
- // If the server restarted, an old pending approval can remain in
1963
- // conversation history without an active resolver. Prune stale entries.
1964
- const conversations = await conversationStore.list(ownerId);
1965
- let prunedStale = false;
1966
- for (const conversation of conversations) {
1967
- if (!Array.isArray(conversation.pendingApprovals)) {
1968
- continue;
1969
- }
1970
- const next = conversation.pendingApprovals.filter(
1971
- (approval) => approval.approvalId !== approvalId,
1972
- );
1973
- if (next.length !== conversation.pendingApprovals.length) {
1974
- conversation.pendingApprovals = next;
1975
- await conversationStore.update(conversation);
1976
- prunedStale = true;
1977
- }
2099
+ const body = (await readRequestBody(request)) as { approved?: boolean };
2100
+ const approved = body.approved === true;
2101
+
2102
+ // Find the approval in the conversation store (checkpoint-based flow)
2103
+ const conversations = await conversationStore.list(ownerId);
2104
+ let foundConversation: Conversation | undefined;
2105
+ let foundApproval: NonNullable<Conversation["pendingApprovals"]>[number] | undefined;
2106
+ for (const conv of conversations) {
2107
+ if (!Array.isArray(conv.pendingApprovals)) continue;
2108
+ const match = conv.pendingApprovals.find(a => a.approvalId === approvalId);
2109
+ if (match) {
2110
+ foundConversation = conv;
2111
+ foundApproval = match;
2112
+ break;
1978
2113
  }
2114
+ }
2115
+
2116
+ if (!foundConversation || !foundApproval) {
1979
2117
  writeJson(response, 404, {
1980
2118
  code: "APPROVAL_NOT_FOUND",
1981
- message: prunedStale
1982
- ? "Approval request is no longer active"
1983
- : "Approval request not found",
2119
+ message: "Approval request not found",
1984
2120
  });
1985
2121
  return;
1986
2122
  }
1987
- const body = (await readRequestBody(request)) as { approved?: boolean };
1988
- const approved = body.approved === true;
1989
- pendingApprovals.delete(approvalId);
1990
- if (pending.conversationId) {
1991
- await persistConversationPendingApprovals(pending.conversationId);
2123
+
2124
+ const conversationId = foundConversation.conversationId;
2125
+
2126
+ if (!foundApproval.checkpointMessages || !foundApproval.toolCallId) {
2127
+ // Legacy approval without checkpoint data — cannot resume
2128
+ foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? [])
2129
+ .filter(a => a.approvalId !== approvalId);
2130
+ await conversationStore.update(foundConversation);
2131
+ writeJson(response, 404, {
2132
+ code: "APPROVAL_NOT_FOUND",
2133
+ message: "Approval request is no longer active (no checkpoint data)",
2134
+ });
2135
+ return;
1992
2136
  }
1993
- pending.resolve(approved);
2137
+
2138
+ // Clear this approval from the conversation before resuming
2139
+ foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? [])
2140
+ .filter(a => a.approvalId !== approvalId);
2141
+ await conversationStore.update(foundConversation);
2142
+
2143
+ // Initialize the event stream so the web UI can connect immediately
2144
+ broadcastEvent(conversationId,
2145
+ approved
2146
+ ? { type: "tool:approval:granted", approvalId }
2147
+ : { type: "tool:approval:denied", approvalId },
2148
+ );
2149
+
2150
+ // Resume the run asynchronously (tool execution + continuation)
2151
+ void (async () => {
2152
+ let toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }>;
2153
+ if (approved) {
2154
+ const toolContext = {
2155
+ runId: foundApproval.runId,
2156
+ agentId: identity.id,
2157
+ step: 0,
2158
+ workingDir,
2159
+ parameters: {},
2160
+ };
2161
+ const execResults = await harness.executeTools(
2162
+ [{ id: foundApproval.toolCallId!, name: foundApproval.tool, input: foundApproval.input }],
2163
+ toolContext,
2164
+ );
2165
+ toolResults = execResults.map(r => ({
2166
+ callId: r.callId,
2167
+ toolName: r.tool,
2168
+ result: r.output,
2169
+ error: r.error,
2170
+ }));
2171
+ } else {
2172
+ toolResults = [{
2173
+ callId: foundApproval.toolCallId!,
2174
+ toolName: foundApproval.tool,
2175
+ error: "Tool execution denied by user",
2176
+ }];
2177
+ }
2178
+ await resumeRunFromCheckpoint(
2179
+ conversationId,
2180
+ foundConversation!,
2181
+ foundApproval!,
2182
+ toolResults,
2183
+ );
2184
+ })();
2185
+
1994
2186
  writeJson(response, 200, { ok: true, approvalId, approved });
1995
2187
  return;
1996
2188
  }
@@ -2056,34 +2248,19 @@ export const createRequestHandler = async (options?: {
2056
2248
  }
2057
2249
  if (request.method === "GET") {
2058
2250
  const storedPending = Array.isArray(conversation.pendingApprovals)
2059
- ? conversation.pendingApprovals
2251
+ ? conversation.pendingApprovals.map(a => ({
2252
+ approvalId: a.approvalId,
2253
+ runId: a.runId,
2254
+ tool: a.tool,
2255
+ input: a.input,
2256
+ }))
2060
2257
  : [];
2061
- const livePending = Array.from(pendingApprovals.entries())
2062
- .filter(
2063
- ([, pending]) =>
2064
- pending.ownerId === ownerId && pending.conversationId === conversationId,
2065
- )
2066
- .map(([approvalId, pending]) => ({
2067
- approvalId,
2068
- runId: pending.runId,
2069
- tool: pending.tool,
2070
- input: pending.input,
2071
- }));
2072
- const mergedPendingById = new Map<string, (typeof livePending)[number]>();
2073
- for (const approval of storedPending) {
2074
- if (approval && typeof approval.approvalId === "string") {
2075
- mergedPendingById.set(approval.approvalId, approval);
2076
- }
2077
- }
2078
- for (const approval of livePending) {
2079
- mergedPendingById.set(approval.approvalId, approval);
2080
- }
2081
2258
  const activeStream = conversationEventStreams.get(conversationId);
2082
2259
  const hasActiveRun = !!activeStream && !activeStream.finished;
2083
2260
  writeJson(response, 200, {
2084
2261
  conversation: {
2085
2262
  ...conversation,
2086
- pendingApprovals: Array.from(mergedPendingById.values()),
2263
+ pendingApprovals: storedPending,
2087
2264
  },
2088
2265
  hasActiveRun,
2089
2266
  });
@@ -2269,6 +2446,7 @@ export const createRequestHandler = async (options?: {
2269
2446
  let currentText = "";
2270
2447
  let currentTools: string[] = [];
2271
2448
  let runCancelled = false;
2449
+ let checkpointedRun = false;
2272
2450
  let userContent: Message["content"] = messageText;
2273
2451
  if (files.length > 0) {
2274
2452
  try {
@@ -2424,17 +2602,40 @@ export const createRequestHandler = async (options?: {
2424
2602
  currentTools.push(toolText);
2425
2603
  await persistDraftAssistantTurn();
2426
2604
  }
2427
- if (event.type === "tool:approval:granted") {
2428
- const toolText = `- approval granted (${event.approvalId})`;
2429
- toolTimeline.push(toolText);
2430
- currentTools.push(toolText);
2431
- await persistDraftAssistantTurn();
2432
- }
2433
- if (event.type === "tool:approval:denied") {
2434
- const toolText = `- approval denied (${event.approvalId})`;
2435
- toolTimeline.push(toolText);
2436
- currentTools.push(toolText);
2437
- await persistDraftAssistantTurn();
2605
+ if (event.type === "tool:approval:checkpoint") {
2606
+ const checkpointSections = [...sections];
2607
+ if (currentTools.length > 0) {
2608
+ checkpointSections.push({ type: "tools", content: [...currentTools] });
2609
+ }
2610
+ if (currentText.length > 0) {
2611
+ checkpointSections.push({ type: "text", content: currentText });
2612
+ }
2613
+ conversation.messages = [
2614
+ ...historyMessages,
2615
+ { role: "user", content: userContent },
2616
+ ...(assistantResponse.length > 0 || toolTimeline.length > 0 || checkpointSections.length > 0
2617
+ ? [{
2618
+ role: "assistant" as const,
2619
+ content: assistantResponse,
2620
+ metadata: (toolTimeline.length > 0 || checkpointSections.length > 0
2621
+ ? { toolActivity: [...toolTimeline], sections: checkpointSections.length > 0 ? checkpointSections : undefined }
2622
+ : undefined) as Message["metadata"],
2623
+ }]
2624
+ : []),
2625
+ ];
2626
+ conversation.pendingApprovals = [{
2627
+ approvalId: event.approvalId,
2628
+ runId: latestRunId,
2629
+ tool: event.tool,
2630
+ toolCallId: event.toolCallId,
2631
+ input: event.input,
2632
+ checkpointMessages: event.checkpointMessages,
2633
+ baseMessageCount: historyMessages.length,
2634
+ pendingToolCalls: event.pendingToolCalls,
2635
+ }];
2636
+ conversation.updatedAt = Date.now();
2637
+ await conversationStore.update(conversation);
2638
+ checkpointedRun = true;
2438
2639
  }
2439
2640
  if (
2440
2641
  event.type === "run:completed" &&
@@ -2459,29 +2660,31 @@ export const createRequestHandler = async (options?: {
2459
2660
  if (currentText.length > 0) {
2460
2661
  sections.push({ type: "text", content: currentText });
2461
2662
  }
2462
- const hasAssistantContent =
2463
- assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
2464
- conversation.messages = hasAssistantContent
2465
- ? [
2466
- ...historyMessages,
2467
- { role: "user", content: userContent },
2468
- {
2469
- role: "assistant",
2470
- content: assistantResponse,
2471
- metadata:
2472
- toolTimeline.length > 0 || sections.length > 0
2473
- ? ({
2474
- toolActivity: toolTimeline,
2475
- sections: sections.length > 0 ? sections : undefined,
2476
- } as Message["metadata"])
2477
- : undefined,
2478
- },
2479
- ]
2480
- : [...historyMessages, { role: "user", content: userContent }];
2481
- conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
2482
- conversation.pendingApprovals = [];
2483
- conversation.updatedAt = Date.now();
2484
- await conversationStore.update(conversation);
2663
+ if (!checkpointedRun) {
2664
+ const hasAssistantContent =
2665
+ assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
2666
+ conversation.messages = hasAssistantContent
2667
+ ? [
2668
+ ...historyMessages,
2669
+ { role: "user", content: userContent },
2670
+ {
2671
+ role: "assistant",
2672
+ content: assistantResponse,
2673
+ metadata:
2674
+ toolTimeline.length > 0 || sections.length > 0
2675
+ ? ({
2676
+ toolActivity: toolTimeline,
2677
+ sections: sections.length > 0 ? sections : undefined,
2678
+ } as Message["metadata"])
2679
+ : undefined,
2680
+ },
2681
+ ]
2682
+ : [...historyMessages, { role: "user", content: userContent }];
2683
+ conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
2684
+ conversation.pendingApprovals = [];
2685
+ conversation.updatedAt = Date.now();
2686
+ await conversationStore.update(conversation);
2687
+ }
2485
2688
  } catch (error) {
2486
2689
  if (abortController.signal.aborted || runCancelled) {
2487
2690
  const fallbackSections = [...sections];
@@ -2559,7 +2762,6 @@ export const createRequestHandler = async (options?: {
2559
2762
  activeConversationRuns.delete(conversationId);
2560
2763
  }
2561
2764
  finishConversationStream(conversationId);
2562
- await persistConversationPendingApprovals(conversationId);
2563
2765
  if (latestRunId) {
2564
2766
  runOwners.delete(latestRunId);
2565
2767
  runConversations.delete(latestRunId);
@@ -3018,44 +3220,10 @@ export const runInteractive = async (
3018
3220
  dotenv.config({ path: resolve(workingDir, ".env") });
3019
3221
  const config = await loadPonchoConfig(workingDir);
3020
3222
 
3021
- // Approval bridge: the harness calls this handler which creates a pending
3022
- // promise. The Ink UI picks up the pending request and shows a Y/N prompt.
3023
- // The user's response resolves the promise.
3024
- type ApprovalRequest = {
3025
- tool: string;
3026
- input: Record<string, unknown>;
3027
- approvalId: string;
3028
- resolve: (approved: boolean) => void;
3029
- };
3030
- let pendingApproval: ApprovalRequest | null = null;
3031
- let onApprovalRequest: ((req: ApprovalRequest) => void) | null = null;
3032
-
3033
- const approvalHandler = async (request: {
3034
- tool: string;
3035
- input: Record<string, unknown>;
3036
- runId: string;
3037
- step: number;
3038
- approvalId: string;
3039
- }): Promise<boolean> => {
3040
- return new Promise<boolean>((resolveApproval) => {
3041
- const req: ApprovalRequest = {
3042
- tool: request.tool,
3043
- input: request.input,
3044
- approvalId: request.approvalId,
3045
- resolve: resolveApproval,
3046
- };
3047
- pendingApproval = req;
3048
- if (onApprovalRequest) {
3049
- onApprovalRequest(req);
3050
- }
3051
- });
3052
- };
3053
-
3054
3223
  const uploadStore = await createUploadStore(config?.uploads, workingDir);
3055
3224
  const harness = new AgentHarness({
3056
3225
  workingDir,
3057
3226
  environment: resolveHarnessEnvironment(),
3058
- approvalHandler,
3059
3227
  uploadStore,
3060
3228
  });
3061
3229
  await harness.initialize();
@@ -3069,7 +3237,6 @@ export const runInteractive = async (
3069
3237
  workingDir: string;
3070
3238
  config?: PonchoConfig;
3071
3239
  conversationStore: ConversationStore;
3072
- onSetApprovalCallback?: (cb: (req: ApprovalRequest) => void) => void;
3073
3240
  }) => Promise<void>
3074
3241
  )({
3075
3242
  harness,
@@ -3080,13 +3247,6 @@ export const runInteractive = async (
3080
3247
  workingDir,
3081
3248
  agentId: identity.id,
3082
3249
  }),
3083
- onSetApprovalCallback: (cb: (req: ApprovalRequest) => void) => {
3084
- onApprovalRequest = cb;
3085
- // If there's already a pending request, fire it immediately
3086
- if (pendingApproval) {
3087
- cb(pendingApproval);
3088
- }
3089
- },
3090
3250
  });
3091
3251
  } finally {
3092
3252
  await harness.shutdown();