@bitkyc08/opencodex 2.43.0 → 2.44.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.
Files changed (138) hide show
  1. package/README.md +25 -17
  2. package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
  3. package/gui/dist/assets/index-ltx3L-WS.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/command-code.ts +23 -2
  7. package/src/adapters/cursor/native-exec.ts +63 -11
  8. package/src/adapters/cursor/tool-definitions.ts +1 -1
  9. package/src/adapters/cursor/tool-schemas.ts +59 -0
  10. package/src/adapters/kiro.ts +54 -8
  11. package/src/adapters/openai-chat.ts +23 -6
  12. package/src/adapters/openai-responses.ts +3 -1
  13. package/src/adapters/opencode-go.ts +35 -0
  14. package/src/claude/agents-inject.ts +12 -5
  15. package/src/claude/desktop-3p-library.ts +89 -0
  16. package/src/claude/desktop-3p.ts +166 -95
  17. package/src/claude/desktop-discovery-inputs.ts +44 -0
  18. package/src/claude/desktop-profile.ts +1 -1
  19. package/src/claude/desktop-remote-store-artifact.ts +185 -0
  20. package/src/claude/desktop-remote-store-io.ts +91 -0
  21. package/src/claude/desktop-remote-store-state.ts +127 -0
  22. package/src/claude/desktop-remote-store.ts +269 -0
  23. package/src/claude/inbound-model-options.ts +10 -2
  24. package/src/claude/inbound-records.ts +7 -0
  25. package/src/claude/inbound.ts +1 -1
  26. package/src/claude/model-info.ts +16 -3
  27. package/src/cli/account-api.ts +7 -1
  28. package/src/cli/aside-profiles.ts +17 -0
  29. package/src/cli/capabilities.ts +39 -0
  30. package/src/cli/claude-agent-startup-sync.ts +3 -1
  31. package/src/cli/claude-desktop.ts +127 -18
  32. package/src/cli/connect.ts +27 -14
  33. package/src/cli/dispatch.ts +30 -16
  34. package/src/cli/export-command.ts +3 -1
  35. package/src/cli/index.ts +28 -16
  36. package/src/cli/integrations.ts +46 -12
  37. package/src/cli/opencode.ts +11 -4
  38. package/src/cli/runtime-api.ts +7 -2
  39. package/src/cli/uninstall-client-state.ts +78 -0
  40. package/src/client/connect.ts +476 -223
  41. package/src/client/hub-client.ts +81 -0
  42. package/src/client/hub-relay.ts +38 -6
  43. package/src/client/lifecycle-lock.ts +129 -0
  44. package/src/client/state.ts +71 -42
  45. package/src/clients/aside-profiles.ts +224 -0
  46. package/src/codex/account-lifecycle.ts +5 -9
  47. package/src/codex/auth-api.ts +50 -4
  48. package/src/codex/catalog/metadata.ts +0 -2
  49. package/src/codex/catalog/provider-fetch.ts +7 -5
  50. package/src/codex/catalog/sync.ts +88 -24
  51. package/src/codex/convergence.ts +2 -0
  52. package/src/codex/inject.ts +45 -14
  53. package/src/codex/journal.ts +6 -6
  54. package/src/codex/quota-refresh-outcome.ts +27 -0
  55. package/src/codex/shim.ts +7 -1
  56. package/src/combos/resolve.ts +10 -7
  57. package/src/config.ts +18 -5
  58. package/src/generated/compatibility-version.json +209 -105
  59. package/src/images/loop.ts +5 -2
  60. package/src/integrations/aside-profile-context.ts +270 -0
  61. package/src/integrations/aside-profile-journal.ts +229 -0
  62. package/src/integrations/aside-profiles.ts +176 -0
  63. package/src/integrations/catalog-refresh.ts +40 -0
  64. package/src/integrations/config-io.ts +16 -1
  65. package/src/integrations/owned-refresh.ts +14 -2
  66. package/src/integrations/state.ts +3 -1
  67. package/src/integrations/writer.ts +3 -1
  68. package/src/lab/fabric/producer-isolate.ts +84 -46
  69. package/src/lib/account-selection-events.ts +32 -0
  70. package/src/lib/errors.ts +9 -1
  71. package/src/lib/provider-outbound.ts +2 -45
  72. package/src/lib/proxy-env.ts +67 -0
  73. package/src/oauth/account-quota-rank.ts +9 -1
  74. package/src/oauth/anthropic-routing.ts +91 -23
  75. package/src/oauth/generic-account-failover.ts +15 -53
  76. package/src/oauth/index.ts +67 -68
  77. package/src/oauth/pool-settings-capability.ts +2 -1
  78. package/src/oauth/store.ts +91 -7
  79. package/src/oauth/types.ts +8 -0
  80. package/src/providers/api-key-selection.ts +110 -0
  81. package/src/providers/api-keys.ts +38 -31
  82. package/src/providers/context-cap.ts +27 -3
  83. package/src/providers/key-failover.ts +27 -17
  84. package/src/providers/provider-id-rewrite.ts +10 -8
  85. package/src/providers/registry.ts +1 -0
  86. package/src/responses/custom-tool-compat.ts +10 -0
  87. package/src/responses/function-call-compat.ts +173 -0
  88. package/src/responses/namespace-tool-compat.ts +27 -4
  89. package/src/responses/parser.ts +8 -0
  90. package/src/responses/task-input.ts +36 -0
  91. package/src/responses/tool-name-aliases.ts +79 -0
  92. package/src/router.ts +2 -0
  93. package/src/routing/capability.ts +10 -3
  94. package/src/routing/compatibility/assemble.ts +16 -1
  95. package/src/routing/evaluator.ts +5 -1
  96. package/src/server/auth-cors.ts +3 -0
  97. package/src/server/chat-native.ts +44 -9
  98. package/src/server/claude-messages.ts +71 -36
  99. package/src/server/grok-responses-snapshot-repair.ts +333 -0
  100. package/src/server/index.ts +26 -10
  101. package/src/server/management/account-selection-stream.ts +70 -0
  102. package/src/server/management/aside-profile-routes.ts +231 -0
  103. package/src/server/management/config-routes.ts +36 -41
  104. package/src/server/management/context.ts +3 -0
  105. package/src/server/management/integration-routes.ts +61 -3
  106. package/src/server/management/logs-usage-routes.ts +1 -0
  107. package/src/server/management/model-routes.ts +20 -9
  108. package/src/server/management/model-rows.ts +10 -2
  109. package/src/server/management/oauth-account-routes.ts +7 -0
  110. package/src/server/management/provider-routes.ts +10 -8
  111. package/src/server/management/route-registry.ts +20 -2
  112. package/src/server/management-auth.ts +16 -0
  113. package/src/server/relay-eager.ts +17 -5
  114. package/src/server/relay.ts +43 -4
  115. package/src/server/request-log.ts +29 -1
  116. package/src/server/responses/agent-task-recovery-cache.ts +11 -0
  117. package/src/server/responses/agent-task-recovery.ts +24 -4
  118. package/src/server/responses/codex-ws-pool.ts +4 -4
  119. package/src/server/responses/codex-ws-session.ts +2 -2
  120. package/src/server/responses/collaboration.ts +2 -1
  121. package/src/server/responses/combo-stream-preflight.ts +26 -7
  122. package/src/server/responses/core.ts +668 -151
  123. package/src/server/responses/fetch-helpers.ts +6 -1
  124. package/src/server/responses/ws-upstream.ts +8 -3
  125. package/src/server/responses-custom-tool-repair.ts +29 -9
  126. package/src/server/responses-function-tool-repair.ts +183 -0
  127. package/src/server/responses-snapshot-codec.ts +14 -0
  128. package/src/server/responses-snapshot-repair.ts +1 -13
  129. package/src/server/responses-undeclared-tool-guard.ts +1 -68
  130. package/src/server/sse-payload-rewrite.ts +3 -1
  131. package/src/service.ts +6 -2
  132. package/src/types/config.ts +23 -16
  133. package/src/types/provider.ts +14 -3
  134. package/src/types/request.ts +2 -0
  135. package/src/usage/log.ts +9 -0
  136. package/src/web-search/loop.ts +5 -2
  137. package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
  138. package/gui/dist/assets/index-Djowl68T.js +0 -112
