@bitkyc08/opencodex 2.81.0 → 2.83.0
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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
1
|
+
import { closeSync, fsyncSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { atomicWriteFileStreamed } from "../config/atomic-write";
|
|
4
|
+
import { withConfigMutationLockSync } from "../config/mutation-lock";
|
|
4
5
|
import { getConfigDir } from "../config/paths";
|
|
5
6
|
|
|
6
|
-
export type GrokResetCouponOperationKind = "execute" | "replay" | "identity-mismatch" | "capacity";
|
|
7
|
+
export type GrokResetCouponOperationKind = "execute" | "replay" | "identity-mismatch" | "token-mismatch" | "capacity";
|
|
7
8
|
|
|
8
9
|
export interface GrokResetCouponOperationIdentity {
|
|
9
10
|
accountId: string;
|
|
@@ -16,6 +17,12 @@ export interface GrokResetCouponOperationRecord {
|
|
|
16
17
|
operationId: string;
|
|
17
18
|
accountId?: string;
|
|
18
19
|
tokenId?: string;
|
|
20
|
+
/** Token expiry (ms) captured at claim time as journal metadata. Remaining
|
|
21
|
+
* availability or expiry cannot establish this operation's outcome. */
|
|
22
|
+
tokenValidityEnd?: number;
|
|
23
|
+
/** When the attempt was marked (ms) — set only on an "attempted" replay so
|
|
24
|
+
* the route can defer inspection while the attempt may still be in flight. */
|
|
25
|
+
attemptedAt?: number;
|
|
19
26
|
code?: string;
|
|
20
27
|
settledAt?: number;
|
|
21
28
|
}
|
|
@@ -23,14 +30,18 @@ export interface GrokResetCouponOperationRecord {
|
|
|
23
30
|
interface GrokResetCouponOperationState {
|
|
24
31
|
accountId: string;
|
|
25
32
|
tokenId?: string;
|
|
26
|
-
|
|
33
|
+
// "attempted" sits between open and settled: the spend call may have fired,
|
|
34
|
+
// so the operation must never execute again. Its replay carries no code —
|
|
35
|
+
// the route reconciles it against upstream instead of trusting a status.
|
|
36
|
+
status: "open" | "attempted" | "settled" | "failed";
|
|
27
37
|
code?: string;
|
|
38
|
+
tokenValidityEnd?: number;
|
|
28
39
|
createdAt: number;
|
|
29
40
|
updatedAt: number;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
interface GrokResetCouponLedger {
|
|
33
|
-
version:
|
|
44
|
+
version: 2;
|
|
34
45
|
operations: Record<string, GrokResetCouponOperationState>;
|
|
35
46
|
}
|
|
36
47
|
|
|
@@ -40,95 +51,249 @@ export function grokCouponJournalPath(customDir?: string): string {
|
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
function readGrokCouponLedger(filePath: string): GrokResetCouponLedger {
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
let raw: string;
|
|
55
|
+
try { raw = readFileSync(filePath, "utf-8"); }
|
|
56
|
+
catch (error) {
|
|
57
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { version: 2, operations: {} };
|
|
58
|
+
throw new Error("Grok coupon ledger is unavailable; existing records were preserved.");
|
|
45
59
|
}
|
|
46
60
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
const parsed = JSON.parse(raw) as Omit<GrokResetCouponLedger, "version"> & { version: unknown };
|
|
62
|
+
if (!parsed || (parsed.version !== 1 && parsed.version !== 2) || !parsed.operations
|
|
63
|
+
|| typeof parsed.operations !== "object" || Array.isArray(parsed.operations)) throw new Error();
|
|
64
|
+
for (const op of Object.values(parsed.operations)) {
|
|
65
|
+
if (!op || typeof op.accountId !== "string" || !op.accountId
|
|
66
|
+
|| !["open", "attempted", "settled", "failed"].includes(op.status)
|
|
67
|
+
|| !Number.isFinite(op.createdAt) || !Number.isFinite(op.updatedAt)
|
|
68
|
+
|| (op.tokenId !== undefined && typeof op.tokenId !== "string")
|
|
69
|
+
|| (op.code !== undefined && typeof op.code !== "string")
|
|
70
|
+
|| (op.tokenValidityEnd !== undefined && !Number.isFinite(op.tokenValidityEnd))) throw new Error();
|
|
71
|
+
}
|
|
72
|
+
if (parsed.version === 1) {
|
|
73
|
+
for (const op of Object.values(parsed.operations)) {
|
|
74
|
+
// Old opens (and transport failures recorded as failed) may already
|
|
75
|
+
// have dispatched. Only version-two opens prove pre-dispatch state.
|
|
76
|
+
if (op.status === "open" || (op.status === "failed" && op.code === "redeem_failed")) {
|
|
77
|
+
op.status = "attempted";
|
|
78
|
+
delete op.code;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { version: 2, operations: parsed.operations };
|
|
52
83
|
} catch {
|
|
53
|
-
|
|
84
|
+
throw new Error("Grok coupon ledger is unreadable; existing records were preserved.");
|
|
54
85
|
}
|
|
55
86
|
}
|
|
56
87
|
|
|
57
88
|
function writeGrokCouponLedger(filePath: string, ledger: GrokResetCouponLedger, now = Date.now()): void {
|
|
58
|
-
// Prune
|
|
89
|
+
// Prune every record past the 30-day window — including "open" and
|
|
90
|
+
// "attempted". Idempotency retention is explicitly bounded to 30 days;
|
|
91
|
+
// operations are not retry authorities after that window.
|
|
59
92
|
const retentionCutoff = now - 30 * 24 * 60 * 60_000;
|
|
60
93
|
ledger.operations = Object.fromEntries(
|
|
61
|
-
Object.entries(ledger.operations).filter(
|
|
62
|
-
([, op]) => op.status === "open" || op.updatedAt > retentionCutoff,
|
|
63
|
-
),
|
|
94
|
+
Object.entries(ledger.operations).filter(([, op]) => op.updatedAt > retentionCutoff),
|
|
64
95
|
);
|
|
65
|
-
|
|
96
|
+
atomicWriteFileStreamed(filePath, descriptor => writeFileSync(descriptor, JSON.stringify(ledger, null, 2)), {
|
|
97
|
+
afterRename: target => {
|
|
98
|
+
// The shared streamed writer flushes the temp. A spend claim must also
|
|
99
|
+
// refuse on a POSIX directory-sync failure before dispatch, rather than
|
|
100
|
+
// relying on that writer's best-effort parent sync. Windows has no
|
|
101
|
+
// portable directory descriptor and retains the writer's platform contract.
|
|
102
|
+
if (process.platform === "win32") return;
|
|
103
|
+
const descriptor = openSync(dirname(target), "r");
|
|
104
|
+
try { fsyncSync(descriptor); }
|
|
105
|
+
finally { closeSync(descriptor); }
|
|
106
|
+
},
|
|
107
|
+
});
|
|
66
108
|
}
|
|
67
109
|
|
|
68
110
|
const MAX_GROK_RESET_COUPON_OPERATION_IDS = 256;
|
|
69
111
|
|
|
112
|
+
/** Resolve an omitted-account retry from its durable identity, within retention. */
|
|
113
|
+
export function getGrokResetCouponOperationAccountId(
|
|
114
|
+
operationId: string,
|
|
115
|
+
now = Date.now(),
|
|
116
|
+
journalPath?: string,
|
|
117
|
+
): string | undefined {
|
|
118
|
+
return withConfigMutationLockSync(() => {
|
|
119
|
+
const record = readGrokCouponLedger(journalPath ?? grokCouponJournalPath()).operations[operationId];
|
|
120
|
+
if (!record || record.updatedAt <= now - 30 * 24 * 60 * 60_000) return undefined;
|
|
121
|
+
return record.accountId;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
70
125
|
export function openGrokResetCouponOperation(
|
|
71
126
|
identity: GrokResetCouponOperationIdentity,
|
|
72
127
|
now = Date.now(),
|
|
73
128
|
journalPath?: string,
|
|
74
129
|
): GrokResetCouponOperationRecord {
|
|
75
|
-
|
|
76
|
-
|
|
130
|
+
// Read→decide→write under the shared config mutation lock: two concurrent
|
|
131
|
+
// coupon requests on the same home must not lose an operation record — the
|
|
132
|
+
// ledger exists precisely to make an irreversible spend replay-safe.
|
|
133
|
+
return withConfigMutationLockSync(() => {
|
|
134
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
135
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
77
136
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
137
|
+
// Prune BEFORE the capacity check: expiry is enforced on write, so a ledger
|
|
138
|
+
// full of stale records would otherwise reject every new operation forever
|
|
139
|
+
// (the prune path inside writeGrokCouponLedger is never reached when all
|
|
140
|
+
// openings are rejected).
|
|
141
|
+
const retentionCutoff = now - 30 * 24 * 60 * 60_000;
|
|
142
|
+
ledger.operations = Object.fromEntries(
|
|
143
|
+
Object.entries(ledger.operations).filter(([, op]) => op.updatedAt > retentionCutoff),
|
|
144
|
+
);
|
|
81
145
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
146
|
+
const existing = ledger.operations[identity.operationId];
|
|
147
|
+
if (existing) {
|
|
148
|
+
if (existing.accountId !== identity.accountId) {
|
|
149
|
+
return { kind: "identity-mismatch", operationId: identity.operationId };
|
|
150
|
+
}
|
|
151
|
+
if (identity.tokenId !== undefined && existing.tokenId !== undefined && existing.tokenId !== identity.tokenId) {
|
|
152
|
+
return { kind: "token-mismatch", operationId: identity.operationId };
|
|
153
|
+
}
|
|
154
|
+
if (existing.status !== "open") {
|
|
155
|
+
// Non-open: replay the recorded outcome. "attempted" records carry no
|
|
156
|
+
// code — the caller must reconcile them against upstream, never re-run.
|
|
157
|
+
return {
|
|
158
|
+
kind: "replay",
|
|
159
|
+
operationId: identity.operationId,
|
|
160
|
+
accountId: existing.accountId,
|
|
161
|
+
tokenId: existing.tokenId,
|
|
162
|
+
tokenValidityEnd: existing.tokenValidityEnd,
|
|
163
|
+
attemptedAt: existing.status === "attempted" ? existing.updatedAt : undefined,
|
|
164
|
+
code: existing.code,
|
|
165
|
+
settledAt: existing.updatedAt,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
90
168
|
return {
|
|
91
|
-
kind: "
|
|
169
|
+
kind: "execute",
|
|
92
170
|
operationId: identity.operationId,
|
|
93
171
|
accountId: existing.accountId,
|
|
94
172
|
tokenId: existing.tokenId,
|
|
95
|
-
code: existing.code,
|
|
96
|
-
settledAt: existing.updatedAt,
|
|
97
173
|
};
|
|
98
174
|
}
|
|
175
|
+
|
|
176
|
+
if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) {
|
|
177
|
+
return { kind: "capacity", operationId: identity.operationId };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ledger.operations[identity.operationId] = {
|
|
181
|
+
accountId: identity.accountId,
|
|
182
|
+
...(identity.tokenId === undefined ? {} : { tokenId: identity.tokenId }),
|
|
183
|
+
status: "open",
|
|
184
|
+
createdAt: now,
|
|
185
|
+
updatedAt: now,
|
|
186
|
+
};
|
|
187
|
+
writeGrokCouponLedger(filePath, ledger, now);
|
|
99
188
|
return {
|
|
100
189
|
kind: "execute",
|
|
101
190
|
operationId: identity.operationId,
|
|
102
|
-
accountId:
|
|
103
|
-
tokenId:
|
|
191
|
+
accountId: identity.accountId,
|
|
192
|
+
tokenId: identity.tokenId,
|
|
104
193
|
};
|
|
105
|
-
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
106
196
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Flip an open operation to `attempted` BEFORE the upstream spend call. The
|
|
199
|
+
* write happens while nothing is irreversible yet: if it throws, the caller
|
|
200
|
+
* aborts without spending; if a later settle write dies instead, the
|
|
201
|
+
* operation still refuses blind re-execution — the next open returns a
|
|
202
|
+
* code-less replay the route resolves via the upstream remaining-resets list.
|
|
203
|
+
*/
|
|
204
|
+
export function markGrokResetCouponAttempt(
|
|
205
|
+
operationId: string,
|
|
206
|
+
tokenId: string,
|
|
207
|
+
now = Date.now(),
|
|
208
|
+
journalPath?: string,
|
|
209
|
+
tokenValidityEnd?: number,
|
|
210
|
+
): boolean {
|
|
211
|
+
return withConfigMutationLockSync(() => {
|
|
212
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
213
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
214
|
+
const existing = ledger.operations[operationId];
|
|
215
|
+
if (!existing || existing.status !== "open"
|
|
216
|
+
|| (existing.tokenId !== undefined && existing.tokenId !== tokenId)) return false;
|
|
217
|
+
existing.status = "attempted";
|
|
218
|
+
existing.tokenId = tokenId;
|
|
219
|
+
if (tokenValidityEnd !== undefined) existing.tokenValidityEnd = tokenValidityEnd;
|
|
220
|
+
existing.updatedAt = now;
|
|
221
|
+
writeGrokCouponLedger(filePath, ledger, now);
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
type GrokResetCouponSettlement = { operationId: string; accountId: string; code: string } & (
|
|
227
|
+
| { tokenId?: string; status: "failed"; expectedStatus: "open" }
|
|
228
|
+
| { tokenId: string; status: "success"; expectedStatus: "attempted" }
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
type GrokResetCouponPreflightResult =
|
|
232
|
+
| { kind: "recorded" | "changed" }
|
|
233
|
+
| { kind: "replay"; code: string; tokenId?: string; settledAt: number };
|
|
234
|
+
|
|
235
|
+
const UNRESOLVED_COUPON_CODES = new Set(["redeem_failed", "attempt_unresolved", "attempt_in_progress", "attempt_reconcile_failed", "operation_state_changed"]);
|
|
236
|
+
|
|
237
|
+
/** The durable definitive outcome for this account and token, if the operation has one. */
|
|
238
|
+
function terminalReplay(
|
|
239
|
+
ledger: GrokResetCouponLedger, operationId: string, accountId: string, tokenId: string | undefined,
|
|
240
|
+
): Extract<GrokResetCouponPreflightResult, { kind: "replay" }> | null {
|
|
241
|
+
const existing = ledger.operations[operationId];
|
|
242
|
+
if (!existing || existing.accountId !== accountId) return null;
|
|
243
|
+
if (tokenId !== undefined && existing.tokenId !== undefined && existing.tokenId !== tokenId) return null;
|
|
244
|
+
if ((existing.status !== "settled" && existing.status !== "failed") || !existing.code || UNRESOLVED_COUPON_CODES.has(existing.code)) return null;
|
|
245
|
+
return { kind: "replay", code: existing.code, tokenId: existing.tokenId, settledAt: existing.updatedAt };
|
|
121
246
|
}
|
|
122
247
|
|
|
248
|
+
/** Settle a refusal or inspect its winner without releasing the mutation transaction. */
|
|
249
|
+
export function settleGrokResetCouponPreflightRefusal(
|
|
250
|
+
settlement: Extract<GrokResetCouponSettlement, { status: "failed" }>,
|
|
251
|
+
now = Date.now(),
|
|
252
|
+
journalPath?: string,
|
|
253
|
+
): GrokResetCouponPreflightResult {
|
|
254
|
+
return withConfigMutationLockSync(() => {
|
|
255
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
256
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
257
|
+
if (recordSettlement(ledger, filePath, settlement, now)) return { kind: "recorded" };
|
|
258
|
+
return terminalReplay(ledger, settlement.operationId, settlement.accountId, settlement.tokenId) ?? { kind: "changed" };
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* After a lost claim: the definitive winner to replay, or null while the
|
|
264
|
+
* operation is still unresolved. Terminal states never revert, so reading after
|
|
265
|
+
* the failed claim cannot replay an outcome that is later replaced.
|
|
266
|
+
*/
|
|
267
|
+
export function readGrokResetCouponTerminalReplay(
|
|
268
|
+
operationId: string, accountId: string, tokenId?: string, journalPath?: string,
|
|
269
|
+
): Extract<GrokResetCouponPreflightResult, { kind: "replay" }> | null {
|
|
270
|
+
return withConfigMutationLockSync(() => {
|
|
271
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
272
|
+
return terminalReplay(readGrokCouponLedger(filePath), operationId, accountId, tokenId);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** A preflight refusal cannot replace a claim or a terminal result. */
|
|
123
277
|
export function recordGrokResetCouponSettlement(
|
|
124
|
-
settlement:
|
|
278
|
+
settlement: GrokResetCouponSettlement,
|
|
125
279
|
now = Date.now(),
|
|
126
280
|
journalPath?: string,
|
|
127
|
-
):
|
|
128
|
-
|
|
129
|
-
|
|
281
|
+
): boolean {
|
|
282
|
+
return withConfigMutationLockSync(() => {
|
|
283
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
284
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
285
|
+
return recordSettlement(ledger, filePath, settlement, now);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function recordSettlement(
|
|
290
|
+
ledger: GrokResetCouponLedger, filePath: string, settlement: GrokResetCouponSettlement, now: number,
|
|
291
|
+
): boolean {
|
|
130
292
|
const existing = ledger.operations[settlement.operationId];
|
|
131
|
-
if (!existing
|
|
293
|
+
if (!existing || existing.status !== settlement.expectedStatus
|
|
294
|
+
|| existing.accountId !== settlement.accountId
|
|
295
|
+
|| (settlement.tokenId !== undefined && existing.tokenId !== undefined && existing.tokenId !== settlement.tokenId)
|
|
296
|
+
|| (settlement.status === "success" && existing.tokenId !== settlement.tokenId)) return false;
|
|
132
297
|
|
|
133
298
|
existing.status = settlement.status === "success" ? "settled" : "failed";
|
|
134
299
|
existing.code = settlement.code;
|
|
@@ -136,4 +301,5 @@ export function recordGrokResetCouponSettlement(
|
|
|
136
301
|
existing.updatedAt = now;
|
|
137
302
|
|
|
138
303
|
writeGrokCouponLedger(filePath, ledger, now);
|
|
304
|
+
return true;
|
|
139
305
|
}
|
|
@@ -230,6 +230,17 @@ function buildGrokHeaders(accessToken: string): Record<string, string> {
|
|
|
230
230
|
/**
|
|
231
231
|
* Reads available Grok reset tokens for the authenticated xAI account.
|
|
232
232
|
*/
|
|
233
|
+
function assertConfirmedCouponStatus(decoded: ReturnType<typeof decodeGrpcWebResponse>): void {
|
|
234
|
+
const rawStatus = decoded.trailers?.metadata["grpc-status"];
|
|
235
|
+
// The shared decoder defaults a missing/malformed status to zero. Coupon
|
|
236
|
+
// availability and an irreversible redemption need affirmative confirmation.
|
|
237
|
+
if (rawStatus === undefined || !/^\d+$/.test(rawStatus) || !Number.isSafeInteger(Number(rawStatus))) {
|
|
238
|
+
throw new Error("Grok coupon response has no confirmed gRPC status");
|
|
239
|
+
}
|
|
240
|
+
const status = Number(rawStatus);
|
|
241
|
+
if (status !== 0) throw new GrpcWebError(status, decoded.statusMessage ?? "Unknown gRPC error");
|
|
242
|
+
}
|
|
243
|
+
|
|
233
244
|
export async function getGrokRemainingResets(options: GetRemainingResetsOptions): Promise<{ tokens: GrokResetCoupon[] }> {
|
|
234
245
|
const fetchImpl = options.fetchFn ?? globalThis.fetch;
|
|
235
246
|
const endpoint = options.endpoint ?? GROK_GET_REMAINING_RESETS_ENDPOINT;
|
|
@@ -251,9 +262,7 @@ export async function getGrokRemainingResets(options: GetRemainingResetsOptions)
|
|
|
251
262
|
const rawBytes = new Uint8Array(await res.arrayBuffer());
|
|
252
263
|
const decoded = decodeGrpcWebResponse(rawBytes);
|
|
253
264
|
|
|
254
|
-
|
|
255
|
-
throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error");
|
|
256
|
-
}
|
|
265
|
+
assertConfirmedCouponStatus(decoded);
|
|
257
266
|
|
|
258
267
|
if (decoded.messages.length === 0) {
|
|
259
268
|
return { tokens: [] };
|
|
@@ -285,9 +294,7 @@ export async function redeemGrokResetCoupon(options: RedeemResetOptions): Promis
|
|
|
285
294
|
const rawBytes = new Uint8Array(await res.arrayBuffer());
|
|
286
295
|
const decoded = decodeGrpcWebResponse(rawBytes);
|
|
287
296
|
|
|
288
|
-
|
|
289
|
-
throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error");
|
|
290
|
-
}
|
|
297
|
+
assertConfirmedCouponStatus(decoded);
|
|
291
298
|
|
|
292
299
|
return {
|
|
293
300
|
success: true,
|
package/src/grok/sync.ts
CHANGED
|
@@ -31,7 +31,7 @@ async function defaultFetchAllModels(config: OcxConfig): Promise<CatalogModel[]>
|
|
|
31
31
|
export async function syncGrokConfig(
|
|
32
32
|
port: number,
|
|
33
33
|
config: OcxConfig,
|
|
34
|
-
opts: { hostname?: string; grokHome?: string } = {},
|
|
34
|
+
opts: { hostname?: string; grokHome?: string; refreshOnly?: { admit: () => boolean } } = {},
|
|
35
35
|
deps: GrokSyncDeps = { fetchAllModels: defaultFetchAllModels, injectGrokConfig },
|
|
36
36
|
): Promise<GrokInjectResult> {
|
|
37
37
|
let projection: ReturnType<typeof projectGrokCatalog>;
|
|
@@ -58,6 +58,7 @@ export async function syncGrokConfig(
|
|
|
58
58
|
? (opts.hostname ?? config.hostname)
|
|
59
59
|
: targetUrl.hostname,
|
|
60
60
|
...(opts.grokHome !== undefined ? { grokHome: opts.grokHome } : {}),
|
|
61
|
+
...opts.refreshOnly ? { refreshOnly: opts.refreshOnly } : {},
|
|
61
62
|
excluded: new Set(config.grokExcludedModels ?? []),
|
|
62
63
|
catalogModelIds: projection.catalogModelIds,
|
|
63
64
|
disabledProviderNamespaces: projection.disabledProviderNamespaces,
|
package/src/images/loop.ts
CHANGED
|
@@ -300,6 +300,11 @@ export interface ImageBridgeDeps {
|
|
|
300
300
|
onRequestBuilt?: (request: AdapterRequest) => void;
|
|
301
301
|
abortSignal?: AbortSignal;
|
|
302
302
|
onFirstOutput?: () => void;
|
|
303
|
+
/** Transfer prepaid cleanup before asynchronous inference can outlive Response construction. */
|
|
304
|
+
onProducerStart?: () => void;
|
|
305
|
+
onProducerEnd?: () => void;
|
|
306
|
+
/** Release a prepaid recovery that did not dispatch during this fetch iteration. */
|
|
307
|
+
onIterationEnd?: () => void;
|
|
303
308
|
/** Max image-generation rounds before forcing a final answer. Defaults to 3; clamped to [0, 10]. */
|
|
304
309
|
maxRounds?: number;
|
|
305
310
|
/** Connect / response-header budget for non-runTurn iterations. */
|
|
@@ -309,7 +314,7 @@ export interface ImageBridgeDeps {
|
|
|
309
314
|
/** Provider-specific fetch (e.g. xAI transport wrapper). Falls back to global fetch. */
|
|
310
315
|
fetchImpl?: typeof globalThis.fetch;
|
|
311
316
|
/** Bind physical dispatch to this iteration's built request; pacing remains owned by the loop. */
|
|
312
|
-
fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest) => typeof globalThis.fetch;
|
|
317
|
+
fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter) => typeof globalThis.fetch;
|
|
313
318
|
/** Reserve the routed provider's next request-start slot before each adapter dispatch. */
|
|
314
319
|
waitForRequestSlot?: (signal?: AbortSignal) => Promise<ProviderRequestSlot | void>;
|
|
315
320
|
/** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
|
|
@@ -372,6 +377,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
372
377
|
let paidImageCalls = 0;
|
|
373
378
|
let paidVideoCalls = 0;
|
|
374
379
|
let hiddenUsage: OcxUsage | undefined;
|
|
380
|
+
let activeProducers = 0;
|
|
375
381
|
|
|
376
382
|
const addUsage = (a: OcxUsage | undefined, b: OcxUsage | undefined): OcxUsage | undefined => {
|
|
377
383
|
if (!a) return b;
|
|
@@ -514,19 +520,25 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
514
520
|
try {
|
|
515
521
|
// Attempt telemetry must fire at dispatch time, not after collection.
|
|
516
522
|
deps.onAttemptSend?.();
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
activeProducers++;
|
|
524
|
+
let producer: Promise<void>;
|
|
525
|
+
try {
|
|
526
|
+
producer = adapter.runTurn(iterParsed, {
|
|
527
|
+
...deps.incomingMeta,
|
|
528
|
+
headers: deps.forwardHeaders ? new Headers(deps.forwardHeaders) : new Headers(),
|
|
529
|
+
...(deps.incomingMeta.providerName ? { providerName: deps.incomingMeta.providerName } : {}),
|
|
530
|
+
abortSignal: signal,
|
|
531
|
+
translatorBudget,
|
|
532
|
+
pacingSlot,
|
|
533
|
+
}, emit);
|
|
534
|
+
} catch (err) { producer = Promise.reject(err); }
|
|
535
|
+
void producer.then(closeOnAbort).catch(err => {
|
|
524
536
|
if (accepting) {
|
|
525
537
|
collectionError = err;
|
|
526
538
|
if (isTranslatorBudgetExceededError(err)) internalAbort.abort(err);
|
|
527
539
|
}
|
|
528
540
|
closeOnAbort();
|
|
529
|
-
});
|
|
541
|
+
}).finally(() => { activeProducers--; deps.onProducerEnd?.(); });
|
|
530
542
|
idle.reset();
|
|
531
543
|
for await (const event of queue.stream()) {
|
|
532
544
|
if (timedOut) break;
|
|
@@ -602,7 +614,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
602
614
|
cachedRequest = request;
|
|
603
615
|
cachedAdapter = requestAdapter;
|
|
604
616
|
}
|
|
605
|
-
const requestFetch = deps.fetchForRequest?.(request, iterParsed) ?? fetchImpl;
|
|
617
|
+
const requestFetch = deps.fetchForRequest?.(request, iterParsed, requestAdapter) ?? fetchImpl;
|
|
606
618
|
let response: Response;
|
|
607
619
|
try {
|
|
608
620
|
if (requestAdapter.fetchResponse) {
|
|
@@ -615,6 +627,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
615
627
|
returnRawErrors: true,
|
|
616
628
|
stream: true,
|
|
617
629
|
executor: requestFetch,
|
|
630
|
+
sendBudget: deps.incomingMeta.sendBudget,
|
|
631
|
+
onPhysicalSend: deps.incomingMeta.onPhysicalSend,
|
|
632
|
+
onRecoveryWithheld: deps.incomingMeta.onRecoveryWithheld,
|
|
618
633
|
}));
|
|
619
634
|
} finally {
|
|
620
635
|
releaseProviderRequestSlot(slot);
|
|
@@ -689,7 +704,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
689
704
|
}
|
|
690
705
|
// 429 key-failover parity with web-search / normal routed path.
|
|
691
706
|
while ((prepared.response.status === 429
|
|
692
|
-
|| (prepared.response.status === 403 && isAnthropicInstanceId(deps.incomingMeta?.providerName))
|
|
707
|
+
|| ((prepared.response.status === 403 || prepared.response.status === 401) && isAnthropicInstanceId(deps.incomingMeta?.providerName))
|
|
693
708
|
|| (iterParsed._kiroAuthContext && (prepared.response.status === 400 || prepared.response.status === 403))) && deps.on429) {
|
|
694
709
|
const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers,
|
|
695
710
|
iterParsed, prepared.response);
|
|
@@ -734,6 +749,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
734
749
|
throw new LoopError(502, `Provider unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
|
735
750
|
} finally {
|
|
736
751
|
headerDeadline.clear();
|
|
752
|
+
deps.onIterationEnd?.();
|
|
737
753
|
}
|
|
738
754
|
};
|
|
739
755
|
|
|
@@ -796,6 +812,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
796
812
|
// non-2xx JSON. Skip for runTurn adapters: their "headers" are synthetic, and awaiting
|
|
797
813
|
// queue.collect() before returning SSE starves clients of headers/heartbeats on slow first turns.
|
|
798
814
|
const skipEagerDrain = !!adapter.runTurn;
|
|
815
|
+
if (skipEagerDrain) deps.onProducerStart?.();
|
|
799
816
|
let firstPrepared: IterationResponse | undefined;
|
|
800
817
|
if (!skipEagerDrain) {
|
|
801
818
|
try {
|
|
@@ -1056,6 +1073,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
1056
1073
|
}
|
|
1057
1074
|
}
|
|
1058
1075
|
} finally {
|
|
1076
|
+
if (skipEagerDrain && activeProducers === 0) deps.onProducerEnd?.();
|
|
1059
1077
|
if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
|
|
1060
1078
|
}
|
|
1061
1079
|
}
|
|
@@ -8,7 +8,7 @@ import { readIntegrationState, type IntegrationState, type IntegrationStatus } f
|
|
|
8
8
|
import {
|
|
9
9
|
applyIntegrationCoordinated, disableIntegrationCoordinated,
|
|
10
10
|
overwriteIntegrationCoordinated, refreshIntegrationCoordinated,
|
|
11
|
-
type IntegrationWriteInput,
|
|
11
|
+
type CoordinatedIntegrationOptions, type IntegrationWriteInput,
|
|
12
12
|
} from "./writer";
|
|
13
13
|
import {
|
|
14
14
|
asideProfileEnabled, asideProfileFailure, asideProfileScope, asideWriteInput,
|
|
@@ -260,7 +260,10 @@ export function mutateAsideProfiles(
|
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
export function refreshAsideProfiles(
|
|
263
|
+
export function refreshAsideProfiles(
|
|
264
|
+
input: AsideProfilesInput,
|
|
265
|
+
options?: { refreshOnly?: boolean; guard?: CoordinatedIntegrationOptions["guard"] },
|
|
266
|
+
): Promise<Array<OwnedIntegrationRefreshOutcome & { profileId: number }>> {
|
|
264
267
|
const policy = input.config.asideProfileSync;
|
|
265
268
|
const selected = Object.values(policy?.profiles ?? {}).some(enabled => enabled === true);
|
|
266
269
|
if (!selected && (policy?.allProfiles === false
|
|
@@ -272,11 +275,15 @@ export function refreshAsideProfiles(input: AsideProfilesInput): Promise<Array<O
|
|
|
272
275
|
try {
|
|
273
276
|
const scope = asideProfileScope(ctx, profile);
|
|
274
277
|
const owned = scope.store.readRecords().aside !== undefined;
|
|
278
|
+
if (options?.refreshOnly && !owned) continue;
|
|
275
279
|
const bound = await asideWriteInput(ctx, scope);
|
|
276
280
|
// A surviving ownership record means a removed block stays removed.
|
|
277
281
|
// A newly discovered, enabled profile may receive its first safe apply.
|
|
278
282
|
const operation = owned ? refreshIntegrationCoordinated : applyIntegrationCoordinated;
|
|
279
|
-
const result = await operation(bound, {
|
|
283
|
+
const result = await operation(bound, {
|
|
284
|
+
lockSeams: input.lockSeams,
|
|
285
|
+
...(options?.guard ? { guard: options.guard } : {}),
|
|
286
|
+
});
|
|
280
287
|
outcomes.push({
|
|
281
288
|
client: "aside", profileId: profile.id, ok: result.ok,
|
|
282
289
|
...(result.ok ? { changed: result.changed } : {}),
|
|
@@ -2,6 +2,7 @@ import { siblingOfLivePort } from "../codex/sibling-start";
|
|
|
2
2
|
import { redactSecretString } from "../lib/redact";
|
|
3
3
|
import type { ExportModel } from "../clients/config-export";
|
|
4
4
|
import type { IntegrationClientId } from "./registry";
|
|
5
|
+
import type { CoordinatedIntegrationOptions, WriteOutcome } from "./writer";
|
|
5
6
|
import {
|
|
6
7
|
refreshOwnedIntegration,
|
|
7
8
|
type OwnedIntegrationRefreshInput,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
export async function refreshOwnedCatalogIntegrations(
|
|
13
14
|
input: Omit<OwnedIntegrationRefreshInput, "clientId">,
|
|
14
15
|
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"],
|
|
16
|
+
options: { refreshOnly?: boolean; admit?: () => boolean } = {},
|
|
15
17
|
): Promise<OwnedIntegrationRefreshOutcome[]> {
|
|
16
18
|
// Client files are shared with the live proxy a sibling instance runs beside; their entries
|
|
17
19
|
// point at the owner's port, and refreshing them here would re-point them at this one.
|
|
@@ -20,14 +22,28 @@ export async function refreshOwnedCatalogIntegrations(
|
|
|
20
22
|
const loadModels = () => models ??= Promise.resolve().then(() =>
|
|
21
23
|
typeof input.models === "function" ? input.models() : input.models);
|
|
22
24
|
const outcomes: OwnedIntegrationRefreshOutcome[] = [];
|
|
25
|
+
const admit = options.admit;
|
|
26
|
+
const writeOptions: CoordinatedIntegrationOptions | undefined = admit ? {
|
|
27
|
+
guard: (frozen): WriteOutcome | null => admit() ? null : {
|
|
28
|
+
ok: false, reason: "superseded_store", state: "current", clientId: frozen.clientId,
|
|
29
|
+
message: "Background refresh superseded",
|
|
30
|
+
},
|
|
31
|
+
} : undefined;
|
|
23
32
|
for (const clientId of clientIds) {
|
|
33
|
+
if (admit?.() === false) break;
|
|
24
34
|
try {
|
|
25
35
|
if (clientId === "aside") {
|
|
26
36
|
const { refreshAsideProfiles } = await import("./aside-profiles");
|
|
27
|
-
|
|
37
|
+
const asideOptions = options.refreshOnly === undefined ? writeOptions
|
|
38
|
+
: { ...writeOptions, refreshOnly: options.refreshOnly };
|
|
39
|
+
const bound = { ...input, models: loadModels };
|
|
40
|
+
outcomes.push(...await (asideOptions === undefined
|
|
41
|
+
? refreshAsideProfiles(bound) : refreshAsideProfiles(bound, asideOptions)));
|
|
28
42
|
continue;
|
|
29
43
|
}
|
|
30
|
-
const
|
|
44
|
+
const bound = { ...input, clientId, models: loadModels };
|
|
45
|
+
const result = await (writeOptions === undefined
|
|
46
|
+
? refreshOwnedIntegration(bound) : refreshOwnedIntegration(bound, writeOptions));
|
|
31
47
|
if (result) outcomes.push(result);
|
|
32
48
|
} catch (error) {
|
|
33
49
|
const busy = error !== null && typeof error === "object"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Detection is read-only and injectable: the proxy never writes anything into a Cursor
|
|
11
11
|
* install, its state database, or its keychain entries (the T20 exclusion in
|
|
12
|
-
* devlog/
|
|
12
|
+
* devlog/_fin/260822_senpi_cursor_transfer/090), and the tests run against a temp tree.
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
15
15
|
import { homedir } from "node:os";
|