@kelceyp/caw-server 1.0.215 → 1.0.217
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/main.js +442 -435
- package/dist/pty-deferred-teardown.mjs +17 -0
- package/dist/pty-wrapper.mjs +65 -7
- package/dist/public_html/main.js +399 -399
- package/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
export const readSentinelPayload = (sentinelPath) => {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(readFileSync(sentinelPath, 'utf-8'));
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const hasPendingWork = (payload) => {
|
|
12
|
+
const tasks = payload?.background_tasks;
|
|
13
|
+
if (Array.isArray(tasks) && tasks.length > 0) return true;
|
|
14
|
+
const crons = payload?.session_crons;
|
|
15
|
+
if (Array.isArray(crons) && crons.length > 0) return true;
|
|
16
|
+
return false;
|
|
17
|
+
};
|
package/dist/pty-wrapper.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* --session-id <uuid> Session ID (first run)
|
|
11
11
|
* --resume <uuid> Resume session (continuation)
|
|
12
12
|
* --model <model> Model override
|
|
13
|
+
* --effort <level> Effort level override
|
|
13
14
|
* --skip-permissions Enable --dangerously-skip-permissions
|
|
14
15
|
*
|
|
15
16
|
* Stdin: The prompt text (piped by JobRunner before spawn)
|
|
@@ -22,6 +23,7 @@ import { mkdtempSync, readFileSync, existsSync, writeFileSync, rmSync, realpathS
|
|
|
22
23
|
import { join } from 'path';
|
|
23
24
|
import { tmpdir, homedir } from 'os';
|
|
24
25
|
import { execSync } from 'child_process';
|
|
26
|
+
import { readSentinelPayload, hasPendingWork } from './pty-deferred-teardown.mjs';
|
|
25
27
|
|
|
26
28
|
// --- Arg parsing ---
|
|
27
29
|
|
|
@@ -35,6 +37,7 @@ const hasFlag = (name) => args.includes(name);
|
|
|
35
37
|
const sessionId = getArg('--session-id');
|
|
36
38
|
const resumeId = getArg('--resume');
|
|
37
39
|
const model = getArg('--model');
|
|
40
|
+
const effortLevel = getArg('--effort');
|
|
38
41
|
const skipPermissions = hasFlag('--skip-permissions');
|
|
39
42
|
|
|
40
43
|
if (!sessionId && !resumeId) {
|
|
@@ -79,17 +82,16 @@ const sentinelPath = join(tempDir, 'result.json');
|
|
|
79
82
|
const hookScriptPath = join(tempDir, 'stop-hook.sh');
|
|
80
83
|
const settingsPath = join(tempDir, 'settings.json');
|
|
81
84
|
const claudePidPath = join(tempDir, 'claude.pid');
|
|
85
|
+
const wrapperPidPath = join(tempDir, 'wrapper.pid');
|
|
82
86
|
|
|
83
87
|
// --- Write Stop hook script ---
|
|
84
88
|
// The hook: (1) cats Stop hook JSON (on stdin) to sentinel file, then
|
|
85
|
-
// (2)
|
|
86
|
-
// Using a PID file avoids a fuzzy *claude* tree walk that could accidentally
|
|
87
|
-
// SIGINT the calling agent process (e.g. when running under a Claude agent).
|
|
89
|
+
// (2) sends SIGUSR1 to the wrapper PID so it can check whether teardown is safe.
|
|
88
90
|
|
|
89
91
|
const hookScript = [
|
|
90
92
|
'#!/bin/bash',
|
|
91
93
|
`cat > '${sentinelPath}'`,
|
|
92
|
-
`kill -
|
|
94
|
+
`kill -USR1 $(cat '${wrapperPidPath}') 2>/dev/null`,
|
|
93
95
|
''
|
|
94
96
|
].join('\n');
|
|
95
97
|
writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });
|
|
@@ -152,6 +154,59 @@ const ensureNonInteractiveSetup = (cwd) => {
|
|
|
152
154
|
|
|
153
155
|
ensureNonInteractiveSetup(process.cwd());
|
|
154
156
|
|
|
157
|
+
// --- Deferred teardown state ---
|
|
158
|
+
|
|
159
|
+
const WATCHDOG_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
160
|
+
|
|
161
|
+
let watchdogTimer = null;
|
|
162
|
+
let teardownInitiated = false;
|
|
163
|
+
|
|
164
|
+
// Forward declarations (assigned after ptyProcess is created)
|
|
165
|
+
let ptyProcess = null;
|
|
166
|
+
|
|
167
|
+
const startOrResetWatchdog = () => {
|
|
168
|
+
if (watchdogTimer) clearTimeout(watchdogTimer);
|
|
169
|
+
log(`Watchdog ${watchdogTimer ? 'reset' : 'armed'}: ${WATCHDOG_TIMEOUT_MS / 60000}m timeout`);
|
|
170
|
+
watchdogTimer = setTimeout(() => {
|
|
171
|
+
log(`Watchdog expired after ${WATCHDOG_TIMEOUT_MS / 60000}m with no progress, force-killing claude`);
|
|
172
|
+
teardown();
|
|
173
|
+
}, WATCHDOG_TIMEOUT_MS);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const teardown = () => {
|
|
177
|
+
if (teardownInitiated) return;
|
|
178
|
+
teardownInitiated = true;
|
|
179
|
+
log('Teardown: stop received, no pending work, killing claude');
|
|
180
|
+
if (watchdogTimer) { clearTimeout(watchdogTimer); watchdogTimer = null; }
|
|
181
|
+
stopPolling();
|
|
182
|
+
if (ptyProcess) ptyProcess.kill('SIGINT');
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// --- Register SIGUSR1 handler and write wrapper PID ---
|
|
186
|
+
// Must happen BEFORE spawning claude so the handler is ready when the first Stop fires.
|
|
187
|
+
|
|
188
|
+
process.on('SIGUSR1', () => {
|
|
189
|
+
log('Received SIGUSR1 (stop signal)');
|
|
190
|
+
tailTranscript();
|
|
191
|
+
const payload = readSentinelPayload(sentinelPath);
|
|
192
|
+
if (!payload) {
|
|
193
|
+
log(`Warning: failed to read sentinel, proceeding with teardown`);
|
|
194
|
+
teardown();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const bgCount = Array.isArray(payload.background_tasks) ? payload.background_tasks.length : 0;
|
|
198
|
+
const cronCount = Array.isArray(payload.session_crons) ? payload.session_crons.length : 0;
|
|
199
|
+
log(`Sentinel: background_tasks=${bgCount}, session_crons=${cronCount}`);
|
|
200
|
+
if (hasPendingWork(payload)) {
|
|
201
|
+
log('Deferring teardown: pending background work');
|
|
202
|
+
startOrResetWatchdog();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
teardown();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
writeFileSync(wrapperPidPath, String(process.pid));
|
|
209
|
+
|
|
155
210
|
// --- Build claude args ---
|
|
156
211
|
|
|
157
212
|
const claudeArgs = [];
|
|
@@ -166,6 +221,9 @@ if (skipPermissions) {
|
|
|
166
221
|
if (model) {
|
|
167
222
|
claudeArgs.push('--model', model);
|
|
168
223
|
}
|
|
224
|
+
if (effortLevel) {
|
|
225
|
+
claudeArgs.push('--effort', effortLevel);
|
|
226
|
+
}
|
|
169
227
|
claudeArgs.push('--settings', settingsPath);
|
|
170
228
|
claudeArgs.push(prompt);
|
|
171
229
|
|
|
@@ -174,7 +232,7 @@ log(`Prompt length: ${prompt.length} chars`);
|
|
|
174
232
|
|
|
175
233
|
// --- Spawn claude via PTY ---
|
|
176
234
|
|
|
177
|
-
|
|
235
|
+
ptyProcess = pty.spawn(claudePath, claudeArgs, {
|
|
178
236
|
name: 'xterm-256color',
|
|
179
237
|
cols: 120,
|
|
180
238
|
rows: 40,
|
|
@@ -252,7 +310,6 @@ const tailTranscript = () => {
|
|
|
252
310
|
for (let i = transcriptLinesRead; i < lines.length; i++) {
|
|
253
311
|
try {
|
|
254
312
|
const entry = JSON.parse(lines[i]);
|
|
255
|
-
// Skip hook progress notifications (internal noise)
|
|
256
313
|
if (entry.type === 'progress' && entry.data?.type === 'hook_progress') continue;
|
|
257
314
|
process.stdout.write(JSON.stringify({ type: 'transcript', entry }) + '\n');
|
|
258
315
|
newLines++;
|
|
@@ -327,7 +384,8 @@ ptyProcess.onExit(({ exitCode }) => {
|
|
|
327
384
|
if (exited) return;
|
|
328
385
|
exited = true;
|
|
329
386
|
|
|
330
|
-
//
|
|
387
|
+
// Clear watchdog and stop polling (sentinelPath still exists on disk at this point)
|
|
388
|
+
if (watchdogTimer) { clearTimeout(watchdogTimer); watchdogTimer = null; }
|
|
331
389
|
stopPolling();
|
|
332
390
|
|
|
333
391
|
// Do a final transcript tail to capture any last entries
|