@poncho-ai/cli 0.30.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;
@@ -1613,6 +1648,8 @@ var WEB_UI_STYLES = `
1613
1648
  line-height: 1.45;
1614
1649
  color: var(--fg-tool-code);
1615
1650
  width: 100%;
1651
+ min-width: 0;
1652
+ overflow: hidden;
1616
1653
  }
1617
1654
  .subagent-result-summary {
1618
1655
  list-style: none;
@@ -1657,6 +1694,19 @@ var WEB_UI_STYLES = `
1657
1694
  display: grid;
1658
1695
  gap: 6px;
1659
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;
1660
1710
  }
1661
1711
 
1662
1712
  /* Todo panel \u2014 inside composer-inner, above the input shell */
@@ -1809,6 +1859,8 @@ var getWebUiClientScript = (markedSource2) => `
1809
1859
  parentConversationId: null,
1810
1860
  todos: [],
1811
1861
  todoPanelCollapsed: false,
1862
+ cronSectionCollapsed: true,
1863
+ cronShowAll: false,
1812
1864
  };
1813
1865
 
1814
1866
  const agentInitial = document.body.dataset.agentInitial || "A";
@@ -2552,11 +2604,86 @@ var getWebUiClientScript = (markedSource2) => `
2552
2604
  }
2553
2605
  };
2554
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
+
2555
2678
  const renderConversationList = () => {
2556
2679
  elements.list.innerHTML = "";
2557
2680
  const pending = state.conversations.filter(c => c.hasPendingApprovals);
2558
2681
  const rest = state.conversations.filter(c => !c.hasPendingApprovals);
2559
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
+
2560
2687
  if (pending.length > 0) {
2561
2688
  const label = document.createElement("div");
2562
2689
  label.className = "sidebar-section-label";
@@ -2575,7 +2702,7 @@ var getWebUiClientScript = (markedSource2) => `
2575
2702
  const latest = [];
2576
2703
  const previous7 = [];
2577
2704
  const older = [];
2578
- for (const c of rest) {
2705
+ for (const c of nonCron) {
2579
2706
  const ts = c.updatedAt || c.createdAt || 0;
2580
2707
  if (ts >= startOfToday) {
2581
2708
  latest.push(c);
@@ -2587,6 +2714,12 @@ var getWebUiClientScript = (markedSource2) => `
2587
2714
  }
2588
2715
 
2589
2716
  let sectionRendered = pending.length > 0;
2717
+
2718
+ if (cronConvs.length > 0) {
2719
+ appendCronSection(cronConvs, sectionRendered);
2720
+ sectionRendered = true;
2721
+ }
2722
+
2590
2723
  const appendSection = (items, labelText) => {
2591
2724
  if (items.length === 0) return;
2592
2725
  if (sectionRendered) {
@@ -4756,7 +4889,7 @@ var getWebUiClientScript = (markedSource2) => `
4756
4889
  state: "resolved",
4757
4890
  resolvedDecision: decision,
4758
4891
  }));
4759
- api("/api/approvals/" + encodeURIComponent(approvalId), {
4892
+ return api("/api/approvals/" + encodeURIComponent(approvalId), {
4760
4893
  method: "POST",
4761
4894
  body: JSON.stringify({ approved: decision === "approve" }),
4762
4895
  }).catch((error) => {
@@ -4804,16 +4937,54 @@ var getWebUiClientScript = (markedSource2) => `
4804
4937
  if (pending.length === 0) return;
4805
4938
  const wasStreaming = state.isStreaming;
4806
4939
  if (!wasStreaming) setStreaming(true);
4807
- 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
+ }
4808
4949
  renderMessages(state.activeMessages, state.isStreaming);
4809
4950
  loadConversations();
4810
- if (!wasStreaming && state.activeConversationId) {
4811
- const cid = state.activeConversationId;
4812
- await streamConversationEvents(cid, { liveOnly: true });
4813
- if (state.activeConversationId === cid) {
4814
- pollUntilRunIdle(cid);
4815
- }
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
+ });
4816
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
+ })();
4817
4988
  return;
4818
4989
  }
4819
4990
 
@@ -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);
@@ -8646,8 +8818,10 @@ data: ${JSON.stringify(statusPayload)}
8646
8818
  }
8647
8819
  };
8648
8820
  const MAX_SUBAGENT_CALLBACK_COUNT = 20;