@@ -0,0 +1,176 @@
1
+ import type { AsideProfile } from "../clients/aside-profiles";
2
+ import { asideHomeDir } from "../clients/config-export";
3
+ import { join } from "node:path";
4
+ import type { OwnedIntegrationRefreshOutcome } from "./owned-refresh";
5
+ import { readIntegrationState, type IntegrationState, type IntegrationStatus } from "./state";
6
+ import {
7
+ applyIntegrationCoordinated, disableIntegrationCoordinated,
8
+ overwriteIntegrationCoordinated, refreshIntegrationCoordinated,
9
+ } from "./writer";
10
+ import {
11
+ asideProfileEnabled, asideProfileFailure, asideProfileScope, asideWriteInput,
12
+ asideRootStore, createAsideProfileContext, persistAsidePolicy, runAsideProfileAction, selectAsideProfiles,
13
+ type AsideProfileContext, type AsideProfilesInput, type AsideProfileWriteOutcome,
14
+ } from "./aside-profile-context";
15
+
16
+ export { AsideProfileError } from "./aside-profile-context";
17
+ export type { AsideProfilesInput, AsideProfileWriteOutcome } from "./aside-profile-context";
18
+
19
+ export interface AsideProfileState extends IntegrationStatus {
20
+ profileId: number;
21
+ name?: string;
22
+ current: boolean;
23
+ enabled: boolean;
24
+ error?: string;
25
+ }
26
+
27
+ export interface AsideProfileList extends IntegrationStatus {
28
+ profiles: AsideProfileState[];
29
+ allEnabled: boolean;
30
+ enabledCount: number;
31
+ appliedCount: number;
32
+ total: number;
33
+ error?: string;
34
+ }
35
+
36
+ export interface AsideProfileMutationResult {
37
+ ok: boolean;
38
+ clientId: "aside";
39
+ changed: boolean;
40
+ state: IntegrationState;
41
+ message: string;
42
+ results: AsideProfileWriteOutcome[];
43
+ /** Preserve the ordinary refusal serializer for a single selected profile. */
44
+ result?: AsideProfileWriteOutcome;
45
+ }
46
+
47
+ function aggregateState(states: readonly IntegrationState[]): IntegrationState {
48
+ if (states.includes("unsafe")) return "unsafe";
49
+ if (states.includes("conflict")) return "conflict";
50
+ if (states.every(state => state === "absent")) return "absent";
51
+ return states.every(state => state === "current") ? "current" : "stale";
52
+ }
53
+
54
+ async function profileState(ctx: AsideProfileContext, profile: AsideProfile): Promise<AsideProfileState> {
55
+ const metadata = {
56
+ profileId: profile.id, ...(profile.name !== undefined ? { name: profile.name } : {}),
57
+ current: profile.current, enabled: asideProfileEnabled(ctx, profile.id),
58
+ };
59
+ try {
60
+ const scope = asideProfileScope(ctx, profile);
61
+ const input = await asideWriteInput(ctx, scope);
62
+ return { ...readIntegrationState(input), ...metadata };
63
+ } catch (error) {
64
+ return {
65
+ clientId: "aside", ...metadata, state: "unsafe", installed: false,
66
+ configPath: profile.configPath, reason: "unresolvable-path", snapshotCount: -1,
67
+ retentionDegraded: true, error: asideProfileFailure(profile.id, error).message,
68
+ };
69
+ }
70
+ }
71
+
72
+ export async function listAsideProfileStates(input: AsideProfilesInput): Promise<AsideProfileList> {
73
+ let ctx: AsideProfileContext;
74
+ try { ctx = createAsideProfileContext(input); }
75
+ catch (error) {
76
+ return {
77
+ clientId: "aside", profiles: [], total: 0, enabledCount: 0, appliedCount: 0, allEnabled: false,
78
+ state: "unsafe", installed: false, configPath: join(asideHomeDir(input.env, input.home), "u"),
79
+ snapshotCount: -1, retentionDegraded: true, reason: "unresolvable-path",
80
+ error: asideProfileFailure(0, error).message,
81
+ };
82
+ }
83
+ const profiles: AsideProfileState[] = [];
84
+ for (const profile of ctx.profiles) profiles.push(await profileState(ctx, profile));
85
+ const enabledCount = profiles.filter(profile => profile.enabled).length;
86
+ const snapshotCount = profiles.some(profile => profile.snapshotCount < 0) ? -1
87
+ : profiles.reduce((sum, profile) => sum + profile.snapshotCount, 0);
88
+ return {
89
+ clientId: "aside", profiles, total: profiles.length, enabledCount,
90
+ allEnabled: profiles.length > 0 && enabledCount === profiles.length,
91
+ appliedCount: profiles.filter(profile => profile.state === "current" || profile.state === "stale").length,
92
+ state: aggregateState(profiles.map(profile => profile.state)),
93
+ installed: profiles.some(profile => profile.installed),
94
+ configPath: profiles.find(profile => profile.current)?.configPath ?? profiles[0]?.configPath ?? "",
95
+ snapshotCount, retentionDegraded: profiles.some(profile => profile.retentionDegraded),
96
+ };
97
+ }
98
+
99
+ export async function getAsideProfileState(input: AsideProfilesInput, id: number): Promise<AsideProfileState> {
100
+ const ctx = createAsideProfileContext(input);
101
+ return profileState(ctx, selectAsideProfiles(ctx, id)[0]!);
102
+ }
103
+
104
+ export function mutateAsideProfiles(
105
+ input: AsideProfilesInput,
106
+ change: { enabled: boolean; profileId?: number; overwriteConflict?: boolean },
107
+ ): Promise<AsideProfileMutationResult> {
108
+ return runAsideProfileAction<AsideProfileMutationResult>(input, change.profileId, `${change.enabled ? "enable" : "disable"}:${Boolean(change.overwriteConflict)}`, async (ctx, profiles) => {
109
+ const refused = new Map<number, AsideProfileWriteOutcome>();
110
+ for (const profile of profiles) {
111
+ try { asideProfileScope(ctx, profile); }
112
+ catch (error) { refused.set(profile.id, asideProfileFailure(profile.id, error)); }
113
+ }
114
+ // This await precedes model loading, writer preflight, snapshots and all client writes.
115
+ await persistAsidePolicy(ctx, change);
116
+ const results: AsideProfileWriteOutcome[] = [];
117
+ for (const profile of profiles) {
118
+ const refusal = refused.get(profile.id);
119
+ if (refusal) { results.push(refusal); continue; }
120
+ try {
121
+ const scope = asideProfileScope(ctx, profile);
122
+ const bound = await asideWriteInput(ctx, scope);
123
+ const operation = !change.enabled ? disableIntegrationCoordinated
124
+ : change.overwriteConflict ? overwriteIntegrationCoordinated : applyIntegrationCoordinated;
125
+ results.push({ ...await operation(bound, { lockSeams: input.lockSeams }), profileId: profile.id });
126
+ } catch (error) { results.push(asideProfileFailure(profile.id, error)); }
127
+ }
128
+ const ok = results.every(result => result.ok);
129
+ return {
130
+ ok, clientId: "aside", changed: results.some(result => result.ok && result.changed),
131
+ state: aggregateState(results.map(result => result.state)),
132
+ message: ok ? "Aside profile preferences applied" : "Aside preferences saved; some profiles could not be updated",
133
+ results, ...(results.length === 1 ? { result: results[0] } : {}),
134
+ };
135
+ });
136
+ }
137
+
138
+ export function refreshAsideProfiles(input: AsideProfilesInput): Promise<Array<OwnedIntegrationRefreshOutcome & { profileId: number }>> {
139
+ const policy = input.config.asideProfileSync;
140
+ const selected = Object.values(policy?.profiles ?? {}).some(enabled => enabled === true);
141
+ if (!selected && (policy?.allProfiles === false
142
+ || (policy?.allProfiles !== true && !asideRootStore(input).readRecords().aside))) return Promise.resolve([]);
143
+ return runAsideProfileAction(input, undefined, "refresh", async (ctx, profiles) => {
144
+ const outcomes: Array<OwnedIntegrationRefreshOutcome & { profileId: number }> = [];
145
+ for (const profile of profiles) {
146
+ if (!asideProfileEnabled(ctx, profile.id)) continue;
147
+ try {
148
+ const scope = asideProfileScope(ctx, profile);
149
+ const owned = scope.store.readRecords().aside !== undefined;
150
+ const bound = await asideWriteInput(ctx, scope);
151
+ // A surviving ownership record means a removed block stays removed.
152
+ // A newly discovered, enabled profile may receive its first safe apply.
153
+ const operation = owned ? refreshIntegrationCoordinated : applyIntegrationCoordinated;
154
+ const result = await operation(bound, { lockSeams: input.lockSeams });
155
+ outcomes.push({
156
+ client: "aside", profileId: profile.id, ok: result.ok,
157
+ ...(result.ok ? { changed: result.changed } : {}),
158
+ ...(!result.ok || result.state === "absent" ? { reason: result.message } : {}),
159
+ ...(!result.ok ? {
160
+ refusalReason: result.reason, state: result.state,
161
+ ...(result.snapshotPath ? { snapshotPath: result.snapshotPath } : {}),
162
+ ...(result.residual ? { residual: true } : {}),
163
+ } : {}),
164
+ });
165
+ } catch (error) {
166
+ const failure = asideProfileFailure(profile.id, error);
167
+ outcomes.push({ client: "aside", profileId: profile.id, ok: false, reason: failure.message,
168
+ ...(!failure.ok ? { refusalReason: failure.reason, state: failure.state,
169
+ ...(failure.snapshotPath ? { snapshotPath: failure.snapshotPath } : {}),
170
+ ...(failure.residual ? { residual: true } : {}) } : {}),
171
+ });
172
+ }
173
+ }
174
+ return outcomes;
175
+ });
176
+ }
@@ -0,0 +1,40 @@
1
+ import { redactSecretString } from "../lib/redact";
2
+ import type { ExportModel } from "../clients/config-export";
3
+ import type { IntegrationClientId } from "./registry";
4
+ import {
5
+ refreshOwnedIntegration,
6
+ type OwnedIntegrationRefreshInput,
7
+ type OwnedIntegrationRefreshOutcome,
8
+ } from "./owned-refresh";
9
+
10
+ /** Refresh only previously connected clients; a refused file never blocks its peers. */
11
+ export async function refreshOwnedCatalogIntegrations(
12
+ input: Omit<OwnedIntegrationRefreshInput, "clientId">,
13
+ clientIds: readonly IntegrationClientId[] = ["pi", "aside"],
14
+ ): Promise<OwnedIntegrationRefreshOutcome[]> {
15
+ let models: Promise<readonly ExportModel[]> | undefined;
16
+ const loadModels = () => models ??= Promise.resolve().then(() =>
17
+ typeof input.models === "function" ? input.models() : input.models);
18
+ const outcomes: OwnedIntegrationRefreshOutcome[] = [];
19
+ for (const clientId of clientIds) {
20
+ try {
21
+ if (clientId === "aside") {
22
+ const { refreshAsideProfiles } = await import("./aside-profiles");
23
+ outcomes.push(...await refreshAsideProfiles({ ...input, models: loadModels }));
24
+ continue;
25
+ }
26
+ const result = await refreshOwnedIntegration({ ...input, clientId, models: loadModels });
27
+ if (result) outcomes.push(result);
28
+ } catch (error) {
29
+ const busy = error !== null && typeof error === "object"
30
+ && "code" in error && error.code === "integration_mutation_busy";
31
+ outcomes.push({
32
+ client: clientId,
33
+ ok: false,
34
+ reason: busy ? "integration_mutation_busy"
35
+ : redactSecretString(error instanceof Error ? error.message : String(error)),
36
+ });
37
+ }
38
+ }
39
+ return outcomes;
40
+ }
@@ -162,7 +162,22 @@ export function parseConfig(text: string | null, format: ConfigFormat): unknown
162
162
  * evidence is gone.
