glasspane-mcp 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,8 +20,61 @@ export interface TrailTurn {
20
20
  /** Hand the trail on: this turn is done with it, or never will be. */
21
21
  release(): void;
22
22
  }
23
+ /**
24
+ * Handed to {@link EvidenceAuditSession.recordLateArrival} when the request that
25
+ * produced a late reply never said which generation it belonged to. It is
26
+ * deliberately not `undefined` and not a number the real epochs can ever reach:
27
+ * an unattributed frame is *known* unattributed, and refusing it is the default.
28
+ */
29
+ export declare const UNKNOWN_GENERATION = -1;
30
+ /** Where a late arrival ended up: written, or refused because a re-attach got there first. */
31
+ export interface LateArrivalOutcome {
32
+ written: boolean;
33
+ /** The trail generation in force at the moment of the decision. */
34
+ generationAtWrite: number;
35
+ }
36
+ /**
37
+ * How an id got into this trail, per id, in the words a renderer can put beside
38
+ * it. The three values are not adjectives of confidence: they name which
39
+ * mechanism wrote the id, and each one is a fact the session itself observes.
40
+ *
41
+ * - `in-order`: written by a call whose own request held a trail turn, answered
42
+ * by a frame that echoed that request's id. This is the only entry the trail
43
+ * may describe as verified.
44
+ * - `late`: written from a reply that landed after its caller had already been
45
+ * answered, so it had no turn left to write at and took the position
46
+ * {@link recordLateArrival} gives it — behind whatever is still outstanding.
47
+ * The *operation* is the daemon's own; only its place in request order is
48
+ * arranged by arrival.
49
+ * - `late-inferred`: that same late write, for a frame that echoed **no request
50
+ * id** (R10-中2). Which request it answers is then a guess from the daemon's
51
+ * in-order scheduling, not a match, and an id pulled out of it can be filed
52
+ * under the wrong operation. The trail still records it — dropping it would
53
+ * claim "no such operation ran", which is the more dangerous lie for an agent
54
+ * deciding whether to click again — but it must not read as verified, and
55
+ * `gp_recent_reports` has to say so from here rather than from the log line.
56
+ *
57
+ * Producers: `EngineJsonRpcClient`'s `LateReply.attribution` decides between the
58
+ * last two (`echoed-id` -> `late`, `arrival-order` -> `late-inferred`); a caller
59
+ * that passes nothing gets `late`, which is everything this session can see on
60
+ * its own.
61
+ */
62
+ export type TrailProvenance = "in-order" | "late" | "late-inferred";
63
+ /**
64
+ * The attribution a late arrival carries: the same two literals
65
+ * `engine-client.ts`'s `FrameAttribution` produces, and `unknown` for a caller
66
+ * that cannot say (a wiring that has not read the field yet).
67
+ */
68
+ export type LateArrivalAttribution = "echoed-id" | "arrival-order" | "unknown";
69
+ /**
70
+ * The sentence the surface that shows the trail puts beside one id. Written here
71
+ * because this is the layer that knows the mechanism; the tool layer renders it.
72
+ */
73
+ export declare const TRAIL_PROVENANCE_NOTES: Readonly<Record<TrailProvenance, string>>;
23
74
  export declare class EvidenceAuditSession {
24
75
  private ids;
76
+ private epoch;
77
+ private attachedAppKey;
25
78
  /**
26
79
  * Turns claimed but not released. This, not a "latest gate", is what a new
27
80
  * claim waits for: a turn that abandons the trail (its engine call failed
@@ -31,18 +84,116 @@ export declare class EvidenceAuditSession {
31
84
  * order the turn machinery exists to remove.
32
85
  */
33
86
  private openGates;
34
- /** Record every operationId/evidenceId found in an engine result frame. */
87
+ /**
88
+ * How each id in {@link ids} got here. Keyed by the id, because that is what a
89
+ * reader holds; the map is trimmed with the list and cleared when the trail
90
+ * restarts, so an id can never outlive the provenance that describes it.
91
+ */
92
+ private readonly provenance;
93
+ /**
94
+ * Non-null while {@link recordLateArrival} holds the trail, and says how the
95
+ * frame being written was attributed. This is what lets a plain
96
+ * `trail.record(result)` inside that callback be filed as a late arrival without
97
+ * the caller having to remember to say so: the write happens inside a window only
98
+ * that method opens, so the mechanism is visible here rather than being a claim
99
+ * somebody must repeat at every call site.
100
+ */
101
+ private lateWindow;
102
+ /**
103
+ * Record every operationId/evidenceId found in an engine result frame, tagged
104
+ * with the provenance of the path that is recording it (see
105
+ * {@link TrailProvenance}): `in-order` from a call that held its own trail turn,
106
+ * `late`/`late-inferred` from inside {@link recordLateArrival}.
107
+ */
35
108
  record(result: unknown): void;
36
109
  /**
37
- * Clear the trail (called after a successful attach).
110
+ * How one id in this trail got recorded, or null when the trail does not hold
111
+ * it — never recorded, evicted past {@link AUDIT_HISTORY_LIMIT}, or cleared by a
112
+ * {@link restart}. A reader that lists ids for an agent renders
113
+ * {@link TRAIL_PROVENANCE_NOTES} for anything that is not `in-order`: an id
114
+ * pulled from a frame this shell only *guessed* an owner for must not appear
115
+ * beside one it matched by id (R10-中2).
116
+ *
117
+ * Read off the map alone, so the map *is* the record of what the trail holds: an
118
+ * id whose provenance outlives its own entry would come back labelled as if it
119
+ * were still listed, and the eviction and the restart are the two places that
120
+ * would have to remember to delete. `test/engine-client.test.mjs` pins both.
121
+ */
122
+ provenanceOf(id: string): TrailProvenance | null;
123
+ /** The ids currently filed as inferred; see {@link TrailProvenance}. */
124
+ inferredIds(): string[];
125
+ /**
126
+ * The trail's generation number. Every {@link restart} bumps it.
127
+ *
128
+ * It exists so a reply that arrives *after* its caller was already answered
129
+ * can be judged against the trail it was written into: {@link recordLateArrival}
130
+ * takes the epoch the request was admitted under, and a mismatch means a
131
+ * re-attach happened in between. Without it, a late write lands in a *new*
132
+ * session's trail — an operationId from the previous app that
133
+ * `gp_recent_reports` then fetches and renders as if it belonged to this one
134
+ * (R8b-高1, measured: `['op_OLDAPP']` in the trail after attaching a second
135
+ * app).
136
+ */
137
+ get generation(): number;
138
+ /** The app the trail currently belongs to, or null before the first attach. */
139
+ get attachedApp(): string | null;
140
+ /**
141
+ * A successful attach: the trail restarts **only when the attached app
142
+ * changed**, because that is the daemon's own rule —
143
+ * `EngineCore.attach` clears `history` under
144
+ * `if attachedApp != app`, and its comment says re-attach to the same app is
145
+ * idempotent. Clearing on *every* attach (what this used to do) is strictly
146
+ * worse in two directions: an idempotent re-attach wiped operations that were
147
+ * really in the trail (including one recorded by a late reply, which put
148
+ * `gp_recent_reports` back to "run gp_act first"), and — because the clear
149
+ * happens whenever the reply lands — it could also drop the record of an act
150
+ * the daemon still has.
151
+ *
152
+ * `appKey` must be built from every stored property of Swift's `AttachedApp`
153
+ * (`pid`, `bundleId`, `appName`); a key that ignored `pid` would keep the
154
+ * trail across an app restart whose history the daemon did clear. The gate
155
+ * that pins this to the Swift struct is
156
+ * `test/attach-identity.test.mjs`.
38
157
  *
39
- * Deliberately does *not* touch {@link openGates}: outstanding turns are not
40
- * outstanding mutations, and forgetting them would let a later read overtake
41
- * an act that is still going to write.
158
+ * Returns whether the trail was restarted. A null `appKey` means the attach
159
+ * result did not identify the app at all, which is treated as a change: an
160
+ * unknown identity cannot be *shown* to be the same app, and a stale trail is
161
+ * the failure that misattributes evidence.
42
162
  */
43
- reset(): void;
163
+ restart(appKey: string | null): boolean;
44
164
  /** The most recent `limit` recorded operationIds, oldest first. */
45
165
  recentIds(limit: number): string[];
166
+ /**
167
+ * Record a frame that arrives with **no request of the caller still holding a
168
+ * turn**: a late engine reply (R8-高3, `EngineJsonRpcClient.onLateReply`). Its
169
+ * own turn was released the moment the timeout was reported to the agent, so
170
+ * there is no request-order position left to write at.
171
+ *
172
+ * It still goes through the turn machinery rather than a bare `record()`,
173
+ * because a direct write could land *between* two admitted calls and reorder
174
+ * the trail against the requests the agent made — the exact thing the turns
175
+ * exist to prevent. Claiming on arrival gives the only honest position left:
176
+ * behind whatever is still outstanding, and ordered among late arrivals by
177
+ * when they arrived.
178
+ *
179
+ * `admittedUnderGeneration` is compared **inside** that wait (see the body):
180
+ * the position and the epoch can only be judged at the moment the write
181
+ * actually happens. A reply whose request carried no generation cannot be
182
+ * placed either — so it is passed as {@link UNKNOWN_GENERATION} and the caller
183
+ * decides whether that is acceptable; this method refuses it by default, because
184
+ * "we do not know which attach this belongs to" is not a reason to write into
185
+ * somebody else's trail.
186
+ *
187
+ * `attribution` is how the transport matched that frame to a request, and it is
188
+ * the difference between `late` and `late-inferred` below: a frame that echoed
189
+ * the id the client sent is confirmed, one with no id is filed against the
190
+ * oldest tracked request by the daemon's in-order scheduling (R10-中2). Anything
191
+ * this method writes is a late arrival by construction, so the window opens
192
+ * whether or not the caller can say more — the default `unknown` is what the
193
+ * session can see on its own, and it is *not* `in-order`: an unconfirmed
194
+ * attribution must not read as a verified one.
195
+ */
196
+ recordLateArrival(apply: (session: EvidenceAuditSession) => void, admittedUnderGeneration?: number, attribution?: LateArrivalAttribution): Promise<LateArrivalOutcome>;
46
197
  /**
47
198
  * Claim the next position in this session's trail request order, so that
48
199
  * overlapping calls mutate and read `ids` in the order they were admitted
@@ -55,4 +206,13 @@ export declare class EvidenceAuditSession {
55
206
  */
56
207
  claimTrailTurn(): TrailTurn;
57
208
  }
