@junghanacs/entwurf 0.23.0 → 0.24.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 +3 -1
- package/BASELINE.md +2 -0
- package/CHANGELOG.md +307 -0
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +43 -2
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +97 -18
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +33 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +7 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +13 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -1
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +15 -13
- package/pi-extensions/entwurf-control.ts +56 -0
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +20 -10
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +12 -4
- package/pi-extensions/lib/herdr-placement.ts +13 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/run.sh +327 -246
- package/scripts/check-acp-backend-preflight.ts +117 -9
- package/scripts/check-acp-carrier-augment.ts +6 -4
- package/scripts/check-acp-event-mapper.ts +17 -6
- package/scripts/check-acp-prompt-builder.ts +17 -9
- package/scripts/check-acp-prompt-lifecycle.ts +23 -6
- package/scripts/check-acp-session-reuse.ts +64 -51
- package/scripts/check-acp-session-store.ts +31 -8
- package/scripts/check-acp-stop-reason.ts +20 -2
- package/scripts/check-acp-stream-hooks.ts +22 -5
- package/scripts/check-acp-usage-accounting.ts +42 -18
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +45 -5
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +22 -6
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-pi-launch.ts +361 -0
- package/scripts/check-release-gate-outcomes.ts +7 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +11 -0
- package/scripts/mutants/herdr-plugin-build.json +1 -1
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +56 -28
- package/scripts/smoke-herdr-raw-install-live.sh +877 -0
- package/scripts/smoke-omp-fresh-live.ts +4 -2
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-provider-state.sh +2 -2
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -391
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -44,8 +44,8 @@ import { type ChildProcessByStdio, spawn } from "node:child_process";
|
|
|
44
44
|
import { readFileSync } from "node:fs";
|
|
45
45
|
import { Readable, Writable } from "node:stream";
|
|
46
46
|
import { ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
|
|
47
|
-
import type { Api, AssistantMessage,
|
|
48
|
-
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
47
|
+
import type { Api, AssistantMessage, Model, SimpleStreamOptions, TranscriptContext } from "@earendil-works/pi-ai";
|
|
48
|
+
import { createAssistantMessageEventStream, getCurrentTools } from "@earendil-works/pi-ai";
|
|
49
49
|
import {
|
|
50
50
|
type AcpClientHandlers,
|
|
51
51
|
type AcpConnectionLike,
|
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
resolveLifecyclePolicy,
|
|
82
82
|
writeSessionRecord,
|
|
83
83
|
} from "./session-store.js";
|
|
84
|
-
import { assertExcludeToolsHonored
|
|
84
|
+
import { assertExcludeToolsHonored } from "./tool-surface.js";
|
|
85
85
|
|
|
86
86
|
// Bootstrap boundaries ONLY. initialize / newSession / set-model are handshake
|
|
87
87
|
// steps that make no model progress, so a stuck one is a dead session and a cold
|
|
@@ -137,7 +137,7 @@ function finiteOrZero(value: unknown): number {
|
|
|
137
137
|
* "the accounting-grade figure per the SDK" — it also counts Task subagents,
|
|
138
138
|
* sidechains, and INTERNAL CALLS SUCH AS COMPACTION, so its rows "can total more
|
|
139
139
|
* than `token_count`" and are "the fuller picture, not a decomposition of it"
|
|
140
|
-
* (read at claude-agent-acp 0.
|
|
140
|
+
* (read at claude-agent-acp 0.79.0 `dist/acp-agent.js:6551-6571`). The narrower
|
|
141
141
|
* `PromptResponse.usage` (== `quota.token_count`) is the MAIN AGENT LOOP only.
|
|
142
142
|
*
|
|
143
143
|
* The wider one is the right numerator because the denominator already has that
|
|
@@ -1108,7 +1108,7 @@ async function applyProviderPayloadHook<T extends { sessionId: string }>(
|
|
|
1108
1108
|
*/
|
|
1109
1109
|
export function streamShellAcp(
|
|
1110
1110
|
model: Model<Api>,
|
|
1111
|
-
context:
|
|
1111
|
+
context: TranscriptContext,
|
|
1112
1112
|
options?: SimpleStreamOptions,
|
|
1113
1113
|
): ReturnType<typeof createAssistantMessageEventStream> {
|
|
1114
1114
|
return streamAcpTurn(model, context, options, defaultDeps());
|
|
@@ -1117,7 +1117,7 @@ export function streamShellAcp(
|
|
|
1117
1117
|
/** The seam-aware turn driver. `streamShellAcp` calls this with the real deps. */
|
|
1118
1118
|
export function streamAcpTurn(
|
|
1119
1119
|
model: Model<Api>,
|
|
1120
|
-
context:
|
|
1120
|
+
context: TranscriptContext,
|
|
1121
1121
|
options: SimpleStreamOptions | undefined,
|
|
1122
1122
|
deps: AcpTurnDeps,
|
|
1123
1123
|
): ReturnType<typeof createAssistantMessageEventStream> {
|
|
@@ -1237,7 +1237,7 @@ export function streamAcpTurn(
|
|
|
1237
1237
|
// Writing zeros is not a placeholder for a better number we could compute:
|
|
1238
1238
|
// the per-request partition is genuinely absent from the wire. The vendor
|
|
1239
1239
|
// builds it in `lastAssistantUsage` and sends only its scalar sum (read at
|
|
1240
|
-
// claude-agent-acp 0.
|
|
1240
|
+
// claude-agent-acp 0.79.0 `dist/acp-agent.js:3920-3945`) — #96.
|
|
1241
1241
|
//
|
|
1242
1242
|
// But silence is NOT the resting state. A cache miss the operator never sees
|
|
1243
1243
|
// is a false reading, not a modest one: a session can run for hours believing
|
|
@@ -1356,9 +1356,9 @@ export function streamAcpTurn(
|
|
|
1356
1356
|
// between them is exactly why the notice below names a MECHANISM and
|
|
1357
1357
|
// never a cause: claude-agent-acp's `conversation_reset` handler only
|
|
1358
1358
|
// switches the SDK to a fresh conversation and touches no cost at all
|
|
1359
|
-
// (read at 0.
|
|
1359
|
+
// (read at 0.79.0 `dist/acp-agent.js:4349-4356`), while claude-agent-sdk
|
|
1360
1360
|
// separately documents that "a mid-session /clear resets the running
|
|
1361
|
-
// total" (read at 0.3.
|
|
1361
|
+
// total" (read at 0.3.274 `sdk.d.ts:5342`). A reset therefore PLAUSIBLY
|
|
1362
1362
|
// explains a backwards total, but nothing here has MEASURED that it did,
|
|
1363
1363
|
// and asserting the cause would be the same unmeasured claim this lane
|
|
1364
1364
|
// exists to end.
|
|
@@ -1510,7 +1510,17 @@ export function streamAcpTurn(
|
|
|
1510
1510
|
// rather than lie to the model. Uses the RESOLVED tool surface (S2g) so an
|
|
1511
1511
|
// operator-narrowed `tools` is what the truthfulness check honors.
|
|
1512
1512
|
try {
|
|
1513
|
-
|
|
1513
|
+
// pi 0.86 folded `Context.systemPrompt`/`Context.tools` into a leading
|
|
1514
|
+
// `role:"system"` message (`normalizeContext`, pi-ai
|
|
1515
|
+
// `utils/transcript.ts`), so a provider only ever sees a
|
|
1516
|
+
// `TranscriptContext`. The active surface is therefore REPLAYED from the
|
|
1517
|
+
// system messages — `getCurrentTools` applies every toolsAdded/toolsRemoved
|
|
1518
|
+
// delta in order. No `?? [...PI_BUILTIN_BACKED_TOOLS]` fallback: under 0.86
|
|
1519
|
+
// the old `context.tools` read was ALWAYS undefined, so that fallback
|
|
1520
|
+
// silently declared the full builtin set and the truthfulness preflight
|
|
1521
|
+
// could never fire (false green). An empty replayed list means the operator
|
|
1522
|
+
// excluded everything — keep that honest and let the preflight reject.
|
|
1523
|
+
const activeToolNames = getCurrentTools(context.messages).map((t) => t.name);
|
|
1514
1524
|
assertExcludeToolsHonored(activeToolNames, { backend: adapter.backend, tools: config.tools });
|
|
1515
1525
|
} catch (err) {
|
|
1516
1526
|
finishError(err, false);
|
|
@@ -8,16 +8,26 @@
|
|
|
8
8
|
//
|
|
9
9
|
// S2c/S2d boundary (GPT S2c Q2): this is CONVERSATION TRANSCRIPT PASSTHROUGH, not
|
|
10
10
|
// rich-carrier identity injection. Deliberately EXCLUDED here (all S2d):
|
|
11
|
-
// -
|
|
11
|
+
// - the system prompt — never read into the prompt or `_meta.systemPrompt`
|
|
12
12
|
// (the billing carrier stays absent — NEXT §S2-scout 핀1);
|
|
13
13
|
// - `~/AGENTS.md` / cwd AGENTS / bridge identity narrative;
|
|
14
14
|
// - first-user-message augment + project-context de-dup;
|
|
15
|
-
// -
|
|
15
|
+
// - the declared tool surface — the ACP child tool surface is the S2b
|
|
16
16
|
// `_meta.claudeCode.options` SSOT, never re-sent here.
|
|
17
|
+
// Since pi 0.86 both of those ride a leading `role:"system"` message inside the
|
|
18
|
+
// `TranscriptContext` (pi-ai `normalizeContext`) rather than their own `Context`
|
|
19
|
+
// fields, so the exclusion is enforced by `renderMessage`'s explicit
|
|
20
|
+
// `case "system"` below, not by simply not reading two fields.
|
|
17
21
|
// Structured tool replay is also excluded: tool calls/results render as plain
|
|
18
22
|
// transcript text, never as ACP tool invocations (the child runs its own tools).
|
|
19
23
|
|
|
20
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
AssistantMessage,
|
|
26
|
+
Message,
|
|
27
|
+
ToolResultMessage,
|
|
28
|
+
TranscriptContext,
|
|
29
|
+
UserMessage,
|
|
30
|
+
} from "@earendil-works/pi-ai";
|
|
21
31
|
|
|
22
32
|
// MUST equal event-mapper.ts `LIFECYCLE_NOTICE_SIGNATURE` (the SSOT/producer).
|
|
23
33
|
// It is mirrored, not imported: the strip-types deterministic gates load these
|
|
@@ -80,16 +90,26 @@ function renderMessage(message: Message): string | undefined {
|
|
|
80
90
|
const tag = message.isError ? "Tool error" : "Tool result";
|
|
81
91
|
return text ? `${tag} (${message.toolName}): ${text}` : undefined;
|
|
82
92
|
}
|
|
93
|
+
case "system":
|
|
94
|
+
// The never-forward-systemPrompt invariant, stated rather than inherited.
|
|
95
|
+
// Since pi 0.86 the system prompt and the declared tool surface arrive AS
|
|
96
|
+
// transcript messages (`normalizeContext`), so what used to be "we simply
|
|
97
|
+
// do not read `context.systemPrompt`" is now an active decision on every
|
|
98
|
+
// turn. `default` would already skip it; the explicit case exists so
|
|
99
|
+
// deleting it is a visible change and a new pi role cannot quietly start
|
|
100
|
+
// leaking the prompt into the ACP child's transcript.
|
|
101
|
+
return undefined;
|
|
83
102
|
default:
|
|
84
103
|
return undefined;
|
|
85
104
|
}
|
|
86
105
|
}
|
|
87
106
|
|
|
88
107
|
/**
|
|
89
|
-
* Flatten a pi
|
|
90
|
-
*
|
|
108
|
+
* Flatten a pi transcript into a single transcript string. Excludes the system
|
|
109
|
+
* prompt and the declared tool surface by construction — since 0.86 both ride
|
|
110
|
+
* the leading `role:"system"` message, which `renderMessage` drops explicitly.
|
|
91
111
|
*/
|
|
92
|
-
export function contextTranscript(context:
|
|
112
|
+
export function contextTranscript(context: TranscriptContext): string {
|
|
93
113
|
const lines: string[] = [];
|
|
94
114
|
for (const message of context.messages) {
|
|
95
115
|
const line = renderMessage(message);
|
|
@@ -99,11 +119,11 @@ export function contextTranscript(context: Context): string {
|
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
/**
|
|
102
|
-
* Convert a pi
|
|
122
|
+
* Convert a pi transcript into the ACP `prompt` array (a single text block holding
|
|
103
123
|
* the flattened transcript). Empty history yields an empty array — the caller
|
|
104
124
|
* decides whether that is a hard error.
|
|
105
125
|
*/
|
|
106
|
-
export function contextToAcpPrompt(context:
|
|
126
|
+
export function contextToAcpPrompt(context: TranscriptContext): AcpTextBlock[] {
|
|
107
127
|
const transcript = contextTranscript(context);
|
|
108
128
|
if (!transcript) return [];
|
|
109
129
|
return [{ type: "text", text: transcript }];
|
|
@@ -129,7 +149,7 @@ export type AcpBootstrapPath = "new" | "reuse" | "resume" | "load";
|
|
|
129
149
|
* (reuse/resume/load): re-sending the whole transcript there would duplicate
|
|
130
150
|
* history the backend already remembers.
|
|
131
151
|
*/
|
|
132
|
-
export function latestUserDelta(context:
|
|
152
|
+
export function latestUserDelta(context: TranscriptContext): AcpTextBlock[] {
|
|
133
153
|
let lastAssistantIdx = -1;
|
|
134
154
|
for (let i = context.messages.length - 1; i >= 0; i--) {
|
|
135
155
|
if (context.messages[i].role === "assistant") {
|
|
@@ -160,7 +180,7 @@ export function latestUserDelta(context: Context): AcpTextBlock[] {
|
|
|
160
180
|
* `contextMessageSignatures` prefix-compat gate (mismatch → fall back to
|
|
161
181
|
* `"new"` + full transcript); this pure function only splits the scope.
|
|
162
182
|
*/
|
|
163
|
-
export function buildAcpPrompt(context:
|
|
183
|
+
export function buildAcpPrompt(context: TranscriptContext, bootstrapPath: AcpBootstrapPath): AcpTextBlock[] {
|
|
164
184
|
switch (bootstrapPath) {
|
|
165
185
|
case "new":
|
|
166
186
|
return contextToAcpPrompt(context);
|
|
@@ -60,8 +60,8 @@ export type AcpPiStreamState = {
|
|
|
60
60
|
*
|
|
61
61
|
* Last write wins, never a sum: both are latest session-level observations, and
|
|
62
62
|
* one turn can legitimately see several (claude emits one per `result` message,
|
|
63
|
-
* including a sub-agent's own — read at claude-agent-acp 0.
|
|
64
|
-
* `dist/acp-agent.js:
|
|
63
|
+
* including a sub-agent's own — read at claude-agent-acp 0.79.0
|
|
64
|
+
* `dist/acp-agent.js:3504-3519`), each carrying that result's current values.
|
|
65
65
|
*/
|
|
66
66
|
observedSessionCostUsd?: number;
|
|
67
67
|
observedContextOccupancyTokens?: number;
|
|
@@ -346,9 +346,9 @@ export function applyAcpSessionUpdate(
|
|
|
346
346
|
case "usage_update": {
|
|
347
347
|
// `used` is OCCUPANCY-shaped — the backend's post-turn context size, not
|
|
348
348
|
// the prompt response's turn aggregate. Claude sends `lastAssistantTotalUsage`
|
|
349
|
-
// as `used` (read at claude-agent-acp 0.
|
|
350
|
-
// `dist/acp-agent.js:
|
|
351
|
-
// latest assistant snapshot (`:
|
|
349
|
+
// as `used` (read at claude-agent-acp 0.79.0
|
|
350
|
+
// `dist/acp-agent.js:3934-3945`), after constructing that scalar from the
|
|
351
|
+
// latest assistant snapshot (`:3916-3929`). pi reads `usage.totalTokens` as
|
|
352
352
|
// exactly that occupancy (`calculateContextTokens(usage) = usage.totalTokens
|
|
353
353
|
// || input + output + cacheRead + cacheWrite`, read at pi-coding-agent
|
|
354
354
|
// `dist/core/compaction/compaction.js:86-88`). The assignment below is the
|
|
@@ -27,7 +27,7 @@ import { createHash } from "node:crypto";
|
|
|
27
27
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
29
|
import { join } from "node:path";
|
|
30
|
-
import type {
|
|
30
|
+
import type { Message, ToolResultMessage, TranscriptContext } from "@earendil-works/pi-ai";
|
|
31
31
|
import type { AcpBootstrapPath } from "./context.js";
|
|
32
32
|
|
|
33
33
|
// MUST equal event-mapper.ts `LIFECYCLE_NOTICE_SIGNATURE` (the SSOT/producer).
|
|
@@ -236,7 +236,7 @@ function rawMessageSignature(message: Message): string {
|
|
|
236
236
|
* record never stores raw prompt/tool text — the prefix check works the same on
|
|
237
237
|
* the digest array (GPT `c617cb` hardening).
|
|
238
238
|
*/
|
|
239
|
-
export function contextMessageSignatures(context:
|
|
239
|
+
export function contextMessageSignatures(context: TranscriptContext): string[] {
|
|
240
240
|
return context.messages.map((m: Message) => sha256(rawMessageSignature(m)));
|
|
241
241
|
}
|
|
242
242
|
|
|
@@ -154,7 +154,8 @@ export function buildClaudeSessionMeta(
|
|
|
154
154
|
// claude-agent-acp also accepts a STRING PATH, and at 0.76.0 the readFile +
|
|
155
155
|
// JSON.parse for that form moved out of `resolvedProvider` up onto the
|
|
156
156
|
// unconditional `session/new` path — a shape that WOULD reach us, and does not,
|
|
157
|
-
// only because we never hand it a path.
|
|
157
|
+
// only because we never hand it a path. Still on that path at 0.79.0
|
|
158
|
+
// (`dist/acp-agent.js:6010-6012`). Keep it inline: a path would put file
|
|
158
159
|
// IO, and its failure modes, inside every session creation.
|
|
159
160
|
settings: {
|
|
160
161
|
permissions: {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback-env — the launch-computed pair that lets a fresh sibling call home
|
|
3
|
+
* without copying a garden id or nonce out of prose.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. Until this seam, `composeFreshCallFraming` put
|
|
6
|
+
* `target=<caller gid>` and `message=<nonce>` into the first-turn prompt, and
|
|
7
|
+
* the sibling had to retype them into `entwurf_v2`. That is the one place in
|
|
8
|
+
* the garden Hard Rule 2 (record is the sole address axis) was violated: the
|
|
9
|
+
* transcript became the address carrier, and a model that dropped a `$` or a
|
|
10
|
+
* hex nibble produced a refused dispatch that looked like a flaky backend.
|
|
11
|
+
*
|
|
12
|
+
* WHAT TRAVELS. Two process-env names, injected by the launcher next to the
|
|
13
|
+
* identity scrub (`PI_SESSION_ID=` / `PI_AGENT_ID=`), never as a general env
|
|
14
|
+
* carrier. The no-arg `entwurf_callback` verb reads BOTH from its own process
|
|
15
|
+
* and refuses by name when they are absent, malformed, or this process is a
|
|
16
|
+
* Codex-provenance bridge (window env never reaches that MCP child — measured).
|
|
17
|
+
*
|
|
18
|
+
* This module does not dispatch. It parses, validates, and formats. Launchers
|
|
19
|
+
* and the verb share it so a drifted regex cannot inject what the verb rejects.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { SESSION_ID_RE } from "./session-id.js";
|
|
23
|
+
|
|
24
|
+
export const CALLBACK_TARGET_ENV = "ENTWURF_CALLBACK_TARGET";
|
|
25
|
+
export const CALLBACK_NONCE_ENV = "ENTWURF_CALLBACK_NONCE";
|
|
26
|
+
|
|
27
|
+
/** Exactly what `mintNonce` emits (`fresh-call-composition.ts`): `mux-fresh-call-` + 12 random
|
|
28
|
+
* bytes as hex. ONE minter, ONE grammar — both rails call the same `mintNonce`, so a second
|
|
29
|
+
* accepted prefix would be a spelling no launcher produces and this leaf exists to refuse
|
|
30
|
+
* exactly that. A gate fixture is an oracle and never a reason to widen the subject. */
|
|
31
|
+
export const CALLBACK_NONCE_RE = /^mux-fresh-call-[0-9a-f]{24}$/;
|
|
32
|
+
|
|
33
|
+
export type CallbackEnvReject = "callback-env-absent" | "callback-env-malformed" | "codex-callback-env-unsupported";
|
|
34
|
+
|
|
35
|
+
export type CallbackEnvRead = { ok: true; target: string; nonce: string } | { ok: false; reason: CallbackEnvReject };
|
|
36
|
+
|
|
37
|
+
function parseCallbackPair(target: unknown, nonce: unknown): CallbackEnvRead {
|
|
38
|
+
if (typeof target !== "string" || typeof nonce !== "string") {
|
|
39
|
+
return { ok: false, reason: "callback-env-malformed" };
|
|
40
|
+
}
|
|
41
|
+
if (!SESSION_ID_RE.test(target) || !CALLBACK_NONCE_RE.test(nonce)) {
|
|
42
|
+
return { ok: false, reason: "callback-env-malformed" };
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, target, nonce };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format the two `KEY=value` assignments a launcher injects. Throws rather than
|
|
49
|
+
* emitting a pair the verb would refuse — a launch that cannot name its caller
|
|
50
|
+
* must not open a window whose first action is a guaranteed reject.
|
|
51
|
+
*/
|
|
52
|
+
export function callbackEnvAssignments(params: { target: string; nonce: string }): readonly [string, string] {
|
|
53
|
+
const parsed = parseCallbackPair(params.target, params.nonce);
|
|
54
|
+
if (!parsed.ok) {
|
|
55
|
+
throw new Error(`callback-env: refusing to inject ${parsed.reason}`);
|
|
56
|
+
}
|
|
57
|
+
return [`${CALLBACK_TARGET_ENV}=${parsed.target}`, `${CALLBACK_NONCE_ENV}=${parsed.nonce}`];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read the pair from a process environment. Codex provenance is a named refuse
|
|
62
|
+
* even when the pair is well-formed: that bridge is an app-server child, and a
|
|
63
|
+
* pane-injected value would be the wrong citizen's (or last-launch-wins).
|
|
64
|
+
*
|
|
65
|
+
* Both names absent → `callback-env-absent`. One present, or either failing
|
|
66
|
+
* grammar → `callback-env-malformed`. No fallback to a model-supplied target.
|
|
67
|
+
*/
|
|
68
|
+
export function readCallbackEnv(env: NodeJS.ProcessEnv = process.env): CallbackEnvRead {
|
|
69
|
+
if (env.ENTWURF_BRIDGE_NATIVE_HOST?.trim() === "codex") {
|
|
70
|
+
return { ok: false, reason: "codex-callback-env-unsupported" };
|
|
71
|
+
}
|
|
72
|
+
const target = env[CALLBACK_TARGET_ENV];
|
|
73
|
+
const nonce = env[CALLBACK_NONCE_ENV];
|
|
74
|
+
if (target === undefined && nonce === undefined) {
|
|
75
|
+
return { ok: false, reason: "callback-env-absent" };
|
|
76
|
+
}
|
|
77
|
+
return parseCallbackPair(target, nonce);
|
|
78
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* compaction-send-guard — whether a control-socket `send` may call `pi.sendMessage`.
|
|
3
3
|
*
|
|
4
|
-
* Pi 0.
|
|
4
|
+
* Pi 0.86.0 has no public `ExtensionContext.isCompacting()` (`[측정 2026-09-20]` the
|
|
5
|
+
* installed `dist/core/extensions/types.d.ts` declares `isIdle(): boolean` at :233 and
|
|
6
|
+
* `signal` at :237, and carries no `isCompacting` member at all; the identifier exists
|
|
7
|
+
* only inside the interactive/rpc modes' own bundles). Compaction is non-idle
|
|
5
8
|
* (`AgentSession.isIdle` = no agent run AND not compacting) but `sendCustomMessage`
|
|
6
9
|
* does not refuse it. A non-streaming `triggerTurn:true` send starts `_runAgentPrompt`
|
|
7
10
|
* while `compact()` is rewriting the session tree — the field path that can make
|
|
@@ -111,7 +111,7 @@ export type DispatchLiveness = (typeof DISPATCH_LIVENESSES)[number];
|
|
|
111
111
|
// unreachable (the native-push rail measures it — entwurf-v2-decider.ts). Widening
|
|
112
112
|
// THIS set is a deliberate future decision, gated by a real compatible
|
|
113
113
|
// control-socket predicate — never by silently mapping sessions to dead/indeterminate
|
|
114
|
-
// (R1 핵심).
|
|
114
|
+
// (R1 핵심). pi-extensions/lib/entwurf-facts.test.ts pins this == ["pi"] and asserts the native-push
|
|
115
115
|
// domain is disjoint from it.
|
|
116
116
|
export const LIVENESS_DOMAIN_BACKENDS = ["pi"] as const;
|
|
117
117
|
export type LivenessDomainBackend = (typeof LIVENESS_DOMAIN_BACKENDS)[number];
|
|
@@ -125,7 +125,7 @@ export function isLivenessSupported(backend: string): boolean {
|
|
|
125
125
|
// live vendor app-server target probe), NOT a control socket. This domain is DISJOINT
|
|
126
126
|
// from LIVENESS_DOMAIN_BACKENDS: an Antigravity or Codex session is `unsupported` on
|
|
127
127
|
// the socket FACT axis (entwurf_peers) yet independently measured on the native-push
|
|
128
|
-
// axis. The two are separate rails on purpose —
|
|
128
|
+
// axis. The two are separate rails on purpose — pi-extensions/lib/entwurf-facts.test.ts pins both sets
|
|
129
129
|
// and asserts their intersection is ∅ (a backend can never be in both domains).
|
|
130
130
|
export const NATIVE_PUSH_BACKENDS = ["antigravity", "codex"] as const;
|
|
131
131
|
export type NativePushBackend = (typeof NATIVE_PUSH_BACKENDS)[number];
|
|
@@ -82,7 +82,7 @@ export const LOCK_SUFFIX = ".lock";
|
|
|
82
82
|
export const LOCK_OWNER = "entwurf_v2" as const;
|
|
83
83
|
|
|
84
84
|
/** The reject reason a lock conflict maps to. Kept as a literal here (the lock
|
|
85
|
-
* primitive stays decoupled from the full contract); `
|
|
85
|
+
* primitive stays decoupled from the full contract); `pi-extensions/lib/entwurf-v2-lock.test.ts`
|
|
86
86
|
* cross-checks it against the contract's ENTWURF_V2_REJECT_REASONS so the two
|
|
87
87
|
* cannot drift. */
|
|
88
88
|
export const LOCK_CONFLICT_REASON = "target-locked" as const;
|
|
@@ -201,7 +201,7 @@ export async function executeDispatch(
|
|
|
201
201
|
/**
|
|
202
202
|
* 5d-2a: the deps `runEntwurfV2` joins. `decide` is the WHOLE decider as ONE injected
|
|
203
203
|
* function — NOT `DispatchDeciderDeps`. The runner does not re-validate the 5b decider
|
|
204
|
-
* logic (that is `
|
|
204
|
+
* logic (that is `pi-extensions/lib/entwurf-v2-decider.test.ts`'s job); it proves only the decide→execute
|
|
205
205
|
* COMPOSITION contract over a fake `decide`. Production wraps the real decider as
|
|
206
206
|
* `decide: (input) => decideDispatch(input, productionDeciderDeps)` (assembled in 5d-2b),
|
|
207
207
|
* so the runner stays gate-provable without any decider IO seam leaking in.
|
|
@@ -54,6 +54,17 @@ export type FreshCallBackend = (typeof FRESH_CALL_BACKENDS)[number];
|
|
|
54
54
|
* permission dialect: omp's approval layer consults the same minted string (`source-audit.md`).
|
|
55
55
|
*/
|
|
56
56
|
export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
|
|
57
|
+
pi: "entwurf_callback",
|
|
58
|
+
"claude-code": "mcp__entwurf-bridge__entwurf_callback",
|
|
59
|
+
copilot: "entwurf-bridge-entwurf_callback",
|
|
60
|
+
omp: "mcp__entwurf_bridge_entwurf_callback",
|
|
61
|
+
// Codex's tool process is the operator app-server, not the pane: the no-arg
|
|
62
|
+
// verb refuses there by name. First action stays the delivery verb with args.
|
|
63
|
+
codex: "mcp__entwurf_bridge__entwurf_v2",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Where a sibling SENDS the task result. Always the delivery verb, never the birth callback. */
|
|
67
|
+
export const FRESH_CALL_DELIVERY_TOOL: Record<FreshCallBackend, string> = {
|
|
57
68
|
pi: "entwurf_v2",
|
|
58
69
|
"claude-code": "mcp__entwurf-bridge__entwurf_v2",
|
|
59
70
|
copilot: "entwurf-bridge-entwurf_v2",
|
|
@@ -101,8 +112,11 @@ export const FRESH_CALL_PEERS_TOOL: Record<FreshCallBackend, string> = {
|
|
|
101
112
|
* `[측정, n=3, same model]` adding the sentence below produced 3/3 callbacks. It is a FACT about the
|
|
102
113
|
* runtime, in the register the rest of this framing uses since the prohibitions came out: it grants
|
|
103
114
|
* nothing, forbids nothing, and names the one call that turns a listed name into a callable tool.
|
|
104
|
-
*
|
|
105
|
-
*
|
|
115
|
+
* ALL THREE tools ride ONE `ToolSearch` select — the birth callback, the DELIVERY verb the last
|
|
116
|
+
* line of the framing names, and the peers listing the corroboration sentence offers. The delivery
|
|
117
|
+
* verb has to be in the same select or the deferral simply moves from the first action to the last:
|
|
118
|
+
* a sibling that finishes its task and cannot call `entwurf_v2` is the same production silence,
|
|
119
|
+
* spent instead of saved.
|
|
106
120
|
*
|
|
107
121
|
* Every other backend gets an EMPTY array, deliberately: pi has no deferred-tool surface, and the
|
|
108
122
|
* other three were never measured to need it. A hint invented for a runtime nobody probed would be
|
|
@@ -112,7 +126,7 @@ export const FRESH_CALL_TOOL_LOAD_HINT: Record<FreshCallBackend, readonly string
|
|
|
112
126
|
pi: [],
|
|
113
127
|
"claude-code": [
|
|
114
128
|
"If ${callbackTool} is not callable yet, the entwurf-bridge server is still connecting or its",
|
|
115
|
-
'tools are deferred: load them first with ToolSearch("select:${callbackTool},${peersTool}") —',
|
|
129
|
+
'tools are deferred: load them first with ToolSearch("select:${callbackTool},${deliveryTool},${peersTool}") —',
|
|
116
130
|
"it waits for the server — then call ${callbackTool}.",
|
|
117
131
|
],
|
|
118
132
|
copilot: [],
|
|
@@ -144,22 +158,22 @@ export const OMP_BOOTSTRAP_FLAG = "entwurf-bootstrap";
|
|
|
144
158
|
|
|
145
159
|
/** Payload grammar version, matched exactly by the decoder. A bump means a stale installed
|
|
146
160
|
* unit, which is the one thing `doctor-omp-bridge` exists to say out loud. */
|
|
147
|
-
export const OMP_BOOTSTRAP_VERSION =
|
|
161
|
+
export const OMP_BOOTSTRAP_VERSION = 2;
|
|
148
162
|
|
|
149
163
|
/**
|
|
150
164
|
* The whole of what a fresh omp sibling is launched with.
|
|
151
165
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
166
|
+
* TWO FIELDS, CLOSED. Address (target + nonce) rides process env, not this payload — a second
|
|
167
|
+
* copy here would be a second address axis. The decoder refuses an unknown key, so this object
|
|
168
|
+
* is the entire remaining contract: the task released after the no-arg callback succeeds.
|
|
169
|
+
* `callerGardenId` / `nonce` stay on the function so launcher call sites do not grow a second
|
|
170
|
+
* shape; they are not serialised.
|
|
157
171
|
*/
|
|
158
172
|
export function buildOmpBootstrapPayload(params: { callerGardenId: string; nonce: string; task: string }): string {
|
|
173
|
+
void params.callerGardenId;
|
|
174
|
+
void params.nonce;
|
|
159
175
|
return JSON.stringify({
|
|
160
176
|
v: OMP_BOOTSTRAP_VERSION,
|
|
161
|
-
target: params.callerGardenId,
|
|
162
|
-
nonce: params.nonce,
|
|
163
177
|
task: params.task,
|
|
164
178
|
});
|
|
165
179
|
}
|
|
@@ -298,7 +312,11 @@ export function composeBackendArgs(
|
|
|
298
312
|
case "pi":
|
|
299
313
|
return [composition.prompt, "--entwurf-control", "--model", model];
|
|
300
314
|
case "claude-code":
|
|
301
|
-
return [
|
|
315
|
+
return [
|
|
316
|
+
composition.prompt,
|
|
317
|
+
`--allowedTools=${FRESH_CALL_CALLBACK_TOOL["claude-code"]},${FRESH_CALL_DELIVERY_TOOL["claude-code"]}`,
|
|
318
|
+
`--model=${model}`,
|
|
319
|
+
];
|
|
302
320
|
case "copilot":
|
|
303
321
|
return ["copilot", "--interactive", composition.prompt, "--model", model, "--yolo"];
|
|
304
322
|
case "omp":
|
|
@@ -354,28 +372,44 @@ export function composeFreshCallFraming(params: {
|
|
|
354
372
|
* default: a sibling told nothing about where it is would be told something false by silence. */
|
|
355
373
|
openingLine: string;
|
|
356
374
|
}): string[] {
|
|
357
|
-
const
|
|
375
|
+
const birthTool = FRESH_CALL_CALLBACK_TOOL[params.backend];
|
|
376
|
+
const deliveryTool = FRESH_CALL_DELIVERY_TOOL[params.backend];
|
|
358
377
|
const peersTool = FRESH_CALL_PEERS_TOOL[params.backend];
|
|
359
378
|
// The hint is a per-backend TEMPLATE so the two tool dialects stay the single source of their
|
|
360
379
|
// own spelling here too — a hint that hard-coded Claude's name would go stale the day a dialect
|
|
361
380
|
// moves, and it would go stale silently, in the one sentence a stuck child depends on.
|
|
362
381
|
const loadHint = FRESH_CALL_TOOL_LOAD_HINT[params.backend].map((line) =>
|
|
363
|
-
line
|
|
382
|
+
line
|
|
383
|
+
.replaceAll("${callbackTool}", birthTool)
|
|
384
|
+
.replaceAll("${deliveryTool}", deliveryTool)
|
|
385
|
+
.replaceAll("${peersTool}", peersTool),
|
|
364
386
|
);
|
|
365
387
|
const hintBlock = loadHint.length === 0 ? [] : ["", ...loadHint];
|
|
366
388
|
if (params.openingLine.length === 0) {
|
|
367
389
|
throw new Error("fresh-call composition: openingLine is empty — the rail must state where it placed the sibling");
|
|
368
390
|
}
|
|
391
|
+
const firstAction =
|
|
392
|
+
params.backend === "codex"
|
|
393
|
+
? [
|
|
394
|
+
`FIRST ACTION, before reading files or anything else: call ${birthTool} with ` +
|
|
395
|
+
`target=${params.callerGardenId}, intent=fire-and-forget, wants_reply=false, and ` +
|
|
396
|
+
`message set to exactly ${params.nonce} — that string alone, nothing added.`,
|
|
397
|
+
`${params.nonce} is a correlation tag this caller minted for this one launch: it names no`,
|
|
398
|
+
"secret and grants no access. Sending it is how the caller learns the garden id you were born",
|
|
399
|
+
"with, which it has no other way to know. Your own record does not carry the caller's address,",
|
|
400
|
+
"so reporting your identity here does not reach it — that tool call does.",
|
|
401
|
+
"entwurf_callback is refused on this rail: the tool process is the operator app-server, not this pane.",
|
|
402
|
+
]
|
|
403
|
+
: [
|
|
404
|
+
`FIRST ACTION, before reading files or anything else: call ${birthTool} with no arguments.`,
|
|
405
|
+
"That call is how the caller learns the garden id you were born with. There are no",
|
|
406
|
+
"parameters: a target or message you type would be a second address axis.",
|
|
407
|
+
"Your own record does not carry the caller's address, so reporting your identity here does not reach it — that tool call does.",
|
|
408
|
+
];
|
|
369
409
|
return [
|
|
370
410
|
params.openingLine,
|
|
371
411
|
"",
|
|
372
|
-
|
|
373
|
-
`target=${params.callerGardenId}, intent=fire-and-forget, wants_reply=false, and ` +
|
|
374
|
-
`message set to exactly ${params.nonce} — that string alone, nothing added.`,
|
|
375
|
-
`${params.nonce} is a correlation tag this caller minted for this one launch: it names no`,
|
|
376
|
-
"secret and grants no access. Sending it is how the caller learns the garden id you were born",
|
|
377
|
-
"with, which it has no other way to know. Your own record does not carry the caller's address,",
|
|
378
|
-
"so reporting your identity here does not reach it — that tool call does.",
|
|
412
|
+
...firstAction,
|
|
379
413
|
"",
|
|
380
414
|
`You can corroborate the caller first if you want to: ${peersTool} is a read-only listing of`,
|
|
381
415
|
`this garden's citizens and ${params.callerGardenId} is one of them. That listing is capped, so`,
|
|
@@ -393,7 +427,7 @@ export function composeFreshCallFraming(params: {
|
|
|
393
427
|
// its window is not the delivery. Nothing here watches for completion or sends on the
|
|
394
428
|
// sibling's behalf — this rail launches and lets go (Hard Rule 16, and the reason there is no
|
|
395
429
|
// supervisor anywhere in it).
|
|
396
|
-
`When the task reaches its requested final result, send that result to the same target with ${
|
|
430
|
+
`When the task reaches its requested final result, send that result to the same target with ${deliveryTool}.`,
|
|
397
431
|
"Output in this sibling window is not delivered to the caller.",
|
|
398
432
|
];
|
|
399
433
|
}
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
import { createHash } from "node:crypto";
|
|
45
45
|
import { statSync } from "node:fs";
|
|
46
46
|
import path from "node:path";
|
|
47
|
+
import { callbackEnvAssignments } from "./callback-env.ts";
|
|
47
48
|
import {
|
|
48
49
|
buildOmpBootstrapPayload,
|
|
49
50
|
composeBackendArgs,
|
|
@@ -443,7 +444,11 @@ export function herdrAgentNameFromNonce(nonce: string): string {
|
|
|
443
444
|
* not from a screen. Repeating the flag is the grammar; `--env KEY=` injects the EMPTY value
|
|
444
445
|
* rather than dropping the key.
|
|
445
446
|
*/
|
|
446
|
-
export function buildHerdrTabCreateArgs(params: {
|
|
447
|
+
export function buildHerdrTabCreateArgs(params: {
|
|
448
|
+
workspaceId: string;
|
|
449
|
+
cwd?: string;
|
|
450
|
+
callback: { target: string; nonce: string };
|
|
451
|
+
}): string[] {
|
|
447
452
|
return [
|
|
448
453
|
"tab",
|
|
449
454
|
"create",
|
|
@@ -455,6 +460,7 @@ export function buildHerdrTabCreateArgs(params: { workspaceId: string; cwd?: str
|
|
|
455
460
|
"PI_SESSION_ID=",
|
|
456
461
|
"--env",
|
|
457
462
|
"PI_AGENT_ID=",
|
|
463
|
+
...callbackEnvAssignments(params.callback).flatMap((assignment) => ["--env", assignment]),
|
|
458
464
|
];
|
|
459
465
|
}
|
|
460
466
|
|
|
@@ -849,6 +855,7 @@ export async function herdrFreshCall(
|
|
|
849
855
|
buildHerdrTabCreateArgs({
|
|
850
856
|
workspaceId: callerPane.workspaceId,
|
|
851
857
|
...(cwd === undefined ? {} : { cwd }),
|
|
858
|
+
callback: { target: callerGardenId, nonce },
|
|
852
859
|
}),
|
|
853
860
|
);
|
|
854
861
|
if (tabRun.status !== 0) {
|
|
@@ -1205,9 +1212,10 @@ export function renderHerdrFreshCall(result: HerdrFreshCallResult): { text: stri
|
|
|
1205
1212
|
`mean the sibling is running, that its first turn ran, or that the task was delivered. The tab and pane ` +
|
|
1206
1213
|
`coordinates are a view and can change under the sibling — they are not an address and nothing may be ` +
|
|
1207
1214
|
`dispatched to them.\n` +
|
|
1208
|
-
`The sibling's garden id arrives separately —
|
|
1209
|
-
`
|
|
1210
|
-
`
|
|
1215
|
+
`The sibling's garden id arrives separately — its first action is the zero-argument callback verb, and ` +
|
|
1216
|
+
`the sender envelope of THAT message is the address. The nonce above is the correlation tag it carries; ` +
|
|
1217
|
+
`the sibling reads it from its own launch env, never from this receipt. Nothing is polling for it; if it ` +
|
|
1218
|
+
`never comes, the pane is visible and can be read directly.`,
|
|
1211
1219
|
isError: false,
|
|
1212
1220
|
};
|
|
1213
1221
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* THE TWO AXES ARE NOT EQUALLY EXACT, AND THE DIFFERENCE IS NAMED. On the claude axis
|
|
22
22
|
* the reported value IS the key, byte for byte. On the pi axis it is a session FILE
|
|
23
23
|
* PATH, and the key is recovered from that filename by a strict conversion measured on
|
|
24
|
-
* pi 0.
|
|
24
|
+
* pi 0.86.0 — a VENDOR FLOOR, not a key equality. Calling both "a unique-key join"
|
|
25
25
|
* would hide which one can drift when a vendor renames a file. The conversion is
|
|
26
26
|
* deliberately strict at both ends so that drift lands as a missed join, never a wrong
|
|
27
27
|
* one, and a listing that declined to read anything says so (see `declinedReports`).
|
|
@@ -112,14 +112,23 @@ const OFFICIAL_REPORTS: Readonly<
|
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* A pi session filename, measured 2026-09-
|
|
115
|
+
* A pi session filename, measured 2026-09-20 on pi 0.86.0:
|
|
116
116
|
*
|
|
117
|
-
* 2026-09-
|
|
117
|
+
* 2026-09-20T00-35-44-197Z_01a0bc3d-87c4-738e-b391-265c8ba5a1b0.jsonl
|
|
118
118
|
* └────────── start stamp ─────────┘ └──────── nativeSessionId ────────┘
|
|
119
119
|
*
|
|
120
|
+
* `[측정 2026-09-20]` that is a REAL file a sandboxed pi 0.86.0 wrote, and its own
|
|
121
|
+
* `{"type":"session"}` header carries `id: 01a0bc3d-87c4-738e-b391-265c8ba5a1b0` —
|
|
122
|
+
* the exact string `piNativeSessionIdFromPath` recovers from the name. The layout was
|
|
123
|
+
* re-measured rather than carried: pi 0.86.0 rewrote session-manager.ts (+269 −141
|
|
124
|
+
* over v0.85.1), but every line that BUILDS a name is byte-unchanged
|
|
125
|
+
* (`${fileTimestamp}_${this.sessionId}.jsonl`, session-manager.ts:991/1527/1703); the
|
|
126
|
+
* only diff in that file's `.jsonl` lines is a lambda parameter rename on the READER
|
|
127
|
+
* side. A source read alone would not have settled it, so the real file is the receipt.
|
|
128
|
+
*
|
|
120
129
|
* WHY THIS IS PINNED HERE AND NOT INFERRED. The uuid is the join key and it lives in
|
|
121
130
|
* a VENDOR filename, so this rule depends on pi's naming and would break silently if
|
|
122
|
-
* pi changed it. **Vendor floor: pi 0.
|
|
131
|
+
* pi changed it. **Vendor floor: pi 0.86.0, measured 2026-09-20** — whether that layout
|
|
123
132
|
* is a vendor contract or a convention is NOT measured, so this is the one place in the
|
|
124
133
|
* axis that can drift under us. It is therefore strict on both ends — the name must end
|
|
125
134
|
* in `.jsonl` and the tail after the last `_` must be a well-formed uuid — and a name
|