@poncho-ai/cli 0.30.0 → 0.30.2

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) {
@@ -3040,6 +3173,101 @@ var getWebUiClientScript = (markedSource2) => `
3040
3173
  });
3041
3174
  } else if (willStream) {
3042
3175
  setStreaming(true);
3176
+ } else if (payload.needsContinuation && !payload.conversation.parentConversationId) {
3177
+ console.log("[poncho] Detected orphaned continuation for", conversationId, "\u2014 auto-resuming");
3178
+ (async () => {
3179
+ try {
3180
+ setStreaming(true);
3181
+ var localMsgs = state.activeMessages || [];
3182
+ var contAssistant = {
3183
+ role: "assistant",
3184
+ content: "",
3185
+ _sections: [],
3186
+ _currentText: "",
3187
+ _currentTools: [],
3188
+ _toolImages: [],
3189
+ _activeActivities: [],
3190
+ _pendingApprovals: [],
3191
+ metadata: { toolActivity: [] }
3192
+ };
3193
+ localMsgs.push(contAssistant);
3194
+ state.activeMessages = localMsgs;
3195
+ state._activeStreamMessages = localMsgs;
3196
+ renderMessages(localMsgs, true);
3197
+ var contResp = await fetch(
3198
+ "/api/conversations/" + encodeURIComponent(conversationId) + "/messages",
3199
+ {
3200
+ method: "POST",
3201
+ credentials: "include",
3202
+ headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
3203
+ body: JSON.stringify({ continuation: true }),
3204
+ },
3205
+ );
3206
+ if (!contResp.ok || !contResp.body) {
3207
+ contAssistant._error = "Failed to resume \u2014 reload to retry";
3208
+ setStreaming(false);
3209
+ renderMessages(localMsgs, false);
3210
+ return;
3211
+ }
3212
+ state.activeStreamConversationId = conversationId;
3213
+ var contReader = contResp.body.getReader();
3214
+ var contDecoder = new TextDecoder();
3215
+ var contBuffer = "";
3216
+ while (true) {
3217
+ var chunk = await contReader.read();
3218
+ if (chunk.done) break;
3219
+ contBuffer += contDecoder.decode(chunk.value, { stream: true });
3220
+ contBuffer = parseSseChunk(contBuffer, function(evtName, evtPayload) {
3221
+ if (evtName === "model:chunk" && evtPayload.content) {
3222
+ contAssistant.content = (contAssistant.content || "") + evtPayload.content;
3223
+ contAssistant._currentText += evtPayload.content;
3224
+ }
3225
+ if (evtName === "tool:started") {
3226
+ if (contAssistant._currentText) {
3227
+ contAssistant._sections.push({ type: "text", content: contAssistant._currentText });
3228
+ contAssistant._currentText = "";
3229
+ }
3230
+ contAssistant._currentTools.push("- start \`" + evtPayload.tool + "\`");
3231
+ }
3232
+ if (evtName === "tool:completed") {
3233
+ contAssistant._currentTools.push("- done \`" + evtPayload.tool + "\` (" + evtPayload.duration + "ms)");
3234
+ }
3235
+ if (evtName === "tool:error") {
3236
+ contAssistant._currentTools.push("- error \`" + evtPayload.tool + "\`: " + evtPayload.error);
3237
+ }
3238
+ if (evtName === "run:completed" || evtName === "run:error" || evtName === "run:cancelled") {
3239
+ if (contAssistant._currentTools.length > 0) {
3240
+ contAssistant._sections.push({ type: "tools", content: contAssistant._currentTools });
3241
+ contAssistant._currentTools = [];
3242
+ }
3243
+ if (contAssistant._currentText) {
3244
+ contAssistant._sections.push({ type: "text", content: contAssistant._currentText });
3245
+ contAssistant._currentText = "";
3246
+ }
3247
+ contAssistant._activeActivities = [];
3248
+ if (evtName === "run:error") {
3249
+ contAssistant._error = evtPayload.error?.message || "Something went wrong";
3250
+ }
3251
+ if (evtName === "run:completed" && evtPayload.result?.continuation === true) {
3252
+ // Another continuation needed \u2014 reload to pick it up
3253
+ loadConversation(conversationId).catch(function() {});
3254
+ }
3255
+ }
3256
+ renderMessages(localMsgs, true);
3257
+ });
3258
+ }
3259
+ setStreaming(false);
3260
+ renderMessages(localMsgs, false);
3261
+ await loadConversations();
3262
+ } catch (contErr) {
3263
+ console.error("[poncho] Auto-continuation failed:", contErr);
3264
+ setStreaming(false);
3265
+ await loadConversation(conversationId).catch(function() {});
3266
+ } finally {
3267
+ state.activeStreamConversationId = null;
3268
+ state._activeStreamMessages = null;
3269
+ }
3270
+ })();
3043
3271
  }
3044
3272
  };
3045
3273
 
@@ -4123,6 +4351,7 @@ var getWebUiClientScript = (markedSource2) => `
4123
4351
  let _totalSteps = 0;
4124
4352
  let _maxSteps = 0;
4125
4353
  let _isContinuation = false;
4354
+ let _receivedTerminalEvent = false;
4126
4355
  while (true) {
4127
4356
  let _shouldContinue = false;
4128
4357
  let fetchOpts;
@@ -4452,6 +4681,7 @@ var getWebUiClientScript = (markedSource2) => `
4452
4681
  }
4453
4682
  }
4454
4683
  if (eventName === "run:completed") {
4684
+ _receivedTerminalEvent = true;
4455
4685
  _totalSteps += typeof payload.result?.steps === "number" ? payload.result.steps : 0;
4456
4686
  if (typeof payload.result?.maxSteps === "number") _maxSteps = payload.result.maxSteps;
4457
4687
  if (payload.result?.continuation === true && (_maxSteps <= 0 || _totalSteps < _maxSteps)) {
@@ -4468,10 +4698,12 @@ var getWebUiClientScript = (markedSource2) => `
4468
4698
  }
4469
4699
  }
4470
4700
  if (eventName === "run:cancelled") {
4701
+ _receivedTerminalEvent = true;
4471
4702
  finalizeAssistantMessage();
4472
4703
  renderIfActiveConversation(false);
4473
4704
  }
