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.cjs CHANGED
@@ -10762,7 +10762,7 @@ function pageOfRcChannel(url, rows, idOf, rowOf) {
10762
10762
  return { data: page.map(rowOf), ...last === void 0 ? {} : { next_cursor: idOf(last) } };
10763
10763
  }
10764
10764
  async function serveRcClientChannels(ctx, tail, method, maxBatchEvents) {
10765
- const { request: request4, response, session } = ctx;
10765
+ const { request: request4, response, host } = ctx;
10766
10766
  if (tail === "client/presence" && method === "POST") {
10767
10767
  const body = parseJsonObject2(await ctx.readBody(request4.body, CONTROL_BODY_CAP_BYTES));
10768
10768
  const clientId = nonEmptyString2(body, "client_id");
@@ -10772,11 +10772,11 @@ async function serveRcClientChannels(ctx, tail, method, maxBatchEvents) {
10772
10772
  return true;
10773
10773
  }
10774
10774
  if (clear === true) {
10775
- session.clients.delete(clientId);
10776
- ctx.log?.(`rc selfhost ${session.id}: client ${clientId} cleared its presence`);
10775
+ host.clients.delete(clientId);
10776
+ ctx.log?.(`rc selfhost ${host.id}: client ${clientId} cleared its presence`);
10777
10777
  } else {
10778
- session.clients.set(clientId, ctx.now());
10779
- ctx.log?.(`rc selfhost ${session.id}: client ${clientId} announced its presence${session.clients.size === 1 ? "" : ` (${String(session.clients.size)} clients now announced)`}`);
10778
+ host.clients.set(clientId, ctx.now());
10779
+ ctx.log?.(`rc selfhost ${host.id}: client ${clientId} announced its presence${host.clients.size === 1 ? "" : ` (${String(host.clients.size)} clients now announced)`}`);
10780
10780
  }
10781
10781
  await ctx.answerJson(response, HTTP_STATUS.ok, { refresh_after_seconds: ctx.presenceRefreshSeconds });
10782
10782
  return true;
@@ -10789,8 +10789,8 @@ async function serveRcClientChannels(ctx, tail, method, maxBatchEvents) {
10789
10789
  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" } });
10790
10790
  return true;
10791
10791
  }
10792
- 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);
10793
- 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)}`);
10792
+ 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 } });
10793
+ 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)}`);
10794
10794
  await ctx.answerJson(response, HTTP_STATUS.ok, {});
10795
10795
  return true;
10796
10796
  }
@@ -10807,16 +10807,16 @@ async function serveRcClientChannels(ctx, tail, method, maxBatchEvents) {
10807
10807
  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" } });
10808
10808
  return true;
10809
10809
  }
10810
- const published = ctx.publish(session, RC_SOURCE_CLIENT, event.payload);
10810
+ const published = ctx.publish(host.conversation, host.writer, RC_SOURCE_CLIENT, event.payload);
10811
10811
  results.push({ sequence_num: String(published.sequenceNum), duplicate: published.duplicate });
10812
10812
  }
10813
10813
  ctx.sweep();
10814
- ctx.log?.(`rc selfhost ${session.id}: teleport write published ${String(results.length)} marker event(s)`);
10814
+ ctx.log?.(`rc selfhost ${host.id}: teleport write published ${String(results.length)} marker event(s)`);
10815
10815
  await ctx.answerJson(response, HTTP_STATUS.ok, { results });
10816
10816
  return true;
10817
10817
  }
10818
10818
  if (tail === "teleport-events" && method === "GET") {
10819
- const page = pageOfRcChannel(request4.url, session.events, (event) => event.eventId, (event) => ({ event_id: event.eventId, payload: event.payload }));
10819
+ const page = pageOfRcChannel(request4.url, host.conversation.events, (event) => event.eventId, (event) => ({ event_id: event.eventId, payload: event.payload }));
10820
10820
  if ("error" in page) {
10821
10821
  await ctx.answerJson(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: page.error } });
10822
10822
  return true;
@@ -10892,10 +10892,7 @@ async function serveRcCompatSessions(ctx, pathname, compatPath) {
10892
10892
  await ctx.answerJson(response, HTTP_STATUS.ok, compatSessionRow(session, ctx.now));
10893
10893
  return;
10894
10894
  }
10895
- for (const sink of [...session.workerStreams, ...session.clientStreams]) {
10896
- sink.retire();
10897
- }
10898
- ctx.sessions.delete(session.id);
10895
+ ctx.retireSession(session);
10899
10896
  ctx.log?.(`rc selfhost ${session.id}: archived through the compatibility family`);
10900
10897
  await ctx.answerJson(response, HTTP_STATUS.ok, {});
10901
10898
  return;
