@poncho-ai/cli 0.30.1 → 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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/cli@0.30.1 build /home/runner/work/poncho-ai/poncho-ai/packages/cli
2
+ > @poncho-ai/cli@0.30.2 build /home/runner/work/poncho-ai/poncho-ai/packages/cli
3
3
  > tsup src/index.ts src/cli.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/cli.ts, src/index.ts
@@ -8,11 +8,11 @@
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
10
  ESM dist/cli.js 94.00 B
11
+ ESM dist/run-interactive-ink-FUMHN6DS.js 56.86 KB
11
12
  ESM dist/index.js 857.00 B
12
- ESM dist/run-interactive-ink-ZSIGWFLZ.js 56.86 KB
13
- ESM dist/chunk-UYZOJWGL.js 481.08 KB
14
- ESM ⚡️ Build success in 64ms
13
+ ESM dist/chunk-FA546WPW.js 490.84 KB
14
+ ESM ⚡️ Build success in 71ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 3869ms
16
+ DTS ⚡️ Build success in 4822ms
17
17
  DTS dist/cli.d.ts 20.00 B
18
18
  DTS dist/index.d.ts 4.16 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @poncho-ai/cli
2
2
 
3
+ ## 0.30.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`98df42f`](https://github.com/cesr/poncho-ai/commit/98df42f79e0a376d0a864598557758bfa644039d) Thanks [@cesr](https://github.com/cesr)! - Fix serverless subagent and continuation reliability
8
+ - Use stable internal secret across serverless instances for callback auth
9
+ - Wrap continuation self-fetches in waitUntil to survive function shutdown
10
+ - Set runStatus during callback re-runs so clients detect active processing
11
+ - Add post-streaming soft deadline check to catch long model responses
12
+ - Client auto-recovers from abrupt stream termination and orphaned continuations
13
+ - Fix callback continuation losing \_continuationMessages when no pending results
14
+
15
+ - Updated dependencies [[`98df42f`](https://github.com/cesr/poncho-ai/commit/98df42f79e0a376d0a864598557758bfa644039d)]:
16
+ - @poncho-ai/harness@0.28.2
17
+
3
18
  ## 0.30.1
4
19
 
5
20
  ### Patch Changes
@@ -3173,6 +3173,101 @@ var getWebUiClientScript = (markedSource2) => `
3173
3173
  });
3174
3174
  } else if (willStream) {
3175
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
+ })();
3176
3271
  }
3177
3272
  };
3178
3273
 
@@ -4256,6 +4351,7 @@ var getWebUiClientScript = (markedSource2) => `
4256
4351
  let _totalSteps = 0;
4257
4352
  let _maxSteps = 0;
4258
4353
  let _isContinuation = false;
4354
+ let _receivedTerminalEvent = false;
4259
4355
  while (true) {
4260
4356
  let _shouldContinue = false;
4261
4357
  let fetchOpts;
@@ -4585,6 +4681,7 @@ var getWebUiClientScript = (markedSource2) => `
4585
4681
  }
4586
4682
  }
4587
4683
  if (eventName === "run:completed") {
4684
+ _receivedTerminalEvent = true;
4588
4685
  _totalSteps += typeof payload.result?.steps === "number" ? payload.result.steps : 0;
4589
4686
  if (typeof payload.result?.maxSteps === "number") _maxSteps = payload.result.maxSteps;
4590
4687
  if (payload.result?.continuation === true && (_maxSteps <= 0 || _totalSteps < _maxSteps)) {
@@ -4601,10 +4698,12 @@ var getWebUiClientScript = (markedSource2) => `
4601
4698
  }
4602
4699
  }
4603
4700
  if (eventName === "run:cancelled") {
4701
+ _receivedTerminalEvent = true;
4604
4702
  finalizeAssistantMessage();
4605
4703
  renderIfActiveConversation(false);
4606
4704
  }
4607
4705
  if (eventName === "run:error") {
4706
+ _receivedTerminalEvent = true;
4608
4707
  finalizeAssistantMessage();
4609
4708
  const errMsg = payload.error?.message || "Something went wrong";
4610
4709
  assistantMessage._error = errMsg;
@@ -4615,7 +4714,19 @@ var getWebUiClientScript = (markedSource2) => `
4615
4714
  }
4616
4715
  });
4617
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
+ }
4618
4728
  if (!_shouldContinue) break;
4729
+ _receivedTerminalEvent = false;
4619
4730
  _isContinuation = true;
4620
4731
  }
4621
4732
  // Update active state only if user is still on this conversation.
@@ -7664,12 +7775,16 @@ Set environment variables on your deployment platform:
7664
7775
  ANTHROPIC_API_KEY=sk-ant-... # Required
7665
7776
  PONCHO_AUTH_TOKEN=your-secret # Optional: protect your endpoint
7666
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
7667
7779
  \`\`\`
7668
7780
 
7669
7781
  When \`PONCHO_MAX_DURATION\` is set, the agent automatically checkpoints and resumes across
7670
7782
  request cycles when it approaches the platform timeout. The web UI and client SDK handle
7671
7783
  this transparently.
7672
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
+
7673
7788
  ## Troubleshooting
7674
7789
 
7675
7790
  ### Vercel deploy issues
@@ -7677,6 +7792,7 @@ this transparently.
7677
7792
  - After upgrading \`@poncho-ai/cli\`, re-run \`poncho build vercel --force\` to refresh generated deploy files.
7678
7793
  - If Vercel fails during \`pnpm install\` due to a lockfile mismatch, run \`pnpm install --no-frozen-lockfile\` locally and commit \`pnpm-lock.yaml\`.
7679
7794
  - Deploy from the project root: \`vercel deploy --prod\`.
7795
+ - For subagents/background callbacks, set \`PONCHO_INTERNAL_SECRET\` and use non-local state storage.
7680
7796
 
7681
7797
  For full reference:
7682
7798
  https://github.com/cesr/poncho-ai
@@ -8392,7 +8508,8 @@ data: ${JSON.stringify(statusPayload)}
8392
8508
  await harness.initialize();
8393
8509
  const telemetry = new TelemetryEmitter(config?.telemetry);
8394
8510
  const identity = await ensureAgentIdentity2(workingDir);
8395
- const conversationStore = createConversationStore(resolveStateConfig(config), {
8511
+ const stateConfig = resolveStateConfig(config);
8512
+ const conversationStore = createConversationStore(stateConfig, {
8396
8513
  workingDir,
8397
8514
  agentId: identity.id
8398
8515
  });
@@ -8731,9 +8848,10 @@ data: ${JSON.stringify(statusPayload)}
8731
8848
  } catch {
8732
8849
  }
8733
8850
  if (isServerless) {
8734
- selfFetchWithRetry(`/api/internal/subagent/${encodeURIComponent(childConversationId)}/run`, { continuation: true }).catch(
8851
+ const work = selfFetchWithRetry(`/api/internal/subagent/${encodeURIComponent(childConversationId)}/run`, { continuation: true }).catch(
8735
8852
  (err) => console.error(`[poncho][subagent] Continuation self-fetch failed:`, err instanceof Error ? err.message : err)
8736
8853
  );
8854
+ doWaitUntil(work);
8737
8855
  } else {
8738
8856
  runSubagent(childConversationId, parentConversationId, task, ownerId, true).catch(
8739
8857
  (err) => console.error(`[poncho][subagent] Continuation failed:`, err instanceof Error ? err.message : err)
@@ -8837,7 +8955,8 @@ data: ${JSON.stringify(statusPayload)}
8837
8955
  const conversation = await conversationStore.get(conversationId);
8838
8956
  if (!conversation) return;
8839
8957
  const pendingResults = conversation.pendingSubagentResults ?? [];
8840
- if (pendingResults.length === 0) return;
8958
+ const hasOrphanedContinuation = pendingResults.length === 0 && Array.isArray(conversation._continuationMessages) && conversation._continuationMessages.length > 0 && !activeConversationRuns.has(conversationId);
8959
+ if (pendingResults.length === 0 && !hasOrphanedContinuation) return;
8841
8960
  if (!skipLockCheck && conversation.runningCallbackSince) {
8842
8961
  const elapsed = Date.now() - conversation.runningCallbackSince;
8843
8962
  if (elapsed < CALLBACK_LOCK_STALE_MS) {
@@ -8847,6 +8966,7 @@ data: ${JSON.stringify(statusPayload)}
8847
8966
  }
8848
8967
  conversation.pendingSubagentResults = [];
8849
8968
  conversation.runningCallbackSince = Date.now();
8969
+ conversation.runStatus = "running";
8850
8970
  const callbackCount = (conversation.subagentCallbackCount ?? 0) + 1;
8851
8971
  conversation.subagentCallbackCount = callbackCount;
8852
8972
  for (const pr of pendingResults) {
@@ -8866,10 +8986,12 @@ ${resultBody}`,
8866
8986
  if (callbackCount > MAX_SUBAGENT_CALLBACK_COUNT) {
8867
8987
  console.warn(`[poncho][subagent-callback] Circuit breaker: ${callbackCount} callbacks for ${conversationId}, skipping re-run`);
8868
8988
  conversation.runningCallbackSince = void 0;
8989
+ conversation.runStatus = "idle";
8869
8990
  await conversationStore.update(conversation);
8870
8991
  return;
8871
8992
  }
8872
- 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)" : ""}`);
8873
8995
  const abortController = new AbortController();
8874
8996
  activeConversationRuns.set(conversationId, {
8875
8997
  ownerId: conversation.ownerId,
@@ -8887,7 +9009,7 @@ ${resultBody}`,
8887
9009
  finished: false
8888
9010
  });
8889
9011
  }
8890
- const historyMessages = [...conversation.messages];
9012
+ const historyMessages = isContinuationResume && conversation._continuationMessages?.length ? [...conversation._continuationMessages] : [...conversation.messages];
8891
9013
  let assistantResponse = "";
8892
9014
  let latestRunId = "";
8893
9015
  let runContinuation = false;
@@ -8976,6 +9098,7 @@ ${resultBody}`,
8976
9098
  }
8977
9099
  freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
8978
9100
  freshConv.runningCallbackSince = void 0;
9101
+ freshConv.runStatus = "idle";
8979
9102
  if (runContextTokens > 0) freshConv.contextTokens = runContextTokens;
8980
9103
  if (runContextWindow > 0) freshConv.contextWindow = runContextWindow;
8981
9104
  freshConv.updatedAt = Date.now();
@@ -9000,9 +9123,14 @@ ${resultBody}`,
9000
9123
  }
9001
9124
  if (runContinuation) {
9002
9125
  if (isServerless) {
9003
- selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(
9126
+ const work = selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(
9004
9127
  (err) => console.error(`[poncho][subagent-callback] Continuation self-fetch failed:`, err instanceof Error ? err.message : err)
9005
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
+ );
9006
9134
  }
9007
9135
  }
9008
9136
  } catch (err) {
@@ -9010,6 +9138,7 @@ ${resultBody}`,
9010
9138
  const errConv = await conversationStore.get(conversationId);
9011
9139
  if (errConv) {
9012
9140
  errConv.runningCallbackSince = void 0;
9141
+ errConv.runStatus = "idle";
9013
9142
  await conversationStore.update(errConv);
9014
9143
  }
9015
9144
  } finally {
@@ -9661,34 +9790,89 @@ ${resultBody}`,
9661
9790
  }
9662
9791
  }
9663
9792
  const isServerless = !!waitUntilHook;
9664
- 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;
9665
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
+ }
9666
9821
  const doWaitUntil = (promise) => {
9667
9822
  if (waitUntilHook) waitUntilHook(promise);
9668
9823
  };
9669
- const selfFetch = (path, body) => {
9670
- if (!selfBaseUrl) return Promise.resolve();
9671
- return fetch(`${selfBaseUrl}${path}`, {
9672
- method: "POST",
9673
- headers: {
9674
- "Content-Type": "application/json",
9675
- "x-poncho-internal": internalSecret
9676
- },
9677
- body: body ? JSON.stringify(body) : void 0
9678
- }).catch((err) => {
9679
- console.error(`[poncho][self-fetch] Failed ${path}:`, err instanceof Error ? err.message : err);
9680
- });
9681
- };
9682
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;
9683
9830
  for (let attempt = 0; attempt < retries; attempt++) {
9684
9831
  try {
9685
- const result = await selfFetch(path, body);
9686
- 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
+ );
9687
9847
  } catch (err) {
9688
- if (attempt === retries - 1) throw err;
9689
- await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
9848
+ lastError = err;
9849
+ }
9850
+ if (attempt === retries - 1) {
9851
+ break;
9690
9852
  }
9853
+ await new Promise((resolveSleep) => setTimeout(resolveSleep, 1e3 * (attempt + 1)));
9691
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;
9692
9876
  };
9693
9877
  const messagingAdapters = /* @__PURE__ */ new Map();
9694
9878
  const messagingBridges = [];
@@ -10005,7 +10189,7 @@ ${resultBody}`,
10005
10189
  }
10006
10190
  }