4474
4705
  if (eventName === "run:error") {
4706
+ _receivedTerminalEvent = true;
4475
4707
  finalizeAssistantMessage();
4476
4708
  const errMsg = payload.error?.message || "Something went wrong";
4477
4709
  assistantMessage._error = errMsg;
@@ -4482,7 +4714,19 @@ var getWebUiClientScript = (markedSource2) => `
4482
4714
  }
4483
4715
  });
4484
4716
  }
4717
+ if (!_shouldContinue && !_receivedTerminalEvent) {
4718
+ try {
4719
+ const recoveryPayload = await api("/api/conversations/" + encodeURIComponent(conversationId));
4720
+ if (recoveryPayload.needsContinuation) {
4721
+ _shouldContinue = true;
4722
+ console.log("[poncho] Stream ended without terminal event, server has continuation \u2014 resuming");
4723
+ }
4724
+ } catch (_recoverErr) {
4725
+ console.warn("[poncho] Recovery check failed after abrupt stream end");
4726
+ }
4727
+ }
4485
4728
  if (!_shouldContinue) break;
4729
+ _receivedTerminalEvent = false;
4486
4730
  _isContinuation = true;
4487
4731
  }
4488
4732
  // Update active state only if user is still on this conversation.
@@ -4756,7 +5000,7 @@ var getWebUiClientScript = (markedSource2) => `
4756
5000
  state: "resolved",
4757
5001
  resolvedDecision: decision,
4758
5002
  }));