163
163
  */
164
164
  if (/(^|[\s,[=])[-+]?(?:inf|nan)(?=[\s,\]]|$)/mi.test(text)) return PARSE_FAILED;
165
- return Bun.TOML.parse(text);
165
+ const document = Bun.TOML.parse(text);
166
+ // TOML date/time scalars are Temporal objects with toJSON methods.
167
+ // The merge layer JSON-clones documents, which silently turns these
168
+ // into strings. Refuse before either status or a writer can admit a
169
+ // lossy rewrite, including dates nested in arrays and inline tables.
170
+ const pending: unknown[] = [document];
171
+ while (pending.length > 0) {
172
+ const value = pending.pop();
173
+ if (value === null || typeof value !== "object") continue;
174
+ if (!Array.isArray(value)) {
175
+ const prototype = Object.getPrototypeOf(value);
176
+ if (prototype !== Object.prototype && prototype !== null) return PARSE_FAILED;
177
+ }
178
+ for (const child of Object.values(value)) pending.push(child);
179
+ }
180
+ return document;
166
181
  }
167
182
  }
168
183
  } catch {
@@ -27,6 +27,8 @@ export interface OwnedIntegrationRefreshInput {
27
27
  home?: string;
28
28
  store?: IntegrationStateStore;
29
29
  io?: IntegrationIO;
30
+ /** Internal profile target selected before entering the coordinated writer. */
31
+ resolvedPaths?: { configPath: string; detectDir: string };
30
32
  }
31
33
 
32
34
  export interface OwnedIntegrationRefreshOutcome {
@@ -34,6 +36,11 @@ export interface OwnedIntegrationRefreshOutcome {
34
36
  readonly ok: boolean;
35
37
  readonly changed?: boolean;
36
38
  readonly reason?: string;
39
+ readonly profileId?: number;
40
+ readonly refusalReason?: string;
41
+ readonly state?: string;
42
+ readonly snapshotPath?: string;
43
+ readonly residual?: boolean;
37
44
  }
38
45
 
39
46
  /**
@@ -59,7 +66,9 @@ export async function refreshOwnedIntegration(
59
66
  const bound = { ...rest, models, store };
60
67
  const result = await runIntegrationMutationFlight(
61
68
  input.clientId,
62
- "refresh",
69
+ // Separate catalog snapshots must not inherit another refresh's success.
70
+ // The shared flight owner returns busy for overlapping operations instead.
71
+ `refresh:${crypto.randomUUID()}`,
63
72
  input.io?.now ?? Date.now,
64
73
  () => refreshIntegrationCoordinated(bound, options),
65
74
  );
@@ -70,5 +79,8 @@ export async function refreshOwnedIntegration(
70
79
  changed: result.changed,
71
80
  ...(result.state === "absent" ? { reason: result.message } : {}),
72
81
  }
73
- : { client: input.clientId, ok: false, reason: result.message };
82
+ : { client: input.clientId, ok: false, reason: result.message, refusalReason: result.reason, state: result.state,
83
+ ...(result.snapshotPath ? { snapshotPath: result.snapshotPath } : {}),
84
+ ...(result.residual ? { residual: true } : {}),
85
+ };
74
86
  }
@@ -352,6 +352,8 @@ export interface IntegrationStateInput {
352
352
  /** The whole integration state store, bound to one root. */
353
353
  store?: IntegrationStateStore;
354
354
  io?: IntegrationIO;
355
+ /** Internal explicit profile target; never accepted as a caller-provided path. */
356
+ resolvedPaths?: { configPath: string; detectDir: string };
355
357
  }
356
358
 
357
359
  export function exportContextOf(input: {
@@ -431,7 +433,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
431
433
  try {
432
434
  // One resolution for both, so a client whose paths come from mutable state
433
435
  // cannot report one account's install beside another account's config path.
434
- const paths = resolveIntegrationPaths(input.clientId, input.env, input.home);
436
+ const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home);
435
437
  configPath = paths.configPath;
436
438
  installed = io.statKind(paths.detectDir) === "dir";
437
439
  } catch (error) {
@@ -711,7 +711,9 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
711
711
  * its manifest, so two independent calls could verify one account's install
712
712
  * and then write another account's catalog if a switch landed between them.
713
713
  */
714
- const resolvedPaths = resolveIntegrationPaths(input.clientId, env, home);
714
+ const resolvedPaths = input.resolvedPaths
715
+ ? { ...input.resolvedPaths }
716
+ : resolveIntegrationPaths(input.clientId, env, home);
715
717
  return { ...input, env, home, store, io, resolvedPaths };
716
718
  }
717
719
 
@@ -84,6 +84,11 @@ function killChild(child: ChildProcess): void {
84
84
  export async function runIsolatedFabricProducer(request: IsolateRequest): Promise<IsolatedProducerResult> {
85
85
  const now = request.now ?? (() => Date.now());
86
86
  let lastActivityAt = now();
87
+ // Budget enforcement must not follow wall-clock adjustments; telemetry still does.
88
+ const budgetNow = request.now ?? (() => performance.now());
89
+ const startedAt = request.now ? lastActivityAt : budgetNow();
90
+ const totalDeadline = startedAt + request.totalTimeoutMs;
91
+ let inactivityDeadline = startedAt + request.inactivityTimeoutMs;
87
92
 
88
93
  return await new Promise<IsolatedProducerResult>((resolve, reject) => {
89
94
  let child: ChildProcess;
@@ -104,48 +109,74 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
104
109
  let stdoutBuffer = "";
105
110
  let stderrBytes = 0;
106
111
  let settled = false;
112
+ let childClosed = false;
107
113
  let receivedResult: SyntheticPatchV1 | undefined;
108
114
  let killReason: FabricTaskError | undefined;
109
115
 
110
116
  const finish = (fn: () => void) => {
111
- if (settled) return;
117
+ // A latched failure owns settlement, but scratch cleanup must wait for close.
118
+ if (settled || (killReason && !childClosed)) return;
112
119
  settled = true;
113
120
  clearTimeout(totalTimer);
114
121
  clearTimeout(inactivityTimer);
115
- fn();
122
+ if (killReason) reject(killReason);
123
+ else fn();
116
124
  };
117
125
 
118
126
  const settleTimeout = (error: FabricTaskError) => {
119
- if (settled) return;
127
+ if (settled || killReason) return;
120
128
  killReason = error;
121
- killChild(child);
129
+ if (childClosed) finish(() => reject(error));
130
+ else killChild(child);
131
+ };
132
+
133
+ const expiredDeadline = (at: number): FabricTaskError | undefined => {
134
+ // Choose the earliest deadline, regardless of which timer/data callback ran first.
135
+ if (at >= inactivityDeadline && inactivityDeadline <= totalDeadline) {
136
+ return new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment");
137
+ }
138
+ if (at >= totalDeadline) {
139
+ return new FabricTaskError("total timeout exceeded", "timeout", "environment");
140
+ }
141
+ return undefined;
142
+ };
143
+
144
+ const onInactivityTimeout = () => {
145
+ settleTimeout(expiredDeadline(budgetNow())
146
+ ?? new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment"));
122
147
  };
123
148
 
124
149
  const armInactivity = () => {
125
150
  clearTimeout(inactivityTimer);
126
- inactivityTimer = setTimeout(() => {
127
- settleTimeout(new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment"));
128
- }, request.inactivityTimeoutMs);
151
+ inactivityTimer = setTimeout(onInactivityTimeout, request.inactivityTimeoutMs);
129
152
  };
130
153
 
131
- let inactivityTimer: ReturnType<typeof setTimeout> = setTimeout(() => {
132
- settleTimeout(new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment"));
133
- }, request.inactivityTimeoutMs);
154
+ let inactivityTimer: ReturnType<typeof setTimeout> = setTimeout(onInactivityTimeout, request.inactivityTimeoutMs);
134
155
 
135
156
  const totalTimer = setTimeout(() => {
136
- settleTimeout(new FabricTaskError("total timeout exceeded", "timeout", "environment"));
157
+ settleTimeout(expiredDeadline(budgetNow())
158
+ ?? new FabricTaskError("total timeout exceeded", "timeout", "environment"));
137
159
  }, request.totalTimeoutMs);
138
160
 
139
161
  const handleProtocolLine = (line: string) => {
162
+ if (settled || killReason) return;
140
163
  try {
141
164
  const message = parseProducerProtocolLine(line);
142
- if (message.type === "activity") {
143
- lastActivityAt = now();
144
- armInactivity();
145
- return;
165
+ if (message.type === "activity" || message.type === "result") {
166
+ const at = budgetNow();
167
+ const expired = expiredDeadline(at);
168
+ if (expired) {
169
+ settleTimeout(expired);
170
+ return;
171
+ }
172
+ if (message.type === "activity") {
173
+ lastActivityAt = request.now ? at : now();
174
+ inactivityDeadline = at + request.inactivityTimeoutMs;
175
+ armInactivity();
176
+ return;
177
+ }
146
178
  }
147
179
  if (message.type === "result") {
148
- if (settled) return;
149
180
  receivedResult = message.patch;
150
181
  finish(() => resolve({ patch: message.patch, lastActivityAt }));
151
182
  return;
@@ -176,6 +207,7 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
176
207
  };
177
208
 
178
209
  const consumeStdout = (chunk: string) => {
210
+ if (settled || killReason) return;
179
211
  stdoutBuffer += chunk;
180
212
  if (Buffer.byteLength(stdoutBuffer, "utf8") > FABRIC_PRODUCER_PROTOCOL_MAX_BYTES) {
181
213
  settleTimeout(new FabricTaskError("producer protocol output exceeded limit", "budget_exhausted", "environment"));
@@ -207,16 +239,50 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
207
239
  }
208
240
  });
209
241
 
242
+ child.stderr?.on("error", (error) => {
243
+ settleTimeout(new FabricTaskError(error.message, "harness_failure", "harness"));
244
+ });
245
+
210
246
  child.on("error", (error) => {
211
247
  finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
212
248
  });
213
249
 
214
250
  child.stdin?.on("error", (error: NodeJS.ErrnoException) => {
215
- if (settled || error.code === "EPIPE") return;
251
+ if (settled || killReason || error.code === "EPIPE") return;
216
252
  killChild(child);
217
253
  finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
218
254
  });
219
255
 
256
+ child.on("close", (code, signal) => {
257
+ childClosed = true;
258
+ if (settled) return;
259
+ if (killReason) {
260
+ finish(() => reject(killReason!));
261
+ return;
262
+ }
263
+ if (receivedResult) {
264
+ finish(() => resolve({ patch: receivedResult!, lastActivityAt }));
265
+ return;
266
+ }
267
+ if (stdoutBuffer.trim()) {
268
+ try {
269
+ handleProtocolLine(stdoutBuffer.trim());
270
+ if (settled) return;
271
+ } catch {
272
+ /* fall through */
273
+ }
274
+ }
275
+ if (signal === "SIGKILL") {
276
+ finish(() => reject(new FabricTaskError("total timeout exceeded", "timeout", "environment")));
277
+ return;
278
+ }
279
+ finish(() => reject(new FabricTaskError(
280
+ code === 0 ? "isolated producer returned no result" : `isolated producer exited (${code ?? signal ?? "unknown"})`,
281
+ "harness_failure",
282
+ "harness",
283
+ )));
284
+ });
285
+
220
286
  const payload = JSON.stringify({
221
287
  harnessKind: request.harnessKind,
222
288
  executorModulePath: request.executorModulePath,
@@ -242,6 +308,7 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
242
308
  child.stdin?.write(payload);
243
309
  child.stdin?.end();
244
310
  } catch (error) {
311
+ if (killReason) return;
245
312
  killChild(child);
246
313
  finish(() => reject(new FabricTaskError(
247
314
  error instanceof Error ? error.message : String(error),
@@ -250,35 +317,6 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
250
317
  )));
251
318
  return;
252
319
  }
253
-
254
- child.on("close", (code, signal) => {
255
- if (settled) return;
256
- if (killReason) {
257
- finish(() => reject(killReason!));
258
- return;
259
- }
260
- if (receivedResult) {
261
- finish(() => resolve({ patch: receivedResult!, lastActivityAt }));
262
- return;
263
- }
264
- if (stdoutBuffer.trim()) {
265
- try {
266
- handleProtocolLine(stdoutBuffer.trim());
267
- if (receivedResult) return;
268
- } catch {
269
- /* fall through */
270
- }
271
- }
272
- if (signal === "SIGKILL") {
273
- finish(() => reject(new FabricTaskError("total timeout exceeded", "timeout", "environment")));
274
- return;
275
- }
276
- finish(() => reject(new FabricTaskError(
277
- code === 0 ? "isolated producer returned no result" : `isolated producer exited (${code ?? signal ?? "unknown"})`,
278
- "harness_failure",
279
- "harness",
280
- )));
281
- });
282
320
  });
283
321
  }
284
322
 
@@ -0,0 +1,32 @@
1
+ /** Process-local invalidations. Connection limits, buffering, and timers belong to consumers. */
2
+ export type AccountSelectionEvent = {
3
+ provider: string;
4
+ kind: "oauth" | "api-key";
5
+ revision: number;
6
+ };
7
+
8
+ const listeners = new Set<(event: AccountSelectionEvent) => void>();
9
+ let revision = 0;
10
+
11
+ /** Call only after the authoritative selection has been persisted. */
12
+ export function publishAccountSelection(provider: string, kind: AccountSelectionEvent["kind"]): void {
13
+ const event: AccountSelectionEvent = Object.freeze({ provider, kind, revision: ++revision });
14
+ for (const listener of [...listeners]) {
15
+ try {
16
+ listener(event);
17
+ } catch {
18
+ // A disconnected consumer must not turn a committed write into a reported failure.
19
+ }
20
+ }
21
+ }
22
+
23
+ export function subscribeAccountSelections(listener: (event: AccountSelectionEvent) => void): () => void {
24
+ // Give each subscription its own lifetime, even when a callback is reused.
25
+ const subscription = (event: AccountSelectionEvent) => listener(event);
26
+ listeners.add(subscription);
27
+ return () => { listeners.delete(subscription); };
28
+ }
29
+
30
+ export function currentAccountSelectionRevision(): number {
31
+ return revision;
32
+ }
package/src/lib/errors.ts CHANGED
@@ -4,10 +4,15 @@ export interface OcxErrorPayload {
4
4
  code: string | null;
5
5
  }
6
6
 
7
+ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
8
+ "Encrypted function output content could not be decrypted or decoded.";
9
+
7
10
  /** Canonical human-readable message paths used by Responses upstream failures. */
8
11
  export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
9
12
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
10
13
  const json = payload as {
14
+ type?: unknown;
15
+ message?: unknown;
11
16
  error?: { message?: unknown };
12
17
  last_error?: { message?: unknown };
13
18
  response?: {
@@ -18,7 +23,10 @@ export function upstreamErrorMessageFromPayload(payload: unknown): string | unde
18
23
  const message = json.error?.message
19
24
  ?? json.last_error?.message
20
25
  ?? json.response?.error?.message
21
- ?? json.response?.incomplete_details?.message;
26
+ ?? json.response?.incomplete_details?.message
27
+ // The Responses stream error event carries a flat message (type/code/message),
28
+ // unlike the response.failed envelope the branches above already cover.
29
+ ?? (json.type === "error" ? json.message : undefined);
22
30
  return typeof message === "string" ? message : undefined;
23
31
  }
24
32
 
@@ -7,7 +7,7 @@ import {
7
7
  resolvePublicAddresses,
8
8
  } from "./destination-policy";
9
9
  import { pinnedHttpGet, pinnedHttpPost } from "./pinned-http";
10
- import { effectiveProxyFor, outboundProxyConfigured } from "./proxy-env";
10
+ import { effectiveProxyFor, noProxyMatches, normalizeProxyHostname, outboundProxyConfigured } from "./proxy-env";
11
11
  import { publicProviderBaseUrl } from "./provider-url";
12
12
 
13
13
  type ProviderGetInit = Omit<RequestInit, "body" | "method" | "redirect">;
@@ -37,10 +37,6 @@ function pickPinnedAddress(addresses: Array<{ address: string; family: number }>
37
37
  return addresses.find(address => address.family === 4) ?? addresses[0]!;
38
38
  }
39
39
 
40
- function configuredProxyFor(): boolean {
41
- return outboundProxyConfigured();
42
- }
43
-
44
40
  /**
45
41
  * Registry-owned fake-IP transparency exception (Clash/Surge/Mihomo TUN mode).
46
42
  *
@@ -76,45 +72,6 @@ function transparentFakeIpException(
76
72
  return isCanonicalUrl(name, url);
77
73
  }
78
74
 
79
- function normalizeProxyHostname(hostname: string): string {
80
- const normalized = hostname.trim().toLowerCase().replace(/\.+$/, "");
81
- return normalized.startsWith("[") && normalized.endsWith("]")
82
- ? normalized.slice(1, -1)
83
- : normalized;
84
- }
85
-
86
- function noProxyMatches(url: URL): boolean {
87
- const raw = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
88
- const hostname = normalizeProxyHostname(url.hostname);
89
- const port = url.port || (url.protocol === "https:" ? "443" : "80");
90
- for (const rawEntry of raw.split(",")) {
91
- let entry = rawEntry.trim().toLowerCase();
92
- if (!entry) continue;
93
- if (entry === "*") return true;
94
- entry = entry.replace(/^https?:\/\//, "").split("/", 1)[0]!;
95
-
96
- let entryHost = entry;
97
- let entryPort = "";
98
- const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(entry);
99
- if (bracketed) {
100
- entryHost = bracketed[1]!;
101
- entryPort = bracketed[2] ?? "";
102
- } else if ((entry.match(/:/g)?.length ?? 0) === 1) {
103
- const separator = entry.lastIndexOf(":");
104
- const possiblePort = entry.slice(separator + 1);
105
- if (/^\d+$/.test(possiblePort)) {
106
- entryHost = entry.slice(0, separator);
107
- entryPort = possiblePort;
108
- }
109
- }
110
- if (entryPort && entryPort !== port) continue;
111
- entryHost = normalizeProxyHostname(entryHost.replace(/^\*?\./, ""));
112
- if (!entryHost) continue;
113
- if (hostname === entryHost || hostname.endsWith(`.${entryHost}`)) return true;
114
- }
115
- return false;
116
- }
117
-
118
75
  let proxyBoundaryWarned = false;
119
76
  let proxyDnsDegradationWarned = false;
120
77
 
@@ -181,7 +138,7 @@ async function providerOutboundRequest(
181
138
  return provider.fetch(url, { ...init, method, redirect: "manual" });
182
139
  }
183
140
  const parsed = postUrl ?? new URL(url);
184
- const proxyConfigured = configuredProxyFor();
141
+ const proxyConfigured = outboundProxyConfigured();
185
142
  // Snapshot the scheme-matched proxy once, before the DNS await, so admission and transport
186
143
  // below reason about the same value. `null` here means "no proxy fetch would actually use",
187
144
  // even if some other proxy variable is set.