@junghanacs/entwurf 0.20.1 → 0.22.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/AGENTS.md +85 -190
- package/BASELINE.md +6 -3
- package/CHANGELOG.md +473 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +332 -60
- package/README.md +100 -22
- package/VERIFY.md +93 -7
- package/docs/acp-backend-rail.md +0 -1
- package/docs/external-mcp-host.md +64 -33
- package/docs/setup-clean-host.md +151 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +154 -116
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
- package/package.json +13 -4
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +233 -29
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +342 -28
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +245 -12
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +8 -3
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-mux-launch-tmux.ts +345 -4
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +653 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +93 -10
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +75 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +700 -0
- package/scripts/smoke-codex-fresh-live.ts +1426 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +125 -10
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-bridge-hook-codex — the OpenAI Codex native-session BIRTH entry (#95).
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS IS, AND WHAT IT DELIBERATELY IS NOT
|
|
5
|
+
*
|
|
6
|
+
* stdin {session_id, transcript_path, cwd, hook_event_name, model, source}
|
|
7
|
+
* -> readCodexBirthEnvelope() strict validation, or REFUSAL
|
|
8
|
+
* -> performCodexBirth() idempotent create/attach of ONE v3 record
|
|
9
|
+
* -> gardenId the session's garden address
|
|
10
|
+
* -> `{}` on stdout, exit 0 always, on every path
|
|
11
|
+
*
|
|
12
|
+
* That is the whole unit. It writes NO pid marker, NO sender marker and NO receiver
|
|
13
|
+
* marker, and each omission has its own reason:
|
|
14
|
+
*
|
|
15
|
+
* 1. NO PID MARKER OF ANY KIND, because on this backend the parent pid is not an
|
|
16
|
+
* identity. `[host]` Measured 2026-09-08 (scripts/raw-codex-measure/README.md,
|
|
17
|
+
* S1b-B): two different TUI windows, two different `session_id`s, and ONE ppid
|
|
18
|
+
* shared by every hook process AND every MCP child of BOTH — the app-server, not
|
|
19
|
+
* the TUI. Neither TUI pid appears in the chain at all. So the Claude/Copilot
|
|
20
|
+
* "shared ancestor is the join key" trick does not merely lack evidence here, it
|
|
21
|
+
* is measurably WRONG: a marker keyed to that pid would name one of two citizens
|
|
22
|
+
* arbitrarily and attribute the other's messages to it. Codex's request-scoped
|
|
23
|
+
* identity rides `_meta.threadId` on the MCP request instead; that is a separate
|
|
24
|
+
* leaf (`lib/meta-sender-identity.ts`), and it needs nothing from this file.
|
|
25
|
+
* 2. NO RECEIVER MARKER, because a receiver marker is a claim that a LIVE process
|
|
26
|
+
* holds a watch. `[source]` Codex has no filesystem-wake vocabulary at all —
|
|
27
|
+
* `watchPaths`/`FileChanged`/`asyncRewake` appear nowhere in its workspace except
|
|
28
|
+
* in a Claude-hooks importer that SKIPS any hook carrying them. Codex receive is
|
|
29
|
+
* native-push and ack-only; nothing about it is armed from here.
|
|
30
|
+
*
|
|
31
|
+
* IDENTITY IS ON THE WIRE, SO NOTHING IS GUESSED. `[host]` The measured 0.153.4
|
|
32
|
+
* `SessionStart` envelope, verbatim:
|
|
33
|
+
*
|
|
34
|
+
* {"session_id":"01a08147-fbad-78d2-b266-e058f322125e",
|
|
35
|
+
* "transcript_path":"<CODEX_HOME>/sessions/2026/09/08/rollout-…-01a08147-….jsonl",
|
|
36
|
+
* "cwd":"/home/junghan/repos/gh/entwurf",
|
|
37
|
+
* "hook_event_name":"SessionStart",
|
|
38
|
+
* "model":"gpt-6-astra",
|
|
39
|
+
* "permission_mode":"default",
|
|
40
|
+
* "source":"startup"}
|
|
41
|
+
*
|
|
42
|
+
* and `[host]` D3 of the same measurement pinned the join: the hook's `session_id`,
|
|
43
|
+
* the app-server's `thread/loaded/list` `threadId`, and the MCP request's
|
|
44
|
+
* `_meta.threadId` are ONE string. Two threads were opened and both matched. That is
|
|
45
|
+
* why this unit stores `session_id` as `nativeSessionId` with no transformation — the
|
|
46
|
+
* send rail (`codex queue --thread <threadId>`) reads that field back verbatim.
|
|
47
|
+
*
|
|
48
|
+
* WHY VALIDATION IS STRICT AND NOT TOLERANT. Every other birth entry in this repo
|
|
49
|
+
* accepts a partially-known envelope because its vendor gave it one (Copilot carries
|
|
50
|
+
* no transcript and no model; omp's shape varies by mode). Codex gives all five axes
|
|
51
|
+
* on every SessionStart, so a MISSING or non-absolute one is not a thin envelope — it
|
|
52
|
+
* is an envelope this unit did not come from, and minting a citizen from it would
|
|
53
|
+
* write a record no live thread can be joined back to. The refusal is cheap and the
|
|
54
|
+
* log names the exact field; a wrong record is neither.
|
|
55
|
+
*
|
|
56
|
+
* THE EVENT NAME IS THE WHOLE TOP-LEVEL PREDICATE, and it is vendor-authoritative.
|
|
57
|
+
* `[source]` A spawned agent does not raise `SessionStart` — it raises `SubagentStart`,
|
|
58
|
+
* and every other subagent source returns before dispatch. `[host]` Measured: exactly
|
|
59
|
+
* ONE `SessionStart` per session, while `SubagentStart` fires per child AND CARRIES THE
|
|
60
|
+
* PARENT'S `session_id` beside its own `agent_id`. So a tolerant reader keyed on
|
|
61
|
+
* `session_id` alone would mint the parent's identity again from a child's event.
|
|
62
|
+
* Hence: `hook_event_name` must be exactly `SessionStart`, and any `agent_id` /
|
|
63
|
+
* `agent_type` key is a refusal even if the event name says otherwise.
|
|
64
|
+
*
|
|
65
|
+
* BIRTH IS ON THE FIRST TURN, NOT AT WINDOW OPEN. `[source]` Session construction only
|
|
66
|
+
* QUEUES the source; `[host]` a TUI sat fully open and idle for ~47s with the hook log
|
|
67
|
+
* EMPTY, then `SessionStart` fired 1.6s after the first prompt. A Codex citizen is born
|
|
68
|
+
* when it is first spoken to. The doctor must therefore read "installed, zero records"
|
|
69
|
+
* as NOT-YET, never as a failure — and never as proof either.
|
|
70
|
+
*
|
|
71
|
+
* FAILURE POLICY, inherited from the Claude/Copilot units: BEST-EFFORT + LOG. Never
|
|
72
|
+
* scream into the operator's terminal, never block the turn. On any error, append a
|
|
73
|
+
* level-tagged line to `<pi-agent-dir>/meta-bridge-hook.log` and exit 0 with `{}` on
|
|
74
|
+
* stdout. The fail-loud surface is the doctor, which reads that log. The four outcomes
|
|
75
|
+
* carry four distinct grep tokens because they need four different fixes:
|
|
76
|
+
*
|
|
77
|
+
* `birth-mint` a new citizen exists. INFO.
|
|
78
|
+
* `birth-attach` an existing citizen was re-seen (resume/clear/compact). INFO.
|
|
79
|
+
* `birth-refused` the envelope was not a trustworthy SessionStart. ERROR — this
|
|
80
|
+
* session did NOT become a citizen.
|
|
81
|
+
* `birth-failed` the envelope was fine and the STORE write broke. ERROR.
|
|
82
|
+
*
|
|
83
|
+
* LAUNCH: never invoked directly by Codex. `[source]` Codex's `command` handler is a
|
|
84
|
+
* SHELL STRING with no exec-form argv variant anywhere in the enum, so entwurf's
|
|
85
|
+
* "no shell-form fallback" rule has no counterpart to bind to here. The declaration in
|
|
86
|
+
* `$CODEX_HOME/hooks.json` therefore names ONE fixed absolute single-quoted launcher,
|
|
87
|
+
* published operator-owned by `scripts/codex-birth-install.sh`, and that launcher `exec`s
|
|
88
|
+
* `node --experimental-strip-types` over a COPY of this file plus its lib closure.
|
|
89
|
+
* Nothing about identity travels in argv or in the environment, so the launcher has no
|
|
90
|
+
* provenance token to stamp (the Claude/Copilot `ENTWURF_META_HOOK_LAUNCH` handshake
|
|
91
|
+
* exists to license a `process.ppid` read, and reason 1 above is why this unit never
|
|
92
|
+
* performs one).
|
|
93
|
+
*/
|
|
94
|
+
import * as fs from "node:fs";
|
|
95
|
+
import * as path from "node:path";
|
|
96
|
+
import { defaultMetaSessionsDir, upsertMetaSession } from "./lib/meta-session.js";
|
|
97
|
+
import { resolveCodexDefaultSocketPath, setCodexThreadName } from "./lib/native-push/codex-ws-client.js";
|
|
98
|
+
/** `[source]` `SessionStartSource::{Startup,Resume,Clear,Compact}` — the complete
|
|
99
|
+
* 4-valued matcher input for this event (`hooks/src/events/session_start.rs:25-38`).
|
|
100
|
+
* All four are real births of the same thread id: `Resumed→Resume`, `New|Forked→Startup`,
|
|
101
|
+
* `Cleared→Clear` (`core/src/session/session.rs:1600-1606`), and the upsert is
|
|
102
|
+
* idempotent, so a resume ATTACHES rather than minting a second citizen. */
|
|
103
|
+
const CODEX_SESSION_START_SOURCES = ["startup", "resume", "clear", "compact"];
|
|
104
|
+
/** The one event that means "a top-level thread exists and has spoken once". */
|
|
105
|
+
const CODEX_BIRTH_EVENT = "SessionStart";
|
|
106
|
+
/** A `session_id` travels into a record filename join key and, later, into the exact
|
|
107
|
+
* vendor argv `codex queue --thread <threadId>`. Whitespace or control bytes there
|
|
108
|
+
* would either truncate the field or smuggle argv structure, so the token is bounded
|
|
109
|
+
* at the boundary rather than at each consumer. The measured value is a 36-char UUID;
|
|
110
|
+
* the bound is deliberately looser than that, because the SHAPE of the id is the
|
|
111
|
+
* vendor's business and only its usability as one token is ours. */
|
|
112
|
+
const MAX_THREAD_ID_LEN = 200;
|
|
113
|
+
/** Append a best-effort diagnostic line; swallow even its own failure. Same log file
|
|
114
|
+
* and same LEVEL vocabulary as the Claude/Copilot units, so one doctor grep covers all
|
|
115
|
+
* three. The `[codex]` tag is what makes this unit's lines separable. */
|
|
116
|
+
function logLine(level, message) {
|
|
117
|
+
try {
|
|
118
|
+
const file = path.join(path.dirname(defaultMetaSessionsDir()), "meta-bridge-hook.log");
|
|
119
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
120
|
+
fs.appendFileSync(file, `${new Date().toISOString()} ${level} [codex] ${message}\n`);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* logging is best-effort; a broken log must not break the session */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function isAbsolutePosix(value) {
|
|
127
|
+
return value.startsWith("/");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Validate a `SessionStart` envelope, or refuse with the exact reason.
|
|
131
|
+
*
|
|
132
|
+
* STRICT ON EVERY AXIS THE VENDOR ALWAYS SENDS, and the refusal messages are written to
|
|
133
|
+
* be read in a log by somebody who cannot re-run the session. Unknown EXTRA keys are
|
|
134
|
+
* tolerated on purpose (`permission_mode` is already one, and a vendor minor may add
|
|
135
|
+
* more); an extra key cannot corrupt an identity, whereas a missing one can.
|
|
136
|
+
*
|
|
137
|
+
* `path.isAbsolute` is not used for the two path axes: it is platform-dependent, and
|
|
138
|
+
* this unit only ever runs from a POSIX declaration (`$CODEX_HOME/hooks.json`). A
|
|
139
|
+
* Windows-shaped absolute path arriving here would mean the envelope came from
|
|
140
|
+
* somewhere this unit was never installed, which is precisely a refusal.
|
|
141
|
+
*/
|
|
142
|
+
export function readCodexBirthEnvelope(env) {
|
|
143
|
+
// 1. The event predicate, first and hardest. `SubagentStart` carries the PARENT's
|
|
144
|
+
// session_id, so a reader that checked identity before the event name would mint
|
|
145
|
+
// the parent again from a child's event.
|
|
146
|
+
if (env.hook_event_name !== CODEX_BIRTH_EVENT) {
|
|
147
|
+
return {
|
|
148
|
+
refusal: `hook_event_name is not exactly "${CODEX_BIRTH_EVENT}" (got ${JSON.stringify(env.hook_event_name)}) — only a top-level thread's own start event may mint a citizen`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// 2. Belt AND braces on the subagent case: these two keys exist only on the
|
|
152
|
+
// SubagentStart/Stop envelopes, so their presence beside a SessionStart name
|
|
153
|
+
// means the envelope was assembled by something other than the vendor.
|
|
154
|
+
for (const key of ["agent_id", "agent_type"]) {
|
|
155
|
+
if (env[key] !== undefined) {
|
|
156
|
+
return {
|
|
157
|
+
refusal: `a subagent key (${key}) is present on a ${CODEX_BIRTH_EVENT} envelope — a spawned agent raises SubagentStart and must never become a citizen`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 3. The join key.
|
|
162
|
+
if (typeof env.session_id !== "string" || env.session_id.length === 0) {
|
|
163
|
+
return { refusal: "session_id missing or not a non-empty string" };
|
|
164
|
+
}
|
|
165
|
+
const threadId = env.session_id;
|
|
166
|
+
if (threadId.length > MAX_THREAD_ID_LEN) {
|
|
167
|
+
return { refusal: `session_id is longer than ${MAX_THREAD_ID_LEN} characters` };
|
|
168
|
+
}
|
|
169
|
+
if (/[^\x21-\x7e]/.test(threadId)) {
|
|
170
|
+
return {
|
|
171
|
+
refusal: "session_id carries whitespace, control or non-ASCII bytes — it must be one printable token (it becomes the `codex queue --thread` argv value)",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// 4. The two absolute paths. `cwd` has NO process.cwd() fallback: the record is the
|
|
175
|
+
// only thing this unit writes, and a guessed cwd is a fact nothing later corrects.
|
|
176
|
+
if (typeof env.cwd !== "string" || env.cwd.length === 0) {
|
|
177
|
+
return { refusal: "cwd missing or not a non-empty string" };
|
|
178
|
+
}
|
|
179
|
+
if (!isAbsolutePosix(env.cwd)) {
|
|
180
|
+
return { refusal: `cwd is not an absolute POSIX path (got ${JSON.stringify(env.cwd)})` };
|
|
181
|
+
}
|
|
182
|
+
if (typeof env.transcript_path !== "string" || env.transcript_path.length === 0) {
|
|
183
|
+
return { refusal: "transcript_path missing or not a non-empty string" };
|
|
184
|
+
}
|
|
185
|
+
if (!isAbsolutePosix(env.transcript_path)) {
|
|
186
|
+
return { refusal: `transcript_path is not an absolute POSIX path (got ${JSON.stringify(env.transcript_path)})` };
|
|
187
|
+
}
|
|
188
|
+
// 5. `model` is the one nullable axis: the record schema admits null, and an
|
|
189
|
+
// explicit vendor null is a fact ("unknown"), while an absent key or wrong TYPE
|
|
190
|
+
// is a refusal. The measured SessionStart always carries the key; silently mapping
|
|
191
|
+
// its disappearance to null would hide a vendor envelope regression.
|
|
192
|
+
if (!("model" in env)) {
|
|
193
|
+
return { refusal: "model key is missing — Codex SessionStart must carry it explicitly (string or null)" };
|
|
194
|
+
}
|
|
195
|
+
let model = null;
|
|
196
|
+
if (env.model !== null) {
|
|
197
|
+
if (typeof env.model !== "string" || env.model.length === 0) {
|
|
198
|
+
return { refusal: `model is present but not a non-empty string or null (got ${JSON.stringify(env.model)})` };
|
|
199
|
+
}
|
|
200
|
+
model = env.model;
|
|
201
|
+
}
|
|
202
|
+
// 6. The 4-valued source. Refusing an unknown value is how a vendor minor that adds
|
|
203
|
+
// a fifth source surfaces here as a named log line instead of as a silent mint.
|
|
204
|
+
if (typeof env.source !== "string" || !CODEX_SESSION_START_SOURCES.includes(env.source)) {
|
|
205
|
+
return {
|
|
206
|
+
refusal: `source is not one of ${CODEX_SESSION_START_SOURCES.join("|")} (got ${JSON.stringify(env.source)})`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
threadId,
|
|
211
|
+
cwd: env.cwd,
|
|
212
|
+
transcriptPath: env.transcript_path,
|
|
213
|
+
model,
|
|
214
|
+
source: env.source,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Create or attach the ONE record this thread owns, and return the outcome.
|
|
219
|
+
*
|
|
220
|
+
* Exported ahead of `main()` on purpose: a caller that already holds a validated
|
|
221
|
+
* envelope (a gate, or the post-birth `thread/name/set` step that names the thread
|
|
222
|
+
* after its garden id) must be able to reach the gardenId and the create/attach action
|
|
223
|
+
* WITHOUT re-reading stdin — stdin is consumed exactly once, by the hook process.
|
|
224
|
+
* This function performs no logging and no process exit; both belong to `main()`.
|
|
225
|
+
*
|
|
226
|
+
* Both nullable axes are supplied, never omitted: Codex sends them on every
|
|
227
|
+
* SessionStart, so passing `undefined` (the store's "keep existing value" intent) would
|
|
228
|
+
* make a re-attach silently preserve a stale model after `/model`, or a stale transcript
|
|
229
|
+
* path after a compact. A fresh envelope's values are always the newer truth.
|
|
230
|
+
*/
|
|
231
|
+
export function performCodexBirth(envelope, opts = {}) {
|
|
232
|
+
const result = upsertMetaSession({
|
|
233
|
+
input: {
|
|
234
|
+
backend: "codex",
|
|
235
|
+
nativeSessionId: envelope.threadId,
|
|
236
|
+
cwd: envelope.cwd,
|
|
237
|
+
model: envelope.model,
|
|
238
|
+
transcriptPath: envelope.transcriptPath,
|
|
239
|
+
},
|
|
240
|
+
dir: opts.dir,
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
action: result.action,
|
|
244
|
+
gardenId: result.record.gardenId,
|
|
245
|
+
recordPath: result.path,
|
|
246
|
+
record: result.record,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** Emit the neutral hook response and leave. `{}` claims nothing: Codex's SessionStart
|
|
250
|
+
* output schema is all-optional, and this unit has no context to inject and no decision
|
|
251
|
+
* to make about the turn. */
|
|
252
|
+
function emit() {
|
|
253
|
+
fs.writeSync(1, "{}\n");
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
async function main() {
|
|
257
|
+
let raw;
|
|
258
|
+
try {
|
|
259
|
+
raw = fs.readFileSync(0, "utf8"); // fd 0 = stdin (the Codex hook envelope)
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
logLine("ERROR", `birth-refused stdin read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
263
|
+
emit();
|
|
264
|
+
}
|
|
265
|
+
let env;
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(raw);
|
|
268
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
269
|
+
throw new Error("envelope is not a JSON object");
|
|
270
|
+
}
|
|
271
|
+
env = parsed;
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
logLine("ERROR", `birth-refused envelope parse failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
275
|
+
emit();
|
|
276
|
+
}
|
|
277
|
+
const envelope = readCodexBirthEnvelope(env);
|
|
278
|
+
if ("refusal" in envelope) {
|
|
279
|
+
// A record keyed to a guess would be a citizen no live thread can be joined back
|
|
280
|
+
// to, so refuse — and make it ERROR, because this session did NOT become a garden
|
|
281
|
+
// citizen. The key list is printed because the fix depends on WHICH envelope this
|
|
282
|
+
// actually was (a SubagentStart, a hand-run, or a vendor schema change).
|
|
283
|
+
logLine("ERROR", `birth-refused ${envelope.refusal} (keys=${Object.keys(env).sort().join(",")})`);
|
|
284
|
+
emit();
|
|
285
|
+
}
|
|
286
|
+
let born;
|
|
287
|
+
try {
|
|
288
|
+
born = performCodexBirth(envelope);
|
|
289
|
+
logLine("INFO", born.action === "create"
|
|
290
|
+
? `birth-mint ${born.gardenId} record=${path.basename(born.recordPath)} thread=${envelope.threadId} source=${envelope.source} model=${envelope.model ?? "null"}`
|
|
291
|
+
: `birth-attach ${born.gardenId} record=${path.basename(born.recordPath)} thread=${envelope.threadId} source=${envelope.source} model=${envelope.model ?? "null"}`);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
logLine("ERROR", `birth-failed thread=${envelope.threadId} source=${envelope.source}: ${err instanceof Error ? err.message : String(err)}`);
|
|
295
|
+
emit();
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
await setCodexThreadName(resolveCodexDefaultSocketPath(process.env), envelope.threadId, born.gardenId);
|
|
299
|
+
logLine("INFO", `title-set ${born.gardenId} thread=${envelope.threadId}`);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
logLine("WARN", `title-unavailable ${born.gardenId} thread=${envelope.threadId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
303
|
+
}
|
|
304
|
+
emit();
|
|
305
|
+
}
|
|
306
|
+
// RUN ONLY AS THE HOOK PROCESS. `main()` consumes stdin and calls `process.exit`, so a
|
|
307
|
+
// gate (or the post-birth `thread/name/set` step) that imports `performCodexBirth` must
|
|
308
|
+
// not trigger it. `import.meta.main` is NOT used for this: it landed in Node 24.2.0 and
|
|
309
|
+
// the package floor is major 24, so on a 24.0/24.1 host it would be `undefined` — the
|
|
310
|
+
// guard would read falsy and the hook would silently mint nothing on every session,
|
|
311
|
+
// which is exactly the failure this file exists to prevent. An argv comparison works on
|
|
312
|
+
// every supported Node.
|
|
313
|
+
const invokedDirectly = (() => {
|
|
314
|
+
const entry = process.argv[1];
|
|
315
|
+
if (typeof entry !== "string" || entry.length === 0)
|
|
316
|
+
return false;
|
|
317
|
+
try {
|
|
318
|
+
return fs.realpathSync(entry) === fs.realpathSync(import.meta.filename);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
})();
|
|
324
|
+
if (invokedDirectly)
|
|
325
|
+
void main();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-socket-path — print the Codex app-server default control-socket path for THIS
|
|
3
|
+
* environment, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* WHY A LEAF EXISTS FOR ONE LINE. `entwurf codex-app-server` is a bash launcher, and bash
|
|
6
|
+
* cannot import `resolveCodexDefaultSocketPath`. Its first version re-derived the path
|
|
7
|
+
* instead — `${CODEX_HOME:-$HOME/.codex}` plus a POSIX `[:space:]` trim — and a gate compared
|
|
8
|
+
* the two spellings over four ASCII-normal inputs, which they agreed on.
|
|
9
|
+
*
|
|
10
|
+
* They did not agree everywhere. `[측정 2026-09-16]` with `CODEX_HOME=$''` the TS leaf
|
|
11
|
+
* trims (JS `String.prototype.trim` strips U+FEFF) and falls back to `$HOME/.codex`, while the
|
|
12
|
+
* bash trim keeps the byte and yields `<BOM>/app-server-control/app-server-control.sock`. Same
|
|
13
|
+
* for `path.join`'s normalization of a trailing slash or a `..` segment. Every one of those is
|
|
14
|
+
* a managed launch starting a server at an address delivery and preflight never look at — a
|
|
15
|
+
* false success of exactly the kind this repo refuses.
|
|
16
|
+
*
|
|
17
|
+
* So the second spelling is gone rather than widened. There is ONE implementation of this
|
|
18
|
+
* address, this leaf prints it, and the launcher asks. Matching a transcription against its
|
|
19
|
+
* original can only ever test the inputs somebody thought of.
|
|
20
|
+
*
|
|
21
|
+
* Reads `process.env` directly: the resolver's contract is the ambient environment of whoever
|
|
22
|
+
* is about to launch, and an argv seam here would be a way to redirect the address.
|
|
23
|
+
*/
|
|
24
|
+
import { resolveCodexDefaultSocketPath } from "../pi-extensions/lib/native-push/codex-ws-client.js";
|
|
25
|
+
if (process.argv.length > 2) {
|
|
26
|
+
console.error("usage: codex-socket-path (no arguments; the environment is the input)");
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
// No trailing newline: the caller substitutes this straight into an address.
|
|
30
|
+
process.stdout.write(resolveCodexDefaultSocketPath(process.env));
|
|
@@ -388,7 +388,12 @@ async function inspectNativePushCitizens(storeDir) {
|
|
|
388
388
|
try {
|
|
389
389
|
const probe = await resolveNativePushAdapter(identity.backend).probe(identity.nativeSessionId);
|
|
390
390
|
status = probe.status;
|
|
391
|
-
reason =
|
|
391
|
+
reason =
|
|
392
|
+
probe.status === "alive"
|
|
393
|
+
? probe.route.backend === "antigravity"
|
|
394
|
+
? `route ${probe.route.lsAddress}`
|
|
395
|
+
: `socket ${probe.route.socketPath}`
|
|
396
|
+
: probe.reason;
|
|
392
397
|
}
|
|
393
398
|
catch (err) {
|
|
394
399
|
// A probe that cannot run is not a dead conversation.
|