@oai404iao/pi-codex-runtime 0.3.0 → 0.4.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/README.md CHANGED
@@ -4,7 +4,7 @@ Alpha bootstrap candidate; initial npm publication is still pending.
4
4
  It has no Pi extension entry
5
5
  and does not automatically register tools or providers.
6
6
 
7
- Peer floor: Pi 0.86.1; tested against 0.86.1.
7
+ Peer floor: Pi 0.87.0; development target: 0.87.1.
8
8
 
9
9
  Owns shared Codex authentication/headers, wire identity, settings/catalog,
10
10
  Responses replay contracts and the session-scoped composition broker.
@@ -1,5 +1,11 @@
1
1
  # Source and license notices
2
2
 
3
+ Sol/Luna profile metadata was independently checked against Apache-2.0 Codex
4
+ revision `40eac3ce8a0c10cbcb9db910d529355eb2f8fc09` and the MIT Pi 0.87.1
5
+ catalog. See `provenance/openai-codex-40eac3ce-sol-luna.json` for exact source
6
+ hashes and limits. The local catalog adapts those capabilities; it does not
7
+ copy model instructions, enable unverified billing multipliers or imply access.
8
+
3
9
  This unpublished package is split from `@oai404iao/pi-codex-minimal-tools`.
4
10
  Project material remains Copyright (c) 2026 oai404iao. The composite LICENSE
5
11
  and verified Apache license/NOTICE snapshots are retained.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oai404iao/pi-codex-runtime",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "Unpublished pi-codex-runtime workspace for the staged Codex split",
6
6
  "type": "module",
@@ -23,8 +23,8 @@
23
23
  "./subagent-inline": "./src/subagent-inline.ts"
24
24
  },
25
25
  "peerDependencies": {
26
- "@earendil-works/pi-ai": ">=0.86.1",
27
- "@earendil-works/pi-coding-agent": ">=0.86.1"
26
+ "@earendil-works/pi-ai": ">=0.87.0",
27
+ "@earendil-works/pi-coding-agent": ">=0.87.0"
28
28
  },