4759
- api("/api/approvals/" + encodeURIComponent(approvalId), {
5003
+ return api("/api/approvals/" + encodeURIComponent(approvalId), {
4760
5004
  method: "POST",
4761
5005
  body: JSON.stringify({ approved: decision === "approve" }),
4762
5006
  }).catch((error) => {
@@ -4804,16 +5048,54 @@ var getWebUiClientScript = (markedSource2) => `
4804
5048
  if (pending.length === 0) return;
4805
5049
  const wasStreaming = state.isStreaming;
4806
5050
  if (!wasStreaming) setStreaming(true);
4807
- pending.forEach((aid) => submitApproval(aid, decision));
5051
+ // Mark all items as resolved in the UI immediately
5052
+ for (const aid of pending) {
5053
+ state.approvalRequestsInFlight[aid] = true;
5054
+ updatePendingApproval(aid, (request) => ({
5055
+ ...request,
5056
+ state: "resolved",
5057
+ resolvedDecision: decision,
5058
+ }));
5059
+ }
4808
5060
  renderMessages(state.activeMessages, state.isStreaming);
4809
5061
  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
- }
5062
+ const streamCid = !wasStreaming && state.activeConversationId
5063
+ ? state.activeConversationId
5064
+ : null;
5065
+ if (streamCid) {
5066
+ streamConversationEvents(streamCid, { liveOnly: true }).finally(() => {
5067
+ if (state.activeConversationId === streamCid) {
5068
+ pollUntilRunIdle(streamCid);
5069
+ }
5070
+ });
4816
5071
  }
5072
+ // Send API calls sequentially so each store write completes
5073
+ // before the next read (avoids last-writer-wins in serverless).
5074
+ void (async () => {
5075
+ for (const aid of pending) {
5076
+ await api("/api/approvals/" + encodeURIComponent(aid), {
5077
+ method: "POST",
5078
+ body: JSON.stringify({ approved: decision === "approve" }),
5079
+ }).catch((error) => {
5080
+ const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
5081
+ if (isStale) {
5082
+ updatePendingApproval(aid, () => null);
5083
+ } else {
5084
+ const errMsg = error instanceof Error ? error.message : String(error);
5085
+ updatePendingApproval(aid, (request) => ({
5086
+ ...request,
5087
+ state: "pending",
5088
+ pendingDecision: null,
5089
+ resolvedDecision: null,
5090
+ _error: errMsg,
5091
+ }));
5092
+ }
5093
+ renderMessages(state.activeMessages, state.isStreaming);
5094
+ }).finally(() => {
5095
+ delete state.approvalRequestsInFlight[aid];
5096
+ });
5097
+ }
5098
+ })();
4817
5099
  return;
4818
5100
  }
4819
5101
 
@@ -7493,12 +7775,16 @@ Set environment variables on your deployment platform:
7493
7775
  ANTHROPIC_API_KEY=sk-ant-... # Required
7494
7776
  PONCHO_AUTH_TOKEN=your-secret # Optional: protect your endpoint
7495
7777
  PONCHO_MAX_DURATION=55 # Optional: serverless timeout in seconds (enables auto-continuation)
7778
+ PONCHO_INTERNAL_SECRET=... # Recommended on serverless: shared secret for internal callback auth
7496
7779
  \`\`\`
7497
7780
 
7498
7781
  When \`PONCHO_MAX_DURATION\` is set, the agent automatically checkpoints and resumes across
7499
7782
  request cycles when it approaches the platform timeout. The web UI and client SDK handle
7500
7783
  this transparently.
7501
7784
 
7785
+ For serverless deployments with subagents or background callbacks, use a shared state backend
7786
+ (\`upstash\`, \`redis\`, or \`dynamodb\`) instead of \`state.provider: 'local'\` / \`'memory'\`.
7787
+
7502
7788
  ## Troubleshooting
7503
7789
 
7504
7790
  ### Vercel deploy issues
@@ -7506,6 +7792,7 @@ this transparently.
7506
7792
  - After upgrading \`@poncho-ai/cli\`, re-run \`poncho build vercel --force\` to refresh generated deploy files.
7507
7793
  - If Vercel fails during \`pnpm install\` due to a lockfile mismatch, run \`pnpm install --no-frozen-lockfile\` locally and commit \`pnpm-lock.yaml\`.
7508
7794
  - Deploy from the project root: \`vercel deploy --prod\`.
7795
+ - For subagents/background callbacks, set \`PONCHO_INTERNAL_SECRET\` and use non-local state storage.
7509
7796
 
7510
7797
  For full reference:
7511
7798
  https://github.com/cesr/poncho-ai
@@ -8221,7 +8508,8 @@ data: ${JSON.stringify(statusPayload)}
8221
8508
  await harness.initialize();
8222
8509
  const telemetry = new TelemetryEmitter(config?.telemetry);
8223
8510
  const identity = await ensureAgentIdentity2(workingDir);
8224
- const conversationStore = createConversationStore(resolveStateConfig(config), {
8511
+ const stateConfig = resolveStateConfig(config);
8512
+ const conversationStore = createConversationStore(stateConfig, {
8225
8513
  workingDir,
8226
8514
  agentId: identity.id
8227
8515
  });
@@ -8229,6 +8517,7 @@ data: ${JSON.stringify(statusPayload)}
8229
8517
  const MAX_CONCURRENT_SUBAGENTS = 5;
8230
8518
  const activeSubagentRuns = /* @__PURE__ */ new Map();
8231
8519
  const pendingSubagentApprovals = /* @__PURE__ */ new Map();
8520
+ const approvalDecisionTracker = /* @__PURE__ */ new Map();
8232
8521
  const getSubagentDepth = async (conversationId) => {
8233
8522
  let depth = 0;
8234
8523
  let current = await conversationStore.get(conversationId);
@@ -8559,9 +8848,10 @@ data: ${JSON.stringify(statusPayload)}
8559
8848
  } catch {
8560
8849
  }
8561
8850
  if (isServerless) {
8562
- selfFetchWithRetry(`/api/internal/subagent/${encodeURIComponent(childConversationId)}/run`, { continuation: true }).catch(
8851
+ const work = selfFetchWithRetry(`/api/internal/subagent/${encodeURIComponent(childConversationId)}/run`, { continuation: true }).catch(
8563
8852
  (err) => console.error(`[poncho][subagent] Continuation self-fetch failed:`, err instanceof Error ? err.message : err)
8564
8853
  );
8854
+ doWaitUntil(work);
8565
8855
  } else {
8566
8856
  runSubagent(childConversationId, parentConversationId, task, ownerId, true).catch(
8567
8857
  (err) => console.error(`[poncho][subagent] Continuation failed:`, err instanceof Error ? err.message : err)
@@ -8646,8 +8936,10 @@ data: ${JSON.stringify(statusPayload)}
8646
8936
  }
8647
8937
  };
8648
8938
  const MAX_SUBAGENT_CALLBACK_COUNT = 20;
8939
+ const pendingCallbackNeeded = /* @__PURE__ */ new Set();
8649
8940
  const triggerParentCallback = async (parentConversationId) => {
8650
8941
  if (activeConversationRuns.has(parentConversationId)) {
8942
+ pendingCallbackNeeded.add(parentConversationId);
8651
8943
  return;
8652
8944
  }
8653
8945
  if (isServerless) {
@@ -8659,12 +8951,13 @@ data: ${JSON.stringify(statusPayload)}
8659
8951
  await processSubagentCallback(parentConversationId);
8660
8952
  };
8661
8953
  const CALLBACK_LOCK_STALE_MS = 5 * 60 * 1e3;
8662
- const processSubagentCallback = async (conversationId) => {
8954
+ const processSubagentCallback = async (conversationId, skipLockCheck = false) => {
8663
8955
  const conversation = await conversationStore.get(conversationId);
8664
8956
  if (!conversation) return;
8665
8957
  const pendingResults = conversation.pendingSubagentResults ?? [];
8666
- if (pendingResults.length === 0) return;
8667
- if (conversation.runningCallbackSince) {
8958
+ const hasOrphanedContinuation = pendingResults.length === 0 && Array.isArray(conversation._continuationMessages) && conversation._continuationMessages.length > 0 && !activeConversationRuns.has(conversationId);
8959
+ if (pendingResults.length === 0 && !hasOrphanedContinuation) return;
8960
+ if (!skipLockCheck && conversation.runningCallbackSince) {
8668
8961
  const elapsed = Date.now() - conversation.runningCallbackSince;
8669
8962
  if (elapsed < CALLBACK_LOCK_STALE_MS) {
8670
8963
  return;
@@ -8673,6 +8966,7 @@ data: ${JSON.stringify(statusPayload)}
8673
8966
  }
8674
8967
  conversation.pendingSubagentResults = [];
8675
8968
  conversation.runningCallbackSince = Date.now();
8969
+ conversation.runStatus = "running";
8676
8970
  const callbackCount = (conversation.subagentCallbackCount ?? 0) + 1;
8677
8971
  conversation.subagentCallbackCount = callbackCount;
8678
8972
  for (const pr of pendingResults) {
@@ -8692,21 +8986,36 @@ ${resultBody}`,
8692
8986
  if (callbackCount > MAX_SUBAGENT_CALLBACK_COUNT) {
8693
8987
  console.warn(`[poncho][subagent-callback] Circuit breaker: ${callbackCount} callbacks for ${conversationId}, skipping re-run`);
8694
8988
  conversation.runningCallbackSince = void 0;
8989
+ conversation.runStatus = "idle";
8695
8990
  await conversationStore.update(conversation);
8696
8991
  return;
8697
8992
  }
8698
- console.log(`[poncho][subagent-callback] Processing ${pendingResults.length} result(s) for ${conversationId} (callback #${callbackCount})`);
8993
+ const isContinuationResume = hasOrphanedContinuation && pendingResults.length === 0;
8994
+ console.log(`[poncho][subagent-callback] Processing ${pendingResults.length} result(s) for ${conversationId} (callback #${callbackCount})${isContinuationResume ? " (continuation resume)" : ""}`);
8699
8995
  const abortController = new AbortController();
8700
8996
  activeConversationRuns.set(conversationId, {
8701
8997
  ownerId: conversation.ownerId,
8702
8998
  abortController,
8703
8999
  runId: null
8704
9000
  });
8705
- const historyMessages = [...conversation.messages];
9001
+ const prevStream = conversationEventStreams.get(conversationId);
9002
+ if (prevStream) {
9003
+ prevStream.finished = false;
9004
+ prevStream.buffer = [];
9005
+ } else {
9006
+ conversationEventStreams.set(conversationId, {
9007
+ buffer: [],
9008
+ subscribers: /* @__PURE__ */ new Set(),
9009
+ finished: false
9010
+ });
9011
+ }
9012
+ const historyMessages = isContinuationResume && conversation._continuationMessages?.length ? [...conversation._continuationMessages] : [...conversation.messages];
8706
9013
  let assistantResponse = "";
8707
9014
  let latestRunId = "";
8708
9015
  let runContinuation = false;
8709
9016
  let runContinuationMessages;
9017
+ let runContextTokens = conversation.contextTokens ?? 0;
9018
+ let runContextWindow = conversation.contextWindow ?? 0;
8710
9019
  const toolTimeline = [];
8711
9020
  const sections = [];
8712
9021
  let currentTools = [];
@@ -8761,6 +9070,8 @@ ${resultBody}`,
8761
9070
  if (assistantResponse.length === 0 && event.result.response) {
8762
9071
  assistantResponse = event.result.response;
8763
9072
  }
9073
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
9074
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
8764
9075
  if (event.result.continuation) {
8765
9076
  runContinuation = true;
8766
9077
  if (event.result.continuationMessages) {
@@ -8787,6 +9098,9 @@ ${resultBody}`,
8787
9098
  }
8788
9099
  freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
8789
9100
  freshConv.runningCallbackSince = void 0;
9101
+ freshConv.runStatus = "idle";
9102
+ if (runContextTokens > 0) freshConv.contextTokens = runContextTokens;
9103
+ if (runContextWindow > 0) freshConv.contextWindow = runContextWindow;
8790
9104
  freshConv.updatedAt = Date.now();
8791
9105
  await conversationStore.update(freshConv);
8792
9106
  if (freshConv.channelMeta && assistantResponse.length > 0) {
@@ -8809,9 +9123,14 @@ ${resultBody}`,
8809
9123
  }
8810
9124
  if (runContinuation) {
8811
9125
  if (isServerless) {
8812
- selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(
9126
+ const work = selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(
8813
9127
  (err) => console.error(`[poncho][subagent-callback] Continuation self-fetch failed:`, err instanceof Error ? err.message : err)
8814
9128
  );
9129
+ doWaitUntil(work);
9130
+ } else {
9131
+ processSubagentCallback(conversationId, true).catch(
9132
+ (err) => console.error(`[poncho][subagent-callback] Continuation failed:`, err instanceof Error ? err.message : err)
9133
+ );
8815
9134
  }
8816
9135
  }
8817
9136
  } catch (err) {
@@ -8819,28 +9138,38 @@ ${resultBody}`,
8819
9138
  const errConv = await conversationStore.get(conversationId);
8820
9139
  if (errConv) {
8821
9140
  errConv.runningCallbackSince = void 0;
9141
+ errConv.runStatus = "idle";
8822
9142
  await conversationStore.update(errConv);
8823
9143
  }
8824
9144
  } finally {
8825
9145
  activeConversationRuns.delete(conversationId);
8826
9146
  finishConversationStream(conversationId);
9147
+ const hadDeferredTrigger = pendingCallbackNeeded.delete(conversationId);
8827
9148
  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) {
9149
+ const hasPendingInStore = !!freshConv?.pendingSubagentResults?.length;
9150
+ if (hadDeferredTrigger || hasPendingInStore) {
8835
9151
  if (isServerless) {
8836
9152
  selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(
8837
9153
  (err) => console.error(`[poncho][subagent-callback] Recursive callback self-fetch failed:`, err instanceof Error ? err.message : err)
8838
9154
  );
8839
9155
  } else {
8840
- processSubagentCallback(conversationId).catch(
9156
+ processSubagentCallback(conversationId, true).catch(
8841
9157
  (err) => console.error(`[poncho][subagent-callback] Recursive callback failed:`, err instanceof Error ? err.message : err)
8842
9158
  );
8843
9159
  }
9160
+ } else if (freshConv?.runningCallbackSince) {
9161
+ const afterClear = await conversationStore.clearCallbackLock(conversationId);
9162
+ if (afterClear?.pendingSubagentResults?.length) {
9163
+ if (isServerless) {
9164
+ selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(
9165
+ (err) => console.error(`[poncho][subagent-callback] Post-clear callback self-fetch failed:`, err instanceof Error ? err.message : err)
9166
+ );
9167
+ } else {
9168
+ processSubagentCallback(conversationId, true).catch(
9169
+ (err) => console.error(`[poncho][subagent-callback] Post-clear callback failed:`, err instanceof Error ? err.message : err)
9170
+ );
9171
+ }
9172
+ }
8844
9173
  }
8845
9174
  }
8846
9175
  };
@@ -9007,14 +9336,6 @@ ${resultBody}`,
9007
9336
  if (active && active.abortController === abortController) {
9008
9337
  active.runId = event.runId;
9009
9338
  }
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
9339
  }
9019
9340
  if (event.type === "model:chunk") {
9020
9341
  if (currentTools.length > 0) {
@@ -9079,8 +9400,12 @@ ${resultBody}`,
9079
9400
  }
9080
9401
  checkpointedRun = true;
9081
9402
  }
9082
- if (event.type === "run:completed" && assistantResponse.length === 0 && event.result.response) {
9083
- assistantResponse = event.result.response;
9403
+ if (event.type === "run:completed") {
9404
+ if (assistantResponse.length === 0 && event.result.response) {
9405
+ assistantResponse = event.result.response;
9406
+ }
9407
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
9408
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
9084
9409
  }
9085
9410
  if (event.type === "run:error") {
9086
9411
  assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
@@ -9163,6 +9488,13 @@ ${resultBody}`,
9163
9488
  runConversations.delete(latestRunId);
9164
9489
  }
9165
9490
  console.log("[resume-run] complete for", conversationId);
9491
+ const hadDeferred = pendingCallbackNeeded.delete(conversationId);
9492
+ const postConv = await conversationStore.get(conversationId);
9493
+ if (hadDeferred || postConv?.pendingSubagentResults?.length) {
9494
+ processSubagentCallback(conversationId, true).catch(
9495
+ (err) => console.error(`[poncho][subagent-callback] Post-resume callback failed:`, err instanceof Error ? err.message : err)
9496
+ );
9497
+ }
9166
9498
  };
9167
9499
  const messagingRoutes = /* @__PURE__ */ new Map();
9168
9500
  const messagingRouteRegistrar = (method, path, routeHandler) => {
@@ -9293,14 +9625,6 @@ ${resultBody}`,
9293
9625
  latestRunId = event.runId;
9294
9626
  runOwners.set(event.runId, "local-owner");
9295
9627
  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
9628
  }
9305
9629
  if (event.type === "model:chunk") {
9306
9630
  if (currentTools.length > 0) {
@@ -9399,6 +9723,8 @@ ${resultBody}`,
9399
9723
  }
9400
9724
  runSteps = event.result.steps;
9401
9725
  if (typeof event.result.maxSteps === "number") runMaxSteps = event.result.maxSteps;
9726
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
9727
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
9402
9728
  }
9403
9729
  if (event.type === "run:error") {
9404
9730
  assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
@@ -9464,34 +9790,89 @@ ${resultBody}`,
9464
9790
  }
9465
9791
  }
9466
9792
  const isServerless = !!waitUntilHook;
9467
- const internalSecret = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
9793
+ const configuredInternalSecret = process.env.PONCHO_INTERNAL_SECRET?.trim();
9794
+ const vercelDeploymentSecret = process.env.VERCEL_DEPLOYMENT_ID?.trim();
9795
+ const fallbackInternalSecret = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
9796
+ const internalSecret = configuredInternalSecret || vercelDeploymentSecret || fallbackInternalSecret;
9797
+ const isUsingEphemeralInternalSecret = !configuredInternalSecret && !vercelDeploymentSecret;
9468
9798
  let selfBaseUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : null;
9799
+ if (!selfBaseUrl && process.env.VERCEL_PROJECT_PRODUCTION_URL) {
9800
+ selfBaseUrl = `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
9801
+ }
9802
+ if (!selfBaseUrl && process.env.PONCHO_SELF_BASE_URL) {
9803
+ selfBaseUrl = process.env.PONCHO_SELF_BASE_URL.replace(/\/+$/, "");
9804
+ }
9805
+ if (isServerless && isUsingEphemeralInternalSecret) {
9806
+ console.warn(
9807
+ "[poncho][serverless] No stable internal secret found. Set PONCHO_INTERNAL_SECRET to avoid intermittent internal callback failures across instances."
9808
+ );
9809
+ }
9810
+ if (isServerless && !selfBaseUrl) {
9811
+ console.warn(
9812
+ "[poncho][serverless] No self base URL available. Set PONCHO_SELF_BASE_URL if internal background callbacks fail."
9813
+ );
9814
+ }
9815
+ const stateProvider = stateConfig?.provider ?? "local";
9816
+ if (isServerless && (stateProvider === "local" || stateProvider === "memory")) {
9817
+ console.warn(
9818
+ `[poncho][serverless] state.provider="${stateProvider}" may lose cross-invocation state. Prefer "upstash", "redis", or "dynamodb" for subagents/reliability.`
9819
+ );
9820
+ }
9469
9821
  const doWaitUntil = (promise) => {
9470
9822
  if (waitUntilHook) waitUntilHook(promise);
9471
9823
  };
9472
- const selfFetch = (path, body) => {
9473
- if (!selfBaseUrl) return Promise.resolve();
9474
- return fetch(`${selfBaseUrl}${path}`, {
9475
- method: "POST",
9476
- headers: {
9477
- "Content-Type": "application/json",
9478
- "x-poncho-internal": internalSecret
9479
- },
9480
- body: body ? JSON.stringify(body) : void 0
9481
- }).catch((err) => {
9482
- console.error(`[poncho][self-fetch] Failed ${path}:`, err instanceof Error ? err.message : err);
9483
- });
9484
- };
9485
9824
  const selfFetchWithRetry = async (path, body, retries = 3) => {
9825
+ if (!selfBaseUrl) {
9826
+ console.error(`[poncho][self-fetch] Missing self base URL for ${path}`);
9827
+ return;
9828
+ }
9829
+ let lastError;
9486
9830
  for (let attempt = 0; attempt < retries; attempt++) {
9487
9831
  try {
9488
- const result = await selfFetch(path, body);
9489
- return result;
9832
+ const result = await fetch(`${selfBaseUrl}${path}`, {
9833
+ method: "POST",
9834
+ headers: {
9835
+ "Content-Type": "application/json",
9836
+ "x-poncho-internal": internalSecret
9837
+ },
9838
+ body: body ? JSON.stringify(body) : void 0
9839
+ });
9840
+ if (result.ok) {
9841
+ return result;
9842
+ }
9843
+ const responseText = await result.text().catch(() => "");
9844
+ lastError = new Error(
9845
+ `HTTP ${result.status}${responseText ? `: ${responseText.slice(0, 200)}` : ""}`
9846
+ );
9490
9847
  } catch (err) {
9491
- if (attempt === retries - 1) throw err;
9492
- await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
9848
+ lastError = err;
9849
+ }
9850
+ if (attempt === retries - 1) {
9851
+ break;
9493
9852
  }
9853
+ await new Promise((resolveSleep) => setTimeout(resolveSleep, 1e3 * (attempt + 1)));
9494
9854
  }
9855
+ if (lastError) {
9856
+ console.error(
9857
+ `[poncho][self-fetch] Failed ${path} after ${retries} attempt(s):`,
9858
+ lastError instanceof Error ? lastError.message : String(lastError)
9859
+ );
9860
+ if (lastError instanceof Error && (lastError.message.includes("HTTP 403") || lastError.message.includes("HTTP 401"))) {
9861
+ console.error(
9862
+ "[poncho][self-fetch] Internal auth failed. Ensure all serverless instances share PONCHO_INTERNAL_SECRET."
9863
+ );
9864
+ }
9865
+ } else {
9866
+ console.error(`[poncho][self-fetch] Failed ${path} after ${retries} attempt(s).`);
9867
+ }
9868
+ };
9869
+ const getInternalRequestHeader = (headers) => {
9870
+ const value = headers["x-poncho-internal"];
9871
+ return Array.isArray(value) ? value[0] : value;
9872
+ };
9873
+ const isValidInternalRequest = (headers) => {
9874
+ const headerValue = getInternalRequestHeader(headers);
9875
+ return typeof headerValue === "string" && headerValue === internalSecret;
9495
9876
  };
9496
9877
  const messagingAdapters = /* @__PURE__ */ new Map();
9497
9878
  const messagingBridges = [];
@@ -9808,7 +10189,7 @@ ${resultBody}`,
9808
10189
  }
9809
10190
  }
9810
10191
  if (pathname?.startsWith("/api/internal/") && request.method === "POST") {
9811
- if (request.headers["x-poncho-internal"] !== internalSecret) {
10192
+ if (!isValidInternalRequest(request.headers)) {
9812
10193
  writeJson(response, 403, { code: "FORBIDDEN", message: "Internal endpoint" });
9813
10194
  return;
9814
10195
  }
@@ -10187,18 +10568,29 @@ data: ${JSON.stringify(frame)}
10187
10568
  });
10188
10569
  return;
10189
10570
  }
