@kelceyp/caw-server 1.0.230 → 1.0.233
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 +533 -514
- package/dist/pty-wrapper.mjs +105 -33
- package/dist/public_html/main.js +427 -427
- package/dist/public_html/styles.css +1 -1
- package/package.json +1 -1
package/dist/pty-wrapper.mjs
CHANGED
|
@@ -98,19 +98,15 @@ writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });
|
|
|
98
98
|
|
|
99
99
|
// --- Write settings file with Stop hook config ---
|
|
100
100
|
|
|
101
|
+
const hookEntry = {
|
|
102
|
+
matcher: '',
|
|
103
|
+
hooks: [{ type: 'command', command: hookScriptPath }]
|
|
104
|
+
};
|
|
105
|
+
|
|
101
106
|
const settings = {
|
|
102
107
|
hooks: {
|
|
103
|
-
Stop: [
|
|
104
|
-
|
|
105
|
-
matcher: '',
|
|
106
|
-
hooks: [
|
|
107
|
-
{
|
|
108
|
-
type: 'command',
|
|
109
|
-
command: hookScriptPath
|
|
110
|
-
}
|
|
111
|
-
]
|
|
112
|
-
}
|
|
113
|
-
]
|
|
108
|
+
Stop: [hookEntry],
|
|
109
|
+
StopFailure: [hookEntry]
|
|
114
110
|
}
|
|
115
111
|
};
|
|
116
112
|
writeFileSync(settingsPath, JSON.stringify(settings));
|
|
@@ -156,7 +152,7 @@ ensureNonInteractiveSetup(process.cwd());
|
|
|
156
152
|
|
|
157
153
|
// --- Deferred teardown state ---
|
|
158
154
|
|
|
159
|
-
const WATCHDOG_TIMEOUT_MS =
|
|
155
|
+
const WATCHDOG_TIMEOUT_MS = 90 * 60 * 1000; // 90 minutes — covers full ScheduleWakeup range (up to 3600s) with 30m headroom
|
|
160
156
|
|
|
161
157
|
let watchdogTimer = null;
|
|
162
158
|
let teardownInitiated = false;
|
|
@@ -194,6 +190,11 @@ process.on('SIGUSR1', () => {
|
|
|
194
190
|
teardown();
|
|
195
191
|
return;
|
|
196
192
|
}
|
|
193
|
+
if (payload.error) {
|
|
194
|
+
log(`StopFailure received: error=${payload.error}`);
|
|
195
|
+
teardown();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
197
198
|
const bgCount = Array.isArray(payload.background_tasks) ? payload.background_tasks.length : 0;
|
|
198
199
|
const cronCount = Array.isArray(payload.session_crons) ? payload.session_crons.length : 0;
|
|
199
200
|
log(`Sentinel: background_tasks=${bgCount}, session_crons=${cronCount}`);
|
|
@@ -267,23 +268,27 @@ ptyProcess.onData((data) => {
|
|
|
267
268
|
|
|
268
269
|
let transcriptPath = null;
|
|
269
270
|
let transcriptLinesRead = 0;
|
|
271
|
+
let transcriptPathResolutionAttempts = 0;
|
|
272
|
+
const processedEntryUUIDs = new Set();
|
|
273
|
+
let lastReadEntryUUID = null;
|
|
270
274
|
|
|
271
275
|
// Resolve transcript path, handling macOS /tmp → /private/tmp symlinks.
|
|
272
276
|
// Returns null if not yet found (retried on next poll).
|
|
273
277
|
const resolveTranscriptPath = () => {
|
|
274
278
|
if (transcriptPath) return transcriptPath;
|
|
275
279
|
|
|
280
|
+
transcriptPathResolutionAttempts++;
|
|
276
281
|
const claudeProjectsDir = join(homedir(), '.claude', 'projects');
|
|
277
282
|
const cwd = process.cwd();
|
|
278
283
|
const paths = new Set([cwd]);
|
|
279
284
|
try { paths.add(realpathSync(cwd)); } catch { /* ignore */ }
|
|
280
285
|
|
|
281
286
|
for (const dir of paths) {
|
|
282
|
-
const hash = dir.replace(
|
|
287
|
+
const hash = dir.replace(/[/.]/g, '-');
|
|
283
288
|
const candidate = join(claudeProjectsDir, hash, `${effectiveSessionId}.jsonl`);
|
|
284
289
|
if (existsSync(candidate)) {
|
|
285
290
|
transcriptPath = candidate;
|
|
286
|
-
log(`Transcript found: ${transcriptPath}`);
|
|
291
|
+
log(`Transcript found (attempt ${transcriptPathResolutionAttempts}): ${transcriptPath}`);
|
|
287
292
|
// On resume: snapshot existing line count so only new content streams
|
|
288
293
|
if (resumeId) {
|
|
289
294
|
try {
|
|
@@ -295,6 +300,9 @@ const resolveTranscriptPath = () => {
|
|
|
295
300
|
return transcriptPath;
|
|
296
301
|
}
|
|
297
302
|
}
|
|
303
|
+
if (transcriptPathResolutionAttempts === 1 || transcriptPathResolutionAttempts % 10 === 0) {
|
|
304
|
+
log(`Transcript not found (attempt ${transcriptPathResolutionAttempts}), cwd=${cwd}, session=${effectiveSessionId}`);
|
|
305
|
+
}
|
|
298
306
|
return null;
|
|
299
307
|
};
|
|
300
308
|
|
|
@@ -307,19 +315,62 @@ const tailTranscript = () => {
|
|
|
307
315
|
try {
|
|
308
316
|
const content = readFileSync(path, 'utf-8');
|
|
309
317
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
318
|
+
const fileLineCount = lines.length;
|
|
319
|
+
|
|
320
|
+
if (fileLineCount < transcriptLinesRead) {
|
|
321
|
+
log(`tailTranscript: FILE SHRINK — transcriptLinesRead=${transcriptLinesRead}, fileLines=${fileLineCount} (file replaced or truncated), resetting to 0`);
|
|
322
|
+
transcriptLinesRead = 0;
|
|
323
|
+
} else if (transcriptLinesRead > 0 && lastReadEntryUUID) {
|
|
324
|
+
try {
|
|
325
|
+
const checkEntry = JSON.parse(lines[transcriptLinesRead - 1]);
|
|
326
|
+
if (checkEntry.uuid && checkEntry.uuid !== lastReadEntryUUID) {
|
|
327
|
+
log(`tailTranscript: FILE REPLACED — UUID mismatch at position ${transcriptLinesRead - 1} (expected ${lastReadEntryUUID}, got ${checkEntry.uuid}), resetting to 0`);
|
|
328
|
+
transcriptLinesRead = 0;
|
|
329
|
+
}
|
|
330
|
+
} catch { /* parse error during write — skip validation this poll */ }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const pending = fileLineCount - transcriptLinesRead;
|
|
334
|
+
if (pending > 0) {
|
|
335
|
+
log(`tailTranscript: ${pending} new lines (fileLines=${fileLineCount}, read=${transcriptLinesRead})`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const typeCounts = {};
|
|
339
|
+
let skipped = 0;
|
|
310
340
|
for (let i = transcriptLinesRead; i < lines.length; i++) {
|
|
311
341
|
try {
|
|
312
342
|
const entry = JSON.parse(lines[i]);
|
|
313
|
-
if (entry.type === 'progress' && entry.data?.type === 'hook_progress')
|
|
343
|
+
if (entry.type === 'progress' && entry.data?.type === 'hook_progress') {
|
|
344
|
+
skipped++;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (entry.uuid && processedEntryUUIDs.has(entry.uuid)) {
|
|
348
|
+
skipped++;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
314
351
|
process.stdout.write(JSON.stringify({ type: 'transcript', entry }) + '\n');
|
|
352
|
+
if (entry.uuid) processedEntryUUIDs.add(entry.uuid);
|
|
315
353
|
newLines++;
|
|
354
|
+
const key = entry.type || 'unknown';
|
|
355
|
+
typeCounts[key] = (typeCounts[key] || 0) + 1;
|
|
316
356
|
} catch {
|
|
317
|
-
|
|
357
|
+
skipped++;
|
|
318
358
|
}
|
|
319
359
|
}
|
|
320
360
|
transcriptLinesRead = lines.length;
|
|
321
|
-
|
|
322
|
-
|
|
361
|
+
if (lines.length > 0) {
|
|
362
|
+
try {
|
|
363
|
+
const lastEntry = JSON.parse(lines[lines.length - 1]);
|
|
364
|
+
if (lastEntry.uuid) lastReadEntryUUID = lastEntry.uuid;
|
|
365
|
+
} catch { /* ignore */ }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (newLines > 0 || skipped > 0) {
|
|
369
|
+
const types = Object.entries(typeCounts).map(([k, v]) => `${k}=${v}`).join(', ');
|
|
370
|
+
log(`tailTranscript: forwarded=${newLines}, skipped=${skipped}${types ? `, types: ${types}` : ''}`);
|
|
371
|
+
}
|
|
372
|
+
} catch (err) {
|
|
373
|
+
log(`tailTranscript: read error: ${err.message}`);
|
|
323
374
|
}
|
|
324
375
|
return newLines;
|
|
325
376
|
};
|
|
@@ -328,24 +379,34 @@ const tailTranscript = () => {
|
|
|
328
379
|
// Called from onExit after polling stops. Reads sentinel written by Stop hook.
|
|
329
380
|
|
|
330
381
|
let resultEmitted = false;
|
|
382
|
+
let failureEmitted = false;
|
|
331
383
|
|
|
332
384
|
const emitResult = () => {
|
|
333
|
-
if (resultEmitted) return;
|
|
385
|
+
if (resultEmitted || failureEmitted) return;
|
|
334
386
|
if (!existsSync(sentinelPath)) return;
|
|
335
387
|
|
|
336
388
|
try {
|
|
337
389
|
const hookData = JSON.parse(readFileSync(sentinelPath, 'utf-8'));
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
390
|
+
|
|
391
|
+
if (hookData.error) {
|
|
392
|
+
process.stdout.write(JSON.stringify({
|
|
393
|
+
type: 'error',
|
|
394
|
+
error: hookData.error,
|
|
395
|
+
message: hookData.last_assistant_message || ''
|
|
396
|
+
}) + '\n');
|
|
397
|
+
failureEmitted = true;
|
|
398
|
+
} else {
|
|
399
|
+
process.stdout.write(JSON.stringify({
|
|
400
|
+
type: 'result',
|
|
401
|
+
result: hookData.last_assistant_message || '',
|
|
402
|
+
metadata: {
|
|
403
|
+
sessionId: hookData.session_id || effectiveSessionId,
|
|
404
|
+
transcriptPath: hookData.transcript_path || transcriptPath || null,
|
|
405
|
+
permissionMode: hookData.permission_mode || null
|
|
406
|
+
}
|
|
407
|
+
}) + '\n');
|
|
408
|
+
resultEmitted = true;
|
|
409
|
+
}
|
|
349
410
|
} catch (err) {
|
|
350
411
|
log(`Error reading sentinel: ${err.message}`);
|
|
351
412
|
}
|
|
@@ -391,16 +452,27 @@ ptyProcess.onExit(({ exitCode }) => {
|
|
|
391
452
|
// Do a final transcript tail to capture any last entries
|
|
392
453
|
tailTranscript();
|
|
393
454
|
|
|
394
|
-
// Read sentinel and emit result (tempDir still present — delete after)
|
|
455
|
+
// Read sentinel and emit result or error (tempDir still present — delete after)
|
|
395
456
|
emitResult();
|
|
396
457
|
|
|
458
|
+
// Prompt-too-long detection: no sentinel written, ttyBuffer has the error text
|
|
459
|
+
if (!resultEmitted && !failureEmitted && ttyBuffer.includes('Prompt is too long')) {
|
|
460
|
+
process.stdout.write(JSON.stringify({
|
|
461
|
+
type: 'error',
|
|
462
|
+
error: 'prompt_too_long',
|
|
463
|
+
message: 'Prompt is too long'
|
|
464
|
+
}) + '\n');
|
|
465
|
+
failureEmitted = true;
|
|
466
|
+
}
|
|
467
|
+
|
|
397
468
|
// Clean up temp directory
|
|
398
469
|
try { rmSync(tempDir, { recursive: true, force: true }); } catch {}
|
|
399
470
|
|
|
400
471
|
log(`Claude exited (code: ${exitCode})`);
|
|
401
472
|
|
|
402
|
-
|
|
403
|
-
|
|
473
|
+
if (failureEmitted) {
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
404
476
|
if (resultEmitted || exitCode === 0) {
|
|
405
477
|
process.exit(0);
|
|
406
478
|
}
|