@@ -10903,6 +10900,202 @@ async function serveRcCompatSessions(ctx, pathname, compatPath) {
10903
10900
  await ctx.answerJson(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the compatibility session family serves no path "${compatTail}"` } });
10904
10901
  }
10905
10902
 
10903
+ // src/frontdoor/rcSelfHostConversations.ts
10904
+ var RC_CONVERSATIONS_PATH_PREFIX = "/v1/code/conversations";
10905
+ var RC_CONVERSATION_ID_PREFIX = "conv_";
10906
+ function isRecord8(value) {
10907
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10908
+ }
10909
+ function parseJsonObject3(body) {
10910
+ let parsed;
10911
+ try {
10912
+ parsed = JSON.parse(body);
10913
+ } catch {
10914
+ return void 0;
10915
+ }
10916
+ return isRecord8(parsed) ? parsed : void 0;
10917
+ }
10918
+ function nonEmptyString3(record, field) {
10919
+ const value = record === void 0 ? void 0 : record[field];
10920
+ return typeof value === "string" && value !== "" ? value : void 0;
10921
+ }
10922
+ function newRcSelfHostConversation(newUuid, now) {
10923
+ 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() };
10924
+ }
10925
+ function conversationRow(conversation) {
10926
+ return { id: conversation.id, created_at: new Date(conversation.createdAt).toISOString(), updated_at: new Date(conversation.lastTrafficAt).toISOString(), sessions: [...conversation.sessionIds] };
10927
+ }
10928
+ async function serveRcConversations(ctx, pathname, method) {
10929
+ const { request: request4, response, conversations } = ctx;
10930
+ const refusal = ctx.credentialRefusal(request4.headers);
10931
+ if (refusal !== void 0) {
10932
+ await ctx.unauthorized(response, refusal);
10933
+ return;
10934
+ }
10935
+ if (pathname === RC_CONVERSATIONS_PATH_PREFIX) {
10936
+ if (method !== "GET") {
10937
+ 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" } });
10938
+ return;
10939
+ }
10940
+ 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) });
10941
+ return;
10942
+ }
10943
+ const rest = pathname.slice(`${RC_CONVERSATIONS_PATH_PREFIX}/`.length);
10944
+ const slash = rest.indexOf("/");
10945
+ const conversationId = slash === -1 ? rest : rest.slice(0, slash);
10946
+ const tail = slash === -1 ? "" : rest.slice(slash + 1);
10947
+ if (!conversationId.startsWith(RC_CONVERSATION_ID_PREFIX)) {
10948
+ await ctx.answerJson(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "the conversation family names one conv_-prefixed conversation id" } });
10949
+ return;
10950
+ }
10951
+ const conversation = conversations.get(conversationId);
10952
+ if (conversation === void 0) {
10953
+ await ctx.answerJson(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "no such Remote Control conversation on this door" } });
10954
+ return;
10955
+ }
10956
+ conversation.lastTrafficAt = ctx.now();
10957
+ if (tail === "" && method === "GET") {
10958
+ await ctx.answerJson(response, HTTP_STATUS.ok, { conversation: conversationRow(conversation) });
10959
+ return;
10960
+ }
10961
+ if (tail === "events" && method === "POST") {
10962
+ const body = parseJsonObject3(await ctx.readBody(request4.body, CONTROL_BODY_CAP_BYTES));
10963
+ const events = body?.events;
10964
+ if (!Array.isArray(events) || events.length === 0 || events.length > ctx.maxBatchEvents) {
10965
+ 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` } });
10966
+ return;
10967
+ }
10968
+ const results = [];
10969
+ for (const event of events) {
10970
+ if (!isRecord8(event) || !isRecord8(event.payload)) {
10971
+ 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" } });
10972
+ return;
10973
+ }
10974
+ const published = ctx.publish(conversation, { session: void 0, half: "client" }, RC_SOURCE_CLIENT, event.payload);
10975
+ results.push({ sequence_num: String(published.sequenceNum), duplicate: published.duplicate });
10976
+ }
10977
+ ctx.sweep();
10978
+ ctx.log?.(`rc selfhost ${conversation.id}: a conversation client wrote ${String(results.length)} event(s) into the shared space`);
10979
+ await ctx.answerJson(response, HTTP_STATUS.ok, { results });
10980
+ return;
10981
+ }
10982
+ if (tail === "events" && method === "GET") {
10983
+ 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 })) });
10984
+ return;
10985
+ }
10986
+ if (tail === "events/stream" && method === "GET") {
10987
+ await ctx.holdStream(conversation, conversation.id, "clientStreams", () => true, request4, response);
10988
+ return;
10989
+ }
10990
+ 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)) {
10991
+ return;
10992
+ }
10993
+ await ctx.answerJson(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the conversation family serves no path "${tail}"` } });
10994
+ }
10995
+
10996
+ // src/frontdoor/rcSelfHostLocal.ts
10997
+ var RC_SELF_HOST_OAUTH_HOST = "platform.claude.com";
10998
+ var RC_SELF_HOST_SCOPE_LIST = ["user:profile", "user:inference", "user:sessions:claude_code", "user:mcp_servers", "user:file_upload"];
10999
+ var RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS = 3600;
11000
+ var MS_PER_SECOND5 = 1e3;
11001
+ var HTTP_SERVICE_UNAVAILABLE = 503;
11002
+ function isRecord9(value) {
11003
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11004
+ }
11005
+ function parseJsonObject4(body) {
11006
+ let parsed;
11007
+ try {
11008
+ parsed = JSON.parse(body);
11009
+ } catch {
11010
+ return void 0;
11011
+ }
11012
+ return isRecord9(parsed) ? parsed : void 0;
11013
+ }
11014
+ function growthBookFeatures() {
11015
+ return {
11016
+ tengu_ccr_bridge: { defaultValue: true },
11017
+ tengu_bridge_repl_v2: { defaultValue: true },
11018
+ tengu_bridge_repl_v2_cse_shim_enabled: { defaultValue: true },
11019
+ tengu_bridge_min_version: { defaultValue: { minVersion: "0.0.0" } }
11020
+ };
11021
+ }
11022
+ var answerServerJson = (response, status, body) => {
11023
+ const text = JSON.stringify(body);
11024
+ response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text, "utf8")) });
11025
+ response.end(text);
11026
+ };
11027
+ function createRcSelfHostLocal(deps) {
11028
+ return {
11029
+ parsesHost: (host) => host === RC_SELF_HOST_OAUTH_HOST,
11030
+ serve: async (host, request4, response) => {
11031
+ const method = (request4.method ?? "").toUpperCase();
11032
+ const pathname = new URL(request4.url ?? "/", "http://127.0.0.1").pathname;
11033
+ if (host === RC_SELF_HOST_OAUTH_HOST) {
11034
+ if (method === "POST" && pathname === "/v1/oauth/token") {
11035
+ const body = parseJsonObject4(await deps.readBody(request4, CONTROL_BODY_CAP_BYTES));
11036
+ const record = deps.credentialRecord();
11037
+ if (record === void 0) {
11038
+ 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" });
11039
+ return true;
11040
+ }
11041
+ if (body?.grant_type !== void 0 && body.grant_type !== "refresh_token") {
11042
+ answerServerJson(response, HTTP_STATUS.badRequest, { error: "unsupported_grant_type" });
11043
+ return true;
11044
+ }
11045
+ 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(" ") });
11046
+ return true;
11047
+ }
11048
+ if (method === "GET" && pathname === "/v1/oauth/hello") {
11049
+ answerServerJson(response, HTTP_STATUS.ok, {});
11050
+ return true;
11051
+ }
11052
+ return false;
11053
+ }
11054
+ if (method === "POST" && pathname.startsWith("/api/eval/")) {
11055
+ await deps.readBody(request4, CONTROL_BODY_CAP_BYTES);
11056
+ answerServerJson(response, HTTP_STATUS.ok, { features: growthBookFeatures(), dateUpdated: Math.floor(deps.now() / MS_PER_SECOND5) });
11057
+ return true;
11058
+ }
11059
+ if (method === "GET" && pathname === "/api/oauth/profile") {
11060
+ const record = deps.credentialRecord();
11061
+ if (record === void 0) {
11062
+ 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" });
11063
+ return true;
11064
+ }
11065
+ answerServerJson(response, HTTP_STATUS.ok, {
11066
+ account: { uuid: record.accountUuid, display_name: "agent-shim self-hosted", created_at: (/* @__PURE__ */ new Date(0)).toISOString() },
11067
+ organization: { uuid: record.organizationUuid, organization_type: "claude_max" }
11068
+ });
11069
+ return true;
11070
+ }
11071
+ if (method === "GET" && pathname === "/api/claude_code/policy_limits") {
11072
+ answerServerJson(response, HTTP_STATUS.ok, {});
11073
+ return true;
11074
+ }
11075
+ if (method === "POST" && pathname === "/api/oauth/validate") {
11076
+ await deps.readBody(request4, CONTROL_BODY_CAP_BYTES);
11077
+ const record = deps.credentialRecord();
11078
+ if (record === void 0) {
11079
+ 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" });
11080
+ return true;
11081
+ }
11082
+ answerServerJson(response, HTTP_STATUS.ok, { scopes: [...RC_SELF_HOST_SCOPE_LIST], expiresAt: null, subscriptionType: "max", account_uuid: record.accountUuid, organization_uuid: record.organizationUuid });
11083
+ return true;
11084
+ }
11085
+ if (method === "POST" && (pathname === "/api/event_logging/v2/batch" || pathname === "/api/claude_code/metrics" || pathname === "/api/claude_cli_feedback")) {
11086
+ await deps.readBody(request4, CONTROL_BODY_CAP_BYTES);
11087
+ answerServerJson(response, HTTP_STATUS.ok, {});
11088
+ return true;
11089
+ }
11090
+ if (method === "GET" && pathname === "/api/hello") {
11091
+ answerServerJson(response, HTTP_STATUS.ok, {});
11092
+ return true;
11093
+ }
11094
+ return false;
11095
+ }
11096
+ };
11097
+ }
11098
+
10906
11099
  // src/frontdoor/rcWebFetch.ts
10907
11100
  var dns2 = __toESM(require("node:dns"), 1);
10908
11101
  var http2 = __toESM(require("node:http"), 1);
@@ -11154,17 +11347,14 @@ var RC_SELF_HOST_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST";
11154
11347
  function rcSelfHostFromEnv(env) {
11155
11348
  return env[RC_SELF_HOST_ENV] === "1";
11156
11349
  }
11157
- var RC_SELF_HOST_SCOPE_LIST = ["user:profile", "user:inference", "user:sessions:claude_code", "user:mcp_servers", "user:file_upload"];
11158
11350
  var RC_SESSIONS_PATH_PREFIX2 = "/v1/code/sessions";
11159
11351
  var RC_COMPAT_SESSIONS_PATH = "/v1/sessions";
11160
11352
  var RC_SESSION_ID_PREFIX2 = "cse_";
11161
11353
  var RC_SELF_HOST_API_BASE_URL = "https://api.anthropic.com";
11162
- var RC_SELF_HOST_OAUTH_HOST = "platform.claude.com";
11163
11354
  var HTTP_CONFLICT = 409;
11164
- var MS_PER_SECOND5 = 1e3;
11165
- var HTTP_SERVICE_UNAVAILABLE = 503;
11355
+ var MS_PER_SECOND6 = 1e3;
11356
+ var HTTP_SERVICE_UNAVAILABLE2 = 503;
11166
11357
  var RC_SELF_HOST_WORKER_JWT_TTL_SECONDS = 46800;
11167
- var RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS = 3600;
11168
11358
  var RC_SELF_HOST_KEEPALIVE_MS = 15e3;
11169
11359
  var RC_SELF_HOST_RETENTION_MS = 6e5;
11170
11360
  var RC_PRESENCE_REFRESH_SECONDS = 60;
@@ -11173,13 +11363,13 @@ var RC_SELF_HOST_WORKER_EPOCH = 1;
11173
11363
  var RC_STREAM_EVENT_NAME2 = "client_event";
11174
11364
  var CONFLICT_REASON_HEADER2 = "x-ccr-conflict-reason";
11175
11365
  var CONFLICT_REASON_EPOCH_STALE = "epoch_stale";
11176
- function isRecord8(value) {
11366
+ function isRecord10(value) {
11177
11367
  return typeof value === "object" && value !== null && !Array.isArray(value);
11178
11368
  }
11179
11369
  function isWebSearchRefusal(value) {
11180
11370
  return !Array.isArray(value);
11181
11371
  }
11182
- function nonEmptyString3(record, field) {
11372
+ function nonEmptyString4(record, field) {
11183
11373
  const value = record === void 0 ? void 0 : record[field];
11184
11374
  return typeof value === "string" && value !== "" ? value : void 0;
11185
11375
  }
@@ -11250,56 +11440,69 @@ async function readBody2(request4, capBytes) {
11250
11440
  });
11251
11441
  });
11252
11442
  }
11253
- function parseJsonObject3(body) {
11443
+ function parseJsonObject5(body) {
11254
11444
  let parsed;
11255
11445
  try {
11256
11446
  parsed = JSON.parse(body);
11257
11447
  } catch {
11258
11448
  return void 0;
11259
11449
  }
11260
- return isRecord8(parsed) ? parsed : void 0;
11450
+ return isRecord10(parsed) ? parsed : void 0;
11261
11451
  }
11262
11452
  function mintWorkerJwt(randomToken, issuedAt) {
11263
11453
  const encode = (value) => Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
11264
11454
  const header = encode({ alg: "HS256", typ: "JWT" });
11265
- 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) });
11455
+ 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) });
11266
11456
  return `${header}.${payload}.${Buffer.from(randomToken(), "utf8").toString("base64url")}`;
11267
11457
  }
11268
- function growthBookFeatures() {
11269
- return {
11270
- tengu_ccr_bridge: { defaultValue: true },
11271
- tengu_bridge_repl_v2: { defaultValue: true },
11272
- tengu_bridge_repl_v2_cse_shim_enabled: { defaultValue: true },
11273
- tengu_bridge_min_version: { defaultValue: { minVersion: "0.0.0" } }
11274
- };
11275
- }
11276
11458
  function createRcSelfHostSurface(deps) {
11277
11459
  const sessions = /* @__PURE__ */ new Map();
11460
+ const conversations = /* @__PURE__ */ new Map();
11461
+ const retireSession = (session) => {
11462
+ for (const sink of [...session.conversation.workerStreams, ...session.conversation.clientStreams]) {
11463
+ if (sink.owner === session.id) {
11464
+ sink.retire();
11465
+ }
11466
+ }
11467
+ session.conversation.sessionIds.delete(session.id);
11468
+ sessions.delete(session.id);
11469
+ };
11278
11470
  const sweep = () => {
11279
11471
  const now = deps.now();
11280
11472
  for (const session of sessions.values()) {
11281
- while (session.events[0] !== void 0 && now - session.events[0].storedAt > RC_SELF_HOST_RETENTION_MS) {
11282
- const dropped = session.events.shift();
11283
- const uuid = dropped === void 0 ? void 0 : nonEmptyString3(dropped.payload, "uuid");
11473
+ for (const [clientId, lastSeenAt] of session.clients) {
11474
+ if (now - lastSeenAt > RC_SELF_HOST_RETENTION_MS) {
11475
+ session.clients.delete(clientId);
11476
+ }
11477
+ }
11478
+ const holdsStream = [...session.conversation.workerStreams, ...session.conversation.clientStreams].some((sink) => sink.owner === session.id);
11479
+ if (!holdsStream && now - session.lastTrafficAt > RC_SELF_HOST_RETENTION_MS) {
11480
+ retireSession(session);
11481
+ }
11482
+ }
11483
+ for (const conversation of conversations.values()) {
11484
+ while (conversation.events[0] !== void 0 && now - conversation.events[0].storedAt > RC_SELF_HOST_RETENTION_MS) {
11485
+ const dropped = conversation.events.shift();
11486
+ const uuid = dropped === void 0 ? void 0 : nonEmptyString4(dropped.payload, "uuid");
11284
11487
  if (uuid !== void 0) {
11285
- session.seenPayloadUuids.delete(uuid);
11488
+ conversation.seenPayloadUuids.delete(uuid);
11286
11489
  }
11287
11490
  }
11288
- for (const [clientId, lastSeenAt] of session.clients) {
11491
+ for (const [clientId, lastSeenAt] of conversation.clients) {
11289
11492
  if (now - lastSeenAt > RC_SELF_HOST_RETENTION_MS) {
11290
- session.clients.delete(clientId);
11493
+ conversation.clients.delete(clientId);
11291
11494
  }
11292
11495
  }
11293
- if (session.workerStreams.size === 0 && session.clientStreams.size === 0 && now - session.lastTrafficAt > RC_SELF_HOST_RETENTION_MS) {
11294
- sessions.delete(session.id);
11496
+ if (conversation.sessionIds.size === 0 && conversation.workerStreams.size === 0 && conversation.clientStreams.size === 0 && now - conversation.lastTrafficAt > RC_SELF_HOST_RETENTION_MS) {
11497
+ conversations.delete(conversation.id);
11295
11498
  }
11296
11499
  }
11297
11500
  };
11298
11501
  const timer = setInterval(sweep, RC_SELF_HOST_RETENTION_MS);
11299
11502
  timer.unref();
11300
11503
  const keepalive = setInterval(() => {
11301
- for (const session of sessions.values()) {
11302
- for (const sink of [...session.workerStreams, ...session.clientStreams]) {
11504
+ for (const conversation of conversations.values()) {
11505
+ for (const sink of [...conversation.workerStreams, ...conversation.clientStreams]) {
11303
11506
  sink.write(": keep-alive\n\n");
11304
11507
  }
11305
11508
  }
@@ -11328,7 +11531,7 @@ function createRcSelfHostSurface(deps) {
11328
11531
  const frameOf = (event) => {
11329
11532
  const envelope = {
11330
11533
  event_id: event.eventId,
11331
- event_type: nonEmptyString3(event.payload, "type") ?? "event",
11534
+ event_type: nonEmptyString4(event.payload, "type") ?? "event",
11332
11535
  sequence_num: event.sequenceNum,
11333
11536
  source: event.source,
11334
11537
  payload: event.payload,
@@ -11340,14 +11543,16 @@ data: ${JSON.stringify(envelope)}
11340
11543
 
11341
11544
  `;
11342
11545
  };
