open-claude-p 1.1.0 → 1.1.2

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/CHANGELOG.md CHANGED
@@ -7,6 +7,111 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.1.2] — 2026-05-19
11
+
12
+ Recovery-path fixes for the sentinel-missing case. When the end-of-reply
13
+ marker fails to land — usually because claude omitted it or the PTY
14
+ frames were truncated — the driver's text extraction was returning
15
+ either an empty string or, worse, the entire re-rendered transcript of
16
+ a `--resume` session. Two changes correct that.
17
+
18
+ ### Fixed
19
+
20
+ - **`extractAssistantText` no longer slices from the FIRST `⏺`** when
21
+ the sentinel is missing. On a `--resume` run the buffer carries old
22
+ history regions before the current response; anchoring on the first
23
+ `⏺` returned the oldest history turn plus everything after it,
24
+ silently leaking prior turns into `result.text`. The fallback now
25
+ uses `lastIndexOf('⏺')` so the slice always covers the current
26
+ turn's response. Fresh (non-resumed) sessions are unaffected because
27
+ the buffer only has one region marker, so first == last.
28
+
29
+ ### Added
30
+
31
+ - **Degraded-capture notice prefix.** When `completionReason` is
32
+ `'idle'` (sentinel never seen, idle-fallback completed) or
33
+ `'jsonl-recovered'` (a stall was rescued by reading the upstream
34
+ JSONL session log), `result.text` is now prefixed with:
35
+
36
+ ```
37
+ [ocp] Streaming capture not detected — showing last result from {source}.
38
+ ```
39
+
40
+ where `{source}` is either `local session log` (JSONL had the
41
+ response) or `terminal buffer (best effort)` (JSONL was unavailable
42
+ and we fell back to the PTY slice above). Callers and chat UIs can
43
+ now distinguish a normal capture from a post-hoc recovery without
44
+ inspecting `completionReason`. The default success path
45
+ (`reason='sentinel'`) is unchanged — no prefix is added.
46
+
47
+ ---
48
+
49
+ ## [1.1.1] — 2026-05-19
50
+
51
+ Post-1.1.0 fixes driven by Cosmica integration testing. Several of
52
+ these are essentially the 1.1.0 release "actually working" — the
53
+ 1.1.0 surface was correct on paper but a handful of subtle parser
54
+ and default-value bugs kept the new behaviour from landing in
55
+ practice.
56
+
57
+ ### Fixed
58
+
59
+ - **Trust dialog auto-accept regressed silently for two distinct
60
+ reasons.**
61
+ - The driver's `❯` chevron pattern matched both the real input
62
+ box and the trust dialog's "currently-selected" row
63
+ (`❯ 1. Yes, I trust this folder`). `prompt-box-shown` fired
64
+ on the dialog row, the await chain proceeded to the prompt
65
+ write, and the trailing `\r` confirmed whichever option was
66
+ highlighted — silently dropping the user message. Pattern is
67
+ now `/^[❯›❮‹](?:$|\s(?!\d+\.\s))/` and a write-time guard
68
+ refuses to send when `dialogState` is in a blocked state.
69
+ - The trust dialog text is rendered with cursor-positioning
70
+ escapes between words rather than real space bytes; after the
71
+ ANSI strip the buffer reads `Quicksafetycheck` (no spaces) and
72
+ the `/Quick safety check/i` pattern never matched. Result: the
73
+ dialog rendered in ~250 ms but the watcher never fired, the
74
+ driver fell back to writing the prompt after
75
+ `OCP_PROMPT_BOX_WAIT_MS`, and the trailing CR confirmed "Yes"
76
+ in the still-open dialog. The watcher now strips ANSI before
77
+ pattern-matching and the patterns accept `\s*` between every
78
+ word.
79
+ - **`OCP_PROMPT_BOX_WAIT_MS` default 6 s → 30 s.** Environments
80
+ with many MCP servers or plugins need 8–25 s before the input
81
+ box renders. 6 s was tripping the timeout before claude had
82
+ even shown the trust dialog. Short-circuited the moment
83
+ `prompt-box-shown` fires, so the common-case latency is
84
+ unchanged.
85
+ - **`dangerouslySkipPermissions` is now ON by default across
86
+ EVERY surface** — not just the CLI. The 1.1.0 commit only
87
+ flipped the CLI's default and left `createDriver().runOneShot()`
88
+ and `createChatClient()` on the pre-1.1 `false`, so anything
89
+ using the SDK directly (the bundled sample chat, third-party
90
+ wrappers) still hung on the first tool call. Sample server's
91
+ redundant env-gate is dropped accordingly.
92
+ - **End-of-reply marker no longer flagged as
93
+ "prompt-injection".** The 1.0/1.1 instruction text used the
94
+ word "token": `Append the literal token ⟦OCP_END:xxx⟧ ...`.
95
+ The model's safety classifier treated "include this token in
96
+ your reply" as an access-token / API-key exfiltration attempt
97
+ and sometimes appended a `prompt injection attempt detected`
98
+ warning to the user-facing reply. Reworded to "end-of-reply
99
+ marker" and framed as `ocp wrapper's plumbing` so claude
100
+ recognises it as harness infrastructure rather than hostile
101
+ injection.
102
+
103
+ ### Changed
104
+
105
+ - **postinstall skips the shebang rewrite when running inside a
106
+ git checkout** (detected by the presence of `.git/` next to
107
+ `package.json`). The rewrite is meant for end-user installs from
108
+ the npm tarball; in a dev `git clone + npm link` setup it
109
+ otherwise overwrote `#!/usr/bin/env node` with the developer's
110
+ absolute node path on every `npm install` / `npm test`, showing
111
+ up as a noisy working-tree diff. Consumer installs unaffected.
112
+
113
+ ---
114
+
10
115
  ## [1.1.0] — 2026-05-18
