@poncho-ai/cli 0.29.0 → 0.30.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.
@@ -442,6 +442,41 @@ var WEB_UI_STYLES = `
442
442
  .conversation-item .delete-btn.confirming:hover {
443
443
  color: var(--error-alt);
444
444
  }
445
+ .cron-section-header {
446
+ display: flex;
447
+ align-items: center;
448
+ gap: 6px;
449
+ padding: 8px 10px 4px;
450
+ cursor: pointer;
451
+ font-size: 11px;
452
+ font-weight: 600;
453
+ color: var(--fg-7);
454
+ text-transform: uppercase;
455
+ letter-spacing: 0.04em;
456
+ user-select: none;
457
+ transition: color 0.15s;
458
+ }
459
+ .cron-section-header:hover { color: var(--fg-5); }
460
+ .cron-section-caret {
461
+ display: inline-flex;
462
+ transition: transform 0.15s;
463
+ }
464
+ .cron-section-caret.open { transform: rotate(90deg); }
465
+ .cron-section-count {
466
+ font-weight: 400;
467
+ color: var(--fg-8);
468
+ font-size: 11px;
469
+ }
470
+ .cron-view-more {
471
+ padding: 6px 10px;
472
+ font-size: 12px;
473
+ color: var(--fg-7);
474
+ cursor: pointer;
475
+ text-align: center;
476
+ transition: color 0.15s;
477
+ user-select: none;
478
+ }
479
+ .cron-view-more:hover { color: var(--fg-3); }
445
480
  .sidebar-footer {
446
481
  margin-top: auto;
447
482
  padding-top: 8px;
@@ -1592,6 +1627,15 @@ var WEB_UI_STYLES = `
1592
1627
  .subagent-link:hover {
1593
1628
  text-decoration: underline;
1594
1629
  }
1630
+ .tool-link {
1631
+ color: var(--accent);
1632
+ text-decoration: none;
1633
+ font-size: 11px;
1634
+ margin-left: 4px;
1635
+ }
1636
+ .tool-link:hover {
1637
+ text-decoration: underline;
1638
+ }
1595
1639
  .subagent-callback-wrap {
1596
1640
  padding: 0;
1597
1641
  }
@@ -1604,6 +1648,8 @@ var WEB_UI_STYLES = `
1604
1648
  line-height: 1.45;
1605
1649
  color: var(--fg-tool-code);
1606
1650
  width: 100%;
1651
+ min-width: 0;
1652
+ overflow: hidden;
1607
1653
  }
1608
1654
  .subagent-result-summary {
1609
1655
  list-style: none;
@@ -1648,6 +1694,19 @@ var WEB_UI_STYLES = `
1648
1694
  display: grid;
1649
1695
  gap: 6px;
1650
1696
  padding: 0 12px 10px;
1697
+ min-width: 0;
1698
+ overflow-x: auto;
1699
+ overflow-wrap: break-word;
1700
+ word-break: break-word;
1701
+ }
1702
+ .subagent-result-body pre {
1703
+ max-width: 100%;
1704
+ overflow-x: auto;
1705
+ }
1706
+ .subagent-result-body table {
1707
+ max-width: 100%;
1708
+ overflow-x: auto;
1709
+ display: block;
1651
1710
  }
1652
1711
 
1653
1712
  /* Todo panel \u2014 inside composer-inner, above the input shell */
@@ -1800,6 +1859,8 @@ var getWebUiClientScript = (markedSource2) => `
1800
1859
  parentConversationId: null,
1801
1860
  todos: [],
1802
1861
  todoPanelCollapsed: false,
1862
+ cronSectionCollapsed: true,
1863
+ cronShowAll: false,
1803
1864
  };
1804
1865
 
1805
1866
  const agentInitial = document.body.dataset.agentInitial || "A";
@@ -2112,14 +2173,24 @@ var getWebUiClientScript = (markedSource2) => `
2112
2173
  return "";
2113
2174
  }
2114
2175
  const subagentLinkRe = /\\s*\\[subagent:([^\\]]+)\\]$/;
