squadrant 0.17.0 → 0.18.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.
@@ -507,7 +507,21 @@ function nextPendingMonitor(current, ev, now) {
507
507
  }
508
508
  function reduce(rec, ev, now) {
509
509
  if (ev.type === "task.reopened") {
510
- return { ...rec, state: "working", question: void 0, error: void 0, lastHeartbeat: now, lastEvent: ev.type };
510
+ return { ...rec, state: "working", question: void 0, error: void 0, lastHeartbeat: now, lastEvent: ev.type, workingStretchStartedAt: now };
511
+ }
512
+ if (ev.type === "crew.takeover.started") {
513
+ if (rec.operatorHold)
514
+ return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
515
+ return {
516
+ ...rec,
517
+ operatorHold: { since: now, ...ev.note !== void 0 ? { note: ev.note } : {} },
518
+ lastHeartbeat: now,
519
+ lastEvent: ev.type
520
+ };
521
+ }
522
+ if (ev.type === "crew.takeover.ended") {
523
+ const { operatorHold: _dropped, ...rest } = rec;
524
+ return { ...rest, lastHeartbeat: now, lastEvent: ev.type };
511
525
  }
512
526
  if (TERMINAL_STATES.has(rec.state))
513
527
  return rec;
@@ -523,8 +537,9 @@ function reduce(rec, ev, now) {
523
537
  // resuming after a blocked→reply clears the question
524
538
  pendingTool: void 0,
525
539
  // #354: a new turn closes any prior tool window
526
- pendingMonitor: void 0
540
+ pendingMonitor: void 0,
527
541
  // #594a: same reset — a new turn moots any prior watch
542
+ workingStretchStartedAt: now
528
543
  };
529
544
  case "task.progress": {
530
545
  const pendingTool = nextPendingTool(rec.pendingTool, ev, now);
@@ -693,6 +708,8 @@ function firePush(deps, project, prev, next, event, lastCaptainTurnAt) {
693
708
  return;
694
709
  if (!ATTENTION_STATES.has(next.state))
695
710
  return;
711
+ if (next.operatorHold)
712
+ return;
696
713
  if (next.state === "awaiting-input" && lastCaptainTurnAt != null && deps.now() - lastCaptainTurnAt <= IDLE_DEBOUNCE_MS) {
697
714
  return;
698
715
  }
@@ -744,8 +761,11 @@ var KNOWN_EVENT_TYPES = /* @__PURE__ */ new Set([
744
761
  "task.reconcile-failed",
745
762
  "task.cancelled",
746
763
  "task.session.ended",
747
- "task.first-turn.confirmed"
764
+ "task.first-turn.confirmed",
748
765
  // #466: delivery confirmation
766
+ "crew.takeover.started",
767
+ "crew.takeover.ended"
768
+ // #649: operator takeover
749
769
  ]);
750
770
  function createDaemon(deps) {
751
771
  const { store, now } = deps;
@@ -927,9 +947,32 @@ function createDaemon(deps) {
927
947
  store.delete(r.project, r.id);
928
948
  continue;
929
949
  }
950
+ if (r.operatorHold) {
951
+ const threshold = (deps.takeoverNudgeHours ?? 6) * 36e5;
952
+ const holdAge = t - r.operatorHold.since;
953
+ if (holdAge > threshold) {
954
+ const timeSinceLastNudge = t - (r.operatorHold.lastNudgeAt ?? 0);
955
+ if (timeSinceLastNudge > threshold) {
956
+ const hrs = Math.round(holdAge / 36e5);
957
+ const tag = crewTag(r);
958
+ const message = `CREW HELD-LONG ${tag} \u2014 held ${hrs}h. Ask the operator whether it is still in use. Do not release it yourself.`;
959
+ store.put({ ...r, operatorHold: { ...r.operatorHold, lastNudgeAt: t } });
960
+ if (deps.notify) {
961
+ try {
962
+ const p = deps.notify({ project: r.project, message, record: r, event: { type: "task.progress", id: r.id } });
963
+ if (p && typeof p.catch === "function")
964
+ p.catch(() => {
965
+ });
966
+ } catch {
967
+ }
968
+ }
969
+ }
970
+ }
971
+ }
930
972
  if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
931
973
  const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
932
- if (t - r.createdAt > ceiling) {
974
+ const refTime = r.workingStretchStartedAt ?? r.createdAt;
975
+ if (t - refTime > ceiling) {
933
976
  const prevState = r.state;
934
977
  const tag = crewTag(r);
935
978
  const hrs = Math.round(ceiling / 36e5);
@@ -1791,7 +1834,9 @@ function buildContext(opts) {
1791
1834
  const sockPath = opts.sockPath ?? join9(homedir6(), ".config", "squadrant", "squadrant.sock");
1792
1835
  const store = createStore(stateRoot);
1793
1836
  const bootedAt = Date.now();
1794
- const taskTimeoutMs = loadConfig().defaults.taskTimeoutMs;
1837
+ const config = loadConfig();
1838
+ const taskTimeoutMs = config.defaults.taskTimeoutMs;
1839
+ const takeoverNudgeHours = config.defaults.takeoverNudgeHours;
1795
1840
  const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
1796
1841
  const spawn2 = opts.spawn ?? realSpawn;
1797
1842
  const resultsDir = join9(stateRoot, "_results");
@@ -1811,6 +1856,7 @@ function buildContext(opts) {
1811
1856
  bootedAt,
1812
1857
  lastSweepAt: { value: null },
1813
1858
  taskTimeoutMs,
1859
+ takeoverNudgeHours,
1814
1860
  isPidAlive,
1815
1861
  spawn: spawn2,
1816
1862
  resultsDir,
@@ -2605,6 +2651,7 @@ function startDaemon(ctx, opts, pkgVersion) {
2605
2651
  isPidAlive,
2606
2652
  notify,
2607
2653
  taskTimeoutMs,
2654
+ takeoverNudgeHours: ctx.takeoverNudgeHours,
2608
2655
  isSurfaceAlive: surfaceProbe,
2609
2656
  resendFirstTurn: ctx.resendFirstTurn,
2610
2657
  launchHeadless: opts.launchHeadless,