29
29
  "peerDependenciesMeta": {
30
30
  "@earendil-works/pi-ai": {
@@ -36,8 +36,8 @@
36
36
  },
37
37
  "dependencies": {},
38
38
  "devDependencies": {
39
- "@earendil-works/pi-ai": "0.86.1",
40
- "@earendil-works/pi-coding-agent": "0.86.1",
39
+ "@earendil-works/pi-ai": "0.87.1",
40
+ "@earendil-works/pi-coding-agent": "0.87.1",
41
41
  "@types/node": "^26.2.0",
42
42
  "tsx": "^4.20.6",
43
43
  "typescript": "^7.0.2"
@@ -64,5 +64,5 @@
64
64
  "engines": {
65
65
  "node": ">=22.19.0"
66
66
  },
67
- "gitHead": "b02484cecad9081886797bf9b4438135aaae1e07"
67
+ "gitHead": "351c1943c7402b56f36abbe22fe45bb639bf5f81"
68
68
  }
@@ -0,0 +1,42 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "upstream": {
4
+ "repository": "https://github.com/openai/codex",
5
+ "revision": "40eac3ce8a0c10cbcb9db910d529355eb2f8fc09",
6
+ "license": "Apache-2.0"
7
+ },
8
+ "files": {
9
+ "codex-rs/models-manager/models.json": {
10
+ "role": "Exact Sol/Luna model capabilities: Lite, freeform patch, summary none, WebSocket preference, standalone web selection",
11
+ "gitBlobSha": "8fd2c078f857aa9e12ee2b74131e539d3ead520f",
12
+ "sha256": "0178d235c589a31abd6ed0ea1e870935dc5819240eb0e813e178d3ebedf534f4"
13
+ },
14
+ "codex-rs/core/src/client.rs": {
15
+ "role": "Lite disables parallel_tool_calls despite the model's parallel capability",
16
+ "sha256": "0d590b240bdbfeafacbe597a02e4bdce467f8850d597814f214184dfb89ee510"
17
+ },
18
+ "codex-rs/core/src/tools/spec_plan.rs": {
19
+ "role": "Lite excludes hosted web/image; standalone image remains gated by provider, feature and account capabilities",
20
+ "sha256": "8dfa01644163e4826021815d064444ec12799aeaf8f7a952ec6289319f2fd36e"
21
+ }
22
+ },
23
+ "catalogReference": {
24
+ "package": "@earendil-works/pi-ai",
25
+ "version": "0.87.1",
26
+ "path": "dist/providers/data/openai-codex.json",
27
+ "sha256": "4bb30a26d1b40e1f67c9f24891fca0ce25b030bc4cbbb529be78608cd4466fdf",
28
+ "fields": ["api", "input", "thinkingLevelMap", "contextWindow", "maxTokens", "compat"]
29
+ },
30
+ "models": ["openai-codex/gpt-6-sol", "openai-codex/gpt-6-luna"],
31
+ "limits": [
32
+ "Source and fixture verification, not real account or endpoint acceptance.",
33
+ "Pi owns model descriptors and effort mappings; no ultra, context-window or pricing override.",
34
+ "Fast mode disabled: no verified Codex billing multiplier.",
35
+ "Public API 30m cache TTL is not evidence for a Codex Lite wire parameter.",
36
+ "Standalone tools retain existing explicit configuration and account gates."
37
+ ],
38
+ "localImplementations": [
39
+ "pi-codex-runtime/src/model-catalog/default-models.json",
40
+ "pi-codex-minimal-tools/src/model-catalog/default-models.json"
41
+ ]
42
+ }
package/src/broker.ts CHANGED
@@ -20,7 +20,7 @@ export interface CodexBroker {
20
20
  readonly closed: boolean;
21
21
  readonly tools: Map<PackageToolName, InstalledTool>;
22
22
  coreEnabled: boolean;
23
- codeModeDefinitions?: Map<string, { parameters: unknown; description: string }>;
23
+ codeModeDefinitions?: Map<string, { parameters: unknown; description: string; providerId: string }>;
24
24
  claim(name: string): boolean;
25
25
  addPresentation(name: string, presentation: ProviderPresentation): void;
26
26
  presentation: ProviderPresentation;
@@ -2,17 +2,21 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
2
2
  import { getCodexBroker } from "./broker.js";
3
3
  import { codeModeOwner, type DirectBinding } from "./code-mode-owner.js";
4
4
  import type { PackageToolName } from "./capabilities.js";
5
+ import { randomUUID } from "node:crypto";
5
6
 
6
7
  /** Unique public schema reference proves which registration won Pi's registry.
7
8
  * If a future Pi clones metadata, cooperation fails closed instead of claiming
8
9
  * a name/source belonging to another extension. */
9
- export function registerCodeModeOwnedTool(pi: ExtensionAPI, definition: Record<string, unknown>): void {
10
+ export function registerCodeModeOwnedTool(pi: ExtensionAPI, definition: Record<string, unknown>, providerId: string): void {
10
11
  if (typeof definition.name !== "string" || typeof definition.description !== "string"
11
- || !definition.parameters || typeof definition.parameters !== "object") throw new Error("Invalid owned Code Mode tool definition");
12
+ || !definition.parameters || typeof definition.parameters !== "object"
13
+ || !/^[a-z][a-z0-9_]{0,39}$/.test(providerId)) throw new Error("Invalid owned Code Mode tool definition");
12
14
  const broker = getCodexBroker(pi);
15
+ const definitions = broker.codeModeDefinitions ??= new Map();
16
+ if (definitions.has(definition.name)) throw new Error(`Duplicate Code Mode tool definition: ${definition.name}`);
13
17
  const owned = { ...definition, name: definition.name, description: definition.description, parameters: { ...definition.parameters } };
14
- (broker.codeModeDefinitions ??= new Map()).set(owned.name, owned);
15
18
  pi.registerTool(owned as never);
19
+ definitions.set(owned.name, { ...owned, providerId });
16
20
  }
17
21
 
18
22
  export interface NestedCodeModeContext {
@@ -28,36 +32,108 @@ export interface OwnedCodeModeTool {
28
32
  parameters: unknown;
29
33
  effect: "read" | "write";
30
34
  parallel?: boolean;
35
+ requires?: readonly string[];
36
+ requiredPolicies?: readonly string[];
37
+ approval?: string;
38
+ availability?: { state: "available" | "unavailable" | "not-ready" | "failed"; reason?: string };
31
39
  direct?: DirectBinding;
32
40
  invoke(input: unknown, context: NestedCodeModeContext): Promise<{ value: unknown }>;
33
41
  }
42
+ const unavailableInvoke = async (): Promise<{ value: unknown }> => { throw new Error("Code Mode tool requirements were not negotiated"); };
34
43
 
35
- /** Structural client for the optional v1 bus contract. Neither side installs
44
+ /** Structural client for the optional v2/v1 bus contract. Neither side installs
36
45
  * the other package; all authority still comes from Code Mode's exact grants. */
37
46
  export function registerCodeModeContribution(pi: ExtensionAPI, id: string,
38
47
  tools: (context: ExtensionContext) => readonly OwnedCodeModeTool[]): void {
39
48
  let context: ExtensionContext | undefined;
40
49
  let disposed = false;
41
- const changed = () => pi.events.emit("@oai404iao/pi-code-mode:changed/v1", { version: 1 });
50
+ let resolved: readonly OwnedCodeModeTool[] | undefined;
51
+ let registration: Readonly<{ owner: string; instanceId: string; revision: number }> = Object.freeze({ owner: id, instanceId: randomUUID(), revision: 1 });
52
+ let accepted = new WeakSet<object>();
53
+ const generations = new Map<string, number>();
54
+ const changed = (phase: "withdrawn" | "ready" | "disposed") => {
55
+ const change = Object.freeze({ protocol: 2, registration, kind: "execution", phase });
56
+ pi.events.emit("@oai404iao/pi-code-mode:changed/v2", change);
57
+ pi.events.emit("@oai404iao/pi-code-mode:changed/v1", { version: 1, change });
58
+ };
59
+ const snapshot = (features: readonly unknown[] = [], legacy = false) => {
60
+ const broker = getCodexBroker(pi);
61
+ const offered = context ? (resolved ??= tools(context).map((tool) => ({ ...tool }))) : [];
62
+ if (offered.length > 64) throw new Error("Code Mode declaration budget exceeded");
63
+ const declarations = offered.map((tool) => {
64
+ if (tool.requires !== undefined && (!Array.isArray(tool.requires) || tool.requires.length > 32))
65
+ throw new Error("Invalid Code Mode feature requirements");
66
+ const requires = [...new Set([...(tool.requires ?? []), ...(tool.approval ? ["approval/1"] : []),
67
+ ...(tool.requiredPolicies?.length ? ["required-policies/1"] : [])])];
68
+ if (requires.length > 32 || requires.some((feature) => typeof feature !== "string" || feature.length > 128
69
+ || !/^[a-z][a-z0-9-]*\/[1-9][0-9]*$/.test(feature)))
70
+ throw new Error("Invalid Code Mode feature requirements");
71
+ return { ...tool, requires };
72
+ });
73
+ return { id, tools: declarations.filter((tool) => !legacy || (!tool.requires.length && !tool.requiredPolicies?.length
74
+ && tool.approval === undefined && (!tool.availability || tool.availability.state === "available"))).map((tool) => {
75
+ if (tool.requires.some((feature) => !features.includes(feature)) || (tool.availability && tool.availability.state !== "available"))
76
+ return { ...tool, direct: undefined, invoke: unavailableInvoke };
77
+ const owned = broker.tools.get(tool.name as PackageToolName);
78
+ const definition = broker.codeModeDefinitions?.get(tool.name);
79
+ return { ...tool, direct: owned && definition?.providerId === id
80
+ ? codeModeOwner(pi, tool.name, owned, definition)?.binding : undefined };
81
+ }) };
82
+ };
83
+ const offV2 = pi.events.on("@oai404iao/pi-code-mode:discover/v2", (value) => {
84
+ const request = value as { hello?: { protocol?: number; instanceId?: string; generation?: number; features?: unknown[] };
85
+ offer?: (offer: unknown) => { status: string } } | undefined;
86
+ const hello = request?.hello;
87
+ if (disposed || !hello || hello.protocol !== 2 || !hello.instanceId || hello.instanceId.length > 128
88
+ || !Number.isSafeInteger(hello.generation) || hello.generation! < 1 || !Array.isArray(hello.features)
89
+ || hello.features.length > 32 || typeof request?.offer !== "function") return;
90
+ const previous = generations.get(hello.instanceId);
91
+ if ((previous !== undefined && hello.generation! < previous) || (previous === undefined && generations.size >= 64)) return;
92
+ generations.set(hello.instanceId, hello.generation!);
93
+ try {
94
+ const provider = snapshot(hello.features);
95
+ const availability = { state: context ? "available" : "not-ready" };
96
+ if (request.offer({ kind: "provider", registration, requires: [], availability, provider }).status === "compatible") accepted.add(hello);
97
+ } catch {
98
+ request.offer({ kind: "provider", registration, requires: [], availability: { state: "failed", reason: "owner-not-ready" }, provider: { id, tools: [] } });
99
+ }
100
+ });
42
101
  const off = pi.events.on("@oai404iao/pi-code-mode:discover/v1", (value) => {
43
- const discovery = value as { version?: number; provider?: (provider: unknown) => void } | undefined;
102
+ const discovery = value as { version?: number; provider?: (provider: unknown) => void;
103
+ consumer?: object; receipts?: { registration: object; consumer: object }[] } | undefined;
44
104
  if (!disposed && context && discovery?.version === 1 && typeof discovery.provider === "function") {
45
- const broker = getCodexBroker(pi);
46
- discovery.provider({ id, tools: tools(context).map((tool) => {
47
- const owned = broker.tools.get(tool.name as PackageToolName);
48
- return { ...tool, direct: owned ? codeModeOwner(pi, tool.name, owned, broker.codeModeDefinitions?.get(tool.name))?.binding : undefined };
49
- }) });
105
+ if (discovery.consumer) {
106
+ const hello = discovery.consumer as { protocol?: number; instanceId?: string; generation?: number };
107
+ if (hello.protocol !== 2 || typeof hello.instanceId !== "string" || !hello.instanceId || hello.instanceId.length > 128
108
+ || !Number.isSafeInteger(hello.generation) || hello.generation! < 1) return;
109
+ const previous = generations.get(hello.instanceId);
110
+ if ((previous !== undefined && hello.generation! < previous) || (previous === undefined && generations.size >= 64)) return;
111
+ generations.set(hello.instanceId, hello.generation!);
112
+ }
113
+ if (discovery.consumer && accepted.has(discovery.consumer) && Array.isArray(discovery.receipts) && discovery.receipts.length <= 80
114
+ && discovery.receipts.some((receipt) => receipt.consumer === discovery.consumer && receipt.registration === registration)) return;
115
+ let provider: ReturnType<typeof snapshot>;
116
+ // Validate the whole declaration before filtering legacy tools:
117
+ // a failed v2 provider must not reappear as a partial v1 mirror.
118
+ try { provider = snapshot([], true); } catch { return; }
119
+ if (provider.tools.length) discovery.provider(provider);
50
120
  }
51
121
  });
52
122
  const update = (_event: unknown, ctx: ExtensionContext) => {
53
123
  if (disposed) return;
124
+ context = undefined;
125
+ resolved = undefined;
126
+ changed("withdrawn");
54
127
  context = ctx;
55
- changed();
128
+ registration = Object.freeze({ ...registration, revision: registration.revision + 1 });
129
+ accepted = new WeakSet();
130
+ changed("ready");
56
131
  };
57
132
  pi.on("session_start", update);
58
133
  pi.on("model_select", update);
134
+ pi.on("session_tree", update);
59
135
  pi.on("session_shutdown", () => {
60
136
  if (disposed) return;
61
- disposed = true; context = undefined; off(); changed();
137
+ disposed = true; context = undefined; off(); offV2(); changed("disposed");
62
138
  });
63
139
  }
@@ -12,34 +12,85 @@ interface Control {
12
12
  interface Factory {
13
13
  version: 1; create(pi: ExtensionAPI, options: { name: string; sourcePath: string }): Control;
14
14
  }
15
- export interface OwnerState { identity: string; replaced?: boolean; factory?: Factory; control?: Control }
15
+ export interface OwnerState {
16
+ identity: string; replaced?: boolean; factory?: Factory; control?: Control;
17
+ transitioning?: boolean; pendingDispose?: () => void; closed?: boolean;
18
+ }
16
19
  export const OWNER_CHANGED = "@oai404iao/pi-code-mode:direct-owner-changed/v1";
20
+
21
+ function retire(state: OwnerState): void {
22
+ if (state.control && !state.pendingDispose) {
23
+ const old = state.control;
24
+ state.pendingDispose = () => old.dispose();
25
+ }
26
+ state.pendingDispose?.();
27
+ state.pendingDispose = undefined;
28
+ state.control = undefined;
29
+ state.factory = undefined;
30
+ }
31
+ export function disposeCodeModeOwner(tool: { codeModeOwner?: OwnerState }, close = true): void {
32
+ const state = tool.codeModeOwner;
33
+ if (!state) return;
34
+ if (close) state.closed = true;
35
+ if (state.transitioning) return;
36
+ state.transitioning = true;
37
+ try { retire(state); } finally { state.transitioning = false; }
38
+ }
39
+
17
40
  /** Optional v1 bus client. No private-package dependency or global registry. */
18
41
  export function codeModeOwner(pi: ExtensionAPI, name: string, tool: { registered: boolean; codeModeOwner?: OwnerState },
19
42
  expected?: { parameters: unknown; description: string }): Control | undefined {
20
- if (!["apply_patch", "web_search"].includes(name) || !tool.registered) return;
43
+ if (!["apply_patch", "web_search"].includes(name) || tool.codeModeOwner?.transitioning || tool.codeModeOwner?.closed) return;
44
+ if (!tool.registered) { disposeCodeModeOwner(tool, false); return; }
21
45
  const info = pi.getAllTools?.().find((item) => item.name === name);
22
- if (!info?.sourceInfo || ["builtin", "sdk"].includes(info.sourceInfo.source)) return tool.codeModeOwner?.control;
23
- if (!expected || info.parameters !== expected.parameters || info.description !== expected.description) {
24
- if (expected) (tool.codeModeOwner ??= { identity: "" }).replaced = true;
46
+ if (!info?.sourceInfo || ["builtin", "sdk"].includes(info.sourceInfo.source)
47
+ || !expected || info.parameters !== expected.parameters || info.description !== expected.description) {
48
+ (tool.codeModeOwner ??= { identity: "" }).replaced = true;
49
+ disposeCodeModeOwner(tool, false);
25
50
  return;
26
51
  }
27
52
  const identity = JSON.stringify([info.sourceInfo, info.description, info.parameters, info.promptGuidelines]);
53
+ const stillOwns = () => {
54
+ const current = pi.getAllTools().find((item) => item.name === name);
55
+ return Boolean(current && current.parameters === expected.parameters
56
+ && JSON.stringify([current.sourceInfo, current.description, current.parameters, current.promptGuidelines]) === identity);
57
+ };
28
58
  const state = tool.codeModeOwner ??= { identity };
29
59
  // Never rebind a replaced registration, even if the optional factory reloads.
30
- if (state.identity !== identity) { state.replaced = true; return state.control; }
31
- if (state.replaced) return state.control;
60
+ if (state.identity !== identity) state.replaced = true;
61
+ if (state.replaced) { disposeCodeModeOwner(tool, false); return; }
32
62
  const factories: Factory[] = [];
33
63
  pi.events.emit("@oai404iao/pi-code-mode:direct-owner/v1", { version: 1, accept(value: Factory) {
34
64
  if (factories.length < 2) factories.push(value);
35
65
  } });
36
66
  const factory = factories.length === 1 && factories[0]?.version === 1
37
67
  && typeof factories[0].create === "function" ? factories[0] : undefined;
38
- if (factory !== state.factory) {
39
- const old = state.control;
40
- state.control = undefined; state.factory = factory;
41
- old?.dispose();
42
- if (factory) state.control = factory.create(pi, { name, sourcePath: info.sourceInfo.path });
68
+ if (state.closed) return;
69
+ if (!stillOwns()) {
70
+ state.replaced = true;
71
+ disposeCodeModeOwner(tool, false);
72
+ return;
73
+ }
74
+ if (state.pendingDispose || factory !== state.factory || (factory && !state.control)) {
75
+ state.transitioning = true;
76
+ try {
77
+ retire(state);
78
+ if (!stillOwns()) state.replaced = true;
79
+ if (state.closed || state.replaced) return;
80
+ if (factory) {
81
+ const control = factory.create(pi, { name, sourcePath: info.sourceInfo.path });
82
+ if (!control || control.binding?.version !== 1 || control.binding.name !== name
83
+ || typeof control.binding.acquire !== "function" || typeof control.projectActive !== "function"
84
+ || typeof control.reconcile !== "function" || typeof control.dispose !== "function") {
85
+ if (typeof control?.dispose === "function") state.pendingDispose = () => control.dispose();
86
+ throw new Error("Invalid Code Mode owner control");
87
+ }
88
+ state.control = control;
89
+ state.factory = factory;
90
+ if (!stillOwns()) state.replaced = true;
91
+ if (state.closed || state.replaced) { retire(state); return; }
92
+ }
93
+ } finally { state.transitioning = false; }
43
94
  }
44
95
  return state.control;
45
96
  }
@@ -150,6 +150,35 @@
150
150
  "endpoint": "codex"
151
151
  }
152
152
  },
153
+ {
154
+ "id": "openai-codex/gpt-6-sol",
155
+ "enabled": true,
156
+ "responses": {
157
+ "providerShim": true,
158
+ "endpoint": "codex",
159
+ "mode": "lite",
160
+ "systemPromptPlacement": "developer",
161
+ "transport": "auto",
162
+ "websocketPrewarm": true,
163
+ "reasoningSummary": "none"
164
+ },
165
+ "tools": {
166
+ "parallelCalls": false,
167
+ "applyPatch": "custom",
168
+ "webSearch": {
169
+ "implementation": "standalone",
170
+ "contentTypes": ["text", "image"]
171
+ },
172
+ "imageGeneration": "standalone",
173
+ "viewImage": false
174
+ },
175
+ "compaction": "responses",
176
+ "fast": false
177
+ },
178
+ {
179
+ "id": "openai-codex/gpt-6-luna",
180
+ "extends": "openai-codex/gpt-6-sol"
181
+ },
153
182
  {
154
183
  "id": "openai-codex/gpt-6-astra",
155
184
  "extends": "openai/gpt-5.6-sol",
@@ -8,7 +8,7 @@ import {
8
8
  import { installCodexIdentityLifecycle } from "./codex-identity-extension.js";
9
9
  import { loadModelSettings } from "./model-catalog/runtime.js";
10
10
  import { loadSettings } from "./settings.js";
11
- import { codeModeOwner, OWNER_CHANGED } from "./code-mode-owner.js";
11
+ import { codeModeOwner, disposeCodeModeOwner, OWNER_CHANGED } from "./code-mode-owner.js";
12
12
 
13
13
  function enableDefinitions(broker: CodexBroker) {
14
14
  for (const tool of broker.tools.values()) {
@@ -38,12 +38,11 @@ export function ensureCodexServices(pi: ExtensionAPI): CodexBroker {
38
38
  for (const [name, index] of [...suppressed].sort(([, a], [, b]) => a - b)) {
39
39
  if (!active.includes(name)) active.splice(Math.min(index, active.length), 0, name);
40
40
  }
41
- suppressed.clear();
42
41
  return active;
43
42
  };
44
43
  let latest: ExtensionContext | undefined;
45
44
  let syncing = false;
46
- const sync = (ctx: ExtensionContext) => {
45
+ const sync = (ctx: ExtensionContext, fromTree = false) => {
47
46
  latest = ctx;
48
47
  if (syncing) return;
49
48
  syncing = true;
@@ -72,7 +71,7 @@ export function ensureCodexServices(pi: ExtensionAPI): CodexBroker {
72
71
  );
73
72
  const desired = available && owned.registered && capabilities[name].enabled && !hostedWithoutCore;
74
73
  if (!desired) active.delete(name);
75
- else if (settings.autoEnable) active.add(name);
74
+ else if (settings.autoEnable && !(fromTree && control?.activeIntent !== undefined)) active.add(name);
76
75
  }
77
76
  const ownsPatch = broker.tools.has("apply_patch") && !broker.tools.get("apply_patch")?.codeModeOwner?.replaced;
78
77
  if (ownsPatch && active.has("apply_patch")) {
@@ -90,6 +89,7 @@ export function ensureCodexServices(pi: ExtensionAPI): CodexBroker {
90
89
  for (const name of physical) if (!next.includes(name)) next.push(name);
91
90
  if (!ownsPatch || !active.has("apply_patch")) restore(next);
92
91
  if (next.join("\0") !== current.join("\0")) pi.setActiveTools(next);
92
+ if (!ownsPatch || !active.has("apply_patch")) suppressed.clear();
93
93
  for (const control of controls.values()) control.reconcile();
94
94
  } finally { syncing = false; }
95
95
  };
@@ -98,21 +98,34 @@ export function ensureCodexServices(pi: ExtensionAPI): CodexBroker {
98
98
  });
99
99
  pi.on("session_start", (_event, ctx) => {
100
100
  broker.presentation.clear();
101
+ suppressed.clear();
101
102
  sync(ctx);
102
103
  });
103
104
  pi.on("model_select", (_event, ctx) => sync(ctx));
104
105
  pi.on("thinking_level_select", (_event, ctx) => sync(ctx));
106
+ pi.on("session_tree", (_event, ctx) => {
107
+ // Restoration receipts belong to the previous physical loadout, not
108
+ // to tools absent from the newly selected branch.
109
+ suppressed.clear();
110
+ sync(ctx, true);
111
+ });
105
112
  pi.on("agent_end", () => broker.presentation.scheduleFlush());
106
113
  pi.on("session_shutdown", () => {
107
114
  offOwner();
108
- for (const tool of broker.tools.values()) tool.codeModeOwner?.control?.dispose();
109
- try { broker.presentation.flush(); }
110
- finally {
111
- broker.presentation.clear();
115
+ const errors: unknown[] = [];
116
+ for (const tool of broker.tools.values()) if (tool.codeModeOwner) tool.codeModeOwner.closed = true;
117
+ for (const tool of broker.tools.values()) {
118
+ try { disposeCodeModeOwner(tool); } catch (error) { errors.push(error); }
119
+ }
120
+ try { broker.presentation.flush(); } catch (error) { errors.push(error); }
121
+ try { broker.presentation.clear(); } catch (error) { errors.push(error); }
122
+ try {
112
123
  const current = pi.getActiveTools?.() ?? [];
113
124
  const next = restore([...current]);
114
125
  if (next.join("\0") !== current.join("\0")) pi.setActiveTools(next);
115
- }
126
+ suppressed.clear();
127
+ } catch (error) { errors.push(error); }
128
+ if (errors.length) throw new AggregateError(errors, "Codex owner shutdown failed");
116
129
  });
117
130
  return broker;
118
131
  }