@junghanacs/entwurf 0.12.8 → 0.12.10
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 +134 -248
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +39 -1
- package/DELIVERY.md +5 -4
- package/README.md +148 -23
- package/VERIFY.md +3 -2
- package/demo/README.md +2 -2
- package/demo/demo.sh +7 -1
- package/docs/setup-clean-host.md +24 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +168 -81
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +12 -12
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +5 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +16 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +14 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +11 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +15 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +500 -54
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +8 -3
- package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +14 -2
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +155 -29
- package/mcp/entwurf-bridge/src/index.ts +200 -94
- package/package.json +11 -9
- package/pi-extensions/entwurf-control.ts +81 -39
- package/pi-extensions/lib/acp/models.ts +12 -12
- package/pi-extensions/lib/acp/overlay.ts +5 -3
- package/pi-extensions/lib/entwurf-core.ts +26 -9
- package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
- package/pi-extensions/lib/entwurf-fact-provider.ts +24 -19
- package/pi-extensions/lib/entwurf-facts.ts +13 -13
- package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
- package/pi-extensions/lib/entwurf-self-address.ts +8 -9
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
- package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
- package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
- package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
- package/pi-extensions/lib/entwurf-v2-production.ts +22 -13
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +11 -3
- package/pi-extensions/lib/meta-sender-identity.ts +15 -5
- package/pi-extensions/lib/meta-session.ts +543 -58
- package/pi-extensions/meta-bridge-hook.ts +8 -2
- package/run.sh +78 -67
- package/scripts/agy-bridge-config.py +249 -49
- package/scripts/agy-bridge.sh +59 -14
- package/scripts/agy-imprint.ts +15 -1
- package/scripts/check-acp-carrier-augment.ts +34 -2
- package/scripts/check-acp-sdk-surface.ts +22 -11
- package/scripts/check-acp-session-reuse.ts +1 -1
- package/scripts/check-acp-session-store.ts +3 -3
- package/scripts/check-agy-permission-matrix.py +655 -0
- package/scripts/check-agy-sender-identity.ts +83 -0
- package/scripts/check-entwurf-control-rpc.ts +2 -2
- package/scripts/check-entwurf-deliverability.ts +9 -6
- package/scripts/check-entwurf-fact-provider.ts +16 -8
- package/scripts/check-entwurf-facts.ts +13 -13
- package/scripts/check-entwurf-resume-args.ts +25 -63
- package/scripts/check-entwurf-self-address.ts +187 -4
- package/scripts/check-entwurf-session-identity.ts +7 -6
- package/scripts/check-entwurf-v2-contract.ts +2 -2
- package/scripts/check-entwurf-v2-production.ts +9 -7
- package/scripts/check-entwurf-v2-spawn-production.ts +8 -4
- package/scripts/check-entwurf-v2-surface.ts +327 -14
- package/scripts/check-fresh-cut-gate.sh +305 -4
- package/scripts/check-gate-qualification.ts +785 -0
- package/scripts/check-meta-identity-consumers.ts +501 -1
- package/scripts/check-meta-listing.ts +91 -9
- package/scripts/check-meta-receiver-marker.ts +54 -0
- package/scripts/check-model-lock.ts +1 -1
- package/scripts/check-shell-quote.ts +2 -1
- package/scripts/lib/mutation-qualify.ts +794 -0
- package/scripts/meta-bridge-fresh-cut.ts +164 -28
- package/scripts/mutants/acp-augment.json +30 -0
- package/scripts/mutants/agy-permission.json +144 -0
- package/scripts/mutants/meta-identity.json +17 -0
- package/scripts/mutants/self-address.json +59 -0
- package/scripts/mutants/v2-surface.json +87 -0
- package/scripts/pi_settings_io.py +65 -0
- package/scripts/register-pi-package.py +183 -37
- package/scripts/register-pi-provider.py +68 -10
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
- package/scripts/smoke-acp-v2-send-live.ts +33 -18
- package/scripts/smoke-agy-install-state.sh +205 -20
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
- package/scripts/smoke-meta-install-state.sh +1 -1
- package/scripts/smoke-pi-attach.ts +7 -2
- package/scripts/smoke-user-scope-citizen.sh +177 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
- package/scripts/check-entwurf-mailbox-guard.ts +0 -262
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* (one-way: provider → facts / socket-discovery / meta-session) — no import cycle
|
|
6
6
|
* with `entwurf-facts.ts` (which owns `SocketProbe`/`resolveFactList`).
|
|
7
7
|
*
|
|
8
|
-
* listAllMetaIdentities →
|
|
9
|
-
* → pre-quarantine
|
|
8
|
+
* listAllMetaIdentities → socket-domain gid extraction → scanSocketProbes
|
|
9
|
+
* → pre-quarantine out-of-domain/socket conflicts → resolveFactList(clean)
|
|
10
10
|
* → { facts, diagnostics }
|
|
11
11
|
*
|
|
12
12
|
* Two throw-vs-diagnostics policies, kept distinct (GPT힣 C-원칙):
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
* — that is not a re-implementation of the collision rule, it is input
|
|
21
21
|
* sanitation that leaves the pure-core invariant intact.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
* socket): gardenId is the universal address and a send path
|
|
23
|
+
* An out-of-socket-domain record/socket collision quarantines BOTH sides (the
|
|
24
|
+
* PeerFact AND the socket): gardenId is the universal address and a send path
|
|
25
25
|
* first, so surfacing the record alone (as a clean `unsupported` PeerFact) while
|
|
26
26
|
* a same-gid socket exists would be half a lie. Both leave the normal output;
|
|
27
27
|
* one diagnostic carries the fact. (pi + same-gid socket = the normal merge.)
|
|
28
28
|
*/
|
|
29
|
-
import {
|
|
29
|
+
import { isOutOfSocketDomainGardenIdConflict, resolveFactList } from "./entwurf-facts.js";
|
|
30
30
|
import { isLivenessSupported } from "./entwurf-v2-contract.js";
|
|
31
|
-
import { FRESH_CUT_PRESCRIPTION, listAllMetaIdentities } from "./meta-session.js";
|
|
31
|
+
import { FRESH_CUT_PRESCRIPTION, listAllMetaIdentities, } from "./meta-session.js";
|
|
32
32
|
import { scanSocketProbes } from "./socket-discovery.js";
|
|
33
33
|
/** The #50 C4 demotion messages, one per probed liveness so the F8 aggregation
|
|
34
34
|
* groups same-state sockets into one line. Each names the true cause (no record
|
|
@@ -80,8 +80,8 @@ export async function listEntwurfFacts(deps) {
|
|
|
80
80
|
// 2. socket axis — probe (dir sockets) ∪ (in-domain citizen canonical paths).
|
|
81
81
|
// Its three hazards (symlink forgery / malformed name / dir-read error) are
|
|
82
82
|
// folded into diagnostics here so the listing survives but never lies.
|
|
83
|
-
const
|
|
84
|
-
const scan = await scanSocketProbes(
|
|
83
|
+
const socketDomainGids = identities.filter((i) => isLivenessSupported(i.backend)).map((i) => i.gardenId);
|
|
84
|
+
const scan = await scanSocketProbes(socketDomainGids, deps.socket ?? {});
|
|
85
85
|
const probes = scan.probes;
|
|
86
86
|
const socketGids = new Set(probes.map((p) => p.gardenId));
|
|
87
87
|
const symlinkedGids = new Set(scan.symlinkedGardenIds);
|
|
@@ -106,23 +106,21 @@ export async function listEntwurfFacts(deps) {
|
|
|
106
106
|
message: `control-socket directory unreadable (socket axis incomplete; meta-record citizens still listed): ${scan.dirError}`,
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
|
-
// 3.
|
|
110
|
-
// predicate is SHARED with
|
|
111
|
-
// listing and dispatch cannot drift
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// (symlinked) socket survive as a clean PeerFact while the legacy send path
|
|
115
|
-
// still followed the symlink — the gap this closes.
|
|
109
|
+
// 3. Pre-quarantine citizens outside the control-socket capability domain that
|
|
110
|
+
// collide with a control socket. The predicate is SHARED with v2 dispatch so
|
|
111
|
+
// listing and dispatch cannot drift. It unions real and symlinked socket gids:
|
|
112
|
+
// the earlier real-socket-only check let an out-of-domain record survive as a
|
|
113
|
+
// clean PeerFact beside a forged symlink receiver.
|
|
116
114
|
const conflictGids = new Set();
|
|
117
115
|
for (const id of identities) {
|
|
118
|
-
if (
|
|
116
|
+
if (isOutOfSocketDomainGardenIdConflict(id.backend, id.gardenId, socketGids, symlinkedGids)) {
|
|
119
117
|
conflictGids.add(id.gardenId);
|
|
120
118
|
diagnostics.push({
|
|
121
119
|
kind: "garden-id-socket-conflict",
|
|
122
120
|
gardenId: id.gardenId,
|
|
123
121
|
backend: id.backend,
|
|
124
|
-
message: `
|
|
125
|
-
"
|
|
122
|
+
message: `out-of-socket-domain citizen (${id.backend}) shares its gardenId with a control socket ` +
|
|
123
|
+
"(real or symlinked) — address ambiguity; both sides are quarantined from the listing.",
|
|
126
124
|
});
|
|
127
125
|
}
|
|
128
126
|
}
|
|
@@ -52,27 +52,27 @@ export function resolvePeerFact(identity, socket) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
56
|
-
* one OR a symlinked/forged one. The gardenId is the universal
|
|
57
|
-
* so a
|
|
58
|
-
* socket
|
|
55
|
+
* An OUT-OF-SOCKET-DOMAIN record whose gardenId collides with a control socket —
|
|
56
|
+
* a real (probed) one OR a symlinked/forged one. The gardenId is the universal
|
|
57
|
+
* address, so a citizen without socket capability sharing it with a socket means
|
|
58
|
+
* the socket reaches a DIFFERENT receiver than the record names — an address split.
|
|
59
59
|
* Both the citizen and the socket are quarantined from the facts listing.
|
|
60
60
|
*
|
|
61
61
|
* The union `socketGids ∪ symlinkedGardenIds` is load-bearing: `socketGids` are
|
|
62
62
|
* gids with a real probed `*.sock`, but `symlinkedGardenIds` are NEVER probed (P1)
|
|
63
63
|
* and so are absent from `socketGids`. Looking at `socketGids` alone (the
|
|
64
|
-
* fact-provider:125 gap this closes) let
|
|
64
|
+
* fact-provider:125 gap this closes) let an out-of-domain citizen with a *symlinked* socket
|
|
65
65
|
* survive as a clean PeerFact while the legacy send path still followed the symlink
|
|
66
66
|
* to a forged receiver. Both axes claim the gid → both must quarantine it.
|
|
67
67
|
*
|
|
68
|
-
* SCOPE: this is the
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* SCOPE: this is the record-side, out-of-socket-domain conflict only — shared by
|
|
69
|
+
* listing and dispatch so they cannot drift; only the observation-bit source is
|
|
70
|
+
* parameterized. A citizen IN the socket domain whose own canonical socket is a
|
|
71
|
+
* symlink is NOT this predicate's concern — that is a
|
|
72
72
|
* target-specific lstat conflict the decider's `inspectTargetControlSocket` raises
|
|
73
73
|
* as `address-conflict`, kept deliberately separate (GPT 1차 검수 C).
|
|
74
74
|
*/
|
|
75
|
-
export function
|
|
75
|
+
export function isOutOfSocketDomainGardenIdConflict(backend, gardenId, socketGids, symlinkedGardenIds) {
|
|
76
76
|
return !isLivenessSupported(backend) && (socketGids.has(gardenId) || symlinkedGardenIds.has(gardenId));
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
@@ -89,8 +89,8 @@ export function isNonPiGardenIdSocketConflict(backend, gardenId, socketGids, sym
|
|
|
89
89
|
* `indeterminate` and strand a dormant citizen as un-resumable); a dormant
|
|
90
90
|
* citizen's absent socket file is probed to `dead` (ENOENT) by the wiring and
|
|
91
91
|
* arrives here AS `dead` → dormant → resumable.
|
|
92
|
-
* - out-of-domain citizen WITH a control socket at its gardenId → fail-loud
|
|
93
|
-
* (address ambiguity; a
|
|
92
|
+
* - out-of-socket-domain citizen WITH a control socket at its gardenId → fail-loud
|
|
93
|
+
* (address ambiguity; a backend without socket capability cannot own that rail).
|
|
94
94
|
* - out-of-domain citizen without a socket → `unsupported` (via resolvePeerFact).
|
|
95
95
|
* - a probed gardenId with NO citizen → `RecordLessSocketFact` (#50 C4: a
|
|
96
96
|
* diagnostic subject the provider folds into a `record-less-socket`
|
|
@@ -125,7 +125,7 @@ export function resolveFactList(identities, socketProbes) {
|
|
|
125
125
|
else {
|
|
126
126
|
if (probeMap.has(gid)) {
|
|
127
127
|
throw new Error(`resolveFactList: out-of-domain citizen ${gid} (${identity.backend}) has a control socket — ` +
|
|
128
|
-
"address ambiguity (
|
|
128
|
+
"address ambiguity (an out-of-socket-domain citizen cannot own a control socket)");
|
|
129
129
|
}
|
|
130
130
|
socket = null;
|
|
131
131
|
}
|
|
@@ -1,59 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* entwurf-resume-args — the SINGLE source of truth for the `pi` argv a resume spawn is
|
|
3
|
-
* launched with (0.11 Stage 0 step 5c-3b).
|
|
4
|
-
*
|
|
5
|
-
* shape — the one place the `--no-extensions` / `--entwurf-control` decision lives.
|
|
3
|
+
* launched with (0.11 Stage 0 step 5c-3b). One place owns the
|
|
4
|
+
* `--no-extensions` / `--entwurf-control` decision so the launch shape cannot drift.
|
|
6
5
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* is the early-exit/failure observation. `plan.launchArgs` (`--approve` or empty, from
|
|
19
|
-
* the decider's preflight) rides along as pi flags before the prompt.
|
|
6
|
+
* The shipped posture (A1): a RESIDENT, addressable garden citizen. The resume turn runs via
|
|
7
|
+
* `-p <prompt>` (the prompt-as-turn authority), and the child is launched WITH
|
|
8
|
+
* `--entwurf-control` and WITHOUT `--no-extensions` — the keep-alive is the GOAL here: the
|
|
9
|
+
* resumed session stands its control socket up and stays addressable. The 5c-3a watcher's
|
|
10
|
+
* `socket-alive` is exactly that "resumed citizen is up" observation (→ release the per-gid
|
|
11
|
+
* lock, child lives on); `child-exited` is the early-exit/failure observation.
|
|
12
|
+
* `plan.launchArgs` (`--approve` or empty, from the decider's preflight) rides along as pi
|
|
13
|
+
* flags before the prompt.
|
|
14
|
+
*
|
|
15
|
+
* A second `legacy` one-shot posture (`--no-extensions`, no control socket, so `pi -p` could
|
|
16
|
+
* exit) lived here until 2026-07-27 and was removed with its launcher and the v1 verbs.
|
|
20
17
|
*
|
|
21
18
|
* Provider/model identity is the caller's existing authority (readSessionIdentity /
|
|
22
19
|
* getEntwurfExplicitExtensions) — this builder only LAYS OUT argv, it never resolves
|
|
23
|
-
* identity. `explicitExtensionArgs` is preserved verbatim
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* identity. `explicitExtensionArgs` is preserved verbatim: a recorded `provider=entwurf`
|
|
21
|
+
* resume needs the bridge re-injected to resolve the provider, and dropping it would
|
|
22
|
+
* re-introduce the "Unknown provider" footgun (#29). (A future slice may dedup against
|
|
23
|
+
* settings-loaded extensions; not here.)
|
|
27
24
|
*
|
|
28
|
-
* This module is import-free on purpose: the
|
|
29
|
-
*
|
|
30
|
-
* the gate import the same source — a self-contained string builder keeps both configs happy.
|
|
25
|
+
* This module is import-free on purpose: the v2 adapter and the gate import the same source,
|
|
26
|
+
* and a self-contained string builder keeps both tsconfigs happy.
|
|
31
27
|
*/
|
|
32
28
|
/**
|
|
33
|
-
* Build the `pi` argv for a resume spawn. The
|
|
34
|
-
*
|
|
35
|
-
*
|
|
29
|
+
* Build the `pi` argv for a resume spawn. The prefix is `--mode json -p` (headless JSON
|
|
30
|
+
* child, prompt-as-turn); then the resident posture; then
|
|
31
|
+
* `[…ext args] --session <file> [--provider <p>] --model <m> <prompt>`.
|
|
36
32
|
*
|
|
37
33
|
* Invariants the gate pins:
|
|
38
|
-
* - legacy carries `--no-extensions` and NO `--entwurf-control`.
|
|
39
34
|
* - v2-control carries `--entwurf-control` and NO `--no-extensions`, plus `-p` + prompt.
|
|
40
|
-
* - `explicitExtensionArgs` appears exactly once
|
|
41
|
-
* - `launchArgs` is included
|
|
42
|
-
* - provider/model/prompt identity is
|
|
35
|
+
* - `explicitExtensionArgs` appears exactly once.
|
|
36
|
+
* - `launchArgs` is included before the suffix.
|
|
37
|
+
* - provider/model/prompt identity layout is fixed.
|
|
43
38
|
*/
|
|
44
39
|
export function buildResumePiArgs(input) {
|
|
45
40
|
const args = ["--mode", "json", "-p"];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// The keep-alive legacy avoided is the goal here. `--approve`/launchArgs ride along.
|
|
53
|
-
args.push("--entwurf-control");
|
|
54
|
-
args.push(...(input.launchArgs ?? []));
|
|
55
|
-
}
|
|
56
|
-
// Shared suffix — identical identity layout in both variants.
|
|
41
|
+
// Resident citizen: stand the control socket up (A1) and keep extensions loaded. The
|
|
42
|
+
// keep-alive the removed one-shot launcher had to avoid is precisely the goal here —
|
|
43
|
+
// the resumed session must stay addressable. `--approve`/launchArgs ride along.
|
|
44
|
+
args.push("--entwurf-control");
|
|
45
|
+
args.push(...(input.launchArgs ?? []));
|
|
46
|
+
// Suffix — the fixed identity layout.
|
|
57
47
|
args.push(...input.explicitExtensionArgs);
|
|
58
48
|
args.push("--session", input.sessionFile);
|
|
59
49
|
if (input.provider)
|
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* "Can a reply to THIS session actually land where its model will see it?"
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* with every fact INJECTED (no IO) so the gate can pin each row.
|
|
6
|
+
* This module closed the former split where the MCP and native pi surfaces inferred
|
|
7
|
+
* `replyable: true` from identity-carrier presence alone. Identity and reachability
|
|
8
|
+
* are separate: pi's env carries the garden id established by record birth, while
|
|
9
|
+
* replyability requires its control socket; a native marker must be record-backed,
|
|
10
|
+
* while replyability additionally requires its own rail's live facts. This is the
|
|
11
|
+
* single truth-table both surfaces compute from, with every fact INJECTED (no IO) so
|
|
12
|
+
* the gate can pin each row.
|
|
14
13
|
*
|
|
15
14
|
* Axes by origin:
|
|
16
15
|
* - pi-session: replyable ⟺ a live control socket exists at the canonical path.
|
|
@@ -33,7 +32,7 @@
|
|
|
33
32
|
* — which never arms a watch — would report replyable:false forever.
|
|
34
33
|
* - external-mcp: never replyable — no authoritative reply address.
|
|
35
34
|
*
|
|
36
|
-
* `origin` stays
|
|
35
|
+
* `origin` stays sender-carrier PROVENANCE, never the citizen identity authority and never a rail.
|
|
37
36
|
* Which rail a meta citizen's reply rides is a SECOND axis — `metaDeliveryDomain`, derived
|
|
38
37
|
* by the caller from `nativePushSupported(backend)`, not from `wakeMode` (direct-inject also
|
|
39
38
|
* covers codex/pi, which have no native-push adapter). Fail-closed: an unsupplied domain is
|
|
@@ -7,25 +7,28 @@
|
|
|
7
7
|
* `entwurf-v2-contract-schema.ts` (0.12.1 B-1) so this module — which the
|
|
8
8
|
* harness-neutral MCP bridge reaches at boot — carries no pi dependency.
|
|
9
9
|
*
|
|
10
|
-
* Why a frozen contract BEFORE the fact-provider (step 4):
|
|
11
|
-
*
|
|
12
|
-
* discovery first
|
|
13
|
-
*
|
|
14
|
-
* it; dispatch computes from facts at call
|
|
15
|
-
*
|
|
10
|
+
* Why a frozen contract BEFORE the fact-provider (step 4): the legacy 3-verb
|
|
11
|
+
* surface (`entwurf`/`entwurf_resume`/`entwurf_send`) was still live when this
|
|
12
|
+
* was written, and building discovery first would have baked verb-routing into
|
|
13
|
+
* the fact layer, taking `entwurf_peers` wrong (동결결정 10 순서 근거). So the
|
|
14
|
+
* SHAPE is locked here; the facts read it; dispatch computes from facts at call
|
|
15
|
+
* time (step 5). Those v1 verbs were REMOVED in the 0.12 cutover — the ordering
|
|
16
|
+
* argument is history, but the layering it produced is the live design.
|
|
16
17
|
*
|
|
17
18
|
* Source-verified invariants folded in (Opus 실측 + GPT 보정 + Fable R1-R5, 2026-06-11):
|
|
18
19
|
* - F1: caller intent is DECLARED in the input, so the contract a caller
|
|
19
20
|
* receives is deterministic — never computed from liveness at call time.
|
|
20
21
|
* `owned-outcome` (caller owns completion) ≠ `fire-and-forget` (ack only).
|
|
21
|
-
* - R1:
|
|
22
|
-
*
|
|
23
|
-
* so its liveness is `unsupported`, NOT folded into dead/indeterminate
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* - R1: liveness is defined PER CAPABILITY DOMAIN. The control-socket domain
|
|
23
|
+
* currently contains backend `pi`; claude-code is self-fetch with no socket,
|
|
24
|
+
* so its socket liveness is `unsupported`, NOT folded into dead/indeterminate.
|
|
25
|
+
* This is transport capability, not identity rank: every target is first a
|
|
26
|
+
* record citizen. `unsupported` is a 4th FACT value, not a 4th table column.
|
|
26
27
|
* - R2: `target` is the garden-id of an EXISTING citizen. spawn-new is out of
|
|
27
|
-
* v2 scope
|
|
28
|
-
*
|
|
28
|
+
* v2 scope — it was the legacy `entwurf` verb's, and since that verb's removal
|
|
29
|
+
* fresh creation is a deferred capability with NO surface, not a fallback that
|
|
30
|
+
* still exists elsewhere. Absent/typo gid = `bad-target` (so F6 "오타 gid가
|
|
31
|
+
* 신규 spawn 사고 막기" holds automatically).
|
|
29
32
|
* - N1/F3: an `indeterminate` target never spawns. N2: `fire-and-forget` to a
|
|
30
33
|
* `dormant` target is "reject for now" (mailbox-wake lacks a reply-correlation
|
|
31
34
|
* id in the substrate; an additive extension later, not a permanent no).
|
|
@@ -58,16 +61,16 @@ export const FACT_LIVENESSES = ["alive", "dead", "indeterminate", "unsupported"]
|
|
|
58
61
|
// handled by the domain guard before the table is consulted.
|
|
59
62
|
export const DISPATCH_LIVENESSES = ["live", "dormant", "indeterminate"];
|
|
60
63
|
// ── Backend liveness domain (R1 + F4) ──────────────────────────────────────
|
|
61
|
-
// Backends whose SOCKET liveness predicate is DEFINED
|
|
62
|
-
// domain
|
|
64
|
+
// Backends whose SOCKET liveness predicate is DEFINED. The control-socket
|
|
65
|
+
// capability domain currently contains `pi` (connect + RPC `get_info`).
|
|
63
66
|
// claude-code (self-fetch, no socket) has no liveness predicate at all → `unsupported`.
|
|
64
67
|
// codex/antigravity are direct-inject; antigravity's liveness IS measured, but by the
|
|
65
68
|
// SEPARATE native-push adapter rail (a live app-server conversation probe), NOT this
|
|
66
69
|
// pi-socket domain — so it must NEVER be added here. Adding it would pull agy into the
|
|
67
|
-
//
|
|
68
|
-
// reporting agy `unsupported` = "outside the
|
|
70
|
+
// control-socket table (inspectSocket/probeSocket are socket-only); the fact layer keeps
|
|
71
|
+
// reporting agy `unsupported` = "outside the control-socket liveness domain", NOT
|
|
69
72
|
// unreachable (the native-push rail measures it — entwurf-v2-decider.ts). Widening
|
|
70
|
-
// THIS set is a deliberate future decision
|
|
73
|
+
// THIS set is a deliberate future decision, gated by a real compatible
|
|
71
74
|
// control-socket predicate — never by silently mapping sessions to dead/indeterminate
|
|
72
75
|
// (R1 핵심). check-entwurf-facts pins this == ["pi"] and asserts the native-push
|
|
73
76
|
// domain is disjoint from it.
|
|
@@ -77,12 +80,12 @@ export function isLivenessSupported(backend) {
|
|
|
77
80
|
}
|
|
78
81
|
// ── Native-push backend domain (봉인 2/4) ───────────────────────────────────
|
|
79
82
|
// 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
|
|
81
|
-
// socket. This domain is DISJOINT from LIVENESS_DOMAIN_BACKENDS
|
|
82
|
-
//
|
|
83
|
+
// live app-server conversation probe — antigravity's LS gRPC), NOT a control
|
|
84
|
+
// socket. This domain is DISJOINT from LIVENESS_DOMAIN_BACKENDS: an agy session
|
|
85
|
+
// is `unsupported` on the socket FACT axis (entwurf_peers) yet fully
|
|
83
86
|
// measured + deliverable on the native-push axis. The two are separate rails on
|
|
84
87
|
// purpose — check-entwurf-facts pins both sets and asserts their intersection is ∅
|
|
85
|
-
// (a backend can never be in both
|
|
88
|
+
// (a backend can never be in both domains).
|
|
86
89
|
export const NATIVE_PUSH_BACKENDS = ["antigravity"];
|
|
87
90
|
export function nativePushSupported(backend) {
|
|
88
91
|
return NATIVE_PUSH_BACKENDS.includes(backend);
|
|
@@ -202,14 +205,18 @@ export const ENTWURF_V2_ACTIONS = ["send", "resume"];
|
|
|
202
205
|
export const ENTWURF_V2_OWNERSHIPS = ["ack-only", "owned"];
|
|
203
206
|
// Delivery mode of the message to the target (how it is injected) — steer =
|
|
204
207
|
// interrupt the current turn, follow_up = queue after it. A SEPARATE axis from
|
|
205
|
-
// both the intent/ownership axis (F1) and the liveness-routing axis
|
|
206
|
-
// entwurf_send
|
|
208
|
+
// both the intent/ownership axis (F1) and the liveness-routing axis. The removed
|
|
209
|
+
// v1 `entwurf_send` carried the same steer|follow_up surface, so this axis is
|
|
210
|
+
// inherited vocabulary, not a second live delivery verb.
|
|
207
211
|
export const ENTWURF_V2_MODES = ["steer", "follow_up"];
|
|
208
212
|
// ── The FROZEN decision table ──────────────────────────────────────────────
|
|
209
213
|
// intent × dispatch-liveness → exactly one verdict (Q2). v2-initial ALLOWS
|
|
210
214
|
// exactly two cells (fire-and-forget+live = send; owned-outcome+dormant =
|
|
211
215
|
// resume); the other four reject. The reject cells are honest "지금은 없음"
|
|
212
|
-
// locks (N2)
|
|
216
|
+
// locks (N2). They were written while the legacy 3-verb surface still covered
|
|
217
|
+
// those flows; that surface is GONE, so a reject cell is now a real absence with
|
|
218
|
+
// no fallback verb behind it — reopening one takes a new contract, never a
|
|
219
|
+
// quiet re-admission.
|
|
213
220
|
export const DISPATCH_TABLE = {
|
|
214
221
|
"fire-and-forget": {
|
|
215
222
|
live: { action: "send", transport: "control-socket", ownership: "ack-only" },
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* MCP-schema bypass for pi-native/internal callers).
|
|
27
27
|
* 2. resolveTarget — no citizen → bad-target; a record-less control socket →
|
|
28
28
|
* record-less-socket (#50 C4: the record is the sole address authority);
|
|
29
|
-
* quarantined (
|
|
29
|
+
* quarantined (out-of-socket-domain record sharing a socket/symlink) → target-address-conflict.
|
|
30
30
|
* PROBE-FREE.
|
|
31
31
|
* 3. backend → isLivenessSupported.
|
|
32
32
|
* 4. acquireLock — IN-DOMAIN ONLY (?7), BEFORE lstat/connect, so the probe
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
* never hand-assembles a `{ok:false, …}` literal, so the pre-probe-null rule cannot
|
|
46
46
|
* be bypassed.
|
|
47
47
|
*/
|
|
48
|
-
import {
|
|
48
|
+
import { isOutOfSocketDomainGardenIdConflict } from "./entwurf-facts.js";
|
|
49
49
|
import { isLivenessSupported, makeRejectReceipt, nativePushSupported, resolveDispatch, resolveNativePushDispatch, } from "./entwurf-v2-contract.js";
|
|
50
50
|
import { defaultMetaMailboxDir, defaultMetaSessionsDir, metaCapabilityFor, } from "./meta-session.js";
|
|
51
51
|
import { isValidSessionId } from "./session-id.js";
|
|
52
52
|
import { controlSocketPath, mapInspectionToLiveness } from "./socket-discovery.js";
|
|
53
53
|
// Re-export the shared conflict predicate so producers of a TargetResolution have a
|
|
54
54
|
// single import site for it (it is the SAME fn the fact-provider listing uses).
|
|
55
|
-
export {
|
|
55
|
+
export { isOutOfSocketDomainGardenIdConflict };
|
|
56
56
|
// ── observe timeout (?3) ───────────────────────────────────────────────────
|
|
57
57
|
// The bounded wait 5c's release-watcher gives a spawned child to surface its
|
|
58
58
|
// control socket / exit. Planted into the resume plan so the hand does not invent
|
|
@@ -178,7 +178,7 @@ export async function decideDispatch(input, deps) {
|
|
|
178
178
|
};
|
|
179
179
|
return { kind: "execute", receipt, plan, lock: null };
|
|
180
180
|
}
|
|
181
|
-
// 4-5.
|
|
181
|
+
// 4-5. control-socket domain (currently backend pi): lock → inspect → route.
|
|
182
182
|
return decideInDomain(gardenId, input, deps, ctx, identity.cwd);
|
|
183
183
|
}
|
|
184
184
|
async function decideInDomain(gardenId, input, deps, ctx, cwd) {
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* entwurf-v2-lock — the per-gid dispatch lock primitive (0.11 Stage 0 step 5a,
|
|
3
3
|
* 버킷 B F2). LOAD-BEARING: the guard against a double-spawn of the same dormant
|
|
4
4
|
* target by two V2 dispatchers that share the substrate through different entry
|
|
5
|
-
* points. SCOPE (honest): this protects v2/v2 only.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* points. SCOPE (honest): this protects v2/v2 only. It was written while the legacy
|
|
6
|
+
* `entwurf_resume` still ran unchanged (동결결정 10 scope A) without taking this
|
|
7
|
+
* lock, which left a v2/legacy concurrent-resume gap. That verb was REMOVED in the
|
|
8
|
+
* 0.12 cutover, so the gap is closed by subtraction — not by this lock growing to
|
|
9
|
+
* cover it. Any NEW resume entry point must take this lock or the gap reopens.
|
|
9
10
|
*
|
|
10
11
|
* ENVIRONMENT ASSUMPTION (stale reclaim): `hostname` equality is used as the
|
|
11
12
|
* proxy for "same machine", so a holder pid is reclaim-probed with kill(0) only
|
|
@@ -21,13 +21,16 @@
|
|
|
21
21
|
*
|
|
22
22
|
* `resolveTarget` (QB1): a MISSING meta-record is a soft `bad-target` (identity:null); a
|
|
23
23
|
* PRESENT-but-corrupt record fails LOUD (the read throws drift) — never matched by string.
|
|
24
|
+
* "Corrupt" includes a record that does not hold its `nativeSessionId` alone (#52): the read
|
|
25
|
+
* here is the ADDRESSABLE one, so the store-wide uniqueness half runs at this boundary and
|
|
26
|
+
* nowhere cheaper. Absent stays soft; ambiguous never does.
|
|
24
27
|
* `preProbeAddressConflict` (QB2): a single record-side `lstat` (NO connect) of the target's
|
|
25
28
|
* canonical socket path; `indeterminate` (EACCES/unknown) fails loud rather than silently
|
|
26
29
|
* claiming "no conflict". The decider's later `inspectSocket` probe is a SEPARATE step.
|
|
27
30
|
*/
|
|
28
31
|
import { formatSenderInfoBlock, sendRpcCommand as realSendRpc, } from "./entwurf-control-rpc.js";
|
|
29
32
|
import { mailboxConversationalDeliverable, receiverMarkerMatchesIdentity, } from "./entwurf-deliverability.js";
|
|
30
|
-
import {
|
|
33
|
+
import { isOutOfSocketDomainGardenIdConflict } from "./entwurf-facts.js";
|
|
31
34
|
import { isLivenessSupported } from "./entwurf-v2-contract.js";
|
|
32
35
|
import { decideDispatch, } from "./entwurf-v2-decider.js";
|
|
33
36
|
import { ENTWURF_V2_LOCK_DIR, acquireLock as realAcquireLock, releaseLock as realReleaseLock, } from "./entwurf-v2-lock.js";
|
|
@@ -37,7 +40,7 @@ import { executeControlSocketSend, } from "./entwurf-v2-send.js";
|
|
|
37
40
|
import { resolveDeadControlSendFallback } from "./entwurf-v2-send-fallback.js";
|
|
38
41
|
import { executeSpawnBgResume } from "./entwurf-v2-spawn.js";
|
|
39
42
|
import { makeProductionSpawnBgResumeDeps } from "./entwurf-v2-spawn-production.js";
|
|
40
|
-
import { defaultMetaMailboxDir, defaultMetaSessionsDir, enqueueMetaMessage, metaCapabilityFor, metaRecordExistsByGardenId,
|
|
43
|
+
import { defaultMetaMailboxDir, defaultMetaSessionsDir, enqueueMetaMessage, metaCapabilityFor, metaRecordExistsByGardenId, readAddressableMetaIdentity, readMetaReceiverMarker, } from "./meta-session.js";
|
|
41
44
|
import { resolveNativePushAdapter as realResolveNativePushAdapter, } from "./native-push/adapter.js";
|
|
42
45
|
import { CONTROL_SOCKET_DIR, controlSocketPath, inspectControlSocketPath, inspectTargetControlSocket, isRecordLessSocketCandidate, } from "./socket-discovery.js";
|
|
43
46
|
import { classifyConnectError, probeSocketLiveness } from "./socket-probe.js";
|
|
@@ -65,7 +68,7 @@ async function lazyProductionPreflight(input) {
|
|
|
65
68
|
return preflight(input);
|
|
66
69
|
}
|
|
67
70
|
/** Map a record-side socket inspection to the singleton (socketGids, symlinkedGids) the
|
|
68
|
-
* `
|
|
71
|
+
* `isOutOfSocketDomainGardenIdConflict` predicate consumes. `indeterminate` fails LOUD (QB2): an
|
|
69
72
|
* unprovable conflict must NOT be folded to "no conflict" — that would silently allow an
|
|
70
73
|
* unsupported-backend mailbox send onto a quarantined address. */
|
|
71
74
|
function conflictSetsFor(gid, inspection) {
|
|
@@ -97,7 +100,7 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
97
100
|
const s = opts.seams ?? {};
|
|
98
101
|
const io = {
|
|
99
102
|
metaRecordExists: s.metaRecordExists ?? metaRecordExistsByGardenId,
|
|
100
|
-
readIdentity: s.readIdentity ??
|
|
103
|
+
readIdentity: s.readIdentity ?? readAddressableMetaIdentity,
|
|
101
104
|
readReceiverMarker: s.readReceiverMarker ?? ((gid) => readMetaReceiverMarker({ gardenId: gid })),
|
|
102
105
|
inspectPath: s.inspectPath ?? inspectControlSocketPath,
|
|
103
106
|
acquireLock: s.acquireLock ?? realAcquireLock,
|
|
@@ -159,20 +162,18 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
159
162
|
};
|
|
160
163
|
}
|
|
161
164
|
const identity = io.readIdentity(gid, sessionsDir);
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
// and (b) turn a pi target's `indeterminate` lstat into a top-level throw, stealing it
|
|
167
|
-
// from the under-lock `inspectSocket → indeterminate → indeterminate-no-spawn` path.
|
|
165
|
+
// This pre-probe conflict applies only OUTSIDE the control-socket capability domain.
|
|
166
|
+
// An in-domain target's lstat/connect MUST run under the later per-target lock, so it
|
|
167
|
+
// short-circuits here with no pre-lock IO. Otherwise an indeterminate lstat would be
|
|
168
|
+
// stolen from the under-lock `inspectSocket → indeterminate-no-spawn` path.
|
|
168
169
|
if (isLivenessSupported(identity.backend)) {
|
|
169
170
|
return { identity, preProbeAddressConflict: false };
|
|
170
171
|
}
|
|
171
|
-
// Only
|
|
172
|
-
//
|
|
172
|
+
// Only a citizen outside the control-socket domain reaches this record-side lstat;
|
|
173
|
+
// `indeterminate` fails loud (QB2 — never silently "no conflict").
|
|
173
174
|
const inspection = await io.inspectPath(controlSocketPath(gid, controlSocketDir));
|
|
174
175
|
const { socketGids, symlinkedGids } = conflictSetsFor(gid, inspection);
|
|
175
|
-
const preProbeAddressConflict =
|
|
176
|
+
const preProbeAddressConflict = isOutOfSocketDomainGardenIdConflict(identity.backend, gid, socketGids, symlinkedGids);
|
|
176
177
|
return { identity, preProbeAddressConflict };
|
|
177
178
|
};
|
|
178
179
|
const inspectSocket = (gid) => io.inspectSocket(gid, controlSocketDir);
|
|
@@ -32,7 +32,7 @@ import * as fs from "node:fs/promises";
|
|
|
32
32
|
import { getEntwurfExplicitExtensions, mirrorChildStderr, readSessionIdentity } from "./entwurf-core.js";
|
|
33
33
|
import { buildResumePiArgs } from "./entwurf-resume-args.js";
|
|
34
34
|
import { releaseLock } from "./entwurf-v2-lock.js";
|
|
35
|
-
import {
|
|
35
|
+
import { readAddressableMetaIdentity } from "./meta-session.js";
|
|
36
36
|
import { inspectControlSocketPath, mapInspectionToLiveness } from "./socket-discovery.js";
|
|
37
37
|
import { probeSocketLiveness } from "./socket-probe.js";
|
|
38
38
|
/**
|
|
@@ -76,15 +76,23 @@ function requireProductionChild(child) {
|
|
|
76
76
|
* record remembers whose it is — a mismatch means the transcriptPath is stale or
|
|
77
77
|
* foreign, and resuming it would put a turn into a different being's session).
|
|
78
78
|
*
|
|
79
|
+
* That check is per-record, and per-record is not enough: the v2 lock domain is keyed on
|
|
80
|
+
* GARDEN ID, so two records sharing one `nativeSessionId` would each pass their own
|
|
81
|
+
* integrity check and resume the SAME transcript concurrently under two different locks.
|
|
82
|
+
* Hence {@link readAddressableMetaIdentity} rather than the plain targeted read (#52) —
|
|
83
|
+
* a resume is exactly the moment a record stops being data and becomes an address.
|
|
84
|
+
*
|
|
79
85
|
* Everything else is unchanged authority: readSessionIdentity (first model_change) for
|
|
80
86
|
* provider/model/cwd, getEntwurfExplicitExtensions for bridge re-injection (#29 fail-fast).
|
|
81
87
|
* Throws on anything that makes a resume impossible; each throw becomes the watcher's
|
|
82
88
|
* `spawn-start-failed` (no child to watch → release), never a silent no-op.
|
|
83
89
|
*/
|
|
84
90
|
export function resolveResumeLaunchIdentity(plan) {
|
|
85
|
-
const record =
|
|
91
|
+
const record = readAddressableMetaIdentity(plan.sessionId);
|
|
86
92
|
if (record.backend !== "pi") {
|
|
87
|
-
throw new Error(`entwurf-v2-spawn-production: ${plan.sessionId} is a ${record.backend} citizen —
|
|
93
|
+
throw new Error(`entwurf-v2-spawn-production: ${plan.sessionId} is a ${record.backend} citizen — ` +
|
|
94
|
+
`spawn-bg resume is a host-adapter capability, and its domain currently contains backend pi only. ` +
|
|
95
|
+
`This is a relaunch capability, not the control-socket rail and not citizen rank.`);
|
|
88
96
|
}
|
|
89
97
|
const sessionFile = record.transcriptPath;
|
|
90
98
|
if (!sessionFile) {
|
|
@@ -16,8 +16,13 @@
|
|
|
16
16
|
* remains compatibility for an MCP host wrapper; the hook never writes a blind grandparent
|
|
17
17
|
* marker because that may be the long-lived login shell.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Three guards make a marker an IDENTITY rather than a hint, and a candidate is only trusted
|
|
20
|
+
* after ALL of them pass:
|
|
21
|
+
* 0. a PLAUSIBLE owner (isPlausibleOwnerPid, shared with both writers and the generation cut):
|
|
22
|
+
* pid <= 1 cannot own a session, and guard 1 cannot catch it — for as long as the host
|
|
23
|
+
* is up, init IS still the very process the marker named. This is a rule about the
|
|
24
|
+
* marker's CLAIM, not about the owner's state, which is why it is asked first and is
|
|
25
|
+
* not opt-out (#53 A).
|
|
21
26
|
* 1. pid + start-key (readMetaSenderMarker): the owner is still the very process that wrote it,
|
|
22
27
|
* so a dead session's pid, reused by something else, cannot inherit its garden-id.
|
|
23
28
|
* 2. the backing meta-record: the record store is the authority — a marker whose record was
|
|
@@ -30,7 +35,7 @@
|
|
|
30
35
|
* the answer depend on which pid or backend happened to be read first; here lookup order carries
|
|
31
36
|
* no meaning, and two live identities are a refusal rather than a race.
|
|
32
37
|
*/
|
|
33
|
-
import { metaRecordExistsByGardenId, parentPid, readMetaIdentityByGardenId, readMetaSenderMarker, } from "./meta-session.js";
|
|
38
|
+
import { isPlausibleOwnerPid, metaRecordExistsByGardenId, parentPid, readMetaIdentityByGardenId, readMetaSenderMarker, } from "./meta-session.js";
|
|
34
39
|
import { resolveNativePushAdapter } from "./native-push/adapter.js";
|
|
35
40
|
/** Every native backend that mints a garden-id from its own hook and writes a sender marker. */
|
|
36
41
|
export const META_SENDER_BACKENDS = ["claude-code", "antigravity"];
|
|
@@ -120,7 +125,13 @@ export function resolveTrustedMetaSenderIdentity(opts = {}) {
|
|
|
120
125
|
markers.push(marker);
|
|
121
126
|
}
|
|
122
127
|
else {
|
|
123
|
-
|
|
128
|
+
// The candidate filter asks the same question the marker readers ask — CAN this
|
|
129
|
+
// pid own a session — so it uses the same predicate rather than a second `> 0`
|
|
130
|
+
// literal. The default set is only the bridge's parent and grandparent, so init
|
|
131
|
+
// enters it just when the native host itself was reparented (a detached/daemonized
|
|
132
|
+
// Claude): narrow, but reachable, and a candidate that cannot be an owner has no
|
|
133
|
+
// business reaching the read at all (#53 A).
|
|
134
|
+
const ownerPids = (opts.ownerPids ?? [process.ppid, parentPid(process.ppid) ?? 0]).filter(isPlausibleOwnerPid);
|
|
124
135
|
for (const ownerPid of [...new Set(ownerPids)]) {
|
|
125
136
|
for (const backend of META_SENDER_BACKENDS) {
|
|
126
137
|
const marker = readMetaSenderMarker({ backend, ownerPid, sendersDir: opts.sendersDir });
|