11
116
 
12
117
  `ocp` 1.1 reshapes the CLI defaults around the assumption that
@@ -64,12 +169,18 @@ extraction path so PTY chrome can no longer leak into responses.
64
169
 
65
170
  ### Changed
66
171
 
67
- - **`--dangerously-skip-permissions` is now the CLI default.**
68
- Rationale: `ocp` is a non-interactive PTY automation surface. A
69
- permission prompt that wants a human y/n hangs forever — every
70
- Bash / Edit / Write / MCP call breaks without this flag. Library
71
- callers (`createDriver` / `createChatClient`) keep the
72
- conservative `false` default; only the `ocp` binary flips.
172
+ - **`--dangerously-skip-permissions` / `dangerouslySkipPermissions`
173
+ is now the default ON across EVERY surface** — `ocp` CLI,
174
+ `createDriver().runOneShot()`, `createChatClient()`, and the
175
+ `sample/` server. Rationale: `ocp` is a PTY automation library; an
176
+ interactive permission prompt that wants a human y/n hangs forever
177
+ every Bash / Edit / Write / MCP / WebSearch call breaks without
178
+ this flag. The pre-1.1 library default of `false` was the cause
179
+ of nearly every "the SDK silently hangs on tool use" report.
180
+ Opt out per surface:
181
+ - CLI: `OCP_NO_SKIP_PERMS=1` (env)
182
+ - `createChatClient({ dangerouslySkipPermissions: false })`
183
+ - `runOneShot({ dangerouslySkipPermissions: false })`
73
184
  Pre-1.1 users with `OCP_DEFAULT_SKIP_PERMS=1` exported see no
74
185
  change; the env is now a no-op.
75
186
  - **Folder-trust dialog is auto-accepted by default.** Same
@@ -183,5 +294,6 @@ Initial release.
183
294
  `OCP_NO_DEFAULT_PROMPT=1`).
184
295
  - Localised READMEs: Korean, Japanese, Chinese.
185
296
 
