@oai404iao/pi-codex-minimal-tools 1.3.0 → 1.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
@@ -29,6 +29,27 @@ The extension adds:
29
29
  Unknown models are not guessed. They keep Pi's native provider implementation
30
30
  and do not receive package tools.
31
31
 
32
+ ## Codex Identity
33
+
34
+ This package owns Codex wire identity independently from Pi's session-file id:
35
+
36
+ - a root has `SessionId === ThreadId`;
37
+ - subagents share the root `SessionId` and receive their own `ThreadId`;
38
+ - `prompt_cache_key` is the shared `SessionId`;
39
+ - `x-client-request-id` is the current `ThreadId`;
40
+ - TurnId and `x-codex-turn-state` are scoped to one logical agent turn;
41
+ - WindowId is thread-scoped and advances after successful compaction;
42
+ - the installation UUIDv4 is stored at
43
+ `<PI_CODING_AGENT_DIR>/pi-codex-minimal-tools/installation_id`.
44
+
45
+ SSE, Responses Lite, WebSocket, native compaction, and standalone `web.run`
46
+ are projections of the same request identity snapshot.
47
+
48
+ The package also exports
49
+ `@oai404iao/pi-codex-minimal-tools/subagent-inline`. Its named inline
50
+ extension installs only identity lifecycle hooks for SDK-created child
51
+ sessions; it does not register providers, tools, commands, or renderers.
52
+
32
53
  ## Install
33
54
 
34
55
  After `1.3.0` is available on npm:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oai404iao/pi-codex-minimal-tools",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Model-profiled Codex Responses tools for Pi: WebSocket, Lite, web search, images, compaction, and apply_patch",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,6 +12,10 @@
12
12
  "url": "https://github.com/oai404iao/omp/issues"
13
13
  },
14
14
  "type": "module",
15
+ "exports": {
16
+ ".": "./src/index.ts",
17
+ "./subagent-inline": "./src/subagent-inline.ts"
18
+ },
15
19
  "keywords": [
16
20
  "pi-package",
17
21
  "pi",
@@ -83,5 +87,5 @@
83
87
  "optional": true
84
88
  }
85
89
  },
86
- "gitHead": "596d799c6f7db3508b6d46bb05cdca6ea9e3b716"
90
+ "gitHead": "a93559be8f8b02739310c492b36270b13607fbf9"
87
91
  }
