@junghanacs/entwurf 0.41.2 → 0.50.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 +4 -2
- package/BASELINE.md +110 -1
- package/CHANGELOG.md +328 -0
- package/CONTRIBUTING.md +1 -1
- package/ENTWURF-CLI.json +1229 -0
- package/ENTWURF.json +284 -0
- package/FAQ.md +4 -4
- package/README.md +125 -184
- package/VERIFY.md +2 -2
- package/docs/external-mcp-host.md +3 -3
- package/docs/setup-clean-host.md +37 -12
- package/mcp/entwurf-bridge/dist/ENTWURF.json +284 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +46 -196
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +21 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +498 -29
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +23 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +68 -21
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -60
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
- package/mcp/entwurf-bridge/src/index.ts +149 -327
- package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
- package/native/win32-x64/manifest.json +1 -1
- package/package.json +8 -4
- package/pi/pi-durable/bootstrap.mjs +179 -6
- package/pi/pi-durable/carrier/runtime.js +108 -9
- package/pi/pi-durable/carrier/session-name.js +11 -0
- package/pi/pi-durable/carrier/sessions.js +96 -20
- package/pi/pi-durable/overlay/upstream-pin.json +11 -5
- package/pi-extensions/lib/agent-guidance.ts +341 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +25 -31
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +725 -54
- package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
- package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
- package/pi-extensions/lib/herdr-resume-call.ts +810 -0
- package/pi-extensions/lib/meta-session.ts +13 -8
- package/pi-extensions/lib/mux-fresh-call.ts +23 -26
- package/pi-extensions/lib/mux-resume-call.ts +95 -23
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
- package/pi-extensions/lib/pi-durable-location.ts +446 -0
- package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
- package/pi-extensions/lib/remote-pair-send.ts +7 -4
- package/pi-extensions/lib/resume-launch-identity.ts +31 -12
- package/pi-extensions/meta-bridge-pi-durable.ts +52 -79
- package/run.sh +417 -654
- package/scripts/build-bridge.sh +10 -0
- package/scripts/check-capability-bundle-reach.ts +86 -0
- package/scripts/check-claude-launch.ts +341 -0
- package/scripts/check-entwurf-self-address.ts +3 -3
- package/scripts/check-entwurf-v2-surface.ts +94 -98
- package/scripts/check-fresh-call-dispatch.ts +70 -9
- package/scripts/check-fresh-cut-gate.sh +23 -9
- package/scripts/check-gate-qualification.ts +24 -17
- package/scripts/check-herdr-activation.ts +301 -23
- package/scripts/check-herdr-fresh-call.ts +161 -25
- package/scripts/check-herdr-plugin-build.ts +173 -1
- package/scripts/check-herdr-plugin.ts +28 -2
- package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
- package/scripts/check-install-surface.ts +1236 -6
- package/scripts/check-meta-doctor-oracle.sh +1 -1
- package/scripts/check-meta-facts.ts +20 -1
- package/scripts/check-mux-resume-call.ts +48 -3
- package/scripts/check-peer-facts.ts +39 -2
- package/scripts/cli-help.mjs +222 -0
- package/scripts/dev-bin.sh +39 -2
- package/scripts/herdr-activation.mjs +28 -6
- package/scripts/herdr-operator-link.mjs +389 -0
- package/scripts/herdr-plugin-activate.mjs +5 -0
- package/scripts/herdr-plugin-deactivate.mjs +18 -5
- package/scripts/herdr-rails.mjs +43 -7
- package/scripts/herdr-runtime.mjs +27 -0
- package/scripts/meta-bridge-fresh-cut.ts +1 -27
- package/scripts/meta-bridge-prune.ts +1 -1
- package/scripts/meta-facts.ts +1 -1
- package/scripts/mutants/agent-guidance.json +160 -0
- package/scripts/mutants/bridge-boot-resume.json +6 -8
- package/scripts/mutants/capability-bundle-reach.json +124 -0
- package/scripts/mutants/claude-launch.json +142 -0
- package/scripts/mutants/cli-help-source.json +420 -0
- package/scripts/mutants/control-send-receipt.json +21 -4
- package/scripts/mutants/dev-gate-dispatch.json +109 -0
- package/scripts/mutants/fresh-call-dispatch.json +38 -2
- package/scripts/mutants/fresh-cut.json +15 -0
- package/scripts/mutants/herdr-activation.json +128 -0
- package/scripts/mutants/herdr-fresh-call.json +278 -5
- package/scripts/mutants/herdr-operator-link.json +318 -0
- package/scripts/mutants/herdr-plugin-build.json +55 -0
- package/scripts/mutants/herdr-plugin.json +17 -0
- package/scripts/mutants/herdr-resume-call.json +288 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
- package/scripts/mutants/meta-facts.json +15 -0
- package/scripts/mutants/mux-fresh-call.json +14 -3
- package/scripts/mutants/mux-resume-call.json +89 -3
- package/scripts/mutants/peer-facts.json +15 -0
- package/scripts/mutants/pi-durable.json +996 -6
- package/scripts/mutants/remote-pair-send.json +21 -2
- package/scripts/mutants/self-address.json +10 -10
- package/scripts/mutants/source-install.json +35 -0
- package/scripts/mutants/v2-surface.json +50 -64
- package/scripts/mutants/v2-visible-resume.json +266 -6
- package/scripts/peer-facts.ts +1 -1
- package/scripts/smoke-agy-install-state.sh +66 -0
- package/scripts/smoke-meta-prune.sh +23 -0
- package/scripts/smoke-omp-fresh-live.ts +12 -3
- package/scripts/smoke-omp-receive-live.ts +7 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
- package/team.json.example +39 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-durable LOCATION EVIDENCE (#157 B3, C4 locator leaf) — where a pi-durable citizen's native
|
|
3
|
+
* session directory physically is, written by the durable host's own birth and bound to its
|
|
4
|
+
* record by the record's `attachIncarnation`.
|
|
5
|
+
*
|
|
6
|
+
* WHAT THIS IS. One garden-keyed file per pi-durable citizen, `<locations>/<gardenId>.location.json`,
|
|
7
|
+
* holding the minimum location + binding evidence its attach already knows: the native session id,
|
|
8
|
+
* the realpath cwd, `realpath(session.directory)`, the owner pid and its start key, and the
|
|
9
|
+
* `attachIncarnation` minted in the same record-arm-locked commit as the record's own. The record
|
|
10
|
+
* stays the SOLE address (AGENTS.md Hard Rule 2): this file is evidence the record points at, keyed
|
|
11
|
+
* by the record's garden id, and it counts only while the record carries the very binding it copied.
|
|
12
|
+
*
|
|
13
|
+
* WHAT THIS IS NOT. Not an address, a registry, a presence marker, a liveness or ownership verdict,
|
|
14
|
+
* a dormancy proof or a guarded enrollment. A matching pair says ONE thing: the record's last
|
|
15
|
+
* committed binding named this physical directory and this owner incarnation. A process holding the
|
|
16
|
+
* session before its own commit (the unbound window from the native lease to this birth) is
|
|
17
|
+
* invisible here; only an owner claim taken before the lease could see it, and none exists in this
|
|
18
|
+
* leaf. Public resume of a pi-durable citizen stays refused.
|
|
19
|
+
*
|
|
20
|
+
* WHERE. `<dirname(receiversDir)>/meta-pi-durable-locations`: a sibling of the receivers root the
|
|
21
|
+
* birth actually uses, the same derivation as the record-arm lock dir, so a sandboxed receivers root
|
|
22
|
+
* sandboxes it too and no new environment carrier exists. It is deliberately NOT inside the record
|
|
23
|
+
* store (every entry there is certified), the receivers or senders roots (fresh-cut walks every entry
|
|
24
|
+
* there as a marker) or the mailbox (fresh-cut archives it).
|
|
25
|
+
*
|
|
26
|
+
* LIFECYCLE (Rule 8). Written by a pi-durable birth only; never removed by retire, close, the
|
|
27
|
+
* doorbell or any reader. No reader in this tree enumerates the directory: fresh-cut archives the
|
|
28
|
+
* store and the mailbox and walks the marker roots, the doctor certifies the store. A carrier whose
|
|
29
|
+
* record is absent (a cut generation) or whose binding no longer matches is INERT — every read
|
|
30
|
+
* below refuses it by name. There is no automatic deletion, migration, GC, doctor framework or
|
|
31
|
+
* recovery verb for it.
|
|
32
|
+
*
|
|
33
|
+
* FAILURE STATES OF A BIRTH (all non-green; none is "binding not committed" by fiat):
|
|
34
|
+
* (a) arm lock busy, or any failure before the minted upsert: the store keeps whatever pair it
|
|
35
|
+
* had — possibly an OLDER matching pair;
|
|
36
|
+
* (b) the minted upsert landed but the carrier write failed: record ≠ carrier, a pair mismatch
|
|
37
|
+
* every read refuses;
|
|
38
|
+
* (c) the pair committed but a later step of the birth failed: a matching pair, and still a failed
|
|
39
|
+
* birth. The two files are never one atomic commit.
|
|
40
|
+
* Calling a cleanup is not a cleanup's success: an arm-lock release that does not answer
|
|
41
|
+
* `released` is reported, never assumed.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import * as fs from "node:fs";
|
|
45
|
+
import * as path from "node:path";
|
|
46
|
+
import { acquireLock, releaseLock } from "./entwurf-v2-lock.ts";
|
|
47
|
+
import {
|
|
48
|
+
defaultMetaRecordArmLocksDir,
|
|
49
|
+
findCertifiedIdentityByNativeSessionId,
|
|
50
|
+
isPlausibleOwnerPid,
|
|
51
|
+
type MetaIdentity,
|
|
52
|
+
mintAttachIncarnation,
|
|
53
|
+
NOT_REGULAR_ENTRY_CODE,
|
|
54
|
+
processStartKey,
|
|
55
|
+
readAddressableMetaIdentity,
|
|
56
|
+
readStoreRecordFile,
|
|
57
|
+
requireAttachIncarnation,
|
|
58
|
+
requireGardenId,
|
|
59
|
+
requireNonEmptyString,
|
|
60
|
+
type UpsertAction,
|
|
61
|
+
UpsertActionMismatchError,
|
|
62
|
+
upsertMetaSession,
|
|
63
|
+
} from "./meta-session.ts";
|
|
64
|
+
|
|
65
|
+
export const PI_DURABLE_LOCATION_DIRNAME = "meta-pi-durable-locations";
|
|
66
|
+
export const PI_DURABLE_LOCATION_SUFFIX = ".location.json";
|
|
67
|
+
export const PI_DURABLE_LOCATION_SCHEMA_VERSION = 1;
|
|
68
|
+
const PI_DURABLE_BACKEND = "pi-durable";
|
|
69
|
+
|
|
70
|
+
const LOCATION_KEYS: readonly string[] = [
|
|
71
|
+
"schemaVersion",
|
|
72
|
+
"gardenId",
|
|
73
|
+
"backend",
|
|
74
|
+
"nativeSessionId",
|
|
75
|
+
"cwd",
|
|
76
|
+
"nativeDirectory",
|
|
77
|
+
"ownerPid",
|
|
78
|
+
"ownerStartKey",
|
|
79
|
+
"attachIncarnation",
|
|
80
|
+
"writtenAt",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/** The location evidence one pi-durable birth commits. Evidence only — see the file header. */
|
|
84
|
+
export interface PiDurableLocation {
|
|
85
|
+
schemaVersion: typeof PI_DURABLE_LOCATION_SCHEMA_VERSION;
|
|
86
|
+
gardenId: string;
|
|
87
|
+
backend: typeof PI_DURABLE_BACKEND;
|
|
88
|
+
nativeSessionId: string;
|
|
89
|
+
/** The realpath cwd the record carries (`sessions.ts:42` realpaths it before the attach sees it). */
|
|
90
|
+
cwd: string;
|
|
91
|
+
/** `realpath(session.directory)` at the birth: the physical native session directory. */
|
|
92
|
+
nativeDirectory: string;
|
|
93
|
+
ownerPid: number;
|
|
94
|
+
ownerStartKey: string;
|
|
95
|
+
/** The record's `attachIncarnation` minted in the same arm-locked commit. Never null here. */
|
|
96
|
+
attachIncarnation: string;
|
|
97
|
+
writtenAt: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A birth that could not commit its location binding. Always non-green; see FAILURE STATES. */
|
|
101
|
+
export class PiDurableLocationError extends Error {
|
|
102
|
+
readonly reason:
|
|
103
|
+
| "owner-pid-implausible"
|
|
104
|
+
| "cwd-not-absolute"
|
|
105
|
+
| "native-directory-invalid"
|
|
106
|
+
| "owner-start-key-unreadable"
|
|
107
|
+
| "record-arm-lock-busy"
|
|
108
|
+
| "record-arm-lock-release-failed"
|
|
109
|
+
| "carrier-write-failed";
|
|
110
|
+
constructor(reason: PiDurableLocationError["reason"], detail: string, options?: { cause?: unknown }) {
|
|
111
|
+
super(`pi-durable location (${reason}): ${detail}`, options);
|
|
112
|
+
this.name = "PiDurableLocationError";
|
|
113
|
+
this.reason = reason;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `<dirname(receiversDir)>/meta-pi-durable-locations`, from the receivers root the caller actually uses. */
|
|
118
|
+
export function piDurableLocationDir(receiversDir: string): string {
|
|
119
|
+
if (!path.isAbsolute(receiversDir)) {
|
|
120
|
+
throw new Error(`pi-durable location: the receivers root must be absolute (got ${JSON.stringify(receiversDir)})`);
|
|
121
|
+
}
|
|
122
|
+
return path.join(path.dirname(receiversDir), PI_DURABLE_LOCATION_DIRNAME);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function piDurableLocationPath(gardenId: string, receiversDir: string): string {
|
|
126
|
+
return path.join(piDurableLocationDir(receiversDir), `${requireGardenId(gardenId)}${PI_DURABLE_LOCATION_SUFFIX}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function requireAbsolute(value: unknown, field: string): string {
|
|
130
|
+
const text = requireNonEmptyString(value, field);
|
|
131
|
+
if (!path.isAbsolute(text)) throw new Error(`location field "${field}" must be an absolute path`);
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Strict parse: exact keyset, live schema version, body garden id equal to the file's. Throws on any drift. */
|
|
136
|
+
export function parsePiDurableLocation(json: string, expectedGardenId: string): PiDurableLocation {
|
|
137
|
+
const raw: unknown = JSON.parse(json);
|
|
138
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("location is not a JSON object");
|
|
139
|
+
const obj = raw as Record<string, unknown>;
|
|
140
|
+
const stray = Object.keys(obj).filter((key) => !LOCATION_KEYS.includes(key));
|
|
141
|
+
if (stray.length > 0) throw new Error(`location carries unexpected key(s) ${stray.join(", ")}`);
|
|
142
|
+
const missing = LOCATION_KEYS.filter((key) => !(key in obj));
|
|
143
|
+
if (missing.length > 0) throw new Error(`location lacks key(s) ${missing.join(", ")}`);
|
|
144
|
+
if (obj.schemaVersion !== PI_DURABLE_LOCATION_SCHEMA_VERSION) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`location schemaVersion is ${JSON.stringify(obj.schemaVersion)}, not ${PI_DURABLE_LOCATION_SCHEMA_VERSION}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (obj.backend !== PI_DURABLE_BACKEND) throw new Error(`location backend is ${JSON.stringify(obj.backend)}`);
|
|
150
|
+
if (!isPlausibleOwnerPid(obj.ownerPid))
|
|
151
|
+
throw new Error(`location ownerPid ${JSON.stringify(obj.ownerPid)} is not a plausible owner`);
|
|
152
|
+
const attachIncarnation = requireAttachIncarnation(obj.attachIncarnation);
|
|
153
|
+
if (attachIncarnation === null)
|
|
154
|
+
throw new Error("location attachIncarnation is null: a location without a binding is not evidence");
|
|
155
|
+
const location: PiDurableLocation = {
|
|
156
|
+
schemaVersion: PI_DURABLE_LOCATION_SCHEMA_VERSION,
|
|
157
|
+
gardenId: requireGardenId(obj.gardenId),
|
|
158
|
+
backend: PI_DURABLE_BACKEND,
|
|
159
|
+
nativeSessionId: requireNonEmptyString(obj.nativeSessionId, "nativeSessionId"),
|
|
160
|
+
cwd: requireAbsolute(obj.cwd, "cwd"),
|
|
161
|
+
nativeDirectory: requireAbsolute(obj.nativeDirectory, "nativeDirectory"),
|
|
162
|
+
ownerPid: obj.ownerPid,
|
|
163
|
+
ownerStartKey: requireNonEmptyString(obj.ownerStartKey, "ownerStartKey"),
|
|
164
|
+
attachIncarnation,
|
|
165
|
+
writtenAt: requireNonEmptyString(obj.writtenAt, "writtenAt"),
|
|
166
|
+
};
|
|
167
|
+
if (location.gardenId !== expectedGardenId) {
|
|
168
|
+
throw new AddressMismatch(`location body names ${location.gardenId}, its file names ${expectedGardenId}`);
|
|
169
|
+
}
|
|
170
|
+
return location;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
class AddressMismatch extends Error {}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Atomic private write: an exclusive tmp beside the final name, then rename. A failed write leaves no tmp of its
|
|
177
|
+
* own; an entry that already held the tmp name (the exclusive create failed) is not this call's and is left as is.
|
|
178
|
+
*/
|
|
179
|
+
function writePiDurableLocation(location: PiDurableLocation, receiversDir: string): string {
|
|
180
|
+
const dir = piDurableLocationDir(receiversDir);
|
|
181
|
+
const file = piDurableLocationPath(location.gardenId, receiversDir);
|
|
182
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
183
|
+
// Ownership of the tmp is its creation by THIS call: a `wx` that failed (EEXIST included) made nothing here.
|
|
184
|
+
let created = false;
|
|
185
|
+
try {
|
|
186
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
187
|
+
const fd = fs.openSync(tmp, "wx", 0o600);
|
|
188
|
+
created = true;
|
|
189
|
+
try {
|
|
190
|
+
fs.writeFileSync(fd, `${JSON.stringify(location, null, "\t")}\n`);
|
|
191
|
+
} finally {
|
|
192
|
+
fs.closeSync(fd);
|
|
193
|
+
}
|
|
194
|
+
fs.renameSync(tmp, file);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
let leftover = "";
|
|
197
|
+
try {
|
|
198
|
+
if (created) fs.rmSync(tmp, { force: true });
|
|
199
|
+
} catch (cleanup) {
|
|
200
|
+
leftover = `; its tmp ${tmp} could not be removed (${cleanup instanceof Error ? cleanup.message : String(cleanup)})`;
|
|
201
|
+
}
|
|
202
|
+
throw new PiDurableLocationError(
|
|
203
|
+
"carrier-write-failed",
|
|
204
|
+
`could not write ${file} (${error instanceof Error ? error.message : String(error)})${leftover}`,
|
|
205
|
+
{ cause: error },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return file;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface CommitPiDurableBirthOptions {
|
|
212
|
+
nativeSessionId: string;
|
|
213
|
+
/** The realpath cwd the durable session reports (absolute). */
|
|
214
|
+
cwd: string;
|
|
215
|
+
/** `session.directory` as the durable runtime reports it; realpath'd and verified here. */
|
|
216
|
+
sessionDirectory: string;
|
|
217
|
+
ownerPid: number;
|
|
218
|
+
sessionsDir: string;
|
|
219
|
+
/** The receivers root this birth uses (absolute); the arm-lock and location dirs derive from it. */
|
|
220
|
+
receiversDir: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface CommitPiDurableBirthResult {
|
|
224
|
+
/** `create` when THIS birth published the record, else `attach`. */
|
|
225
|
+
action: UpsertAction;
|
|
226
|
+
record: MetaIdentity;
|
|
227
|
+
dir: string;
|
|
228
|
+
/** Absolute path of the written record. */
|
|
229
|
+
path: string;
|
|
230
|
+
locationPath: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The pi-durable birth's record write, in the order the Claude Code hook already uses for its own
|
|
235
|
+
* binding (`meta-bridge-hook.ts` commitRecord): a read-only certified lookup; a create-only publish
|
|
236
|
+
* when absent (a create replaces nothing, so it takes no lock); the record-arm lock for the garden id;
|
|
237
|
+
* the attach upsert minting `attachIncarnation`; the location carrier copying that same value; the
|
|
238
|
+
* release. Everything that can be refused before a write — the receivers root, the owner pid, the cwd,
|
|
239
|
+
* the native directory, the owner start key — is checked first. See FAILURE STATES for what each failure leaves behind.
|
|
240
|
+
*/
|
|
241
|
+
export function commitPiDurableBirth(opts: CommitPiDurableBirthOptions): CommitPiDurableBirthResult {
|
|
242
|
+
// A relative receivers root would put the arm lock and the carrier wherever the process happens to be.
|
|
243
|
+
piDurableLocationDir(opts.receiversDir);
|
|
244
|
+
// The writer refuses, before the first record write, what the carrier's parser would refuse: no
|
|
245
|
+
// birth may commit a record whose location evidence could never be read back.
|
|
246
|
+
if (!isPlausibleOwnerPid(opts.ownerPid)) {
|
|
247
|
+
throw new PiDurableLocationError(
|
|
248
|
+
"owner-pid-implausible",
|
|
249
|
+
`owner pid ${JSON.stringify(opts.ownerPid)} is not a plausible owner; nothing was written`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (typeof opts.cwd !== "string" || !path.isAbsolute(opts.cwd)) {
|
|
253
|
+
throw new PiDurableLocationError(
|
|
254
|
+
"cwd-not-absolute",
|
|
255
|
+
`the durable session cwd ${JSON.stringify(opts.cwd)} is not absolute; nothing was written`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
let nativeDirectory: string;
|
|
259
|
+
try {
|
|
260
|
+
if (!path.isAbsolute(opts.sessionDirectory)) throw new Error("not absolute");
|
|
261
|
+
nativeDirectory = fs.realpathSync(opts.sessionDirectory);
|
|
262
|
+
if (!fs.statSync(nativeDirectory).isDirectory()) throw new Error("not a directory");
|
|
263
|
+
} catch (error) {
|
|
264
|
+
throw new PiDurableLocationError(
|
|
265
|
+
"native-directory-invalid",
|
|
266
|
+
`the durable session directory ${JSON.stringify(opts.sessionDirectory)} is not an existing directory ` +
|
|
267
|
+
`(${error instanceof Error ? error.message : String(error)}); nothing was written`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const ownerStartKey = processStartKey(opts.ownerPid);
|
|
271
|
+
if (ownerStartKey === "") {
|
|
272
|
+
throw new PiDurableLocationError(
|
|
273
|
+
"owner-start-key-unreadable",
|
|
274
|
+
`no start key could be read for owner pid ${opts.ownerPid}; nothing was written`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const input = {
|
|
278
|
+
backend: PI_DURABLE_BACKEND,
|
|
279
|
+
nativeSessionId: opts.nativeSessionId,
|
|
280
|
+
cwd: opts.cwd,
|
|
281
|
+
model: null,
|
|
282
|
+
transcriptPath: null,
|
|
283
|
+
} as const;
|
|
284
|
+
|
|
285
|
+
let action: UpsertAction = "attach";
|
|
286
|
+
let existing = findCertifiedIdentityByNativeSessionId(opts.nativeSessionId, opts.sessionsDir);
|
|
287
|
+
if (existing === null) {
|
|
288
|
+
try {
|
|
289
|
+
existing = upsertMetaSession({ input, expectAction: "create", dir: opts.sessionsDir }).record;
|
|
290
|
+
action = "create";
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (!(error instanceof UpsertActionMismatchError)) throw error;
|
|
293
|
+
existing = findCertifiedIdentityByNativeSessionId(opts.nativeSessionId, opts.sessionsDir);
|
|
294
|
+
if (existing === null) throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const lockDir = defaultMetaRecordArmLocksDir(opts.receiversDir);
|
|
299
|
+
const acquired = acquireLock(existing.gardenId, { dir: lockDir });
|
|
300
|
+
if (!acquired.ok) {
|
|
301
|
+
throw new PiDurableLocationError(
|
|
302
|
+
"record-arm-lock-busy",
|
|
303
|
+
`${acquired.conflict.detail}; the minted binding was not written, so the store keeps whatever pair it had`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
let result: CommitPiDurableBirthResult;
|
|
307
|
+
try {
|
|
308
|
+
const attachIncarnation = mintAttachIncarnation();
|
|
309
|
+
const upsert = upsertMetaSession({
|
|
310
|
+
input: { ...input, attachIncarnation },
|
|
311
|
+
expectAction: "attach",
|
|
312
|
+
dir: opts.sessionsDir,
|
|
313
|
+
});
|
|
314
|
+
const locationPath = writePiDurableLocation(
|
|
315
|
+
{
|
|
316
|
+
schemaVersion: PI_DURABLE_LOCATION_SCHEMA_VERSION,
|
|
317
|
+
gardenId: upsert.record.gardenId,
|
|
318
|
+
backend: PI_DURABLE_BACKEND,
|
|
319
|
+
nativeSessionId: upsert.record.nativeSessionId,
|
|
320
|
+
cwd: upsert.record.cwd,
|
|
321
|
+
nativeDirectory,
|
|
322
|
+
ownerPid: opts.ownerPid,
|
|
323
|
+
ownerStartKey,
|
|
324
|
+
attachIncarnation,
|
|
325
|
+
writtenAt: new Date().toISOString(),
|
|
326
|
+
},
|
|
327
|
+
opts.receiversDir,
|
|
328
|
+
);
|
|
329
|
+
result = { action, record: upsert.record, dir: upsert.dir, path: upsert.path, locationPath };
|
|
330
|
+
} catch (error) {
|
|
331
|
+
let released: string;
|
|
332
|
+
try {
|
|
333
|
+
released = releaseLock(acquired.claim);
|
|
334
|
+
} catch (cleanup) {
|
|
335
|
+
released = `threw: ${cleanup instanceof Error ? cleanup.message : String(cleanup)}`;
|
|
336
|
+
}
|
|
337
|
+
if (released !== "released" && error instanceof Error) {
|
|
338
|
+
error.message += ` (the record-arm lock release then answered ${released}, not released)`;
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
const released = releaseLock(acquired.claim);
|
|
343
|
+
if (released !== "released") {
|
|
344
|
+
throw new PiDurableLocationError(
|
|
345
|
+
"record-arm-lock-release-failed",
|
|
346
|
+
`the pair for ${result.record.gardenId} was written, but releasing its record-arm lock answered ${released}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export type PiDurableLocationRefusal =
|
|
353
|
+
| "record-unaddressable"
|
|
354
|
+
| "record-not-pi-durable"
|
|
355
|
+
| "record-unbound"
|
|
356
|
+
| "carrier-absent"
|
|
357
|
+
| "carrier-not-regular"
|
|
358
|
+
| "carrier-unreadable"
|
|
359
|
+
| "carrier-malformed"
|
|
360
|
+
| "carrier-address-mismatch"
|
|
361
|
+
| "binding-mismatch"
|
|
362
|
+
| "native-directory-invalid";
|
|
363
|
+
|
|
364
|
+
export type PiDurableLocationVerdict =
|
|
365
|
+
| { ok: true; record: MetaIdentity; location: PiDurableLocation }
|
|
366
|
+
| { ok: false; reason: PiDurableLocationRefusal; detail: string };
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The verification leaf: from a garden id (the address) to location evidence, or a named refusal.
|
|
370
|
+
* The record is read through the addressable reader; the carrier through the store's no-follow,
|
|
371
|
+
* regular-file read; nothing in the native directory is opened. A pass means ONLY that the record's
|
|
372
|
+
* current binding and this carrier agree and the named directory exists as itself — never liveness,
|
|
373
|
+
* ownership, dormancy or an admission.
|
|
374
|
+
*/
|
|
375
|
+
export function verifyPiDurableLocation(opts: {
|
|
376
|
+
gardenId: string;
|
|
377
|
+
sessionsDir: string;
|
|
378
|
+
receiversDir: string;
|
|
379
|
+
}): PiDurableLocationVerdict {
|
|
380
|
+
let record: MetaIdentity;
|
|
381
|
+
try {
|
|
382
|
+
record = readAddressableMetaIdentity(opts.gardenId, opts.sessionsDir);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
reason: "record-unaddressable",
|
|
387
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (record.backend !== PI_DURABLE_BACKEND) {
|
|
391
|
+
return { ok: false, reason: "record-not-pi-durable", detail: `${record.gardenId} is a ${record.backend} record` };
|
|
392
|
+
}
|
|
393
|
+
if (record.attachIncarnation === null) {
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
reason: "record-unbound",
|
|
397
|
+
detail: `${record.gardenId} carries no attachIncarnation: no committed location binding to check`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const file = piDurableLocationPath(record.gardenId, opts.receiversDir);
|
|
401
|
+
let text: string;
|
|
402
|
+
try {
|
|
403
|
+
text = readStoreRecordFile(file);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
const code = (error as { code?: unknown }).code;
|
|
406
|
+
if (code === "ENOENT") return { ok: false, reason: "carrier-absent", detail: `${file} does not exist` };
|
|
407
|
+
if (code === "ELOOP" || code === NOT_REGULAR_ENTRY_CODE) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
reason: "carrier-not-regular",
|
|
411
|
+
detail: `${file} is not a regular file and is never followed`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return { ok: false, reason: "carrier-unreadable", detail: error instanceof Error ? error.message : String(error) };
|
|
415
|
+
}
|
|
416
|
+
let location: PiDurableLocation;
|
|
417
|
+
try {
|
|
418
|
+
location = parsePiDurableLocation(text, record.gardenId);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
421
|
+
if (error instanceof AddressMismatch) return { ok: false, reason: "carrier-address-mismatch", detail };
|
|
422
|
+
return { ok: false, reason: "carrier-malformed", detail };
|
|
423
|
+
}
|
|
424
|
+
const differs = (["nativeSessionId", "cwd", "attachIncarnation"] as const).filter(
|
|
425
|
+
(field) => location[field] !== record[field],
|
|
426
|
+
);
|
|
427
|
+
if (differs.length > 0) {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
reason: "binding-mismatch",
|
|
431
|
+
detail: `the carrier and the record of ${record.gardenId} differ on ${differs.join(", ")}`,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const real = fs.realpathSync(location.nativeDirectory);
|
|
436
|
+
if (real !== location.nativeDirectory) throw new Error(`it now resolves to ${real}`);
|
|
437
|
+
if (!fs.statSync(real).isDirectory()) throw new Error("it is not a directory");
|
|
438
|
+
} catch (error) {
|
|
439
|
+
return {
|
|
440
|
+
ok: false,
|
|
441
|
+
reason: "native-directory-invalid",
|
|
442
|
+
detail: `${location.nativeDirectory}: ${error instanceof Error ? error.message : String(error)}`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return { ok: true, record, location };
|
|
446
|
+
}
|