@poncho-ai/cli 0.30.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +13 -0
- package/dist/{chunk-5OLH7U3C.js → chunk-UYZOJWGL.js} +487 -180
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-EMTC7MK7.js → run-interactive-ink-ZSIGWFLZ.js} +1 -1
- package/package.json +4 -4
- package/src/index.ts +249 -82
- package/src/web-ui-client.ts +130 -9
- package/src/web-ui-styles.ts +50 -0
package/src/index.ts
CHANGED
|
@@ -1391,6 +1391,11 @@ export type RequestHandler = ((
|
|
|
1391
1391
|
_cronJobs?: Record<string, CronJobConfig>;
|
|
1392
1392
|
_conversationStore?: ConversationStore;
|
|
1393
1393
|
_messagingAdapters?: Map<string, MessagingAdapter>;
|
|
1394
|
+
_activeConversationRuns?: Map<string, { ownerId: string; abortController: AbortController; runId: string | null }>;
|
|
1395
|
+
_pendingCallbackNeeded?: Set<string>;
|
|
1396
|
+
_processSubagentCallback?: (conversationId: string, skipLockCheck?: boolean) => Promise<void>;
|
|
1397
|
+
_broadcastEvent?: (conversationId: string, event: AgentEvent) => void;
|
|
1398
|
+
_finishConversationStream?: (conversationId: string) => void;
|
|
1394
1399
|
};
|
|
1395
1400
|
|
|
1396
1401
|
export const createRequestHandler = async (options?: {
|
|
@@ -1573,6 +1578,11 @@ export const createRequestHandler = async (options?: {
|
|
|
1573
1578
|
};
|
|
1574
1579
|
const pendingSubagentApprovals = new Map<string, PendingSubagentApproval>();
|
|
1575
1580
|
|
|
1581
|
+
// Tracks approval decisions in memory so parallel batch requests don't
|
|
1582
|
+
// race against the conversation store (each file-store read returns a
|
|
1583
|
+
// separate copy, causing last-writer-wins when decisions overlap).
|
|
1584
|
+
const approvalDecisionTracker = new Map<string, Map<string, boolean>>();
|
|
1585
|
+
|
|
1576
1586
|
const getSubagentDepth = async (conversationId: string): Promise<number> => {
|
|
1577
1587
|
let depth = 0;
|
|
1578
1588
|
let current = await conversationStore.get(conversationId);
|
|
@@ -2042,8 +2052,15 @@ export const createRequestHandler = async (options?: {
|
|
|
2042
2052
|
// ---------------------------------------------------------------------------
|
|
2043
2053
|
const MAX_SUBAGENT_CALLBACK_COUNT = 20;
|
|
2044
2054
|
|
|
2055
|
+
// Track conversations that received subagent results while a run was active.
|
|
2056
|
+
// processSubagentCallback's finally block checks this to reliably re-trigger
|
|
2057
|
+
// even if the store-level pendingSubagentResults was clobbered by a concurrent
|
|
2058
|
+
// read-modify-write.
|
|
2059
|
+
const pendingCallbackNeeded = new Set<string>();
|
|
2060
|
+
|
|
2045
2061
|
const triggerParentCallback = async (parentConversationId: string): Promise<void> => {
|
|
2046
2062
|
if (activeConversationRuns.has(parentConversationId)) {
|
|
2063
|
+
pendingCallbackNeeded.add(parentConversationId);
|
|
2047
2064
|
return;
|
|
2048
2065
|
}
|
|
2049
2066
|
if (isServerless) {
|
|
@@ -2057,15 +2074,17 @@ export const createRequestHandler = async (options?: {
|
|
|
2057
2074
|
|
|
2058
2075
|
const CALLBACK_LOCK_STALE_MS = 5 * 60 * 1000;
|
|
2059
2076
|
|
|
2060
|
-
const processSubagentCallback = async (conversationId: string): Promise<void> => {
|
|
2077
|
+
const processSubagentCallback = async (conversationId: string, skipLockCheck = false): Promise<void> => {
|
|
2061
2078
|
const conversation = await conversationStore.get(conversationId);
|
|
2062
2079
|
if (!conversation) return;
|
|
2063
2080
|
|
|
2064
2081
|
const pendingResults = conversation.pendingSubagentResults ?? [];
|
|
2065
2082
|
if (pendingResults.length === 0) return;
|
|
2066
2083
|
|
|
2067
|
-
// Store-based lock for serverless: skip if another invocation is processing
|
|
2068
|
-
|
|
2084
|
+
// Store-based lock for serverless: skip if another invocation is processing.
|
|
2085
|
+
// When re-triggered from a previous callback's finally block, skipLockCheck
|
|
2086
|
+
// is true because we know the previous callback has finished.
|
|
2087
|
+
if (!skipLockCheck && conversation.runningCallbackSince) {
|
|
2069
2088
|
const elapsed = Date.now() - conversation.runningCallbackSince;
|
|
2070
2089
|
if (elapsed < CALLBACK_LOCK_STALE_MS) {
|
|
2071
2090
|
return;
|
|
@@ -2109,12 +2128,27 @@ export const createRequestHandler = async (options?: {
|
|
|
2109
2128
|
abortController,
|
|
2110
2129
|
runId: null,
|
|
2111
2130
|
});
|
|
2131
|
+
// Reopen/reset the parent stream for this callback run so clients that stay
|
|
2132
|
+
// on the main conversation can subscribe to live callback events.
|
|
2133
|
+
const prevStream = conversationEventStreams.get(conversationId);
|
|
2134
|
+
if (prevStream) {
|
|
2135
|
+
prevStream.finished = false;
|
|
2136
|
+
prevStream.buffer = [];
|
|
2137
|
+
} else {
|
|
2138
|
+
conversationEventStreams.set(conversationId, {
|
|
2139
|
+
buffer: [],
|
|
2140
|
+
subscribers: new Set(),
|
|
2141
|
+
finished: false,
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2112
2144
|
|
|
2113
2145
|
const historyMessages = [...conversation.messages];
|
|
2114
2146
|
let assistantResponse = "";
|
|
2115
2147
|
let latestRunId = "";
|
|
2116
2148
|
let runContinuation = false;
|
|
2117
2149
|
let runContinuationMessages: Message[] | undefined;
|
|
2150
|
+
let runContextTokens = conversation.contextTokens ?? 0;
|
|
2151
|
+
let runContextWindow = conversation.contextWindow ?? 0;
|
|
2118
2152
|
const toolTimeline: string[] = [];
|
|
2119
2153
|
const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
|
|
2120
2154
|
let currentTools: string[] = [];
|
|
@@ -2170,6 +2204,8 @@ export const createRequestHandler = async (options?: {
|
|
|
2170
2204
|
if (assistantResponse.length === 0 && event.result.response) {
|
|
2171
2205
|
assistantResponse = event.result.response;
|
|
2172
2206
|
}
|
|
2207
|
+
runContextTokens = event.result.contextTokens ?? runContextTokens;
|
|
2208
|
+
runContextWindow = event.result.contextWindow ?? runContextWindow;
|
|
2173
2209
|
if (event.result.continuation) {
|
|
2174
2210
|
runContinuation = true;
|
|
2175
2211
|
if (event.result.continuationMessages) {
|
|
@@ -2200,6 +2236,8 @@ export const createRequestHandler = async (options?: {
|
|
|
2200
2236
|
}
|
|
2201
2237
|
freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
|
|
2202
2238
|
freshConv.runningCallbackSince = undefined;
|
|
2239
|
+
if (runContextTokens > 0) freshConv.contextTokens = runContextTokens;
|
|
2240
|
+
if (runContextWindow > 0) freshConv.contextWindow = runContextWindow;
|
|
2203
2241
|
freshConv.updatedAt = Date.now();
|
|
2204
2242
|
await conversationStore.update(freshConv);
|
|
2205
2243
|
|
|
@@ -2242,24 +2280,43 @@ export const createRequestHandler = async (options?: {
|
|
|
2242
2280
|
activeConversationRuns.delete(conversationId);
|
|
2243
2281
|
finishConversationStream(conversationId);
|
|
2244
2282
|
|
|
2283
|
+
// Check both the in-memory flag (always reliable) and the store.
|
|
2284
|
+
// We drain the flag first so a concurrent triggerParentCallback that
|
|
2285
|
+
// sets it right after our delete above is still caught on the next
|
|
2286
|
+
// iteration.
|
|
2287
|
+
const hadDeferredTrigger = pendingCallbackNeeded.delete(conversationId);
|
|
2245
2288
|
const freshConv = await conversationStore.get(conversationId);
|
|
2246
|
-
|
|
2247
|
-
if (freshConv.runningCallbackSince) {
|
|
2248
|
-
freshConv.runningCallbackSince = undefined;
|
|
2249
|
-
await conversationStore.update(freshConv);
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2289
|
+
const hasPendingInStore = !!freshConv?.pendingSubagentResults?.length;
|
|
2252
2290
|
|
|
2253
|
-
if (
|
|
2291
|
+
if (hadDeferredTrigger || hasPendingInStore) {
|
|
2292
|
+
// Re-trigger immediately. Skip the runningCallbackSince lock check
|
|
2293
|
+
// because we know this callback just finished. The re-triggered
|
|
2294
|
+
// callback will overwrite runningCallbackSince with its own timestamp.
|
|
2254
2295
|
if (isServerless) {
|
|
2255
2296
|
selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(err =>
|
|
2256
2297
|
console.error(`[poncho][subagent-callback] Recursive callback self-fetch failed:`, err instanceof Error ? err.message : err),
|
|
2257
2298
|
);
|
|
2258
2299
|
} else {
|
|
2259
|
-
processSubagentCallback(conversationId).catch(err =>
|
|
2300
|
+
processSubagentCallback(conversationId, true).catch(err =>
|
|
2260
2301
|
console.error(`[poncho][subagent-callback] Recursive callback failed:`, err instanceof Error ? err.message : err),
|
|
2261
2302
|
);
|
|
2262
2303
|
}
|
|
2304
|
+
} else if (freshConv?.runningCallbackSince) {
|
|
2305
|
+
// No re-trigger needed. Use the atomic clearCallbackLock to avoid
|
|
2306
|
+
// clobbering concurrent appendSubagentResult writes.
|
|
2307
|
+
const afterClear = await conversationStore.clearCallbackLock(conversationId);
|
|
2308
|
+
// Double-check: an append may have raced even the atomic clear
|
|
2309
|
+
if (afterClear?.pendingSubagentResults?.length) {
|
|
2310
|
+
if (isServerless) {
|
|
2311
|
+
selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(conversationId)}/subagent-callback`).catch(err =>
|
|
2312
|
+
console.error(`[poncho][subagent-callback] Post-clear callback self-fetch failed:`, err instanceof Error ? err.message : err),
|
|
2313
|
+
);
|
|
2314
|
+
} else {
|
|
2315
|
+
processSubagentCallback(conversationId, true).catch(err =>
|
|
2316
|
+
console.error(`[poncho][subagent-callback] Post-clear callback failed:`, err instanceof Error ? err.message : err),
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2263
2320
|
}
|
|
2264
2321
|
}
|
|
2265
2322
|
};
|
|
@@ -2461,14 +2518,6 @@ export const createRequestHandler = async (options?: {
|
|
|
2461
2518
|
if (active && active.abortController === abortController) {
|
|
2462
2519
|
active.runId = event.runId;
|
|
2463
2520
|
}
|
|
2464
|
-
if (typeof event.contextWindow === "number" && event.contextWindow > 0) {
|
|
2465
|
-
runContextWindow = event.contextWindow;
|
|
2466
|
-
}
|
|
2467
|
-
}
|
|
2468
|
-
if (event.type === "model:response") {
|
|
2469
|
-
if (typeof event.usage?.input === "number") {
|
|
2470
|
-
runContextTokens = event.usage.input;
|
|
2471
|
-
}
|
|
2472
2521
|
}
|
|
2473
2522
|
if (event.type === "model:chunk") {
|
|
2474
2523
|
if (currentTools.length > 0) {
|
|
@@ -2533,12 +2582,12 @@ export const createRequestHandler = async (options?: {
|
|
|
2533
2582
|
}
|
|
2534
2583
|
checkpointedRun = true;
|
|
2535
2584
|
}
|
|
2536
|
-
if (
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2585
|
+
if (event.type === "run:completed") {
|
|
2586
|
+
if (assistantResponse.length === 0 && event.result.response) {
|
|
2587
|
+
assistantResponse = event.result.response;
|
|
2588
|
+
}
|
|
2589
|
+
runContextTokens = event.result.contextTokens ?? runContextTokens;
|
|
2590
|
+
runContextWindow = event.result.contextWindow ?? runContextWindow;
|
|
2542
2591
|
}
|
|
2543
2592
|
if (event.type === "run:error") {
|
|
2544
2593
|
assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
|
|
@@ -2627,6 +2676,15 @@ export const createRequestHandler = async (options?: {
|
|
|
2627
2676
|
runConversations.delete(latestRunId);
|
|
2628
2677
|
}
|
|
2629
2678
|
console.log("[resume-run] complete for", conversationId);
|
|
2679
|
+
|
|
2680
|
+
// Check for pending subagent results that arrived during the run
|
|
2681
|
+
const hadDeferred = pendingCallbackNeeded.delete(conversationId);
|
|
2682
|
+
const postConv = await conversationStore.get(conversationId);
|
|
2683
|
+
if (hadDeferred || postConv?.pendingSubagentResults?.length) {
|
|
2684
|
+
processSubagentCallback(conversationId, true).catch(err =>
|
|
2685
|
+
console.error(`[poncho][subagent-callback] Post-resume callback failed:`, err instanceof Error ? err.message : err),
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2630
2688
|
};
|
|
2631
2689
|
|
|
2632
2690
|
// ---------------------------------------------------------------------------
|
|
@@ -2783,14 +2841,6 @@ export const createRequestHandler = async (options?: {
|
|
|
2783
2841
|
latestRunId = event.runId;
|
|
2784
2842
|
runOwners.set(event.runId, "local-owner");
|
|
2785
2843
|
runConversations.set(event.runId, conversationId);
|
|
2786
|
-
if (typeof event.contextWindow === "number" && event.contextWindow > 0) {
|
|
2787
|
-
runContextWindow = event.contextWindow;
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
if (event.type === "model:response") {
|
|
2791
|
-
if (typeof event.usage?.input === "number") {
|
|
2792
|
-
runContextTokens = event.usage.input;
|
|
2793
|
-
}
|
|
2794
2844
|
}
|
|
2795
2845
|
if (event.type === "model:chunk") {
|
|
2796
2846
|
if (currentTools.length > 0) {
|
|
@@ -2892,6 +2942,8 @@ export const createRequestHandler = async (options?: {
|
|
|
2892
2942
|
}
|
|
2893
2943
|
runSteps = event.result.steps;
|
|
2894
2944
|
if (typeof event.result.maxSteps === "number") runMaxSteps = event.result.maxSteps;
|
|
2945
|
+
runContextTokens = event.result.contextTokens ?? runContextTokens;
|
|
2946
|
+
runContextWindow = event.result.contextWindow ?? runContextWindow;
|
|
2895
2947
|
}
|
|
2896
2948
|
if (event.type === "run:error") {
|
|
2897
2949
|
assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
|
|
@@ -3813,7 +3865,15 @@ export const createRequestHandler = async (options?: {
|
|
|
3813
3865
|
return;
|
|
3814
3866
|
}
|
|
3815
3867
|
|
|
3816
|
-
//
|
|
3868
|
+
// Track decision in memory so parallel batch requests see a consistent
|
|
3869
|
+
// view (file-store reads return independent copies, causing lost updates).
|
|
3870
|
+
let batchDecisions = approvalDecisionTracker.get(conversationId);
|
|
3871
|
+
if (!batchDecisions) {
|
|
3872
|
+
batchDecisions = new Map();
|
|
3873
|
+
approvalDecisionTracker.set(conversationId, batchDecisions);
|
|
3874
|
+
}
|
|
3875
|
+
batchDecisions.set(approvalId, approved);
|
|
3876
|
+
|
|
3817
3877
|
foundApproval.decision = approved ? "approved" : "denied";
|
|
3818
3878
|
|
|
3819
3879
|
broadcastEvent(conversationId,
|
|
@@ -3823,16 +3883,26 @@ export const createRequestHandler = async (options?: {
|
|
|
3823
3883
|
);
|
|
3824
3884
|
|
|
3825
3885
|
const allApprovals = foundConversation.pendingApprovals ?? [];
|
|
3826
|
-
const allDecided = allApprovals.length > 0 &&
|
|
3886
|
+
const allDecided = allApprovals.length > 0 &&
|
|
3887
|
+
allApprovals.every(a => batchDecisions!.has(a.approvalId));
|
|
3827
3888
|
|
|
3828
3889
|
if (!allDecided) {
|
|
3829
|
-
// Still waiting for more decisions — persist and respond
|
|
3890
|
+
// Still waiting for more decisions — persist best-effort and respond.
|
|
3891
|
+
// The write may be overwritten by a concurrent request, but that's
|
|
3892
|
+
// fine: the in-memory tracker is the source of truth for completion.
|
|
3830
3893
|
await conversationStore.update(foundConversation);
|
|
3831
3894
|
writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
|
|
3832
3895
|
return;
|
|
3833
3896
|
}
|
|
3834
3897
|
|
|
3835
|
-
// All approvals in the batch are decided —
|
|
3898
|
+
// All approvals in the batch are decided — apply tracked decisions,
|
|
3899
|
+
// execute approved tools, and resume the run.
|
|
3900
|
+
for (const a of allApprovals) {
|
|
3901
|
+
const d = batchDecisions.get(a.approvalId);
|
|
3902
|
+
if (d != null) a.decision = d ? "approved" : "denied";
|
|
3903
|
+
}
|
|
3904
|
+
approvalDecisionTracker.delete(conversationId);
|
|
3905
|
+
|
|
3836
3906
|
foundConversation.pendingApprovals = [];
|
|
3837
3907
|
foundConversation.runStatus = "running";
|
|
3838
3908
|
await conversationStore.update(foundConversation);
|
|
@@ -4517,14 +4587,6 @@ export const createRequestHandler = async (options?: {
|
|
|
4517
4587
|
if (active && active.abortController === abortController) {
|
|
4518
4588
|
active.runId = event.runId;
|
|
4519
4589
|
}
|
|
4520
|
-
if (typeof event.contextWindow === "number" && event.contextWindow > 0) {
|
|
4521
|
-
runContextWindow = event.contextWindow;
|
|
4522
|
-
}
|
|
4523
|
-
}
|
|
4524
|
-
if (event.type === "model:response") {
|
|
4525
|
-
if (typeof event.usage?.input === "number") {
|
|
4526
|
-
runContextTokens = event.usage.input;
|
|
4527
|
-
}
|
|
4528
4590
|
}
|
|
4529
4591
|
if (event.type === "run:cancelled") {
|
|
4530
4592
|
runCancelled = true;
|
|
@@ -4623,6 +4685,8 @@ export const createRequestHandler = async (options?: {
|
|
|
4623
4685
|
if (assistantResponse.length === 0 && event.result.response) {
|
|
4624
4686
|
assistantResponse = event.result.response;
|
|
4625
4687
|
}
|
|
4688
|
+
runContextTokens = event.result.contextTokens ?? runContextTokens;
|
|
4689
|
+
runContextWindow = event.result.contextWindow ?? runContextWindow;
|
|
4626
4690
|
if (event.result.continuation && event.result.continuationMessages) {
|
|
4627
4691
|
runContinuationMessages = event.result.continuationMessages;
|
|
4628
4692
|
conversation._continuationMessages = runContinuationMessages;
|
|
@@ -4783,9 +4847,10 @@ export const createRequestHandler = async (options?: {
|
|
|
4783
4847
|
// Already closed.
|
|
4784
4848
|
}
|
|
4785
4849
|
// Check for pending subagent results that arrived during the run
|
|
4850
|
+
const hadDeferred = pendingCallbackNeeded.delete(conversationId);
|
|
4786
4851
|
const freshConv = await conversationStore.get(conversationId);
|
|
4787
|
-
if (freshConv?.pendingSubagentResults?.length) {
|
|
4788
|
-
processSubagentCallback(conversationId).catch(err =>
|
|
4852
|
+
if (hadDeferred || freshConv?.pendingSubagentResults?.length) {
|
|
4853
|
+
processSubagentCallback(conversationId, true).catch(err =>
|
|
4789
4854
|
console.error(`[poncho][subagent-callback] Post-run callback failed:`, err instanceof Error ? err.message : err),
|
|
4790
4855
|
);
|
|
4791
4856
|
}
|
|
@@ -4948,6 +5013,14 @@ export const createRequestHandler = async (options?: {
|
|
|
4948
5013
|
);
|
|
4949
5014
|
}
|
|
4950
5015
|
|
|
5016
|
+
const convId = conversation.conversationId;
|
|
5017
|
+
activeConversationRuns.set(convId, {
|
|
5018
|
+
ownerId: conversation.ownerId,
|
|
5019
|
+
abortController: new AbortController(),
|
|
5020
|
+
runId: null,
|
|
5021
|
+
});
|
|
5022
|
+
|
|
5023
|
+
try {
|
|
4951
5024
|
const abortController = new AbortController();
|
|
4952
5025
|
let assistantResponse = "";
|
|
4953
5026
|
let latestRunId = "";
|
|
@@ -4955,7 +5028,7 @@ export const createRequestHandler = async (options?: {
|
|
|
4955
5028
|
const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
|
|
4956
5029
|
let currentTools: string[] = [];
|
|
4957
5030
|
let currentText = "";
|
|
4958
|
-
let runResult: { status: string; steps: number; continuation?: boolean } = {
|
|
5031
|
+
let runResult: { status: string; steps: number; continuation?: boolean; contextTokens?: number; contextWindow?: number } = {
|
|
4959
5032
|
status: "completed",
|
|
4960
5033
|
steps: 0,
|
|
4961
5034
|
};
|
|
@@ -4967,8 +5040,8 @@ export const createRequestHandler = async (options?: {
|
|
|
4967
5040
|
|
|
4968
5041
|
for await (const event of harness.runWithTelemetry({
|
|
4969
5042
|
task: cronJob.task,
|
|
4970
|
-
conversationId:
|
|
4971
|
-
parameters: { __activeConversationId:
|
|
5043
|
+
conversationId: convId,
|
|
5044
|
+
parameters: { __activeConversationId: convId },
|
|
4972
5045
|
messages: historyMessages,
|
|
4973
5046
|
abortSignal: abortController.signal,
|
|
4974
5047
|
})) {
|
|
@@ -5010,14 +5083,19 @@ export const createRequestHandler = async (options?: {
|
|
|
5010
5083
|
status: event.result.status,
|
|
5011
5084
|
steps: event.result.steps,
|
|
5012
5085
|
continuation: event.result.continuation,
|
|
5086
|
+
contextTokens: event.result.contextTokens,
|
|
5087
|
+
contextWindow: event.result.contextWindow,
|
|
5013
5088
|
};
|
|
5014
5089
|
if (!assistantResponse && event.result.response) {
|
|
5015
5090
|
assistantResponse = event.result.response;
|
|
5016
5091
|
}
|
|
5017
5092
|
}
|
|
5093
|
+
broadcastEvent(convId, event);
|
|
5018
5094
|
await telemetry.emit(event);
|
|
5019
5095
|
}
|
|
5020
5096
|
|
|
5097
|
+
finishConversationStream(convId);
|
|
5098
|
+
|
|
5021
5099
|
if (currentTools.length > 0) {
|
|
5022
5100
|
sections.push({ type: "tools", content: currentTools });
|
|
5023
5101
|
}
|
|
@@ -5026,7 +5104,8 @@ export const createRequestHandler = async (options?: {
|
|
|
5026
5104
|
currentText = "";
|
|
5027
5105
|
}
|
|
5028
5106
|
|
|
5029
|
-
// Persist the conversation
|
|
5107
|
+
// Persist the conversation — read fresh state to avoid clobbering
|
|
5108
|
+
// pendingSubagentResults appended during the run.
|
|
5030
5109
|
const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
|
|
5031
5110
|
const assistantMetadata =
|
|
5032
5111
|
toolTimeline.length > 0 || sections.length > 0
|
|
@@ -5044,14 +5123,19 @@ export const createRequestHandler = async (options?: {
|
|
|
5044
5123
|
? [{ role: "assistant" as const, content: assistantResponse, metadata: assistantMetadata }]
|
|
5045
5124
|
: []),
|
|
5046
5125
|
];
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5126
|
+
const freshConv = await conversationStore.get(convId);
|
|
5127
|
+
if (freshConv) {
|
|
5128
|
+
freshConv.messages = messages;
|
|
5129
|
+
freshConv.runtimeRunId = latestRunId || freshConv.runtimeRunId;
|
|
5130
|
+
if (runResult.contextTokens) freshConv.contextTokens = runResult.contextTokens;
|
|
5131
|
+
if (runResult.contextWindow) freshConv.contextWindow = runResult.contextWindow;
|
|
5132
|
+
freshConv.updatedAt = Date.now();
|
|
5133
|
+
await conversationStore.update(freshConv);
|
|
5134
|
+
}
|
|
5051
5135
|
|
|
5052
5136
|
// Self-continuation for serverless timeouts
|
|
5053
5137
|
if (runResult.continuation && softDeadlineMs > 0) {
|
|
5054
|
-
const selfUrl = `http://${request.headers.host ?? "localhost"}${pathname}?continue=${encodeURIComponent(
|
|
5138
|
+
const selfUrl = `http://${request.headers.host ?? "localhost"}${pathname}?continue=${encodeURIComponent(convId)}&continuation=${continuationCount + 1}`;
|
|
5055
5139
|
try {
|
|
5056
5140
|
const selfRes = await fetch(selfUrl, {
|
|
5057
5141
|
method: "GET",
|
|
@@ -5061,7 +5145,7 @@ export const createRequestHandler = async (options?: {
|
|
|
5061
5145
|
});
|
|
5062
5146
|
const selfBody = await selfRes.json() as Record<string, unknown>;
|
|
5063
5147
|
writeJson(response, 200, {
|
|
5064
|
-
conversationId:
|
|
5148
|
+
conversationId: convId,
|
|
5065
5149
|
status: "continued",
|
|
5066
5150
|
continuations: continuationCount + 1,
|
|
5067
5151
|
finalResult: selfBody,
|
|
@@ -5069,7 +5153,7 @@ export const createRequestHandler = async (options?: {
|
|
|
5069
5153
|
});
|
|
5070
5154
|
} catch (continueError) {
|
|
5071
5155
|
writeJson(response, 200, {
|
|
5072
|
-
conversationId:
|
|
5156
|
+
conversationId: convId,
|
|
5073
5157
|
status: "continuation_failed",
|
|
5074
5158
|
error: continueError instanceof Error ? continueError.message : "Unknown error",
|
|
5075
5159
|
duration: Date.now() - start,
|
|
@@ -5080,12 +5164,28 @@ export const createRequestHandler = async (options?: {
|
|
|
5080
5164
|
}
|
|
5081
5165
|
|
|
5082
5166
|
writeJson(response, 200, {
|
|
5083
|
-
conversationId:
|
|
5167
|
+
conversationId: convId,
|
|
5084
5168
|
status: runResult.status,
|
|
5085
5169
|
response: assistantResponse.slice(0, 500),
|
|
5086
5170
|
duration: Date.now() - start,
|
|
5087
5171
|
steps: runResult.steps,
|
|
5088
5172
|
});
|
|
5173
|
+
} finally {
|
|
5174
|
+
activeConversationRuns.delete(convId);
|
|
5175
|
+
const hadDeferred = pendingCallbackNeeded.delete(convId);
|
|
5176
|
+
const checkConv = await conversationStore.get(convId);
|
|
5177
|
+
if (hadDeferred || checkConv?.pendingSubagentResults?.length) {
|
|
5178
|
+
if (isServerless) {
|
|
5179
|
+
selfFetchWithRetry(`/api/internal/conversations/${encodeURIComponent(convId)}/subagent-callback`).catch(err =>
|
|
5180
|
+
console.error(`[cron] subagent callback self-fetch failed:`, err instanceof Error ? err.message : err),
|
|
5181
|
+
);
|
|
5182
|
+
} else {
|
|
5183
|
+
processSubagentCallback(convId, true).catch(err =>
|
|
5184
|
+
console.error(`[cron] subagent callback failed:`, err instanceof Error ? err.message : err),
|
|
5185
|
+
);
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5089
5189
|
} catch (error) {
|
|
5090
5190
|
writeJson(response, 500, {
|
|
5091
5191
|
code: "CRON_RUN_ERROR",
|
|
@@ -5101,6 +5201,11 @@ export const createRequestHandler = async (options?: {
|
|
|
5101
5201
|
handler._cronJobs = cronJobs;
|
|
5102
5202
|
handler._conversationStore = conversationStore;
|
|
5103
5203
|
handler._messagingAdapters = messagingAdapters;
|
|
5204
|
+
handler._activeConversationRuns = activeConversationRuns;
|
|
5205
|
+
handler._pendingCallbackNeeded = pendingCallbackNeeded;
|
|
5206
|
+
handler._processSubagentCallback = processSubagentCallback;
|
|
5207
|
+
handler._broadcastEvent = broadcastEvent;
|
|
5208
|
+
handler._finishConversationStream = finishConversationStream;
|
|
5104
5209
|
|
|
5105
5210
|
// Recover stale subagent runs that were "running" when the server last stopped
|
|
5106
5211
|
// or that have been inactive longer than the staleness threshold.
|
|
@@ -5169,6 +5274,8 @@ export const startDevServer = async (
|
|
|
5169
5274
|
steps: number;
|
|
5170
5275
|
assistantMetadata?: Message["metadata"];
|
|
5171
5276
|
hasContent: boolean;
|
|
5277
|
+
contextTokens: number;
|
|
5278
|
+
contextWindow: number;
|
|
5172
5279
|
};
|
|
5173
5280
|
|
|
5174
5281
|
const runCronAgent = async (
|
|
@@ -5176,9 +5283,12 @@ export const startDevServer = async (
|
|
|
5176
5283
|
task: string,
|
|
5177
5284
|
conversationId: string,
|
|
5178
5285
|
historyMessages: Message[],
|
|
5286
|
+
onEvent?: (event: AgentEvent) => void,
|
|
5179
5287
|
): Promise<CronRunResult> => {
|
|
5180
5288
|
let assistantResponse = "";
|
|
5181
5289
|
let steps = 0;
|
|
5290
|
+
let contextTokens = 0;
|
|
5291
|
+
let contextWindow = 0;
|
|
5182
5292
|
const toolTimeline: string[] = [];
|
|
5183
5293
|
const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
|
|
5184
5294
|
let currentTools: string[] = [];
|
|
@@ -5189,6 +5299,7 @@ export const startDevServer = async (
|
|
|
5189
5299
|
parameters: { __activeConversationId: conversationId },
|
|
5190
5300
|
messages: historyMessages,
|
|
5191
5301
|
})) {
|
|
5302
|
+
onEvent?.(event);
|
|
5192
5303
|
if (event.type === "model:chunk") {
|
|
5193
5304
|
if (currentTools.length > 0) {
|
|
5194
5305
|
sections.push({ type: "tools", content: currentTools });
|
|
@@ -5221,6 +5332,8 @@ export const startDevServer = async (
|
|
|
5221
5332
|
}
|
|
5222
5333
|
if (event.type === "run:completed") {
|
|
5223
5334
|
steps = event.result.steps;
|
|
5335
|
+
contextTokens = event.result.contextTokens ?? 0;
|
|
5336
|
+
contextWindow = event.result.contextWindow ?? 0;
|
|
5224
5337
|
if (!assistantResponse && event.result.response) {
|
|
5225
5338
|
assistantResponse = event.result.response;
|
|
5226
5339
|
}
|
|
@@ -5240,7 +5353,7 @@ export const startDevServer = async (
|
|
|
5240
5353
|
sections: sections.length > 0 ? sections : undefined,
|
|
5241
5354
|
} as Message["metadata"])
|
|
5242
5355
|
: undefined;
|
|
5243
|
-
return { response: assistantResponse, steps, assistantMetadata, hasContent };
|
|
5356
|
+
return { response: assistantResponse, steps, assistantMetadata, hasContent, contextTokens, contextWindow };
|
|
5244
5357
|
};
|
|
5245
5358
|
|
|
5246
5359
|
const buildCronMessages = (
|
|
@@ -5267,6 +5380,9 @@ export const startDevServer = async (
|
|
|
5267
5380
|
const harnessRef = handler._harness;
|
|
5268
5381
|
const store = handler._conversationStore;
|
|
5269
5382
|
const adapters = handler._messagingAdapters;
|
|
5383
|
+
const activeRuns = handler._activeConversationRuns;
|
|
5384
|
+
const deferredCallbacks = handler._pendingCallbackNeeded;
|
|
5385
|
+
const runCallback = handler._processSubagentCallback;
|
|
5270
5386
|
if (!harnessRef || !store) return;
|
|
5271
5387
|
|
|
5272
5388
|
for (const [jobName, config] of entries) {
|
|
@@ -5308,32 +5424,56 @@ export const startDevServer = async (
|
|
|
5308
5424
|
|
|
5309
5425
|
const task = `[Scheduled: ${jobName}]\n${config.task}`;
|
|
5310
5426
|
const historyMessages = [...conversation.messages];
|
|
5427
|
+
const convId = conversation.conversationId;
|
|
5311
5428
|
|
|
5429
|
+
activeRuns?.set(convId, {
|
|
5430
|
+
ownerId: "local-owner",
|
|
5431
|
+
abortController: new AbortController(),
|
|
5432
|
+
runId: null,
|
|
5433
|
+
});
|
|
5312
5434
|
try {
|
|
5313
|
-
const
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5435
|
+
const broadcastCh = handler._broadcastEvent;
|
|
5436
|
+
const result = await runCronAgent(harnessRef, task, convId, historyMessages,
|
|
5437
|
+
broadcastCh ? (ev) => broadcastCh(convId, ev) : undefined,
|
|
5438
|
+
);
|
|
5439
|
+
handler._finishConversationStream?.(convId);
|
|
5440
|
+
|
|
5441
|
+
const freshConv = await store.get(convId);
|
|
5442
|
+
if (freshConv) {
|
|
5443
|
+
freshConv.messages = buildCronMessages(task, historyMessages, result);
|
|
5444
|
+
if (result.contextTokens > 0) freshConv.contextTokens = result.contextTokens;
|
|
5445
|
+
if (result.contextWindow > 0) freshConv.contextWindow = result.contextWindow;
|
|
5446
|
+
freshConv.updatedAt = Date.now();
|
|
5447
|
+
await store.update(freshConv);
|
|
5448
|
+
|
|
5449
|
+
if (result.response) {
|
|
5450
|
+
try {
|
|
5451
|
+
await adapter.sendReply(
|
|
5452
|
+
{
|
|
5453
|
+
channelId: chatId,
|
|
5454
|
+
platformThreadId: freshConv.channelMeta?.platformThreadId ?? chatId,
|
|
5455
|
+
},
|
|
5456
|
+
result.response,
|
|
5457
|
+
);
|
|
5458
|
+
} catch (sendError) {
|
|
5459
|
+
const sendMsg = sendError instanceof Error ? sendError.message : String(sendError);
|
|
5460
|
+
process.stderr.write(`[cron] ${jobName}: send to ${chatId} failed: ${sendMsg}\n`);
|
|
5461
|
+
}
|
|
5331
5462
|
}
|
|
5332
5463
|
}
|
|
5333
5464
|
totalChats++;
|
|
5334
5465
|
} catch (runError) {
|
|
5335
5466
|
const runMsg = runError instanceof Error ? runError.message : String(runError);
|
|
5336
5467
|
process.stderr.write(`[cron] ${jobName}: run for chat ${chatId} failed: ${runMsg}\n`);
|
|
5468
|
+
} finally {
|
|
5469
|
+
activeRuns?.delete(convId);
|
|
5470
|
+
const hadDeferred = deferredCallbacks?.delete(convId) ?? false;
|
|
5471
|
+
const checkConv = await store.get(convId);
|
|
5472
|
+
if (hadDeferred || checkConv?.pendingSubagentResults?.length) {
|
|
5473
|
+
runCallback?.(convId, true).catch((err: unknown) =>
|
|
5474
|
+
console.error(`[cron] ${jobName}: subagent callback for ${chatId} failed:`, err instanceof Error ? err.message : err),
|
|
5475
|
+
);
|
|
5476
|
+
}
|
|
5337
5477
|
}
|
|
5338
5478
|
}
|
|
5339
5479
|
|
|
@@ -5347,15 +5487,31 @@ export const startDevServer = async (
|
|
|
5347
5487
|
return;
|
|
5348
5488
|
}
|
|
5349
5489
|
|
|
5490
|
+
let cronConvId: string | undefined;
|
|
5350
5491
|
try {
|
|
5351
5492
|
const conversation = await store.create(
|
|
5352
5493
|
"local-owner",
|
|
5353
5494
|
`[cron] ${jobName} ${timestamp}`,
|
|
5354
5495
|
);
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5496
|
+
cronConvId = conversation.conversationId;
|
|
5497
|
+
activeRuns?.set(cronConvId, {
|
|
5498
|
+
ownerId: "local-owner",
|
|
5499
|
+
abortController: new AbortController(),
|
|
5500
|
+
runId: null,
|
|
5501
|
+
});
|
|
5502
|
+
const broadcast = handler._broadcastEvent;
|
|
5503
|
+
const result = await runCronAgent(harnessRef, config.task, cronConvId, [],
|
|
5504
|
+
broadcast ? (ev) => broadcast(cronConvId!, ev) : undefined,
|
|
5505
|
+
);
|
|
5506
|
+
handler._finishConversationStream?.(cronConvId);
|
|
5507
|
+
const freshConv = await store.get(cronConvId);
|
|
5508
|
+
if (freshConv) {
|
|
5509
|
+
freshConv.messages = buildCronMessages(config.task, [], result);
|
|
5510
|
+
if (result.contextTokens > 0) freshConv.contextTokens = result.contextTokens;
|
|
5511
|
+
if (result.contextWindow > 0) freshConv.contextWindow = result.contextWindow;
|
|
5512
|
+
freshConv.updatedAt = Date.now();
|
|
5513
|
+
await store.update(freshConv);
|
|
5514
|
+
}
|
|
5359
5515
|
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
5360
5516
|
process.stdout.write(
|
|
5361
5517
|
`[cron] ${jobName} completed in ${elapsed}s (${result.steps} steps)\n`,
|
|
@@ -5366,6 +5522,17 @@ export const startDevServer = async (
|
|
|
5366
5522
|
process.stderr.write(
|
|
5367
5523
|
`[cron] ${jobName} failed after ${elapsed}s: ${msg}\n`,
|
|
5368
5524
|
);
|
|
5525
|
+
} finally {
|
|
5526
|
+
if (cronConvId) {
|
|
5527
|
+
activeRuns?.delete(cronConvId);
|
|
5528
|
+
const hadDeferred = deferredCallbacks?.delete(cronConvId) ?? false;
|
|
5529
|
+
const checkConv = await store.get(cronConvId);
|
|
5530
|
+
if (hadDeferred || checkConv?.pendingSubagentResults?.length) {
|
|
5531
|
+
runCallback?.(cronConvId, true).catch((err: unknown) =>
|
|
5532
|
+
console.error(`[cron] ${jobName}: subagent callback failed:`, err instanceof Error ? err.message : err),
|
|
5533
|
+
);
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5369
5536
|
}
|
|
5370
5537
|
},
|
|
5371
5538
|
);
|