zcode-acp-server 0.34.0 → 0.35.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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Terminal tab/window titles for TUI-hosted bridges (martty).
3
+ *
4
+ * martty never emits a terminal title, so a CLI-launched or hub-incubated
5
+ * window keeps whatever the shell last set — usually the command name — and a
6
+ * user with several windows cannot tell conversations apart. The bridge runs
7
+ * under martty in the same process group, so it shares the terminal's
8
+ * controlling tty and can name the tab itself with an OSC 0 sequence (the
9
+ * same escape the hub's TUI script prints at incubation time; this module
10
+ * keeps it live as the session gains a real title).
11
+ *
12
+ * Gated on `marttyClientSeen`: only a bridge spawned by a martty may write.
13
+ * A Zed extension host CAN have a controlling tty (Zed launched from a
14
+ * shell) — writing there would hijack an unrelated terminal's title, so
15
+ * non-martty bridges never write. Writes are rare (title lifecycle events,
16
+ * ~40 bytes) and OSC output is terminal chrome, not screen content, so they
17
+ * cannot corrupt martty's drawing beyond a single self-healing frame.
18
+ */
19
+ import type { ZcodeAcpServer } from "./server.js";
20
+ /**
21
+ * Strip everything a terminal title must not carry: C0/C1 controls and DEL
22
+ * (an embedded ESC or BEL could inject escape sequences; BEL also terminates
23
+ * the OSC string early), collapse whitespace runs, trim, cap.
24
+ */
25
+ export declare function sanitizeTitle(raw: string): string;
26
+ /** Injection seam for tests: write the OSC 0 sequence somewhere. */
27
+ export interface TitleIo {
28
+ write(sequence: string): boolean;
29
+ }
30
+ /**
31
+ * The production writer: /dev/tty is the controlling terminal regardless of
32
+ * where stdout/stderr point (both are pipes to martty). Never throws — any
33
+ * failure means "no title support here" and is silently ignored. Also a hard
34
+ * no-op under vitest: a developer's test run shares their real terminal, and
35
+ * titles flashing by during `pnpm test` would be obnoxious.
36
+ */
37
+ export declare const ttyTitleIo: TitleIo;
38
+ /**
39
+ * The tab title for a session: `<project> · <topic>` once a conversation
40
+ * title is known (auto-title, adopted stored title, or rename), else the
41
+ * project directory name alone. The project is the folder basename, capped at
42
+ * PROJECT_MAX chars (ellipsis on cut) so an unwieldy dir name cannot crowd
43
+ * out the topic; when the combined title would exceed TITLE_MAX the TOPIC is
44
+ * trimmed to fit — the project anchors which window this is, the topic is
45
+ * the freshest information. The hub's incubation script builds the same
46
+ * string (ZCODE_ACP_TAB_TITLE), so the title never churns when the bridge
47
+ * takes over from the script's initial printf.
48
+ */
49
+ export declare function sessionTabTitle(title: string | undefined, cwd: string | undefined): string;
50
+ /**
51
+ * Push the current session title to the terminal tab (martty-hosted bridges
52
+ * only — see the module doc for why the gate is mandatory).
53
+ */
54
+ export declare function refreshTerminalTabTitle(server: ZcodeAcpServer, acpSid: string, io?: TitleIo): boolean;
55
+ //# sourceMappingURL=terminal-title.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-title.d.ts","sourceRoot":"","sources":["../src/terminal-title.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAQlD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,oEAAoE;AACpE,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,EAAE,OAoBxB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAe1F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,EACd,EAAE,GAAE,OAAoB,GACvB,OAAO,CAMT"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Terminal tab/window titles for TUI-hosted bridges (martty).
3
+ *
4
+ * martty never emits a terminal title, so a CLI-launched or hub-incubated
5
+ * window keeps whatever the shell last set — usually the command name — and a
6
+ * user with several windows cannot tell conversations apart. The bridge runs
7
+ * under martty in the same process group, so it shares the terminal's
8
+ * controlling tty and can name the tab itself with an OSC 0 sequence (the
9
+ * same escape the hub's TUI script prints at incubation time; this module
10
+ * keeps it live as the session gains a real title).
11
+ *
12
+ * Gated on `marttyClientSeen`: only a bridge spawned by a martty may write.
13
+ * A Zed extension host CAN have a controlling tty (Zed launched from a
14
+ * shell) — writing there would hijack an unrelated terminal's title, so
15
+ * non-martty bridges never write. Writes are rare (title lifecycle events,
16
+ * ~40 bytes) and OSC output is terminal chrome, not screen content, so they
17
+ * cannot corrupt martty's drawing beyond a single self-healing frame.
18
+ */
19
+ import { closeSync, openSync, writeSync } from "node:fs";
20
+ import path from "node:path";
21
+ import { log } from "./utils.js";
22
+ /** Longest total title we ship; tab bars truncate visually anyway. */
23
+ const TITLE_MAX = 100;
24
+ /** Longest project (dir name) segment — the topic is the part worth reading. */
25
+ const PROJECT_MAX = 24;
26
+ /**
27
+ * Strip everything a terminal title must not carry: C0/C1 controls and DEL
28
+ * (an embedded ESC or BEL could inject escape sequences; BEL also terminates
29
+ * the OSC string early), collapse whitespace runs, trim, cap.
30
+ */
31
+ export function sanitizeTitle(raw) {
32
+ const collapsed = raw
33
+ // eslint-disable-next-line no-control-regex
34
+ .replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ")
35
+ .replace(/\s+/g, " ")
36
+ .trim();
37
+ return collapsed.length > TITLE_MAX ? collapsed.slice(0, TITLE_MAX - 1) + "…" : collapsed;
38
+ }
39
+ /**
40
+ * The production writer: /dev/tty is the controlling terminal regardless of
41
+ * where stdout/stderr point (both are pipes to martty). Never throws — any
42
+ * failure means "no title support here" and is silently ignored. Also a hard
43
+ * no-op under vitest: a developer's test run shares their real terminal, and
44
+ * titles flashing by during `pnpm test` would be obnoxious.
45
+ */
46
+ export const ttyTitleIo = {
47
+ write(sequence) {
48
+ if (process.env.VITEST)
49
+ return false;
50
+ let fd;
51
+ try {
52
+ fd = openSync("/dev/tty", "w");
53
+ writeSync(fd, sequence);
54
+ return true;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ finally {
60
+ if (fd !== undefined) {
61
+ try {
62
+ closeSync(fd);
63
+ }
64
+ catch {
65
+ // best-effort close
66
+ }
67
+ }
68
+ }
69
+ },
70
+ };
71
+ /**
72
+ * The tab title for a session: `<project> · <topic>` once a conversation
73
+ * title is known (auto-title, adopted stored title, or rename), else the
74
+ * project directory name alone. The project is the folder basename, capped at
75
+ * PROJECT_MAX chars (ellipsis on cut) so an unwieldy dir name cannot crowd
76
+ * out the topic; when the combined title would exceed TITLE_MAX the TOPIC is
77
+ * trimmed to fit — the project anchors which window this is, the topic is
78
+ * the freshest information. The hub's incubation script builds the same
79
+ * string (ZCODE_ACP_TAB_TITLE), so the title never churns when the bridge
80
+ * takes over from the script's initial printf.
81
+ */
82
+ export function sessionTabTitle(title, cwd) {
83
+ const cwdTrimmed = (cwd ?? "").trim();
84
+ // Both segments are sanitized: the cwd is CLIENT-supplied (a remote client
85
+ // controls its session/new cwd), and a dirname containing ESC/BEL would
86
+ // otherwise flow raw into the OSC payload (arbitrary OSC injection, e.g.
87
+ // clipboard writes). sanitizeTitle also collapses the length cap below.
88
+ let project = cwdTrimmed ? sanitizeTitle(path.basename(cwdTrimmed)) : "";
89
+ if (project.length > PROJECT_MAX)
90
+ project = project.slice(0, PROJECT_MAX - 1) + "…";
91
+ if (!project)
92
+ project = "zcode";
93
+ const topic = sanitizeTitle(title ?? "");
94
+ if (!topic)
95
+ return project;
96
+ // PROJECT_MAX keeps this comfortably positive (100 - 24 - 3).
97
+ const budget = TITLE_MAX - project.length - 3;
98
+ const fitTopic = topic.length > budget ? topic.slice(0, budget - 1) + "…" : topic;
99
+ return `${project} · ${fitTopic}`;
100
+ }
101
+ /**
102
+ * Push the current session title to the terminal tab (martty-hosted bridges
103
+ * only — see the module doc for why the gate is mandatory).
104
+ */
105
+ export function refreshTerminalTabTitle(server, acpSid, io = ttyTitleIo) {
106
+ if (!server.marttyClientSeen)
107
+ return false;
108
+ const title = sessionTabTitle(server.sessionTitles.get(acpSid), server.sessionCwds.get(acpSid));
109
+ const ok = io.write(`\x1b]0;${title}\x07`);
110
+ if (!ok)
111
+ log(`terminal title not written (no tty or unsupported)`);
112
+ return ok;
113
+ }
114
+ //# sourceMappingURL=terminal-title.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-title.js","sourceRoot":"","sources":["../src/terminal-title.ts"],"names":[],"mappings":"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"}
@@ -318,8 +318,10 @@ zcode interaction request received
318
318
  │ └─ Always uses session/request_permission (its native purpose)
319
319
  │
320
320
  ├─ ExitPlanMode (interaction/requestUserInput + plan_approval)
321
- │ ├─ Client supports elicitation.form → elicitation/create (approve/reject form)
322
- │ └─ Otherwise → session/request_permission (fallback)
321
+ │ ├─ martty attached (hasMarttyClient) → elicitation/create (approve/reject form;
322
+ │ │ its permission overlay draws only the title — the plan needs the form)
323
+ │ └─ Otherwise → session/request_permission (editors render toolCall.content
324
+ │ as full markdown; elicitation form descriptions are plain text there)
323
325
  │
324
326
  └─ AskUserQuestion (interaction/requestUserInput)
325
327
  ├─ Client supports elicitation.form → elicitation/create (single form)
@@ -327,8 +329,11 @@ zcode interaction request received
327
329
  ```
328
330
 
329
331
  **Key**: `server.supportsElicitationForm()` is detected at `initialize` time from
330
- `clientCapabilities.elicitation.form`. Tool auth always goes through
331
- request_permission, since that is its native purpose.
332
+ `clientCapabilities.elicitation.form` (AskUserQuestion). ExitPlanMode keys on
333
+ `server.hasMarttyClient()` instead — Zed ≥1.12 declares `elicitation.form` too,
334
+ but its form descriptions are plain text while its permission popups render
335
+ `toolCall.content` as markdown, so only martty benefits from the form. Tool auth
336
+ always goes through request_permission, since that is its native purpose.
332
337
 
333
338
  ## Deferred Notification Mechanism
334
339
 
package/docs/PROTOCOL.md CHANGED
@@ -566,17 +566,24 @@ User input request (ExitPlanMode / AskUserQuestion).
566
566
  ### Bridge routing (protocol negotiation)
567
567
 
568
568
  ZCode `interaction/*` requests are routed to different ACP interaction
569
- mechanisms based on client capabilities:
570
-
571
- | Request type | Client supports elicitation.form | Client does not |
572
- | ------------------------------------------------------------- | :----------------------------------------: | :---------------------------------------: |
573
- | Tool auth (`interaction/requestPermission`) | `session/request_permission` | `session/request_permission` |
574
- | ExitPlanMode (`interaction/requestUserInput` + plan_approval) | `elicitation/create` (approve/reject form) | `session/request_permission` |
575
- | AskUserQuestion (`interaction/requestUserInput`) | `elicitation/create` (single form) | per-question `session/request_permission` |
576
-
577
- **Capability detection**: at `initialize` time the client declares support via
578
- `clientCapabilities.elicitation.form`. The server detects it with
579
- `server.supportsElicitationForm()`.
569
+ mechanisms. Each request type has its OWN gate:
570
+
571
+ | Request type | Routing when the gate is OPEN | Routing when the gate is CLOSED | Gate |
572
+ | ------------------------------------------------------------- | :----------------------------------------: | :---------------------------------------: | :--------------------------------- |
573
+ | Tool auth (`interaction/requestPermission`) | `session/request_permission` | `session/request_permission` | none (always the same path) |
574
+ | ExitPlanMode (`interaction/requestUserInput` + plan_approval) | `elicitation/create` (approve/reject form) | `session/request_permission` | `server.hasMarttyClient()` |
575
+ | AskUserQuestion (`interaction/requestUserInput`) | `elicitation/create` (single form) | per-question `session/request_permission` | `server.supportsElicitationForm()` |
576
+
577
+ **Capability detection**: AskUserQuestion keys on
578
+ `clientCapabilities.elicitation.form` (detected at `initialize` via
579
+ `server.supportsElicitationForm()`) — structured input is the correct
580
+ elicitation use for any form-capable client. ExitPlanMode keys on
581
+ `server.hasMarttyClient()` instead: martty's request_permission overlay draws
582
+ only the title, so the plan needs the form's scrollable detail pane — while
583
+ editors render `toolCall.content` as full markdown (Zed: `MarkdownElement`,
584
+ code blocks and links) and draw form descriptions as plain text, so the form
585
+ would downgrade them. Zed ≥1.12 declares `elicitation.form`, which is why the
586
+ gate keys on the client kind, not the capability.
580
587
 
581
588
  **Reconnect resend**: interaction requests are one-shot and raced across the
582
589
  clients connected when they fire (first response wins). A client that was
@@ -638,10 +645,10 @@ that question without cancelling the form.
638
645
  }
639
646
  ```
640
647
 
641
- **ExitPlanMode elicitation form** — sent only to clients that advertised
642
- elicitation form support; everyone else gets `session/request_permission` with
643
- the plan in `toolCall.content`. The form carries the COMPLETE plan markdown as
644
- the `approval` field's `description` (markdown-capable clients render it in a
648
+ **ExitPlanMode elicitation form** — sent only when a martty TUI is attached
649
+ (`server.hasMarttyClient()`); everyone else gets `session/request_permission`
650
+ with the plan in `toolCall.content`. The form carries the COMPLETE plan
651
+ markdown as the `approval` field's `description` (martty renders it in a
645
652
  scrollable detail pane); the decision itself is a required approve/reject enum.
646
653
  An `accept` whose `content.approval` is `"approve"` maps to the zcode accept
647
654
  (`answer_0: "approve"`); a `reject`/`cancel` action, or `"reject"` in the
@@ -67,13 +67,13 @@ a hardcoded version string — read the message text to identify the root cause.
67
67
 
68
68
  **Common causes:**
69
69
 
70
- | Message fragment | Cause |
71
- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
72
- | `reader exited (backend dead)` | The zcode subprocess crashed/exited. Restart the editor session. |
73
- | `timeout` | The per-attempt 5s subscribe deadline elapsed. The bridge retries transient timeouts once (2 attempts total, ~10.5s worst case); if both fail, the backend was unresponsive for that window. |
74
- | `pipe broken` | The stdin pipe to the zcode subprocess broke (process died mid-write). |
75
- | `method not found (code -32601)` | The CLI genuinely is too old (< 0.14.8). Upgrade. |
76
- | `Session is not active` (-32004) | The backend evicted the session's resident runtime (idle ~10min, or its LRU cap). The bridge self-heals via `session/resume` (see below). |
70
+ | Message fragment | Cause |
71
+ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
72
+ | `reader exited (backend dead)` | The zcode subprocess crashed/exited. Restart the editor session. |
73
+ | `timeout` | The per-attempt 5s subscribe deadline elapsed. The bridge retries transient timeouts once (2 attempts total, ~10.5s worst case); if both fail, the backend was unresponsive for that window. |
74
+ | `pipe broken` | The stdin pipe to the zcode subprocess broke (process died mid-write). |
75
+ | `method not found (code -32601)` | The CLI genuinely is too old (< 0.14.8). Upgrade. |
76
+ | `Session is not active` (-32004) | The backend evicted the session's resident runtime (idle ~10min, or its LRU cap). The bridge self-heals via `session/resume` (see below). |
77
77
 
78
78
  **`Session is not active` (code -32004) in detail:**
79
79
 
@@ -182,6 +182,10 @@ log (`~/.zcode/cli/log/zcode-YYYY-MM-DD.jsonl`) for the underlying cause
182
182
  - `⟳ AskUserQuestion forwarding elicitation/create (form, N fields)` → elicitation path
183
183
  - `⟳ AskUserQuestion forwarding session/request_permission` → fallback path
184
184
  - The path is decided by `clientCapabilities.elicitation.form`
185
+ - ExitPlanMode has its own gate: `⟳ ExitPlanMode forwarding elicitation/create` only
186
+ fires when `hasMarttyClient` is true (a martty TUI attached to this bridge — the
187
+ flag is sticky for the process lifetime); other clients take
188
+ `session/request_permission` (their popups render the plan markdown)
185
189
 
186
190
  3. Check whether `askOnce` (fallback path) or `handleAskUserViaElicitation`
187
191
  (elicitation path) successfully sent the request:
@@ -382,6 +386,26 @@ re-registered from the durable store without a resume (or left behind by a
382
386
  failed one) therefore replayed nothing. Fixed by explicit backend-loaded
383
387
  tracking; the backend also logs a warning now when `session/messages` errors.
384
388
 
389
+ ### Remote access: a conversation replays only PARTLY on first entry
390
+
391
+ **Symptom:** entering a resumed conversation the FIRST time shows history
392
+ that ends in the middle; leaving and re-entering shows the full conversation.
393
+ Happens often (but not always) with large sessions.
394
+
395
+ **Cause:** the hub answers the resume request as soon as the incubated
396
+ terminal bridge registers — before the terminal's boot-resume finishes — so
397
+ the App's `session/load` raced the boot-resume for the SAME backend session.
398
+ Both sent `session/resume` concurrently, and `session/messages` reflects only
399
+ what the backend has hydrated so far: a query landing mid-restore returns a
400
+ PREFIX, which was replayed as if it were the whole conversation. Fixed by
401
+ single-flighting `session/resume` TOGETHER WITH its hydration settle per
402
+ backend session id (a concurrent load joins the in-flight flight and shares
403
+ its settled history snapshot; the settle requires two consecutive
404
+ non-growing reads, capped). The bridge log line
405
+ `session/load: replayed N messages (total M)` now prints the total
406
+ unconditionally — a first-entry total below the session's real size was the
407
+ signature of this bug.
408
+
385
409
  ### Start Plan (zcode-plan) providers fail headless — 1113 / signing errors
386
410
 
387
411
  **Symptom:** every turn fails with HTTP 429 error `1113` ("Insufficient
@@ -433,6 +457,38 @@ tool output contains `Operation not permitted` — that warning is your signal
433
457
  to suspect the sandbox. Path grants for legitimate writes go in
434
458
  `.zcode/acp/sandbox.json`.
435
459
 
460
+ ### UNRESOLVED: CLI (martty) freezes at an old state while the mobile app keeps updating
461
+
462
+ **Symptom (observed once, 2026-09, no live instance preserved):** a CLI window
463
+ attached to a conversation stops receiving everything — messages, model
464
+ dropdown, thought-level updates — while the mobile app on the same
465
+ conversation keeps receiving and interacting normally. The CLI stays frozen at
466
+ a state noticeably older than the conversation.
467
+
468
+ **Working hypotheses (in rough likelihood order), none confirmed:**
469
+
470
+ 1. **Session-alias divergence** — the interaction moved to an ACP session id
471
+ the martty connection never adopted (e.g. a phone-side `session/new` racing
472
+ the window's binding). martty drops every update addressed to a session id
473
+ it does not know (same failure shape as the 2026-09-06 boot-create
474
+ diagnosis in AGENTS.md). Everything stalling at once — config updates
475
+ included — fits a sessionId mismatch, since all of them are session-scoped.
476
+ 2. **Two different bridge processes** — the phone attached to the hub's serve
477
+ bridge while the CLI runs its own bridge: live events do not cross
478
+ processes by design (only session listings do). The CLI then only ever
479
+ updates for its own turns.
480
+ 3. **martty stdio wedge** — the TUI's own input thread stalls; the bridge's
481
+ notifies buffer into the pipe and the mobile (WS path) is unaffected.
482
+ Bridge-side nothing is wrong; typing in the frozen CLI would also be dead.
483
+
484
+ **If it recurs, capture before closing anything:** whether typing still works
485
+ in the frozen CLI (separates hypothesis 3 from 1/2); the bridge's stderr with
486
+ `ZCODE_ACP_DEBUG=1` (do `session/update` notifies still leave?); the hub's
487
+ instance listing (`GET /api/instances`) — which bridge id the phone's
488
+ conversation is on vs the CLI's; and both ends' session ids (CLI transcript vs
489
+ phone). See also the resume-race diagnosis in the same period: a partial
490
+ first-entry replay is a different bug (history prefix), do not conflate.
491
+
436
492
  ## Log Debugging
437
493
 
438
494
  ### Enable verbose logging
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zcode-acp-server",
3
- "version": "0.34.0",
3
+ "version": "0.35.1",
4
4
  "description": "Agent Client Protocol (ACP) server bridging headless ZCode to editors like Zed and JetBrains.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",