negotium 0.2.22 → 0.2.23

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/main.js CHANGED
@@ -1867,7 +1867,7 @@ var exports_version = {};
1867
1867
  __export(exports_version, {
1868
1868
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
1869
1869
  });
1870
- var NEGOTIUM_VERSION = "0.2.22";
1870
+ var NEGOTIUM_VERSION = "0.2.23";
1871
1871
 
1872
1872
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
1873
1873
  import { spawn } from "child_process";
@@ -29442,7 +29442,8 @@ function createNodeControlHandler(options) {
29442
29442
  "turn-events-sse-resume",
29443
29443
  "canonical-topic-read",
29444
29444
  "canonical-message-read",
29445
- "canonical-topic-list"
29445
+ "canonical-topic-list",
29446
+ "canonical-topic-create"
29446
29447
  ],
29447
29448
  cursor: latestRuntimeEventSeq()
29448
29449
  });
@@ -29519,6 +29520,25 @@ function createNodeControlHandler(options) {
29519
29520
  cursor: latestRuntimeEventSeq()
29520
29521
  });
29521
29522
  }
29523
+ if (req.method === "POST" && runtimePath === "/topics") {
29524
+ const body = await bodyRecord(req);
29525
+ if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
29526
+ return jsonError(400, "Unsupported v");
29527
+ const userId = requiredText(body.userId, "userId");
29528
+ const title = requiredText(body.title, "title");
29529
+ const agent = body.agent;
29530
+ if (agent !== undefined && !["claude", "codex", "maestro"].includes(String(agent))) {
29531
+ return jsonError(400, "Invalid agent");
29532
+ }
29533
+ const topic = topicService.create({
29534
+ title,
29535
+ userId,
29536
+ kind: "agent",
29537
+ accessMode: "shared",
29538
+ ...agent ? { agent } : {}
29539
+ });
29540
+ return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, topic }, { status: 201 });
29541
+ }
29522
29542
  const runtimeTopicMatch = runtimePath.match(/^\/topics\/([^/]+)$/);
