@cohortapp/agent-sdk 2.18.7 → 2.18.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cohortapp/agent-sdk",
3
- "version": "2.18.7",
3
+ "version": "2.18.8",
4
4
  "description": "Cohort Agent SDK — autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -417,6 +417,46 @@ restart_daemon(){
417
417
  if [ -z "$DAEMON_LABEL" ]; then log "no -daemon plist under ~/Library/LaunchAgents — nothing to kickstart (maestro upgrade installs it; doctor will say)"; return 0; fi
418
418
  launchctl kickstart -k "gui/$UID_/$DAEMON_LABEL" >> "$LOG" 2>&1 || log "WARN: kickstart $DAEMON_LABEL nonzero"
419
419
  }
420
+ session_beating(){ # is the front door actually ANSWERING, not merely loaded?
421
+ # `session_reconciled` asks whether the job is loaded and a supervisor process
422
+ # is alive. Both were true on the seats that went silent: on 2026-09-24 three
423
+ # front doors had been wedged on an unanswered modal for days with their
424
+ # supervisors up and their jobs loaded. A process that exists is not a
425
+ # colleague who answers — the same substitution this gate's daemon half was
426
+ # rewritten to stop making.
427
+ local hb="$AGENT_DIR/state/session/heartbeat.json" age
428
+ [ -f "$hb" ] || return 1
429
+ age="$(node -e '
430
+ try {
431
+ const j = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
432
+ const t = Date.parse(j && j.ts);
433
+ process.stdout.write(Number.isFinite(t) ? String(Math.round((Date.now() - t) / 1000)) : "");
434
+ } catch {}
435
+ ' "$hb" 2>/dev/null)"
436
+ [ -n "$age" ] || return 1
437
+ [ "$age" -le "$SESSION_STALE_S" ]
438
+ }
439
+
440
+ revive_session(){ # a wedged front door cannot restart itself; restart it FOR it
441
+ # The upgrade notice asks the session to restart "at an idle moment". A
442
+ # session sitting on a modal never reaches an idle moment, so the polite ask
443
+ # is exactly the wrong instrument for the one case that needs it. A hard
444
+ # kickstart drops a wedged session and the new supervisor launches a fresh
445
+ # one — which clears the modal by construction and, from this release,
446
+ # watches the pane so it does not happen again.
447
+ [ -n "$SESSION_LABEL" ] || return 0
448
+ # An UNLOADED job is a different fault with a different remedy (`maestro
449
+ # session start`), and kickstarting a label launchd does not know just fails.
450
+ # Leave that one to session_reconciled, which already names it.
451
+ label_loaded "$SESSION_LABEL" || return 0
452
+ session_beating && return 0
453
+ log "front door $SESSION_LABEL is loaded but not beating (> ${SESSION_STALE_S}s) — kickstarting it rather than waiting for a session that cannot answer"
454
+ launchctl kickstart -k "gui/$(id -u)/$SESSION_LABEL" >> "$LOG" 2>&1 \
455
+ && { log "front door $SESSION_LABEL restarted"; return 0; }
456
+ log "WARN: could not kickstart $SESSION_LABEL (maestro session restart --force)"
457
+ return 1
458
+ }
459
+
420
460
  session_reconciled(){ # only asked when a -session plist was generated; never a rollback reason
421
461
  label_loaded "$SESSION_LABEL" || { log "reconcile-failed: session job $SESSION_LABEL is not loaded (maestro session start)"; return 1; }
422
462
  pgrep -f "$AGENT_DIR/scripts/session/supervisor" >/dev/null 2>&1 || { log "reconcile-failed: $SESSION_LABEL is loaded but no supervisor process is alive for $AGENT_DIR (maestro session status)"; return 1; }
@@ -544,6 +584,12 @@ if [ "$CUR" = "$LATEST" ] || [ "$(printf '%s\n%s\n' "$CUR" "$LATEST" | sort -V |
544
584
  if is_healthy; then
545
585
  log "healthy on $CUR"
546
586
  sibling_job_audit
587
+ # The up-to-date path is NOT exempt from the front door, for the same
588
+ # reason it stopped being exempt from the daemon: the seats that went
589
+ # silent for days were all "up to date". A daemon that beats while the
590
+ # front door is wedged is a seat that looks perfect from here and answers
591
+ # nobody.
592
+ revive_session || true
547
593
  if [ -n "$STALE_HOP" ]; then
548
594
  # A real version hop: complete it the way an automatic one completes —
549
595
  # the notice tells the session to restart itself onto the new code.
@@ -597,6 +643,11 @@ fi
597
643
  # installed version, never a LATEST). Fail-open: an unreadable last.json holds
598
644
  # nothing.
599
645
  FAILED_HOLD_S="${MAESTRO_AUTOUPDATE_FAILED_HOLD_S:-86400}"
646
+ # How long a front door may go without beating before this run restarts it FOR
647
+ # it. Ten minutes: long enough that a busy session is never interrupted (the
648
+ # heartbeat rides every tool use), short enough that a wedged one is measured in
649
+ # minutes rather than the days it took to notice the last three.
650
+ SESSION_STALE_S="${MAESTRO_SESSION_STALE_S:-600}"
600
651
  failed_hold_reason(){ # prints "<reason> at <at>" when LATEST failed health within FAILED_HOLD_S
601
652
  node -e '
602
653
  const [file, latest, holdS] = process.argv.slice(1);
@@ -668,7 +719,11 @@ restart_daemon
668
719
 
669
720
  if is_healthy; then
670
721
  REASON=""
671
- if [ -n "$SESSION_LABEL" ] && ! session_reconciled; then REASON="session-reconcile-failed"; fi
722
+ # Revive BEFORE reconciling: the new code is installed, and a front door that
723
+ # is not beating must be restarted onto it rather than left holding an inbox
724
+ # it cannot answer.
725
+ revive_session || REASON="session-revive-failed"
726
+ if [ -n "$SESSION_LABEL" ] && ! session_reconciled; then REASON="${REASON:-session-reconcile-failed}"; fi
672
727
  log "OK: healthy on $LATEST${REASON:+ ($REASON)}"
673
728
  sibling_job_audit
674
729
  write_upgrade_notice "$CUR" "$LATEST"