8821
+ const pendingCallbackNeeded = /* @__PURE__ */ new Set();
8649
8822
  const triggerParentCallback = async (parentConversationId) => {
8650
8823
  if (activeConversationRuns.has(parentConversationId)) {
8824
+ pendingCallbackNeeded.add(parentConversationId);
8651
8825
  return;
8652
8826
  }
8653
8827
  if (isServerless) {
@@ -8659,12 +8833,12 @@ data: ${JSON.stringify(statusPayload)}
8659
8833
  await processSubagentCallback(parentConversationId);
8660
8834
  };
8661
8835
  const CALLBACK_LOCK_STALE_MS = 5 * 60 * 1e3;
8662
- const processSubagentCallback = async (conversationId) => {
8836
+ const processSubagentCallback = async (conversationId, skipLockCheck = false) => {
8663
8837
  const conversation = await conversationStore.get(conversationId);
8664
8838
  if (!conversation) return;
8665
8839
  const pendingResults = conversation.pendingSubagentResults ?? [];
8666
8840
  if (pendingResults.length === 0) return;
8667
- if (conversation.runningCallbackSince) {
8841
+ if (!skipLockCheck && conversation.runningCallbackSince) {
8668
8842
  const elapsed = Date.now() - conversation.runningCallbackSince;
8669
8843
  if (elapsed < CALLBACK_LOCK_STALE_MS) {
8670
8844
  return;
@@ -8702,11 +8876,24 @@ ${resultBody}`,
8702
8876
  abortController,
8703
8877
  runId: null
8704
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
+ }
8705
8890
  const historyMessages = [...conversation.messages];
8706
8891
  let assistantResponse = "";
8707
8892
  let latestRunId = "";
8708
8893
  let runContinuation = false;
8709
8894
  let runContinuationMessages;
8895
+ let runContextTokens = conversation.contextTokens ?? 0;
8896
+ let runContextWindow = conversation.contextWindow ?? 0;
8710
8897
  const toolTimeline = [];
8711
8898
  const sections = [];
8712
8899
  let currentTools = [];
@@ -8761,6 +8948,8 @@ ${resultBody}`,
8761
8948
  if (assistantResponse.length === 0 && event.result.response) {
8762
8949
  assistantResponse = event.result.response;
8763
8950
  }
8951
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
8952
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
8764
8953
  if (event.result.continuation) {
8765
8954
  runContinuation = true;
8766
8955
  if (event.result.continuationMessages) {
@@ -8787,6 +8976,8 @@ ${resultBody}`,
8787
8976
  }
8788
8977
  freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
8789
8978
  freshConv.runningCallbackSince = void 0;
8979
+ if (runContextTokens > 0) freshConv.contextTokens = runContextTokens;
8980
+ if (runContextWindow > 0) freshConv.contextWindow = runContextWindow;
8790
8981
  freshConv.updatedAt = Date.now();
8791
8982
  await conversationStore.update(freshConv);
8792
8983
  if (freshConv.channelMeta && assistantResponse.length > 0) {
@@ -8824,23 +9015,32 @@ ${resultBody}`,
8824
9015
  } finally {
8825
9016
  activeConversationRuns.delete(conversationId);
8826
9017
  finishConversationStream(conversationId);
9018
+ const hadDeferredTrigger = pendingCallbackNeeded.delete(conversationId);
8827
9019
  const freshConv = await conversationStore.get(conversationId);
8828
- if (freshConv) {
8829
- if (freshConv.runningCallbackSince) {
8830
- freshConv.runningCallbackSince = void 0;
8831
- await conversationStore.update(freshConv);
8832
- }
8833
- }
8834
- if (freshConv?.pendingSubagentResults?.length) {
9020
+ const hasPendingInStore = !!freshConv?.pendingSubagentResults?.length;
9021
+ if (hadDeferredTrigger || hasPendingInStore) {
8835
9022
  if (isServerless) {
8836
9023
  selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(
8837
9024
  (err) => console.error(`[poncho][subagent-callback] Recursive callback self-fetch failed:`, err instanceof Error ? err.message : err)
8838
9025
  );
8839
9026
  } else {
8840
- processSubagentCallback(conversationId).catch(
9027
+ processSubagentCallback(conversationId, true).catch(
8841
9028
  (err) => console.error(`[poncho][subagent-callback] Recursive callback failed:`, err instanceof Error ? err.message : err)
8842
9029
  );
8843
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
+ }
8844
9044
  }
8845
9045
  }
8846
9046
  };
@@ -9007,14 +9207,6 @@ ${resultBody}`,
9007
9207
  if (active && active.abortController === abortController) {
9008
9208
  active.runId = event.runId;
9009
9209
  }
9010
- if (typeof event.contextWindow === "number" && event.contextWindow > 0) {
9011
- runContextWindow = event.contextWindow;
9012
- }
9013
- }
9014
- if (event.type === "model:response") {
9015
- if (typeof event.usage?.input === "number") {
9016
- runContextTokens = event.usage.input;
9017
- }
9018
9210
  }
9019
9211
  if (event.type === "model:chunk") {
9020
9212
  if (currentTools.length > 0) {
@@ -9079,8 +9271,12 @@ ${resultBody}`,
9079
9271
  }
9080
9272
  checkpointedRun = true;
9081
9273
  }
9082
- if (event.type === "run:completed" && assistantResponse.length === 0 && event.result.response) {
9083
- 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;
9084
9280
  }
9085
9281
  if (event.type === "run:error") {
9086
9282
  assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
@@ -9163,6 +9359,13 @@ ${resultBody}`,
9163
9359
  runConversations.delete(latestRunId);
9164
9360
  }
9165
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
+ }
9166
9369
  };
9167
9370
  const messagingRoutes = /* @__PURE__ */ new Map();
9168
9371
  const messagingRouteRegistrar = (method, path, routeHandler) => {
@@ -9293,14 +9496,6 @@ ${resultBody}`,
9293
9496
  latestRunId = event.runId;
9294
9497
  runOwners.set(event.runId, "local-owner");
9295
9498
  runConversations.set(event.runId, conversationId);
9296
- if (typeof event.contextWindow === "number" && event.contextWindow > 0) {
9297
- runContextWindow = event.contextWindow;
9298
- }
9299
- }
9300
- if (event.type === "model:response") {
9301
- if (typeof event.usage?.input === "number") {
9302
- runContextTokens = event.usage.input;
9303
- }
9304
9499
  }
9305
9500
  if (event.type === "model:chunk") {
9306
9501
  if (currentTools.length > 0) {
@@ -9399,6 +9594,8 @@ ${resultBody}`,
9399
9594
  }
9400
9595
  runSteps = event.result.steps;
9401
9596
  if (typeof event.result.maxSteps === "number") runMaxSteps = event.result.maxSteps;
9597
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
9598
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
9402
9599
  }
9403
9600
  if (event.type === "run:error") {
9404
9601
  assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
@@ -10187,18 +10384,29 @@ data: ${JSON.stringify(frame)}
10187
10384
  });
10188
10385
  return;
10189
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);
10190
10393
  foundApproval.decision = approved ? "approved" : "denied";
10191
10394
  broadcastEvent(
10192
10395
  conversationId,
10193
10396
  approved ? { type: "tool:approval:granted", approvalId } : { type: "tool:approval:denied", approvalId }
10194
10397
  );
10195
10398
  const allApprovals = foundConversation.pendingApprovals ?? [];
10196
- const allDecided = allApprovals.length > 0 && allApprovals.every((a) => a.decision != null);
10399
+ const allDecided = allApprovals.length > 0 && allApprovals.every((a) => batchDecisions.has(a.approvalId));
10197
10400
  if (!allDecided) {
10198
10401
  await conversationStore.update(foundConversation);
10199
10402
  writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
10200
10403
  return;
10201
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);
10202
10410
  foundConversation.pendingApprovals = [];
10203
10411
  foundConversation.runStatus = "running";
10204
10412
  await conversationStore.update(foundConversation);
@@ -10820,14 +11028,6 @@ data: ${JSON.stringify(frame)}
10820
11028
  if (active && active.abortController === abortController) {
10821
11029
  active.runId = event.runId;
10822
11030
  }
10823
- if (typeof event.contextWindow === "number" && event.contextWindow > 0) {
10824
- runContextWindow = event.contextWindow;
10825
- }
10826
- }
10827
- if (event.type === "model:response") {
10828
- if (typeof event.usage?.input === "number") {
10829
- runContextTokens = event.usage.input;
10830
- }
10831
11031
  }
10832
11032
  if (event.type === "run:cancelled") {
10833
11033
  runCancelled = true;
@@ -10920,6 +11120,8 @@ data: ${JSON.stringify(frame)}
10920
11120
  if (assistantResponse.length === 0 && event.result.response) {
10921
11121
  assistantResponse = event.result.response;
10922
11122
  }
11123
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
11124
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
10923
11125
  if (event.result.continuation && event.result.continuationMessages) {
10924
11126
  runContinuationMessages = event.result.continuationMessages;
10925
11127
  conversation._continuationMessages = runContinuationMessages;
@@ -11059,9 +11261,10 @@ data: ${JSON.stringify(frame)}
11059
11261
  response.end();
11060
11262
  } catch {
11061
11263
  }
11264
+ const hadDeferred = pendingCallbackNeeded.delete(conversationId);
11062
11265
  const freshConv = await conversationStore.get(conversationId);
11063
- if (freshConv?.pendingSubagentResults?.length) {
11064
- processSubagentCallback(conversationId).catch(
11266
+ if (hadDeferred || freshConv?.pendingSubagentResults?.length) {
11267
+ processSubagentCallback(conversationId, true).catch(
11065
11268
  (err) => console.error(`[poncho][subagent-callback] Post-run callback failed:`, err instanceof Error ? err.message : err)
11066
11269
  );
11067
11270
  }
@@ -11209,125 +11412,157 @@ ${cronJob.task}`;
11209
11412
  `[cron] ${jobName} ${timestamp}`
11210
11413
  );
11211
11414
  }
11212
- const abortController = new AbortController();
11213
- let assistantResponse = "";
11214
- let latestRunId = "";
11215
- const toolTimeline = [];
11216
- const sections = [];
11217
- let currentTools = [];
11218
- let currentText = "";
11219
- let runResult = {
11220
- status: "completed",
11221
- steps: 0
11222
- };
11223
- const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
11224
- const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
11225
- for await (const event of harness.runWithTelemetry({
11226
- task: cronJob.task,
11227
- conversationId: conversation.conversationId,
11228
- parameters: { __activeConversationId: conversation.conversationId },
11229
- messages: historyMessages,
11230
- abortSignal: abortController.signal
11231
- })) {
11232
- if (event.type === "run:started") {
11233
- latestRunId = event.runId;
11234
- }
11235
- if (event.type === "model:chunk") {
11236
- if (currentTools.length > 0) {
11237
- sections.push({ type: "tools", content: currentTools });
11238
- currentTools = [];
11239
- if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
11240
- assistantResponse += " ";
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;
11444
+ }
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
+ }
11241
11452
  }
11453
+ assistantResponse += event.content;
11454
+ currentText += event.content;
11242
11455
  }
11243
- assistantResponse += event.content;
11244
- currentText += event.content;
11245
- }
11246
- if (event.type === "tool:started") {
11247
- if (currentText.length > 0) {
11248
- sections.push({ type: "text", content: currentText });
11249
- currentText = "";
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);
11250
11464
  }
11251
- const toolText = `- start \`${event.tool}\``;
11252
- toolTimeline.push(toolText);
11253
- currentTools.push(toolText);
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);
11254
11489
  }
11255
- if (event.type === "tool:completed") {
11256
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
11257
- toolTimeline.push(toolText);
11258
- currentTools.push(toolText);
11490
+ finishConversationStream(convId);
11491
+ if (currentTools.length > 0) {
11492
+ sections.push({ type: "tools", content: currentTools });
11259
11493
  }
11260
- if (event.type === "tool:error") {
11261
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
11262
- toolTimeline.push(toolText);
11263
- currentTools.push(toolText);
11494
+ if (currentText.length > 0) {
11495
+ sections.push({ type: "text", content: currentText });
11496
+ currentText = "";
11264
11497
  }
11265
- if (event.type === "run:completed") {
11266
- runResult = {
11267
- status: event.result.status,
11268
- steps: event.result.steps,
11269
- continuation: event.result.continuation
11270
- };
11271
- if (!assistantResponse && event.result.response) {
11272
- 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
+ });
11273
11540
  }
11541
+ return;
11274
11542
  }
11275
- await telemetry.emit(event);
11276
- }
11277
- if (currentTools.length > 0) {
11278
- sections.push({ type: "tools", content: currentTools });
11279
- }
11280
- if (currentText.length > 0) {
11281
- sections.push({ type: "text", content: currentText });
11282
- currentText = "";
11283
- }
11284
- const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
11285
- const assistantMetadata = toolTimeline.length > 0 || sections.length > 0 ? {
11286
- toolActivity: [...toolTimeline],
11287
- sections: sections.length > 0 ? sections : void 0
11288
- } : void 0;
11289
- const messages = [
11290
- ...historyMessages,
11291
- ...continueConversationId ? [] : [{ role: "user", content: cronJob.task }],
11292
- ...hasContent ? [{ role: "assistant", content: assistantResponse, metadata: assistantMetadata }] : []
11293
- ];
11294
- conversation.messages = messages;
11295
- conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
11296
- conversation.updatedAt = Date.now();
11297
- await conversationStore.update(conversation);
11298
- if (runResult.continuation && softDeadlineMs > 0) {
11299
- const selfUrl = `http://${request.headers.host ?? "localhost"}${pathname}?continue=${encodeURIComponent(conversation.conversationId)}&continuation=${continuationCount + 1}`;
11300
- try {
11301
- const selfRes = await fetch(selfUrl, {
11302
- method: "GET",
11303
- headers: request.headers.authorization ? { authorization: request.headers.authorization } : {}
11304
- });
11305
- const selfBody = await selfRes.json();
11306
- writeJson(response, 200, {
11307
- conversationId: conversation.conversationId,
11308
- status: "continued",
11309
- continuations: continuationCount + 1,
11310
- finalResult: selfBody,
11311
- duration: Date.now() - start
11312
- });
11313
- } catch (continueError) {
11314
- writeJson(response, 200, {
11315
- conversationId: conversation.conversationId,
11316
- status: "continuation_failed",
11317
- error: continueError instanceof Error ? continueError.message : "Unknown error",
11318
- duration: Date.now() - start,
11319
- steps: runResult.steps
11320
- });
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
+ }
11321
11564
  }
11322
- return;
11323
11565
  }
