svamp-cli 0.2.336 → 0.2.337

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.
Files changed (26) hide show
  1. package/dist/{adminCommands-DaITgElz.mjs → adminCommands-i7zkVIFU.mjs} +1 -1
  2. package/dist/{agentCommands-BcdnoUr6.mjs → agentCommands-DmRCmbeU.mjs} +14 -9
  3. package/dist/{auth-9yoac3_n.mjs → auth-CXJGid9o.mjs} +1 -1
  4. package/dist/{cli-lUpt1gfZ.mjs → cli-v0NMiesq.mjs} +77 -77
  5. package/dist/cli.mjs +2 -2
  6. package/dist/{commands-CLDMHR24.mjs → commands-B8KlOrBS.mjs} +3 -3
  7. package/dist/{commands-i4024Ot3.mjs → commands-BRI0wxnE.mjs} +2 -2
  8. package/dist/{commands-CJ4_f3ok.mjs → commands-BeSB75OM.mjs} +3 -3
  9. package/dist/{commands-DsC3w_GZ.mjs → commands-Bn-JAlUf.mjs} +3 -3
  10. package/dist/{commands-CwiQbkMh.mjs → commands-DWNYEYRO.mjs} +1 -1
  11. package/dist/{commands-y3NpuM8E.mjs → commands-DZF_q6a0.mjs} +2 -2
  12. package/dist/{commands-BAoTIJjJ.mjs → commands-Dw_oiiqX.mjs} +1 -1
  13. package/dist/{commands-CEs0WPWz.mjs → commands-dYyv0P82.mjs} +11 -11
  14. package/dist/{fleet-BW-km2ZJ.mjs → fleet-Dion32w1.mjs} +1 -1
  15. package/dist/{headlessCli-BaBHCl_x.mjs → headlessCli-bB0pjxSC.mjs} +2 -2
  16. package/dist/index.mjs +1 -1
  17. package/dist/{notifyCommands-C428H5qy.mjs → notifyCommands-BVESEkQL.mjs} +1 -1
  18. package/dist/{package-B-GyIfBE.mjs → package-DBcAF326.mjs} +3 -3
  19. package/dist/{rpc-COwRfHss.mjs → rpc-BSWkkPHd.mjs} +1 -1
  20. package/dist/{rpc-DUP-qb40.mjs → rpc-Cj31v2sm.mjs} +1 -1
  21. package/dist/{run-DiwHqXsO.mjs → run-C6gfyMsM.mjs} +1 -1
  22. package/dist/{run-CBNIHWhE.mjs → run-bo0i02Dy.mjs} +252 -53
  23. package/dist/{scheduler-Cx3yWga4.mjs → scheduler-DkfqQjih.mjs} +1 -1
  24. package/dist/{serveCommands-nhyPDFIK.mjs → serveCommands-D2sKo0xC.mjs} +10 -10
  25. package/dist/{sideband-QYJM3m1p.mjs → sideband-D5Kan4J1.mjs} +1 -1
  26. package/package.json +3 -3
@@ -5521,27 +5521,63 @@ function sleepSync$1(ms) {
5521
5521
  } catch {
5522
5522
  }
5523
5523
  }
