@junghanacs/entwurf 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +229 -0
- package/DELIVERY.md +1 -0
- package/README.md +49 -4
- package/VERIFY.md +23 -4
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +111 -18
- package/docs/setup-clean-host.md +104 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +43 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +30 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +288 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
- package/mcp/entwurf-bridge/src/index.ts +7 -7
- package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
- package/package.json +15 -12
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/pi-durable/bootstrap.mjs +281 -0
- package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
- package/pi/pi-durable/overlay/upstream-pin.json +16 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +68 -0
- package/pi-extensions/lib/acp/backend.ts +41 -5
- package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
- package/pi-extensions/lib/fresh-call-composition.ts +42 -7
- package/pi-extensions/lib/meta-sender-identity.ts +7 -1
- package/pi-extensions/lib/meta-session.ts +28 -2
- package/pi-extensions/lib/mux-fresh-call.ts +34 -2
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
- package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
- package/run.sh +214 -36
- package/scripts/check-fresh-call-dispatch.ts +18 -0
- package/scripts/check-gate-qualification.ts +4 -2
- package/scripts/check-herdr-plugin-build.ts +159 -57
- package/scripts/check-pi-durable-contact.ts +834 -0
- package/scripts/check-pi-durable-receive.ts +1116 -0
- package/scripts/check-pi-durable-send.ts +1661 -0
- package/scripts/fixtures/herdr-supply.json +8 -8
- package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
- package/scripts/lib/pi-durable-r1.ts +100 -0
- package/scripts/lib/pi-durable-reap.ts +27 -0
- package/scripts/lib/pi-durable-receive.ts +137 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
- package/scripts/mutants/acp-vendor-rejection.json +62 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/copilot-birth.json +2 -2
- package/scripts/mutants/herdr-plugin-build.json +11 -0
- package/scripts/mutants/herdr-supply.json +3 -3
- package/scripts/mutants/meta-hook-session-switch.json +4 -2
- package/scripts/mutants/mux-fresh-call.json +69 -3
- package/scripts/mutants/omp-birth.json +2 -2
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +390 -0
- package/scripts/mutants/pi-floor.json +2 -2
- package/scripts/pi-durable-contact-host.mjs +105 -0
- package/scripts/pi-durable-receive-host.mjs +127 -0
- package/scripts/pi-durable-send-host.mjs +213 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +21 -7
- package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
- package/scripts/smoke-setup-verdict.sh +21 -2
- package/scripts/tsconfig.json +1 -0
package/docs/acp-backend-rail.md
CHANGED
|
@@ -48,6 +48,7 @@ Source of truth: `pi-extensions/lib/acp/backend-adapter.ts`.
|
|
|
48
48
|
| `buildSessionMeta` | Build optional `newSession._meta` from the already-loaded carrier. |
|
|
49
49
|
| `enforceModel` | Make the requested native model authoritative before the prompt. |
|
|
50
50
|
| `configSignatureFields` | Return a stable primitive map whose changes invalidate reuse. |
|
|
51
|
+
| `readPromptRejection?` | Name a backend-owned typed prompt rejection and its fixed app-policy verdict; absent means the existing verbatim-first failure path. |
|
|
51
52
|
|
|
52
53
|
`backend.ts` resolves the adapter once at turn entry. Common config never branches on
|
|
53
54
|
backend-specific keys; `adapterSettings` remains opaque until handed back to its owner.
|
|
@@ -85,21 +86,58 @@ turned one long turn into four in 0.13.0. Gates: `check-acp-prompt-lifecycle` (b
|
|
|
85
86
|
with pi's own classifier as the oracle), `check-probe-ordering` (no production prompt
|
|
86
87
|
cutoff in source).
|
|
87
88
|
|
|
89
|
+
### Typed Claude prompt rejection — no automatic whole-prompt replay (#127)
|
|
90
|
+
|
|
91
|
+
Claude ACP 0.85.1 can reject a prompt with the wire SDK's `RequestError`, code `-32603`,
|
|
92
|
+
`data.errorKind === "incomplete_tool_call"`, after marking unfinished foreground tools
|
|
93
|
+
failed. Some tools may already have run. This is an RPC failure, not a successful
|
|
94
|
+
`stopReason`, and a failed tool notification alone is not a turn settlement. The code
|
|
95
|
+
is JSON-RPC's internal error; the kind is Claude's extension vocabulary, not a standard
|
|
96
|
+
retry permission. AIR session-failure metadata remains capability-gated off by
|
|
97
|
+
`clientCapabilities: {}`.
|
|
98
|
+
|
|
99
|
+
Only the Claude adapter declares `readPromptRejection`. The common loop passes the
|
|
100
|
+
once-resolved adapter at the two prompt catches, not at bootstrap failures, and skips
|
|
101
|
+
this reader when the operator's abort wins. A recognized rejection seals once as
|
|
102
|
+
`error/error`, drops the new or reused session under the existing policy, and sets a
|
|
103
|
+
fixed semantic `errorMessage` which pi's real retry **and** overflow classifiers must
|
|
104
|
+
reject. No global retry disable, substring sanitizer, or new failure ledger is added.
|
|
105
|
+
|
|
106
|
+
The first raw failure remains in pi's existing `AssistantMessage.diagnostics`: original
|
|
107
|
+
name/message/stack/code, serialized data, and applicable stderr tail, lifecycle, launch
|
|
108
|
+
signal and overflow hint. None is remixed into the semantic verdict. Actual wire data
|
|
109
|
+
is parsed JSON; a hand-built non-wire value that cannot serialize gets an explicit
|
|
110
|
+
unserializable marker rather than escaping before the stream seal. Assistant content,
|
|
111
|
+
context replay and reuse signatures remain unchanged; persisted message JSON gains
|
|
112
|
+
these diagnostics. Cortex, other error kinds/codes, preprompt failures and aborts keep
|
|
113
|
+
the existing path. No warm reuse after this failure is newly claimed.
|
|
114
|
+
|
|
115
|
+
`check-acp-vendor-rejection` owns the source-adjacent adapter/pipeline assertions and
|
|
116
|
+
four defect mutants. Its real classifier oracles, raw positive controls, failed-tool
|
|
117
|
+
ordering, exact-once seal/drop, raw evidence and separate cooperative-cancel cells do
|
|
118
|
+
not certify a real vendor runtime. Target closure installation, focused receipts,
|
|
119
|
+
qualification, full floor and LIVE compatibility remain separate in the
|
|
120
|
+
[#127 thread](https://github.com/junghan0611/entwurf/issues/127).
|
|
121
|
+
|
|
88
122
|
## Support contract (#81)
|
|
89
123
|
|
|
90
124
|
What "supported" means here, per declaration class. The classes are kept apart on purpose: one
|
|
91
125
|
undifferentiated "supported" column is what let a Claude PASS read as if it also certified Cortex.
|
|
92
126
|
The rows are THIS checkout's declarations, not a shipped release: what a published version carried
|
|
93
127
|
is that version's CHANGELOG entry, and a candidate's own evidence is its issue thread, LIVE receipts
|
|
94
|
-
and exact-SHA CI
|
|
128
|
+
and exact-SHA CI. The pi row shipped in 0.30.0 (#125). The ACP wire SDK, Claude adapter and Claude
|
|
129
|
+
Agent SDK rows are the unpublished #127 candidate: published 0.30.0 still declares
|
|
130
|
+
`claude-agent-acp 0.79.0` and `@agentclientprotocol/sdk 1.4.0`
|
|
131
|
+
(`[측정 2026-10-04]` `npm view @junghanacs/entwurf@0.30.0`; per-bump record in the ROADMAP Dep bump
|
|
132
|
+
ledger, 2026-10-03).
|
|
95
133
|
|
|
96
134
|
| Surface | Declaration | Class | What a green actually says |
|
|
97
135
|
|---|---|---|---|
|
|
98
136
|
| Entwurf package | `package.json` `version` | checkout declaration, not a release receipt | names the package label these rows sit beside; whether that label shipped, and with which rows, is CHANGELOG's record |
|
|
99
|
-
| pi runtime | devDep exact `1.0.
|
|
100
|
-
| ACP wire SDK | `@agentclientprotocol/sdk 1.
|
|
101
|
-
| Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.
|
|
102
|
-
| Claude Agent SDK | `0.3.
|
|
137
|
+
| pi runtime | devDep exact `1.0.2`, peer `>=1.0.2 <1.1` | **exact** oracle + **closed range** | the source pin this checkout builds and gates against; hosts inside the range are accepted, and the ceiling moves only on measurement. Unshipped candidate floor: the published 0.30.0 and 0.30.1 declare floor 1.0.0 with the same next-minor ceiling (`[측정 2026-10-05]` `npm view @junghanacs/entwurf@0.30.1 peerDependencies`; acceptance record CHANGELOG 0.30.0, #125 branch evidence). The 1.0.2 move and its receipts are ROADMAP's 2026-10-05 bump entry |
|
|
138
|
+
| ACP wire SDK | `@agentclientprotocol/sdk 1.6.0` | **exact** | the shared wire oracle both adapters speak |
|
|
139
|
+
| Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.85.1` | **exact**, bundled | checkout candidate; resolved before any PATH fallback; declaration is not compatibility evidence |
|
|
140
|
+
| Claude Agent SDK | `0.3.286` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
|
|
103
141
|
| Anthropic SDK | `0.100.1` | **exact**, peer-resolution only | satisfies the Agent SDK peer floor (0.93.0+); never an API client here (gate L4) |
|
|
104
142
|
| Claude Code runtime | `>=2.1.217` (`entwurf.claudeCodeFloor`) | **floor** | below it, hook args are silently dropped; entwurf enforces this itself |
|
|
105
143
|
| Node | `>=24` (`engines.node`) | **floor** | single axis, derived everywhere else |
|
|
@@ -181,7 +219,7 @@ different reasons, and collapsing them would hide a real risk**:
|
|
|
181
219
|
imported nowhere: `[측정 2026-09-18]` `git grep -c` over `pi-extensions/`, `test/`, `scripts/`,
|
|
182
220
|
`mcp/` is **0**. We never imported the adapter as a library at all — we spawn its binary.
|
|
183
221
|
- **0.77.0's `allowDangerouslySkipPermissions` opt-out is a new lever we deliberately do not
|
|
184
|
-
pull, and our effective permission mode
|
|
222
|
+
pull, and at 0.79.0 our effective permission mode was unchanged.** 0.76.0 sent
|
|
185
223
|
`allowDangerouslySkipPermissions: ALLOW_BYPASS` unconditionally and computed
|
|
186
224
|
`initialPermissionMode = creationOpts.permissionMode ?? resolvePermissionMode(settings…)`.
|
|
187
225
|
0.79.0 computes `allowBypass = ALLOW_BYPASS && sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false`
|
|
@@ -192,7 +230,9 @@ different reasons, and collapsing them would hide a real risk**:
|
|
|
192
230
|
versions (`dist/permissions/modes.js` 직독). Our overlay pins
|
|
193
231
|
`permissions.defaultMode: "bypassPermissions"` (`overlay.ts:122`), which resolves the same
|
|
194
232
|
under both. The lever now EXISTS for a host that wants a non-bypass sibling; declaring it is a
|
|
195
|
-
separate axis, not a one-line flip, and nothing in this bump takes it.
|
|
233
|
+
separate axis, not a one-line flip, and nothing in this bump takes it. That is the 0.79.0
|
|
234
|
+
measurement; at 0.85.1 project or managed settings can drop bypass (the 0.79.0 → 0.85.1 entry
|
|
235
|
+
below).
|
|
196
236
|
- **0.77.0's system-reminder strip never touches our first-user-message augment.**
|
|
197
237
|
`INJECTED_CONTEXT_MARKERS = ["system-reminder"]` joins the local-command markers in
|
|
198
238
|
`stripMarkerTags`, and `stripLocalCommandMetadata` has exactly two call sites
|
|
@@ -247,6 +287,51 @@ different reasons, and collapsing them would hide a real risk**:
|
|
|
247
287
|
is byte-identical (`md5 cfd031d0…` both versions). The `settings`-as-STRING-PATH branch our
|
|
248
288
|
call-site contract names is still on the unconditional `session/new` path
|
|
249
289
|
(`dist/acp-agent.js:6010-6012`).
|
|
290
|
+
- **0.79.0 → 0.85.1 (#127) is a source classification, not a runtime certification.**
|
|
291
|
+
`[측정 2026-10-03, tag v0.85.1 = 686c0c99, src and dist read directly; nothing executed]`.
|
|
292
|
+
Coordinates and evidence classes are in the ROADMAP Dep bump entry for 2026-10-03. The typed
|
|
293
|
+
rejection above is the only failure-policy change Entwurf wrote for this bump. The reachable
|
|
294
|
+
upstream changes below are classified, not new Entwurf contracts:
|
|
295
|
+
- **Permission mode is conditional (#1165, reachable on a host/project basis).** The adapter
|
|
296
|
+
builds its own `SettingsManager(params.cwd, …)` over `claudeConfigDir/settings.json`, the
|
|
297
|
+
cwd's `.claude/settings.json` and `settings.local.json`, and the managed path, then calls
|
|
298
|
+
`resolveSettings({ cwd })` without a `settingSources` argument. That call flow is separate
|
|
299
|
+
from our SDK query's `settingSources: []` (`tool-surface.ts:152`). When those settings carry
|
|
300
|
+
`disableBypassPermissionsMode: "disable"`, `allowBypass` drops, more permission requests reach
|
|
301
|
+
our approve-all-by-kind handler, and `[permission:approved]` notices appear. The SDK's
|
|
302
|
+
default `resolveSettings` source set is UNREAD, and no gate covers this case.
|
|
303
|
+
- **Reachable, classified from source only.**
|
|
304
|
+
- `createSession` starts a background `getContextUsage()` refresh (call at tag `85:8317`), and
|
|
305
|
+
does so again after a model change. Upstream comments (`85:8301`, `85:8350-8352`) describe
|
|
306
|
+
SDK control requests as serialized. The SDK internals are UNREAD, so whether our bounded
|
|
307
|
+
set-model and early-abort grace queue behind it is a lead, and its timing is UNRUN.
|
|
308
|
+
- A turn made only of `<synthetic>` frames emits no `usage_update` (#1132), and our
|
|
309
|
+
keep-prior occupancy seals the prior value.
|
|
310
|
+
- `tool_call_update` omits unchanged fields for every client (#1153). No producer was found
|
|
311
|
+
that sends a pre-terminal `rawOutput.isError`, so a tool failure still arrives as
|
|
312
|
+
`status: "failed"`.
|
|
313
|
+
- #1216's `teardownSession` stops awaiting the interrupt reply. It is reached only
|
|
314
|
+
indirectly, through dispose on stdin EOF or a signal: we never send `session/close`, and
|
|
315
|
+
our `session/cancel` still awaits.
|
|
316
|
+
- A sign-out during an unsettled turn fails it with `RequestError.authRequired()` and no
|
|
317
|
+
extra message (#1179, tag `85:4420`). The stock text is the SDK default
|
|
318
|
+
`Authentication required` (sdk 1.6.0 `jsonrpc.js:1032-1033`), and that message ALONE is not
|
|
319
|
+
transient to pi's real classifier. The sealed legacy `errorMessage` also carries the
|
|
320
|
+
stderr tail, lifecycle and hint (`backend.ts:1463-1490`), and a tail can still match the
|
|
321
|
+
retry pattern. `[측정 2026-10-04, coordinator classifier probe]`: `"Authentication required"`
|
|
322
|
+
→ `retryable: false`; the same text plus `\n--- backend stderr (tail) ---\n500` →
|
|
323
|
+
`retryable: true`. #127 does not sanitize this path and certifies no auth runtime.
|
|
324
|
+
- The nine other vendor `internalError` sites keep the same legacy path. Their
|
|
325
|
+
`Internal error:` prefix can match the classifier, and that pre-existing policy is
|
|
326
|
+
unchanged here. This is a source and classifier reading, not a frequency claim and not a
|
|
327
|
+
complete-callee identity claim.
|
|
328
|
+
- **Capability-gated:** `notice`, the one new `sessionUpdate` literal (17 → 18 kinds).
|
|
329
|
+
- **Ambient only:** `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1` selects a v1/v2 router. Entwurf
|
|
330
|
+
neither sets nor scrubs it, and the router's v1 path for a v1 client is UNRUN.
|
|
331
|
+
- **Entry identity is not settlement identity.** `newSession`, `prompt`,
|
|
332
|
+
`setSessionConfigOption`, `requestPermission`, `closeSession`, `sessionUsage`,
|
|
333
|
+
`turnQuotaMeta` and `quotaTokenCount` are byte-identical as extracted entry bodies. Turn
|
|
334
|
+
settlement moved inside the changed `runConsumer`.
|
|
250
335
|
|
|
251
336
|
- **The one 0.73.0 → 0.75.1 change that DOES reach us:** context compaction is now surfaced as a
|
|
252
337
|
synthetic ACP tool lifecycle (0.75.0, #991) — a `tool_call` with `kind: "think"`, title
|
|
@@ -373,10 +458,15 @@ caller-session `_meta`, and cross-machine certification.
|
|
|
373
458
|
|
|
374
459
|
A backend can return `newSession` before its declared MCP server is callable. This was
|
|
375
460
|
observed intermittently on the Claude rail and directly on Cortex's private `mcp.json`
|
|
376
|
-
path.
|
|
377
|
-
readiness fence over a session's declared MCP servers
|
|
378
|
-
|
|
379
|
-
|
|
461
|
+
path. Entwurf's common loop calls `mcpServerStatus()` nowhere; it does not add a
|
|
462
|
+
client-side readiness fence over a session's declared MCP servers. The historical
|
|
463
|
+
0.76.0 → 0.79.0 source measurement below is dated evidence, not a current-version
|
|
464
|
+
runtime claim. For the 0.85.1 candidate, #127 re-measured the MCP-authentication
|
|
465
|
+
window as byte-identical and ran the MCP/self/send LIVE samples; neither source
|
|
466
|
+
identity nor green samples establish the cause of the intermittent readiness gap
|
|
467
|
+
or close #72. Changed SDK internals and session initialization remain separate
|
|
468
|
+
risk surfaces.
|
|
469
|
+
(Historical measurement at the 0.76.0 → 0.79.0 bump, not inherited — the previous bump's argument is
|
|
380
470
|
not reused, the way the 0.75.1 → 0.76.0 entry did not reuse 0.73.0 → 0.75.1's.
|
|
381
471
|
`mcpServerStatus` call sites in `src/acp-agent.ts` are **2 at v0.76.0 and 2 at v0.79.0**
|
|
382
472
|
`[측정 2026-09-18, upstream v0.79.0/src/acp-agent.ts read directly, grep -n]`; they first
|
|
@@ -392,7 +482,8 @@ empty) and the region moved +11 while the file shrank 10,405 → 10,329 lines. T
|
|
|
392
482
|
agent-picker removal, the 0.78.0 compaction/checkpoint/AIR work and the 0.79.0 permission
|
|
393
483
|
presentation touch no part of this path, so the other reachable-surface findings stand as
|
|
394
484
|
re-measured in the capability-posture section above.
|
|
395
|
-
|
|
485
|
+
That historical source measurement closes no part of #72; it is not a claim that
|
|
486
|
+
all effective readiness behavior stayed unchanged in the 0.85.1 runtime.)
|
|
396
487
|
|
|
397
488
|
### 11-7-a/b. Instrument and first measurement
|
|
398
489
|
|
|
@@ -500,12 +591,14 @@ nothing here is promised and then quietly not delivered. `[QK:PI-QUEUE-NO-MIDFLI
|
|
|
500
591
|
while a turn is in flight never joins that turn and arrives as a later one. The delay's SIZE is
|
|
501
592
|
that turn's duration, which the same cell fixes by construction.
|
|
502
593
|
|
|
503
|
-
**Forwarding is not adopted here.** `[측정 2026-09-22]`
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
`_session/steering` in
|
|
507
|
-
(`dist/acp-agent.js:163`, handler at `:7792`),
|
|
508
|
-
`InitializeResponse._meta.steering.supported`.
|
|
594
|
+
**Forwarding is not adopted here.** The dated `[측정 2026-09-22]` SDK-method enumeration
|
|
595
|
+
and private-extension observation below belong to the then-installed oracle, not a
|
|
596
|
+
0.85.1 runtime measurement: `AGENT_METHODS` and `CLIENT_METHODS` contained zero entries
|
|
597
|
+
matching `steer`; `_session/steering` was a PRIVATE vendor extension in
|
|
598
|
+
`@agentclientprotocol/claude-agent-acp@0.79.0` (`dist/acp-agent.js:163`, handler at `:7792`),
|
|
599
|
+
client→agent, advertised through `InitializeResponse._meta.steering.supported`.
|
|
600
|
+
The current boundary is our client's lack of a steering path, not a claim that the
|
|
601
|
+
old vendor coordinates describe the current pin. Our client sends `initialize`, `session/new`,
|
|
509
602
|
`session/prompt`, `session/set_config_option` and `session/cancel` (`acp-client.ts:138-149`) and
|
|
510
603
|
has no steering path at all.
|
|
511
604
|
|
package/docs/setup-clean-host.md
CHANGED
|
@@ -28,7 +28,8 @@ evidence split, and its three states must not collapse into "macOS is supported"
|
|
|
28
28
|
| npm/pnpm | npm is bundled with Node; pnpm is required for source setup | package or source installation |
|
|
29
29
|
| Python 3 | required by `setup`/`install` (project-path normalization + settings writers); `--help`/`check-bridge` stay Python-free | pi/Claude/agy/Copilot/Codex wiring writers |
|
|
30
30
|
| entwurf | global/project-local `@junghanacs/entwurf`, or a source checkout | operator command and garden capability |
|
|
31
|
-
| pi | optional-by-presence, `>=1.0.
|
|
31
|
+
| pi | optional-by-presence, `>=1.0.2 <1.1` — absent is an explicit setup SKIP, below-floor is a named FAIL | ACP provider, control sockets |
|
|
32
|
+
| Native durable (`pi-durable`) | optional-by-presence, operator-provided experimental source runtime at the fixed `…/entwurf/pi-durable/runtime`, pinned to `cd32f77` plus the narrow runtime-contact overlay; not supplied by ordinary Pi npm installation — absent is an explicit setup SKIP, present-but-not-the-pin a named FAIL | optional 0.31.0 candidate contact via `entwurf pi-durable`; first native Pi ↔ pi-durable admission/LIVE accepted on one Linux host, runtime provision and release acceptance separate (§2b) |
|
|
32
33
|
| Claude Code | optional, **`>=2.1.217`** — the exec-form hook floor | Claude ACP auth/runtime and mailbox-backed native citizen |
|
|
33
34
|
| GitHub Copilot CLI | optional-by-presence, operator-installed and authenticated — absent is an explicit setup SKIP; detected composes all four units (birth/MCP/receiver/footer) | self-fetch citizen and visible fresh |
|
|
34
35
|
| OMP (`omp`) | optional-by-presence, operator-installed — absent is an explicit setup SKIP; detected composes all four units (birth/MCP/`tools.xdev` setting/receiver) | self-fetch citizen and visible fresh (accepted on one host — see §4b) |
|
|
@@ -190,7 +191,7 @@ on Darwin; that is wiring, not a rail receipt. If `pi` is on PATH,
|
|
|
190
191
|
Install the exact release floor, then wire the project:
|
|
191
192
|
|
|
192
193
|
```bash
|
|
193
|
-
npm install -g @earendil-works/pi-coding-agent@1.0.
|
|
194
|
+
npm install -g @earendil-works/pi-coding-agent@1.0.2
|
|
194
195
|
pi --version
|
|
195
196
|
|
|
196
197
|
cd ~/entwurf-smoke
|
|
@@ -198,7 +199,7 @@ entwurf install .
|
|
|
198
199
|
pi -e "$(npm root -g)/@junghanacs/entwurf" --list-models entwurf
|
|
199
200
|
```
|
|
200
201
|
|
|
201
|
-
The supported range is `>=1.0.
|
|
202
|
+
The supported range is `>=1.0.2 <1.1`. It is a hard minimum: installing this
|
|
202
203
|
release onto a 0.83.x pi host upgrades the runtime rather than keeping the older
|
|
203
204
|
minor. A host using only the external MCP bridge can skip pi until it needs a
|
|
204
205
|
control socket; no delivery rail launches a pi process.
|
|
@@ -282,6 +283,105 @@ emacsclient -s "${PI_EMACS_AGENT_SOCKET:-server}" --eval '(...)'
|
|
|
282
283
|
|
|
283
284
|
</details>
|
|
284
285
|
|
|
286
|
+
## 2b. Optional native durable (`pi-durable`)
|
|
287
|
+
|
|
288
|
+
**0.31.0 installation contract; first native admission/LIVE accepted on one Linux host.**
|
|
289
|
+
The [DELIVERY matrix](../DELIVERY.md) records the 2026-10-06 LIVE36 candidate
|
|
290
|
+
acceptance; release/artifact and runtime-supply evidence remain separate.
|
|
291
|
+
Durable is a separate native harness contact, not an option automatically enabled
|
|
292
|
+
by `entwurf pi` or by registering ordinary Pi's adapter.
|
|
293
|
+
|
|
294
|
+
There are three different things to install or supply:
|
|
295
|
+
|
|
296
|
+
1. **Entwurf:** the existing global/project-local npm or source-checkout route in
|
|
297
|
+
§1 stays unchanged. It supplies Entwurf bytes, not the native harness.
|
|
298
|
+
2. **Durable library:** `@earendil-works/pi-durable` is npm-installable. A library
|
|
299
|
+
dependency does not install the experimental coding app/TUI or birth a citizen.
|
|
300
|
+
3. **Native app/runtime:** upstream's experimental durable app runs from a Pi
|
|
301
|
+
source checkout through its source resolver. The pinned 1.0.2 ordinary
|
|
302
|
+
`pi-coding-agent` package excludes experimental app output; upgrading the global
|
|
303
|
+
`pi` CLI does not supply it. Entwurf's maintained contact requires the exact
|
|
304
|
+
upstream commit `cd32f7725fdbddbaecdff5b1e68491563394e0ca` plus its narrow
|
|
305
|
+
runtime-contact overlay, operator-provided dependencies and pinned runtime data.
|
|
306
|
+
|
|
307
|
+
The upstream [library installation guide](https://github.com/earendil-works/pi/blob/cd32f7725fdbddbaecdff5b1e68491563394e0ca/packages/durable/README.md#installation)
|
|
308
|
+
and [experimental app guide](https://github.com/earendil-works/pi/blob/cd32f7725fdbddbaecdff5b1e68491563394e0ca/packages/coding-agent/src/experimental/durable/README.md)
|
|
309
|
+
describe different consumers. Running the bare upstream app does not attach
|
|
310
|
+
Entwurf's garden contact. Entwurf does not install a second harness distribution,
|
|
311
|
+
rebuild its controller/model/auth/storage system or proxy a subscription.
|
|
312
|
+
|
|
313
|
+
### What the operator supplies
|
|
314
|
+
|
|
315
|
+
The runtime lives at ONE fixed place, with no path override:
|
|
316
|
+
`$XDG_DATA_HOME/entwurf/pi-durable/runtime` (an absolute `XDG_DATA_HOME`; otherwise
|
|
317
|
+
`~/.local/share/entwurf/pi-durable/runtime`). It counts as the pin only when:
|
|
318
|
+
|
|
319
|
+
- HEAD is `cd32f7725fdbddbaecdff5b1e68491563394e0ca`, nothing is staged, `git diff`
|
|
320
|
+
equals the packaged overlay `pi/pi-durable/overlay/runtime-contacts.patch` byte for
|
|
321
|
+
byte, and no non-ignored file is untracked;
|
|
322
|
+
- the upstream source resolver
|
|
323
|
+
`packages/coding-agent/src/experimental/source-resolver.ts` is present;
|
|
324
|
+
- the git-ignored model data in `packages/ai/src/providers/data` matches the pinned
|
|
325
|
+
digest: the bytes `@earendil-works/pi-ai@1.0.2` ships in `dist/providers/data`,
|
|
326
|
+
**including its `.manifest.json` dotfile** (recorded in
|
|
327
|
+
`pi/pi-durable/overlay/upstream-pin.json`).
|
|
328
|
+
|
|
329
|
+
The operator supplies it by hand; Entwurf never downloads, installs, repairs,
|
|
330
|
+
moves or deletes the runtime. The sequence below was run on a Linux host
|
|
331
|
+
(2026-10-06); afterwards the runtime verifier `entwurf setup` consults returned
|
|
332
|
+
the pin (exit 0) and the checkout's native contact cells passed against it.
|
|
333
|
+
`ENTWURF_ROOT` is your Entwurf package root or checkout, which ships the patch:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
case "${XDG_DATA_HOME:-}" in
|
|
337
|
+
/*) R="$XDG_DATA_HOME/entwurf/pi-durable/runtime" ;;
|
|
338
|
+
*) R="$HOME/.local/share/entwurf/pi-durable/runtime" ;;
|
|
339
|
+
esac
|
|
340
|
+
git clone --no-checkout https://github.com/earendil-works/pi.git "$R"
|
|
341
|
+
git -C "$R" checkout --detach cd32f7725fdbddbaecdff5b1e68491563394e0ca
|
|
342
|
+
git -C "$R" apply "$ENTWURF_ROOT/pi/pi-durable/overlay/runtime-contacts.patch" # worktree only; do not stage
|
|
343
|
+
(cd "$R" && npm ci --ignore-scripts) # upstream lockfile; dependency install scripts skipped
|
|
344
|
+
T=$(mktemp -d)
|
|
345
|
+
(cd "$T" && npm pack @earendil-works/pi-ai@1.0.2)
|
|
346
|
+
tar -xzf "$T/earendil-works-pi-ai-1.0.2.tgz" -C "$T" package/dist/providers/data
|
|
347
|
+
cp -a "$T/package/dist/providers/data/." "$R/packages/ai/src/providers/data/" # dotfiles too
|
|
348
|
+
entwurf setup # expect: pi-durable PASS
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Why it is a source checkout and not `npm install`: `@earendil-works/pi-coding-agent`
|
|
352
|
+
(1.0.2 and 1.0.4 alike) excludes `dist/experimental` from its `files`, and its only
|
|
353
|
+
experimental export resolves to `src/`, which the tarball does not ship, so the
|
|
354
|
+
durable app's `experimental/durable/runtime.ts` and `tui.ts` exist only in a source
|
|
355
|
+
checkout. `@earendil-works/pi-durable` on npm is the library (no app, TUI or bin).
|
|
356
|
+
|
|
357
|
+
### What Entwurf does with it
|
|
358
|
+
|
|
359
|
+
- `entwurf setup` reports a `pi-durable` row from that check and writes no durable
|
|
360
|
+
wiring in any case: **PASS** when the runtime is the pin, **SKIP** when nothing is
|
|
361
|
+
at the fixed place, **FAIL** with the named refusal when something is there but is
|
|
362
|
+
not the pin.
|
|
363
|
+
- `entwurf pi-durable [args…]` is the managed launch. It verifies the same runtime
|
|
364
|
+
first (refusal exit 4 when absent, 3 otherwise), then runs the packaged bootstrap
|
|
365
|
+
`pi/pi-durable/bootstrap.mjs` under the runtime's source resolver, owning this
|
|
366
|
+
terminal. The bootstrap imports only the compiled closure shipped beside it
|
|
367
|
+
(`mcp/entwurf-bridge/dist/…`), so an installed package never imports checkout
|
|
368
|
+
TypeScript; a source checkout needs a built bridge (`pnpm run build-bridge`) first.
|
|
369
|
+
`plugins/pi-durable/bootstrap.mjs` is only the checkout path that re-exports it.
|
|
370
|
+
|
|
371
|
+
Existing users who do not enable durable keep the same installation steps. For
|
|
372
|
+
durable, native Pi owns credentials, model settings, SQLite sessions and recovery;
|
|
373
|
+
keep them in place rather than copying credentials or migrating ordinary Pi
|
|
374
|
+
transcripts. Native recovery does not prove exactly-once Entwurf dispatch or inbox
|
|
375
|
+
read recovery. The contact provides garden attachment, visible identity and
|
|
376
|
+
announce-only root-directed self-fetch. First native admission closed
|
|
377
|
+
[#129](https://github.com/junghan0611/entwurf/issues/129) at reviewed main:
|
|
378
|
+
visible fresh/callback, addressed receive and both real Pi ↔ pi-durable LIVE legs
|
|
379
|
+
passed on one Linux host. Installed-bootstrap import-only proof is not a native
|
|
380
|
+
runtime/TUI launch or a live Claude ↔ pi-durable receipt. This section states the
|
|
381
|
+
installation contract; exact release/artifact acceptance is a separate floor.
|
|
382
|
+
[#130](https://github.com/junghan0611/entwurf/issues/130) owns npm-only runtime
|
|
383
|
+
supply, later Pi versions, ACP and UI follow-ups.
|
|
384
|
+
|
|
285
385
|
## 3. Optional Claude Code native citizen
|
|
286
386
|
|
|
287
387
|
This rail is CERTIFIED on Linux; on macOS it is NOT CERTIFIED — pending physical
|
|
@@ -445,7 +545,7 @@ as a note rather than a fault.
|
|
|
445
545
|
|
|
446
546
|
**entwurf sets no OMP version floor in code, and will not grow one on schedule.** Detection is
|
|
447
547
|
presence-only (`command -v "${OMP_BIN:-omp}"`); there is no `entwurf.ompFloor`, no coherence
|
|
448
|
-
gate, and no exact pin — unlike Node (`engines.node`), pi (`>=1.0.
|
|
548
|
+
gate, and no exact pin — unlike Node (`engines.node`), pi (`>=1.0.2 <1.1`) and Claude Code
|
|
449
549
|
(`entwurf.claudeCodeFloor`), each of which has an enforcement point. That asymmetry is a
|
|
450
550
|
decision, not an omission. A floor is the answer to a vendor that fails SILENTLY — Claude Code
|
|
451
551
|
earned one because an older binary validates the exec manifest, drops `args` at runtime, and
|
|
@@ -630,8 +630,8 @@ server.tool("entwurf_register_native", "Register an ALREADY-RUNNING native conve
|
|
|
630
630
|
server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling beside you and hand it a first task. WHERE it opens is decided by where THIS " +
|
|
631
631
|
"agent runs, never by a parameter: inside herdr (HERDR_ENV=1) it opens a NEW UNFOCUSED TAB in your own " +
|
|
632
632
|
"herdr workspace, pi and claude-code " +
|
|
633
|
-
"only; everywhere else it opens in the operator's tmux with all
|
|
634
|
-
"claude-code, copilot, omp, codex). No fallback: an incomplete herdr context is refused by name, " +
|
|
633
|
+
"only; everywhere else it opens in the operator's tmux with all six backends (pi, " +
|
|
634
|
+
"claude-code, copilot, omp, codex, pi-durable). No fallback: an incomplete herdr context is refused by name, " +
|
|
635
635
|
"never by a tmux window you cannot see from herdr. The sibling's FIRST action " +
|
|
636
636
|
"is a callback to you carrying a nonce, whose sender envelope is its garden id — that is how you learn the " +
|
|
637
637
|
"address of a thing that did not exist a moment ago. This returns a LAUNCH receipt (the owner's " +
|
|
@@ -639,8 +639,8 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling beside you and
|
|
|
639
639
|
"the task was delivered. Those coordinates are a VIEW, never an address — a herdr tab/pane id can change under " +
|
|
640
640
|
"a running sibling. Nothing polls. " +
|
|
641
641
|
"For EXISTING citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the chosen " +
|
|
642
|
-
"runtime CLI (`provider/model` for pi, an id/alias for Claude Code, a pattern for the rest). On tmux, copilot/omp/codex are refused BEFORE any window " +
|
|
643
|
-
"opens when their birth, MCP, receive or visible-identity units are absent, and codex also needs the " +
|
|
642
|
+
"runtime CLI (`provider/model` for pi, an id/alias for Claude Code, `provider/model id` exactly as the catalog names it for pi-durable, a pattern for the rest). On tmux, copilot/omp/codex/pi-durable are refused BEFORE any window " +
|
|
643
|
+
"opens when their birth, MCP, receive or visible-identity units are absent, pi-durable also when its operator-provided pinned runtime is absent or drifted, and codex also needs the " +
|
|
644
644
|
"operator-owned app-server socket entwurf never starts; in herdr they are refused by name first. " +
|
|
645
645
|
"Optional placement.tmuxSession is a TMUX-ONLY seat naming ONE EXISTING session and " +
|
|
646
646
|
"ALWAYS wins; omitted, the seat follows the CALLER, never the backend opened: a CODEX CALLER opens beside " +
|
|
@@ -648,8 +648,8 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling beside you and
|
|
|
648
648
|
"NOTHING is created. In herdr the field is refused by name. Do not put secrets in the task — model and task argv are visible to same-user " +
|
|
649
649
|
"processes.", {
|
|
650
650
|
backend: z
|
|
651
|
-
.enum(["pi", "claude-code", "copilot", "omp", "codex"])
|
|
652
|
-
.describe("Which fixed runtime to open. Only these
|
|
651
|
+
.enum(["pi", "claude-code", "copilot", "omp", "codex", "pi-durable"])
|
|
652
|
+
.describe("Which fixed runtime to open. Only these six, and only pi/claude-code when this agent runs inside herdr; there is no arbitrary command."),
|
|
653
653
|
model: z
|
|
654
654
|
.string()
|
|
655
655
|
.min(1)
|
|
@@ -663,7 +663,7 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling beside you and
|
|
|
663
663
|
// as an unused suppression while the escape goes back to being fixable.
|
|
664
664
|
// biome-ignore lint/complexity/noUselessEscapeInRegex: emitted to a Rust regex validator, see above
|
|
665
665
|
.regex(/^[A-Za-z0-9][A-Za-z0-9._/:\[\]-]*$/)
|
|
666
|
-
.describe("Required runtime model: canonical provider/model for pi, a Claude Code model id/alias, or a Copilot/OMP/Codex model name."),
|
|
666
|
+
.describe("Required runtime model: canonical provider/model for pi, provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name."),
|
|
667
667
|
task: z
|
|
668
668
|
.string()
|
|
669
669
|
.min(1)
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"codex": { "wakeMode": "direct-inject", "deliveryLevel": "D6", "nativeIdLabel": "threadId" },
|
|
7
7
|
"copilot": { "wakeMode": "self-fetch", "deliveryLevel": "D6", "nativeIdLabel": "sessionId" },
|
|
8
8
|
"omp": { "wakeMode": "self-fetch", "deliveryLevel": "D6", "nativeIdLabel": "sessionId" },
|
|
9
|
+
"pi-durable": { "wakeMode": "self-fetch", "deliveryLevel": "D2", "nativeIdLabel": "sessionId" },
|
|
9
10
|
"pi": { "wakeMode": "direct-inject", "deliveryLevel": "D6", "nativeIdLabel": "sessionId" }
|
|
10
11
|
}
|
|
11
12
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { createRequire } from "node:module";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { RequestError } from "@agentclientprotocol/sdk";
|
|
23
24
|
import { enrichMcpServersWithEnvelope } from "./config.js";
|
|
24
25
|
import { loadEngraving } from "./engraving.js";
|
|
25
26
|
import { CORTEX_MODEL_PREFIX, curatedClaudeModels, curatedCortexModels, SUPPORTED_ANTHROPIC_MODEL_IDS, SUPPORTED_CORTEX_MODEL_IDS, } from "./models.js";
|
|
@@ -34,10 +35,46 @@ import { buildClaudeSessionMeta } from "./tool-surface.js";
|
|
|
34
35
|
function shellQuote(value) {
|
|
35
36
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
36
37
|
}
|
|
38
|
+
/** The pi diagnostic type under which backend.ts preserves a recognised rejection verbatim. */
|
|
39
|
+
export const ACP_PROMPT_REJECTION_DIAGNOSTIC = "entwurf_acp_prompt_rejection";
|
|
40
|
+
/** The verdict for claude-agent-acp's `incomplete_tool_call` (#127; upstream #1212, 0.85.0+). */
|
|
41
|
+
export const INCOMPLETE_TOOL_CALL_VERDICT = "ACP turn failed because foreground tool results are incomplete. Some tools may already have run; automatic replay is unsafe. Inspect the preserved vendor rejection before continuing.";
|
|
37
42
|
// ---------------------------------------------------------------------------
|
|
38
43
|
// claude adapter — the first implementation (the rail's reference backend)
|
|
39
44
|
// ---------------------------------------------------------------------------
|
|
40
45
|
const SUPPORTED_CLAUDE_IDS = new Set(SUPPORTED_ANTHROPIC_MODEL_IDS);
|
|
46
|
+
/** JSON-RPC internal error — the code claude-agent-acp rejects an unfinished-tool turn with. */
|
|
47
|
+
const JSONRPC_INTERNAL_ERROR = -32603;
|
|
48
|
+
/** claude-agent-acp's own kind for a turn that hit `end_turn` with foreground tools unanswered
|
|
49
|
+
* (read at v0.85.1 `src/acp-agent.ts:4286-4340`, `errorKindData("incomplete_tool_call")`). */
|
|
50
|
+
const CLAUDE_INCOMPLETE_TOOL_CALL = "incomplete_tool_call";
|
|
51
|
+
/**
|
|
52
|
+
* The claude reader for `readPromptRejection` (#127). Only the root wire SDK's own decode
|
|
53
|
+
* counts: its client rejects a JSON-RPC error response with `new RequestError(code, message,
|
|
54
|
+
* data)`, `data` being the parsed `error.data` (sdk 1.4.0 `dist/jsonrpc.js:842-844`). A
|
|
55
|
+
* look-alike object, another code, or another `errorKind` is not this failure and keeps the
|
|
56
|
+
* verbatim-first path.
|
|
57
|
+
*/
|
|
58
|
+
function readClaudePromptRejection(err) {
|
|
59
|
+
if (!(err instanceof RequestError) || err.code !== JSONRPC_INTERNAL_ERROR)
|
|
60
|
+
return undefined;
|
|
61
|
+
const data = err.data;
|
|
62
|
+
if (typeof data !== "object" || data === null || !("errorKind" in data))
|
|
63
|
+
return undefined;
|
|
64
|
+
if (data.errorKind !== CLAUDE_INCOMPLETE_TOOL_CALL)
|
|
65
|
+
return undefined;
|
|
66
|
+
return { errorKind: CLAUDE_INCOMPLETE_TOOL_CALL, verdict: INCOMPLETE_TOOL_CALL_VERDICT, dataJson: jsonText(data) };
|
|
67
|
+
}
|
|
68
|
+
/** Evidence text for a wire value. A value that came off the wire serializes; one that cannot
|
|
69
|
+
* (a hand-built fake) is named as such, so recording evidence never breaks the turn's seal. */
|
|
70
|
+
function jsonText(value) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.stringify(value) ?? String(value);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return `[unserializable: ${err instanceof Error ? err.name : typeof err}]`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
41
78
|
/**
|
|
42
79
|
* Resolve the claude launch — an ENTWURF-OWNED launcher, or the env override for debug.
|
|
43
80
|
* This is the single source for the claude launch spec; backend.ts holds no private copy.
|
|
@@ -139,6 +176,9 @@ export const claudeAdapter = {
|
|
|
139
176
|
// The token partition is NOT declared here: ACP's only token carrier is a
|
|
140
177
|
// per-turn round-trip aggregate, which is not what pi's four fields mean.
|
|
141
178
|
sealsTurnAccounting: true,
|
|
179
|
+
// #127: claude-agent-acp 0.85.0+ rejects an `end_turn` with unanswered foreground tools as
|
|
180
|
+
// -32603 `incomplete_tool_call` (upstream #1212). Its raw text reads as transient to pi.
|
|
181
|
+
readPromptRejection: readClaudePromptRejection,
|
|
142
182
|
};
|
|
143
183
|
// ---------------------------------------------------------------------------
|
|
144
184
|
// Registry — modelId → adapter
|
|
@@ -285,6 +325,9 @@ export const cortexAdapter = {
|
|
|
285
325
|
// backend.ts seals nothing for a backend without this flag, so cortex's
|
|
286
326
|
// emitted usage is byte-identical to what it was before #93. Declaring this
|
|
287
327
|
// with a guess would mint exactly the silent misaccounting #93 exists to end.
|
|
328
|
+
// NO readPromptRejection — also deliberate. Nobody has read which failures cortex's ACP
|
|
329
|
+
// server rejects a prompt with, so every cortex failure keeps the verbatim-first
|
|
330
|
+
// errorMessage. Borrowing claude's `incomplete_tool_call` reader would be a guess.
|
|
288
331
|
};
|
|
289
332
|
const ADAPTERS = [claudeAdapter, cortexAdapter];
|
|
290
333
|
/**
|
|
@@ -105,7 +105,13 @@ export function receiverMarkerMatchesIdentity(marker, identity) {
|
|
|
105
105
|
// previous garden in-process on its `/new` edge (the unarm the claude hook lacked),
|
|
106
106
|
// so the cell this join closes has a different owner there. Admitting it needs its
|
|
107
107
|
// own measurement, not this file's optimism.
|
|
108
|
-
|
|
108
|
+
//
|
|
109
|
+
// `pi-durable-host` is admitted because the join holds by construction: the durable contact
|
|
110
|
+
// writes its sender marker under the host's own pid and arms its watch in that same process
|
|
111
|
+
// (`meta-bridge-pi-durable.ts`), so the receiver marker's `ownerPid` IS the sender-marker key.
|
|
112
|
+
// Inside the scope a host whose sender marker is gone or names another garden is a retired
|
|
113
|
+
// watch, which is the refusal this admission exists to make reachable.
|
|
114
|
+
export const SENDER_JOINED_RECEIVER_OWNER_KINDS = ["claude-code-cli", "pi-durable-host"];
|
|
109
115
|
/** Does this watch owner share its pid with the backend's sender marker? */
|
|
110
116
|
export function receiverOwnerKindJoinsSender(ownerKind) {
|
|
111
117
|
return SENDER_JOINED_RECEIVER_OWNER_KINDS.includes(ownerKind);
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { randomBytes } from "node:crypto";
|
|
25
25
|
/** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
|
|
26
|
-
* not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C,
|
|
27
|
-
* by #95 after its system birth and app-server rails were measured
|
|
26
|
+
* not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, `codex`
|
|
27
|
+
* by #95 after its system birth and app-server rails were measured, and `pi-durable` by #129
|
|
28
|
+
* (the operator-provided durable app through the managed `entwurf pi-durable`). The set is joined to the
|
|
28
29
|
* citizen backends by `check-harness-admission-parity`: a harness that mints records but is
|
|
29
30
|
* missing HERE is not an unwired convenience, it is a release blocker. */
|
|
30
|
-
export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "codex"];
|
|
31
|
+
export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "codex", "pi-durable"];
|
|
31
32
|
/**
|
|
32
33
|
* The callback tool NAME differs per backend and that is not cosmetic: every session reaches the
|
|
33
34
|
* capability under whatever name its harness composes for the `entwurf-bridge` MCP server, and
|
|
@@ -64,6 +65,11 @@ export const FRESH_CALL_CALLBACK_TOOL = {
|
|
|
64
65
|
// Codex's tool process is the operator app-server, not the pane: the no-arg
|
|
65
66
|
// verb refuses there by name. First action stays the delivery verb with args.
|
|
66
67
|
codex: "mcp__entwurf_bridge__entwurf_v2",
|
|
68
|
+
// The durable contact registers the bridge verbs as its OWN tools under their bare names, and the
|
|
69
|
+
// app offers a tool to the model by its registered name (`durable/harness/registry.ts:99`
|
|
70
|
+
// @cd32f77). Measured for `entwurf_v2` on the S+native-H wire (receipt 4012c066); the callback
|
|
71
|
+
// spelling follows the same rule and is owed its first LIVE observation.
|
|
72
|
+
"pi-durable": "entwurf_callback",
|
|
67
73
|
};
|
|
68
74
|
/** Where a sibling SENDS the task result. Always the delivery verb, never the birth callback. */
|
|
69
75
|
export const FRESH_CALL_DELIVERY_TOOL = {
|
|
@@ -72,6 +78,7 @@ export const FRESH_CALL_DELIVERY_TOOL = {
|
|
|
72
78
|
copilot: "entwurf-bridge-entwurf_v2",
|
|
73
79
|
omp: "mcp__entwurf_bridge_entwurf_v",
|
|
74
80
|
codex: "mcp__entwurf_bridge__entwurf_v2",
|
|
81
|
+
"pi-durable": "entwurf_v2",
|
|
75
82
|
};
|
|
76
83
|
/**
|
|
77
84
|
* The same dialects, for the tool the framing OFFERS rather than requires.
|
|
@@ -93,6 +100,7 @@ export const FRESH_CALL_PEERS_TOOL = {
|
|
|
93
100
|
copilot: "entwurf-bridge-entwurf_peers",
|
|
94
101
|
omp: "mcp__entwurf_bridge_entwurf_peers",
|
|
95
102
|
codex: "mcp__entwurf_bridge__entwurf_peers",
|
|
103
|
+
"pi-durable": "entwurf_peers",
|
|
96
104
|
};
|
|
97
105
|
/**
|
|
98
106
|
* ONE BACKEND NEEDS TO BE TOLD HOW TO REACH ITS OWN CALLBACK TOOL `[측정 2026-09-18, n=5 + n=3]`.
|
|
@@ -132,6 +140,7 @@ export const FRESH_CALL_TOOL_LOAD_HINT = {
|
|
|
132
140
|
copilot: [],
|
|
133
141
|
omp: [],
|
|
134
142
|
codex: [],
|
|
143
|
+
"pi-durable": [],
|
|
135
144
|
};
|
|
136
145
|
/**
|
|
137
146
|
* The omp bootstrap flag, spelled WITHOUT dashes — the vendor's flag map is keyed by bare
|
|
@@ -311,6 +320,28 @@ export function composeBackendArgs(backend, composition, model, resolveCodexSock
|
|
|
311
320
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
312
321
|
composition.prompt,
|
|
313
322
|
];
|
|
323
|
+
case "pi-durable": {
|
|
324
|
+
// The managed verb (`entwurf pi-durable`) carries the explicit model as pi's own
|
|
325
|
+
// `--provider`/`--model` pair, split at the FIRST `/` (the preflight already refused a model
|
|
326
|
+
// with no provider), the explicit width, and the WHOLE framing as the closed `{v,task}`
|
|
327
|
+
// payload — the durable bootstrap submits it to the root once, after the birth. The
|
|
328
|
+
// caller's address rides the env pair, never this payload.
|
|
329
|
+
const at = model.indexOf("/");
|
|
330
|
+
if (at <= 0 || at === model.length - 1) {
|
|
331
|
+
throw new Error(`fresh-call composition: pi-durable model ${JSON.stringify(model)} is not <provider>/<model id>`);
|
|
332
|
+
}
|
|
333
|
+
return [
|
|
334
|
+
"pi-durable",
|
|
335
|
+
"--provider",
|
|
336
|
+
model.slice(0, at),
|
|
337
|
+
"--model",
|
|
338
|
+
model.slice(at + 1),
|
|
339
|
+
"--width",
|
|
340
|
+
"task-wide",
|
|
341
|
+
`--${OMP_BOOTSTRAP_FLAG}`,
|
|
342
|
+
JSON.stringify({ v: OMP_BOOTSTRAP_VERSION, task: composition.prompt }),
|
|
343
|
+
];
|
|
344
|
+
}
|
|
314
345
|
}
|
|
315
346
|
}
|
|
316
347
|
/**
|
|
@@ -57,7 +57,13 @@ import { resolveNativePushAdapter } from "./native-push/adapter.js";
|
|
|
57
57
|
* exactly the #46 defect on agy, and copilot joined the list only once its own hook wrote one
|
|
58
58
|
* (#82 RAIL 5b). Membership says a marker may EXIST, never that a reply can land: the reply
|
|
59
59
|
* rail is chosen from `nativePushSupported` at the bridge, not from this list. */
|
|
60
|
-
export const META_SENDER_BACKENDS = [
|
|
60
|
+
export const META_SENDER_BACKENDS = [
|
|
61
|
+
"claude-code",
|
|
62
|
+
"antigravity",
|
|
63
|
+
"copilot",
|
|
64
|
+
"omp",
|
|
65
|
+
"pi-durable",
|
|
66
|
+
];
|
|
61
67
|
/**
|
|
62
68
|
* Two live native sessions claim this MCP process as their own. We can SEE both identities but
|
|
63
69
|
* cannot say which one is calling, so we send under neither. Choosing by lookup order, backend
|
|
@@ -76,7 +76,7 @@ export class MetaRecordError extends Error {
|
|
|
76
76
|
* every write, including its own. `./run.sh doctor-meta-bridge` already flags a
|
|
77
77
|
* stale deployed writer — run it after the change.
|
|
78
78
|
*/
|
|
79
|
-
export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp"];
|
|
79
|
+
export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp", "pi-durable"];
|
|
80
80
|
export const META_BACKEND_DESCRIPTORS = {
|
|
81
81
|
"claude-code": {
|
|
82
82
|
backend: "claude-code",
|
|
@@ -189,6 +189,24 @@ export const META_BACKEND_DESCRIPTORS = {
|
|
|
189
189
|
deliveryLevel: "D6",
|
|
190
190
|
nativeIdLabel: "sessionId",
|
|
191
191
|
},
|
|
192
|
+
// The long-running durable app (#129). Graded only as far as its RECORDED receipts reach.
|
|
193
|
+
// sessionId the durable session directory name `<13-digit ms>-<uuid>` that
|
|
194
|
+
// `selectSession` creates and `--continue` reopens
|
|
195
|
+
// (`experimental/durable/sessions.ts:18-55` @cd32f77), surfaced as
|
|
196
|
+
// `view.current().session.id`. Not `pi.provider.sessionId`, which a
|
|
197
|
+
// fork changes.
|
|
198
|
+
// D2 receiver armed: the contact mints the record, names the host as sender, and arms
|
|
199
|
+
// a root doorbell (the receiver marker is owned by the host pid as `pi-durable-host`,
|
|
200
|
+
// which the owner join admits). The addressed-enqueue → idle-wake → root-notice → drain
|
|
201
|
+
// chain has ONE recorded receipt, on the scripted-model + native-harness lab cell
|
|
202
|
+
// (`check-pi-durable-receive`, S+H, V not run) — lab evidence, not a vendor-turn D4–D6
|
|
203
|
+
// acceptance, and no sibling-isolation (D3) or LIVE fresh receipt exists yet.
|
|
204
|
+
"pi-durable": {
|
|
205
|
+
backend: "pi-durable",
|
|
206
|
+
wakeMode: "self-fetch",
|
|
207
|
+
deliveryLevel: "D2",
|
|
208
|
+
nativeIdLabel: "sessionId",
|
|
209
|
+
},
|
|
192
210
|
};
|
|
193
211
|
// ---------------------------------------------------------------------------
|
|
194
212
|
// Validation helpers (crash, don't warn)
|
|
@@ -261,7 +279,15 @@ function isoNow(now) {
|
|
|
261
279
|
/** The one live identity schema number. */
|
|
262
280
|
export const META_SCHEMA_VERSION_V3 = 3;
|
|
263
281
|
/** Every backend admitted by the one V3 record-citizen schema. */
|
|
264
|
-
export const META_CITIZEN_BACKENDS = [
|
|
282
|
+
export const META_CITIZEN_BACKENDS = [
|
|
283
|
+
"claude-code",
|
|
284
|
+
"antigravity",
|
|
285
|
+
"codex",
|
|
286
|
+
"copilot",
|
|
287
|
+
"omp",
|
|
288
|
+
"pi-durable",
|
|
289
|
+
"pi",
|
|
290
|
+
];
|
|
265
291
|
export function requireCitizenBackend(value) {
|
|
266
292
|
if (typeof value !== "string" || !META_CITIZEN_BACKENDS.includes(value)) {
|
|
267
293
|
throw new MetaRecordError(`meta-record "backend" must be one of ${META_CITIZEN_BACKENDS.join(" | ")} (got ${describe(value)}).`);
|