29523
29543
  if (runtimeTopicMatch && req.method === "GET") {
29524
29544
  const topic = getTopic(decodeURIComponent(runtimeTopicMatch[1]));
@@ -41439,18 +41459,30 @@ var init_cli2 = __esm(async () => {
41439
41459
  if (false) {}
41440
41460
  });
41441
41461
 
41442
- // ../../adapters/otium/src/central.ts
41443
- var exports_central = {};
41444
- __export(exports_central, {
41445
- verifyPeerToken: () => verifyPeerToken,
41446
- selfPeerNode: () => selfPeerNode,
41447
- resolvePeerNodeByCellId: () => resolvePeerNodeByCellId,
41448
- resetPeerCentralCaches: () => resetPeerCentralCaches,
41449
- otiumCentralConfig: () => otiumCentralConfig,
41450
- mintPeerToken: () => mintPeerToken,
41451
- listPeerNodes: () => listPeerNodes,
41452
- configureOtiumCentral: () => configureOtiumCentral
41462
+ // ../../packages/core/src/config-public.ts
41463
+ var init_config_public = __esm(() => {
41464
+ init_config();
41453
41465
  });
41466
+
41467
+ // ../../adapters/otium/src/join-status.ts
41468
+ var exports_join_status = {};
41469
+ __export(exports_join_status, {
41470
+ hasConfiguredOtiumJoin: () => hasConfiguredOtiumJoin
41471
+ });
41472
+ import { existsSync as existsSync36 } from "fs";
41473
+ import { resolve as resolve23 } from "path";
41474
+ function hasConfiguredOtiumJoin() {
41475
+ const envJoin = Boolean(process.env.OTIUM_CENTRAL_URL?.trim() && process.env.OTIUM_CELL_ID?.trim() && process.env.OTIUM_CELL_SECRET?.trim());
41476
+ return envJoin || existsSync36(resolve23(DATA_DIR, "otium-join.json"));
41477
+ }
41478
+ var init_join_status = __esm(() => {
41479
+ init_config_public();
41480
+ });
41481
+
41482
+ // ../../adapters/otium/src/control-protocol.ts
41483
+ var OTIUM_ADAPTER_CONTROL_PREFIX = "/api/v1/adapter/otium", OTIUM_ADAPTER_CONTROL_HEADER = "x-negotium-adapter-token";
41484
+
41485
+ // ../../adapters/otium/src/central.ts
41454
41486
  function configureOtiumCentral(join42) {
41455
41487
  joinConfig = join42;
41456
41488
  resetPeerCentralCaches();
@@ -41555,418 +41587,14 @@ var init_central = __esm(async () => {
41555
41587
  tokenCache = new Map;
41556
41588
  });
41557
41589
 
41558
- // ../../adapters/otium/src/secure-transport.ts
41559
- function isLoopbackHostname(hostname2) {
41560
- const normalized = hostname2.toLowerCase();
41561
- if (normalized === "localhost" || normalized === "::1" || normalized === "[::1]")
41562
- return true;
41563
- const octets = normalized.split(".");
41564
- return octets.length === 4 && octets[0] === "127" && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
41565
- }
41566
- function credentialTransportUrl(value, label) {
41567
- let url;
41568
- try {
41569
- url = new URL(value);
41570
- } catch {
41571
- throw new Error(`${label} is not a valid URL`);
41572
- }
41573
- if (url.username || url.password)
41574
- throw new Error(`${label} must not contain URL credentials`);
41575
- return url;
41576
- }
41577
- function assertSecureCentralUrl(value) {
41578
- const url = credentialTransportUrl(value, "Otium central URL");
41579
- if (url.protocol === "https:")
41580
- return;
41581
- if (url.protocol === "http:" && isLoopbackHostname(url.hostname))
41582
- return;
41583
- throw new Error("Otium central requires HTTPS or loopback HTTP");
41584
- }
41585
- function assertSecureRelayUrl(value) {
41586
- const url = credentialTransportUrl(value, "Otium relay URL");
41587
- if (url.protocol === "https:" || url.protocol === "wss:")
41588
- return;
41589
- if ((url.protocol === "http:" || url.protocol === "ws:") && isLoopbackHostname(url.hostname)) {
41590
- return;
41591
- }
41592
- throw new Error("Otium relay requires HTTPS/WSS or loopback HTTP/WS");
41593
- }
41594
-
41595
- // ../../adapters/otium/src/join.ts
41596
- var exports_join = {};
41597
- __export(exports_join, {
41598
- withJoinCredentialLock: () => withJoinCredentialLock,
41599
- saveJoinWhileLocked: () => saveJoinWhileLocked,
41600
- saveJoin: () => saveJoin,
41601
- removeJoin: () => removeJoin,
41602
- parseInviteCode: () => parseInviteCode,
41603
- loadJoin: () => loadJoin,
41604
- joinFilePath: () => joinFilePath,
41605
- joinCredentialDigest: () => joinCredentialDigest,
41606
- isJoinPersisted: () => isJoinPersisted
41607
- });
41608
- import { createHash as createHash11, randomUUID as randomUUID27 } from "crypto";
41609
- import {
41610
- chmodSync as chmodSync7,
41611
- closeSync as closeSync5,
41612
- existsSync as existsSync36,
41613
- fsyncSync as fsyncSync2,
41614
- linkSync,
41615
- lstatSync,
41616
- mkdirSync as mkdirSync32,
41617
- openSync as openSync5,
41618
- readFileSync as readFileSync28,
41619
- renameSync as renameSync16,
41620
- rmSync as rmSync10,
41621
- statSync as statSync20,
41622
- unlinkSync as unlinkSync23,
41623
- writeFileSync as writeFileSync23
41624
- } from "fs";
41625
- import { dirname as dirname21, resolve as resolve23 } from "path";
41626
- function joinFilePath() {
41627
- return resolve23(DATA_DIR, "otium-join.json");
41628
- }
41629
- function isHttpUrl(value) {
41630
- return /^https?:\/\//.test(value);
41631
- }
41632
- function isRelayUrl(value) {
41633
- return /^(?:https?|wss?):\/\//.test(value);
41634
- }
41635
- function normalizeJoin(raw) {
41636
- const central = typeof raw.central === "string" ? raw.central.trim().replace(/\/+$/, "") : "";
41637
- const relay = typeof raw.relay === "string" ? raw.relay.trim().replace(/\/+$/, "") : "";
41638
- const cellId = typeof raw.cellId === "string" ? raw.cellId.trim() : "";
41639
- const secret = typeof raw.secret === "string" ? raw.secret.trim() : "";
41640
- if (!central || !isHttpUrl(central)) {
41641
- throw new Error("invite code is missing a valid http(s) central URL");
41642
- }
41643
- if (relay && !isRelayUrl(relay))
41644
- throw new Error("invite code has an invalid relay URL");
41645
- if (!cellId)
41646
- throw new Error("invite code is missing cellId");
41647
- if (!secret)
41648
- throw new Error("invite code is missing secret");
41649
- assertSecureCentralUrl(central);
41650
- if (relay)
41651
- assertSecureRelayUrl(relay);
41652
- return {
41653
- ...typeof raw.v === "number" ? { v: raw.v } : {},
41654
- central,
41655
- ...relay ? { relay } : {},
41656
- cellId,
41657
- secret
41658
- };
41659
- }
41660
- function parseInviteCode(code) {
41661
- const trimmed = code.trim();
41662
- if (!trimmed)
41663
- throw new Error("invite code is empty");
41664
- let decoded;
41665
- try {
41666
- decoded = Buffer.from(trimmed, "base64url").toString("utf-8");
41667
- } catch {
41668
- throw new Error("invite code is not valid base64url");
41669
- }
41670
- let parsed;
41671
- try {
41672
- parsed = JSON.parse(decoded);
41673
- } catch {
41674
- throw new Error("invite code does not decode to JSON");
41675
- }
41676
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
41677
- throw new Error("invite code does not decode to a JSON object");
41678
- }
41679
- return normalizeJoin(parsed);
41680
- }
41681
- function joinLockPath() {
41682
- return resolve23(DATA_DIR, ".otium-join.lock");
41683
- }
41684
- function processIsAlive(pid) {
41685
- if (!Number.isSafeInteger(pid) || pid <= 0)
41686
- return false;
41687
- try {
41688
- process.kill(pid, 0);
41689
- return true;
41690
- } catch (error2) {
41691
- return error2.code !== "ESRCH";
41692
- }
41693
- }
41694
- function withJoinCredentialLock(operation) {
41695
- const lockPath = joinLockPath();
41696
- const ownerPath = resolve23(lockPath, "owner.json");
41697
- const owner = { pid: process.pid, token: randomUUID27() };
41698
- mkdirSync32(dirname21(lockPath), { recursive: true });
41699
- for (let attempt = 0;; attempt += 1) {
41700
- let created = false;
41701
- try {
41702
- mkdirSync32(lockPath, { mode: 448 });
41703
- created = true;
41704
- writeFileSync23(ownerPath, `${JSON.stringify(owner)}
41705
- `, { mode: 384 });
41706
- const ownerFd = openSync5(ownerPath, "r");
41707
- try {
41708
- fsyncSync2(ownerFd);
41709
- } finally {
41710
- closeSync5(ownerFd);
41711
- }
41712
- break;
41713
- } catch (error2) {
41714
- if (created) {
41715
- rmSync10(lockPath, { recursive: true, force: true });
41716
- throw error2;
41717
- }
41718
- if (error2.code !== "EEXIST")
41719
- throw error2;
41720
- let current3 = null;
41721
- try {
41722
- current3 = JSON.parse(readFileSync28(ownerPath, "utf8"));
41723
- } catch {}
41724
- let ageMs;
41725
- try {
41726
- ageMs = Date.now() - statSync20(lockPath).mtimeMs;
41727
- } catch (statError) {
41728
- if (statError.code === "ENOENT")
41729
- continue;
41730
- throw statError;
41731
- }
41732
- if (current3 && processIsAlive(current3.pid) || !current3 && ageMs <= JOIN_LOCK_STALE_MS) {
41733
- throw new Error(`another Otium join credential operation is in progress at ${lockPath}`);
41734
- }
41735
- if (attempt > 0) {
41736
- throw new Error(`could not recover stale Otium join credential lock at ${lockPath}`);
41737
- }
41738
- const stalePath = `${lockPath}.stale.${process.pid}.${randomUUID27()}`;
41739
- try {
41740
- renameSync16(lockPath, stalePath);
41741
- rmSync10(stalePath, { recursive: true, force: true });
41742
- } catch (staleError) {
41743
- if (staleError.code !== "ENOENT")
41744
- throw staleError;
41745
- }
41746
- }
41747
- }
41748
- try {
41749
- return operation();
41750
- } finally {
41751
- try {
41752
- const current3 = JSON.parse(readFileSync28(ownerPath, "utf8"));
41753
- if (current3.pid === owner.pid && current3.token === owner.token) {
41754
- rmSync10(lockPath, { recursive: true, force: true });
41755
- }
41756
- } catch {}
41757
- }
41758
- }
41759
- function joinsEqual(left, right) {
41760
- return left.central === right.central && left.relay === right.relay && left.cellId === right.cellId && left.secret === right.secret;
41761
- }
41762
- function normalizedJoin(join42) {
41763
- return normalizeJoin({
41764
- v: join42.v,
41765
- central: join42.central,
41766
- relay: join42.relay,
41767
- cellId: join42.cellId,
41768
- secret: join42.secret
41769
- });
41770
- }
41771
- function joinCredentialDigest(join42) {
41772
- return createHash11("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
41773
- }
41774
- function readPersistedJoin(path = joinFilePath()) {
41775
- if (!existsSync36(path))
41776
- return null;
41777
- const parsed = JSON.parse(readFileSync28(path, "utf-8"));
41778
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
41779
- throw new Error("persisted join credentials are not a JSON object");
41780
- }
41781
- return normalizeJoin(parsed);
41782
- }
41783
- function isJoinPersisted(join42) {
41784
- try {
41785
- const persisted = readPersistedJoin();
41786
- return persisted !== null && joinsEqual(persisted, normalizedJoin(join42));
41787
- } catch {
41788
- return false;
41789
- }
41790
- }
41791
- function saveJoinWhileLocked(join42, options = {}) {
41792
- const path = joinFilePath();
41793
- const directory = dirname21(path);
41794
- const normalized = normalizedJoin(join42);
41795
- mkdirSync32(directory, { recursive: true });
41796
- if (existsSync36(path)) {
41797
- if (lstatSync(path).isSymbolicLink()) {
41798
- throw new Error(`refusing to replace symlinked Otium join file at ${path}`);
41799
- }
41800
- let existing = null;
41801
- try {
41802
- existing = readPersistedJoin(path);
41803
- } catch (error2) {
41804
- if (!options.replaceExisting) {
41805
- throw new Error(`existing Otium join file at ${path} is invalid; pass --replace to replace it`, { cause: error2 });
41806
- }
41807
- }
41808
- if (existing && joinsEqual(existing, normalized)) {
41809
- chmodSync7(path, 384);
41810
- const fileFd = openSync5(path, "r");
41811
- try {
41812
- fsyncSync2(fileFd);
41813
- } finally {
41814
- closeSync5(fileFd);
41815
- }
41816
- const directoryFd = openSync5(directory, "r");
41817
- try {
41818
- fsyncSync2(directoryFd);
41819
- } finally {
41820
- closeSync5(directoryFd);
41821
- }
41822
- return path;
41823
- }
41824
- if (!options.replaceExisting) {
41825
- throw new Error(`this node is already joined${existing ? ` as ${existing.cellId}` : " with an invalid join file"}; pass --replace to replace its credentials`);
41826
- }
41827
- }
41828
- const temporaryPath = resolve23(directory, `.otium-join.json.${process.pid}.${randomUUID27()}.tmp`);
41829
- let fd;
41830
- try {
41831
- fd = openSync5(temporaryPath, "wx", 384);
41832
- writeFileSync23(fd, `${JSON.stringify(normalized, null, 2)}
41833
- `, "utf8");
41834
- fsyncSync2(fd);
41835
- closeSync5(fd);
41836
- fd = undefined;
41837
- if (options.replaceExisting) {
41838
- renameSync16(temporaryPath, path);
41839
- } else {
41840
- linkSync(temporaryPath, path);
41841
- unlinkSync23(temporaryPath);
41842
- }
41843
- chmodSync7(path, 384);
41844
- const directoryFd = openSync5(directory, "r");
41845
- try {
41846
- fsyncSync2(directoryFd);
41847
- } finally {
41848
- closeSync5(directoryFd);
41849
- }
41850
- } catch (error2) {
41851
- if (fd !== undefined)
41852
- closeSync5(fd);
41853
- if (existsSync36(temporaryPath))
41854
- unlinkSync23(temporaryPath);
41855
- throw error2;
41856
- }
41857
- return path;
41858
- }
41859
- function saveJoin(join42, options = {}) {
41860
- return withJoinCredentialLock(() => saveJoinWhileLocked(join42, options));
41861
- }
41862
- function removeJoin() {
41863
- return withJoinCredentialLock(() => {
41864
- const path = joinFilePath();
41865
- if (!existsSync36(path))
41866
- return false;
41867
- if (lstatSync(path).isSymbolicLink()) {
41868
- throw new Error(`refusing to remove symlinked Otium join file at ${path}`);
41869
- }
41870
- unlinkSync23(path);
41871
- const directoryFd = openSync5(dirname21(path), "r");
41872
- try {
41873
- fsyncSync2(directoryFd);
41874
- } finally {
41875
- closeSync5(directoryFd);
41876
- }
41877
- return true;
41878
- });
41879
- }
41880
- function loadJoin() {
41881
- const central = process.env.OTIUM_CENTRAL_URL?.trim();
41882
- const cellId = process.env.OTIUM_CELL_ID?.trim();
41883
- const secret = process.env.OTIUM_CELL_SECRET?.trim();
41884
- const relay = process.env.OTIUM_RELAY_URL?.trim();
41885
- if (central && cellId && secret) {
41886
- try {
41887
- return normalizeJoin({ central, relay, cellId, secret });
41888
- } catch (err2) {
41889
- logger.warn({ err: err2 }, "otium: invalid OTIUM_CENTRAL_URL/OTIUM_CELL_ID/OTIUM_CELL_SECRET env");
41890
- return null;
41891
- }
41892
- }
41893
- if (central || cellId || secret) {
41894
- logger.warn("otium: OTIUM_CENTRAL_URL, OTIUM_CELL_ID, OTIUM_CELL_SECRET must be set together \u2014 ignoring partial env");
41895
- }
41896
- const path = joinFilePath();
41897
- if (!existsSync36(path))
41898
- return null;
41899
- try {
41900
- const parsed = JSON.parse(readFileSync28(path, "utf-8"));
41901
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
41902
- return null;
41903
- return normalizeJoin(parsed);
41904
- } catch (err2) {
41905
- logger.warn({ err: err2, path }, "otium: failed to read join file");
41906
- return null;
41907
- }
41908
- }
41909
- var JOIN_LOCK_STALE_MS = 30000;
41910
- var init_join = __esm(async () => {
41911
- await init_src();
41912
- });
41913
-
41914
- // ../../packages/core/src/config-public.ts
41915
- var init_config_public = __esm(() => {
41916
- init_config();
41917
- });
41918
-
41919
- // ../../adapters/otium/src/join-status.ts
41920
- var exports_join_status = {};
41921
- __export(exports_join_status, {
41922
- hasConfiguredOtiumJoin: () => hasConfiguredOtiumJoin
41923
- });
41924
- import { existsSync as existsSync37 } from "fs";
41925
- import { resolve as resolve24 } from "path";
41926
- function hasConfiguredOtiumJoin() {
41927
- const envJoin = Boolean(process.env.OTIUM_CENTRAL_URL?.trim() && process.env.OTIUM_CELL_ID?.trim() && process.env.OTIUM_CELL_SECRET?.trim());
41928
- return envJoin || existsSync37(resolve24(DATA_DIR, "otium-join.json"));
41929
- }
41930
- var init_join_status = __esm(() => {
41931
- init_config_public();
41932
- });
41933
-
41934
- // ../../adapters/otium/src/control-protocol.ts
41935
- var OTIUM_ADAPTER_CONTROL_PREFIX = "/api/v1/adapter/otium", OTIUM_ADAPTER_CONTROL_HEADER = "x-negotium-adapter-token";
41936
-
41937
41590
  // ../../adapters/otium/src/protocol.ts
41938
- function str(body, field) {
41939
- const value = body[field];
41940
- return typeof value === "string" && value.trim() ? value : null;
41941
- }
41942
- function parseExecutionSpec(value) {
41943
- if (!value || typeof value !== "object" || Array.isArray(value))
41944
- return null;
41945
- const raw = value;
41946
- const agent = str(raw, "agent");
41947
- const model = str(raw, "model");
41948
- const effort = str(raw, "effort");
41949
- const rawMcp = raw.mcp;
41950
- if (!agent || !model || !effort || !Array.isArray(rawMcp) || !rawMcp.every((entry) => typeof entry === "string" && entry.trim().length > 0) || typeof raw.canSpawnSubagents !== "boolean") {
41951
- return null;
41952
- }
41953
- return {
41954
- agent,
41955
- model,
41956
- effort,
41957
- ...str(raw, "description") ? { description: str(raw, "description") } : {},
41958
- mcp: [...new Set(rawMcp.map((entry) => entry.trim()))],
41959
- canSpawnSubagents: raw.canSpawnSubagents
41960
- };
41961
- }
41962
- var PEER_PROTOCOL_VERSION = 1, MAX_PEER_MESSAGE_LENGTH = 1e4, MAX_PEER_INPUT_FILE_BYTES, MAX_PEER_INPUT_REQUEST_BYTES;
41591
+ var PEER_PROTOCOL_VERSION = 1, MAX_PEER_MESSAGE_LENGTH = 1e4, MAX_PEER_REQUEST_BODY_BYTES;
41963
41592
  var init_protocol = __esm(() => {
41964
- MAX_PEER_INPUT_FILE_BYTES = 2 * 1024 * 1024 * 1024;
41965
- MAX_PEER_INPUT_REQUEST_BYTES = MAX_PEER_INPUT_FILE_BYTES + 8 * 1024 * 1024;
41593
+ MAX_PEER_REQUEST_BODY_BYTES = 2 * 1024 * 1024 * 1024 + 8 * 1024 * 1024;
41966
41594
  });
41967
41595
 
41968
41596
  // ../../adapters/otium/src/canonical-mcp-bridge.ts
41969
- import { randomUUID as randomUUID28 } from "crypto";
41597
+ import { randomUUID as randomUUID27 } from "crypto";
41970
41598
  function readAuthorization(request) {
41971
41599
  const value = request.headers.get("authorization");
41972
41600
  return value?.startsWith("Bearer ") ? value.slice(7) : null;
@@ -42107,7 +41735,7 @@ function startCanonicalMcpBridge(options = {}) {
42107
41735
  const url = `http://127.0.0.1:${server.port}/`;
42108
41736
  const unregister = registerCanonicalMcpBridgeEnvProvider((scope) => {
42109
41737
  sweep();
42110
- const token = `${randomUUID28()}${randomUUID28()}`;
41738
+ const token = `${randomUUID27()}${randomUUID27()}`;
42111
41739
  capabilities.set(token, {
42112
41740
  surface: scope.surface,
42113
41741
  userId: scope.userId,
@@ -42159,736 +41787,360 @@ var init_canonical_mcp_bridge = __esm(async () => {
42159
41787
  };
42160
41788
  });
42161
41789
 
42162
- // ../../adapters/otium/src/store.ts
42163
- import { createHash as createHash12 } from "crypto";
42164
- function isPeerDetached(hubNodeId) {
42165
- const row = db.query("SELECT status FROM otium_peer_lifecycle WHERE hub_node_id = ?").get(hubNodeId);
42166
- return row?.status === "detached";
42167
- }
42168
- function enqueueSharedMessage(args) {
42169
- db.run(`INSERT OR REPLACE INTO otium_shared_message_outbox
42170
- (local_topic_id, source_message_id, message_json, created_at)
42171
- VALUES (?, ?, ?, ?)`, [
42172
- args.localTopicId,
42173
- args.sourceMessageId,
42174
- JSON.stringify(args.message),
42175
- new Date().toISOString()
42176
- ]);
42177
- }
42178
- function listSharedMessages(localTopicId) {
42179
- return db.query("SELECT * FROM otium_shared_message_outbox WHERE local_topic_id = ? ORDER BY created_at").all(localTopicId);
42180
- }
42181
- function deleteSharedMessage(localTopicId, sourceMessageId) {
42182
- return db.run("DELETE FROM otium_shared_message_outbox WHERE local_topic_id = ? AND source_message_id = ?", [localTopicId, sourceMessageId]).changes === 1;
42183
- }
42184
- function getSharedTopicState(localTopicId) {
42185
- return db.query("SELECT * FROM otium_shared_topic_state WHERE local_topic_id = ?").get(localTopicId) ?? null;
42186
- }
42187
- function listSharedTopicStates() {
42188
- return db.query("SELECT * FROM otium_shared_topic_state ORDER BY updated_at").all();
42189
- }
42190
- function setSharedTopicState(args) {
42191
- db.run(`INSERT INTO otium_shared_topic_state (local_topic_id, host_topic_id, status, updated_at)
42192
- VALUES (?, ?, ?, ?)
42193
- ON CONFLICT(local_topic_id) DO UPDATE SET
42194
- host_topic_id = excluded.host_topic_id,
42195
- status = excluded.status,
42196
- updated_at = excluded.updated_at`, [args.localTopicId, args.hostTopicId ?? null, args.status, new Date().toISOString()]);
42197
- }
42198
- function deleteSharedTopicState(localTopicId) {
42199
- return db.run("DELETE FROM otium_shared_topic_state WHERE local_topic_id = ?", [localTopicId]).changes === 1;
42200
- }
42201
- function getPeerSession(hostNodeId, hostTopicId) {
42202
- return db.query("SELECT * FROM otium_peer_sessions WHERE host_node_id = ? AND host_topic_id = ?").get(hostNodeId, hostTopicId) ?? null;
42203
- }
42204
- function createPeerSession(hostNodeId, hostTopicId, localTopicId) {
42205
- const row = {
42206
- host_node_id: hostNodeId,
42207
- host_topic_id: hostTopicId,
42208
- local_topic_id: localTopicId,
42209
- binding_mode: "mirror",
42210
- created_at: new Date().toISOString()
42211
- };
42212
- db.run("INSERT INTO otium_peer_sessions (host_node_id, host_topic_id, local_topic_id, binding_mode, created_at) VALUES (?, ?, ?, ?, ?)", [row.host_node_id, row.host_topic_id, row.local_topic_id, row.binding_mode, row.created_at]);
42213
- return row;
42214
- }
42215
- function bindPeerSession(hostNodeId, hostTopicId, localTopicId, mode = "shared") {
42216
- const row = {
42217
- host_node_id: hostNodeId,
42218
- host_topic_id: hostTopicId,
42219
- local_topic_id: localTopicId,
42220
- binding_mode: mode,
42221
- created_at: new Date().toISOString()
42222
- };
42223
- db.run(`INSERT INTO otium_peer_sessions
42224
- (host_node_id, host_topic_id, local_topic_id, binding_mode, created_at)
42225
- VALUES (?, ?, ?, ?, ?)
42226
- ON CONFLICT(host_node_id, host_topic_id) DO UPDATE SET
42227
- local_topic_id = excluded.local_topic_id,
42228
- binding_mode = excluded.binding_mode,
42229
- created_at = excluded.created_at`, [hostNodeId, hostTopicId, localTopicId, mode, row.created_at]);
42230
- return row;
42231
- }
42232
- function unbindPeerSession(hostNodeId, hostTopicId) {
42233
- return db.run("DELETE FROM otium_peer_sessions WHERE host_node_id = ? AND host_topic_id = ?", [
42234
- hostNodeId,
42235
- hostTopicId
42236
- ]).changes === 1;
42237
- }
42238
- function unbindSharedPeerSessionsForLocalTopic(localTopicId) {
42239
- return db.run("DELETE FROM otium_peer_sessions WHERE local_topic_id = ? AND binding_mode = 'shared'", [localTopicId]).changes;
42240
- }
42241
- function downgradeSharedTopicsLocally(hubNodeId) {
42242
- return db.transaction(() => {
42243
- const topics = db.query("SELECT id FROM api_topics WHERE access_mode = 'shared'").all().map((row) => row.id);
42244
- db.run("UPDATE api_topics SET access_mode = 'private' WHERE access_mode = 'shared'");
42245
- db.run("DELETE FROM otium_peer_sessions WHERE host_node_id = ?", [hubNodeId]);
42246
- db.run("DELETE FROM otium_shared_message_outbox");
42247
- db.run("DELETE FROM otium_shared_topic_state");
42248
- db.run(`INSERT INTO otium_peer_lifecycle (hub_node_id, status, updated_at)
42249
- VALUES (?, 'detached', ?)
42250
- ON CONFLICT(hub_node_id) DO UPDATE SET status = 'detached', updated_at = excluded.updated_at`, [hubNodeId, new Date().toISOString()]);
42251
- return topics;
42252
- })();
42253
- }
42254
- function listPeerSessions() {
42255
- return db.query("SELECT * FROM otium_peer_sessions").all();
42256
- }
42257
- function cleanupPeerStateForLocalTopic(localTopicId) {
42258
- return db.transaction(() => {
42259
- const terminalOutbox = db.run(`DELETE FROM otium_peer_terminal_outbox
42260
- WHERE EXISTS (
42261
- SELECT 1 FROM otium_peer_turn_requests turn_request
42262
- JOIN otium_peer_sessions session
42263
- ON session.host_node_id = turn_request.host_node_id
42264
- AND session.host_topic_id = turn_request.host_topic_id
42265
- WHERE session.local_topic_id = ?
42266
- AND turn_request.host_node_id = otium_peer_terminal_outbox.host_node_id
42267
- AND turn_request.request_id = otium_peer_terminal_outbox.request_id
42268
- )`, [localTopicId]).changes;
42269
- const turns = db.run(`DELETE FROM otium_peer_turn_requests
42270
- WHERE EXISTS (
42271
- SELECT 1 FROM otium_peer_sessions session
42272
- WHERE session.local_topic_id = ?
42273
- AND session.host_node_id = otium_peer_turn_requests.host_node_id
42274
- AND session.host_topic_id = otium_peer_turn_requests.host_topic_id
42275
- )`, [localTopicId]).changes;
42276
- const inboxRequests = db.run("DELETE FROM otium_peer_inbox_requests WHERE topic_id = ?", [
42277
- localTopicId
42278
- ]).changes;
42279
- const remoteAsks = db.run("DELETE FROM otium_remote_asks WHERE caller_topic_id = ?", [
42280
- localTopicId
42281
- ]).changes;
42282
- const sessions = db.run("DELETE FROM otium_peer_sessions WHERE local_topic_id = ?", [
42283
- localTopicId
42284
- ]).changes;
42285
- return { sessions, turns, terminalOutbox, inboxRequests, remoteAsks };
42286
- })();
42287
- }
42288
- function sweepStalePeerBindings(topicExists) {
42289
- const stale = [
42290
- ...new Set(listPeerSessions().map((row) => row.local_topic_id).filter((localTopicId) => localTopicId && !topicExists(localTopicId)))
42291
- ];
42292
- const removed = {
42293
- sessions: 0,
42294
- turns: 0,
42295
- terminalOutbox: 0,
42296
- inboxRequests: 0,
42297
- remoteAsks: 0
42298
- };
42299
- for (const localTopicId of stale) {
42300
- const result = cleanupPeerStateForLocalTopic(localTopicId);
42301
- removed.sessions += result.sessions;
42302
- removed.turns += result.turns;
42303
- removed.terminalOutbox += result.terminalOutbox;
42304
- removed.inboxRequests += result.inboxRequests;
42305
- removed.remoteAsks += result.remoteAsks;
42306
- }
42307
- return { topicIds: stale, removed };
42308
- }
42309
- function failInterruptedPeerTurnRequestsOnStartup() {
42310
- return db.run(`UPDATE otium_peer_turn_requests
42311
- SET status = 'failed', error = 'worker restarted during turn', updated_at = ?
42312
- WHERE status IN ('claimed', 'running')
42313
- AND NOT EXISTS (
42314
- SELECT 1 FROM otium_peer_terminal_outbox terminal
42315
- WHERE terminal.host_node_id = otium_peer_turn_requests.host_node_id
42316
- AND terminal.request_id = otium_peer_turn_requests.request_id
42317
- )`, [new Date().toISOString()]).changes;
42318
- }
42319
- function claimPeerTurnRequest(hostNodeId, requestId, hostTopicId) {
42320
- const now = new Date().toISOString();
42321
- const inserted = db.run(`INSERT OR IGNORE INTO otium_peer_turn_requests
42322
- (host_node_id, request_id, host_topic_id, status, created_at, updated_at)
42323
- VALUES (?, ?, ?, 'claimed', ?, ?)`, [hostNodeId, requestId, hostTopicId, now, now]);
42324
- const row = getPeerTurnRequest(hostNodeId, requestId);
42325
- if (!row)
42326
- throw new Error("otium peer turn request claim disappeared");
42327
- return { claimed: inserted.changes === 1, row };
42328
- }
42329
- function getPeerTurnRequest(hostNodeId, requestId) {
42330
- return db.query("SELECT * FROM otium_peer_turn_requests WHERE host_node_id = ? AND request_id = ?").get(hostNodeId, requestId) ?? null;
42331
- }
42332
- function setPeerTurnRequestStatus(hostNodeId, requestId, status, error2 = null) {
42333
- db.run(`UPDATE otium_peer_turn_requests
42334
- SET status = ?, error = ?, updated_at = ?
42335
- WHERE host_node_id = ? AND request_id = ?`, [status, error2, new Date().toISOString(), hostNodeId, requestId]);
42336
- }
42337
- function markPeerTurnRequestRunning(hostNodeId, requestId) {
42338
- setPeerTurnRequestStatus(hostNodeId, requestId, "running");
42339
- }
42340
- function markPeerTurnRequestFinished(hostNodeId, requestId) {
42341
- setPeerTurnRequestStatus(hostNodeId, requestId, "finished");
42342
- }
42343
- function markPeerTurnRequestFailed(hostNodeId, requestId, error2) {
42344
- setPeerTurnRequestStatus(hostNodeId, requestId, "failed", error2);
42345
- }
42346
- function upsertPeerTerminalOutbox(args) {
42347
- const now = Date.now();
42348
- db.run(`INSERT INTO otium_peer_terminal_outbox
42349
- (host_node_id, request_id, seq, event_json, created_at, updated_at)
42350
- VALUES (?, ?, ?, ?, ?, ?)
42351
- ON CONFLICT(host_node_id, request_id) DO UPDATE SET
42352
- seq = excluded.seq, event_json = excluded.event_json, updated_at = excluded.updated_at`, [args.hostNodeId, args.requestId, args.seq, JSON.stringify(args.event), now, now]);
42353
- }
42354
- function listPeerTerminalOutbox(limit = 100) {
42355
- return db.query("SELECT * FROM otium_peer_terminal_outbox ORDER BY created_at LIMIT ?").all(limit);
42356
- }
42357
- function acknowledgePeerTerminal(hostNodeId, requestId) {
42358
- return db.transaction(() => {
42359
- const removed = db.run("DELETE FROM otium_peer_terminal_outbox WHERE host_node_id = ? AND request_id = ?", [hostNodeId, requestId]).changes;
42360
- if (removed !== 1)
42361
- return false;
42362
- markPeerTurnRequestFinished(hostNodeId, requestId);
41790
+ // ../../adapters/otium/src/secure-transport.ts
41791
+ function isLoopbackHostname(hostname2) {
41792
+ const normalized = hostname2.toLowerCase();
41793
+ if (normalized === "localhost" || normalized === "::1" || normalized === "[::1]")
42363
41794
  return true;
42364
- })();
41795
+ const octets = normalized.split(".");
41796
+ return octets.length === 4 && octets[0] === "127" && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
42365
41797
  }
42366
- function peerInboxPayloadHash(value) {
42367
- return createHash12("sha256").update(JSON.stringify(value)).digest("hex");
41798
+ function credentialTransportUrl(value, label) {
41799
+ let url;
41800
+ try {
41801
+ url = new URL(value);
41802
+ } catch {
41803
+ throw new Error(`${label} is not a valid URL`);
41804
+ }
41805
+ if (url.username || url.password)
41806
+ throw new Error(`${label} must not contain URL credentials`);
41807
+ return url;
42368
41808
  }
42369
- function claimPeerInboxRequest(args) {
42370
- const inserted = db.run(`INSERT OR IGNORE INTO otium_peer_inbox_requests
42371
- (from_cell_id, request_id, kind, topic_id, payload_hash, created_at)
42372
- VALUES (?, ?, ?, ?, ?, ?)`, [
42373
- args.fromCellId,
42374
- args.requestId,
42375
- args.kind,
42376
- args.topicId,
42377
- args.payloadHash,
42378
- new Date().toISOString()
42379
- ]);
42380
- if (inserted.changes === 1)
42381
- return { outcome: "claimed" };
42382
- const existing = db.query("SELECT payload_hash FROM otium_peer_inbox_requests WHERE from_cell_id = ? AND request_id = ? AND kind = ?").get(args.fromCellId, args.requestId, args.kind);
42383
- if (!existing)
42384
- return { outcome: "conflict" };
42385
- return { outcome: existing.payload_hash === args.payloadHash ? "replay" : "conflict" };
41809
+ function assertSecureCentralUrl(value) {
41810
+ const url = credentialTransportUrl(value, "Otium central URL");
41811
+ if (url.protocol === "https:")
41812
+ return;
41813
+ if (url.protocol === "http:" && isLoopbackHostname(url.hostname))
41814
+ return;
41815
+ throw new Error("Otium central requires HTTPS or loopback HTTP");
42386
41816
  }
42387
- function releasePeerInboxRequest(fromCellId, requestId, kind) {
42388
- db.run("DELETE FROM otium_peer_inbox_requests WHERE from_cell_id = ? AND request_id = ? AND kind = ?", [fromCellId, requestId, kind]);
41817
+ function assertSecureRelayUrl(value) {
41818
+ const url = credentialTransportUrl(value, "Otium relay URL");
41819
+ if (url.protocol === "https:" || url.protocol === "wss:")
41820
+ return;
41821
+ if ((url.protocol === "http:" || url.protocol === "ws:") && isLoopbackHostname(url.hostname)) {
41822
+ return;
41823
+ }
41824
+ throw new Error("Otium relay requires HTTPS/WSS or loopback HTTP/WS");
42389
41825
  }
42390
- function createRemoteAsk(args) {
42391
- const result = db.run(`INSERT OR IGNORE INTO otium_remote_asks
42392
- (request_id, expected_cell_id, user_id, caller_topic_id, from_key, to_key,
42393
- source_query_id, created_at)
42394
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
42395
- args.requestId,
42396
- args.expectedCellId,
42397
- args.userId,
42398
- args.callerTopicId,
42399
- args.from,
42400
- args.to,
42401
- args.sourceQueryId ?? null,
42402
- args.createdAt ?? Date.now()
42403
- ]);
42404
- return result.changes === 1;
41826
+
41827
+ // ../../adapters/otium/src/join.ts
41828
+ var exports_join = {};
41829
+ __export(exports_join, {
41830
+ withJoinCredentialLock: () => withJoinCredentialLock,
41831
+ saveJoinWhileLocked: () => saveJoinWhileLocked,
41832
+ saveJoin: () => saveJoin,
41833
+ removeJoin: () => removeJoin,
41834
+ parseInviteCode: () => parseInviteCode,
41835
+ loadJoin: () => loadJoin,
41836
+ joinFilePath: () => joinFilePath,
41837
+ joinCredentialDigest: () => joinCredentialDigest,
41838
+ isJoinPersisted: () => isJoinPersisted
41839
+ });
41840
+ import { createHash as createHash11, randomUUID as randomUUID28 } from "crypto";
41841
+ import {
41842
+ chmodSync as chmodSync7,
41843
+ closeSync as closeSync5,
41844
+ existsSync as existsSync37,
41845
+ fsyncSync as fsyncSync2,
41846
+ linkSync,
41847
+ lstatSync,
41848
+ mkdirSync as mkdirSync32,
41849
+ openSync as openSync5,
41850
+ readFileSync as readFileSync28,
41851
+ renameSync as renameSync16,
41852
+ rmSync as rmSync10,
41853
+ statSync as statSync20,
41854
+ unlinkSync as unlinkSync23,
41855
+ writeFileSync as writeFileSync23
41856
+ } from "fs";
41857
+ import { dirname as dirname21, resolve as resolve24 } from "path";
41858
+ function joinFilePath() {
41859
+ return resolve24(DATA_DIR, "otium-join.json");
42405
41860
  }
42406
- function getRemoteAsk(requestId) {
42407
- return db.query("SELECT * FROM otium_remote_asks WHERE request_id = ?").get(requestId) ?? null;
41861
+ function isHttpUrl(value) {
41862
+ return /^https?:\/\//.test(value);
42408
41863
  }
42409
- function deleteRemoteAsk(requestId) {
42410
- return db.run("DELETE FROM otium_remote_asks WHERE request_id = ?", [requestId]).changes === 1;
41864
+ function isRelayUrl(value) {
41865
+ return /^(?:https?|wss?):\/\//.test(value);
42411
41866
  }
42412
- function pruneRemoteAsks(olderThan) {
42413
- return db.run("DELETE FROM otium_remote_asks WHERE created_at < ?", [olderThan]).changes;
41867
+ function normalizeJoin(raw) {
41868
+ const central = typeof raw.central === "string" ? raw.central.trim().replace(/\/+$/, "") : "";
41869
+ const relay = typeof raw.relay === "string" ? raw.relay.trim().replace(/\/+$/, "") : "";
41870
+ const cellId = typeof raw.cellId === "string" ? raw.cellId.trim() : "";
41871
+ const secret = typeof raw.secret === "string" ? raw.secret.trim() : "";
41872
+ if (!central || !isHttpUrl(central)) {
41873
+ throw new Error("invite code is missing a valid http(s) central URL");
41874
+ }
41875
+ if (relay && !isRelayUrl(relay))
41876
+ throw new Error("invite code has an invalid relay URL");
41877
+ if (!cellId)
41878
+ throw new Error("invite code is missing cellId");
41879
+ if (!secret)
41880
+ throw new Error("invite code is missing secret");
41881
+ assertSecureCentralUrl(central);
41882
+ if (relay)
41883
+ assertSecureRelayUrl(relay);
41884
+ return {
41885
+ ...typeof raw.v === "number" ? { v: raw.v } : {},
41886
+ central,
41887
+ ...relay ? { relay } : {},
41888
+ cellId,
41889
+ secret
41890
+ };
42414
41891
  }
42415
- function upsertPeerReplyOutbox(args) {
42416
- const now = Date.now();
42417
- db.run(`INSERT INTO otium_peer_reply_outbox
42418
- (node_cell_id, request_id, node_name, topic_id, user_id, source_title,
42419
- reply_text, kind, created_at, updated_at)
42420
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
42421
- ON CONFLICT(node_cell_id, request_id) DO UPDATE SET
42422
- node_name = excluded.node_name,
42423
- topic_id = excluded.topic_id,
42424
- user_id = excluded.user_id,
42425
- source_title = excluded.source_title,
42426
- reply_text = excluded.reply_text,
42427
- kind = excluded.kind,
42428
- updated_at = excluded.updated_at`, [
42429
- args.nodeCellId,
42430
- args.requestId,
42431
- args.nodeName,
42432
- args.topicId,
42433
- args.userId,
42434
- args.sourceTitle,
42435
- args.replyText,
42436
- args.kind,
42437
- now,
42438
- now
42439
- ]);
41892
+ function parseInviteCode(code) {
41893
+ const trimmed = code.trim();
41894
+ if (!trimmed)
41895
+ throw new Error("invite code is empty");
41896
+ let decoded;
41897
+ try {
41898
+ decoded = Buffer.from(trimmed, "base64url").toString("utf-8");
41899
+ } catch {
41900
+ throw new Error("invite code is not valid base64url");
41901
+ }
41902
+ let parsed;
41903
+ try {
41904
+ parsed = JSON.parse(decoded);
41905
+ } catch {
41906
+ throw new Error("invite code does not decode to JSON");
41907
+ }
41908
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
41909
+ throw new Error("invite code does not decode to a JSON object");
41910
+ }
41911
+ return normalizeJoin(parsed);
42440
41912
  }
42441
- function listPeerReplyOutbox(limit = 100) {
42442
- return db.query("SELECT * FROM otium_peer_reply_outbox ORDER BY created_at LIMIT ?").all(limit);
41913
+ function joinLockPath() {
41914
+ return resolve24(DATA_DIR, ".otium-join.lock");
42443
41915
  }
42444
- function deletePeerReplyOutbox(nodeCellId, requestId) {
42445
- return db.run("DELETE FROM otium_peer_reply_outbox WHERE node_cell_id = ? AND request_id = ?", [
42446
- nodeCellId,
42447
- requestId
42448
- ]).changes === 1;
41916
+ function processIsAlive(pid) {
41917
+ if (!Number.isSafeInteger(pid) || pid <= 0)
41918
+ return false;
41919
+ try {
41920
+ process.kill(pid, 0);
41921
+ return true;
41922
+ } catch (error2) {
41923
+ return error2.code !== "ESRCH";
41924
+ }
42449
41925
  }
42450
- var peerSessionColumns;
42451
- var init_store2 = __esm(async () => {
42452
- await init_src();
42453
- db.exec(`
42454
- CREATE TABLE IF NOT EXISTS otium_peer_sessions (
42455
- host_node_id TEXT NOT NULL,
42456
- host_topic_id TEXT NOT NULL,
42457
- local_topic_id TEXT NOT NULL,
42458
- binding_mode TEXT NOT NULL DEFAULT 'mirror',
42459
- created_at TEXT NOT NULL,
42460
- PRIMARY KEY (host_node_id, host_topic_id)
42461
- )
42462
- `);
42463
- peerSessionColumns = new Set(db.query("PRAGMA table_info(otium_peer_sessions)").all().map((row) => row.name));
42464
- if (!peerSessionColumns.has("binding_mode")) {
42465
- db.exec("ALTER TABLE otium_peer_sessions ADD COLUMN binding_mode TEXT NOT NULL DEFAULT 'mirror'");
42466
- }
42467
- db.run(`UPDATE api_topics
42468
- SET visibility = 'visible', access_mode = 'shared', is_subagent = 0
42469
- WHERE id IN (
42470
- SELECT local_topic_id FROM otium_peer_sessions WHERE binding_mode = 'mirror'
42471
- )`);
42472
- db.run(`UPDATE api_topics
42473
- SET access_mode = 'shared'
42474
- WHERE id IN (SELECT local_topic_id FROM otium_peer_sessions)`);
42475
- db.exec(`
42476
- CREATE TABLE IF NOT EXISTS otium_peer_turn_requests (
42477
- host_node_id TEXT NOT NULL,
42478
- request_id TEXT NOT NULL,
42479
- host_topic_id TEXT NOT NULL,
42480
- status TEXT NOT NULL CHECK (status IN
42481
- ('claimed', 'running', 'finished', 'failed')),
42482
- error TEXT,
42483
- created_at TEXT NOT NULL,
42484
- updated_at TEXT NOT NULL,
42485
- PRIMARY KEY (host_node_id, request_id)
42486
- )
42487
- `);
42488
- db.exec(`
42489
- CREATE TABLE IF NOT EXISTS otium_peer_terminal_outbox (
42490
- host_node_id TEXT NOT NULL,
42491
- request_id TEXT NOT NULL,
42492
- seq INTEGER NOT NULL,
42493
- event_json TEXT NOT NULL,
42494
- created_at INTEGER NOT NULL,
42495
- updated_at INTEGER NOT NULL,
42496
- PRIMARY KEY (host_node_id, request_id)
42497
- )
42498
- `);
42499
- db.exec(`
42500
- CREATE TABLE IF NOT EXISTS otium_peer_inbox_requests (
42501
- from_cell_id TEXT NOT NULL,
42502
- request_id TEXT NOT NULL,
42503
- kind TEXT NOT NULL CHECK (kind IN ('tell', 'ask')),
42504
- topic_id TEXT NOT NULL,
42505
- payload_hash TEXT NOT NULL,
42506
- created_at TEXT NOT NULL,
42507
- PRIMARY KEY (from_cell_id, request_id, kind)
42508
- )
42509
- `);
42510
- db.exec(`
42511
- CREATE TABLE IF NOT EXISTS otium_remote_asks (
42512
- request_id TEXT PRIMARY KEY,
42513
- expected_cell_id TEXT NOT NULL,
42514
- user_id TEXT NOT NULL,
42515
- caller_topic_id TEXT NOT NULL,
42516
- from_key TEXT NOT NULL,
42517
- to_key TEXT NOT NULL,
42518
- source_query_id TEXT,
42519
- created_at INTEGER NOT NULL
42520
- )
42521
- `);
42522
- db.exec(`
42523
- CREATE INDEX IF NOT EXISTS idx_otium_remote_asks_created
42524
- ON otium_remote_asks(created_at)
42525
- `);
42526
- db.exec(`
42527
- CREATE TABLE IF NOT EXISTS otium_peer_reply_outbox (
42528
- node_cell_id TEXT NOT NULL,
42529
- request_id TEXT NOT NULL,
42530
- node_name TEXT NOT NULL,
42531
- topic_id TEXT NOT NULL,
42532
- user_id TEXT NOT NULL,
42533
- source_title TEXT NOT NULL,
42534
- reply_text TEXT NOT NULL,
42535
- kind TEXT NOT NULL CHECK (kind IN ('reply', 'error')),
42536
- created_at INTEGER NOT NULL,
42537
- updated_at INTEGER NOT NULL,
42538
- PRIMARY KEY (node_cell_id, request_id)
42539
- )
42540
- `);
42541
- db.exec(`
42542
- CREATE TABLE IF NOT EXISTS otium_shared_topic_state (
42543
- local_topic_id TEXT PRIMARY KEY,
42544
- host_topic_id TEXT,
42545
- status TEXT NOT NULL CHECK (status IN ('publishing', 'published', 'unpublishing')),
42546
- updated_at TEXT NOT NULL
42547
- )
42548
- `);
42549
- db.exec(`
42550
- CREATE TABLE IF NOT EXISTS otium_shared_message_outbox (
42551
- local_topic_id TEXT NOT NULL,
42552
- source_message_id TEXT NOT NULL,
42553
- message_json TEXT NOT NULL,
42554
- created_at TEXT NOT NULL,
42555
- PRIMARY KEY (local_topic_id, source_message_id)
42556
- )
42557
- `);
42558
- db.exec(`
42559
- CREATE TABLE IF NOT EXISTS otium_peer_lifecycle (
42560
- hub_node_id TEXT PRIMARY KEY,
42561
- status TEXT NOT NULL CHECK (status IN ('attached', 'detached')),
42562
- updated_at TEXT NOT NULL
42563
- )
42564
- `);
42565
- });
42566
-
42567
- // ../../adapters/otium/src/event-backflow.ts
42568
- function hubEventSender(hubNode) {
42569
- return async (payload) => {
42570
- let token;
41926
+ function withJoinCredentialLock(operation) {
41927
+ const lockPath = joinLockPath();
41928
+ const ownerPath = resolve24(lockPath, "owner.json");
41929
+ const owner = { pid: process.pid, token: randomUUID28() };
41930
+ mkdirSync32(dirname21(lockPath), { recursive: true });
41931
+ for (let attempt = 0;; attempt += 1) {
41932
+ let created = false;
42571
41933
  try {
42572
- token = await mintPeerToken(hubNode.cellId);
42573
- } catch (err2) {
42574
- return { ok: false, error: `peer token mint failed: ${err2.message}` };
41934
+ mkdirSync32(lockPath, { mode: 448 });
41935
+ created = true;
41936
+ writeFileSync23(ownerPath, `${JSON.stringify(owner)}
41937
+ `, { mode: 384 });
41938
+ const ownerFd = openSync5(ownerPath, "r");
41939
+ try {
41940
+ fsyncSync2(ownerFd);
41941
+ } finally {
41942
+ closeSync5(ownerFd);
41943
+ }
41944
+ break;
41945
+ } catch (error2) {
41946
+ if (created) {
41947
+ rmSync10(lockPath, { recursive: true, force: true });
41948
+ throw error2;
41949
+ }
41950
+ if (error2.code !== "EEXIST")
41951
+ throw error2;
41952
+ let current3 = null;
41953
+ try {
41954
+ current3 = JSON.parse(readFileSync28(ownerPath, "utf8"));
41955
+ } catch {}
41956
+ let ageMs;
41957
+ try {
41958
+ ageMs = Date.now() - statSync20(lockPath).mtimeMs;
41959
+ } catch (statError) {
41960
+ if (statError.code === "ENOENT")
41961
+ continue;
41962
+ throw statError;
41963
+ }
41964
+ if (current3 && processIsAlive(current3.pid) || !current3 && ageMs <= JOIN_LOCK_STALE_MS) {
41965
+ throw new Error(`another Otium join credential operation is in progress at ${lockPath}`);
41966
+ }
41967
+ if (attempt > 0) {
41968
+ throw new Error(`could not recover stale Otium join credential lock at ${lockPath}`);
41969
+ }
41970
+ const stalePath = `${lockPath}.stale.${process.pid}.${randomUUID28()}`;
41971
+ try {
41972
+ renameSync16(lockPath, stalePath);
41973
+ rmSync10(stalePath, { recursive: true, force: true });
41974
+ } catch (staleError) {
41975
+ if (staleError.code !== "ENOENT")
41976
+ throw staleError;
41977
+ }
42575
41978
  }
42576
- let response;
41979
+ }
41980
+ try {
41981
+ return operation();
41982
+ } finally {
42577
41983
  try {
42578
- response = await fetch(`${hubNode.baseUrl.replace(/\/+$/, "")}/api/v1/peer/event`, {
42579
- method: "POST",
42580
- headers: {
42581
- authorization: `Bearer ${token}`,
42582
- "content-type": "application/json"
42583
- },
42584
- body: JSON.stringify(payload),
42585
- signal: AbortSignal.timeout(PEER_EVENT_TIMEOUT_MS)
42586
- });
42587
- } catch {
42588
- return { ok: false, error: `hub "${hubNode.nodeName ?? hubNode.cellId}" unreachable` };
42589
- }
42590
- const parsed = await response.json().catch(() => null);
42591
- if (!response.ok || !parsed?.ok) {
42592
- return {
42593
- ok: false,
42594
- error: parsed?.error ?? `peer event rejected (${response.status})`,
42595
- status: response.status
42596
- };
42597
- }
42598
- return { ok: true };
42599
- };
42600
- }
42601
- function defined(record) {
42602
- const out = {};
42603
- for (const [key, value] of Object.entries(record)) {
42604
- if (value !== undefined)
42605
- out[key] = value;
41984
+ const current3 = JSON.parse(readFileSync28(ownerPath, "utf8"));
41985
+ if (current3.pid === owner.pid && current3.token === owner.token) {
41986
+ rmSync10(lockPath, { recursive: true, force: true });
41987
+ }
41988
+ } catch {}
42606
41989
  }
42607
- return out;
42608
41990
  }
42609
- function translateBusEvent(event) {
42610
- const topicId = event.topicId;
42611
- if (event.type === "message") {
42612
- return { type: "message", topicId, message: event.payload };
42613
- }
42614
- if (event.type === "message-updated") {
42615
- const payload = event.payload;
42616
- return defined({
42617
- type: "message_updated",
42618
- topicId,
42619
- messageId: payload.messageId,
42620
- text: payload.patch.text,
42621
- deleted: payload.patch.deleted,
42622
- editedAt: payload.patch.editedAt,
42623
- usage: payload.patch.usage
42624
- });
42625
- }
42626
- if (event.type !== "ai-status")
41991
+ function joinsEqual(left, right) {
41992
+ return left.central === right.central && left.relay === right.relay && left.cellId === right.cellId && left.secret === right.secret;
41993
+ }
41994
+ function normalizedJoin(join42) {
41995
+ return normalizeJoin({
41996
+ v: join42.v,
41997
+ central: join42.central,
41998
+ relay: join42.relay,
41999
+ cellId: join42.cellId,
42000
+ secret: join42.secret
42001
+ });
42002
+ }
42003
+ function joinCredentialDigest(join42) {
42004
+ return createHash11("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
42005
+ }
42006
+ function readPersistedJoin(path = joinFilePath()) {
42007
+ if (!existsSync37(path))
42627
42008
  return null;
42628
- const status = event.payload;
42629
- const kind = typeof status.kind === "string" ? status.kind : "";
42630
- switch (kind) {
42631
- case "typing":
42632
- return { type: "typing", topicId, userId: status.userId ?? "" };
42633
- case "tool_call":
42634
- return defined({
42635
- type: "tool_call",
42636
- topicId,
42637
- queryId: status.queryId,
42638
- name: status.name,
42639
- input: status.input,
42640
- label: status.label,
42641
- toolUseId: status.toolUseId
42642
- });
42643
- case "tool_output":
42644
- return defined({
42645
- type: "tool_output",
42646
- topicId,
42647
- queryId: status.queryId,
42648
- toolUseId: status.toolUseId,
42649
- content: status.content,
42650
- isError: status.isError
42651
- });
42652
- case "tool_status":
42653
- return defined({
42654
- type: "tool_status",
42655
- topicId,
42656
- queryId: status.queryId,
42657
- kind: status.statusKind,
42658
- content: status.content,
42659
- toolName: status.toolName,
42660
- elapsed: status.elapsed
42661
- });
42662
- case "file_ready":
42663
- return defined({
42664
- type: "file_ready",
42665
- topicId,
42666
- queryId: status.queryId,
42667
- path: status.path,
42668
- source: status.source
42669
- });
42670
- case "visual":
42671
- return defined({
42672
- type: "visual",
42673
- topicId,
42674
- queryId: status.queryId,
42675
- url: status.url,
42676
- id: status.id,
42677
- title: status.title,
42678
- kind: status.visualKind
42679
- });
42680
- case "ai_done":
42681
- return defined({
42682
- type: "ai_done",
42683
- topicId,
42684
- queryId: status.queryId,
42685
- usage: status.usage,
42686
- agent: status.agent,
42687
- model: status.model
42688
- });
42689
- case "ai_error":
42690
- return defined({
42691
- type: "ai_error",
42692
- topicId,
42693
- queryId: status.queryId,
42694
- error: status.error
42695
- });
42696
- case "ai_aborted":
42697
- return defined({
42698
- type: "ai_aborted",
42699
- topicId,
42700
- queryId: status.queryId,
42701
- reason: status.reason
42702
- });
42703
- default:
42704
- return null;
42009
+ const parsed = JSON.parse(readFileSync28(path, "utf-8"));
42010
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
42011
+ throw new Error("persisted join credentials are not a JSON object");
42705
42012
  }
42013
+ return normalizeJoin(parsed);
42706
42014
  }
42707
- function getActiveForwarder(localTopicId) {
42708
- return activeForwarders.get(localTopicId);
42015
+ function isJoinPersisted(join42) {
42016
+ try {
42017
+ const persisted = readPersistedJoin();
42018
+ return persisted !== null && joinsEqual(persisted, normalizedJoin(join42));
42019
+ } catch {
42020
+ return false;
42021
+ }
42709
42022
  }
42710
- function createTurnForwarder(opts) {
42711
- const { hostNodeId, requestId, localTopicId, sendEvent } = opts;
42712
- const retryBaseMs = opts.retryBaseMs ?? PEER_EVENT_RETRY_BASE_MS;
42713
- const maxPendingEvents = Math.max(2, opts.maxPendingEvents ?? PEER_EVENT_MAX_PENDING);
42714
- const forwarder = {
42715
- requestId,
42716
- queryId: null,
42717
- finished: false,
42718
- deliveryBlocked: false,
42719
- pendingEvents: 0,
42720
- seq: 0,
42721
- chain: Promise.resolve(),
42722
- tap: () => {},
42723
- finish: () => {}
42724
- };
42725
- let terminalQueued = false;
42726
- const detach = () => {
42727
- forwarder.finished = true;
42728
- if (activeForwarders.get(localTopicId) === forwarder) {
42729
- activeForwarders.delete(localTopicId);
42730
- }
42731
- };
42732
- const post = (event) => {
42733
- const isTerminal = TERMINAL_TYPES.has(String(event.type ?? ""));
42734
- if (isTerminal)
42735
- terminalQueued = true;
42736
- forwarder.seq += 1;
42737
- const seq = forwarder.seq;
42738
- forwarder.pendingEvents += 1;
42739
- forwarder.chain = forwarder.chain.then(async () => {
42023
+ function saveJoinWhileLocked(join42, options = {}) {
42024
+ const path = joinFilePath();
42025
+ const directory = dirname21(path);
42026
+ const normalized = normalizedJoin(join42);
42027
+ mkdirSync32(directory, { recursive: true });
42028
+ if (existsSync37(path)) {
42029
+ if (lstatSync(path).isSymbolicLink()) {
42030
+ throw new Error(`refusing to replace symlinked Otium join file at ${path}`);
42031
+ }
42032
+ let existing = null;
42033
+ try {
42034
+ existing = readPersistedJoin(path);
42035
+ } catch (error2) {
42036
+ if (!options.replaceExisting) {
42037
+ throw new Error(`existing Otium join file at ${path} is invalid; pass --replace to replace it`, { cause: error2 });
42038
+ }
42039
+ }
42040
+ if (existing && joinsEqual(existing, normalized)) {
42041
+ chmodSync7(path, 384);
42042
+ const fileFd = openSync5(path, "r");
42740
42043
  try {
42741
- if (forwarder.deliveryBlocked)
42742
- return;
42743
- if (isTerminal) {
42744
- upsertPeerTerminalOutbox({ hostNodeId, requestId, seq, event });
42745
- }
42746
- for (let attempt = 1;attempt <= PEER_EVENT_MAX_ATTEMPTS; attempt++) {
42747
- const result = await sendEvent({ v: PEER_PROTOCOL_VERSION, requestId, seq, event });
42748
- if (result.ok) {
42749
- if (isTerminal)
42750
- acknowledgePeerTerminal(hostNodeId, requestId);
42751
- return;
42752
- }
42753
- logger.warn({ requestId, seq, type: event.type, attempt, error: result.error }, "otium: peer event delivery to hub failed");
42754
- if (attempt < PEER_EVENT_MAX_ATTEMPTS) {
42755
- const delayMs = retryBaseMs * 2 ** (attempt - 1);
42756
- await new Promise((resolve25) => setTimeout(resolve25, delayMs));
42757
- }
42758
- }
42759
- forwarder.deliveryBlocked = true;
42760
- logger.error({ requestId, seq, type: event.type }, "otium: peer event delivery exhausted");
42044
+ fsyncSync2(fileFd);
42761
42045
  } finally {
42762
- forwarder.pendingEvents -= 1;
42763
- }
42764
- });
42765
- };
42766
- forwarder.tap = (raw) => {
42767
- if (forwarder.finished)
42768
- return;
42769
- const type = String(raw.type ?? "");
42770
- if (!FORWARDED_TYPES.has(type))
42771
- return;
42772
- if (type === "message") {
42773
- const message = raw.message;
42774
- if (message?.authorId === "ai" && forwarder.queryId && message.queryId !== forwarder.queryId) {
42775
- return;
42046
+ closeSync5(fileFd);
42776
42047
  }
42777
- } else if (type !== "typing") {
42778
- const queryId = raw.queryId;
42779
- if (forwarder.queryId && typeof queryId === "string" && queryId !== forwarder.queryId) {
42780
- return;
42048
+ const directoryFd = openSync5(directory, "r");
42049
+ try {
42050
+ fsyncSync2(directoryFd);
42051
+ } finally {
42052
+ closeSync5(directoryFd);
42781
42053
  }
42054
+ return path;
42782
42055
  }
42783
- const isTerminal = TERMINAL_TYPES.has(type);
42784
- if (!isTerminal && forwarder.pendingEvents >= maxPendingEvents - 1) {
42785
- if (!terminalQueued) {
42786
- post({
42787
- type: "ai_error",
42788
- topicId: localTopicId,
42789
- queryId: forwarder.queryId ?? requestId,
42790
- error: `worker event queue saturated (${maxPendingEvents})`
42791
- });
42792
- detach();
42793
- }
42794
- return;
42056
+ if (!options.replaceExisting) {
42057
+ throw new Error(`this node is already joined${existing ? ` as ${existing.cellId}` : " with an invalid join file"}; pass --replace to replace its credentials`);
42795
42058
  }
42796
- post(raw);
42797
- if (isTerminal)
42798
- detach();
42799
- };
42800
- forwarder.finish = (event) => {
42801
- if (forwarder.finished)
42802
- return;
42803
- post(event);
42804
- detach();
42805
- };
42806
- return forwarder;
42059
+ }
42060
+ const temporaryPath = resolve24(directory, `.otium-join.json.${process.pid}.${randomUUID28()}.tmp`);
42061
+ let fd;
42062
+ try {
42063
+ fd = openSync5(temporaryPath, "wx", 384);
42064
+ writeFileSync23(fd, `${JSON.stringify(normalized, null, 2)}
42065
+ `, "utf8");
42066
+ fsyncSync2(fd);
42067
+ closeSync5(fd);
42068
+ fd = undefined;
42069
+ if (options.replaceExisting) {
42070
+ renameSync16(temporaryPath, path);
42071
+ } else {
42072
+ linkSync(temporaryPath, path);
42073
+ unlinkSync23(temporaryPath);
42074
+ }
42075
+ chmodSync7(path, 384);
42076
+ const directoryFd = openSync5(directory, "r");
42077
+ try {
42078
+ fsyncSync2(directoryFd);
42079
+ } finally {
42080
+ closeSync5(directoryFd);
42081
+ }
42082
+ } catch (error2) {
42083
+ if (fd !== undefined)
42084
+ closeSync5(fd);
42085
+ if (existsSync37(temporaryPath))
42086
+ unlinkSync23(temporaryPath);
42087
+ throw error2;
42088
+ }
42089
+ return path;
42807
42090
  }
42808
- function registerTurnForwarder(localTopicId, forwarder) {
42809
- activeForwarders.set(localTopicId, forwarder);
42810
- ensureBackflowSubscription();
42091
+ function saveJoin(join42, options = {}) {
42092
+ return withJoinCredentialLock(() => saveJoinWhileLocked(join42, options));
42811
42093
  }
42812
- async function flushPeerTerminalOutbox() {
42813
- if (terminalOutboxFlushInFlight)
42814
- return 0;
42815
- terminalOutboxFlushInFlight = true;
42816
- let acknowledged = 0;
42817
- try {
42818
- for (const row of listPeerTerminalOutbox()) {
42819
- const node = await resolvePeerNodeByCellId(row.host_node_id).catch(() => null);
42820
- if (!node)
42821
- continue;
42822
- let event;
42823
- try {
42824
- event = JSON.parse(row.event_json);
42825
- } catch {
42826
- continue;
42827
- }
42828
- const result = await hubEventSender(node)({
42829
- v: PEER_PROTOCOL_VERSION,
42830
- requestId: row.request_id,
42831
- seq: row.seq,
42832
- event
42833
- });
42834
- if (result.ok && acknowledgePeerTerminal(row.host_node_id, row.request_id))
42835
- acknowledged += 1;
42094
+ function removeJoin() {
42095
+ return withJoinCredentialLock(() => {
42096
+ const path = joinFilePath();
42097
+ if (!existsSync37(path))
42098
+ return false;
42099
+ if (lstatSync(path).isSymbolicLink()) {
42100
+ throw new Error(`refusing to remove symlinked Otium join file at ${path}`);
42836
42101
  }
42837
- return acknowledged;
42838
- } finally {
42839
- terminalOutboxFlushInFlight = false;
42102
+ unlinkSync23(path);
42103
+ const directoryFd = openSync5(dirname21(path), "r");
42104
+ try {
42105
+ fsyncSync2(directoryFd);
42106
+ } finally {
42107
+ closeSync5(directoryFd);
42108
+ }
42109
+ return true;
42110
+ });
42111
+ }
42112
+ function loadJoin() {
42113
+ const central = process.env.OTIUM_CENTRAL_URL?.trim();
42114
+ const cellId = process.env.OTIUM_CELL_ID?.trim();
42115
+ const secret = process.env.OTIUM_CELL_SECRET?.trim();
42116
+ const relay = process.env.OTIUM_RELAY_URL?.trim();
42117
+ if (central && cellId && secret) {
42118
+ try {
42119
+ return normalizeJoin({ central, relay, cellId, secret });
42120
+ } catch (err2) {
42121
+ logger.warn({ err: err2 }, "otium: invalid OTIUM_CENTRAL_URL/OTIUM_CELL_ID/OTIUM_CELL_SECRET env");
42122
+ return null;
42123
+ }
42124
+ }
42125
+ if (central || cellId || secret) {
42126
+ logger.warn("otium: OTIUM_CENTRAL_URL, OTIUM_CELL_ID, OTIUM_CELL_SECRET must be set together \u2014 ignoring partial env");
42127
+ }
42128
+ const path = joinFilePath();
42129
+ if (!existsSync37(path))
42130
+ return null;
42131
+ try {
42132
+ const parsed = JSON.parse(readFileSync28(path, "utf-8"));
42133
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
42134
+ return null;
42135
+ return normalizeJoin(parsed);
42136
+ } catch (err2) {
42137
+ logger.warn({ err: err2, path }, "otium: failed to read join file");
42138
+ return null;
42840
42139
  }
42841
42140
  }
42842
- function onBusEvent(event) {
42843
- const forwarder = activeForwarders.get(event.topicId);
42844
- if (!forwarder)
42845
- return;
42846
- const raw = translateBusEvent(event);
42847
- if (raw)
42848
- forwarder.tap(raw);
42849
- }
42850
- function ensureBackflowSubscription() {
42851
- if (!unsubscribe)
42852
- unsubscribe = runtimeBus().subscribe(onBusEvent);
42853
- }
42854
- function startEventBackflow() {
42855
- ensureBackflowSubscription();
42856
- if (!terminalOutboxTimer) {
42857
- flushPeerTerminalOutbox();
42858
- terminalOutboxTimer = setInterval(() => void flushPeerTerminalOutbox(), 5000);
42859
- terminalOutboxTimer.unref?.();
42860
- }
42861
- return stopEventBackflow;
42862
- }
42863
- function stopEventBackflow() {
42864
- unsubscribe?.();
42865
- unsubscribe = null;
42866
- if (terminalOutboxTimer)
42867
- clearInterval(terminalOutboxTimer);
42868
- terminalOutboxTimer = null;
42869
- activeForwarders.clear();
42870
- }
42871
- var FORWARDED_TYPES, TERMINAL_TYPES, PEER_EVENT_MAX_ATTEMPTS = 5, PEER_EVENT_RETRY_BASE_MS = 100, PEER_EVENT_TIMEOUT_MS = 15000, PEER_EVENT_MAX_PENDING = 256, activeForwarders, unsubscribe = null, terminalOutboxTimer = null, terminalOutboxFlushInFlight = false;
42872
- var init_event_backflow = __esm(async () => {
42141
+ var JOIN_LOCK_STALE_MS = 30000;
42142
+ var init_join = __esm(async () => {
42873
42143
  await init_src();
42874
- await init_central();
42875
- init_protocol();
42876
- await init_store2();
42877
- FORWARDED_TYPES = new Set([
42878
- "message",
42879
- "message_updated",
42880
- "typing",
42881
- "tool_call",
42882
- "tool_output",
42883
- "tool_status",
42884
- "visual",
42885
- "file_ready",
42886
- "ai_done",
42887
- "ai_error",
42888
- "ai_aborted"
42889
- ]);
42890
- TERMINAL_TYPES = new Set(["ai_done", "ai_error", "ai_aborted"]);
42891
- activeForwarders = new Map;
42892
42144
  });
42893
42145
 
42894
42146
  // ../../adapters/otium/src/peer-files.ts
@@ -42915,10 +42167,6 @@ function attachment(row) {
42915
42167
  function rowFor(fileId) {
42916
42168
  return db.query("SELECT * FROM otium_peer_files WHERE id = ?").get(fileId) ?? null;
42917
42169
  }
42918
- function peerFileAllowsAccess(fileId, access) {
42919
- const row = rowFor(fileId);
42920
- return row?.topic_id === access.topicId && row.owner_user_id === access.ownerUserId;
42921
- }
42922
42170
  function safeFilename(filename) {
42923
42171
  const value = basename13(filename).replace(/[^A-Za-z0-9._ -]/g, "_").slice(0, 120);
42924
42172
  return value || "upload";
@@ -42974,21 +42222,6 @@ function deletePeerFilesForTopic(topicId) {
42974
42222
  for (const row of rows)
42975
42223
  rmSync11(row.path, { force: true });
42976
42224
  }
42977
- async function storePeerInputFile(file, access) {
42978
- const id = randomUUID29();
42979
- mkdirSync33(PEER_FILES_DIR, { recursive: true });
42980
- const filename = file.name || "upload";
42981
- const path = join42(PEER_FILES_DIR, `${id}-${safeFilename(filename)}`);
42982
- const sizeBytes = await Bun.write(path, file);
42983
- return recordFile({
42984
- id,
42985
- path,
42986
- sizeBytes,
42987
- filename,
42988
- mimeType: file.type || "application/octet-stream",
42989
- ...access
42990
- });
42991
- }
42992
42225
  function installPeerFileHooks() {
42993
42226
  const previous = fileHooks();
42994
42227
  const hooks = {
@@ -43052,15 +42285,7 @@ var PEER_BRIDGE_TIMEOUT_MS2 = 15000, otiumPeerRuntimeBridge;
43052
42285
  var init_runtime_bridge = __esm(async () => {
43053
42286
  await init_src();
43054
42287
  await init_central();
43055
- await init_event_backflow();
43056
42288
  otiumPeerRuntimeBridge = {
43057
- async flushEvents(localTopicId) {
43058
- const forwarder = getActiveForwarder(localTopicId);
43059
- if (!forwarder)
43060
- return false;
43061
- await forwarder.chain;
43062
- return !forwarder.deliveryBlocked;
43063
- },
43064
42289
  async spawnSubagent(request) {
43065
42290
  const hubNode = await resolvePeerNodeByCellId(request.bridge.hubCellId).catch(() => null);
43066
42291
  if (!hubNode)
@@ -43280,6 +42505,149 @@ var init_runtime_bridge = __esm(async () => {
43280
42505
  };
43281
42506
  });
43282
42507
 
42508
+ // ../../adapters/otium/src/store.ts
42509
+ import { createHash as createHash12 } from "crypto";
42510
+ function cleanupPeerStateForLocalTopic(localTopicId) {
42511
+ return db.transaction(() => {
42512
+ const inboxRequests = db.run("DELETE FROM otium_peer_inbox_requests WHERE topic_id = ?", [
42513
+ localTopicId
42514
+ ]).changes;
42515
+ const remoteAsks = db.run("DELETE FROM otium_remote_asks WHERE caller_topic_id = ?", [
42516
+ localTopicId
42517
+ ]).changes;
42518
+ return { inboxRequests, remoteAsks };
42519
+ })();
42520
+ }
42521
+ function peerInboxPayloadHash(value) {
42522
+ return createHash12("sha256").update(JSON.stringify(value)).digest("hex");
42523
+ }
42524
+ function claimPeerInboxRequest(args) {
42525
+ const inserted = db.run(`INSERT OR IGNORE INTO otium_peer_inbox_requests
42526
+ (from_cell_id, request_id, kind, topic_id, payload_hash, created_at)
42527
+ VALUES (?, ?, ?, ?, ?, ?)`, [
42528
+ args.fromCellId,
42529
+ args.requestId,
42530
+ args.kind,
42531
+ args.topicId,
42532
+ args.payloadHash,
42533
+ new Date().toISOString()
42534
+ ]);
42535
+ if (inserted.changes === 1)
42536
+ return { outcome: "claimed" };
42537
+ const existing = db.query("SELECT payload_hash FROM otium_peer_inbox_requests WHERE from_cell_id = ? AND request_id = ? AND kind = ?").get(args.fromCellId, args.requestId, args.kind);
42538
+ if (!existing)
42539
+ return { outcome: "conflict" };
42540
+ return { outcome: existing.payload_hash === args.payloadHash ? "replay" : "conflict" };
42541
+ }
42542
+ function releasePeerInboxRequest(fromCellId, requestId, kind) {
42543
+ db.run("DELETE FROM otium_peer_inbox_requests WHERE from_cell_id = ? AND request_id = ? AND kind = ?", [fromCellId, requestId, kind]);
42544
+ }
42545
+ function createRemoteAsk(args) {
42546
+ const result = db.run(`INSERT OR IGNORE INTO otium_remote_asks
42547
+ (request_id, expected_cell_id, user_id, caller_topic_id, from_key, to_key,
42548
+ source_query_id, created_at)
42549
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
42550
+ args.requestId,
42551
+ args.expectedCellId,
42552
+ args.userId,
42553
+ args.callerTopicId,
42554
+ args.from,
42555
+ args.to,
42556
+ args.sourceQueryId ?? null,
42557
+ args.createdAt ?? Date.now()
42558
+ ]);
42559
+ return result.changes === 1;
42560
+ }
42561
+ function getRemoteAsk(requestId) {
42562
+ return db.query("SELECT * FROM otium_remote_asks WHERE request_id = ?").get(requestId) ?? null;
42563
+ }
42564
+ function deleteRemoteAsk(requestId) {
42565
+ return db.run("DELETE FROM otium_remote_asks WHERE request_id = ?", [requestId]).changes === 1;
42566
+ }
42567
+ function pruneRemoteAsks(olderThan) {
42568
+ return db.run("DELETE FROM otium_remote_asks WHERE created_at < ?", [olderThan]).changes;
42569
+ }
42570
+ function upsertPeerReplyOutbox(args) {
42571
+ const now = Date.now();
42572
+ db.run(`INSERT INTO otium_peer_reply_outbox
42573
+ (node_cell_id, request_id, node_name, topic_id, user_id, source_title,
42574
+ reply_text, kind, created_at, updated_at)
42575
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
42576
+ ON CONFLICT(node_cell_id, request_id) DO UPDATE SET
42577
+ node_name = excluded.node_name,
42578
+ topic_id = excluded.topic_id,
42579
+ user_id = excluded.user_id,
42580
+ source_title = excluded.source_title,
42581
+ reply_text = excluded.reply_text,
42582
+ kind = excluded.kind,
42583
+ updated_at = excluded.updated_at`, [
42584
+ args.nodeCellId,
42585
+ args.requestId,
42586
+ args.nodeName,
42587
+ args.topicId,
42588
+ args.userId,
42589
+ args.sourceTitle,
42590
+ args.replyText,
42591
+ args.kind,
42592
+ now,
42593
+ now
42594
+ ]);
42595
+ }
42596
+ function listPeerReplyOutbox(limit = 100) {
42597
+ return db.query("SELECT * FROM otium_peer_reply_outbox ORDER BY created_at LIMIT ?").all(limit);
42598
+ }
42599
+ function deletePeerReplyOutbox(nodeCellId, requestId) {
42600
+ return db.run("DELETE FROM otium_peer_reply_outbox WHERE node_cell_id = ? AND request_id = ?", [
42601
+ nodeCellId,
42602
+ requestId
42603
+ ]).changes === 1;
42604
+ }
42605
+ var init_store2 = __esm(async () => {
42606
+ await init_src();
42607
+ db.exec(`
42608
+ CREATE TABLE IF NOT EXISTS otium_peer_inbox_requests (
42609
+ from_cell_id TEXT NOT NULL,
42610
+ request_id TEXT NOT NULL,
42611
+ kind TEXT NOT NULL CHECK (kind IN ('tell', 'ask')),
42612
+ topic_id TEXT NOT NULL,
42613
+ payload_hash TEXT NOT NULL,
42614
+ created_at TEXT NOT NULL,
42615
+ PRIMARY KEY (from_cell_id, request_id, kind)
42616
+ )
42617
+ `);
42618
+ db.exec(`
42619
+ CREATE TABLE IF NOT EXISTS otium_remote_asks (
42620
+ request_id TEXT PRIMARY KEY,
42621
+ expected_cell_id TEXT NOT NULL,
42622
+ user_id TEXT NOT NULL,
42623
+ caller_topic_id TEXT NOT NULL,
42624
+ from_key TEXT NOT NULL,
42625
+ to_key TEXT NOT NULL,
42626
+ source_query_id TEXT,
42627
+ created_at INTEGER NOT NULL
42628
+ )
42629
+ `);
42630
+ db.exec(`
42631
+ CREATE INDEX IF NOT EXISTS idx_otium_remote_asks_created
42632
+ ON otium_remote_asks(created_at)
42633
+ `);
42634
+ db.exec(`
42635
+ CREATE TABLE IF NOT EXISTS otium_peer_reply_outbox (
42636
+ node_cell_id TEXT NOT NULL,
42637
+ request_id TEXT NOT NULL,
42638
+ node_name TEXT NOT NULL,
42639
+ topic_id TEXT NOT NULL,
42640
+ user_id TEXT NOT NULL,
42641
+ source_title TEXT NOT NULL,
42642
+ reply_text TEXT NOT NULL,
42643
+ kind TEXT NOT NULL CHECK (kind IN ('reply', 'error')),
42644
+ created_at INTEGER NOT NULL,
42645
+ updated_at INTEGER NOT NULL,
42646
+ PRIMARY KEY (node_cell_id, request_id)
42647
+ )
42648
+ `);
42649
+ });
42650
+
43283
42651
  // ../../adapters/otium/src/session-bridge.ts
43284
42652
  function prunePendingRemoteAsks(now = Date.now()) {
43285
42653
  pruneRemoteAsks(now - PENDING_ASK_TTL_MS2);
@@ -43671,287 +43039,6 @@ var init_session_bridge_ipc = __esm(() => {
43671
43039
  MAX_BODY_BYTES2 = 1024 * 1024;
43672
43040
  });
43673
43041
 
43674
- // ../../adapters/otium/src/shared-topic-sync.ts
43675
- var exports_shared_topic_sync = {};
43676
- __export(exports_shared_topic_sync, {
43677
- startSharedTopicSync: () => startSharedTopicSync,
43678
- forwardSharedTopicMessage: () => forwardSharedTopicMessage,
43679
- downgradeSharedTopicsForHub: () => downgradeSharedTopicsForHub,
43680
- disconnectSharedTopics: () => disconnectSharedTopics,
43681
- checkPeerAttachment: () => checkPeerAttachment,
43682
- acceptSharedTopicMessages: () => acceptSharedTopicMessages
43683
- });
43684
- function isMirrorTopic(topicId) {
43685
- return listPeerSessions().some((row) => row.local_topic_id === topicId && row.binding_mode === "mirror");
43686
- }
43687
- function metadata(topic) {
43688
- if (!topic.agent)
43689
- throw new Error(`topic ${topic.id} has no executable agent`);
43690
- const registry = getRegistry(topic.agent);
43691
- return {
43692
- localTopicId: topic.id,
43693
- title: topic.title,
43694
- ...topic.description ? { description: topic.description } : {},
43695
- agent: topic.agent,
43696
- model: topic.defaultModel || registry.defaultModel,
43697
- effort: topic.defaultEffort ?? "medium"
43698
- };
43699
- }
43700
- async function peerRequest(node, path, init) {
43701
- const token = await mintPeerToken(node.cellId);
43702
- const response = await fetch(`${node.baseUrl.replace(/\/+$/, "")}${path}`, {
43703
- ...init,
43704
- headers: {
43705
- authorization: `Bearer ${token}`,
43706
- "content-type": "application/json",
43707
- ...init.headers ?? {}
43708
- },
43709
- signal: AbortSignal.timeout(15000)
43710
- });
43711
- const body = await response.json().catch(() => null);
43712
- if (!response.ok || !body?.ok)
43713
- throw new Error(String(body?.error ?? `peer request failed (${response.status})`));
43714
- return body;
43715
- }
43716
- function messageEnvelope(message) {
43717
- const author = message.agentType ? "ai" : message.kind === "system" || message.kind === "tool" ? "system" : "user";
43718
- return {
43719
- sourceMessageId: message.sourceMessageId ?? message.id,
43720
- author,
43721
- text: message.text,
43722
- createdAt: message.createdAt,
43723
- ...message.agentType ? { agent: message.agentType } : {},
43724
- ...message.model ? { model: message.model } : {},
43725
- ...message.kind ? { kind: message.kind } : {}
43726
- };
43727
- }
43728
- async function publishTopic(join43, topicId, node) {
43729
- const topic = getTopic(topicId);
43730
- if (!topic || topic.kind !== "agent" || !topic.agent || !isTopicShared(topic) || !topic.visibility || topic.visibility === "hidden" || isMirrorTopic(topicId))
43731
- return;
43732
- const state2 = getSharedTopicState(topicId);
43733
- setSharedTopicState({
43734
- localTopicId: topicId,
43735
- hostTopicId: state2?.host_topic_id,
43736
- status: "publishing"
43737
- });
43738
- try {
43739
- const body = await peerRequest(node, "/api/v1/peer/shared-topic", {
43740
- method: "POST",
43741
- body: JSON.stringify({
43742
- v: PEER_PROTOCOL_VERSION,
43743
- ...metadata(topic)
43744
- })
43745
- });
43746
- const hostTopicId = typeof body.hostTopicId === "string" ? body.hostTopicId : state2?.host_topic_id;
43747
- if (!hostTopicId)
43748
- throw new Error("shared-topic response omitted hostTopicId");
43749
- setSharedTopicState({ localTopicId: topicId, hostTopicId, status: "published" });
43750
- const messages = listApiMessages(topicId, { limit: 200 }).page.filter((message) => !message.sourceNode).map(messageEnvelope);
43751
- if (messages.length) {
43752
- await peerRequest(node, "/api/v1/peer/shared-topic/messages", {
43753
- method: "POST",
43754
- body: JSON.stringify({
43755
- v: PEER_PROTOCOL_VERSION,
43756
- localTopicId: topicId,
43757
- hostTopicId,
43758
- messages
43759
- })
43760
- });
43761
- }
43762
- for (const row of listSharedMessages(topicId)) {
43763
- await peerRequest(node, "/api/v1/peer/shared-topic/messages", {
43764
- method: "POST",
43765
- body: JSON.stringify({
43766
- v: PEER_PROTOCOL_VERSION,
43767
- localTopicId: topicId,
43768
- hostTopicId,
43769
- messages: [JSON.parse(row.message_json)]
43770
- })
43771
- });
43772
- deleteSharedMessage(topicId, row.source_message_id);
43773
- }
43774
- } catch (error2) {
43775
- logger.warn({ error: error2, topicId }, "otium: shared topic publish deferred");
43776
- setTimeout(() => void reconcileTopic(join43, topicId), RETRY_MS).unref?.();
43777
- }
43778
- }
43779
- async function unpublishTopic(join43, topicId, node) {
43780
- const state2 = getSharedTopicState(topicId);
43781
- if (!state2)
43782
- return;
43783
- setSharedTopicState({
43784
- localTopicId: topicId,
43785
- hostTopicId: state2.host_topic_id,
43786
- status: "unpublishing"
43787
- });
43788
- try {
43789
- await peerRequest(node, `/api/v1/peer/shared-topic/${encodeURIComponent(topicId)}`, {
43790
- method: "DELETE"
43791
- });
43792
- deleteSharedTopicState(topicId);
43793
- } catch (error2) {
43794
- logger.warn({ error: error2, topicId }, "otium: shared topic unpublish deferred");
43795
- setTimeout(() => void reconcileTopic(join43, topicId), RETRY_MS).unref?.();
43796
- }
43797
- }
43798
- async function reconcileTopic(join43, topicId, explicit = false) {
43799
- if (!otiumCentralConfig())
43800
- return;
43801
- if (!explicit && isPeerDetached(join43.cellId))
43802
- return;
43803
- const target = (await listPeerNodes()).find((node) => node.isPrimary) ?? null;
43804
- if (!target)
43805
- return;
43806
- const topic = getTopic(topicId);
43807
- if (topic?.kind === "agent" && topic.agent && isTopicShared(topic) && topic.visibility !== "hidden" && !isMirrorTopic(topicId))
43808
- return publishTopic(join43, topicId, target);
43809
- return unpublishTopic(join43, topicId, target);
43810
- }
43811
- function disconnectSharedTopics(join43) {
43812
- const states = listSharedTopicStates();
43813
- const hubPromise = listPeerNodes({ fresh: true }).then((nodes) => nodes.find((node) => node.isPrimary) ?? null, () => null);
43814
- const updated = downgradeSharedTopicsLocally(join43.cellId);
43815
- for (const topicId of updated)
43816
- WsHub.get().broadcastTopicUpdated(topicId);
43817
- const deletions = states.map(async (state2) => {
43818
- const hub = await hubPromise;
43819
- if (!hub)
43820
- return;
43821
- try {
43822
- await peerRequest(hub, `/api/v1/peer/shared-topic/${encodeURIComponent(state2.local_topic_id)}`, {
43823
- method: "DELETE"
43824
- });
43825
- } catch {}
43826
- });
43827
- return Promise.all(deletions).then(() => {
43828
- return;
43829
- });
43830
- }
43831
- function downgradeSharedTopicsForHub(hubNodeId) {
43832
- const updated = downgradeSharedTopicsLocally(hubNodeId);
43833
- for (const topicId of updated)
43834
- WsHub.get().broadcastTopicUpdated(topicId);
43835
- return updated.length;
43836
- }
43837
- async function checkPeerAttachment(join43) {
43838
- const nodes = await listPeerNodes({ fresh: true });
43839
- if (nodes.some((node) => node.cellId === join43.cellId))
43840
- return true;
43841
- downgradeSharedTopicsForHub(join43.cellId);
43842
- return false;
43843
- }
43844
- function startSharedTopicSync(join43) {
43845
- let stopped = false;
43846
- let detached = isPeerDetached(join43.cellId);
43847
- const queue = new Set;
43848
- const schedule = (topicId, explicit = false) => {
43849
- if (stopped || queue.has(topicId))
43850
- return;
43851
- queue.add(topicId);
43852
- queueMicrotask(async () => {
43853
- queue.delete(topicId);
43854
- try {
43855
- await reconcileTopic(join43, topicId, explicit);
43856
- } catch (error2) {
43857
- logger.warn({ error: error2, topicId }, "otium: shared topic reconciliation failed");
43858
- }
43859
- });
43860
- };
43861
- for (const topic of listTopics())
43862
- schedule(topic.id, false);
43863
- for (const state2 of listSharedTopicStates())
43864
- schedule(state2.local_topic_id);
43865
- const attachmentCheck = setInterval(async () => {
43866
- if (stopped || detached)
43867
- return;
43868
- try {
43869
- if (!await checkPeerAttachment(join43))
43870
- detached = true;
43871
- } catch {}
43872
- }, 30000);
43873
- attachmentCheck.unref?.();
43874
- const unsubscribe2 = runtimeBus().subscribe((event) => {
43875
- if (event.type === "topic-created" || event.type === "topic-updated" || event.type === "topic-deleted")
43876
- schedule(event.topicId, true);
43877
- if (event.type === "message") {
43878
- const message = event.payload;
43879
- if (!message.sourceNode) {
43880
- schedule(event.topicId);
43881
- forwardSharedTopicMessage(join43, message).catch((error2) => logger.warn({ error: error2, topicId: event.topicId }, "otium: shared message deferred"));
43882
- }
43883
- }
43884
- });
43885
- return () => {
43886
- stopped = true;
43887
- clearInterval(attachmentCheck);
43888
- unsubscribe2();
43889
- };
43890
- }
43891
- async function forwardSharedTopicMessage(_join, message) {
43892
- if (message.sourceNode || isMirrorTopic(message.topicId))
43893
- return;
43894
- if (getActiveForwarder(message.topicId))
43895
- return;
43896
- const state2 = getSharedTopicState(message.topicId);
43897
- if (!state2?.host_topic_id || state2.status !== "published")
43898
- return;
43899
- const hub = (await listPeerNodes()).find((node) => node.isPrimary) ?? null;
43900
- if (!hub)
43901
- return;
43902
- const envelope = messageEnvelope(message);
43903
- try {
43904
- await peerRequest(hub, "/api/v1/peer/shared-topic/messages", {
43905
- method: "POST",
43906
- body: JSON.stringify({
43907
- v: PEER_PROTOCOL_VERSION,
43908
- localTopicId: message.topicId,
43909
- hostTopicId: state2.host_topic_id,
43910
- messages: [envelope]
43911
- })
43912
- });
43913
- } catch (error2) {
43914
- enqueueSharedMessage({
43915
- localTopicId: message.topicId,
43916
- sourceMessageId: envelope.sourceMessageId,
43917
- message: envelope
43918
- });
43919
- throw error2;
43920
- }
43921
- }
43922
- function acceptSharedTopicMessages(messages, localTopicId, sourceNode) {
43923
- let inserted = 0;
43924
- for (const incoming of messages) {
43925
- if (!incoming.sourceMessageId || typeof incoming.text !== "string")
43926
- continue;
43927
- if (getApiMessage(localTopicId, incoming.sourceMessageId))
43928
- continue;
43929
- const message = {
43930
- id: incoming.sourceMessageId,
43931
- topicId: localTopicId,
43932
- authorId: sourceNode,
43933
- text: incoming.text,
43934
- createdAt: incoming.createdAt,
43935
- ...incoming.agent ? { agentType: incoming.agent } : {},
43936
- ...incoming.model ? { model: incoming.model } : {},
43937
- ...incoming.kind ? { kind: incoming.kind } : {},
43938
- sourceNode,
43939
- sourceMessageId: incoming.sourceMessageId
43940
- };
43941
- appendApiMessage(message);
43942
- inserted++;
43943
- }
43944
- return inserted;
43945
- }
43946
- var RETRY_MS = 1000;
43947
- var init_shared_topic_sync = __esm(async () => {
43948
- await init_src();
43949
- await init_central();
43950
- await init_event_backflow();
43951
- init_protocol();
43952
- await init_store2();
43953
- });
43954
-
43955
43042
  // ../../adapters/otium/src/relay-protocol.ts
43956
43043
  function encodeFrame(frame) {
43957
43044
  const encoded = JSON.stringify(frame);
@@ -44520,109 +43607,6 @@ var init_tunnel_client = __esm(() => {
44520
43607
  silentLogger = { info: noop, warn: noop, error: noop };
44521
43608
  });
44522
43609
 
44523
- // ../../adapters/otium/src/bindings.ts
44524
- var exports_bindings = {};
44525
- __export(exports_bindings, {
44526
- unbindOtiumTopic: () => unbindOtiumTopic,
44527
- shareOtiumTopic: () => shareOtiumTopic,
44528
- setOtiumTopicPrivate: () => setOtiumTopicPrivate,
44529
- listOtiumTopicBindings: () => listOtiumTopicBindings,
44530
- bindOtiumTopic: () => bindOtiumTopic
44531
- });
44532
- function ownsTopic(localTopicId, userId) {
44533
- const topic = getTopic(localTopicId);
44534
- if (!topic)
44535
- return { ok: false, error: "local topic not found", status: 404 };
44536
- if (!isTopicVisible(topic)) {
44537
- return { ok: false, error: "internal topics have no user access mode", status: 409 };
44538
- }
44539
- if (!topic.participants.some((participant) => participant.userId === userId)) {
44540
- return { ok: false, error: "local topic is not visible to this user", status: 403 };
44541
- }
44542
- if (!topic.participants.some((participant) => participant.userId === userId && participant.role === "owner")) {
44543
- return {
44544
- ok: false,
44545
- error: "only a topic owner can change its access mode",
44546
- status: 403
44547
- };
44548
- }
44549
- return { ok: true, topic };
44550
- }
44551
- function bindOtiumTopic(options) {
44552
- const topic = getTopic(options.localTopicId);
44553
- if (!topic)
44554
- return { ok: false, error: "local topic not found", status: 404 };
44555
- if (!isTopicVisible(topic)) {
44556
- return { ok: false, error: "hidden local topics cannot be shared", status: 409 };
44557
- }
44558
- if (!topic.participants.some((participant) => participant.userId === options.userId)) {
44559
- return { ok: false, error: "local topic is not visible to this user", status: 403 };
44560
- }
44561
- if (!isTopicShared(topic)) {
44562
- return { ok: false, error: "private topics must be shared explicitly first", status: 409 };
44563
- }
44564
- const previous = getPeerSession(options.hostNodeId, options.hostTopicId);
44565
- bindPeerSession(options.hostNodeId, options.hostTopicId, options.localTopicId, "shared");
44566
- return {
44567
- ok: true,
44568
- localTopicId: options.localTopicId,
44569
- replaced: Boolean(previous && previous.local_topic_id !== options.localTopicId)
44570
- };
44571
- }
44572
- function shareOtiumTopic(options) {
44573
- const owned = ownsTopic(options.localTopicId, options.userId);
44574
- if (!owned.ok)
44575
- return owned;
44576
- if (!isTopicShared(owned.topic)) {
44577
- const switched = switchTopicAccessMode({
44578
- topicId: options.localTopicId,
44579
- userId: options.userId,
44580
- accessMode: "shared"
44581
- });
44582
- if (!switched.ok)
44583
- return { ok: false, error: switched.error, status: 409 };
44584
- }
44585
- return bindOtiumTopic(options);
44586
- }
44587
- function setOtiumTopicPrivate(options) {
44588
- const owned = ownsTopic(options.localTopicId, options.userId);
44589
- if (!owned.ok)
44590
- return owned;
44591
- const removedBindings = unbindSharedPeerSessionsForLocalTopic(options.localTopicId);
44592
- if (isTopicShared(owned.topic)) {
44593
- const switched = switchTopicAccessMode({
44594
- topicId: options.localTopicId,
44595
- userId: options.userId,
44596
- accessMode: "private"
44597
- });
44598
- if (!switched.ok)
44599
- return { ok: false, error: switched.error, status: 409 };
44600
- }
44601
- return { ok: true, localTopicId: options.localTopicId, removedBindings };
44602
- }
44603
- function unbindOtiumTopic(hostNodeId, hostTopicId) {
44604
- return unbindPeerSession(hostNodeId, hostTopicId);
44605
- }
44606
- function listOtiumTopicBindings() {
44607
- return listPeerSessions().map((row) => {
44608
- const topic = getTopic(row.local_topic_id);
44609
- return {
44610
- hostNodeId: row.host_node_id,
44611
- hostTopicId: row.host_topic_id,
44612
- localTopicId: row.local_topic_id,
44613
- transport: row.binding_mode === "shared" ? "shared-binding" : "internal-mirror",
44614
- ...topic ? { topicAccessMode: topic.accessMode ?? "private" } : {},
44615
- ...topic ? { localTopicTitle: topic.title } : {},
44616
- localTopicExists: Boolean(topic),
44617
- createdAt: row.created_at
44618
- };
44619
- });
44620
- }
44621
- var init_bindings = __esm(async () => {
44622
- await init_src();
44623
- await init_store2();
44624
- });
44625
-
44626
43610
  // ../../adapters/otium/src/enrollment.ts
44627
43611
  import {
44628
43612
  createDecipheriv as createDecipheriv2,
@@ -44907,205 +43891,6 @@ var init_gateway_forward = __esm(async () => {
44907
43891
  await init_src();
44908
43892
  });
44909
43893
 
44910
- // ../../adapters/otium/src/turn-bridge.ts
44911
- import { randomUUID as randomUUID32 } from "crypto";
44912
- function executionFor(payload) {
44913
- if (payload.execution)
44914
- return payload.execution;
44915
- if (!payload.agent)
44916
- return null;
44917
- return {
44918
- agent: payload.agent,
44919
- model: payload.model ?? "",
44920
- effort: payload.effort ?? "medium",
44921
- mcp: [],
44922
- canSpawnSubagents: false
44923
- };
44924
- }
44925
- function provisionMirrorTopic(hostCellId, payload) {
44926
- const execution = payload.execution;
44927
- if (!isAgentKind(execution.agent)) {
44928
- return { ok: false, error: `unknown agent "${execution.agent}"`, status: 400 };
44929
- }
44930
- const existing = getPeerSession(hostCellId, payload.hostTopicId);
44931
- if (existing?.binding_mode === "shared") {
44932
- const shared = getTopic(existing.local_topic_id);
44933
- if (!shared) {
44934
- return { ok: false, error: "bound local topic no longer exists", status: 404 };
44935
- }
44936
- if (!isTopicVisible(shared)) {
44937
- return { ok: false, error: "bound local topic is hidden", status: 409 };
44938
- }
44939
- if (!isTopicShared(shared)) {
44940
- return { ok: false, error: "bound local topic is private", status: 409 };
44941
- }
44942
- if (!shared.participants.some((participant) => participant.userId === payload.userId)) {
44943
- return { ok: false, error: "bound local topic is not visible to this user", status: 403 };
44944
- }
44945
- return { ok: true, localTopicId: shared.id, bindingMode: "shared" };
44946
- }
44947
- const localTopicId = existing?.local_topic_id ?? `peer-${randomUUID32()}`;
44948
- const now = new Date().toISOString();
44949
- const current3 = existing ? getTopic(localTopicId) : null;
44950
- const currentConfig = current3 ? getApiTopicConfig(localTopicId) : undefined;
44951
- const currentModel = currentConfig?.model ?? current3?.defaultModel ?? "";
44952
- const nextModel = execution.model || current3?.defaultModel || "";
44953
- const providerSessionIsStale = Boolean(current3) && (current3?.agent !== execution.agent || currentModel !== nextModel);
44954
- upsertTopic({
44955
- id: localTopicId,
44956
- title: payload.topicTitle,
44957
- kind: "agent",
44958
- agent: execution.agent,
44959
- aiMode: "always",
44960
- defaultModel: nextModel,
44961
- defaultEffort: EFFORT_LEVELS.includes(execution.effort) ? execution.effort : current3?.defaultEffort ?? "medium",
44962
- participants: [{ userId: payload.userId, role: "owner" }],
44963
- isSubagent: undefined,
44964
- visibility: "visible",
44965
- accessMode: "shared",
44966
- ...execution.description ? { description: execution.description } : {},
44967
- createdAt: current3?.createdAt ?? now,
44968
- lastMessageAt: now
44969
- });
44970
- setApiTopicConfig(localTopicId, {
44971
- ...execution.model ? { model: execution.model } : {},
44972
- ...EFFORT_LEVELS.includes(execution.effort) ? { effort: execution.effort } : {},
44973
- mcp: execution.mcp
44974
- });
44975
- if (providerSessionIsStale) {
44976
- clearTopicSessionId(localTopicId, "peer-execution-spec-changed");
44977
- }
44978
- if (!existing)
44979
- createPeerSession(hostCellId, payload.hostTopicId, localTopicId);
44980
- return { ok: true, localTopicId, bindingMode: "mirror" };
44981
- }
44982
- function runPeerTurn(hubNode, hostCellId, payload, opts = {}) {
44983
- const execution = executionFor(payload);
44984
- if (!execution || !isAgentKind(execution.agent)) {
44985
- return { ok: false, error: `unknown agent "${execution?.agent ?? ""}"`, status: 400 };
44986
- }
44987
- const claim = claimPeerTurnRequest(hostCellId, payload.requestId, payload.hostTopicId);
44988
- if (!claim.claimed) {
44989
- if (claim.row.host_topic_id !== payload.hostTopicId) {
44990
- return { ok: false, error: "requestId already belongs to another room", status: 409 };
44991
- }
44992
- if (claim.row.status === "failed") {
44993
- return { ok: false, error: claim.row.error ?? "previous attempt failed", status: 409 };
44994
- }
44995
- logger.info({ requestId: payload.requestId, hostTopicId: payload.hostTopicId, status: claim.row.status }, "otium: peer turn replay acknowledged without re-execution");
44996
- return { ok: true };
44997
- }
44998
- const provisioned = provisionMirrorTopic(hostCellId, {
44999
- userId: payload.userId,
45000
- hostTopicId: payload.hostTopicId,
45001
- topicTitle: payload.topicTitle,
45002
- execution
45003
- });
45004
- if (!provisioned.ok) {
45005
- markPeerTurnRequestFailed(hostCellId, payload.requestId, provisioned.error);
45006
- return provisioned;
45007
- }
45008
- const localTopicId = provisioned.localTopicId;
45009
- const localTopic = getTopic(localTopicId);
45010
- if (payload.attachments?.some((fileId) => !peerFileAllowsAccess(fileId, { topicId: localTopicId, ownerUserId: payload.userId }))) {
45011
- markPeerTurnRequestFailed(hostCellId, payload.requestId, "attachment access denied");
45012
- return { ok: false, error: "attachment access denied", status: 403 };
45013
- }
45014
- const turnAgent = provisioned.bindingMode === "shared" && localTopic?.agent ? localTopic.agent : execution.agent;
45015
- const previous = getActiveForwarder(localTopicId);
45016
- if (previous) {
45017
- previous.finish({
45018
- type: "ai_aborted",
45019
- queryId: previous.queryId ?? previous.requestId,
45020
- topicId: localTopicId,
45021
- reason: "superseded"
45022
- });
45023
- }
45024
- const forwarder = createTurnForwarder({
45025
- hostNodeId: hostCellId,
45026
- requestId: payload.requestId,
45027
- localTopicId,
45028
- sendEvent: opts.sendEvent ?? hubEventSender(hubNode)
45029
- });
45030
- registerTurnForwarder(localTopicId, forwarder);
45031
- let queryId = null;
45032
- try {
45033
- queryId = executeExternalUserTurn({
45034
- topicId: localTopicId,
45035
- userId: payload.userId,
45036
- text: payload.message,
45037
- agent: turnAgent,
45038
- options: {
45039
- origin: "user",
45040
- requestId: payload.requestId,
45041
- injectAuthorId: payload.userId,
45042
- injectSourceNode: hostCellId,
45043
- ...payload.sourceMessageId ? { injectSourceMessageId: payload.sourceMessageId } : {},
45044
- attachments: payload.attachments,
45045
- visualTools: true,
45046
- fileDeliveryTools: true,
45047
- onDispatched: (dispatchedQueryId) => {
45048
- forwarder.queryId = dispatchedQueryId;
45049
- },
45050
- peerBridge: {
45051
- hubCellId: hostCellId,
45052
- hostTopicId: payload.hostTopicId,
45053
- hostQueryId: payload.requestId,
45054
- canSpawnSubagents: execution.canSpawnSubagents
45055
- }
45056
- },
45057
- ...turnTrigger ? { dispatch: turnTrigger } : {}
45058
- });
45059
- } catch (err2) {
45060
- forwarder.finish({
45061
- type: "ai_error",
45062
- queryId: payload.requestId,
45063
- topicId: localTopicId,
45064
- error: `worker dispatch crashed: ${err2.message}`
45065
- });
45066
- return { ok: false, error: "failed to start turn", status: 500 };
45067
- }
45068
- if (!queryId) {
45069
- forwarder.finish({
45070
- type: "ai_error",
45071
- queryId: payload.requestId,
45072
- topicId: localTopicId,
45073
- error: "worker could not start the turn"
45074
- });
45075
- return { ok: false, error: "failed to start turn", status: 500 };
45076
- }
45077
- forwarder.queryId = queryId;
45078
- markPeerTurnRequestRunning(hostCellId, payload.requestId);
45079
- logger.info({ requestId: payload.requestId, localTopicId, queryId, title: payload.topicTitle }, "otium: peer turn running");
45080
- return { ok: true };
45081
- }
45082
- function abortHostedPeerTurn(hostNodeId, requestId, userId, topicTitle) {
45083
- const request = getPeerTurnRequest(hostNodeId, requestId);
45084
- if (!request || request.status !== "running")
45085
- return false;
45086
- const session = getPeerSession(hostNodeId, request.host_topic_id);
45087
- if (!session)
45088
- return false;
45089
- const topic = getTopic(session.local_topic_id);
45090
- if (!topic || topic.title !== topicTitle || !topic.participants.some((participant) => participant.userId === userId)) {
45091
- return false;
45092
- }
45093
- const forwarder = getActiveForwarder(session.local_topic_id);
45094
- if (!forwarder || forwarder.requestId !== requestId)
45095
- return false;
45096
- if (!getRoomQuery(session.local_topic_id))
45097
- return false;
45098
- return abortRoom(session.local_topic_id);
45099
- }
45100
- var EFFORT_LEVELS, turnTrigger;
45101
- var init_turn_bridge = __esm(async () => {
45102
- await init_src();
45103
- await init_event_backflow();
45104
- await init_peer_files();
45105
- await init_store2();
45106
- EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
45107
- });
45108
-
45109
43894
  // ../../adapters/otium/src/peer-server.ts
45110
43895
  import { statfsSync } from "fs";
45111
43896
  import { cpus as cpus2, freemem as freemem2, loadavg as loadavg2, totalmem as totalmem2 } from "os";
@@ -45122,7 +43907,7 @@ async function readBody(req) {
45122
43907
  return null;
45123
43908
  return body;
45124
43909
  }
45125
- function str2(body, field) {
43910
+ function str(body, field) {
45126
43911
  const value = body[field];
45127
43912
  return typeof value === "string" && value.trim() ? value : null;
45128
43913
  }
@@ -45148,6 +43933,9 @@ async function requirePeer(req) {
45148
43933
  function requirePrimaryOrigin(peer) {
45149
43934
  return peer.verified.fromIsPrimary ? null : jsonError2("only the workspace hub may call this endpoint", 403);
45150
43935
  }
43936
+ function peerAddressable(topic) {
43937
+ return isTopicShared(topic);
43938
+ }
45151
43939
  function localCapabilities() {
45152
43940
  const agents = SUPPORTED_AGENTS.map((kind) => {
45153
43941
  const registry = getRegistry(kind);
@@ -45165,7 +43953,7 @@ function localCapabilities() {
45165
43953
  runtimeVersion: RUNTIME_VERSION,
45166
43954
  features: {
45167
43955
  remoteAsk: true,
45168
- inputFiles: true,
43956
+ inputFiles: false,
45169
43957
  outputFiles: true,
45170
43958
  visualBridge: true,
45171
43959
  askUserBridge: true,
@@ -45199,180 +43987,6 @@ function localHealth() {
45199
43987
  ...disk ? { disk } : {}
45200
43988
  };
45201
43989
  }
45202
- async function handleProvision(req) {
45203
- const peer = await requirePeer(req);
45204
- if (!peer.ok)
45205
- return peer.response;
45206
- const originError = requirePrimaryOrigin(peer);
45207
- if (originError)
45208
- return originError;
45209
- const body = await readBody(req);
45210
- if (!body)
45211
- return jsonError2("invalid JSON body", 400);
45212
- const protocolError = checkProtocol(body);
45213
- if (protocolError)
45214
- return protocolError;
45215
- const userId = str2(body, "userId");
45216
- const hostTopicId = str2(body, "hostTopicId");
45217
- const topicTitle = str2(body, "topicTitle");
45218
- const execution = parseExecutionSpec(body.execution);
45219
- if (!userId || !hostTopicId || !topicTitle || !execution) {
45220
- return jsonError2("invalid peer provision request", 400);
45221
- }
45222
- const result = provisionMirrorTopic(peer.verified.fromCellId, {
45223
- userId,
45224
- hostTopicId,
45225
- topicTitle,
45226
- execution
45227
- });
45228
- if (!result.ok)
45229
- return jsonError2(result.error, result.status);
45230
- logger.info({ hostTopicId, localTopicId: result.localTopicId, fromNode: peer.verified.fromNodeName }, "otium: mirror room provisioned");
45231
- return Response.json({ ok: true });
45232
- }
45233
- async function handleBind(req) {
45234
- const peer = await requirePeer(req);
45235
- if (!peer.ok)
45236
- return peer.response;
45237
- const originError = requirePrimaryOrigin(peer);
45238
- if (originError)
45239
- return originError;
45240
- const body = await readBody(req);
45241
- if (!body)
45242
- return jsonError2("invalid JSON body", 400);
45243
- const protocolError = checkProtocol(body);
45244
- if (protocolError)
45245
- return protocolError;
45246
- const userId = str2(body, "userId");
45247
- const hostTopicId = str2(body, "hostTopicId");
45248
- const localTopicId = str2(body, "localTopicId");
45249
- if (!userId || !hostTopicId || !localTopicId) {
45250
- return jsonError2("invalid peer bind request", 400);
45251
- }
45252
- const result = bindOtiumTopic({
45253
- hostNodeId: peer.verified.fromCellId,
45254
- hostTopicId,
45255
- localTopicId,
45256
- userId
45257
- });
45258
- if (!result.ok)
45259
- return jsonError2(result.error, result.status);
45260
- return Response.json({ ok: true, localTopicId: result.localTopicId, replaced: result.replaced });
45261
- }
45262
- async function handleSharedTopicMessages(req) {
45263
- const peer = await requirePeer(req);
45264
- if (!peer.ok)
45265
- return peer.response;
45266
- const originError = requirePrimaryOrigin(peer);
45267
- if (originError)
45268
- return originError;
45269
- const body = await readBody(req);
45270
- if (!body)
45271
- return jsonError2("invalid JSON body", 400);
45272
- const protocolError = checkProtocol(body);
45273
- if (protocolError)
45274
- return protocolError;
45275
- const localTopicId = str2(body, "localTopicId");
45276
- const hostTopicId = str2(body, "hostTopicId");
45277
- const messages = body.messages;
45278
- if (!localTopicId || !hostTopicId || !Array.isArray(messages)) {
45279
- return jsonError2("localTopicId, hostTopicId and messages are required", 400);
45280
- }
45281
- const session = getPeerSession(peer.verified.fromCellId, hostTopicId);
45282
- if (!session || session.local_topic_id !== localTopicId) {
45283
- return jsonError2("shared topic binding not found", 404);
45284
- }
45285
- const accepted = acceptSharedTopicMessages(messages, localTopicId, peer.verified.fromCellId);
45286
- return Response.json({ ok: true, accepted });
45287
- }
45288
- async function handleSharedTopicsPrivate(req) {
45289
- const peer = await requirePeer(req);
45290
- if (!peer.ok)
45291
- return peer.response;
45292
- const originError = requirePrimaryOrigin(peer);
45293
- if (originError)
45294
- return originError;
45295
- const body = await readBody(req);
45296
- if (!body)
45297
- return jsonError2("invalid JSON body", 400);
45298
- const protocolError = checkProtocol(body);
45299
- if (protocolError)
45300
- return protocolError;
45301
- if (body.reason !== "hub-removal")
45302
- return jsonError2("reason must be hub-removal", 400);
45303
- const updated = downgradeSharedTopicsForHub(peer.verified.fromCellId);
45304
- return Response.json({ ok: true, updated });
45305
- }
45306
- async function handleUnbind(req) {
45307
- const peer = await requirePeer(req);
45308
- if (!peer.ok)
45309
- return peer.response;
45310
- const originError = requirePrimaryOrigin(peer);
45311
- if (originError)
45312
- return originError;
45313
- const body = await readBody(req);
45314
- if (!body)
45315
- return jsonError2("invalid JSON body", 400);
45316
- const protocolError = checkProtocol(body);
45317
- if (protocolError)
45318
- return protocolError;
45319
- const hostTopicId = str2(body, "hostTopicId");
45320
- if (!hostTopicId)
45321
- return jsonError2("hostTopicId is required", 400);
45322
- const removed = unbindOtiumTopic(peer.verified.fromCellId, hostTopicId);
45323
- return Response.json({ ok: true, removed });
45324
- }
45325
- async function handleTurn(req) {
45326
- const peer = await requirePeer(req);
45327
- if (!peer.ok)
45328
- return peer.response;
45329
- const originError = requirePrimaryOrigin(peer);
45330
- if (originError)
45331
- return originError;
45332
- const body = await readBody(req);
45333
- if (!body)
45334
- return jsonError2("invalid JSON body", 400);
45335
- const protocolError = checkProtocol(body);
45336
- if (protocolError)
45337
- return protocolError;
45338
- const requestId = str2(body, "requestId");
45339
- const userId = str2(body, "userId");
45340
- const hostTopicId = str2(body, "hostTopicId");
45341
- const topicTitle = str2(body, "topicTitle");
45342
- const execution = parseExecutionSpec(body.execution);
45343
- if (body.execution !== undefined && !execution) {
45344
- return jsonError2("invalid placed-topic execution spec", 400);
45345
- }
45346
- const agent = execution?.agent ?? str2(body, "agent");
45347
- const message = str2(body, "message");
45348
- if (!requestId || !userId || !hostTopicId || !topicTitle || !agent || !message) {
45349
- return jsonError2("requestId, userId, hostTopicId, topicTitle, agent, message are required", 400);
45350
- }
45351
- if (message.length > MAX_PEER_MESSAGE_LENGTH)
45352
- return jsonError2("message too long", 400);
45353
- const hubNode = await resolvePeerNodeByCellId(peer.verified.fromCellId).catch(() => null);
45354
- if (!hubNode)
45355
- return jsonError2("calling node is not in this workspace", 403);
45356
- const payload = {
45357
- v: PEER_PROTOCOL_VERSION,
45358
- requestId,
45359
- userId,
45360
- hostTopicId,
45361
- topicTitle,
45362
- ...execution ? { execution } : {},
45363
- ...agent ? { agent } : {},
45364
- ...str2(body, "model") ? { model: str2(body, "model") } : {},
45365
- ...str2(body, "effort") ? { effort: str2(body, "effort") } : {},
45366
- ...Array.isArray(body.attachments) && body.attachments.every((entry) => typeof entry === "string") ? { attachments: body.attachments } : {},
45367
- ...str2(body, "sourceMessageId") ? { sourceMessageId: str2(body, "sourceMessageId") } : {},
45368
- message
45369
- };
45370
- const result = runPeerTurn(hubNode, peer.verified.fromCellId, payload);
45371
- if (!result.ok)
45372
- return jsonError2(result.error, result.status);
45373
- logger.info({ requestId, hostTopicId, fromNode: peer.verified.fromNodeName }, "otium: peer turn accepted");
45374
- return Response.json({ ok: true });
45375
- }
45376
43990
  async function handleAbort(req) {
45377
43991
  const peer = await requirePeer(req);
45378
43992
  if (!peer.ok)
@@ -45386,20 +44000,12 @@ async function handleAbort(req) {
45386
44000
  const protocolError = checkProtocol(body);
45387
44001
  if (protocolError)
45388
44002
  return protocolError;
45389
- const userId = str2(body, "userId");
45390
- const toTopic = str2(body, "toTopic");
44003
+ const userId = str(body, "userId");
44004
+ const toTopic = str(body, "toTopic");
45391
44005
  if (!userId || !toTopic)
45392
44006
  return jsonError2("userId and toTopic are required", 400);
45393
- const requestId = str2(body, "requestId");
45394
- if (requestId) {
45395
- const aborted = abortHostedPeerTurn(peer.verified.fromCellId, requestId, userId, toTopic);
45396
- if (!aborted)
45397
- return jsonError2("turn not found or already completed", 404);
45398
- logger.info({ fromNode: peer.verified.fromNodeName, toTopic, requestId }, "otium: exact peer turn abort accepted");
45399
- return Response.json({ ok: true });
45400
- }
45401
44007
  const topic = getTopicByNameForUser(toTopic, userId);
45402
- if (!topic || !isTopicShared(topic)) {
44008
+ if (!topic || !peerAddressable(topic)) {
45403
44009
  return jsonError2(`shared topic "${toTopic}" not found on this node`, 404);
45404
44010
  }
45405
44011
  appendJsonlEntry(sessionInboxPath(userId, topic.id), {
@@ -45423,11 +44029,11 @@ async function handleTell(req) {
45423
44029
  const protocolError = checkProtocol(body);
45424
44030
  if (protocolError)
45425
44031
  return protocolError;
45426
- const requestId = str2(body, "requestId");
45427
- const userId = str2(body, "userId");
45428
- const toTopic = str2(body, "toTopic");
45429
- const fromLabel = str2(body, "fromLabel");
45430
- const message = str2(body, "message");
44032
+ const requestId = str(body, "requestId");
44033
+ const userId = str(body, "userId");
44034
+ const toTopic = str(body, "toTopic");
44035
+ const fromLabel = str(body, "fromLabel");
44036
+ const message = str(body, "message");
45431
44037
  const depth = typeof body.depth === "number" ? body.depth : Number.NaN;
45432
44038
  if (!requestId || !userId || !toTopic || !fromLabel || !message) {
45433
44039
  return jsonError2("requestId, userId, toTopic, fromLabel, message are required", 400);
@@ -45441,7 +44047,7 @@ async function handleTell(req) {
45441
44047
  return jsonError2(`tell depth limit exceeded (max ${MAX_TELL_DEPTH})`, 400);
45442
44048
  }
45443
44049
  const topic = getTopicByNameForUser(toTopic, userId);
45444
- if (!topic || !isTopicShared(topic)) {
44050
+ if (!topic || !peerAddressable(topic)) {
45445
44051
  return jsonError2(`shared topic "${toTopic}" not found on this node`, 404);
45446
44052
  }
45447
44053
  const claim = claimInboundPeerMessage({
@@ -45495,10 +44101,10 @@ async function handleSessions(req) {
45495
44101
  const protocolError = checkProtocol(body);
45496
44102
  if (protocolError)
45497
44103
  return protocolError;
45498
- const userId = str2(body, "userId");
44104
+ const userId = str(body, "userId");
45499
44105
  if (!userId)
45500
44106
  return jsonError2("userId is required", 400);
45501
- const topics = listTopics().filter((topic) => topic.kind !== "manager" && !topic.isSubagent && isTopicShared(topic) && topic.participants.some((p) => p.userId === userId));
44107
+ const topics = listTopics().filter((topic) => topic.kind !== "manager" && !topic.isSubagent && peerAddressable(topic) && topic.participants.some((p) => p.userId === userId));
45502
44108
  const titleCounts = new Map;
45503
44109
  for (const topic of topics) {
45504
44110
  const normalized = topic.title.toLowerCase();
@@ -45530,11 +44136,11 @@ async function handleAsk(req) {
45530
44136
  const protocolError = checkProtocol(body);
45531
44137
  if (protocolError)
45532
44138
  return protocolError;
45533
- const requestId = str2(body, "requestId");
45534
- const userId = str2(body, "userId");
45535
- const toTopic = str2(body, "toTopic");
45536
- const fromLabel = str2(body, "fromLabel");
45537
- const message = str2(body, "message");
44139
+ const requestId = str(body, "requestId");
44140
+ const userId = str(body, "userId");
44141
+ const toTopic = str(body, "toTopic");
44142
+ const fromLabel = str(body, "fromLabel");
44143
+ const message = str(body, "message");
45538
44144
  const fromDepth = body.fromDepth === undefined ? 0 : typeof body.fromDepth === "number" ? body.fromDepth : Number.NaN;
45539
44145
  const replyTo = body.replyTo;
45540
44146
  const replyTopicId = typeof replyTo?.topicId === "string" ? replyTo.topicId : null;
@@ -45547,7 +44153,7 @@ async function handleAsk(req) {
45547
44153
  return jsonError2("fromDepth must be a non-negative integer", 400);
45548
44154
  }
45549
44155
  const topic = getTopicByNameForUser(toTopic, userId);
45550
- if (!topic || !isTopicShared(topic)) {
44156
+ if (!topic || !peerAddressable(topic)) {
45551
44157
  return jsonError2(`shared topic "${toTopic}" not found on this node`, 404);
45552
44158
  }
45553
44159
  if (!topic.agent)
@@ -45600,10 +44206,10 @@ async function handleReply(req) {
45600
44206
  const protocolError = checkProtocol(body);
45601
44207
  if (protocolError)
45602
44208
  return protocolError;
45603
- const requestId = str2(body, "requestId");
45604
- const userId = str2(body, "userId");
44209
+ const requestId = str(body, "requestId");
44210
+ const userId = str(body, "userId");
45605
44211
  const replyText = typeof body.replyText === "string" ? body.replyText : null;
45606
- const fromLabel = str2(body, "fromLabel") ?? "peer";
44212
+ const fromLabel = str(body, "fromLabel") ?? "peer";
45607
44213
  const kind = body.kind === "error" ? "error" : "reply";
45608
44214
  if (!requestId || !userId || replyText === null) {
45609
44215
  return jsonError2("requestId, userId and replyText are required", 400);
@@ -45622,39 +44228,6 @@ async function handleReply(req) {
45622
44228
  return jsonError2("ask callback delivery is retryable", 503);
45623
44229
  return jsonError2("no pending ask for this requestId", 404);
45624
44230
  }
45625
- async function handleInputFile(req) {
45626
- const peer = await requirePeer(req);
45627
- if (!peer.ok)
45628
- return peer.response;
45629
- const originError = requirePrimaryOrigin(peer);
45630
- if (originError)
45631
- return originError;
45632
- const contentLength = Number(req.headers.get("content-length"));
45633
- if (Number.isFinite(contentLength) && contentLength > MAX_PEER_INPUT_REQUEST_BYTES) {
45634
- return jsonError2("file too large", 413);
45635
- }
45636
- const form = await req.formData().catch(() => null);
45637
- if (!form)
45638
- return jsonError2("expected multipart/form-data", 400);
45639
- const hostTopicId = form.get("hostTopicId");
45640
- const userId = form.get("userId");
45641
- const file = form.get("file");
45642
- if (typeof hostTopicId !== "string" || typeof userId !== "string" || !(file instanceof File)) {
45643
- return jsonError2("hostTopicId, userId, and file are required", 400);
45644
- }
45645
- if (file.size > MAX_PEER_INPUT_FILE_BYTES)
45646
- return jsonError2("file too large", 413);
45647
- const session = getPeerSession(peer.verified.fromCellId, hostTopicId);
45648
- const topic = session ? getTopic(session.local_topic_id) : null;
45649
- if (!session || !topic?.participants.some((participant) => participant.userId === userId)) {
45650
- return jsonError2("provisioned peer room not found", 404);
45651
- }
45652
- const stored = await storePeerInputFile(file, {
45653
- topicId: session.local_topic_id,
45654
- ownerUserId: userId
45655
- });
45656
- return Response.json({ ok: true, fileId: stored.id });
45657
- }
45658
44231
  async function handleDeviceVault(req) {
45659
44232
  const peer = await requirePeer(req);
45660
44233
  if (!peer.ok)
@@ -45668,14 +44241,14 @@ async function handleDeviceVault(req) {
45668
44241
  const protocolError = checkProtocol(body);
45669
44242
  if (protocolError)
45670
44243
  return protocolError;
45671
- const userId = str2(body, "userId");
45672
- const operation = str2(body, "operation");
44244
+ const userId = str(body, "userId");
44245
+ const operation = str(body, "operation");
45673
44246
  if (!userId || !operation)
45674
44247
  return jsonError2("userId and operation are required", 400);
45675
44248
  if (operation === "list") {
45676
44249
  return Response.json({ ok: true, entries: vaultList(userId) });
45677
44250
  }
45678
- const rawKey = str2(body, "key");
44251
+ const rawKey = str(body, "key");
45679
44252
  if (!rawKey || !validateVaultKey(rawKey))
45680
44253
  return jsonError2("invalid vault key", 400);
45681
44254
  const key = normalizeVaultKey(rawKey);
@@ -45740,18 +44313,6 @@ async function handleOtiumPeerRequest(req) {
45740
44313
  if (req.method !== "POST")
45741
44314
  return jsonError2("not found", 404);
45742
44315
  switch (path) {
45743
- case "/api/v1/peer/provision":
45744
- return handleProvision(req);
45745
- case "/api/v1/peer/bind":
45746
- return handleBind(req);
45747
- case "/api/v1/peer/shared-topic/messages":
45748
- return handleSharedTopicMessages(req);
45749
- case "/api/v1/peer/shared-topics/private":
45750
- return handleSharedTopicsPrivate(req);
45751
- case "/api/v1/peer/unbind":
45752
- return handleUnbind(req);
45753
- case "/api/v1/peer/turn":
45754
- return handleTurn(req);
45755
44316
  case "/api/v1/peer/abort":
45756
44317
  return handleAbort(req);
45757
44318
  case "/api/v1/peer/tell":
@@ -45762,8 +44323,6 @@ async function handleOtiumPeerRequest(req) {
45762
44323
  return handleSessions(req);
45763
44324
  case "/api/v1/peer/reply":
45764
44325
  return handleReply(req);
45765
- case "/api/v1/peer/input-file":
45766
- return handleInputFile(req);
45767
44326
  case "/api/v1/peer/device-vault":
45768
44327
  return handleDeviceVault(req);
45769
44328
  default:
@@ -45773,15 +44332,11 @@ async function handleOtiumPeerRequest(req) {
45773
44332
  var RUNTIME_VERSION;
45774
44333
  var init_peer_server = __esm(async () => {
45775
44334
  await init_src();
45776
- await init_bindings();
45777
44335
  await init_central();
45778
44336
  await init_gateway_forward();
45779
- await init_peer_files();
45780
44337
  init_protocol();
45781
44338
  await init_session_bridge();
45782
- await init_shared_topic_sync();
45783
44339
  await init_store2();
45784
- await init_turn_bridge();
45785
44340
  RUNTIME_VERSION = NEGOTIUM_VERSION;
45786
44341
  });
45787
44342
 
@@ -45789,16 +44344,6 @@ var init_peer_server = __esm(async () => {
45789
44344
  function startOtiumNodeRuntime(options) {
45790
44345
  const { join: join43 } = options;
45791
44346
  configureOtiumCentral(join43);
45792
- const failed = failInterruptedPeerTurnRequestsOnStartup();
45793
- if (failed > 0) {
45794
- logger.warn({ failed }, "otium: failed interrupted peer turns from previous process");
45795
- }
45796
- const staleBindings = sweepStalePeerBindings((localTopicId) => getTopic(localTopicId) !== null);
45797
- if (staleBindings.topicIds.length > 0) {
45798
- logger.info({ topicIds: staleBindings.topicIds, ...staleBindings.removed }, "otium: removed peer state for local topics deleted while this node was offline");
45799
- }
45800
- const stopBackflow = startEventBackflow();
45801
- const stopSharedTopicSync = startSharedTopicSync(join43);
45802
44347
  const unregisterRuntimeBridge = registerPeerRuntimeBridge(otiumPeerRuntimeBridge);
45803
44348
  const unregisterSessionBridge = registerPeerSessionBridge(otiumPeerSessionBridge);
45804
44349
  const sessionBridgeIpc = startPeerSessionBridgeIpc(otiumPeerSessionBridge);
@@ -45814,7 +44359,7 @@ function startOtiumNodeRuntime(options) {
45814
44359
  if (event.type !== "topic-deleted")
45815
44360
  return;
45816
44361
  const removed = cleanupPeerStateForLocalTopic(event.topicId);
45817
- if (removed.sessions + removed.turns + removed.inboxRequests + removed.remoteAsks > 0) {
44362
+ if (removed.inboxRequests + removed.remoteAsks > 0) {
45818
44363
  logger.info({ topicId: event.topicId, ...removed }, "otium: removed peer state for deleted local topic");
45819
44364
  }
45820
44365
  });
@@ -45841,8 +44386,6 @@ function startOtiumNodeRuntime(options) {
45841
44386
  canonicalMcpBridge.stop();
45842
44387
  stopPeerReplyOutbox();
45843
44388
  uninstallFileHooks();
45844
- stopBackflow();
45845
- stopSharedTopicSync();
45846
44389
  configureOtiumCentral(null);
45847
44390
  }
45848
44391
  };
@@ -45886,34 +44429,28 @@ var init_src8 = __esm(async () => {
45886
44429
  await init_src();
45887
44430
  await init_canonical_mcp_bridge();
45888
44431
  await init_central();
45889
- await init_event_backflow();
45890
44432
  await init_join();
45891
44433
  await init_peer_files();
45892
44434
  await init_runtime_bridge();
45893
44435
  await init_session_bridge();
45894
44436
  init_session_bridge_ipc();
45895
- await init_shared_topic_sync();
45896
44437
  await init_store2();
45897
44438
  init_tunnel_client();
45898
- await init_bindings();
45899
44439
  await init_central();
45900
44440
  await init_enrollment();
45901
- await init_event_backflow();
45902
44441
  await init_join();
45903
44442
  await init_peer_server();
45904
44443
  init_protocol();
45905
44444
  init_relay_protocol();
45906
44445
  await init_runtime_bridge();
45907
- await init_shared_topic_sync();
45908
44446
  await init_store2();
45909
44447
  init_tunnel_client();
45910
- await init_turn_bridge();
45911
44448
  otiumAdapter = defineNegotiumAdapter({
45912
44449
  name: "otium",
45913
44450
  capabilities: {
45914
44451
  localUserInput: false,
45915
44452
  topicManagement: false,
45916
- externalPlacedTurn: true
44453
+ externalPlacedTurn: false
45917
44454
  },
45918
44455
  projection: {
45919
44456
  transcript: "full",
@@ -45931,7 +44468,7 @@ __export(exports_node_runtime, {
45931
44468
  handleOtiumAdapterControlRequest: () => handleOtiumAdapterControlRequest,
45932
44469
  OTIUM_ADAPTER_CONTROL_PREFIX: () => OTIUM_ADAPTER_CONTROL_PREFIX,
45933
44470
  OTIUM_ADAPTER_CONTROL_HEADER: () => OTIUM_ADAPTER_CONTROL_HEADER,
45934
- MAX_PEER_INPUT_REQUEST_BYTES: () => MAX_PEER_INPUT_REQUEST_BYTES
44471
+ MAX_PEER_REQUEST_BODY_BYTES: () => MAX_PEER_REQUEST_BODY_BYTES
45935
44472
  });
45936
44473
  async function handleOtiumAdapterControlRequest(req) {
45937
44474
  const url = new URL(req.url);
@@ -46131,7 +44668,7 @@ async function runOtiumSidecar(options) {
46131
44668
  port: options.port,
46132
44669
  hostname: "127.0.0.1",
46133
44670
  idleTimeout: 240,
46134
- maxRequestBodySize: MAX_PEER_INPUT_REQUEST_BYTES,
44671
+ maxRequestBodySize: MAX_PEER_REQUEST_BODY_BYTES,
46135
44672
  fetch: (req) => proxyOtiumPeerRequest(req)
46136
44673
  });
46137
44674
  } catch (error2) {
@@ -46223,27 +44760,6 @@ function parseOtiumServeRelayUrl(args) {
46223
44760
  }
46224
44761
  return raw.replace(/\/+$/, "");
46225
44762
  }
46226
- async function resolveHostNodeId(explicit) {
46227
- if (explicit?.trim())
46228
- return explicit.trim();
46229
- const [{ configureOtiumCentral: configureOtiumCentral2, listPeerNodes: listPeerNodes2 }, { loadJoin: loadJoin2 }] = await Promise.all([
46230
- init_central().then(() => exports_central),
46231
- init_join().then(() => exports_join)
46232
- ]);
46233
- const join43 = loadJoin2();
46234
- if (!join43)
46235
- throw new Error("not joined to an Otium workspace; pass --host-node or join first");
46236
- configureOtiumCentral2(join43);
46237
- try {
46238
- const nodes = await listPeerNodes2({ fresh: true });
46239
- const primary = nodes.find((node) => node.isPrimary && !node.self) ?? nodes.find((node) => node.isPrimary);
46240
- if (!primary)
46241
- throw new Error("workspace has no primary Otium node");
46242
- return primary.cellId;
46243
- } finally {
46244
- configureOtiumCentral2(null);
46245
- }
46246
- }
46247
44763
  async function spawnCanonicalNode2() {
46248
44764
  const entry = process.argv[1];
46249
44765
  if (!entry)
@@ -46275,11 +44791,11 @@ async function runCanonicalNodeChild2() {
46275
44791
  let maxRequestBodySize;
46276
44792
  if (hasConfiguredOtiumJoin2()) {
46277
44793
  const { onShutdown: onShutdown2 } = await init_node_host().then(() => exports_node_host);
46278
- const { MAX_PEER_INPUT_REQUEST_BYTES: MAX_PEER_INPUT_REQUEST_BYTES2, mountConfiguredOtiumNodeRuntime: mountConfiguredOtiumNodeRuntime2 } = await init_node_runtime().then(() => exports_node_runtime);
44794
+ const { MAX_PEER_REQUEST_BODY_BYTES: MAX_PEER_REQUEST_BODY_BYTES2, mountConfiguredOtiumNodeRuntime: mountConfiguredOtiumNodeRuntime2 } = await init_node_runtime().then(() => exports_node_runtime);
46279
44795
  const runtime2 = mountConfiguredOtiumNodeRuntime2();
46280
44796
  if (runtime2)
46281
44797
  onShutdown2("otium-node-runtime", 125, () => runtime2.stop());
46282
- maxRequestBodySize = MAX_PEER_INPUT_REQUEST_BYTES2;
44798
+ maxRequestBodySize = MAX_PEER_REQUEST_BODY_BYTES2;
46283
44799
  }
46284
44800
  await runNodeDaemon2({ port: 0, ...maxRequestBodySize ? { maxRequestBodySize } : {} });
46285
44801
  }
@@ -46301,20 +44817,29 @@ async function runOtiumCli(args = process.argv.slice(2)) {
46301
44817
  if (process.env.OTIUM_CENTRAL_URL || process.env.OTIUM_CELL_ID || process.env.OTIUM_CELL_SECRET) {
46302
44818
  throw new Error("Otium join is configured by environment; remove OTIUM_CENTRAL_URL, OTIUM_CELL_ID, and OTIUM_CELL_SECRET to disconnect");
46303
44819
  }
46304
- const { configureOtiumCentral: configureOtiumCentral2 } = await init_central().then(() => exports_central);
46305
44820
  const { loadJoin: loadJoin2, removeJoin: removeJoin2 } = await init_join().then(() => exports_join);
46306
- const { disconnectSharedTopics: disconnectSharedTopics2 } = await init_shared_topic_sync().then(() => exports_shared_topic_sync);
46307
- const join43 = loadJoin2();
46308
- if (!join43)
44821
+ if (!loadJoin2())
46309
44822
  throw new Error("not joined to an Otium workspace");
46310
- configureOtiumCentral2(join43);
46311
- try {
46312
- await disconnectSharedTopics2(join43);
46313
- removeJoin2();
46314
- } finally {
46315
- configureOtiumCentral2(null);
44823
+ const { getVisibleTopics: getVisibleTopics2, isTopicShared: isTopicShared2, switchTopicAccessMode: switchTopicAccessMode2 } = await init_src().then(() => exports_src);
44824
+ let downgraded = 0;
44825
+ for (const topic of getVisibleTopics2()) {
44826
+ if (!isTopicShared2(topic))
44827
+ continue;
44828
+ const owner = topic.participants.find((participant) => participant.role === "owner");
44829
+ if (!owner)
44830
+ continue;
44831
+ if (topic.isSubagent)
44832
+ continue;
44833
+ const switched = switchTopicAccessMode2({
44834
+ topicId: topic.id,
44835
+ userId: owner.userId,
44836
+ accessMode: "private"
44837
+ });
44838
+ if (switched.ok)
44839
+ downgraded += switched.topicIds.length;
46316
44840
  }
46317
- console.log("disconnected from Otium; local shared topics are now private");
44841
+ removeJoin2();
44842
+ console.log(`disconnected from Otium; workspace credentials removed` + (downgraded > 0 ? `; ${downgraded} topic(s) are now private` : ""));
46318
44843
  break;
46319
44844
  }
46320
44845
  case "serve": {
@@ -46326,63 +44851,19 @@ async function runOtiumCli(args = process.argv.slice(2)) {
46326
44851
  await runOtiumSidecar2({ port, relayUrl });
46327
44852
  break;
46328
44853
  }
46329
- case "bindings": {
46330
- const { listOtiumTopicBindings: listOtiumTopicBindings2 } = await init_bindings().then(() => exports_bindings);
46331
- const bindings = listOtiumTopicBindings2();
46332
- if (bindings.length === 0) {
46333
- console.log("no Otium topic bindings");
46334
- break;
46335
- }
46336
- for (const binding of bindings) {
46337
- const local = binding.localTopicTitle ? `${binding.localTopicTitle} (${binding.localTopicId})` : `${binding.localTopicId} [missing]`;
46338
- console.log(`${binding.transport.padEnd(16)} ${binding.hostNodeId}/${binding.hostTopicId} -> ${local}`);
46339
- }
46340
- break;
46341
- }
46342
- case "share": {
46343
- const parsed = parseArgs(commandArgs);
46344
- const [hostTopicId, localTopicId] = parsed.positional;
46345
- const userId = parsed.options.get("user")?.trim();
46346
- if (!hostTopicId || !localTopicId || !userId) {
46347
- throw new Error("usage: negotium otium share <host-topic-id> <local-topic-id> --user <user-id> [--host-node <cell-id>]");
46348
- }
46349
- const hostNodeId = await resolveHostNodeId(parsed.options.get("host-node"));
46350
- const { shareOtiumTopic: shareOtiumTopic2 } = await init_bindings().then(() => exports_bindings);
46351
- const result = shareOtiumTopic2({ hostNodeId, hostTopicId, localTopicId, userId });
46352
- if (!result.ok)
46353
- throw new Error(result.error);
46354
- console.log(`shared ${hostNodeId}/${hostTopicId} with local topic ${result.localTopicId}` + (result.replaced ? " (replaced previous binding)" : ""));
46355
- break;
46356
- }
46357
- case "private": {
46358
- const parsed = parseArgs(commandArgs);
46359
- const [localTopicId] = parsed.positional;
46360
- const userId = parsed.options.get("user")?.trim();
46361
- if (!localTopicId || !userId) {
46362
- throw new Error("usage: negotium otium private <local-topic-id> --user <user-id>");
46363
- }
46364
- const { setOtiumTopicPrivate: setOtiumTopicPrivate2 } = await init_bindings().then(() => exports_bindings);
46365
- const result = setOtiumTopicPrivate2({ localTopicId, userId });
46366
- if (!result.ok)
46367
- throw new Error(result.error);
46368
- console.log(`private mode selected for ${result.localTopicId}; removed ${result.removedBindings} Otium binding(s)`);
46369
- break;
46370
- }
46371
44854
  default: {
46372
44855
  console.log([
46373
44856
  "negotium otium \u2014 attach a Negotium node to an Otium workspace",
46374
44857
  "",
46375
- "usage: negotium otium <join|leave|serve|bindings|share|private> [args]",
44858
+ "usage: negotium otium <join|leave|serve> [args]",
46376
44859
  "",
46377
44860
  " join <code> store credentials from an Otium invite code",
46378
- " leave delete Hub copies, make local topics private, and remove credentials",
44861
+ " leave remove the stored workspace credentials",
46379
44862
  " serve [--port <port>] [--relay <url>]",
46380
44863
  " run peer routes and an outbound relay tunnel",
46381
- " bindings list internal mirrors and shared topic bindings",
46382
- " share <host-topic> <local-topic> --user <id> [--host-node <cell>]",
46383
- " publish one private local topic to Otium as shared",
46384
- " private <local-topic> --user <id>",
46385
- " remove all Otium bindings; keep Terminal/Telegram access"
44864
+ "",
44865
+ "Publish a topic to the workspace with /public in that topic; the hub",
44866
+ "discovers it over the Runtime Gateway. /private withdraws it."
46386
44867
  ].join(`
46387
44868
  `));
46388
44869
  if (command && command !== "help" && command !== "--help")
@@ -46898,8 +45379,8 @@ var CLI_COMMANDS = [
46898
45379
  },
46899
45380
  {
46900
45381
  name: "otium",
46901
- usage: "otium join|bindings|share|private|leave",
46902
- description: "manage the Otium workspace connection and topic bindings",
45382
+ usage: "otium join|leave|serve",
45383
+ description: "manage the Otium workspace connection",
46903
45384
  group: "Channels"
46904
45385
  }
46905
45386
  ];
@@ -46939,11 +45420,11 @@ async function runCanonicalNode(port) {
46939
45420
  let maxRequestBodySize;
46940
45421
  if (hasConfiguredOtiumJoin2()) {
46941
45422
  const { onShutdown: onShutdown2 } = await init_node_host().then(() => exports_node_host);
46942
- const { MAX_PEER_INPUT_REQUEST_BYTES: MAX_PEER_INPUT_REQUEST_BYTES2, mountConfiguredOtiumNodeRuntime: mountConfiguredOtiumNodeRuntime2 } = await init_node_runtime().then(() => exports_node_runtime);
45423
+ const { MAX_PEER_REQUEST_BODY_BYTES: MAX_PEER_REQUEST_BODY_BYTES2, mountConfiguredOtiumNodeRuntime: mountConfiguredOtiumNodeRuntime2 } = await init_node_runtime().then(() => exports_node_runtime);
46943
45424
  const otiumRuntime = mountConfiguredOtiumNodeRuntime2();
46944
45425
  if (otiumRuntime)
46945
45426
  onShutdown2("otium-node-runtime", 125, () => otiumRuntime.stop());
46946
- maxRequestBodySize = MAX_PEER_INPUT_REQUEST_BYTES2;
45427
+ maxRequestBodySize = MAX_PEER_REQUEST_BODY_BYTES2;
46947
45428
  }
46948
45429
  const node = await startDefaultNode2({
46949
45430
  port,
@@ -47084,4 +45565,4 @@ switch (command) {
47084
45565
  }
47085
45566
  }
47086
45567
 
47087
- //# debugId=31C48E669F9A292A64756E2164756E21
45568
+ //# debugId=4A37A5D1997871CA64756E2164756E21