@junghanacs/entwurf 0.12.6 → 0.12.7
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 +31 -17
- package/BASELINE.md +42 -8
- package/CHANGELOG.md +22 -0
- package/DELIVERY.md +71 -14
- package/README.md +77 -45
- package/VERIFY.md +18 -13
- package/docs/setup-clean-host.md +80 -22
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
- package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
- package/mcp/entwurf-bridge/src/index.ts +101 -67
- package/mcp/entwurf-bridge/test.sh +1 -1
- package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
- package/package.json +10 -5
- package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
- package/pi-extensions/lib/entwurf-self-address.ts +58 -15
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
- package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
- package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
- package/pi-extensions/lib/meta-sender-identity.ts +154 -0
- package/pi-extensions/lib/native-push/adapter.ts +255 -0
- package/pi-extensions/lib/native-push/register.ts +99 -0
- package/run.sh +756 -214
- package/scripts/agy-bridge-config.py +446 -0
- package/scripts/agy-bridge.sh +359 -0
- package/scripts/agy-hooks-bridge.sh +193 -0
- package/scripts/agy-hooks-config.py +257 -0
- package/scripts/agy-imprint.sh +28 -0
- package/scripts/agy-imprint.ts +193 -0
- package/scripts/agy-statusline-bridge.sh +176 -0
- package/scripts/agy-statusline-config.py +213 -0
- package/scripts/agy-statusline.sh +256 -0
- package/scripts/build-bridge.sh +20 -0
- package/scripts/check-agy-sender-identity.ts +364 -0
- package/scripts/check-entwurf-bridge-boot.ts +8 -2
- package/scripts/check-entwurf-deliverability.ts +34 -0
- package/scripts/check-entwurf-self-address.ts +78 -11
- package/scripts/check-entwurf-v2-contract.ts +136 -1
- package/scripts/check-entwurf-v2-decider.ts +95 -1
- package/scripts/check-entwurf-v2-matrix.ts +14 -3
- package/scripts/check-entwurf-v2-native-push.ts +193 -0
- package/scripts/check-entwurf-v2-production.ts +68 -1
- package/scripts/check-entwurf-v2-runner.ts +58 -0
- package/scripts/check-entwurf-v2-surface.ts +35 -0
- package/scripts/check-install-surface.ts +357 -0
- package/scripts/check-native-push-adapter.ts +319 -0
- package/scripts/check-native-push-register.ts +130 -0
- package/scripts/dev-bin.sh +195 -0
- package/scripts/doctor-pi-provider.ts +140 -0
- package/scripts/meta-bridge-doctor.sh +36 -2
- package/scripts/register-pi-package.py +37 -3
- package/scripts/register-pi-provider.py +287 -0
- package/scripts/smoke-agy-hooks-state.sh +172 -0
- package/scripts/smoke-agy-install-state.sh +660 -0
- package/scripts/smoke-agy-native-push-live.ts +243 -0
- package/scripts/smoke-agy-statusline-state.sh +300 -0
- package/scripts/smoke-meta-async-drift.sh +9 -2
- package/scripts/smoke-meta-install-state.sh +20 -0
- package/scripts/smoke-pi-provider-state.sh +182 -0
- package/scripts/smoke-user-scope-citizen.sh +62 -0
- package/scripts/with-dist-lock.sh +81 -0
- package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
- package/scripts/__pycache__/meta-bridge-state.cpython-313.pyc +0 -0
- package/scripts/__pycache__/register-pi-package.cpython-313.pyc +0 -0
|
@@ -24,9 +24,22 @@
|
|
|
24
24
|
* caller passes watchArmed=false, so meta-self is intentionally
|
|
25
25
|
* FAIL-CLOSED until slice 2. Slices 1 and 2 close in the SAME release
|
|
26
26
|
* block, so no intermediate "meta self all-false" state is ever pushed.
|
|
27
|
+
* - meta-session (native-push backend, e.g. antigravity): replyable ⟺ recordBacked AND
|
|
28
|
+
* probeAlive — the SEPARATE native-push axis (봉인 6 / 보정①). A reply to
|
|
29
|
+
* an agy citizen is a direct injection into a live app-server conversation,
|
|
30
|
+
* so what makes it land is an adapter probe finding that conversation, NOT a
|
|
31
|
+
* mailbox watch. Reusing the receiver atom here would smuggle `watchArmed`
|
|
32
|
+
* (a mailbox-only signal) into a domain with no mailbox, and an agy sender
|
|
33
|
+
* — which never arms a watch — would report replyable:false forever.
|
|
27
34
|
* - external-mcp: never replyable — no authoritative reply address.
|
|
35
|
+
*
|
|
36
|
+
* `origin` stays identity PROVENANCE (where the sender identity came from), never a rail.
|
|
37
|
+
* Which rail a meta citizen's reply rides is a SECOND axis — `metaDeliveryDomain`, derived
|
|
38
|
+
* by the caller from `nativePushSupported(backend)`, not from `wakeMode` (direct-inject also
|
|
39
|
+
* covers codex/pi, which have no native-push adapter). Fail-closed: an unsupplied domain is
|
|
40
|
+
* not replyable.
|
|
28
41
|
*/
|
|
29
|
-
import { computeMetaReceiverActive } from "./entwurf-deliverability.js";
|
|
42
|
+
import { computeMetaReceiverActive, nativePushDeliverable } from "./entwurf-deliverability.js";
|
|
30
43
|
/**
|
|
31
44
|
* Decide whether a reply to this session is actually deliverable, from injected
|
|
32
45
|
* facts only. Pure: no env reads, no fs, no probing — the caller gathers the facts
|
|
@@ -53,18 +66,41 @@ export function computeSelfAddressability(facts) {
|
|
|
53
66
|
};
|
|
54
67
|
}
|
|
55
68
|
case "meta-session": {
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
// TWO rails, pinned apart (보정①). Each branch composes the predicate that OWNS its
|
|
70
|
+
// axis — the mailbox receiver atom and the native-push predicate share nothing, so a
|
|
71
|
+
// mailbox liveness fact can never leak into a backend that has no mailbox.
|
|
72
|
+
switch (facts.metaDeliveryDomain) {
|
|
73
|
+
case "native-push": {
|
|
74
|
+
const push = nativePushDeliverable({ recordBacked: facts.recordBacked, probeAlive: facts.probeAlive });
|
|
75
|
+
return {
|
|
76
|
+
replyable: push.deliverable,
|
|
77
|
+
socketState: "none",
|
|
78
|
+
reason: push.deliverable
|
|
79
|
+
? `native-push reachable (${push.reason})`
|
|
80
|
+
: `native-push unreachable — ${push.reason}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
case "self-fetch": {
|
|
84
|
+
// Share the active-receiver atom with the deliverability predicate (one
|
|
85
|
+
// source of truth for "record backed AND owner alive AND watch armed").
|
|
86
|
+
const recv = computeMetaReceiverActive({
|
|
87
|
+
recordBacked: facts.recordBacked,
|
|
88
|
+
ownerAlive: facts.ownerAlive,
|
|
89
|
+
watchArmed: facts.watchArmed,
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
replyable: recv.active,
|
|
93
|
+
socketState: "none",
|
|
94
|
+
reason: recv.active ? `meta receiver active (${recv.reason})` : `meta receiver inactive — ${recv.reason}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
return {
|
|
99
|
+
replyable: false,
|
|
100
|
+
socketState: "none",
|
|
101
|
+
reason: "meta delivery domain not supplied — cannot say which rail a reply would ride (fail-closed)",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
68
104
|
}
|
|
69
105
|
case "external-mcp":
|
|
70
106
|
return {
|
|
@@ -58,16 +58,35 @@ export const FACT_LIVENESSES = ["alive", "dead", "indeterminate", "unsupported"]
|
|
|
58
58
|
// handled by the domain guard before the table is consulted.
|
|
59
59
|
export const DISPATCH_LIVENESSES = ["live", "dormant", "indeterminate"];
|
|
60
60
|
// ── Backend liveness domain (R1 + F4) ──────────────────────────────────────
|
|
61
|
-
// Backends whose liveness predicate is DEFINED
|
|
62
|
-
// connect + RPC `get_info`, entwurf-control.ts).
|
|
63
|
-
// socket)
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
61
|
+
// Backends whose SOCKET liveness predicate is DEFINED — the pi control-socket
|
|
62
|
+
// domain ONLY (connect + RPC `get_info`, entwurf-control.ts). It stays ["pi"].
|
|
63
|
+
// claude-code (self-fetch, no socket) has no liveness predicate at all → `unsupported`.
|
|
64
|
+
// codex/antigravity are direct-inject; antigravity's liveness IS measured, but by the
|
|
65
|
+
// SEPARATE native-push adapter rail (a live app-server conversation probe), NOT this
|
|
66
|
+
// pi-socket domain — so it must NEVER be added here. Adding it would pull agy into the
|
|
67
|
+
// pi socket table (inspectSocket/probeSocket are socket-only); the fact layer keeps
|
|
68
|
+
// reporting agy `unsupported` = "outside the pi-socket liveness domain", NOT
|
|
69
|
+
// unreachable (the native-push rail measures it — entwurf-v2-decider.ts). Widening
|
|
70
|
+
// THIS set is a deliberate future decision (Stage 1+), gated by a REAL pi-shaped
|
|
71
|
+
// control-socket predicate — never by silently mapping sessions to dead/indeterminate
|
|
72
|
+
// (R1 핵심). check-entwurf-facts pins this == ["pi"] and asserts the native-push
|
|
73
|
+
// domain is disjoint from it.
|
|
67
74
|
export const LIVENESS_DOMAIN_BACKENDS = ["pi"];
|
|
68
75
|
export function isLivenessSupported(backend) {
|
|
69
76
|
return LIVENESS_DOMAIN_BACKENDS.includes(backend);
|
|
70
77
|
}
|
|
78
|
+
// ── Native-push backend domain (봉인 2/4) ───────────────────────────────────
|
|
79
|
+
// A backend whose liveness is measured by the SEPARATE native-push adapter rail (a
|
|
80
|
+
// live app-server conversation probe — antigravity's LS gRPC), NOT the pi control
|
|
81
|
+
// socket. This domain is DISJOINT from LIVENESS_DOMAIN_BACKENDS (pi socket): an agy
|
|
82
|
+
// session is `unsupported` at the pi-socket FACT level (entwurf_peers) yet fully
|
|
83
|
+
// measured + deliverable on the native-push axis. The two are separate rails on
|
|
84
|
+
// purpose — check-entwurf-facts pins both sets and asserts their intersection is ∅
|
|
85
|
+
// (a backend can never be in both a socket-liveness domain and a native-push domain).
|
|
86
|
+
export const NATIVE_PUSH_BACKENDS = ["antigravity"];
|
|
87
|
+
export function nativePushSupported(backend) {
|
|
88
|
+
return NATIVE_PUSH_BACKENDS.includes(backend);
|
|
89
|
+
}
|
|
71
90
|
/**
|
|
72
91
|
* Compose the 4-value FACT liveness from a backend and its socket probe.
|
|
73
92
|
* Out-of-domain backend → `unsupported` (NOT dead/indeterminate, R1). An
|
|
@@ -93,6 +112,9 @@ export const ENTWURF_V2_REJECT_REASONS = [
|
|
|
93
112
|
"owned-live-no-autosend", // Q2/F1: owned-outcome to a live target is not an auto-send
|
|
94
113
|
"backend-liveness-unsupported", // R1: backend has no liveness predicate (e.g. claude-code) — owned-outcome only
|
|
95
114
|
"mailbox-undeliverable", // F-mailbox: fire-and-forget to an unsupported citizen whose mailbox is not deliverable (fail-closed; future pi-backend non-drainable mailbox)
|
|
115
|
+
"native-push-target-dead", // 봉인 1: fire-and-forget to a native-push (agy) target whose adapter probe found NO live conversation. Post-probe; observedLiveness = dead. NOT `backend-liveness-unsupported` — a native-push backend IS measured, so that name would be a lie.
|
|
116
|
+
"native-push-probe-indeterminate", // 봉인 1: fire-and-forget to a native-push target whose adapter probe was inconclusive (agy alive but no port served the conv, or a probe error). Post-probe; observedLiveness = indeterminate. Never spawns, never coerced to dead.
|
|
117
|
+
"native-push-no-resume-authority", // 봉인 1: owned-outcome to a native-push target (any liveness — single, state-independent). A native-push backend has no resume/spawn authority (there is no pi-child to own), so the caller cannot own its completion; use fire-and-forget. Post-probe; observedLiveness = the measured value.
|
|
96
118
|
"bad-target", // R2: absent/typo garden-id (no existing citizen); spawn-new out of v2 scope
|
|
97
119
|
"untrusted-fail-fast", // 동결결정 5: controlled launch into an untrusted cwd
|
|
98
120
|
"socket-only-no-resume-authority", // A1: a record-less socket-only endpoint resolved to a resume verdict (owned-outcome × dormant), but spawn-bg cannot open into it — no trusted cwd/resume authority. Post-probe guard reject (NOT pre-probe, NOT a table resolver cell): the in-domain probe ran and measured the liveness, then `allowResume:false` refused the resume. Carries the honest measured FactLiveness (non-null), unlike the pre-probe `bad-target` it replaces here — a live/addressable socket-only citizen must NEVER be mislabeled absent.
|
|
@@ -160,7 +182,18 @@ export const RESOLVER_REJECT_REASONS = [
|
|
|
160
182
|
// `meta-mailbox` (F-mailbox) = liveness-free delivery via the 0.10.0 meta-bridge
|
|
161
183
|
// mailbox + doorbell. The ack is "enqueued + doorbell rung", NOT a read and NOT a
|
|
162
184
|
// turn injection — so `mode` (steer/follow_up) is meaningless on this transport.
|
|
163
|
-
|
|
185
|
+
// `native-push` (봉인 1) = direct injection into a LIVE native app-server conversation
|
|
186
|
+
// (antigravity `agentapi send-message`). Like meta-mailbox it is a fire-and-forget
|
|
187
|
+
// send arm (ack-only), but it requires a live-probe (NATIVE_PUSH_DISPATCH_TABLE),
|
|
188
|
+
// where meta-mailbox is liveness-free. It is NOT a mailbox enqueue and NOT a pi socket
|
|
189
|
+
// send — it is its own rail.
|
|
190
|
+
export const ENTWURF_V2_TRANSPORTS = [
|
|
191
|
+
"control-socket",
|
|
192
|
+
"spawn-bg",
|
|
193
|
+
"tmux-live",
|
|
194
|
+
"meta-mailbox",
|
|
195
|
+
"native-push",
|
|
196
|
+
];
|
|
164
197
|
// Allow-branch facets (exported so the schema↔types gate asserts every enum).
|
|
165
198
|
export const ENTWURF_V2_ACTIONS = ["send", "resume"];
|
|
166
199
|
export const ENTWURF_V2_OWNERSHIPS = ["ack-only", "owned"];
|
|
@@ -188,10 +221,13 @@ export const DISPATCH_TABLE = {
|
|
|
188
221
|
},
|
|
189
222
|
};
|
|
190
223
|
// ── The unsupported-backend mailbox mini-table (F-mailbox) ─────────────────
|
|
191
|
-
// SEPARATE from the in-domain 6-cell DISPATCH_TABLE (Fable (i)):
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
224
|
+
// SEPARATE from the in-domain 6-cell DISPATCH_TABLE (Fable (i)): a backend with no
|
|
225
|
+
// pi-socket liveness predicate never enters the liveness-keyed table. Instead the
|
|
226
|
+
// domain guard routes it here, keyed on intent alone. Reaches here: claude-code
|
|
227
|
+
// (self-fetch mailbox) and codex (no adapter yet). Does NOT reach here: antigravity —
|
|
228
|
+
// the decider intercepts a native-push backend in its own rail BEFORE this mailbox
|
|
229
|
+
// mini-table (entwurf-v2-decider.ts), so agy is `unsupported` at the fact level yet
|
|
230
|
+
// never falls through to a mailbox it does not have. The cells, keyed on intent alone:
|
|
195
231
|
// - fire-and-forget needs no liveness — the 0.10.0 meta-bridge mailbox delivers
|
|
196
232
|
// to any DELIVERABLE citizen. This cell is the deliverable path; resolveDispatch
|
|
197
233
|
// downgrades it to `mailbox-undeliverable` when the separate mailboxDeliverable
|
|
@@ -282,6 +318,63 @@ export function resolveDispatch(intent, liveness, mailboxDeliverable) {
|
|
|
282
318
|
observedLiveness: liveness,
|
|
283
319
|
};
|
|
284
320
|
}
|
|
321
|
+
// ── The native-push dispatch table (봉인 1/2/4) ─────────────────────────────
|
|
322
|
+
// A THIRD table, distinct from both the pi 6-cell DISPATCH_TABLE and the unsupported
|
|
323
|
+
// mailbox mini-table. Keyed intent × NativePushLiveness (NOT intent-only): a
|
|
324
|
+
// native-push backend (antigravity) IS measured by its adapter probe, so the
|
|
325
|
+
// send/reject decision depends on the probed liveness. The decider intercepts a
|
|
326
|
+
// native-push backend in its own rail (nativePushSupported → probe → this table)
|
|
327
|
+
// BEFORE the unsupported branch, so agy never falls through to a mailbox it lacks.
|
|
328
|
+
//
|
|
329
|
+
// fire-and-forget × alive → native-push send (the ONE allow cell)
|
|
330
|
+
// fire-and-forget × dead → reject native-push-target-dead
|
|
331
|
+
// fire-and-forget × indeterminate → reject native-push-probe-indeterminate
|
|
332
|
+
// owned-outcome × * → reject native-push-no-resume-authority (state-
|
|
333
|
+
// independent: no pi-child to own; `backend-
|
|
334
|
+
// liveness-unsupported` is NOT reused — false name).
|
|
335
|
+
export const NATIVE_PUSH_DISPATCH_TABLE = {
|
|
336
|
+
"fire-and-forget": {
|
|
337
|
+
alive: { action: "send", transport: "native-push", ownership: "ack-only" },
|
|
338
|
+
dead: { action: "reject", reason: "native-push-target-dead" },
|
|
339
|
+
indeterminate: { action: "reject", reason: "native-push-probe-indeterminate" },
|
|
340
|
+
},
|
|
341
|
+
"owned-outcome": {
|
|
342
|
+
alive: { action: "reject", reason: "native-push-no-resume-authority" },
|
|
343
|
+
dead: { action: "reject", reason: "native-push-no-resume-authority" },
|
|
344
|
+
indeterminate: { action: "reject", reason: "native-push-no-resume-authority" },
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
// The reasons the native-push resolver emits — a THIRD post-probe reject set, parallel
|
|
348
|
+
// to RESOLVER_REJECT_REASONS (pi/mailbox). All post-probe: resolveNativePushDispatch
|
|
349
|
+
// always has a real probed liveness in hand, so observedLiveness is non-null. None may
|
|
350
|
+
// be pre-probe (they are never in PRE_PROBE_REJECT_REASONS).
|
|
351
|
+
export const NATIVE_PUSH_REJECT_REASONS = [
|
|
352
|
+
"native-push-target-dead",
|
|
353
|
+
"native-push-probe-indeterminate",
|
|
354
|
+
"native-push-no-resume-authority",
|
|
355
|
+
];
|
|
356
|
+
/**
|
|
357
|
+
* PURE native-push dispatch decision (봉인 4). Given the caller intent and the adapter
|
|
358
|
+
* probe's 3-value liveness, mint the receipt from NATIVE_PUSH_DISPATCH_TABLE. Mirrors
|
|
359
|
+
* resolveDispatch's shape; observedLiveness is ALWAYS the probed value (non-null,
|
|
360
|
+
* post-probe). The decider calls this only AFTER nativePushSupported(backend) gates the
|
|
361
|
+
* backend and the adapter probe returns a liveness — it never touches the pi socket
|
|
362
|
+
* table or the mailbox mini-table (those are other domains). No IO here (the probe is
|
|
363
|
+
* the decider's injected dep); this only maps (intent, liveness) → verdict.
|
|
364
|
+
*/
|
|
365
|
+
export function resolveNativePushDispatch(intent, liveness) {
|
|
366
|
+
const cell = NATIVE_PUSH_DISPATCH_TABLE[intent][liveness];
|
|
367
|
+
if (cell.action === "reject") {
|
|
368
|
+
return makeRejectReceipt(cell.reason, liveness);
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
ok: true,
|
|
372
|
+
action: cell.action,
|
|
373
|
+
transport: cell.transport,
|
|
374
|
+
ownership: cell.ownership,
|
|
375
|
+
observedLiveness: liveness,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
285
378
|
// ── TypeBox schemas ────────────────────────────────────────────────────────
|
|
286
379
|
// MOVED to `entwurf-v2-contract-schema.ts` (0.12.1 B-1): the pi-ai TypeBox
|
|
287
380
|
// builders (StringEnum/Type) are a pi-lane dependency, so they cannot live in
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* be bypassed.
|
|
45
45
|
*/
|
|
46
46
|
import { isNonPiGardenIdSocketConflict } from "./entwurf-facts.js";
|
|
47
|
-
import { isLivenessSupported, makeRejectReceipt, resolveDispatch, } from "./entwurf-v2-contract.js";
|
|
47
|
+
import { isLivenessSupported, makeRejectReceipt, nativePushSupported, resolveDispatch, resolveNativePushDispatch, } from "./entwurf-v2-contract.js";
|
|
48
48
|
import { defaultMetaMailboxDir, defaultMetaSessionsDir, metaCapabilityFor, } from "./meta-session.js";
|
|
49
49
|
import { isValidSessionId } from "./session-id.js";
|
|
50
50
|
import { controlSocketPath, mapInspectionToLiveness } from "./socket-discovery.js";
|
|
@@ -131,6 +131,35 @@ export async function decideDispatch(input, deps) {
|
|
|
131
131
|
return reject(makeRejectReceipt("target-address-conflict", null));
|
|
132
132
|
}
|
|
133
133
|
const identity = resolution.identity;
|
|
134
|
+
// 2d. native-push rail (봉인 4): a native-push backend (antigravity) is measured by its
|
|
135
|
+
// adapter probe, NOT the pi socket and NOT the mailbox. Intercept it HERE — after identity
|
|
136
|
+
// resolution + the address-conflict guard, but BEFORE the unsupported mailbox branch — so
|
|
137
|
+
// agy routes to native-push and never falls through to a mailbox it does not have. This
|
|
138
|
+
// branch is LOCK-FREE (봉인 4): the pi in-domain lock closes a socket TOCTOU, but a
|
|
139
|
+
// volatile probe route has no lock meaning (a duplicate-send idempotency is a D8 future).
|
|
140
|
+
if (nativePushSupported(identity.backend)) {
|
|
141
|
+
const probe = await deps.nativePushProbe(identity);
|
|
142
|
+
const receipt = resolveNativePushDispatch(input.intent, probe.status);
|
|
143
|
+
if (!receipt.ok)
|
|
144
|
+
return reject(receipt);
|
|
145
|
+
// The ONLY allow cell is fire-and-forget × alive, so an ok receipt ⟹ the probe is
|
|
146
|
+
// alive and carries a route. The narrow is defensive: a contract-breaking probe/table
|
|
147
|
+
// mismatch fails loud rather than planting a routeless send plan.
|
|
148
|
+
if (probe.status !== "alive") {
|
|
149
|
+
throw new Error("entwurf_v2 decider: native-push send verdict without an alive probe route (contract invariant broken).");
|
|
150
|
+
}
|
|
151
|
+
const plan = {
|
|
152
|
+
transport: "native-push",
|
|
153
|
+
action: "send",
|
|
154
|
+
targetGardenId: gardenId,
|
|
155
|
+
backend: identity.backend,
|
|
156
|
+
nativeSessionId: identity.nativeSessionId,
|
|
157
|
+
route: probe.route,
|
|
158
|
+
wantsReply,
|
|
159
|
+
message: input.message,
|
|
160
|
+
};
|
|
161
|
+
return { kind: "execute", receipt, plan, lock: null };
|
|
162
|
+
}
|
|
134
163
|
// 3. backend.
|
|
135
164
|
if (!isLivenessSupported(identity.backend)) {
|
|
136
165
|
// 6. unsupported path — NO lock (?7). Deliverability comes from the REQUIRED seam
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entwurf-v2-native-push — the native-push SEND hand (봉인 3/4, the executor half of the
|
|
3
|
+
* native-push rail). It takes the `native-push` ExecutionPlan the 5b decider ALREADY chose
|
|
4
|
+
* and direct-injects the message into the live conversation via the adapter. It DECIDES NO
|
|
5
|
+
* ROUTING (the NATIVE_PUSH_DISPATCH_TABLE did) and carries NO release seam — a native-push
|
|
6
|
+
* plan is the LOCK-FREE path (봉인 4), so a lock leak is structurally impossible here.
|
|
7
|
+
*
|
|
8
|
+
* This is where the 1-shot retry lives — NOT in the adapter leaf (봉인 3). The decider
|
|
9
|
+
* probed a fresh route and planted it in the plan; the executor sends over it, and on a
|
|
10
|
+
* failure it re-probes ONCE (the volatile LS port may have shifted between decide and
|
|
11
|
+
* execute) and re-sends. A second failure is fail-loud (no infinite loop). This mirrors the
|
|
12
|
+
* control-socket send-fallback pattern: the decider stays pure, the hand owns the retry.
|
|
13
|
+
*
|
|
14
|
+
* A native-push send has NO in-band refuse (there is no live receiver to answer
|
|
15
|
+
* success:false) — like a mailbox enqueue, it either succeeds or THROWS. The hand never
|
|
16
|
+
* folds a throw into {success:false}; the runner maps a thrown error to execution-failed.
|
|
17
|
+
*/
|
|
18
|
+
import { resolveNativePushAdapter } from "./native-push/adapter.js";
|
|
19
|
+
/**
|
|
20
|
+
* Deliver `content` into the conversation, owning the 1-shot retry (봉인 3). Send over the
|
|
21
|
+
* planted (decider-probed) route first; on failure re-probe ONCE and re-send over the fresh
|
|
22
|
+
* route; a second failure PROPAGATES (fail-loud). If the re-probe finds the target no longer
|
|
23
|
+
* alive, throw — a failed send into a now-dead conversation is an honest non-delivery, never
|
|
24
|
+
* a silent success. The re-probe is the ONLY re-derivation of the volatile route.
|
|
25
|
+
*/
|
|
26
|
+
export async function deliverViaNativePush(adapter, route, nativeSessionId, content) {
|
|
27
|
+
try {
|
|
28
|
+
await adapter.send(route, nativeSessionId, content);
|
|
29
|
+
return { success: true, retried: false };
|
|
30
|
+
}
|
|
31
|
+
catch (firstErr) {
|
|
32
|
+
// 1-shot re-probe → re-send: the volatile LS route may have shifted since the decider
|
|
33
|
+
// probed it. Re-discover it fresh and retry exactly once.
|
|
34
|
+
const reprobe = await adapter.probe(nativeSessionId);
|
|
35
|
+
if (reprobe.status !== "alive") {
|
|
36
|
+
throw new Error(`native-push deliver: first send failed and re-probe found target ${reprobe.status} (${reprobe.reason}) — not retried`, { cause: firstErr });
|
|
37
|
+
}
|
|
38
|
+
// A second failure THROWS out of this call (fail-loud) — no third attempt.
|
|
39
|
+
await adapter.send(reprobe.route, nativeSessionId, content);
|
|
40
|
+
return { success: true, retried: true };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the production `sendNativePush(plan, lock)` adapter the runner consumes. It IGNORES
|
|
45
|
+
* `lock` entirely (a native-push plan is lock-free, 봉인 4) — the field exists only to match
|
|
46
|
+
* the DispatchExecutorDeps hand signature. It resolves the adapter from the plan's backend
|
|
47
|
+
* and delivers with the 1-shot retry. A delivery throw surfaces as a REJECTED promise (the
|
|
48
|
+
* runner's try/catch maps it to execution-failed).
|
|
49
|
+
*/
|
|
50
|
+
export function makeNativePushSend(deps = {}) {
|
|
51
|
+
const resolveAdapter = deps.resolveAdapter ?? resolveNativePushAdapter;
|
|
52
|
+
// `_lock` is named for the hand contract but NEVER read — native-push owns/releases no lock.
|
|
53
|
+
return async (plan, _lock) => {
|
|
54
|
+
const adapter = resolveAdapter(plan.backend);
|
|
55
|
+
return deliverViaNativePush(adapter, plan.route, plan.nativeSessionId, plan.message);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -32,11 +32,13 @@ import { isLivenessSupported } from "./entwurf-v2-contract.js";
|
|
|
32
32
|
import { decideDispatch, } from "./entwurf-v2-decider.js";
|
|
33
33
|
import { ENTWURF_V2_LOCK_DIR, acquireLock as realAcquireLock, releaseLock as realReleaseLock, } from "./entwurf-v2-lock.js";
|
|
34
34
|
import { makeProductionSendViaMailbox } from "./entwurf-v2-mailbox.js";
|
|
35
|
+
import { makeNativePushSend } from "./entwurf-v2-native-push.js";
|
|
35
36
|
import { executeControlSocketSend, } from "./entwurf-v2-send.js";
|
|
36
37
|
import { resolveDeadControlSendFallback } from "./entwurf-v2-send-fallback.js";
|
|
37
38
|
import { executeSpawnBgResume } from "./entwurf-v2-spawn.js";
|
|
38
39
|
import { makeProductionSpawnBgResumeDeps } from "./entwurf-v2-spawn-production.js";
|
|
39
40
|
import { defaultMetaMailboxDir, defaultMetaSessionsDir, enqueueMetaMessage, metaCapabilityFor, metaRecordExistsByGardenId, readMetaIdentityByGardenId, readMetaReceiverMarker, } from "./meta-session.js";
|
|
41
|
+
import { resolveNativePushAdapter as realResolveNativePushAdapter, } from "./native-push/adapter.js";
|
|
40
42
|
import { CONTROL_SOCKET_DIR, controlSocketPath, inspectControlSocketPath, inspectTargetControlSocket, isSocketOnlyPiCandidate, } from "./socket-discovery.js";
|
|
41
43
|
import { classifyConnectError, probeSocketLiveness } from "./socket-probe.js";
|
|
42
44
|
/**
|
|
@@ -96,6 +98,7 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
96
98
|
classifyConnect: s.classifyConnect ?? classifyConnectError,
|
|
97
99
|
sendRpc: s.sendRpc ?? realSendRpc,
|
|
98
100
|
enqueue: s.enqueue ?? enqueueMetaMessage,
|
|
101
|
+
resolveNativePushAdapter: s.resolveNativePushAdapter ?? realResolveNativePushAdapter,
|
|
99
102
|
spawnOverrides: s.spawnOverrides ?? {},
|
|
100
103
|
};
|
|
101
104
|
// ── ONE lock domain (Q2/QB3): a single lockDir-bound release for ALL hands ─
|
|
@@ -168,6 +171,10 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
168
171
|
probeSocket,
|
|
169
172
|
preflightForCwd: (cwd) => io.preflight({ cwd, agentDir: opts.agentDir, prefixRoots: opts.prefixRoots }),
|
|
170
173
|
mailboxDeliverabilityFor,
|
|
174
|
+
// 봉인 4: resolve the native-push adapter for this backend + probe the conversation.
|
|
175
|
+
// Only reached on a nativePushSupported backend (the decider gates it), so the resolver
|
|
176
|
+
// never throws for a non-native-push backend here.
|
|
177
|
+
nativePushProbe: (identity) => Promise.resolve(io.resolveNativePushAdapter(identity.backend).probe(identity.nativeSessionId)),
|
|
171
178
|
mailboxDir,
|
|
172
179
|
sessionsDir,
|
|
173
180
|
observeTimeoutMs: opts.observeTimeoutMs,
|
|
@@ -210,6 +217,9 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
210
217
|
releaseFn: release,
|
|
211
218
|
})),
|
|
212
219
|
sendMailbox: (plan, _lock) => sendViaMailbox(plan, _lock),
|
|
220
|
+
// native-push (봉인 4): the SAME injected adapter resolver drives the executor send,
|
|
221
|
+
// so the decider's probe and the delivery use one adapter. Lock-free (lock ignored).
|
|
222
|
+
sendNativePush: makeNativePushSend({ resolveAdapter: io.resolveNativePushAdapter }),
|
|
213
223
|
};
|
|
214
224
|
return {
|
|
215
225
|
decide: (input) => decideDispatch(input, deciderDeps),
|
|
@@ -62,6 +62,15 @@ export function decideReleasePolicy(plan, lock) {
|
|
|
62
62
|
throw new Error("entwurf-v2-release: a meta-mailbox plan must carry no lock (?7 invariant violated).");
|
|
63
63
|
}
|
|
64
64
|
return { kind: "no-lock" };
|
|
65
|
+
case "native-push":
|
|
66
|
+
// Native-push is the other lock-free rail (봉인 4) — like meta-mailbox it holds no
|
|
67
|
+
// lock, so a non-null lock is a decider contract violation. (The native-push hand
|
|
68
|
+
// never actually routes through this reducer; the case keeps the switch exhaustive
|
|
69
|
+
// and pins the lock-free invariant.)
|
|
70
|
+
if (lock !== null) {
|
|
71
|
+
throw new Error("entwurf-v2-release: a native-push plan must carry no lock (봉인 4 lock-free rail violated).");
|
|
72
|
+
}
|
|
73
|
+
return { kind: "no-lock" };
|
|
65
74
|
case "control-socket":
|
|
66
75
|
assertInDomainLock(plan, lock);
|
|
67
76
|
return { kind: "release-after-send-final" };
|
|
@@ -98,6 +98,27 @@ export async function executeDispatch(decision, deps) {
|
|
|
98
98
|
return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
case "native-push": {
|
|
102
|
+
try {
|
|
103
|
+
// lock is null here (lock-free rail, 봉인 4) — passed verbatim; the hand ignores it.
|
|
104
|
+
const r = await deps.sendNativePush(plan, lock);
|
|
105
|
+
// Like meta-mailbox, native-push has NO in-band reject (no live receiver answers
|
|
106
|
+
// success:false). A `success:false` is therefore a CONTRACT VIOLATION — fail loud
|
|
107
|
+
// rather than render a non-delivery as delivered ("Never warn. Throw.").
|
|
108
|
+
if (r.success !== true) {
|
|
109
|
+
throw new Error("entwurf-v2-runner: native-push send returned success:false (contract violation; native-push has no in-band reject).");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
kind: "executed",
|
|
113
|
+
receipt,
|
|
114
|
+
transport,
|
|
115
|
+
outcome: { transport: "native-push", success: true, retried: r.retried },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
101
122
|
}
|
|
102
123
|
}
|
|
103
124
|
/**
|
|
@@ -132,6 +132,11 @@ async function driveDeadFallback(plan, lock, deps) {
|
|
|
132
132
|
// contract violation (a send and a spawn are different actions), so fail loud
|
|
133
133
|
// rather than silently mis-execute.
|
|
134
134
|
throw new Error("entwurf-v2-send: re-resolve returned a spawn-bg plan for a send fallback (contract violation).");
|
|
135
|
+
case "native-push":
|
|
136
|
+
// The dead-control-socket fallback re-resolves only the pi socket domain
|
|
137
|
+
// (control-socket / mailbox / spawn); it never routes the native-push rail. A
|
|
138
|
+
// native-push rePlan here is structurally impossible — fail loud.
|
|
139
|
+
throw new Error("entwurf-v2-send: re-resolve returned a native-push plan for a send fallback (contract violation).");
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
/**
|
|
@@ -71,6 +71,16 @@ export function actionableRejectHint(reason) {
|
|
|
71
71
|
case "owned-live-no-autosend":
|
|
72
72
|
// A live target is reachable, but owned-outcome is not an auto-send (Q2/F1).
|
|
73
73
|
return "target is live — owned-outcome never auto-sends. Use intent: fire-and-forget (with wants_reply if you need a reply).";
|
|
74
|
+
case "native-push-no-resume-authority":
|
|
75
|
+
// A native-push backend (antigravity) has no pi-child to own, so owned-outcome has
|
|
76
|
+
// nothing to own. Delivery is a direct inject via fire-and-forget.
|
|
77
|
+
return "native-push target (e.g. antigravity) has no resume/spawn authority → owned-outcome is unsupported. Use intent: fire-and-forget to direct-inject into the live conversation.";
|
|
78
|
+
case "native-push-target-dead":
|
|
79
|
+
// The adapter probe found no live host process for the conversation.
|
|
80
|
+
return "native-push conversation is not live (no host process found). Re-open the conversation, then retry — there is nothing to inject into.";
|
|
81
|
+
case "native-push-probe-indeterminate":
|
|
82
|
+
// Host up, but no LS port served the conversation — inconclusive, not a hard dead.
|
|
83
|
+
return "native-push host is up but no port served this conversation (probe inconclusive). Retry once the conversation is loaded, or verify the conversation id.";
|
|
74
84
|
default:
|
|
75
85
|
return undefined;
|
|
76
86
|
}
|
|
@@ -127,6 +137,13 @@ export function renderEntwurfV2Result(result) {
|
|
|
127
137
|
const exit = res.kind === "child-exited" ? ` exitCode=${res.exitCode}` : "";
|
|
128
138
|
return { text: `entwurf_v2 spawn-bg → ${res.kind}${pid}${exit}, lock released`, isError: false };
|
|
129
139
|
}
|
|
140
|
+
if (o.transport === "native-push") {
|
|
141
|
+
// direct-inject succeeded; note if the 1-shot re-probe retry fired.
|
|
142
|
+
return {
|
|
143
|
+
text: `entwurf_v2 native-push → delivered${o.retried ? " (after a 1-shot re-probe retry)" : ""}`,
|
|
144
|
+
isError: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
130
147
|
// meta-mailbox
|
|
131
148
|
return { text: "entwurf_v2 meta-mailbox → enqueued", isError: false };
|
|
132
149
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-sender-identity — WHO is calling this MCP child?
|
|
3
|
+
*
|
|
4
|
+
* A native backend (Claude Code, agy) spawns the entwurf-bridge MCP server as a child of the
|
|
5
|
+
* same process that runs its session hook. The hook writes a sender marker keyed by ITS parent
|
|
6
|
+
* pid; the child looks a marker up under its own parent. That shared ancestor is the join key —
|
|
7
|
+
* NOT cwd (one repo can hold many sessions) and not a wire field (neither host carries one).
|
|
8
|
+
*
|
|
9
|
+
* Measured 2026-07-13 on both backends: hook.ppid == bridge.ppid == the native host pid, same
|
|
10
|
+
* start-key. The extra `parentPid(ppid)` candidate covers a host that runs its hook through a
|
|
11
|
+
* shell wrapper, which shifts the shared ancestor one step up.
|
|
12
|
+
*
|
|
13
|
+
* Two guards make a marker an IDENTITY rather than a hint, and a candidate is only trusted after
|
|
14
|
+
* BOTH pass:
|
|
15
|
+
* 1. pid + start-key (readMetaSenderMarker): the owner is still the very process that wrote it,
|
|
16
|
+
* so a dead session's pid, reused by something else, cannot inherit its garden-id.
|
|
17
|
+
* 2. the backing meta-record: the record store is the authority — a marker whose record was
|
|
18
|
+
* deleted, or whose backend/nativeSessionId drifted from it, names nobody.
|
|
19
|
+
*
|
|
20
|
+
* Every candidate is collected and validated BEFORE one is chosen. A first-match loop would make
|
|
21
|
+
* the answer depend on which pid or backend happened to be read first; here lookup order carries
|
|
22
|
+
* no meaning, and two live identities are a refusal rather than a race.
|
|
23
|
+
*/
|
|
24
|
+
import { parentPid, readMetaIdentityByGardenId, readMetaSenderMarker, } from "./meta-session.js";
|
|
25
|
+
import { resolveNativePushAdapter } from "./native-push/adapter.js";
|
|
26
|
+
/** Every native backend that mints a garden-id from its own hook and writes a sender marker. */
|
|
27
|
+
export const META_SENDER_BACKENDS = ["claude-code", "antigravity"];
|
|
28
|
+
/**
|
|
29
|
+
* Two live native sessions claim this MCP process as their own. We can SEE both identities but
|
|
30
|
+
* cannot say which one is calling, so we send under neither. Choosing by lookup order, backend
|
|
31
|
+
* priority, or recency would attribute a message to a citizen that did not write it; falling back
|
|
32
|
+
* to anonymous would hide an identity we already hold. Both are the SE-1 shape — a layer answering
|
|
33
|
+
* "yes" where the truth is unknown — so this is a hard refusal, not a warning.
|
|
34
|
+
*
|
|
35
|
+
* WHAT THIS CAN AND CANNOT SEE. It fires only when the candidate pids yield MORE THAN ONE marker
|
|
36
|
+
* FILE naming different citizens — two backends on one pid, or the two candidate pids naming
|
|
37
|
+
* different citizens. It CANNOT see two conversations of the same backend racing under one host
|
|
38
|
+
* pid: they share the single marker path `<senders>/<backend>/<pid>.json`, so the later hook write
|
|
39
|
+
* overwrites the earlier one and only one identity is ever on disk. That case is guarded by a
|
|
40
|
+
* runtime invariant instead (a native host serializes its model invocations), not by this error —
|
|
41
|
+
* do not read a green resolver as proof that same-process concurrency is safe.
|
|
42
|
+
*/
|
|
43
|
+
export class EntwurfSenderIdentityAmbiguityError extends Error {
|
|
44
|
+
gardenIds;
|
|
45
|
+
constructor(gardenIds) {
|
|
46
|
+
super("entwurf-bridge refused: ambiguous sender identity. This MCP process's owner pid carries live, " +
|
|
47
|
+
`record-backed sender markers for MORE than one garden citizen (${gardenIds.join(", ")}), so which ` +
|
|
48
|
+
"one is calling cannot be determined. A send under the wrong identity is worse than no send — one " +
|
|
49
|
+
"native host process appears to be driving several sessions at once. Report it; do not work around it.");
|
|
50
|
+
this.name = "EntwurfSenderIdentityAmbiguityError";
|
|
51
|
+
this.gardenIds = gardenIds;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The record store is the authority; the marker is only a pid→garden hint it must agree with. */
|
|
55
|
+
function trustMarker(marker) {
|
|
56
|
+
try {
|
|
57
|
+
// dual-read (3D-4 commit1): identity-only check, so it survives the v2 cut.
|
|
58
|
+
const identity = readMetaIdentityByGardenId(marker.gardenId);
|
|
59
|
+
if (identity.backend !== marker.backend || identity.nativeSessionId !== marker.nativeSessionId)
|
|
60
|
+
return null;
|
|
61
|
+
return { marker, identity };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the ONE identity that owns this MCP process, or refuse.
|
|
69
|
+
*
|
|
70
|
+
* 0 trusted → null (anonymous — or a hard refusal upstream under REQUIRE_META_SENDER).
|
|
71
|
+
* 1 trusted → that identity.
|
|
72
|
+
* 2+ distinct → throw EntwurfSenderIdentityAmbiguityError.
|
|
73
|
+
*
|
|
74
|
+
* Markers that agree on the SAME garden-id are not a conflict: an older release wrote a marker for
|
|
75
|
+
* the parent AND the grandparent, and both can still sit on disk pointing at one citizen.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveTrustedMetaSenderIdentity(opts = {}) {
|
|
78
|
+
const markers = [];
|
|
79
|
+
if (opts.markerPath) {
|
|
80
|
+
const marker = readMetaSenderMarker({ markerPath: opts.markerPath });
|
|
81
|
+
if (marker)
|
|
82
|
+
markers.push(marker);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const ownerPids = (opts.ownerPids ?? [process.ppid, parentPid(process.ppid) ?? 0]).filter((p) => typeof p === "number" && p > 0);
|
|
86
|
+
for (const ownerPid of [...new Set(ownerPids)]) {
|
|
87
|
+
for (const backend of META_SENDER_BACKENDS) {
|
|
88
|
+
const marker = readMetaSenderMarker({ backend, ownerPid, sendersDir: opts.sendersDir });
|
|
89
|
+
if (marker)
|
|
90
|
+
markers.push(marker);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const byGardenId = new Map();
|
|
95
|
+
for (const marker of markers) {
|
|
96
|
+
const trusted = trustMarker(marker);
|
|
97
|
+
if (trusted)
|
|
98
|
+
byGardenId.set(trusted.identity.gardenId, trusted);
|
|
99
|
+
}
|
|
100
|
+
const distinct = [...byGardenId.values()];
|
|
101
|
+
if (distinct.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
if (distinct.length > 1)
|
|
104
|
+
throw new EntwurfSenderIdentityAmbiguityError(distinct.map((t) => t.identity.gardenId));
|
|
105
|
+
return distinct[0];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Can a reply to THIS native-push citizen actually land? Only an adapter probe can say: a reply is
|
|
109
|
+
* a direct injection into a live app-server conversation, and the route is re-discovered on every
|
|
110
|
+
* probe. This is the `replyable` fact for a native-push sender — it must never be inferred from the
|
|
111
|
+
* mailbox axis, which such a backend has no part in (보정①).
|
|
112
|
+
*
|
|
113
|
+
* ERROR POLICY (deliberate, and the reason this is not a `try { … } catch { return false }`): the
|
|
114
|
+
* adapter already expresses every OPERATIONAL outcome as a value — `dead` (no host) and
|
|
115
|
+
* `indeterminate` (host up, no port served it). So a THROW out of here is never a fact about the
|
|
116
|
+
* citizen; it is a defect — an unresolvable adapter for a backend we just confirmed is native-push
|
|
117
|
+
* (a registry bug), or a probe runner that could not run at all (a wiring bug). Folding those into
|
|
118
|
+
* `replyable:false` would tell the receiver a lie about this sender AND bury the defect, which is
|
|
119
|
+
* exactly the Crash-Don't-Warn shape this lane exists to remove. Let them propagate.
|
|
120
|
+
*/
|
|
121
|
+
export async function probeNativeSenderAlive(identity, deps = {}) {
|
|
122
|
+
const resolveAdapter = deps.resolveAdapter ?? resolveNativePushAdapter;
|
|
123
|
+
const probe = await resolveAdapter(identity.backend).probe(identity.nativeSessionId);
|
|
124
|
+
return probe.status === "alive";
|
|
125
|
+
}
|