@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.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +15 -0
- package/dist/{chunk-UYZOJWGL.js → chunk-FA546WPW.js} +217 -29
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-ZSIGWFLZ.js → run-interactive-ink-FUMHN6DS.js} +1 -1
- package/package.json +4 -4
- package/src/index.ts +118 -27
- package/src/web-ui-client.ts +111 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/cli@0.30.
|
|
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
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mESM[39m Build start
|
|
10
10
|
[32mESM[39m [1mdist/cli.js [22m[32m94.00 B[39m
|
|
11
|
+
[32mESM[39m [1mdist/run-interactive-ink-FUMHN6DS.js [22m[32m56.86 KB[39m
|
|
11
12
|
[32mESM[39m [1mdist/index.js [22m[32m857.00 B[39m
|
|
12
|
-
[32mESM[39m [1mdist/
|
|
13
|
-
[32mESM[39m
|
|
14
|
-
[32mESM[39m ⚡️ Build success in 64ms
|
|
13
|
+
[32mESM[39m [1mdist/chunk-FA546WPW.js [22m[32m490.84 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 71ms
|
|
15
15
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 4822ms
|
|
17
17
|
[32mDTS[39m [1mdist/cli.d.ts [22m[32m20.00 B[39m
|
|
18
18
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m4.16 KB[39m
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9686
|
-
|
|
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
|
-
|
|
9689
|
-
|
|
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
|
|
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-
|
|
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
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/cli",
|
|
3
|
-
"version": "0.30.
|
|
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.
|
|
31
|
-
"@poncho-ai/
|
|
32
|
-
"@poncho-ai/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
3044
|
-
|
|
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
|
-
|
|
3047
|
-
|
|
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
|
|
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
|
}
|
package/src/web-ui-client.ts
CHANGED
|
@@ -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.
|