2176
+ const toolLinkRe = /\\s*\\[link:(https?:\\/\\/[^\\]]+)\\]$/;
2115
2177
  const renderActivityItem = (item) => {
2116
- const match = item.match(subagentLinkRe);
2117
- if (match) {
2178
+ const subMatch = item.match(subagentLinkRe);
2179
+ if (subMatch) {
2118
2180
  const cleaned = escapeHtml(item.replace(subagentLinkRe, ""));
2119
- const subId = escapeHtml(match[1]);
2181
+ const subId = escapeHtml(subMatch[1]);
2120
2182
  return '<div class="tool-activity-item">' + cleaned +
2121
2183
  ' <a class="subagent-link" href="javascript:void(0)" data-subagent-id="' + subId + '">View subagent</a></div>';
2122
2184
  }
2185
+ const linkMatch = item.match(toolLinkRe);
2186
+ if (linkMatch) {
2187
+ const cleaned = escapeHtml(item.replace(toolLinkRe, ""));
2188
+ const href = escapeHtml(linkMatch[1]);
2189
+ var displayUrl = linkMatch[1].replace(/^https?:\\/\\//, "");
2190
+ if (displayUrl.length > 55) displayUrl = displayUrl.slice(0, 52) + "...";
2191
+ return '<div class="tool-activity-item">' + cleaned +
2192
+ ' <a class="tool-link" href="' + href + '" target="_blank" rel="noopener">' + escapeHtml(displayUrl) + '</a></div>';
2193
+ }
2123
2194
  return '<div class="tool-activity-item">' + escapeHtml(item) + "</div>";
2124
2195
  };
2125
2196
  const chips = hasItems
@@ -2533,11 +2604,86 @@ var getWebUiClientScript = (markedSource2) => `
2533
2604
  }
2534
2605
  };
2535
2606
 
2607
+ const cronCaretSvg = '<svg viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M4.5 2.75L8 6L4.5 9.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
2608
+
2609
+ const parseCronTitle = (title) => {
2610
+ const rest = title.replace(/^[cron]s*/, "");
2611
+ const isoMatch = rest.match(/s(d{4}-d{2}-d{2}T[d:.]+Z?)$/);
2612
+ if (isoMatch) {
2613
+ return { jobName: rest.slice(0, isoMatch.index).trim(), timestamp: isoMatch[1] };
2614
+ }
2615
+ return { jobName: rest, timestamp: "" };
2616
+ };
2617
+
2618
+ const formatCronTimestamp = (isoStr) => {
2619
+ if (!isoStr) return "";
2620
+ try {
2621
+ const d = new Date(isoStr);
2622
+ if (isNaN(d.getTime())) return isoStr;
2623
+ return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
2624
+ } catch { return isoStr; }
2625
+ };
2626
+
2627
+ const CRON_PAGE_SIZE = 20;
2628
+
2629
+ const appendCronSection = (cronConvs, needsDivider) => {
2630
+ if (needsDivider) {
2631
+ const divider = document.createElement("div");
2632
+ divider.className = "sidebar-section-divider";
2633
+ elements.list.appendChild(divider);
2634
+ }
2635
+
2636
+ cronConvs.sort((a, b) => (b.updatedAt || b.createdAt || 0) - (a.updatedAt || a.createdAt || 0));
2637
+
2638
+ const isOpen = !state.cronSectionCollapsed;
2639
+ const header = document.createElement("div");
2640
+ header.className = "cron-section-header";
2641
+ header.innerHTML =
2642
+ '<span class="cron-section-caret' + (isOpen ? ' open' : '') + '">' + cronCaretSvg + '</span>' +
2643
+ '<span>Cron jobs</span>' +
2644
+ '<span class="cron-section-count">' + cronConvs.length + '</span>';
2645
+ header.onclick = () => {
2646
+ state.cronSectionCollapsed = !state.cronSectionCollapsed;
2647
+ state.cronShowAll = false;
2648
+ renderConversationList();
2649
+ };
2650
+ elements.list.appendChild(header);
2651
+
2652
+ if (state.cronSectionCollapsed) return;
2653
+
2654
+ const limit = state.cronShowAll ? cronConvs.length : CRON_PAGE_SIZE;
2655
+ const visible = cronConvs.slice(0, limit);
2656
+
2657
+ for (const c of visible) {
2658
+ const { jobName, timestamp } = parseCronTitle(c.title);
2659
+ const fmtTime = formatCronTimestamp(timestamp);
2660
+ const displayTitle = fmtTime ? jobName + " \\u00b7 " + fmtTime : c.title;
2661
+ elements.list.appendChild(buildConversationItem(Object.assign({}, c, { title: displayTitle })));
2662
+ appendSubagentsIfActive(c.conversationId);
2663
+ }
2664
+
2665
+ if (!state.cronShowAll && cronConvs.length > CRON_PAGE_SIZE) {
2666
+ const remaining = cronConvs.length - CRON_PAGE_SIZE;
2667
+ const viewMore = document.createElement("div");
2668
+ viewMore.className = "cron-view-more";
2669
+ viewMore.textContent = "View " + remaining + " more\\u2026";
2670
+ viewMore.onclick = () => {
2671
+ state.cronShowAll = true;
2672
+ renderConversationList();
2673
+ };
2674
+ elements.list.appendChild(viewMore);
2675
+ }
2676
+ };
2677
+
2536
2678
  const renderConversationList = () => {
2537
2679
  elements.list.innerHTML = "";
2538
2680
  const pending = state.conversations.filter(c => c.hasPendingApprovals);
2539
2681
  const rest = state.conversations.filter(c => !c.hasPendingApprovals);
2540
2682
 
2683
+ const isCron = (c) => c.title && c.title.startsWith("[cron]");
2684
+ const cronConvs = rest.filter(isCron);
2685
+ const nonCron = rest.filter(c => !isCron(c));
2686
+
2541
2687
  if (pending.length > 0) {
2542
2688
  const label = document.createElement("div");
2543
2689
  label.className = "sidebar-section-label";
@@ -2556,7 +2702,7 @@ var getWebUiClientScript = (markedSource2) => `
2556
2702
  const latest = [];
2557
2703
  const previous7 = [];
2558
2704
  const older = [];
2559
- for (const c of rest) {
2705
+ for (const c of nonCron) {
2560
2706
  const ts = c.updatedAt || c.createdAt || 0;
2561
2707
  if (ts >= startOfToday) {
2562
2708
  latest.push(c);
@@ -2568,6 +2714,12 @@ var getWebUiClientScript = (markedSource2) => `
2568
2714
  }
2569
2715
 
2570
2716
  let sectionRendered = pending.length > 0;
2717
+
2718
+ if (cronConvs.length > 0) {
2719
+ appendCronSection(cronConvs, sectionRendered);
2720
+ sectionRendered = true;
2721
+ }
2722
+
2571
2723
  const appendSection = (items, labelText) => {
2572
2724
  if (items.length === 0) return;
2573
2725
  if (sectionRendered) {
@@ -3365,10 +3517,17 @@ var getWebUiClientScript = (markedSource2) => `
3365
3517
  );
3366
3518
  const duration =
3367
3519
  typeof payload.duration === "number" ? payload.duration : null;
3368
- const detail =
3520
+ var detail =
3369
3521
  activeActivity && typeof activeActivity.detail === "string"
3370
3522
  ? activeActivity.detail.trim()
3371
3523
  : "";
3524
+ const out = payload.output && typeof payload.output === "object" ? payload.output : {};
3525
+ if (!detail && toolName === "web_search" && typeof out.query === "string") {
3526
+ detail = "\\x22" + (out.query.length > 60 ? out.query.slice(0, 57) + "..." : out.query) + "\\x22";
3527
+ }
3528
+ if (!detail && toolName === "web_fetch" && typeof out.url === "string") {
3529
+ detail = out.url;
3530
+ }
3372
3531
  const meta = [];
3373
3532
  if (duration !== null) meta.push(duration + "ms");
3374
3533
  if (detail) meta.push(detail);
@@ -3380,6 +3539,12 @@ var getWebUiClientScript = (markedSource2) => `
3380
3539
  if (toolName === "spawn_subagent" && payload.output && typeof payload.output === "object" && payload.output.subagentId) {
3381
3540
  toolText += " [subagent:" + payload.output.subagentId + "]";
3382
3541
  }
3542
+ if (toolName === "web_fetch" && typeof out.url === "string") {
3543
+ toolText += " [link:" + out.url + "]";
3544
+ }
3545
+ if (toolName === "web_search" && Array.isArray(out.results)) {
3546
+ toolText += " \\u2014 " + out.results.length + " result" + (out.results.length !== 1 ? "s" : "");
3547
+ }
3383
3548
  assistantMessage._currentTools.push(toolText);
3384
3549
  assistantMessage.metadata.toolActivity.push(toolText);
3385
3550
  if (typeof payload.outputTokenEstimate === "number" && payload.outputTokenEstimate > 0 && state.contextWindow > 0) {
@@ -3819,6 +3984,28 @@ var getWebUiClientScript = (markedSource2) => `
3819
3984
  };
3820
3985
  }
3821
3986
 
3987
+ if (toolName === "web_search") {
3988
+ const query = getStringInputField(input, "query");
3989
+ const short = query && query.length > 60 ? query.slice(0, 57) + "..." : query;
3990
+ return {
3991
+ kind: "tool",
3992
+ tool: toolName,
3993
+ label: "Searching" + (short ? " \\x22" + short + "\\x22" : ""),
3994
+ detail: short ? "\\x22" + short + "\\x22" : "",
3995
+ };
3996
+ }
3997
+
3998
+ if (toolName === "web_fetch") {
3999
+ const url = getStringInputField(input, "url");
4000
+ const short = url && url.length > 60 ? url.slice(0, 57) + "..." : url;
4001
+ return {
4002
+ kind: "tool",
4003
+ tool: toolName,
4004
+ label: "Fetching " + (short || "page"),
4005
+ detail: url || "",
4006
+ };
4007
+ }
4008
+
3822
4009
  return {
3823
4010
  kind: "tool",
3824
4011
  tool: toolName,
@@ -4206,10 +4393,17 @@ var getWebUiClientScript = (markedSource2) => `
4206
4393
  toolName,
4207
4394
  );
4208
4395
  const duration = typeof payload.duration === "number" ? payload.duration : null;
4209
- const detail =
4396
+ var detail =
4210
4397
  activeActivity && typeof activeActivity.detail === "string"
4211
4398
  ? activeActivity.detail.trim()
4212
4399
  : "";
4400
+ const out = payload.output && typeof payload.output === "object" ? payload.output : {};
4401
+ if (!detail && toolName === "web_search" && typeof out.query === "string") {
4402
+ detail = "\\x22" + (out.query.length > 60 ? out.query.slice(0, 57) + "..." : out.query) + "\\x22";
4403
+ }
4404
+ if (!detail && toolName === "web_fetch" && typeof out.url === "string") {
4405
+ detail = out.url;
4406
+ }
4213
4407
  const meta = [];
4214
4408
  if (duration !== null) meta.push(duration + "ms");
4215
4409
  if (detail) meta.push(detail);
@@ -4218,6 +4412,12 @@ var getWebUiClientScript = (markedSource2) => `
4218
4412
  if (toolName === "spawn_subagent" && payload.output && typeof payload.output === "object" && payload.output.subagentId) {
4219
4413
  toolText += " [subagent:" + payload.output.subagentId + "]";
4220
4414
  }
4415
+ if (toolName === "web_fetch" && typeof out.url === "string") {
4416
+ toolText += " [link:" + out.url + "]";
4417
+ }
4418
+ if (toolName === "web_search" && Array.isArray(out.results)) {
4419
+ toolText += " \\u2014 " + out.results.length + " result" + (out.results.length !== 1 ? "s" : "");
4420
+ }
4221
4421
  assistantMessage._currentTools.push(toolText);
4222
4422
  if (!assistantMessage.metadata) assistantMessage.metadata = {};
4223
4423
  if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
@@ -4689,7 +4889,7 @@ var getWebUiClientScript = (markedSource2) => `
4689
4889
  state: "resolved",
4690
4890
  resolvedDecision: decision,
4691
4891
  }));
4692
- api("/api/approvals/" + encodeURIComponent(approvalId), {
4892
+ return api("/api/approvals/" + encodeURIComponent(approvalId), {
4693
4893
  method: "POST",
4694
4894
  body: JSON.stringify({ approved: decision === "approve" }),
4695
4895
  }).catch((error) => {
@@ -4737,16 +4937,54 @@ var getWebUiClientScript = (markedSource2) => `
4737
4937
  if (pending.length === 0) return;
4738
4938
  const wasStreaming = state.isStreaming;
4739
4939
  if (!wasStreaming) setStreaming(true);
4740
- pending.forEach((aid) => submitApproval(aid, decision));
4940
+ // Mark all items as resolved in the UI immediately
4941
+ for (const aid of pending) {
4942
+ state.approvalRequestsInFlight[aid] = true;
4943
+ updatePendingApproval(aid, (request) => ({
4944
+ ...request,
4945
+ state: "resolved",
4946
+ resolvedDecision: decision,
4947
+ }));
4948
+ }
4741
4949
  renderMessages(state.activeMessages, state.isStreaming);
4742
4950
  loadConversations();
4743
- if (!wasStreaming && state.activeConversationId) {
4744
- const cid = state.activeConversationId;
4745
- await streamConversationEvents(cid, { liveOnly: true });
4746
- if (state.activeConversationId === cid) {
4747
- pollUntilRunIdle(cid);
4748
- }
4951
+ const streamCid = !wasStreaming && state.activeConversationId
4952
+ ? state.activeConversationId
4953
+ : null;
4954
+ if (streamCid) {
4955
+ streamConversationEvents(streamCid, { liveOnly: true }).finally(() => {
4956
+ if (state.activeConversationId === streamCid) {
4957
+ pollUntilRunIdle(streamCid);
4958
+ }
4959
+ });
4749
4960
  }
4961
+ // Send API calls sequentially so each store write completes
4962
+ // before the next read (avoids last-writer-wins in serverless).
4963
+ void (async () => {
4964
+ for (const aid of pending) {
4965
+ await api("/api/approvals/" + encodeURIComponent(aid), {
4966
+ method: "POST",
4967
+ body: JSON.stringify({ approved: decision === "approve" }),
4968
+ }).catch((error) => {
4969
+ const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
4970
+ if (isStale) {
4971
+ updatePendingApproval(aid, () => null);
4972
+ } else {
4973
+ const errMsg = error instanceof Error ? error.message : String(error);
4974
+ updatePendingApproval(aid, (request) => ({
4975
+ ...request,
4976
+ state: "pending",
4977
+ pendingDecision: null,
4978
+ resolvedDecision: null,
4979
+ _error: errMsg,
4980
+ }));
4981
+ }
4982
+ renderMessages(state.activeMessages, state.isStreaming);
4983
+ }).finally(() => {
4984
+ delete state.approvalRequestsInFlight[aid];
4985
+ });
4986
+ }
4987
+ })();
4750
4988
  return;
4751
4989
  }
4752
4990
 
@@ -7291,14 +7529,10 @@ ${name}/
7291
7529
  \u251C\u2500\u2500 tests/
7292
7530
  \u2502 \u2514\u2500\u2500 basic.yaml # Test suite
7293
7531
  \u2514\u2500\u2500 skills/
7294
- \u251C\u2500\u2500 starter/
7295
- \u2502 \u251C\u2500\u2500 SKILL.md
7296
- \u2502 \u2514\u2500\u2500 scripts/
7297
- \u2502 \u2514\u2500\u2500 starter-echo.ts
7298
- \u2514\u2500\u2500 fetch-page/
7532
+ \u2514\u2500\u2500 starter/
7299
7533
  \u251C\u2500\u2500 SKILL.md
7300
7534
  \u2514\u2500\u2500 scripts/
7301
- \u2514\u2500\u2500 fetch-page.ts
7535
+ \u2514\u2500\u2500 starter-echo.ts
7302
7536
  \`\`\`
7303
7537
 
7304
7538
  ## Cron Jobs
@@ -7476,67 +7710,6 @@ var SKILL_TOOL_TEMPLATE = `export default async function run(input) {
7476
7710
  return { echoed: message };
7477
7711
  }
7478
7712
  `;
7479
- var FETCH_PAGE_SKILL_TEMPLATE = `---
7480
- name: fetch-page
7481
- description: Fetch a web page and return its text content
7482
- allowed-tools:
7483
- - ./scripts/fetch-page.ts
7484
- ---
7485
-
7486
- # Fetch Page
7487
-
7488
- Fetches a URL and returns the page body as plain text (HTML tags stripped).
7489
-
7490
- ## Usage
7491
-
7492
- Call \`run_skill_script\` with:
7493
- - **skill**: \`fetch-page\`
7494
- - **script**: \`./scripts/fetch-page.ts\`
7495
- - **input**: \`{ "url": "https://example.com" }\`
7496
-
7497
- The script returns \`{ url, status, content }\` where \`content\` is the
7498
- text-only body (capped at ~32 000 chars to stay context-friendly).
7499
- `;
7500
- var FETCH_PAGE_SCRIPT_TEMPLATE = `export default async function run(input) {
7501
- const url = typeof input?.url === "string" ? input.url.trim() : "";
7502
- if (!url) {
7503
- return { error: "A \\"url\\" string is required." };
7504
- }
7505
-
7506
- const MAX_LENGTH = 32_000;
7507
-
7508
- const response = await fetch(url, {
7509
- headers: { "User-Agent": "poncho-fetch-page/1.0" },
7510
- redirect: "follow",
7511
- });
7512
-
7513
- if (!response.ok) {
7514
- return { url, status: response.status, error: response.statusText };
7515
- }
7516
-
7517
- const html = await response.text();
7518
-
7519
- // Lightweight HTML-to-text: strip tags, collapse whitespace.
7520
- const text = html
7521
- .replace(/<script[\\s\\S]*?<\\/script>/gi, "")
7522
- .replace(/<style[\\s\\S]*?<\\/style>/gi, "")
7523
- .replace(/<[^>]+>/g, " ")
7524
- .replace(/&nbsp;/gi, " ")
7525
- .replace(/&amp;/gi, "&")
7526
- .replace(/&lt;/gi, "<")
7527
- .replace(/&gt;/gi, ">")
7528
- .replace(/&quot;/gi, '"')
7529
- .replace(/&#39;/gi, "'")
7530
- .replace(/\\s+/g, " ")
7531
- .trim();
7532
-
7533
- const content = text.length > MAX_LENGTH
7534
- ? text.slice(0, MAX_LENGTH) + "\u2026 (truncated)"
7535
- : text;
7536
-
7537
- return { url, status: response.status, content };
7538
- }
7539
- `;
7540
7713
  var ensureFile = async (path, content) => {
7541
7714
  await mkdir3(dirname4(path), { recursive: true });
7542
7715
  await writeFile3(path, content, { encoding: "utf8", flag: "wx" });
@@ -8003,9 +8176,7 @@ var initProject = async (projectName, options) => {
8003
8176
  { path: ".gitignore", content: GITIGNORE_TEMPLATE },
8004
8177
  { path: "tests/basic.yaml", content: TEST_TEMPLATE },
8005
8178
  { path: "skills/starter/SKILL.md", content: SKILL_TEMPLATE },
8006
- { path: "skills/starter/scripts/starter-echo.ts", content: SKILL_TOOL_TEMPLATE },
8007
- { path: "skills/fetch-page/SKILL.md", content: FETCH_PAGE_SKILL_TEMPLATE },
8008
- { path: "skills/fetch-page/scripts/fetch-page.ts", content: FETCH_PAGE_SCRIPT_TEMPLATE }
8179
+ { path: "skills/starter/scripts/starter-echo.ts", content: SKILL_TOOL_TEMPLATE }
8009
8180
  ];
8010
8181
  if (onboarding.envFile) {
8011
8182
  scaffoldFiles.push({ path: ".env", content: onboarding.envFile });
@@ -8229,6 +8400,7 @@ data: ${JSON.stringify(statusPayload)}
8229
8400
  const MAX_CONCURRENT_SUBAGENTS = 5;
8230
8401
  const activeSubagentRuns = /* @__PURE__ */ new Map();
8231
8402
  const pendingSubagentApprovals = /* @__PURE__ */ new Map();
8403
+ const approvalDecisionTracker = /* @__PURE__ */ new Map();
8232
8404
  const getSubagentDepth = async (conversationId) => {
8233
8405
  let depth = 0;
8234
8406
  let current = await conversationStore.get(conversationId);
@@ -8363,6 +8535,9 @@ data: ${JSON.stringify(statusPayload)}
8363
8535
  if (currentTools.length > 0) {
8364
8536
  sections.push({ type: "tools", content: currentTools });
8365
8537
  currentTools = [];
8538
+ if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
8539
+ assistantResponse += " ";
8540
+ }
8366
8541
  }
8367
8542
  assistantResponse += event.content;
8368
8543
  currentText += event.content;
@@ -8478,6 +8653,9 @@ data: ${JSON.stringify(statusPayload)}
8478
8653
  if (currentTools.length > 0) {
8479
8654
  sections.push({ type: "tools", content: currentTools });
8480
8655
  currentTools = [];
8656
+ if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
8657
+ assistantResponse += " ";
8658
+ }
8481
8659
  }
8482
8660
  assistantResponse += resumeEvent.content;
8483
8661
  currentText += resumeEvent.content;
@@ -8640,8 +8818,10 @@ data: ${JSON.stringify(statusPayload)}
8640
8818
  }
8641
8819
  };
8642
8820
  const MAX_SUBAGENT_CALLBACK_COUNT = 20;
8821
+ const pendingCallbackNeeded = /* @__PURE__ */ new Set();
8643
8822
  const triggerParentCallback = async (parentConversationId) => {
8644
8823
  if (activeConversationRuns.has(parentConversationId)) {
8824
+ pendingCallbackNeeded.add(parentConversationId);
8645
8825
  return;
8646
8826
  }
8647
8827
  if (isServerless) {
@@ -8653,12 +8833,12 @@ data: ${JSON.stringify(statusPayload)}
8653
8833
  await processSubagentCallback(parentConversationId);
8654
8834
  };
8655
8835
  const CALLBACK_LOCK_STALE_MS = 5 * 60 * 1e3;
8656
- const processSubagentCallback = async (conversationId) => {
8836
+ const processSubagentCallback = async (conversationId, skipLockCheck = false) => {
8657
8837
  const conversation = await conversationStore.get(conversationId);
8658
8838
  if (!conversation) return;
8659
8839
  const pendingResults = conversation.pendingSubagentResults ?? [];
8660
8840
  if (pendingResults.length === 0) return;
8661
- if (conversation.runningCallbackSince) {
8841
+ if (!skipLockCheck && conversation.runningCallbackSince) {
8662
8842
  const elapsed = Date.now() - conversation.runningCallbackSince;
8663
8843
  if (elapsed < CALLBACK_LOCK_STALE_MS) {
8664
8844
  return;
@@ -8696,11 +8876,24 @@ ${resultBody}`,
8696
8876
  abortController,
8697
8877
  runId: null
8698
8878
  });
8879
+ const prevStream = conversationEventStreams.get(conversationId);
8880
+ if (prevStream) {
8881
+ prevStream.finished = false;
8882
+ prevStream.buffer = [];
8883
+ } else {
8884
+ conversationEventStreams.set(conversationId, {
8885
+ buffer: [],
8886
+ subscribers: /* @__PURE__ */ new Set(),
8887
+ finished: false
8888
+ });
8889
+ }
8699
8890
  const historyMessages = [...conversation.messages];
8700
8891
  let assistantResponse = "";
8701
8892
  let latestRunId = "";
8702
8893
  let runContinuation = false;
8703
8894
  let runContinuationMessages;
8895
+ let runContextTokens = conversation.contextTokens ?? 0;
8896
+ let runContextWindow = conversation.contextWindow ?? 0;
8704
8897
  const toolTimeline = [];
8705
8898
  const sections = [];
8706
8899
  let currentTools = [];
@@ -8725,6 +8918,9 @@ ${resultBody}`,
8725
8918
  if (currentTools.length > 0) {
8726
8919
  sections.push({ type: "tools", content: currentTools });
8727
8920
  currentTools = [];
8921
+ if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
8922
+ assistantResponse += " ";
8923
+ }
8728
8924
  }
8729
8925
  assistantResponse += event.content;
8730
8926
  currentText += event.content;
@@ -8752,6 +8948,8 @@ ${resultBody}`,
8752
8948
  if (assistantResponse.length === 0 && event.result.response) {
8753
8949
  assistantResponse = event.result.response;
8754
8950
  }
8951
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
8952
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
8755
8953
  if (event.result.continuation) {
8756
8954
  runContinuation = true;
8757
8955
  if (event.result.continuationMessages) {
@@ -8778,6 +8976,8 @@ ${resultBody}`,
8778
8976
  }
8779
8977
  freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
8780
8978
  freshConv.runningCallbackSince = void 0;
8979
+ if (runContextTokens > 0) freshConv.contextTokens = runContextTokens;
8980
+ if (runContextWindow > 0) freshConv.contextWindow = runContextWindow;
8781
8981
  freshConv.updatedAt = Date.now();
8782
8982
  await conversationStore.update(freshConv);
8783
8983
  if (freshConv.channelMeta && assistantResponse.length > 0) {
@@ -8815,23 +9015,32 @@ ${resultBody}`,
8815
9015
  } finally {
8816
9016
  activeConversationRuns.delete(conversationId);
8817
9017
  finishConversationStream(conversationId);
9018
+ const hadDeferredTrigger = pendingCallbackNeeded.delete(conversationId);
8818
9019
  const freshConv = await conversationStore.get(conversationId);
8819
- if (freshConv) {
8820
- if (freshConv.runningCallbackSince) {
8821
- freshConv.runningCallbackSince = void 0;
8822
- await conversationStore.update(freshConv);
8823
- }
8824
- }
8825
- if (freshConv?.pendingSubagentResults?.length) {
9020
+ const hasPendingInStore = !!freshConv?.pendingSubagentResults?.length;
9021
+ if (hadDeferredTrigger || hasPendingInStore) {
8826
9022
  if (isServerless) {
8827
9023
  selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(
8828
9024
  (err) => console.error(`[poncho][subagent-callback] Recursive callback self-fetch failed:`, err instanceof Error ? err.message : err)
8829
9025
  );
8830
9026
  } else {
8831
- processSubagentCallback(conversationId).catch(
9027
+ processSubagentCallback(conversationId, true).catch(
8832
9028
  (err) => console.error(`[poncho][subagent-callback] Recursive callback failed:`, err instanceof Error ? err.message : err)
8833
9029
  );
8834
9030
  }
9031
+ } else if (freshConv?.runningCallbackSince) {
9032
+ const afterClear = await conversationStore.clearCallbackLock(conversationId);
9033
+ if (afterClear?.pendingSubagentResults?.length) {
9034
+ if (isServerless) {
9035
+ selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(
9036
+ (err) => console.error(`[poncho][subagent-callback] Post-clear callback self-fetch failed:`, err instanceof Error ? err.message : err)
9037
+ );
9038
+ } else {
9039
+ processSubagentCallback(conversationId, true).catch(
9040
+ (err) => console.error(`[poncho][subagent-callback] Post-clear callback failed:`, err instanceof Error ? err.message : err)
9041
+ );
9042
+ }
9043
+ }
8835
9044
  }
8836
9045
  }
8837
9046
  };
@@ -8998,19 +9207,14 @@ ${resultBody}`,
8998
9207
  if (active && active.abortController === abortController) {
8999
9208
  active.runId = event.runId;
9000
9209
  }
9001
- if (typeof event.contextWindow === "number" && event.contextWindow > 0) {
9002
- runContextWindow = event.contextWindow;
9003
- }
9004
- }
9005
- if (event.type === "model:response") {
9006
- if (typeof event.usage?.input === "number") {
9007
- runContextTokens = event.usage.input;
9008
- }
9009
9210
  }
9010
9211
  if (event.type === "model:chunk") {
9011
9212
  if (currentTools.length > 0) {
9012
9213
  sections.push({ type: "tools", content: currentTools });
9013
9214
  currentTools = [];
9215
+ if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
9216
+ assistantResponse += " ";
9217
+ }
9014
9218
  }
9015
9219
  assistantResponse += event.content;
9016
9220
  currentText += event.content;
@@ -9067,8 +9271,12 @@ ${resultBody}`,
9067
9271
  }
9068
9272
  checkpointedRun = true;
9069
9273
  }
9070
- if (event.type === "run:completed" && assistantResponse.length === 0 && event.result.response) {
9071
- assistantResponse = event.result.response;
9274
+ if (event.type === "run:completed") {
9275
+ if (assistantResponse.length === 0 && event.result.response) {
9276
+ assistantResponse = event.result.response;
9277
+ }
9278
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
9279
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
9072
9280
  }
9073
9281
  if (event.type === "run:error") {
9074
9282
  assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
@@ -9151,6 +9359,13 @@ ${resultBody}`,
9151
9359
  runConversations.delete(latestRunId);
9152
9360
  }
9153
9361
  console.log("[resume-run] complete for", conversationId);
9362
+ const hadDeferred = pendingCallbackNeeded.delete(conversationId);
9363
+ const postConv = await conversationStore.get(conversationId);
9364
+ if (hadDeferred || postConv?.pendingSubagentResults?.length) {
9365
+ processSubagentCallback(conversationId, true).catch(
9366
+ (err) => console.error(`[poncho][subagent-callback] Post-resume callback failed:`, err instanceof Error ? err.message : err)
9367
+ );
9368
+ }
9154
9369
  };
9155
9370
  const messagingRoutes = /* @__PURE__ */ new Map();
9156
9371
  const messagingRouteRegistrar = (method, path, routeHandler) => {
@@ -9281,19 +9496,14 @@ ${resultBody}`,
9281
9496
  latestRunId = event.runId;
9282
9497
  runOwners.set(event.runId, "local-owner");
9283
9498
  runConversations.set(event.runId, conversationId);
9284
- if (typeof event.contextWindow === "number" && event.contextWindow > 0) {
9285
- runContextWindow = event.contextWindow;
9286
- }
9287
- }
9288
- if (event.type === "model:response") {
9289
- if (typeof event.usage?.input === "number") {
9290
- runContextTokens = event.usage.input;
9291
- }
9292
9499
  }
9293
9500
  if (event.type === "model:chunk") {
9294
9501
  if (currentTools.length > 0) {
9295
9502
  sections.push({ type: "tools", content: currentTools });
9296
9503
  currentTools = [];
9504
+ if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
9505
+ assistantResponse += " ";
9506
+ }
9297
9507
  }
9298
9508
  assistantResponse += event.content;
9299
9509
  currentText += event.content;
@@ -9384,6 +9594,8 @@ ${resultBody}`,
9384
9594
  }
9385
9595
  runSteps = event.result.steps;
9386
9596
  if (typeof event.result.maxSteps === "number") runMaxSteps = event.result.maxSteps;
9597
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
9598
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
9387
9599
  }
9388
9600
  if (event.type === "run:error") {
9389
9601
  assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
@@ -10172,18 +10384,29 @@ data: ${JSON.stringify(frame)}
10172
10384
  });
10173
10385
  return;
10174
10386
  }
10387
+ let batchDecisions = approvalDecisionTracker.get(conversationId);
10388
+ if (!batchDecisions) {
10389
+ batchDecisions = /* @__PURE__ */ new Map();
10390
+ approvalDecisionTracker.set(conversationId, batchDecisions);
10391
+ }
10392
+ batchDecisions.set(approvalId, approved);
10175
10393
  foundApproval.decision = approved ? "approved" : "denied";
10176
10394
  broadcastEvent(
10177
10395
  conversationId,
10178
10396
  approved ? { type: "tool:approval:granted", approvalId } : { type: "tool:approval:denied", approvalId }
10179
10397
  );
10180
10398
  const allApprovals = foundConversation.pendingApprovals ?? [];
10181
- const allDecided = allApprovals.length > 0 && allApprovals.every((a) => a.decision != null);
10399
+ const allDecided = allApprovals.length > 0 && allApprovals.every((a) => batchDecisions.has(a.approvalId));
10182
10400
  if (!allDecided) {
10183
10401
  await conversationStore.update(foundConversation);
10184
10402
  writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
10185
10403
  return;
10186
10404
  }
10405
+ for (const a of allApprovals) {
10406
+ const d = batchDecisions.get(a.approvalId);
10407
+ if (d != null) a.decision = d ? "approved" : "denied";
10408
+ }
10409
+ approvalDecisionTracker.delete(conversationId);
10187
10410
  foundConversation.pendingApprovals = [];
10188
10411
  foundConversation.runStatus = "running";
10189
10412
  await conversationStore.update(foundConversation);
@@ -10805,14 +11028,6 @@ data: ${JSON.stringify(frame)}
10805
11028
  if (active && active.abortController === abortController) {
10806
11029
  active.runId = event.runId;
10807
11030
  }
10808
- if (typeof event.contextWindow === "number" && event.contextWindow > 0) {
10809
- runContextWindow = event.contextWindow;
10810
- }
10811
- }
10812
- if (event.type === "model:response") {
10813
- if (typeof event.usage?.input === "number") {
10814
- runContextTokens = event.usage.input;
10815
- }
10816
11031
  }
