@poncho-ai/cli 0.32.4 → 0.32.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
32
32
  todoPanelCollapsed: false,
33
33
  cronSectionCollapsed: true,
34
34
  cronShowAll: false,
35
+ subagentPollInFlight: {},
35
36
  };
36
37
 
37
38
  const agentInitial = document.body.dataset.agentInitial || "A";
@@ -491,11 +492,20 @@ export const getWebUiClientScript = (markedSource: string): string => `
491
492
  return null;
492
493
  }
493
494
  const toolName = item && typeof item.tool === "string" ? item.tool : "tool";
495
+ const decision =
496
+ item && typeof item.decision === "string" ? item.decision : null;
497
+ const isResolved = decision === "approved" || decision === "denied";
494
498
  return {
495
499
  approvalId,
496
500
  tool: toolName,
497
501
  input: item?.input ?? {},
498
- state: "pending",
502
+ state: isResolved ? "resolved" : "pending",
503
+ resolvedDecision:
504
+ decision === "approved"
505
+ ? "approve"
506
+ : decision === "denied"
507
+ ? "deny"
508
+ : null,
499
509
  };
500
510
  })
501
511
  .filter(Boolean);
@@ -1545,8 +1555,8 @@ export const getWebUiClientScript = (markedSource: string): string => `
1545
1555
  updateContextRing();
1546
1556
  renderMessages(state.activeMessages, payload.hasActiveRun);
1547
1557
  }
1548
- if (payload.hasActiveRun) {
1549
- if (window._connectBrowserStream) window._connectBrowserStream();
1558
+ if (payload.hasActiveRun || payload.hasRunningSubagents) {
1559
+ if (payload.hasActiveRun && window._connectBrowserStream) window._connectBrowserStream();
1550
1560
  setTimeout(poll, 2000);
1551
1561
  } else {
1552
1562
  setStreaming(false);
@@ -1561,9 +1571,15 @@ export const getWebUiClientScript = (markedSource: string): string => `
1561
1571
  };
1562
1572
 
1563
1573
  const pollForSubagentResults = (conversationId) => {
1574
+ if (state.subagentPollInFlight[conversationId]) return;
1575
+ state.subagentPollInFlight[conversationId] = true;
1564
1576
  let lastMessageCount = state.activeMessages ? state.activeMessages.length : 0;
1577
+ let lastUpdatedAt = 0;
1565
1578
  const poll = async () => {
1566
- if (state.activeConversationId !== conversationId) return;
1579
+ if (state.activeConversationId !== conversationId) {
1580
+ delete state.subagentPollInFlight[conversationId];
1581
+ return;
1582
+ }
1567
1583
  try {
1568
1584
  var payload = await api("/api/conversations/" + encodeURIComponent(conversationId));
1569
1585
  if (state.activeConversationId !== conversationId) return;
@@ -1584,13 +1600,17 @@ export const getWebUiClientScript = (markedSource: string): string => `
1584
1600
  });
1585
1601
  });
1586
1602
  }