5524
+ function isProcessDead(pid) {
5525
+ try {
5526
+ process.kill(pid, 0);
5527
+ return false;
5528
+ } catch (e) {
5529
+ return e?.code === "ESRCH";
5530
+ }
5531
+ }
5524
5532
  function withFileLock(lockPath, fn, opts) {
5525
5533
  const deadlineMs = opts?.deadlineMs ?? 50;
5526
5534
  const staleMs = opts?.staleMs ?? 5e3;
5535
+ const trackOwner = opts?.breakOnDeadOwner ?? false;
5536
+ const pidPath = join$1(lockPath, "owner.pid");
5527
5537
  const deadline = Date.now() + deadlineMs;
5528
5538
  let held = false;
5529
5539
  while (Date.now() < deadline) {
5530
5540
  try {
5531
5541
  mkdirSync$1(lockPath);
5532
5542
  held = true;
5543
+ if (trackOwner) {
5544
+ try {
5545
+ writeFileSync$1(pidPath, String(process.pid));
5546
+ } catch {
5547
+ }
5548
+ }
5533
5549
  break;
5534
5550
  } catch {
5535
- try {
5536
- if (Date.now() - statSync(lockPath).mtimeMs > staleMs) {
5537
- rmSync$1(lockPath, { recursive: true, force: true });
5538
- continue;
5551
+ let broke = false;
5552
+ if (trackOwner) {
5553
+ try {
5554
+ const owner = Number(readFileSync(pidPath, "utf8"));
5555
+ if (Number.isInteger(owner) && owner > 0 && owner !== process.pid && isProcessDead(owner)) {
5556
+ rmSync$1(lockPath, { recursive: true, force: true });
5557
+ broke = true;
5558
+ }
5559
+ } catch {
5539
5560
  }
5540
- } catch {
5541
5561
  }
5562
+ if (!broke) {
5563
+ try {
5564
+ if (Date.now() - statSync(lockPath).mtimeMs > staleMs) {
5565
+ rmSync$1(lockPath, { recursive: true, force: true });
5566
+ broke = true;
5567
+ }
5568
+ } catch {
5569
+ }
5570
+ }
5571
+ if (broke) continue;
5542
5572
  sleepSync$1(5);
5543
5573
  }
5544
5574
  }
5575
+ if (!held) {
5576
+ try {
5577
+ opts?.onFailOpen?.(lockPath);
5578
+ } catch {
5579
+ }
5580
+ }
5545
5581
  try {
5546
5582
  return fn();
5547
5583
  } finally {
@@ -5554,6 +5590,33 @@ function withFileLock(lockPath, fn, opts) {
5554
5590
  }
5555
5591
  }
5556
5592
 
5593
+ function revGuardedMutate(lockPath, io, mutate, opts) {
5594
+ const deadlineMs = opts?.deadlineMs ?? 2e3;
5595
+ const maxAttempts = opts?.maxAttempts ?? 50;
5596
+ let last;
5597
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
5598
+ const settled = withFileLock(lockPath, () => {
5599
+ const base = io.read();
5600
+ const m = mutate(base.state);
5601
+ last = m.result;
5602
+ if (m.next === null) return { committed: true, result: m.result };
5603
+ if (opts?._seam) opts._seam(attempt);
5604
+ const again = io.read();
5605
+ if (again.rev !== base.rev) return { committed: false, result: m.result };
5606
+ io.write(m.next, base.rev + 1);
5607
+ return { committed: true, result: m.result };
5608
+ }, {
5609
+ deadlineMs,
5610
+ breakOnDeadOwner: true,
5611
+ onFailOpen: opts?.onFailOpen
5612
+ });
5613
+ last = settled.result;
5614
+ if (settled.committed) return settled.result;
5615
+ sleepSync$1(3 + attempt % 5);
5616
+ }
5617
+ return last;
5618
+ }
5619
+
5557
5620
  const HARD_MAX_BYTES = 25 * 1024 * 1024;
5558
5621
  const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
5559
5622
  const DEFAULT_MAX_COUNT = 100;
@@ -5805,6 +5868,23 @@ const _CHANNEL_CACHE_TTL_MS = 1e3;
5805
5868
  function invalidateChannelCache(dir) {
5806
5869
  _channelListCache.delete(dir);
5807
5870
  }
5871
+ function makeRmwSeam() {
5872
+ const delayMs = Number(process.env.SVAMP_TEST_RMW_DELAY_MS || 0);
5873
+ if (!delayMs || !Number.isFinite(delayMs)) return void 0;
5874
+ const readyFile = process.env.SVAMP_TEST_RMW_READY_FILE;
5875
+ let fired = false;
5876
+ return (attempt) => {
5877
+ if (attempt !== 0 || fired) return;
5878
+ fired = true;
5879
+ if (readyFile) {
5880
+ try {
5881
+ writeFileSync$1(readyFile, "1");
5882
+ } catch {
5883
+ }
5884
+ }
5885
+ sleepSync$1(delayMs);
5886
+ };
5887
+ }
5808
5888
  class ChannelStore {
5809
5889
  dir;
5810
5890
  constructor(projectDir) {
@@ -5818,12 +5898,18 @@ class ChannelStore {
5818
5898
  }
5819
5899
  _lock(id) {
5820
5900
  if (!isValidChannelId(id)) throw new Error(`invalid channel id: ${String(id).slice(0, 64)}`);
5901
+ mkdirSync$1(this.dir, { recursive: true });
5821
5902
  return join$1(this.dir, `${id}.json.lock`);
5822
5903
  }
5904
+ /** The default-filled canonical form of a channel (the minimal fields the store guarantees).
5905
+ * Extracted so `save()` can return the SAME shape it persists without re-running the write. */
5906
+ _applyDefaults(channel) {
5907
+ return { enabled: true, bind: "dynamic", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
5908
+ }
5823
5909
  // #0679: the actual validate + atomic write, WITHOUT the lock, so a locked RMW mutator can
5824
5910
  // reuse it inside its own lock section (no re-entrant re-lock / deadlock).
5825
5911
  _writeChannel(channel) {
5826
- const c = { enabled: true, bind: "dynamic", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
5912
+ const c = this._applyDefaults(channel);
5827
5913
  if (!c.id) c.id = genId();
5828
5914
  const errs = validateChannel(c);
5829
5915
  if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
@@ -5875,8 +5961,11 @@ class ChannelStore {
5875
5961
  }
5876
5962
  }
5877
5963
  save(channel) {
5878
- if (channel.id) return withFileLock(this._lock(channel.id), () => this._writeChannel(channel));
5879
- return this._writeChannel(channel);
5964
+ if (!channel.id) return this._writeChannel(channel);
5965
+ return this._rmw(channel.id, "save", () => {
5966
+ const merged = this._applyDefaults(channel);
5967
+ return { next: merged, result: merged };
5968
+ });
5880
5969
  }
5881
5970
  remove(id) {
5882
5971
  return withFileLock(this._lock(id), () => {
@@ -5887,41 +5976,85 @@ class ChannelStore {
5887
5976
  return true;
5888
5977
  }
5889
5978
  return false;
5979
+ }, {
5980
+ deadlineMs: 2e3,
5981
+ breakOnDeadOwner: true,
5982
+ onFailOpen: () => console.warn(`[channels] remove(${id}): lock contended for 2000ms \u2014 proceeding WITHOUT it`)
5890
5983
  });
5891
5984
  }
5892
- // #0679: setEnabled/recordCall/addCaller are read-modify-write mutators — multiple ChannelStore
5893
- // instances (one per session) point at the same .svamp/channels/<id>.json, so without
5894
- // serialization two concurrent RMWs both read the same base and the second save() clobbers the
5895
- // first (addCaller silently DROPS a freshly-generated caller key). Hold the bounded fail-open
5896
- // per-channel lock across the whole read→mutate→write so the read sees the other's committed
5897
- // change (mirrors inboxGuard's #0625 withAwaitLock). _writeChannel is the un-locked write.
5985
+ // #0679: setEnabled/recordCall/addCaller/removeCaller are read-modify-write mutators — multiple
5986
+ // ChannelStore instances (one per session, plus the CLI) point at the same
5987
+ // .svamp/channels/<id>.json, so without serialization two concurrent RMWs both read the same base
5988
+ // and the second write clobbers the first (addCaller silently DROPS a freshly-generated caller
5989
+ // key; removeCaller silently RESURRECTS a revoked one a SECURITY outcome).
5990
+ /**
5991
+ * #1226: the STRUCTURAL fix for those lost updates — a rev-guarded read-modify-write.
5992
+ *
5993
+ * withFileLock is bounded and FAIL-OPEN by design: on timeout it runs the mutation WITHOUT the
5994
+ * lock rather than hanging the daemon's single thread. That fail-open is a genuine lost-update
5995
+ * window, and merely widening the deadline (the earlier 50→2000 change) only shrinks the
5996
+ * PROBABILITY proportional to N×holdTime — the child's barrier harness reproduced 36-of-48
5997
+ * mutations lost at deadline=2000/hold=50/N=48. `revGuardedMutate` instead embeds a monotonic
5998
+ * `_rev`: it reads the base at rev R, applies the mutation, RE-READS immediately before writing,
5999
+ * and if the rev has advanced (a concurrent commit landed in its window) it retries against the
6000
+ * fresh base rather than clobbering. When the lock genuinely holds this is airtight; when it
6001
+ * fails open it collapses the loss window to the sub-millisecond re-read→rename gap (see
6002
+ * util/revLock.ts for the honest residual). `breakOnDeadOwner` (enabled inside revLock) also
6003
+ * closes the #1228 gap where a crashed holder's lock was never broken because deadlineMs(2000)
6004
+ * < staleMs(5000) — safe precisely because the rev guard tolerates the transient double-writer.
6005
+ *
6006
+ * recordCall uses a SHORT deadline + few attempts: it runs on every channel send (a hot path)
6007
+ * and all it can lose is one entry of a 20-deep rolling log, so it must not block the loop — but
6008
+ * it is still rev-guarded, so it no longer silently drops a log entry to a concurrent mutator
6009
+ * when it does acquire.
6010
+ */
6011
+ _rmw(id, op, mutate, opts) {
6012
+ return revGuardedMutate(
6013
+ this._lock(id),
6014
+ {
6015
+ read: () => {
6016
+ const c = this.get(id);
6017
+ return { state: c, rev: c?._rev ?? 0 };
6018
+ },
6019
+ write: (state, rev) => {
6020
+ state._rev = rev;
6021
+ this._writeChannel(state);
6022
+ }
6023
+ },
6024
+ mutate,
6025
+ {
6026
+ deadlineMs: opts?.deadlineMs ?? 2e3,
6027
+ maxAttempts: opts?.maxAttempts,
6028
+ onFailOpen: () => {
6029
+ console.warn(`[channels] ${op}(${id}): lock contended \u2014 proceeding WITHOUT it (rev-guarded); a concurrent update is still highly unlikely to be lost`);
6030
+ },
6031
+ _seam: makeRmwSeam()
6032
+ }
6033
+ );
6034
+ }
5898
6035
  setEnabled(id, enabled) {
5899
- return withFileLock(this._lock(id), () => {
5900
- const c = this.get(id);
5901
- if (!c) return null;
6036
+ return this._rmw(id, "setEnabled", (c) => {
6037
+ if (!c) return { next: null, result: null };
5902
6038
  c.enabled = enabled;
5903
- return this._writeChannel(c);
6039
+ return { next: c, result: c };
5904
6040
  });
5905
6041
  }
5906
6042
  recordCall(id, entry) {
5907
- withFileLock(this._lock(id), () => {
5908
- const c = this.get(id);
5909
- if (!c) return;
6043
+ this._rmw(id, "recordCall", (c) => {
6044
+ if (!c) return { next: null, result: void 0 };
5910
6045
  c.last_calls = c.last_calls || [];
5911
6046
  c.last_calls.unshift({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
5912
6047
  c.last_calls = c.last_calls.slice(0, 20);
5913
- this._writeChannel(c);
5914
- });
6048
+ return { next: c, result: void 0 };
6049
+ }, { deadlineMs: 200, maxAttempts: 3 });
5915
6050
  }
5916
6051
  addCaller(id, name, kind = "agent") {
5917
- return withFileLock(this._lock(id), () => {
5918
- const c = this.get(id);
5919
- if (!c) return null;
6052
+ return this._rmw(id, "addCaller", (c) => {
6053
+ if (!c) return { next: null, result: null };
5920
6054
  c.identity.callers = c.identity.callers || [];
5921
6055
  const caller = { name, kind, key: genKey() };
5922
6056
  c.identity.callers.push(caller);
5923
- this._writeChannel(c);
5924
- return caller;
6057
+ return { next: c, result: caller };
5925
6058
  });
5926
6059
  }
5927
6060
  /**
@@ -5936,14 +6069,12 @@ class ChannelStore {
5936
6069
  * resurrect the revoked entry by writing a stale base. Returns true when a caller was removed.
5937
6070
  */
5938
6071
  removeCaller(id, name) {
5939
- return withFileLock(this._lock(id), () => {
5940
- const c = this.get(id);
5941
- if (!c?.identity?.callers) return false;
6072
+ return this._rmw(id, "removeCaller", (c) => {
6073
+ if (!c?.identity?.callers) return { next: null, result: false };
5942
6074
  const before = c.identity.callers.length;
5943
6075
  c.identity.callers = c.identity.callers.filter((x) => x.name !== name);
5944
- if (c.identity.callers.length === before) return false;
5945
- this._writeChannel(c);
5946
- return true;
6076
+ if (c.identity.callers.length === before) return { next: null, result: false };
6077
+ return { next: c, result: true };
5947
6078
  });
5948
6079
  }
5949
6080
  }
@@ -8378,7 +8509,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8378
8509
  }
8379
8510
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
8380
8511
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
8381
- const { toolsForRole } = await import('./sideband-QYJM3m1p.mjs');
8512
+ const { toolsForRole } = await import('./sideband-D5Kan4J1.mjs');
8382
8513
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
8383
8514
  return fmt(r2);
8384
8515
  }
@@ -8510,7 +8641,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8510
8641
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
8511
8642
  }
8512
8643
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
8513
- const { queryCore } = await import('./commands-BAoTIJjJ.mjs');
8644
+ const { queryCore } = await import('./commands-Dw_oiiqX.mjs');
8514
8645
  const timeout = c.reply?.timeout_sec || 120;
8515
8646
  let result;
8516
8647
  let thrownSessionId;
@@ -18610,6 +18741,26 @@ function decideCompactUnsupported(input) {
18610
18741
  function describeNoBoundary(strikes, limit) {
18611
18742
  return `Automatic compaction ran but reported no compaction boundary (attempt ${strikes} of ${limit}) \u2014 usually because there was nothing left to compact. Keeping automatic compaction enabled.`;
18612
18743
  }
18744
+ function shouldAccountLoopTurn(state) {
18745
+ if (state.justCompacted) return false;
18746
+ if (state.compactPending) return false;
18747
+ return true;
18748
+ }
18749
+ function shouldPersistObservedContext(state) {
18750
+ if (state.justCompacted) return false;
18751
+ if (state.compactPending) return false;
18752
+ if (state.turnErrored) return false;
18753
+ return true;
18754
+ }
18755
+ function postCompactionObservedContext(compactMetadata) {
18756
+ const md = compactMetadata;
18757
+ if (!md || typeof md !== "object") return 0;
18758
+ for (const key of ["post_tokens", "postTokens", "post_token_count", "tokens_after", "after_tokens"]) {
18759
+ const v = md[key];
18760
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return Math.floor(v);
18761
+ }
18762
+ return 0;
18763
+ }
18613
18764
 
18614
18765
  function fmtTokens(n) {
18615
18766
  return Math.max(0, Math.round(n)).toLocaleString("en-US");
@@ -20741,7 +20892,7 @@ async function startDaemon(options) {
20741
20892
  try {
20742
20893
  const dir = loadSessionIndex()[sessionId]?.directory;
20743
20894
  if (!dir) return;
20744
- const { reconcileServiceLinks } = await import('./agentCommands-BcdnoUr6.mjs');
20895
+ const { reconcileServiceLinks } = await import('./agentCommands-DmRCmbeU.mjs');
20745
20896
  const configPath = getSvampConfigPath(dir, sessionId);
20746
20897
  const config = readSvampConfig(configPath);
20747
20898
  const entries = Array.from(urls.entries());
@@ -20763,7 +20914,7 @@ async function startDaemon(options) {
20763
20914
  try {
20764
20915
  const dir = loadSessionIndex()[sessionId]?.directory;
20765
20916
  if (!dir) return;
20766
- const { reconcileServiceLinks } = await import('./agentCommands-BcdnoUr6.mjs');
20917
+ const { reconcileServiceLinks } = await import('./agentCommands-DmRCmbeU.mjs');
20767
20918
  const configPath = getSvampConfigPath(dir, sessionId);
20768
20919
  const config = readSvampConfig(configPath);
20769
20920
  const incoming = [{
@@ -20784,7 +20935,7 @@ async function startDaemon(options) {
20784
20935
  try {
20785
20936
  const dir = loadSessionIndex()[sessionId]?.directory;
20786
20937
  if (!dir) return;
20787
- const { dropServiceLinks } = await import('./agentCommands-BcdnoUr6.mjs');
20938
+ const { dropServiceLinks } = await import('./agentCommands-DmRCmbeU.mjs');
20788
20939
  const configPath = getSvampConfigPath(dir, sessionId);
20789
20940
  const config = readSvampConfig(configPath);
20790
20941
  if (dropServiceLinks(config, "serve", mountName)) {
@@ -20800,7 +20951,7 @@ async function startDaemon(options) {
20800
20951
  try {
20801
20952
  const dir = loadSessionIndex()[sessionId]?.directory;
20802
20953
  if (!dir) return;
20803
- const { dropServiceLinks } = await import('./agentCommands-BcdnoUr6.mjs');
20954
+ const { dropServiceLinks } = await import('./agentCommands-DmRCmbeU.mjs');
20804
20955
  const configPath = getSvampConfigPath(dir, sessionId);
20805
20956
  const config = readSvampConfig(configPath);
20806
20957
  if (dropServiceLinks(config, "tunnel", tunnelName)) {
@@ -20812,9 +20963,12 @@ async function startDaemon(options) {
20812
20963
  }
20813
20964
  }
20814
20965
  let liveSessionDirs;
20966
+ let serviceRegistriesRestored = false;
20967
+ const managedLinkSessions = /* @__PURE__ */ new Set();
20968
+ let managedLinkSessionsSeeded = false;
20815
20969
  async function reconcileAllServiceLinks() {
20816
20970
  try {
20817
- const { reconcileServiceLinksExact } = await import('./agentCommands-BcdnoUr6.mjs');
20971
+ const { reconcileServiceLinksExact, hasManagedServiceLink } = await import('./agentCommands-DmRCmbeU.mjs');
20818
20972
  const desiredBySession = /* @__PURE__ */ new Map();
20819
20973
  const push = (sessionId, entry) => {
20820
20974
  if (!sessionId || !entry.url) return;
@@ -20842,7 +20996,19 @@ async function startDaemon(options) {
20842
20996
  for (const s of liveSessionDirs?.() ?? []) {
20843
20997
  if (s.sessionId && s.directory) dirBySession.set(s.sessionId, s.directory);
20844
20998
  }
20845
- const sessionIds = /* @__PURE__ */ new Set([...dirBySession.keys(), ...desiredBySession.keys()]);
20999
+ if (!managedLinkSessionsSeeded) {
21000
+ for (const [sessionId, dir] of dirBySession) {
21001
+ try {
21002
+ if (hasManagedServiceLink(readSvampConfig(getSvampConfigPath(dir, sessionId)), ["serve", "tunnel"])) {
21003
+ managedLinkSessions.add(sessionId);
21004
+ }
21005
+ } catch {
21006
+ }
21007
+ }
21008
+ managedLinkSessionsSeeded = true;
21009
+ logger.log(`[launchpad] Seeded managed-link set: ${managedLinkSessions.size} of ${dirBySession.size} known sessions carry a serve/tunnel link`);
21010
+ }
21011
+ const sessionIds = /* @__PURE__ */ new Set([...managedLinkSessions, ...desiredBySession.keys()]);
20846
21012
  for (const sessionId of sessionIds) {
20847
21013
  try {
20848
21014
  const dir = dirBySession.get(sessionId);
@@ -20850,9 +21016,14 @@ async function startDaemon(options) {
20850
21016
  const configPath = getSvampConfigPath(dir, sessionId);
20851
21017
  const config = readSvampConfig(configPath);
20852
21018
  const desired = desiredBySession.get(sessionId) ?? [];
20853
- if (reconcileServiceLinksExact(config, ["serve", "tunnel"], desired)) {
21019
+ if (reconcileServiceLinksExact(config, ["serve", "tunnel"], desired, { dropStale: serviceRegistriesRestored })) {
20854
21020
  writeSvampConfig(configPath, config);
20855
- logger.log(`[launchpad] Reconciled service links for session ${sessionId} (${desired.length} live)`);
21021
+ logger.log(`[launchpad] Reconciled service links for session ${sessionId} (${desired.length} live${serviceRegistriesRestored ? "" : ", upsert-only \u2014 restore in flight"})`);
21022
+ }
21023
+ if (desired.length > 0 || hasManagedServiceLink(config, ["serve", "tunnel"])) {
21024
+ managedLinkSessions.add(sessionId);
21025
+ } else {
21026
+ managedLinkSessions.delete(sessionId);
20856
21027
  }
20857
21028
  } catch (err) {
20858
21029
  logger.log(`[launchpad] Reconcile failed for session ${sessionId}: ${err?.message || err}`);
@@ -21848,6 +22019,7 @@ ${parts.join("\n")}`);
21848
22019
  let autoCompactDisabled = false;
21849
22020
  let autoCompactRecoveryTried = false;
21850
22021
  let justCompactedTurn = false;
22022
+ let compactTurnLatch = false;
21851
22023
  let compactNoBoundaryStrikes = 0;
21852
22024
  let compactSupported = void 0;
21853
22025
  let noIsolationWarned = false;
@@ -22340,11 +22512,17 @@ ${parts.join("\n")}`);
22340
22512
  currentWindow: sessionMetadata.contextWindow
22341
22513
  }), maxContextWindow);
22342
22514
  const observedThisTurn = computeObservedContext(msg.usage);
22343
- if (resolvedWindow > 0 && resolvedWindow !== sessionMetadata.contextWindow || observedThisTurn > 0 && observedThisTurn !== sessionMetadata.observedContext) {
22515
+ const mayPersistObserved = shouldPersistObservedContext({
22516
+ justCompacted: justCompactedTurn,
22517
+ compactPending: compactTurnLatch,
22518
+ turnErrored: !!msg.is_error
22519
+ });
22520
+ const persistObserved = mayPersistObserved && observedThisTurn > 0 && observedThisTurn !== sessionMetadata.observedContext;
22521
+ if (resolvedWindow > 0 && resolvedWindow !== sessionMetadata.contextWindow || persistObserved) {
22344
22522
  sessionMetadata = {
22345
22523
  ...sessionMetadata,
22346
22524
  ...resolvedWindow > 0 ? { contextWindow: resolvedWindow } : {},
22347
- ...observedThisTurn > 0 ? { observedContext: observedThisTurn } : {}
22525
+ ...persistObserved ? { observedContext: observedThisTurn } : {}
22348
22526
  };
22349
22527
  sessionService.updateMetadata(sessionMetadata);
22350
22528
  }
@@ -22365,7 +22543,11 @@ ${parts.join("\n")}`);
22365
22543
  clearInboundContext(sessionId);
22366
22544
  try {
22367
22545
  const ls = readLoopState(directory, sessionId);
22368
- if (ls && ls.engine === "goal" && ls.active !== false && ls.phase !== "dormant" && ls.phase !== "done" && ls.phase !== "gave_up" && ls.phase !== "cancelled") {
22546
+ const accountLoopTurn = shouldAccountLoopTurn({
22547
+ justCompacted: justCompactedTurn,
22548
+ compactPending: compactTurnLatch
22549
+ });
22550
+ if (accountLoopTurn && ls && ls.engine === "goal" && ls.active !== false && ls.phase !== "dormant" && ls.phase !== "done" && ls.phase !== "gave_up" && ls.phase !== "cancelled") {
22369
22551
  const now = Date.now();
22370
22552
  const ledger = accumulateLedger(ls.ledger, {
22371
22553
  turns: Number(msg.num_turns) || 0,
@@ -22422,6 +22604,7 @@ ${parts.join("\n")}`);
22422
22604
  enabled: autoCompactEnabled
22423
22605
  });
22424
22606
  justCompactedTurn = false;
22607
+ compactTurnLatch = false;
22425
22608
  if (mayEvaluateLayer1) {
22426
22609
  try {
22427
22610
  const observed = computeObservedContext(msg.usage);
@@ -22541,7 +22724,16 @@ ${parts.join("\n")}`);
22541
22724
  autoCompactWasAutoInjected = false;
22542
22725
  autoCompactRecoveryTried = false;
22543
22726
  justCompactedTurn = true;
22727
+ compactTurnLatch = true;
22544
22728
  compactNoBoundaryStrikes = 0;
22729
+ try {
22730
+ const postObserved = postCompactionObservedContext(msg.compact_metadata);
22731
+ if (sessionMetadata.observedContext !== postObserved) {
22732
+ sessionMetadata = { ...sessionMetadata, observedContext: postObserved };
22733
+ sessionService.updateMetadata(sessionMetadata);
22734
+ }
22735
+ } catch {
22736
+ }
22545
22737
  const compactLine = describeCompaction(msg.compact_metadata);
22546
22738
  logger.log(`[Session ${sessionId}] ${compactLine}`);
22547
22739
  sessionService.pushMessage(
@@ -22970,6 +23162,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22970
23162
  rateLimitRetryTimer = null;
22971
23163
  }
22972
23164
  pendingCompactTurn = isCompactCommand(text);
23165
+ if (pendingCompactTurn) compactTurnLatch = true;
22973
23166
  if (!claudeProcess || claudeProcess.exitCode !== null) {
22974
23167
  const preTurnWindow = applyMaxContextWindow(sessionMetadata.contextWindow || 0, maxContextWindow);
22975
23168
  const preTurnObserved = sessionMetadata.observedContext || 0;
@@ -22994,6 +23187,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22994
23187
  };
22995
23188
  sessionService.updateMetadata(sessionMetadata);
22996
23189
  pendingCompactTurn = true;
23190
+ compactTurnLatch = true;
22997
23191
  autoCompactWasAutoInjected = true;
22998
23192
  spawnClaude("/compact", msgMeta);
22999
23193
  signalProcessing(true);
@@ -23387,11 +23581,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
23387
23581
  });
23388
23582
  },
23389
23583
  onIssue: async (params) => {
23390
- const { issueRpc } = await import('./rpc-DUP-qb40.mjs');
23584
+ const { issueRpc } = await import('./rpc-Cj31v2sm.mjs');
23391
23585
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
23392
23586
  },
23393
23587
  onWorkflow: async (params) => {
23394
- const { workflowRpc } = await import('./rpc-COwRfHss.mjs');
23588
+ const { workflowRpc } = await import('./rpc-BSWkkPHd.mjs');
23395
23589
  return workflowRpc(params?.cwd || directory, params || {});
23396
23590
  },
23397
23591
  onRipgrep: async (args, cwd) => {
@@ -23531,6 +23725,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
23531
23725
  turnInitiatedByUser = true;
23532
23726
  currentTurnMessage = next.text;
23533
23727
  pendingCompactTurn = isCompactCommand(next.text);
23728
+ if (pendingCompactTurn) compactTurnLatch = true;
23534
23729
  autoCompactWasAutoInjected = pendingCompactTurn && !!next.autoCompact;
23535
23730
  resetRateLimitStreak();
23536
23731
  if (rateLimitRetryTimer) {
@@ -24114,11 +24309,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
24114
24309
  });
24115
24310
  },
24116
24311
  onIssue: async (params) => {
24117
- const { issueRpc } = await import('./rpc-DUP-qb40.mjs');
24312
+ const { issueRpc } = await import('./rpc-Cj31v2sm.mjs');
24118
24313
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
24119
24314
  },
24120
24315
  onWorkflow: async (params) => {
24121
- const { workflowRpc } = await import('./rpc-COwRfHss.mjs');
24316
+ const { workflowRpc } = await import('./rpc-BSWkkPHd.mjs');
24122
24317
  return workflowRpc(params?.cwd || directory, params || {});
24123
24318
  },
24124
24319
  onRipgrep: async (args, cwd) => {
@@ -25119,8 +25314,12 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
25119
25314
  } catch (err) {
25120
25315
  logger.log(`[serve] mount link reconcile failed: ${err?.message || err}`);
25121
25316
  }
25317
+ serviceRegistriesRestored = true;
25122
25318
  scheduleServiceLinkReconcile(3e3);
25123
- }).catch((err) => logger.error(`[serve] mount restore failed: ${err?.message || err}`));
25319
+ }).catch((err) => {
25320
+ serviceRegistriesRestored = true;
25321
+ logger.error(`[serve] mount restore failed: ${err?.message || err}`);
25322
+ });
25124
25323
  const daemonOwnerEmail = parseJwtEmail(process.env.HYPHA_TOKEN || "") || null;
25125
25324
  serveManager$1.setSessionResolver((sessionId) => {
25126
25325
  for (const [, session] of pidToTrackedSession) {
@@ -25469,7 +25668,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
25469
25668
  const PING_TIMEOUT_MS = 15e3;
25470
25669
  const POST_RECONNECT_GRACE_MS = 2e4;
25471
25670
  const RECONNECT_JITTER_MS = 2500;
25472
- const { WorkflowScheduler } = await import('./scheduler-Cx3yWga4.mjs');
25671
+ const { WorkflowScheduler } = await import('./scheduler-DkfqQjih.mjs');
25473
25672
  const workflowProjectRoots = () => {
25474
25673
  const dirs = /* @__PURE__ */ new Set();
25475
25674
  for (const s of pidToTrackedSession.values()) {
@@ -1,4 +1,4 @@
1
- import { f as resolveProjectRoot, E as listWorkflows, F as isWorkflowEnabled, G as workflowSchedules, H as inZone, x as runWorkflow, I as cronMatches } from './run-CBNIHWhE.mjs';
1
+ import { f as resolveProjectRoot, E as listWorkflows, F as isWorkflowEnabled, G as workflowSchedules, H as inZone, x as runWorkflow, I as cronMatches } from './run-bo0i02Dy.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -1,6 +1,6 @@
1
1
  import * as path from 'path';
2
- import { w as wantsHelp } from './cli-lUpt1gfZ.mjs';
3
- import './run-CBNIHWhE.mjs';
2
+ import { w as wantsHelp } from './cli-v0NMiesq.mjs';
3
+ import './run-bo0i02Dy.mjs';
4
4
  import 'os';
5
5
  import 'fs/promises';
6
6
  import 'fs';
@@ -77,7 +77,7 @@ async function handleServeCommand() {
77
77
  }
78
78
  }
79
79
  async function serveAdd(args, machineId) {
80
- const { connectAndGetMachine } = await import('./commands-BAoTIJjJ.mjs');
80
+ const { connectAndGetMachine } = await import('./commands-Dw_oiiqX.mjs');
81
81
  const pos = positionalArgs(args);
82
82
  const name = pos[0];
83
83
  if (!name) {
@@ -110,7 +110,7 @@ async function serveAdd(args, machineId) {
110
110
  }
111
111
  if (sessionId && result?.url) {
112
112
  try {
113
- const { autoAddSessionLink } = await import('./agentCommands-BcdnoUr6.mjs');
113
+ const { autoAddSessionLink } = await import('./agentCommands-DmRCmbeU.mjs');
114
114
  autoAddSessionLink(String(result.url), name, void 0, { kind: "serve", name });
115
115
  } catch {
116
116
  }
@@ -124,7 +124,7 @@ async function serveAdd(args, machineId) {
124
124
  }
125
125
  }
126
126
  async function serveApply(args, machineId) {
127
- const { connectAndGetMachine } = await import('./commands-BAoTIJjJ.mjs');
127
+ const { connectAndGetMachine } = await import('./commands-Dw_oiiqX.mjs');
128
128
  const fs = await import('fs');
129
129
  const yaml = await import('yaml');
130
130
  const file = positionalArgs(args)[0];
@@ -209,7 +209,7 @@ async function serveApply(args, machineId) {
209
209
  console.log(`URL: ${result.url}`);
210
210
  if (params.sessionId && result?.url) {
211
211
  try {
212
- const { autoAddSessionLink } = await import('./agentCommands-BcdnoUr6.mjs');
212
+ const { autoAddSessionLink } = await import('./agentCommands-DmRCmbeU.mjs');
213
213
  const prevSession = process.env.SVAMP_SESSION_ID;
214
214
  process.env.SVAMP_SESSION_ID = String(params.sessionId);
215
215
  try {
@@ -230,7 +230,7 @@ async function serveApply(args, machineId) {
230
230
  }
231
231
  }
232
232
  async function serveRemove(args, machineId) {
233
- const { connectAndGetMachine } = await import('./commands-BAoTIJjJ.mjs');
233
+ const { connectAndGetMachine } = await import('./commands-Dw_oiiqX.mjs');
234
234
  const pos = positionalArgs(args);
235
235
  const name = pos[0];
236
236
  if (!name) {
@@ -240,7 +240,7 @@ async function serveRemove(args, machineId) {
240
240
  const { machine, server } = await connectAndGetMachine(machineId);
241
241
  try {
242
242
  await machine.serveRemove({ name });
243
- const { removeSessionLinkByService } = await import('./agentCommands-BcdnoUr6.mjs');
243
+ const { removeSessionLinkByService } = await import('./agentCommands-DmRCmbeU.mjs');
244
244
  removeSessionLinkByService("serve", name);
245
245
  console.log(`Mount '${name}' removed.`);
246
246
  } catch (err) {
@@ -252,7 +252,7 @@ async function serveRemove(args, machineId) {
252
252
  }
253
253
  }
254
254
  async function serveList(args, machineId) {
255
- const { connectAndGetMachine } = await import('./commands-BAoTIJjJ.mjs');
255
+ const { connectAndGetMachine } = await import('./commands-Dw_oiiqX.mjs');
256
256
  const all = hasFlag(args, "--all", "-a");
257
257
  const json = hasFlag(args, "--json");
258
258
  const sessionId = getFlag(args, "--session");
@@ -286,7 +286,7 @@ async function serveList(args, machineId) {
286
286
  }
287
287
  }
288
288
  async function serveInfo(machineId) {
289
- const { connectAndGetMachine } = await import('./commands-BAoTIJjJ.mjs');
289
+ const { connectAndGetMachine } = await import('./commands-Dw_oiiqX.mjs');
290
290
  const { machine, server } = await connectAndGetMachine(machineId);
291
291
  try {
292
292
  const info = await machine.serveInfo();
@@ -1,4 +1,4 @@
1
- import { R as READ_ONLY_TOOLS, P as loadMachineContext, Q as buildMachineInstructions, T as machineToolsForRole, U as buildMachineTools } from './run-CBNIHWhE.mjs';
1
+ import { R as READ_ONLY_TOOLS, P as loadMachineContext, Q as buildMachineInstructions, T as machineToolsForRole, U as buildMachineTools } from './run-bo0i02Dy.mjs';
2
2
  import 'node:child_process';
3
3
  import 'os';
4
4
  import 'fs/promises';