10817
11032
  if (event.type === "run:cancelled") {
10818
11033
  runCancelled = true;
@@ -10821,6 +11036,9 @@ data: ${JSON.stringify(frame)}
10821
11036
  if (currentTools.length > 0) {
10822
11037
  sections.push({ type: "tools", content: currentTools });
10823
11038
  currentTools = [];
11039
+ if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
11040
+ assistantResponse += " ";
11041
+ }
10824
11042
  }
10825
11043
  assistantResponse += event.content;
10826
11044
  currentText += event.content;
@@ -10902,6 +11120,8 @@ data: ${JSON.stringify(frame)}
10902
11120
  if (assistantResponse.length === 0 && event.result.response) {
10903
11121
  assistantResponse = event.result.response;
10904
11122
  }
11123
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
11124
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
10905
11125
  if (event.result.continuation && event.result.continuationMessages) {
10906
11126
  runContinuationMessages = event.result.continuationMessages;
10907
11127
  conversation._continuationMessages = runContinuationMessages;
@@ -11041,9 +11261,10 @@ data: ${JSON.stringify(frame)}
11041
11261
  response.end();
11042
11262
  } catch {
11043
11263
  }
11264
+ const hadDeferred = pendingCallbackNeeded.delete(conversationId);
11044
11265
  const freshConv = await conversationStore.get(conversationId);
