@kelceyp/caw-server 1.0.290 → 1.1.0
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 +2 -3
- package/dist/main.js +975 -729
- package/dist/public_html/main.js +317 -317
- package/package.json +3 -5
- package/dist/prompt-rejection-detector.mjs +0 -158
- package/dist/pty-deferred-teardown.mjs +0 -17
- package/dist/pty-emit-result.mjs +0 -204
- package/dist/pty-non-interactive-setup.mjs +0 -319
- package/dist/pty-wrapper.mjs +0 -875
- package/dist/teardownReasons.mjs +0 -100
- package/dist/transcript-tailer.mjs +0 -206
package/dist/teardownReasons.mjs
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* teardownReasons.mjs — closed enum for the pty-wrapper's `teardown()` reason,
|
|
3
|
-
* shared between the observability path (As1012) and the classification path
|
|
4
|
-
* (As1033).
|
|
5
|
-
*
|
|
6
|
-
* Authored under As1012 as part of the S2 workshop's Q4 outcome — imported by
|
|
7
|
-
* `pty-wrapper.mjs` (emit site) and by `JobRunner.js`'s watchdog gate + the
|
|
8
|
-
* stream-handler metadata plumbing on the As1033 side.
|
|
9
|
-
*
|
|
10
|
-
* Non-forced-path contract (joint As1012 × As1033)
|
|
11
|
-
* ------------------------------------------------
|
|
12
|
-
*
|
|
13
|
-
* 1. `teardown()` runs on every graceful exit path. Four call sites; four
|
|
14
|
-
* enum values: `WATCHDOG` (expiry), `SENTINEL_UNREADABLE`, `STOP_FAILURE`,
|
|
15
|
-
* `STOP` (normal completion via the Stop hook). Normal completion is NOT a
|
|
16
|
-
* no-teardown case — it is `teardown('stop')`. **`STOP` is a real value.**
|
|
17
|
-
*
|
|
18
|
-
* 2. `emitResult` always writes `metadata.teardownReason` when it runs, with
|
|
19
|
-
* a value from this enum. The field is NOT optional on the graceful path —
|
|
20
|
-
* consumers read it directly without an `undefined` check.
|
|
21
|
-
*
|
|
22
|
-
* 3. **`metadata.teardownReason` is omitted (not `null`) whenever the pty
|
|
23
|
-
* child exits without `teardown()` firing.** Absence is the signal — the
|
|
24
|
-
* enum does not carry a *no-teardown* value. Four non-teardown exit
|
|
25
|
-
* paths, listed exhaustively so a future debugger does not misread the
|
|
26
|
-
* gap:
|
|
27
|
-
*
|
|
28
|
-
* a. **Child self-exit** — `claude` returns of its own accord before
|
|
29
|
-
* the Stop hook fires (rare; e.g. an unhandled internal error).
|
|
30
|
-
* `onExit` runs with a non-zero code; `emitResult` runs; the field
|
|
31
|
-
* is omitted because `teardown()` never called `emitResult` with a
|
|
32
|
-
* reason. Classifier reads a distinct exit class from `exitCode`.
|
|
33
|
-
* b. **External signal to the wrapper** (SIGTERM/SIGINT delivered
|
|
34
|
-
* externally, not from the wrapper's own deadline path). Wrapper
|
|
35
|
-
* may or may not manage to emit anything depending on the signal
|
|
36
|
-
* and its handler set at the moment.
|
|
37
|
-
* c. **Prompt-too-long emission** — the wrapper's stdout emits its
|
|
38
|
-
* result branch directly without routing through `teardown()`
|
|
39
|
-
* (`pty-wrapper.mjs:733` and its neighbours; see
|
|
40
|
-
* Easy-to-Miss #5 in As1012's design). Field omitted.
|
|
41
|
-
* d. **As1006's F2 SIGKILL** — the wrapper-self-terminate path with a
|
|
42
|
-
* process-group SIGKILL of the PTY child under the wrapper's own
|
|
43
|
-
* deadline. SIGKILL is unmaskable; the wrapper is dead before it
|
|
44
|
-
* can emit; the entire `type:"result"` JSONL line is absent.
|
|
45
|
-
*
|
|
46
|
-
* **`As1006's F2 SIGKILL path is an OUT-OF-ENUM EXIT PATH`, as are the
|
|
47
|
-
* other three above.** Do not read this enum as enumerating every wrapper
|
|
48
|
-
* exit. An F2-killed job leaves NO wrapper-side breadcrumb via this
|
|
49
|
-
* enum — that gap is by design, not by omission. As1033's dissent from
|
|
50
|
-
* workshop round 3 is honoured by this note.
|
|
51
|
-
*
|
|
52
|
-
* The classifier's contract for any of the four non-teardown cases:
|
|
53
|
-
* `turnSignals.teardownReason === undefined` is a **distinct exit
|
|
54
|
-
* class** — spawn failure / SIGKILL / crash / prompt-too-long / external
|
|
55
|
-
* signal — not a teardown. Classification gate ordering (As1033):
|
|
56
|
-
* `WATCHDOG` gate first (positive match on `reason === WATCHDOG`); other
|
|
57
|
-
* reasons fall through unchanged; missing reason routes to the
|
|
58
|
-
* spawn-failure / no-result branch.
|
|
59
|
-
*
|
|
60
|
-
* 4. Lifecycle events (`armed`, `reset`, `expired`, `teardown`) share the
|
|
61
|
-
* enum only via the `teardown` event's `reason` field. `armed` / `reset`
|
|
62
|
-
* / `expired` observability events carry `type`, not `reason` — no enum
|
|
63
|
-
* value on those.
|
|
64
|
-
*
|
|
65
|
-
* 5. **The enum is closed at story-close.** Adding a value requires a
|
|
66
|
-
* coordinated change to both this module AND the classification gate in
|
|
67
|
-
* `JobRunner.js`. This is a maintenance rule, not a note — treat the set
|
|
68
|
-
* below as fixed.
|
|
69
|
-
*
|
|
70
|
-
* Consumers
|
|
71
|
-
* ---------
|
|
72
|
-
*
|
|
73
|
-
* - `pty-wrapper.mjs` — emit sites: `teardown(reason)` calls at the watchdog
|
|
74
|
-
* expiry closure (`WATCHDOG`), sentinel-read failure (`SENTINEL_UNREADABLE`),
|
|
75
|
-
* StopFailure hook (`STOP_FAILURE`), clean no-pending-work path (`STOP`).
|
|
76
|
-
* - `pty-emit-result.mjs` — writes `metadata.teardownReason` on the result
|
|
77
|
-
* JSONL line (As1033).
|
|
78
|
-
* - `claude-session.js` — `getMetadata()` propagates the value into
|
|
79
|
-
* `turnSignals.teardownReason` (As1033).
|
|
80
|
-
* - `JobRunner.js` — watchdog classification gate reads
|
|
81
|
-
* `turnSignals.teardownReason` and compares against `TEARDOWN_REASONS.WATCHDOG`
|
|
82
|
-
* (As1033).
|
|
83
|
-
* - `Log.js` observability — the `WATCHDOG event=teardown reason=<value>`
|
|
84
|
-
* line format (As1012) references the enum values verbatim.
|
|
85
|
-
*
|
|
86
|
-
* Packaging
|
|
87
|
-
* ---------
|
|
88
|
-
*
|
|
89
|
-
* Copied verbatim into `dist/` by `server/package.json`'s `build:all` script,
|
|
90
|
-
* alongside `pty-wrapper.mjs`, `pty-emit-result.mjs`, and
|
|
91
|
-
* `pty-deferred-teardown.mjs`. Not bundled — the wrapper is executed as a
|
|
92
|
-
* standalone node process and imports this module by relative path.
|
|
93
|
-
*/
|
|
94
|
-
|
|
95
|
-
export const TEARDOWN_REASONS = Object.freeze({
|
|
96
|
-
WATCHDOG: 'watchdog',
|
|
97
|
-
STOP: 'stop',
|
|
98
|
-
SENTINEL_UNREADABLE: 'sentinel-unreadable',
|
|
99
|
-
STOP_FAILURE: 'stop-failure',
|
|
100
|
-
});
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
// TranscriptTailer — polls a Claude Code session transcript JSONL file, forwards
|
|
2
|
-
// post-baseline entries to a consumer, and recovers transparently when the file
|
|
3
|
-
// shrinks, is replaced (dev/ino change), tears mid-write, or contains
|
|
4
|
-
// non-final unparseable lines. Sibling of `pty-wrapper.mjs`; not imported by it
|
|
5
|
-
// at this task's end-of-task (wired in at Task 6).
|
|
6
|
-
//
|
|
7
|
-
// Instance-owned mutable state — no module-level mutables. Two instances
|
|
8
|
-
// constructed against the same fixture path do not share state.
|
|
9
|
-
|
|
10
|
-
import * as fs from 'node:fs';
|
|
11
|
-
|
|
12
|
-
const TORN_PREFIX = 'tailTranscript: torn trailing line at cursor=';
|
|
13
|
-
const REOPENED_PREFIX = 'tailTranscript: reopened path=';
|
|
14
|
-
const UNPARSEABLE_PREFIX = 'tailTranscript: non-final unparseable line at offset=';
|
|
15
|
-
const UNUSABLE_PREFIX = 'tailTranscript: boundary unusable, cause=';
|
|
16
|
-
const READ_ERROR_PREFIX = 'tailTranscript: read error:';
|
|
17
|
-
|
|
18
|
-
// Regex UUID extraction — used by the pre-baseline walk and re-walks so a
|
|
19
|
-
// single unparseable line cannot poison the baseline the way per-line
|
|
20
|
-
// JSON.parse would.
|
|
21
|
-
const UUID_REGEX = /"uuid"\s*:\s*"([0-9a-fA-F-]+)"/g;
|
|
22
|
-
|
|
23
|
-
function extractUuidsInto(text, set) {
|
|
24
|
-
UUID_REGEX.lastIndex = 0;
|
|
25
|
-
let match;
|
|
26
|
-
while ((match = UUID_REGEX.exec(text)) !== null) {
|
|
27
|
-
set.add(match[1]);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function createTranscriptTailer({ log } = {}) {
|
|
32
|
-
const emit = typeof log === 'function'
|
|
33
|
-
? log
|
|
34
|
-
: (message) => { process.stderr.write(message + '\n'); };
|
|
35
|
-
|
|
36
|
-
const state = {
|
|
37
|
-
path: null,
|
|
38
|
-
dev: null,
|
|
39
|
-
ino: null,
|
|
40
|
-
cursor: 0,
|
|
41
|
-
preBaselineUUIDs: new Set(),
|
|
42
|
-
processedUUIDs: new Set(),
|
|
43
|
-
boundaryEstablished: false,
|
|
44
|
-
unusable: false,
|
|
45
|
-
closed: false,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
function setUnusable(cause) {
|
|
49
|
-
if (state.unusable) return;
|
|
50
|
-
state.unusable = true;
|
|
51
|
-
emit(`${UNUSABLE_PREFIX}${cause} — subsequent ticks will return no entries`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function initialiseBoundary(path, options = {}) {
|
|
55
|
-
if (state.boundaryEstablished) return;
|
|
56
|
-
state.boundaryEstablished = true;
|
|
57
|
-
state.path = path;
|
|
58
|
-
const { resume = false } = options;
|
|
59
|
-
if (!resume) {
|
|
60
|
-
state.cursor = 0;
|
|
61
|
-
state.preBaselineUUIDs = new Set();
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
// Case B: --resume — synchronous pre-spawn baseline take.
|
|
65
|
-
try {
|
|
66
|
-
const stats = fs.statSync(path);
|
|
67
|
-
const content = fs.readFileSync(path, 'utf-8');
|
|
68
|
-
state.dev = stats.dev;
|
|
69
|
-
state.ino = stats.ino;
|
|
70
|
-
state.cursor = stats.size;
|
|
71
|
-
state.preBaselineUUIDs = new Set();
|
|
72
|
-
extractUuidsInto(content, state.preBaselineUUIDs);
|
|
73
|
-
} catch {
|
|
74
|
-
setUnusable('readError');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function reopenAndRewalkBoundary() {
|
|
79
|
-
if (!state.boundaryEstablished) return;
|
|
80
|
-
if (state.unusable) return;
|
|
81
|
-
try {
|
|
82
|
-
const prevDev = state.dev;
|
|
83
|
-
const prevIno = state.ino;
|
|
84
|
-
const stats = fs.statSync(state.path);
|
|
85
|
-
const content = fs.readFileSync(state.path, 'utf-8');
|
|
86
|
-
const rebuilt = new Set();
|
|
87
|
-
extractUuidsInto(content, rebuilt);
|
|
88
|
-
state.dev = stats.dev;
|
|
89
|
-
state.ino = stats.ino;
|
|
90
|
-
state.cursor = stats.size;
|
|
91
|
-
state.preBaselineUUIDs = rebuilt;
|
|
92
|
-
emit(
|
|
93
|
-
`${REOPENED_PREFIX}${state.path} dev=${state.dev} ino=${state.ino} `
|
|
94
|
-
+ `(was dev=${prevDev} ino=${prevIno}), cursor=0, `
|
|
95
|
-
+ `re-walking to rebuild preBaselineUUIDs (${rebuilt.size} entries suppressed from forwarding)`
|
|
96
|
-
);
|
|
97
|
-
} catch {
|
|
98
|
-
setUnusable('reopenFailed');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function tick() {
|
|
103
|
-
if (!state.boundaryEstablished) return [];
|
|
104
|
-
if (state.unusable) return [];
|
|
105
|
-
if (state.closed) return [];
|
|
106
|
-
|
|
107
|
-
let stats;
|
|
108
|
-
try {
|
|
109
|
-
stats = fs.statSync(state.path);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
if (err && err.code === 'ENOENT') return [];
|
|
112
|
-
emit(`${READ_ERROR_PREFIX} ${err && err.message ? err.message : String(err)}`);
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (state.dev !== null && state.ino !== null) {
|
|
117
|
-
if (stats.dev !== state.dev || stats.ino !== state.ino || stats.size < state.cursor) {
|
|
118
|
-
reopenAndRewalkBoundary();
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
// First successful stat since boundary establishment (Case A): record
|
|
123
|
-
// (dev, ino) implicitly so subsequent ticks can detect replacement.
|
|
124
|
-
state.dev = stats.dev;
|
|
125
|
-
state.ino = stats.ino;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (stats.size <= state.cursor) return [];
|
|
129
|
-
|
|
130
|
-
const toRead = stats.size - state.cursor;
|
|
131
|
-
let buf;
|
|
132
|
-
try {
|
|
133
|
-
const fd = fs.openSync(state.path, 'r');
|
|
134
|
-
try {
|
|
135
|
-
buf = Buffer.alloc(toRead);
|
|
136
|
-
let totalRead = 0;
|
|
137
|
-
while (totalRead < toRead) {
|
|
138
|
-
const n = fs.readSync(fd, buf, totalRead, toRead - totalRead, state.cursor + totalRead);
|
|
139
|
-
if (n === 0) break;
|
|
140
|
-
totalRead += n;
|
|
141
|
-
}
|
|
142
|
-
if (totalRead < toRead) buf = buf.slice(0, totalRead);
|
|
143
|
-
} finally {
|
|
144
|
-
fs.closeSync(fd);
|
|
145
|
-
}
|
|
146
|
-
} catch (err) {
|
|
147
|
-
emit(`${READ_ERROR_PREFIX} ${err && err.message ? err.message : String(err)}`);
|
|
148
|
-
return [];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const entries = [];
|
|
152
|
-
const startCursor = state.cursor;
|
|
153
|
-
let localOffset = 0;
|
|
154
|
-
|
|
155
|
-
while (localOffset < buf.length) {
|
|
156
|
-
const nlIdx = buf.indexOf(0x0A, localOffset);
|
|
157
|
-
if (nlIdx === -1) {
|
|
158
|
-
// Incomplete trailing line — leave cursor at its start; re-read next poll.
|
|
159
|
-
const tornPos = startCursor + localOffset;
|
|
160
|
-
state.cursor = tornPos;
|
|
161
|
-
emit(`${TORN_PREFIX}${tornPos}, deferring; will re-read next poll`);
|
|
162
|
-
return entries;
|
|
163
|
-
}
|
|
164
|
-
const line = buf.slice(localOffset, nlIdx).toString('utf-8');
|
|
165
|
-
const lineByteOffset = startCursor + localOffset;
|
|
166
|
-
|
|
167
|
-
if (line.length > 0) {
|
|
168
|
-
let entry;
|
|
169
|
-
let parsed = false;
|
|
170
|
-
try {
|
|
171
|
-
entry = JSON.parse(line);
|
|
172
|
-
parsed = true;
|
|
173
|
-
} catch {
|
|
174
|
-
emit(`${UNPARSEABLE_PREFIX}${lineByteOffset} — skipping (cursor still advances)`);
|
|
175
|
-
}
|
|
176
|
-
if (parsed) {
|
|
177
|
-
const uuid = entry && typeof entry === 'object' ? entry.uuid : undefined;
|
|
178
|
-
if (!uuid || (!state.preBaselineUUIDs.has(uuid) && !state.processedUUIDs.has(uuid))) {
|
|
179
|
-
if (uuid) state.processedUUIDs.add(uuid);
|
|
180
|
-
entries.push(entry);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
localOffset = nlIdx + 1;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
state.cursor = startCursor + buf.length;
|
|
189
|
-
return entries;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function close() {
|
|
193
|
-
state.closed = true;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
initialiseBoundary,
|
|
198
|
-
tick,
|
|
199
|
-
close,
|
|
200
|
-
reopenAndRewalkBoundary,
|
|
201
|
-
get path() { return state.path; },
|
|
202
|
-
get unusable() { return state.unusable; },
|
|
203
|
-
get preBaselineUUIDs() { return state.preBaselineUUIDs; },
|
|
204
|
-
get boundaryEstablished() { return state.boundaryEstablished; },
|
|
205
|
-
};
|
|
206
|
-
}
|