@junghanacs/entwurf 0.20.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +85 -190
- package/BASELINE.md +6 -3
- package/CHANGELOG.md +473 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +332 -60
- package/README.md +100 -22
- package/VERIFY.md +93 -7
- package/docs/acp-backend-rail.md +0 -1
- package/docs/external-mcp-host.md +64 -33
- package/docs/setup-clean-host.md +151 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +154 -116
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
- package/package.json +13 -4
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +233 -29
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +342 -28
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +245 -12
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +8 -3
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-mux-launch-tmux.ts +345 -4
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +653 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +93 -10
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +75 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +700 -0
- package/scripts/smoke-codex-fresh-live.ts +1426 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +125 -10
- package/scripts/tsconfig.json +1 -0
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
isOutOfSocketDomainGardenIdConflict,
|
|
33
33
|
type PeerObserver,
|
|
34
34
|
resolveFactList,
|
|
35
|
+
UNOBSERVED_PEER,
|
|
35
36
|
} from "./entwurf-facts.ts";
|
|
36
37
|
import { observePeerFacts } from "./entwurf-peer-observe.ts";
|
|
37
38
|
import { isLivenessSupported } from "./entwurf-v2-contract.ts";
|
|
@@ -102,9 +103,12 @@ export interface EntwurfFactsDeps {
|
|
|
102
103
|
/** Observation axis (#101): per-citizen receiver + transcript facts. Defaults to the
|
|
103
104
|
* REAL measurement — the same seam shape `makeProductionEntwurfV2Deps` uses, so the two
|
|
104
105
|
* wiring sites (MCP + pi-native) cannot drift by each passing their own observer, and a
|
|
105
|
-
* gate still drives the assembly with a fake and no filesystem.
|
|
106
|
-
* nothing gets facts; a caller that injects `() => UNOBSERVED_PEER` says so on every row. */
|
|
106
|
+
* gate still drives the assembly with a fake and no filesystem. */
|
|
107
107
|
observe?: PeerObserver;
|
|
108
|
+
/** Optional presentation budget. Undefined preserves the generic fact provider's full
|
|
109
|
+
* observation contract; peers surfaces pass their shared render limit. Older machine
|
|
110
|
+
* payload rows then say `unobserved` rather than fabricating `active` / `exists`. */
|
|
111
|
+
observationLimit?: number;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
function diagnosticSortKey(d: EntwurfDiagnostic): string {
|
|
@@ -195,9 +199,31 @@ export async function listEntwurfFacts(deps: EntwurfFactsDeps): Promise<EntwurfF
|
|
|
195
199
|
// 4. resolveFactList over CLEAN inputs only. Its throws (duplicate identity /
|
|
196
200
|
// unprobed in-domain citizen) are impossible wiring invariants — left to
|
|
197
201
|
// fire as the last line of defense, never caught here.
|
|
202
|
+
//
|
|
203
|
+
// A peers surface may bound observation to its SAME newest rendered rows. Receiver
|
|
204
|
+
// observation can inspect process state and historical Claude markers, so doing it
|
|
205
|
+
// for every retained record made bounded text grow linearly with history. The generic
|
|
206
|
+
// provider stays unbounded when no limit is supplied. Either way the full clean
|
|
207
|
+
// identity/probe arrays reach the core: certification, duplicate detection,
|
|
208
|
+
// diagnostics, liveness, and the machine payload remain complete. Only an observation
|
|
209
|
+
// not performed becomes `unobserved`.
|
|
198
210
|
const cleanIdentities = identities.filter((i) => !conflictGids.has(i.gardenId));
|
|
199
211
|
const cleanProbes = probes.filter((p) => !conflictGids.has(p.gardenId));
|
|
200
|
-
const
|
|
212
|
+
const observe = deps.observe ?? observePeerFacts;
|
|
213
|
+
let selectedObserve = observe;
|
|
214
|
+
if (deps.observationLimit !== undefined) {
|
|
215
|
+
if (!Number.isSafeInteger(deps.observationLimit) || deps.observationLimit < 0) {
|
|
216
|
+
throw new Error("listEntwurfFacts: observationLimit must be a non-negative safe integer");
|
|
217
|
+
}
|
|
218
|
+
const observedGardenIds = new Set(
|
|
219
|
+
[...cleanIdentities]
|
|
220
|
+
.sort((a, b) => (a.gardenId < b.gardenId ? -1 : a.gardenId > b.gardenId ? 1 : 0))
|
|
221
|
+
.slice(Math.max(0, cleanIdentities.length - deps.observationLimit))
|
|
222
|
+
.map((identity) => identity.gardenId),
|
|
223
|
+
);
|
|
224
|
+
selectedObserve = (identity) => (observedGardenIds.has(identity.gardenId) ? observe(identity) : UNOBSERVED_PEER);
|
|
225
|
+
}
|
|
226
|
+
const facts: FactList = resolveFactList(cleanIdentities, cleanProbes, selectedObserve);
|
|
201
227
|
|
|
202
228
|
// 5. #50 C4 demotion: a record-less socket is a diagnostic, not a listing
|
|
203
229
|
// section. One diagnostic per socket (subjects aggregate at render, F8);
|
|
@@ -39,6 +39,9 @@ export interface EntwurfPeersRender {
|
|
|
39
39
|
payload: EntwurfPeersPayload;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Human rows and expensive provider observations share this one presentation budget. */
|
|
43
|
+
export const ENTWURF_PEERS_RENDER_LIMIT = 32;
|
|
44
|
+
|
|
42
45
|
// `receiver` and `transcript` are OBSERVED facts, not routing (#101): they say what was
|
|
43
46
|
// found on disk for this citizen, and a caller still asks entwurf_v2 what may be done
|
|
44
47
|
// about it. They earn a column because for every claude-code row `liveness` reads
|
|
@@ -124,7 +127,7 @@ function renderDiagnosticLines(diagnostics: EntwurfDiagnostic[]): string[] {
|
|
|
124
127
|
return lines;
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
function compactLines(lines: string[], max: number =
|
|
130
|
+
function compactLines(lines: string[], max: number = ENTWURF_PEERS_RENDER_LIMIT): string[] {
|
|
128
131
|
if (lines.length <= max) return lines;
|
|
129
132
|
const omitted = lines.length - max;
|
|
130
133
|
return [` … (${omitted} older entries omitted; showing latest ${max})`, ...lines.slice(-max)];
|
|
@@ -122,13 +122,12 @@ export function isLivenessSupported(backend: string): boolean {
|
|
|
122
122
|
|
|
123
123
|
// ── Native-push backend domain (봉인 2/4) ───────────────────────────────────
|
|
124
124
|
// A backend whose liveness is measured by the SEPARATE native-push adapter rail (a
|
|
125
|
-
// live app-server
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
export const NATIVE_PUSH_BACKENDS = ["antigravity"] as const;
|
|
125
|
+
// live vendor app-server target probe), NOT a control socket. This domain is DISJOINT
|
|
126
|
+
// from LIVENESS_DOMAIN_BACKENDS: an Antigravity or Codex session is `unsupported` on
|
|
127
|
+
// the socket FACT axis (entwurf_peers) yet independently measured on the native-push
|
|
128
|
+
// axis. The two are separate rails on purpose — check-entwurf-facts pins both sets
|
|
129
|
+
// and asserts their intersection is ∅ (a backend can never be in both domains).
|
|
130
|
+
export const NATIVE_PUSH_BACKENDS = ["antigravity", "codex"] as const;
|
|
132
131
|
export type NativePushBackend = (typeof NATIVE_PUSH_BACKENDS)[number];
|
|
133
132
|
|
|
134
133
|
export function nativePushSupported(backend: string): backend is NativePushBackend {
|
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
* ROUTING (the NATIVE_PUSH_DISPATCH_TABLE did) and carries NO release seam — a native-push
|
|
6
6
|
* plan is the LOCK-FREE path (봉인 4), so a lock leak is structurally impossible here.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* control-socket send-fallback pattern: the decider stays pure, the hand owns the retry.
|
|
8
|
+
* Retry is an adapter capability, not a rail-wide assumption. Antigravity re-probes once
|
|
9
|
+
* because its volatile LS port can shift before send. Codex never retries: `codex queue`
|
|
10
|
+
* may have accepted the message before its receipt failed, so replay could duplicate it.
|
|
11
|
+
* The decider stays pure; this hand enforces the adapter's declared policy.
|
|
13
12
|
*
|
|
14
13
|
* A native-push send has NO in-band refuse (there is no live receiver to answer
|
|
15
14
|
* success:false) — like a mailbox enqueue, it either succeeds or THROWS. The hand never
|
|
@@ -18,6 +17,7 @@
|
|
|
18
17
|
|
|
19
18
|
import type { ExecutionPlan } from "./entwurf-v2-decider.ts";
|
|
20
19
|
import type { LockClaim } from "./entwurf-v2-lock.ts";
|
|
20
|
+
import { formatMetaMailboxBody, type MailboxSenderEnvelope } from "./meta-mailbox-body.ts";
|
|
21
21
|
import { type NativePushAdapter, type NativePushRoute, resolveNativePushAdapter } from "./native-push/adapter.ts";
|
|
22
22
|
|
|
23
23
|
/** The native-push slice of the ExecutionPlan union (the decider plants it, this consumes it). */
|
|
@@ -25,16 +25,14 @@ export type NativePushPlan = Extract<ExecutionPlan, { transport: "native-push" }
|
|
|
25
25
|
|
|
26
26
|
export interface NativePushSendResult {
|
|
27
27
|
success: true;
|
|
28
|
-
/** Whether the
|
|
28
|
+
/** Whether the adapter's one permitted re-probe→re-send fired. */
|
|
29
29
|
retried: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Deliver `content`
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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.
|
|
33
|
+
* Deliver `content` through the decider-probed route. A non-retriable adapter propagates
|
|
34
|
+
* the first failure unchanged. A retriable adapter re-probes once and sends once more only
|
|
35
|
+
* when the target is still alive; no adapter gets a third attempt.
|
|
38
36
|
*/
|
|
39
37
|
export async function deliverViaNativePush(
|
|
40
38
|
adapter: NativePushAdapter,
|
|
@@ -46,8 +44,8 @@ export async function deliverViaNativePush(
|
|
|
46
44
|
await adapter.send(route, nativeSessionId, content);
|
|
47
45
|
return { success: true, retried: false };
|
|
48
46
|
} catch (firstErr) {
|
|
49
|
-
|
|
50
|
-
//
|
|
47
|
+
if (!adapter.retriable) throw firstErr;
|
|
48
|
+
// One re-probe and one retry for adapters whose send boundary is safe to replay.
|
|
51
49
|
const reprobe = await adapter.probe(nativeSessionId);
|
|
52
50
|
if (reprobe.status !== "alive") {
|
|
53
51
|
throw new Error(
|
|
@@ -65,22 +63,41 @@ export async function deliverViaNativePush(
|
|
|
65
63
|
* registry). Injected so the 5d gate proves the wiring with a fake adapter. */
|
|
66
64
|
export interface NativePushSendDeps {
|
|
67
65
|
resolveAdapter?: (backend: string) => NativePushAdapter;
|
|
66
|
+
/** The SAME provider the mailbox hand receives (`makeProductionEntwurfV2Deps` passes one
|
|
67
|
+
* `opts.senderProvider` to both). When it yields an envelope, the injected content is the
|
|
68
|
+
* mailbox-serialized body — see the render note on `makeNativePushSend`. Absent/undefined
|
|
69
|
+
* preserves the raw `plan.message`. */
|
|
70
|
+
senderProvider?: () => MailboxSenderEnvelope | undefined;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
/**
|
|
71
74
|
* Build the production `sendNativePush(plan, lock)` adapter the runner consumes. It IGNORES
|
|
72
|
-
* `lock` entirely (a native-push plan is lock-free, 봉인 4)
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
75
|
+
* `lock` entirely (a native-push plan is lock-free, 봉인 4), resolves the adapter, then applies
|
|
76
|
+
* that adapter's retry policy. A delivery throw surfaces as a REJECTED promise (the runner's
|
|
77
|
+
* try/catch maps it to execution-failed).
|
|
78
|
+
*
|
|
79
|
+
* SENDER ENVELOPE (measured #95). Direct injection used to hand the adapter the raw
|
|
80
|
+
* `plan.message`, so a native-push citizen received a body with no sender. The #95 A LIVE run
|
|
81
|
+
* measured what that costs: a fresh Pi's nonce-only callback landed in the Codex thread as
|
|
82
|
+
* bare text, and Codex could not name who called it — the callback was uncorrelatable. The
|
|
83
|
+
* control-socket rail carries the envelope in its RPC framing and the mailbox rail serializes
|
|
84
|
+
* it into the body; this rail has neither, so it renders the SAME `formatMetaMailboxBody` SSOT
|
|
85
|
+
* the mailbox uses. Rendered ONCE, before `deliverViaNativePush`, so an Antigravity re-probe
|
|
86
|
+
* retry replays byte-identical content (including its timestamp) instead of a second render.
|
|
87
|
+
* The adapter stays a dumb pipe — its signature is unchanged and it never learns about senders.
|
|
76
88
|
*/
|
|
77
89
|
export function makeNativePushSend(
|
|
78
90
|
deps: NativePushSendDeps = {},
|
|
79
91
|
): (plan: NativePushPlan, lock: LockClaim | null) => Promise<NativePushSendResult> {
|
|
80
92
|
const resolveAdapter = deps.resolveAdapter ?? resolveNativePushAdapter;
|
|
93
|
+
const senderProvider = deps.senderProvider;
|
|
81
94
|
// `_lock` is named for the hand contract but NEVER read — native-push owns/releases no lock.
|
|
82
95
|
return async (plan: NativePushPlan, _lock: LockClaim | null): Promise<NativePushSendResult> => {
|
|
96
|
+
// Resolve the sender BEFORE the adapter is resolved or touched: a provider throw must
|
|
97
|
+
// reject this send with nothing injected, not leave a half-delivered conversation.
|
|
98
|
+
const sender = senderProvider?.();
|
|
99
|
+
const content = sender ? formatMetaMailboxBody(sender, plan.message, plan.wantsReply) : plan.message;
|
|
83
100
|
const adapter = resolveAdapter(plan.backend);
|
|
84
|
-
return deliverViaNativePush(adapter, plan.route, plan.nativeSessionId,
|
|
101
|
+
return deliverViaNativePush(adapter, plan.route, plan.nativeSessionId, content);
|
|
85
102
|
};
|
|
86
103
|
}
|
|
@@ -137,8 +137,9 @@ export interface ProductionEntwurfV2Seams {
|
|
|
137
137
|
export interface ProductionEntwurfV2Opts {
|
|
138
138
|
/** Built at the wiring site from `buildLocalSenderEnvelope(ctx)`, decorated with its
|
|
139
139
|
* HONEST pi-session replyability (SE-1 2e-a: `replyable` reflects whether the canonical
|
|
140
|
-
* control socket actually exists, not a hardcoded true). ONE provider feeds
|
|
141
|
-
* control-socket RPC sender
|
|
140
|
+
* control socket actually exists, not a hardcoded true). ONE provider feeds ALL THREE
|
|
141
|
+
* rails — the control-socket RPC sender, the meta-mailbox body sender, and the
|
|
142
|
+
* native-push injected body sender — so no rail can present a different who-sent-it. */
|
|
142
143
|
senderProvider: () => SenderEnvelope | undefined;
|
|
143
144
|
lockDir?: string;
|
|
144
145
|
sessionsDir?: string;
|
|
@@ -339,7 +340,13 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
|
|
|
339
340
|
sendMailbox: (plan, _lock) => sendViaMailbox(plan as MetaMailboxPlan, _lock as LockClaim),
|
|
340
341
|
// native-push (봉인 4): the SAME injected adapter resolver drives the executor send,
|
|
341
342
|
// so the decider's probe and the delivery use one adapter. Lock-free (lock ignored).
|
|
342
|
-
|
|
343
|
+
// It also receives the SAME `opts.senderProvider` the mailbox hand got — one envelope
|
|
344
|
+
// source for all three rails, so a native-push citizen can name its sender exactly as
|
|
345
|
+
// a mailbox citizen does (#95: an envelope-less direct injection is uncorrelatable).
|
|
346
|
+
sendNativePush: makeNativePushSend({
|
|
347
|
+
resolveAdapter: io.resolveNativePushAdapter,
|
|
348
|
+
senderProvider: opts.senderProvider,
|
|
349
|
+
}),
|
|
343
350
|
};
|
|
344
351
|
|
|
345
352
|
return {
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Why a result type richer than the receipt: the carry-over contracts from 5c demand it.
|
|
14
14
|
* - N3 (5c-2b): a dead-path re-resolve `rejected` carries the resolver's `rejectReason`
|
|
15
|
-
* (dormant-fire-forget-unsupported / mailbox-undeliverable / …)
|
|
16
|
-
*
|
|
15
|
+
* (dormant-fire-forget-unsupported / mailbox-undeliverable / …), while an in-band
|
|
16
|
+
* refusal carries its supplied receiver error. The runner carries either verbatim;
|
|
17
|
+
* receiver evidence is not reclassified as resolver taxonomy.
|
|
17
18
|
* - N1 (5c-2a): a non-`failed` outcome whose `releaseLock` then threw is a
|
|
18
19
|
* `SendDeliveredReleaseFailedError` — the delivery HAPPENED, the lock is dirty, a
|
|
19
20
|
* re-send would double-deliver. The runner surfaces this as `execution-failed` with
|
|
@@ -101,12 +101,12 @@ export interface ControlSocketSendDeps {
|
|
|
101
101
|
|
|
102
102
|
export interface ControlSocketSendResult {
|
|
103
103
|
outcome: SendFinalOutcome;
|
|
104
|
-
/** Present ONLY on a `rejected` outcome
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
104
|
+
/** Present ONLY on a `rejected` outcome. It carries either the dead-path re-resolver's
|
|
105
|
+
* machine-readable reason (dormant-fire-forget-unsupported / mailbox-undeliverable /
|
|
106
|
+
* indeterminate-no-spawn / bad-target / target-address-conflict), or the non-empty
|
|
107
|
+
* error returned by an in-band RPC/mailbox refusal. The latter is receiver evidence,
|
|
108
|
+
* not a resolver taxonomy and is never invented when the completed result has no error.
|
|
109
|
+
* The 5d runner carries this verbatim to the sender-visible surface. */
|
|
110
110
|
rejectReason?: string;
|
|
111
111
|
/** #98 R, fallback leg: the `.msg` a dead-socket re-resolve enqueued. Present ONLY
|
|
112
112
|
* when the fallback actually routed to the mailbox and the enqueue succeeded — a
|
|
@@ -120,7 +120,7 @@ export interface ControlSocketSendResult {
|
|
|
120
120
|
|
|
121
121
|
// A drive step's verdict: the terminal outcome, plus the original error to RETHROW on
|
|
122
122
|
// a `failed` (the hand releases first, then rethrows — never swallows the failure), plus
|
|
123
|
-
// the optional resolver
|
|
123
|
+
// the optional resolver or in-band receiver reason to carry on a `rejected` (N3).
|
|
124
124
|
interface SendDrive {
|
|
125
125
|
outcome: SendFinalOutcome;
|
|
126
126
|
error?: unknown;
|
|
@@ -177,8 +177,9 @@ async function driveSend(plan: ControlSocketPlan, lock: LockClaim, deps: Control
|
|
|
177
177
|
// dead ⇒ proven non-delivery ⇒ same-lock one-shot re-resolve (lock still held).
|
|
178
178
|
return await driveDeadFallback(plan, lock, deps);
|
|
179
179
|
}
|
|
180
|
-
// A completed RPC: ack ⇒ sent; in-band refusal ⇒ rejected, NO fallback.
|
|
181
|
-
|
|
180
|
+
// A completed RPC: ack ⇒ sent; in-band refusal ⇒ rejected, NO fallback. Preserve
|
|
181
|
+
// its receiver error verbatim when supplied; do not make a reason up when absent.
|
|
182
|
+
return result.success ? { outcome: "sent" } : inBandRejected(result);
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
/**
|
|
@@ -187,6 +188,10 @@ async function driveSend(plan: ControlSocketPlan, lock: LockClaim, deps: Control
|
|
|
187
188
|
* re-enter the fallback, it finalizes as failed). The hand only executes; the resolver
|
|
188
189
|
* decided.
|
|
189
190
|
*/
|
|
191
|
+
function inBandRejected(result: RpcSendResult): SendDrive {
|
|
192
|
+
return { outcome: "rejected", rejectReason: result.error === "" ? undefined : result.error };
|
|
193
|
+
}
|
|
194
|
+
|
|
190
195
|
async function driveDeadFallback(
|
|
191
196
|
plan: ControlSocketPlan,
|
|
192
197
|
lock: LockClaim,
|
|
@@ -219,7 +224,7 @@ async function driveDeadFallback(
|
|
|
219
224
|
// connect failure finalizes as failed (no further fallback).
|
|
220
225
|
try {
|
|
221
226
|
const r = await deps.sendOverSocket(rePlan);
|
|
222
|
-
return
|
|
227
|
+
return r.success ? { outcome: "fallback-sent" } : inBandRejected(r);
|
|
223
228
|
} catch (err) {
|
|
224
229
|
return { outcome: "failed", error: err };
|
|
225
230
|
}
|
|
@@ -231,7 +236,7 @@ async function driveDeadFallback(
|
|
|
231
236
|
// #98 R: this leg writes a `.msg` exactly like the primary mailbox rail, so it
|
|
232
237
|
// owes the sender the same per-message receipt. Carried only on success — a
|
|
233
238
|
// `rejected` enqueue wrote no file to name.
|
|
234
|
-
return r.success ? { outcome: "fallback-sent", messagePath: r.messagePath } :
|
|
239
|
+
return r.success ? { outcome: "fallback-sent", messagePath: r.messagePath } : inBandRejected(r);
|
|
235
240
|
} catch (err) {
|
|
236
241
|
return { outcome: "failed", error: err };
|
|
237
242
|
}
|
|
@@ -100,11 +100,14 @@ export function actionableRejectHint(reason: string): string | undefined {
|
|
|
100
100
|
"that garden id by a session that died without cleaning up."
|
|
101
101
|
);
|
|
102
102
|
case "native-push-target-dead":
|
|
103
|
-
// The adapter probe found no live host
|
|
104
|
-
|
|
103
|
+
// The adapter probe found no live native host serving this conversation/thread. Stays
|
|
104
|
+
// backend-neutral: agy speaks conversations over an LS port, Codex threads over an
|
|
105
|
+
// app-server socket, and this hint is read by callers of both.
|
|
106
|
+
return "the native-push conversation/thread is not live — no native host (harness process or app-server) answered the probe for it. Re-open that conversation/thread in its own harness, then retry; there is nothing to inject into.";
|
|
105
107
|
case "native-push-probe-indeterminate":
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
+
// Native host up, but nothing served this conversation/thread — inconclusive, not a
|
|
109
|
+
// hard dead. Backend-neutral for the same reason as the dead cell above.
|
|
110
|
+
return "the native host (harness process or app-server) is up, but nothing served this conversation/thread, so the probe is INCONCLUSIVE — not a measured death. Retry once it is loaded, or verify the conversation/thread id.";
|
|
108
111
|
case "record-less-socket":
|
|
109
112
|
// #50 C4: name the true cause AND the fix — a bare socket is a diagnostic
|
|
110
113
|
// state, not an addressable citizen (the record is the address).
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* meta-mailbox-body — the SINGLE source for rendering an entwurf message as a
|
|
3
|
-
* meta-bridge mailbox body.
|
|
4
|
-
* with no live control socket
|
|
3
|
+
* meta-bridge mailbox body. Every transport that can deliver to a garden citizen
|
|
4
|
+
* with no live control socket uses this:
|
|
5
5
|
* - the MCP bridge entwurf_v2 (mcp/entwurf-bridge) — external/Claude-host sends
|
|
6
6
|
* - the pi-native entwurf_v2 (pi-extensions/entwurf-control.ts) — pi-session sends
|
|
7
|
+
* - the native-push rail (pi-extensions/lib/entwurf-v2-native-push.ts) — direct
|
|
8
|
+
* injection into an Antigravity conversation or a loaded Codex thread. Its
|
|
9
|
+
* THIRD consumer, added after the #95 A LIVE run measured what an envelope-less
|
|
10
|
+
* injection costs: a nonce-only callback arrived in a Codex thread as bare text
|
|
11
|
+
* and the receiving citizen could not name who had called it.
|
|
7
12
|
*
|
|
8
13
|
* The control-socket path carries the sender envelope inside its RPC framing; the
|
|
9
|
-
* mailbox path is just a file
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
+
* mailbox path is just a file and direct injection is just a string, so for both the
|
|
15
|
+
* envelope must be SERIALIZED INTO the body —
|
|
16
|
+
* else a receiver — reading entwurf_inbox_read, or reading an injected turn in its own
|
|
17
|
+
* conversation — would not know who sent it, whether the sender is replyable (and at
|
|
18
|
+
* which sessionId), or whether a reply was wanted. The render mirrors the live
|
|
19
|
+
* "[entwurf received ⟵]" header so a transcript reads the same whether the message
|
|
20
|
+
* arrived over a socket, a mailbox, or direct injection.
|
|
14
21
|
*
|
|
15
22
|
* No filesystem/network IO and no mutation — the only ambient read is
|
|
16
23
|
* process.env.HOME for display abbreviation (so not strictly referentially pure,
|
|
17
|
-
* but deterministic per environment). Extracted so the
|
|
18
|
-
* in how a
|
|
19
|
-
* garden-id replies.
|
|
24
|
+
* but deterministic per environment). Extracted so the three consumers cannot
|
|
25
|
+
* drift in how a delivered message presents who-sent-it — the field that
|
|
26
|
+
* round-trips garden-id replies.
|
|
20
27
|
*/
|
|
21
28
|
|
|
22
|
-
/** The fields a
|
|
23
|
-
* the SenderEnvelope of both entwurf_v2 surfaces. */
|
|
29
|
+
/** The fields a sender-envelope presentation needs from a sender. Structurally
|
|
30
|
+
* compatible with the SenderEnvelope of both entwurf_v2 surfaces. */
|
|
24
31
|
export interface MailboxSenderEnvelope {
|
|
25
32
|
sessionId: string;
|
|
26
33
|
agentId: string;
|
|
@@ -53,7 +60,9 @@ function formatKstTimestamp(iso: string): string {
|
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
/**
|
|
56
|
-
* Render the full
|
|
63
|
+
* Render the full body: header envelope + separator + message. The mailbox writes this
|
|
64
|
+
* into its message file and the native-push rail injects it directly into a live
|
|
65
|
+
* conversation; both present the sender identically. A
|
|
57
66
|
* replyable sender (pi-session, or a trusted meta-session) advertises its
|
|
58
67
|
* sessionId as the reply address; a non-replyable sender says so WITHOUT
|
|
59
68
|
* losing its origin — a record-backed meta-session that is currently inactive
|