@oai404iao/pi-codex-runtime 0.3.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oai404iao/pi-codex-runtime",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": false,
5
5
  "description": "Unpublished pi-codex-runtime workspace for the staged Codex split",
6
6
  "type": "module",
@@ -64,5 +64,5 @@
64
64
  "engines": {
65
65
  "node": ">=22.19.0"
66
66
  },
67
- "gitHead": "b02484cecad9081886797bf9b4438135aaae1e07"
67
+ "gitHead": "c791e1192d0547834c3e2f2bae7853fb214f9808"
68
68
  }
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
  }
@@ -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
  }