@keyou007/agent-bot 0.1.16-alpha.2 → 0.1.16-alpha.4
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/CHANGELOG.md +12 -0
- package/dist/cli/LocalControlServer.js +4 -1
- package/dist/cli/LocalControlServer.js.map +1 -1
- package/dist/feishu/CardRenderer.js +75 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +155 -21
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/SessionMetadataHydrator.js +0 -25
- package/dist/startup/SessionMetadataHydrator.js.map +1 -1
- package/dist/state/StateStore.js +40 -0
- package/dist/state/StateStore.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/cli/LocalControlServer.ts +3 -1
- package/src/feishu/CardRenderer.ts +74 -10
- package/src/proxy/ProxySessionController.ts +173 -33
- package/src/startup/SessionMetadataHydrator.ts +0 -27
- package/src/state/StateStore.ts +43 -0
|
@@ -119,7 +119,7 @@ const SESSION_PROMPT_PREVIEW_LENGTH = 50;
|
|
|
119
119
|
const DIRECTORY_PAGE_SIZE = 15;
|
|
120
120
|
const RESET_HISTORY_PAGE_SIZE = 10;
|
|
121
121
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
122
|
-
const RECOVERY_ACTIVITY_WINDOW_MS =
|
|
122
|
+
const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
|
|
123
123
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
124
124
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
125
125
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
@@ -1912,6 +1912,7 @@ export class ProxySessionController {
|
|
|
1912
1912
|
}
|
|
1913
1913
|
|
|
1914
1914
|
private async runStartupRecovery(): Promise<void> {
|
|
1915
|
+
await this.finishPersistedCancellationRequests();
|
|
1915
1916
|
await this.recoverShellCommandJobs();
|
|
1916
1917
|
this.backfillTurnAttemptsForUpgrade();
|
|
1917
1918
|
const attempts = this.store.listIncompleteTurnAttempts();
|
|
@@ -1942,6 +1943,40 @@ export class ProxySessionController {
|
|
|
1942
1943
|
this.restorePersistedQueuedPrompts();
|
|
1943
1944
|
}
|
|
1944
1945
|
|
|
1946
|
+
private async finishPersistedCancellationRequests(): Promise<void> {
|
|
1947
|
+
for (const attempt of this.store.listCancellationRequestedTurnAttempts()) {
|
|
1948
|
+
const session = this.store.getSession(attempt.localSessionId);
|
|
1949
|
+
if (!attempt.turnId) {
|
|
1950
|
+
this.store.updateTurnAttempt(attempt.attemptId, { status: "cancelled" });
|
|
1951
|
+
if (session?.status === "starting" || session?.status === "running") {
|
|
1952
|
+
this.store.updateSession(session.localSessionId, { status: "ready" });
|
|
1953
|
+
}
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
if (session) {
|
|
1957
|
+
this.outbound.registerSession(
|
|
1958
|
+
session.localSessionId,
|
|
1959
|
+
attempt.contextKey,
|
|
1960
|
+
session.title,
|
|
1961
|
+
session.cwd,
|
|
1962
|
+
this.agentLabel(session.agentName),
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
try {
|
|
1966
|
+
await this.handleRuntimeEvent({
|
|
1967
|
+
type: "turn_cancelled",
|
|
1968
|
+
sessionId: attempt.localSessionId,
|
|
1969
|
+
turnId: attempt.turnId,
|
|
1970
|
+
});
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
this.logger.warn(
|
|
1973
|
+
{ error, attemptId: attempt.attemptId, turnId: attempt.turnId },
|
|
1974
|
+
"Failed to finish a persisted user-requested turn cancellation.",
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1945
1980
|
private backfillTurnAttemptsForUpgrade(): void {
|
|
1946
1981
|
for (const session of this.store.listAllSessions()) {
|
|
1947
1982
|
if (session.status !== "starting" && session.status !== "running" && session.status !== "ready") continue;
|
|
@@ -2001,7 +2036,7 @@ export class ProxySessionController {
|
|
|
2001
2036
|
session.localSessionId,
|
|
2002
2037
|
attempt.contextKey,
|
|
2003
2038
|
oldCardTurnId,
|
|
2004
|
-
"执行中断已超过
|
|
2039
|
+
"执行中断已超过 10 分钟,未自动恢复。",
|
|
2005
2040
|
).catch((error: unknown) => {
|
|
2006
2041
|
this.logger.warn(
|
|
2007
2042
|
{ error, attemptId: attempt.attemptId, turnId: oldCardTurnId },
|
|
@@ -3082,28 +3117,80 @@ export class ProxySessionController {
|
|
|
3082
3117
|
});
|
|
3083
3118
|
}
|
|
3084
3119
|
let turnId: string;
|
|
3120
|
+
let runtimeText = text;
|
|
3121
|
+
let cardPrompt = options.displayPrompt ?? text;
|
|
3122
|
+
let retriedStart = false;
|
|
3085
3123
|
try {
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3124
|
+
while (true) {
|
|
3125
|
+
let pendingTurnId: string | undefined;
|
|
3126
|
+
try {
|
|
3127
|
+
pendingTurnId = await this.outbound.startPendingTurn(
|
|
3128
|
+
loaded.record.localSessionId,
|
|
3129
|
+
loaded.record.contextKey,
|
|
3130
|
+
title,
|
|
3131
|
+
replyTarget,
|
|
3132
|
+
cardPrompt,
|
|
3133
|
+
);
|
|
3134
|
+
} catch (error) {
|
|
3135
|
+
this.store.updateTurnAttempt(attemptId, {
|
|
3136
|
+
status: options.preserveAttemptOnFailure ? "recovering" : "failed",
|
|
3137
|
+
});
|
|
3138
|
+
throw error;
|
|
3139
|
+
}
|
|
3140
|
+
this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
|
|
3141
|
+
|
|
3142
|
+
try {
|
|
3143
|
+
turnId = await loaded.runtime.startTurn(
|
|
3144
|
+
loaded.record.localSessionId,
|
|
3145
|
+
runtimePrompt(runtimeText, localImagePaths),
|
|
3146
|
+
);
|
|
3147
|
+
break;
|
|
3148
|
+
} catch (error) {
|
|
3149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3150
|
+
const attempt = this.store.getTurnAttempt(attemptId);
|
|
3151
|
+
const retryAttempt = attempt
|
|
3152
|
+
&& isRetryableLlmTurnFailure(message)
|
|
3153
|
+
&& attempt.retryCount < MAX_LLM_TURN_RETRIES
|
|
3154
|
+
? this.store.prepareUnstartedTurnAttemptRetry(attemptId)
|
|
3155
|
+
: undefined;
|
|
3156
|
+
if (!retryAttempt) {
|
|
3157
|
+
const exhausted = attempt
|
|
3158
|
+
&& isRetryableLlmTurnFailure(message)
|
|
3159
|
+
&& attempt.retryCount >= MAX_LLM_TURN_RETRIES;
|
|
3160
|
+
const finalMessage = exhausted
|
|
3161
|
+
? `${message}\n\n已自动重试 ${MAX_LLM_TURN_RETRIES} 次,仍未成功。`
|
|
3162
|
+
: message;
|
|
3163
|
+
this.store.updateTurnAttempt(attemptId, {
|
|
3164
|
+
status: options.preserveAttemptOnFailure ? "recovering" : "failed",
|
|
3165
|
+
});
|
|
3166
|
+
await this.outbound.failPendingTurn(loaded.record.localSessionId, finalMessage);
|
|
3167
|
+
if (finalMessage === message) throw error;
|
|
3168
|
+
throw new Error(finalMessage, { cause: error });
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
retriedStart = true;
|
|
3172
|
+
this.llmRetryingSessions.add(loaded.record.localSessionId);
|
|
3173
|
+
this.store.audit(loaded.record.contextKey, "llm_turn_start_retry_started", {
|
|
3174
|
+
attemptId,
|
|
3175
|
+
retryNumber: retryAttempt.retryCount,
|
|
3176
|
+
maxRetries: MAX_LLM_TURN_RETRIES,
|
|
3177
|
+
error: message,
|
|
3178
|
+
});
|
|
3179
|
+
await this.outbound.failPendingTurn(
|
|
3180
|
+
loaded.record.localSessionId,
|
|
3181
|
+
`${message}\n\n检测到临时模型服务错误,正在自动重试(${retryAttempt.retryCount}/${MAX_LLM_TURN_RETRIES})。`,
|
|
3182
|
+
).catch((presentationError: unknown) => {
|
|
3183
|
+
this.logger.warn(
|
|
3184
|
+
{ error: presentationError, sessionId: loaded.record.localSessionId },
|
|
3185
|
+
"Failed to finalize the pending thinking card before an LLM start retry.",
|
|
3186
|
+
);
|
|
3187
|
+
});
|
|
3188
|
+
runtimeText = llmRetryRuntimePrompt(retryAttempt.promptText, retryAttempt.retryCount);
|
|
3189
|
+
cardPrompt = llmRetryCardPrompt(options.displayPrompt ?? retryAttempt.promptText, retryAttempt.retryCount);
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
} finally {
|
|
3193
|
+
if (retriedStart) this.llmRetryingSessions.delete(loaded.record.localSessionId);
|
|
3107
3194
|
}
|
|
3108
3195
|
const attempt = this.store.getTurnAttempt(attemptId);
|
|
3109
3196
|
this.store.updateTurnAttempt(attemptId, {
|
|
@@ -3956,7 +4043,13 @@ export class ProxySessionController {
|
|
|
3956
4043
|
await this.outbound.sendText(record.contextKey, "当前没有正在执行的任务。");
|
|
3957
4044
|
return;
|
|
3958
4045
|
}
|
|
3959
|
-
|
|
4046
|
+
const cancellationAttempt = this.store.requestTurnAttemptCancellation(turnId);
|
|
4047
|
+
try {
|
|
4048
|
+
await runtime.interruptRemoteTurn(record.remoteSessionId, turnId);
|
|
4049
|
+
} catch (error) {
|
|
4050
|
+
if (cancellationAttempt) this.store.restoreTurnAttemptAfterCancellationFailure(cancellationAttempt);
|
|
4051
|
+
throw error;
|
|
4052
|
+
}
|
|
3960
4053
|
this.store.audit(record.contextKey, "turn_interrupt_sent", {
|
|
3961
4054
|
localSessionId: record.localSessionId,
|
|
3962
4055
|
remoteSessionId: record.remoteSessionId,
|
|
@@ -3972,7 +4065,13 @@ export class ProxySessionController {
|
|
|
3972
4065
|
await this.outbound.sendText(record.contextKey, "当前没有正在执行的任务。");
|
|
3973
4066
|
return;
|
|
3974
4067
|
}
|
|
3975
|
-
|
|
4068
|
+
const cancellationAttempt = this.store.requestTurnAttemptCancellation(turnId);
|
|
4069
|
+
try {
|
|
4070
|
+
await loaded.runtime.cancelTurn(record.localSessionId, turnId);
|
|
4071
|
+
} catch (error) {
|
|
4072
|
+
if (cancellationAttempt) this.store.restoreTurnAttemptAfterCancellationFailure(cancellationAttempt);
|
|
4073
|
+
throw error;
|
|
4074
|
+
}
|
|
3976
4075
|
}
|
|
3977
4076
|
|
|
3978
4077
|
private async resetCurrentSessionToTurn(
|
|
@@ -4851,7 +4950,43 @@ export class ProxySessionController {
|
|
|
4851
4950
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
4852
4951
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
4853
4952
|
}
|
|
4854
|
-
const
|
|
4953
|
+
const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
|
|
4954
|
+
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
4955
|
+
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
4956
|
+
&& !completedTurnIds.has(current.lastTurnId)
|
|
4957
|
+
? current.lastTurnId
|
|
4958
|
+
: undefined;
|
|
4959
|
+
const persistedRunningSnapshot = runningTurnId
|
|
4960
|
+
? turnViewSnapshot(this.store.getTurnSnapshot(runningTurnId))
|
|
4961
|
+
: undefined;
|
|
4962
|
+
const runningAttempt = runningTurnId
|
|
4963
|
+
? this.store.findIncompleteTurnAttemptByTurnId(runningTurnId)
|
|
4964
|
+
: undefined;
|
|
4965
|
+
const attemptStartedAt = Date.parse(runningAttempt?.createdAt ?? "");
|
|
4966
|
+
const runningSnapshot: TurnViewState | undefined = runningTurnId
|
|
4967
|
+
? persistedRunningSnapshot ?? {
|
|
4968
|
+
sessionId: current.localSessionId,
|
|
4969
|
+
turnId: runningTurnId,
|
|
4970
|
+
prompt: runningAttempt?.promptText ?? current.title,
|
|
4971
|
+
status: "running",
|
|
4972
|
+
startedAt: Number.isFinite(attemptStartedAt) ? attemptStartedAt : Date.now(),
|
|
4973
|
+
assistantText: "",
|
|
4974
|
+
plan: [],
|
|
4975
|
+
activities: [],
|
|
4976
|
+
completedTools: [],
|
|
4977
|
+
failedTools: [],
|
|
4978
|
+
fileSummary: [],
|
|
4979
|
+
}
|
|
4980
|
+
: undefined;
|
|
4981
|
+
const runningTurn = runningTurnId && runningSnapshot
|
|
4982
|
+
? {
|
|
4983
|
+
turnId: runningTurnId,
|
|
4984
|
+
parentTurnId: this.store.getTurnParent(runningTurnId, current.localSessionId),
|
|
4985
|
+
snapshot: runningSnapshot,
|
|
4986
|
+
updatedAt: runningAttempt?.updatedAt ?? new Date(runningSnapshot.startedAt).toISOString(),
|
|
4987
|
+
}
|
|
4988
|
+
: undefined;
|
|
4989
|
+
const allTurns = runningTurn ? [runningTurn, ...completedTurns] : completedTurns;
|
|
4855
4990
|
const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
|
|
4856
4991
|
turnId: turn.turnId,
|
|
4857
4992
|
parentTurnId: turn.parentTurnId,
|
|
@@ -4870,16 +5005,18 @@ export class ProxySessionController {
|
|
|
4870
5005
|
100,
|
|
4871
5006
|
);
|
|
4872
5007
|
const isCurrent = current.lastTurnId === turn.turnId && current.lastTurnStatus === "completed";
|
|
5008
|
+
const isRunning = runningTurnId === turn.turnId;
|
|
4873
5009
|
return [{
|
|
4874
5010
|
sequence: graph.sequence,
|
|
4875
5011
|
graphNodeLine: graph.nodeLine,
|
|
4876
5012
|
graphConnectorLine: graph.connectorLine,
|
|
4877
5013
|
lines: [
|
|
4878
|
-
|
|
4879
|
-
`${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${
|
|
5014
|
+
cardText(summary || "未记录对话内容"),
|
|
5015
|
+
`${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardText(turn.turnId)}`,
|
|
4880
5016
|
],
|
|
4881
5017
|
current: isCurrent,
|
|
4882
|
-
|
|
5018
|
+
running: isRunning,
|
|
5019
|
+
actions: isCurrent || isRunning ? undefined : [{
|
|
4883
5020
|
text: "Reset",
|
|
4884
5021
|
value: {
|
|
4885
5022
|
action: "turn_reset",
|
|
@@ -4915,7 +5052,9 @@ export class ProxySessionController {
|
|
|
4915
5052
|
const card = this.cardRenderer.renderResetHistoryCard({
|
|
4916
5053
|
entries,
|
|
4917
5054
|
footerLines: [
|
|
4918
|
-
|
|
5055
|
+
runningTurn
|
|
5056
|
+
? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
|
|
5057
|
+
: `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
|
|
4919
5058
|
],
|
|
4920
5059
|
pageActions,
|
|
4921
5060
|
});
|
|
@@ -5282,8 +5421,8 @@ export class ProxySessionController {
|
|
|
5282
5421
|
reference: entry.reference,
|
|
5283
5422
|
summary: `${offset + index + 1}. ${marker}${title} · ${entry.agentName}`,
|
|
5284
5423
|
detailLines: [
|
|
5285
|
-
cardText(lastUserPrompt)
|
|
5286
|
-
|
|
5424
|
+
`**最新 Prompt**:${cardText(lastUserPrompt)}`,
|
|
5425
|
+
`<font color='grey'>最近更新:${cardText(entry.updatedLabel)}</font>`,
|
|
5287
5426
|
],
|
|
5288
5427
|
actions,
|
|
5289
5428
|
current: entry.current,
|
|
@@ -6996,7 +7135,8 @@ function isRetryableLlmTurnFailure(message: string): boolean {
|
|
|
6996
7135
|
/service unavailable|internal server error|bad gateway|gateway timeout/u,
|
|
6997
7136
|
/(?:^|\D)(?:500|502|503|504)(?:\D|$)|upstream (?:error|failure|timeout)/u,
|
|
6998
7137
|
/deadline exceeded|request timed out|request timeout|response timeout/u,
|
|
6999
|
-
/connection (?:reset|closed|aborted)|network error|socket hang up/u,
|
|
7138
|
+
/connection (?:reset|closed|aborted|disconnected)|network error|socket hang up/u,
|
|
7139
|
+
/app server .*(?:closed|disconnected|exited|reset|aborted)/u,
|
|
7000
7140
|
/stream (?:closed|disconnected|interrupted|error)/u,
|
|
7001
7141
|
/\b(?:llm|model|provider|inference)\b.*\b(?:error|failed|failure|unavailable|timeout|timed out)\b/u,
|
|
7002
7142
|
].some((pattern) => pattern.test(normalized));
|
|
@@ -9,33 +9,6 @@ export class SessionMetadataHydrator {
|
|
|
9
9
|
) {}
|
|
10
10
|
|
|
11
11
|
async hydrate(session: SessionRecord): Promise<SessionRecord> {
|
|
12
|
-
if (
|
|
13
|
-
session.runtimeKind === "codex"
|
|
14
|
-
&& session.remoteSessionId
|
|
15
|
-
&& session.lastTurnId
|
|
16
|
-
&& session.status === "running"
|
|
17
|
-
&& session.lastTurnStatus === "running"
|
|
18
|
-
) {
|
|
19
|
-
const runtime = this.runtimes.forAgent(session.agentName);
|
|
20
|
-
const loaded = runtime.getSession(session.localSessionId) ?? await runtime.resumeSession({
|
|
21
|
-
localSessionId: session.localSessionId,
|
|
22
|
-
remoteSessionId: session.remoteSessionId,
|
|
23
|
-
activeTurnId: session.lastTurnId,
|
|
24
|
-
lastTurnId: session.lastTurnId,
|
|
25
|
-
lastTurnStatus: session.lastTurnStatus,
|
|
26
|
-
agentName: session.agentName,
|
|
27
|
-
cwd: session.cwd,
|
|
28
|
-
title: session.title,
|
|
29
|
-
model: session.model,
|
|
30
|
-
reasoningEffort: session.reasoningEffort,
|
|
31
|
-
permissionMode: session.permissionMode ?? "auto",
|
|
32
|
-
});
|
|
33
|
-
const synchronized = await runtime.synchronizeSession(loaded.localSessionId);
|
|
34
|
-
if (synchronized.title) {
|
|
35
|
-
this.store.updateRuntimeSession(session.localSessionId, { title: synchronized.title });
|
|
36
|
-
}
|
|
37
|
-
return this.store.getSession(session.localSessionId) ?? session;
|
|
38
|
-
}
|
|
39
12
|
if (session.title || !session.runtimeKind || !session.remoteSessionId) return session;
|
|
40
13
|
if (session.runtimeKind === "codex" && !session.lastTurnId) return session;
|
|
41
14
|
const metadata = await this.runtimes.forAgent(session.agentName).readSessionMetadata(session.remoteSessionId);
|
package/src/state/StateStore.ts
CHANGED
|
@@ -111,6 +111,7 @@ export type TurnAttemptStatus =
|
|
|
111
111
|
| "accepted"
|
|
112
112
|
| "running"
|
|
113
113
|
| "recovering"
|
|
114
|
+
| "cancelling"
|
|
114
115
|
| "completed"
|
|
115
116
|
| "failed"
|
|
116
117
|
| "cancelled"
|
|
@@ -633,6 +634,15 @@ export class StateStore {
|
|
|
633
634
|
return rows.map(mapTurnAttempt);
|
|
634
635
|
}
|
|
635
636
|
|
|
637
|
+
listCancellationRequestedTurnAttempts(): TurnAttemptRecord[] {
|
|
638
|
+
const rows = this.db.prepare(`
|
|
639
|
+
SELECT * FROM turn_attempts
|
|
640
|
+
WHERE status = 'cancelling'
|
|
641
|
+
ORDER BY created_at ASC, rowid ASC
|
|
642
|
+
`).all() as TurnAttemptRow[];
|
|
643
|
+
return rows.map(mapTurnAttempt);
|
|
644
|
+
}
|
|
645
|
+
|
|
636
646
|
updateTurnAttempt(
|
|
637
647
|
attemptId: string,
|
|
638
648
|
patch: Partial<{
|
|
@@ -697,6 +707,19 @@ export class StateStore {
|
|
|
697
707
|
return row ? mapTurnAttempt(row) : undefined;
|
|
698
708
|
}
|
|
699
709
|
|
|
710
|
+
requestTurnAttemptCancellation(turnId: string): TurnAttemptRecord | undefined {
|
|
711
|
+
const existing = this.findIncompleteTurnAttemptByTurnId(turnId);
|
|
712
|
+
if (!existing) return undefined;
|
|
713
|
+
this.updateTurnAttempt(existing.attemptId, { status: "cancelling" });
|
|
714
|
+
return existing;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
restoreTurnAttemptAfterCancellationFailure(attempt: TurnAttemptRecord): void {
|
|
718
|
+
const current = this.getTurnAttempt(attempt.attemptId);
|
|
719
|
+
if (current?.status !== "cancelling") return;
|
|
720
|
+
this.updateTurnAttempt(attempt.attemptId, { status: attempt.status });
|
|
721
|
+
}
|
|
722
|
+
|
|
700
723
|
touchTurnAttempt(turnId: string, activityAt = new Date()): void {
|
|
701
724
|
const timestamp = activityAt.toISOString();
|
|
702
725
|
const throttleCutoff = new Date(activityAt.getTime() - 15_000).toISOString();
|
|
@@ -737,6 +760,17 @@ export class StateStore {
|
|
|
737
760
|
return this.getTurnAttempt(attemptId);
|
|
738
761
|
}
|
|
739
762
|
|
|
763
|
+
prepareUnstartedTurnAttemptRetry(attemptId: string): TurnAttemptRecord | undefined {
|
|
764
|
+
const existing = this.getTurnAttempt(attemptId);
|
|
765
|
+
if (!existing || existing.turnId || !isIncompleteTurnAttemptStatus(existing.status)) return undefined;
|
|
766
|
+
this.updateTurnAttempt(attemptId, {
|
|
767
|
+
pendingTurnId: null,
|
|
768
|
+
retryCount: existing.retryCount + 1,
|
|
769
|
+
status: "recovering",
|
|
770
|
+
});
|
|
771
|
+
return this.getTurnAttempt(attemptId);
|
|
772
|
+
}
|
|
773
|
+
|
|
740
774
|
rebindPendingTurnMessages(localSessionId: string, sourceTurnId: string, targetTurnId: string): void {
|
|
741
775
|
const now = new Date().toISOString();
|
|
742
776
|
const rebind = this.db.transaction(() => {
|
|
@@ -1057,6 +1091,15 @@ export class StateStore {
|
|
|
1057
1091
|
`).run(turnId, localSessionId, parentTurnId ?? null, new Date().toISOString());
|
|
1058
1092
|
}
|
|
1059
1093
|
|
|
1094
|
+
getTurnParent(turnId: string, localSessionId: string): string | undefined {
|
|
1095
|
+
const row = this.db.prepare(`
|
|
1096
|
+
SELECT parent_turn_id
|
|
1097
|
+
FROM turn_parent_links
|
|
1098
|
+
WHERE turn_id = ? AND local_session_id = ?
|
|
1099
|
+
`).get(turnId, localSessionId) as { parent_turn_id: string | null } | undefined;
|
|
1100
|
+
return row?.parent_turn_id ?? undefined;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1060
1103
|
findLatestCompletedTurnId(localSessionId: string, contextKey: string): string | undefined {
|
|
1061
1104
|
const row = this.db
|
|
1062
1105
|
.prepare(`
|