agent-shim 8.26.0 → 8.27.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/index.mjs CHANGED
@@ -10457,7 +10457,7 @@ function pageOfRcChannel(url, rows, idOf, rowOf) {
10457
10457
  return { data: page.map(rowOf), ...last === void 0 ? {} : { next_cursor: idOf(last) } };
10458
10458
  }
10459
10459
  async function serveRcClientChannels(ctx, tail, method, maxBatchEvents) {
10460
- const { request: request4, response, session } = ctx;
10460
+ const { request: request4, response, host } = ctx;
10461
10461
  if (tail === "client/presence" && method === "POST") {
10462
10462
  const body = parseJsonObject2(await ctx.readBody(request4.body, CONTROL_BODY_CAP_BYTES));
10463
10463
  const clientId = nonEmptyString2(body, "client_id");
@@ -10467,11 +10467,11 @@ async function serveRcClientChannels(ctx, tail, method, maxBatchEvents) {
10467
10467
  return true;
10468
10468
  }
10469
10469
  if (clear === true) {
10470
- session.clients.delete(clientId);
10471
- ctx.log?.(`rc selfhost ${session.id}: client ${clientId} cleared its presence`);
10470
+ host.clients.delete(clientId);
10471
+ ctx.log?.(`rc selfhost ${host.id}: client ${clientId} cleared its presence`);
10472
10472
  } else {
10473
- session.clients.set(clientId, ctx.now());
10474
- ctx.log?.(`rc selfhost ${session.id}: client ${clientId} announced its presence${session.clients.size === 1 ? "" : ` (${String(session.clients.size)} clients now announced)`}`);
10473
+ host.clients.set(clientId, ctx.now());
10474
+ ctx.log?.(`rc selfhost ${host.id}: client ${clientId} announced its presence${host.clients.size === 1 ? "" : ` (${String(host.clients.size)} clients now announced)`}`);
10475
10475
  }
10476
10476
  await ctx.answerJson(response, HTTP_STATUS.ok, { refresh_after_seconds: ctx.presenceRefreshSeconds });
10477
10477
  return true;
@@ -10484,8 +10484,8 @@ async function serveRcClientChannels(ctx, tail, method, maxBatchEvents) {
10484
10484
  await ctx.answerJson(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a read receipt is JSON naming the event_id it read or none for the whole session, and client_id only as a non-empty string or absent" } });
10485
10485
  return true;
10486
10486
  }
10487
- const receipt = ctx.publish(session, clientId ?? RC_SOURCE_CLIENT, { type: "mark_read", ...eventId === void 0 ? {} : { event_id: eventId }, ...clientId === void 0 ? {} : { client_id: clientId } }, true);
10488
- ctx.log?.(`rc selfhost ${session.id}: ${clientId === void 0 ? "an unnamed client" : `client ${clientId}`} marked ${eventId === void 0 ? "the session read" : `event ${eventId} read`} as sequence ${String(receipt.sequenceNum)}`);
10487
+ const receipt = ctx.publish(host.conversation, host.writer, clientId ?? RC_SOURCE_CLIENT, { type: "mark_read", ...eventId === void 0 ? {} : { event_id: eventId }, ...clientId === void 0 ? {} : { client_id: clientId } });
10488
+ ctx.log?.(`rc selfhost ${host.id}: ${clientId === void 0 ? "an unnamed client" : `client ${clientId}`} marked ${eventId === void 0 ? "the session read" : `event ${eventId} read`} as sequence ${String(receipt.sequenceNum)}`);
10489
10489
  await ctx.answerJson(response, HTTP_STATUS.ok, {});
10490
10490
  return true;
10491
10491
  }
@@ -10502,16 +10502,16 @@ async function serveRcClientChannels(ctx, tail, method, maxBatchEvents) {
10502
10502
  await ctx.answerJson(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "every event in a teleport write is an object carrying one payload object" } });
10503
10503
  return true;
10504
10504
  }
10505
- const published = ctx.publish(session, RC_SOURCE_CLIENT, event.payload);
10505
+ const published = ctx.publish(host.conversation, host.writer, RC_SOURCE_CLIENT, event.payload);
10506
10506
  results.push({ sequence_num: String(published.sequenceNum), duplicate: published.duplicate });
10507
10507
  }
10508
10508
  ctx.sweep();
10509
- ctx.log?.(`rc selfhost ${session.id}: teleport write published ${String(results.length)} marker event(s)`);
10509
+ ctx.log?.(`rc selfhost ${host.id}: teleport write published ${String(results.length)} marker event(s)`);
10510
10510
  await ctx.answerJson(response, HTTP_STATUS.ok, { results });
10511
10511
  return true;
10512
10512
  }
10513
10513
  if (tail === "teleport-events" && method === "GET") {
10514
- const page = pageOfRcChannel(request4.url, session.events, (event) => event.eventId, (event) => ({ event_id: event.eventId, payload: event.payload }));
10514
+ const page = pageOfRcChannel(request4.url, host.conversation.events, (event) => event.eventId, (event) => ({ event_id: event.eventId, payload: event.payload }));
10515
10515
  if ("error" in page) {
10516
10516
  await ctx.answerJson(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: page.error } });
10517
10517
  return true;
@@ -10587,10 +10587,7 @@ async function serveRcCompatSessions(ctx, pathname, compatPath) {
10587
10587
  await ctx.answerJson(response, HTTP_STATUS.ok, compatSessionRow(session, ctx.now));
10588
10588
  return;
10589
10589
  }
10590
- for (const sink of [...session.workerStreams, ...session.clientStreams]) {
10591
- sink.retire();
10592
- }
10593
- ctx.sessions.delete(session.id);
10590
+ ctx.retireSession(session);
10594
10591
  ctx.log?.(`rc selfhost ${session.id}: archived through the compatibility family`);
10595
10592
  await ctx.answerJson(response, HTTP_STATUS.ok, {});
10596
10593
  return;
