@gajae-code/coding-agent 0.11.8 → 0.11.9
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 +18 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +12 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -3
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +13 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/package.json +8 -8
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli.ts +32 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/model-profiles.ts +2 -3
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +842 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- package/src/gjc-runtime/ralplan-runtime.ts +3 -1
- package/src/gjc-runtime/state-writer.ts +166 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/modes/components/welcome.ts +10 -2
- package/src/modes/interactive-mode.ts +29 -9
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/runtime-mcp/json-rpc.ts +7 -4
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +18 -2
- package/src/session/agent-session.ts +86 -0
- package/src/session/internal/managed-session-scope.ts +206 -111
- package/src/session/internal/managed-session-storage.ts +17 -4
- package/src/session/session-manager.ts +96 -33
- package/src/session/session-storage.ts +23 -0
- package/src/skill-state/active-state.ts +10 -0
- package/src/task/executor.ts +13 -5
- package/src/tools/ask.ts +1 -0
|
@@ -70,6 +70,7 @@ import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
|
70
70
|
import { StatusLineComponent } from "./components/tool-status-header";
|
|
71
71
|
import { composeToolText } from "./components/tool-transcript-format";
|
|
72
72
|
import {
|
|
73
|
+
type RecentSession,
|
|
73
74
|
WelcomeComponent,
|
|
74
75
|
type WelcomeLogoMode,
|
|
75
76
|
type LspServerInfo as WelcomeLspServerInfo,
|
|
@@ -631,15 +632,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
631
632
|
const modelName = this.session.model?.name ?? "Unknown";
|
|
632
633
|
const providerName = this.session.model?.provider ?? "Unknown";
|
|
633
634
|
|
|
634
|
-
//
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
sessions.map(s => ({
|
|
638
|
-
name: s.name,
|
|
639
|
-
timeAgo: s.timeAgo,
|
|
640
|
-
})),
|
|
641
|
-
),
|
|
642
|
-
);
|
|
635
|
+
// Session history is display-only. Never hold the editor and keyboard behind
|
|
636
|
+
// transcript I/O; populate the welcome trail after the interactive surface exists.
|
|
637
|
+
const recentSessions: RecentSession[] = [];
|
|
643
638
|
|
|
644
639
|
const startupQuiet = settings.get("startup.quiet");
|
|
645
640
|
const welcomeLogoMode = resolveWelcomeLogoMode(settings.get("startup.welcomeBannerMode"));
|
|
@@ -763,6 +758,31 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
763
758
|
if (this.settings.get("tasksPane.defaultVisible")) this.showTasksPane();
|
|
764
759
|
this.#syncIrcSidebarAvailabilityFromSettings();
|
|
765
760
|
this.ui.requestRender(true);
|
|
761
|
+
if (this.#welcomeComponent) {
|
|
762
|
+
const welcomeComponent = this.#welcomeComponent;
|
|
763
|
+
const recentSessionsTimer = setTimeout(() => {
|
|
764
|
+
void logger
|
|
765
|
+
.time("InteractiveMode.init:recentSessions", () =>
|
|
766
|
+
getRecentSessions(this.sessionManager.getSessionDir()),
|
|
767
|
+
)
|
|
768
|
+
.then(sessions => {
|
|
769
|
+
if (this.#welcomeComponent !== welcomeComponent) return;
|
|
770
|
+
welcomeComponent.setRecentSessions(
|
|
771
|
+
sessions.map(session => ({
|
|
772
|
+
name: session.name,
|
|
773
|
+
timeAgo: session.timeAgo,
|
|
774
|
+
})),
|
|
775
|
+
);
|
|
776
|
+
this.ui.requestRender();
|
|
777
|
+
})
|
|
778
|
+
.catch(error => {
|
|
779
|
+
logger.debug("Failed to load recent sessions for welcome screen", {
|
|
780
|
+
error: error instanceof Error ? error.message : String(error),
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
}, 0);
|
|
784
|
+
recentSessionsTimer.unref?.();
|
|
785
|
+
}
|
|
766
786
|
|
|
767
787
|
// GitHub star reminder (interactive-only). Register the decline-driven
|
|
768
788
|
// injection contributor and schedule the launch nudge after the first
|
|
@@ -24,6 +24,7 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
|
|
|
24
24
|
- Informational questions are answer-only/read-only unless the user explicitly requests a change, command, or execution.
|
|
25
25
|
- Vague requirements use `/skill:deep-interview`: a requirements-only workflow that must not mutate product code. Its spec hands off as deep-interview → ralplan consensus → pending approval → separately approved execution.
|
|
26
26
|
- Clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate` and stops pending approval.
|
|
27
|
+
- Deep-interview state is runtime-owned: diagnose with `gjc deep-interview sanity-check`, inspect only the affected bounded selector, and repair through typed operations using CLI-generated/edited drafts (`draft create|edit|show|check|rebase|discard`) consumed with `--draft-id`. Inline JSON request flags are compatibility-only; normal flow must never reconstruct a payload or full envelope.
|
|
27
28
|
- Use `/skill:ultragoal` for durable goal ledgers and `/skill:team` for approved coordinated persistent work.
|
|
28
29
|
- Delegate large implementation slices to `executor`; use `planner`, `architect`, or `critic` for bounded planning and review.
|
|
29
30
|
- Active skills are authoritative: never ignore an invoked skill; read the full skill text and follow it exactly.
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Lightweight utilities for calling MCP servers directly via HTTP
|
|
5
5
|
* without maintaining persistent connections.
|
|
6
6
|
*/
|
|
7
|
-
// biome-ignore assist/source/organizeImports: Keep independent MCP security imports on separate merge anchors.
|
|
8
|
-
import { cancelMCPStream, MCP_HTTP_TIMEOUT_MS, MCP_MAX_CONTENT_BYTES, readMCPResponseText } from "./content-limits";
|
|
9
7
|
import { logger } from "@gajae-code/utils";
|
|
8
|
+
import { cancelMCPStream, MCP_HTTP_TIMEOUT_MS, MCP_MAX_CONTENT_BYTES, readMCPResponseText } from "./content-limits";
|
|
9
|
+
import { redactMCPDiagnosticValue, redactMCPEndpoint } from "./redaction";
|
|
10
10
|
|
|
11
11
|
/** Parse SSE response format (lines starting with "data: ") */
|
|
12
12
|
export function parseSSE(text: string): unknown {
|
|
@@ -82,7 +82,7 @@ export async function callMCP<T = unknown>(
|
|
|
82
82
|
if (!response.ok) {
|
|
83
83
|
cancelMCPStream(response.body);
|
|
84
84
|
const errorMsg = `MCP request failed: ${response.status} ${response.statusText}`;
|
|
85
|
-
logger.error(errorMsg, { url, method, params });
|
|
85
|
+
logger.error(errorMsg, { url: redactMCPEndpoint(url), method, params: redactMCPDiagnosticValue(params) });
|
|
86
86
|
throw new Error(errorMsg);
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -90,7 +90,10 @@ export async function callMCP<T = unknown>(
|
|
|
90
90
|
const result = parseSSE(text) as JsonRpcResponse<T> | null;
|
|
91
91
|
|
|
92
92
|
if (!result) {
|
|
93
|
-
logger.error("Failed to parse MCP response", {
|
|
93
|
+
logger.error("Failed to parse MCP response", {
|
|
94
|
+
url: redactMCPEndpoint(url),
|
|
95
|
+
method,
|
|
96
|
+
});
|
|
94
97
|
throw new Error("Failed to parse MCP response");
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const REDACTED = "<redacted>";
|
|
2
|
+
const SENSITIVE_KEY_PATTERN = /(?:token|secret|key|credential|password|authorization|auth|bearer|cookie|session)/i;
|
|
3
|
+
|
|
4
|
+
export function redactMCPEndpoint(value: string | undefined): string | undefined {
|
|
5
|
+
if (!value) return value;
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
if (url.username) url.username = REDACTED;
|
|
9
|
+
if (url.password) url.password = REDACTED;
|
|
10
|
+
if (url.pathname !== "/") url.pathname = `/${REDACTED}`;
|
|
11
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
12
|
+
url.searchParams.set(key, REDACTED);
|
|
13
|
+
}
|
|
14
|
+
url.hash = "";
|
|
15
|
+
return url.toString();
|
|
16
|
+
} catch {
|
|
17
|
+
return REDACTED;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function redactMCPDiagnosticValue(value: unknown): unknown {
|
|
22
|
+
if (Array.isArray(value)) return value.map(redactMCPDiagnosticValue);
|
|
23
|
+
if (!value || typeof value !== "object") return value;
|
|
24
|
+
return Object.fromEntries(
|
|
25
|
+
Object.entries(value).map(([key, entry]) => [
|
|
26
|
+
key,
|
|
27
|
+
SENSITIVE_KEY_PATTERN.test(key) ? REDACTED : redactMCPDiagnosticValue(entry),
|
|
28
|
+
]),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -131,6 +131,7 @@ function compactEvents(events: SessionIndexEvent[]): SessionIndexEvent[] {
|
|
|
131
131
|
|
|
132
132
|
async function appendSync(file: string, value: string): Promise<void> {
|
|
133
133
|
const h = await fs.open(file, "a", 0o600);
|
|
134
|
+
let failure: { error: unknown } | undefined;
|
|
134
135
|
try {
|
|
135
136
|
const data = Buffer.from(`${value}\n`);
|
|
136
137
|
for (let offset = 0; offset < data.length; ) {
|
|
@@ -139,9 +140,18 @@ async function appendSync(file: string, value: string): Promise<void> {
|
|
|
139
140
|
offset += bytesWritten;
|
|
140
141
|
}
|
|
141
142
|
await h.sync();
|
|
142
|
-
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
failure = { error };
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
143
147
|
await h.close();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// Bun may report EBADF when concurrent child-pipe teardown has already
|
|
150
|
+
// released a fully written and fsynced append handle. The descriptor is
|
|
151
|
+
// closed in that case; every other close failure remains fatal.
|
|
152
|
+
if ((error as NodeJS.ErrnoException).code !== "EBADF" && failure === undefined) failure = { error };
|
|
144
153
|
}
|
|
154
|
+
if (failure !== undefined) throw failure.error;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
async function syncDirectory(file: string): Promise<void> {
|
|
@@ -32,11 +32,13 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
32
32
|
* authority declaration contract with bounded frame-delivery acknowledgement.
|
|
33
33
|
* Generation 11 accepts typed retained exact-unlink cleanup authority — a
|
|
34
34
|
* concrete detached quarantine plus a proven-absent canonical lock pathname —
|
|
35
|
-
* when deleting an observed owner-lock lease.
|
|
35
|
+
* when deleting an observed owner-lock lease. Generation 12 refreshes retained
|
|
36
|
+
* native path and process authority semantics. Generation 13 rejects special
|
|
37
|
+
* files before retained native authority opens.
|
|
36
38
|
*/
|
|
37
39
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
38
|
-
discord:
|
|
39
|
-
slack:
|
|
40
|
+
discord: 13,
|
|
41
|
+
slack: 13,
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -38,5 +38,7 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
|
|
|
38
38
|
* Generation 25 adds startup dead-root prune + leak-artifact self-heal
|
|
39
39
|
* on TelegramNotificationDaemon.run (#2958). Generation 26 adds bounded reload
|
|
40
40
|
* cooldown and lazy Telegram topic lifecycle safeguards (#2956, #2960, #2984).
|
|
41
|
+
* Generation 27 refreshes retained native path and process authority semantics.
|
|
42
|
+
* Generation 28 rejects special files before retained native authority opens.
|
|
41
43
|
*/
|
|
42
|
-
export const DAEMON_GENERATION =
|
|
44
|
+
export const DAEMON_GENERATION = 28;
|
|
@@ -3668,6 +3668,12 @@ export class TelegramNotificationDaemon {
|
|
|
3668
3668
|
private readonly botApi: BotApi;
|
|
3669
3669
|
private readonly effects = new TelegramEffectSupervisor();
|
|
3670
3670
|
private readonly topics = new TopicRegistry();
|
|
3671
|
+
/**
|
|
3672
|
+
* Telegram may accept createForumTopic while returning an unusable success
|
|
3673
|
+
* payload. Remember that ambiguity per endpoint so later frames cannot repeat
|
|
3674
|
+
* the external side effect. Explicit API failures remain retryable.
|
|
3675
|
+
*/
|
|
3676
|
+
readonly #malformedTopicCreateEndpoints = new Map<string, string>();
|
|
3671
3677
|
/** Serializes registry snapshots so an older atomic write cannot overwrite newer rename state. */
|
|
3672
3678
|
/** Legacy sockets have no durable token, so explicit teardown is their revocation fence. */
|
|
3673
3679
|
private readonly droppedSessions = new WeakSet<SessionSocket>();
|
|
@@ -5844,6 +5850,9 @@ export class TelegramNotificationDaemon {
|
|
|
5844
5850
|
if (session && sessionId === session.sessionId && this.#logicalSessionId(session) !== sessionId) return undefined;
|
|
5845
5851
|
const capturedCreationLease = creationLease ?? (session ? this.#socketLease(session, sessionId) : undefined);
|
|
5846
5852
|
if (session?.logicalSessionIdTrusted && !capturedCreationLease) return undefined;
|
|
5853
|
+
const creationEndpointKey = session?.endpointDigest ?? session?.endpointKey ?? "unbound";
|
|
5854
|
+
if (this.#malformedTopicCreateEndpoints.get(sessionId) === creationEndpointKey)
|
|
5855
|
+
throw new Error("createForumTopic: invalid message_thread_id");
|
|
5847
5856
|
const existing = this.topics.get(sessionId);
|
|
5848
5857
|
if (existing?.authorityState === "delete_pending" || existing?.bindingMalformed) return undefined;
|
|
5849
5858
|
if (existing) return existing.topicId;
|
|
@@ -5867,10 +5876,17 @@ export class TelegramNotificationDaemon {
|
|
|
5867
5876
|
throw new Error("topic authority was revoked during creation");
|
|
5868
5877
|
const res = await this.botApi.call("createForumTopic", { chat_id: this.opts.chatId, name });
|
|
5869
5878
|
if (isThreadedModeCapabilityRefusal(res)) throw new ThreadedModeCapabilityRefusal();
|
|
5870
|
-
const
|
|
5871
|
-
|
|
5879
|
+
const response = res as {
|
|
5880
|
+
ok?: unknown;
|
|
5881
|
+
result?: { message_thread_id?: unknown };
|
|
5882
|
+
};
|
|
5883
|
+
const tid = response.result?.message_thread_id;
|
|
5884
|
+
if (typeof tid !== "number" || !Number.isSafeInteger(tid) || tid <= 0) {
|
|
5885
|
+
if (response.ok === true) this.#malformedTopicCreateEndpoints.set(sessionId, creationEndpointKey);
|
|
5872
5886
|
throw new Error("createForumTopic: invalid message_thread_id");
|
|
5887
|
+
}
|
|
5873
5888
|
acceptedTopicId = String(tid);
|
|
5889
|
+
this.#malformedTopicCreateEndpoints.delete(sessionId);
|
|
5874
5890
|
if (capturedCreationLease && !(await this.#awaitCreationLeaseAuthority(capturedCreationLease))) {
|
|
5875
5891
|
if (
|
|
5876
5892
|
!this.topics.fenceAcceptedCreateForLease(
|
|
@@ -785,6 +785,22 @@ type RetryErrorClassification =
|
|
|
785
785
|
|
|
786
786
|
const BARE_DEFAULT_WATCHDOG_ERROR =
|
|
787
787
|
/^(?:[A-Za-z][A-Za-z0-9-]*(?: [A-Za-z][A-Za-z0-9-]*){0,3} )stream (?:timed out while waiting for the first event|stalled while waiting for the next event)$/;
|
|
788
|
+
const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
789
|
+
"anthropic-messages": new Set([
|
|
790
|
+
"Provider stream timed out while waiting for the first event",
|
|
791
|
+
"Anthropic stream timed out while waiting for the first event",
|
|
792
|
+
]),
|
|
793
|
+
"openai-completions": new Set([
|
|
794
|
+
"Provider stream timed out while waiting for the first event",
|
|
795
|
+
"OpenAI completions stream timed out while waiting for the first event",
|
|
796
|
+
]),
|
|
797
|
+
} as const;
|
|
798
|
+
|
|
799
|
+
const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
|
|
800
|
+
const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
801
|
+
"openai-responses": "OpenAI responses stream timed out while waiting for the first event",
|
|
802
|
+
"openai-completions": "OpenAI completions stream timed out while waiting for the first event",
|
|
803
|
+
} as const;
|
|
788
804
|
|
|
789
805
|
function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boolean {
|
|
790
806
|
if (message.errorKind !== undefined || message.errorStatus !== undefined) return true;
|
|
@@ -12432,6 +12448,7 @@ export class AgentSession {
|
|
|
12432
12448
|
}
|
|
12433
12449
|
const retryAfterMs = this.#parseRetryAfterMsFromError(message);
|
|
12434
12450
|
const shouldRetry =
|
|
12451
|
+
!this.#isTerminalProviderCompactionTimeout(candidate, message) &&
|
|
12435
12452
|
retrySettings.enabled &&
|
|
12436
12453
|
attempt < retrySettings.maxRetries &&
|
|
12437
12454
|
(retryAfterMs !== undefined ||
|
|
@@ -12617,7 +12634,61 @@ export class AgentSession {
|
|
|
12617
12634
|
* unknown/no-code errors are retryable.
|
|
12618
12635
|
*/
|
|
12619
12636
|
|
|
12637
|
+
#isKimiCodeFirstEventTimeout(message: AssistantMessage): boolean {
|
|
12638
|
+
if (message.stopReason !== "error" || message.provider !== "kimi-code") return false;
|
|
12639
|
+
const errorMessage = message.errorMessage ?? "";
|
|
12640
|
+
if (message.api === "anthropic-messages") {
|
|
12641
|
+
return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["anthropic-messages"].has(errorMessage);
|
|
12642
|
+
}
|
|
12643
|
+
if (message.api === "openai-completions") {
|
|
12644
|
+
return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["openai-completions"].has(errorMessage);
|
|
12645
|
+
}
|
|
12646
|
+
return false;
|
|
12647
|
+
}
|
|
12648
|
+
|
|
12649
|
+
#isKimiCodeCompactionTimeout(candidate: Model, errorMessage: string): boolean {
|
|
12650
|
+
if (candidate.provider !== "kimi-code") return false;
|
|
12651
|
+
return /^(?:Summarization failed|Turn prefix summarization failed): (?:Provider|Anthropic|OpenAI completions) stream timed out while waiting for the first event$/.test(
|
|
12652
|
+
errorMessage,
|
|
12653
|
+
);
|
|
12654
|
+
}
|
|
12655
|
+
|
|
12656
|
+
#alibabaTokenPlanCanonicalTimeoutForApi(api: string): string | undefined {
|
|
12657
|
+
if (api === "openai-responses" || api === "openai-completions") {
|
|
12658
|
+
return ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES[api];
|
|
12659
|
+
}
|
|
12660
|
+
return undefined;
|
|
12661
|
+
}
|
|
12662
|
+
|
|
12663
|
+
#isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
|
|
12664
|
+
if (message.stopReason !== "error" || message.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
|
|
12665
|
+
const canonicalMessage = this.#alibabaTokenPlanCanonicalTimeoutForApi(message.api);
|
|
12666
|
+
return canonicalMessage !== undefined && message.errorMessage === canonicalMessage;
|
|
12667
|
+
}
|
|
12668
|
+
|
|
12669
|
+
#isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
|
|
12670
|
+
if (candidate.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
|
|
12671
|
+
const canonicalMessage = this.#alibabaTokenPlanCanonicalTimeoutForApi(candidate.api);
|
|
12672
|
+
if (canonicalMessage === undefined) return false;
|
|
12673
|
+
return (
|
|
12674
|
+
errorMessage === `Summarization failed: ${canonicalMessage}` ||
|
|
12675
|
+
errorMessage === `Turn prefix summarization failed: ${canonicalMessage}`
|
|
12676
|
+
);
|
|
12677
|
+
}
|
|
12678
|
+
|
|
12679
|
+
#isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
|
|
12680
|
+
return this.#isKimiCodeFirstEventTimeout(message) || this.#isAlibabaTokenPlanFirstEventTimeout(message);
|
|
12681
|
+
}
|
|
12682
|
+
|
|
12683
|
+
#isTerminalProviderCompactionTimeout(candidate: Model, errorMessage: string): boolean {
|
|
12684
|
+
return (
|
|
12685
|
+
this.#isKimiCodeCompactionTimeout(candidate, errorMessage) ||
|
|
12686
|
+
this.#isAlibabaTokenPlanCompactionTimeout(candidate, errorMessage)
|
|
12687
|
+
);
|
|
12688
|
+
}
|
|
12689
|
+
|
|
12620
12690
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
12691
|
+
if (this.#isTerminalProviderFirstEventTimeout(message)) return false;
|
|
12621
12692
|
if (message.errorMessage?.startsWith("Model fallback chain exhausted;")) return false;
|
|
12622
12693
|
const transportFailure = message.transportFailure;
|
|
12623
12694
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
@@ -12767,6 +12838,9 @@ export class AgentSession {
|
|
|
12767
12838
|
if (isTerminalHttp4xx && (explicitStatus !== undefined || !/rate.?limit|too many requests/i.test(err))) {
|
|
12768
12839
|
return "terminal";
|
|
12769
12840
|
}
|
|
12841
|
+
if (this.#isTerminalProviderFirstEventTimeout(message)) {
|
|
12842
|
+
return "terminal";
|
|
12843
|
+
}
|
|
12770
12844
|
// A first-event timeout on ollama-cloud (the ollama-chat API) must not
|
|
12771
12845
|
// join the unbounded transient class: each continuation retry re-issues
|
|
12772
12846
|
// the full request to a remote, billable backend, so an unbounded loop
|
|
@@ -12949,6 +13023,18 @@ export class AgentSession {
|
|
|
12949
13023
|
},
|
|
12950
13024
|
};
|
|
12951
13025
|
}
|
|
13026
|
+
if (this.#isTerminalProviderFirstEventTimeout(outcome.failure.message)) {
|
|
13027
|
+
// The managed transport discarded this attempt before session policy saw it.
|
|
13028
|
+
// Remove its provisional controller charge and surface the original message.
|
|
13029
|
+
this.#defaultFallbackChain().discardStartedAttempt();
|
|
13030
|
+
return {
|
|
13031
|
+
type: "terminal",
|
|
13032
|
+
terminal: {
|
|
13033
|
+
stopReason: "error",
|
|
13034
|
+
messages: [outcome.failure.message],
|
|
13035
|
+
},
|
|
13036
|
+
};
|
|
13037
|
+
}
|
|
12952
13038
|
return this.#handleRetryableError(
|
|
12953
13039
|
outcome.failure.message,
|
|
12954
13040
|
true,
|