10007
10191
  if (pathname?.startsWith("/api/internal/") && request.method === "POST") {
10008
- if (request.headers["x-poncho-internal"] !== internalSecret) {
10192
+ if (!isValidInternalRequest(request.headers)) {
10009
10193
  writeJson(response, 403, { code: "FORBIDDEN", message: "Internal endpoint" });
10010
10194
  return;
10011
10195
  }
@@ -10641,14 +10825,18 @@ data: ${JSON.stringify(frame)}
10641
10825
  }
10642
10826
  }
10643
10827
  }
10828
+ const hasPendingCallbackResults = Array.isArray(conversation.pendingSubagentResults) && conversation.pendingSubagentResults.length > 0;
10829
+ const needsContinuation = !hasActiveRun && Array.isArray(conversation._continuationMessages) && conversation._continuationMessages.length > 0;
10644
10830
  writeJson(response, 200, {
10645
10831
  conversation: {
10646
10832
  ...conversation,
10647
- pendingApprovals: storedPending
10833
+ pendingApprovals: storedPending,
10834
+ _continuationMessages: void 0
10648
10835
  },
10649
10836
  subagentPendingApprovals: subagentPending,
10650
- hasActiveRun,
10651
- hasRunningSubagents
10837
+ hasActiveRun: hasActiveRun || hasPendingCallbackResults,
10838
+ hasRunningSubagents,
10839
+ needsContinuation
10652
10840
  });
