@kelceyp/caw-server 1.0.288 → 1.0.290
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/dist/claude-config-dir-guard.mjs +105 -0
- package/dist/cli.js +1 -1
- package/dist/main.js +534 -512
- package/dist/pty-emit-result.mjs +140 -34
- package/dist/pty-non-interactive-setup.mjs +30 -18
- package/dist/pty-wrapper.mjs +29 -9
- package/dist/public_html/main.js +649 -649
- package/dist/public_html/styles.css +1 -1
- package/package.json +4 -3
package/dist/pty-emit-result.mjs
CHANGED
|
@@ -14,30 +14,136 @@
|
|
|
14
14
|
* `hookData`; a null / falsy `hookData` is a no-op, mirroring the wrapper's
|
|
15
15
|
* pre-extraction `if (!existsSync(sentinelPath)) return;` guard one level up.
|
|
16
16
|
*
|
|
17
|
-
* Wire
|
|
18
|
-
* - success line
|
|
19
|
-
* `
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
17
|
+
* Wire. This function writes at most one line, of one of two shapes:
|
|
18
|
+
* - success line (Stop):
|
|
19
|
+
* `{ type: 'result', result, metadata, stopFallback }`.
|
|
20
|
+
* - error line (StopFailure, `hookData.error` truthy):
|
|
21
|
+
* `{ type: 'error', error, message[, error_details], metadata, stopFallback }`
|
|
22
|
+
* (As1071 design S4). `error_details` is the StopFailure payload's own
|
|
23
|
+
* `error_details`, copied verbatim (any JSON type, `null` included) and
|
|
24
|
+
* only when the payload has the key; it is diagnostic evidence, never a
|
|
25
|
+
* reset-time source. The success line never carries it.
|
|
26
|
+
* Both lines get `metadata` from the one builder, `buildMetadata()`, so the two
|
|
27
|
+
* cannot drift: `{ sessionId, transcriptPath, permissionMode[, durationMs][, teardownReason] }`
|
|
28
|
+
* (`durationMs` when both timestamps are given, `teardownReason` when the
|
|
29
|
+
* wrapper passes one, omitted rather than `null` otherwise; fallback chains
|
|
30
|
+
* `hookData.session_id || effectiveSessionId`,
|
|
31
|
+
* `hookData.transcript_path || transcriptPath || null`,
|
|
32
|
+
* `hookData.permission_mode || null`). On a real StopFailure turn the wrapper
|
|
33
|
+
* passes `teardownReason: 'stop-failure'` and both timestamps, so the error
|
|
34
|
+
* line meets `teardownReasons.mjs` contract point 2 too.
|
|
35
|
+
* `stopFallback` (As1072) is appended as the LAST top-level key of both lines
|
|
36
|
+
* and is not a `metadata` key. The success line is the pre-As1072 line with
|
|
37
|
+
* that one key added before its closing brace. The no-op paths
|
|
38
|
+
* (`alreadyEmitted`, a falsy `hookData`, and the `catch` below) write no line
|
|
39
|
+
* at all.
|
|
25
40
|
*
|
|
26
41
|
* The double-emit guard (previously `if (resultEmitted || failureEmitted) return;`
|
|
27
42
|
* at `pty-wrapper.mjs:401`) is preserved via the `alreadyEmitted` input: the
|
|
28
43
|
* caller passes the current flag state, and the function no-ops when true.
|
|
29
44
|
* The return value `{ emitted: 'result' | 'error' | null }` lets the caller
|
|
30
45
|
* translate a successful call back into its module-level `resultEmitted` /
|
|
31
|
-
* `failureEmitted` flags.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
46
|
+
* `failureEmitted` flags. This is not the wrapper's only writer of
|
|
47
|
+
* `{ type: 'error', ... }` lines: As1080's `prompt-rejection-detector.mjs`
|
|
48
|
+
* writes its own `prompt_too_long` line, which does not pass through here. A
|
|
49
|
+
* vendor StopFailure reaches this function only when the Stop-hook sentinel
|
|
35
50
|
* carries a structured `error` field.
|
|
36
51
|
*/
|
|
37
52
|
|
|
53
|
+
// --- stopFallback vocabulary (As1072) ---
|
|
54
|
+
//
|
|
55
|
+
// What happened to the Stop-hook `last_assistant_message` fallback on a turn.
|
|
56
|
+
// The wrapper writes the value on its own terminal line, so the component that
|
|
57
|
+
// holds `hookData` is the one that decides it:
|
|
58
|
+
// - `present` / `empty` / `absent` / `invalid` — written by `emitResult()` on
|
|
59
|
+
// both lines, from `deriveStopFallback()`;
|
|
60
|
+
// - `suppressed` — written by `emitResult()` on the success line only, when
|
|
61
|
+
// the CAW_PTY_SUPPRESS_STOP_FALLBACK test override is set;
|
|
62
|
+
// - `not-emitted` — NEVER written by the wrapper. No terminal line exists to
|
|
63
|
+
// carry it, so only `claude-session.js` derives it, from the absence of a
|
|
64
|
+
// `result` / `error` line.
|
|
65
|
+
// `unknown` is deliberately in NEITHER export below. It is the record layer's
|
|
66
|
+
// sentinel (`sentinels.js`) with one writer, `JobService.updateJobMetadata`,
|
|
67
|
+
// which uses it when no value reached the record. Adding it here would create
|
|
68
|
+
// a second writer.
|
|
69
|
+
// The vocabulary lives in this module, the only wrapper-side writer, so the
|
|
70
|
+
// wrapper's import closure (and with it `build:all` and `Dockerfile.session`)
|
|
71
|
+
// does not grow.
|
|
72
|
+
|
|
73
|
+
export const STOP_FALLBACK = Object.freeze({
|
|
74
|
+
PRESENT: 'present',
|
|
75
|
+
EMPTY: 'empty',
|
|
76
|
+
ABSENT: 'absent',
|
|
77
|
+
INVALID: 'invalid',
|
|
78
|
+
SUPPRESSED: 'suppressed',
|
|
79
|
+
NOT_EMITTED: 'not-emitted', // derived by claude-session.js; never written by the wrapper
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Exactly the values a wrapper line may carry; claude-session.js accepts only these.
|
|
83
|
+
// A frozen array, not a Set: Object.freeze does not stop Set.prototype.add/delete.
|
|
84
|
+
export const STOP_FALLBACK_WIRE_VALUES = Object.freeze([
|
|
85
|
+
STOP_FALLBACK.PRESENT,
|
|
86
|
+
STOP_FALLBACK.EMPTY,
|
|
87
|
+
STOP_FALLBACK.ABSENT,
|
|
88
|
+
STOP_FALLBACK.INVALID,
|
|
89
|
+
STOP_FALLBACK.SUPPRESSED,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
export const isStopFallbackWireValue = (value) =>
|
|
93
|
+
typeof value === 'string' && STOP_FALLBACK_WIRE_VALUES.includes(value);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Classify the Stop / StopFailure payload's `last_assistant_message` by own-key
|
|
97
|
+
* presence, not truthiness. First match wins:
|
|
98
|
+
* 1. `suppressFallback` set → `suppressed`
|
|
99
|
+
* 2. not an own key, or own with value `undefined` → `absent`
|
|
100
|
+
* 3. not a string (`null` included) → `invalid`
|
|
101
|
+
* 4. `''` → `empty`
|
|
102
|
+
* 5. any other string (whitespace-only included) → `present`
|
|
103
|
+
*
|
|
104
|
+
* An own `undefined` is `absent` (As1072 KD3): `hookData` is parsed JSON, which
|
|
105
|
+
* cannot carry one, and the vendor serialises an `undefined` value as a missing
|
|
106
|
+
* key. Total: an omitted options argument means no override; a `hookData` that
|
|
107
|
+
* cannot hold the own key (`undefined`, `null`, a primitive, an array) is
|
|
108
|
+
* `absent`; it never throws and only returns wire values. `emitResult()` passes
|
|
109
|
+
* `suppressFallback` on the success branch only.
|
|
110
|
+
*/
|
|
111
|
+
export const deriveStopFallback = (hookData, options) => {
|
|
112
|
+
if (options?.suppressFallback) return STOP_FALLBACK.SUPPRESSED;
|
|
113
|
+
if (hookData === null || hookData === undefined) return STOP_FALLBACK.ABSENT;
|
|
114
|
+
if (!Object.hasOwn(hookData, 'last_assistant_message')) return STOP_FALLBACK.ABSENT;
|
|
115
|
+
const value = hookData.last_assistant_message;
|
|
116
|
+
if (value === undefined) return STOP_FALLBACK.ABSENT;
|
|
117
|
+
if (typeof value !== 'string') return STOP_FALLBACK.INVALID;
|
|
118
|
+
if (value === '') return STOP_FALLBACK.EMPTY;
|
|
119
|
+
return STOP_FALLBACK.PRESENT;
|
|
120
|
+
};
|
|
121
|
+
|
|
38
122
|
const defaultWrite = (line) => process.stdout.write(line);
|
|
39
123
|
const defaultLog = () => {};
|
|
40
124
|
|
|
125
|
+
// The one `metadata` builder both lines use (As1071 S4). Field order is part
|
|
126
|
+
// of the success line's wire, which must stay byte-identical.
|
|
127
|
+
const buildMetadata = ({ hookData, transcriptPath, effectiveSessionId, startedAt, endedAt, teardownReason }) => {
|
|
128
|
+
const metadata = {
|
|
129
|
+
sessionId: hookData.session_id || effectiveSessionId,
|
|
130
|
+
transcriptPath: hookData.transcript_path || transcriptPath || null,
|
|
131
|
+
permissionMode: hookData.permission_mode || null
|
|
132
|
+
};
|
|
133
|
+
if (startedAt !== null && endedAt !== null) {
|
|
134
|
+
metadata.durationMs = endedAt - startedAt;
|
|
135
|
+
}
|
|
136
|
+
// As1033 Task 6: append teardownReason ('stop' | 'watchdog' |
|
|
137
|
+
// 'stop-failure' | ...) so the downstream stream handler and the
|
|
138
|
+
// JobRunner classifier gate can distinguish a clean Stop finish from a
|
|
139
|
+
// watchdog force-kill. Omitted, never `null`, when the wrapper passes
|
|
140
|
+
// none (`teardownReasons.mjs` contract point 3).
|
|
141
|
+
if (teardownReason !== null) {
|
|
142
|
+
metadata.teardownReason = teardownReason;
|
|
143
|
+
}
|
|
144
|
+
return metadata;
|
|
145
|
+
};
|
|
146
|
+
|
|
41
147
|
export function emitResult({
|
|
42
148
|
hookData,
|
|
43
149
|
transcriptPath = null,
|
|
@@ -54,41 +160,41 @@ export function emitResult({
|
|
|
54
160
|
if (!hookData) return { emitted: null };
|
|
55
161
|
|
|
56
162
|
try {
|
|
163
|
+
const metadataInputs = { hookData, transcriptPath, effectiveSessionId, startedAt, endedAt, teardownReason };
|
|
164
|
+
|
|
57
165
|
if (hookData.error) {
|
|
58
|
-
|
|
166
|
+
const line = {
|
|
59
167
|
type: 'error',
|
|
60
168
|
error: hookData.error,
|
|
61
169
|
message: hookData.last_assistant_message || ''
|
|
62
|
-
}
|
|
170
|
+
};
|
|
171
|
+
// Present-only and verbatim (As1071 S4): own-key presence, not
|
|
172
|
+
// truthiness, so `null`, `false`, `0` and `''` are copied as-is.
|
|
173
|
+
if (Object.hasOwn(hookData, 'error_details')) {
|
|
174
|
+
line.error_details = hookData.error_details;
|
|
175
|
+
}
|
|
176
|
+
line.metadata = buildMetadata(metadataInputs);
|
|
177
|
+
// No override on this branch (KD7): the line records the payload's
|
|
178
|
+
// real state, never `suppressed`.
|
|
179
|
+
line.stopFallback = deriveStopFallback(hookData);
|
|
180
|
+
write(JSON.stringify(line) + '\n');
|
|
63
181
|
return { emitted: 'error' };
|
|
64
182
|
}
|
|
65
183
|
|
|
66
|
-
const metadata =
|
|
67
|
-
sessionId: hookData.session_id || effectiveSessionId,
|
|
68
|
-
transcriptPath: hookData.transcript_path || transcriptPath || null,
|
|
69
|
-
permissionMode: hookData.permission_mode || null
|
|
70
|
-
};
|
|
71
|
-
if (startedAt !== null && endedAt !== null) {
|
|
72
|
-
metadata.durationMs = endedAt - startedAt;
|
|
73
|
-
}
|
|
74
|
-
// As1033 Task 6: append teardownReason ('stop' | 'watchdog') so the
|
|
75
|
-
// downstream stream handler (Task 5 landed the capture) and the
|
|
76
|
-
// JobRunner classifier gate (Task 7) can distinguish a clean Stop
|
|
77
|
-
// finish from a watchdog force-kill. Field order of the four
|
|
78
|
-
// pre-existing fields is preserved byte-for-byte.
|
|
79
|
-
if (teardownReason !== null) {
|
|
80
|
-
metadata.teardownReason = teardownReason;
|
|
81
|
-
}
|
|
184
|
+
const metadata = buildMetadata(metadataInputs);
|
|
82
185
|
// D11 test-only override: when the wrapper's CAW_PTY_SUPPRESS_STOP_FALLBACK
|
|
83
186
|
// gate is set, blank the `result` field on the success branch only. The
|
|
84
|
-
//
|
|
85
|
-
// the
|
|
86
|
-
//
|
|
187
|
+
// line still declares itself (As1072): `stopFallback: 'suppressed'`,
|
|
188
|
+
// whatever the payload held, so the record can tell the override from a
|
|
189
|
+
// real payload. `result` is still `''`, so the As1005 D11 transcript-tail
|
|
190
|
+
// + override combination still reaches the loud empty-turn failure
|
|
191
|
+
// downstream. `metadata` is untouched.
|
|
87
192
|
const result = suppressFallback ? '' : (hookData.last_assistant_message || '');
|
|
88
193
|
write(JSON.stringify({
|
|
89
194
|
type: 'result',
|
|
90
195
|
result,
|
|
91
|
-
metadata
|
|
196
|
+
metadata,
|
|
197
|
+
stopFallback: deriveStopFallback(hookData, { suppressFallback })
|
|
92
198
|
}) + '\n');
|
|
93
199
|
return { emitted: 'result' };
|
|
94
200
|
} catch (err) {
|
|
@@ -7,14 +7,22 @@
|
|
|
7
7
|
* `--skip-non-interactive-setup`.
|
|
8
8
|
*
|
|
9
9
|
* Fail-fast / atomic / idempotent write contract (As1016):
|
|
10
|
+
* - `CLAUDE_CONFIG_DIR` refused → before any filesystem access, throw the
|
|
11
|
+
* As1065 guard's own error (`assertClaudeConfigDirForwardable` in
|
|
12
|
+
* `claude-config-dir-guard.mjs`), whose message starts with
|
|
13
|
+
* `CLAUDE_CONFIG_DIR_REFUSED (<RULE>): ...`. This module does not own
|
|
14
|
+
* the config-dir check; it delegates to the guard, the same predicate
|
|
15
|
+
* the claude strategies' direct-mode `validatePrerequisites` apply.
|
|
10
16
|
* - Absent → seed with data = {} and mode 0o644, then atomic write.
|
|
11
17
|
* - Present but stat/read/parse fails (with one-shot SyntaxError retry) →
|
|
12
18
|
* throw a typed error whose message starts with
|
|
13
|
-
* `CLAUDE_JSON_UNREADABLE (<code>): <path>. ...`. No write.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
19
|
+
* `CLAUDE_JSON_UNREADABLE (<code>): <path>. ...`. No write.
|
|
20
|
+
* - The wrapper's discriminating catch (the `ensureNonInteractiveSetup`
|
|
21
|
+
* call site in `pty-wrapper.mjs`) classifies both prefixes,
|
|
22
|
+
* `CLAUDE_JSON_UNREADABLE` and `CLAUDE_CONFIG_DIR_REFUSED`: it re-emits
|
|
23
|
+
* `err.message + '\n'` to stderr and calls `process.exit(1)` —
|
|
24
|
+
* byte-for-byte identical to today's inline behaviour. Every other
|
|
25
|
+
* error is rethrown by the wrapper.
|
|
18
26
|
* - Present and read succeeded → apply the guarded mutations; short-circuit
|
|
19
27
|
* via the idempotent guard when both onboarding and every trust-set entry
|
|
20
28
|
* are already recorded.
|
|
@@ -36,7 +44,9 @@
|
|
|
36
44
|
* from `server/dist/`, and the wrapper carries its own inline copy for the
|
|
37
45
|
* same reason. Three co-located literals (`shared/jobErrorPrefixes.js`,
|
|
38
46
|
* `pty-wrapper.mjs`, this module) must stay in sync per As1016 D8 (extended
|
|
39
|
-
* from dual- to triple-literal by this commit).
|
|
47
|
+
* from dual- to triple-literal by this commit). The
|
|
48
|
+
* `CLAUDE_CONFIG_DIR_REFUSED` prefix is not carried here: it lives in the
|
|
49
|
+
* guard (literal copy 2 of 2; copy 1 is in `shared/jobErrorPrefixes.js`).
|
|
40
50
|
*/
|
|
41
51
|
|
|
42
52
|
import {
|
|
@@ -53,6 +63,7 @@ import {
|
|
|
53
63
|
readdirSync,
|
|
54
64
|
} from 'fs';
|
|
55
65
|
import { join, dirname, basename, resolve, isAbsolute } from 'path';
|
|
66
|
+
import { assertClaudeConfigDirForwardable } from './claude-config-dir-guard.mjs';
|
|
56
67
|
|
|
57
68
|
export const CLAUDE_JSON_UNREADABLE_PREFIX = 'CLAUDE_JSON_UNREADABLE';
|
|
58
69
|
|
|
@@ -83,18 +94,19 @@ export const ensureNonInteractiveSetup = (cwd, { home, configDir, log = noopLog
|
|
|
83
94
|
// not consult, silently defeating both the setup and the mutex that
|
|
84
95
|
// serialises writes to it. Route the seed through `$CLAUDE_CONFIG_DIR`
|
|
85
96
|
// when it is present, else fall back to `$HOME` (Task 3 pre-AC-19
|
|
86
|
-
// behaviour).
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
// behaviour).
|
|
98
|
+
//
|
|
99
|
+
// The value check belongs to the As1065 guard
|
|
100
|
+
// (`assertClaudeConfigDirForwardable`), the same predicate the claude
|
|
101
|
+
// strategies' direct-mode `validatePrerequisites` apply. A refused value
|
|
102
|
+
// throws the guard's own `CLAUDE_CONFIG_DIR_REFUSED (<RULE>): ...` error,
|
|
103
|
+
// unwrapped, before anything below touches the filesystem; the wrapper's
|
|
104
|
+
// discriminating catch classifies that prefix alongside
|
|
105
|
+
// `CLAUDE_JSON_UNREADABLE`. `configDir` is `string | undefined`: a
|
|
106
|
+
// non-string, including `null`, throws the guard's `TypeError` (As1065
|
|
107
|
+
// KD5).
|
|
108
|
+
assertClaudeConfigDirForwardable(configDir);
|
|
109
|
+
const baseDir = configDir === undefined ? home : configDir;
|
|
98
110
|
|
|
99
111
|
const nominal = join(baseDir, '.claude.json');
|
|
100
112
|
|
package/dist/pty-wrapper.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { readSentinelPayload, hasPendingWork } from './pty-deferred-teardown.mjs
|
|
|
27
27
|
import { emitResult as emitResultLine } from './pty-emit-result.mjs';
|
|
28
28
|
import { TEARDOWN_REASONS } from './teardownReasons.mjs';
|
|
29
29
|
import { ensureNonInteractiveSetup } from './pty-non-interactive-setup.mjs';
|
|
30
|
+
import { CLAUDE_CONFIG_DIR_REFUSED_PREFIX } from './claude-config-dir-guard.mjs';
|
|
30
31
|
import { createTranscriptTailer } from './transcript-tailer.mjs';
|
|
31
32
|
import { createPromptRejectionDetector } from './prompt-rejection-detector.mjs';
|
|
32
33
|
|
|
@@ -91,8 +92,10 @@ try {
|
|
|
91
92
|
// (`claude-session.js#getDockerConfig`'s `commandParts` omits it by
|
|
92
93
|
// design), so the in-container wrapper still runs the setup itself. This
|
|
93
94
|
// wrapper's local call site preserves the same discriminating catch that
|
|
94
|
-
// re-emits classified
|
|
95
|
-
//
|
|
95
|
+
// re-emits classified errors byte-for-byte with today's stderr + exit
|
|
96
|
+
// contract and rethrows every other error. Two prefixes are classified:
|
|
97
|
+
// `CLAUDE_JSON_UNREADABLE` (As1016) and `CLAUDE_CONFIG_DIR_REFUSED` (As1065,
|
|
98
|
+
// the guard's refusal of the container's own `CLAUDE_CONFIG_DIR`). See As1016
|
|
96
99
|
// for the fail-fast / atomic / idempotent invariants that module preserves.
|
|
97
100
|
//
|
|
98
101
|
// The invocation stays here (post log()/stdin-drain/claude-binary,
|
|
@@ -117,6 +120,12 @@ try {
|
|
|
117
120
|
// `server/dist/`, and the wrapper keeps this local const so the
|
|
118
121
|
// discriminating catch below can classify without importing the sibling's
|
|
119
122
|
// export. Keep all three literals in sync per As1016 D8.
|
|
123
|
+
//
|
|
124
|
+
// The second classified prefix, `CLAUDE_CONFIG_DIR_REFUSED_PREFIX`, is NOT
|
|
125
|
+
// carried as a literal here: it is imported above from
|
|
126
|
+
// `./claude-config-dir-guard.mjs`, which is already in this wrapper's
|
|
127
|
+
// closure via `pty-non-interactive-setup.mjs`. It has exactly two literals
|
|
128
|
+
// (`shared/jobErrorPrefixes.js` and the guard; As1065 KD4).
|
|
120
129
|
const CLAUDE_JSON_UNREADABLE_PREFIX = 'CLAUDE_JSON_UNREADABLE';
|
|
121
130
|
|
|
122
131
|
// As1050 Commit B (C3) — the `--skip-non-interactive-setup` CLI arg is
|
|
@@ -138,14 +147,20 @@ if (!hasFlag('--skip-non-interactive-setup')) {
|
|
|
138
147
|
log
|
|
139
148
|
});
|
|
140
149
|
} catch (err) {
|
|
141
|
-
// Discriminating catch: only classified
|
|
142
|
-
//
|
|
150
|
+
// Discriminating catch: only throws with one of the two classified
|
|
151
|
+
// prefixes are re-emitted to stderr as a fail-fast (matching today's
|
|
143
152
|
// process.stderr.write + process.exit(1) contract byte-for-byte; the
|
|
144
153
|
// extracted module's `.message` omits the trailing `\n` that the wrapper
|
|
145
|
-
// concatenates here)
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
154
|
+
// concatenates here):
|
|
155
|
+
// - `CLAUDE_JSON_UNREADABLE` — the As1016 file-level and `HOME_UNSET`
|
|
156
|
+
// fail-fast branches;
|
|
157
|
+
// - `CLAUDE_CONFIG_DIR_REFUSED` — the As1065 guard refusing the
|
|
158
|
+
// container's own `CLAUDE_CONFIG_DIR` (`ensureNonInteractiveSetup`
|
|
159
|
+
// delegates its config-dir check to `assertClaudeConfigDirForwardable`).
|
|
160
|
+
// All other errors are RETHROWN — the wrapper does NOT swallow them, so
|
|
161
|
+
// today's uncaught-exception behaviour on unclassified failures is
|
|
162
|
+
// preserved.
|
|
163
|
+
if (err && typeof err.message === 'string' && (err.message.startsWith(CLAUDE_JSON_UNREADABLE_PREFIX) || err.message.startsWith(CLAUDE_CONFIG_DIR_REFUSED_PREFIX))) {
|
|
149
164
|
process.stderr.write(err.message + '\n');
|
|
150
165
|
process.exit(1);
|
|
151
166
|
}
|
|
@@ -204,7 +219,12 @@ const WATCHDOG_TIMEOUT_MS = Number.parseInt(process.env.CAW_PTY_WATCHDOG_TIMEOUT
|
|
|
204
219
|
// fallback consumes `result` instead of the missing transcript stream.
|
|
205
220
|
// CAW_PTY_SUPPRESS_STOP_FALLBACK: blank the emitted `result` field on the
|
|
206
221
|
// success branch of pty-emit-result (only when hookData is non-null and
|
|
207
|
-
// the success branch runs).
|
|
222
|
+
// the success branch runs). The line declares the override as
|
|
223
|
+
// `stopFallback: 'suppressed'` (As1072), so it is not mistaken for a real
|
|
224
|
+
// empty-message turn; the error branch is never affected. Used to prove the
|
|
225
|
+
// AC-5 gate downstream: `result` is still `''`, so with
|
|
226
|
+
// CAW_PTY_DISABLE_TRANSCRIPT_TAIL the turn still reaches the loud empty-turn
|
|
227
|
+
// failure.
|
|
208
228
|
const DISABLE_TRANSCRIPT_TAIL = process.env.CAW_PTY_DISABLE_TRANSCRIPT_TAIL === '1';
|
|
209
229
|
const SUPPRESS_STOP_FALLBACK = process.env.CAW_PTY_SUPPRESS_STOP_FALLBACK === '1';
|
|
210
230
|
|