10571
+ let batchDecisions = approvalDecisionTracker.get(conversationId);
10572
+ if (!batchDecisions) {
10573
+ batchDecisions = /* @__PURE__ */ new Map();
10574
+ approvalDecisionTracker.set(conversationId, batchDecisions);
10575
+ }
10576
+ batchDecisions.set(approvalId, approved);
10190
10577
  foundApproval.decision = approved ? "approved" : "denied";
10191
10578
  broadcastEvent(
10192
10579
  conversationId,
10193
10580
  approved ? { type: "tool:approval:granted", approvalId } : { type: "tool:approval:denied", approvalId }
10194
10581
  );
10195
10582
  const allApprovals = foundConversation.pendingApprovals ?? [];
10196
- const allDecided = allApprovals.length > 0 && allApprovals.every((a) => a.decision != null);
10583
+ const allDecided = allApprovals.length > 0 && allApprovals.every((a) => batchDecisions.has(a.approvalId));
10197
10584
  if (!allDecided) {
10198
10585
  await conversationStore.update(foundConversation);
10199
10586
  writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
10200
10587
  return;
10201
10588
  }
10589
+ for (const a of allApprovals) {
10590
+ const d = batchDecisions.get(a.approvalId);
10591
+ if (d != null) a.decision = d ? "approved" : "denied";
10592
+ }
10593
+ approvalDecisionTracker.delete(conversationId);
10202
10594
  foundConversation.pendingApprovals = [];