11343
- const holdStream = async (session, which, wantsReplay, request4, response) => {
11546
+ const reachesWorkerStream = (event, owner) => !(event.writer.half === "worker" && event.writer.session === owner);
11547
+ const holdStream = async (conversation, owner, which, wantsReplay, request4, response) => {
11344
11548
  const url = new URL(request4.url, "http://127.0.0.1");
11345
11549
  const cursor = numberOf2({ value: url.searchParams.get("from_sequence_num") }, "value") ?? numberOf2({ value: singleHeader2(request4.headers, "last-event-id") }, "value");
11346
- const replay = cursor === void 0 ? [] : session.events.filter((event) => wantsReplay(event) && event.sequenceNum > cursor);
11550
+ const replay = cursor === void 0 ? [] : conversation.events.filter((event) => wantsReplay(event) && event.sequenceNum > cursor);
11347
11551
  response.start(HTTP_STATUS.ok, { "content-type": "text/event-stream", "cache-control": "no-cache" });
11348
11552
  response.flush();
11349
11553
  let pending = Promise.resolve();
11350
11554
  const sink = {
11555
+ owner,
11351
11556
  write: (frame) => {
11352
11557
  pending = pending.then(async () => {
11353
11558
  await response.write(frame);
@@ -11355,11 +11560,11 @@ data: ${JSON.stringify(envelope)}
11355
11560
  });
11356
11561
  },
11357
11562
  retire: () => {
11358
- session[which].delete(sink);
11563
+ conversation[which].delete(sink);
11359
11564
  }
11360
11565
  };
11361
- session[which].add(sink);
11362
- session.lastTrafficAt = deps.now();
11566
+ conversation[which].add(sink);
11567
+ conversation.lastTrafficAt = deps.now();
11363
11568
  for (const event of replay) {
11364
11569
  sink.write(frameOf(event));
11365
11570
  }
@@ -11371,36 +11576,37 @@ data: ${JSON.stringify(envelope)}
11371
11576
  sink.retire();
11372
11577
  response.end();
11373
11578
  };
11374
- const publish = (session, source, payload, toWorker = source === RC_SOURCE_CLIENT) => {
11375
- const uuid = nonEmptyString3(payload, "uuid");
11376
- const duplicate = uuid !== void 0 && session.seenPayloadUuids.has(uuid);
11377
- session.sequenceNum += 1;
11579
+ const publish = (conversation, writer, source, payload) => {
11580
+ const uuid = nonEmptyString4(payload, "uuid");
11581
+ const duplicate = uuid !== void 0 && conversation.seenPayloadUuids.has(uuid);
11582
+ conversation.sequenceNum += 1;
11378
11583
  const event = {
11379
- sequenceNum: session.sequenceNum,
11584
+ sequenceNum: conversation.sequenceNum,
11380
11585
  eventId: uuid ?? deps.newUuid(),
11381
11586
  source,
11382
11587
  // 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.
11383
11588
  payload: source === RC_SOURCE_WORKER ? payload : { ...payload, server_received_wall_ms: deps.now() },
11384
11589
  createdAt: new Date(deps.now()).toISOString(),
11385
- toWorker,
11590
+ writer,
11386
11591
  storedAt: deps.now()
11387
11592
  };
11388
- session.events.push(event);
11593
+ conversation.events.push(event);
11389
11594
  if (uuid !== void 0) {
11390
- session.seenPayloadUuids.add(uuid);
11595
+ conversation.seenPayloadUuids.add(uuid);
11391
11596
  }
11392
- session.lastTrafficAt = event.storedAt;
11597
+ conversation.lastTrafficAt = event.storedAt;
11393
11598
  const frame = frameOf(event);
11394
- if (event.toWorker) {
11395
- for (const sink of [...session.workerStreams]) {
11599
+ for (const sink of [...conversation.workerStreams]) {
11600
+ if (reachesWorkerStream(event, sink.owner)) {
11396
11601
  sink.write(frame);
11397
11602
  }
11398
11603
  }
11399
- for (const sink of [...session.clientStreams]) {
11604
+ for (const sink of [...conversation.clientStreams]) {
11400
11605
  sink.write(frame);
11401
11606
  }
11402
11607
  return { sequenceNum: event.sequenceNum, eventId: event.eventId, duplicate };
11403
11608
  };
11609
+ const sessionRow = (session) => ({ id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active", conversation_id: session.conversation.id });
11404
11610
  const answerJson2 = async (response, status, body, extraHeaders = {}) => {
11405
11611
  const text = JSON.stringify(body);
11406
11612
  response.start(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text, "utf8")), ...extraHeaders });
@@ -11408,7 +11614,7 @@ data: ${JSON.stringify(envelope)}
11408
11614
  response.end();
11409
11615
  };
11410
11616
  const noCredential = async (response) => {
11411
- 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" } });
11617
+ 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" } });
11412
11618
  };
11413
11619
  const epochStale = async (response) => {
11414
11620
  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 });
@@ -11436,8 +11642,8 @@ data: ${JSON.stringify(envelope)}
11436
11642
  await unauthorized(response, refusal);
11437
11643
  return;
11438
11644
  }
11439
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11440
- const title = nonEmptyString3(body, "title");
11645
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11646
+ const title = nonEmptyString4(body, "title");
11441
11647
  if (title === void 0) {
11442
11648
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a session update is JSON naming a non-empty title" } });
11443
11649
  return;
@@ -11452,7 +11658,7 @@ data: ${JSON.stringify(envelope)}
11452
11658
  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" } });
11453
11659
  return;
11454
11660
  }
11455
- await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
11661
+ await answerJson2(response, HTTP_STATUS.ok, { session: sessionRow(session) });
11456
11662
  return;
11457
11663
  }
11458
11664
  if (tail === "bridge" && method === "POST") {
@@ -11473,10 +11679,7 @@ data: ${JSON.stringify(envelope)}
11473
11679
  await unauthorized(response, refusal);
11474
11680
  return;
11475
11681
  }
11476
- for (const sink of [...session.workerStreams, ...session.clientStreams]) {
11477
- sink.retire();
11478
- }
11479
- sessions.delete(session.id);
11682
+ retireSession(session);
11480
11683
  deps.log?.(`rc selfhost ${session.id}: archived at its client's request`);
11481
11684
  await answerJson2(response, HTTP_STATUS.ok, {});
11482
11685
  return;
@@ -11494,17 +11697,17 @@ data: ${JSON.stringify(envelope)}
11494
11697
  return;
11495
11698
  }