297
+ [1.1.1]: https://github.com/empty-user77/open-claude-p/releases/tag/v1.1.1
186
298
  [1.1.0]: https://github.com/empty-user77/open-claude-p/releases/tag/v1.1.0
187
299
  [1.0.0]: https://github.com/empty-user77/open-claude-p/releases/tag/v1.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-claude-p",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "A PTY-backed compatibility shim for `claude -p` (Claude Code headless/print mode). Drives the interactive `claude` CLI via node-pty and exposes the same option surface and stream-json contract. Ships as both a library and an `ocp` CLI binary.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -79,6 +79,23 @@ if (!fixedAny) {
79
79
  // and the file extension makes shebang lines a no-op.
80
80
  if (process.platform !== 'win32') {
81
81
  const here = path.dirname(fileURLToPath(import.meta.url));
82
+ const repoRoot = path.resolve(here, '..');
83
+ // Skip the shebang rewrite when this install is a git checkout
84
+ // (dev environment via `git clone + npm link`, or a contributor
85
+ // running `npm install` inside the repo). The rewrite would
86
+ // otherwise overwrite the portable `#!/usr/bin/env node` with the
87
+ // developer's absolute node path, which then shows up as a noisy
88
+ // working-tree diff on every commit and risks getting committed
89
+ // by mistake. Consumers installing from the npm tarball never
90
+ // have a `.git/` so they still get the GUI-launch fix.
91
+ try {
92
+ const st = await lstat(path.join(repoRoot, '.git'));
93
+ if (st.isDirectory() || st.isFile()) {
94
+ process.stdout.write('postinstall: detected git checkout — skipping shebang rewrite\n');
95
+ process.exit(0);
96
+ }
97
+ } catch { /* no .git/ — npm-tarball install, proceed */ }
98
+
82
99
  // Every shipped bin entry needs the same shebang treatment. List them
83
100
  // here rather than re-reading package.json (the file may be pruned
84
101
  // from production installs and the bin field is small enough to
package/src/chat/index.js CHANGED
@@ -500,9 +500,13 @@ const SENTINEL_REGEX = /⟦OCP_END:[a-f0-9]+⟧/g;
500
500
  * @param {string} [opts.dbPath] Path to the conversations JSON file. Default `./conversations.json`
501
501
  * in the process cwd, so each project keeps its own history.
502
502
  * @param {string} [opts.skillsDir] Directory holding `<name>/SKILL.md` files. Default `~/.claude/skills`.
503
- * @param {boolean} [opts.dangerouslySkipPermissions=false]
503
+ * @param {boolean} [opts.dangerouslySkipPermissions=true]
504
504
  * Forward `--dangerously-skip-permissions` to upstream so tool
505
- * calls (WebSearch, Bash, …) do not block on a permission prompt.
505
+ * calls (WebSearch, Bash, …) do not block on a permission prompt
506
+ * PTY automation cannot answer. Default ON in 1.1+ across every
507
+ * surface (CLI, driver, chat client) — pass `false` explicitly
508
+ * to restore claude's interactive permission prompts (and accept
509
+ * that every tool call will then hang).
506
510
  * @param {string|null} [opts.appendSystemPrompt] Appended to every turn's system prompt. Pass `null` to disable
507
511
  * the default ("use WebSearch immediately …") text.
508
512
  * @param {object} [opts.pricing] Per-token rates: `{ input, cacheWrite, cacheRead, output }`.