10203
10595
  foundConversation.runStatus = "running";
10204
10596
  await conversationStore.update(foundConversation);
@@ -10433,14 +10825,18 @@ data: ${JSON.stringify(frame)}
10433
10825
  }
10434
10826
  }
10435
10827
  }
10828
+ const hasPendingCallbackResults = Array.isArray(conversation.pendingSubagentResults) && conversation.pendingSubagentResults.length > 0;
10829
+ const needsContinuation = !hasActiveRun && Array.isArray(conversation._continuationMessages) && conversation._continuationMessages.length > 0;
10436
10830
  writeJson(response, 200, {
10437
10831
  conversation: {
10438
10832
  ...conversation,
10439
- pendingApprovals: storedPending
10833
+ pendingApprovals: storedPending,
10834
+ _continuationMessages: void 0
10440
10835
  },
10441
10836
  subagentPendingApprovals: subagentPending,
10442
- hasActiveRun,
10443
- hasRunningSubagents
10837
+ hasActiveRun: hasActiveRun || hasPendingCallbackResults,
10838
+ hasRunningSubagents,
10839
+ needsContinuation
10444
10840
  });
10445
10841
  return;
10446
10842
  }
@@ -10820,14 +11216,6 @@ data: ${JSON.stringify(frame)}
10820
11216
  if (active && active.abortController === abortController) {
10821
11217
  active.runId = event.runId;
10822
11218
  }
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
11219
  }