11496
11699
  if (workerTail === "" && method === "PUT") {
11497
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11700
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11498
11701
  const claimedEpoch = numberOf2(body, "worker_epoch");
11499
11702
  if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
11500
11703
  await epochStale(response);
11501
11704
  return;
11502
11705
  }
11503
- const status = nonEmptyString3(body, "worker_status");
11706
+ const status = nonEmptyString4(body, "worker_status");
11504
11707
  if (status !== void 0) {
11505
11708
  session.workerStatus = status;
11506
11709
  }
11507
- if (isRecord8(body?.external_metadata)) {
11710
+ if (isRecord10(body?.external_metadata)) {
11508
11711
  session.externalMetadata = body.external_metadata;
11509
11712
  }
11510
11713
  await answerJson2(response, HTTP_STATUS.ok, {});
@@ -11521,7 +11724,7 @@ data: ${JSON.stringify(envelope)}
11521
11724
  return;
11522
11725
  }
11523
11726
  if (workerTail === "events" && method === "POST") {
11524
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11727
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11525
11728
  const claimedEpoch = numberOf2(body, "worker_epoch");
11526
11729
  if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
11527
11730
  await epochStale(response);
@@ -11534,11 +11737,11 @@ data: ${JSON.stringify(envelope)}
11534
11737
  }