@@ -518,7 +522,12 @@ export function createChatClient(opts = {}) {
518
522
  ? path.resolve(opts.dbPath)
519
523
  : path.resolve(process.cwd(), DEFAULT_DB_FILENAME);
520
524
  const skillsDir = path.resolve(opts.skillsDir ?? DEFAULT_SKILLS_DIR);
521
- const dangerouslySkipPermissions = opts.dangerouslySkipPermissions ?? false;
525
+ // Default ON across the SDK in 1.1+: PTY automation cannot answer
526
+ // an interactive permission prompt, so leaving this off makes the
527
+ // first tool call hang forever. Library callers who genuinely want
528
+ // claude to prompt must pass `dangerouslySkipPermissions: false`
529
+ // explicitly. The `??` operator preserves explicit `false`.
530
+ const dangerouslySkipPermissions = opts.dangerouslySkipPermissions ?? true;
522
531
  // Store the raw client-level option (string | null | undefined); the
523
532
  // per-turn compose merges it with the SDK base default and any
524
533
  // per-turn override.
package/src/index.js CHANGED
@@ -163,7 +163,15 @@ class Driver {
163
163
  maxEvents: finiteNonNeg(opts.maxEvents, 10_000),
164
164
  firstResponseMs: finiteNonNeg(opts.firstResponseMs, numberFromEnv('OCP_FIRST_RESPONSE_MS', 120_000)),
165
165
  trustSettleMs: finiteNonNeg(opts.trustSettleMs, numberFromEnv('OCP_TRUST_SETTLE_MS', 2_500)),
166
- promptBoxWaitMs: finiteNonNeg(opts.promptBoxWaitMs, numberFromEnv('OCP_PROMPT_BOX_WAIT_MS', 6_000)),
166
+ // 30 s default: in environments with many MCP servers, plugins,
167
+ // or hooks the first prompt-box render can take 8–25 s. The pre-
168
+ // 1.1 default of 6 s caused the driver to give up on
169
+ // prompt-box-shown and write the prompt anyway, *before* claude
170
+ // had even shown the trust dialog — the prompt's trailing CR
171
+ // then confirmed whatever option was highlighted on the dialog
172
+ // and the user message was dropped. 30 s leaves room for slow
173
+ // startups while still bounding total spawn latency.
174
+ promptBoxWaitMs: finiteNonNeg(opts.promptBoxWaitMs, numberFromEnv('OCP_PROMPT_BOX_WAIT_MS', 30_000)),
167
175
  initialSessionId: opts.initialSessionId ?? null,
168
176
  printMode:
169
177
  opts.printMode === true ||
@@ -199,7 +207,11 @@ class Driver {
199
207
  * @param {string} [req.systemPrompt]
200
208
  * @param {string[]} [req.allowedTools]
201
209
  * @param {string[]} [req.disallowedTools]
202
- * @param {boolean} [req.dangerouslySkipPermissions]
210
+ * @param {boolean} [req.dangerouslySkipPermissions=true]
211
+ * Default ON in 1.1+. PTY automation cannot answer claude's
212
+ * interactive y/n permission prompt, so leaving it OFF makes
213
+ * the first tool call hang forever. Pass `false` explicitly to
214
+ * restore the prompts (and accept the hang).
203
215
  * @param {boolean} [req.debug]
204
216
  * @param {boolean} [req.verbose]
205
217
  * @param {string[]} [req.passThroughArgv] extra argv to forward verbatim
@@ -215,6 +227,14 @@ class Driver {
215
227
  throw new Error('runOneShot: `prompt` (string) is required');
216
228
  }
217
229
 
230
+ // Default ON across the driver in 1.1+: PTY automation has no
231
+ // way to answer claude's interactive permission prompt, so a
232
+ // request that doesn't explicitly set it would otherwise hang on
233
+ // the first tool call. We mutate `req` in place because every
234
+ // downstream branch (print-mode, daemon proxy, buildSpawnArgs)
235
+ // reads the same field. `??` keeps an explicit `false` intact.
236
+ req.dangerouslySkipPermissions = req.dangerouslySkipPermissions ?? true;
237
+
218
238
  // Print-mode short-circuit: spawn `claude --print` directly, bypass
219
239
  // the PTY+TUI pipeline. Caller opts in via req.printMode, driver-level
220
240
  // opts.printMode, or env OCP_PRINT_MODE=1.
@@ -426,10 +446,16 @@ class Driver {
426
446
  || process.env.OCP_NO_AUTO_ACCEPT_TRUST === 'true'
427
447
  || req.autoAcceptFolderTrust === false;
428
448
  const autoAcceptTrust = !autoAcceptOptOut;
449
+ // The trust dialog text is rendered with cursor-positioning
450
+ // escapes between words rather than real space bytes. After our
451
+ // ANSI strip the buffer reads `Quicksafetycheck...` with the
452
+ // spaces gone, so `/Quick safety check/i` no longer matches.
453
+ // Allow zero-or-more whitespace between every word and accept
454
+ // the seq with or without spaces.
429
455
  const TRUST_PATTERNS = [
430
- /Quick safety check/i,
431
- /Is this a project you (?:created|trust)/i,
432
- /trust this folder/i,
456
+ /Quick\s*safety\s*check/i,
457
+ /Is\s*this\s*a\s*project\s*you\s*(?:created|trust)/i,
458
+ /trust\s*this\s*folder/i,
433
459
  ];
434
460
  const TRUST_SETTLE_MS = this.opts.trustSettleMs
435
461
  ?? numberFromEnv('OCP_TRUST_SETTLE_MS')
@@ -439,7 +465,21 @@ class Driver {
439
465
 
440
466
  const dialogWatchHandler = (chunk) => {
441
467
  if (dialogState !== 'none') return;
442
- dialogScanBuf += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
468
+ // Strip ANSI escapes BEFORE pattern-matching. claude's TUI wraps
469
+ // the dialog text in color codes mid-word ("\x1b[1mQ\x1b[muick
470
+ // safety check..."), which prevents a simple `/Quick safety/`
471
+ // regex from ever matching the raw byte stream. Result observed
472
+ // in user testing: trust dialog renders in 1-2 s but our
473
+ // detector never fires, the driver falls back to writing the
474
+ // user's prompt after `OCP_PROMPT_BOX_WAIT_MS`, and the
475
+ // trailing `\r` confirms whichever dialog option is highlighted
476
+ // (silently dropping the user message). Stripping here keeps
477
+ // the watcher decoupled from the main parser pipeline while
478
+ // still seeing the plain text claude actually rendered.
479
+ const raw = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
480
+ dialogScanBuf += raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '')
481
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
482
+ .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
443
483
  if (dialogScanBuf.length > 16384) dialogScanBuf = dialogScanBuf.slice(-8192);
444
484
  if (TRUST_PATTERNS.some((p) => p.test(dialogScanBuf))) {
445
485
  if (autoAcceptTrust) {
@@ -508,6 +548,27 @@ class Driver {
508
548
  promptBoxReadyPromise,
509
549
  sleep(PROMPT_BOX_WAIT_MS).then(() => { promptBoxTimeoutHit = true; }),
510
550
  ]);
551
+ // If the dialog watcher is mid-flow (trust just auto-accepted)
552
+ // when our prompt-box wait times out, claude is still transitioning
553
+ // from the dialog to the main UI — the chevron has not landed
554
+ // because it doesn't exist yet. Give it the trust-settle window
555
+ // plus another bounded wait for the real input box to appear.
556
+ // Writing now would land the prompt into the dialog (and its
557
+ // trailing `\r` would confirm whichever option is highlighted),
558
+ // silently dropping the user's message.
559
+ if (promptBoxTimeoutHit && !promptBoxReady && dialogState === 'trust-accepted') {
560
+ const EXTRA_WAIT = PROMPT_BOX_WAIT_MS;
561
+ this._logDebug(
562
+ `prompt-box-shown still not seen but trust dialog is processing — ` +
563
+ `extending wait by ${EXTRA_WAIT}ms`,
564
+ );
565
+ let extendedTimeoutHit = false;
566
+ await Promise.race([
567
+ promptBoxReadyPromise,
568
+ sleep(EXTRA_WAIT).then(() => { extendedTimeoutHit = true; }),
569
+ ]);
570
+ promptBoxTimeoutHit = extendedTimeoutHit;
571
+ }
511
572
  if (promptBoxTimeoutHit && !promptBoxReady) {
512
573
  this._logDebug(`prompt-box-shown not seen within ${PROMPT_BOX_WAIT_MS}ms — sending anyway`);
513
574
  } else if (promptBoxReady && PROMPT_BOX_SETTLE_MS > 0) {
@@ -516,22 +577,50 @@ class Driver {
516
577
  await sleep(PROMPT_BOX_SETTLE_MS);
517
578
  }
518
579
 
519
- // Sentinel marker appended to every user turn so PTY automation
520
- // can tell when the reply is done. Phrased to be inert to model
521
- // behaviour — do NOT use words like "finish", "complete",
522
- // "wrap up", or "final" that nudge the model to cut tool use
523
- // short. The reply itself is the actual signal of completion;
524
- // this marker is just the bookkeeping byte we emit afterwards.
580
+ // End-of-reply marker appended to every user turn so PTY
581
+ // automation can tell when the reply is done. Word choice
582
+ // matters here:
583
+ // - **Avoid "token"**: the model's safety training flags
584
+ // prompts of the form "include this token in your reply"
585
+ // as access-token / API-key exfiltration attempts; it then
586
+ // warns the user about a "prompt injection attempt" in
587
+ // the response, which is alarming and user-visible. Use
588
+ // "marker" / "sentinel" instead — they carry no auth
589
+ // baggage.
590
+ // - **Avoid "finish", "complete", "wrap up", "final"**: those
591
+ // nudge the model to cut tool use short. The reply itself
592
+ // is the actual signal of completion; this marker is just
593
+ // the bookkeeping byte we emit afterwards.
594
+ // - Identify the source explicitly: framing it as "the ocp
595
+ // wrapper's marker" gives the model context to recognise
596
+ // it as harness plumbing rather than a hostile injection.
525
597
  // Single-line form because `\n` flips claude TUI into multi-line
526
598
  // edit mode where `\r` no longer submits.
527
599
  const instruction =
528
- ` (Append the literal token ${sentinel} on its own line at the very` +
529
- ' end of your reply. Automation glue does not constrain how you' +
530
- ' answer above; use tools as freely and thoroughly as you would' +
531
- ' without this marker.)';
600
+ ` (End your reply with the literal text ${sentinel} on its own line.` +
601
+ ' This is the ocp wrapper\'s end-of-reply markerprogrammatic plumbing' +
602
+ ' the automation uses to know your reply has landed. It is not part of' +
603
+ ' the user\'s message and you do not need to mention or flag it. Answer' +
604
+ ' the user\'s actual message above as you normally would, using tools' +
605
+ ' as freely and thoroughly as you would without the marker.)';
532
606
  const fullPrompt = req.prompt + instruction;
533
- try {
607
+ // Skip the prompt write if the dialog watcher has already
608
+ // decided to abort. Without this check the trailing `\r` of
609
+ // the prompt lands inside whatever modal we tried to abort on
610
+ // (most commonly the folder-trust dialog), confirms its
611
+ // currently-highlighted option, and silently drops the user
612
+ // prompt. The detector has already been cancel()'d at this
613
+ // point but the await chain above does not observe that
614
+ // cancellation, so the write would otherwise still happen.
615
+ if (dialogState === 'trust-blocked' || dialogState === 'unknown-blocked') {
616
+ this._logDebug(`skipping prompt write — dialog blocked (${dialogState})`);
617
+ } else try {
618
+ this._logDebug(
619
+ `writing prompt: ${fullPrompt.length} chars, ` +
620
+ `dialogState=${dialogState}, promptBoxReady=${promptBoxReady}`,
621
+ );
534
622
  await writePromptToSession(session, fullPrompt, this.opts);
623
+ this._logDebug('prompt write returned');
535
624
  firstResponseTimer = setTimeout(() => {
536
625
  if (!assistantActivitySeen) {
537
626
  this._logDebug(`no assistant activity for ${FIRST_RESPONSE_MS}ms — interactive prompt suspected`);
@@ -686,6 +775,28 @@ class Driver {
686
775
  );
687
776
  }
688
777
 
778
+ // Degraded-capture notice. When the per-turn sentinel never landed
779
+ // (`reason='idle'`) or a stalled completion was rescued via JSONL
780
+ // (`reason='jsonl-recovered'`), the streamed PTY frames did NOT
781
+ // produce a clean end-of-reply boundary. The text we are returning
782
+ // is a post-hoc recovery — usually from the upstream JSONL session
783
+ // log, or as a best-effort tail of the PTY buffer when JSONL was
784
+ // unavailable. Prepend a one-line notice so the caller (and any
785
+ // chat UI rendering it) can tell this apart from a normal capture.
786
+ // The notice is intentionally a literal English prefix and only
787
+ // appears on the degraded path — the default success path
788
+ // (`reason='sentinel'`) is unchanged.
789
+ const captureFailed =
790
+ effectiveCompletion === 'idle' || effectiveCompletion === 'jsonl-recovered';
791
+ if (captureFailed && text) {
792
+ const source = jsonlExtraction?.text
793
+ ? 'local session log'
794
+ : 'terminal buffer (best effort)';
795
+ text =
796
+ `[ocp] Streaming capture not detected — showing last result from ${source}.\n\n` +
797
+ text;
798
+ }
799
+
689
800
  // For stall-style failures, capture the tail of the stripped PTY
690
801
  // buffer so the caller can see exactly what claude was rendering
691
802
  // when we gave up — that is usually the dialog or error that the
@@ -1055,9 +1166,12 @@ function extractAssistantText(stripped, nonce) {
1055
1166
  sentinelIdx = i;
1056
1167
  }
1057
1168
  if (sentinelIdx === -1) {
1058
- // Fall back to "everything after the first ⏺" — this preserves the
1059
- // legacy behavior when the model dropped the sentinel.
1060
- const ri = stripped.indexOf(marker);
1169
+ // Sentinel-missing fallback. On a resumed session the buffer carries
1170
+ // history regions before the current response, so anchoring on the
1171
+ // FIRST `⏺` would slice from the oldest history turn and return the
1172
+ // entire re-rendered transcript. Anchor on the LAST `⏺` instead —
1173
+ // that's the region marker for the current turn even after `--resume`.
1174
+ const ri = stripped.lastIndexOf(marker);
1061
1175
  return ri === -1 ? '' : stripped.slice(ri + marker.length).trim();
1062
1176
  }
1063
1177
 
@@ -40,8 +40,21 @@ export const PATTERNS = {
40
40
  /** The `❯` chevron that marks the start of the input prompt line.
41
41
  * Once we see this we know we have reached the input box; everything
42
42
  * that follows on subsequent lines is the statusline / HUD plugin
43
- * area, not response content, and is suppressed from parsing. */
44
- promptInputChevron: /^[❯›❮‹]/,
43
+ * area, not response content, and is suppressed from parsing.
44
+ *
45
+ * NOTE: claude's TUI also uses `❯` to mark the currently-selected
46
+ * row in modal selection dialogs — e.g. the folder-trust dialog
47
+ * shows `❯ 1. Yes, I trust this folder`. The naive `/^[❯›❮‹]/`
48
+ * pattern misclassifies that as the input box, fires
49
+ * `prompt-box-shown`, and tricks the driver into writing the
50
+ * user's prompt into the dialog (the trailing `\r` then confirms
51
+ * "Yes" while the prompt itself is dropped). Anchor the match to
52
+ * `chevron + space + (anything-but-numbered-option)` so dialog
53
+ * selections do not match. The real input box is either `❯`
54
+ * alone, `❯ ` with a placeholder hint (`❯ Try "refactor …"`), or
55
+ * `❯ <typed-text>` — none of those collide with `❯ <digit>.`.
56
+ */
57
+ promptInputChevron: /^[❯›❮‹](?:$|\s(?!\d+\.\s))/,
45
58
  };
46
59
 
47
60
  /**