agent-shim 8.13.0 → 8.14.1

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/dist/cli.cjs CHANGED
@@ -61710,7 +61710,7 @@ function registerIdentityCommand(program2, deps) {
61710
61710
  // package.json
61711
61711
  var package_default = {
61712
61712
  name: "agent-shim",
61713
- version: "8.13.0",
61713
+ version: "8.14.1",
61714
61714
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
61715
61715
  license: "MIT",
61716
61716
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -65063,6 +65063,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
65063
65063
  ...lastDirectPort === void 0 ? {} : { lastDirectPort }
65064
65064
  });
65065
65065
  const fail = (message) => {
65066
+ ports.closeRcStreamHub();
65066
65067
  writeFrontDoorState(fs19, paths.frontdoorStateFile, { lastError: message, ...sticky() });
65067
65068
  fs19.removeRecursive(paths.frontdoorLockFile);
65068
65069
  ports.log(`agent-shim frontdoor supervisor: ${message}`);
@@ -65123,6 +65124,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
65123
65124
  await direct.close();
65124
65125
  await connect3.close();
65125
65126
  await http6.close();
65127
+ ports.closeRcStreamHub();
65126
65128
  writeFrontDoorState(fs19, paths.frontdoorStateFile, sticky());
65127
65129
  return 0;
65128
65130
  }
@@ -71793,31 +71795,54 @@ function credentialOfJson(parsed) {
71793
71795
  anthropicClientPlatform: typeof anthropicClientPlatform === "string" ? anthropicClientPlatform : void 0
71794
71796
  };
71795
71797
  }
