@bitkyc08/opencodex 2.64.0-preview.20260923 → 2.65.0-preview.20260925
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.md +28 -25
- package/bin/ocx.mjs +16 -2
- package/gui/dist/assets/App-8NMiZxT0.css +1 -0
- package/gui/dist/assets/App-CsYvvpr3.js +50 -0
- package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
- package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
- package/gui/dist/assets/{index-BEq4OCOz.js → index-BB0iHG8-.js} +12 -12
- package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -4
- package/src/adapters/anthropic.ts +30 -1
- package/src/adapters/claude-cli/adapter.ts +218 -0
- package/src/adapters/claude-cli/profiles.ts +38 -0
- package/src/adapters/codebuddy/profiles.ts +2 -0
- package/src/adapters/coding-agent/profile.ts +12 -4
- package/src/adapters/coding-agent/turn.ts +16 -6
- package/src/adapters/command-code-tool-text.ts +133 -23
- package/src/adapters/cursor/catalog.ts +4 -8
- package/src/adapters/cursor/discovery.ts +10 -5
- package/src/adapters/cursor/effort-map.ts +2 -6
- package/src/adapters/cursor/envelope-echo.ts +10 -5
- package/src/adapters/cursor/request-builder.ts +9 -4
- package/src/adapters/cursor/thread-continuity.ts +53 -0
- package/src/adapters/cursor.ts +29 -13
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/google-tool-schema.ts +26 -3
- package/src/adapters/identity.ts +214 -2
- package/src/adapters/openai-chat/passthrough.ts +1 -0
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
- package/src/adapters/openai-chat.ts +43 -15
- package/src/adapters/openai-responses/canonical-forward.ts +17 -10
- package/src/adapters/openai-responses/passthrough.ts +19 -4
- package/src/adapters/openai-responses/request-strips.ts +25 -0
- package/src/adapters/openai-responses/web-search.ts +22 -4
- package/src/adapters/qoder/profiles.ts +2 -0
- package/src/adapters/registry.ts +8 -0
- package/src/adapters/run-turn-queue.ts +7 -0
- package/src/bridge/response-json.ts +6 -4
- package/src/bridge/sse.ts +7 -5
- package/src/claude/alias.ts +73 -26
- package/src/claude/context-windows.ts +15 -6
- package/src/claude/desktop-3p-library.ts +3 -2
- package/src/claude/desktop-3p.ts +1 -1
- package/src/claude/desktop-first-party.ts +63 -22
- package/src/claude/desktop-picker-profile.ts +302 -0
- package/src/claude/desktop-picker.ts +365 -0
- package/src/claude/desktop-risk.ts +20 -0
- package/src/claude/gateway-cache.ts +13 -4
- package/src/claude/inbound.ts +5 -2
- package/src/claude/intercept/connect-proxy.ts +106 -29
- package/src/claude/intercept/local-ca.ts +76 -18
- package/src/claude/intercept/model-bindings.ts +145 -0
- package/src/claude/intercept/picker-bootstrap.ts +141 -0
- package/src/claude/intercept/picker-ca.ts +126 -0
- package/src/claude/intercept/picker-listener.ts +213 -0
- package/src/claude/intercept/picker-models.ts +101 -0
- package/src/claude/intercept/picker-runtime.ts +330 -0
- package/src/claude/intercept/picker-trust.ts +125 -0
- package/src/claude/intercept/runtime.ts +149 -2
- package/src/claude/model-info.ts +18 -4
- package/src/cli/agent.ts +16 -2
- package/src/cli/capabilities.ts +71 -2
- package/src/cli/claude-desktop.ts +319 -33
- package/src/cli/claude.ts +15 -7
- package/src/cli/codex-shim-autorestore.ts +1 -0
- package/src/cli/dispatch.ts +29 -23
- package/src/cli/effort.ts +16 -6
- package/src/cli/ensure-desired-integrations.ts +29 -4
- package/src/cli/ready.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/cli/restart-scope.ts +9 -1
- package/src/cli/runtime-api.ts +17 -0
- package/src/cli/system-command.ts +11 -12
- package/src/client/connect.ts +12 -2
- package/src/client/state.ts +39 -1
- package/src/clients/config-export.ts +57 -9
- package/src/codex/auth-context.ts +51 -11
- package/src/codex/catalog/derive-entry.ts +5 -5
- package/src/codex/catalog/gather-capture.ts +25 -6
- package/src/codex/catalog/metadata.ts +3 -3
- package/src/codex/catalog/parsing.ts +39 -1
- package/src/codex/catalog/provider-models.ts +8 -6
- package/src/codex/catalog/retained-sync.ts +17 -7
- package/src/codex/catalog/sync.ts +2 -2
- package/src/codex/desktop-app-restart.ts +8 -0
- package/src/codex/desktop-switches.ts +9 -1
- package/src/codex/home.ts +43 -4
- package/src/codex/inject/config-toml.ts +137 -0
- package/src/codex/inject/plan.ts +23 -0
- package/src/codex/inject/remove.ts +21 -1
- package/src/codex/inject.ts +63 -8
- package/src/codex/journal.ts +36 -0
- package/src/codex/main-account-hard-lock.ts +14 -1
- package/src/codex/main-account-policy-wait.ts +109 -0
- package/src/codex/native-profile-startup.ts +24 -6
- package/src/codex/native-residue.ts +28 -10
- package/src/codex/quota-types.ts +8 -1
- package/src/codex/quota.ts +49 -7
- package/src/codex/runtime.ts +31 -10
- package/src/combos/failover.ts +7 -6
- package/src/combos/resolve.ts +91 -14
- package/src/combos/types.ts +18 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +21 -2
- package/src/config/schema/leaf-validators.ts +6 -3
- package/src/config/subagent-models.ts +3 -1
- package/src/generated/compatibility-version.json +275 -163
- package/src/images/artifacts.ts +14 -0
- package/src/integrations/owned-refresh.ts +1 -1
- package/src/integrations/ownership-policy.ts +32 -1
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +9 -0
- package/src/lib/config-ownership.ts +3 -0
- package/src/lib/errors.ts +87 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/request-execution-budget.ts +42 -14
- package/src/lib/request-resend-gate.ts +4 -3
- package/src/lib/retry-delay.ts +7 -1
- package/src/lib/tool-envelope-echo-filter.ts +236 -0
- package/src/lib/upstream-retry.ts +20 -12
- package/src/oauth/index.ts +1 -0
- package/src/oauth/kiro-credentials.ts +14 -1
- package/src/oauth/login-cli.ts +1 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fast-opt-in.ts +31 -0
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/provider-id-rewrite.ts +2 -0
- package/src/providers/quota/vendor-probes-key.ts +8 -2
- package/src/providers/registry/entries-core.ts +45 -2
- package/src/providers/registry/entries-extended.ts +99 -12
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/types.ts +7 -1
- package/src/providers/resolved-model-policy.ts +11 -4
- package/src/providers/service-tier.ts +10 -3
- package/src/responses/code-mode-helper-compat.ts +3 -0
- package/src/responses/parser.ts +45 -3
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/claude-messages.ts +69 -14
- package/src/server/grok-upstream-envelope-echo.ts +120 -0
- package/src/server/index/claude-intercept-lifecycle.ts +24 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index.ts +7 -0
- package/src/server/management/agent-settings-routes.ts +204 -112
- package/src/server/management/claude-desktop-picker-routes.ts +128 -0
- package/src/server/management/combo-routes.ts +59 -2
- package/src/server/management/config-routes.ts +81 -39
- package/src/server/management/context.ts +3 -0
- package/src/server/management/native-integration-routes.ts +169 -93
- package/src/server/management/provider-routes.ts +46 -4
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/routing-profile-routes.ts +9 -0
- package/src/server/management/sidebar-routes.ts +52 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/proxy-liveness.ts +38 -3
- package/src/server/request-log-account-rotation.ts +53 -0
- package/src/server/request-log.ts +3 -16
- package/src/server/responses/adapter-dispatch.ts +17 -1
- package/src/server/responses/agent-task-recovery.ts +49 -26
- package/src/server/responses/codex-ws-exchange.ts +18 -7
- package/src/server/responses/codex-ws-wire.ts +27 -4
- package/src/server/responses/core-combo.ts +21 -3
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-opaque-recovery.ts +3 -2
- package/src/server/responses/encrypted-payload.ts +15 -14
- package/src/server/responses/fetch-helpers.ts +1 -1
- package/src/server/responses/input-admission.ts +10 -6
- package/src/server/responses/native-response-control.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +182 -12
- package/src/server/responses/passthrough-dispatch.ts +102 -44
- package/src/server/responses/policy-fallback.ts +3 -0
- package/src/server/responses/policy-refusal.ts +67 -0
- package/src/server/responses/request-send-budget.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +42 -11
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/system-env-shell.ts +0 -1
- package/src/server/system-env.ts +25 -4
- package/src/service/cli.ts +34 -3
- package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
- package/src/tray/windows-tray.ps1 +188 -7
- package/src/tray/windows.ts +23 -2
- package/src/types/config.ts +52 -12
- package/src/types/provider.ts +22 -5
- package/src/types/tools.ts +11 -5
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +109 -0
- package/src/update/badge.ts +11 -17
- package/src/update/check-types.ts +9 -0
- package/src/update/desktop-badge.ts +102 -0
- package/src/update/index.ts +48 -8
- package/src/update/install-detection.d.mts +29 -2
- package/src/update/install-detection.mjs +213 -7
- package/src/update/job.ts +17 -12
- package/src/update/notify.ts +16 -11
- package/src/update/pnpm-owner-worker.ts +12 -0
- package/src/update/refresh-scheduler.ts +141 -0
- package/src/usage/cost.ts +31 -4
- package/src/usage/expected-prices.ts +57 -0
- package/assets/download-linux.svg +0 -10
- package/assets/download-macos.svg +0 -10
- package/assets/download-windows.svg +0 -10
- package/gui/dist/assets/App-D5eN1TID.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-Bh_ErQDh.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +0 -1
|
@@ -274,6 +274,9 @@ export function createResponsesSendBudget(
|
|
|
274
274
|
noteAdapterRecoveryWithheld,
|
|
275
275
|
sendBudgetExhausted,
|
|
276
276
|
claimAmbiguousResend,
|
|
277
|
+
get ambiguousResendSpent(): boolean {
|
|
278
|
+
return isRequestExecutionBudget(sendBudget) && sendBudget.ambiguousResendSpent === true;
|
|
279
|
+
},
|
|
277
280
|
get pendingHopPermit(): SingleUseDispatchPermit | undefined {
|
|
278
281
|
return pendingHopPermit;
|
|
279
282
|
},
|
|
@@ -313,6 +316,7 @@ function adapterDispatchBudgetView(
|
|
|
313
316
|
get lastTargetKey(): string | undefined { return budget.lastTargetKey; },
|
|
314
317
|
remainingBaseSends: (cap: number): number => budget.remainingBaseSends(cap),
|
|
315
318
|
claimAmbiguousResend: (limit: number): boolean => budget.claimAmbiguousResend?.(limit) === true,
|
|
319
|
+
get ambiguousResendSpent(): boolean { return budget.ambiguousResendSpent === true; },
|
|
316
320
|
reserveDispatch(intent: DispatchIntent): DispatchDecision {
|
|
317
321
|
// A dispatch whose upstream state is unknown is refused on its own merits. A hop that
|
|
318
322
|
// already paid does not make an unsafe replay safe, so that check stays with the budget.
|
|
@@ -18,9 +18,9 @@ import type { AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
|
18
18
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
19
19
|
import { providerFetch } from "./fetch-helpers";
|
|
20
20
|
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
21
|
-
import type
|
|
21
|
+
import { normalizeDeclaredToolName, type AdapterEvent, type OcxProviderContinuationState } from "../../types";
|
|
22
22
|
import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
|
|
23
|
-
import { SendBudgetExhaustedError } from "../../lib/upstream-retry";
|
|
23
|
+
import { SendBudgetExhaustedError, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
24
24
|
import {
|
|
25
25
|
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
26
26
|
hasEligibleGenericOAuthFailoverTarget,
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { rememberResponseState } from "../../responses/state";
|
|
40
40
|
import { trackStreamLifetime } from "../lifecycle";
|
|
41
41
|
import { awaitThoughtSignatureDurability } from "../../responses/thought-signature-replay";
|
|
42
|
+
import { undeclaredToolCallMessage } from "../responses-undeclared-tool-guard";
|
|
42
43
|
|
|
43
44
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
44
45
|
export async function executeResponsesRunTurn(
|
|
@@ -366,6 +367,20 @@ export async function executeResponsesRunTurn(
|
|
|
366
367
|
};
|
|
367
368
|
|
|
368
369
|
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
370
|
+
const enforceDeclaredToolNames = inboundWire !== "chat" && inboundWire !== "anthropic";
|
|
371
|
+
const classifyUndeclaredFirstTool = (
|
|
372
|
+
event: AdapterEvent,
|
|
373
|
+
): Extract<AdapterEvent, { type: "error" }> | undefined => {
|
|
374
|
+
if (!enforceDeclaredToolNames || event.type !== "tool_call_start") return undefined;
|
|
375
|
+
const effectiveName = normalizeDeclaredToolName(event.name, declaredToolNames);
|
|
376
|
+
if (declaredToolNames.has(effectiveName)) return undefined;
|
|
377
|
+
return {
|
|
378
|
+
type: "error",
|
|
379
|
+
status: 502,
|
|
380
|
+
errorType: "upstream_error",
|
|
381
|
+
message: undeclaredToolCallMessage(effectiveName),
|
|
382
|
+
};
|
|
383
|
+
};
|
|
369
384
|
if (parsed.stream) {
|
|
370
385
|
void runTurn();
|
|
371
386
|
let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
|
|
@@ -375,12 +390,16 @@ export async function executeResponsesRunTurn(
|
|
|
375
390
|
eventSource = await preflightRunTurnFailover(eventSource);
|
|
376
391
|
}
|
|
377
392
|
if (options.comboAttempt) {
|
|
378
|
-
const preflight = await preflightAdapterEvents(eventSource);
|
|
393
|
+
const preflight = await preflightAdapterEvents(eventSource, classifyUndeclaredFirstTool);
|
|
379
394
|
if (preflight.error || preflight.empty) {
|
|
380
395
|
runTurnAbort.abort();
|
|
381
396
|
queue.close();
|
|
382
397
|
const message = preflight.error?.message ?? "Adapter ended before producing a response";
|
|
383
|
-
|
|
398
|
+
const failure = formatErrorResponse(502, "upstream_error", redactSecretString(message));
|
|
399
|
+
// A replay-unsafe heartbeat means the adapter already ran a local side effect, so the
|
|
400
|
+
// combo must not send this turn to another target: the failure stays with this child.
|
|
401
|
+
if (preflight.replayUnsafe) markResponseNonReplayable(failure);
|
|
402
|
+
return failure;
|
|
384
403
|
}
|
|
385
404
|
eventSource = preflight.stream;
|
|
386
405
|
}
|
|
@@ -412,7 +431,7 @@ export async function executeResponsesRunTurn(
|
|
|
412
431
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
413
432
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
414
433
|
declaredToolNames,
|
|
415
|
-
enforceDeclaredToolNames
|
|
434
|
+
enforceDeclaredToolNames,
|
|
416
435
|
toolParameterSchemas,
|
|
417
436
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
418
437
|
...(routedCompaction ? { compaction: true } : {}),
|
|
@@ -468,12 +487,24 @@ export async function executeResponsesRunTurn(
|
|
|
468
487
|
events = runTurnEvents;
|
|
469
488
|
}
|
|
470
489
|
if (options.comboAttempt) {
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
490
|
+
const firstMeaningfulIndex = events.findIndex(event => event.type !== "heartbeat");
|
|
491
|
+
const firstMeaningful = firstMeaningfulIndex === -1 ? undefined : events[firstMeaningfulIndex];
|
|
492
|
+
// Same boundary as the streaming preflight: a replay-unsafe heartbeat means the adapter
|
|
493
|
+
// already ran a local side effect, so an undeclared tool call after it keeps the bridge's
|
|
494
|
+
// fail-closed refusal instead of becoming a hop that sends the turn to another target.
|
|
495
|
+
const replayUnsafe = events
|
|
496
|
+
.slice(0, firstMeaningfulIndex === -1 ? events.length : firstMeaningfulIndex)
|
|
497
|
+
.some(event => event.type === "heartbeat" && event.replayUnsafe === true);
|
|
498
|
+
const classifiedError = firstMeaningful && !replayUnsafe
|
|
499
|
+
? classifyUndeclaredFirstTool(firstMeaningful)
|
|
500
|
+
: undefined;
|
|
501
|
+
if (!firstMeaningful || firstMeaningful.type === "error" || classifiedError) {
|
|
502
|
+
const message = classifiedError?.message ?? (firstMeaningful?.type === "error"
|
|
474
503
|
? firstMeaningful.message
|
|
475
|
-
: "Adapter ended before producing a response";
|
|
476
|
-
|
|
504
|
+
: "Adapter ended before producing a response");
|
|
505
|
+
const failure = formatErrorResponse(502, "upstream_error", redactSecretString(message));
|
|
506
|
+
if (replayUnsafe) markResponseNonReplayable(failure);
|
|
507
|
+
return failure;
|
|
477
508
|
}
|
|
478
509
|
}
|
|
479
510
|
let providerState: OcxProviderContinuationState | undefined;
|
|
@@ -483,7 +514,7 @@ export async function executeResponsesRunTurn(
|
|
|
483
514
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
484
515
|
toolNsMap,
|
|
485
516
|
declaredToolNames,
|
|
486
|
-
enforceDeclaredToolNames
|
|
517
|
+
enforceDeclaredToolNames,
|
|
487
518
|
toolParameterSchemas,
|
|
488
519
|
freeformToolNames,
|
|
489
520
|
toolSearchToolNames,
|
|
@@ -85,10 +85,11 @@ export function shouldUseCodexWsUpstream(
|
|
|
85
85
|
url: string,
|
|
86
86
|
init?: RequestInit,
|
|
87
87
|
runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
|
|
88
|
-
upstreamWebsocketConfigured
|
|
88
|
+
upstreamWebsocketConfigured?: boolean,
|
|
89
89
|
): boolean {
|
|
90
90
|
if (!bunSupportsBoundedCodexWsRelay(runtime)) return false;
|
|
91
91
|
if (socks5ProxyFromEnv()) return false;
|
|
92
|
+
if (url === CODEX_RESPONSES_HTTP_URL && upstreamWebsocketConfigured === false) return false;
|
|
92
93
|
// Bun's client WebSocket API delivers only fully assembled messages and has
|
|
93
94
|
// no enforceable inbound payload limit. Keep arbitrary provider endpoints on
|
|
94
95
|
// bounded HTTP/SSE until the client can reject fragmented text and binary
|
|
@@ -120,7 +120,6 @@ export function writeShellEnvFile(
|
|
|
120
120
|
const maxCtx = config.claudeCode?.maxContextTokens;
|
|
121
121
|
if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
|
|
122
122
|
lines.push(conditional("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx))));
|
|
123
|
-
lines.push(conditional("DISABLE_COMPACT", "1"));
|
|
124
123
|
}
|
|
125
124
|
// Auto-context (devlog 260712 020): same contract as `ocx claude` / launchctl.
|
|
126
125
|
const autoShell = auto ?? resolveAutoContext(config.claudeCode);
|
package/src/server/system-env.ts
CHANGED
|
@@ -282,9 +282,12 @@ export async function injectSystemEnv(
|
|
|
282
282
|
}
|
|
283
283
|
// Lever keys (devlog 136 B6): user-wins — skip any key the user already set in the
|
|
284
284
|
// launchd domain, and track ONLY the keys we actually injected so revert cannot
|
|
285
|
-
// delete a pre-existing user value (audit 139 #3).
|
|
285
|
+
// delete a pre-existing user value (audit 139 #3). A key we already track is ours
|
|
286
|
+
// (revertSystemEnv unsets it regardless of value), so it is refreshed, not skipped.
|
|
287
|
+
const producedLevers = new Set<string>();
|
|
286
288
|
const injectLever = (name: string, value: string) => {
|
|
287
|
-
|
|
289
|
+
producedLevers.add(name);
|
|
290
|
+
if (launchctlGetenv(name) !== undefined && !injectedKeys.includes(name)) return;
|
|
288
291
|
inject(name, value);
|
|
289
292
|
};
|
|
290
293
|
// Model slots (default + tier defaults + legacy small-fast) with [1m] auto-marking
|
|
@@ -293,7 +296,9 @@ export async function injectSystemEnv(
|
|
|
293
296
|
// Auto-context: a user-owned launchd value drives the marking predicate so the
|
|
294
297
|
// marker and threshold never separate (audit 021 #2); injectLever's user-wins
|
|
295
298
|
// check below keeps that value untouched.
|
|
296
|
-
const userAutoCompact =
|
|
299
|
+
const userAutoCompact = injectedKeys.includes("CLAUDE_CODE_AUTO_COMPACT_WINDOW")
|
|
300
|
+
? undefined
|
|
301
|
+
: launchctlGetenv("CLAUDE_CODE_AUTO_COMPACT_WINDOW");
|
|
297
302
|
const auto = resolveAutoContext(config.claudeCode, userAutoCompact);
|
|
298
303
|
const { modelEnv, windows } = await computeEffectiveModelEnv(config, auto);
|
|
299
304
|
for (const [name, value] of Object.entries(modelEnv)) {
|
|
@@ -303,7 +308,6 @@ export async function injectSystemEnv(
|
|
|
303
308
|
const maxCtx = config.claudeCode?.maxContextTokens;
|
|
304
309
|
if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
|
|
305
310
|
injectLever("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx)));
|
|
306
|
-
injectLever("DISABLE_COMPACT", "1");
|
|
307
311
|
}
|
|
308
312
|
// Auto-context (devlog 260712 020): user-wins lever, inert when maxContextTokens set.
|
|
309
313
|
if (auto.enabled) injectLever("CLAUDE_CODE_AUTO_COMPACT_WINDOW", String(auto.compactWindow));
|
|
@@ -315,6 +319,23 @@ export async function injectSystemEnv(
|
|
|
315
319
|
// instead of only terminal sessions. injectLever keeps a user-owned launchd value.
|
|
316
320
|
const toolSearch = claudeToolSearchEnv(config.claudeCode?.toolSearch);
|
|
317
321
|
if (toolSearch !== undefined) injectLever("ENABLE_TOOL_SEARCH", toolSearch);
|
|
322
|
+
// A lever injected on an earlier run that this config no longer produces (a cleared
|
|
323
|
+
// smallFastModel, a removed tier slot, or the DISABLE_COMPACT older releases paired with
|
|
324
|
+
// maxContextTokens) would otherwise stay in launchd until the proxy stops, and a
|
|
325
|
+
// same-port restart keeps the tracking record. Only tracked keys are touched, so a user-owned value is never removed.
|
|
326
|
+
for (const name of [...injectedKeys]) {
|
|
327
|
+
if ((SYSTEM_ENV_NAMES as readonly string[]).includes(name) || producedLevers.has(name)) continue;
|
|
328
|
+
// Older releases only ever injected DISABLE_COMPACT=1. Any other value was set by hand,
|
|
329
|
+
// so ownership is released without deleting it.
|
|
330
|
+
if (name === "DISABLE_COMPACT" && launchctlGetenv(name) !== "1") {
|
|
331
|
+
injectedKeys.splice(injectedKeys.indexOf(name), 1);
|
|
332
|
+
writeTracking(port, injectedKeys, tracked);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
unsetLaunchctlEnv(name);
|
|
336
|
+
injectedKeys.splice(injectedKeys.indexOf(name), 1);
|
|
337
|
+
writeTracking(port, injectedKeys, tracked);
|
|
338
|
+
}
|
|
318
339
|
|
|
319
340
|
// Shell-hook env file: works for new shells in already-running Terminal.app.
|
|
320
341
|
writeShellEnvFile(port, config, modelEnv, auto, deps);
|
package/src/service/cli.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { existsSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { restoreNativeCodexAsync } from "../codex/inject";
|
|
4
4
|
import { describeRetainedCodexProviderTable } from "../codex/inject/restore";
|
|
5
5
|
import { stripGrokConfig } from "../grok/inject";
|
|
6
|
-
import {
|
|
6
|
+
import { withConfigMutationLockSync } from "../config/mutation-lock";
|
|
7
|
+
import { withClientLifecycleSync, type ClientLifecycleLockDeps } from "../client/lifecycle-lock";
|
|
8
|
+
import { pendingClientConnectMayOwnToken, readClientConnectionState } from "../client/state";
|
|
9
|
+
import { readServiceApiTokenState, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
7
10
|
import { statusWinswRaw, type WinswStatus } from "../lib/winsw";
|
|
8
11
|
import { withWindowsServiceMutationLock } from "../lib/windows-service-mutation-lock";
|
|
9
12
|
import { maybeShowStarPrompt } from "../cli/star-prompt";
|
|
@@ -182,6 +185,32 @@ export function parseServiceArgs(args: string[]): ParsedServiceArgs {
|
|
|
182
185
|
return { sub: normalizeServiceSubcommand(sub), backend, invalid };
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
/** Remove the service credential only when no client connection can own it. */
|
|
189
|
+
export function removeServiceTokenAfterUninstall(
|
|
190
|
+
lockDeps: ClientLifecycleLockDeps = {},
|
|
191
|
+
): "removed" | "absent" | "retained" | "unverified" {
|
|
192
|
+
try {
|
|
193
|
+
return withClientLifecycleSync(() => withConfigMutationLockSync(() => {
|
|
194
|
+
const path = serviceApiTokenFilePath();
|
|
195
|
+
try { lstatSync(path); }
|
|
196
|
+
catch (error) {
|
|
197
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "absent";
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
if (readClientConnectionState().kind !== "disconnected") return "retained";
|
|
201
|
+
const token = readServiceApiTokenState();
|
|
202
|
+
if (token.kind !== "present") return token.kind === "absent" ? "absent" : "unverified";
|
|
203
|
+
if (pendingClientConnectMayOwnToken(token.fingerprint)) return "retained";
|
|
204
|
+
unlinkSync(path);
|
|
205
|
+
return "removed";
|
|
206
|
+
}), lockDeps);
|
|
207
|
+
} catch {
|
|
208
|
+
// Lock, state-read and unlink failures all leave cleanup unverified, not successful.
|
|
209
|
+
return "unverified";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Execute a service verb while preserving client-owned credentials during uninstall. */
|
|
185
214
|
export async function serviceCommand(...args: (string | undefined)[]): Promise<void> {
|
|
186
215
|
const filteredArgs = args.filter((a): a is string => Boolean(a));
|
|
187
216
|
const execute = async (): Promise<void> => {
|
|
@@ -424,7 +453,9 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
424
453
|
}
|
|
425
454
|
}
|
|
426
455
|
removeServiceInstallState();
|
|
427
|
-
|
|
456
|
+
const tokenCleanup = removeServiceTokenAfterUninstall();
|
|
457
|
+
if (tokenCleanup === "retained") console.warn("⚠️ Service token kept because client state may own it.");
|
|
458
|
+
else if (tokenCleanup === "unverified") console.warn("⚠️ Service token cleanup could not be verified; inspect client state before deleting it.");
|
|
428
459
|
console.log("✅ service uninstalled.");
|
|
429
460
|
break;
|
|
430
461
|
default:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -41,6 +41,9 @@ function Load-TrayIcon([string]$Name, [System.Drawing.Icon]$Fallback) {
|
|
|
41
41
|
$onlineIcon = Load-TrayIcon "opencodex-tray-online.ico" ([System.Drawing.SystemIcons]::Information)
|
|
42
42
|
$warningIcon = Load-TrayIcon "opencodex-tray-warning.ico" ([System.Drawing.SystemIcons]::Warning)
|
|
43
43
|
$offlineIcon = Load-TrayIcon "opencodex-tray-offline.ico" ([System.Drawing.SystemIcons]::Error)
|
|
44
|
+
$onlineUpdateIcon = Load-TrayIcon "opencodex-tray-online-update.ico" $onlineIcon
|
|
45
|
+
$warningUpdateIcon = Load-TrayIcon "opencodex-tray-warning-update.ico" $warningIcon
|
|
46
|
+
$offlineUpdateIcon = Load-TrayIcon "opencodex-tray-offline-update.ico" $offlineIcon
|
|
44
47
|
|
|
45
48
|
function Get-StableHash([string]$Value) {
|
|
46
49
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
@@ -86,6 +89,20 @@ function ConvertTo-NativeArgument([string]$Value) {
|
|
|
86
89
|
return '"' + $Value + '"'
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
# A fresh profile may not have created the default %USERPROFILE%.codex yet, and the
|
|
93
|
+
# CLI rejects a CODEX_HOME that does not exist. Only that default home is dropped so
|
|
94
|
+
# children resolve it themselves; any other home is passed through unchanged.
|
|
95
|
+
function Set-OcxChildEnvironment([System.Diagnostics.ProcessStartInfo]$StartInfo) {
|
|
96
|
+
$defaultHome = Normalize-HomePath (Join-Path $env:USERPROFILE ".codex")
|
|
97
|
+
$isDefaultHome = [string]::Equals($CodexHome, $defaultHome, [System.StringComparison]::OrdinalIgnoreCase)
|
|
98
|
+
if ($isDefaultHome -and -not [System.IO.Directory]::Exists($CodexHome)) {
|
|
99
|
+
$StartInfo.EnvironmentVariables.Remove("CODEX_HOME")
|
|
100
|
+
} else {
|
|
101
|
+
$StartInfo.EnvironmentVariables["CODEX_HOME"] = $CodexHome
|
|
102
|
+
}
|
|
103
|
+
$StartInfo.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
function Start-OcxCommand([string[]]$CommandArgs, [switch]$TrackExit) {
|
|
90
107
|
try {
|
|
91
108
|
$allArgs = @($CliPath) + $CommandArgs
|
|
@@ -95,8 +112,7 @@ function Start-OcxCommand([string[]]$CommandArgs, [switch]$TrackExit) {
|
|
|
95
112
|
$psi.UseShellExecute = $false
|
|
96
113
|
$psi.CreateNoWindow = $true
|
|
97
114
|
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
98
|
-
$psi
|
|
99
|
-
$psi.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome
|
|
115
|
+
Set-OcxChildEnvironment $psi
|
|
100
116
|
if ($BunRuntimeSource) {
|
|
101
117
|
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_SOURCE"] = $BunRuntimeSource
|
|
102
118
|
# Paired with the source so a later relaunch can tell the marker still describes
|
|
@@ -155,8 +171,7 @@ function Start-StartupHealthProbe {
|
|
|
155
171
|
$psi.RedirectStandardError = $true
|
|
156
172
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
|
|
157
173
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
|
|
158
|
-
$psi
|
|
159
|
-
$psi.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome
|
|
174
|
+
Set-OcxChildEnvironment $psi
|
|
160
175
|
if ($BunRuntimeSource) {
|
|
161
176
|
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_SOURCE"] = $BunRuntimeSource
|
|
162
177
|
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_PATH"] = $BunPath
|
|
@@ -198,6 +213,147 @@ function Complete-StartupHealthProbe {
|
|
|
198
213
|
}
|
|
199
214
|
}
|
|
200
215
|
|
|
216
|
+
function Initialize-UpdateBadgeReader {
|
|
217
|
+
if (([System.Management.Automation.PSTypeName]"TrayUpdateBadgeReader").Type) { return }
|
|
218
|
+
Add-Type -TypeDefinition @'
|
|
219
|
+
using System;
|
|
220
|
+
using System.IO;
|
|
221
|
+
using System.Text;
|
|
222
|
+
using System.Threading.Tasks;
|
|
223
|
+
public static class TrayUpdateBadgeReader {
|
|
224
|
+
public static async Task<string> ReadAsync(Stream input, int maxBytes) {
|
|
225
|
+
byte[] chunk = new byte[4096];
|
|
226
|
+
using (var content = new MemoryStream()) {
|
|
227
|
+
while (true) {
|
|
228
|
+
int count = await input.ReadAsync(chunk, 0, chunk.Length).ConfigureAwait(false);
|
|
229
|
+
if (count == 0) break;
|
|
230
|
+
if (content.Length + count > maxBytes) throw new InvalidDataException("badge pipe byte cap exceeded");
|
|
231
|
+
content.Write(chunk, 0, count);
|
|
232
|
+
}
|
|
233
|
+
return new UTF8Encoding(false, true).GetString(content.ToArray());
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
'@
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function Parse-UpdateBadgeText([string]$Text) {
|
|
241
|
+
$lines = @($Text -split "\r?\n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
242
|
+
if ($lines.Count -ne 1) { return $null }
|
|
243
|
+
try {
|
|
244
|
+
$badge = $lines[0] | ConvertFrom-Json
|
|
245
|
+
if (($badge.updateAvailable -isnot [bool]) -or ($badge.unknown -isnot [bool]) -or
|
|
246
|
+
($badge.canUpdate -isnot [bool])) { return $null }
|
|
247
|
+
return [bool]($badge.updateAvailable -and -not $badge.unknown -and $badge.canUpdate)
|
|
248
|
+
} catch { return $null }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function Complete-UpdateBadgeProbe {
|
|
252
|
+
$process = $script:updateBadgeProcess
|
|
253
|
+
$script:updateBadgeProcess = $null
|
|
254
|
+
$script:updateBadgeOutputTask = $null
|
|
255
|
+
$script:updateBadgeErrorTask = $null
|
|
256
|
+
$script:updateBadgeTerminating = $false
|
|
257
|
+
if ($null -ne $process) {
|
|
258
|
+
try {
|
|
259
|
+
$process.StandardOutput.Dispose()
|
|
260
|
+
$process.StandardError.Dispose()
|
|
261
|
+
$process.Dispose()
|
|
262
|
+
} catch { Write-ActionLog "update badge probe dispose failed: $($_.Exception.GetType().Name)" }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function Stop-UpdateBadgeProbe([switch]$Shutdown) {
|
|
267
|
+
if ($null -eq $script:updateBadgeProcess) { return }
|
|
268
|
+
$script:updateBadgeTerminating = $true
|
|
269
|
+
try {
|
|
270
|
+
if (-not $script:updateBadgeProcess.HasExited) { $script:updateBadgeProcess.Kill() }
|
|
271
|
+
} catch { Write-ActionLog "update badge probe termination failed: $($_.Exception.GetType().Name)" }
|
|
272
|
+
if ($Shutdown) {
|
|
273
|
+
# Only shutdown may wait, and it is bounded; the UI tick never calls this branch.
|
|
274
|
+
try { [void]$script:updateBadgeProcess.WaitForExit(500) } catch { $null = $_ }
|
|
275
|
+
Complete-UpdateBadgeProbe
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function Start-UpdateBadgeProbe {
|
|
280
|
+
try {
|
|
281
|
+
Initialize-UpdateBadgeReader
|
|
282
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
283
|
+
$psi.FileName = $BunPath
|
|
284
|
+
$psi.Arguments = ((@($CliPath, "__update-badge") | ForEach-Object { ConvertTo-NativeArgument $_ }) -join " ")
|
|
285
|
+
$psi.UseShellExecute = $false
|
|
286
|
+
$psi.CreateNoWindow = $true
|
|
287
|
+
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
288
|
+
$psi.RedirectStandardOutput = $true
|
|
289
|
+
$psi.RedirectStandardError = $true
|
|
290
|
+
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
|
|
291
|
+
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
|
|
292
|
+
Set-OcxChildEnvironment $psi
|
|
293
|
+
if ($BunRuntimeSource) {
|
|
294
|
+
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_SOURCE"] = $BunRuntimeSource
|
|
295
|
+
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_PATH"] = $BunPath
|
|
296
|
+
}
|
|
297
|
+
$script:updateBadgeProcess = [System.Diagnostics.Process]::Start($psi)
|
|
298
|
+
if ($null -eq $script:updateBadgeProcess) { throw "Process did not start" }
|
|
299
|
+
$script:updateBadgeOutputTask = [TrayUpdateBadgeReader]::ReadAsync($script:updateBadgeProcess.StandardOutput.BaseStream, $script:updateBadgeMaxBytesPerStream)
|
|
300
|
+
$script:updateBadgeErrorTask = [TrayUpdateBadgeReader]::ReadAsync($script:updateBadgeProcess.StandardError.BaseStream, $script:updateBadgeMaxBytesPerStream)
|
|
301
|
+
$script:updateBadgeStarted = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
302
|
+
} catch {
|
|
303
|
+
Write-ActionLog "update badge probe launch failed: $($_.Exception.GetType().Name)"
|
|
304
|
+
if ($null -ne $script:updateBadgeProcess) { Stop-UpdateBadgeProbe }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function Maintain-UpdateBadgeProbe([long]$Now) {
|
|
309
|
+
if ($null -ne $script:updateBadgeProcess) {
|
|
310
|
+
$terminatingAtStart = $script:updateBadgeTerminating
|
|
311
|
+
$expired = ($Now - $script:updateBadgeStarted) -gt $script:updateBadgeTimeoutMs
|
|
312
|
+
$exited = $false
|
|
313
|
+
try { $exited = $script:updateBadgeProcess.HasExited } catch { Stop-UpdateBadgeProbe }
|
|
314
|
+
$drained = ($null -eq $script:updateBadgeOutputTask -or $script:updateBadgeOutputTask.IsCompleted) -and
|
|
315
|
+
($null -eq $script:updateBadgeErrorTask -or $script:updateBadgeErrorTask.IsCompleted)
|
|
316
|
+
$failedRead = ($null -ne $script:updateBadgeOutputTask -and
|
|
317
|
+
($script:updateBadgeOutputTask.IsFaulted -or $script:updateBadgeOutputTask.IsCanceled)) -or
|
|
318
|
+
($null -ne $script:updateBadgeErrorTask -and
|
|
319
|
+
($script:updateBadgeErrorTask.IsFaulted -or $script:updateBadgeErrorTask.IsCanceled))
|
|
320
|
+
if ($failedRead -and -not $script:updateBadgeTerminating) {
|
|
321
|
+
Write-ActionLog "update badge probe pipe read failed or exceeded byte cap"
|
|
322
|
+
Stop-UpdateBadgeProbe
|
|
323
|
+
}
|
|
324
|
+
if ($script:updateBadgeTerminating) {
|
|
325
|
+
# Reap on a later tick after both bounded readers settle; no replacement starts meanwhile.
|
|
326
|
+
if ($terminatingAtStart -and -not $exited) { Stop-UpdateBadgeProbe }
|
|
327
|
+
if ($terminatingAtStart -and $exited -and $drained) { Complete-UpdateBadgeProbe }
|
|
328
|
+
} elseif ($exited -and $drained -and $null -ne $script:updateBadgeOutputTask -and
|
|
329
|
+
$null -ne $script:updateBadgeErrorTask) {
|
|
330
|
+
$answer = $null
|
|
331
|
+
try {
|
|
332
|
+
if ($script:updateBadgeProcess.ExitCode -eq 0) {
|
|
333
|
+
$answer = Parse-UpdateBadgeText $script:updateBadgeOutputTask.Result
|
|
334
|
+
}
|
|
335
|
+
} catch { Write-ActionLog "update badge probe read failed: $($_.Exception.GetType().Name)" }
|
|
336
|
+
if ($null -ne $answer) {
|
|
337
|
+
$script:updateAvailable = [bool]$answer
|
|
338
|
+
$script:updateBadgeObservedAt = $Now
|
|
339
|
+
}
|
|
340
|
+
Complete-UpdateBadgeProbe
|
|
341
|
+
} elseif ($expired) {
|
|
342
|
+
Write-ActionLog "update badge probe timed out"
|
|
343
|
+
Stop-UpdateBadgeProbe
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if ($script:updateBadgeObservedAt -eq 0 -or
|
|
347
|
+
($Now - $script:updateBadgeObservedAt) -gt $script:updateBadgeExpiryMs) {
|
|
348
|
+
$script:updateAvailable = $false
|
|
349
|
+
}
|
|
350
|
+
if ($null -eq $script:updateBadgeProcess -and -not $script:updateBadgeTerminating -and
|
|
351
|
+
($script:updateBadgeAttemptAt -eq 0 -or ($Now - $script:updateBadgeAttemptAt) -ge $script:updateBadgeRefreshMs)) {
|
|
352
|
+
$script:updateBadgeAttemptAt = $Now
|
|
353
|
+
Start-UpdateBadgeProbe
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
201
357
|
function Read-ListenTarget {
|
|
202
358
|
foreach ($path in @((Join-Path $OpenCodexHome "runtime-port.json"), (Join-Path $OpenCodexHome "config.json"))) {
|
|
203
359
|
try {
|
|
@@ -241,6 +397,9 @@ $statusItem.Enabled = $false
|
|
|
241
397
|
$safetyItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
242
398
|
$safetyItem.Enabled = $false
|
|
243
399
|
$openItem = $menu.Items.Add("Open Dashboard")
|
|
400
|
+
$updateItem = $menu.Items.Add("Update available")
|
|
401
|
+
$updateItem.Visible = $false
|
|
402
|
+
$updateItem.Enabled = $false
|
|
244
403
|
$startItem = $menu.Items.Add("Start Proxy")
|
|
245
404
|
$stopItem = $menu.Items.Add("Stop Proxy and Restore Native Routing")
|
|
246
405
|
$restartItem = $menu.Items.Add("Restart Proxy")
|
|
@@ -263,6 +422,18 @@ $script:startupProbeOutputTask = $null
|
|
|
263
422
|
$script:startupProbeErrorTask = $null
|
|
264
423
|
$script:startupProbeStarted = 0L
|
|
265
424
|
$script:startupProbeTimeoutMs = 30000L
|
|
425
|
+
$script:updateAvailable = $false
|
|
426
|
+
$script:updateBadgeObservedAt = 0L
|
|
427
|
+
$script:updateBadgeAttemptAt = 0L
|
|
428
|
+
$script:updateBadgeRefreshMs = 60000L
|
|
429
|
+
$script:updateBadgeExpiryMs = 180000L
|
|
430
|
+
$script:updateBadgeTimeoutMs = 12000L
|
|
431
|
+
$script:updateBadgeMaxBytesPerStream = 16384
|
|
432
|
+
$script:updateBadgeProcess = $null
|
|
433
|
+
$script:updateBadgeOutputTask = $null
|
|
434
|
+
$script:updateBadgeErrorTask = $null
|
|
435
|
+
$script:updateBadgeStarted = 0L
|
|
436
|
+
$script:updateBadgeTerminating = $false
|
|
266
437
|
$script:pendingAction = $null
|
|
267
438
|
$script:pendingStarted = 0L
|
|
268
439
|
$script:pendingDeadline = 0L
|
|
@@ -322,6 +493,7 @@ function Update-TrayState {
|
|
|
322
493
|
$cameOnline = $script:online -and -not $script:wasOnline
|
|
323
494
|
$script:wasOnline = $script:online
|
|
324
495
|
$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
496
|
+
Maintain-UpdateBadgeProbe $now
|
|
325
497
|
if ($null -ne $script:startupProbeProcess) {
|
|
326
498
|
$probeExited = $false
|
|
327
499
|
try {
|
|
@@ -377,20 +549,26 @@ function Update-TrayState {
|
|
|
377
549
|
if ($null -ne $startup) {
|
|
378
550
|
$label = if ($startup.status -eq "at-risk") { "At risk" } elseif ($startup.status -eq "protected") { "Protected" } else { "Native routing" }
|
|
379
551
|
$safetyItem.Text = "Restart safety: $label"
|
|
380
|
-
$notify.Icon = if ($startup.status -eq "at-risk") {
|
|
552
|
+
$notify.Icon = if ($startup.status -eq "at-risk") {
|
|
553
|
+
if ($script:updateAvailable) { $warningUpdateIcon } else { $warningIcon }
|
|
554
|
+
} else {
|
|
555
|
+
if ($script:updateAvailable) { $onlineUpdateIcon } else { $onlineIcon }
|
|
556
|
+
}
|
|
381
557
|
} else {
|
|
382
558
|
$safetyItem.Text = "Restart safety: unavailable"
|
|
383
|
-
$notify.Icon = $warningIcon
|
|
559
|
+
$notify.Icon = if ($script:updateAvailable) { $warningUpdateIcon } else { $warningIcon }
|
|
384
560
|
}
|
|
385
561
|
} else {
|
|
386
562
|
$statusItem.Text = "Proxy: Offline"
|
|
387
563
|
$safetyItem.Text = "Restart safety: start the proxy to inspect"
|
|
388
564
|
$notify.Text = "opencodex: Offline"
|
|
389
|
-
$notify.Icon = $offlineIcon
|
|
565
|
+
$notify.Icon = if ($script:updateAvailable) { $offlineUpdateIcon } else { $offlineIcon }
|
|
390
566
|
$startItem.Enabled = $true
|
|
391
567
|
$stopItem.Enabled = $false
|
|
392
568
|
$restartItem.Enabled = $false
|
|
393
569
|
}
|
|
570
|
+
$updateItem.Visible = $script:updateAvailable
|
|
571
|
+
$updateItem.Enabled = $script:updateAvailable
|
|
394
572
|
if ($null -ne $script:pendingAction) {
|
|
395
573
|
$startItem.Enabled = $false
|
|
396
574
|
$stopItem.Enabled = $false
|
|
@@ -426,6 +604,7 @@ function Update-TrayState {
|
|
|
426
604
|
}
|
|
427
605
|
|
|
428
606
|
$openItem.add_Click({ Start-OcxCommand @("gui") })
|
|
607
|
+
$updateItem.add_Click({ Start-OcxCommand @("gui") })
|
|
429
608
|
$startItem.add_Click({
|
|
430
609
|
if (-not (Set-PendingAction "Start Proxy" 75)) { return }
|
|
431
610
|
$statusItem.Text = "Proxy: Starting..."
|
|
@@ -489,6 +668,7 @@ $notify.Visible = $true
|
|
|
489
668
|
$notify.Text = "opencodex: Checking..."
|
|
490
669
|
|
|
491
670
|
try {
|
|
671
|
+
Initialize-UpdateBadgeReader
|
|
492
672
|
Update-TrayState
|
|
493
673
|
$timer.Start()
|
|
494
674
|
[System.Windows.Forms.Application]::Run()
|
|
@@ -512,6 +692,7 @@ try {
|
|
|
512
692
|
}
|
|
513
693
|
Complete-StartupHealthProbe
|
|
514
694
|
}
|
|
695
|
+
Stop-UpdateBadgeProbe -Shutdown
|
|
515
696
|
$notify.Dispose()
|
|
516
697
|
foreach ($icon in $script:ownedIcons) { $icon.Dispose() }
|
|
517
698
|
$menu.Dispose()
|
package/src/tray/windows.ts
CHANGED
|
@@ -19,6 +19,9 @@ const TRAY_ICON_FILES = [
|
|
|
19
19
|
"opencodex-tray-online.ico",
|
|
20
20
|
"opencodex-tray-warning.ico",
|
|
21
21
|
"opencodex-tray-offline.ico",
|
|
22
|
+
"opencodex-tray-online-update.ico",
|
|
23
|
+
"opencodex-tray-warning-update.ico",
|
|
24
|
+
"opencodex-tray-offline-update.ico",
|
|
22
25
|
] as const;
|
|
23
26
|
|
|
24
27
|
export interface WindowsTrayEntry {
|
|
@@ -68,6 +71,25 @@ function installedTrayIconPaths(): string[] {
|
|
|
68
71
|
return TRAY_ICON_FILES.map(name => join(getConfigDir(), name));
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Files an installed tray must still have for its registration to count as ours.
|
|
76
|
+
*
|
|
77
|
+
* The dotted update icons arrived after the tray shipped, so an install made by an older
|
|
78
|
+
* release has only the three base icons. Requiring all six here classified that install as
|
|
79
|
+
* stale, and the updater then stopped the tray without reinstalling it (trayWasInstalled was
|
|
80
|
+
* false). The dotted icons stay in the install, rollback and uninstall lists; the tray script
|
|
81
|
+
* falls back to the base icon when one is missing, and the next `tray install` adds them.
|
|
82
|
+
*/
|
|
83
|
+
export function windowsTrayRequiredFilesPresent(
|
|
84
|
+
state: Pick<WindowsTrayEntry, "bun" | "cli" | "script"> & { launcherPath?: string },
|
|
85
|
+
iconPaths: readonly string[],
|
|
86
|
+
exists: (path: string) => boolean = existsSync,
|
|
87
|
+
): boolean {
|
|
88
|
+
const baseIcons = iconPaths.filter(path => !/-update\.ico$/i.test(path));
|
|
89
|
+
return [state.bun, state.cli, state.script, ...(state.launcherPath ? [state.launcherPath] : []), ...baseIcons]
|
|
90
|
+
.every(path => exists(path));
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
export function windowsTrayStatePathsOwned(
|
|
72
94
|
state: Pick<WindowsTrayEntry, "script" | "opencodexHome"> & { launcherPath?: string },
|
|
73
95
|
configDir = getConfigDir(),
|
|
@@ -469,8 +491,7 @@ function trayStatusFrom(registered: string | null): WindowsTrayStatus {
|
|
|
469
491
|
const running = heartbeatProcessAlive(heartbeat);
|
|
470
492
|
const registrationOwned = state !== null
|
|
471
493
|
&& registered === state.runCommand
|
|
472
|
-
&&
|
|
473
|
-
.every(path => existsSync(path));
|
|
494
|
+
&& windowsTrayRequiredFilesPresent(state, installedTrayIconPaths());
|
|
474
495
|
const stale = windowsTrayRegistrationIsStale({
|
|
475
496
|
registered: registered !== null,
|
|
476
497
|
registrationOwned,
|