11324
- writeJson(response, 200, {
11325
- conversationId: conversation.conversationId,
11326
- status: runResult.status,
11327
- response: assistantResponse.slice(0, 500),
11328
- duration: Date.now() - start,
11329
- steps: runResult.steps
11330
- });
11331
11566
  } catch (error) {
11332
11567
  writeJson(response, 500, {
11333
11568
  code: "CRON_RUN_ERROR",
@@ -11342,6 +11577,11 @@ ${cronJob.task}`;
11342
11577
  handler._cronJobs = cronJobs;
11343
11578
  handler._conversationStore = conversationStore;
11344
11579
  handler._messagingAdapters = messagingAdapters;
11580
+ handler._activeConversationRuns = activeConversationRuns;
11581
+ handler._pendingCallbackNeeded = pendingCallbackNeeded;
11582
+ handler._processSubagentCallback = processSubagentCallback;
11583
+ handler._broadcastEvent = broadcastEvent;
11584
+ handler._finishConversationStream = finishConversationStream;
11345
11585
  const STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
11346
11586
  try {
11347
11587
  const allSummaries = await conversationStore.listSummaries();
@@ -11392,9 +11632,11 @@ var startDevServer = async (port, options) => {
11392
11632
  await checkVercelCronDrift(workingDir);
11393
11633
  const { Cron } = await import("croner");
11394
11634
  let activeJobs = [];
11395
- const runCronAgent = async (harnessRef, task, conversationId, historyMessages) => {
11635
+ const runCronAgent = async (harnessRef, task, conversationId, historyMessages, onEvent) => {
11396
11636
  let assistantResponse = "";
11397
11637
  let steps = 0;
11638
+ let contextTokens = 0;
11639
+ let contextWindow = 0;
11398
11640
  const toolTimeline = [];
11399
11641
  const sections = [];
11400
11642
  let currentTools = [];
@@ -11405,6 +11647,7 @@ var startDevServer = async (port, options) => {
11405
11647
  parameters: { __activeConversationId: conversationId },
11406
11648
  messages: historyMessages
11407
11649
  })) {
11650
+ onEvent?.(event);
11408
11651
  if (event.type === "model:chunk") {
11409
11652
  if (currentTools.length > 0) {
11410
11653
  sections.push({ type: "tools", content: currentTools });
@@ -11437,6 +11680,8 @@ var startDevServer = async (port, options) => {
11437
11680
  }
11438
11681
  if (event.type === "run:completed") {
11439
11682
  steps = event.result.steps;
11683
+ contextTokens = event.result.contextTokens ?? 0;
11684
+ contextWindow = event.result.contextWindow ?? 0;
11440
11685
  if (!assistantResponse && event.result.response) {
11441
11686
  assistantResponse = event.result.response;
11442
11687
  }
@@ -11453,7 +11698,7 @@ var startDevServer = async (port, options) => {
11453
11698
  toolActivity: [...toolTimeline],
11454
11699
  sections: sections.length > 0 ? sections : void 0
11455
11700
  } : void 0;
11456
- return { response: assistantResponse, steps, assistantMetadata, hasContent };
11701
+ return { response: assistantResponse, steps, assistantMetadata, hasContent, contextTokens, contextWindow };
11457
11702
  };
11458
11703
  const buildCronMessages = (task, historyMessages, result) => [
11459
11704
  ...historyMessages,
@@ -11470,6 +11715,9 @@ var startDevServer = async (port, options) => {
11470
11715
  const harnessRef = handler._harness;
11471
11716
  const store = handler._conversationStore;
11472
11717
  const adapters = handler._messagingAdapters;
11718
+ const activeRuns = handler._activeConversationRuns;
11719
+ const deferredCallbacks = handler._pendingCallbackNeeded;
11720
+ const runCallback = handler._processSubagentCallback;
11473
11721
  if (!harnessRef || !store) return;
11474
11722
  for (const [jobName, config] of entries) {
11475
11723
  const job = new Cron(
@@ -11510,24 +11758,43 @@ var startDevServer = async (port, options) => {
11510
11758
  const task = `[Scheduled: ${jobName}]
11511
11759
  ${config.task}`;
11512
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
+ });
11513
11767
  try {
11514
- const result = await runCronAgent(harnessRef, task, conversation.conversationId, historyMessages);
11515
- conversation.messages = buildCronMessages(task, historyMessages, result);
11516
- conversation.updatedAt = Date.now();
11517
- await store.update(conversation);
11518
- if (result.response) {
11519
- try {
11520
- await adapter.sendReply(
11521
- {
11522
- channelId: chatId,
11523
- platformThreadId: conversation.channelMeta?.platformThreadId ?? chatId
11524
- },
11525
- result.response
11526
- );
11527
- } catch (sendError) {
11528
- const sendMsg = sendError instanceof Error ? sendError.message : String(sendError);
11529
- 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}
11530
11796
  `);
11797
+ }
11531
11798
  }