71798
+ function sequenceNumOfJson(value2) {
71799
+ return typeof value2 === "number" && Number.isInteger(value2) && value2 >= 0 ? value2 : void 0;
71800
+ }
71796
71801
  function createRcCredentialStore(fs19, dir) {
71797
71802
  fs19.mkdirPrivate(dir);
71798
71803
  const fileFor = (sessionId) => import_node_path32.default.join(dir, `${sessionId}.json`);
71804
+ const readParsed = (sessionId) => {
71805
+ const raw = fs19.readFileUtf8(fileFor(sessionId));
71806
+ if (raw === void 0) {
71807
+ return void 0;
71808
+ }
71809
+ let parsed;
71810
+ try {
71811
+ parsed = JSON.parse(raw);
71812
+ } catch {
71813
+ return void 0;
71814
+ }
71815
+ return isRecord4(parsed) ? parsed : void 0;
71816
+ };
71799
71817
  return {
71800
71818
  read: (sessionId) => {
71801
71819
  if (!isSafeSessionId(sessionId)) {
71802
71820
  return void 0;
71803
71821
  }
71804
- const raw = fs19.readFileUtf8(fileFor(sessionId));
71805
- if (raw === void 0) {
71806
- return void 0;
71822
+ return credentialOfJson(readParsed(sessionId));
71823
+ },
71824
+ write: (sessionId, credential) => {
71825
+ if (!isSafeSessionId(sessionId)) {
71826
+ return;
71807
71827
  }
71808
- let parsed;
71809
- try {
71810
- parsed = JSON.parse(raw);
71811
- } catch {
71828
+ const cursor3 = sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
71829
+ const record2 = { ...credential, ...cursor3 === void 0 ? {} : { highestSequenceNum: cursor3 } };
71830
+ fs19.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record2, null, 2)}
71831
+ `);
71832
+ },
71833
+ readCursor: (sessionId) => {
71834
+ if (!isSafeSessionId(sessionId)) {
71812
71835
  return void 0;
71813
71836
  }
71814
- return credentialOfJson(parsed);
71837
+ return sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
71815
71838
  },
71816
- write: (sessionId, credential) => {
71839
+ writeCursor: (sessionId, highestSequenceNum) => {
71817
71840
  if (!isSafeSessionId(sessionId)) {
71818
71841
  return;
71819
71842
  }
71820
- fs19.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(credential, null, 2)}
71843
+ const credential = credentialOfJson(readParsed(sessionId));
71844
+ const record2 = credential === void 0 ? { highestSequenceNum } : { ...credential, highestSequenceNum };
71845
+ fs19.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record2, null, 2)}
71821
71846
  `);
71822
71847
  },
71823
71848
  remove: (sessionId) => {
@@ -72643,6 +72668,18 @@ function createRcEventFanout() {
72643
72668
  }
72644
72669
  function createRcStreamHub(deps) {
72645
72670
  const attachments = /* @__PURE__ */ new Map();
72671
+ const lastSavedSequenceNums = /* @__PURE__ */ new Map();
72672
+ const persistCursor = (sessionId) => {
72673
+ if (deps.saveSequenceNum === void 0) {
72674
+ return;
72675
+ }
72676
+ const cursor3 = deps.sequenceNumOf(sessionId);
72677
+ if (cursor3 === void 0 || cursor3 === lastSavedSequenceNums.get(sessionId)) {
72678
+ return;
72679
+ }
72680
+ deps.saveSequenceNum(sessionId, cursor3);
72681
+ lastSavedSequenceNums.set(sessionId, cursor3);
72682
+ };
72646
72683
  const birth = (sessionId) => {
72647
72684
  const controller = new AbortController();
72648
72685
  let stopped = false;
@@ -72679,7 +72716,7 @@ function createRcStreamHub(deps) {
72679
72716
  if (!isSuccessful3(presence.status)) {
72680
72717
  throw new Error(`the presence call answered HTTP ${String(presence.status)}: ${presence.body}`);
72681
72718
  }
72682
- const cursor3 = deps.sequenceNumOf(attachment.sessionId);
72719
+ const cursor3 = deps.sequenceNumOf(attachment.sessionId) ?? deps.storedSequenceNumOf?.(attachment.sessionId);
72683
72720
  const resume = cursor3 === void 0 ? void 0 : { fromSequenceNum: cursor3 };
72684
72721
  const answer = await deps.dial.openStream(attachment.sessionId, headers, resume, attachment.controller.signal);
72685
72722
  if (answer.status === HTTP_STATUS.unauthorized) {
@@ -72713,6 +72750,7 @@ function createRcStreamHub(deps) {
72713
72750
  const credential = deps.credentialOf(attachment.sessionId);
72714
72751
  if (!live || credential?.authorization === void 0) {
72715
72752
  deps.log?.(`rc stream ${attachment.sessionId}: the attachment ended (${live ? "the session's client credential is no longer known" : "the tracker gave up on the session"})`);
72753
+ persistCursor(attachment.sessionId);
72716
72754
  if (attachments.get(attachment.sessionId) === attachment) {
72717
72755
  attachments.delete(attachment.sessionId);
72718
72756
  }
@@ -72722,6 +72760,7 @@ function createRcStreamHub(deps) {
72722
72760
  try {
72723
72761
  outcome = await attempt(attachment, credential);
72724
72762
  } catch (error62) {
72763
+ persistCursor(attachment.sessionId);
72725
72764
  if (attachment.isStopped()) {
72726
72765
  return;
72727
72766
  }
@@ -72734,6 +72773,7 @@ function createRcStreamHub(deps) {
72734
72773
  return;
72735
72774
  }
72736
72775
  if (outcome === "dropped") {
72776
+ persistCursor(attachment.sessionId);
72737
72777
  deps.log?.(`rc stream ${attachment.sessionId}: the stream ended of its own accord, reconnecting`);
72738
72778
  retriedUnauthorized = false;
72739
72779
  continue;
@@ -72772,6 +72812,7 @@ function createRcStreamHub(deps) {
72772
72812
  },
72773
72813
  close: () => {
72774
72814
  for (const attachment of attachments.values()) {
72815
+ persistCursor(attachment.sessionId);
72775
72816
  attachment.stop();
72776
72817
  }
72777
72818
  attachments.clear();
@@ -73107,7 +73148,8 @@ function realFrontDoorSupervisorPorts(paths) {
73107
73148
  credentialRecord: () => readRcSelfHostRecord(realFarmFs, paths.frontdoorDir),
73108
73149
  log
73109
73150
  }) : void 0;
73110
- const rcTracker = createRcSessionTracker({ now: () => Date.now(), idleMs: RC_IDLE_EXPIRY_MS, credentialStore: createRcCredentialStore(realFarmFs, paths.frontdoorRcCredentialsDir) });
73151
+ const rcCredentialStore = createRcCredentialStore(realFarmFs, paths.frontdoorRcCredentialsDir);
73152
+ const rcTracker = createRcSessionTracker({ now: () => Date.now(), idleMs: RC_IDLE_EXPIRY_MS, credentialStore: rcCredentialStore });
73111
73153
  const rcFanout = createRcEventFanout();
73112
73154
  const rcHub = createRcStreamHub({
73113
73155
  now: () => Date.now(),
@@ -73115,6 +73157,8 @@ function realFrontDoorSupervisorPorts(paths) {
73115
73157
  trackedSessions: () => rcTracker.list().map((session) => session.id),
73116
73158
  fileStreamEvent: rcTracker.fileStreamEvent,
73117
73159
  sequenceNumOf: rcTracker.sequenceNumOf,
73160
+ storedSequenceNumOf: rcCredentialStore.readCursor,
73161
+ saveSequenceNum: rcCredentialStore.writeCursor,
73118
73162
  dial: rcSelfHost ? lateRcStreamDial(rcSelfHostDialTarget) : realRcStreamDial(),
73119
73163
  fanout: rcFanout,
73120
73164
  newClientId: import_node_crypto12.randomUUID,
@@ -73282,6 +73326,7 @@ function realFrontDoorSupervisorPorts(paths) {
73282
73326
  });
73283
73327
  return { port: handle.port, close: handle.close };
73284
73328
  },
73329
+ closeRcStreamHub: rcHub.close,
73285
73330
  log: (line) => {
73286
73331
  appendLog(paths, line);
73287
73332
  }
package/dist/index.cjs CHANGED
@@ -2930,6 +2930,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2930
2930
  ...lastDirectPort === void 0 ? {} : { lastDirectPort }
2931
2931
  });
2932
2932
  const fail = (message) => {
2933
+ ports.closeRcStreamHub();
2933
2934
  writeFrontDoorState(fs12, paths.frontdoorStateFile, { lastError: message, ...sticky() });
2934
2935
  fs12.removeRecursive(paths.frontdoorLockFile);
2935
2936
  ports.log(`agent-shim frontdoor supervisor: ${message}`);
@@ -2990,6 +2991,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2990
2991
  await direct.close();
2991
2992
  await connect3.close();
2992
2993
  await http2.close();
2994
+ ports.closeRcStreamHub();
2993
2995
  writeFrontDoorState(fs12, paths.frontdoorStateFile, sticky());
2994
2996
  return 0;
2995
2997
  }
@@ -4941,31 +4943,54 @@ function credentialOfJson(parsed) {
4941
4943
  anthropicClientPlatform: typeof anthropicClientPlatform === "string" ? anthropicClientPlatform : void 0
4942
4944
  };
4943
4945
  }
4946
+ function sequenceNumOfJson(value) {
4947
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
4948
+ }
4944
4949
  function createRcCredentialStore(fs12, dir) {
4945
4950
  fs12.mkdirPrivate(dir);
4946
4951
  const fileFor = (sessionId) => import_node_path9.default.join(dir, `${sessionId}.json`);
4952
+ const readParsed = (sessionId) => {
4953
+ const raw = fs12.readFileUtf8(fileFor(sessionId));
4954
+ if (raw === void 0) {
4955
+ return void 0;
4956
+ }
4957
+ let parsed;
4958
+ try {
4959
+ parsed = JSON.parse(raw);
4960
+ } catch {
4961
+ return void 0;
4962
+ }
4963
+ return isRecord3(parsed) ? parsed : void 0;
4964
+ };
4947
4965
  return {
4948
4966
  read: (sessionId) => {
4949
4967
  if (!isSafeSessionId(sessionId)) {
4950
4968
  return void 0;
4951
4969
  }
4952
- const raw = fs12.readFileUtf8(fileFor(sessionId));
4953
- if (raw === void 0) {
4954
- return void 0;
4970
+ return credentialOfJson(readParsed(sessionId));
4971
+ },
4972
+ write: (sessionId, credential) => {
4973
+ if (!isSafeSessionId(sessionId)) {
4974
+ return;
4955
4975
  }
4956
- let parsed;
4957
- try {
4958
- parsed = JSON.parse(raw);
4959
- } catch {
4976
+ const cursor = sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
4977
+ const record = { ...credential, ...cursor === void 0 ? {} : { highestSequenceNum: cursor } };
4978
+ fs12.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record, null, 2)}
4979
+ `);
4980
+ },
4981
+ readCursor: (sessionId) => {
4982
+ if (!isSafeSessionId(sessionId)) {
4960
4983
  return void 0;
4961
4984
  }
4962
- return credentialOfJson(parsed);
4985
+ return sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
4963
4986
  },
4964
- write: (sessionId, credential) => {
4987
+ writeCursor: (sessionId, highestSequenceNum) => {
4965
4988
  if (!isSafeSessionId(sessionId)) {
4966
4989
  return;
4967
4990
  }
4968
- fs12.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(credential, null, 2)}
4991
+ const credential = credentialOfJson(readParsed(sessionId));
4992
+ const record = credential === void 0 ? { highestSequenceNum } : { ...credential, highestSequenceNum };
4993
+ fs12.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record, null, 2)}
4969
4994
  `);
4970
4995
  },
4971
4996
  remove: (sessionId) => {
@@ -5300,6 +5325,18 @@ function createRcEventFanout() {
5300
5325
  }
5301
5326
  function createRcStreamHub(deps) {
5302
5327
  const attachments = /* @__PURE__ */ new Map();
5328
+ const lastSavedSequenceNums = /* @__PURE__ */ new Map();
5329
+ const persistCursor = (sessionId) => {
5330
+ if (deps.saveSequenceNum === void 0) {
5331
+ return;
5332
+ }
5333
+ const cursor = deps.sequenceNumOf(sessionId);
5334
+ if (cursor === void 0 || cursor === lastSavedSequenceNums.get(sessionId)) {
5335
+ return;
5336
+ }
5337
+ deps.saveSequenceNum(sessionId, cursor);
5338
+ lastSavedSequenceNums.set(sessionId, cursor);
5339
+ };
5303
5340
  const birth = (sessionId) => {
5304
5341
  const controller = new AbortController();
5305
5342
  let stopped = false;
@@ -5336,7 +5373,7 @@ function createRcStreamHub(deps) {
5336
5373
  if (!isSuccessful3(presence.status)) {
5337
5374
  throw new Error(`the presence call answered HTTP ${String(presence.status)}: ${presence.body}`);
5338
5375
  }
5339
- const cursor = deps.sequenceNumOf(attachment.sessionId);
5376
+ const cursor = deps.sequenceNumOf(attachment.sessionId) ?? deps.storedSequenceNumOf?.(attachment.sessionId);
5340
5377
  const resume = cursor === void 0 ? void 0 : { fromSequenceNum: cursor };
5341
5378
  const answer = await deps.dial.openStream(attachment.sessionId, headers, resume, attachment.controller.signal);
5342
5379
  if (answer.status === HTTP_STATUS.unauthorized) {
@@ -5370,6 +5407,7 @@ function createRcStreamHub(deps) {
5370
5407
  const credential = deps.credentialOf(attachment.sessionId);
5371
5408
  if (!live || credential?.authorization === void 0) {
5372
5409
  deps.log?.(`rc stream ${attachment.sessionId}: the attachment ended (${live ? "the session's client credential is no longer known" : "the tracker gave up on the session"})`);
5410
+ persistCursor(attachment.sessionId);
5373
5411
  if (attachments.get(attachment.sessionId) === attachment) {
5374
5412
  attachments.delete(attachment.sessionId);
5375
5413
  }
@@ -5379,6 +5417,7 @@ function createRcStreamHub(deps) {
5379
5417
  try {
5380
5418
  outcome = await attempt(attachment, credential);
5381
5419
  } catch (error) {
5420
+ persistCursor(attachment.sessionId);
5382
5421
  if (attachment.isStopped()) {
5383
5422
  return;
5384
5423
  }
@@ -5391,6 +5430,7 @@ function createRcStreamHub(deps) {
5391
5430
  return;
5392
5431
  }
5393
5432
  if (outcome === "dropped") {
5433
+ persistCursor(attachment.sessionId);
5394
5434
  deps.log?.(`rc stream ${attachment.sessionId}: the stream ended of its own accord, reconnecting`);
5395
5435
  retriedUnauthorized = false;
5396
5436
  continue;
@@ -5429,6 +5469,7 @@ function createRcStreamHub(deps) {
5429
5469
  },
5430
5470
  close: () => {
5431
5471
  for (const attachment of attachments.values()) {
5472
+ persistCursor(attachment.sessionId);
5432
5473
  attachment.stop();
5433
5474
  }
5434
5475
  attachments.clear();
package/dist/index.mjs CHANGED
@@ -2700,6 +2700,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2700
2700
  ...lastDirectPort === void 0 ? {} : { lastDirectPort }
2701
2701
  });
2702
2702
  const fail = (message) => {
2703
+ ports.closeRcStreamHub();
2703
2704
  writeFrontDoorState(fs12, paths.frontdoorStateFile, { lastError: message, ...sticky() });
2704
2705
  fs12.removeRecursive(paths.frontdoorLockFile);
2705
2706
  ports.log(`agent-shim frontdoor supervisor: ${message}`);
@@ -2760,6 +2761,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2760
2761
  await direct.close();
2761
2762
  await connect3.close();
2762
2763
  await http2.close();
2764
+ ports.closeRcStreamHub();
2763
2765
  writeFrontDoorState(fs12, paths.frontdoorStateFile, sticky());
2764
2766
  return 0;
2765
2767
  }
@@ -4711,31 +4713,54 @@ function credentialOfJson(parsed) {
4711
4713
  anthropicClientPlatform: typeof anthropicClientPlatform === "string" ? anthropicClientPlatform : void 0
4712
4714
  };
4713
4715
  }
4716
+ function sequenceNumOfJson(value) {
4717
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
4718
+ }
4714
4719
  function createRcCredentialStore(fs12, dir) {
4715
4720
  fs12.mkdirPrivate(dir);
4716
4721
  const fileFor = (sessionId) => path9.join(dir, `${sessionId}.json`);
4722
+ const readParsed = (sessionId) => {
4723
+ const raw = fs12.readFileUtf8(fileFor(sessionId));
4724
+ if (raw === void 0) {
4725
+ return void 0;
4726
+ }
4727
+ let parsed;
4728
+ try {
4729
+ parsed = JSON.parse(raw);
4730
+ } catch {
4731
+ return void 0;
4732
+ }
4733
+ return isRecord3(parsed) ? parsed : void 0;
4734
+ };
4717
4735
  return {
4718
4736
  read: (sessionId) => {
4719
4737
  if (!isSafeSessionId(sessionId)) {
4720
4738
  return void 0;
4721
4739
  }
4722
- const raw = fs12.readFileUtf8(fileFor(sessionId));
4723
- if (raw === void 0) {
4724
- return void 0;
4740
+ return credentialOfJson(readParsed(sessionId));
4741
+ },
4742
+ write: (sessionId, credential) => {
4743
+ if (!isSafeSessionId(sessionId)) {
4744
+ return;
4725
4745
  }
4726
- let parsed;
4727
- try {
4728
- parsed = JSON.parse(raw);
4729
- } catch {
4746
+ const cursor = sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
4747
+ const record = { ...credential, ...cursor === void 0 ? {} : { highestSequenceNum: cursor } };
4748
+ fs12.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record, null, 2)}
4749
+ `);
4750
+ },
4751
+ readCursor: (sessionId) => {
4752
+ if (!isSafeSessionId(sessionId)) {
4730
4753
  return void 0;
4731
4754
  }
4732
- return credentialOfJson(parsed);
4755
+ return sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
4733
4756
  },
4734
- write: (sessionId, credential) => {
4757
+ writeCursor: (sessionId, highestSequenceNum) => {
4735
4758
  if (!isSafeSessionId(sessionId)) {
4736
4759
  return;
4737
4760
  }
4738
- fs12.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(credential, null, 2)}
4761
+ const credential = credentialOfJson(readParsed(sessionId));
4762
+ const record = credential === void 0 ? { highestSequenceNum } : { ...credential, highestSequenceNum };
4763
+ fs12.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record, null, 2)}
4739
4764
  `);
4740
4765
  },
4741
4766
  remove: (sessionId) => {
@@ -5070,6 +5095,18 @@ function createRcEventFanout() {
5070
5095
  }
5071
5096
  function createRcStreamHub(deps) {
5072
5097
  const attachments = /* @__PURE__ */ new Map();
5098
+ const lastSavedSequenceNums = /* @__PURE__ */ new Map();
5099
+ const persistCursor = (sessionId) => {
5100
+ if (deps.saveSequenceNum === void 0) {
5101
+ return;
5102
+ }
5103
+ const cursor = deps.sequenceNumOf(sessionId);
5104
+ if (cursor === void 0 || cursor === lastSavedSequenceNums.get(sessionId)) {
5105
+ return;
5106
+ }
5107
+ deps.saveSequenceNum(sessionId, cursor);
5108
+ lastSavedSequenceNums.set(sessionId, cursor);
5109
+ };
5073
5110
  const birth = (sessionId) => {
5074
5111
  const controller = new AbortController();
5075
5112
  let stopped = false;
@@ -5106,7 +5143,7 @@ function createRcStreamHub(deps) {
5106
5143
  if (!isSuccessful3(presence.status)) {
5107
5144
  throw new Error(`the presence call answered HTTP ${String(presence.status)}: ${presence.body}`);
5108
5145
  }
5109
- const cursor = deps.sequenceNumOf(attachment.sessionId);
5146
+ const cursor = deps.sequenceNumOf(attachment.sessionId) ?? deps.storedSequenceNumOf?.(attachment.sessionId);
5110
5147
  const resume = cursor === void 0 ? void 0 : { fromSequenceNum: cursor };
5111
5148
  const answer = await deps.dial.openStream(attachment.sessionId, headers, resume, attachment.controller.signal);
5112
5149
  if (answer.status === HTTP_STATUS.unauthorized) {
@@ -5140,6 +5177,7 @@ function createRcStreamHub(deps) {
5140
5177
  const credential = deps.credentialOf(attachment.sessionId);
5141
5178
  if (!live || credential?.authorization === void 0) {
5142
5179
  deps.log?.(`rc stream ${attachment.sessionId}: the attachment ended (${live ? "the session's client credential is no longer known" : "the tracker gave up on the session"})`);
5180
+ persistCursor(attachment.sessionId);
5143
5181
  if (attachments.get(attachment.sessionId) === attachment) {
5144
5182
  attachments.delete(attachment.sessionId);
5145
5183
  }
@@ -5149,6 +5187,7 @@ function createRcStreamHub(deps) {
5149
5187
  try {
5150
5188
  outcome = await attempt(attachment, credential);
5151
5189
  } catch (error) {
5190
+ persistCursor(attachment.sessionId);
5152
5191
  if (attachment.isStopped()) {
5153
5192
  return;
5154
5193
  }
@@ -5161,6 +5200,7 @@ function createRcStreamHub(deps) {
5161
5200
  return;
5162
5201
  }
5163
5202
  if (outcome === "dropped") {
5203
+ persistCursor(attachment.sessionId);
5164
5204
  deps.log?.(`rc stream ${attachment.sessionId}: the stream ended of its own accord, reconnecting`);
5165
5205
  retriedUnauthorized = false;
5166
5206
  continue;
@@ -5199,6 +5239,7 @@ function createRcStreamHub(deps) {
5199
5239
  },
5200
5240
  close: () => {
5201
5241
  for (const attachment of attachments.values()) {
5242
+ persistCursor(attachment.sessionId);
5202
5243
  attachment.stop();
5203
5244
  }
5204
5245
  attachments.clear();
@@ -1648,18 +1648,22 @@ declare function realConnectCertStore(paths: {
1648
1648
  }): ConnectCertStore;
1649
1649
 
1650
1650
  /**
1651
- * The persisted half of the tracker's client credential: the OAuth-kind bearer and protocol headers a Remote Control session's client-half calls were last observed with, written per session id so a later door generation can still attach to a session whose create crossed an earlier one.
1651
+ * The persisted half of the tracker's client credential and of its sequence cursor: the OAuth-kind bearer and protocol headers a Remote Control session's client-half calls were last observed with, and the highest sequence number the door's stream attachment had confirmed when its last attachment boundary passed, written per session id so a later door generation can still attach to a session whose create crossed an earlier one and resume its read stream from where the last generation left off.
1652
1652
  *
1653
- * The tracker holds the credential in memory only, and the worker's recurring calls carry the worker JWT rather than the OAuth bearer, so a door that starts after the session's create (an idle shutdown and restart, a release upgrade, a reboot) would never learn it: nothing the door can observe re-states it until some client-half call happens to cross. Since the session outlives any one door process, the credential must outlive the process too. It is the same secret the identity's own store already holds on the same machine, so persisting it adds a second copy inside the same trust boundary, at the same owner-only protection: one JSON file per session, mode 0600, under the door's own front-door directory.
1653
+ * The tracker holds both in memory only, and the worker's recurring calls carry the worker JWT rather than the OAuth bearer, so a door that starts after the session's create (an idle shutdown and restart, a release upgrade, a reboot) would never learn the credential: nothing the door can observe re-states it until some client-half call happens to cross. Since the session outlives any one door process, the credential must outlive the process too, and so must the cursor, or every restarted generation would re-read the stream from its head. The credential is the same secret the identity's own store already holds on the same machine, so persisting it adds a second copy inside the same trust boundary, at the same owner-only protection: one JSON file per session, mode 0600, under the door's own front-door directory, with the cursor (no secret, but the same file and the same lifecycle) beside it. The native client does exactly this: its `bridge-session` transcript entry carries `lastSequenceNum` beside the session id.
1654
1654
  *
1655
- * A file is removed when the tracker observes the session closing (the conflict header, or an accepted archive). Nothing else removes it: the tracker's idle sweep is not a close (a quiet session is not a dead one, and a resumed session must re-attach), and the credential's own expiry is the protocol's to judge, as a 401 on the dial the hub already retries and backs off from. A session that ends without an observable close leaves its file behind, overwritten only if the same id is ever observed again.
1655
+ * Each half is written at its own boundaries (the credential whenever an OAuth-kind bearer is observed, the cursor when the stream hub's attachment reaches a boundary), so each write preserves the other half, and neither is written per stream event. A file is removed when the tracker observes the session closing (the conflict header, or an accepted archive). Nothing else removes it: the tracker's idle sweep is not a close (a quiet session is not a dead one, and a resumed session must re-attach), and the credential's own expiry is the protocol's to judge, as a 401 on the dial the hub already retries and backs off from. A session that ends without an observable close leaves its file behind, overwritten only if the same id is ever observed again.
1656
1656
  */
1657
1657
  interface RcCredentialStore {
1658
1658
  /** The session's last observed client credential, or undefined when none was ever persisted for it (or what was persisted does not parse). */
1659
1659
  readonly read: (sessionId: string) => RcObservedCredential | undefined;
1660
- /** Persists the session's client credential, replacing whatever was held for it. */
1660
+ /** Persists the session's client credential, replacing whatever was held for it and keeping the session's persisted cursor. */
1661
1661
  readonly write: (sessionId: string, credential: RcObservedCredential) => void;
1662
- /** Forgets the session's persisted credential, succeeding silently when none was held. */
1662
+ /** The session's persisted sequence cursor, or undefined when none was ever persisted for it (or what was persisted is not a usable number). */
1663
+ readonly readCursor: (sessionId: string) => number | undefined;
1664
+ /** Persists the session's sequence cursor, keeping whatever credential the same file holds. */
1665
+ readonly writeCursor: (sessionId: string, highestSequenceNum: number) => void;
1666
+ /** Forgets the session's persisted record, credential and cursor together, succeeding silently when none was held. */
1663
1667
  readonly remove: (sessionId: string) => void;
1664
1668
  }
1665
1669
  /** Creates the credential store over one directory: one owner-only JSON file per session id. */
@@ -2061,6 +2065,10 @@ interface RcStreamHubDeps {
2061
2065
  readonly fileStreamEvent: (sessionId: string, event: RcStreamEnvelope) => void;
2062
2066
  /** Reads the session's sequence cursor: the highest number the door has seen, from the stream or from its own confirmed writes. */
2063
2067
  readonly sequenceNumOf: (sessionId: string) => number | undefined;
2068
+ /** Reads the session's persisted sequence cursor, consulted only when the tracker's in-memory cursor is undefined at attach time (a door generation that started after the session's traffic, the same fresh-generation case the persisted credential solves for attaching at all), so that generation's first stream resumes where the last one left off instead of at the stream's head. */
2069
+ readonly storedSequenceNumOf?: (sessionId: string) => number | undefined;
2070
+ /** Persists the session's sequence cursor at an attachment boundary (a stream drop, a failed attempt, the attachment's end), never per stream event: the native client persists its own cursor at bridge-session boundaries, and so does this. */
2071
+ readonly saveSequenceNum?: (sessionId: string, sequenceNum: number) => void;
2064
2072
  readonly dial: RcStreamDial;
2065
2073
  readonly fanout: RcEventFanout;
2066
2074
  /** Mints the stable client id one session's attachment announces: a v4 UUID or better in production. Stable means it survives reconnects, so the host sees one client, not a new one per drop. */
@@ -2586,6 +2594,10 @@ interface FrontDoorSupervisorPorts {
2586
2594
  * Starts the direct listener on `preferredPort` (same bind-time fallback): the same routes as the provider listener, minus the headroom hop, over plain HTTP because headroom is what connects to it. Headroom forwards routed traffic back here, which is what keeps the hop from looping, and the port is sticky like the others because headroom's allowlist admits its exact origin. It admits only the hop's own requests and holds no credential until it redeems one from the hop's custody.
2587
2595
  */
2588
2596
  readonly startDirectListener: (preferredPort: number | undefined) => Promise<FrontDoorListenerHandle>;
2597
+ /**
2598
+ * Stops the Remote Control stream hub: persists every live attachment's sequence cursor synchronously, then frees the dials. Called exactly once on every exit the supervisor owns, after every listener it started is down (a live listener could still settle an observed Remote Control exchange, and the reconcile that follows would re-attach a stream the close had just ended) and before the final state write and the process's exit.
2599
+ */
2600
+ readonly closeRcStreamHub: () => void;
2589
2601
  readonly log: (line: string) => void;
2590
2602
  }
2591
2603
  /** Options for `runFrontDoorSupervisor`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-shim",
3
- "version": "8.13.0",
3
+ "version": "8.14.1",
4
4
  "description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5
5
  "license": "MIT",
6
6
  "author": "Joseph Mearman <joseph@mearman.co.uk>",