squadrant 0.19.3 → 0.19.4

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.
@@ -2322,6 +2322,26 @@ function parseProgramArgs(plistXml) {
2322
2322
  return null;
2323
2323
  return { nodeBin: xmlUnescape(m[1]), daemonEntry: xmlUnescape(m[2]) };
2324
2324
  }
2325
+ function isVersionUpgrade(registeredEntry, thisEntry) {
2326
+ const a = registeredEntry.split("/");
2327
+ const b = thisEntry.split("/");
2328
+ if (a.length !== b.length)
2329
+ return false;
2330
+ let diffIndex = -1;
2331
+ for (let i = 0; i < a.length; i++) {
2332
+ if (a[i] !== b[i]) {
2333
+ if (diffIndex !== -1)
2334
+ return false;
2335
+ diffIndex = i;
2336
+ }
2337
+ }
2338
+ if (diffIndex === -1)
2339
+ return false;
2340
+ const versionedSegment = /^(.+)@([^@]+)$/;
2341
+ const ma = a[diffIndex].match(versionedSegment);
2342
+ const mb = b[diffIndex].match(versionedSegment);
2343
+ return ma !== null && mb !== null && ma[1] === mb[1] && ma[2] !== mb[2];
2344
+ }
2325
2345
  function detectForeignInstall(parsed, thisEntry, registeredEntryExists) {
2326
2346
  if (!parsed)
2327
2347
  return null;
@@ -2329,7 +2349,11 @@ function detectForeignInstall(parsed, thisEntry, registeredEntryExists) {
2329
2349
  return null;
2330
2350
  if (!registeredEntryExists)
2331
2351
  return null;
2332
- return { registeredEntry: parsed.daemonEntry, thisEntry };
2352
+ return {
2353
+ registeredEntry: parsed.daemonEntry,
2354
+ thisEntry,
2355
+ isUpgrade: isVersionUpgrade(parsed.daemonEntry, thisEntry)
2356
+ };
2333
2357
  }
2334
2358
  function sanitizePathForPlist(path21) {
2335
2359
  const seen = /* @__PURE__ */ new Set();
@@ -2496,7 +2520,7 @@ function forceKickstartAndVerify(target, opts = {}) {
2496
2520
  const pollDelayMs = opts.pollDelayMs ?? 300;
2497
2521
  const kickstartRetries = opts.kickstartRetries ?? 10;
2498
2522
  const kickstartRetryDelayMs = opts.kickstartRetryDelayMs ?? 300;
2499
- const pidBefore = getDaemonPid(target);
2523
+ const pidBefore = opts.pidBefore !== void 0 ? opts.pidBefore : getDaemonPid(target);
2500
2524
  let kickstartError = null;
2501
2525
  for (let i = 0; i < kickstartRetries; i++) {
2502
2526
  try {
@@ -2533,6 +2557,9 @@ function forceKickstartAndVerify(target, opts = {}) {
2533
2557
  function isOperatorInitiatedCommand(topLevelArg) {
2534
2558
  return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
2535
2559
  }
2560
+ function isReadOnlyCrewCommand(argv) {
2561
+ return argv[2] === "crew" && argv[3] !== void 0 && READ_ONLY_CREW_SUBCOMMANDS.has(argv[3]);
2562
+ }
2536
2563
  function ensureDaemon(nodeBin = process.execPath, opts = {}) {
2537
2564
  if (restartInFlight)
2538
2565
  return;
@@ -2553,8 +2580,10 @@ function ensureDaemon(nodeBin = process.execPath, opts = {}) {
2553
2580
  try {
2554
2581
  const drift = computeDaemonDrift(nodeBin);
2555
2582
  if (drift.foreignInstall) {
2556
- process.stderr.write(printForeignInstallError(drift.foreignInstall));
2557
- return;
2583
+ const decision = decideForeignInstall(drift.foreignInstall);
2584
+ process.stderr.write(decision.message);
2585
+ if (!decision.proceed)
2586
+ return;
2558
2587
  }
2559
2588
  applyDaemonDrift(drift);
2560
2589
  } catch (e) {
@@ -2564,6 +2593,12 @@ function ensureDaemon(nodeBin = process.execPath, opts = {}) {
2564
2593
  releaseDaemonLock();
2565
2594
  }
2566
2595
  }
2596
+ function decideForeignInstall(foreign) {
2597
+ if (foreign.isUpgrade) {
2598
+ return { proceed: true, message: printVersionUpgradeNotice(foreign) };
2599
+ }
2600
+ return { proceed: false, message: printForeignInstallError(foreign) };
2601
+ }
2567
2602
  function printForeignInstallError(foreign) {
2568
2603
  return `[squadrant] refusing to restart the daemon: the registered launchd config belongs to a DIFFERENT squadrant install than this one.
2569
2604
  registered install: ${foreign.registeredEntry}
@@ -2571,14 +2606,19 @@ function printForeignInstallError(foreign) {
2571
2606
  Two squadrant installs on this machine will keep fighting over the daemon (#670). Uninstall the one you don't use, then run \`squadrant heal daemon\` to reconcile.
2572
2607
  `;
2573
2608
  }
2609
+ function printVersionUpgradeNotice(foreign) {
2610
+ return `[squadrant] upgraded plist: ${foreign.registeredEntry} -> ${foreign.thisEntry}
2611
+ `;
2612
+ }
2574
2613
  function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
2575
2614
  if (!tryAcquireDaemonLock()) {
2576
2615
  throw new Error("could not acquire the daemon lock \u2014 another squadrant process is already restarting the daemon");
2577
2616
  }
2578
2617
  try {
2579
2618
  const drift = computeDaemonDrift(nodeBin);
2619
+ const pidBeforeReconcile = getDaemonPid(drift.target);
2580
2620
  reconcilePlistAndService(drift);
2581
- const result = forceKickstartAndVerify(drift.target, kickstartOpts);
2621
+ const result = forceKickstartAndVerify(drift.target, { ...kickstartOpts, pidBefore: pidBeforeReconcile });
2582
2622
  if (!result.restarted) {
2583
2623
  throw new Error(`daemon did not restart (pid before=${result.pidBefore ?? "none"}, after=${result.pidAfter ?? "none"}) \u2014 \`launchctl kickstart -k ${drift.target}\` ran but the pid never changed`);
2584
2624
  }
@@ -2587,13 +2627,14 @@ function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
2587
2627
  releaseDaemonLock();
2588
2628
  }
2589
2629
  }
2590
- var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS;
2630
+ var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS, READ_ONLY_CREW_SUBCOMMANDS;
2591
2631
  var init_launchd = __esm({
2592
2632
  "packages/core/dist/launchd.js"() {
2593
2633
  LABEL = "com.squadrant.daemon";
2594
2634
  AGENT_BINS = ["cmux", "claude", "opencode", "codex", "gemini", "node"];
2595
2635
  restartInFlight = false;
2596
2636
  OPERATOR_INITIATED_COMMANDS = /* @__PURE__ */ new Set(["launch", "init"]);
2637
+ READ_ONLY_CREW_SUBCOMMANDS = /* @__PURE__ */ new Set(["list", "read", "tasks"]);
2597
2638
  }
2598
2639
  });
2599
2640
 
@@ -3290,6 +3331,51 @@ var init_captain_delivery = __esm({
3290
3331
  }
3291
3332
  });
3292
3333
 
3334
+ // packages/core/dist/daemon/down-alert.js
3335
+ function pad2(n) {
3336
+ return String(n).padStart(2, "0");
3337
+ }
3338
+ function formatLocalDateTime(d) {
3339
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
3340
+ }
3341
+ function formatTzOffset(d) {
3342
+ const offMin = -d.getTimezoneOffset();
3343
+ const sign = offMin >= 0 ? "+" : "-";
3344
+ const abs = Math.abs(offMin);
3345
+ const hh = pad2(Math.floor(abs / 60));
3346
+ const mm = abs % 60;
3347
+ return mm === 0 ? `${sign}${hh}` : `${sign}${hh}:${pad2(mm)}`;
3348
+ }
3349
+ function formatDownAlertText(opts) {
3350
+ const start = new Date(opts.startTs);
3351
+ const end = new Date(opts.endTs);
3352
+ return `\u26A0\uFE0F daemon was down ${formatLocalDateTime(start)} \u2192 ${formatLocalDateTime(end)} (${formatTzOffset(end)}), ${opts.minutes} min (last exit reason=${opts.reasonText})`;
3353
+ }
3354
+ function formatRelative(ms) {
3355
+ const minutes = Math.round(ms / 6e4);
3356
+ if (minutes < 60)
3357
+ return `${minutes} min`;
3358
+ const hours = Math.floor(minutes / 60);
3359
+ const remMinutes = minutes % 60;
3360
+ if (hours < 24)
3361
+ return remMinutes > 0 ? `${hours}h ${remMinutes}m` : `${hours}h`;
3362
+ const days = Math.floor(hours / 24);
3363
+ const remHours = hours % 24;
3364
+ return remHours > 0 ? `${days}d ${remHours}h` : `${days}d`;
3365
+ }
3366
+ function stalePrefix(generatedAtMs, deliveredAtMs) {
3367
+ const ageMs = deliveredAtMs - generatedAtMs;
3368
+ if (ageMs <= STALE_ALERT_THRESHOLD_MS)
3369
+ return null;
3370
+ return `[stale \u2014 generated ${formatRelative(ageMs)} ago] `;
3371
+ }
3372
+ var STALE_ALERT_THRESHOLD_MS;
3373
+ var init_down_alert = __esm({
3374
+ "packages/core/dist/daemon/down-alert.js"() {
3375
+ STALE_ALERT_THRESHOLD_MS = 60 * 60 * 1e3;
3376
+ }
3377
+ });
3378
+
3293
3379
  // packages/core/dist/control-channel.js
3294
3380
  function fallsBackToPane(o) {
3295
3381
  return o.status === "gone" || o.status === "unsupported";
@@ -3442,12 +3528,20 @@ function ensureSocksDir(dir = CC_SOCKS_DIR) {
3442
3528
  if ((fs9.statSync(dir).mode & 511) !== 448)
3443
3529
  fs9.chmodSync(dir, 448);
3444
3530
  }
3445
- async function pollFirstTurnConfirmedAt(getTaskRecord, project, id) {
3446
- const deadline = Date.now() + FIRST_TURN_HOOK_CONFIRM_WINDOW_MS;
3531
+ async function pollFirstTurnConfirmedAt(getTaskRecord, project, id, scrapeSettled, cancel) {
3532
+ let scrapeSettledAt;
3533
+ const markSettled = () => {
3534
+ scrapeSettledAt ??= Date.now();
3535
+ };
3536
+ scrapeSettled.then(markSettled, markSettled);
3537
+ const hardDeadline = Date.now() + FIRST_TURN_HOOK_CONFIRM_MAX_MS;
3447
3538
  for (; ; ) {
3539
+ if (cancel.stopped)
3540
+ return false;
3448
3541
  const rec = await getTaskRecord(project, id).catch(() => void 0);
3449
3542
  if (rec?.firstTurnConfirmedAt)
3450
3543
  return true;
3544
+ const deadline = scrapeSettledAt !== void 0 ? Math.min(hardDeadline, scrapeSettledAt + FIRST_TURN_HOOK_CONFIRM_GRACE_MS) : hardDeadline;
3451
3545
  if (Date.now() >= deadline)
3452
3546
  return false;
3453
3547
  await new Promise((r) => setTimeout(r, FIRST_TURN_HOOK_POLL_INTERVAL_MS));
@@ -3629,7 +3723,9 @@ async function runCrewSpawn(input, config, deps) {
3629
3723
 
3630
3724
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
3631
3725
  const scrapeDelivered = sendPromise.then((r) => r.delivered).catch(() => false);
3632
- const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id)) : await scrapeDelivered;
3726
+ const cancelHookPoll = { stopped: false };
3727
+ const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id, scrapeDelivered, cancelHookPoll)) : await scrapeDelivered;
3728
+ cancelHookPoll.stopped = true;
3633
3729
  if (!delivered) {
3634
3730
  process.stderr.write(`\u26A0\uFE0F First turn not delivered for crew '${name}' \u2014 use 'squadrant crew send ${input.project} ${name}' to re-send the task.
3635
3731
  `);
@@ -3911,7 +4007,7 @@ async function runCrewList(project, runtime, workspaceId) {
3911
4007
  surfaceId: c.surfaceId
3912
4008
  }));
3913
4009
  }
3914
- var CC_SOCKS_DIR, FIRST_TURN_INLINE_MAX_BYTES, TEMPLATES_DIR, STATE_ROOT, FIRST_TURN_HOOK_CONFIRM_WINDOW_MS, FIRST_TURN_HOOK_POLL_INTERVAL_MS, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
4010
+ var CC_SOCKS_DIR, FIRST_TURN_INLINE_MAX_BYTES, TEMPLATES_DIR, STATE_ROOT, FIRST_TURN_HOOK_CONFIRM_GRACE_MS, FIRST_TURN_HOOK_CONFIRM_MAX_MS, FIRST_TURN_HOOK_POLL_INTERVAL_MS, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
3915
4011
  var init_crew_spawn = __esm({
3916
4012
  "packages/core/dist/crew-spawn.js"() {
3917
4013
  init_control_channel();
@@ -3923,7 +4019,8 @@ var init_crew_spawn = __esm({
3923
4019
  FIRST_TURN_INLINE_MAX_BYTES = 1200;
3924
4020
  TEMPLATES_DIR = path9.join(os4.homedir(), ".config", "squadrant", "templates");
3925
4021
  STATE_ROOT = path9.join(os4.homedir(), ".config", "squadrant", "state");
3926
- FIRST_TURN_HOOK_CONFIRM_WINDOW_MS = 1e5;
4022
+ FIRST_TURN_HOOK_CONFIRM_GRACE_MS = 15e3;
4023
+ FIRST_TURN_HOOK_CONFIRM_MAX_MS = 18e4;
3927
4024
  FIRST_TURN_HOOK_POLL_INTERVAL_MS = 2e3;
3928
4025
  CLOSE_LOOKUP_RETRIES = 3;
3929
4026
  CLOSE_LOOKUP_RETRY_DELAY_MS = 150;
@@ -4169,7 +4266,13 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4169
4266
  log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4170
4267
  }
4171
4268
  }
4172
- const result = await d.deliver(entry, async (text, sendOpts) => {
4269
+ let deliverEntry = entry;
4270
+ if (entry.kind === "captain.message" && entry.payload?.source === "daemon" && entry.message) {
4271
+ const prefix = stalePrefix(new Date(entry.ts).getTime(), Date.now());
4272
+ if (prefix)
4273
+ deliverEntry = { ...entry, message: `${prefix}${entry.message}` };
4274
+ }
4275
+ const result = await d.deliver(deliverEntry, async (text, sendOpts) => {
4173
4276
  let handledByChannel = false;
4174
4277
  try {
4175
4278
  const mode = ctx.captainChannelMode?.() ?? "off";
@@ -4261,6 +4364,7 @@ var init_delivery_loop = __esm({
4261
4364
  init_defer_delivery();
4262
4365
  init_dist();
4263
4366
  init_interactive_probe();
4367
+ init_down_alert();
4264
4368
  init_liveness2();
4265
4369
  init_captain_channel();
4266
4370
  CURSOR_SUBSCRIBER = "captain";
@@ -4695,8 +4799,8 @@ function startDaemon(ctx, opts, pkgVersion) {
4695
4799
  log(`boot pid=${process.pid} version=${pkgVersion} socket=${ctx.sockPath} stateRoot=${stateRoot}`);
4696
4800
  const bootTs = (/* @__PURE__ */ new Date()).toISOString();
4697
4801
  {
4698
- const sendDownAlert = (minutes, reasonText) => {
4699
- const text = `\u26A0\uFE0F daemon was down for ${minutes} min (last exit reason=${reasonText})`;
4802
+ const sendDownAlert = (startTs, endTs, minutes, reasonText) => {
4803
+ const text = formatDownAlertText({ startTs, endTs, minutes, reasonText });
4700
4804
  const alertProjects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4701
4805
  for (const project of alertProjects) {
4702
4806
  appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`boot-gap alert failed project=${project}: ${e.message}`));
@@ -4707,13 +4811,13 @@ function startDaemon(ctx, opts, pkgVersion) {
4707
4811
  if (marker) {
4708
4812
  log(`previous exit ts=${marker.ts} reason=${marker.reason} gap=${((gapMs ?? 0) / 1e3).toFixed(1)}s`);
4709
4813
  if ((gapMs ?? 0) > 6e4)
4710
- sendDownAlert(Math.round((gapMs ?? 0) / 6e4), marker.reason);
4814
+ sendDownAlert(marker.ts, bootTs, Math.round((gapMs ?? 0) / 6e4), marker.reason);
4711
4815
  } else if (prevRunning) {
4712
4816
  const lastHeartbeatMs = new Date(prevRunning.lastHeartbeatTs).getTime();
4713
4817
  const uncleanGapMs = Math.max(0, Date.now() - lastHeartbeatMs);
4714
4818
  log(`previous exit: UNCLEAN (no marker; last heartbeat ${prevRunning.lastHeartbeatTs}, gap=${(uncleanGapMs / 1e3).toFixed(1)}s)`);
4715
4819
  if (uncleanGapMs > 6e4) {
4716
- sendDownAlert(Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
4820
+ sendDownAlert(prevRunning.lastHeartbeatTs, bootTs, Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
4717
4821
  }
4718
4822
  } else {
4719
4823
  log("previous exit: none (clean or first boot)");
@@ -4835,6 +4939,7 @@ var init_start = __esm({
4835
4939
  init_server();
4836
4940
  init_mailbox();
4837
4941
  init_exit_marker();
4942
+ init_down_alert();
4838
4943
  init_liveness2();
4839
4944
  init_dist();
4840
4945
  init_snapshot_gather();
@@ -6804,6 +6909,7 @@ __export(dist_exports, {
6804
6909
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
6805
6910
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
6806
6911
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
6912
+ READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
6807
6913
  STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
6808
6914
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
6809
6915
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
@@ -6852,6 +6958,7 @@ __export(dist_exports, {
6852
6958
  crewTag: () => crewTag,
6853
6959
  daemonEntryPath: () => daemonEntryPath,
6854
6960
  daemonLockPath: () => daemonLockPath,
6961
+ decideForeignInstall: () => decideForeignInstall,
6855
6962
  decodeFrames: () => decodeFrames,
6856
6963
  defaultIsPidAlive: () => defaultIsPidAlive,
6857
6964
  defaultListenError: () => defaultListenError,
@@ -6893,9 +7000,11 @@ __export(dist_exports, {
6893
7000
  isDaemonSocketLive: () => isDaemonSocketLive,
6894
7001
  isNotifyActive: () => isNotifyActive,
6895
7002
  isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
7003
+ isReadOnlyCrewCommand: () => isReadOnlyCrewCommand,
6896
7004
  isSideTitle: () => isSideTitle,
6897
7005
  isStickyAttention: () => isStickyAttention,
6898
7006
  isTurnAccepted: () => isTurnAccepted,
7007
+ isVersionUpgrade: () => isVersionUpgrade,
6899
7008
  kickstartArgv: () => kickstartArgv,
6900
7009
  launchOneWorkspace: () => launchOneWorkspace,
6901
7010
  loadSessions: () => loadSessions,
@@ -6912,6 +7021,7 @@ __export(dist_exports, {
6912
7021
  parseProgramArgs: () => parseProgramArgs,
6913
7022
  plistPath: () => plistPath,
6914
7023
  printForeignInstallError: () => printForeignInstallError,
7024
+ printVersionUpgradeNotice: () => printVersionUpgradeNotice,
6915
7025
  programArgsBlock: () => programArgsBlock,
6916
7026
  projectHealth: () => projectHealth,
6917
7027
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
@@ -9809,9 +9919,12 @@ async function confirmedSendToPane(runtime, pane, message) {
9809
9919
  let sawDraft = await settleInputBox(runtime, pane);
9810
9920
  await runtime.sendKeyToPane(pane, "Enter");
9811
9921
  let repasted = false;
9922
+ let screenMoved = false;
9812
9923
  for (let attempt = 0; attempt < SUBMIT_RETRY_LIMIT; attempt++) {
9813
9924
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
9814
9925
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
9926
+ if (afterScreen !== preSendScreen)
9927
+ screenMoved = true;
9815
9928
  const draft = parseDraftFromScreen(afterScreen);
9816
9929
  if (draft !== "" && draft !== null)
9817
9930
  sawDraft = true;
@@ -9822,7 +9935,7 @@ async function confirmedSendToPane(runtime, pane, message) {
9822
9935
  const settled = await settleInputBox(runtime, pane);
9823
9936
  if (settled)
9824
9937
  sawDraft = true;
9825
- if (!sawDraft && !repasted) {
9938
+ if (!sawDraft && !repasted && !screenMoved) {
9826
9939
  repasted = true;
9827
9940
  await runtime.pasteToPane(pane, message);
9828
9941
  }