11535
11738
  const results = [];
11536
11739
  for (const event of events) {
11537
- if (!isRecord8(event) || !isRecord8(event.payload)) {
11740
+ if (!isRecord10(event) || !isRecord10(event.payload)) {
11538
11741
  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" } });
11539
11742
  return;
11540
11743
  }
11541
- const published = publish(session, RC_SOURCE_WORKER, event.payload);
11744
+ const published = publish(session.conversation, { session: session.id, half: "worker" }, RC_SOURCE_WORKER, event.payload);
11542
11745
  results.push({ event_id: published.eventId, sequence_num: String(published.sequenceNum) });
11543
11746
  }
11544
11747
  sweep();
@@ -11546,7 +11749,7 @@ data: ${JSON.stringify(envelope)}
11546
11749
  return;
11547
11750
  }
11548
11751
  if (workerTail === "events/stream" && method === "GET") {
11549
- await holdStream(session, "workerStreams", (event) => event.toWorker, request4, response);
11752
+ await holdStream(session.conversation, session.id, "workerStreams", (event) => reachesWorkerStream(event, session.id), request4, response);
11550
11753
  return;
11551
11754
  }
11552
11755
  if (workerTail === "events/delivery" && method === "POST") {
@@ -11555,7 +11758,7 @@ data: ${JSON.stringify(envelope)}
11555
11758
  return;
11556
11759
  }
