@bitkyc08/opencodex 2.7.33 → 2.7.34
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/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +21 -10
- package/README.ru.md +1 -1
- package/README.zh-CN.md +1 -1
- package/gui/dist/assets/index-BkmJJgg6.js +52 -0
- package/gui/dist/assets/index-Sg-7L_oZ.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +13 -6
- package/src/adapters/cursor/discovery.ts +39 -4
- package/src/adapters/cursor/exec-policy.ts +11 -13
- package/src/adapters/cursor/live-transport.ts +22 -4
- package/src/adapters/cursor/protobuf-events.ts +140 -8
- package/src/adapters/cursor/protobuf-request.ts +15 -0
- package/src/adapters/cursor/request-builder.ts +10 -5
- package/src/adapters/cursor/transport.ts +3 -2
- package/src/adapters/cursor/types.ts +14 -0
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro-errors.ts +111 -2
- package/src/adapters/kiro-events.ts +154 -35
- package/src/adapters/kiro-retry.ts +116 -32
- package/src/adapters/kiro-tools.ts +30 -20
- package/src/adapters/kiro-wire.ts +47 -6
- package/src/adapters/kiro.ts +891 -228
- package/src/adapters/openai-chat.ts +12 -5
- package/src/adapters/openai-responses.ts +7 -2
- package/src/bridge.ts +109 -26
- package/src/claude/outbound.ts +27 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog.ts +375 -33
- package/src/combos/index.ts +3 -0
- package/src/combos/request.ts +4 -4
- package/src/combos/resolve.ts +2 -2
- package/src/combos/types.ts +104 -2
- package/src/config.ts +70 -1
- package/src/lib/eventstream-decoder.ts +9 -0
- package/src/oauth/index.ts +3 -1
- package/src/oauth/kiro-credentials.ts +48 -20
- package/src/oauth/login-cli.ts +2 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/kiro-models.ts +2 -2
- package/src/providers/openai-sidecar.ts +28 -1
- package/src/providers/registry.ts +39 -2
- package/src/responses/parser.ts +22 -10
- package/src/responses/schema.ts +1 -0
- package/src/responses/state.ts +50 -10
- package/src/router.ts +15 -3
- package/src/server/auth-cors.ts +7 -0
- package/src/server/claude-messages.ts +6 -0
- package/src/server/index.ts +6 -3
- package/src/server/management-api.ts +187 -43
- package/src/server/ports.ts +4 -2
- package/src/server/request-log.ts +3 -2
- package/src/server/responses-item-id-repair.ts +281 -0
- package/src/server/responses.ts +274 -73
- package/src/types.ts +109 -16
- package/src/update/job.ts +81 -1
- package/src/vision/describe.ts +2 -1
- package/src/web-search/executor.ts +2 -1
- package/src/web-search/loop.ts +9 -1
- package/src/web-search/progress-stream.ts +12 -10
- package/gui/dist/assets/index-D6Fcl4yM.css +0 -1
- package/gui/dist/assets/index-d63HMU0x.js +0 -52
package/src/types.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface OcxParsedRequest {
|
|
|
9
9
|
_previousResponseInputExpanded?: boolean;
|
|
10
10
|
/** Provider-private stable Cursor conversation id resolved from the Responses previous_response_id chain. */
|
|
11
11
|
_cursorConversationId?: string;
|
|
12
|
+
/** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
|
|
13
|
+
_providerContinuation?: OcxProviderContinuationState;
|
|
12
14
|
/**
|
|
13
15
|
* The hosted `{type:"web_search", ...}` tool config, stashed when Codex enables web search. Routed
|
|
14
16
|
* (non-OpenAI) providers can't run it server-side, so the proxy re-exposes it as a function tool and
|
|
@@ -28,6 +30,12 @@ export interface OcxParsedRequest {
|
|
|
28
30
|
* (see src/responses/compaction.ts).
|
|
29
31
|
*/
|
|
30
32
|
_compactionRequest?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* True when the current request newly introduced a stored compaction summary/marker. Historical
|
|
35
|
+
* markers restored by previous_response_id expansion were already acknowledged and do not reset
|
|
36
|
+
* provider-private continuation caches again on every later turn.
|
|
37
|
+
*/
|
|
38
|
+
_contextCompactionBoundary?: boolean;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
export interface OcxContext {
|
|
@@ -51,6 +59,8 @@ export interface OcxUserMessage {
|
|
|
51
59
|
export interface OcxAssistantMessage {
|
|
52
60
|
role: "assistant";
|
|
53
61
|
content: OcxAssistantContentPart[];
|
|
62
|
+
/** Responses message phase, preserved when replaying translated provider output. */
|
|
63
|
+
phase?: OcxMessagePhase;
|
|
54
64
|
model?: string;
|
|
55
65
|
timestamp: number;
|
|
56
66
|
}
|
|
@@ -69,6 +79,8 @@ export interface OcxToolResultMessage {
|
|
|
69
79
|
toolNamespace?: string;
|
|
70
80
|
/** Text, or content parts when a tool (e.g. Codex view_image) returns an image in its output. */
|
|
71
81
|
content: string | OcxContentPart[];
|
|
82
|
+
/** True when the Responses result contained opaque encrypted output Kiro cannot translate. */
|
|
83
|
+
containsEncryptedContent?: boolean;
|
|
72
84
|
isError: boolean;
|
|
73
85
|
timestamp: number;
|
|
74
86
|
}
|
|
@@ -191,9 +203,26 @@ export interface OcxRequestOptions {
|
|
|
191
203
|
promptCacheKey?: string;
|
|
192
204
|
}
|
|
193
205
|
|
|
206
|
+
export type OcxMessagePhase = "commentary" | "final_answer";
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Provider-private state that must follow a locally expanded `previous_response_id` chain.
|
|
210
|
+
* Kept out of public Responses output and persisted only in the bounded local continuation cache.
|
|
211
|
+
*/
|
|
212
|
+
export interface OcxProviderContinuationState {
|
|
213
|
+
cursor?: {
|
|
214
|
+
conversationId?: string;
|
|
215
|
+
checkpointUsable?: boolean;
|
|
216
|
+
};
|
|
217
|
+
kiro?: {
|
|
218
|
+
conversationId?: string;
|
|
219
|
+
};
|
|
220
|
+
[provider: string]: Record<string, unknown> | undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
194
223
|
export type AdapterEvent =
|
|
195
224
|
| { type: "heartbeat" }
|
|
196
|
-
| { type: "text_delta"; text: string }
|
|
225
|
+
| { type: "text_delta"; text: string; phase?: OcxMessagePhase }
|
|
197
226
|
| { type: "thinking_delta"; thinking: string }
|
|
198
227
|
// Anthropic extended-thinking round-trip: signature_delta for the current thinking block, and
|
|
199
228
|
// opaque redacted_thinking blocks. Both must be replayed verbatim or tool-use turns 400.
|
|
@@ -211,10 +240,35 @@ export type AdapterEvent =
|
|
|
211
240
|
// the SAME output index, so the activity animates instead of flashing completed instantly.
|
|
212
241
|
| { type: "web_search_call_begin"; id: string }
|
|
213
242
|
| { type: "web_search_call_end"; id: string; queries: string[]; status?: "completed" | "failed"; sources?: OcxUrlCitation[] }
|
|
214
|
-
| {
|
|
243
|
+
| {
|
|
244
|
+
type: "done";
|
|
245
|
+
usage?: OcxUsage;
|
|
246
|
+
stopReason?: string;
|
|
247
|
+
endTurn?: boolean;
|
|
248
|
+
providerState?: OcxProviderContinuationState;
|
|
249
|
+
}
|
|
250
|
+
| {
|
|
251
|
+
type: "incomplete";
|
|
252
|
+
reason: string;
|
|
253
|
+
message?: string;
|
|
254
|
+
usage?: OcxUsage;
|
|
255
|
+
retryable?: boolean;
|
|
256
|
+
endTurn?: boolean;
|
|
257
|
+
providerState?: OcxProviderContinuationState;
|
|
258
|
+
}
|
|
215
259
|
// `usage` carries best-effort partial consumption when a turn dies before a clean done
|
|
216
260
|
// (e.g. cursor upstream 502 mid-stream), so failed requests can log real token counts.
|
|
217
|
-
| {
|
|
261
|
+
| {
|
|
262
|
+
type: "error";
|
|
263
|
+
message: string;
|
|
264
|
+
usage?: OcxUsage;
|
|
265
|
+
/** Authoritative upstream/proxy status when known; avoids message-based classification. */
|
|
266
|
+
status?: number;
|
|
267
|
+
/** Responses error type and code when the adapter has a structured provider failure. */
|
|
268
|
+
errorType?: string;
|
|
269
|
+
code?: string;
|
|
270
|
+
retryable?: boolean;
|
|
271
|
+
};
|
|
218
272
|
|
|
219
273
|
/**
|
|
220
274
|
* A web source backing a search answer. Surfaced on the search-end event and rendered by the bridge
|
|
@@ -399,6 +453,11 @@ export interface OcxConfig {
|
|
|
399
453
|
* the resolved sub-agent roster block ("" when nothing resolves).
|
|
400
454
|
*/
|
|
401
455
|
injectionPrompt?: string;
|
|
456
|
+
/**
|
|
457
|
+
* Proxy-authored multi-agent developer guidance. Undefined/true = enabled for
|
|
458
|
+
* backward compatibility; false suppresses both v1 and v2 guidance injection.
|
|
459
|
+
*/
|
|
460
|
+
multiAgentGuidanceEnabled?: boolean;
|
|
402
461
|
/**
|
|
403
462
|
* Global hard ceiling for the reasoning effort of EVERY proxied turn (main agent AND
|
|
404
463
|
* sub-agents). Ladder value "low".."max"; incoming efforts ranking above it are rewritten
|
|
@@ -520,6 +579,12 @@ export interface OcxComboConfig {
|
|
|
520
579
|
stickyLimit?: number;
|
|
521
580
|
/** Used when the client omits reasoning.effort. null/omitted leaves the target default unchanged. */
|
|
522
581
|
defaultEffort?: OcxComboDefaultEffort | null;
|
|
582
|
+
/**
|
|
583
|
+
* Optional public model name replacing the default `combo/<id>` slug. Bare names
|
|
584
|
+
* without "/" are allowed (e.g. "deepseek-v4-flash") so the combo can answer to a
|
|
585
|
+
* mandated model id; exact-match requests route here before any provider resolution.
|
|
586
|
+
*/
|
|
587
|
+
alias?: string;
|
|
523
588
|
}
|
|
524
589
|
|
|
525
590
|
/**
|
|
@@ -614,9 +679,24 @@ export interface OpenRouterProviderRouting {
|
|
|
614
679
|
allowFallbacks?: boolean;
|
|
615
680
|
}
|
|
616
681
|
|
|
682
|
+
export interface ResponsesItemIdRepairConfig {
|
|
683
|
+
/** Exact `message` item ids that the proxy should rewrite to request-local canonical ids. */
|
|
684
|
+
message?: string[];
|
|
685
|
+
/** Exact `reasoning` item ids that the proxy should rewrite to request-local canonical ids. */
|
|
686
|
+
reasoning?: string[];
|
|
687
|
+
/** Backfill missing `output_item.done` / terminal snapshot ids from the matching output_index. */
|
|
688
|
+
repairMissingTerminalIds?: boolean;
|
|
689
|
+
}
|
|
690
|
+
|
|
617
691
|
export interface OcxProviderConfig {
|
|
618
692
|
adapter: string;
|
|
619
693
|
baseUrl: string;
|
|
694
|
+
/**
|
|
695
|
+
* Optional relative resource path for key-auth openai-responses requests. Must start with `/`
|
|
696
|
+
* and must not include a URL scheme, query string, or fragment. When omitted, the adapter keeps
|
|
697
|
+
* the legacy `/v1/responses` construction.
|
|
698
|
+
*/
|
|
699
|
+
responsesPath?: string;
|
|
620
700
|
/**
|
|
621
701
|
* Explicit opt-in for non-registry private-network destinations such as localhost, RFC1918,
|
|
622
702
|
* link-local, or unique-local upstreams. Metadata endpoints remain blocked.
|
|
@@ -661,6 +741,13 @@ export interface OcxProviderConfig {
|
|
|
661
741
|
modelInputModalities?: Record<string, string[]>;
|
|
662
742
|
/** Model-specific max input token limits. Values cap auto_compact_token_limit. */
|
|
663
743
|
modelMaxInputTokens?: Record<string, number>;
|
|
744
|
+
/**
|
|
745
|
+
* Provider-wide fallback for chat-completions `max_tokens` when the caller omits
|
|
746
|
+
* Responses `max_output_tokens`. Adapters still let an explicit request win.
|
|
747
|
+
*/
|
|
748
|
+
defaultMaxOutputTokens?: number;
|
|
749
|
+
/** Model-specific fallback output token budgets. Exact/model-pattern entries beat the provider default. */
|
|
750
|
+
modelMaxOutputTokens?: Record<string, number>;
|
|
664
751
|
headers?: Record<string, string>;
|
|
665
752
|
/** Default provider-routing preferences for models sent through the canonical OpenRouter API. */
|
|
666
753
|
openRouterRouting?: OpenRouterProviderRouting;
|
|
@@ -731,6 +818,12 @@ export interface OcxProviderConfig {
|
|
|
731
818
|
* fields. Default off; only enable for providers that document this parameter.
|
|
732
819
|
*/
|
|
733
820
|
promptCacheKey?: boolean;
|
|
821
|
+
/**
|
|
822
|
+
* Provider-local passthrough SSE repair for broken openai-responses gateways that reuse exact
|
|
823
|
+
* placeholder message/reasoning ids or omit the terminal id after a stable added event.
|
|
824
|
+
* Disabled by default; function_call ids and call_id pairing are never rewritten.
|
|
825
|
+
*/
|
|
826
|
+
responsesItemIdRepair?: ResponsesItemIdRepairConfig;
|
|
734
827
|
/** Model ids whose tool_choice only accepts `auto` or `none`; forced/named choices are downgraded. */
|
|
735
828
|
autoToolChoiceOnlyModels?: string[];
|
|
736
829
|
/** Model ids that expect prior assistant `reasoning_content` to be preserved in chat history. */
|
|
@@ -778,23 +871,23 @@ export interface OcxProviderConfig {
|
|
|
778
871
|
desktopExecutor?: import("./adapters/cursor/native-exec-desktop").DesktopExecutorConfig;
|
|
779
872
|
/**
|
|
780
873
|
* Cursor adapter only: unsafe opt-in escape hatch for Cursor server-driven built-in local
|
|
781
|
-
* read/write/delete/ls/grep/shell/fetch execution.
|
|
782
|
-
*
|
|
783
|
-
*
|
|
874
|
+
* read/write/delete/ls/grep/shell/fetch execution. Prefer `nativeLocalExec: "on"` for new
|
|
875
|
+
* configs; this legacy boolean remains a server-local explicit opt-in for existing operators.
|
|
876
|
+
* Defaults to false so remote Cursor messages cannot bypass Codex approval/sandbox semantics.
|
|
877
|
+
* Explicit MCP and desktop executors remain controlled by their own opt-in config.
|
|
784
878
|
*/
|
|
785
879
|
unsafeAllowNativeLocalExec?: boolean;
|
|
786
880
|
/**
|
|
787
881
|
* Cursor adapter only: native local exec policy mode (exec-policy.ts).
|
|
788
|
-
* "
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
* browser origins by default but not loopback-origin or origin-less callers.
|
|
882
|
+
* "off" (default) rejects server-driven local exec; "on" always allows it for this
|
|
883
|
+
* provider and should be used only for a trusted local experiment on a host where every
|
|
884
|
+
* data-plane caller is trusted. "codex-sandbox" is accepted for backwards compatibility
|
|
885
|
+
* but is fail-closed like "off": Responses instructions/system/developer text is
|
|
886
|
+
* caller-controlled prose, and opencodex has no trustworthy per-request attestation that it
|
|
887
|
+
* reflects a real Codex sandbox state. The default loopback bind admits ANY local process
|
|
888
|
+
* without auth (including other local users on multi-user machines), and
|
|
889
|
+
* isAllowedRequestOrigin blocks non-loopback browser origins by default but not
|
|
890
|
+
* loopback-origin or origin-less callers.
|
|
798
891
|
*/
|
|
799
892
|
nativeLocalExec?: "off" | "codex-sandbox" | "on";
|
|
800
893
|
}
|
package/src/update/job.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { atomicWriteFile, getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
|
|
6
6
|
import { killProxy } from "../lib/process-control";
|
|
7
7
|
import { waitForPortAvailable } from "../server/ports";
|
|
8
|
+
import { proxyIdentityAt } from "../server/proxy-liveness";
|
|
8
9
|
import { isServiceInstalled } from "../service";
|
|
9
10
|
import {
|
|
10
11
|
type Channel,
|
|
@@ -24,6 +25,8 @@ const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/lates
|
|
|
24
25
|
const UPDATE_JOB_FILENAME = "update-job.json";
|
|
25
26
|
const UPDATE_TIMEOUT_MS = 180_000;
|
|
26
27
|
const RESTART_TIMEOUT_MS = 60_000;
|
|
28
|
+
const RESTART_HEALTH_TIMEOUT_MS = 15_000;
|
|
29
|
+
const RESTART_STABILITY_WINDOW_MS = 15_000;
|
|
27
30
|
|
|
28
31
|
export type UpdateJobStatus = "running" | "restarting" | "succeeded" | "failed";
|
|
29
32
|
|
|
@@ -291,6 +294,9 @@ export interface RestartIo {
|
|
|
291
294
|
waitForPort?: typeof waitForPortAvailable;
|
|
292
295
|
spawnStart?: (job: UpdateJobState, installer: Installer, port?: number) => void;
|
|
293
296
|
serviceInstalledFn?: () => boolean;
|
|
297
|
+
probeProxy?: (port: number, hostname?: string) => Promise<boolean>;
|
|
298
|
+
sleepMs?: (ms: number) => Promise<void>;
|
|
299
|
+
now?: () => number;
|
|
294
300
|
/** Service-mode install/reinstall command (defaults to spawnSync via runLoggedCommand). */
|
|
295
301
|
runService?: (
|
|
296
302
|
job: UpdateJobState,
|
|
@@ -377,6 +383,79 @@ export function restartAfterUpdateForTests(
|
|
|
377
383
|
return restartAfterUpdate(job, captured, io);
|
|
378
384
|
}
|
|
379
385
|
|
|
386
|
+
function restartFailureHint(port: number): string {
|
|
387
|
+
return `Update installed, but the restarted proxy did not stay healthy on port ${port}. `
|
|
388
|
+
+ "Try 'ocx start'. If the update log shows bun postinstall or EPERM warnings, "
|
|
389
|
+
+ "reinstall with 'npm install -g --allow-scripts=bun @bitkyc08/opencodex'.";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Confirm that the detached/service restart really came back and stayed up. The GUI worker
|
|
394
|
+
* used to mark success immediately after spawning the new process, which hid Windows cases
|
|
395
|
+
* where npm left the bundled Bun runtime half-updated and the restarted proxy died seconds
|
|
396
|
+
* later. A healthy /healthz must appear, then remain healthy for one short stability window.
|
|
397
|
+
*/
|
|
398
|
+
async function confirmRestartedProxy(
|
|
399
|
+
job: UpdateJobState,
|
|
400
|
+
captured: { port: number; hostname: string },
|
|
401
|
+
io: RestartIo = {},
|
|
402
|
+
): Promise<boolean> {
|
|
403
|
+
/* [Decision Log]
|
|
404
|
+
- 목적과 의도: GUI update job이 detached restart 요청만 보고 성공 처리하지 않도록, 실제 프록시 복귀 여부를 확인한다.
|
|
405
|
+
- 기존 구현 및 제약 조건: update-job.json은 spawn/service reinstall 직후 `succeeded`로 끝났고, Windows npm/Bun 교체 실패처럼 몇 초 후 죽는 재시작을 잡지 못했다.
|
|
406
|
+
- 검토한 주요 대안: (1) 포트 점유만 확인 — 외부 프로세스/죽기 직전 프로세스를 성공으로 오인할 수 있다. (2) 무기한 /healthz 폴링 — UX가 느려지고 worker 종료 시점이 불명확하다. (3) 짧은 healthy 등장 + 안정성 창 확인 — 실제 복귀를 확인하면서도 대기 시간을 제한할 수 있다.
|
|
407
|
+
- 선택한 방식: identity-aware /healthz probe가 일정 시간 안에 나타나고, 추가 안정성 창 동안 유지되는지 확인한다.
|
|
408
|
+
- 다른 대안 대신 이 방식을 선택한 이유: GUI는 "업데이트가 설치됐지만 재시작은 실패"를 분리해 알려줘야 하며, 이 방식이 가장 적은 오탐으로 그 경계를 만든다.
|
|
409
|
+
- 장점, 단점 및 영향: 장점은 silent restart failure가 update-job 상태로 드러난다는 점이다. 단점은 성공 판정이 최대 30초 늦어질 수 있다는 점이며, 대신 실제 복귀를 더 정확히 반영한다.
|
|
410
|
+
*/
|
|
411
|
+
const probe = io.probeProxy ?? (async (port: number, hostname?: string) => (
|
|
412
|
+
!!(await proxyIdentityAt(port, { hostname }))
|
|
413
|
+
));
|
|
414
|
+
const sleep = io.sleepMs ?? (async (ms: number) => {
|
|
415
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
416
|
+
});
|
|
417
|
+
const now = io.now ?? (() => Date.now());
|
|
418
|
+
const port = captured.port;
|
|
419
|
+
const hostname = captured.hostname;
|
|
420
|
+
const startDeadline = now() + RESTART_HEALTH_TIMEOUT_MS;
|
|
421
|
+
|
|
422
|
+
while (now() < startDeadline) {
|
|
423
|
+
if (await probe(port, hostname)) {
|
|
424
|
+
updateJob(job, {}, `Proxy reported healthy on ${hostname}:${port}; confirming it stays up...`);
|
|
425
|
+
const stableUntil = now() + RESTART_STABILITY_WINDOW_MS;
|
|
426
|
+
while (now() < stableUntil) {
|
|
427
|
+
if (!(await probe(port, hostname))) {
|
|
428
|
+
updateJob(job, {
|
|
429
|
+
status: "failed",
|
|
430
|
+
restarted: false,
|
|
431
|
+
error: `proxy restart became unhealthy on ${hostname}:${port}`,
|
|
432
|
+
}, restartFailureHint(port));
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
await sleep(500);
|
|
436
|
+
}
|
|
437
|
+
updateJob(job, {}, `Proxy stayed healthy for ${Math.trunc(RESTART_STABILITY_WINDOW_MS / 1000)}s after restart.`);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
await sleep(250);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
updateJob(job, {
|
|
444
|
+
status: "failed",
|
|
445
|
+
restarted: false,
|
|
446
|
+
error: `proxy restart never became healthy on ${hostname}:${port}`,
|
|
447
|
+
}, restartFailureHint(port));
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function confirmRestartAfterUpdateForTests(
|
|
452
|
+
job: UpdateJobState,
|
|
453
|
+
captured: { port: number; hostname: string },
|
|
454
|
+
io: RestartIo,
|
|
455
|
+
): Promise<boolean> {
|
|
456
|
+
return confirmRestartedProxy(job, captured, io);
|
|
457
|
+
}
|
|
458
|
+
|
|
380
459
|
export async function runGuiUpdateWorker(jobId: string, channel: Channel, restart: boolean): Promise<void> {
|
|
381
460
|
let job = readUpdateJob(jobId);
|
|
382
461
|
const check = checkForUpdate(channel);
|
|
@@ -457,7 +536,8 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
|
|
|
457
536
|
if (restart) {
|
|
458
537
|
job = updateJob(job, { status: "restarting" }, "Update installed. Restarting proxy...");
|
|
459
538
|
await restartAfterUpdate(job, captured);
|
|
460
|
-
|
|
539
|
+
if (!(await confirmRestartedProxy(job, captured))) return;
|
|
540
|
+
updateJob(job, { status: "succeeded", restarted: true }, "Restart requested and proxy is healthy.");
|
|
461
541
|
return;
|
|
462
542
|
}
|
|
463
543
|
|
package/src/vision/describe.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
2
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
3
3
|
import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
|
|
4
|
+
import { redactSecretString } from "../lib/redact";
|
|
4
5
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
5
6
|
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
6
7
|
import { parseSidecarSSE } from "../web-search/parse";
|
|
@@ -99,7 +100,7 @@ export async function describeImage(
|
|
|
99
100
|
if (!res.ok) {
|
|
100
101
|
const t = await res.text().catch(() => "");
|
|
101
102
|
console.warn(`[vision] sidecar HTTP ${res.status} (${Date.now() - t0}ms)`);
|
|
102
|
-
return { text: "", error: `vision sidecar HTTP ${res.status}: ${t.slice(0, 200)}` };
|
|
103
|
+
return { text: "", error: `vision sidecar HTTP ${res.status}: ${redactSecretString(t.slice(0, 200))}` };
|
|
103
104
|
}
|
|
104
105
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
105
106
|
let parsed;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
2
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
3
3
|
import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
|
|
4
|
+
import { redactSecretString } from "../lib/redact";
|
|
4
5
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
5
6
|
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
6
7
|
import { parseSidecarSSE, type WebSearchResult } from "./parse";
|
|
@@ -84,7 +85,7 @@ export async function runWebSearch(
|
|
|
84
85
|
if (!res.ok) {
|
|
85
86
|
const t = await res.text().catch(() => "");
|
|
86
87
|
console.warn(`[web-search] sidecar HTTP ${res.status} for query "${query.slice(0, 80)}" (${Date.now() - t0}ms)`);
|
|
87
|
-
return { text: "", sources: [], error: `sidecar HTTP ${res.status}: ${t.slice(0, 200)}` };
|
|
88
|
+
return { text: "", sources: [], error: `sidecar HTTP ${res.status}: ${redactSecretString(t.slice(0, 200))}` };
|
|
88
89
|
}
|
|
89
90
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
90
91
|
try {
|
package/src/web-search/loop.ts
CHANGED
|
@@ -364,6 +364,13 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
364
364
|
inactivityTimeoutMs: routedModelStallTimeoutMs,
|
|
365
365
|
})) {
|
|
366
366
|
if (event.type === "heartbeat") yield event;
|
|
367
|
+
// Kiro's explicit-completion protocol marks ordinary assistant text as commentary while
|
|
368
|
+
// it performs a bounded final-answer retry. That text is safe to surface immediately and
|
|
369
|
+
// is exactly what the native Kiro transport streams. Keeping it in the search scanner made
|
|
370
|
+
// Codex show only `Working` until both Kiro attempts had finished (often 30-40 seconds).
|
|
371
|
+
// Tool events remain buffered below, so the decision to invoke the hosted sidecar is still
|
|
372
|
+
// atomic and no search call can escape before its stream has validated successfully.
|
|
373
|
+
else if (event.type === "text_delta" && event.phase === "commentary") yield event;
|
|
367
374
|
else events.push(event);
|
|
368
375
|
}
|
|
369
376
|
} catch (error) {
|
|
@@ -373,7 +380,8 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
373
380
|
throw new LoopError(502, `Provider stream error: ${error instanceof Error ? error.message : String(error)}`);
|
|
374
381
|
}
|
|
375
382
|
|
|
376
|
-
const terminalIndexes = events.flatMap((event, index) =>
|
|
383
|
+
const terminalIndexes = events.flatMap((event, index) =>
|
|
384
|
+
event.type === "done" || event.type === "incomplete" || event.type === "error" ? [index] : []);
|
|
377
385
|
if (terminalIndexes.length !== 1 || terminalIndexes[0] !== events.length - 1) {
|
|
378
386
|
throw new LoopError(502, `Web-search adapter stream protocol error: expected one final terminal event, received ${terminalIndexes.length}`);
|
|
379
387
|
}
|
|
@@ -241,7 +241,7 @@ export async function* parseStreamWithProgress(
|
|
|
241
241
|
resetInactivity();
|
|
242
242
|
|
|
243
243
|
const parserPump = (async (): Promise<void> => {
|
|
244
|
-
let
|
|
244
|
+
let heldTerminal: Extract<AdapterEvent, { type: "done" | "incomplete" }> | undefined;
|
|
245
245
|
try {
|
|
246
246
|
iterator = parseStream(tappedResponse);
|
|
247
247
|
if (settled) {
|
|
@@ -250,11 +250,11 @@ export async function* parseStreamWithProgress(
|
|
|
250
250
|
}
|
|
251
251
|
while (true) {
|
|
252
252
|
let result: IteratorResult<AdapterEvent>;
|
|
253
|
-
if (
|
|
253
|
+
if (heldTerminal) {
|
|
254
254
|
let drainTimer: ReturnType<typeof setTimeout> | undefined;
|
|
255
255
|
const drainTimeout = new Promise<never>((_, reject) => {
|
|
256
256
|
drainTimer = setTimeout(() => reject(new WebSearchStreamProtocolError(
|
|
257
|
-
`adapter did not return within ${postTerminalDrainTimeoutMs}ms after
|
|
257
|
+
`adapter did not return within ${postTerminalDrainTimeoutMs}ms after ${heldTerminal?.type ?? "terminal event"}`,
|
|
258
258
|
)), postTerminalDrainTimeoutMs);
|
|
259
259
|
});
|
|
260
260
|
const stoppedDuringDrain = stopped.then(reason => { throw reason; });
|
|
@@ -268,8 +268,8 @@ export async function* parseStreamWithProgress(
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
if (result.done) {
|
|
271
|
-
if (!
|
|
272
|
-
await handoff.deliver(
|
|
271
|
+
if (!heldTerminal) throw new WebSearchStreamProtocolError("adapter returned without a terminal event");
|
|
272
|
+
await handoff.deliver(heldTerminal);
|
|
273
273
|
if (!settled) {
|
|
274
274
|
settled = true;
|
|
275
275
|
clearInactivity();
|
|
@@ -281,17 +281,19 @@ export async function* parseStreamWithProgress(
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
const event = result.value;
|
|
284
|
-
if (
|
|
284
|
+
if (heldTerminal) {
|
|
285
285
|
throw new WebSearchStreamProtocolError(
|
|
286
|
-
event.type === "done"
|
|
286
|
+
event.type === "done" || event.type === "incomplete"
|
|
287
|
+
? "adapter yielded more than one terminal event"
|
|
288
|
+
: "adapter yielded an event after its terminal event",
|
|
287
289
|
);
|
|
288
290
|
}
|
|
289
291
|
if (event.type === "error") {
|
|
290
292
|
fail(new Error(event.message));
|
|
291
293
|
return;
|
|
292
294
|
}
|
|
293
|
-
if (event.type === "done") {
|
|
294
|
-
|
|
295
|
+
if (event.type === "done" || event.type === "incomplete") {
|
|
296
|
+
heldTerminal = event;
|
|
295
297
|
continue;
|
|
296
298
|
}
|
|
297
299
|
await handoff.deliver(event);
|
|
@@ -300,7 +302,7 @@ export async function* parseStreamWithProgress(
|
|
|
300
302
|
fail(error instanceof WebSearchStreamProtocolError
|
|
301
303
|
? error
|
|
302
304
|
: new WebSearchStreamProtocolError(
|
|
303
|
-
`adapter threw${
|
|
305
|
+
`adapter threw${heldTerminal ? " after its terminal event" : ""}: ${error instanceof Error ? error.message : String(error)}`,
|
|
304
306
|
));
|
|
305
307
|
}
|
|
306
308
|
})();
|