@@ -0,0 +1,339 @@
1
+ import {
2
+ SessionManager,
3
+ type ExtensionAPI,
4
+ type InlineExtension,
5
+ type SessionEntry,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ advanceCodexWindow,
9
+ beginCodexTurn,
10
+ codexThreadIdentityFor,
11
+ createCodexChildIdentity,
12
+ createCodexRootIdentity,
13
+ endCodexTurn,
14
+ parseCodexThreadIdentity,
15
+ registerCodexThreadIdentity,
16
+ type CodexThreadIdentity,
17
+ } from "./codex-wire-identity.js";
18
+
19
+ export const CODEX_IDENTITY_CUSTOM_TYPE = "pi-codex/thread-identity";
20
+ const SUBAGENT_DESCRIPTOR_CUSTOM_TYPE = "pi-subagent/descriptor";
21
+ const SUBAGENT_LINEAGE_CUSTOM_TYPE = "pi-subagent/lineage";
22
+ const IDENTITY_LIFECYCLE_SYMBOL = Symbol.for(
23
+ "@oai404iao/pi-codex/identity-lifecycle/v1",
24
+ );
25
+
26
+ export interface CodexIdentitySessionView {
27
+ getSessionId(): string;
28
+ getSessionFile(): string | undefined;
29
+ getSessionDir(): string;
30
+ getCwd(): string;
31
+ getEntries(): SessionEntry[];
32
+ getBranch?(): SessionEntry[];
33
+ appendCustomEntry?(customType: string, data?: unknown): string;
34
+ }
35
+
36
+ interface SubagentLineage {
37
+ agentId: string;
38
+ parentAgentId: string;
39
+ parentPiSessionId: string;
40
+ parentSessionFile?: string;
41
+ relation: "spawn" | "fork";
42
+ agentName?: string;
43
+ }
44
+
45
+ function record(value: unknown): Record<string, unknown> | undefined {
46
+ return value && typeof value === "object" && !Array.isArray(value)
47
+ ? value as Record<string, unknown>
48
+ : undefined;
49
+ }
50
+
51
+ function readIdentity(
52
+ entries: readonly SessionEntry[],
53
+ piSessionId?: string,
54
+ ): CodexThreadIdentity | undefined {
55
+ for (let index = entries.length - 1; index >= 0; index--) {
56
+ const entry = entries[index];
57
+ if (
58
+ entry?.type !== "custom"
59
+ || entry.customType !== CODEX_IDENTITY_CUSTOM_TYPE
60
+ ) {
61
+ continue;
62
+ }
63
+ try {
64
+ const identity = parseCodexThreadIdentity(entry.data);
65
+ if (!piSessionId || identity.piSessionId === piSessionId) {
66
+ return identity;
67
+ }
68
+ } catch {
69
+ // A newer valid entry can repair a corrupt historical checkpoint.
70
+ }
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ function readSubagentLineage(
76
+ entries: readonly SessionEntry[],
77
+ ): SubagentLineage | undefined {
78
+ for (let index = entries.length - 1; index >= 0; index--) {
79
+ const entry = entries[index];
80
+ if (
81
+ entry?.type === "custom"
82
+ && entry.customType === SUBAGENT_LINEAGE_CUSTOM_TYPE
83
+ ) {
84
+ const lineage = record(entry.data);
85
+ if (
86
+ lineage?.version !== 1
87
+ || lineage.openAIIdentity !== true
88
+ || (lineage.relation !== "spawn" && lineage.relation !== "fork")
89
+ || typeof lineage.agentId !== "string"
90
+ || typeof lineage.parentAgentId !== "string"
91
+ || typeof lineage.parentPiSessionId !== "string"
92
+ ) {
93
+ return undefined;
94
+ }
95
+ return {
96
+ agentId: lineage.agentId,
97
+ parentAgentId: lineage.parentAgentId,
98
+ parentPiSessionId: lineage.parentPiSessionId,
99
+ relation: lineage.relation,
100
+ ...(typeof lineage.parentSessionFile === "string"
101
+ ? { parentSessionFile: lineage.parentSessionFile }
102
+ : {}),
103
+ ...(typeof lineage.agentName === "string"
104
+ ? { agentName: lineage.agentName }
105
+ : {}),
106
+ };
107
+ }
108
+ if (
109
+ entry?.type !== "custom"
110
+ || entry.customType !== SUBAGENT_DESCRIPTOR_CUSTOM_TYPE
111
+ ) {
112
+ continue;
113
+ }
114
+ const descriptor = record(entry.data);
115
+ const runtime = record(descriptor?.runtime);
116
+ if (
117
+ descriptor?.version !== 2
118
+ || runtime?.openAIIdentity !== true
119
+ || (descriptor.provider !== "spawn" && descriptor.provider !== "fork")
120
+ || typeof descriptor.agentId !== "string"
121
+ || typeof descriptor.parentAgentId !== "string"
122
+ || typeof descriptor.parentPiSessionId !== "string"
123
+ ) {
124
+ return undefined;
125
+ }
126
+ const agent = record(descriptor.agent);
127
+ return {
128
+ agentId: descriptor.agentId,
129
+ parentAgentId: descriptor.parentAgentId,
130
+ parentPiSessionId: descriptor.parentPiSessionId,
131
+ relation: descriptor.provider,
132
+ ...(typeof descriptor.parentSessionFile === "string"
133
+ ? { parentSessionFile: descriptor.parentSessionFile }
134
+ : {}),
135
+ ...(typeof agent?.name === "string" ? { agentName: agent.name } : {}),
136
+ };
137
+ }
138
+ return undefined;
139
+ }
140
+
141
+ function appendIdentity(
142
+ session: CodexIdentitySessionView,
143
+ identity: CodexThreadIdentity,
144
+ appendCurrent?: (identity: CodexThreadIdentity) => void,
145
+ ): void {
146
+ if (appendCurrent) {
147
+ appendCurrent(identity);
148
+ return;
149
+ }
150
+ session.appendCustomEntry?.(
151
+ CODEX_IDENTITY_CUSTOM_TYPE,
152
+ structuredClone(identity),
153
+ );
154
+ }
155
+
156
+ function openParentSession(
157
+ session: CodexIdentitySessionView,
158
+ lineage: SubagentLineage,
159
+ ): CodexIdentitySessionView | undefined {
160
+ if (!lineage.parentSessionFile) return undefined;
161
+ try {
162
+ return SessionManager.open(
163
+ lineage.parentSessionFile,
164
+ session.getSessionDir(),
165
+ session.getCwd(),
166
+ );
167
+ } catch {
168
+ return undefined;
169
+ }
170
+ }
171
+
172
+ function resolveParentIdentity(
173
+ session: CodexIdentitySessionView,
174
+ lineage: SubagentLineage,
175
+ ): CodexThreadIdentity {
176
+ const active = codexThreadIdentityFor(lineage.parentPiSessionId);
177
+ if (active) return active;
178
+
179
+ const parent = openParentSession(session, lineage);
180
+ if (parent) {
181
+ const persisted = readIdentity(
182
+ parent.getEntries(),
183
+ lineage.parentPiSessionId,
184
+ );
185
+ if (persisted) return registerCodexThreadIdentity(persisted);
186
+
187
+ // pi-codex-minimal-tools owns the Codex identity even when the parent
188
+ // happened to use a non-Codex model before creating this OpenAI child.
189
+ const root = createCodexRootIdentity(lineage.parentPiSessionId);
190
+ appendIdentity(parent, root);
191
+ return registerCodexThreadIdentity(root);
192
+ }
193
+
194
+ // Ephemeral parents have no file to reopen. They are still represented by a
195
+ // process-local root identity for the lifetime of the child tree.
196
+ const root = createCodexRootIdentity(lineage.parentPiSessionId);
197
+ return registerCodexThreadIdentity(root);
198
+ }
199
+
200
+ export function ensureCodexSessionIdentity(
201
+ session: CodexIdentitySessionView,
202
+ options: {
203
+ sessionStartReason?: string;
204
+ appendCurrent?: (identity: CodexThreadIdentity) => void;
205
+ } = {},
206
+ ): CodexThreadIdentity {
207
+ const piSessionId = session.getSessionId();
208
+ const persisted = readIdentity(session.getEntries(), piSessionId);
209
+ if (persisted) return registerCodexThreadIdentity(persisted);
210
+
211
+ const lineage = readSubagentLineage(session.getEntries());
212
+ let identity: CodexThreadIdentity;
213
+ if (lineage) {
214
+ identity = createCodexChildIdentity(
215
+ piSessionId,
216
+ resolveParentIdentity(session, lineage),
217
+ {
218
+ relation: lineage.relation,
219
+ agentName: lineage.agentName,
220
+ },
221
+ );
222
+ } else {
223
+ const copied = readIdentity(session.getEntries());
224
+ identity = createCodexRootIdentity(piSessionId, {
225
+ ...(options.sessionStartReason === "fork" && copied
226
+ ? { forkedFromThreadId: copied.threadId }
227
+ : {}),
228
+ });
229
+ }
230
+
231
+ appendIdentity(session, identity, options.appendCurrent);
232
+ return registerCodexThreadIdentity(identity);
233
+ }
234
+
235
+ /**
236
+ * Install only the session/turn/window lifecycle needed by the Codex provider.
237
+ * It deliberately does not register providers, tools, commands, or renderers.
238
+ */
239
+ export function installCodexIdentityLifecycle(pi: ExtensionAPI): void {
240
+ const guard = pi as unknown as Record<PropertyKey, unknown>;
241
+ if (guard[IDENTITY_LIFECYCLE_SYMBOL]) return;
242
+ guard[IDENTITY_LIFECYCLE_SYMBOL] = true;
243
+
244
+ const ensure = (
245
+ ctx: {
246
+ sessionManager?: Partial<CodexIdentitySessionView>;
247
+ },
248
+ sessionStartReason?: string,
249
+ ): CodexThreadIdentity | undefined => {
250
+ const session = ctx.sessionManager;
251
+ if (!session || typeof session.getSessionId !== "function") {
252
+ return undefined;
253
+ }
254
+ const piSessionId = session.getSessionId();
255
+ if (typeof session.getEntries !== "function") {
256
+ const existing = codexThreadIdentityFor(piSessionId);
257
+ if (existing) return existing;
258
+ return registerCodexThreadIdentity(
259
+ createCodexRootIdentity(piSessionId),
260
+ );
261
+ }
262
+ return ensureCodexSessionIdentity(session as CodexIdentitySessionView, {
263
+ sessionStartReason,
264
+ appendCurrent: (identity) => {
265
+ pi.appendEntry(
266
+ CODEX_IDENTITY_CUSTOM_TYPE,
267
+ structuredClone(identity),
268
+ );
269
+ },
270
+ });
271
+ };
272
+
273
+ pi.on("session_start", async (event, ctx) => {
274
+ ensure(ctx as unknown as { sessionManager: CodexIdentitySessionView }, event.reason);
275
+ });
276
+
277
+ pi.on("before_agent_start", async (_event, ctx) => {
278
+ const typed = ctx as unknown as { sessionManager: CodexIdentitySessionView };
279
+ const identity = ensure(typed);
280
+ if (!identity) return;
281
+ const lineage =
282
+ typeof typed.sessionManager.getEntries === "function"
283
+ ? readSubagentLineage(typed.sessionManager.getEntries())
284
+ : undefined;
285
+ beginCodexTurn(identity.piSessionId, {
286
+ ...(lineage?.parentPiSessionId
287
+ ? { parentPiSessionId: lineage.parentPiSessionId }
288
+ : {}),
289
+ });
290
+ });
291
+
292
+ pi.on("agent_settled", async (_event, ctx) => {
293
+ const piSessionId = ctx.sessionManager?.getSessionId?.();
294
+ if (piSessionId) endCodexTurn(piSessionId);
295
+ });
296
+
297
+ pi.on("session_compact", async (event, ctx) => {
298
+ const typed = ctx as unknown as { sessionManager: CodexIdentitySessionView };
299
+ if (!ensure(typed)) return;
300
+ const identity = advanceCodexWindow(
301
+ typed.sessionManager.getSessionId(),
302
+ event.compactionEntry.id,
303
+ );
304
+ pi.appendEntry(
305
+ CODEX_IDENTITY_CUSTOM_TYPE,
306
+ structuredClone(identity),
307
+ );
308
+ });
309
+
310
+ pi.on("session_tree", async (_event, ctx) => {
311
+ const typed = ctx as unknown as { sessionManager: CodexIdentitySessionView };
312
+ const piSessionId = typed.sessionManager.getSessionId();
313
+ const branch = typed.sessionManager.getBranch?.()
314
+ ?? typed.sessionManager.getEntries();
315
+ const identity = readIdentity(branch, piSessionId)
316
+ ?? readIdentity(typed.sessionManager.getEntries(), piSessionId);
317
+ if (identity) registerCodexThreadIdentity(identity);
318
+ });
319
+
320
+ pi.on("session_shutdown", async (_event, ctx) => {
321
+ const piSessionId = ctx.sessionManager?.getSessionId?.();
322
+ if (piSessionId) endCodexTurn(piSessionId);
323
+ });
324
+ }
325
+
326
+ /** Named inline extension used by pi-subagent when normal inheritance is off. */
327
+ export function createCodexSubagentInlineExtension(
328
+ options: { parentSessionManager?: CodexIdentitySessionView } = {},
329
+ ): InlineExtension {
330
+ if (options.parentSessionManager) {
331
+ ensureCodexSessionIdentity(options.parentSessionManager);
332
+ }
333
+ return {
334
+ name: "pi-codex-subagent-identity",
335
+ factory: (pi) => {
336
+ installCodexIdentityLifecycle(pi);
337
+ },
338
+ };
339
+ }