11532
11799
  }
11533
11800
  totalChats++;
@@ -11535,6 +11802,15 @@ ${config.task}`;
11535
11802
  const runMsg = runError instanceof Error ? runError.message : String(runError);
11536
11803
  process.stderr.write(`[cron] ${jobName}: run for chat ${chatId} failed: ${runMsg}
11537
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
+ }
11538
11814
  }
11539
11815
  }
11540
11816
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
@@ -11548,15 +11824,35 @@ ${config.task}`;
11548
11824
  }
11549
11825
  return;
11550
11826
  }
11827
+ let cronConvId;
11551
11828
  try {
11552
11829
  const conversation = await store.create(
11553
11830
  "local-owner",
11554
11831
  `[cron] ${jobName} ${timestamp}`
11555
11832
  );
11556
- const result = await runCronAgent(harnessRef, config.task, conversation.conversationId, []);
11557
- conversation.messages = buildCronMessages(config.task, [], result);
11558
- conversation.updatedAt = Date.now();
11559
- 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
+ }
11560
11856
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
11561
11857
  process.stdout.write(
11562
11858
  `[cron] ${jobName} completed in ${elapsed}s (${result.steps} steps)
@@ -11569,6 +11865,17 @@ ${config.task}`;
11569
11865
  `[cron] ${jobName} failed after ${elapsed}s: ${msg}
11570
11866
  `
11571
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
+ }
11572
11879
  }
11573
11880
  }
11574
11881
  );
@@ -11676,7 +11983,7 @@ var runInteractive = async (workingDir, params) => {
11676
11983
  await harness.initialize();
11677
11984
  const identity = await ensureAgentIdentity2(workingDir);
11678
11985
  try {
11679
- const { runInteractiveInk } = await import("./run-interactive-ink-EMTC7MK7.js");
11986
+ const { runInteractiveInk } = await import("./run-interactive-ink-ZSIGWFLZ.js");
11680
11987
  await runInteractiveInk({
11681
11988
  harness,
11682
11989
  params,