1587
- if (messages.length > lastMessageCount) {
1603
+ const conversationUpdatedAt =
1604
+ typeof payload.conversation.updatedAt === "number" ? payload.conversation.updatedAt : 0;
1605
+ if (messages.length > lastMessageCount || conversationUpdatedAt > lastUpdatedAt) {
1588
1606
  lastMessageCount = messages.length;
1607
+ lastUpdatedAt = conversationUpdatedAt;
1589
1608
  state.activeMessages = hydratePendingApprovals(messages, allPending);
1590
1609
  renderMessages(state.activeMessages, payload.hasActiveRun || payload.hasRunningSubagents);
1591
1610
  }
1592
1611
  if (payload.hasActiveRun) {
1593
1612
  // Parent agent started its continuation run — switch to live stream
1613
+ delete state.subagentPollInFlight[conversationId];
1594
1614
  setStreaming(true);
1595
1615
  state.activeMessages = hydratePendingApprovals(messages, allPending);
1596
1616
  streamConversationEvents(conversationId, { liveOnly: true }).finally(() => {
@@ -1603,6 +1623,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
1603
1623
  } else {
1604
1624
  renderMessages(state.activeMessages, false);
1605
1625
  await loadConversations();
1626
+ delete state.subagentPollInFlight[conversationId];
1606
1627
  }
1607
1628
  }
1608
1629
  } catch {
@@ -2530,6 +2551,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2530
2551
  let _maxSteps = 0;
2531
2552
  let _receivedTerminalEvent = false;
2532
2553
  let _shouldContinue = false;
2554
+ let _pendingSubagentsConversation = null;
2533
2555
 
2534
2556
  // Helper to read an SSE stream from a fetch response
2535
2557
  const readSseStream = async (response) => {
@@ -2847,7 +2869,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2847
2869
  assistantMessage.content = String(payload.result?.response || "");
2848
2870
  }
2849
2871
  if (payload.pendingSubagents) {
2850
- pollForSubagentResults(conversationId);
2872
+ _pendingSubagentsConversation = conversationId;
2851
2873
  }
2852
2874
  renderIfActiveConversation(false);
2853
2875
  }
@@ -2975,6 +2997,19 @@ export const getWebUiClientScript = (markedSource: string): string => `
2975
2997
  }
2976
2998
  elements.prompt.focus();
2977
2999
  }
3000
+
3001
+ // Subagent callback: after sendMessage fully completes (including
3002
+ // finally cleanup), reload the conversation. loadConversation is
3003
+ // the exact same code path as a manual refresh — if the callback
3004
+ // is still running it connects to the event stream; if it already
3005
+ // finished it just renders the final persisted state.
3006
+ if (_pendingSubagentsConversation && state.activeConversationId === _pendingSubagentsConversation) {
3007
+ const cbConvId = _pendingSubagentsConversation;
3008
+ await new Promise(r => setTimeout(r, 1200));
3009
+ if (state.activeConversationId === cbConvId && !state.isStreaming) {
3010
+ await loadConversation(cbConvId);
3011
+ }
3012
+ }
2978
3013
  };
2979
3014
 
2980
3015
  const requireAuth = async () => {
@@ -3208,9 +3243,22 @@ export const getWebUiClientScript = (markedSource: string): string => `
3208
3243
  }));
