@junghanacs/entwurf 0.24.0 → 0.25.1
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/CHANGELOG.md +564 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +24 -1
- package/README.md +2 -2
- package/VERIFY.md +20 -2
- package/demo/README.md +5 -2
- package/docs/acp-backend-rail.md +49 -2
- package/docs/setup-clean-host.md +4 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +13 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +11 -0
- package/mcp/entwurf-bridge/dist/protocol.js +0 -7
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
- package/mcp/entwurf-bridge/src/index.ts +5 -4
- package/package.json +10 -9
- package/pi-extensions/entwurf-control.ts +27 -154
- package/pi-extensions/lib/compaction-send-guard.ts +5 -3
- package/pi-extensions/lib/control-send-receipt.ts +108 -0
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
- package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
- package/pi-extensions/lib/herdr-fresh-call.ts +13 -3
- package/pi-extensions/lib/herdr-placement.ts +11 -0
- package/protocol.js +0 -8
- package/run.sh +202 -33
- package/scripts/check-entwurf-v2-surface.ts +289 -10
- package/scripts/check-gate-qualification.ts +2354 -11
- package/scripts/check-herdr-fresh-call.ts +10 -2
- package/scripts/check-herdr-placement.ts +51 -0
- package/scripts/check-herdr-plugin-build.ts +11 -4
- package/scripts/check-herdr-plugin.ts +745 -38
- package/scripts/check-mux-launcher-fence.ts +37 -0
- package/scripts/check-pi-launch.ts +7 -4
- package/scripts/check-release-gate-outcomes.ts +165 -0
- package/scripts/doctor-pi-provider.ts +44 -4
- package/scripts/herdr-rails.mjs +75 -0
- package/scripts/lib/qualification-acceptance.ts +146 -0
- package/scripts/lib/qualification-artifact.ts +359 -0
- package/scripts/lib/qualification-collection.ts +384 -0
- package/scripts/lib/qualification-gh.ts +231 -0
- package/scripts/lib/qualification-ledger.ts +669 -0
- package/scripts/lib/qualification-oracle-core.ts +279 -0
- package/scripts/lib/qualification-receipt.ts +561 -0
- package/scripts/lib/qualification-release.ts +271 -0
- package/scripts/meta-bridge-state.py +18 -1
- package/scripts/mutants/bridge-command-boot.json +38 -0
- package/scripts/mutants/control-send-receipt.json +235 -0
- package/scripts/mutants/herdr-fresh-call.json +4 -4
- package/scripts/mutants/herdr-plugin-build.json +7 -11
- package/scripts/mutants/herdr-plugin.json +177 -9
- package/scripts/mutants/meta-retire.json +22 -0
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +17 -0
- package/scripts/mutants/qualification-ledger.json +489 -0
- package/scripts/mutants/release-gate.json +48 -0
- package/scripts/mutants/retired-sent-surface.json +32 -0
- package/scripts/mutants/v2-surface.json +7 -7
- package/scripts/qualification-oracle.ts +124 -0
- package/scripts/smoke-codex-fresh-live.ts +94 -13
- package/scripts/smoke-herdr-fresh-call-live.ts +15 -8
- package/scripts/smoke-herdr-raw-install-live.sh +66 -4
- package/scripts/smoke-meta-install-state.sh +107 -1
- package/scripts/smoke-mux-lifecycle-live.ts +9 -2
- package/scripts/smoke-pi-provider-state.sh +106 -0
- package/scripts/smoke-setup-verdict.sh +18 -2
|
@@ -143,11 +143,24 @@ log(` project ${projectPath}: ${projScopeDesc}`);
|
|
|
143
143
|
log(` EFFECTIVE (${effectiveScope}): ${effectiveDesc}`);
|
|
144
144
|
// install-state ownership (user scope). absent state on a configured effective is either a
|
|
145
145
|
// pre-Task-2 install or a user-override we deliberately did not own.
|
|
146
|
+
//
|
|
147
|
+
// `command` is read beside it because it — not the bare name — is the CANONICAL managed command
|
|
148
|
+
// (#116 M3-b2). `register-pi-provider.py` writes the same string into the settings entry and into
|
|
149
|
+
// this record (:332-342, :368-369), and in plugin mode that string is an absolute bridge under the
|
|
150
|
+
// certified stable runtime, because a Herdr-plugin host has nothing entwurf on PATH. Reading it
|
|
151
|
+
// here is what lets this doctor judge drift without re-deriving the stable runtime root: the root
|
|
152
|
+
// is the writer's business, and a second derivation in this file would be a second address axis.
|
|
153
|
+
//
|
|
154
|
+
// `ownership` is the PREIMAGE classification — what stood at the key BEFORE that install — so
|
|
155
|
+
// `absent` means "we created it", not "nobody owns it". Treating it as an ownership verdict is what
|
|
156
|
+
// made every plugin-mode host red while its bridge booted and served all eight verbs.
|
|
146
157
|
let ownership;
|
|
158
|
+
let stateCommand;
|
|
147
159
|
if (existsSync(statePath)) {
|
|
148
160
|
try {
|
|
149
161
|
const st = JSON.parse(readFileSync(statePath, "utf8"));
|
|
150
162
|
ownership = typeof st.ownership === "string" ? st.ownership : undefined;
|
|
163
|
+
stateCommand = typeof st.command === "string" ? st.command : undefined;
|
|
151
164
|
log(` state: install-state present (ownership=${ownership}).`);
|
|
152
165
|
}
|
|
153
166
|
catch {
|
|
@@ -200,9 +213,33 @@ else {
|
|
|
200
213
|
hardFail = 1;
|
|
201
214
|
}
|
|
202
215
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
// Ownership classification never rounds a broken runtime up to green. It only says who may
|
|
217
|
+
// repair the entry after the independent boot verdict above.
|
|
218
|
+
//
|
|
219
|
+
// A MANAGED record is the pair (known preimage ownership, recorded command). For that pair the
|
|
220
|
+
// recorded command is canonical and the comparison runs REGARDLESS of whether the effective
|
|
221
|
+
// command happens to be the bare name. Keeping this decision inside a `!isBare` branch is how
|
|
222
|
+
// real drift escaped: a record naming the absolute plugin bridge, with the settings repointed to
|
|
223
|
+
// the bare `entwurf-bridge`, is drift — and a bare bridge that boots would otherwise carry it to
|
|
224
|
+
// green. "The effective command is spellable as our old default" is not evidence that it is the
|
|
225
|
+
// command this install wrote.
|
|
226
|
+
//
|
|
227
|
+
// Everything else — a record that names no command, or one whose ownership is a word this
|
|
228
|
+
// doctor does not know — falls through to the ORIGINAL logic below, unchanged. Repairing this
|
|
229
|
+
// defect is not a licence to migrate the state schema, to grant an amnesty to records written
|
|
230
|
+
// before `command` existed, or to let an unknown ownership word buy a managed green because a
|
|
231
|
+
// string happened to match. The state is rewritten by `./run.sh setup`, not by its reader.
|
|
232
|
+
const knownManagedOwnership = ownership === "absent" || ownership === "managed-current" || ownership === "managed-legacy";
|
|
233
|
+
if (knownManagedOwnership && stateCommand !== undefined) {
|
|
234
|
+
if (cmd !== stateCommand) {
|
|
235
|
+
log(` FAIL: state records '${stateCommand}' as the managed command (ownership=${ownership}), but the effective command is '${cmd}' — the key drifted to something entwurf never wrote.`);
|
|
236
|
+
hardFail = 1;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
log(` ok: effective command is the managed command this install recorded ('${stateCommand}', preimage ownership=${ownership}).`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (!isBare) {
|
|
206
243
|
if (ownership && ownership !== "user-override") {
|
|
207
244
|
log(` FAIL: state owns entwurf-bridge (ownership=${ownership}) but the effective command drifted to '${cmd}'.`);
|
|
208
245
|
hardFail = 1;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* herdr-rails — the ONE place the herdr rail's backend set and its per-backend model SYNTAX
|
|
3
|
+
* example are written down (#116 A7).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. Three surfaces were about to say the same two things in three
|
|
6
|
+
* dialects: `herdr-fresh-call.ts` owned the closed backend tuple, `build.mjs` narrated one
|
|
7
|
+
* model id per harness at install time, and the status pane was about to print a RAILS block.
|
|
8
|
+
* Three copies of one fact is how a rail quietly grows a fourth backend in prose that no
|
|
9
|
+
* dispatch honours. So the fact lives here and the three read it.
|
|
10
|
+
*
|
|
11
|
+
* WHY `.mjs` AND NOT `.ts`. It is read by two runtimes that cannot agree on anything else:
|
|
12
|
+
* the Herdr plugin runs `node lib/status.mjs` with no loader flags at all, and the pi
|
|
13
|
+
* extension is typechecked by `tsc`. A `.ts` leaf would be unloadable by the first; a second
|
|
14
|
+
* copy would defeat the point. `protocol.js` set this precedent for exactly the same reason
|
|
15
|
+
* (root tsconfig.json `allowJs`), and this file keeps its shape: dependency-free, no imports,
|
|
16
|
+
* no environment, no IO, so importing it can never be a side effect.
|
|
17
|
+
*
|
|
18
|
+
* WHAT THIS FILE IS NOT, and each absence is load-bearing:
|
|
19
|
+
* - NOT a model catalogue. One example per backend, chosen to show the SHAPE of the string
|
|
20
|
+
* (`provider/model` vs a bare vendor id). Adding a second example starts a list, a list
|
|
21
|
+
* invites completeness, and completeness is a catalogue Entwurf would then be expected to
|
|
22
|
+
* keep current for every vendor. `entwurf_fresh_call` takes a model string with no default
|
|
23
|
+
* and validates nothing; that is the contract, and an example must not imply otherwise.
|
|
24
|
+
* - NOT a supported-model set, a recommendation, or a validator. Nothing reads MODEL_SYNTAX_EXAMPLE
|
|
25
|
+
* to decide anything — it is display text on three surfaces.
|
|
26
|
+
* - NOT a provider, billing or quota statement. No rail here names a provider, a subscription
|
|
27
|
+
* or a cost. `#76` is about which rail a sibling may be launched on, and a syntax example
|
|
28
|
+
* that mentioned a provider would read as an answer to it. It does not answer it.
|
|
29
|
+
* - NOT a launcher, and not a claim that any binary exists. `HERDR_AGENT_KIND` (the herdr
|
|
30
|
+
* `--kind` token) stays with the rail that spends it; this leaf carries no argv.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The closed pilot set for the herdr launch rail. Herdr's own `--kind` enum is much larger and
|
|
34
|
+
* that is not evidence of support: every other backend is a named pre-mutation reject.
|
|
35
|
+
*
|
|
36
|
+
* The JSDoc cast is what keeps the TYPE half of this single source honest. Without it TypeScript
|
|
37
|
+
* infers `readonly string[]` from a `.mjs` file and `HerdrFreshCallBackend` silently widens to
|
|
38
|
+
* `string` — the tuple would still be one source at runtime while the compiler had stopped
|
|
39
|
+
* checking anything. A gate pins both halves.
|
|
40
|
+
*
|
|
41
|
+
* @type {readonly ["pi", "claude-code"]}
|
|
42
|
+
*/
|
|
43
|
+
export const HERDR_FRESH_CALL_BACKENDS = Object.freeze(["pi", "claude-code"]);
|
|
44
|
+
/**
|
|
45
|
+
* ONE model string per backend, and it is an example of SYNTAX, never of choice.
|
|
46
|
+
*
|
|
47
|
+
* `[관측: GLG, 날것 PC, 2026-09-17]` "일단 정확한 모델명을 모른다" — after a green install the first
|
|
48
|
+
* thing missing is a call that works, and the two runtimes spell a model differently enough that
|
|
49
|
+
* guessing fails: pi wants a provider-qualified path, Claude Code wants the vendor id alone.
|
|
50
|
+
* That difference is the whole content of this map.
|
|
51
|
+
*
|
|
52
|
+
* @type {Readonly<Record<"pi" | "claude-code", string>>}
|
|
53
|
+
*/
|
|
54
|
+
export const MODEL_SYNTAX_EXAMPLE = Object.freeze({
|
|
55
|
+
pi: "openai-codex/gpt-5.6-terra",
|
|
56
|
+
"claude-code": "claude-sonnet-5",
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* How a citizen on each backend is REACHED once it exists, in the operator's words. This is the
|
|
60
|
+
* rail fact the status pane's RAILS block renders and the install narration states — the same
|
|
61
|
+
* sentence, so a host cannot be told two different things about one backend.
|
|
62
|
+
*
|
|
63
|
+
* `[관측: GLG, 날것 PC, 2026-09-17]` from the operator's chair a plain `pi` looks identical to a
|
|
64
|
+
* citizen pi: the install says green, `pi` starts, and nothing is there. Citizenship is argv-gated
|
|
65
|
+
* on purpose, and that gate is invisible unless something says it out loud.
|
|
66
|
+
*
|
|
67
|
+
* @type {Readonly<Record<"pi" | "claude-code", string>>}
|
|
68
|
+
*/
|
|
69
|
+
export const RAIL_ENTRY_NOTE = Object.freeze({
|
|
70
|
+
pi: "start it as `pi --entwurf-control` to be a garden citizen — a plain `pi` loads the extension but has no garden id, no control socket and no entwurf tools",
|
|
71
|
+
"claude-code": "nothing to add — an ordinary `claude` picks up the entwurf tools through MCP",
|
|
72
|
+
});
|
|
@@ -431,7 +431,9 @@ server.tool(
|
|
|
431
431
|
"alone does not say which rail that citizen answers on. Give target + intent; the decider picks transport " +
|
|
432
432
|
"from liveness (live socket citizen → control-socket send; deliverable self-fetch citizen → meta-bridge " +
|
|
433
433
|
"mailbox; probe-alive native-push citizen → direct injection into its conversation) and reports ONE " +
|
|
434
|
-
"outcome
|
|
434
|
+
"outcome, named per rail: a control receiver's acceptance boundary (`sent` / `queued-steer` / " +
|
|
435
|
+
"`queued-follow-up` / `accepted-unknown-boundary` — those four only), a mailbox ENQUEUE receipt, a " +
|
|
436
|
+
"native-push INJECTION, a rejection, or lock-dirty. EXISTING targets only; discover with " +
|
|
435
437
|
"entwurf_peers. A peer entwurf_peers shows as liveness=alive → fire-and-forget. A " +
|
|
436
438
|
"citizen with NO socket liveness (liveness=unsupported) is ALSO fire-and-forget — unsupported means only " +
|
|
437
439
|
'"no control-socket probe." The decider resolves the actual rail at dispatch time: a self-fetch ' +
|
|
@@ -472,9 +474,8 @@ server.tool(
|
|
|
472
474
|
.enum(["steer", "follow_up"])
|
|
473
475
|
.optional()
|
|
474
476
|
.describe(
|
|
475
|
-
"Injection style for a CONTROL-SOCKET send only: steer (
|
|
476
|
-
"
|
|
477
|
-
"effect on those rails.",
|
|
477
|
+
"Injection style for a CONTROL-SOCKET send only: steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. " +
|
|
478
|
+
"The mailbox and native-push plans carry no mode, so it has no effect on those rails.",
|
|
478
479
|
),
|
|
479
480
|
wants_reply: z.boolean().optional().describe("Human-conversation reply hint (default false)"),
|
|
480
481
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junghanacs/entwurf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "Garden-citizen dispatch substrate and meta-bridge for pi, Claude Code, Copilot CLI, OMP, Antigravity, and Codex CLI; Claude and Snowflake Cortex Code ACP backends.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -92,16 +92,17 @@
|
|
|
92
92
|
"zod": "^3.25.0 || ^4.0.0"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@earendil-works/pi-ai": ">=0.
|
|
96
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
97
|
-
"@earendil-works/pi-tui": ">=0.
|
|
95
|
+
"@earendil-works/pi-ai": ">=0.87.1 <0.88",
|
|
96
|
+
"@earendil-works/pi-coding-agent": ">=0.87.1 <0.88",
|
|
97
|
+
"@earendil-works/pi-tui": ">=0.87.1 <0.88",
|
|
98
98
|
"typebox": "*"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
101
|
"@biomejs/biome": "^2.4.13",
|
|
102
|
-
"@earendil-works/pi-
|
|
103
|
-
"@earendil-works/pi-
|
|
104
|
-
"@earendil-works/pi-
|
|
102
|
+
"@earendil-works/pi-agent-core": "0.87.1",
|
|
103
|
+
"@earendil-works/pi-ai": "0.87.1",
|
|
104
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
105
|
+
"@earendil-works/pi-tui": "0.87.1",
|
|
105
106
|
"@types/node": "^24.3.0",
|
|
106
107
|
"husky": "^9.1.7",
|
|
107
108
|
"rregex": "1.13.1",
|
|
@@ -175,8 +176,8 @@
|
|
|
175
176
|
"check:full": "bash scripts/check-elapsed.sh check:full check:core check:hermetic check:package",
|
|
176
177
|
"check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
|
|
177
178
|
"check:toolchain": "pnpm lint && pnpm typecheck",
|
|
178
|
-
"check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-tests-beside-behavior",
|
|
179
|
-
"check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push",
|
|
179
|
+
"check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-pi-queue-oracle && ./run.sh check-tests-beside-behavior",
|
|
180
|
+
"check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push",
|
|
180
181
|
"check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
|
|
181
182
|
"check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
|
|
182
183
|
},
|
|
@@ -78,9 +78,8 @@ import type {
|
|
|
78
78
|
ExtensionContext,
|
|
79
79
|
MessageRenderer,
|
|
80
80
|
} from "@earendil-works/pi-coding-agent";
|
|
81
|
-
import { getMarkdownTheme
|
|
82
|
-
import { Box,
|
|
83
|
-
import { ENTWURF_SENT_MESSAGE_TYPE } from "../protocol.js";
|
|
81
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
82
|
+
import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
84
83
|
import { readCallbackEnv } from "./lib/callback-env.js";
|
|
85
84
|
import {
|
|
86
85
|
type CompactionGuard,
|
|
@@ -89,6 +88,7 @@ import {
|
|
|
89
88
|
noteCompactionBefore,
|
|
90
89
|
noteCompactionTerminal,
|
|
91
90
|
} from "./lib/compaction-send-guard.js";
|
|
91
|
+
import { sendReceiptBoundary } from "./lib/control-send-receipt.js";
|
|
92
92
|
import { CONTROL_SOCKET_SUFFIX, controlSocketPathIn, defaultControlSocketDir } from "./lib/control-socket-path.js";
|
|
93
93
|
import {
|
|
94
94
|
attachAcceptedSocketDisconnectPolicy,
|
|
@@ -112,15 +112,6 @@ const EMACS_AGENT_SOCKET_FLAG = "emacs-agent-socket";
|
|
|
112
112
|
// comes from the `.js` leaf both runtime lanes can import.
|
|
113
113
|
const ENTWURF_DIR = defaultControlSocketDir(os.homedir());
|
|
114
114
|
const SESSION_MESSAGE_TYPE = "entwurf-message";
|
|
115
|
-
// Sender-side UI marker. Layer B (ACP path) emits a CustomMessage with this
|
|
116
|
-
// customType so the operator sees a first-class [entwurf sent →] box paired
|
|
117
|
-
// with the receive-side [entwurf received ⟵] box. The provider-level context
|
|
118
|
-
// filter in index.ts drops this customType before the LLM sees it — colocated
|
|
119
|
-
// with the emitter so sessions without --entwurf-control are still protected.
|
|
120
|
-
// There is no second emitter: the v1 native send path ("Layer A") that also drew
|
|
121
|
-
// this box through renderSentMessage() was removed in the 0.12 cutover, and
|
|
122
|
-
// renderSentMessage is now registered for exactly one customType (see
|
|
123
|
-
// registerMessageRenderer below). Do not describe a native sender-side box.
|
|
124
115
|
const SENDER_INFO_PATTERN = /<sender_info>[\s\S]*?<\/sender_info>/g;
|
|
125
116
|
|
|
126
117
|
// ============================================================================
|
|
@@ -478,10 +469,12 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
|
|
|
478
469
|
//
|
|
479
470
|
// The label is "[entwurf received ⟵]" with a left-pointing arrow so the
|
|
480
471
|
// receiving operator immediately sees the directionality (this is an
|
|
481
|
-
// incoming message).
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
472
|
+
// incoming message). There is no matching box on the sending side: the one
|
|
473
|
+
// that used to be described here had zero producers and was removed (#120 P3).
|
|
474
|
+
// A sender sees a TEXT receipt naming the acceptance boundary
|
|
475
|
+
// (`entwurf-v2-surface.ts`), which is the honest thing to show — a box would
|
|
476
|
+
// claim the message was rendered into someone's transcript, and only the
|
|
477
|
+
// receiver can say that.
|
|
485
478
|
//
|
|
486
479
|
// wants_reply defaults to false (etiquette marker, not protocol contract).
|
|
487
480
|
// We show the badge only when the sender explicitly set it true; an
|
|
@@ -523,124 +516,6 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
|
|
|
523
516
|
return box;
|
|
524
517
|
};
|
|
525
518
|
|
|
526
|
-
// Sender-side payload — what renderSentMessage needs to draw the [entwurf sent →]
|
|
527
|
-
// box. Carried by the ACP path's CustomMessage `details` — the ONLY carrier since
|
|
528
|
-
// the v1 native renderResult path was removed (0.12 cutover). All four envelope fields are intentionally
|
|
529
|
-
// echoed in the box even though the sender is "this same session" — operators
|
|
530
|
-
// reading a busy multi-session transcript should be able to verify at a glance
|
|
531
|
-
// which 담당자 is on the wire (cwd) and which model identity (agentId) actually
|
|
532
|
-
// signed the message, without scrolling up to find the session header.
|
|
533
|
-
//
|
|
534
|
-
// timestamp is captured at execute() / send-emit time, not at render time, so
|
|
535
|
-
// re-renders (resize, expand toggle) keep showing the moment the message was
|
|
536
|
-
// actually delivered rather than drifting forward to "now".
|
|
537
|
-
//
|
|
538
|
-
// wants_reply mirrors the receive-side etiquette badge. It stays optional because
|
|
539
|
-
// the box is drawn from a CustomMessage whose `details` may predate the field; the
|
|
540
|
-
// v1 `registerSessionTool` / `entwurfSendParameters` schema this note used to point
|
|
541
|
-
// at is gone (0.12 cutover), so there is no native call site left to grow.
|
|
542
|
-
interface SentBoxData {
|
|
543
|
-
to: string; // target sessionId
|
|
544
|
-
from?: string; // sender agentId, e.g. "entwurf/claude-opus-5"
|
|
545
|
-
cwd?: string; // sender cwd (raw, abbreviateHome applied at render)
|
|
546
|
-
timestamp?: string; // ISO 8601 UTC; rendered in KST
|
|
547
|
-
mode?: string; // "steer" | "follow_up" | string passed through
|
|
548
|
-
wants_reply?: boolean;
|
|
549
|
-
deliveredAs?: string; // RPC echo — surfaces when receiver remapped (e.g. queued as followUp)
|
|
550
|
-
body: string; // message text the operator sent
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Visual mirror of renderSessionMessage. Same Box / Markdown / theme tokens —
|
|
554
|
-
// the two boxes must share the customMessageBg / customMessageLabel /
|
|
555
|
-
// customMessageText surface so they are pixel-equivalent in any theme. The
|
|
556
|
-
// only deliberate visual differences are:
|
|
557
|
-
// - label: [entwurf sent →] vs [entwurf received ⟵]
|
|
558
|
-
// - "to:" leads, "from:" follows vs "from:" only
|
|
559
|
-
// - mode: line (no equivalent on receive side — receiver
|
|
560
|
-
// doesn't see how the sender queued it)
|
|
561
|
-
//
|
|
562
|
-
// `expanded` truncates the body the same way as renderSessionMessage so a
|
|
563
|
-
// large send shows the same preview shape as a large receive. operators
|
|
564
|
-
// reading the transcript should not need different mental models.
|
|
565
|
-
// Return type is `Component`, the interface `MessageRenderer` actually asks for
|
|
566
|
-
// (`Component | undefined`, read at pi-coding-agent
|
|
567
|
-
// `dist/core/extensions/types.d.ts:889`) — NOT the concrete `Container`.
|
|
568
|
-
// `[측정 2026-09-06]` pi 0.85.0's mouse work gave `Container` a `private mouseLayout?`
|
|
569
|
-
// (`pi-tui dist/tui.d.ts:198`; 0.84.4's `Container` had no private member at all), and
|
|
570
|
-
// `Box` declares a SEPARATE private `mouseLayout` of its own. TypeScript only accepts a
|
|
571
|
-
// private member from the same declaration, so the structural assignment `Box -> Container`
|
|
572
|
-
// that held through 0.84.4 became TS2322 at 0.85.x. This was an UNDECLARED break — the
|
|
573
|
-
// upstream Breaking section names only `createGatewayBindingFetch`. Annotating the shared
|
|
574
|
-
// interface is the honest fix: nothing here ever needed Container's own surface.
|
|
575
|
-
const buildSentMessageBox = (data: SentBoxData, expanded: boolean, theme: Theme): Component => {
|
|
576
|
-
let body = data.body || "(no content)";
|
|
577
|
-
if (!expanded) {
|
|
578
|
-
const lines = body.split("\n");
|
|
579
|
-
if (lines.length > 5) {
|
|
580
|
-
body = `${lines.slice(0, 5).join("\n")}\n...`;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
585
|
-
const labelBase = theme.fg("customMessageLabel", `\x1b[1m[entwurf sent →]\x1b[22m`);
|
|
586
|
-
|
|
587
|
-
// Header line: label + KST + optional (wants reply) badge — matches the
|
|
588
|
-
// receive-side header layout 1:1.
|
|
589
|
-
const kst = formatTimestampKst(data.timestamp) ?? "(unknown time)";
|
|
590
|
-
const replyBadge = data.wants_reply === true ? " (wants reply)" : "";
|
|
591
|
-
const headerLine = `${labelBase} ${theme.fg("dim", `${kst}${replyBadge}`)}`;
|
|
592
|
-
box.addChild(new Text(headerLine, 0, 0));
|
|
593
|
-
|
|
594
|
-
// to: <sessionId> — target peer
|
|
595
|
-
box.addChild(new Text(theme.fg("dim", `to: ${data.to || "(unknown sessionId)"}`), 0, 0));
|
|
596
|
-
|
|
597
|
-
// from: <agentId> @ <cwd> — self identity. Shown even though it's "us"
|
|
598
|
-
// because in a multi-session human-greeted topology the operator is
|
|
599
|
-
// switching between several pi sessions and needs to confirm which one
|
|
600
|
-
// signed this send.
|
|
601
|
-
const fromAgent = data.from ?? "(unknown agent)";
|
|
602
|
-
const fromCwd = data.cwd ? abbreviateHome(data.cwd) : "(unknown cwd)";
|
|
603
|
-
box.addChild(new Text(theme.fg("dim", `from: ${fromAgent} @ ${fromCwd}`), 0, 0));
|
|
604
|
-
|
|
605
|
-
// mode: <mode>[ → deliveredAs] — show RPC remap when it differs from
|
|
606
|
-
// what the caller asked for (e.g. caller said "steer" but receiver was
|
|
607
|
-
// idle so it became a direct prompt). Silent when they agree.
|
|
608
|
-
if (data.mode) {
|
|
609
|
-
const remap = data.deliveredAs && data.deliveredAs !== data.mode ? theme.fg("muted", ` → ${data.deliveredAs}`) : "";
|
|
610
|
-
box.addChild(new Text(theme.fg("dim", `mode: ${data.mode}${remap}`), 0, 0));
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
box.addChild(new Spacer(1));
|
|
614
|
-
box.addChild(
|
|
615
|
-
new Markdown(body, 0, 0, getMarkdownTheme(), {
|
|
616
|
-
color: (value: string) => theme.fg("customMessageText", value),
|
|
617
|
-
}),
|
|
618
|
-
);
|
|
619
|
-
return box;
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
// CustomMessageRenderer adapter for Layer B (ACP path). The CustomMessage
|
|
623
|
-
// carries the SentBoxData under `details` (set by index.ts streamShellAcp
|
|
624
|
-
// when a completed mcp__entwurf-bridge__entwurf_v2 is observed). `content`
|
|
625
|
-
// holds the raw message body too, but we prefer details.body because the
|
|
626
|
-
// content channel may have been routed through string-only persistence and
|
|
627
|
-
// trimmed.
|
|
628
|
-
const renderSentMessage: MessageRenderer = (message, { expanded }, theme) => {
|
|
629
|
-
const details = (message.details ?? {}) as Partial<SentBoxData>;
|
|
630
|
-
const fallbackBody = extractTextContent(message.content);
|
|
631
|
-
const data: SentBoxData = {
|
|
632
|
-
to: details.to ?? "(unknown sessionId)",
|
|
633
|
-
from: details.from,
|
|
634
|
-
cwd: details.cwd,
|
|
635
|
-
timestamp: details.timestamp,
|
|
636
|
-
mode: details.mode,
|
|
637
|
-
wants_reply: details.wants_reply,
|
|
638
|
-
deliveredAs: details.deliveredAs,
|
|
639
|
-
body: details.body ?? fallbackBody,
|
|
640
|
-
};
|
|
641
|
-
return buildSentMessageBox(data, expanded, theme);
|
|
642
|
-
};
|
|
643
|
-
|
|
644
519
|
// ============================================================================
|
|
645
520
|
// Command Handlers
|
|
646
521
|
// ============================================================================
|
|
@@ -824,9 +699,17 @@ async function handleCommand(
|
|
|
824
699
|
return;
|
|
825
700
|
}
|
|
826
701
|
|
|
702
|
+
// #120 P2: the receiver reports the BOUNDARY it actually observed — `sent` for an idle
|
|
703
|
+
// direct trigger, `queued-steer` / `queued-follow-up` for the two in-process queues — and
|
|
704
|
+
// nothing else. The bare `delivered:true` that used to lead this payload is GONE rather
|
|
705
|
+
// than kept for compatibility: it was equally true of all three states and therefore said
|
|
706
|
+
// nothing, which is the bare `delivered` DELIVERY.md refuses by name. The `deliveredAs`
|
|
707
|
+
// beside it had no consumer at all. Skew is safe in both directions without a translator —
|
|
708
|
+
// an old sender keys on `response.success` and never reads `data`, and a new sender reads
|
|
709
|
+
// an old receiver's payload as `accepted-unknown-boundary`, the honest name for an
|
|
710
|
+
// acceptance it cannot classify.
|
|
827
711
|
respond(true, "send", {
|
|
828
|
-
|
|
829
|
-
deliveredAs: isIdle ? "direct" : mode === "follow_up" ? "followUp" : "steer",
|
|
712
|
+
boundary: sendReceiptBoundary({ idle: isIdle, mode }),
|
|
830
713
|
wants_reply: wantsReply,
|
|
831
714
|
});
|
|
832
715
|
return;
|
|
@@ -1104,17 +987,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1104
987
|
};
|
|
1105
988
|
|
|
1106
989
|
pi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);
|
|
1107
|
-
// Layer B (ACP path) sender-side UI box. Registered unconditionally because
|
|
1108
|
-
// renderer registration happens at extension load, BEFORE we know whether
|
|
1109
|
-
// this session ends up with a routable identity: `updateSessionEnv` only
|
|
1110
|
-
// publishes PI_SESSION_ID once a garden id exists, and clears it otherwise.
|
|
1111
|
-
// A session without `--entwurf-control` therefore does NOT send — a bundled
|
|
1112
|
-
// `entwurf_v2` sender call there fails loud on the missing envelope (the
|
|
1113
|
-
// documented provider-vs-citizen boundary), and this renderer simply never
|
|
1114
|
-
// fires. Registering it up front keeps one registration path instead of a
|
|
1115
|
-
// conditional seam, and guarantees the [entwurf sent →] box exists for every
|
|
1116
|
-
// session that DOES send.
|
|
1117
|
-
pi.registerMessageRenderer(ENTWURF_SENT_MESSAGE_TYPE, renderSentMessage);
|
|
1118
990
|
|
|
1119
991
|
if (shouldRegisterControlTools(pi)) {
|
|
1120
992
|
registerListSessionsTool(pi);
|
|
@@ -1387,7 +1259,7 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1387
1259
|
mode: Type.Optional(
|
|
1388
1260
|
StringEnum(["steer", "follow_up"] as const, {
|
|
1389
1261
|
description:
|
|
1390
|
-
"Injection style for a CONTROL-SOCKET send only: steer (
|
|
1262
|
+
"Injection style for a CONTROL-SOCKET send only: steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. " +
|
|
1391
1263
|
"The mailbox and native-push plans carry no mode, so it has no effect on those rails.",
|
|
1392
1264
|
}),
|
|
1393
1265
|
),
|
|
@@ -1415,9 +1287,11 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1415
1287
|
description: `CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id alone
|
|
1416
1288
|
does not say which rail that citizen answers on. Give target + intent; the decider picks transport from
|
|
1417
1289
|
liveness (live socket citizen → control-socket send; deliverable self-fetch citizen → meta-bridge mailbox;
|
|
1418
|
-
probe-alive native-push citizen → direct injection into its conversation) and reports ONE outcome
|
|
1419
|
-
(
|
|
1420
|
-
|
|
1290
|
+
probe-alive native-push citizen → direct injection into its conversation) and reports ONE outcome, per rail: a control
|
|
1291
|
+
receiver's acceptance boundary ("sent" / "queued-steer" / "queued-follow-up" /
|
|
1292
|
+
"accepted-unknown-boundary" — those four only), a mailbox ENQUEUE receipt, a native-push INJECTION,
|
|
1293
|
+
a rejection, or lock-dirty. EXISTING targets only (see entwurf_peers).
|
|
1294
|
+
A peer shown as liveness=alive → fire-and-forget. A citizen with NO socket liveness
|
|
1421
1295
|
(liveness=unsupported) is ALSO fire-and-forget — unsupported means only "no control-socket probe" — and the
|
|
1422
1296
|
decider picks its own rail: a self-fetch backend (e.g. Claude Code) gets the mailbox, a native-push backend
|
|
1423
1297
|
(e.g. Antigravity) gets direct injection and has NO mailbox at all. THERE IS A THIRD RESULT: the mailbox
|
|
@@ -1426,9 +1300,8 @@ rather than queued for an inbox nobody drains. The native-push probe is 3-valued
|
|
|
1426
1300
|
alive → injected; dead → native-push-target-dead; indeterminate → native-push-probe-indeterminate
|
|
1427
1301
|
(unestablished ≠ gone). DORMANT IS UNREACHABLE: a socket-domain citizen that is not running gets
|
|
1428
1302
|
dormant-fire-forget-unsupported — same receiver rule as the mailbox, no active drainer means no
|
|
1429
|
-
delivery. The intent that used to answer there, owned-outcome,
|
|
1430
|
-
|
|
1431
|
-
yourself and dispatch again. LOCK: taken for a control-socket-DOMAIN dispatch. The mailbox and native-push
|
|
1303
|
+
delivery. The intent that used to answer there, owned-outcome, was withdrawn
|
|
1304
|
+
under the visible-first rule — re-open the session yourself and dispatch again. LOCK: taken for a control-socket-DOMAIN dispatch. The mailbox and native-push
|
|
1432
1305
|
rails are lock-free — deliverability and the adapter probe guard them. mode applies to a CONTROL-SOCKET
|
|
1433
1306
|
send only; other plans carry no mode. wants_reply rides every rail. message caps at 16000 chars; send an
|
|
1434
1307
|
artifact path + digest for more.`,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* compaction-send-guard — whether a control-socket `send` may call `pi.sendMessage`.
|
|
3
3
|
*
|
|
4
|
-
* Pi
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
4
|
+
* Pi has no public `ExtensionContext.isCompacting()`. `[측정 2026-09-20, pi 0.86.0]` and
|
|
5
|
+
* re-measured `[측정 2026-09-22, pi 0.87.0]` and `[측정 2026-09-23, pi 0.87.1]`: the installed
|
|
6
|
+
* `dist/core/extensions/types.d.ts` declares `isIdle(): boolean` at :233 and `signal` at :237 —
|
|
7
|
+
* the SAME coordinates at all three floors — and carries no `isCompacting` member at all (grep
|
|
8
|
+
* count 0, re-counted on 0.87.1); the identifier exists
|
|
7
9
|
* only inside the interactive/rpc modes' own bundles). Compaction is non-idle
|
|
8
10
|
* (`AgentSession.isIdle` = no agent run AND not compacting) but `sendCustomMessage`
|
|
9
11
|
* does not refuse it. A non-streaming `triggerTurn:true` send starts `_runAgentPrompt`
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* control-send-receipt — the ONE place the control-socket's acceptance boundary is named,
|
|
3
|
+
* on both ends of the wire (#120 P2).
|
|
4
|
+
*
|
|
5
|
+
* WHY A BOUNDARY AXIS AT ALL, AND WHY IT IS NOT THE ROUTE AXIS.
|
|
6
|
+
* `SendFinalOutcome` (`entwurf-v2-send.ts`) answers WHICH LEG delivered — first socket,
|
|
7
|
+
* re-resolved socket, mailbox — and it is what the release reducer keys on. It is an INTERNAL
|
|
8
|
+
* route/release algebra and it is NOT the sender-visible delivery boundary: `sent` and
|
|
9
|
+
* `fallback-sent` say a receiver accepted the bytes, and nothing whatever about what the receiver
|
|
10
|
+
* then did with them. Answering "what happened to my message" with a route word is how
|
|
11
|
+
* `control-socket → sent` came to mean four different things, one of which was
|
|
12
|
+
* "queued behind ten steers and dropped when the operator hit ESC".
|
|
13
|
+
*
|
|
14
|
+
* `[측정 2026-09-22, pi-agent-core 0.87.0; 재측정 2026-09-23, 0.87.1 — 좌표 동일]` the receiver's two queues are independent and each is
|
|
15
|
+
* `one-at-a-time` (`dist/agent.js:60-71,96-97,137-138`); steering is drained after every turn
|
|
16
|
+
* (`dist/agent-loop.js:85,186`) while follow-ups are drained ONLY when the inner loop has ended
|
|
17
|
+
* (`:191-197`). There is no global FIFO across them. So `steer` and `follow_up` are not two
|
|
18
|
+
* spellings of one queue — they are two different promises about when, and a receipt that cannot
|
|
19
|
+
* tell them apart cannot be used to reason about either.
|
|
20
|
+
*
|
|
21
|
+
* WHAT EACH BOUNDARY CLAIMS, EXACTLY:
|
|
22
|
+
* sent the receiver was idle and the message was accepted as a DIRECT
|
|
23
|
+
* turn trigger. It is an accepted REQUEST, not proof that the model
|
|
24
|
+
* saw it and not proof the turn completed.
|
|
25
|
+
* queued-steer accepted into the receiver's in-process steering queue. Volatile
|
|
26
|
+
* process memory: no durability, no ordering against the other
|
|
27
|
+
* queue, and an abort clears it.
|
|
28
|
+
* queued-follow-up accepted into the receiver's in-process follow-up queue. Same
|
|
29
|
+
* volatility, and it waits for the inner loop to end — a steer that
|
|
30
|
+
* arrives later still goes first.
|
|
31
|
+
* accepted-unknown-boundary the receiver ACCEPTED (its RPC answered success) but did not name
|
|
32
|
+
* a boundary this build understands. Version skew, not failure: the
|
|
33
|
+
* message is gone from our hands either way, so this must never be
|
|
34
|
+
* rendered as a failure to retry — and never as `sent`, which would
|
|
35
|
+
* put back the exact false claim this axis exists to remove.
|
|
36
|
+
*
|
|
37
|
+
* NO LEGACY MAPPER LIVES HERE. The previous wire carried `delivered:true` plus a `deliveredAs`
|
|
38
|
+
* producer nothing consumed; both are gone rather than translated. A new receiver answers an old
|
|
39
|
+
* sender fine (it keys on `response.success` and ignores `data`), and an old receiver answers a
|
|
40
|
+
* new sender as `accepted-unknown-boundary` — which is the honest name for it. A dual route would
|
|
41
|
+
* be a second address for one fact.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** The closed boundary set, in the order a reader should learn them. */
|
|
45
|
+
export const ACCEPTANCE_BOUNDARIES = ["sent", "queued-steer", "queued-follow-up", "accepted-unknown-boundary"] as const;
|
|
46
|
+
|
|
47
|
+
export type AcceptanceBoundary = (typeof ACCEPTANCE_BOUNDARIES)[number];
|
|
48
|
+
|
|
49
|
+
/** What a receiver can actually observe about itself. `accepted-unknown-boundary` is a READER's
|
|
50
|
+
* verdict about an answer it could not classify, so a receiver may never produce it. */
|
|
51
|
+
export type ObservedBoundary = Exclude<AcceptanceBoundary, "accepted-unknown-boundary">;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* RECEIVER SIDE. The whole classification, from the two facts the receiver already has at the
|
|
55
|
+
* moment it accepts: was it idle, and which queue did the caller ask for. Pure — it reads no
|
|
56
|
+
* clock, no socket and no session, so the same inputs always name the same boundary.
|
|
57
|
+
*
|
|
58
|
+
* `idle` wins over `mode`: an idle receiver takes the message as a direct trigger and neither
|
|
59
|
+
* queue is involved, which is why `mode` is meaningless there (`entwurf-control.ts` passes no
|
|
60
|
+
* `deliverAs` on that branch).
|
|
61
|
+
*/
|
|
62
|
+
export function sendReceiptBoundary(input: { idle: boolean; mode: "steer" | "follow_up" }): ObservedBoundary {
|
|
63
|
+
if (input.idle) return "sent";
|
|
64
|
+
return input.mode === "follow_up" ? "queued-follow-up" : "queued-steer";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Is this an acceptance a receiver could have observed about itself? */
|
|
68
|
+
export function isObservedBoundary(value: unknown): value is ObservedBoundary {
|
|
69
|
+
return value === "sent" || value === "queued-steer" || value === "queued-follow-up";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* SENDER SIDE. Read the receiver's typed boundary off an RPC `data` payload.
|
|
74
|
+
*
|
|
75
|
+
* Anything this build does not recognise — a missing field, an older receiver's shape, a value
|
|
76
|
+
* outside the set — is `accepted-unknown-boundary` and NOT an error: by the time `data` exists the
|
|
77
|
+
* RPC has already answered success, so the message has been accepted. Inventing a failure here
|
|
78
|
+
* would invite a retry that double-delivers, and answering `sent` would claim a direct injection
|
|
79
|
+
* nobody reported.
|
|
80
|
+
*/
|
|
81
|
+
export function parseAcceptanceBoundary(data: unknown): AcceptanceBoundary {
|
|
82
|
+
if (typeof data !== "object" || data === null) return "accepted-unknown-boundary";
|
|
83
|
+
const value = (data as { boundary?: unknown }).boundary;
|
|
84
|
+
return isObservedBoundary(value) ? value : "accepted-unknown-boundary";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** How much of an unrecognised answer may be repeated back. A receiver's payload is untrusted
|
|
88
|
+
* input from another process; a full object on a status line is an injection surface and an
|
|
89
|
+
* unbounded one at that. */
|
|
90
|
+
const UNKNOWN_DIAGNOSTIC_MAX = 80;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A BOUNDED, single-line description of an answer we could not classify — for the operator who has
|
|
94
|
+
* to decide whether this is version skew or a broken peer. Returns `undefined` when there is
|
|
95
|
+
* nothing worth saying, so a caller renders the bare boundary rather than an empty parenthesis.
|
|
96
|
+
*
|
|
97
|
+
* It describes the SHAPE and never trusts the content: the raw value is stringified, stripped of
|
|
98
|
+
* newlines and truncated. Never the whole object.
|
|
99
|
+
*/
|
|
100
|
+
export function describeUnknownBoundary(data: unknown): string | undefined {
|
|
101
|
+
if (typeof data !== "object" || data === null) return data === undefined ? undefined : `data: ${typeof data}`;
|
|
102
|
+
const value = (data as { boundary?: unknown }).boundary;
|
|
103
|
+
if (value === undefined) return "no boundary field";
|
|
104
|
+
const raw = typeof value === "string" ? value : typeof value;
|
|
105
|
+
const flat = raw.replace(/\s+/g, " ").trim();
|
|
106
|
+
if (flat === "") return "empty boundary";
|
|
107
|
+
return flat.length > UNKNOWN_DIAGNOSTIC_MAX ? `${flat.slice(0, UNKNOWN_DIAGNOSTIC_MAX)}…` : flat;
|
|
108
|
+
}
|
|
@@ -51,7 +51,7 @@ export const EntwurfV2InputSchema = Type.Object(
|
|
|
51
51
|
mode: Type.Optional(
|
|
52
52
|
StringEnum(ENTWURF_V2_MODES, {
|
|
53
53
|
description:
|
|
54
|
-
"delivery mode (steer
|
|
54
|
+
"delivery mode — NOT the ownership axis (F1) nor liveness routing. steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. MEANINGLESS on the meta-mailbox transport (F-mailbox): a mailbox ack is enqueue+doorbell, not a turn injection, so steer/follow_up does not apply when the verdict transport is meta-mailbox.",
|
|
55
55
|
}),
|
|
56
56
|
),
|
|
57
57
|
wantsReply: Type.Optional(
|
|
@@ -265,8 +265,12 @@ export type EntwurfV2Transport = (typeof ENTWURF_V2_TRANSPORTS)[number];
|
|
|
265
265
|
// and a future visible-resume verb would speak them again.
|
|
266
266
|
export const ENTWURF_V2_ACTIONS = ["send"] as const;
|
|
267
267
|
export const ENTWURF_V2_OWNERSHIPS = ["ack-only"] as const;
|
|
268
|
-
// Delivery mode of the message to the target
|
|
269
|
-
//
|
|
268
|
+
// Delivery mode of the message to the target: WHICH QUEUE a busy receiver is asked for.
|
|
269
|
+
// `[측정 2026-09-22, pi-agent-core 0.87.0; 재측정 2026-09-23, 0.87.1 — 좌표 동일]` neither is an interrupt — `dist/agent-loop.js:186`
|
|
270
|
+
// drains steering after each turn and `:191-197` drains follow-ups only once the inner loop has
|
|
271
|
+
// ended, and the two queues (`dist/agent.js:96-97,137-138`) have no FIFO between them, so a later
|
|
272
|
+
// steer overtakes every earlier follow_up. Both are volatile process memory and an abort drops
|
|
273
|
+
// them. A SEPARATE axis from
|
|
270
274
|
// both the intent/ownership axis (F1) and the liveness-routing axis. The removed
|
|
271
275
|
// v1 `entwurf_send` carried the same steer|follow_up surface, so this axis is
|
|
272
276
|
// inherited vocabulary, not a second live delivery verb.
|