@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,13 +24,29 @@
|
|
|
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
42
|
|
|
30
|
-
import { computeMetaReceiverActive } from "./entwurf-deliverability.ts";
|
|
43
|
+
import { computeMetaReceiverActive, nativePushDeliverable } from "./entwurf-deliverability.ts";
|
|
31
44
|
|
|
32
45
|
export type SelfOrigin = "pi-session" | "meta-session" | "external-mcp";
|
|
33
46
|
|
|
47
|
+
/** Which delivery rail carries a reply back to a meta citizen (the second axis, never `origin`). */
|
|
48
|
+
export type MetaDeliveryDomain = "self-fetch" | "native-push";
|
|
49
|
+
|
|
34
50
|
/**
|
|
35
51
|
* pi control-socket reachability for a reply addressed back to this session.
|
|
36
52
|
*
|
|
@@ -49,12 +65,16 @@ export interface SelfAddressabilityFacts {
|
|
|
49
65
|
socketAlive?: boolean;
|
|
50
66
|
/** pi-session: a session id is present so the canonical socket path is computable. */
|
|
51
67
|
socketPathComputable?: boolean;
|
|
68
|
+
/** meta-session: which rail a reply rides — from nativePushSupported(backend), NOT wakeMode. */
|
|
69
|
+
metaDeliveryDomain?: MetaDeliveryDomain;
|
|
52
70
|
/** meta-session: the sender marker's identity is backed by a live meta-record. */
|
|
53
71
|
recordBacked?: boolean;
|
|
54
|
-
/** meta-session: the marker's owner pid is still the same process (start-key match). */
|
|
72
|
+
/** meta-session (self-fetch): the marker's owner pid is still the same process (start-key match). */
|
|
55
73
|
ownerAlive?: boolean;
|
|
56
|
-
/** meta-session: the idle-wake watch is armed (slice-2 presence marker; fail-closed until then). */
|
|
74
|
+
/** meta-session (self-fetch): the idle-wake watch is armed (slice-2 presence marker; fail-closed until then). */
|
|
57
75
|
watchArmed?: boolean;
|
|
76
|
+
/** meta-session (native-push): an adapter probe found this citizen's live native conversation. */
|
|
77
|
+
probeAlive?: boolean;
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
export interface SelfAddressabilityResult {
|
|
@@ -89,18 +109,41 @@ export function computeSelfAddressability(facts: SelfAddressabilityFacts): SelfA
|
|
|
89
109
|
};
|
|
90
110
|
}
|
|
91
111
|
case "meta-session": {
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
// TWO rails, pinned apart (보정①). Each branch composes the predicate that OWNS its
|
|
113
|
+
// axis — the mailbox receiver atom and the native-push predicate share nothing, so a
|
|
114
|
+
// mailbox liveness fact can never leak into a backend that has no mailbox.
|
|
115
|
+
switch (facts.metaDeliveryDomain) {
|
|
116
|
+
case "native-push": {
|
|
117
|
+
const push = nativePushDeliverable({ recordBacked: facts.recordBacked, probeAlive: facts.probeAlive });
|
|
118
|
+
return {
|
|
119
|
+
replyable: push.deliverable,
|
|
120
|
+
socketState: "none",
|
|
121
|
+
reason: push.deliverable
|
|
122
|
+
? `native-push reachable (${push.reason})`
|
|
123
|
+
: `native-push unreachable — ${push.reason}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
case "self-fetch": {
|
|
127
|
+
// Share the active-receiver atom with the deliverability predicate (one
|
|
128
|
+
// source of truth for "record backed AND owner alive AND watch armed").
|
|
129
|
+
const recv = computeMetaReceiverActive({
|
|
130
|
+
recordBacked: facts.recordBacked,
|
|
131
|
+
ownerAlive: facts.ownerAlive,
|
|
132
|
+
watchArmed: facts.watchArmed,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
replyable: recv.active,
|
|
136
|
+
socketState: "none",
|
|
137
|
+
reason: recv.active ? `meta receiver active (${recv.reason})` : `meta receiver inactive — ${recv.reason}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
default:
|
|
141
|
+
return {
|
|
142
|
+
replyable: false,
|
|
143
|
+
socketState: "none",
|
|
144
|
+
reason: "meta delivery domain not supplied — cannot say which rail a reply would ride (fail-closed)",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
104
147
|
}
|
|
105
148
|
case "external-mcp":
|
|
106
149
|
return {
|
|
@@ -67,12 +67,19 @@ export const DISPATCH_LIVENESSES = ["live", "dormant", "indeterminate"] as const
|
|
|
67
67
|
export type DispatchLiveness = (typeof DISPATCH_LIVENESSES)[number];
|
|
68
68
|
|
|
69
69
|
// ── Backend liveness domain (R1 + F4) ──────────────────────────────────────
|
|
70
|
-
// Backends whose liveness predicate is DEFINED
|
|
71
|
-
// connect + RPC `get_info`, entwurf-control.ts).
|
|
72
|
-
// socket)
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
70
|
+
// Backends whose SOCKET liveness predicate is DEFINED — the pi control-socket
|
|
71
|
+
// domain ONLY (connect + RPC `get_info`, entwurf-control.ts). It stays ["pi"].
|
|
72
|
+
// claude-code (self-fetch, no socket) has no liveness predicate at all → `unsupported`.
|
|
73
|
+
// codex/antigravity are direct-inject; antigravity's liveness IS measured, but by the
|
|
74
|
+
// SEPARATE native-push adapter rail (a live app-server conversation probe), NOT this
|
|
75
|
+
// pi-socket domain — so it must NEVER be added here. Adding it would pull agy into the
|
|
76
|
+
// pi socket table (inspectSocket/probeSocket are socket-only); the fact layer keeps
|
|
77
|
+
// reporting agy `unsupported` = "outside the pi-socket liveness domain", NOT
|
|
78
|
+
// unreachable (the native-push rail measures it — entwurf-v2-decider.ts). Widening
|
|
79
|
+
// THIS set is a deliberate future decision (Stage 1+), gated by a REAL pi-shaped
|
|
80
|
+
// control-socket predicate — never by silently mapping sessions to dead/indeterminate
|
|
81
|
+
// (R1 핵심). check-entwurf-facts pins this == ["pi"] and asserts the native-push
|
|
82
|
+
// domain is disjoint from it.
|
|
76
83
|
export const LIVENESS_DOMAIN_BACKENDS = ["pi"] as const;
|
|
77
84
|
export type LivenessDomainBackend = (typeof LIVENESS_DOMAIN_BACKENDS)[number];
|
|
78
85
|
|
|
@@ -80,6 +87,30 @@ export function isLivenessSupported(backend: string): boolean {
|
|
|
80
87
|
return (LIVENESS_DOMAIN_BACKENDS as readonly string[]).includes(backend);
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
// ── Native-push backend domain (봉인 2/4) ───────────────────────────────────
|
|
91
|
+
// A backend whose liveness is measured by the SEPARATE native-push adapter rail (a
|
|
92
|
+
// live app-server conversation probe — antigravity's LS gRPC), NOT the pi control
|
|
93
|
+
// socket. This domain is DISJOINT from LIVENESS_DOMAIN_BACKENDS (pi socket): an agy
|
|
94
|
+
// session is `unsupported` at the pi-socket FACT level (entwurf_peers) yet fully
|
|
95
|
+
// measured + deliverable on the native-push axis. The two are separate rails on
|
|
96
|
+
// purpose — check-entwurf-facts pins both sets and asserts their intersection is ∅
|
|
97
|
+
// (a backend can never be in both a socket-liveness domain and a native-push domain).
|
|
98
|
+
export const NATIVE_PUSH_BACKENDS = ["antigravity"] as const;
|
|
99
|
+
export type NativePushBackend = (typeof NATIVE_PUSH_BACKENDS)[number];
|
|
100
|
+
|
|
101
|
+
export function nativePushSupported(backend: string): backend is NativePushBackend {
|
|
102
|
+
return (NATIVE_PUSH_BACKENDS as readonly string[]).includes(backend);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// NativePushLiveness = the 3-value liveness the native-push adapter probe yields.
|
|
106
|
+
// The SAME three values as SocketLiveness, reused so there is ONE 3-value liveness
|
|
107
|
+
// vocabulary — but these are NOT socket-bound (봉인 2: "주석만 socket-전용 오독 정정"):
|
|
108
|
+
// the value is a live-app-server-conversation probe result (agentapi
|
|
109
|
+
// get-conversation-metadata answered = alive; no live port served the conv = dead;
|
|
110
|
+
// probe error/ambiguity = indeterminate). All three are valid FactLiveness values, so
|
|
111
|
+
// a native-push receipt stamps observedLiveness ∈ {alive, dead, indeterminate}.
|
|
112
|
+
export type NativePushLiveness = SocketLiveness;
|
|
113
|
+
|
|
83
114
|
/**
|
|
84
115
|
* Compose the 4-value FACT liveness from a backend and its socket probe.
|
|
85
116
|
* Out-of-domain backend → `unsupported` (NOT dead/indeterminate, R1). An
|
|
@@ -106,6 +137,9 @@ export const ENTWURF_V2_REJECT_REASONS = [
|
|
|
106
137
|
"owned-live-no-autosend", // Q2/F1: owned-outcome to a live target is not an auto-send
|
|
107
138
|
"backend-liveness-unsupported", // R1: backend has no liveness predicate (e.g. claude-code) — owned-outcome only
|
|
108
139
|
"mailbox-undeliverable", // F-mailbox: fire-and-forget to an unsupported citizen whose mailbox is not deliverable (fail-closed; future pi-backend non-drainable mailbox)
|
|
140
|
+
"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.
|
|
141
|
+
"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.
|
|
142
|
+
"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.
|
|
109
143
|
"bad-target", // R2: absent/typo garden-id (no existing citizen); spawn-new out of v2 scope
|
|
110
144
|
"untrusted-fail-fast", // 동결결정 5: controlled launch into an untrusted cwd
|
|
111
145
|
"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.
|
|
@@ -183,7 +217,18 @@ export const RESOLVER_REJECT_REASONS = [
|
|
|
183
217
|
// `meta-mailbox` (F-mailbox) = liveness-free delivery via the 0.10.0 meta-bridge
|
|
184
218
|
// mailbox + doorbell. The ack is "enqueued + doorbell rung", NOT a read and NOT a
|
|
185
219
|
// turn injection — so `mode` (steer/follow_up) is meaningless on this transport.
|
|
186
|
-
|
|
220
|
+
// `native-push` (봉인 1) = direct injection into a LIVE native app-server conversation
|
|
221
|
+
// (antigravity `agentapi send-message`). Like meta-mailbox it is a fire-and-forget
|
|
222
|
+
// send arm (ack-only), but it requires a live-probe (NATIVE_PUSH_DISPATCH_TABLE),
|
|
223
|
+
// where meta-mailbox is liveness-free. It is NOT a mailbox enqueue and NOT a pi socket
|
|
224
|
+
// send — it is its own rail.
|
|
225
|
+
export const ENTWURF_V2_TRANSPORTS = [
|
|
226
|
+
"control-socket",
|
|
227
|
+
"spawn-bg",
|
|
228
|
+
"tmux-live",
|
|
229
|
+
"meta-mailbox",
|
|
230
|
+
"native-push",
|
|
231
|
+
] as const;
|
|
187
232
|
export type EntwurfV2Transport = (typeof ENTWURF_V2_TRANSPORTS)[number];
|
|
188
233
|
|
|
189
234
|
// Allow-branch facets (exported so the schema↔types gate asserts every enum).
|
|
@@ -196,7 +241,7 @@ export const ENTWURF_V2_OWNERSHIPS = ["ack-only", "owned"] as const;
|
|
|
196
241
|
export const ENTWURF_V2_MODES = ["steer", "follow_up"] as const;
|
|
197
242
|
|
|
198
243
|
export type DispatchVerdict =
|
|
199
|
-
| { action: "send"; transport: "control-socket" | "meta-mailbox"; ownership: "ack-only" }
|
|
244
|
+
| { action: "send"; transport: "control-socket" | "meta-mailbox" | "native-push"; ownership: "ack-only" }
|
|
200
245
|
| { action: "resume"; transport: "spawn-bg" | "tmux-live"; ownership: "owned" }
|
|
201
246
|
| { action: "reject"; reason: EntwurfV2RejectReason };
|
|
202
247
|
|
|
@@ -220,10 +265,13 @@ export const DISPATCH_TABLE: Record<EntwurfIntent, Record<DispatchLiveness, Disp
|
|
|
220
265
|
};
|
|
221
266
|
|
|
222
267
|
// ── The unsupported-backend mailbox mini-table (F-mailbox) ─────────────────
|
|
223
|
-
// SEPARATE from the in-domain 6-cell DISPATCH_TABLE (Fable (i)):
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
268
|
+
// SEPARATE from the in-domain 6-cell DISPATCH_TABLE (Fable (i)): a backend with no
|
|
269
|
+
// pi-socket liveness predicate never enters the liveness-keyed table. Instead the
|
|
270
|
+
// domain guard routes it here, keyed on intent alone. Reaches here: claude-code
|
|
271
|
+
// (self-fetch mailbox) and codex (no adapter yet). Does NOT reach here: antigravity —
|
|
272
|
+
// the decider intercepts a native-push backend in its own rail BEFORE this mailbox
|
|
273
|
+
// mini-table (entwurf-v2-decider.ts), so agy is `unsupported` at the fact level yet
|
|
274
|
+
// never falls through to a mailbox it does not have. The cells, keyed on intent alone:
|
|
227
275
|
// - fire-and-forget needs no liveness — the 0.10.0 meta-bridge mailbox delivers
|
|
228
276
|
// to any DELIVERABLE citizen. This cell is the deliverable path; resolveDispatch
|
|
229
277
|
// downgrades it to `mailbox-undeliverable` when the separate mailboxDeliverable
|
|
@@ -351,6 +399,66 @@ export function resolveDispatch(
|
|
|
351
399
|
};
|
|
352
400
|
}
|
|
353
401
|
|
|
402
|
+
// ── The native-push dispatch table (봉인 1/2/4) ─────────────────────────────
|
|
403
|
+
// A THIRD table, distinct from both the pi 6-cell DISPATCH_TABLE and the unsupported
|
|
404
|
+
// mailbox mini-table. Keyed intent × NativePushLiveness (NOT intent-only): a
|
|
405
|
+
// native-push backend (antigravity) IS measured by its adapter probe, so the
|
|
406
|
+
// send/reject decision depends on the probed liveness. The decider intercepts a
|
|
407
|
+
// native-push backend in its own rail (nativePushSupported → probe → this table)
|
|
408
|
+
// BEFORE the unsupported branch, so agy never falls through to a mailbox it lacks.
|
|
409
|
+
//
|
|
410
|
+
// fire-and-forget × alive → native-push send (the ONE allow cell)
|
|
411
|
+
// fire-and-forget × dead → reject native-push-target-dead
|
|
412
|
+
// fire-and-forget × indeterminate → reject native-push-probe-indeterminate
|
|
413
|
+
// owned-outcome × * → reject native-push-no-resume-authority (state-
|
|
414
|
+
// independent: no pi-child to own; `backend-
|
|
415
|
+
// liveness-unsupported` is NOT reused — false name).
|
|
416
|
+
export const NATIVE_PUSH_DISPATCH_TABLE: Record<EntwurfIntent, Record<NativePushLiveness, DispatchVerdict>> = {
|
|
417
|
+
"fire-and-forget": {
|
|
418
|
+
alive: { action: "send", transport: "native-push", ownership: "ack-only" },
|
|
419
|
+
dead: { action: "reject", reason: "native-push-target-dead" },
|
|
420
|
+
indeterminate: { action: "reject", reason: "native-push-probe-indeterminate" },
|
|
421
|
+
},
|
|
422
|
+
"owned-outcome": {
|
|
423
|
+
alive: { action: "reject", reason: "native-push-no-resume-authority" },
|
|
424
|
+
dead: { action: "reject", reason: "native-push-no-resume-authority" },
|
|
425
|
+
indeterminate: { action: "reject", reason: "native-push-no-resume-authority" },
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// The reasons the native-push resolver emits — a THIRD post-probe reject set, parallel
|
|
430
|
+
// to RESOLVER_REJECT_REASONS (pi/mailbox). All post-probe: resolveNativePushDispatch
|
|
431
|
+
// always has a real probed liveness in hand, so observedLiveness is non-null. None may
|
|
432
|
+
// be pre-probe (they are never in PRE_PROBE_REJECT_REASONS).
|
|
433
|
+
export const NATIVE_PUSH_REJECT_REASONS = [
|
|
434
|
+
"native-push-target-dead",
|
|
435
|
+
"native-push-probe-indeterminate",
|
|
436
|
+
"native-push-no-resume-authority",
|
|
437
|
+
] as const satisfies readonly EntwurfV2RejectReason[];
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* PURE native-push dispatch decision (봉인 4). Given the caller intent and the adapter
|
|
441
|
+
* probe's 3-value liveness, mint the receipt from NATIVE_PUSH_DISPATCH_TABLE. Mirrors
|
|
442
|
+
* resolveDispatch's shape; observedLiveness is ALWAYS the probed value (non-null,
|
|
443
|
+
* post-probe). The decider calls this only AFTER nativePushSupported(backend) gates the
|
|
444
|
+
* backend and the adapter probe returns a liveness — it never touches the pi socket
|
|
445
|
+
* table or the mailbox mini-table (those are other domains). No IO here (the probe is
|
|
446
|
+
* the decider's injected dep); this only maps (intent, liveness) → verdict.
|
|
447
|
+
*/
|
|
448
|
+
export function resolveNativePushDispatch(intent: EntwurfIntent, liveness: NativePushLiveness): EntwurfV2Receipt {
|
|
449
|
+
const cell = NATIVE_PUSH_DISPATCH_TABLE[intent][liveness];
|
|
450
|
+
if (cell.action === "reject") {
|
|
451
|
+
return makeRejectReceipt(cell.reason, liveness);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
ok: true,
|
|
455
|
+
action: cell.action,
|
|
456
|
+
transport: cell.transport,
|
|
457
|
+
ownership: cell.ownership,
|
|
458
|
+
observedLiveness: liveness,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
354
462
|
// ── TypeBox schemas ────────────────────────────────────────────────────────
|
|
355
463
|
// MOVED to `entwurf-v2-contract-schema.ts` (0.12.1 B-1): the pi-ai TypeBox
|
|
356
464
|
// builders (StringEnum/Type) are a pi-lane dependency, so they cannot live in
|
|
@@ -52,7 +52,10 @@ import {
|
|
|
52
52
|
type EntwurfV2Receipt,
|
|
53
53
|
isLivenessSupported,
|
|
54
54
|
makeRejectReceipt,
|
|
55
|
+
type NativePushBackend,
|
|
56
|
+
nativePushSupported,
|
|
55
57
|
resolveDispatch,
|
|
58
|
+
resolveNativePushDispatch,
|
|
56
59
|
} from "./entwurf-v2-contract.ts";
|
|
57
60
|
import type { AcquireLockResult, LockClaim, LockConflict } from "./entwurf-v2-lock.ts";
|
|
58
61
|
import {
|
|
@@ -63,6 +66,7 @@ import {
|
|
|
63
66
|
type MetaIdentity,
|
|
64
67
|
metaCapabilityFor,
|
|
65
68
|
} from "./meta-session.ts";
|
|
69
|
+
import type { NativePushProbeResult, NativePushRoute } from "./native-push/adapter.ts";
|
|
66
70
|
import { isValidSessionId } from "./session-id.js";
|
|
67
71
|
import { controlSocketPath, mapInspectionToLiveness, type TargetSocketInspection } from "./socket-discovery.ts";
|
|
68
72
|
import type { SocketLiveness } from "./socket-probe.ts";
|
|
@@ -124,6 +128,21 @@ export type ExecutionPlan =
|
|
|
124
128
|
expectedSocketPath: string;
|
|
125
129
|
observeTimeoutMs: number;
|
|
126
130
|
releaseWhen: "socket-alive-or-child-exited";
|
|
131
|
+
}
|
|
132
|
+
// native-push send (봉인 4): direct-inject into a live app-server conversation. LOCK-FREE
|
|
133
|
+
// (the DispatchDecision carries lock:null). Carries the decider-probed VOLATILE route so
|
|
134
|
+
// the executor sends without re-deriving it (봉인 3 "used within the same dispatch"); the
|
|
135
|
+
// executor still owns the 1-shot re-probe→re-send on failure. `backend` lets the executor
|
|
136
|
+
// resolve the adapter for that re-probe.
|
|
137
|
+
| {
|
|
138
|
+
transport: "native-push";
|
|
139
|
+
action: "send";
|
|
140
|
+
targetGardenId: string;
|
|
141
|
+
backend: NativePushBackend;
|
|
142
|
+
nativeSessionId: string;
|
|
143
|
+
route: NativePushRoute;
|
|
144
|
+
wantsReply: boolean;
|
|
145
|
+
message: string;
|
|
127
146
|
};
|
|
128
147
|
|
|
129
148
|
// ── DispatchDecision (the decider's only output) ────────────────────────────
|
|
@@ -212,6 +231,16 @@ export interface DispatchDeciderDeps {
|
|
|
212
231
|
mailboxDeliverabilityFor: (
|
|
213
232
|
identity: MetaIdentity,
|
|
214
233
|
) => MailboxDeliverabilityResult | Promise<MailboxDeliverabilityResult>;
|
|
234
|
+
/**
|
|
235
|
+
* 봉인 4: the native-push liveness+route probe seam (REQUIRED, no default). Called ONLY on
|
|
236
|
+
* the native-push branch (a nativePushSupported backend, e.g. antigravity), it returns the
|
|
237
|
+
* adapter probe result — the 3-value liveness the NATIVE_PUSH table routes on PLUS the
|
|
238
|
+
* volatile route the executor sends over. The decider does NOT probe itself (purity); the
|
|
239
|
+
* production wrapper resolves the native-push adapter and calls its probe. Making it
|
|
240
|
+
* required forces every construction site to wire it, so a native-push dispatch can never
|
|
241
|
+
* silently fall through to the pi-socket / mailbox path.
|
|
242
|
+
*/
|
|
243
|
+
nativePushProbe: (identity: MetaIdentity) => NativePushProbeResult | Promise<NativePushProbeResult>;
|
|
215
244
|
mailboxDir?: string;
|
|
216
245
|
sessionsDir?: string;
|
|
217
246
|
observeTimeoutMs?: number;
|
|
@@ -298,6 +327,37 @@ export async function decideDispatch(input: DispatchInput, deps: DispatchDecider
|
|
|
298
327
|
}
|
|
299
328
|
const identity = resolution.identity;
|
|
300
329
|
|
|
330
|
+
// 2d. native-push rail (봉인 4): a native-push backend (antigravity) is measured by its
|
|
331
|
+
// adapter probe, NOT the pi socket and NOT the mailbox. Intercept it HERE — after identity
|
|
332
|
+
// resolution + the address-conflict guard, but BEFORE the unsupported mailbox branch — so
|
|
333
|
+
// agy routes to native-push and never falls through to a mailbox it does not have. This
|
|
334
|
+
// branch is LOCK-FREE (봉인 4): the pi in-domain lock closes a socket TOCTOU, but a
|
|
335
|
+
// volatile probe route has no lock meaning (a duplicate-send idempotency is a D8 future).
|
|
336
|
+
if (nativePushSupported(identity.backend)) {
|
|
337
|
+
const probe = await deps.nativePushProbe(identity);
|
|
338
|
+
const receipt = resolveNativePushDispatch(input.intent, probe.status);
|
|
339
|
+
if (!receipt.ok) return reject(receipt);
|
|
340
|
+
// The ONLY allow cell is fire-and-forget × alive, so an ok receipt ⟹ the probe is
|
|
341
|
+
// alive and carries a route. The narrow is defensive: a contract-breaking probe/table
|
|
342
|
+
// mismatch fails loud rather than planting a routeless send plan.
|
|
343
|
+
if (probe.status !== "alive") {
|
|
344
|
+
throw new Error(
|
|
345
|
+
"entwurf_v2 decider: native-push send verdict without an alive probe route (contract invariant broken).",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const plan: ExecutionPlan = {
|
|
349
|
+
transport: "native-push",
|
|
350
|
+
action: "send",
|
|
351
|
+
targetGardenId: gardenId,
|
|
352
|
+
backend: identity.backend,
|
|
353
|
+
nativeSessionId: identity.nativeSessionId,
|
|
354
|
+
route: probe.route,
|
|
355
|
+
wantsReply,
|
|
356
|
+
message: input.message,
|
|
357
|
+
};
|
|
358
|
+
return { kind: "execute", receipt, plan, lock: null };
|
|
359
|
+
}
|
|
360
|
+
|
|
301
361
|
// 3. backend.
|
|
302
362
|
if (!isLivenessSupported(identity.backend)) {
|
|
303
363
|
// 6. unsupported path — NO lock (?7). Deliverability comes from the REQUIRED seam
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
|
|
19
|
+
import type { ExecutionPlan } from "./entwurf-v2-decider.ts";
|
|
20
|
+
import type { LockClaim } from "./entwurf-v2-lock.ts";
|
|
21
|
+
import { type NativePushAdapter, type NativePushRoute, resolveNativePushAdapter } from "./native-push/adapter.ts";
|
|
22
|
+
|
|
23
|
+
/** The native-push slice of the ExecutionPlan union (the decider plants it, this consumes it). */
|
|
24
|
+
export type NativePushPlan = Extract<ExecutionPlan, { transport: "native-push" }>;
|
|
25
|
+
|
|
26
|
+
export interface NativePushSendResult {
|
|
27
|
+
success: true;
|
|
28
|
+
/** Whether the 1-shot re-probe→re-send fired (the first send over the planted route failed). */
|
|
29
|
+
retried: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Deliver `content` into the conversation, owning the 1-shot retry (봉인 3). Send over the
|
|
34
|
+
* planted (decider-probed) route first; on failure re-probe ONCE and re-send over the fresh
|
|
35
|
+
* route; a second failure PROPAGATES (fail-loud). If the re-probe finds the target no longer
|
|
36
|
+
* alive, throw — a failed send into a now-dead conversation is an honest non-delivery, never
|
|
37
|
+
* a silent success. The re-probe is the ONLY re-derivation of the volatile route.
|
|
38
|
+
*/
|
|
39
|
+
export async function deliverViaNativePush(
|
|
40
|
+
adapter: NativePushAdapter,
|
|
41
|
+
route: NativePushRoute,
|
|
42
|
+
nativeSessionId: string,
|
|
43
|
+
content: string,
|
|
44
|
+
): Promise<NativePushSendResult> {
|
|
45
|
+
try {
|
|
46
|
+
await adapter.send(route, nativeSessionId, content);
|
|
47
|
+
return { success: true, retried: false };
|
|
48
|
+
} catch (firstErr) {
|
|
49
|
+
// 1-shot re-probe → re-send: the volatile LS route may have shifted since the decider
|
|
50
|
+
// probed it. Re-discover it fresh and retry exactly once.
|
|
51
|
+
const reprobe = await adapter.probe(nativeSessionId);
|
|
52
|
+
if (reprobe.status !== "alive") {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`native-push deliver: first send failed and re-probe found target ${reprobe.status} (${reprobe.reason}) — not retried`,
|
|
55
|
+
{ cause: firstErr },
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
// A second failure THROWS out of this call (fail-loud) — no third attempt.
|
|
59
|
+
await adapter.send(reprobe.route, nativeSessionId, content);
|
|
60
|
+
return { success: true, retried: true };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Deps for the production native-push send hand — the adapter resolver (default: the real
|
|
65
|
+
* registry). Injected so the 5d gate proves the wiring with a fake adapter. */
|
|
66
|
+
export interface NativePushSendDeps {
|
|
67
|
+
resolveAdapter?: (backend: string) => NativePushAdapter;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build the production `sendNativePush(plan, lock)` adapter the runner consumes. It IGNORES
|
|
72
|
+
* `lock` entirely (a native-push plan is lock-free, 봉인 4) — the field exists only to match
|
|
73
|
+
* the DispatchExecutorDeps hand signature. It resolves the adapter from the plan's backend
|
|
74
|
+
* and delivers with the 1-shot retry. A delivery throw surfaces as a REJECTED promise (the
|
|
75
|
+
* runner's try/catch maps it to execution-failed).
|
|
76
|
+
*/
|
|
77
|
+
export function makeNativePushSend(
|
|
78
|
+
deps: NativePushSendDeps = {},
|
|
79
|
+
): (plan: NativePushPlan, lock: LockClaim | null) => Promise<NativePushSendResult> {
|
|
80
|
+
const resolveAdapter = deps.resolveAdapter ?? resolveNativePushAdapter;
|
|
81
|
+
// `_lock` is named for the hand contract but NEVER read — native-push owns/releases no lock.
|
|
82
|
+
return async (plan: NativePushPlan, _lock: LockClaim | null): Promise<NativePushSendResult> => {
|
|
83
|
+
const adapter = resolveAdapter(plan.backend);
|
|
84
|
+
return deliverViaNativePush(adapter, plan.route, plan.nativeSessionId, plan.message);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
releaseLock as realReleaseLock,
|
|
61
61
|
} from "./entwurf-v2-lock.ts";
|
|
62
62
|
import { makeProductionSendViaMailbox } from "./entwurf-v2-mailbox.ts";
|
|
63
|
+
import { makeNativePushSend } from "./entwurf-v2-native-push.ts";
|
|
63
64
|
import type { DispatchExecutorDeps, EntwurfV2RunDeps } from "./entwurf-v2-runner.ts";
|
|
64
65
|
import {
|
|
65
66
|
type ControlSocketPlan,
|
|
@@ -85,6 +86,11 @@ import {
|
|
|
85
86
|
readMetaIdentityByGardenId,
|
|
86
87
|
readMetaReceiverMarker,
|
|
87
88
|
} from "./meta-session.ts";
|
|
89
|
+
import {
|
|
90
|
+
type NativePushAdapter,
|
|
91
|
+
type NativePushProbeResult,
|
|
92
|
+
resolveNativePushAdapter as realResolveNativePushAdapter,
|
|
93
|
+
} from "./native-push/adapter.ts";
|
|
88
94
|
import {
|
|
89
95
|
CONTROL_SOCKET_DIR,
|
|
90
96
|
controlSocketPath,
|
|
@@ -121,6 +127,11 @@ export interface ProductionEntwurfV2Seams {
|
|
|
121
127
|
classifyConnect: (code: string | undefined) => "dead" | "indeterminate";
|
|
122
128
|
sendRpc: (socketPath: string, command: RpcCommand, options?: RpcClientOptions) => Promise<{ response: RpcResponse }>;
|
|
123
129
|
enqueue: (opts: EnqueueMetaMessageOptions) => EnqueueMetaMessageResult;
|
|
130
|
+
/** Resolve the native-push adapter for a backend id (봉인 4). The SAME resolver feeds the
|
|
131
|
+
* decider's `nativePushProbe` AND the executor's `sendNativePush`, so a single injected
|
|
132
|
+
* fake adapter drives both the probe (routing decision) and the send (delivery + retry).
|
|
133
|
+
* Default: the real registry resolver. */
|
|
134
|
+
resolveNativePushAdapter: (backend: string) => NativePushAdapter;
|
|
124
135
|
/** Extra spawn-factory overrides (timers/spawnChild/probe) for a deterministic spawn gate.
|
|
125
136
|
* `releaseFn` is NOT overridable here — the factory injects the shared `release` (QB3). */
|
|
126
137
|
spawnOverrides: Omit<ProductionSpawnOpts, "releaseFn">;
|
|
@@ -211,6 +222,7 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
|
|
|
211
222
|
classifyConnect: s.classifyConnect ?? classifyConnectError,
|
|
212
223
|
sendRpc: s.sendRpc ?? realSendRpc,
|
|
213
224
|
enqueue: s.enqueue ?? enqueueMetaMessage,
|
|
225
|
+
resolveNativePushAdapter: s.resolveNativePushAdapter ?? realResolveNativePushAdapter,
|
|
214
226
|
spawnOverrides: s.spawnOverrides ?? {},
|
|
215
227
|
};
|
|
216
228
|
|
|
@@ -290,6 +302,11 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
|
|
|
290
302
|
preflightForCwd: (cwd: string): PreflightOutcome | Promise<PreflightOutcome> =>
|
|
291
303
|
io.preflight({ cwd, agentDir: opts.agentDir, prefixRoots: opts.prefixRoots }),
|
|
292
304
|
mailboxDeliverabilityFor,
|
|
305
|
+
// 봉인 4: resolve the native-push adapter for this backend + probe the conversation.
|
|
306
|
+
// Only reached on a nativePushSupported backend (the decider gates it), so the resolver
|
|
307
|
+
// never throws for a non-native-push backend here.
|
|
308
|
+
nativePushProbe: (identity: MetaIdentity): Promise<NativePushProbeResult> =>
|
|
309
|
+
Promise.resolve(io.resolveNativePushAdapter(identity.backend).probe(identity.nativeSessionId)),
|
|
293
310
|
mailboxDir,
|
|
294
311
|
sessionsDir,
|
|
295
312
|
observeTimeoutMs: opts.observeTimeoutMs,
|
|
@@ -339,6 +356,9 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
|
|
|
339
356
|
}),
|
|
340
357
|
),
|
|
341
358
|
sendMailbox: (plan, _lock) => sendViaMailbox(plan as MetaMailboxPlan, _lock as LockClaim),
|
|
359
|
+
// native-push (봉인 4): the SAME injected adapter resolver drives the executor send,
|
|
360
|
+
// so the decider's probe and the delivery use one adapter. Lock-free (lock ignored).
|
|
361
|
+
sendNativePush: makeNativePushSend({ resolveAdapter: io.resolveNativePushAdapter }),
|
|
342
362
|
};
|
|
343
363
|
|
|
344
364
|
return {
|
|
@@ -96,6 +96,15 @@ export function decideReleasePolicy(plan: ExecutionPlan, lock: LockClaim | null)
|
|
|
96
96
|
throw new Error("entwurf-v2-release: a meta-mailbox plan must carry no lock (?7 invariant violated).");
|
|
97
97
|
}
|
|
98
98
|
return { kind: "no-lock" };
|
|
99
|
+
case "native-push":
|
|
100
|
+
// Native-push is the other lock-free rail (봉인 4) — like meta-mailbox it holds no
|
|
101
|
+
// lock, so a non-null lock is a decider contract violation. (The native-push hand
|
|
102
|
+
// never actually routes through this reducer; the case keeps the switch exhaustive
|
|
103
|
+
// and pins the lock-free invariant.)
|
|
104
|
+
if (lock !== null) {
|
|
105
|
+
throw new Error("entwurf-v2-release: a native-push plan must carry no lock (봉인 4 lock-free rail violated).");
|
|
106
|
+
}
|
|
107
|
+
return { kind: "no-lock" };
|
|
99
108
|
case "control-socket":
|
|
100
109
|
assertInDomainLock(plan, lock);
|
|
101
110
|
return { kind: "release-after-send-final" };
|
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
SuccessReceipt,
|
|
39
39
|
} from "./entwurf-v2-decider.ts";
|
|
40
40
|
import type { LockClaim } from "./entwurf-v2-lock.ts";
|
|
41
|
+
import type { NativePushPlan, NativePushSendResult } from "./entwurf-v2-native-push.ts";
|
|
41
42
|
import {
|
|
42
43
|
type ControlSocketPlan,
|
|
43
44
|
type ControlSocketSendResult,
|
|
@@ -58,6 +59,9 @@ export interface DispatchExecutorDeps {
|
|
|
58
59
|
sendControl: (plan: ControlSocketPlan, lock: LockClaim | null) => Promise<ControlSocketSendResult>;
|
|
59
60
|
resumeSpawnBg: (plan: SpawnBgPlan, lock: LockClaim | null) => Promise<SpawnBgResumeResult>;
|
|
60
61
|
sendMailbox: (plan: MetaMailboxPlan, lock: LockClaim | null) => Promise<RpcSendResult>;
|
|
62
|
+
// native-push (봉인 4): lock-free like meta-mailbox — the runner passes the null lock
|
|
63
|
+
// verbatim and the hand ignores it. Owns the 1-shot re-probe→re-send retry internally.
|
|
64
|
+
sendNativePush: (plan: NativePushPlan, lock: LockClaim | null) => Promise<NativePushSendResult>;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
/** The per-transport success outcome, discriminated by transport so the surface renders
|
|
@@ -68,7 +72,9 @@ export interface DispatchExecutorDeps {
|
|
|
68
72
|
export type ExecutedOutcome =
|
|
69
73
|
| { transport: "control-socket"; outcome: SendFinalOutcome; rejectReason?: string }
|
|
70
74
|
| { transport: "spawn-bg"; result: SpawnBgResumeResult }
|
|
71
|
-
| { transport: "meta-mailbox"; success: true }
|
|
75
|
+
| { transport: "meta-mailbox"; success: true }
|
|
76
|
+
// native-push carries `retried` so the surface can note the 1-shot re-probe retry fired.
|
|
77
|
+
| { transport: "native-push"; success: true; retried: boolean };
|
|
72
78
|
|
|
73
79
|
/** The single outcome-rich result the 5d surface renders. `rejected` = the decider
|
|
74
80
|
* refused (no execution). `executed` = a hand ran to a terminal result. `execution-failed`
|
|
@@ -164,6 +170,28 @@ export async function executeDispatch(
|
|
|
164
170
|
return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
|
|
165
171
|
}
|
|
166
172
|
}
|
|
173
|
+
case "native-push": {
|
|
174
|
+
try {
|
|
175
|
+
// lock is null here (lock-free rail, 봉인 4) — passed verbatim; the hand ignores it.
|
|
176
|
+
const r = await deps.sendNativePush(plan, lock);
|
|
177
|
+
// Like meta-mailbox, native-push has NO in-band reject (no live receiver answers
|
|
178
|
+
// success:false). A `success:false` is therefore a CONTRACT VIOLATION — fail loud
|
|
179
|
+
// rather than render a non-delivery as delivered ("Never warn. Throw.").
|
|
180
|
+
if (r.success !== true) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"entwurf-v2-runner: native-push send returned success:false (contract violation; native-push has no in-band reject).",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
kind: "executed",
|
|
187
|
+
receipt,
|
|
188
|
+
transport,
|
|
189
|
+
outcome: { transport: "native-push", success: true, retried: r.retried },
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
167
195
|
}
|
|
168
196
|
}
|
|
169
197
|
|
|
@@ -217,6 +217,13 @@ async function driveDeadFallback(
|
|
|
217
217
|
// contract violation (a send and a spawn are different actions), so fail loud
|
|
218
218
|
// rather than silently mis-execute.
|
|
219
219
|
throw new Error("entwurf-v2-send: re-resolve returned a spawn-bg plan for a send fallback (contract violation).");
|
|
220
|
+
case "native-push":
|
|
221
|
+
// The dead-control-socket fallback re-resolves only the pi socket domain
|
|
222
|
+
// (control-socket / mailbox / spawn); it never routes the native-push rail. A
|
|
223
|
+
// native-push rePlan here is structurally impossible — fail loud.
|
|
224
|
+
throw new Error(
|
|
225
|
+
"entwurf-v2-send: re-resolve returned a native-push plan for a send fallback (contract violation).",
|
|
226
|
+
);
|
|
220
227
|
}
|
|
221
228
|
}
|
|
222
229
|
|
|
@@ -106,6 +106,16 @@ export function actionableRejectHint(reason: string): string | undefined {
|
|
|
106
106
|
case "owned-live-no-autosend":
|
|
107
107
|
// A live target is reachable, but owned-outcome is not an auto-send (Q2/F1).
|
|
108
108
|
return "target is live — owned-outcome never auto-sends. Use intent: fire-and-forget (with wants_reply if you need a reply).";
|
|
109
|
+
case "native-push-no-resume-authority":
|
|
110
|
+
// A native-push backend (antigravity) has no pi-child to own, so owned-outcome has
|
|
111
|
+
// nothing to own. Delivery is a direct inject via fire-and-forget.
|
|
112
|
+
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.";
|
|
113
|
+
case "native-push-target-dead":
|
|
114
|
+
// The adapter probe found no live host process for the conversation.
|
|
115
|
+
return "native-push conversation is not live (no host process found). Re-open the conversation, then retry — there is nothing to inject into.";
|
|
116
|
+
case "native-push-probe-indeterminate":
|
|
117
|
+
// Host up, but no LS port served the conversation — inconclusive, not a hard dead.
|
|
118
|
+
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.";
|
|
109
119
|
default:
|
|
110
120
|
return undefined;
|
|
111
121
|
}
|
|
@@ -163,6 +173,13 @@ export function renderEntwurfV2Result(result: EntwurfV2RunResult): EntwurfV2Surf
|
|
|
163
173
|
const exit = res.kind === "child-exited" ? ` exitCode=${res.exitCode}` : "";
|
|
164
174
|
return { text: `entwurf_v2 spawn-bg → ${res.kind}${pid}${exit}, lock released`, isError: false };
|
|
165
175
|
}
|
|
176
|
+
if (o.transport === "native-push") {
|
|
177
|
+
// direct-inject succeeded; note if the 1-shot re-probe retry fired.
|
|
178
|
+
return {
|
|
179
|
+
text: `entwurf_v2 native-push → delivered${o.retried ? " (after a 1-shot re-probe retry)" : ""}`,
|
|
180
|
+
isError: false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
166
183
|
// meta-mailbox
|
|
167
184
|
return { text: "entwurf_v2 meta-mailbox → enqueued", isError: false };
|
|
168
185
|
}
|