open-claude-p 1.1.1 → 1.1.3
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 +99 -0
- package/package.json +1 -1
- package/src/completion/detector.js +20 -1
- package/src/index.js +71 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,105 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.1.3] — 2026-05-19
|
|
11
|
+
|
|
12
|
+
Fix the `/compact` 24-hour hang and tighten the slash-command path so
|
|
13
|
+
skill invocations are not collateral damage. Driven by a captured
|
|
14
|
+
session (`4af68584-…`) where a manually-triggered `/compact` ran for
|
|
15
|
+
57 s of compaction activity, returned to the input box with no
|
|
16
|
+
assistant turn, and left `runOneShot` waiting for a sentinel that — by
|
|
17
|
+
the upstream's design — could never arrive.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`/compact` and other local-builtin slash commands no longer block
|
|
22
|
+
for `maxResponseMs`.** Claude TUI splits `/`-prefixed prompts into
|
|
23
|
+
two classes: *local builtins* (`/compact`, `/clear`, `/help`,
|
|
24
|
+
`/exit`, `/quit`, `/login`, `/logout`, `/cost`, `/status`, `/model`,
|
|
25
|
+
`/permissions`, `/config`) which run a local handler and never open
|
|
26
|
+
an `⏺` region, and *LLM-bearing slash invocations* (skills like
|
|
27
|
+
`/init`, `/review`, `/security-review`, plus every user-installed
|
|
28
|
+
skill) which DO go through the model. The driver previously
|
|
29
|
+
appended the OCP_END marker instruction to both and required
|
|
30
|
+
`hadAssistantText` before the completion detector's idle fallback
|
|
31
|
+
could fire — fine for the LLM-bearing class, fatal for builtins,
|
|
32
|
+
which would wait for an assistant region that never opened until
|
|
33
|
+
the 24 h hard timeout.
|
|
34
|
+
|
|
35
|
+
Now `runOneShot` matches the prompt against a narrow whitelist of
|
|
36
|
+
local builtins. On a match it (a) skips the OCP_END instruction
|
|
37
|
+
append (the TUI's command parser drops it as junk args anyway, and
|
|
38
|
+
appending it can pollute free-form-arg commands like `/bug`) and
|
|
39
|
+
(b) sets the detector's new `allowIdleWithoutResponse` flag so the
|
|
40
|
+
pre-sentinel idle path is reachable without a prior region entry.
|
|
41
|
+
Skills and unknown `/<name>` prompts are NOT in the whitelist and
|
|
42
|
+
keep the existing instruction + strict-idle-gate behaviour, so
|
|
43
|
+
their LLM responses still complete cleanly via the sentinel.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **`CompletionDetector.allowIdleWithoutResponse` (default false).**
|
|
48
|
+
When true, `_onTick`'s pre-sentinel idle fallback fires after
|
|
49
|
+
`preIdleMs` of silence even without a prior `assistant-region-
|
|
50
|
+
entered`. This is the policy switch the driver uses for local
|
|
51
|
+
builtins; library callers that drive prompts known to produce no
|
|
52
|
+
assistant turn can opt in directly.
|
|
53
|
+
|
|
54
|
+
### Tests
|
|
55
|
+
|
|
56
|
+
- **End-to-end integration coverage for the slash-command path.**
|
|
57
|
+
`test/driver-slash-command.test.js` spawns the real driver against
|
|
58
|
+
`test/fixtures/fake-claude-tui.mjs` — a minimal node-pty fixture
|
|
59
|
+
that reproduces the captured `/compact` shape (spinner activity,
|
|
60
|
+
`Compacted` stdout, chevron return, no region, no sentinel) — and
|
|
61
|
+
asserts: `/compact` completes via `reason='idle'` in well under
|
|
62
|
+
`maxResponseMs`; a plain prompt still completes via `sentinel` with
|
|
63
|
+
no degraded-capture notice prefix; `/init` (a skill, not a builtin)
|
|
64
|
+
also completes via `sentinel`, proving the whitelist isn't too
|
|
65
|
+
greedy. Three new unit tests in `test/detector.test.js` cover the
|
|
66
|
+
bare `allowIdleWithoutResponse` flag.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## [1.1.2] — 2026-05-19
|
|
71
|
+
|
|
72
|
+
Recovery-path fixes for the sentinel-missing case. When the end-of-reply
|
|
73
|
+
marker fails to land — usually because claude omitted it or the PTY
|
|
74
|
+
frames were truncated — the driver's text extraction was returning
|
|
75
|
+
either an empty string or, worse, the entire re-rendered transcript of
|
|
76
|
+
a `--resume` session. Two changes correct that.
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- **`extractAssistantText` no longer slices from the FIRST `⏺`** when
|
|
81
|
+
the sentinel is missing. On a `--resume` run the buffer carries old
|
|
82
|
+
history regions before the current response; anchoring on the first
|
|
83
|
+
`⏺` returned the oldest history turn plus everything after it,
|
|
84
|
+
silently leaking prior turns into `result.text`. The fallback now
|
|
85
|
+
uses `lastIndexOf('⏺')` so the slice always covers the current
|
|
86
|
+
turn's response. Fresh (non-resumed) sessions are unaffected because
|
|
87
|
+
the buffer only has one region marker, so first == last.
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- **Degraded-capture notice prefix.** When `completionReason` is
|
|
92
|
+
`'idle'` (sentinel never seen, idle-fallback completed) or
|
|
93
|
+
`'jsonl-recovered'` (a stall was rescued by reading the upstream
|
|
94
|
+
JSONL session log), `result.text` is now prefixed with:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
[ocp] Streaming capture not detected — showing last result from {source}.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
where `{source}` is either `local session log` (JSONL had the
|
|
101
|
+
response) or `terminal buffer (best effort)` (JSONL was unavailable
|
|
102
|
+
and we fell back to the PTY slice above). Callers and chat UIs can
|
|
103
|
+
now distinguish a normal capture from a post-hoc recovery without
|
|
104
|
+
inspecting `completionReason`. The default success path
|
|
105
|
+
(`reason='sentinel'`) is unchanged — no prefix is added.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
10
109
|
## [1.1.1] — 2026-05-19
|
|
11
110
|
|
|
12
111
|
Post-1.1.0 fixes driven by Cosmica integration testing. Several of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-claude-p",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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",
|
|
@@ -37,6 +37,20 @@ export class CompletionDetector {
|
|
|
37
37
|
* turn; when the (N+1)th turn
|
|
38
38
|
* opens, the request is aborted
|
|
39
39
|
* with reason 'max-turns'.
|
|
40
|
+
* @param {boolean} [opts.allowIdleWithoutResponse=false]
|
|
41
|
+
* Permit `_onTick`'s idle fallback
|
|
42
|
+
* even when no assistant region has
|
|
43
|
+
* opened. Set this for prompts that
|
|
44
|
+
* legitimately produce no assistant
|
|
45
|
+
* turn — slash commands (`/compact`,
|
|
46
|
+
* `/clear`, `/help`, …) whose
|
|
47
|
+
* upstream handler runs a local
|
|
48
|
+
* operation and returns directly to
|
|
49
|
+
* the input box. Without this flag
|
|
50
|
+
* the request would block until
|
|
51
|
+
* `maxResponseMs` (24 h default)
|
|
52
|
+
* because `hadAssistantText` stays
|
|
53
|
+
* false for the entire turn.
|
|
40
54
|
*/
|
|
41
55
|
constructor({
|
|
42
56
|
nonce,
|
|
@@ -44,6 +58,7 @@ export class CompletionDetector {
|
|
|
44
58
|
preIdleMs = 8000,
|
|
45
59
|
maxResponseMs = 60000,
|
|
46
60
|
maxTurns,
|
|
61
|
+
allowIdleWithoutResponse = false,
|
|
47
62
|
} = {}) {
|
|
48
63
|
if (!nonce) throw new Error('CompletionDetector: nonce is required');
|
|
49
64
|
this.nonce = nonce;
|
|
@@ -51,6 +66,7 @@ export class CompletionDetector {
|
|
|
51
66
|
this.preIdleMs = preIdleMs;
|
|
52
67
|
this.maxResponseMs = maxResponseMs;
|
|
53
68
|
this.maxTurns = Number.isFinite(maxTurns) && maxTurns >= 0 ? maxTurns : null;
|
|
69
|
+
this.allowIdleWithoutResponse = !!allowIdleWithoutResponse;
|
|
54
70
|
|
|
55
71
|
this.startTime = Date.now();
|
|
56
72
|
this.regionEntered = false;
|
|
@@ -142,7 +158,10 @@ export class CompletionDetector {
|
|
|
142
158
|
// Pre-sentinel fallback: only valid once we've seen any assistant
|
|
143
159
|
// signal, with the longer `preIdleMs` threshold so that brief render
|
|
144
160
|
// pauses (common during `--resume`) do not trip premature completion.
|
|
145
|
-
|
|
161
|
+
// `allowIdleWithoutResponse` relaxes the gate for callers that know
|
|
162
|
+
// the turn legitimately produces no assistant region (e.g. slash
|
|
163
|
+
// commands like `/compact`).
|
|
164
|
+
if (this.hadAssistantText || this.allowIdleWithoutResponse) {
|
|
146
165
|
if (idleFor >= this.preIdleMs) {
|
|
147
166
|
this._complete('idle', false);
|
|
148
167
|
}
|
package/src/index.js
CHANGED
|
@@ -273,6 +273,39 @@ class Driver {
|
|
|
273
273
|
|
|
274
274
|
const spawnArgs = buildSpawnArgs(req);
|
|
275
275
|
|
|
276
|
+
// Local-builtin slash command detection. Claude TUI has two distinct
|
|
277
|
+
// `/`-prefixed prompt classes:
|
|
278
|
+
//
|
|
279
|
+
// - **Local builtins** (this list) — handled entirely by the TUI's
|
|
280
|
+
// command dispatcher. No LLM turn runs, no `⏺` region opens, no
|
|
281
|
+
// sentinel can ever arrive. `/compact` is the motivating case:
|
|
282
|
+
// 57 s of compaction activity, then chevron returns silently,
|
|
283
|
+
// then ocp would block until `maxResponseMs` (24 h default)
|
|
284
|
+
// waiting for a response that by design never comes.
|
|
285
|
+
//
|
|
286
|
+
// - **LLM-bearing slash invocations** — skills (`/init`, `/review`,
|
|
287
|
+
// `/security-review`, custom user skills like `/ui-ux-pro-max`)
|
|
288
|
+
// and any unknown `/<name>` we don't recognise. These DO run
|
|
289
|
+
// the LLM, DO open `⏺`, and emit the sentinel like a normal
|
|
290
|
+
// prompt. They MUST keep the OCP_END instruction and the
|
|
291
|
+
// strict idle gate — relaxing either would drop the marker
|
|
292
|
+
// from a real LLM turn and trigger a false-positive idle
|
|
293
|
+
// completion (returning a half-streamed response prefixed
|
|
294
|
+
// with the 1.1.2 "Streaming capture not detected" notice).
|
|
295
|
+
//
|
|
296
|
+
// The list is intentionally narrow — only claude TUI commands
|
|
297
|
+
// confirmed to bypass the LLM. Anything not in the set (including
|
|
298
|
+
// every user-installed skill) goes through the standard path.
|
|
299
|
+
const LOCAL_BUILTIN_SLASH_COMMANDS = new Set([
|
|
300
|
+
'compact', 'clear', 'help', 'exit', 'quit',
|
|
301
|
+
'login', 'logout', 'cost', 'status',
|
|
302
|
+
'model', 'permissions', 'config',
|
|
303
|
+
]);
|
|
304
|
+
const slashMatch = /^\s*\/([a-z][\w-]*)/i.exec(req.prompt ?? '');
|
|
305
|
+
const isLocalBuiltin =
|
|
306
|
+
!!slashMatch &&
|
|
307
|
+
LOCAL_BUILTIN_SLASH_COMMANDS.has(slashMatch[1].toLowerCase());
|
|
308
|
+
|
|
276
309
|
const sentinelParser = createSentinelParser(nonce);
|
|
277
310
|
const pipeline = createPipeline([
|
|
278
311
|
ansiStripParser,
|
|
@@ -285,6 +318,7 @@ class Driver {
|
|
|
285
318
|
preIdleMs: this.opts.preIdleMs,
|
|
286
319
|
maxResponseMs: this.opts.maxResponseMs,
|
|
287
320
|
maxTurns: req.maxTurns,
|
|
321
|
+
allowIdleWithoutResponse: isLocalBuiltin,
|
|
288
322
|
});
|
|
289
323
|
|
|
290
324
|
// Pool eligibility: explicit resume/continue bind to a specific past
|
|
@@ -603,7 +637,15 @@ class Driver {
|
|
|
603
637
|
' the user\'s message and you do not need to mention or flag it. Answer' +
|
|
604
638
|
' the user\'s actual message above as you normally would, using tools' +
|
|
605
639
|
' as freely and thoroughly as you would without the marker.)';
|
|
606
|
-
|
|
640
|
+
// Local builtins route through claude TUI's command dispatcher
|
|
641
|
+
// (not the LLM), so the marker instruction is silently dropped as
|
|
642
|
+
// junk args and never round-trips back. Some builtins also accept
|
|
643
|
+
// free-form text (e.g. `/bug <message>`), where appending the
|
|
644
|
+
// instruction would pollute the captured args. Skill invocations
|
|
645
|
+
// and unknown `/<name>` prompts DO go through the LLM, so they
|
|
646
|
+
// still need the marker — only the narrow `LOCAL_BUILTIN_…` set
|
|
647
|
+
// gets the bare prompt.
|
|
648
|
+
const fullPrompt = isLocalBuiltin ? req.prompt : (req.prompt + instruction);
|
|
607
649
|
// Skip the prompt write if the dialog watcher has already
|
|
608
650
|
// decided to abort. Without this check the trailing `\r` of
|
|
609
651
|
// the prompt lands inside whatever modal we tried to abort on
|
|
@@ -775,6 +817,28 @@ class Driver {
|
|
|
775
817
|
);
|
|
776
818
|
}
|
|
777
819
|
|
|
820
|
+
// Degraded-capture notice. When the per-turn sentinel never landed
|
|
821
|
+
// (`reason='idle'`) or a stalled completion was rescued via JSONL
|
|
822
|
+
// (`reason='jsonl-recovered'`), the streamed PTY frames did NOT
|
|
823
|
+
// produce a clean end-of-reply boundary. The text we are returning
|
|
824
|
+
// is a post-hoc recovery — usually from the upstream JSONL session
|
|
825
|
+
// log, or as a best-effort tail of the PTY buffer when JSONL was
|
|
826
|
+
// unavailable. Prepend a one-line notice so the caller (and any
|
|
827
|
+
// chat UI rendering it) can tell this apart from a normal capture.
|
|
828
|
+
// The notice is intentionally a literal English prefix and only
|
|
829
|
+
// appears on the degraded path — the default success path
|
|
830
|
+
// (`reason='sentinel'`) is unchanged.
|
|
831
|
+
const captureFailed =
|
|
832
|
+
effectiveCompletion === 'idle' || effectiveCompletion === 'jsonl-recovered';
|
|
833
|
+
if (captureFailed && text) {
|
|
834
|
+
const source = jsonlExtraction?.text
|
|
835
|
+
? 'local session log'
|
|
836
|
+
: 'terminal buffer (best effort)';
|
|
837
|
+
text =
|
|
838
|
+
`[ocp] Streaming capture not detected — showing last result from ${source}.\n\n` +
|
|
839
|
+
text;
|
|
840
|
+
}
|
|
841
|
+
|
|
778
842
|
// For stall-style failures, capture the tail of the stripped PTY
|
|
779
843
|
// buffer so the caller can see exactly what claude was rendering
|
|
780
844
|
// when we gave up — that is usually the dialog or error that the
|
|
@@ -1144,9 +1208,12 @@ function extractAssistantText(stripped, nonce) {
|
|
|
1144
1208
|
sentinelIdx = i;
|
|
1145
1209
|
}
|
|
1146
1210
|
if (sentinelIdx === -1) {
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
|
|
1211
|
+
// Sentinel-missing fallback. On a resumed session the buffer carries
|
|
1212
|
+
// history regions before the current response, so anchoring on the
|
|
1213
|
+
// FIRST `⏺` would slice from the oldest history turn and return the
|
|
1214
|
+
// entire re-rendered transcript. Anchor on the LAST `⏺` instead —
|
|
1215
|
+
// that's the region marker for the current turn even after `--resume`.
|
|
1216
|
+
const ri = stripped.lastIndexOf(marker);
|
|
1150
1217
|
return ri === -1 ? '' : stripped.slice(ri + marker.length).trim();
|
|
1151
1218
|
}
|
|
1152
1219
|
|