11045
- if (freshConv?.pendingSubagentResults?.length) {
11046
- processSubagentCallback(conversationId).catch(
11266
+ if (hadDeferred || freshConv?.pendingSubagentResults?.length) {
11267
+ processSubagentCallback(conversationId, true).catch(
11047
11268
  (err) => console.error(`[poncho][subagent-callback] Post-run callback failed:`, err instanceof Error ? err.message : err)
11048
11269
  );
11049
11270
  }
@@ -11191,122 +11412,157 @@ ${cronJob.task}`;
11191
11412
  `[cron] ${jobName} ${timestamp}`
11192
11413
  );
11193
11414
  }
11194
- const abortController = new AbortController();
11195
- let assistantResponse = "";
11196
- let latestRunId = "";
11197
- const toolTimeline = [];
11198
- const sections = [];
11199
- let currentTools = [];
11200
- let currentText = "";
11201
- let runResult = {
11202
- status: "completed",
11203
- steps: 0
11204
- };
11205
- const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
11206
- const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
11207
- for await (const event of harness.runWithTelemetry({
11208
- task: cronJob.task,
11209
- conversationId: conversation.conversationId,
11210
- parameters: { __activeConversationId: conversation.conversationId },
11211
- messages: historyMessages,
11212
- abortSignal: abortController.signal
11213
- })) {
11214
- if (event.type === "run:started") {
11215
- latestRunId = event.runId;
11216
- }
11217
- if (event.type === "model:chunk") {
11218
- if (currentTools.length > 0) {
11219
- sections.push({ type: "tools", content: currentTools });
11220
- currentTools = [];
11415
+ const convId = conversation.conversationId;
11416
+ activeConversationRuns.set(convId, {
11417
+ ownerId: conversation.ownerId,
11418
+ abortController: new AbortController(),
11419
+ runId: null
11420
+ });
11421
+ try {
11422
+ const abortController = new AbortController();
11423
+ let assistantResponse = "";
11424
+ let latestRunId = "";
11425
+ const toolTimeline = [];
11426
+ const sections = [];
11427
+ let currentTools = [];
11428
+ let currentText = "";
11429
+ let runResult = {
11430
+ status: "completed",
11431
+ steps: 0
11432
+ };
11433
+ const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
11434
+ const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
11435
+ for await (const event of harness.runWithTelemetry({
11436
+ task: cronJob.task,
11437
+ conversationId: convId,
11438
+ parameters: { __activeConversationId: convId },
11439
+ messages: historyMessages,
11440
+ abortSignal: abortController.signal
11441
+ })) {
11442
+ if (event.type === "run:started") {
11443
+ latestRunId = event.runId;
11221
11444
  }
11222
- assistantResponse += event.content;
11223
- currentText += event.content;
11224
- }
11225
- if (event.type === "tool:started") {
11226
- if (currentText.length > 0) {
11227
- sections.push({ type: "text", content: currentText });
11228
- currentText = "";
11445
+ if (event.type === "model:chunk") {
11446
+ if (currentTools.length > 0) {
11447
+ sections.push({ type: "tools", content: currentTools });
11448
+ currentTools = [];
11449
+ if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
11450
+ assistantResponse += " ";
11451
+ }
11452
+ }
11453
+ assistantResponse += event.content;
11454
+ currentText += event.content;
11229
11455
  }
11230
- const toolText = `- start \`${event.tool}\``;
11231
- toolTimeline.push(toolText);
11232
- currentTools.push(toolText);
11456
+ if (event.type === "tool:started") {
11457
+ if (currentText.length > 0) {
11458
+ sections.push({ type: "text", content: currentText });
11459
+ currentText = "";
11460
+ }
11461
+ const toolText = `- start \`${event.tool}\``;
11462
+ toolTimeline.push(toolText);
11463
+ currentTools.push(toolText);
11464
+ }
11465
+ if (event.type === "tool:completed") {
11466
+ const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
11467
+ toolTimeline.push(toolText);
11468
+ currentTools.push(toolText);
11469
+ }
11470
+ if (event.type === "tool:error") {
11471
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
11472
+ toolTimeline.push(toolText);
11473
+ currentTools.push(toolText);
11474
+ }
11475
+ if (event.type === "run:completed") {
11476
+ runResult = {
11477
+ status: event.result.status,
11478
+ steps: event.result.steps,
11479
+ continuation: event.result.continuation,
11480
+ contextTokens: event.result.contextTokens,
11481
+ contextWindow: event.result.contextWindow
11482
+ };
11483
+ if (!assistantResponse && event.result.response) {
11484
+ assistantResponse = event.result.response;
11485
+ }
11486
+ }
11487
+ broadcastEvent(convId, event);
11488
+ await telemetry.emit(event);
11233
11489
  }
11234
- if (event.type === "tool:completed") {
11235
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
11236
- toolTimeline.push(toolText);
11237
- currentTools.push(toolText);
11490
+ finishConversationStream(convId);
11491
+ if (currentTools.length > 0) {
11492
+ sections.push({ type: "tools", content: currentTools });
11238
11493
  }
11239
- if (event.type === "tool:error") {
11240
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
11241
- toolTimeline.push(toolText);
11242
- currentTools.push(toolText);
11494
+ if (currentText.length > 0) {
11495
+ sections.push({ type: "text", content: currentText });
11496
+ currentText = "";
11243
11497
  }
11244
- if (event.type === "run:completed") {
11245
- runResult = {
11246
- status: event.result.status,
11247
- steps: event.result.steps,
11248
- continuation: event.result.continuation
11249
- };
11250
- if (!assistantResponse && event.result.response) {
11251
- assistantResponse = event.result.response;
11498
+ const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
11499
+ const assistantMetadata = toolTimeline.length > 0 || sections.length > 0 ? {
11500
+ toolActivity: [...toolTimeline],
11501
+ sections: sections.length > 0 ? sections : void 0
11502
+ } : void 0;
11503
+ const messages = [
11504
+ ...historyMessages,
11505
+ ...continueConversationId ? [] : [{ role: "user", content: cronJob.task }],
11506
+ ...hasContent ? [{ role: "assistant", content: assistantResponse, metadata: assistantMetadata }] : []
11507
+ ];
11508
+ const freshConv = await conversationStore.get(convId);
11509
+ if (freshConv) {
11510
+ freshConv.messages = messages;
11511
+ freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
11512
+ if (runResult.contextTokens) freshConv.contextTokens = runResult.contextTokens;
11513
+ if (runResult.contextWindow) freshConv.contextWindow = runResult.contextWindow;
11514
+ freshConv.updatedAt = Date.now();
11515
+ await conversationStore.update(freshConv);
11516
+ }
11517
+ if (runResult.continuation && softDeadlineMs > 0) {
11518
+ const selfUrl = `http://${request.headers.host ?? "localhost"}${pathname}?continue=${encodeURIComponent(convId)}&continuation=${continuationCount + 1}`;
11519
+ try {
11520
+ const selfRes = await fetch(selfUrl, {
11521
+ method: "GET",
11522
+ headers: request.headers.authorization ? { authorization: request.headers.authorization } : {}
11523
+ });
11524
+ const selfBody = await selfRes.json();
11525
+ writeJson(response, 200, {
11526
+ conversationId: convId,
11527
+ status: "continued",
11528
+ continuations: continuationCount + 1,
11529
+ finalResult: selfBody,
11530
+ duration: Date.now() - start
11531
+ });
11532
+ } catch (continueError) {
11533
+ writeJson(response, 200, {
11534
+ conversationId: convId,
11535
+ status: "continuation_failed",
11536
+ error: continueError instanceof Error ? continueError.message : "Unknown error",
11537
+ duration: Date.now() - start,
11538
+ steps: runResult.steps
11539
+ });
11252
11540
  }
11541
+ return;
11253
11542
  }
11254
- await telemetry.emit(event);
11255
- }
11256
- if (currentTools.length > 0) {
11257
- sections.push({ type: "tools", content: currentTools });
11258
- }
11259
- if (currentText.length > 0) {
11260
- sections.push({ type: "text", content: currentText });
11261
- currentText = "";
11262
- }
11263
- const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
11264
- const assistantMetadata = toolTimeline.length > 0 || sections.length > 0 ? {
11265
- toolActivity: [...toolTimeline],
11266
- sections: sections.length > 0 ? sections : void 0
11267
- } : void 0;
11268
- const messages = [
11269
- ...historyMessages,
11270
- ...continueConversationId ? [] : [{ role: "user", content: cronJob.task }],
11271
- ...hasContent ? [{ role: "assistant", content: assistantResponse, metadata: assistantMetadata }] : []
11272
- ];
11273
- conversation.messages = messages;
11274
- conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
11275
- conversation.updatedAt = Date.now();
11276
- await conversationStore.update(conversation);
11277
- if (runResult.continuation && softDeadlineMs > 0) {
11278
- const selfUrl = `http://${request.headers.host ?? "localhost"}${pathname}?continue=${encodeURIComponent(conversation.conversationId)}&continuation=${continuationCount + 1}`;
11279
- try {
11280
- const selfRes = await fetch(selfUrl, {
11281
- method: "GET",
11282
- headers: request.headers.authorization ? { authorization: request.headers.authorization } : {}
11283
- });
11284
- const selfBody = await selfRes.json();
11285
- writeJson(response, 200, {
11286
- conversationId: conversation.conversationId,
11287
- status: "continued",
11288
- continuations: continuationCount + 1,
11289
- finalResult: selfBody,
11290
- duration: Date.now() - start
11291
- });
11292
- } catch (continueError) {
11293
- writeJson(response, 200, {
11294
- conversationId: conversation.conversationId,
11295
- status: "continuation_failed",
11296
- error: continueError instanceof Error ? continueError.message : "Unknown error",
11297
- duration: Date.now() - start,
11298
- steps: runResult.steps
11299
- });
11543
+ writeJson(response, 200, {
11544
+ conversationId: convId,
11545
+ status: runResult.status,
11546
+ response: assistantResponse.slice(0, 500),
11547
+ duration: Date.now() - start,
11548
+ steps: runResult.steps
11549
+ });
11550
+ } finally {
11551
+ activeConversationRuns.delete(convId);
11552
+ const hadDeferred = pendingCallbackNeeded.delete(convId);
11553
+ const checkConv = await conversationStore.get(convId);
11554
+ if (hadDeferred || checkConv?.pendingSubagentResults?.length) {
11555
+ if (isServerless) {
11556
+ selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(convId)}/subagent-callback`).catch(
11557
+ (err) => console.error(`[cron] subagent callback self-fetch failed:`, err instanceof Error ? err.message : err)
11558
+ );
11559
+ } else {
11560
+ processSubagentCallback(convId, true).catch(
11561
+ (err) => console.error(`[cron] subagent callback failed:`, err instanceof Error ? err.message : err)
11562
+ );
11563
+ }
11300
11564
  }
11301
- return;
11302
11565
  }
11303
- writeJson(response, 200, {
11304
- conversationId: conversation.conversationId,
11305
- status: runResult.status,
11306
- response: assistantResponse.slice(0, 500),
11307
- duration: Date.now() - start,
11308
- steps: runResult.steps
11309
- });
11310
11566
  } catch (error) {
11311
11567
  writeJson(response, 500, {
11312
11568
  code: "CRON_RUN_ERROR",
@@ -11321,6 +11577,11 @@ ${cronJob.task}`;
11321
11577
  handler._cronJobs = cronJobs;
11322
11578
  handler._conversationStore = conversationStore;
11323
11579
  handler._messagingAdapters = messagingAdapters;
11580
+ handler._activeConversationRuns = activeConversationRuns;
11581
+ handler._pendingCallbackNeeded = pendingCallbackNeeded;
11582
+ handler._processSubagentCallback = processSubagentCallback;
11583
+ handler._broadcastEvent = broadcastEvent;
11584
+ handler._finishConversationStream = finishConversationStream;
11324
11585
  const STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
11325
11586
  try {
11326
11587
  const allSummaries = await conversationStore.listSummaries();
@@ -11371,9 +11632,11 @@ var startDevServer = async (port, options) => {
11371
11632
  await checkVercelCronDrift(workingDir);
11372
11633
  const { Cron } = await import("croner");
11373
11634
  let activeJobs = [];
11374
- const runCronAgent = async (harnessRef, task, conversationId, historyMessages) => {
11635
+ const runCronAgent = async (harnessRef, task, conversationId, historyMessages, onEvent) => {
11375
11636
  let assistantResponse = "";
11376
11637
  let steps = 0;
11638
+ let contextTokens = 0;
11639
+ let contextWindow = 0;
11377
11640
  const toolTimeline = [];
11378
11641
  const sections = [];
11379
11642
  let currentTools = [];
@@ -11384,10 +11647,14 @@ var startDevServer = async (port, options) => {
11384
11647
  parameters: { __activeConversationId: conversationId },
11385
11648
  messages: historyMessages
11386
11649
  })) {
11650
+ onEvent?.(event);
11387
11651
  if (event.type === "model:chunk") {
11388
11652
  if (currentTools.length > 0) {
11389
11653
  sections.push({ type: "tools", content: currentTools });
11390
11654
  currentTools = [];
11655
+ if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
11656
+ assistantResponse += " ";
11657
+ }
11391
11658
  }
11392
11659
  assistantResponse += event.content;
11393
11660
  currentText += event.content;
@@ -11413,6 +11680,8 @@ var startDevServer = async (port, options) => {
11413
11680
  }
11414
11681
  if (event.type === "run:completed") {
11415
11682
  steps = event.result.steps;
11683
+ contextTokens = event.result.contextTokens ?? 0;
11684
+ contextWindow = event.result.contextWindow ?? 0;
11416
11685
  if (!assistantResponse && event.result.response) {
11417
11686
  assistantResponse = event.result.response;
11418
11687
  }
@@ -11429,7 +11698,7 @@ var startDevServer = async (port, options) => {
11429
11698
  toolActivity: [...toolTimeline],
11430
11699
  sections: sections.length > 0 ? sections : void 0
11431
11700
  } : void 0;
11432
- return { response: assistantResponse, steps, assistantMetadata, hasContent };
11701
+ return { response: assistantResponse, steps, assistantMetadata, hasContent, contextTokens, contextWindow };
11433
11702
  };
11434
11703
  const buildCronMessages = (task, historyMessages, result) => [
11435
11704
  ...historyMessages,
@@ -11446,6 +11715,9 @@ var startDevServer = async (port, options) => {
11446
11715
  const harnessRef = handler._harness;
11447
11716
  const store = handler._conversationStore;
11448
11717
  const adapters = handler._messagingAdapters;
11718
+ const activeRuns = handler._activeConversationRuns;
11719
+ const deferredCallbacks = handler._pendingCallbackNeeded;
11720
+ const runCallback = handler._processSubagentCallback;
11449
11721
  if (!harnessRef || !store) return;
11450
11722
  for (const [jobName, config] of entries) {
11451
11723
  const job = new Cron(
@@ -11486,24 +11758,43 @@ var startDevServer = async (port, options) => {
11486
11758
  const task = `[Scheduled: ${jobName}]
11487
11759
  ${config.task}`;
11488
11760
  const historyMessages = [...conversation.messages];
11761
+ const convId = conversation.conversationId;
11762
+ activeRuns?.set(convId, {
11763
+ ownerId: "local-owner",
11764
+ abortController: new AbortController(),
11765
+ runId: null
11766
+ });
11489
11767
  try {
11490
- const result = await runCronAgent(harnessRef, task, conversation.conversationId, historyMessages);
11491
- conversation.messages = buildCronMessages(task, historyMessages, result);
11492
- conversation.updatedAt = Date.now();
11493
- await store.update(conversation);
11494
- if (result.response) {
11495
- try {
11496
- await adapter.sendReply(
11497
- {
11498
- channelId: chatId,
11499
- platformThreadId: conversation.channelMeta?.platformThreadId ?? chatId
11500
- },
11501
- result.response
11502
- );
11503
- } catch (sendError) {
11504
- const sendMsg = sendError instanceof Error ? sendError.message : String(sendError);
11505
- process.stderr.write(`[cron] ${jobName}: send to ${chatId} failed: ${sendMsg}
11768
+ const broadcastCh = handler._broadcastEvent;
11769
+ const result = await runCronAgent(
11770
+ harnessRef,
11771
+ task,
11772
+ convId,
11773
+ historyMessages,
11774
+ broadcastCh ? (ev) => broadcastCh(convId, ev) : void 0
11775
+ );
11776
+ handler._finishConversationStream?.(convId);
11777
+ const freshConv = await store.get(convId);
11778
+ if (freshConv) {
11779
+ freshConv.messages = buildCronMessages(task, historyMessages, result);
11780
+ if (result.contextTokens > 0) freshConv.contextTokens = result.contextTokens;
11781
+ if (result.contextWindow > 0) freshConv.contextWindow = result.contextWindow;
11782
+ freshConv.updatedAt = Date.now();
11783
+ await store.update(freshConv);
11784
+ if (result.response) {
11785
+ try {
11786
+ await adapter.sendReply(
11787
+ {
11788
+ channelId: chatId,
11789
+ platformThreadId: freshConv.channelMeta?.platformThreadId ?? chatId
11790
+ },
11791
+ result.response
11792
+ );
11793
+ } catch (sendError) {
11794
+ const sendMsg = sendError instanceof Error ? sendError.message : String(sendError);
11795
+ process.stderr.write(`[cron] ${jobName}: send to ${chatId} failed: ${sendMsg}
11506
11796
  `);
11797
+ }
11507
11798
  }
11508
11799
  }
11509
11800
  totalChats++;
@@ -11511,6 +11802,15 @@ ${config.task}`;
11511
11802
  const runMsg = runError instanceof Error ? runError.message : String(runError);
11512
11803
  process.stderr.write(`[cron] ${jobName}: run for chat ${chatId} failed: ${runMsg}
11513
11804
  `);
11805
+ } finally {
11806
+ activeRuns?.delete(convId);
11807
+ const hadDeferred = deferredCallbacks?.delete(convId) ?? false;
11808
+ const checkConv = await store.get(convId);
11809
+ if (hadDeferred || checkConv?.pendingSubagentResults?.length) {
11810
+ runCallback?.(convId, true).catch(
11811
+ (err) => console.error(`[cron] ${jobName}: subagent callback for ${chatId} failed:`, err instanceof Error ? err.message : err)
11812
+ );
11813
+ }
11514
11814
  }
11515
11815
  }
11516
11816
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
@@ -11524,15 +11824,35 @@ ${config.task}`;
11524
11824
  }
11525
11825
  return;
11526
11826
  }
11827
+ let cronConvId;
11527
11828
  try {
11528
11829
  const conversation = await store.create(
11529
11830
  "local-owner",
11530
11831
  `[cron] ${jobName} ${timestamp}`
11531
11832
  );
11532
- const result = await runCronAgent(harnessRef, config.task, conversation.conversationId, []);
11533
- conversation.messages = buildCronMessages(config.task, [], result);
11534
- conversation.updatedAt = Date.now();
11535
- await store.update(conversation);
11833
+ cronConvId = conversation.conversationId;
11834
+ activeRuns?.set(cronConvId, {
11835
+ ownerId: "local-owner",
11836
+ abortController: new AbortController(),
11837
+ runId: null
11838
+ });
11839
+ const broadcast = handler._broadcastEvent;
11840
+ const result = await runCronAgent(
11841
+ harnessRef,
11842
+ config.task,
11843
+ cronConvId,
11844
+ [],
11845
+ broadcast ? (ev) => broadcast(cronConvId, ev) : void 0
11846
+ );
11847
+ handler._finishConversationStream?.(cronConvId);
11848
+ const freshConv = await store.get(cronConvId);
11849
+ if (freshConv) {
11850
+ freshConv.messages = buildCronMessages(config.task, [], result);
11851
+ if (result.contextTokens > 0) freshConv.contextTokens = result.contextTokens;
11852
+ if (result.contextWindow > 0) freshConv.contextWindow = result.contextWindow;
11853
+ freshConv.updatedAt = Date.now();
11854
+ await store.update(freshConv);
11855
+ }
11536
11856
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
11537
11857
  process.stdout.write(
11538
11858
  `[cron] ${jobName} completed in ${elapsed}s (${result.steps} steps)
@@ -11545,6 +11865,17 @@ ${config.task}`;
11545
11865
  `[cron] ${jobName} failed after ${elapsed}s: ${msg}
11546
11866
  `
11547
11867
  );
11868
+ } finally {
11869
+ if (cronConvId) {
11870
+ activeRuns?.delete(cronConvId);
11871
+ const hadDeferred = deferredCallbacks?.delete(cronConvId) ?? false;
11872
+ const checkConv = await store.get(cronConvId);
11873
+ if (hadDeferred || checkConv?.pendingSubagentResults?.length) {
11874
+ runCallback?.(cronConvId, true).catch(
11875
+ (err) => console.error(`[cron] ${jobName}: subagent callback failed:`, err instanceof Error ? err.message : err)
11876
+ );
11877
+ }
11878
+ }
11548
11879
  }
11549
11880
  }
11550
11881
  );
@@ -11652,7 +11983,7 @@ var runInteractive = async (workingDir, params) => {
11652
11983
  await harness.initialize();
11653
11984
  const identity = await ensureAgentIdentity2(workingDir);
11654
11985
  try {
11655
- const { runInteractiveInk } = await import("./run-interactive-ink-V4KWIUHB.js");
11986
+ const { runInteractiveInk } = await import("./run-interactive-ink-ZSIGWFLZ.js");
11656
11987
  await runInteractiveInk({
11657
11988
  harness,
11658
11989
  params,