3209
3244
  return api("/api/approvals/" + encodeURIComponent(approvalId), {
3210
3245
  method: "POST",
3211
- body: JSON.stringify({ approved: decision === "approve" }),
3246
+ body: JSON.stringify({
3247
+ approved: decision === "approve",
3248
+ conversationId: state.activeConversationId || undefined,
3249
+ }),
3212
3250
  }).catch((error) => {
3213
3251
  const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
3252
+ const isNotReady = error && error.payload && error.payload.code === "APPROVAL_NOT_READY";
3253
+ if (isNotReady) {
3254
+ updatePendingApproval(approvalId, (request) => ({
3255
+ ...request,
3256
+ state: "pending",
3257
+ pendingDecision: null,
3258
+ resolvedDecision: null,
3259
+ }));
3260
+ return;
3261
+ }
3214
3262
  if (isStale) {
3215
3263
  updatePendingApproval(approvalId, () => null);
3216
3264
  } else {
@@ -3281,9 +3329,23 @@ export const getWebUiClientScript = (markedSource: string): string => `
3281
3329
  for (const aid of pending) {
3282
3330
  await api("/api/approvals/" + encodeURIComponent(aid), {
3283
3331
  method: "POST",
3284
- body: JSON.stringify({ approved: decision === "approve" }),
3332
+ body: JSON.stringify({
3333
+ approved: decision === "approve",
3334
+ conversationId: state.activeConversationId || undefined,
3335
+ }),
3285
3336
  }).catch((error) => {
3286
3337
  const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
3338
+ const isNotReady = error && error.payload && error.payload.code === "APPROVAL_NOT_READY";
3339
+ if (isNotReady) {
3340
+ updatePendingApproval(aid, (request) => ({
3341
+ ...request,
3342
+ state: "pending",
3343
+ pendingDecision: null,
3344
+ resolvedDecision: null,
3345
+ }));
3346
+ renderMessages(state.activeMessages, state.isStreaming);
3347
+ return;
3348
+ }
3287
3349
  if (isStale) {
3288
3350
  updatePendingApproval(aid, () => null);
3289
3351
  } else {
@@ -0,0 +1,171 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AgentEvent, Message } from "@poncho-ai/sdk";
3
+ import { __internalRunOrchestration } from "../src/index.js";
4
+
5
+ const baseMessages: Message[] = [{ role: "user", content: "hello" }];
6
+
7
+ describe("run orchestration helpers", () => {
8
+ it("falls back to user messages when harness history is unavailable", () => {
9
+ const conversation = {
10
+ messages: baseMessages,
11
+ _harnessMessages: "invalid",
12
+ };
13
+ const resolved = __internalRunOrchestration.loadRunHistory(
14
+ conversation as unknown as { messages: Message[]; _harnessMessages?: unknown; _continuationMessages?: unknown },
15
+ );
16
+ expect(resolved.source).toBe("messages");
17
+ expect(resolved.shouldRebuildCanonical).toBe(true);
18
+ expect(resolved.messages).toEqual(baseMessages);
19
+ });
20
+
21
+ it("prefers continuation history when requested", () => {
22
+ const continuation: Message[] = [{ role: "assistant", content: "next" }];
23
+ const conversation = {
24
+ messages: baseMessages,
25
+ _harnessMessages: baseMessages,
26
+ _continuationMessages: continuation,
27
+ };
28
+ const resolved = __internalRunOrchestration.loadRunHistory(
29
+ conversation as unknown as { messages: Message[]; _harnessMessages?: unknown; _continuationMessages?: unknown },
30
+ { preferContinuation: true },
31
+ );
32
+ expect(resolved.source).toBe("continuation");
33
+ expect(resolved.messages).toEqual(continuation);
34
+ });
35
+
36
+ it("normalizes legacy approval checkpoint payloads", () => {
37
+ const normalized = __internalRunOrchestration.normalizeApprovalCheckpoint(
38
+ {
39
+ approvalId: "approval_1",
40
+ runId: "run_1",
41
+ tool: "read_file",
42
+ toolCallId: "tool_1",
43
+ input: {},
44
+ checkpointMessages: undefined,
45
+ baseMessageCount: -1,
46
+ pendingToolCalls: [{ id: "t2", name: "list_directory", input: {} }, { foo: "bar" }],
47
+ } as unknown as {
48
+ approvalId: string;
49
+ runId: string;
50
+ tool: string;
51
+ toolCallId?: string;
52
+ input: Record<string, unknown>;
53
+ checkpointMessages?: Message[];
54
+ baseMessageCount?: number;
55
+ pendingToolCalls?: Array<{ id: string; name: string; input: Record<string, unknown> }>;
56
+ decision?: "approved" | "denied";
57
+ },
58
+ baseMessages,
59
+ );
60
+ expect(normalized.checkpointMessages).toEqual(baseMessages);
61
+ expect(normalized.baseMessageCount).toBe(0);
62
+ expect(normalized.pendingToolCalls).toEqual([{ id: "t2", name: "list_directory", input: {} }]);
63
+ });
64
+
65
+ it("builds checkpoint approvals with a canonical shape", () => {
66
+ const checkpoints = __internalRunOrchestration.buildApprovalCheckpoints({
67
+ approvals: [
68
+ {
69
+ approvalId: "approval_1",
70
+ tool: "read_file",
71
+ toolCallId: "tool_1",
72
+ input: { path: "README.md" },
73
+ },
74
+ ],
75
+ runId: "run_1",
76
+ checkpointMessages: baseMessages,
77
+ baseMessageCount: 2,
78
+ pendingToolCalls: [{ id: "tool_2", name: "list_directory", input: {} }],
79
+ });
80
+ expect(checkpoints).toEqual([
81
+ {
82
+ approvalId: "approval_1",
83
+ runId: "run_1",
84
+ tool: "read_file",
85
+ toolCallId: "tool_1",
86
+ input: { path: "README.md" },
87
+ checkpointMessages: baseMessages,
88
+ baseMessageCount: 2,
89
+ pendingToolCalls: [{ id: "tool_2", name: "list_directory", input: {} }],
90
+ },
91
+ ]);
92
+ });
93
+
94
+ it("resolves run request by preferring continuation and preserves rebuild signal", () => {
95
+ const continuation: Message[] = [{ role: "assistant", content: "continuation" }];
96
+ const resolved = __internalRunOrchestration.resolveRunRequest(
97
+ {
98
+ messages: baseMessages,
99
+ _harnessMessages: "invalid",
100
+ _continuationMessages: continuation,
101
+ } as unknown as { messages: Message[]; _harnessMessages?: unknown; _continuationMessages?: unknown },
102
+ {
103
+ conversationId: "conv_1",
104
+ messages: baseMessages,
105
+ preferContinuation: true,
106
+ },
107
+ );
108
+ expect(resolved.source).toBe("continuation");
109
+ expect(resolved.messages).toEqual(continuation);
110
+ expect(resolved.shouldRebuildCanonical).toBe(true);
111
+ });
112
+
113
+ it("records standard draft events for tool timeline and text", () => {
114
+ const draft = __internalRunOrchestration.createTurnDraftState();
115
+ __internalRunOrchestration.recordStandardTurnEvent(
116
+ draft,
117
+ { type: "tool:started", runId: "run_1", step: 1, tool: "read_file", input: {} } as AgentEvent,
118
+ );
119
+ __internalRunOrchestration.recordStandardTurnEvent(
120
+ draft,
121
+ { type: "tool:completed", runId: "run_1", step: 1, tool: "read_file", duration: 42, output: {} } as AgentEvent,
122
+ );
123
+ __internalRunOrchestration.recordStandardTurnEvent(
124
+ draft,
125
+ { type: "model:chunk", runId: "run_1", step: 1, content: "done" } as AgentEvent,
126
+ );
127
+ expect(draft.toolTimeline).toEqual(["- start `read_file`", "- done `read_file` (42ms)"]);
128
+ expect(draft.assistantResponse).toBe("done");
129
+ });
130
+
131
+ it("executes a conversation turn through the shared executor", async () => {
132
+ const events: AgentEvent[] = [
133
+ { type: "run:started", runId: "run_1", startedAt: Date.now() } as AgentEvent,
134
+ { type: "tool:started", runId: "run_1", step: 1, tool: "list_directory", input: {} } as AgentEvent,
135
+ { type: "model:chunk", runId: "run_1", step: 1, content: "hello" } as AgentEvent,
136
+ {
137
+ type: "run:completed",
138
+ runId: "run_1",
139
+ result: {
140
+ status: "completed",
141
+ response: "hello",
142
+ steps: 2,
143
+ duration: 10,
144
+ continuation: false,
145
+ continuationMessages: baseMessages,
146
+ usage: { inputTokens: 1, outputTokens: 1 },
147
+ contextTokens: 321,
148
+ contextWindow: 1000,
149
+ },
150
+ } as AgentEvent,
151
+ ];
152
+ const fakeHarness = {
153
+ async *runWithTelemetry() {
154
+ for (const event of events) yield event;
155
+ },
156
+ };
157
+ const seenTypes: string[] = [];
158
+ const result = await __internalRunOrchestration.executeConversationTurn({
159
+ harness: fakeHarness as never,
160
+ runInput: { task: "test", messages: baseMessages, conversationId: "conv_1" } as never,
161
+ onEvent: (event) => {
162
+ seenTypes.push(event.type);
163
+ },
164
+ });
165
+ expect(result.latestRunId).toBe("run_1");
166
+ expect(result.runSteps).toBe(2);
167
+ expect(result.runContextTokens).toBe(321);
168
+ expect(result.draft.assistantResponse).toBe("hello");
169
+ expect(seenTypes).toEqual(["run:started", "tool:started", "model:chunk", "run:completed"]);
170
+ });
171
+ });