@junghanacs/entwurf 0.23.1 → 0.25.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/BASELINE.md +1 -0
- package/CHANGELOG.md +628 -0
- package/DELIVERY.md +24 -1
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +20 -2
- package/demo/README.md +5 -2
- package/docs/acp-backend-rail.md +49 -2
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +38 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +20 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +24 -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/protocol.js +0 -7
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +46 -5
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +11 -9
- package/pi-extensions/entwurf-control.ts +83 -154
- package/pi-extensions/lib/acp/backend.ts +16 -6
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +6 -1
- package/pi-extensions/lib/control-send-receipt.ts +108 -0
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
- package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +25 -7
- package/pi-extensions/lib/herdr-placement.ts +24 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/protocol.js +0 -8
- package/run.sh +306 -53
- 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 +33 -9
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-entwurf-v2-surface.ts +289 -10
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +51 -9
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +30 -6
- package/scripts/check-herdr-placement.ts +51 -0
- package/scripts/check-herdr-plugin-build.ts +11 -4
- package/scripts/check-herdr-plugin.ts +745 -38
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-mux-launcher-fence.ts +37 -0
- package/scripts/check-pi-launch.ts +8 -2
- package/scripts/check-release-gate-outcomes.ts +165 -0
- package/scripts/doctor-pi-provider.ts +44 -4
- package/scripts/herdr-rails.mjs +75 -0
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/meta-bridge-state.py +18 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/bridge-command-boot.json +38 -0
- package/scripts/mutants/control-send-receipt.json +235 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +15 -4
- package/scripts/mutants/herdr-plugin-build.json +7 -11
- package/scripts/mutants/herdr-plugin.json +177 -9
- package/scripts/mutants/meta-retire.json +22 -0
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +17 -0
- package/scripts/mutants/release-gate.json +48 -0
- package/scripts/mutants/retired-sent-surface.json +32 -0
- package/scripts/mutants/v2-surface.json +7 -7
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-codex-fresh-live.ts +94 -13
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +71 -36
- package/scripts/smoke-herdr-raw-install-live.sh +90 -5
- package/scripts/smoke-meta-install-state.sh +107 -1
- package/scripts/smoke-mux-lifecycle-live.ts +9 -2
- 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 +108 -2
- package/scripts/smoke-setup-verdict.sh +18 -2
|
@@ -78,9 +78,9 @@ import type {
|
|
|
78
78
|
ExtensionContext,
|
|
79
79
|
MessageRenderer,
|
|
80
80
|
} from "@earendil-works/pi-coding-agent";
|
|
81
|
-
import { getMarkdownTheme
|
|
82
|
-
import { Box,
|
|
83
|
-
import {
|
|
81
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
82
|
+
import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
83
|
+
import { readCallbackEnv } from "./lib/callback-env.js";
|
|
84
84
|
import {
|
|
85
85
|
type CompactionGuard,
|
|
86
86
|
compactionSendReject,
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
noteCompactionBefore,
|
|
89
89
|
noteCompactionTerminal,
|
|
90
90
|
} from "./lib/compaction-send-guard.js";
|
|
91
|
+
import { sendReceiptBoundary } from "./lib/control-send-receipt.js";
|
|
91
92
|
import { CONTROL_SOCKET_SUFFIX, controlSocketPathIn, defaultControlSocketDir } from "./lib/control-socket-path.js";
|
|
92
93
|
import {
|
|
93
94
|
attachAcceptedSocketDisconnectPolicy,
|
|
@@ -111,15 +112,6 @@ const EMACS_AGENT_SOCKET_FLAG = "emacs-agent-socket";
|
|
|
111
112
|
// comes from the `.js` leaf both runtime lanes can import.
|
|
112
113
|
const ENTWURF_DIR = defaultControlSocketDir(os.homedir());
|
|
113
114
|
const SESSION_MESSAGE_TYPE = "entwurf-message";
|
|
114
|
-
// Sender-side UI marker. Layer B (ACP path) emits a CustomMessage with this
|
|
115
|
-
// customType so the operator sees a first-class [entwurf sent →] box paired
|
|
116
|
-
// with the receive-side [entwurf received ⟵] box. The provider-level context
|
|
117
|
-
// filter in index.ts drops this customType before the LLM sees it — colocated
|
|
118
|
-
// with the emitter so sessions without --entwurf-control are still protected.
|
|
119
|
-
// There is no second emitter: the v1 native send path ("Layer A") that also drew
|
|
120
|
-
// this box through renderSentMessage() was removed in the 0.12 cutover, and
|
|
121
|
-
// renderSentMessage is now registered for exactly one customType (see
|
|
122
|
-
// registerMessageRenderer below). Do not describe a native sender-side box.
|
|
123
115
|
const SENDER_INFO_PATTERN = /<sender_info>[\s\S]*?<\/sender_info>/g;
|
|
124
116
|
|
|
125
117
|
// ============================================================================
|
|
@@ -477,10 +469,12 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
|
|
|
477
469
|
//
|
|
478
470
|
// The label is "[entwurf received ⟵]" with a left-pointing arrow so the
|
|
479
471
|
// receiving operator immediately sees the directionality (this is an
|
|
480
|
-
// incoming message).
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
472
|
+
// incoming message). There is no matching box on the sending side: the one
|
|
473
|
+
// that used to be described here had zero producers and was removed (#120 P3).
|
|
474
|
+
// A sender sees a TEXT receipt naming the acceptance boundary
|
|
475
|
+
// (`entwurf-v2-surface.ts`), which is the honest thing to show — a box would
|
|
476
|
+
// claim the message was rendered into someone's transcript, and only the
|
|
477
|
+
// receiver can say that.
|
|
484
478
|
//
|
|
485
479
|
// wants_reply defaults to false (etiquette marker, not protocol contract).
|
|
486
480
|
// We show the badge only when the sender explicitly set it true; an
|
|
@@ -522,124 +516,6 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
|
|
|
522
516
|
return box;
|
|
523
517
|
};
|
|
524
518
|
|
|
525
|
-
// Sender-side payload — what renderSentMessage needs to draw the [entwurf sent →]
|
|
526
|
-
// box. Carried by the ACP path's CustomMessage `details` — the ONLY carrier since
|
|
527
|
-
// the v1 native renderResult path was removed (0.12 cutover). All four envelope fields are intentionally
|
|
528
|
-
// echoed in the box even though the sender is "this same session" — operators
|
|
529
|
-
// reading a busy multi-session transcript should be able to verify at a glance
|
|
530
|
-
// which 담당자 is on the wire (cwd) and which model identity (agentId) actually
|
|
531
|
-
// signed the message, without scrolling up to find the session header.
|
|
532
|
-
//
|
|
533
|
-
// timestamp is captured at execute() / send-emit time, not at render time, so
|
|
534
|
-
// re-renders (resize, expand toggle) keep showing the moment the message was
|
|
535
|
-
// actually delivered rather than drifting forward to "now".
|
|
536
|
-
//
|
|
537
|
-
// wants_reply mirrors the receive-side etiquette badge. It stays optional because
|
|
538
|
-
// the box is drawn from a CustomMessage whose `details` may predate the field; the
|
|
539
|
-
// v1 `registerSessionTool` / `entwurfSendParameters` schema this note used to point
|
|
540
|
-
// at is gone (0.12 cutover), so there is no native call site left to grow.
|
|
541
|
-
interface SentBoxData {
|
|
542
|
-
to: string; // target sessionId
|
|
543
|
-
from?: string; // sender agentId, e.g. "entwurf/claude-opus-5"
|
|
544
|
-
cwd?: string; // sender cwd (raw, abbreviateHome applied at render)
|
|
545
|
-
timestamp?: string; // ISO 8601 UTC; rendered in KST
|
|
546
|
-
mode?: string; // "steer" | "follow_up" | string passed through
|
|
547
|
-
wants_reply?: boolean;
|
|
548
|
-
deliveredAs?: string; // RPC echo — surfaces when receiver remapped (e.g. queued as followUp)
|
|
549
|
-
body: string; // message text the operator sent
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Visual mirror of renderSessionMessage. Same Box / Markdown / theme tokens —
|
|
553
|
-
// the two boxes must share the customMessageBg / customMessageLabel /
|
|
554
|
-
// customMessageText surface so they are pixel-equivalent in any theme. The
|
|
555
|
-
// only deliberate visual differences are:
|
|
556
|
-
// - label: [entwurf sent →] vs [entwurf received ⟵]
|
|
557
|
-
// - "to:" leads, "from:" follows vs "from:" only
|
|
558
|
-
// - mode: line (no equivalent on receive side — receiver
|
|
559
|
-
// doesn't see how the sender queued it)
|
|
560
|
-
//
|
|
561
|
-
// `expanded` truncates the body the same way as renderSessionMessage so a
|
|
562
|
-
// large send shows the same preview shape as a large receive. operators
|
|
563
|
-
// reading the transcript should not need different mental models.
|
|
564
|
-
// Return type is `Component`, the interface `MessageRenderer` actually asks for
|
|
565
|
-
// (`Component | undefined`, read at pi-coding-agent
|
|
566
|
-
// `dist/core/extensions/types.d.ts:889`) — NOT the concrete `Container`.
|
|
567
|
-
// `[측정 2026-09-06]` pi 0.85.0's mouse work gave `Container` a `private mouseLayout?`
|
|
568
|
-
// (`pi-tui dist/tui.d.ts:198`; 0.84.4's `Container` had no private member at all), and
|
|
569
|
-
// `Box` declares a SEPARATE private `mouseLayout` of its own. TypeScript only accepts a
|
|
570
|
-
// private member from the same declaration, so the structural assignment `Box -> Container`
|
|
571
|
-
// that held through 0.84.4 became TS2322 at 0.85.x. This was an UNDECLARED break — the
|
|
572
|
-
// upstream Breaking section names only `createGatewayBindingFetch`. Annotating the shared
|
|
573
|
-
// interface is the honest fix: nothing here ever needed Container's own surface.
|
|
574
|
-
const buildSentMessageBox = (data: SentBoxData, expanded: boolean, theme: Theme): Component => {
|
|
575
|
-
let body = data.body || "(no content)";
|
|
576
|
-
if (!expanded) {
|
|
577
|
-
const lines = body.split("\n");
|
|
578
|
-
if (lines.length > 5) {
|
|
579
|
-
body = `${lines.slice(0, 5).join("\n")}\n...`;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
584
|
-
const labelBase = theme.fg("customMessageLabel", `\x1b[1m[entwurf sent →]\x1b[22m`);
|
|
585
|
-
|
|
586
|
-
// Header line: label + KST + optional (wants reply) badge — matches the
|
|
587
|
-
// receive-side header layout 1:1.
|
|
588
|
-
const kst = formatTimestampKst(data.timestamp) ?? "(unknown time)";
|
|
589
|
-
const replyBadge = data.wants_reply === true ? " (wants reply)" : "";
|
|
590
|
-
const headerLine = `${labelBase} ${theme.fg("dim", `${kst}${replyBadge}`)}`;
|
|
591
|
-
box.addChild(new Text(headerLine, 0, 0));
|
|
592
|
-
|
|
593
|
-
// to: <sessionId> — target peer
|
|
594
|
-
box.addChild(new Text(theme.fg("dim", `to: ${data.to || "(unknown sessionId)"}`), 0, 0));
|
|
595
|
-
|
|
596
|
-
// from: <agentId> @ <cwd> — self identity. Shown even though it's "us"
|
|
597
|
-
// because in a multi-session human-greeted topology the operator is
|
|
598
|
-
// switching between several pi sessions and needs to confirm which one
|
|
599
|
-
// signed this send.
|
|
600
|
-
const fromAgent = data.from ?? "(unknown agent)";
|
|
601
|
-
const fromCwd = data.cwd ? abbreviateHome(data.cwd) : "(unknown cwd)";
|
|
602
|
-
box.addChild(new Text(theme.fg("dim", `from: ${fromAgent} @ ${fromCwd}`), 0, 0));
|
|
603
|
-
|
|
604
|
-
// mode: <mode>[ → deliveredAs] — show RPC remap when it differs from
|
|
605
|
-
// what the caller asked for (e.g. caller said "steer" but receiver was
|
|
606
|
-
// idle so it became a direct prompt). Silent when they agree.
|
|
607
|
-
if (data.mode) {
|
|
608
|
-
const remap = data.deliveredAs && data.deliveredAs !== data.mode ? theme.fg("muted", ` → ${data.deliveredAs}`) : "";
|
|
609
|
-
box.addChild(new Text(theme.fg("dim", `mode: ${data.mode}${remap}`), 0, 0));
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
box.addChild(new Spacer(1));
|
|
613
|
-
box.addChild(
|
|
614
|
-
new Markdown(body, 0, 0, getMarkdownTheme(), {
|
|
615
|
-
color: (value: string) => theme.fg("customMessageText", value),
|
|
616
|
-
}),
|
|
617
|
-
);
|
|
618
|
-
return box;
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
// CustomMessageRenderer adapter for Layer B (ACP path). The CustomMessage
|
|
622
|
-
// carries the SentBoxData under `details` (set by index.ts streamShellAcp
|
|
623
|
-
// when a completed mcp__entwurf-bridge__entwurf_v2 is observed). `content`
|
|
624
|
-
// holds the raw message body too, but we prefer details.body because the
|
|
625
|
-
// content channel may have been routed through string-only persistence and
|
|
626
|
-
// trimmed.
|
|
627
|
-
const renderSentMessage: MessageRenderer = (message, { expanded }, theme) => {
|
|
628
|
-
const details = (message.details ?? {}) as Partial<SentBoxData>;
|
|
629
|
-
const fallbackBody = extractTextContent(message.content);
|
|
630
|
-
const data: SentBoxData = {
|
|
631
|
-
to: details.to ?? "(unknown sessionId)",
|
|
632
|
-
from: details.from,
|
|
633
|
-
cwd: details.cwd,
|
|
634
|
-
timestamp: details.timestamp,
|
|
635
|
-
mode: details.mode,
|
|
636
|
-
wants_reply: details.wants_reply,
|
|
637
|
-
deliveredAs: details.deliveredAs,
|
|
638
|
-
body: details.body ?? fallbackBody,
|
|
639
|
-
};
|
|
640
|
-
return buildSentMessageBox(data, expanded, theme);
|
|
641
|
-
};
|
|
642
|
-
|
|
643
519
|
// ============================================================================
|
|
644
520
|
// Command Handlers
|
|
645
521
|
// ============================================================================
|
|
@@ -823,9 +699,17 @@ async function handleCommand(
|
|
|
823
699
|
return;
|
|
824
700
|
}
|
|
825
701
|
|
|
702
|
+
// #120 P2: the receiver reports the BOUNDARY it actually observed — `sent` for an idle
|
|
703
|
+
// direct trigger, `queued-steer` / `queued-follow-up` for the two in-process queues — and
|
|
704
|
+
// nothing else. The bare `delivered:true` that used to lead this payload is GONE rather
|
|
705
|
+
// than kept for compatibility: it was equally true of all three states and therefore said
|
|
706
|
+
// nothing, which is the bare `delivered` DELIVERY.md refuses by name. The `deliveredAs`
|
|
707
|
+
// beside it had no consumer at all. Skew is safe in both directions without a translator —
|
|
708
|
+
// an old sender keys on `response.success` and never reads `data`, and a new sender reads
|
|
709
|
+
// an old receiver's payload as `accepted-unknown-boundary`, the honest name for an
|
|
710
|
+
// acceptance it cannot classify.
|
|
826
711
|
respond(true, "send", {
|
|
827
|
-
|
|
828
|
-
deliveredAs: isIdle ? "direct" : mode === "follow_up" ? "followUp" : "steer",
|
|
712
|
+
boundary: sendReceiptBoundary({ idle: isIdle, mode }),
|
|
829
713
|
wants_reply: wantsReply,
|
|
830
714
|
});
|
|
831
715
|
return;
|
|
@@ -1103,21 +987,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1103
987
|
};
|
|
1104
988
|
|
|
1105
989
|
pi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);
|
|
1106
|
-
// Layer B (ACP path) sender-side UI box. Registered unconditionally because
|
|
1107
|
-
// renderer registration happens at extension load, BEFORE we know whether
|
|
1108
|
-
// this session ends up with a routable identity: `updateSessionEnv` only
|
|
1109
|
-
// publishes PI_SESSION_ID once a garden id exists, and clears it otherwise.
|
|
1110
|
-
// A session without `--entwurf-control` therefore does NOT send — a bundled
|
|
1111
|
-
// `entwurf_v2` sender call there fails loud on the missing envelope (the
|
|
1112
|
-
// documented provider-vs-citizen boundary), and this renderer simply never
|
|
1113
|
-
// fires. Registering it up front keeps one registration path instead of a
|
|
1114
|
-
// conditional seam, and guarantees the [entwurf sent →] box exists for every
|
|
1115
|
-
// session that DOES send.
|
|
1116
|
-
pi.registerMessageRenderer(ENTWURF_SENT_MESSAGE_TYPE, renderSentMessage);
|
|
1117
990
|
|
|
1118
991
|
if (shouldRegisterControlTools(pi)) {
|
|
1119
992
|
registerListSessionsTool(pi);
|
|
1120
993
|
registerEntwurfV2Tool(pi);
|
|
994
|
+
registerCallbackTool(pi);
|
|
1121
995
|
registerFreshCallTool(pi);
|
|
1122
996
|
registerResumeCallTool(pi);
|
|
1123
997
|
}
|
|
@@ -1385,7 +1259,7 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1385
1259
|
mode: Type.Optional(
|
|
1386
1260
|
StringEnum(["steer", "follow_up"] as const, {
|
|
1387
1261
|
description:
|
|
1388
|
-
"Injection style for a CONTROL-SOCKET send only: steer (
|
|
1262
|
+
"Injection style for a CONTROL-SOCKET send only: steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. " +
|
|
1389
1263
|
"The mailbox and native-push plans carry no mode, so it has no effect on those rails.",
|
|
1390
1264
|
}),
|
|
1391
1265
|
),
|
|
@@ -1413,9 +1287,11 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1413
1287
|
description: `CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id alone
|
|
1414
1288
|
does not say which rail that citizen answers on. Give target + intent; the decider picks transport from
|
|
1415
1289
|
liveness (live socket citizen → control-socket send; deliverable self-fetch citizen → meta-bridge mailbox;
|
|
1416
|
-
probe-alive native-push citizen → direct injection into its conversation) and reports ONE outcome
|
|
1417
|
-
(
|
|
1418
|
-
|
|
1290
|
+
probe-alive native-push citizen → direct injection into its conversation) and reports ONE outcome, per rail: a control
|
|
1291
|
+
receiver's acceptance boundary ("sent" / "queued-steer" / "queued-follow-up" /
|
|
1292
|
+
"accepted-unknown-boundary" — those four only), a mailbox ENQUEUE receipt, a native-push INJECTION,
|
|
1293
|
+
a rejection, or lock-dirty. EXISTING targets only (see entwurf_peers).
|
|
1294
|
+
A peer shown as liveness=alive → fire-and-forget. A citizen with NO socket liveness
|
|
1419
1295
|
(liveness=unsupported) is ALSO fire-and-forget — unsupported means only "no control-socket probe" — and the
|
|
1420
1296
|
decider picks its own rail: a self-fetch backend (e.g. Claude Code) gets the mailbox, a native-push backend
|
|
1421
1297
|
(e.g. Antigravity) gets direct injection and has NO mailbox at all. THERE IS A THIRD RESULT: the mailbox
|
|
@@ -1424,9 +1300,8 @@ rather than queued for an inbox nobody drains. The native-push probe is 3-valued
|
|
|
1424
1300
|
alive → injected; dead → native-push-target-dead; indeterminate → native-push-probe-indeterminate
|
|
1425
1301
|
(unestablished ≠ gone). DORMANT IS UNREACHABLE: a socket-domain citizen that is not running gets
|
|
1426
1302
|
dormant-fire-forget-unsupported — same receiver rule as the mailbox, no active drainer means no
|
|
1427
|
-
delivery. The intent that used to answer there, owned-outcome,
|
|
1428
|
-
|
|
1429
|
-
yourself and dispatch again. LOCK: taken for a control-socket-DOMAIN dispatch. The mailbox and native-push
|
|
1303
|
+
delivery. The intent that used to answer there, owned-outcome, was withdrawn
|
|
1304
|
+
under the visible-first rule — re-open the session yourself and dispatch again. LOCK: taken for a control-socket-DOMAIN dispatch. The mailbox and native-push
|
|
1430
1305
|
rails are lock-free — deliverability and the adapter probe guard them. mode applies to a CONTROL-SOCKET
|
|
1431
1306
|
send only; other plans carry no mode. wants_reply rides every rail. message caps at 16000 chars; send an
|
|
1432
1307
|
artifact path + digest for more.`,
|
|
@@ -1740,6 +1615,60 @@ interface VisibleResumeModule {
|
|
|
1740
1615
|
renderVisibleResume(result: { ok: boolean }): { text: string; isError: boolean };
|
|
1741
1616
|
}
|
|
1742
1617
|
|
|
1618
|
+
function registerCallbackTool(pi: ExtensionAPI): void {
|
|
1619
|
+
const registerTool = pi.registerTool as (def: any) => void;
|
|
1620
|
+
registerTool({
|
|
1621
|
+
name: "entwurf_callback",
|
|
1622
|
+
label: "Fresh Callback",
|
|
1623
|
+
description: `ZERO-ARGUMENT callback for a fresh sibling this process was launched as. Reads ENTWURF_CALLBACK_TARGET and ENTWURF_CALLBACK_NONCE from this process environment (injected by the launcher next to the identity scrub), validates garden-id and nonce grammar, and dispatches through the existing v2 runner with intent fire-and-forget, message=nonce, wants_reply=false. The target is re-resolved by record/decider. REFUSES BY NAME when the pair is absent, malformed, or this process is a Codex-provenance bridge. No arguments, no fallback to a model-supplied target.`,
|
|
1624
|
+
parameters: Type.Object({}),
|
|
1625
|
+
async execute(
|
|
1626
|
+
_toolCallId: string,
|
|
1627
|
+
_params: Record<string, never>,
|
|
1628
|
+
_signal: AbortSignal | undefined,
|
|
1629
|
+
_onUpdate: unknown,
|
|
1630
|
+
ctx: ExtensionContext,
|
|
1631
|
+
) {
|
|
1632
|
+
const read = readCallbackEnv(process.env);
|
|
1633
|
+
if (!read.ok) {
|
|
1634
|
+
return {
|
|
1635
|
+
content: [{ type: "text", text: `entwurf_callback: ${read.reason}` }],
|
|
1636
|
+
isError: true,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
try {
|
|
1640
|
+
const selfMod = (await import(ENTWURF_SELF_ADDRESS_MODULE)) as unknown as EntwurfSelfAddressModule;
|
|
1641
|
+
const senderProvider = (): SenderEnvelope | undefined => {
|
|
1642
|
+
const s = buildLocalSenderEnvelope(ctx);
|
|
1643
|
+
return s ? decoratePiSenderAddressability(s, selfMod.computeSelfAddressability) : undefined;
|
|
1644
|
+
};
|
|
1645
|
+
const mod = (await import(ENTWURF_V2_SURFACE_MODULE)) as unknown as EntwurfV2SurfaceModule;
|
|
1646
|
+
const rendered = await mod.runAndRenderEntwurfV2FromSurface(
|
|
1647
|
+
{
|
|
1648
|
+
target: read.target,
|
|
1649
|
+
intent: "fire-and-forget",
|
|
1650
|
+
message: read.nonce,
|
|
1651
|
+
wants_reply: false,
|
|
1652
|
+
},
|
|
1653
|
+
{ senderProvider },
|
|
1654
|
+
);
|
|
1655
|
+
return {
|
|
1656
|
+
content: [{ type: "text", text: rendered.text }],
|
|
1657
|
+
isError: rendered.isError,
|
|
1658
|
+
details: { isError: rendered.isError },
|
|
1659
|
+
};
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1662
|
+
return {
|
|
1663
|
+
content: [{ type: "text", text: `entwurf_callback error: ${msg}` }],
|
|
1664
|
+
isError: true,
|
|
1665
|
+
details: { error: msg },
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
},
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1743
1672
|
/**
|
|
1744
1673
|
* Unlike `entwurf_fresh_call`, this tool needs NO caller identity: a resume names an existing
|
|
1745
1674
|
* citizen, so the address is the parameter rather than something the sibling has to report back.
|
|
@@ -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
|
|
@@ -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> {
|
|
@@ -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);
|
|
@@ -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
|
|
|
@@ -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,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* compaction-send-guard — whether a control-socket `send` may call `pi.sendMessage`.
|
|
3
3
|
*
|
|
4
|
-
* Pi
|
|
4
|
+
* Pi has no public `ExtensionContext.isCompacting()`. `[측정 2026-09-20, pi 0.86.0]` and
|
|
5
|
+
* re-measured `[측정 2026-09-22, pi 0.87.0]` and `[측정 2026-09-23, pi 0.87.1]`: the installed
|
|
6
|
+
* `dist/core/extensions/types.d.ts` declares `isIdle(): boolean` at :233 and `signal` at :237 —
|
|
7
|
+
* the SAME coordinates at all three floors — and carries no `isCompacting` member at all (grep
|
|
8
|
+
* count 0, re-counted on 0.87.1); the identifier exists
|
|
9
|
+
* only inside the interactive/rpc modes' own bundles). Compaction is non-idle
|
|
5
10
|
* (`AgentSession.isIdle` = no agent run AND not compacting) but `sendCustomMessage`
|
|
6
11
|
* does not refuse it. A non-streaming `triggerTurn:true` send starts `_runAgentPrompt`
|
|
7
12
|
* while `compact()` is rewriting the session tree — the field path that can make
|