11557
11760
  if (workerTail === "internal-events" && method === "POST") {
11558
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11761
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11559
11762
  const claimedEpoch = numberOf2(body, "worker_epoch");
11560
11763
  if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
11561
11764
  await epochStale(response);
@@ -11567,7 +11770,7 @@ data: ${JSON.stringify(envelope)}
11567
11770
  return;
11568
11771
  }
11569
11772
  for (const event of events) {
11570
- if (isRecord8(event)) {
11773
+ if (isRecord10(event)) {
11571
11774
  session.internalEvents.push({ eventId: deps.newUuid(), body: event });
11572
11775
  }
11573
11776
  }
@@ -11595,8 +11798,8 @@ data: ${JSON.stringify(envelope)}
11595
11798
  return;
11596
11799
  }
11597
11800
  if (workerTail === "web-fetch" && method === "POST") {
11598
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11599
- const target = nonEmptyString3(body, "url");
11801
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11802
+ const target = nonEmptyString4(body, "url");
11600
11803
  if (target === void 0) {
11601
11804
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-fetch carries one url" } });
11602
11805
  return;
@@ -11618,8 +11821,8 @@ data: ${JSON.stringify(envelope)}
11618
11821
  return;
11619
11822
  }
11620
11823
  if (workerTail === "web-search" && method === "POST") {
11621
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11622
- const query = nonEmptyString3(body, "query");
11824
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11825
+ const query = nonEmptyString4(body, "query");
11623
11826
  if (query === void 0) {
11624
11827
  await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-search carries one query" } });
11625
11828
  return;
@@ -11630,7 +11833,7 @@ data: ${JSON.stringify(envelope)}
11630
11833
  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" } });
11631
11834
  return;
11632
11835
  }
11633
- const searchProfile = nonEmptyString3(body, "search_profile");
11836
+ const searchProfile = nonEmptyString4(body, "search_profile");
11634
11837
  const backend = deps.webSearch;
11635
11838
  if (backend === void 0) {
11636
11839
  deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" refused: no search backend serves this door`);
@@ -11655,7 +11858,7 @@ data: ${JSON.stringify(envelope)}
11655
11858
  return;
11656
11859
  }
11657
11860
  if (tail === "events" && method === "POST") {
11658
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11861
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11659
11862
  const events = body?.events;
11660
11863
  if (!Array.isArray(events) || events.length === 0 || events.length > RC_SELF_HOST_MAX_BATCH_EVENTS) {
11661
11864
  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` } });
@@ -11663,11 +11866,11 @@ data: ${JSON.stringify(envelope)}
11663
11866
  }
11664
11867
  const results = [];
11665
11868
  for (const event of events) {
11666
- if (!isRecord8(event) || !isRecord8(event.payload)) {
11869
+ if (!isRecord10(event) || !isRecord10(event.payload)) {
11667
11870
  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" } });
11668
11871
  return;
11669
11872
  }
11670
- const published = publish(session, RC_SOURCE_CLIENT, event.payload);
11873
+ const published = publish(session.conversation, { session: session.id, half: "client" }, RC_SOURCE_CLIENT, event.payload);
11671
11874
  results.push({ sequence_num: String(published.sequenceNum), duplicate: published.duplicate });
11672
11875
  }
11673
11876
  sweep();
@@ -11675,14 +11878,14 @@ data: ${JSON.stringify(envelope)}
11675
11878
  return;
11676
11879
  }
11677
11880
  if (tail === "events" && method === "GET") {
11678
- 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 })) });
11881
+ 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 })) });
11679
11882
  return;
11680
11883
  }
11681
11884
  if (tail === "events/stream" && method === "GET") {
11682
- await holdStream(session, "clientStreams", () => true, request4, response);
11885
+ await holdStream(session.conversation, session.id, "clientStreams", () => true, request4, response);
11683
11886
  return;
11684
11887
  }
11685
- 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)) {
11888
+ 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)) {
11686
11889
  return;
11687
11890
  }
11688
11891
  await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no path "${tail}"` } });
@@ -11700,7 +11903,11 @@ data: ${JSON.stringify(envelope)}
11700
11903
  return;
11701
11904
  }
11702
11905
  if (pathname === RC_COMPAT_SESSIONS_PATH || pathname.startsWith(`${RC_COMPAT_SESSIONS_PATH}/`)) {
11703
- 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);
11906
+ 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);
11907
+ return;
11908
+ }
11909
+ if (pathname === RC_CONVERSATIONS_PATH_PREFIX || pathname.startsWith(`${RC_CONVERSATIONS_PATH_PREFIX}/`)) {
11910
+ 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);
11704
11911
  return;