10832
11220
  if (event.type === "run:cancelled") {
10833
11221
  runCancelled = true;
@@ -10920,6 +11308,8 @@ data: ${JSON.stringify(frame)}
10920
11308
  if (assistantResponse.length === 0 && event.result.response) {
10921
11309
  assistantResponse = event.result.response;
10922
11310
  }
11311
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
11312
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
10923
11313
  if (event.result.continuation && event.result.continuationMessages) {
10924
11314
  runContinuationMessages = event.result.continuationMessages;
10925
11315
  conversation._continuationMessages = runContinuationMessages;
@@ -11059,9 +11449,10 @@ data: ${JSON.stringify(frame)}
11059
11449
  response.end();
11060
11450
  } catch {
11061
11451
  }
11452
+ const hadDeferred = pendingCallbackNeeded.delete(conversationId);
11062
11453
  const freshConv = await conversationStore.get(conversationId);
11063
- if (freshConv?.pendingSubagentResults?.length) {
11064
- processSubagentCallback(conversationId).catch(
11454
+ if (hadDeferred || freshConv?.pendingSubagentResults?.length) {
11455
+ processSubagentCallback(conversationId, true).catch(
11065
11456
  (err) => console.error(`[poncho][subagent-callback] Post-run callback failed:`, err instanceof Error ? err.message : err)
11066
11457
  );
11067
11458
  }
@@ -11209,125 +11600,157 @@ ${cronJob.task}`;
11209
11600
  `[cron] ${jobName} ${timestamp}`
11210
11601
  );
11211
11602
  }
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 += " ";
11603
+ const convId = conversation.conversationId;
11604
+ activeConversationRuns.set(convId, {
11605
+ ownerId: conversation.ownerId,
11606
+ abortController: new AbortController(),
11607
+ runId: null
11608
+ });
11609
+ try {
11610
+ const abortController = new AbortController();
11611
+ let assistantResponse = "";
11612
+ let latestRunId = "";
11613
+ const toolTimeline = [];
11614
+ const sections = [];
11615
+ let currentTools = [];
11616
+ let currentText = "";
11617
+ let runResult = {
11618
+ status: "completed",
11619
+ steps: 0
11620
+ };
11621
+ const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
11622
+ const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
11623
+ for await (const event of harness.runWithTelemetry({
11624
+ task: cronJob.task,
11625
+ conversationId: convId,
11626
+ parameters: { __activeConversationId: convId },
11627
+ messages: historyMessages,
11628
+ abortSignal: abortController.signal
11629
+ })) {
11630
+ if (event.type === "run:started") {
11631
+ latestRunId = event.runId;
11632
+ }
11633
+ if (event.type === "model:chunk") {
11634
+ if (currentTools.length > 0) {
11635
+ sections.push({ type: "tools", content: currentTools });
11636
+ currentTools = [];
11637
+ if (assistantResponse.length > 0 && !/\s$/.test(assistantResponse)) {
11638
+ assistantResponse += " ";
11639
+ }
11241
11640
  }
11641
+ assistantResponse += event.content;
11642
+ currentText += event.content;
11242
11643
  }
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 = "";
11644
+ if (event.type === "tool:started") {
11645
+ if (currentText.length > 0) {
11646
+ sections.push({ type: "text", content: currentText });
11647
+ currentText = "";
11648
+ }
11649
+ const toolText = `- start \`${event.tool}\``;
11650
+ toolTimeline.push(toolText);
11651
+ currentTools.push(toolText);
11250
11652
  }
11251
- const toolText = `- start \`${event.tool}\``;
11252
- toolTimeline.push(toolText);
11253
- currentTools.push(toolText);
11653
+ if (event.type === "tool:completed") {
11654
+ const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
11655
+ toolTimeline.push(toolText);
11656
+ currentTools.push(toolText);
11657
+ }
11658
+ if (event.type === "tool:error") {
11659
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
11660
+ toolTimeline.push(toolText);
11661
+ currentTools.push(toolText);
11662
+ }
11663
+ if (event.type === "run:completed") {
11664
+ runResult = {
11665
+ status: event.result.status,
11666
+ steps: event.result.steps,
11667
+ continuation: event.result.continuation,
11668
+ contextTokens: event.result.contextTokens,
11669
+ contextWindow: event.result.contextWindow
11670
+ };
11671
+ if (!assistantResponse && event.result.response) {
11672
+ assistantResponse = event.result.response;
11673
+ }
11674
+ }
11675
+ broadcastEvent(convId, event);
11676
+ await telemetry.emit(event);
11254
11677
  }
11255
- if (event.type === "tool:completed") {
11256
- const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
11257
- toolTimeline.push(toolText);
11258
- currentTools.push(toolText);
11678
+ finishConversationStream(convId);
11679
+ if (currentTools.length > 0) {
11680
+ sections.push({ type: "tools", content: currentTools });
11259
11681
  }
11260
- if (event.type === "tool:error") {
11261
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
11262
- toolTimeline.push(toolText);
11263
- currentTools.push(toolText);
11682
+ if (currentText.length > 0) {
11683
+ sections.push({ type: "text", content: currentText });
11684
+ currentText = "";
11264
11685
  }
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;
11686
+ const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
11687
+ const assistantMetadata = toolTimeline.length > 0 || sections.length > 0 ? {
11688
+ toolActivity: [...toolTimeline],
11689
+ sections: sections.length > 0 ? sections : void 0
11690
+ } : void 0;
11691
+ const messages = [
11692
+ ...historyMessages,
11693
+ ...continueConversationId ? [] : [{ role: "user", content: cronJob.task }],
11694
+ ...hasContent ? [{ role: "assistant", content: assistantResponse, metadata: assistantMetadata }] : []
11695
+ ];
11696
+ const freshConv = await conversationStore.get(convId);
11697
+ if (freshConv) {
11698
+ freshConv.messages = messages;
11699
+ freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
11700
+ if (runResult.contextTokens) freshConv.contextTokens = runResult.contextTokens;
11701
+ if (runResult.contextWindow) freshConv.contextWindow = runResult.contextWindow;
11702
+ freshConv.updatedAt = Date.now();
11703
+ await conversationStore.update(freshConv);
11704
+ }
11705
+ if (runResult.continuation && softDeadlineMs > 0) {
11706
+ const selfUrl = `http://${request.headers.host ?? "localhost"}${pathname}?continue=${encodeURIComponent(convId)}&continuation=${continuationCount + 1}`;
11707
+ try {
11708
+ const selfRes = await fetch(selfUrl, {
11709
+ method: "GET",
11710
+ headers: request.headers.authorization ? { authorization: request.headers.authorization } : {}
11711
+ });
11712
+ const selfBody = await selfRes.json();
11713
+ writeJson(response, 200, {
11714
+ conversationId: convId,
11715
+ status: "continued",
11716
+ continuations: continuationCount + 1,
11717
+ finalResult: selfBody,
11718
+ duration: Date.now() - start
11719
+ });
11720
+ } catch (continueError) {
11721
+ writeJson(response, 200, {
11722
+ conversationId: convId,
11723
+ status: "continuation_failed",
11724
+ error: continueError instanceof Error ? continueError.message : "Unknown error",
11725
+ duration: Date.now() - start,
11726
+ steps: runResult.steps
11727
+ });
11273
11728
  }
11729
+ return;
11274
11730
  }
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
- });
11731
+ writeJson(response, 200, {
11732
+ conversationId: convId,
11733
+ status: runResult.status,
11734
+ response: assistantResponse.slice(0, 500),
11735
+ duration: Date.now() - start,
11736
+ steps: runResult.steps
11737
+ });
11738
+ } finally {
11739
+ activeConversationRuns.delete(convId);
11740
+ const hadDeferred = pendingCallbackNeeded.delete(convId);
11741
+ const checkConv = await conversationStore.get(convId);
11742
+ if (hadDeferred || checkConv?.pendingSubagentResults?.length) {
11743
+ if (isServerless) {
11744
+ selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(convId)}/subagent-callback`).catch(
11745
+ (err) => console.error(`[cron] subagent callback self-fetch failed:`, err instanceof Error ? err.message : err)
11746
+ );
11747
+ } else {
11748
+ processSubagentCallback(convId, true).catch(
11749
+ (err) => console.error(`[cron] subagent callback failed:`, err instanceof Error ? err.message : err)
11750
+ );
11751
+ }
11321
11752
  }
11322
- return;
11323
11753
  }
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
11754
  } catch (error) {
11332
11755
  writeJson(response, 500, {
11333
11756
  code: "CRON_RUN_ERROR",
@@ -11342,6 +11765,11 @@ ${cronJob.task}`;
11342
11765
  handler._cronJobs = cronJobs;
11343
11766
  handler._conversationStore = conversationStore;
11344
11767
  handler._messagingAdapters = messagingAdapters;
11768
+ handler._activeConversationRuns = activeConversationRuns;
11769
+ handler._pendingCallbackNeeded = pendingCallbackNeeded;
11770
+ handler._processSubagentCallback = processSubagentCallback;
11771
+ handler._broadcastEvent = broadcastEvent;
11772
+ handler._finishConversationStream = finishConversationStream;
11345
11773
  const STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
11346
11774
  try {
11347
11775
  const allSummaries = await conversationStore.listSummaries();
@@ -11392,9 +11820,11 @@ var startDevServer = async (port, options) => {
11392
11820
  await checkVercelCronDrift(workingDir);
11393
11821
  const { Cron } = await import("croner");
11394
11822
  let activeJobs = [];
11395
- const runCronAgent = async (harnessRef, task, conversationId, historyMessages) => {
11823
+ const runCronAgent = async (harnessRef, task, conversationId, historyMessages, onEvent) => {
11396
11824
  let assistantResponse = "";
11397
11825
  let steps = 0;
11826
+ let contextTokens = 0;
11827
+ let contextWindow = 0;
11398
11828
  const toolTimeline = [];
11399
11829
  const sections = [];
11400
11830
  let currentTools = [];
@@ -11405,6 +11835,7 @@ var startDevServer = async (port, options) => {
11405
11835
  parameters: { __activeConversationId: conversationId },
11406
11836
  messages: historyMessages
11407
11837
  })) {
11838
+ onEvent?.(event);
11408
11839
  if (event.type === "model:chunk") {
11409
11840
  if (currentTools.length > 0) {
11410
11841
  sections.push({ type: "tools", content: currentTools });
@@ -11437,6 +11868,8 @@ var startDevServer = async (port, options) => {
11437
11868
  }
11438
11869
  if (event.type === "run:completed") {
11439
11870
  steps = event.result.steps;
11871
+ contextTokens = event.result.contextTokens ?? 0;
11872
+ contextWindow = event.result.contextWindow ?? 0;
11440
11873
  if (!assistantResponse && event.result.response) {
11441
11874
  assistantResponse = event.result.response;
11442
11875
  }
@@ -11453,7 +11886,7 @@ var startDevServer = async (port, options) => {
11453
11886
  toolActivity: [...toolTimeline],
11454
11887
  sections: sections.length > 0 ? sections : void 0
11455
11888
  } : void 0;
11456
- return { response: assistantResponse, steps, assistantMetadata, hasContent };
11889
+ return { response: assistantResponse, steps, assistantMetadata, hasContent, contextTokens, contextWindow };
11457
11890
  };
11458
11891
  const buildCronMessages = (task, historyMessages, result) => [
11459
11892
  ...historyMessages,
@@ -11470,6 +11903,9 @@ var startDevServer = async (port, options) => {
11470
11903
  const harnessRef = handler._harness;
11471
11904
  const store = handler._conversationStore;
11472
11905
  const adapters = handler._messagingAdapters;
11906
+ const activeRuns = handler._activeConversationRuns;
11907
+ const deferredCallbacks = handler._pendingCallbackNeeded;
11908
+ const runCallback = handler._processSubagentCallback;
11473
11909
  if (!harnessRef || !store) return;
11474
11910
  for (const [jobName, config] of entries) {
11475
11911
  const job = new Cron(
@@ -11510,24 +11946,43 @@ var startDevServer = async (port, options) => {
11510
11946
  const task = `[Scheduled: ${jobName}]
11511
11947
  ${config.task}`;
11512
11948
  const historyMessages = [...conversation.messages];
11949
+ const convId = conversation.conversationId;
11950
+ activeRuns?.set(convId, {
11951
+ ownerId: "local-owner",
11952
+ abortController: new AbortController(),
11953
+ runId: null
11954
+ });
11513
11955
  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}
11956
+ const broadcastCh = handler._broadcastEvent;
11957
+ const result = await runCronAgent(
11958
+ harnessRef,
11959
+ task,
11960
+ convId,
11961
+ historyMessages,
11962
+ broadcastCh ? (ev) => broadcastCh(convId, ev) : void 0
11963
+ );
11964
+ handler._finishConversationStream?.(convId);
11965
+ const freshConv = await store.get(convId);
11966
+ if (freshConv) {
11967
+ freshConv.messages = buildCronMessages(task, historyMessages, result);
11968
+ if (result.contextTokens > 0) freshConv.contextTokens = result.contextTokens;
11969
+ if (result.contextWindow > 0) freshConv.contextWindow = result.contextWindow;
11970
+ freshConv.updatedAt = Date.now();
11971
+ await store.update(freshConv);
11972
+ if (result.response) {
11973
+ try {
11974
+ await adapter.sendReply(
11975
+ {
11976
+ channelId: chatId,
11977
+ platformThreadId: freshConv.channelMeta?.platformThreadId ?? chatId
11978
+ },
11979
+ result.response
11980
+ );
11981
+ } catch (sendError) {
11982
+ const sendMsg = sendError instanceof Error ? sendError.message : String(sendError);
11983
+ process.stderr.write(`[cron] ${jobName}: send to ${chatId} failed: ${sendMsg}
11530
11984
  `);
11985
+ }
11531
11986
  }
11532
11987
  }
11533
11988
  totalChats++;
@@ -11535,6 +11990,15 @@ ${config.task}`;
11535
11990
  const runMsg = runError instanceof Error ? runError.message : String(runError);
11536
11991
  process.stderr.write(`[cron] ${jobName}: run for chat ${chatId} failed: ${runMsg}
11537
11992
  `);
11993
+ } finally {
11994
+ activeRuns?.delete(convId);
11995
+ const hadDeferred = deferredCallbacks?.delete(convId) ?? false;
11996
+ const checkConv = await store.get(convId);
11997
+ if (hadDeferred || checkConv?.pendingSubagentResults?.length) {
11998
+ runCallback?.(convId, true).catch(
11999
+ (err) => console.error(`[cron] ${jobName}: subagent callback for ${chatId} failed:`, err instanceof Error ? err.message : err)
12000
+ );
12001
+ }
11538
12002
  }
11539
12003
  }
11540
12004
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
@@ -11548,15 +12012,35 @@ ${config.task}`;
11548
12012
  }
11549
12013
  return;
11550
12014
  }
12015
+ let cronConvId;
11551
12016
  try {
11552
12017
  const conversation = await store.create(
11553
12018
  "local-owner",
11554
12019
  `[cron] ${jobName} ${timestamp}`
11555
12020
  );
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);
12021
+ cronConvId = conversation.conversationId;
12022
+ activeRuns?.set(cronConvId, {
12023
+ ownerId: "local-owner",
12024
+ abortController: new AbortController(),
12025
+ runId: null
12026
+ });
12027
+ const broadcast = handler._broadcastEvent;
12028
+ const result = await runCronAgent(
12029
+ harnessRef,
12030
+ config.task,
12031
+ cronConvId,
12032
+ [],
12033
+ broadcast ? (ev) => broadcast(cronConvId, ev) : void 0
12034
+ );
12035
+ handler._finishConversationStream?.(cronConvId);
12036
+ const freshConv = await store.get(cronConvId);
12037
+ if (freshConv) {
12038
+ freshConv.messages = buildCronMessages(config.task, [], result);
12039
+ if (result.contextTokens > 0) freshConv.contextTokens = result.contextTokens;
12040
+ if (result.contextWindow > 0) freshConv.contextWindow = result.contextWindow;
12041
+ freshConv.updatedAt = Date.now();
12042
+ await store.update(freshConv);
12043
+ }
11560
12044
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
11561
12045
  process.stdout.write(
11562
12046
  `[cron] ${jobName} completed in ${elapsed}s (${result.steps} steps)
@@ -11569,6 +12053,17 @@ ${config.task}`;
11569
12053
  `[cron] ${jobName} failed after ${elapsed}s: ${msg}
11570
12054
  `
11571
12055
  );
12056
+ } finally {
12057
+ if (cronConvId) {
12058
+ activeRuns?.delete(cronConvId);
12059
+ const hadDeferred = deferredCallbacks?.delete(cronConvId) ?? false;
12060
+ const checkConv = await store.get(cronConvId);
12061
+ if (hadDeferred || checkConv?.pendingSubagentResults?.length) {
12062
+ runCallback?.(cronConvId, true).catch(
12063
+ (err) => console.error(`[cron] ${jobName}: subagent callback failed:`, err instanceof Error ? err.message : err)
12064
+ );
12065
+ }
12066
+ }
11572
12067
  }
11573
12068
  }
11574
12069
  );
@@ -11676,7 +12171,7 @@ var runInteractive = async (workingDir, params) => {
11676
12171
  await harness.initialize();
11677
12172
  const identity = await ensureAgentIdentity2(workingDir);
11678
12173
  try {
11679
- const { runInteractiveInk } = await import("./run-interactive-ink-EMTC7MK7.js");
12174
+ const { runInteractiveInk } = await import("./run-interactive-ink-FUMHN6DS.js");
11680
12175
  await runInteractiveInk({
11681
12176
  harness,
11682
12177
  params,