@@ -10598,6 +10595,202 @@ async function serveRcCompatSessions(ctx, pathname, compatPath) {
10598
10595
  await ctx.answerJson(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the compatibility session family serves no path "${compatTail}"` } });
10599
10596
  }
10600
10597
 
10598
+ // src/frontdoor/rcSelfHostConversations.ts
10599
+ var RC_CONVERSATIONS_PATH_PREFIX = "/v1/code/conversations";
10600
+ var RC_CONVERSATION_ID_PREFIX = "conv_";
10601
+ function isRecord8(value) {
10602
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10603
+ }
10604
+ function parseJsonObject3(body) {
10605
+ let parsed;
10606
+ try {
10607
+ parsed = JSON.parse(body);
10608
+ } catch {
10609
+ return void 0;
10610
+ }
10611
+ return isRecord8(parsed) ? parsed : void 0;
10612
+ }
10613
+ function nonEmptyString3(record, field) {
10614
+ const value = record === void 0 ? void 0 : record[field];
10615
+ return typeof value === "string" && value !== "" ? value : void 0;
10616
+ }
10617
+ function newRcSelfHostConversation(newUuid, now) {
10618
+ return { id: `${RC_CONVERSATION_ID_PREFIX}${newUuid()}`, createdAt: now(), sequenceNum: 0, events: [], seenPayloadUuids: /* @__PURE__ */ new Set(), workerStreams: /* @__PURE__ */ new Set(), clientStreams: /* @__PURE__ */ new Set(), clients: /* @__PURE__ */ new Map(), sessionIds: /* @__PURE__ */ new Set(), lastTrafficAt: now() };
10619
+ }
10620
+ function conversationRow(conversation) {
10621
+ return { id: conversation.id, created_at: new Date(conversation.createdAt).toISOString(), updated_at: new Date(conversation.lastTrafficAt).toISOString(), sessions: [...conversation.sessionIds] };
10622
+ }
10623
+ async function serveRcConversations(ctx, pathname, method) {
10624
+ const { request: request4, response, conversations } = ctx;
10625
+ const refusal = ctx.credentialRefusal(request4.headers);
10626
+ if (refusal !== void 0) {
10627
+ await ctx.unauthorized(response, refusal);
10628
+ return;
10629
+ }
10630
+ if (pathname === RC_CONVERSATIONS_PATH_PREFIX) {
10631
+ if (method !== "GET") {
10632
+ await ctx.answerJson(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the conversation collection is a GET; a conversation is born from a session create, never here" } });
10633
+ return;
10634
+ }
10635
+ await ctx.answerJson(response, HTTP_STATUS.ok, { data: [...conversations.values()].sort((left, right) => left.createdAt - right.createdAt || (left.id < right.id ? -1 : 1)).map(conversationRow) });
10636
+ return;
10637
+ }
10638
+ const rest = pathname.slice(`${RC_CONVERSATIONS_PATH_PREFIX}/`.length);
10639
+ const slash = rest.indexOf("/");
10640
+ const conversationId = slash === -1 ? rest : rest.slice(0, slash);
10641
+ const tail = slash === -1 ? "" : rest.slice(slash + 1);
10642
+ if (!conversationId.startsWith(RC_CONVERSATION_ID_PREFIX)) {
10643
+ await ctx.answerJson(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "the conversation family names one conv_-prefixed conversation id" } });
10644
+ return;
10645
+ }
10646
+ const conversation = conversations.get(conversationId);
10647
+ if (conversation === void 0) {
10648
+ await ctx.answerJson(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "no such Remote Control conversation on this door" } });
10649
+ return;
10650
+ }
10651
+ conversation.lastTrafficAt = ctx.now();
10652
+ if (tail === "" && method === "GET") {
10653
+ await ctx.answerJson(response, HTTP_STATUS.ok, { conversation: conversationRow(conversation) });
10654
+ return;
10655
+ }
10656
+ if (tail === "events" && method === "POST") {
10657
+ const body = parseJsonObject3(await ctx.readBody(request4.body, CONTROL_BODY_CAP_BYTES));
10658
+ const events = body?.events;
10659
+ if (!Array.isArray(events) || events.length === 0 || events.length > ctx.maxBatchEvents) {
10660
+ await ctx.answerJson(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: `a client event write is a non-empty array of at most ${String(ctx.maxBatchEvents)} events` } });
10661
+ return;
10662
+ }
10663
+ const results = [];
10664
+ for (const event of events) {
10665
+ if (!isRecord8(event) || !isRecord8(event.payload)) {
10666
+ await ctx.answerJson(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "every event in a write is an object carrying one payload object" } });
10667
+ return;
10668
+ }
10669
+ const published = ctx.publish(conversation, { session: void 0, half: "client" }, RC_SOURCE_CLIENT, event.payload);
10670
+ results.push({ sequence_num: String(published.sequenceNum), duplicate: published.duplicate });
10671
+ }
10672
+ ctx.sweep();
10673
+ ctx.log?.(`rc selfhost ${conversation.id}: a conversation client wrote ${String(results.length)} event(s) into the shared space`);
10674
+ await ctx.answerJson(response, HTTP_STATUS.ok, { results });
10675
+ return;
10676
+ }
10677
+ if (tail === "events" && method === "GET") {
10678
+ await ctx.answerJson(response, HTTP_STATUS.ok, { data: conversation.events.map((event) => ({ event_id: event.eventId, event_type: nonEmptyString3(event.payload, "type") ?? "event", sequence_num: event.sequenceNum, source: event.source, payload: event.payload, created_at: event.createdAt })) });
10679
+ return;
10680
+ }
10681
+ if (tail === "events/stream" && method === "GET") {
10682
+ await ctx.holdStream(conversation, conversation.id, "clientStreams", () => true, request4, response);
10683
+ return;
10684
+ }
10685
+ if (await serveRcClientChannels({ request: request4, response, host: { id: conversation.id, clients: conversation.clients, conversation, writer: { session: void 0, half: "client" } }, answerJson: ctx.answerJson, readBody: ctx.readBody, publish: ctx.publish, sweep: ctx.sweep, now: ctx.now, presenceRefreshSeconds: ctx.presenceRefreshSeconds, ...ctx.log === void 0 ? {} : { log: ctx.log } }, tail, method, ctx.maxBatchEvents)) {
10686
+ return;
10687
+ }
10688
+ await ctx.answerJson(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the conversation family serves no path "${tail}"` } });
10689
+ }
10690
+
10691
+ // src/frontdoor/rcSelfHostLocal.ts
10692
+ var RC_SELF_HOST_OAUTH_HOST = "platform.claude.com";
10693
+ var RC_SELF_HOST_SCOPE_LIST = ["user:profile", "user:inference", "user:sessions:claude_code", "user:mcp_servers", "user:file_upload"];
10694
+ var RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS = 3600;
10695
+ var MS_PER_SECOND5 = 1e3;
10696
+ var HTTP_SERVICE_UNAVAILABLE = 503;
10697
+ function isRecord9(value) {
10698
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10699
+ }
10700
+ function parseJsonObject4(body) {
10701
+ let parsed;
10702
+ try {
10703
+ parsed = JSON.parse(body);
10704
+ } catch {
10705
+ return void 0;
10706
+ }
10707
+ return isRecord9(parsed) ? parsed : void 0;
10708
+ }
10709
+ function growthBookFeatures() {
10710
+ return {
10711
+ tengu_ccr_bridge: { defaultValue: true },
10712
+ tengu_bridge_repl_v2: { defaultValue: true },
10713
+ tengu_bridge_repl_v2_cse_shim_enabled: { defaultValue: true },
10714
+ tengu_bridge_min_version: { defaultValue: { minVersion: "0.0.0" } }
10715
+ };
10716
+ }
10717
+ var answerServerJson = (response, status, body) => {
10718
+ const text = JSON.stringify(body);
10719
+ response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text, "utf8")) });
10720
+ response.end(text);
10721
+ };
10722
+ function createRcSelfHostLocal(deps) {
10723
+ return {
10724
+ parsesHost: (host) => host === RC_SELF_HOST_OAUTH_HOST,
10725
+ serve: async (host, request4, response) => {
10726
+ const method = (request4.method ?? "").toUpperCase();
10727
+ const pathname = new URL(request4.url ?? "/", "http://127.0.0.1").pathname;
10728
+ if (host === RC_SELF_HOST_OAUTH_HOST) {
10729
+ if (method === "POST" && pathname === "/v1/oauth/token") {
10730
+ const body = parseJsonObject4(await deps.readBody(request4, CONTROL_BODY_CAP_BYTES));
10731
+ const record = deps.credentialRecord();
10732
+ if (record === void 0) {
10733
+ answerServerJson(response, HTTP_SERVICE_UNAVAILABLE, { error: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it" });
10734
+ return true;
10735
+ }
10736
+ if (body?.grant_type !== void 0 && body.grant_type !== "refresh_token") {
10737
+ answerServerJson(response, HTTP_STATUS.badRequest, { error: "unsupported_grant_type" });
10738
+ return true;
10739
+ }
10740
+ answerServerJson(response, HTTP_STATUS.ok, { access_token: record.accessToken, refresh_token: record.refreshToken, expires_in: RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS, scope: RC_SELF_HOST_SCOPE_LIST.join(" ") });
10741
+ return true;
10742
+ }
10743
+ if (method === "GET" && pathname === "/v1/oauth/hello") {
10744
+ answerServerJson(response, HTTP_STATUS.ok, {});
10745
+ return true;
10746
+ }
10747
+ return false;
10748
+ }
10749
+ if (method === "POST" && pathname.startsWith("/api/eval/")) {
10750
+ await deps.readBody(request4, CONTROL_BODY_CAP_BYTES);
10751
+ answerServerJson(response, HTTP_STATUS.ok, { features: growthBookFeatures(), dateUpdated: Math.floor(deps.now() / MS_PER_SECOND5) });
10752
+ return true;
10753
+ }
10754
+ if (method === "GET" && pathname === "/api/oauth/profile") {
10755
+ const record = deps.credentialRecord();
10756
+ if (record === void 0) {
10757
+ answerServerJson(response, HTTP_SERVICE_UNAVAILABLE, { error: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it" });
10758
+ return true;
10759
+ }
10760
+ answerServerJson(response, HTTP_STATUS.ok, {
10761
+ account: { uuid: record.accountUuid, display_name: "agent-shim self-hosted", created_at: (/* @__PURE__ */ new Date(0)).toISOString() },
10762
+ organization: { uuid: record.organizationUuid, organization_type: "claude_max" }
10763
+ });
10764
+ return true;
10765
+ }
10766
+ if (method === "GET" && pathname === "/api/claude_code/policy_limits") {
10767
+ answerServerJson(response, HTTP_STATUS.ok, {});
10768
+ return true;
10769
+ }
10770
+ if (method === "POST" && pathname === "/api/oauth/validate") {
10771
+ await deps.readBody(request4, CONTROL_BODY_CAP_BYTES);
10772
+ const record = deps.credentialRecord();
10773
+ if (record === void 0) {
10774
+ answerServerJson(response, HTTP_SERVICE_UNAVAILABLE, { error: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it" });
10775
+ return true;
10776
+ }
10777
+ answerServerJson(response, HTTP_STATUS.ok, { scopes: [...RC_SELF_HOST_SCOPE_LIST], expiresAt: null, subscriptionType: "max", account_uuid: record.accountUuid, organization_uuid: record.organizationUuid });
10778
+ return true;
10779
+ }
10780
+ if (method === "POST" && (pathname === "/api/event_logging/v2/batch" || pathname === "/api/claude_code/metrics" || pathname === "/api/claude_cli_feedback")) {
10781
+ await deps.readBody(request4, CONTROL_BODY_CAP_BYTES);
10782
+ answerServerJson(response, HTTP_STATUS.ok, {});
10783
+ return true;
10784
+ }
10785
+ if (method === "GET" && pathname === "/api/hello") {
10786
+ answerServerJson(response, HTTP_STATUS.ok, {});
10787
+ return true;
10788
+ }
10789
+ return false;
10790
+ }
10791
+ };
10792
+ }
10793
+
10601
10794
  // src/frontdoor/rcWebFetch.ts
10602
10795
  import * as dns2 from "node:dns";
10603
10796
  import * as http2 from "node:http";
@@ -10849,17 +11042,14 @@ var RC_SELF_HOST_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST";
10849
11042
  function rcSelfHostFromEnv(env) {
10850
11043
  return env[RC_SELF_HOST_ENV] === "1";
10851
11044
  }
10852
- var RC_SELF_HOST_SCOPE_LIST = ["user:profile", "user:inference", "user:sessions:claude_code", "user:mcp_servers", "user:file_upload"];
10853
11045
  var RC_SESSIONS_PATH_PREFIX2 = "/v1/code/sessions";
10854
11046
  var RC_COMPAT_SESSIONS_PATH = "/v1/sessions";
10855
11047
  var RC_SESSION_ID_PREFIX2 = "cse_";
10856
11048
  var RC_SELF_HOST_API_BASE_URL = "https://api.anthropic.com";
10857
- var RC_SELF_HOST_OAUTH_HOST = "platform.claude.com";
10858
11049
  var HTTP_CONFLICT = 409;
10859
- var MS_PER_SECOND5 = 1e3;
10860
- var HTTP_SERVICE_UNAVAILABLE = 503;
11050
+ var MS_PER_SECOND6 = 1e3;
11051
+ var HTTP_SERVICE_UNAVAILABLE2 = 503;
10861
11052
  var RC_SELF_HOST_WORKER_JWT_TTL_SECONDS = 46800;
10862
- var RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS = 3600;
10863
11053
  var RC_SELF_HOST_KEEPALIVE_MS = 15e3;
10864
11054
  var RC_SELF_HOST_RETENTION_MS = 6e5;
10865
11055
  var RC_PRESENCE_REFRESH_SECONDS = 60;
@@ -10868,13 +11058,13 @@ var RC_SELF_HOST_WORKER_EPOCH = 1;
10868
11058
  var RC_STREAM_EVENT_NAME2 = "client_event";
10869
11059
  var CONFLICT_REASON_HEADER2 = "x-ccr-conflict-reason";
10870
11060
  var CONFLICT_REASON_EPOCH_STALE = "epoch_stale";
10871
- function isRecord8(value) {
11061
+ function isRecord10(value) {
10872
11062
  return typeof value === "object" && value !== null && !Array.isArray(value);
10873
11063
  }
10874
11064
  function isWebSearchRefusal(value) {
10875
11065
  return !Array.isArray(value);
10876
11066
  }
10877
- function nonEmptyString3(record, field) {
11067
+ function nonEmptyString4(record, field) {
10878
11068
  const value = record === void 0 ? void 0 : record[field];
10879
11069
  return typeof value === "string" && value !== "" ? value : void 0;
10880
11070
  }
@@ -10945,56 +11135,69 @@ async function readBody2(request4, capBytes) {
10945
11135
  });
10946
11136
  });
10947
11137
  }
10948
- function parseJsonObject3(body) {
11138
+ function parseJsonObject5(body) {
10949
11139
  let parsed;
10950
11140
  try {
10951
11141
  parsed = JSON.parse(body);
10952
11142
  } catch {
10953
11143
  return void 0;
10954
11144
  }
10955
- return isRecord8(parsed) ? parsed : void 0;
11145
+ return isRecord10(parsed) ? parsed : void 0;
10956
11146
  }
10957
11147
  function mintWorkerJwt(randomToken, issuedAt) {
10958
11148
  const encode = (value) => Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
10959
11149
  const header = encode({ alg: "HS256", typ: "JWT" });
10960
- const payload = encode({ exp: Math.floor((issuedAt + RC_SELF_HOST_WORKER_JWT_TTL_SECONDS * MS_PER_SECOND5) / MS_PER_SECOND5), iat: Math.floor(issuedAt / MS_PER_SECOND5) });
11150
+ const payload = encode({ exp: Math.floor((issuedAt + RC_SELF_HOST_WORKER_JWT_TTL_SECONDS * MS_PER_SECOND6) / MS_PER_SECOND6), iat: Math.floor(issuedAt / MS_PER_SECOND6) });
10961
11151
  return `${header}.${payload}.${Buffer.from(randomToken(), "utf8").toString("base64url")}`;
10962
11152
  }
10963
- function growthBookFeatures() {
10964
- return {
10965
- tengu_ccr_bridge: { defaultValue: true },
10966
- tengu_bridge_repl_v2: { defaultValue: true },
10967
- tengu_bridge_repl_v2_cse_shim_enabled: { defaultValue: true },
10968
- tengu_bridge_min_version: { defaultValue: { minVersion: "0.0.0" } }
10969
- };
10970
- }
10971
11153
  function createRcSelfHostSurface(deps) {
10972
11154
  const sessions = /* @__PURE__ */ new Map();
11155
+ const conversations = /* @__PURE__ */ new Map();
11156
+ const retireSession = (session) => {
11157
+ for (const sink of [...session.conversation.workerStreams, ...session.conversation.clientStreams]) {
11158
+ if (sink.owner === session.id) {
11159
+ sink.retire();
11160
+ }
11161
+ }
11162
+ session.conversation.sessionIds.delete(session.id);
11163
+ sessions.delete(session.id);
11164
+ };
10973
11165
  const sweep = () => {
10974
11166
  const now = deps.now();
10975
11167
  for (const session of sessions.values()) {
10976
- while (session.events[0] !== void 0 && now - session.events[0].storedAt > RC_SELF_HOST_RETENTION_MS) {
10977
- const dropped = session.events.shift();
10978
- const uuid = dropped === void 0 ? void 0 : nonEmptyString3(dropped.payload, "uuid");
11168
+ for (const [clientId, lastSeenAt] of session.clients) {
11169
+ if (now - lastSeenAt > RC_SELF_HOST_RETENTION_MS) {
11170
+ session.clients.delete(clientId);
11171
+ }
11172
+ }
11173
+ const holdsStream = [...session.conversation.workerStreams, ...session.conversation.clientStreams].some((sink) => sink.owner === session.id);
11174
+ if (!holdsStream && now - session.lastTrafficAt > RC_SELF_HOST_RETENTION_MS) {
11175
+ retireSession(session);
11176
+ }
11177
+ }
11178
+ for (const conversation of conversations.values()) {
11179
+ while (conversation.events[0] !== void 0 && now - conversation.events[0].storedAt > RC_SELF_HOST_RETENTION_MS) {
11180
+ const dropped = conversation.events.shift();
11181
+ const uuid = dropped === void 0 ? void 0 : nonEmptyString4(dropped.payload, "uuid");
10979
11182
  if (uuid !== void 0) {
10980
- session.seenPayloadUuids.delete(uuid);
11183
+ conversation.seenPayloadUuids.delete(uuid);
10981
11184
  }
10982
11185
  }
10983
- for (const [clientId, lastSeenAt] of session.clients) {
11186
+ for (const [clientId, lastSeenAt] of conversation.clients) {
10984
11187
  if (now - lastSeenAt > RC_SELF_HOST_RETENTION_MS) {
10985
- session.clients.delete(clientId);
11188
+ conversation.clients.delete(clientId);
10986
11189
  }
10987
11190
  }
10988
- if (session.workerStreams.size === 0 && session.clientStreams.size === 0 && now - session.lastTrafficAt > RC_SELF_HOST_RETENTION_MS) {
10989
- sessions.delete(session.id);
11191
+ if (conversation.sessionIds.size === 0 && conversation.workerStreams.size === 0 && conversation.clientStreams.size === 0 && now - conversation.lastTrafficAt > RC_SELF_HOST_RETENTION_MS) {
11192
+ conversations.delete(conversation.id);
10990
11193
  }
10991
11194
  }
10992
11195
  };
10993
11196
  const timer = setInterval(sweep, RC_SELF_HOST_RETENTION_MS);
10994
11197
  timer.unref();
10995
11198
  const keepalive = setInterval(() => {
10996
- for (const session of sessions.values()) {
10997
- for (const sink of [...session.workerStreams, ...session.clientStreams]) {
11199
+ for (const conversation of conversations.values()) {
11200
+ for (const sink of [...conversation.workerStreams, ...conversation.clientStreams]) {
10998
11201
  sink.write(": keep-alive\n\n");
10999
11202
  }
11000
11203
  }
@@ -11023,7 +11226,7 @@ function createRcSelfHostSurface(deps) {
11023
11226
  const frameOf = (event) => {
11024
11227
  const envelope = {
11025
11228
  event_id: event.eventId,
11026
- event_type: nonEmptyString3(event.payload, "type") ?? "event",
11229
+ event_type: nonEmptyString4(event.payload, "type") ?? "event",
11027
11230
  sequence_num: event.sequenceNum,
11028
11231
  source: event.source,
11029
11232
  payload: event.payload,
@@ -11035,14 +11238,16 @@ data: ${JSON.stringify(envelope)}
11035
11238
 
11036
11239
  `;
11037
11240
  };
11038
- const holdStream = async (session, which, wantsReplay, request4, response) => {
11241
+ const reachesWorkerStream = (event, owner) => !(event.writer.half === "worker" && event.writer.session === owner);
11242
+ const holdStream = async (conversation, owner, which, wantsReplay, request4, response) => {
11039
11243
  const url = new URL(request4.url, "http://127.0.0.1");
11040
11244
  const cursor = numberOf2({ value: url.searchParams.get("from_sequence_num") }, "value") ?? numberOf2({ value: singleHeader2(request4.headers, "last-event-id") }, "value");
11041
- const replay = cursor === void 0 ? [] : session.events.filter((event) => wantsReplay(event) && event.sequenceNum > cursor);
11245
+ const replay = cursor === void 0 ? [] : conversation.events.filter((event) => wantsReplay(event) && event.sequenceNum > cursor);
11042
11246
  response.start(HTTP_STATUS.ok, { "content-type": "text/event-stream", "cache-control": "no-cache" });
11043
11247
  response.flush();
11044
11248
  let pending = Promise.resolve();
11045
11249
  const sink = {
11250
+ owner,
11046
11251
  write: (frame) => {
11047
11252
  pending = pending.then(async () => {
11048
11253
  await response.write(frame);
@@ -11050,11 +11255,11 @@ data: ${JSON.stringify(envelope)}
11050
11255
  });
11051
11256
  },
11052
11257
  retire: () => {
11053
- session[which].delete(sink);
11258
+ conversation[which].delete(sink);
11054
11259
  }
11055
11260
  };
11056
- session[which].add(sink);
11057
- session.lastTrafficAt = deps.now();
11261
+ conversation[which].add(sink);
11262
+ conversation.lastTrafficAt = deps.now();
11058
11263
  for (const event of replay) {
11059
11264
  sink.write(frameOf(event));
11060
11265
  }
@@ -11066,36 +11271,37 @@ data: ${JSON.stringify(envelope)}
11066
11271
  sink.retire();
11067
11272
  response.end();
11068
11273
  };
11069
- const publish = (session, source, payload, toWorker = source === RC_SOURCE_CLIENT) => {
11070
- const uuid = nonEmptyString3(payload, "uuid");
11071
- const duplicate = uuid !== void 0 && session.seenPayloadUuids.has(uuid);
11072
- session.sequenceNum += 1;
11274
+ const publish = (conversation, writer, source, payload) => {
11275
+ const uuid = nonEmptyString4(payload, "uuid");
11276
+ const duplicate = uuid !== void 0 && conversation.seenPayloadUuids.has(uuid);
11277
+ conversation.sequenceNum += 1;
11073
11278
  const event = {
11074
- sequenceNum: session.sequenceNum,
11279
+ sequenceNum: conversation.sequenceNum,
11075
11280
  eventId: uuid ?? deps.newUuid(),
11076
11281
  source,
11077
11282
  // The one enrichment the real service was observed making on the live rig's stream: the wall-clock instant it took the write. The worker's handler ignores it; the door's own parser forwards it verbatim.
11078
11283
  payload: source === RC_SOURCE_WORKER ? payload : { ...payload, server_received_wall_ms: deps.now() },
11079
11284
  createdAt: new Date(deps.now()).toISOString(),
11080
- toWorker,
11285
+ writer,
11081
11286
  storedAt: deps.now()
11082
11287
  };
11083
- session.events.push(event);
11288
+ conversation.events.push(event);
11084
11289
  if (uuid !== void 0) {
11085
- session.seenPayloadUuids.add(uuid);
11290
+ conversation.seenPayloadUuids.add(uuid);
11086
11291
  }
11087
- session.lastTrafficAt = event.storedAt;
11292
+ conversation.lastTrafficAt = event.storedAt;
11088
11293
  const frame = frameOf(event);
11089
- if (event.toWorker) {
11090
- for (const sink of [...session.workerStreams]) {
11294
+ for (const sink of [...conversation.workerStreams]) {
11295
+ if (reachesWorkerStream(event, sink.owner)) {
11091
11296
  sink.write(frame);
11092
11297
  }
11093
11298
  }
11094
- for (const sink of [...session.clientStreams]) {
11299
+ for (const sink of [...conversation.clientStreams]) {
11095
11300
  sink.write(frame);
11096
11301
  }
11097
11302
  return { sequenceNum: event.sequenceNum, eventId: event.eventId, duplicate };
11098
11303
  };
11304
+ const sessionRow = (session) => ({ id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active", conversation_id: session.conversation.id });
11099
11305
  const answerJson2 = async (response, status, body, extraHeaders = {}) => {
11100
11306
  const text = JSON.stringify(body);
11101
11307
  response.start(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text, "utf8")), ...extraHeaders });
@@ -11103,7 +11309,7 @@ data: ${JSON.stringify(envelope)}
11103
11309
  response.end();
11104
11310
  };
11105
11311
  const noCredential = async (response) => {
11106
- await answerJson2(response, HTTP_SERVICE_UNAVAILABLE, { type: "error", error: { type: "api_error", message: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it; run agent-shim frontdoor rc selfhost mint first" } });
11312
+ await answerJson2(response, HTTP_SERVICE_UNAVAILABLE2, { type: "error", error: { type: "api_error", message: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it; run agent-shim frontdoor rc selfhost mint first" } });
11107
11313
  };
11108
11314
  const epochStale = async (response) => {
11109
11315
  await answerJson2(response, HTTP_CONFLICT, { type: "error", error: { type: "api_error", message: "worker_epoch does not match the session's live epoch" } }, { [CONFLICT_REASON_HEADER2]: CONFLICT_REASON_EPOCH_STALE });
@@ -11131,8 +11337,8 @@ data: ${JSON.stringify(envelope)}
11131
11337
  await unauthorized(response, refusal);
11132
11338
  return;
11133
11339
  }
11134
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11135
- const title = nonEmptyString3(body, "title");
11340
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11341
+ const title = nonEmptyString4(body, "title");
11136
11342
  if (title === void 0) {
11137
11343
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a session update is JSON naming a non-empty title" } });
11138
11344
  return;
@@ -11147,7 +11353,7 @@ data: ${JSON.stringify(envelope)}
11147
11353
  await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the session read is a GET, and its update a PATCH or PUT" } });
11148
11354
  return;
11149
11355
  }
11150
- await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
11356
+ await answerJson2(response, HTTP_STATUS.ok, { session: sessionRow(session) });
11151
11357
  return;
11152
11358
  }
11153
11359
  if (tail === "bridge" && method === "POST") {
@@ -11168,10 +11374,7 @@ data: ${JSON.stringify(envelope)}
11168
11374
  await unauthorized(response, refusal);
11169
11375
  return;
11170
11376
  }
11171
- for (const sink of [...session.workerStreams, ...session.clientStreams]) {
11172
- sink.retire();
11173
- }
11174
- sessions.delete(session.id);
11377
+ retireSession(session);
11175
11378
  deps.log?.(`rc selfhost ${session.id}: archived at its client's request`);
11176
11379
  await answerJson2(response, HTTP_STATUS.ok, {});
11177
11380
  return;
@@ -11189,17 +11392,17 @@ data: ${JSON.stringify(envelope)}
11189
11392
  return;
11190
11393
  }
11191
11394
  if (workerTail === "" && method === "PUT") {
11192
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11395
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11193
11396
  const claimedEpoch = numberOf2(body, "worker_epoch");
11194
11397
  if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
11195
11398
  await epochStale(response);
11196
11399
  return;
11197
11400
  }
11198
- const status = nonEmptyString3(body, "worker_status");
11401
+ const status = nonEmptyString4(body, "worker_status");
11199
11402
  if (status !== void 0) {
11200
11403
  session.workerStatus = status;
11201
11404
  }
11202
- if (isRecord8(body?.external_metadata)) {
11405
+ if (isRecord10(body?.external_metadata)) {
11203
11406
  session.externalMetadata = body.external_metadata;
11204
11407
  }
11205
11408
  await answerJson2(response, HTTP_STATUS.ok, {});
@@ -11216,7 +11419,7 @@ data: ${JSON.stringify(envelope)}
11216
11419
  return;
11217
11420
  }
11218
11421
  if (workerTail === "events" && method === "POST") {
11219
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11422
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11220
11423
  const claimedEpoch = numberOf2(body, "worker_epoch");
11221
11424
  if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
11222
11425
  await epochStale(response);
@@ -11229,11 +11432,11 @@ data: ${JSON.stringify(envelope)}
11229
11432
  }
11230
11433
  const results = [];
11231
11434
  for (const event of events) {
11232
- if (!isRecord8(event) || !isRecord8(event.payload)) {
11435
+ if (!isRecord10(event) || !isRecord10(event.payload)) {
11233
11436
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "every event in a batch is an object carrying one payload object" } });
11234
11437
  return;
11235
11438
  }
11236
- const published = publish(session, RC_SOURCE_WORKER, event.payload);
11439
+ const published = publish(session.conversation, { session: session.id, half: "worker" }, RC_SOURCE_WORKER, event.payload);
11237
11440
  results.push({ event_id: published.eventId, sequence_num: String(published.sequenceNum) });
11238
11441
  }
11239
11442
  sweep();
@@ -11241,7 +11444,7 @@ data: ${JSON.stringify(envelope)}
11241
11444
  return;
11242
11445
  }
11243
11446
  if (workerTail === "events/stream" && method === "GET") {
11244
- await holdStream(session, "workerStreams", (event) => event.toWorker, request4, response);
11447
+ await holdStream(session.conversation, session.id, "workerStreams", (event) => reachesWorkerStream(event, session.id), request4, response);
11245
11448
  return;
11246
11449
  }
11247
11450
  if (workerTail === "events/delivery" && method === "POST") {
@@ -11250,7 +11453,7 @@ data: ${JSON.stringify(envelope)}
11250
11453
  return;
11251
11454
  }
11252
11455
  if (workerTail === "internal-events" && method === "POST") {
11253
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11456
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11254
11457
  const claimedEpoch = numberOf2(body, "worker_epoch");
11255
11458
  if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
11256
11459
  await epochStale(response);
@@ -11262,7 +11465,7 @@ data: ${JSON.stringify(envelope)}
11262
11465
  return;
11263
11466
  }
11264
11467
  for (const event of events) {
11265
- if (isRecord8(event)) {
11468
+ if (isRecord10(event)) {
11266
11469
  session.internalEvents.push({ eventId: deps.newUuid(), body: event });
11267
11470
  }
11268
11471
  }
@@ -11290,8 +11493,8 @@ data: ${JSON.stringify(envelope)}
11290
11493
  return;
11291
11494
  }
11292
11495
  if (workerTail === "web-fetch" && method === "POST") {
11293
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11294
- const target = nonEmptyString3(body, "url");
11496
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11497
+ const target = nonEmptyString4(body, "url");
11295
11498
  if (target === void 0) {
11296
11499
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-fetch carries one url" } });
11297
11500
  return;
@@ -11313,8 +11516,8 @@ data: ${JSON.stringify(envelope)}
11313
11516
  return;
11314
11517
  }
11315
11518
  if (workerTail === "web-search" && method === "POST") {
11316
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11317
- const query = nonEmptyString3(body, "query");
11519
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11520
+ const query = nonEmptyString4(body, "query");
11318
11521
  if (query === void 0) {
11319
11522
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-search carries one query" } });
11320
11523
  return;
@@ -11325,7 +11528,7 @@ data: ${JSON.stringify(envelope)}
11325
11528
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "the domain filters of a worker web-search are string arrays" } });
11326
11529
  return;
11327
11530
  }
11328
- const searchProfile = nonEmptyString3(body, "search_profile");
11531
+ const searchProfile = nonEmptyString4(body, "search_profile");
11329
11532
  const backend = deps.webSearch;
11330
11533
  if (backend === void 0) {
11331
11534
  deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" refused: no search backend serves this door`);
@@ -11350,7 +11553,7 @@ data: ${JSON.stringify(envelope)}
11350
11553
  return;
11351
11554
  }
11352
11555
  if (tail === "events" && method === "POST") {
11353
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11556
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11354
11557
  const events = body?.events;
11355
11558
  if (!Array.isArray(events) || events.length === 0 || events.length > RC_SELF_HOST_MAX_BATCH_EVENTS) {
11356
11559
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: `a client event write is a non-empty array of at most ${String(RC_SELF_HOST_MAX_BATCH_EVENTS)} events` } });
@@ -11358,11 +11561,11 @@ data: ${JSON.stringify(envelope)}
11358
11561
  }
11359
11562
  const results = [];
11360
11563
  for (const event of events) {
11361
- if (!isRecord8(event) || !isRecord8(event.payload)) {
11564
+ if (!isRecord10(event) || !isRecord10(event.payload)) {
11362
11565
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "every event in a write is an object carrying one payload object" } });
11363
11566
  return;
11364
11567
  }
11365
- const published = publish(session, RC_SOURCE_CLIENT, event.payload);
11568
+ const published = publish(session.conversation, { session: session.id, half: "client" }, RC_SOURCE_CLIENT, event.payload);
11366
11569
  results.push({ sequence_num: String(published.sequenceNum), duplicate: published.duplicate });
11367
11570
  }
11368
11571
  sweep();
@@ -11370,14 +11573,14 @@ data: ${JSON.stringify(envelope)}
11370
11573
  return;
11371
11574
  }
11372
11575
  if (tail === "events" && method === "GET") {
11373
- await answerJson2(response, HTTP_STATUS.ok, { data: session.events.map((event) => ({ event_id: event.eventId, event_type: nonEmptyString3(event.payload, "type") ?? "event", sequence_num: event.sequenceNum, source: event.source, payload: event.payload, created_at: event.createdAt })) });
11576
+ await answerJson2(response, HTTP_STATUS.ok, { data: session.conversation.events.map((event) => ({ event_id: event.eventId, event_type: nonEmptyString4(event.payload, "type") ?? "event", sequence_num: event.sequenceNum, source: event.source, payload: event.payload, created_at: event.createdAt })) });
11374
11577
  return;
11375
11578
  }
11376
11579
  if (tail === "events/stream" && method === "GET") {
11377
- await holdStream(session, "clientStreams", () => true, request4, response);
11580
+ await holdStream(session.conversation, session.id, "clientStreams", () => true, request4, response);
11378
11581
  return;
11379
11582
  }
11380
- if (await serveRcClientChannels({ request: request4, response, session, answerJson: answerJson2, readBody: readBody2, publish, sweep, now: deps.now, presenceRefreshSeconds: RC_PRESENCE_REFRESH_SECONDS, ...deps.log === void 0 ? {} : { log: deps.log } }, tail, method, RC_SELF_HOST_MAX_BATCH_EVENTS)) {
11583
+ if (await serveRcClientChannels({ request: request4, response, host: { id: session.id, clients: session.clients, conversation: session.conversation, writer: { session: session.id, half: "client" } }, answerJson: answerJson2, readBody: readBody2, publish, sweep, now: deps.now, presenceRefreshSeconds: RC_PRESENCE_REFRESH_SECONDS, ...deps.log === void 0 ? {} : { log: deps.log } }, tail, method, RC_SELF_HOST_MAX_BATCH_EVENTS)) {
11381
11584
  return;
11382
11585
  }
11383
11586
  await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no path "${tail}"` } });
@@ -11395,7 +11598,11 @@ data: ${JSON.stringify(envelope)}
11395
11598
  return;
11396
11599
  }
11397
11600
  if (pathname === RC_COMPAT_SESSIONS_PATH || pathname.startsWith(`${RC_COMPAT_SESSIONS_PATH}/`)) {
11398
- await serveRcCompatSessions({ request: request4, response, sessions, answerJson: answerJson2, readBody: readBody2, credentialRefusal, unauthorized, now: deps.now, ...deps.log === void 0 ? {} : { log: deps.log } }, pathname, RC_COMPAT_SESSIONS_PATH);
11601
+ await serveRcCompatSessions({ request: request4, response, sessions, retireSession, answerJson: answerJson2, readBody: readBody2, credentialRefusal, unauthorized, now: deps.now, ...deps.log === void 0 ? {} : { log: deps.log } }, pathname, RC_COMPAT_SESSIONS_PATH);
11602
+ return;
11603
+ }
11604
+ if (pathname === RC_CONVERSATIONS_PATH_PREFIX || pathname.startsWith(`${RC_CONVERSATIONS_PATH_PREFIX}/`)) {
11605
+ await serveRcConversations({ request: request4, response, conversations, answerJson: answerJson2, readBody: readBody2, credentialRefusal, unauthorized, publish, holdStream, sweep, now: deps.now, presenceRefreshSeconds: RC_PRESENCE_REFRESH_SECONDS, maxBatchEvents: RC_SELF_HOST_MAX_BATCH_EVENTS, ...deps.log === void 0 ? {} : { log: deps.log } }, pathname, method);
11399
11606
  return;
11400
11607
  }
11401
11608
  if (pathname !== RC_SESSIONS_PATH_PREFIX2 && !pathname.startsWith(`${RC_SESSIONS_PATH_PREFIX2}/`)) {
@@ -11409,26 +11616,36 @@ data: ${JSON.stringify(envelope)}
11409
11616
  await unauthorized(response, createRefusal);
11410
11617
  return;
11411
11618
  }
11412
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11619
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11620
+ const namedConversation = nonEmptyString4(body, "conversation");
11621
+ let conversation;
11622
+ if (namedConversation === void 0) {
11623
+ conversation = newRcSelfHostConversation(deps.newUuid, deps.now);
11624
+ conversations.set(conversation.id, conversation);
11625
+ } else {
11626
+ conversation = conversations.get(namedConversation);
11627
+ if (conversation === void 0) {
11628
+ await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `no such Remote Control conversation on this door: ${namedConversation}` } });
11629
+ return;
11630
+ }
11631
+ conversation.lastTrafficAt = deps.now();
11632
+ }
11413
11633
  const session = {
11414
11634
  id: `${RC_SESSION_ID_PREFIX2}${deps.newUuid()}`,
11415
11635
  createdAt: deps.now(),
11416
- title: nonEmptyString3(body, "title"),
11636
+ title: nonEmptyString4(body, "title"),
11637
+ conversation,
11417
11638
  workerJwts: /* @__PURE__ */ new Set(),
11418
- sequenceNum: 0,
11419
- events: [],
11420
- seenPayloadUuids: /* @__PURE__ */ new Set(),
11421
- workerStreams: /* @__PURE__ */ new Set(),
11422
- clientStreams: /* @__PURE__ */ new Set(),
11423
11639
  internalEvents: [],
11424
11640
  clients: /* @__PURE__ */ new Map(),
11425
11641
  workerStatus: void 0,
11426
11642
  externalMetadata: void 0,
11427
11643
  lastTrafficAt: deps.now()
11428
11644
  };
11645
+ conversation.sessionIds.add(session.id);
11429
11646
  sessions.set(session.id, session);
11430
- deps.log?.(`rc selfhost ${session.id}: session created on the door's own surface`);
11431
- await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
11647
+ deps.log?.(`rc selfhost ${session.id}: session created on the door's own surface, attached to conversation ${conversation.id}${namedConversation === void 0 ? "" : " at its client's request"}`);
11648
+ await answerJson2(response, HTTP_STATUS.ok, { session: sessionRow(session) });
11432
11649
  return;
11433
11650
  }
11434
11651
  if (method === "GET") {
@@ -11437,7 +11654,7 @@ data: ${JSON.stringify(envelope)}
11437
11654
  await unauthorized(response, readRefusal);
11438
11655
  return;
11439
11656
  }
11440
- await answerJson2(response, HTTP_STATUS.ok, { data: [...sessions.values()].map((session) => ({ id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), updated_at: new Date(session.lastTrafficAt).toISOString(), session_status: "active" })) });
11657
+ await answerJson2(response, HTTP_STATUS.ok, { data: [...sessions.values()].map((session) => ({ id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), updated_at: new Date(session.lastTrafficAt).toISOString(), session_status: "active", conversation_id: session.conversation.id })) });
11441
11658
  return;
11442
11659
  }
11443
11660
  await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the session collection is a POST (create) and a GET (list)" } });
@@ -11454,92 +11671,20 @@ data: ${JSON.stringify(envelope)}
11454
11671
  await serveSessionPath(sessions.get(id), tail, request4, response);
11455
11672
  }
11456
11673
  };
11457
- const answerServerJson = (response, status, body) => {
11458
- const text = JSON.stringify(body);
11459
- response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text, "utf8")) });
11460
- response.end(text);
11461
- };
11462
- const local = {
11463
- parsesHost: (host) => host === RC_SELF_HOST_OAUTH_HOST,
11464
- serve: async (host, request4, response) => {
11465
- const method = (request4.method ?? "").toUpperCase();
11466
- const pathname = new URL(request4.url ?? "/", "http://127.0.0.1").pathname;
11467
- if (host === RC_SELF_HOST_OAUTH_HOST) {
11468
- if (method === "POST" && pathname === "/v1/oauth/token") {
11469
- const body = parseJsonObject3(await readBody2(request4, CONTROL_BODY_CAP_BYTES));
11470
- const record = deps.credentialRecord();
11471
- if (record === void 0) {
11472
- answerServerJson(response, HTTP_SERVICE_UNAVAILABLE, { error: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it" });
11473
- return true;
11474
- }
11475
- if (body?.grant_type !== void 0 && body.grant_type !== "refresh_token") {
11476
- answerServerJson(response, HTTP_STATUS.badRequest, { error: "unsupported_grant_type" });
11477
- return true;
11478
- }
11479
- answerServerJson(response, HTTP_STATUS.ok, { access_token: record.accessToken, refresh_token: record.refreshToken, expires_in: RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS, scope: RC_SELF_HOST_SCOPE_LIST.join(" ") });
11480
- return true;
11481
- }
11482
- if (method === "GET" && pathname === "/v1/oauth/hello") {
11483
- answerServerJson(response, HTTP_STATUS.ok, {});
11484
- return true;
11485
- }
11486
- return false;
11487
- }
11488
- if (method === "POST" && pathname.startsWith("/api/eval/")) {
11489
- await readBody2(request4, CONTROL_BODY_CAP_BYTES);
11490
- answerServerJson(response, HTTP_STATUS.ok, { features: growthBookFeatures(), dateUpdated: Math.floor(deps.now() / MS_PER_SECOND5) });
11491
- return true;
11492
- }
11493
- if (method === "GET" && pathname === "/api/oauth/profile") {
11494
- const record = deps.credentialRecord();
11495
- if (record === void 0) {
11496
- answerServerJson(response, HTTP_SERVICE_UNAVAILABLE, { error: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it" });
11497
- return true;
11498
- }
11499
- answerServerJson(response, HTTP_STATUS.ok, {
11500
- account: { uuid: record.accountUuid, display_name: "agent-shim self-hosted", created_at: (/* @__PURE__ */ new Date(0)).toISOString() },
11501
- organization: { uuid: record.organizationUuid, organization_type: "claude_max" }
11502
- });
11503
- return true;
11504
- }
11505
- if (method === "GET" && pathname === "/api/claude_code/policy_limits") {
11506
- answerServerJson(response, HTTP_STATUS.ok, {});
11507
- return true;
11508
- }
11509
- if (method === "POST" && pathname === "/api/oauth/validate") {
11510
- await readBody2(request4, CONTROL_BODY_CAP_BYTES);
11511
- const record = deps.credentialRecord();
11512
- if (record === void 0) {
11513
- answerServerJson(response, HTTP_SERVICE_UNAVAILABLE, { error: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it" });
11514
- return true;
11515
- }
11516
- answerServerJson(response, HTTP_STATUS.ok, { scopes: [...RC_SELF_HOST_SCOPE_LIST], expiresAt: null, subscriptionType: "max", account_uuid: record.accountUuid, organization_uuid: record.organizationUuid });
11517
- return true;
11518
- }
11519
- if (method === "POST" && (pathname === "/api/event_logging/v2/batch" || pathname === "/api/claude_code/metrics" || pathname === "/api/claude_cli_feedback")) {
11520
- await readBody2(request4, CONTROL_BODY_CAP_BYTES);
11521
- answerServerJson(response, HTTP_STATUS.ok, {});
11522
- return true;
11523
- }
11524
- if (method === "GET" && pathname === "/api/hello") {
11525
- answerServerJson(response, HTTP_STATUS.ok, {});
11526
- return true;
11527
- }
11528
- return false;
11529
- }
11530
- };
11674
+ const local = createRcSelfHostLocal({ now: deps.now, credentialRecord: deps.credentialRecord, readBody: readBody2 });
11531
11675
  return {
11532
11676
  route,
11533
11677
  local,
11534
11678
  close: () => {
11535
11679
  clearInterval(timer);
11536
11680
  clearInterval(keepalive);
11537
- for (const session of sessions.values()) {
11538
- for (const sink of [...session.workerStreams, ...session.clientStreams]) {
11681
+ for (const conversation of conversations.values()) {
11682
+ for (const sink of [...conversation.workerStreams, ...conversation.clientStreams]) {
11539
11683
  sink.retire();
11540
11684
  }
11541
11685
  }
11542
11686
  sessions.clear();
11687
+ conversations.clear();
11543
11688
  }
11544
11689
  };
11545
11690
  }
@@ -11552,7 +11697,7 @@ var RC_SELF_HOST_RECORD_DIR = "rc-selfhost";
11552
11697
  var RC_SELF_HOST_RECORD_FILE = "credential.json";
11553
11698
  var SELF_HOST_EMAIL = "selfhost@agent-shim.invalid";
11554
11699
  var SELF_HOST_SUBSCRIPTION_TYPE = "max";
11555
- function isRecord9(value) {
11700
+ function isRecord11(value) {
11556
11701
  return typeof value === "object" && value !== null && !Array.isArray(value);
11557
11702
  }
11558
11703
  function readJsonObject(fs12, file) {
@@ -11566,7 +11711,7 @@ function readJsonObject(fs12, file) {
11566
11711
  } catch {
11567
11712
  return void 0;
11568
11713
  }
11569
- return isRecord9(parsed) ? parsed : void 0;
11714
+ return isRecord11(parsed) ? parsed : void 0;
11570
11715
  }
11571
11716
  function readRcSelfHostRecord(fs12, frontdoorDir) {
11572
11717
  const parsed = readJsonObject(fs12, path28.join(frontdoorDir, RC_SELF_HOST_RECORD_DIR, RC_SELF_HOST_RECORD_FILE));
@@ -11590,7 +11735,7 @@ function mintRcSelfHostCredential(deps) {
11590
11735
  const recordFile = path28.join(recordDir, RC_SELF_HOST_RECORD_FILE);
11591
11736
  const previousRecord = readRcSelfHostRecord(deps.fs, deps.frontdoorDir);
11592
11737
  const existingCredentials = readJsonObject(deps.fs, credentialsFile);
11593
- const existingOauth = isRecord9(existingCredentials?.claudeAiOauth) ? existingCredentials.claudeAiOauth : void 0;
11738
+ const existingOauth = isRecord11(existingCredentials?.claudeAiOauth) ? existingCredentials.claudeAiOauth : void 0;
11594
11739
  const existingToken = typeof existingOauth?.accessToken === "string" && existingOauth.accessToken !== "" ? existingOauth.accessToken : void 0;
11595
11740
  const existingIsOwn = existingToken !== void 0 && previousRecord?.accessToken === existingToken;
11596
11741
  if (existingToken !== void 0 && !existingIsOwn && !deps.force) {
@@ -11607,7 +11752,7 @@ function mintRcSelfHostCredential(deps) {
11607
11752
  deps.fs.writeFilePrivate(credentialsFile, `${JSON.stringify({ ...existingCredentials, claudeAiOauth: { accessToken, refreshToken, expiresAt: null, scopes: [...RC_SELF_HOST_SCOPE_LIST], subscriptionType: SELF_HOST_SUBSCRIPTION_TYPE, rateLimitTier: null } }, null, 2)}
11608
11753
  `);
11609
11754
  const claudeJson = readJsonObject(deps.fs, claudeJsonFile) ?? {};
11610
- const existingBlock = (value) => isRecord9(value) ? value : {};
11755
+ const existingBlock = (value) => isRecord11(value) ? value : {};
11611
11756
  deps.fs.writeFilePrivate(claudeJsonFile, `${JSON.stringify({
11612
11757
  ...claudeJson,
11613
11758
  oauthAccount: {
@@ -11806,7 +11951,7 @@ import path30 from "node:path";
11806
11951
  // package.json
11807
11952
  var package_default = {
11808
11953
  name: "agent-shim",
11809
- version: "8.26.0",
11954
+ version: "8.27.1",
11810
11955
  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.",
11811
11956
  license: "MIT",
11812
11957
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -12222,6 +12367,11 @@ function buildFlagArgs(flags) {
12222
12367
  function buildArgv(params) {
12223
12368
  return [...params.toolFlags, ...params.extraFlags, ...params.passthrough];
12224
12369
  }
12370
+ var LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "::1"];
12371
+ function withLoopbackHosts(existing) {
12372
+ const listed = (existing ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
12373
+ return [...listed, ...LOOPBACK_HOSTS.filter((host) => !listed.includes(host))].join(",");
12374
+ }
12225
12375
  function buildEnv(params) {
12226
12376
  const env = { ...params.baseEnv };
12227
12377
  const parentCustomHeaders = params.baseEnv.ANTHROPIC_CUSTOM_HEADERS;
@@ -12266,6 +12416,8 @@ function buildEnv(params) {
12266
12416
  );
12267
12417
  env.NODE_EXTRA_CA_CERTS = params.frontdoor.trustBundlePath;
12268
12418
  env.HTTPS_PROXY = connectProxyUrl(params.frontdoor.connectPort, params.frontdoor.sessionToken);
12419
+ env.NO_PROXY = withLoopbackHosts(env.NO_PROXY);
12420
+ env.no_proxy = withLoopbackHosts(env.no_proxy);
12269
12421
  }
12270
12422
  return env;
12271
12423
  }