11705
11912
  }
11706
11913
  if (pathname !== RC_SESSIONS_PATH_PREFIX2 && !pathname.startsWith(`${RC_SESSIONS_PATH_PREFIX2}/`)) {
@@ -11714,26 +11921,36 @@ data: ${JSON.stringify(envelope)}
11714
11921
  await unauthorized(response, createRefusal);
11715
11922
  return;
11716
11923
  }
11717
- const body = parseJsonObject3(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11924
+ const body = parseJsonObject5(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
11925
+ const namedConversation = nonEmptyString4(body, "conversation");
11926
+ let conversation;
11927
+ if (namedConversation === void 0) {
11928
+ conversation = newRcSelfHostConversation(deps.newUuid, deps.now);
11929
+ conversations.set(conversation.id, conversation);
11930
+ } else {
11931
+ conversation = conversations.get(namedConversation);
11932
+ if (conversation === void 0) {
11933
+ await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `no such Remote Control conversation on this door: ${namedConversation}` } });
11934
+ return;
11935
+ }
11936
+ conversation.lastTrafficAt = deps.now();
11937
+ }
11718
11938
  const session = {
11719
11939
  id: `${RC_SESSION_ID_PREFIX2}${deps.newUuid()}`,
11720
11940
  createdAt: deps.now(),
11721
- title: nonEmptyString3(body, "title"),
11941
+ title: nonEmptyString4(body, "title"),
11942
+ conversation,
11722
11943
  workerJwts: /* @__PURE__ */ new Set(),
11723
- sequenceNum: 0,
11724
- events: [],
11725
- seenPayloadUuids: /* @__PURE__ */ new Set(),
11726
- workerStreams: /* @__PURE__ */ new Set(),
11727
- clientStreams: /* @__PURE__ */ new Set(),
11728
11944
  internalEvents: [],
11729
11945
  clients: /* @__PURE__ */ new Map(),
11730
11946
  workerStatus: void 0,
11731
11947
  externalMetadata: void 0,
11732
11948
  lastTrafficAt: deps.now()
11733
11949
  };
11950
+ conversation.sessionIds.add(session.id);
11734
11951
  sessions.set(session.id, session);
11735
- deps.log?.(`rc selfhost ${session.id}: session created on the door's own surface`);
11736
- await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
11952
+ 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"}`);
11953
+ await answerJson2(response, HTTP_STATUS.ok, { session: sessionRow(session) });
11737
11954
  return;
11738
11955
  }
11739
11956
  if (method === "GET") {
@@ -11742,7 +11959,7 @@ data: ${JSON.stringify(envelope)}
11742
11959
  await unauthorized(response, readRefusal);
11743
11960
  return;
11744
11961
  }
11745
- 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" })) });
11962
+ 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 })) });
11746
11963
  return;
11747
11964
  }
11748
11965
  await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the session collection is a POST (create) and a GET (list)" } });
@@ -11759,92 +11976,20 @@ data: ${JSON.stringify(envelope)}
11759
11976
  await serveSessionPath(sessions.get(id), tail, request4, response);
11760
11977
  }
11761
11978
  };