209
+ /**
210
+ * Bounded recursive walk (depth ≤ 3) that collects string-valued
211
+ * `operationId` / `evidenceId` fields — from act/assert/diagnose/restore
212
+ * results and from nested `evidencePack` frames. Depth is capped so an
213
+ * adversarial frame cannot force an unbounded descent.
214
+ */
215
+ /** The same bounded walk, as the shell's one way to ask "does this frame name an
216
+ * operation?" — the late-reply wiring needs it to say *what* it dropped. */
217
+ export declare function operationIds(value: unknown): string[];
58
218
  //# sourceMappingURL=audit-session.d.ts.map
@@ -26,7 +26,7 @@ export declare const MCP_PROTOCOL_VERSION: "2025-06-18";
26
26
  export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
27
27
  export declare const SERVER_INFO: {
28
28
  readonly name: "glasspane-mcp";
29
- readonly version: "1.2.0";
29
+ readonly version: "1.3.1";
30
30
  };
31
31
  export declare const PARSE_ERROR = -32700;
32
32
  export declare const INVALID_REQUEST = -32600;
@@ -46,8 +46,23 @@ export interface RpcResponse {
46
46
  export interface McpServerDeps {
47
47
  engine: EngineJsonRpcClient;
48
48
  specs?: readonly ToolSpec[];
49
- /** Overridable audit session (per-server operationId trail, spec v1.3 §10.3). */
50
- session?: EvidenceAuditSession;
49
+ /**
50
+ * The per-server operationId trail (spec v1.3 §10.3). **Required**: it used to
51
+ * be optional with a `?? new EvidenceAuditSession()` default, which let a
52
+ * server be built with a trail nobody else holds a reference to — every tool
53
+ * result then looked correct while `gp_recent_reports` answered from an empty
54
+ * trail, and the late-reply sink was wired to a *different* session. The
55
+ * composition is `createTrackedMcpServer`'s job now, so the default had no
56
+ * legitimate user (R8b-低).
57
+ */
58
+ session: EvidenceAuditSession;
59
+ /**
60
+ * Sink for facts the protocol gives no answer to: a notification this shell
61
+ * does not act on. Without it `notifications/cancelled` — a client saying it
62
+ * has given up on a request — vanished with zero trace while the daemon
63
+ * request it refers to stayed in flight and kept acting on the user's screen.
64
+ */
65
+ report?: (note: string) => void;
51
66
  }
52
67
  export declare class McpServer {
53
68
  private readonly deps;
@@ -72,4 +87,24 @@ export declare class McpServer {
72
87
  private result;
73
88
  private error;
74
89
  }
90
+ /**
91
+ * Wire an engine client to a server over one shared audit session, including the
92
+ * late-reply sink (R8-高3).
93
+ *
94
+ * This is a function rather than four lines inside `main()` so that the test for
95
+ * "a reply that lands after the caller was answered still reaches the trail"
96
+ * drives **the** wiring an MCP client actually gets. Inlining it would leave the
97
+ * only copy of that decision in a file that runs `main()` on import, i.e. a fix
98
+ * on a path no test can execute.
99
+ *
100
+ * The session is created here and handed to `McpServer` rather than left to its
101
+ * default, because the engine client must reach the *same* instance: a late
102
+ * reply names an operation that really ran, and the only place an agent can get
103
+ * that back from is this trail (`gp_recent_reports`). A sink that recorded into
104
+ * a second session would look fixed and change nothing.
105
+ */
106
+ export declare function createTrackedMcpServer(engine: EngineJsonRpcClient, report: (note: string) => void): {
107
+ server: McpServer;
108
+ session: EvidenceAuditSession;
109
+ };
75
110
  //# sourceMappingURL=dispatch.d.ts.map
@@ -36,8 +36,26 @@ export declare const ENGINE_SOCKET_ENV = "GLASSPANE_ENGINE_SOCK";
36
36
  *
37
37
  * Deliberately not "fixed" by asking the daemon: before a connection exists
38
38
  * there is no daemon to ask, and `hello` is itself answered after connecting.
39
- * Matching the daemon's home lookup from Node would take an FFI call to
40
- * `NSHomeDirectory()`, which this shell does not have.
39
+ *
40
+ * R8-中9, and read this before "harmonising" it with `systemHome()`: matching the
41
+ * daemon's lookup from Node **is** available — `os.userInfo().homedir` reads the
42
+ * user record, the same source `NSHomeDirectory()` uses, no FFI needed, and
43
+ * `project-registry.ts`'s `systemHome()` does exactly that. This function
44
+ * deliberately does not. The two files sit on opposite sides of a safety
45
+ * question:
46
+ * - `projects.json` is *our* data: naming a file the daemon cannot load turns
47
+ * a write into a stray, so it must follow the daemon's rule.
48
+ * - the engine socket is *the user's screen*: a process that pointed `$HOME`
49
+ * somewhere private as its own isolation boundary currently fails to connect
50
+ * ("nothing is listening on the guessed path", an honest refusal). Resolve
51
+ * the home the daemon's way here and that same process silently drives the
52
+ * live daemon — clicks, state writes, the whole act surface — against a
53
+ * machine it asked not to be on. `$HOME`-redirect is not a supported way to
54
+ * isolate from a daemon (name the socket, or don't), but it must not be
55
+ * upgraded into a way to reach it either.
56
+ *
57
+ * `test/engine-client.test.mjs` pins both halves of that split, so changing
58
+ * either side is a deliberate edit against a red test rather than a cleanup.
41
59
  *
42
60
  * Single source all the same: the CLI argument parser in index.ts and the
43
61
  * reconnecting transport both resolve the path through this function, so an env
@@ -46,6 +64,24 @@ export declare const ENGINE_SOCKET_ENV = "GLASSPANE_ENGINE_SOCK";
46
64
  export declare function defaultSocketPath(): string;
47
65
  /** Fallback deadline for a method missing from the table below. */
48
66
  export declare const ENGINE_TIMEOUT_MS = 10000;
67
+ /**
68
+ * B-02: how long a *caller* may be kept waiting for any method, whatever the
69
+ * daemon's own worst case is.
70
+ *
71
+ * A real MCP client times a single request out at around 60 s, stops waiting,
72
+ * and the agent — holding no answer, no code and no remedy — re-issues the
73
+ * call. On `act` that re-issue performs the same click on the user's screen a
74
+ * second time, which is precisely the harm a deadline is meant to remove. So
75
+ * the ceiling sits below a client's patience: the shell answers at 50 s with
76
+ * `GP_E_ENGINE_TIMEOUT` and the no-replay remedy, and the request stays
77
+ * registered, so the reply that arrives later is still attributed and written
78
+ * to the log (`onDeadline` -> `reportLateReply`). Nothing is discarded by
79
+ * answering early — the only thing that ends that registration is the transport
80
+ * going away, and `teardown` logs the tracked requests it drops when it does —
81
+ * which is what makes the multi-minute deadlines above safe to cap: they
82
+ * describe the daemon, not the caller's wait.
83
+ */
84
+ export declare const CALLER_VISIBLE_CEILING_MS = 50000;
49
85
  /**
50
86
  * Per-method wall-clock deadlines. 10 s is the daemon's *internal* latency
51
87
  * flag (`EngineCore.performanceLatencyBudgetMs`), not its worst-case wall
@@ -60,9 +96,23 @@ export declare const ENGINE_TIMEOUT_MS = 10000;
60
96
  * method below the ceiling, and above it they are the daemon-side worst case
61
97
  * the timeout text quotes — the caller answered at the ceiling, while the
62
98
  * request stays registered so the real reply is attributed when it lands.
99
+ *
100
+ * `probe_status` is deliberately *not* "the cheap call, so give it the shortest
101
+ * window". The daemon answers one request at a time, so a probe is queued
102
+ * behind whatever the agent is standing over, and {@link slowEngineRemedy}
103
+ * tells the agent to send exactly this probe *while an `act` is outstanding*. A
104
+ * probe with a shorter window than the request in front of it expires against a
105
+ * busy daemon, and the agent then reads "the probe timed out too" as permission
106
+ * to restart — which cancels and rolls back an action mid-flight on the user's
107
+ * screen. It gets the full client-safe wait so the busy-but-will-finish case is
108
+ * answerable at all; the case where it is not (`act` up to 120 s against a 50 s
109
+ * ceiling — no deadline here can cover it, since the ceiling is the client's
110
+ * patience, not the daemon's) is what
111
+ * {@link livenessProbeDecision}'s `timed-out` entry exists to disarm.
112
+ * `test/probe-liveness.test.mjs` pins both halves: the armed timer, and the
113
+ * arithmetic invariant that no method can outlive the probe meant to diagnose it.
63
114
  */
64
115
  export declare const ENGINE_DEADLINES_MS: {
65
- readonly hello: 15000;
66
116
  readonly attach: 30000;
67
117
  readonly act: 120000;
68
118
  readonly observe: 45000;
@@ -70,31 +120,29 @@ export declare const ENGINE_DEADLINES_MS: {
70
120
  readonly diagnose: 30000;
71
121
  readonly last_evidence: 60000;
72
122
  readonly snapshot: 90000;
73
- readonly probe_status: 15000;
123
+ readonly probe_status: 50000;
124
+ readonly hello: 50000;
74
125
  readonly shutdown: 15000;
126
+ readonly audit_ui: 45000;
127
+ readonly capture_view: 30000;
75
128
  };
76
129
  /** Fixed part of an ffwd restore deadline (per-step cost is `act` above). */
77
130
  export declare const RESTORE_BASE_DEADLINE_MS = 30000;
78
131
  /**
79
- * B-02: how long a *caller* may be kept waiting for any method, whatever the
80
- * daemon's own worst case is.
81
- *
82
- * A real MCP client times a single request out at around 60 s, stops waiting,
83
- * and the agent — holding no answer, no code and no remedy — re-issues the
84
- * call. On `act` that re-issue performs the same click on the user's screen a
85
- * second time, which is precisely the harm a deadline is meant to remove. So
86
- * the ceiling sits below a client's patience: the shell answers at 50 s with
87
- * `GP_E_ENGINE_TIMEOUT` and the no-replay remedy, and the request stays
88
- * registered, so the reply that arrives later is still attributed and written
89
- * to the log (`onDeadline` -> `reportLateReply`). Nothing is discarded by
90
- * answering early, which is what makes the multi-minute deadlines above safe to
91
- * cap: they describe the daemon, not the caller's wait.
132
+ * The single answer to "will re-issuing this method perform a change the daemon may
133
+ * already have carried out?". Exported because two other layers have to ask it —
134
+ * `tools.ts` when it rewrites an oversized reply, `http-gateway.ts` when it answers
135
+ * a curl caller — and a membership list copied into each of them is how the advice
136
+ * for one tool drifts back into "just retry the click". A caller that wants the
137
+ * *reason* per method owns that text; membership comes from here.
92
138
  */
93
- export declare const CALLER_VISIBLE_CEILING_MS = 50000;
94
- /** When the caller gets its answer for `method`: its deadline, capped. */
95
- export declare function callerDeadlineMs(method: string, params?: Record<string, unknown>, ceilingMs?: number): number;
139
+ export declare function isReplayUnsafeMethod(method: string): boolean;
140
+ /** The non-replayable methods, for gates that must see the set change on purpose. */
141
+ export declare const REPLAY_UNSAFE_METHOD_NAMES: readonly string[];
96
142
  /** Deadline for one request; `restore` scales with its replayed step count. */
97
143
  export declare function engineDeadlineMs(method: string, params?: Record<string, unknown>): number;
144
+ /** When the caller gets its answer for `method`: its deadline, capped. */
145
+ export declare function callerDeadlineMs(method: string, params?: Record<string, unknown>, ceilingMs?: number): number;
98
146
  /** Engine protocol version this shell speaks (daemon `hello.protocolVersion`). */
99
147
  export declare const ENGINE_PROTOCOL_VERSION = "0";
100
148
  /** Daemon self-report the shell compares a fresh connection against. */
@@ -126,8 +174,91 @@ export declare class EngineCallError extends Error {
126
174
  constructor(code: string, message: string, remedy: string);
127
175
  toBody(): EngineErrorBody;
128
176
  }
129
- /** Remedy for a deadline overrun: wait and measure, never restart or replay. */
130
- export declare function slowEngineRemedy(method: string): string;
177
+ /**
178
+ * What an agent can learn from a `gp_probe_status` it sent while another call is
179
+ * outstanding, and — the part that was wrong before — what each answer
180
+ * authorises.
181
+ *
182
+ * R8-高1: the sentence this replaces told the agent to "restart only if
183
+ * gp_probe_status times out too". The daemon serves **one request at a time**,
184
+ * so a probe is queued behind the very request it is meant to diagnose: against
185
+ * a busy daemon "the probe timed out as well" is the *expected* answer, not
186
+ * evidence of death. Following it therefore meant running `--restore-launchd`
187
+ * against a daemon that is mid-`act` on the user's screen — and SIGTERM makes
188
+ * that daemon cancel and roll back the in-flight action (`main.swift`'s early
189
+ * shutdown path). A liveness check whose failure branch is reached exactly when
190
+ * the thing it checks is alive is not a check. Only a transport outcome can tell
191
+ * "gone" from "busy", so every outcome now states its own decision, and
192
+ * `test/probe-liveness.test.mjs` pins that exactly one of them may name the
193
+ * restore command.
194
+ */
195
+ export type LivenessProbeOutcome = "answered" | "engine-error" | "timed-out" | "unreachable";
196
+ /** Every outcome the four sentences below cover; the gate iterates this list. */
197
+ export declare const LIVENESS_PROBE_OUTCOMES: readonly LivenessProbeOutcome[];
198
+ /**
199
+ * The decision for one probe outcome. There is no `default` on purpose: adding
200
+ * an outcome to {@link LIVENESS_PROBE_OUTCOMES} without deciding what it
201
+ * authorises is a compile error rather than a silent hole.
202
+ *
203
+ * Only `unreachable` may name the restore command, because only it is raised
204
+ * when the socket itself is gone ({@link GP_E_ENGINE_UNREACHABLE}); `timed-out`
205
+ * is {@link GP_E_ENGINE_TIMEOUT}, which by construction means "no answer yet".
206
+ * The lookup is a function, not a frozen object, so the filesystem probe inside
207
+ * `daemonUnreachableRemedy()` stays out of module initialisation.
208
+ */
209
+ export declare function livenessProbeDecision(outcome: LivenessProbeOutcome): string;
210
+ /**
211
+ * Which way into this shell an answer is being delivered to. The two surfaces
212
+ * hold different state: only the MCP stdio server has an
213
+ * `EvidenceAuditSession`, so only it can promise that a late reply's
214
+ * operationId will come back through `gp_recent_reports`. Promising that to a
215
+ * `curl` caller of the HTTP gateway sends it to a tool that route does not even
216
+ * expose (`POST /v1/tools/recent_reports` is a 404 — `FORWARDABLE_TOOLS` skips
217
+ * every tool with its own `execute`), so each surface gets the sentence about
218
+ * the state it really has (R8b-高3).
219
+ */
220
+ export type ShellSurface = "mcp" | "http";
221
+ /** Where the answer to a timed-out request can be found, per surface. */
222
+ export declare function lateReplyRoute(surface: ShellSurface): string;
223
+ /**
224
+ * Remedy for a deadline overrun: wait and measure, never restart on a probe that
225
+ * merely went unanswered, and never replay a request whose re-issue has a side
226
+ * effect.
227
+ */
228
+ export declare function slowEngineRemedy(method: string, surface?: ShellSurface): string;
229
+ /** Log ceiling for a frame body; excess is marked, never silently cut. */
230
+ export declare const LOG_BODY_CHARS: number;
231
+ /**
232
+ * How a reply frame was matched to the request it is reported for.
233
+ *
234
+ * `echoed-id` is the frame carrying the request id this client sent and the
235
+ * lookup hitting; that is the only attribution this transport can *confirm*.
236
+ * `arrival-order` is the daemon's id-less answer (it loses the id on the paths
237
+ * that could not parse or could not serialize the frame), matched to the oldest
238
+ * request still tracked because the daemon answers in the order it received
239
+ * them — an inference from scheduling, never a confirmation, so anything written
240
+ * from it into an evidence trail has to carry that caveat with it
241
+ * (`EvidenceAuditSession.provenanceOf`).
242
+ *
243
+ * Spelled as the same two literals `audit-session.ts` accepts for the same
244
+ * reason: the transport is the only thing that sees the frame, and the session is
245
+ * the only thing that decides what the trail claims about an id.
246
+ */
247
+ export type FrameAttribution = "echoed-id" | "arrival-order";
248
+ /** One late engine reply, as handed to {@link EngineJsonRpcClient.onLateReply}. */
249
+ export interface LateReply {
250
+ method: string;
251
+ result: unknown;
252
+ /** Whatever `call()` was given; see {@link Pending.correlation}. */
253
+ correlation?: number;
254
+ /**
255
+ * Whether this reply's *request* was confirmed by an echoed id or inferred
256
+ * from arrival order; see {@link FrameAttribution}. A sink that records this
257
+ * body into a trail must not present an `arrival-order` frame as a confirmed
258
+ * one, because the method it is filed under is a guess.
259
+ */
260
+ attribution: FrameAttribution;
261
+ }
131
262
  /**
132
263
  * Request/response association over a line transport. Kept independent of
133
264
  * the transport so tests inject a fake LineIo (spec §7.1 "fake engine
@@ -144,10 +275,29 @@ export declare class EngineJsonRpcClient {
144
275
  * registration.
145
276
  */
146
277
  private readonly callerCeilingMs;
147
- private nextId;
148
- private closed;
278
+ /** Whom a timeout's remedy is being written for; see {@link ShellSurface}. */
279
+ private readonly surface;
280
+ /**
281
+ * Every mutable piece of this client's state, in one object that is *never*
282
+ * reassigned through `this`.
283
+ *
284
+ * That is not a style choice (R10-中4). `tools.ts` hands one tool an engine view
285
+ * built with `Object.create(engine)`, which puts the real client on the shadow's
286
+ * prototype chain so the shadow shares its map, its sinks and its transport — and
287
+ * any method that runs with the *shadow* as `this` then writes its field on the
288
+ * shadow, because assigning a property that exists only on the prototype creates
289
+ * an own property instead of touching the prototype's. `pending` survived that
290
+ * only because it is a `Map` that gets mutated rather than reassigned. A
291
+ * `this.closed = true` did not: it marked the shadow closed and left the client
292
+ * sending requests on a transport it had been told to end. Reading state out of
293
+ * this box still resolves through the chain, and writing a property of it mutates
294
+ * the one box the client itself holds.
295
+ *
296
+ * `test/engine-client.test.mjs` closes the *shadow* and asserts the client behind
297
+ * it is closed, which is the observable form of the whole rule.
298
+ */
299
+ private readonly state;
149
300
  private readonly pending;
150
- private noteHandler;
151
301
  constructor(io: LineIo, timeoutMs?: number | undefined, identity?: EngineIdentityExpectation | null,
152
302
  /**
153
303
  * Ceiling on the caller's wait (B-02). Injectable because a test cannot
@@ -155,7 +305,9 @@ export declare class EngineJsonRpcClient {
155
305
  * caller with a longer patience gets a longer wait, never a longer
156
306
  * registration.
157
307
  */
158
- callerCeilingMs?: number);
308
+ callerCeilingMs?: number,
309
+ /** Whom a timeout's remedy is being written for; see {@link ShellSurface}. */
310
+ surface?: ShellSurface);
159
311
  /**
160
312
  * Sink for facts that cannot be attached to any request of the caller: a
161
313
  * late reply, a frame with an id this client never issued, a daemon that
@@ -163,8 +315,31 @@ export declare class EngineJsonRpcClient {
163
315
  * "not measured" turns into a confident wrong story.
164
316
  */
165
317
  onEngineNote(handler: (note: string) => void): void;
166
- /** Send a request and await its matching response frame. */
167
- call(method: string, params?: Record<string, unknown>): Promise<unknown>;
318
+ /**
319
+ * Sink for the *body* of a reply that lands after its caller was already
320
+ * answered (R8-高3). Logging it is not enough: the operationId in that frame
321
+ * is the only thing that puts the operation into this session's evidence
322
+ * trail, and without it `gp_recent_reports` answers
323
+ * `GP_E_NO_EVIDENCE ... run gp_act first` for an act that genuinely ran and
324
+ * genuinely mutated the user's screen — an agent reading that answer clicks
325
+ * again. The entry the reply is attributed to is only kept for
326
+ * `LATE_REPLY_RETENTION` requests, so this sink sees the tracked window and
327
+ * the eviction note says when something fell outside it.
328
+ */
329
+ onLateReply(handler: (reply: LateReply) => void): void;
330
+ /**
331
+ * Whether {@link close} has ended this client. Public because the answer has to
332
+ * be readable *through* the `Object.create` shadow the tool layer builds: a
333
+ * shadow that was closed has to report the client behind it closed, otherwise
334
+ * "closed" is a property of whichever object happened to receive the call.
335
+ */
336
+ isClosed(): boolean;
337
+ /**
338
+ * Send a request and await its matching response frame. `correlation` is
339
+ * echoed to {@link onLateReply} if this request is answered after its caller
340
+ * has already been given a timeout; it is otherwise unused by this client.
341
+ */
342
+ call(method: string, params?: Record<string, unknown>, correlation?: number): Promise<unknown>;
168
343
  close(): void;
169
344
  /**
170
345
  * Timeout path: settle the caller with a *slow* diagnosis but keep the
@@ -181,14 +356,31 @@ export declare class EngineJsonRpcClient {
181
356
  private settleFromFrame;
182
357
  /**
183
358
  * A frame over the cap: attribute it to the request it belonged to — from
184
- * the retained head when the daemon echoed an id, else by arrival order —
185
- * and fail that one call only.
359
+ * the retained head when the daemon echoed an id, else by arrival order, which
360
+ * is the oldest *tracked* request for the same reason the id-less frame path
361
+ * uses it (R9-高1) — and fail that one call only.
186
362
  */
187
363
  private failOversizedReply;
188
364
  private engineError;
189
- /** Oldest request the caller is still waiting on (daemon FIFO order). */
190
- private oldestOpen;
365
+ /**
366
+ * Oldest request this client still tracks, settled ones included: that is the
367
+ * request the daemon's arrival-ordered answer belongs to. `Map` iteration is
368
+ * insertion order, which is the order the requests went out.
369
+ */
370
+ private oldestTracked;
371
+ /**
372
+ * Route the reply that lands after its caller was already answered. `inferred`
373
+ * marks an attribution made by arrival order rather than by a matching id, and
374
+ * says so in the note — and hands the same distinction to the sink, which is
375
+ * the only place the caveat can travel with the body it records (R10-中2: an
376
+ * id-less result is a *guess about which request this is the answer to*, and a
377
+ * trail that files it without that note presents a guessed frame as a verified
378
+ * one). What the transport may not do is claim the sink recorded anything: it
379
+ * cannot see whether the trail accepted the frame or refused it for belonging to
380
+ * a superseded attach.
381
+ */
191
382
  private reportLateReply;
383
+ private reportSinkFailure;
192
384
  private resolve;
193
385
  private reject;
194
386
  /**
@@ -197,7 +389,17 @@ export declare class EngineJsonRpcClient {
197
389
  * longer answer it, and keep it after a timeout for the late reply.
198
390
  */
199
391
  private complete;
200
- /** Reject everything outstanding and drop every handle. */
392
+ /**
393
+ * Reject everything outstanding and drop every handle. The settled entries go
394
+ * too, and that has to be reported: each of them was answered with "the
395
+ * request stays registered" (`onDeadline`) and with {@link lateReplyRoute}'s
396
+ * promise that its reply is attributed when it lands. This map is what that
397
+ * promise was standing on, so clearing it silently leaves an agent reading the
398
+ * log for an operation that really ran, finding nothing, and unable to tell
399
+ * "this shell stopped tracking it" from "it never happened" — the difference
400
+ * that decides whether it re-performs an `act` on the user's screen (R9-中1).
401
+ * The reverse mutation is to delete this note and keep the silent clear.
402
+ */
201
403
  private teardown;
202
404
  private report;
203
405
  /**
@@ -209,6 +411,7 @@ export declare class EngineJsonRpcClient {
209
411
  private handshake;
210
412
  private verifyIdentity;
211
413
  }
414
+ export declare function truncate(text: string): string;
212
415
  /** How a transport stream is opened; injectable so tests need no socket. */
213
416
  export type EngineTransportOpener = (socketPath: string) => Duplex;
214
417
  /**
@@ -219,6 +422,8 @@ export declare function unixSocketEngineClient(socketPath: string, options?: {
219
422
  identity?: EngineIdentityExpectation;
220
423
  open?: EngineTransportOpener;
221
424
  callerCeilingMs?: number;
425
+ /** Pass `"http"` from the gateway: its callers have no operation trail. */
426
+ surface?: ShellSurface;
222
427
  }): EngineJsonRpcClient;
223
428
  /**
224
429
  * Build a client over an existing LineIo (used by tests with a fake