@kelceyp/caw-server 1.0.278 → 1.0.279

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.
@@ -41,6 +41,7 @@ export function emitResult({
41
41
  effectiveSessionId = null,
42
42
  startedAt = null,
43
43
  endedAt = null,
44
+ teardownReason = null,
44
45
  alreadyEmitted = false,
45
46
  suppressFallback = false,
46
47
  write = defaultWrite,
@@ -67,6 +68,14 @@ export function emitResult({
67
68
  if (startedAt !== null && endedAt !== null) {
68
69
  metadata.durationMs = endedAt - startedAt;
69
70
  }
71
+ // As1033 Task 6: append teardownReason ('stop' | 'watchdog') so the
72
+ // downstream stream handler (Task 5 landed the capture) and the
73
+ // JobRunner classifier gate (Task 7) can distinguish a clean Stop
74
+ // finish from a watchdog force-kill. Field order of the four
75
+ // pre-existing fields is preserved byte-for-byte.
76
+ if (teardownReason !== null) {
77
+ metadata.teardownReason = teardownReason;
78
+ }
70
79
  // D11 test-only override: when the wrapper's CAW_PTY_SUPPRESS_STOP_FALLBACK
71
80
  // gate is set, blank the `result` field on the success branch only. The
72
81
  // wire is deliberately identical to a real empty-message turn — that IS
@@ -370,6 +370,11 @@ const SUPPRESS_STOP_FALLBACK = process.env.CAW_PTY_SUPPRESS_STOP_FALLBACK === '1
370
370
 
371
371
  let watchdogTimer = null;
372
372
  let teardownInitiated = false;
373
+ // Captured at the top of teardown() on both paths (clean-Stop passes 'stop',
374
+ // watchdog callback passes 'watchdog'). Read by emitResult() on the success
375
+ // branch and written to metadata.teardownReason so the downstream
376
+ // stream-handler and classifier (Task 7) can distinguish the two paths.
377
+ let teardownReason = null;
373
378
 
374
379
  // Duration capture: wall clock from `pty.spawn` to top of `teardown()`.
375
380
  // `endedAt` is set by whichever teardown path fires first (normal SIGUSR1
@@ -388,16 +393,17 @@ const startOrResetWatchdog = () => {
388
393
  if (watchdogTimer) clearTimeout(watchdogTimer);
389
394
  log(`Watchdog ${watchdogTimer ? 'reset' : 'armed'}: ${WATCHDOG_TIMEOUT_MS / 60000}m timeout`);
390
395
  watchdogTimer = setTimeout(() => {
391
- log(`Watchdog expired after ${WATCHDOG_TIMEOUT_MS / 60000}m with no progress, force-killing claude`);
392
- teardown();
396
+ log(`Watchdog expired: ${WATCHDOG_TIMEOUT_MS / 60000}m elapsed since last deferred Stop, force-killing claude`);
397
+ teardown('watchdog');
393
398
  }, WATCHDOG_TIMEOUT_MS);
394
399
  };
395
400
 
396
- const teardown = () => {
401
+ const teardown = (reason = 'stop') => {
397
402
  if (teardownInitiated) return;
398
403
  teardownInitiated = true;
399
404
  endedAt = Date.now();
400
- log('Teardown: stop received, no pending work, killing claude');
405
+ teardownReason = reason;
406
+ log(`Teardown (reason=${reason}): killing claude`);
401
407
  log(`Duration: ${startedAt !== null ? (endedAt - startedAt) : 'unknown'} ms (startedAt=${startedAt}, endedAt=${endedAt})`);
402
408
  if (watchdogTimer) { clearTimeout(watchdogTimer); watchdogTimer = null; }
403
409
  stopPolling();
@@ -654,6 +660,7 @@ const emitResult = (hookData) => {
654
660
  effectiveSessionId,
655
661
  startedAt,
656
662
  endedAt,
663
+ teardownReason,
657
664
  alreadyEmitted: resultEmitted || failureEmitted,
658
665
  suppressFallback: SUPPRESS_STOP_FALLBACK,
659
666
  log