11762
- const answerServerJson = (response, status, body) => {
11763
- const text = JSON.stringify(body);
11764
- response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text, "utf8")) });
11765
- response.end(text);
11766
- };
11767
- const local = {
11768
- parsesHost: (host) => host === RC_SELF_HOST_OAUTH_HOST,
11769
- serve: async (host, request4, response) => {
11770
- const method = (request4.method ?? "").toUpperCase();
11771
- const pathname = new URL(request4.url ?? "/", "http://127.0.0.1").pathname;
11772
- if (host === RC_SELF_HOST_OAUTH_HOST) {
11773
- if (method === "POST" && pathname === "/v1/oauth/token") {
11774
- const body = parseJsonObject3(await readBody2(request4, CONTROL_BODY_CAP_BYTES));
11775
- const record = deps.credentialRecord();
11776
- if (record === void 0) {
11777
- 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" });
11778
- return true;
11779
- }
11780
- if (body?.grant_type !== void 0 && body.grant_type !== "refresh_token") {
11781
- answerServerJson(response, HTTP_STATUS.badRequest, { error: "unsupported_grant_type" });
11782
- return true;
11783
- }
11784
- 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(" ") });
11785
- return true;
11786
- }
11787
- if (method === "GET" && pathname === "/v1/oauth/hello") {
11788
- answerServerJson(response, HTTP_STATUS.ok, {});
11789
- return true;
11790
- }
11791
- return false;
11792
- }
11793
- if (method === "POST" && pathname.startsWith("/api/eval/")) {
11794
- await readBody2(request4, CONTROL_BODY_CAP_BYTES);
11795
- answerServerJson(response, HTTP_STATUS.ok, { features: growthBookFeatures(), dateUpdated: Math.floor(deps.now() / MS_PER_SECOND5) });
11796
- return true;
11797
- }
11798
- if (method === "GET" && pathname === "/api/oauth/profile") {
11799
- const record = deps.credentialRecord();
11800
- if (record === void 0) {
11801
- 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" });
11802
- return true;
11803
- }
11804
- answerServerJson(response, HTTP_STATUS.ok, {
11805
- account: { uuid: record.accountUuid, display_name: "agent-shim self-hosted", created_at: (/* @__PURE__ */ new Date(0)).toISOString() },
11806
- organization: { uuid: record.organizationUuid, organization_type: "claude_max" }
11807
- });
11808
- return true;
11809
- }
11810
- if (method === "GET" && pathname === "/api/claude_code/policy_limits") {
11811
- answerServerJson(response, HTTP_STATUS.ok, {});
11812
- return true;
11813
- }
11814
- if (method === "POST" && pathname === "/api/oauth/validate") {
11815
- await readBody2(request4, CONTROL_BODY_CAP_BYTES);
11816
- const record = deps.credentialRecord();
11817
- if (record === void 0) {
11818
- 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" });
11819
- return true;
11820
- }
11821
- answerServerJson(response, HTTP_STATUS.ok, { scopes: [...RC_SELF_HOST_SCOPE_LIST], expiresAt: null, subscriptionType: "max", account_uuid: record.accountUuid, organization_uuid: record.organizationUuid });
11822
- return true;
11823
- }
11824
- if (method === "POST" && (pathname === "/api/event_logging/v2/batch" || pathname === "/api/claude_code/metrics" || pathname === "/api/claude_cli_feedback")) {
11825
- await readBody2(request4, CONTROL_BODY_CAP_BYTES);
11826
- answerServerJson(response, HTTP_STATUS.ok, {});
11827
- return true;
11828
- }
11829
- if (method === "GET" && pathname === "/api/hello") {
11830
- answerServerJson(response, HTTP_STATUS.ok, {});
11831
- return true;
11832
- }
11833
- return false;
11834
- }
11835
- };
11979
+ const local = createRcSelfHostLocal({ now: deps.now, credentialRecord: deps.credentialRecord, readBody: readBody2 });
11836
11980
  return {
11837
11981
  route,
11838
11982
  local,
11839
11983
  close: () => {
11840
11984
  clearInterval(timer);
11841
11985
  clearInterval(keepalive);
11842
- for (const session of sessions.values()) {
11843
- for (const sink of [...session.workerStreams, ...session.clientStreams]) {
11986
+ for (const conversation of conversations.values()) {
11987
+ for (const sink of [...conversation.workerStreams, ...conversation.clientStreams]) {
11844
11988
  sink.retire();
11845
11989
  }
11846
11990
  }
11847
11991
  sessions.clear();
11992
+ conversations.clear();
11848
11993
  }
11849
11994
  };
11850
11995
  }
@@ -11857,7 +12002,7 @@ var RC_SELF_HOST_RECORD_DIR = "rc-selfhost";
11857
12002
  var RC_SELF_HOST_RECORD_FILE = "credential.json";
11858
12003
  var SELF_HOST_EMAIL = "selfhost@agent-shim.invalid";
11859
12004
  var SELF_HOST_SUBSCRIPTION_TYPE = "max";
11860
- function isRecord9(value) {
12005
+ function isRecord11(value) {
11861
12006
  return typeof value === "object" && value !== null && !Array.isArray(value);
11862
12007
  }
11863
12008
  function readJsonObject(fs12, file) {
@@ -11871,7 +12016,7 @@ function readJsonObject(fs12, file) {
11871
12016
  } catch {
11872
12017
  return void 0;
11873
12018
  }
11874
- return isRecord9(parsed) ? parsed : void 0;
12019
+ return isRecord11(parsed) ? parsed : void 0;
11875
12020
  }
11876
12021
  function readRcSelfHostRecord(fs12, frontdoorDir) {
11877
12022
  const parsed = readJsonObject(fs12, import_node_path28.default.join(frontdoorDir, RC_SELF_HOST_RECORD_DIR, RC_SELF_HOST_RECORD_FILE));
@@ -11895,7 +12040,7 @@ function mintRcSelfHostCredential(deps) {
11895
12040
  const recordFile = import_node_path28.default.join(recordDir, RC_SELF_HOST_RECORD_FILE);
11896
12041
  const previousRecord = readRcSelfHostRecord(deps.fs, deps.frontdoorDir);
11897
12042
  const existingCredentials = readJsonObject(deps.fs, credentialsFile);
11898
- const existingOauth = isRecord9(existingCredentials?.claudeAiOauth) ? existingCredentials.claudeAiOauth : void 0;
12043
+ const existingOauth = isRecord11(existingCredentials?.claudeAiOauth) ? existingCredentials.claudeAiOauth : void 0;
11899
12044
  const existingToken = typeof existingOauth?.accessToken === "string" && existingOauth.accessToken !== "" ? existingOauth.accessToken : void 0;
11900
12045
  const existingIsOwn = existingToken !== void 0 && previousRecord?.accessToken === existingToken;
11901
12046
  if (existingToken !== void 0 && !existingIsOwn && !deps.force) {
@@ -11912,7 +12057,7 @@ function mintRcSelfHostCredential(deps) {
11912
12057
  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)}
11913
12058
  `);
11914
12059
  const claudeJson = readJsonObject(deps.fs, claudeJsonFile) ?? {};
11915
- const existingBlock = (value) => isRecord9(value) ? value : {};
12060
+ const existingBlock = (value) => isRecord11(value) ? value : {};
11916
12061
  deps.fs.writeFilePrivate(claudeJsonFile, `${JSON.stringify({
11917
12062
  ...claudeJson,
11918
12063
  oauthAccount: {
@@ -12111,7 +12256,7 @@ var import_node_path30 = __toESM(require("node:path"), 1);
12111
12256
  // package.json
12112
12257
  var package_default = {
12113
12258
  name: "agent-shim",
12114
- version: "8.26.0",
12259
+ version: "8.27.1",
12115
12260
  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.",
12116
12261
  license: "MIT",
12117
12262
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -12527,6 +12672,11 @@ function buildFlagArgs(flags) {
12527
12672
  function buildArgv(params) {
12528
12673
  return [...params.toolFlags, ...params.extraFlags, ...params.passthrough];
12529
12674
  }
12675
+ var LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "::1"];
12676
+ function withLoopbackHosts(existing) {
12677
+ const listed = (existing ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
12678
+ return [...listed, ...LOOPBACK_HOSTS.filter((host) => !listed.includes(host))].join(",");
12679
+ }
12530
12680
  function buildEnv(params) {
12531
12681
  const env = { ...params.baseEnv };
12532
12682
  const parentCustomHeaders = params.baseEnv.ANTHROPIC_CUSTOM_HEADERS;
@@ -12571,6 +12721,8 @@ function buildEnv(params) {
12571
12721
  );
12572
12722
  env.NODE_EXTRA_CA_CERTS = params.frontdoor.trustBundlePath;
12573
12723
  env.HTTPS_PROXY = connectProxyUrl(params.frontdoor.connectPort, params.frontdoor.sessionToken);
12724
+ env.NO_PROXY = withLoopbackHosts(env.NO_PROXY);
12725
+ env.no_proxy = withLoopbackHosts(env.no_proxy);
12574
12726
  }
12575
12727
  return env;
12576
12728
  }