10653
10841
  return;
10654
10842
  }
@@ -11983,7 +12171,7 @@ var runInteractive = async (workingDir, params) => {
11983
12171
  await harness.initialize();
11984
12172
  const identity = await ensureAgentIdentity2(workingDir);
11985
12173
  try {
11986
- const { runInteractiveInk } = await import("./run-interactive-ink-ZSIGWFLZ.js");
12174
+ const { runInteractiveInk } = await import("./run-interactive-ink-FUMHN6DS.js");
11987
12175
  await runInteractiveInk({
11988
12176
  harness,
11989
12177
  params,
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-UYZOJWGL.js";
4
+ } from "./chunk-FA546WPW.js";
5
5
 
6
6
  // src/cli.ts
7
7
  void main();
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  runTests,
24
24
  startDevServer,
25
25
  updateAgentGuidance
26
- } from "./chunk-UYZOJWGL.js";
26
+ } from "./chunk-FA546WPW.js";
27
27
  export {
28
28
  addSkill,
29
29
  buildCli,
@@ -2,7 +2,7 @@ import {
2
2
  consumeFirstRunIntro,
3
3
  inferConversationTitle,
4
4
  resolveHarnessEnvironment
5
- } from "./chunk-UYZOJWGL.js";
5
+ } from "./chunk-FA546WPW.js";
6
6
 
7
7
  // src/run-interactive-ink.ts
8
8
  import * as readline from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/cli",
3
- "version": "0.30.1",
3
+ "version": "0.30.2",
4
4
  "description": "CLI for building and deploying AI agents",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,9 +27,9 @@
27
27
  "react": "^19.2.4",
28
28
  "react-devtools-core": "^6.1.5",
29
29
  "yaml": "^2.8.1",
30
- "@poncho-ai/harness": "0.28.1",
31
- "@poncho-ai/messaging": "0.7.2",
32
- "@poncho-ai/sdk": "1.6.1"
30
+ "@poncho-ai/harness": "0.28.2",
31
+ "@poncho-ai/sdk": "1.6.1",
32
+ "@poncho-ai/messaging": "0.7.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/busboy": "^1.5.4",
package/src/index.ts CHANGED
@@ -721,12 +721,16 @@ Set environment variables on your deployment platform:
721
721
  ANTHROPIC_API_KEY=sk-ant-... # Required
722
722
  PONCHO_AUTH_TOKEN=your-secret # Optional: protect your endpoint
723
723
  PONCHO_MAX_DURATION=55 # Optional: serverless timeout in seconds (enables auto-continuation)
724
+ PONCHO_INTERNAL_SECRET=... # Recommended on serverless: shared secret for internal callback auth
724
725
  \`\`\`
725
726
 
726
727
  When \`PONCHO_MAX_DURATION\` is set, the agent automatically checkpoints and resumes across
727
728
  request cycles when it approaches the platform timeout. The web UI and client SDK handle
728
729
  this transparently.
729
730
 
731
+ For serverless deployments with subagents or background callbacks, use a shared state backend
732
+ (\`upstash\`, \`redis\`, or \`dynamodb\`) instead of \`state.provider: 'local'\` / \`'memory'\`.
733
+
730
734
  ## Troubleshooting
731
735
 
732
736
  ### Vercel deploy issues
@@ -734,6 +738,7 @@ this transparently.
734
738
  - After upgrading \`@poncho-ai/cli\`, re-run \`poncho build vercel --force\` to refresh generated deploy files.
735
739
  - If Vercel fails during \`pnpm install\` due to a lockfile mismatch, run \`pnpm install --no-frozen-lockfile\` locally and commit \`pnpm-lock.yaml\`.
736
740
  - Deploy from the project root: \`vercel deploy --prod\`.
741
+ - For subagents/background callbacks, set \`PONCHO_INTERNAL_SECRET\` and use non-local state storage.
737
742
 
738
743
  For full reference:
739
744
  https://github.com/cesr/poncho-ai
@@ -1554,7 +1559,8 @@ export const createRequestHandler = async (options?: {
1554
1559
  await harness.initialize();
1555
1560
  const telemetry = new TelemetryEmitter(config?.telemetry);
1556
1561
  const identity = await ensureAgentIdentity(workingDir);
1557
- const conversationStore = createConversationStore(resolveStateConfig(config), {
1562
+ const stateConfig = resolveStateConfig(config);
1563
+ const conversationStore = createConversationStore(stateConfig, {
1558
1564
  workingDir,
1559
1565
  agentId: identity.id,
1560
1566
  });
@@ -1956,9 +1962,10 @@ export const createRequestHandler = async (options?: {
1956
1962
  try { await childHarness.shutdown(); } catch {}
1957
1963
 
1958
1964
  if (isServerless) {
1959
- selfFetchWithRetry(`/api/internal/subagent/${encodeURIComponent(childConversationId)}/run`, { continuation: true }).catch(err =>
1965
+ const work = selfFetchWithRetry(`/api/internal/subagent/${encodeURIComponent(childConversationId)}/run`, { continuation: true }).catch(err =>
1960
1966
  console.error(`[poncho][subagent] Continuation self-fetch failed:`, err instanceof Error ? err.message : err),
1961
1967
  );
1968
+ doWaitUntil(work);
1962
1969
  } else {
1963
1970
  runSubagent(childConversationId, parentConversationId, task, ownerId, true).catch(err =>
1964
1971
  console.error(`[poncho][subagent] Continuation failed:`, err instanceof Error ? err.message : err),
@@ -2079,7 +2086,11 @@ export const createRequestHandler = async (options?: {
2079
2086
  if (!conversation) return;
2080
2087
 
2081
2088
  const pendingResults = conversation.pendingSubagentResults ?? [];
2082
- if (pendingResults.length === 0) return;
2089
+ const hasOrphanedContinuation = pendingResults.length === 0
2090
+ && Array.isArray(conversation._continuationMessages)
2091
+ && conversation._continuationMessages.length > 0
2092
+ && !activeConversationRuns.has(conversationId);
2093
+ if (pendingResults.length === 0 && !hasOrphanedContinuation) return;
2083
2094
 
2084
2095
  // Store-based lock for serverless: skip if another invocation is processing.
2085
2096
  // When re-triggered from a previous callback's finally block, skipLockCheck
@@ -2095,6 +2106,7 @@ export const createRequestHandler = async (options?: {
2095
2106
  // Acquire lock and clear pending
2096
2107
  conversation.pendingSubagentResults = [];
2097
2108
  conversation.runningCallbackSince = Date.now();
2109
+ conversation.runStatus = "running";
2098
2110
  const callbackCount = (conversation.subagentCallbackCount ?? 0) + 1;
2099
2111
  conversation.subagentCallbackCount = callbackCount;
2100
2112
 
@@ -2116,11 +2128,13 @@ export const createRequestHandler = async (options?: {
2116
2128
  if (callbackCount > MAX_SUBAGENT_CALLBACK_COUNT) {
2117
2129
  console.warn(`[poncho][subagent-callback] Circuit breaker: ${callbackCount} callbacks for ${conversationId}, skipping re-run`);
2118
2130
  conversation.runningCallbackSince = undefined;
2131
+ conversation.runStatus = "idle";
2119
2132
  await conversationStore.update(conversation);
2120
2133
  return;
2121
2134
  }
2122
2135
 
2123
- console.log(`[poncho][subagent-callback] Processing ${pendingResults.length} result(s) for ${conversationId} (callback #${callbackCount})`);
2136
+ const isContinuationResume = hasOrphanedContinuation && pendingResults.length === 0;
2137
+ console.log(`[poncho][subagent-callback] Processing ${pendingResults.length} result(s) for ${conversationId} (callback #${callbackCount})${isContinuationResume ? " (continuation resume)" : ""}`);
2124
2138
 
2125
2139
  const abortController = new AbortController();
2126
2140
  activeConversationRuns.set(conversationId, {
@@ -2142,7 +2156,9 @@ export const createRequestHandler = async (options?: {
2142
2156
  });
2143
2157
  }
2144
2158
 
2145
- const historyMessages = [...conversation.messages];
2159
+ const historyMessages = isContinuationResume && conversation._continuationMessages?.length
2160
+ ? [...conversation._continuationMessages]
2161
+ : [...conversation.messages];
2146
2162
  let assistantResponse = "";
2147
2163
  let latestRunId = "";
2148
2164
  let runContinuation = false;
@@ -2236,6 +2252,7 @@ export const createRequestHandler = async (options?: {
2236
2252
  }
2237
2253
  freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
2238
2254
  freshConv.runningCallbackSince = undefined;
2255
+ freshConv.runStatus = "idle";
2239
2256
  if (runContextTokens > 0) freshConv.contextTokens = runContextTokens;
2240
2257
  if (runContextWindow > 0) freshConv.contextWindow = runContextWindow;
2241
2258
  freshConv.updatedAt = Date.now();
@@ -2264,9 +2281,14 @@ export const createRequestHandler = async (options?: {
2264
2281
  // Handle continuation for the callback run itself
2265
2282
  if (runContinuation) {
2266
2283
  if (isServerless) {
2267
- selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(err =>
2284
+ const work = selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(err =>
2268
2285
  console.error(`[poncho][subagent-callback] Continuation self-fetch failed:`, err instanceof Error ? err.message : err),
2269
2286
  );
2287
+ doWaitUntil(work);
2288
+ } else {
2289
+ processSubagentCallback(conversationId, true).catch(err =>
2290
+ console.error(`[poncho][subagent-callback] Continuation failed:`, err instanceof Error ? err.message : err),
2291
+ );
2270
2292
  }
2271
2293
  }
2272
2294
  } catch (err) {
@@ -2274,6 +2296,7 @@ export const createRequestHandler = async (options?: {
2274
2296
  const errConv = await conversationStore.get(conversationId);
2275
2297
  if (errConv) {
2276
2298
  errConv.runningCallbackSince = undefined;
2299
+ errConv.runStatus = "idle";
2277
2300
  await conversationStore.update(errConv);
2278
2301
  }
2279
2302
  } finally {
@@ -3014,41 +3037,102 @@ export const createRequestHandler = async (options?: {
3014
3037
  }
3015
3038
 
3016
3039
  const isServerless = !!waitUntilHook;
3017
- const internalSecret = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
3040
+ const configuredInternalSecret = process.env.PONCHO_INTERNAL_SECRET?.trim();
3041
+ const vercelDeploymentSecret = process.env.VERCEL_DEPLOYMENT_ID?.trim();
3042
+ const fallbackInternalSecret = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
3043
+ const internalSecret = configuredInternalSecret || vercelDeploymentSecret || fallbackInternalSecret;
3044
+ const isUsingEphemeralInternalSecret = !configuredInternalSecret && !vercelDeploymentSecret;
3018
3045
  let selfBaseUrl: string | null = process.env.VERCEL_URL
3019
3046
  ? `https://${process.env.VERCEL_URL}`
3020
3047
  : null;
3021
3048
 
3049
+ if (!selfBaseUrl && process.env.VERCEL_PROJECT_PRODUCTION_URL) {
3050
+ selfBaseUrl = `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
3051
+ }
3052
+ if (!selfBaseUrl && process.env.PONCHO_SELF_BASE_URL) {
3053
+ selfBaseUrl = process.env.PONCHO_SELF_BASE_URL.replace(/\/+$/, "");
3054
+ }
3055
+
3056
+ if (isServerless && isUsingEphemeralInternalSecret) {
3057
+ console.warn(
3058
+ "[poncho][serverless] No stable internal secret found. Set PONCHO_INTERNAL_SECRET to avoid intermittent internal callback failures across instances.",
3059
+ );
3060
+ }
3061
+ if (isServerless && !selfBaseUrl) {
3062
+ console.warn(
3063
+ "[poncho][serverless] No self base URL available. Set PONCHO_SELF_BASE_URL if internal background callbacks fail.",
3064
+ );
3065
+ }
3066
+ const stateProvider = stateConfig?.provider ?? "local";
3067
+ if (isServerless && (stateProvider === "local" || stateProvider === "memory")) {
3068
+ console.warn(
3069
+ `[poncho][serverless] state.provider="${stateProvider}" may lose cross-invocation state. Prefer "upstash", "redis", or "dynamodb" for subagents/reliability.`,
3070
+ );
3071
+ }
3072
+
3022
3073
  const doWaitUntil = (promise: Promise<unknown>): void => {
3023
3074
  if (waitUntilHook) waitUntilHook(promise);
3024
3075
  };
3025
3076
 
3026
- const selfFetch = (path: string, body?: Record<string, unknown>): Promise<Response | void> => {
3027
- if (!selfBaseUrl) return Promise.resolve();
3028
- return fetch(`${selfBaseUrl}${path}`, {
3029
- method: "POST",
3030
- headers: {
3031
- "Content-Type": "application/json",
3032
- "x-poncho-internal": internalSecret,
3033
- },
3034
- body: body ? JSON.stringify(body) : undefined,
3035
- }).catch(err => {
3036
- console.error(`[poncho][self-fetch] Failed ${path}:`, err instanceof Error ? err.message : err);
3037
- }) as Promise<Response | void>;
3038
- };
3039
-
3040
3077
  const selfFetchWithRetry = async (path: string, body?: Record<string, unknown>, retries = 3): Promise<Response | void> => {
3078
+ if (!selfBaseUrl) {
3079
+ console.error(`[poncho][self-fetch] Missing self base URL for ${path}`);
3080
+ return;
3081
+ }
3082
+ let lastError: unknown;
3041
3083
  for (let attempt = 0; attempt < retries; attempt++) {
3042
3084
  try {
3043
- const result = await selfFetch(path, body);
3044
- return result;
3085
+ const result = await fetch(`${selfBaseUrl}${path}`, {
3086
+ method: "POST",
3087
+ headers: {
3088
+ "Content-Type": "application/json",
3089
+ "x-poncho-internal": internalSecret,
3090
+ },
3091
+ body: body ? JSON.stringify(body) : undefined,
3092
+ });
3093
+ if (result.ok) {
3094
+ return result;
3095
+ }
3096
+ const responseText = await result.text().catch(() => "");
3097
+ lastError = new Error(
3098
+ `HTTP ${result.status}${responseText ? `: ${responseText.slice(0, 200)}` : ""}`,
3099
+ );
3045
3100
  } catch (err) {
3046
- if (attempt === retries - 1) throw err;
3047
- await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
3101
+ lastError = err;
3102
+ }
3103
+ if (attempt === retries - 1) {
3104
+ break;
3105
+ }
3106
+ await new Promise((resolveSleep) => setTimeout(resolveSleep, 1000 * (attempt + 1)));
3107
+ }
3108
+ if (lastError) {
3109
+ console.error(
3110
+ `[poncho][self-fetch] Failed ${path} after ${retries} attempt(s):`,
3111
+ lastError instanceof Error ? lastError.message : String(lastError),
3112
+ );
3113
+ if (
3114
+ lastError instanceof Error
3115
+ && (lastError.message.includes("HTTP 403") || lastError.message.includes("HTTP 401"))
3116
+ ) {
3117
+ console.error(
3118
+ "[poncho][self-fetch] Internal auth failed. Ensure all serverless instances share PONCHO_INTERNAL_SECRET.",
3119
+ );
3048
3120
  }
3121
+ } else {
3122
+ console.error(`[poncho][self-fetch] Failed ${path} after ${retries} attempt(s).`);
3049
3123
  }
3050
3124
  };
3051
3125
 
3126
+ const getInternalRequestHeader = (headers: IncomingMessage["headers"]): string | undefined => {
3127
+ const value = headers["x-poncho-internal"];
3128
+ return Array.isArray(value) ? value[0] : value;
3129
+ };
3130
+
3131
+ const isValidInternalRequest = (headers: IncomingMessage["headers"]): boolean => {
3132
+ const headerValue = getInternalRequestHeader(headers);
3133
+ return typeof headerValue === "string" && headerValue === internalSecret;
3134
+ };
3135
+
3052
3136
  const messagingAdapters = new Map<string, MessagingAdapter>();
3053
3137
  const messagingBridges: AgentBridge[] = [];
3054
3138
  if (config?.messaging && config.messaging.length > 0) {
@@ -3417,7 +3501,7 @@ export const createRequestHandler = async (options?: {
3417
3501
 
3418
3502
  // ── Internal endpoints (self-fetch only, secured by startup secret) ──
3419
3503
  if (pathname?.startsWith("/api/internal/") && request.method === "POST") {
3420
- if (request.headers["x-poncho-internal"] !== internalSecret) {
3504
+ if (!isValidInternalRequest(request.headers)) {
3421
3505
  writeJson(response, 403, { code: "FORBIDDEN", message: "Internal endpoint" });
3422
3506
  return;
3423
3507
  }
@@ -4171,14 +4255,21 @@ export const createRequestHandler = async (options?: {
4171
4255
  }
4172
4256
  }
4173
4257
  }
4258
+ const hasPendingCallbackResults = Array.isArray(conversation.pendingSubagentResults)
4259
+ && conversation.pendingSubagentResults.length > 0;
4260
+ const needsContinuation = !hasActiveRun
4261
+ && Array.isArray(conversation._continuationMessages)
4262
+ && conversation._continuationMessages.length > 0;
4174
4263
  writeJson(response, 200, {
4175
4264
  conversation: {
4176
4265
  ...conversation,
4177
4266
  pendingApprovals: storedPending,
4267
+ _continuationMessages: undefined,
4178
4268
  },
4179
4269
  subagentPendingApprovals: subagentPending,
4180
- hasActiveRun,
4270
+ hasActiveRun: hasActiveRun || hasPendingCallbackResults,
4181
4271
  hasRunningSubagents,
4272
+ needsContinuation,
4182
4273
  });
4183
4274
  return;
4184
4275
  }
@@ -1344,6 +1344,101 @@ export const getWebUiClientScript = (markedSource: string): string => `
1344
1344
  });
1345
1345
  } else if (willStream) {
1346
1346
  setStreaming(true);
1347
+ } else if (payload.needsContinuation && !payload.conversation.parentConversationId) {
1348
+ console.log("[poncho] Detected orphaned continuation for", conversationId, "— auto-resuming");
1349
+ (async () => {
1350
+ try {
1351
+ setStreaming(true);
1352
+ var localMsgs = state.activeMessages || [];
1353
+ var contAssistant = {
1354
+ role: "assistant",
1355
+ content: "",
1356
+ _sections: [],
1357
+ _currentText: "",
1358
+ _currentTools: [],
1359
+ _toolImages: [],
1360
+ _activeActivities: [],
1361
+ _pendingApprovals: [],
1362
+ metadata: { toolActivity: [] }
1363
+ };
1364
+ localMsgs.push(contAssistant);
1365
+ state.activeMessages = localMsgs;
1366
+ state._activeStreamMessages = localMsgs;
1367
+ renderMessages(localMsgs, true);
1368
+ var contResp = await fetch(
1369
+ "/api/conversations/" + encodeURIComponent(conversationId) + "/messages",
1370
+ {
1371
+ method: "POST",
1372
+ credentials: "include",
1373
+ headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
1374
+ body: JSON.stringify({ continuation: true }),
1375
+ },
1376
+ );
1377
+ if (!contResp.ok || !contResp.body) {
1378
+ contAssistant._error = "Failed to resume — reload to retry";
1379
+ setStreaming(false);
1380
+ renderMessages(localMsgs, false);
1381
+ return;
1382
+ }
1383
+ state.activeStreamConversationId = conversationId;
1384
+ var contReader = contResp.body.getReader();
1385
+ var contDecoder = new TextDecoder();
1386
+ var contBuffer = "";
1387
+ while (true) {
1388
+ var chunk = await contReader.read();
1389
+ if (chunk.done) break;
1390
+ contBuffer += contDecoder.decode(chunk.value, { stream: true });
1391
+ contBuffer = parseSseChunk(contBuffer, function(evtName, evtPayload) {
1392
+ if (evtName === "model:chunk" && evtPayload.content) {
1393
+ contAssistant.content = (contAssistant.content || "") + evtPayload.content;
1394
+ contAssistant._currentText += evtPayload.content;
1395
+ }
1396
+ if (evtName === "tool:started") {
1397
+ if (contAssistant._currentText) {
1398
+ contAssistant._sections.push({ type: "text", content: contAssistant._currentText });
1399
+ contAssistant._currentText = "";
1400
+ }
1401
+ contAssistant._currentTools.push("- start \`" + evtPayload.tool + "\`");
1402
+ }
1403
+ if (evtName === "tool:completed") {
1404
+ contAssistant._currentTools.push("- done \`" + evtPayload.tool + "\` (" + evtPayload.duration + "ms)");
1405
+ }
1406
+ if (evtName === "tool:error") {
1407
+ contAssistant._currentTools.push("- error \`" + evtPayload.tool + "\`: " + evtPayload.error);
1408
+ }
1409
+ if (evtName === "run:completed" || evtName === "run:error" || evtName === "run:cancelled") {
1410
+ if (contAssistant._currentTools.length > 0) {
1411
+ contAssistant._sections.push({ type: "tools", content: contAssistant._currentTools });
1412
+ contAssistant._currentTools = [];
1413
+ }
1414
+ if (contAssistant._currentText) {
1415
+ contAssistant._sections.push({ type: "text", content: contAssistant._currentText });
1416
+ contAssistant._currentText = "";
1417
+ }
1418
+ contAssistant._activeActivities = [];
1419
+ if (evtName === "run:error") {
1420
+ contAssistant._error = evtPayload.error?.message || "Something went wrong";
1421
+ }
1422
+ if (evtName === "run:completed" && evtPayload.result?.continuation === true) {
1423
+ // Another continuation needed — reload to pick it up
1424
+ loadConversation(conversationId).catch(function() {});
1425
+ }
1426
+ }
1427
+ renderMessages(localMsgs, true);
1428
+ });
1429
+ }
1430
+ setStreaming(false);
1431
+ renderMessages(localMsgs, false);
1432
+ await loadConversations();
1433
+ } catch (contErr) {
1434
+ console.error("[poncho] Auto-continuation failed:", contErr);
1435
+ setStreaming(false);
1436
+ await loadConversation(conversationId).catch(function() {});
1437
+ } finally {
1438
+ state.activeStreamConversationId = null;
1439
+ state._activeStreamMessages = null;
1440
+ }
1441
+ })();
1347
1442
  }
1348
1443
  };
1349
1444
 
@@ -2427,6 +2522,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2427
2522
  let _totalSteps = 0;
2428
2523
  let _maxSteps = 0;
2429
2524
  let _isContinuation = false;
2525
+ let _receivedTerminalEvent = false;
2430
2526
  while (true) {
2431
2527
  let _shouldContinue = false;
2432
2528
  let fetchOpts;
@@ -2756,6 +2852,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2756
2852
  }
2757
2853
  }
2758
2854
  if (eventName === "run:completed") {
2855
+ _receivedTerminalEvent = true;
2759
2856
  _totalSteps += typeof payload.result?.steps === "number" ? payload.result.steps : 0;
2760
2857
  if (typeof payload.result?.maxSteps === "number") _maxSteps = payload.result.maxSteps;
2761
2858
  if (payload.result?.continuation === true && (_maxSteps <= 0 || _totalSteps < _maxSteps)) {
@@ -2772,10 +2869,12 @@ export const getWebUiClientScript = (markedSource: string): string => `
2772
2869
  }
2773
2870
  }
2774
2871
  if (eventName === "run:cancelled") {
2872
+ _receivedTerminalEvent = true;
2775
2873
  finalizeAssistantMessage();
2776
2874
  renderIfActiveConversation(false);
2777
2875
  }
2778
2876
  if (eventName === "run:error") {
2877
+ _receivedTerminalEvent = true;
2779
2878
  finalizeAssistantMessage();
2780
2879
  const errMsg = payload.error?.message || "Something went wrong";
2781
2880
  assistantMessage._error = errMsg;
@@ -2786,7 +2885,19 @@ export const getWebUiClientScript = (markedSource: string): string => `
2786
2885
  }
2787
2886
  });
2788
2887
  }
2888
+ if (!_shouldContinue && !_receivedTerminalEvent) {
2889
+ try {
2890
+ const recoveryPayload = await api("/api/conversations/" + encodeURIComponent(conversationId));
2891
+ if (recoveryPayload.needsContinuation) {
2892
+ _shouldContinue = true;
2893
+ console.log("[poncho] Stream ended without terminal event, server has continuation — resuming");
2894
+ }
2895
+ } catch (_recoverErr) {
2896
+ console.warn("[poncho] Recovery check failed after abrupt stream end");
2897
+ }
2898
+ }
2789
2899
  if (!_shouldContinue) break;
2900
+ _receivedTerminalEvent = false;
2790
2901
  _isContinuation = true;
2791
2902
  }
2792
2903
  // Update active state only if user is still on this conversation.