negotium 0.2.27 → 0.2.28
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/agent-helpers.js +197 -113
- package/dist/agent-helpers.js.map +13 -13
- package/dist/hosted-agent.js +2 -2
- package/dist/hosted-agent.js.map +2 -2
- package/dist/main.js +1121 -506
- package/dist/main.js.map +43 -41
- package/dist/mcp-factories.js +291 -211
- package/dist/mcp-factories.js.map +13 -13
- package/dist/registry.js +2 -2
- package/dist/registry.js.map +2 -2
- package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +8 -0
- package/dist/runtime/src/index.ts +6 -0
- package/dist/runtime/src/mcp/session-comm/default-host.ts +25 -6
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +14 -3
- package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
- package/dist/runtime/src/mcp/session-comm/topics.ts +52 -16
- package/dist/runtime/src/mcp-runtime-host.ts +2 -2
- package/dist/runtime/src/node-host.ts +1 -1
- package/dist/runtime/src/runtime/turn-event-stream.ts +9 -1
- package/dist/runtime/src/runtime/turn-runner.ts +11 -2
- package/dist/runtime/src/storage/api-topics.ts +202 -26
- package/dist/runtime/src/storage/runtime-turn-requests.ts +26 -2
- package/dist/runtime/src/storage/token-stats.ts +27 -2
- package/dist/runtime/src/topics/create.ts +27 -1
- package/dist/runtime/src/topics/derive.ts +20 -6
- package/dist/runtime/src/topics/lifecycle.ts +2 -0
- package/dist/runtime/src/topics/personal-general.ts +28 -4
- package/dist/runtime/src/types/api.ts +7 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/storage.js +138 -26
- package/dist/storage.js.map +4 -4
- package/dist/types/packages/core/src/mcp/session-comm/peer-forward.d.ts +7 -2
- package/dist/types/packages/core/src/runtime/turn-event-stream.d.ts +2 -0
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +3 -0
- package/dist/types/packages/core/src/storage/api-topics.d.ts +42 -3
- package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +6 -0
- package/dist/types/packages/core/src/storage/token-stats.d.ts +2 -1
- package/dist/types/packages/core/src/topics/derive.d.ts +2 -0
- package/dist/types/packages/core/src/topics/personal-general.d.ts +4 -2
- package/dist/types/packages/core/src/types/api.d.ts +7 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1868,7 +1868,7 @@ var exports_version = {};
|
|
|
1868
1868
|
__export(exports_version, {
|
|
1869
1869
|
NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
|
|
1870
1870
|
});
|
|
1871
|
-
var NEGOTIUM_VERSION = "0.2.
|
|
1871
|
+
var NEGOTIUM_VERSION = "0.2.28";
|
|
1872
1872
|
|
|
1873
1873
|
// ../../packages/core/src/agents/codex-native-multi-agent.ts
|
|
1874
1874
|
import { spawn } from "child_process";
|
|
@@ -7506,8 +7506,11 @@ var init_constants = __esm(() => {
|
|
|
7506
7506
|
var exports_api_topics = {};
|
|
7507
7507
|
__export(exports_api_topics, {
|
|
7508
7508
|
upsertTopic: () => upsertTopic,
|
|
7509
|
+
stampUnscopedOtiumTopics: () => stampUnscopedOtiumTopics,
|
|
7509
7510
|
setTopicSurfaces: () => setTopicSurfaces,
|
|
7510
7511
|
setTopicSessionId: () => setTopicSessionId,
|
|
7512
|
+
setSurfaceScopeRequired: () => setSurfaceScopeRequired,
|
|
7513
|
+
setDefaultSurfaceScope: () => setDefaultSurfaceScope,
|
|
7511
7514
|
setApiTopicAgent: () => setApiTopicAgent,
|
|
7512
7515
|
revokeSubagentTellTarget: () => revokeSubagentTellTarget,
|
|
7513
7516
|
reparentTopicChildren: () => reparentTopicChildren,
|
|
@@ -7516,10 +7519,12 @@ __export(exports_api_topics, {
|
|
|
7516
7519
|
normalizeTopicSurface: () => normalizeTopicSurface,
|
|
7517
7520
|
normalizeTopicState: () => normalizeTopicState,
|
|
7518
7521
|
normalizeTopicKind: () => normalizeTopicKind,
|
|
7522
|
+
normalizeSurfaceScope: () => normalizeSurfaceScope,
|
|
7519
7523
|
normalizeAiMode: () => normalizeAiMode,
|
|
7520
7524
|
listTopics: () => listTopics,
|
|
7521
7525
|
listSubagentTellTargetIds: () => listSubagentTellTargetIds,
|
|
7522
7526
|
isTopicVisible: () => isTopicVisible,
|
|
7527
|
+
isSurfaceScopeRequired: () => isSurfaceScopeRequired,
|
|
7523
7528
|
inferTopicKind: () => inferTopicKind,
|
|
7524
7529
|
inferAiMode: () => inferAiMode,
|
|
7525
7530
|
grantSubagentTellTarget: () => grantSubagentTellTarget,
|
|
@@ -7533,6 +7538,7 @@ __export(exports_api_topics, {
|
|
|
7533
7538
|
findTopicTitleConflict: () => findTopicTitleConflict,
|
|
7534
7539
|
deleteTopic: () => deleteTopic,
|
|
7535
7540
|
defaultTopicSurface: () => defaultTopicSurface,
|
|
7541
|
+
defaultSurfaceScope: () => defaultSurfaceScope,
|
|
7536
7542
|
clearTopicSessionId: () => clearTopicSessionId,
|
|
7537
7543
|
aiMentionFromMode: () => aiMentionFromMode,
|
|
7538
7544
|
addParticipantToDB: () => addParticipantToDB
|
|
@@ -7543,6 +7549,26 @@ function defaultTopicSurface() {
|
|
|
7543
7549
|
function normalizeTopicSurface(value) {
|
|
7544
7550
|
return value === "telegram" || value === "otium" || value === "terminal" ? value : "terminal";
|
|
7545
7551
|
}
|
|
7552
|
+
function normalizeSurfaceScope(value) {
|
|
7553
|
+
if (typeof value !== "string")
|
|
7554
|
+
return null;
|
|
7555
|
+
const trimmed = value.trim();
|
|
7556
|
+
return trimmed ? trimmed : null;
|
|
7557
|
+
}
|
|
7558
|
+
function defaultSurfaceScope() {
|
|
7559
|
+
return activeSurfaceScope ?? normalizeSurfaceScope(process.env.NEGOTIUM_SURFACE_SCOPE);
|
|
7560
|
+
}
|
|
7561
|
+
function setDefaultSurfaceScope(scope) {
|
|
7562
|
+
const previous = activeSurfaceScope;
|
|
7563
|
+
activeSurfaceScope = normalizeSurfaceScope(scope);
|
|
7564
|
+
return previous;
|
|
7565
|
+
}
|
|
7566
|
+
function setSurfaceScopeRequired(required) {
|
|
7567
|
+
surfaceScopeRequired = required;
|
|
7568
|
+
}
|
|
7569
|
+
function isSurfaceScopeRequired() {
|
|
7570
|
+
return surfaceScopeRequired;
|
|
7571
|
+
}
|
|
7546
7572
|
function tableColumns2(table) {
|
|
7547
7573
|
const rows = db.query(`PRAGMA table_info(${table})`).all();
|
|
7548
7574
|
return new Set(rows.map((row) => row.name));
|
|
@@ -7674,6 +7700,7 @@ function initializeApiTopicsSchema() {
|
|
|
7674
7700
|
is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
|
|
7675
7701
|
visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
|
|
7676
7702
|
surface TEXT NOT NULL DEFAULT 'terminal' CHECK (surface IN ('terminal','telegram','otium')),
|
|
7703
|
+
surface_scope TEXT,
|
|
7677
7704
|
browser_profile TEXT NOT NULL DEFAULT 'default',
|
|
7678
7705
|
browser_profile_owner TEXT,
|
|
7679
7706
|
session_id TEXT,
|
|
@@ -7719,8 +7746,8 @@ function initializeApiTopicsSchema() {
|
|
|
7719
7746
|
const legacyBaseEffort = row.base_effort ?? row.default_effort;
|
|
7720
7747
|
db.query(`INSERT INTO api_topics_next
|
|
7721
7748
|
(id,title,kind,description,agent,base_model,base_effort,response_policy,
|
|
7722
|
-
created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,session_id)
|
|
7723
|
-
VALUES (
|
|
7749
|
+
created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,surface_scope,session_id)
|
|
7750
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(String(row.id), String(row.title), normalized.kind, typeof row.description === "string" ? row.description : null, normalized.agent ?? null, typeof legacyBaseModel === "string" ? legacyBaseModel : null, typeof legacyBaseEffort === "string" ? legacyBaseEffort : null, normalized.aiMode, String(row.created_at), typeof row.last_message_at === "string" ? row.last_message_at : null, typeof row.parent_topic_id === "string" ? row.parent_topic_id : null, typeof row.memory_topic_id === "string" ? row.memory_topic_id : null, typeof row.memory_key === "string" ? row.memory_key : null, Number(row.is_fork ?? 0) !== 0 ? 1 : 0, Number(row.is_subagent ?? 0) !== 0 ? 1 : 0, row.visibility === "hidden" ? "hidden" : "visible", row.surface === undefined ? defaultTopicSurface() : normalizeTopicSurface(row.surface), normalizeSurfaceScope(row.surface_scope), typeof row.session_id === "string" ? row.session_id : null);
|
|
7724
7751
|
}
|
|
7725
7752
|
db.exec("DROP TABLE IF EXISTS topic_members");
|
|
7726
7753
|
db.exec("DROP TABLE api_topics");
|
|
@@ -7777,6 +7804,9 @@ function initializeApiTopicsSchema() {
|
|
|
7777
7804
|
if (!tableColumns2("api_topics").has("surface")) {
|
|
7778
7805
|
db.exec("ALTER TABLE api_topics ADD COLUMN surface TEXT NOT NULL DEFAULT 'terminal'");
|
|
7779
7806
|
}
|
|
7807
|
+
if (!tableColumns2("api_topics").has("surface_scope")) {
|
|
7808
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN surface_scope TEXT");
|
|
7809
|
+
}
|
|
7780
7810
|
if (tableColumns2("api_topics").has("access_mode")) {
|
|
7781
7811
|
try {
|
|
7782
7812
|
db.exec("ALTER TABLE api_topics DROP COLUMN access_mode");
|
|
@@ -7812,6 +7842,30 @@ function initializeApiTopicsSchema() {
|
|
|
7812
7842
|
backfillTopicSurfaces();
|
|
7813
7843
|
db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_last_message ON api_topics(last_message_at DESC)");
|
|
7814
7844
|
db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_surface ON api_topics(surface)");
|
|
7845
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_api_topics_surface_scope ON api_topics(surface, surface_scope)");
|
|
7846
|
+
}
|
|
7847
|
+
function stampUnscopedOtiumTopics(scope) {
|
|
7848
|
+
const normalized = normalizeSurfaceScope(scope);
|
|
7849
|
+
if (!normalized)
|
|
7850
|
+
return 0;
|
|
7851
|
+
db.exec(`
|
|
7852
|
+
CREATE TABLE IF NOT EXISTS api_schema_migrations (
|
|
7853
|
+
key TEXT PRIMARY KEY,
|
|
7854
|
+
applied_at TEXT NOT NULL
|
|
7855
|
+
)
|
|
7856
|
+
`);
|
|
7857
|
+
const applied = db.query("SELECT key FROM api_schema_migrations WHERE key = ?").get(SURFACE_SCOPE_STAMP_MIGRATION);
|
|
7858
|
+
if (applied)
|
|
7859
|
+
return 0;
|
|
7860
|
+
let stamped = 0;
|
|
7861
|
+
db.transaction(() => {
|
|
7862
|
+
stamped = Number(db.query("UPDATE api_topics SET surface_scope = ? WHERE surface = 'otium' AND surface_scope IS NULL").run(normalized).changes ?? 0);
|
|
7863
|
+
db.query("INSERT INTO api_schema_migrations (key, applied_at) VALUES (?, ?)").run(SURFACE_SCOPE_STAMP_MIGRATION, new Date().toISOString());
|
|
7864
|
+
})();
|
|
7865
|
+
if (stamped > 0) {
|
|
7866
|
+
logger.info({ scope: normalized, stamped }, "api_topics: surface scope stamped");
|
|
7867
|
+
}
|
|
7868
|
+
return stamped;
|
|
7815
7869
|
}
|
|
7816
7870
|
function backfillTopicSurfaces() {
|
|
7817
7871
|
db.exec(`
|
|
@@ -7833,21 +7887,23 @@ function backfillTopicSurfaces() {
|
|
|
7833
7887
|
}
|
|
7834
7888
|
function renameSurfaceTitleCollisions() {
|
|
7835
7889
|
const rows = db.query("SELECT id, title, kind, surface FROM api_topics WHERE kind != 'manager' ORDER BY created_at ASC, rowid ASC").all();
|
|
7836
|
-
const
|
|
7890
|
+
const keyOf = (surface, kind, title) => [surface, kind, normalizedTitle(title)].join("\x00");
|
|
7891
|
+
const reserved = new Set(rows.map((row) => keyOf(row.surface, row.kind, row.title)));
|
|
7892
|
+
const used = new Set;
|
|
7837
7893
|
const update = db.query("UPDATE api_topics SET title = ? WHERE id = ?");
|
|
7838
7894
|
for (const row of rows) {
|
|
7839
|
-
const key = (title) =>
|
|
7840
|
-
if (!
|
|
7841
|
-
|
|
7895
|
+
const key = (title) => keyOf(row.surface, row.kind, title);
|
|
7896
|
+
if (!used.has(key(row.title))) {
|
|
7897
|
+
used.add(key(row.title));
|
|
7842
7898
|
continue;
|
|
7843
7899
|
}
|
|
7844
7900
|
let suffix = 2;
|
|
7845
7901
|
let candidate = `${row.title} (${suffix})`;
|
|
7846
|
-
while (
|
|
7902
|
+
while (reserved.has(key(candidate)) || used.has(key(candidate))) {
|
|
7847
7903
|
suffix += 1;
|
|
7848
7904
|
candidate = `${row.title} (${suffix})`;
|
|
7849
7905
|
}
|
|
7850
|
-
|
|
7906
|
+
used.add(key(candidate));
|
|
7851
7907
|
update.run(candidate, row.id);
|
|
7852
7908
|
logger.warn({ topicId: row.id, surface: row.surface, from: row.title, to: candidate }, "api_topics: renamed a duplicate title for surface-scoped uniqueness");
|
|
7853
7909
|
}
|
|
@@ -7910,7 +7966,8 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
|
|
|
7910
7966
|
subagentReportMode: r.subagent_report_mode === "tell" || r.subagent_report_mode === "status-only" ? r.subagent_report_mode : "auto"
|
|
7911
7967
|
} : {},
|
|
7912
7968
|
visibility: normalizeTopicVisibility(r.visibility),
|
|
7913
|
-
surface: normalizeTopicSurface(r.surface)
|
|
7969
|
+
surface: normalizeTopicSurface(r.surface),
|
|
7970
|
+
surfaceScope: normalizeSurfaceScope(r.surface_scope)
|
|
7914
7971
|
};
|
|
7915
7972
|
}
|
|
7916
7973
|
function normalizeTopicVisibility(value) {
|
|
@@ -7978,6 +8035,13 @@ function normalizeTopicState(input) {
|
|
|
7978
8035
|
function normalizedTitle(title) {
|
|
7979
8036
|
return title.trim().toLowerCase();
|
|
7980
8037
|
}
|
|
8038
|
+
function surfaceScopeForWrite(t) {
|
|
8039
|
+
if (normalizeTopicSurface(t.surface ?? defaultTopicSurface()) !== "otium")
|
|
8040
|
+
return null;
|
|
8041
|
+
if (t.surfaceScope !== undefined)
|
|
8042
|
+
return normalizeSurfaceScope(t.surfaceScope);
|
|
8043
|
+
return defaultSurfaceScope();
|
|
8044
|
+
}
|
|
7981
8045
|
function upsertTopic(t) {
|
|
7982
8046
|
const normalized = normalizeTopicState({
|
|
7983
8047
|
id: t.id,
|
|
@@ -7990,8 +8054,8 @@ function upsertTopic(t) {
|
|
|
7990
8054
|
db.query(`INSERT INTO api_topics
|
|
7991
8055
|
(id,title,kind,description,agent,base_model,base_effort,response_policy,
|
|
7992
8056
|
created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,
|
|
7993
|
-
subagent_report_mode)
|
|
7994
|
-
VALUES (
|
|
8057
|
+
surface_scope,subagent_report_mode)
|
|
8058
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
7995
8059
|
ON CONFLICT(id) DO UPDATE SET
|
|
7996
8060
|
title = excluded.title,
|
|
7997
8061
|
kind = excluded.kind,
|
|
@@ -8009,7 +8073,11 @@ function upsertTopic(t) {
|
|
|
8009
8073
|
is_subagent = excluded.is_subagent,
|
|
8010
8074
|
visibility = excluded.visibility,
|
|
8011
8075
|
surface = excluded.surface,
|
|
8012
|
-
|
|
8076
|
+
-- A room's workspace is fixed at creation (M-1). COALESCE, not
|
|
8077
|
+
-- assignment: an update may fill in a scope that was unknown when the
|
|
8078
|
+
-- room was created, but may never move a room to another workspace.
|
|
8079
|
+
surface_scope = COALESCE(api_topics.surface_scope, excluded.surface_scope),
|
|
8080
|
+
subagent_report_mode = excluded.subagent_report_mode`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.memoryKey ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicSurface(t.surface ?? defaultTopicSurface()), surfaceScopeForWrite(t), t.subagentReportMode ?? "auto");
|
|
8013
8081
|
db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
|
|
8014
8082
|
for (const participant of t.participants) {
|
|
8015
8083
|
db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
|
|
@@ -8023,7 +8091,8 @@ function upsertTopic(t) {
|
|
|
8023
8091
|
})();
|
|
8024
8092
|
}
|
|
8025
8093
|
function listTopics(opts = {}) {
|
|
8026
|
-
const
|
|
8094
|
+
const scoped = Object.hasOwn(opts, "surfaceScope");
|
|
8095
|
+
const rows = opts.surface ? db.query(`SELECT * FROM api_topics WHERE surface = ?${scoped ? " AND surface_scope IS ?" : ""} ORDER BY last_message_at DESC`).all(...scoped ? [normalizeTopicSurface(opts.surface), normalizeSurfaceScope(opts.surfaceScope)] : [normalizeTopicSurface(opts.surface)]) : db.query("SELECT * FROM api_topics ORDER BY last_message_at DESC").all();
|
|
8027
8096
|
const participants = getAllTopicParticipants();
|
|
8028
8097
|
const tellTargets = rows.some((row) => row.is_subagent !== 0) ? getAllSubagentTellTargets() : undefined;
|
|
8029
8098
|
return rows.map((row) => rowToDto2(row, participants.get(row.id) ?? [], tellTargets));
|
|
@@ -8032,15 +8101,24 @@ function getTopic(id) {
|
|
|
8032
8101
|
const r = db.query("SELECT * FROM api_topics WHERE id = ?").get(id);
|
|
8033
8102
|
return r ? rowToDto2(r) : null;
|
|
8034
8103
|
}
|
|
8035
|
-
function getManagerTopicForUser(userId) {
|
|
8036
|
-
const
|
|
8104
|
+
function getManagerTopicForUser(userId, surface, opts = {}) {
|
|
8105
|
+
const scoped = Object.hasOwn(opts, "surfaceScope");
|
|
8106
|
+
const sql = `SELECT t.* FROM api_topics t
|
|
8037
8107
|
JOIN topic_members m ON m.topic_id = t.id
|
|
8038
8108
|
WHERE t.kind = 'manager'
|
|
8039
8109
|
AND t.id != ?
|
|
8040
8110
|
AND m.user_id = ?
|
|
8041
8111
|
AND m.role = 'owner'
|
|
8112
|
+
${surface ? "AND t.surface = ?" : ""}
|
|
8113
|
+
${scoped ? "AND t.surface_scope IS ?" : ""}
|
|
8042
8114
|
ORDER BY t.created_at ASC
|
|
8043
|
-
LIMIT 1
|
|
8115
|
+
LIMIT 1`;
|
|
8116
|
+
const params = [GENERAL_TOPIC_ID, userId];
|
|
8117
|
+
if (surface)
|
|
8118
|
+
params.push(surface);
|
|
8119
|
+
if (scoped)
|
|
8120
|
+
params.push(normalizeSurfaceScope(opts.surfaceScope));
|
|
8121
|
+
const row = db.query(sql).get(...params);
|
|
8044
8122
|
return row ? rowToDto2(row) : null;
|
|
8045
8123
|
}
|
|
8046
8124
|
function getTopicMemoryOrigin(id) {
|
|
@@ -8075,6 +8153,7 @@ function getTopicByNameAndKind(title, kind) {
|
|
|
8075
8153
|
function findTopicTitleConflict(title, kind, opts = {}) {
|
|
8076
8154
|
const wanted = normalizedTitle(title);
|
|
8077
8155
|
const surface = normalizeTopicSurface(opts.surface ?? defaultTopicSurface());
|
|
8156
|
+
const surfaceScope = Object.hasOwn(opts, "surfaceScope") ? normalizeSurfaceScope(opts.surfaceScope) : surface === "otium" ? defaultSurfaceScope() : null;
|
|
8078
8157
|
const generalTitleRequested = wanted === normalizedTitle(GENERAL_TOPIC_ID);
|
|
8079
8158
|
if (generalTitleRequested && opts.excludeTopicId !== GENERAL_TOPIC_ID) {
|
|
8080
8159
|
const general = db.query("SELECT * FROM api_topics WHERE id = ?").get(GENERAL_TOPIC_ID);
|
|
@@ -8083,8 +8162,8 @@ function findTopicTitleConflict(title, kind, opts = {}) {
|
|
|
8083
8162
|
}
|
|
8084
8163
|
if (kind === "manager")
|
|
8085
8164
|
return null;
|
|
8086
|
-
const params = [wanted, surface, kind, GENERAL_TOPIC_ID];
|
|
8087
|
-
let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ? AND (kind = ? OR id = ?)";
|
|
8165
|
+
const params = [wanted, surface, surfaceScope, kind, GENERAL_TOPIC_ID];
|
|
8166
|
+
let sql = "SELECT * FROM api_topics WHERE LOWER(TRIM(title)) = ? AND surface = ? AND surface_scope IS ? AND (kind = ? OR id = ?)";
|
|
8088
8167
|
if (opts.excludeTopicId) {
|
|
8089
8168
|
sql += " AND id != ?";
|
|
8090
8169
|
params.push(opts.excludeTopicId);
|
|
@@ -8111,14 +8190,16 @@ function getTopicByNameForUser(title, userId, opts = {}) {
|
|
|
8111
8190
|
const qualified = /^(agent|channel|manager):(.+)$/i.exec(trimmed);
|
|
8112
8191
|
const requestedKind = qualified ? normalizeTopicKind(qualified[1]?.toLowerCase()) : null;
|
|
8113
8192
|
const requestedTitle = qualified ? qualified[2].trim() : trimmed;
|
|
8193
|
+
const scoped = Object.hasOwn(opts, "surfaceScope");
|
|
8114
8194
|
const rows = db.query(`SELECT t.* FROM api_topics t
|
|
8115
8195
|
WHERE LOWER(t.title) = LOWER(?)
|
|
8116
8196
|
AND t.id != ?
|
|
8117
8197
|
AND t.visibility != 'hidden'
|
|
8118
8198
|
AND (? IS NULL OR t.surface = ?)
|
|
8199
|
+
${scoped ? "AND t.surface_scope IS ?" : ""}
|
|
8119
8200
|
AND EXISTS (
|
|
8120
8201
|
SELECT 1 FROM topic_members m WHERE m.topic_id = t.id AND m.user_id = ?
|
|
8121
|
-
)`).all(requestedTitle, GENERAL_TOPIC_ID, opts.surface ?? null, opts.surface ?? null, userId);
|
|
8202
|
+
)`).all(requestedTitle, GENERAL_TOPIC_ID, opts.surface ?? null, opts.surface ?? null, ...scoped ? [normalizeSurfaceScope(opts.surfaceScope)] : [], userId);
|
|
8122
8203
|
const matches = requestedKind ? rows.filter((row) => row.kind === requestedKind) : rows;
|
|
8123
8204
|
return matches.length === 1 ? rowToDto2(matches[0]) : null;
|
|
8124
8205
|
}
|
|
@@ -8206,7 +8287,7 @@ function removeParticipantFromDB(topicId, userId) {
|
|
|
8206
8287
|
db.query("DELETE FROM topic_members WHERE topic_id = ? AND user_id = ?").run(topicId, userId);
|
|
8207
8288
|
return true;
|
|
8208
8289
|
}
|
|
8209
|
-
var DEFAULT_AGENT_ROOM_AGENT = "maestro", SURFACE_BACKFILL_MIGRATION = "api_topics_surface_backfill_20260808";
|
|
8290
|
+
var DEFAULT_AGENT_ROOM_AGENT = "maestro", activeSurfaceScope = null, surfaceScopeRequired = false, SURFACE_SCOPE_STAMP_MIGRATION = "api_topics_surface_scope_stamp_20260809", SURFACE_BACKFILL_MIGRATION = "api_topics_surface_backfill_20260808";
|
|
8210
8291
|
var init_api_topics = __esm(async () => {
|
|
8211
8292
|
init_constants();
|
|
8212
8293
|
init_logger();
|
|
@@ -11426,8 +11507,10 @@ var init_active_rooms = __esm(async () => {
|
|
|
11426
11507
|
|
|
11427
11508
|
// ../../packages/core/src/topics/personal-general.ts
|
|
11428
11509
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
11429
|
-
function ensurePersonalGeneral(userId) {
|
|
11430
|
-
const
|
|
11510
|
+
function ensurePersonalGeneral(userId, surface, opts = {}) {
|
|
11511
|
+
const scope = normalizeTopicSurface(surface ?? defaultTopicSurface());
|
|
11512
|
+
const surfaceScope = opts.surfaceScope !== undefined ? normalizeSurfaceScope(opts.surfaceScope) : scope === "otium" ? defaultSurfaceScope() : null;
|
|
11513
|
+
const existing = getManagerTopicForUser(userId, scope, { surfaceScope });
|
|
11431
11514
|
if (existing) {
|
|
11432
11515
|
if (existing.description === LEGACY_PERSONAL_GENERAL_DESCRIPTION) {
|
|
11433
11516
|
existing.description = PERSONAL_GENERAL_DESCRIPTION;
|
|
@@ -11451,6 +11534,8 @@ function ensurePersonalGeneral(userId) {
|
|
|
11451
11534
|
aiMode: "always",
|
|
11452
11535
|
aiMention: false,
|
|
11453
11536
|
participants: [{ userId, role: "owner" }],
|
|
11537
|
+
surface: scope,
|
|
11538
|
+
surfaceScope,
|
|
11454
11539
|
createdAt: now,
|
|
11455
11540
|
lastMessageAt: now
|
|
11456
11541
|
};
|
|
@@ -13438,7 +13523,8 @@ function mergeRuntimeUserTurnRequest(input) {
|
|
|
13438
13523
|
const now = Date.now();
|
|
13439
13524
|
return db.transaction(() => {
|
|
13440
13525
|
const rows = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? ORDER BY created_at ASC, rowid ASC").all(input.topicId);
|
|
13441
|
-
const
|
|
13526
|
+
const thread = input.execution.threadRootId;
|
|
13527
|
+
const previous = rows.map(rowToRequest).filter((request) => request.execution?.threadRootId === thread);
|
|
13442
13528
|
const omittedRequestIds = new Set([
|
|
13443
13529
|
...input.omitRequestIds ?? [],
|
|
13444
13530
|
...previous.filter((request) => Boolean(request.execution?.providerSessionId)).map((request) => request.requestId)
|
|
@@ -13474,7 +13560,11 @@ function mergeRuntimeUserTurnRequest(input) {
|
|
|
13474
13560
|
execution.sessionIdSpecified = true;
|
|
13475
13561
|
}
|
|
13476
13562
|
const attachments = flattenUserTurnAttachments(userMessages);
|
|
13477
|
-
|
|
13563
|
+
const absorbed = previous.map((request) => request.requestId);
|
|
13564
|
+
if (absorbed.length > 0) {
|
|
13565
|
+
db.query(`DELETE FROM runtime_user_turn_requests
|
|
13566
|
+
WHERE topic_id = ? AND request_id IN (${absorbed.map(() => "?").join(",")})`).run(input.topicId, ...absorbed);
|
|
13567
|
+
}
|
|
13478
13568
|
db.query(`INSERT INTO runtime_user_turn_requests
|
|
13479
13569
|
(request_id, topic_id, user_id, prompt, user_messages_json, attachments_json,
|
|
13480
13570
|
allow_auto_continue, execution_json, topic_epoch, created_at,
|
|
@@ -14321,8 +14411,8 @@ function updateTopic(topicId, patch) {
|
|
|
14321
14411
|
function isParticipant(topic, userId) {
|
|
14322
14412
|
return topic.participants.some((p) => p.userId === userId);
|
|
14323
14413
|
}
|
|
14324
|
-
function nextDerivedTopicTitle(sourceTitle, kind, suffix, surface) {
|
|
14325
|
-
const visibleTitles = new Set(listTopics(surface ? { surface } : {}).filter((topic) => topic.kind === kind).map((topic) => topic.title.toLowerCase()));
|
|
14414
|
+
function nextDerivedTopicTitle(sourceTitle, kind, suffix, surface, surfaceScope) {
|
|
14415
|
+
const visibleTitles = new Set(listTopics(surface ? { surface, surfaceScope: surfaceScope ?? null } : {}).filter((topic) => topic.kind === kind).map((topic) => topic.title.toLowerCase()));
|
|
14326
14416
|
let n = 1;
|
|
14327
14417
|
let title = `${sourceTitle}-${suffix}-${n}`;
|
|
14328
14418
|
while (visibleTitles.has(title.toLowerCase())) {
|
|
@@ -14387,8 +14477,9 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
|
|
|
14387
14477
|
] : [{ userId, role: "owner" }];
|
|
14388
14478
|
const kind = topic.kind ?? inferTopicKind(topic);
|
|
14389
14479
|
const surface = topic.surface ?? defaultTopicSurface();
|
|
14390
|
-
const
|
|
14391
|
-
const
|
|
14480
|
+
const surfaceScope = topic.surfaceScope ?? null;
|
|
14481
|
+
const title = opts?.name?.trim() || nextDerivedTopicTitle(topic.title, kind, suffix, surface, surfaceScope);
|
|
14482
|
+
const conflict = findTopicTitleConflict(title, kind, { surface, surfaceScope });
|
|
14392
14483
|
if (conflict) {
|
|
14393
14484
|
logger.info({ sourceTopicId, title, kind, conflictTopicId: conflict.id }, "createDerivedTopic: title conflict");
|
|
14394
14485
|
throw new TopicTitleConflictError(title);
|
|
@@ -14411,7 +14502,8 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
|
|
|
14411
14502
|
isFork: copyHistory,
|
|
14412
14503
|
...subagent ? { isSubagent: true } : {},
|
|
14413
14504
|
visibility: topic.visibility,
|
|
14414
|
-
surface
|
|
14505
|
+
surface,
|
|
14506
|
+
surfaceScope
|
|
14415
14507
|
};
|
|
14416
14508
|
let sessionId;
|
|
14417
14509
|
let rollbackHandle;
|
|
@@ -14519,7 +14611,10 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
|
|
|
14519
14611
|
if (!currentSource || currentSource.kind === "manager" || !isParticipant(currentSource, userId) || subagent && isRuntimeTopicMaintenance(sourceTopicId)) {
|
|
14520
14612
|
throw new TopicDeriveBusyError("Source topic changed while deriving; try again");
|
|
14521
14613
|
}
|
|
14522
|
-
const transactionalConflict = findTopicTitleConflict(title, kind, {
|
|
14614
|
+
const transactionalConflict = findTopicTitleConflict(title, kind, {
|
|
14615
|
+
surface,
|
|
14616
|
+
surfaceScope
|
|
14617
|
+
});
|
|
14523
14618
|
if (transactionalConflict)
|
|
14524
14619
|
throw new TopicTitleConflictError(title);
|
|
14525
14620
|
upsertTopic(derived);
|
|
@@ -15389,13 +15484,14 @@ async function forwardToPeer(args) {
|
|
|
15389
15484
|
error: forwarded.configured ? "remote session bridge is configured but unavailable" : "remote nodes are not connected on this negotium node (standalone mode)"
|
|
15390
15485
|
};
|
|
15391
15486
|
}
|
|
15392
|
-
async function peerSessionsForUser(userId, sourceQueryId) {
|
|
15487
|
+
async function peerSessionsForUser(userId, sourceQueryId, fromTopicId) {
|
|
15393
15488
|
if (activeBridge)
|
|
15394
|
-
return activeBridge.sessions(userId, sourceQueryId);
|
|
15489
|
+
return activeBridge.sessions(userId, sourceQueryId, fromTopicId);
|
|
15395
15490
|
const sessions = await callLoopbackBridge({
|
|
15396
15491
|
action: "sessions",
|
|
15397
15492
|
userId,
|
|
15398
|
-
sourceQueryId
|
|
15493
|
+
sourceQueryId,
|
|
15494
|
+
fromTopicId
|
|
15399
15495
|
});
|
|
15400
15496
|
if (sessions.result)
|
|
15401
15497
|
return sessions.result;
|
|
@@ -16151,6 +16247,226 @@ body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--
|
|
|
16151
16247
|
</style>`;
|
|
16152
16248
|
});
|
|
16153
16249
|
|
|
16250
|
+
// ../../packages/core/src/storage/token-stats.ts
|
|
16251
|
+
var exports_token_stats = {};
|
|
16252
|
+
__export(exports_token_stats, {
|
|
16253
|
+
tokenStatsFileId: () => tokenStatsFileId,
|
|
16254
|
+
recordUsage: () => recordUsage,
|
|
16255
|
+
getTopicStats: () => getTopicStats,
|
|
16256
|
+
getStats: () => getStats,
|
|
16257
|
+
deleteTopicStats: () => deleteTopicStats,
|
|
16258
|
+
calcCost: () => calcCost
|
|
16259
|
+
});
|
|
16260
|
+
import { createHash as createHash7 } from "crypto";
|
|
16261
|
+
import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync14 } from "fs";
|
|
16262
|
+
import { join as join25 } from "path";
|
|
16263
|
+
function emptyBucket() {
|
|
16264
|
+
return {
|
|
16265
|
+
inputTokens: 0,
|
|
16266
|
+
outputTokens: 0,
|
|
16267
|
+
cacheCreationInputTokens: 0,
|
|
16268
|
+
cacheReadInputTokens: 0,
|
|
16269
|
+
queries: 0,
|
|
16270
|
+
estimatedCostUsd: 0
|
|
16271
|
+
};
|
|
16272
|
+
}
|
|
16273
|
+
function tokenStatsFileId(userId) {
|
|
16274
|
+
const rawUserId = String(userId);
|
|
16275
|
+
return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
|
|
16276
|
+
}
|
|
16277
|
+
function queriesPath(userId) {
|
|
16278
|
+
const fileId = tokenStatsFileId(userId);
|
|
16279
|
+
const logDir = resolveStorageLogDir();
|
|
16280
|
+
mkdirSync16(logDir, { recursive: true });
|
|
16281
|
+
return join25(logDir, `token-queries-${fileId}.jsonl`);
|
|
16282
|
+
}
|
|
16283
|
+
function loadRecords(userId) {
|
|
16284
|
+
try {
|
|
16285
|
+
return readJsonlLines(queriesPath(userId)).flatMap((line) => {
|
|
16286
|
+
try {
|
|
16287
|
+
return [JSON.parse(line)];
|
|
16288
|
+
} catch {
|
|
16289
|
+
return [];
|
|
16290
|
+
}
|
|
16291
|
+
});
|
|
16292
|
+
} catch {
|
|
16293
|
+
return [];
|
|
16294
|
+
}
|
|
16295
|
+
}
|
|
16296
|
+
function calcCost(b) {
|
|
16297
|
+
return b.estimatedCostUsd;
|
|
16298
|
+
}
|
|
16299
|
+
function estimateUsageCost(agent, model, usage) {
|
|
16300
|
+
const prices = TOKEN_PRICES[`${agent}:${model}`];
|
|
16301
|
+
if (!prices)
|
|
16302
|
+
return 0;
|
|
16303
|
+
return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
|
|
16304
|
+
}
|
|
16305
|
+
function isQueryRecord(value) {
|
|
16306
|
+
if (!value || typeof value !== "object")
|
|
16307
|
+
return false;
|
|
16308
|
+
const record = value;
|
|
16309
|
+
return record.schemaVersion === 2 && typeof record.timestamp === "string" && typeof record.session === "string" && typeof record.topicId === "string" && typeof record.agent === "string" && typeof record.model === "string" && typeof record.inputTokens === "number" && typeof record.outputTokens === "number" && typeof record.cacheCreationInputTokens === "number" && typeof record.cacheReadInputTokens === "number" && typeof record.estimatedCostUsd === "number";
|
|
16310
|
+
}
|
|
16311
|
+
function recordUsage(userId, session, usage, context) {
|
|
16312
|
+
const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
|
|
16313
|
+
const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
16314
|
+
const normalized = {
|
|
16315
|
+
inputTokens,
|
|
16316
|
+
outputTokens: usage.outputTokens,
|
|
16317
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
|
|
16318
|
+
cacheReadInputTokens
|
|
16319
|
+
};
|
|
16320
|
+
const record = {
|
|
16321
|
+
schemaVersion: 2,
|
|
16322
|
+
timestamp: new Date().toISOString(),
|
|
16323
|
+
session,
|
|
16324
|
+
topicId: context.topicId,
|
|
16325
|
+
...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
|
|
16326
|
+
agent: context.agent,
|
|
16327
|
+
model: context.model,
|
|
16328
|
+
...normalized,
|
|
16329
|
+
...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
|
|
16330
|
+
...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
|
|
16331
|
+
estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
|
|
16332
|
+
};
|
|
16333
|
+
try {
|
|
16334
|
+
appendJsonlEntry(queriesPath(userId), record);
|
|
16335
|
+
} catch (e) {
|
|
16336
|
+
logger.warn({ err: e, userId }, "token-stats: Failed to record");
|
|
16337
|
+
}
|
|
16338
|
+
}
|
|
16339
|
+
function deleteTopicStats(userId, topicId) {
|
|
16340
|
+
const path = queriesPath(userId);
|
|
16341
|
+
try {
|
|
16342
|
+
const kept = readJsonlLines(path).filter((line) => {
|
|
16343
|
+
try {
|
|
16344
|
+
const record = JSON.parse(line);
|
|
16345
|
+
return record.topicId !== topicId;
|
|
16346
|
+
} catch {
|
|
16347
|
+
return true;
|
|
16348
|
+
}
|
|
16349
|
+
});
|
|
16350
|
+
writeFileSync14(path, kept.length > 0 ? `${kept.join(`
|
|
16351
|
+
`)}
|
|
16352
|
+
` : "", "utf-8");
|
|
16353
|
+
} catch (e) {
|
|
16354
|
+
if (e.code === "ENOENT")
|
|
16355
|
+
return;
|
|
16356
|
+
logger.warn({ err: e, userId, topicId }, "token-stats: Failed to delete topic stats");
|
|
16357
|
+
}
|
|
16358
|
+
}
|
|
16359
|
+
function getStats(userId, from, to) {
|
|
16360
|
+
const records = loadRecords(userId);
|
|
16361
|
+
const fromTs = from ? new Date(from).getTime() : 0;
|
|
16362
|
+
const toTs = to ? new Date(to).getTime() : Infinity;
|
|
16363
|
+
if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
|
|
16364
|
+
logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
|
|
16365
|
+
return {
|
|
16366
|
+
total: emptyBucket(),
|
|
16367
|
+
byHour: {},
|
|
16368
|
+
bySession: {},
|
|
16369
|
+
currentSessions: [],
|
|
16370
|
+
ignoredLegacyRecords: 0,
|
|
16371
|
+
estimatedCostUsd: 0
|
|
16372
|
+
};
|
|
16373
|
+
}
|
|
16374
|
+
const total = emptyBucket();
|
|
16375
|
+
const byHour = {};
|
|
16376
|
+
const bySession = {};
|
|
16377
|
+
const currentSessions = new Map;
|
|
16378
|
+
let ignoredLegacyRecords = 0;
|
|
16379
|
+
for (const raw of records) {
|
|
16380
|
+
if (!isQueryRecord(raw)) {
|
|
16381
|
+
ignoredLegacyRecords += 1;
|
|
16382
|
+
continue;
|
|
16383
|
+
}
|
|
16384
|
+
const r = raw;
|
|
16385
|
+
const ts = new Date(r.timestamp).getTime();
|
|
16386
|
+
if (ts < fromTs || ts > toTs)
|
|
16387
|
+
continue;
|
|
16388
|
+
const hourKey = r.timestamp.slice(0, 13);
|
|
16389
|
+
if (!byHour[hourKey])
|
|
16390
|
+
byHour[hourKey] = emptyBucket();
|
|
16391
|
+
if (!bySession[r.session])
|
|
16392
|
+
bySession[r.session] = emptyBucket();
|
|
16393
|
+
for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
|
|
16394
|
+
bucket.inputTokens += r.inputTokens;
|
|
16395
|
+
bucket.outputTokens += r.outputTokens;
|
|
16396
|
+
bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
|
|
16397
|
+
bucket.cacheReadInputTokens += r.cacheReadInputTokens;
|
|
16398
|
+
bucket.queries += 1;
|
|
16399
|
+
bucket.estimatedCostUsd += r.estimatedCostUsd;
|
|
16400
|
+
}
|
|
16401
|
+
if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
|
|
16402
|
+
currentSessions.set(r.topicId, {
|
|
16403
|
+
timestamp: r.timestamp,
|
|
16404
|
+
topicId: r.topicId,
|
|
16405
|
+
topicTitle: r.session,
|
|
16406
|
+
...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
|
|
16407
|
+
agent: r.agent,
|
|
16408
|
+
model: r.model,
|
|
16409
|
+
contextTokens: r.contextTokens,
|
|
16410
|
+
contextWindow: r.contextWindow
|
|
16411
|
+
});
|
|
16412
|
+
}
|
|
16413
|
+
}
|
|
16414
|
+
return {
|
|
16415
|
+
total,
|
|
16416
|
+
byHour,
|
|
16417
|
+
bySession,
|
|
16418
|
+
currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
|
|
16419
|
+
ignoredLegacyRecords,
|
|
16420
|
+
estimatedCostUsd: calcCost(total)
|
|
16421
|
+
};
|
|
16422
|
+
}
|
|
16423
|
+
function getTopicStats(userId, topicId, activeProviderSessionId = getTopicSessionId(topicId) ?? undefined) {
|
|
16424
|
+
const total = emptyBucket();
|
|
16425
|
+
let currentSession;
|
|
16426
|
+
for (const raw of loadRecords(userId)) {
|
|
16427
|
+
if (!isQueryRecord(raw) || raw.topicId !== topicId)
|
|
16428
|
+
continue;
|
|
16429
|
+
total.inputTokens += raw.inputTokens;
|
|
16430
|
+
total.outputTokens += raw.outputTokens;
|
|
16431
|
+
total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
|
|
16432
|
+
total.cacheReadInputTokens += raw.cacheReadInputTokens;
|
|
16433
|
+
total.queries += 1;
|
|
16434
|
+
total.estimatedCostUsd += raw.estimatedCostUsd;
|
|
16435
|
+
if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && activeProviderSessionId !== undefined && raw.providerSessionId === activeProviderSessionId && (!currentSession || raw.timestamp > currentSession.timestamp)) {
|
|
16436
|
+
currentSession = {
|
|
16437
|
+
timestamp: raw.timestamp,
|
|
16438
|
+
topicId: raw.topicId,
|
|
16439
|
+
topicTitle: raw.session,
|
|
16440
|
+
...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
|
|
16441
|
+
agent: raw.agent,
|
|
16442
|
+
model: raw.model,
|
|
16443
|
+
contextTokens: raw.contextTokens,
|
|
16444
|
+
contextWindow: raw.contextWindow
|
|
16445
|
+
};
|
|
16446
|
+
}
|
|
16447
|
+
}
|
|
16448
|
+
return { topicId, ...total, ...currentSession ? { currentSession } : {} };
|
|
16449
|
+
}
|
|
16450
|
+
var TOKEN_PRICES;
|
|
16451
|
+
var init_token_stats = __esm(async () => {
|
|
16452
|
+
init_jsonl();
|
|
16453
|
+
init_logger();
|
|
16454
|
+
await init_api_topics();
|
|
16455
|
+
await init_storage_host();
|
|
16456
|
+
TOKEN_PRICES = {
|
|
16457
|
+
"codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
|
|
16458
|
+
"codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
|
|
16459
|
+
"codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
|
|
16460
|
+
"claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
|
|
16461
|
+
"claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
|
|
16462
|
+
"claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
|
|
16463
|
+
"maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
|
|
16464
|
+
"maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
|
|
16465
|
+
"maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
|
|
16466
|
+
"maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
|
|
16467
|
+
};
|
|
16468
|
+
});
|
|
16469
|
+
|
|
16154
16470
|
// ../../packages/core/src/topics/lifecycle.ts
|
|
16155
16471
|
var exports_lifecycle = {};
|
|
16156
16472
|
__export(exports_lifecycle, {
|
|
@@ -16200,6 +16516,7 @@ async function cleanupParticipantResources(topic, userIds, sessionId, cwd, purge
|
|
|
16200
16516
|
cleanupSessionInboxFiles(participantUserId, topic.id, topic.title);
|
|
16201
16517
|
clearQueryState(participantUserId, topic.id, topic.title);
|
|
16202
16518
|
clearQueryUsageAlert(participantUserId, topic.id);
|
|
16519
|
+
deleteTopicStats(participantUserId, topic.id);
|
|
16203
16520
|
deletePendingAsksForTopic({ userId: participantUserId, topicName: topic.title });
|
|
16204
16521
|
}
|
|
16205
16522
|
return true;
|
|
@@ -16354,6 +16671,7 @@ var init_lifecycle = __esm(async () => {
|
|
|
16354
16671
|
await init_runtime_turn_requests();
|
|
16355
16672
|
await init_self_schedules();
|
|
16356
16673
|
await init_session_asks();
|
|
16674
|
+
await init_token_stats();
|
|
16357
16675
|
await init_topic_archive();
|
|
16358
16676
|
await init_topic_archive_state();
|
|
16359
16677
|
TopicArchiveRequiredError = class TopicArchiveRequiredError extends Error {
|
|
@@ -16388,8 +16706,8 @@ var init_lifecycle = __esm(async () => {
|
|
|
16388
16706
|
|
|
16389
16707
|
// ../../packages/core/src/runtime/attachments.ts
|
|
16390
16708
|
import { randomUUID as randomUUID14 } from "crypto";
|
|
16391
|
-
import { copyFileSync as copyFileSync2, mkdirSync as
|
|
16392
|
-
import { basename as basename5, join as
|
|
16709
|
+
import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
|
|
16710
|
+
import { basename as basename5, join as join26 } from "path";
|
|
16393
16711
|
function workspaceCwdFor(topicId) {
|
|
16394
16712
|
return resolveTopicWorkspaceDir(topicId);
|
|
16395
16713
|
}
|
|
@@ -16402,7 +16720,7 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
|
|
|
16402
16720
|
if (!attachmentIds?.length)
|
|
16403
16721
|
return [];
|
|
16404
16722
|
const out = [];
|
|
16405
|
-
const destDir =
|
|
16723
|
+
const destDir = join26(workspaceCwdFor(topicId), "attachments", queryId);
|
|
16406
16724
|
for (const rawId of attachmentIds) {
|
|
16407
16725
|
if (typeof rawId !== "string")
|
|
16408
16726
|
continue;
|
|
@@ -16416,10 +16734,10 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
|
|
|
16416
16734
|
continue;
|
|
16417
16735
|
}
|
|
16418
16736
|
try {
|
|
16419
|
-
|
|
16737
|
+
mkdirSync17(destDir, { recursive: true });
|
|
16420
16738
|
const index = String(out.length + 1).padStart(2, "0");
|
|
16421
16739
|
const safeName = safeAttachmentFilename(attachment.filename, fileId);
|
|
16422
|
-
const destPath =
|
|
16740
|
+
const destPath = join26(destDir, `${index}-${fileId.slice(0, 8)}-${safeName}`);
|
|
16423
16741
|
copyFileSync2(sourcePath, destPath);
|
|
16424
16742
|
out.push({
|
|
16425
16743
|
id: attachment.id,
|
|
@@ -16449,14 +16767,14 @@ function promptWithAttachments(prompt, attachments) {
|
|
|
16449
16767
|
return composeAttachmentPrompt(prompt, attachments.map(({ filename, path }) => attachmentPromptLine(filename, path)));
|
|
16450
16768
|
}
|
|
16451
16769
|
function ingestAttachment(args) {
|
|
16452
|
-
const destDir =
|
|
16453
|
-
|
|
16770
|
+
const destDir = join26(UPLOADS_DIR, args.topicId);
|
|
16771
|
+
mkdirSync17(destDir, { recursive: true });
|
|
16454
16772
|
const safeName = safeAttachmentFilename(args.filename, "upload");
|
|
16455
|
-
const destPath =
|
|
16773
|
+
const destPath = join26(destDir, `${Date.now()}-${randomUUID14().slice(0, 8)}-${safeName}`);
|
|
16456
16774
|
if (args.sourcePath !== undefined) {
|
|
16457
16775
|
copyFileSync2(args.sourcePath, destPath);
|
|
16458
16776
|
} else if (args.bytes !== undefined) {
|
|
16459
|
-
|
|
16777
|
+
writeFileSync15(destPath, args.bytes);
|
|
16460
16778
|
} else {
|
|
16461
16779
|
throw new Error("ingestAttachment: provide sourcePath or bytes");
|
|
16462
16780
|
}
|
|
@@ -17052,204 +17370,6 @@ var init_visuals = __esm(async () => {
|
|
|
17052
17370
|
init_visual_html();
|
|
17053
17371
|
});
|
|
17054
17372
|
|
|
17055
|
-
// ../../packages/core/src/storage/token-stats.ts
|
|
17056
|
-
var exports_token_stats = {};
|
|
17057
|
-
__export(exports_token_stats, {
|
|
17058
|
-
tokenStatsFileId: () => tokenStatsFileId,
|
|
17059
|
-
recordUsage: () => recordUsage,
|
|
17060
|
-
getTopicStats: () => getTopicStats,
|
|
17061
|
-
getStats: () => getStats,
|
|
17062
|
-
calcCost: () => calcCost
|
|
17063
|
-
});
|
|
17064
|
-
import { createHash as createHash7 } from "crypto";
|
|
17065
|
-
import { mkdirSync as mkdirSync17 } from "fs";
|
|
17066
|
-
import { join as join26 } from "path";
|
|
17067
|
-
function emptyBucket() {
|
|
17068
|
-
return {
|
|
17069
|
-
inputTokens: 0,
|
|
17070
|
-
outputTokens: 0,
|
|
17071
|
-
cacheCreationInputTokens: 0,
|
|
17072
|
-
cacheReadInputTokens: 0,
|
|
17073
|
-
queries: 0,
|
|
17074
|
-
estimatedCostUsd: 0
|
|
17075
|
-
};
|
|
17076
|
-
}
|
|
17077
|
-
function tokenStatsFileId(userId) {
|
|
17078
|
-
const rawUserId = String(userId);
|
|
17079
|
-
return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
|
|
17080
|
-
}
|
|
17081
|
-
function queriesPath(userId) {
|
|
17082
|
-
const fileId = tokenStatsFileId(userId);
|
|
17083
|
-
const logDir = resolveStorageLogDir();
|
|
17084
|
-
mkdirSync17(logDir, { recursive: true });
|
|
17085
|
-
return join26(logDir, `token-queries-${fileId}.jsonl`);
|
|
17086
|
-
}
|
|
17087
|
-
function loadRecords(userId) {
|
|
17088
|
-
try {
|
|
17089
|
-
return readJsonlLines(queriesPath(userId)).flatMap((line) => {
|
|
17090
|
-
try {
|
|
17091
|
-
return [JSON.parse(line)];
|
|
17092
|
-
} catch {
|
|
17093
|
-
return [];
|
|
17094
|
-
}
|
|
17095
|
-
});
|
|
17096
|
-
} catch {
|
|
17097
|
-
return [];
|
|
17098
|
-
}
|
|
17099
|
-
}
|
|
17100
|
-
function calcCost(b) {
|
|
17101
|
-
return b.estimatedCostUsd;
|
|
17102
|
-
}
|
|
17103
|
-
function estimateUsageCost(agent, model, usage) {
|
|
17104
|
-
const prices = TOKEN_PRICES[`${agent}:${model}`];
|
|
17105
|
-
if (!prices)
|
|
17106
|
-
return 0;
|
|
17107
|
-
return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
|
|
17108
|
-
}
|
|
17109
|
-
function isQueryRecord(value) {
|
|
17110
|
-
if (!value || typeof value !== "object")
|
|
17111
|
-
return false;
|
|
17112
|
-
const record = value;
|
|
17113
|
-
return record.schemaVersion === 2 && typeof record.timestamp === "string" && typeof record.session === "string" && typeof record.topicId === "string" && typeof record.agent === "string" && typeof record.model === "string" && typeof record.inputTokens === "number" && typeof record.outputTokens === "number" && typeof record.cacheCreationInputTokens === "number" && typeof record.cacheReadInputTokens === "number" && typeof record.estimatedCostUsd === "number";
|
|
17114
|
-
}
|
|
17115
|
-
function recordUsage(userId, session, usage, context) {
|
|
17116
|
-
const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
|
|
17117
|
-
const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
17118
|
-
const normalized = {
|
|
17119
|
-
inputTokens,
|
|
17120
|
-
outputTokens: usage.outputTokens,
|
|
17121
|
-
cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
|
|
17122
|
-
cacheReadInputTokens
|
|
17123
|
-
};
|
|
17124
|
-
const record = {
|
|
17125
|
-
schemaVersion: 2,
|
|
17126
|
-
timestamp: new Date().toISOString(),
|
|
17127
|
-
session,
|
|
17128
|
-
topicId: context.topicId,
|
|
17129
|
-
...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
|
|
17130
|
-
agent: context.agent,
|
|
17131
|
-
model: context.model,
|
|
17132
|
-
...normalized,
|
|
17133
|
-
...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
|
|
17134
|
-
...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
|
|
17135
|
-
estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
|
|
17136
|
-
};
|
|
17137
|
-
try {
|
|
17138
|
-
appendJsonlEntry(queriesPath(userId), record);
|
|
17139
|
-
} catch (e) {
|
|
17140
|
-
logger.warn({ err: e, userId }, "token-stats: Failed to record");
|
|
17141
|
-
}
|
|
17142
|
-
}
|
|
17143
|
-
function getStats(userId, from, to) {
|
|
17144
|
-
const records = loadRecords(userId);
|
|
17145
|
-
const fromTs = from ? new Date(from).getTime() : 0;
|
|
17146
|
-
const toTs = to ? new Date(to).getTime() : Infinity;
|
|
17147
|
-
if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
|
|
17148
|
-
logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
|
|
17149
|
-
return {
|
|
17150
|
-
total: emptyBucket(),
|
|
17151
|
-
byHour: {},
|
|
17152
|
-
bySession: {},
|
|
17153
|
-
currentSessions: [],
|
|
17154
|
-
ignoredLegacyRecords: 0,
|
|
17155
|
-
estimatedCostUsd: 0
|
|
17156
|
-
};
|
|
17157
|
-
}
|
|
17158
|
-
const total = emptyBucket();
|
|
17159
|
-
const byHour = {};
|
|
17160
|
-
const bySession = {};
|
|
17161
|
-
const currentSessions = new Map;
|
|
17162
|
-
let ignoredLegacyRecords = 0;
|
|
17163
|
-
for (const raw of records) {
|
|
17164
|
-
if (!isQueryRecord(raw)) {
|
|
17165
|
-
ignoredLegacyRecords += 1;
|
|
17166
|
-
continue;
|
|
17167
|
-
}
|
|
17168
|
-
const r = raw;
|
|
17169
|
-
const ts = new Date(r.timestamp).getTime();
|
|
17170
|
-
if (ts < fromTs || ts > toTs)
|
|
17171
|
-
continue;
|
|
17172
|
-
const hourKey = r.timestamp.slice(0, 13);
|
|
17173
|
-
if (!byHour[hourKey])
|
|
17174
|
-
byHour[hourKey] = emptyBucket();
|
|
17175
|
-
if (!bySession[r.session])
|
|
17176
|
-
bySession[r.session] = emptyBucket();
|
|
17177
|
-
for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
|
|
17178
|
-
bucket.inputTokens += r.inputTokens;
|
|
17179
|
-
bucket.outputTokens += r.outputTokens;
|
|
17180
|
-
bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
|
|
17181
|
-
bucket.cacheReadInputTokens += r.cacheReadInputTokens;
|
|
17182
|
-
bucket.queries += 1;
|
|
17183
|
-
bucket.estimatedCostUsd += r.estimatedCostUsd;
|
|
17184
|
-
}
|
|
17185
|
-
if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
|
|
17186
|
-
currentSessions.set(r.topicId, {
|
|
17187
|
-
timestamp: r.timestamp,
|
|
17188
|
-
topicId: r.topicId,
|
|
17189
|
-
topicTitle: r.session,
|
|
17190
|
-
...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
|
|
17191
|
-
agent: r.agent,
|
|
17192
|
-
model: r.model,
|
|
17193
|
-
contextTokens: r.contextTokens,
|
|
17194
|
-
contextWindow: r.contextWindow
|
|
17195
|
-
});
|
|
17196
|
-
}
|
|
17197
|
-
}
|
|
17198
|
-
return {
|
|
17199
|
-
total,
|
|
17200
|
-
byHour,
|
|
17201
|
-
bySession,
|
|
17202
|
-
currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
|
|
17203
|
-
ignoredLegacyRecords,
|
|
17204
|
-
estimatedCostUsd: calcCost(total)
|
|
17205
|
-
};
|
|
17206
|
-
}
|
|
17207
|
-
function getTopicStats(userId, topicId) {
|
|
17208
|
-
const total = emptyBucket();
|
|
17209
|
-
let currentSession;
|
|
17210
|
-
for (const raw of loadRecords(userId)) {
|
|
17211
|
-
if (!isQueryRecord(raw) || raw.topicId !== topicId)
|
|
17212
|
-
continue;
|
|
17213
|
-
total.inputTokens += raw.inputTokens;
|
|
17214
|
-
total.outputTokens += raw.outputTokens;
|
|
17215
|
-
total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
|
|
17216
|
-
total.cacheReadInputTokens += raw.cacheReadInputTokens;
|
|
17217
|
-
total.queries += 1;
|
|
17218
|
-
total.estimatedCostUsd += raw.estimatedCostUsd;
|
|
17219
|
-
if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && (!currentSession || raw.timestamp > currentSession.timestamp)) {
|
|
17220
|
-
currentSession = {
|
|
17221
|
-
timestamp: raw.timestamp,
|
|
17222
|
-
topicId: raw.topicId,
|
|
17223
|
-
topicTitle: raw.session,
|
|
17224
|
-
...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
|
|
17225
|
-
agent: raw.agent,
|
|
17226
|
-
model: raw.model,
|
|
17227
|
-
contextTokens: raw.contextTokens,
|
|
17228
|
-
contextWindow: raw.contextWindow
|
|
17229
|
-
};
|
|
17230
|
-
}
|
|
17231
|
-
}
|
|
17232
|
-
return { topicId, ...total, ...currentSession ? { currentSession } : {} };
|
|
17233
|
-
}
|
|
17234
|
-
var TOKEN_PRICES;
|
|
17235
|
-
var init_token_stats = __esm(async () => {
|
|
17236
|
-
init_jsonl();
|
|
17237
|
-
init_logger();
|
|
17238
|
-
await init_storage_host();
|
|
17239
|
-
TOKEN_PRICES = {
|
|
17240
|
-
"codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
|
|
17241
|
-
"codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
|
|
17242
|
-
"codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
|
|
17243
|
-
"claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
|
|
17244
|
-
"claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
|
|
17245
|
-
"claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
|
|
17246
|
-
"maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
|
|
17247
|
-
"maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
|
|
17248
|
-
"maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
|
|
17249
|
-
"maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
|
|
17250
|
-
};
|
|
17251
|
-
});
|
|
17252
|
-
|
|
17253
17373
|
// ../../packages/core/src/runtime/turn-event-stream.ts
|
|
17254
17374
|
import { randomUUID as randomUUID15 } from "crypto";
|
|
17255
17375
|
import { realpathSync as realpathSync5, statSync as statSync9 } from "fs";
|
|
@@ -17365,10 +17485,11 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
|
|
|
17365
17485
|
agentType,
|
|
17366
17486
|
model,
|
|
17367
17487
|
sourceNode: execution?.sourceNode,
|
|
17488
|
+
...execution?.threadRootId ? { threadRootId: execution.threadRootId } : {},
|
|
17368
17489
|
usage,
|
|
17369
17490
|
createdAt: new Date().toISOString()
|
|
17370
17491
|
};
|
|
17371
|
-
appendApiMessage(message);
|
|
17492
|
+
appendApiMessage(message, execution?.threadRootId ? { updateTopicLastMessageAt: false } : undefined);
|
|
17372
17493
|
hub.broadcastMessage(topicId, message);
|
|
17373
17494
|
lastVisibleMessageId = message.id;
|
|
17374
17495
|
visibleMessageIds.push(message.id);
|
|
@@ -17890,7 +18011,7 @@ __export(exports_app_settings, {
|
|
|
17890
18011
|
getGlobalAiName: () => getGlobalAiName,
|
|
17891
18012
|
DEFAULT_AI_NAME: () => DEFAULT_AI_NAME
|
|
17892
18013
|
});
|
|
17893
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync18, readFileSync as readFileSync16, writeFileSync as
|
|
18014
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync18, readFileSync as readFileSync16, writeFileSync as writeFileSync16 } from "fs";
|
|
17894
18015
|
import { dirname as dirname14, join as join27 } from "path";
|
|
17895
18016
|
function settingsFile() {
|
|
17896
18017
|
return join27(resolveStorageDataDir(), "otium-settings.json");
|
|
@@ -17920,7 +18041,7 @@ function setGlobalAiName(name) {
|
|
|
17920
18041
|
aiName = name.trim() || DEFAULT_AI_NAME;
|
|
17921
18042
|
try {
|
|
17922
18043
|
mkdirSync18(dirname14(path), { recursive: true });
|
|
17923
|
-
|
|
18044
|
+
writeFileSync16(path, JSON.stringify({ aiName }, null, 2));
|
|
17924
18045
|
} catch {}
|
|
17925
18046
|
return aiName;
|
|
17926
18047
|
}
|
|
@@ -18417,6 +18538,7 @@ async function drainOneDurableUserTurn() {
|
|
|
18417
18538
|
bridgeSessionFromHistory: execution?.bridgeSessionFromHistory,
|
|
18418
18539
|
peerBridge: execution?.peerBridge,
|
|
18419
18540
|
from: execution?.from,
|
|
18541
|
+
threadRootId: execution?.threadRootId,
|
|
18420
18542
|
_queryId: request.requestId,
|
|
18421
18543
|
_runtimeEpoch: execution?.runtimeEpoch ?? request.topicEpoch,
|
|
18422
18544
|
onSettled: () => {
|
|
@@ -18489,6 +18611,7 @@ function startAiTurn(params) {
|
|
|
18489
18611
|
let sessionId = sessionResolution.sessionId;
|
|
18490
18612
|
const deferredSessionId = params.sessionId === undefined && !sessionResolution.isolated ? undefined : sessionId;
|
|
18491
18613
|
const sourceNode = params.sourceNode;
|
|
18614
|
+
const threadRootId = params.threadRootId;
|
|
18492
18615
|
const topicId = topic.id;
|
|
18493
18616
|
const requestId = params.requestId;
|
|
18494
18617
|
const depth = params.depth;
|
|
@@ -19017,7 +19140,7 @@ function startAiTurn(params) {
|
|
|
19017
19140
|
WsHub.get().broadcastTyping(topicId, "ai");
|
|
19018
19141
|
WsHub.get().broadcastAiActive(topicId, queryId);
|
|
19019
19142
|
}
|
|
19020
|
-
streamAgentEvents(topicId, topic.title, queryId, events, control, agentKind, resolvedModel, resolvedEffort, userId, !sessionRetried, onSessionId, { silent, peerBridge, sourceNode }).then(async (streamOutcome) => {
|
|
19143
|
+
streamAgentEvents(topicId, topic.title, queryId, events, control, agentKind, resolvedModel, resolvedEffort, userId, !sessionRetried, onSessionId, { silent, peerBridge, sourceNode, ...threadRootId ? { threadRootId } : {} }).then(async (streamOutcome) => {
|
|
19021
19144
|
let outcome = streamOutcome;
|
|
19022
19145
|
if (outcome.kind === "session-expired") {
|
|
19023
19146
|
const retry = resolveSessionRetry({
|
|
@@ -20277,7 +20400,8 @@ function gatewayPayloadHash(params, requestId, actorUserId) {
|
|
|
20277
20400
|
params.text,
|
|
20278
20401
|
params.clientMessageId,
|
|
20279
20402
|
requestId,
|
|
20280
|
-
params.allowAutoContinue ?? true
|
|
20403
|
+
params.allowAutoContinue ?? true,
|
|
20404
|
+
params.threadRootId ?? null
|
|
20281
20405
|
])).digest("hex");
|
|
20282
20406
|
}
|
|
20283
20407
|
function submitRuntimeGatewayTurn(params) {
|
|
@@ -20297,6 +20421,7 @@ function submitRuntimeGatewayTurn(params) {
|
|
|
20297
20421
|
sourceAdapter: "runtime-gateway",
|
|
20298
20422
|
sourceMessageId: params.clientMessageId,
|
|
20299
20423
|
text: params.text,
|
|
20424
|
+
...params.threadRootId ? { threadRootId: params.threadRootId } : {},
|
|
20300
20425
|
createdAt
|
|
20301
20426
|
};
|
|
20302
20427
|
const submission = {
|
|
@@ -20331,7 +20456,8 @@ function submitRuntimeGatewayTurn(params) {
|
|
|
20331
20456
|
sessionIdSpecified: true,
|
|
20332
20457
|
conversationPrompts: [params.text],
|
|
20333
20458
|
loggedUserMessageCount: 0,
|
|
20334
|
-
vaultUserId: params.vaultUserId
|
|
20459
|
+
vaultUserId: params.vaultUserId,
|
|
20460
|
+
...params.threadRootId ? { threadRootId: params.threadRootId } : {}
|
|
20335
20461
|
}
|
|
20336
20462
|
});
|
|
20337
20463
|
const acceptedEvent = appendRuntimeEvent("runtime-gateway-ingress", {
|
|
@@ -20524,7 +20650,11 @@ function registerTopic(opts) {
|
|
|
20524
20650
|
throw new TopicValidationError("Manager rooms are system-managed");
|
|
20525
20651
|
}
|
|
20526
20652
|
const surface = normalizeTopicSurface(opts.surface ?? defaultTopicSurface());
|
|
20527
|
-
const
|
|
20653
|
+
const surfaceScope = opts.surfaceScope !== undefined ? normalizeSurfaceScope(opts.surfaceScope) : surface === "otium" ? defaultSurfaceScope() : null;
|
|
20654
|
+
if (surface === "otium" && !surfaceScope && isSurfaceScopeRequired()) {
|
|
20655
|
+
throw new TopicValidationError("this node serves several Otium workspaces; a room must name one to be created");
|
|
20656
|
+
}
|
|
20657
|
+
const conflict = findTopicTitleConflict(title, requestedKind, { surface, surfaceScope });
|
|
20528
20658
|
if (conflict) {
|
|
20529
20659
|
throw new TopicValidationError(`A topic named "${title}" already exists on ${surface}`);
|
|
20530
20660
|
}
|
|
@@ -20561,6 +20691,7 @@ function registerTopic(opts) {
|
|
|
20561
20691
|
aiMode,
|
|
20562
20692
|
participants: [{ userId: opts.userId, role: "owner" }],
|
|
20563
20693
|
surface,
|
|
20694
|
+
surfaceScope,
|
|
20564
20695
|
createdAt: now,
|
|
20565
20696
|
lastMessageAt: now
|
|
20566
20697
|
};
|
|
@@ -21714,7 +21845,7 @@ var init_file_ops = __esm(() => {
|
|
|
21714
21845
|
|
|
21715
21846
|
// ../../packages/core/src/runtime/inbox.ts
|
|
21716
21847
|
import { createHash as createHash9, randomUUID as randomUUID20 } from "crypto";
|
|
21717
|
-
import { readdirSync as readdirSync8, statSync as statSync13, writeFileSync as
|
|
21848
|
+
import { readdirSync as readdirSync8, statSync as statSync13, writeFileSync as writeFileSync17 } from "fs";
|
|
21718
21849
|
import { join as join32 } from "path";
|
|
21719
21850
|
async function createAskForkPlan(options) {
|
|
21720
21851
|
const snapshot = structuredClone(options.entries);
|
|
@@ -22004,7 +22135,7 @@ function sweepScheduledSessionInbox(nowMs = Date.now()) {
|
|
|
22004
22135
|
total: due.length + pending.length
|
|
22005
22136
|
}, "self-schedule: promotion interrupted; retaining only the unpromoted entries");
|
|
22006
22137
|
try {
|
|
22007
|
-
|
|
22138
|
+
writeFileSync17(drained.processingPath, `${unfinished.map((entry) => JSON.stringify(entry)).join(`
|
|
22008
22139
|
`)}
|
|
22009
22140
|
`);
|
|
22010
22141
|
} catch (rewriteErr) {
|
|
@@ -22411,13 +22542,16 @@ __export(exports_src, {
|
|
|
22411
22542
|
startBashrsCompletionsWorker: () => startBashrsCompletionsWorker,
|
|
22412
22543
|
startAskUserQuestionGateOwner: () => startAskUserQuestionGateOwner,
|
|
22413
22544
|
startAiTurn: () => startAiTurn,
|
|
22545
|
+
stampUnscopedOtiumTopics: () => stampUnscopedOtiumTopics,
|
|
22414
22546
|
setTopicSurfaces: () => setTopicSurfaces,
|
|
22415
22547
|
setTopicSessionId: () => setTopicSessionId,
|
|
22548
|
+
setSurfaceScopeRequired: () => setSurfaceScopeRequired,
|
|
22416
22549
|
setRuntimeMcpPort: () => setRuntimeMcpPort,
|
|
22417
22550
|
setRuntimeBus: () => setRuntimeBus,
|
|
22418
22551
|
setPlaywrightUnavailableNotifier: () => setPlaywrightUnavailableNotifier,
|
|
22419
22552
|
setNodeMcpServers: () => setNodeMcpServers,
|
|
22420
22553
|
setFileHooks: () => setFileHooks,
|
|
22554
|
+
setDefaultSurfaceScope: () => setDefaultSurfaceScope,
|
|
22421
22555
|
setBashrsCompletionSink: () => setBashrsCompletionSink,
|
|
22422
22556
|
setApiTopicConfig: () => setApiTopicConfig,
|
|
22423
22557
|
sessionInboxPath: () => sessionInboxPath,
|
|
@@ -22459,6 +22593,7 @@ __export(exports_src, {
|
|
|
22459
22593
|
onShutdown: () => onShutdown,
|
|
22460
22594
|
normalizeVaultKey: () => normalizeVaultKey,
|
|
22461
22595
|
normalizeTopicSurface: () => normalizeTopicSurface,
|
|
22596
|
+
normalizeSurfaceScope: () => normalizeSurfaceScope,
|
|
22462
22597
|
nodeRequestHandlerNames: () => nodeRequestHandlerNames,
|
|
22463
22598
|
modelOwner: () => modelOwner,
|
|
22464
22599
|
markPlaywrightUnavailable: () => markPlaywrightUnavailable,
|
|
@@ -22486,6 +22621,7 @@ __export(exports_src, {
|
|
|
22486
22621
|
isTranscriptionConfigured: () => isTranscriptionConfigured,
|
|
22487
22622
|
isTopicVisible: () => isTopicVisible,
|
|
22488
22623
|
isTopicRunning: () => isTopicRunning,
|
|
22624
|
+
isSurfaceScopeRequired: () => isSurfaceScopeRequired,
|
|
22489
22625
|
isSensitivePath: () => isSensitivePath,
|
|
22490
22626
|
isParticipant: () => isParticipant,
|
|
22491
22627
|
isHostedMcpSurface: () => isHostedMcpSurface,
|
|
@@ -22535,6 +22671,7 @@ __export(exports_src, {
|
|
|
22535
22671
|
deleteVaultEntry: () => deleteVaultEntry,
|
|
22536
22672
|
deleteTopicCascade: () => deleteTopicCascade,
|
|
22537
22673
|
defaultTopicSurface: () => defaultTopicSurface,
|
|
22674
|
+
defaultSurfaceScope: () => defaultSurfaceScope,
|
|
22538
22675
|
db: () => db,
|
|
22539
22676
|
createSubagentManagementToolDefinitions: () => createSubagentManagementToolDefinitions,
|
|
22540
22677
|
createSpawnSubagentToolDefinition: () => createSpawnSubagentToolDefinition,
|
|
@@ -22832,6 +22969,7 @@ __export(exports_node_host, {
|
|
|
22832
22969
|
getVisibleTopics: () => getVisibleTopics,
|
|
22833
22970
|
getTopicStats: () => getTopicStats,
|
|
22834
22971
|
getTopic: () => getTopic,
|
|
22972
|
+
getApiMessage: () => getApiMessage,
|
|
22835
22973
|
flushBashrsCompletions: () => flushBashrsCompletions,
|
|
22836
22974
|
executeVaultCommand: () => executeVaultCommand,
|
|
22837
22975
|
ensurePersonalGeneral: () => ensurePersonalGeneral,
|
|
@@ -23166,6 +23304,7 @@ __export(exports_mcp_runtime_host, {
|
|
|
23166
23304
|
dispatchPeerRuntimeFile: () => dispatchPeerRuntimeFile,
|
|
23167
23305
|
dispatchPeerRuntimeAskUser: () => dispatchPeerRuntimeAskUser,
|
|
23168
23306
|
deleteTopicCascade: () => deleteTopicCascade,
|
|
23307
|
+
defaultTopicSurface: () => defaultTopicSurface,
|
|
23169
23308
|
createSubagentManagementToolDefinitions: () => createSubagentManagementToolDefinitions,
|
|
23170
23309
|
createSpawnSubagentToolDefinition: () => createSpawnSubagentToolDefinition,
|
|
23171
23310
|
createSelfConfigToolDefinitions: () => createSelfConfigToolDefinitions,
|
|
@@ -23222,6 +23361,9 @@ var init_mcp_runtime_host = __esm(async () => {
|
|
|
23222
23361
|
|
|
23223
23362
|
// ../../packages/mcp/src/node-tools.ts
|
|
23224
23363
|
import { z as z6 } from "zod";
|
|
23364
|
+
function callerSurface(ctx) {
|
|
23365
|
+
return getTopic(ctx.topicId)?.surface ?? defaultTopicSurface();
|
|
23366
|
+
}
|
|
23225
23367
|
function resolveTopicForUser(ctx, ref) {
|
|
23226
23368
|
const trimmed = ref.trim();
|
|
23227
23369
|
if (!trimmed)
|
|
@@ -23233,7 +23375,7 @@ function resolveTopicForUser(ctx, ref) {
|
|
|
23233
23375
|
return { error: notFound };
|
|
23234
23376
|
return { topic: byId };
|
|
23235
23377
|
}
|
|
23236
|
-
const byTitle = getTopicByNameForUser(trimmed, ctx.userId);
|
|
23378
|
+
const byTitle = getTopicByNameForUser(trimmed, ctx.userId, { surface: callerSurface(ctx) });
|
|
23237
23379
|
if (byTitle)
|
|
23238
23380
|
return { topic: byTitle };
|
|
23239
23381
|
return { error: notFound };
|
|
@@ -23255,6 +23397,7 @@ function registerNodeTools(server, ctx) {
|
|
|
23255
23397
|
const topic = registerTopic({
|
|
23256
23398
|
title,
|
|
23257
23399
|
userId: ctx.userId,
|
|
23400
|
+
surface: callerSurface(ctx),
|
|
23258
23401
|
agent,
|
|
23259
23402
|
model,
|
|
23260
23403
|
effort,
|
|
@@ -23276,7 +23419,7 @@ function registerNodeTools(server, ctx) {
|
|
|
23276
23419
|
}
|
|
23277
23420
|
});
|
|
23278
23421
|
server.tool("list_topics", "List the calling user's topics on this negotium node: title, id, kind, agent, and whether a turn is currently running.", {}, async () => {
|
|
23279
|
-
const topics = getTopics().filter((topic) => isParticipant(topic, ctx.userId));
|
|
23422
|
+
const topics = getTopics({ surface: callerSurface(ctx) }).filter((topic) => isParticipant(topic, ctx.userId));
|
|
23280
23423
|
if (topics.length === 0) {
|
|
23281
23424
|
return textResult("No topics found. Use register_topic to create one.");
|
|
23282
23425
|
}
|
|
@@ -24253,13 +24396,15 @@ function currentTopic(context) {
|
|
|
24253
24396
|
return topic;
|
|
24254
24397
|
}
|
|
24255
24398
|
function targetCatalog(context) {
|
|
24256
|
-
const
|
|
24399
|
+
const current3 = context.currentTopicId ? getTopic(context.currentTopicId) : null;
|
|
24400
|
+
const surface = current3?.surface ?? defaultTopicSurface();
|
|
24401
|
+
const surfaceScope = current3 ? current3.surfaceScope ?? null : surface === "otium" ? defaultSurfaceScope() : null;
|
|
24257
24402
|
return createSessionTargetCatalog({
|
|
24258
24403
|
currentTopicId: context.currentTopicId,
|
|
24259
24404
|
currentTopicName: context.currentTopic,
|
|
24260
24405
|
currentSurface: surface,
|
|
24261
24406
|
isAgent: isAgentKind,
|
|
24262
|
-
listRows: () => listTopics().filter((topic) => topic.participants.some((p) => p.userId === context.userId)).map((topic) => ({
|
|
24407
|
+
listRows: () => listTopics({ surface, surfaceScope }).filter((topic) => topic.participants.some((p) => p.userId === context.userId)).map((topic) => ({
|
|
24263
24408
|
id: topic.id,
|
|
24264
24409
|
title: topic.title,
|
|
24265
24410
|
kind: topic.kind ?? null,
|
|
@@ -24311,7 +24456,7 @@ function createDefaultSessionCommMcpHost() {
|
|
|
24311
24456
|
const entries = targetCatalog(context).listTargets().filter(({ topic }) => !identity.restricted || Boolean(topic.topicId && canSubagentTellTarget(identity, topic.topicId))).filter(({ topic }) => Boolean(topic.agent)).map(({ key, topic }) => `- ${key}: ${topic.sessionId ? "active" : "fresh-start ready"}${topic.description ? `
|
|
24312
24457
|
description: ${topic.description.slice(0, 80)}` : ""}`);
|
|
24313
24458
|
if (!identity.restricted) {
|
|
24314
|
-
const peers = await peerSessionsForUser(context.userId, context.peerHostQueryId);
|
|
24459
|
+
const peers = await peerSessionsForUser(context.userId, context.peerHostQueryId, context.currentTopicId);
|
|
24315
24460
|
for (const node of peers.nodes ?? []) {
|
|
24316
24461
|
for (const session of node.sessions ?? []) {
|
|
24317
24462
|
if (session.agent)
|
|
@@ -25156,7 +25301,7 @@ import {
|
|
|
25156
25301
|
renameSync as renameSync11,
|
|
25157
25302
|
statSync as statSync15,
|
|
25158
25303
|
unlinkSync as unlinkSync19,
|
|
25159
|
-
writeFileSync as
|
|
25304
|
+
writeFileSync as writeFileSync18
|
|
25160
25305
|
} from "fs";
|
|
25161
25306
|
import { basename as basename7, dirname as dirname16, join as join36, relative as relative2, resolve as resolve17 } from "path";
|
|
25162
25307
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -25194,7 +25339,7 @@ function acquireRecoveryGuard(lockPath) {
|
|
|
25194
25339
|
try {
|
|
25195
25340
|
const fd = openSync4(guardPath, "wx");
|
|
25196
25341
|
try {
|
|
25197
|
-
|
|
25342
|
+
writeFileSync18(fd, ownerToken, "utf-8");
|
|
25198
25343
|
} catch (error2) {
|
|
25199
25344
|
closeSync4(fd);
|
|
25200
25345
|
try {
|
|
@@ -25227,7 +25372,7 @@ function acquireFileLock(path) {
|
|
|
25227
25372
|
try {
|
|
25228
25373
|
const fd = openSync4(lockPath, "wx");
|
|
25229
25374
|
try {
|
|
25230
|
-
|
|
25375
|
+
writeFileSync18(fd, ownerToken, "utf-8");
|
|
25231
25376
|
} catch (error2) {
|
|
25232
25377
|
closeSync4(fd);
|
|
25233
25378
|
try {
|
|
@@ -25271,7 +25416,7 @@ function acquireFileLock(path) {
|
|
|
25271
25416
|
}
|
|
25272
25417
|
function atomicWriteFile(path, content) {
|
|
25273
25418
|
const temporaryPath = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
25274
|
-
|
|
25419
|
+
writeFileSync18(temporaryPath, content, "utf-8");
|
|
25275
25420
|
try {
|
|
25276
25421
|
renameSync11(temporaryPath, path);
|
|
25277
25422
|
} catch (error2) {
|
|
@@ -26151,7 +26296,7 @@ ${fresh.join(`
|
|
|
26151
26296
|
const existing = readFileSync23(skillPath, "utf-8");
|
|
26152
26297
|
finalContent = mergeGotchas(content, extractGotchas(existing));
|
|
26153
26298
|
} catch {}
|
|
26154
|
-
|
|
26299
|
+
writeFileSync18(skillPath, finalContent, "utf-8");
|
|
26155
26300
|
return {
|
|
26156
26301
|
content: [
|
|
26157
26302
|
{
|
|
@@ -26170,7 +26315,7 @@ function writeSummaryDocument(rawTopic, content, dateStr) {
|
|
|
26170
26315
|
let counter = 1;
|
|
26171
26316
|
while (true) {
|
|
26172
26317
|
try {
|
|
26173
|
-
|
|
26318
|
+
writeFileSync18(resolve17(runtime().summariesDir, summaryName), content, {
|
|
26174
26319
|
encoding: "utf-8",
|
|
26175
26320
|
flag: "wx"
|
|
26176
26321
|
});
|
|
@@ -26210,7 +26355,7 @@ function writeTopicDocument(rawTopic, content) {
|
|
|
26210
26355
|
const briefPath = resolve17(runtime().topicsDir, `${fileSlug}.md`);
|
|
26211
26356
|
ensureDir(dirname16(briefPath));
|
|
26212
26357
|
const existed = existsSync27(briefPath);
|
|
26213
|
-
|
|
26358
|
+
writeFileSync18(briefPath, content, "utf-8");
|
|
26214
26359
|
const setTopicBrief2 = runtime().host.setTopicBrief;
|
|
26215
26360
|
let sqliteUpdated = false;
|
|
26216
26361
|
if (topicId && setTopicBrief2) {
|
|
@@ -26877,7 +27022,7 @@ import {
|
|
|
26877
27022
|
renameSync as renameSync12,
|
|
26878
27023
|
statSync as statSync16,
|
|
26879
27024
|
unlinkSync as unlinkSync20,
|
|
26880
|
-
writeFileSync as
|
|
27025
|
+
writeFileSync as writeFileSync19
|
|
26881
27026
|
} from "fs";
|
|
26882
27027
|
import { basename as basename8, join as join37 } from "path";
|
|
26883
27028
|
function startLogRotation() {
|
|
@@ -26977,7 +27122,7 @@ function removeSentFilesForTopic(userId, topicName) {
|
|
|
26977
27122
|
}
|
|
26978
27123
|
});
|
|
26979
27124
|
if (remaining.length > 0) {
|
|
26980
|
-
|
|
27125
|
+
writeFileSync19(file, `${remaining.join(`
|
|
26981
27126
|
`)}
|
|
26982
27127
|
`);
|
|
26983
27128
|
} else {
|
|
@@ -27499,6 +27644,7 @@ __export(exports_storage_public, {
|
|
|
27499
27644
|
tasks: () => exports_tasks,
|
|
27500
27645
|
taskScopeKey: () => taskScopeKey,
|
|
27501
27646
|
taskFileMtimeNs: () => taskFileMtimeNs,
|
|
27647
|
+
stampUnscopedOtiumTopics: () => stampUnscopedOtiumTopics,
|
|
27502
27648
|
softDeleteApiMessagesByIdPrefix: () => softDeleteApiMessagesByIdPrefix,
|
|
27503
27649
|
softDeleteApiMessage: () => softDeleteApiMessage,
|
|
27504
27650
|
settleTopicArchiveJob: () => settleTopicArchiveJob,
|
|
@@ -27513,10 +27659,12 @@ __export(exports_storage_public, {
|
|
|
27513
27659
|
setTopicAgentAndSession: () => setTopicAgentAndSession,
|
|
27514
27660
|
setTopicAgentAndClearSession: () => setTopicAgentAndClearSession,
|
|
27515
27661
|
setTopicAgent: () => setTopicAgent,
|
|
27662
|
+
setSurfaceScopeRequired: () => setSurfaceScopeRequired,
|
|
27516
27663
|
setSessionForTopic: () => setSessionForTopic,
|
|
27517
27664
|
setLastShownConfig: () => setLastShownConfig,
|
|
27518
27665
|
setGlobalAiName: () => setGlobalAiName,
|
|
27519
27666
|
setDmSessionId: () => setDmSessionId,
|
|
27667
|
+
setDefaultSurfaceScope: () => setDefaultSurfaceScope,
|
|
27520
27668
|
setApiTopicConfig: () => setApiTopicConfig,
|
|
27521
27669
|
setApiTopicAgent: () => setApiTopicAgent,
|
|
27522
27670
|
setApiMessageReactions: () => setApiMessageReactions,
|
|
@@ -27549,6 +27697,7 @@ __export(exports_storage_public, {
|
|
|
27549
27697
|
normalizeTopicSurface: () => normalizeTopicSurface,
|
|
27550
27698
|
normalizeTopicState: () => normalizeTopicState,
|
|
27551
27699
|
normalizeTopicKind: () => normalizeTopicKind,
|
|
27700
|
+
normalizeSurfaceScope: () => normalizeSurfaceScope,
|
|
27552
27701
|
normalizeAiMode: () => normalizeAiMode,
|
|
27553
27702
|
markPendingAskState: () => markPendingAskState,
|
|
27554
27703
|
markPendingAskSources: () => markPendingAskSources,
|
|
@@ -27564,6 +27713,7 @@ __export(exports_storage_public, {
|
|
|
27564
27713
|
isTopicVisible: () => isTopicVisible,
|
|
27565
27714
|
isTopicSummaryFile: () => isTopicSummaryFile,
|
|
27566
27715
|
isTopicBriefFile: () => isTopicBriefFile,
|
|
27716
|
+
isSurfaceScopeRequired: () => isSurfaceScopeRequired,
|
|
27567
27717
|
isRuntimeProcessLeaseAlive: () => isRuntimeProcessLeaseAlive,
|
|
27568
27718
|
inferTopicKind: () => inferTopicKind,
|
|
27569
27719
|
inferAiMode: () => inferAiMode,
|
|
@@ -27616,6 +27766,7 @@ __export(exports_storage_public, {
|
|
|
27616
27766
|
findRecentUserMessage: () => findRecentUserMessage,
|
|
27617
27767
|
findLastSessionIdForAgent: () => findLastSessionIdForAgent,
|
|
27618
27768
|
describePendingAskState: () => describePendingAskState,
|
|
27769
|
+
deleteTopicStats: () => deleteTopicStats,
|
|
27619
27770
|
deleteTopicBrief: () => deleteTopicBrief,
|
|
27620
27771
|
deleteTopicArchiveState: () => deleteTopicArchiveState,
|
|
27621
27772
|
deleteTopic: () => deleteTopic,
|
|
@@ -27624,6 +27775,7 @@ __export(exports_storage_public, {
|
|
|
27624
27775
|
deleteMessagesForTopic: () => deleteMessagesForTopic,
|
|
27625
27776
|
deleteApiTopicConfig: () => deleteApiTopicConfig,
|
|
27626
27777
|
defaultTopicSurface: () => defaultTopicSurface,
|
|
27778
|
+
defaultSurfaceScope: () => defaultSurfaceScope,
|
|
27627
27779
|
db: () => db2,
|
|
27628
27780
|
createTasks: () => createTasks,
|
|
27629
27781
|
createPendingAsk: () => createPendingAsk,
|
|
@@ -28483,7 +28635,7 @@ var init_spec = __esm(() => {
|
|
|
28483
28635
|
});
|
|
28484
28636
|
|
|
28485
28637
|
// ../../packages/mcp-host/src/manifest.ts
|
|
28486
|
-
import { existsSync as existsSync29, mkdirSync as mkdirSync26, readFileSync as readFileSync24, renameSync as renameSync13, writeFileSync as
|
|
28638
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync26, readFileSync as readFileSync24, renameSync as renameSync13, writeFileSync as writeFileSync20 } from "fs";
|
|
28487
28639
|
import { dirname as dirname17 } from "path";
|
|
28488
28640
|
import { z as z14 } from "zod";
|
|
28489
28641
|
|
|
@@ -28550,7 +28702,7 @@ class McpManifest {
|
|
|
28550
28702
|
servers: [...this.entries.values()]
|
|
28551
28703
|
};
|
|
28552
28704
|
const tmp = `${this.file}.tmp-${process.pid}`;
|
|
28553
|
-
|
|
28705
|
+
writeFileSync20(tmp, `${JSON.stringify(payload, null, 2)}
|
|
28554
28706
|
`);
|
|
28555
28707
|
renameSync13(tmp, this.file);
|
|
28556
28708
|
}
|
|
@@ -28585,7 +28737,7 @@ import {
|
|
|
28585
28737
|
readFileSync as readFileSync25,
|
|
28586
28738
|
renameSync as renameSync14,
|
|
28587
28739
|
unlinkSync as unlinkSync21,
|
|
28588
|
-
writeFileSync as
|
|
28740
|
+
writeFileSync as writeFileSync21
|
|
28589
28741
|
} from "fs";
|
|
28590
28742
|
import { connect, createServer as createServer3 } from "net";
|
|
28591
28743
|
import { join as join38 } from "path";
|
|
@@ -28902,7 +29054,7 @@ class McpHost {
|
|
|
28902
29054
|
mkdirSync27(this.portsDir, { recursive: true });
|
|
28903
29055
|
const file = join38(this.portsDir, fileName);
|
|
28904
29056
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
28905
|
-
|
|
29057
|
+
writeFileSync21(tmp, String(port));
|
|
28906
29058
|
renameSync14(tmp, file);
|
|
28907
29059
|
} catch (e) {
|
|
28908
29060
|
this.log("warn", "Failed to write MCP port file", { fileName, port, err: String(e) });
|
|
@@ -29020,7 +29172,7 @@ import {
|
|
|
29020
29172
|
readFileSync as readFileSync26,
|
|
29021
29173
|
rmSync as rmSync9,
|
|
29022
29174
|
statSync as statSync18,
|
|
29023
|
-
writeFileSync as
|
|
29175
|
+
writeFileSync as writeFileSync22
|
|
29024
29176
|
} from "fs";
|
|
29025
29177
|
import { basename as basename10, extname as extname4, join as join39 } from "path";
|
|
29026
29178
|
function safeExtension(filename) {
|
|
@@ -29110,7 +29262,7 @@ class NodeFileStore {
|
|
|
29110
29262
|
...access.visibility ? { visibility: access.visibility } : {}
|
|
29111
29263
|
};
|
|
29112
29264
|
copyFileSync4(absPath, savedPath);
|
|
29113
|
-
|
|
29265
|
+
writeFileSync22(this.#metadataPath(fileId), JSON.stringify(metadata), { mode: 384 });
|
|
29114
29266
|
return this.#attachment(fileId, metadata);
|
|
29115
29267
|
} catch (error2) {
|
|
29116
29268
|
rmSync9(savedPath, { force: true });
|
|
@@ -29303,12 +29455,38 @@ import {
|
|
|
29303
29455
|
readFileSync as readFileSync27,
|
|
29304
29456
|
renameSync as renameSync15,
|
|
29305
29457
|
unlinkSync as unlinkSync22,
|
|
29306
|
-
writeFileSync as
|
|
29458
|
+
writeFileSync as writeFileSync23
|
|
29307
29459
|
} from "fs";
|
|
29308
29460
|
import { dirname as dirname18, resolve as resolve21 } from "path";
|
|
29309
29461
|
function jsonError(status, error2) {
|
|
29310
29462
|
return Response.json({ ok: false, error: error2 }, { status });
|
|
29311
29463
|
}
|
|
29464
|
+
function requestSurfaceScope(req) {
|
|
29465
|
+
const header = req.headers.get(NODE_RUNTIME_SURFACE_SCOPE_HEADER);
|
|
29466
|
+
if (header === null)
|
|
29467
|
+
return {};
|
|
29468
|
+
const scope = header.trim();
|
|
29469
|
+
return { surfaceScope: scope ? scope : null };
|
|
29470
|
+
}
|
|
29471
|
+
function topicInRequestScope(req, topic) {
|
|
29472
|
+
const scope = requestSurfaceScope(req);
|
|
29473
|
+
if (!("surfaceScope" in scope))
|
|
29474
|
+
return true;
|
|
29475
|
+
const roomScope = topic.surfaceScope ?? null;
|
|
29476
|
+
if (roomScope === null)
|
|
29477
|
+
return req.headers.get(NODE_RUNTIME_SURFACE_SCOPE_STRICT_HEADER) !== "1";
|
|
29478
|
+
return roomScope === (scope.surfaceScope ?? null);
|
|
29479
|
+
}
|
|
29480
|
+
function gatewayVisibleTopics(req) {
|
|
29481
|
+
const scope = requestSurfaceScope(req);
|
|
29482
|
+
if (!("surfaceScope" in scope))
|
|
29483
|
+
return getVisibleTopics({ surface: "otium" });
|
|
29484
|
+
const own = getVisibleTopics({ surface: "otium", surfaceScope: scope.surfaceScope ?? null });
|
|
29485
|
+
const strict = req.headers.get(NODE_RUNTIME_SURFACE_SCOPE_STRICT_HEADER) === "1";
|
|
29486
|
+
if (strict || scope.surfaceScope === null)
|
|
29487
|
+
return own;
|
|
29488
|
+
return own.concat(getVisibleTopics({ surface: "otium", surfaceScope: null }));
|
|
29489
|
+
}
|
|
29312
29490
|
function topicServiceError(error2) {
|
|
29313
29491
|
const status = error2.code === "TOPIC_NOT_FOUND" ? 404 : error2.code === "TOPIC_FORBIDDEN" ? 403 : 400;
|
|
29314
29492
|
return jsonError(status, error2.message);
|
|
@@ -29363,7 +29541,21 @@ function runtimeEvent(event) {
|
|
|
29363
29541
|
createdAt: event.createdAt
|
|
29364
29542
|
};
|
|
29365
29543
|
}
|
|
29366
|
-
function createRuntimeContractEventStream(req, after, topicId) {
|
|
29544
|
+
function createRuntimeContractEventStream(req, after, topicId, scope = {}) {
|
|
29545
|
+
const scoped = "surfaceScope" in scope;
|
|
29546
|
+
const wanted = scope.surfaceScope ?? null;
|
|
29547
|
+
const visible = new Map;
|
|
29548
|
+
const eventInScope = (eventTopicId) => {
|
|
29549
|
+
if (!scoped)
|
|
29550
|
+
return true;
|
|
29551
|
+
const cached = visible.get(eventTopicId);
|
|
29552
|
+
if (cached !== undefined)
|
|
29553
|
+
return cached;
|
|
29554
|
+
const roomScope = getTopic(eventTopicId)?.surfaceScope ?? null;
|
|
29555
|
+
const allowed = roomScope === null || roomScope === wanted;
|
|
29556
|
+
visible.set(eventTopicId, allowed);
|
|
29557
|
+
return allowed;
|
|
29558
|
+
};
|
|
29367
29559
|
let cursor = Math.max(0, after);
|
|
29368
29560
|
return createPollingSseStream(req, {
|
|
29369
29561
|
ready: { v: NODE_RUNTIME_CONTRACT_VERSION, cursor },
|
|
@@ -29374,7 +29566,7 @@ function createRuntimeContractEventStream(req, after, topicId) {
|
|
|
29374
29566
|
break;
|
|
29375
29567
|
for (const event of events) {
|
|
29376
29568
|
cursor = event.seq;
|
|
29377
|
-
if (!topicId || event.topicId === topicId) {
|
|
29569
|
+
if ((!topicId || event.topicId === topicId) && eventInScope(event.topicId)) {
|
|
29378
29570
|
send("runtime", runtimeEvent(event), event.seq);
|
|
29379
29571
|
}
|
|
29380
29572
|
}
|
|
@@ -29385,8 +29577,8 @@ function createRuntimeContractEventStream(req, after, topicId) {
|
|
|
29385
29577
|
}
|
|
29386
29578
|
});
|
|
29387
29579
|
}
|
|
29388
|
-
function createEventStream(req, userId, after) {
|
|
29389
|
-
const allowedTopics = new Set(topicsForUser(userId).map((topic) => topic.id));
|
|
29580
|
+
function createEventStream(req, userId, after, surface) {
|
|
29581
|
+
const allowedTopics = new Set(topicsForUser(userId, surface).map((topic) => topic.id));
|
|
29390
29582
|
let cursor = Math.max(0, after);
|
|
29391
29583
|
return createPollingSseStream(req, {
|
|
29392
29584
|
ready: { protocolVersion: NODE_CONTROL_PROTOCOL_VERSION, cursor },
|
|
@@ -29399,7 +29591,8 @@ function createEventStream(req, userId, after) {
|
|
|
29399
29591
|
cursor = event.seq;
|
|
29400
29592
|
if (event.type === "topic-created" || event.type === "topic-updated") {
|
|
29401
29593
|
const topic = getTopic(event.topicId);
|
|
29402
|
-
|
|
29594
|
+
const admissible = topic && isParticipant(topic, userId) && (!surface || topic.surface === surface);
|
|
29595
|
+
if (admissible)
|
|
29403
29596
|
allowedTopics.add(event.topicId);
|
|
29404
29597
|
else
|
|
29405
29598
|
allowedTopics.delete(event.topicId);
|
|
@@ -29451,6 +29644,10 @@ function createNodeControlHandler(options) {
|
|
|
29451
29644
|
if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
|
|
29452
29645
|
return jsonError(400, "Unsupported v");
|
|
29453
29646
|
const topicId = requiredText(body.topicId, "topicId");
|
|
29647
|
+
const turnTopic = getTopic(topicId);
|
|
29648
|
+
if (turnTopic && !topicInRequestScope(req, turnTopic)) {
|
|
29649
|
+
return jsonError(404, "Topic not found");
|
|
29650
|
+
}
|
|
29454
29651
|
const userId = requiredText(body.userId, "userId");
|
|
29455
29652
|
const actorUserId = body.actorUserId === undefined ? undefined : requiredText(body.actorUserId, "actorUserId");
|
|
29456
29653
|
const actorLabel = body.actorLabel === undefined ? undefined : requiredText(body.actorLabel, "actorLabel");
|
|
@@ -29458,12 +29655,19 @@ function createNodeControlHandler(options) {
|
|
|
29458
29655
|
const text2 = requiredText(body.text, "text");
|
|
29459
29656
|
const clientMessageId = requiredText(body.clientMessageId, "clientMessageId");
|
|
29460
29657
|
const requestId = body.requestId === undefined ? undefined : requiredText(body.requestId, "requestId");
|
|
29658
|
+
const threadRootId = body.threadRootId === undefined ? undefined : requiredText(body.threadRootId, "threadRootId");
|
|
29461
29659
|
const topic = getTopic(topicId);
|
|
29462
29660
|
if (!topic)
|
|
29463
29661
|
return jsonError(404, "Topic not found");
|
|
29464
29662
|
if (!topic.participants.some((participant) => participant.userId === userId)) {
|
|
29465
29663
|
return jsonError(404, "Topic not found");
|
|
29466
29664
|
}
|
|
29665
|
+
if (threadRootId) {
|
|
29666
|
+
const root = getApiMessage(topicId, threadRootId);
|
|
29667
|
+
if (!root || root.deleted || root.threadRootId) {
|
|
29668
|
+
return jsonError(400, "threadRootId does not identify a thread root in this topic");
|
|
29669
|
+
}
|
|
29670
|
+
}
|
|
29467
29671
|
const submission = submitRuntimeGatewayTurn({
|
|
29468
29672
|
topic,
|
|
29469
29673
|
userId,
|
|
@@ -29473,7 +29677,8 @@ function createNodeControlHandler(options) {
|
|
|
29473
29677
|
text: text2,
|
|
29474
29678
|
clientMessageId,
|
|
29475
29679
|
requestId,
|
|
29476
|
-
allowAutoContinue: body.allowAutoContinue !== false
|
|
29680
|
+
allowAutoContinue: body.allowAutoContinue !== false,
|
|
29681
|
+
...threadRootId ? { threadRootId } : {}
|
|
29477
29682
|
});
|
|
29478
29683
|
return Response.json({
|
|
29479
29684
|
ok: true,
|
|
@@ -29490,13 +29695,15 @@ function createNodeControlHandler(options) {
|
|
|
29490
29695
|
if (req.method === "GET" && runtimePath === "/events") {
|
|
29491
29696
|
const parsed = Number.parseInt(url.searchParams.get("after") ?? "0", 10);
|
|
29492
29697
|
const topicId = url.searchParams.get("topicId")?.trim() || undefined;
|
|
29493
|
-
return createRuntimeContractEventStream(req, Number.isFinite(parsed) ? parsed : 0, topicId);
|
|
29698
|
+
return createRuntimeContractEventStream(req, Number.isFinite(parsed) ? parsed : 0, topicId, requestSurfaceScope(req));
|
|
29494
29699
|
}
|
|
29495
29700
|
const runtimeMessagesMatch = runtimePath.match(/^\/topics\/([^/]+)\/messages$/);
|
|
29496
29701
|
if (runtimeMessagesMatch && req.method === "GET") {
|
|
29497
29702
|
const topicId = decodeURIComponent(runtimeMessagesMatch[1]);
|
|
29498
|
-
|
|
29703
|
+
const messagesTopic = getTopic(topicId);
|
|
29704
|
+
if (!messagesTopic || !topicInRequestScope(req, messagesTopic)) {
|
|
29499
29705
|
return jsonError(404, "Topic not found");
|
|
29706
|
+
}
|
|
29500
29707
|
const cursor = url.searchParams.get("cursor");
|
|
29501
29708
|
const parsedLimit = Number.parseInt(url.searchParams.get("limit") ?? "50", 10);
|
|
29502
29709
|
const result = listApiMessages(topicId, {
|
|
@@ -29506,7 +29713,7 @@ function createNodeControlHandler(options) {
|
|
|
29506
29713
|
return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, ...result });
|
|
29507
29714
|
}
|
|
29508
29715
|
if (req.method === "GET" && runtimePath === "/topics") {
|
|
29509
|
-
const topics =
|
|
29716
|
+
const topics = gatewayVisibleTopics(req);
|
|
29510
29717
|
return Response.json({
|
|
29511
29718
|
ok: true,
|
|
29512
29719
|
v: NODE_RUNTIME_CONTRACT_VERSION,
|
|
@@ -29529,6 +29736,7 @@ function createNodeControlHandler(options) {
|
|
|
29529
29736
|
userId,
|
|
29530
29737
|
kind: "agent",
|
|
29531
29738
|
surface: "otium",
|
|
29739
|
+
...requestSurfaceScope(req),
|
|
29532
29740
|
...agent ? { agent } : {}
|
|
29533
29741
|
});
|
|
29534
29742
|
return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, topic }, { status: 201 });
|
|
@@ -29540,7 +29748,7 @@ function createNodeControlHandler(options) {
|
|
|
29540
29748
|
return jsonError(400, "Unsupported v");
|
|
29541
29749
|
const topicId = decodeURIComponent(importMatch[1]);
|
|
29542
29750
|
const topic = getTopic(topicId);
|
|
29543
|
-
if (!topic)
|
|
29751
|
+
if (!topic || !topicInRequestScope(req, topic))
|
|
29544
29752
|
return jsonError(404, "Topic not found");
|
|
29545
29753
|
if (!Array.isArray(body.messages)) {
|
|
29546
29754
|
return jsonError(400, "messages must be an array");
|
|
@@ -29585,6 +29793,8 @@ function createNodeControlHandler(options) {
|
|
|
29585
29793
|
const runtimeTopicMatch = runtimePath.match(/^\/topics\/([^/]+)$/);
|
|
29586
29794
|
if (runtimeTopicMatch && req.method === "GET") {
|
|
29587
29795
|
const topic = getTopic(decodeURIComponent(runtimeTopicMatch[1]));
|
|
29796
|
+
if (topic && !topicInRequestScope(req, topic))
|
|
29797
|
+
return jsonError(404, "Topic not found");
|
|
29588
29798
|
if (!topic)
|
|
29589
29799
|
return jsonError(404, "Topic not found");
|
|
29590
29800
|
return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, topic });
|
|
@@ -29608,7 +29818,7 @@ function createNodeControlHandler(options) {
|
|
|
29608
29818
|
}
|
|
29609
29819
|
if (req.method === "GET" && path === "/session") {
|
|
29610
29820
|
const userId = requiredText(url.searchParams.get("user"), "user");
|
|
29611
|
-
ensurePersonalGeneral(userId);
|
|
29821
|
+
ensurePersonalGeneral(userId, requestedSurface(url));
|
|
29612
29822
|
return Response.json({
|
|
29613
29823
|
ok: true,
|
|
29614
29824
|
protocolVersion: NODE_CONTROL_PROTOCOL_VERSION,
|
|
@@ -29679,7 +29889,7 @@ function createNodeControlHandler(options) {
|
|
|
29679
29889
|
if (req.method === "GET" && path === "/events") {
|
|
29680
29890
|
const userId = requiredText(url.searchParams.get("user"), "user");
|
|
29681
29891
|
const parsed = Number.parseInt(url.searchParams.get("after") ?? "0", 10);
|
|
29682
|
-
return createEventStream(req, userId, Number.isFinite(parsed) ? parsed : 0);
|
|
29892
|
+
return createEventStream(req, userId, Number.isFinite(parsed) ? parsed : 0, requestedSurface(url));
|
|
29683
29893
|
}
|
|
29684
29894
|
const messagesMatch = path.match(/^\/topics\/([^/]+)\/messages$/);
|
|
29685
29895
|
if (messagesMatch && req.method === "GET") {
|
|
@@ -29876,7 +30086,7 @@ function writeNodeDaemonInfo(port, startedAt) {
|
|
|
29876
30086
|
};
|
|
29877
30087
|
mkdirSync29(dirname18(NODE_DAEMON_INFO_PATH), { recursive: true });
|
|
29878
30088
|
const temporary = `${NODE_DAEMON_INFO_PATH}.${process.pid}.${randomUUID25()}.tmp`;
|
|
29879
|
-
|
|
30089
|
+
writeFileSync23(temporary, `${JSON.stringify(info, null, 2)}
|
|
29880
30090
|
`, { mode: 384 });
|
|
29881
30091
|
chmodSync6(temporary, 384);
|
|
29882
30092
|
renameSync15(temporary, NODE_DAEMON_INFO_PATH);
|
|
@@ -29964,7 +30174,7 @@ async function stopNodeDaemon(timeoutMs = 3000) {
|
|
|
29964
30174
|
throw new Error(`node shutdown returned HTTP ${response.status}`);
|
|
29965
30175
|
return true;
|
|
29966
30176
|
}
|
|
29967
|
-
var NODE_CONTROL_PROTOCOL_VERSION = 1, NODE_CONTROL_BASE_PATH = "/api/v1/control", NODE_RUNTIME_CONTRACT_VERSION = 1, NODE_RUNTIME_CONTRACT_BASE_PATH, NODE_DAEMON_ROLE = "node-daemon", NODE_DAEMON_INFO_PATH, NODE_VERSION, ControlRequestError;
|
|
30177
|
+
var NODE_CONTROL_PROTOCOL_VERSION = 1, NODE_CONTROL_BASE_PATH = "/api/v1/control", NODE_RUNTIME_CONTRACT_VERSION = 1, NODE_RUNTIME_SURFACE_SCOPE_HEADER = "x-negotium-surface-scope", NODE_RUNTIME_SURFACE_SCOPE_STRICT_HEADER = "x-negotium-surface-scope-strict", NODE_RUNTIME_CONTRACT_BASE_PATH, NODE_DAEMON_ROLE = "node-daemon", NODE_DAEMON_INFO_PATH, NODE_VERSION, ControlRequestError;
|
|
29968
30178
|
var init_control = __esm(async () => {
|
|
29969
30179
|
await init_node_host();
|
|
29970
30180
|
await init_files();
|
|
@@ -32893,8 +33103,10 @@ function activeContextBreakdown(state) {
|
|
|
32893
33103
|
if (!topic)
|
|
32894
33104
|
return;
|
|
32895
33105
|
const messages = activeMessages(state);
|
|
32896
|
-
const
|
|
32897
|
-
const stored =
|
|
33106
|
+
const topicUsage = state.topicUsage[topic.id];
|
|
33107
|
+
const stored = topicUsage?.currentSession;
|
|
33108
|
+
const contextWasReset = Boolean(topicUsage) && !stored;
|
|
33109
|
+
const latest = contextWasReset ? undefined : latestUsageMessage(messages);
|
|
32898
33110
|
const confirmed = latest?.usage.context ?? stored?.contextTokens;
|
|
32899
33111
|
const contextWindow = latest?.usage.contextWindow ?? stored?.contextWindow;
|
|
32900
33112
|
if (confirmed === undefined || !contextWindow)
|
|
@@ -34182,10 +34394,10 @@ function topicPickerHints(topicPickerRoot) {
|
|
|
34182
34394
|
`\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 ${exit}`,
|
|
34183
34395
|
`\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 ${shortExit}`,
|
|
34184
34396
|
`\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 ${exit}`,
|
|
34185
|
-
`\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N/D
|
|
34186
|
-
"\u2191\u2193 \xB7 Enter \xB7 type to filter \xB7 Ctrl-N/D
|
|
34187
|
-
"type to filter \xB7 Ctrl-N/D
|
|
34188
|
-
"Ctrl-N/D
|
|
34397
|
+
`\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N/D \xB7 ${shortExit}`,
|
|
34398
|
+
"\u2191\u2193 \xB7 Enter \xB7 type to filter \xB7 Ctrl-N/D",
|
|
34399
|
+
"type to filter \xB7 Ctrl-N/D",
|
|
34400
|
+
"Ctrl-N/D"
|
|
34189
34401
|
];
|
|
34190
34402
|
return [...new Set(candidates)].sort((a, b) => displayWidth(b) - displayWidth(a));
|
|
34191
34403
|
}
|
|
@@ -35392,7 +35604,7 @@ class EmbeddedNegotiumClient {
|
|
|
35392
35604
|
try {
|
|
35393
35605
|
if (this.#startNode)
|
|
35394
35606
|
this.#node = await startDefaultNode({ port: this.#port });
|
|
35395
|
-
ensurePersonalGeneral(this.#userId);
|
|
35607
|
+
ensurePersonalGeneral(this.#userId, "terminal");
|
|
35396
35608
|
} catch (error2) {
|
|
35397
35609
|
this.#unsubscribe?.();
|
|
35398
35610
|
this.#unsubscribe = null;
|
|
@@ -35776,7 +35988,7 @@ class RemoteNegotiumClient {
|
|
|
35776
35988
|
}
|
|
35777
35989
|
}
|
|
35778
35990
|
async#openEventStream(after, signal) {
|
|
35779
|
-
const response = await fetch(`${this.#baseUrl}${NODE_CONTROL_BASE_PATH}/events?user=${encodeURIComponent(this.#userId)}&after=${after}`, {
|
|
35991
|
+
const response = await fetch(`${this.#baseUrl}${NODE_CONTROL_BASE_PATH}/events?user=${encodeURIComponent(this.#userId)}&after=${after}&surface=terminal`, {
|
|
35780
35992
|
signal,
|
|
35781
35993
|
headers: {
|
|
35782
35994
|
accept: "text/event-stream",
|
|
@@ -39862,7 +40074,7 @@ function startTelegramAdapter(opts) {
|
|
|
39862
40074
|
if (initialForumChatId !== undefined && !forumMode) {
|
|
39863
40075
|
logger.warn({ forumChatId: initialForumChatId }, "telegram adapter: forumChatId set but client lacks createForumTopic \u2014 forum mode disabled");
|
|
39864
40076
|
}
|
|
39865
|
-
const personalGeneral = ensurePersonalGeneral(userId);
|
|
40077
|
+
const personalGeneral = ensurePersonalGeneral(userId, "telegram");
|
|
39866
40078
|
const byKey = new Map;
|
|
39867
40079
|
const byTopic = new Map;
|
|
39868
40080
|
const targetByQueryId = new Map;
|
|
@@ -40109,7 +40321,9 @@ function startTelegramAdapter(opts) {
|
|
|
40109
40321
|
return true;
|
|
40110
40322
|
}
|
|
40111
40323
|
if (!store.isFlagSet(SURFACE_BACKFILL_FLAG)) {
|
|
40112
|
-
const mappedIds = [
|
|
40324
|
+
const mappedIds = [
|
|
40325
|
+
...new Set(store.load().map((mapping) => mapping.topicId).filter((topicId) => getTopic(topicId)?.kind !== "manager"))
|
|
40326
|
+
];
|
|
40113
40327
|
const moved = setTopicSurfaces(mappedIds, "telegram");
|
|
40114
40328
|
store.setFlag(SURFACE_BACKFILL_FLAG);
|
|
40115
40329
|
if (moved > 0) {
|
|
@@ -41358,45 +41572,101 @@ var init_join_status = __esm(() => {
|
|
|
41358
41572
|
});
|
|
41359
41573
|
|
|
41360
41574
|
// ../../adapters/otium/src/control-protocol.ts
|
|
41361
|
-
var OTIUM_ADAPTER_CONTROL_PREFIX = "/api/v1/adapter/otium", OTIUM_ADAPTER_CONTROL_HEADER = "x-negotium-adapter-token";
|
|
41575
|
+
var OTIUM_ADAPTER_CONTROL_PREFIX = "/api/v1/adapter/otium", OTIUM_ADAPTER_CONTROL_HEADER = "x-negotium-adapter-token", OTIUM_WORKSPACES_CONTROL_PATH = "/_workspaces", OTIUM_RELAYED_HEADER = "x-negotium-otium-relayed";
|
|
41362
41576
|
|
|
41363
41577
|
// ../../adapters/otium/src/central.ts
|
|
41578
|
+
function cellState(cellId) {
|
|
41579
|
+
return cells.get(cellId) ?? null;
|
|
41580
|
+
}
|
|
41581
|
+
function firstCell() {
|
|
41582
|
+
for (const cell of cells.values())
|
|
41583
|
+
return cell;
|
|
41584
|
+
return null;
|
|
41585
|
+
}
|
|
41586
|
+
function attachOtiumCentralCell(join42) {
|
|
41587
|
+
cells.set(join42.cellId, {
|
|
41588
|
+
join: join42,
|
|
41589
|
+
nodesCache: null,
|
|
41590
|
+
verifyCache: new Map,
|
|
41591
|
+
tokenCache: new Map
|
|
41592
|
+
});
|
|
41593
|
+
}
|
|
41594
|
+
function detachOtiumCentralCell(cellId) {
|
|
41595
|
+
return cells.delete(cellId);
|
|
41596
|
+
}
|
|
41597
|
+
function attachedOtiumCells() {
|
|
41598
|
+
return [...cells.values()].map((cell) => cell.join);
|
|
41599
|
+
}
|
|
41364
41600
|
function configureOtiumCentral(join42) {
|
|
41365
|
-
|
|
41366
|
-
|
|
41601
|
+
cells.clear();
|
|
41602
|
+
if (join42)
|
|
41603
|
+
attachOtiumCentralCell(join42);
|
|
41367
41604
|
}
|
|
41368
|
-
function
|
|
41369
|
-
return
|
|
41605
|
+
function isOtiumCentralConfigured() {
|
|
41606
|
+
return cells.size > 0;
|
|
41370
41607
|
}
|
|
41371
|
-
function centralFetch(path, init) {
|
|
41372
|
-
|
|
41373
|
-
throw new Error("otium: join credentials missing");
|
|
41374
|
-
return fetch(`${joinConfig.central}${path}`, {
|
|
41608
|
+
function centralFetch(cell, path, init) {
|
|
41609
|
+
return fetch(`${cell.join.central}${path}`, {
|
|
41375
41610
|
...init,
|
|
41376
41611
|
headers: {
|
|
41377
|
-
authorization: `Bearer ${
|
|
41612
|
+
authorization: `Bearer ${cell.join.secret}`,
|
|
41378
41613
|
"content-type": "application/json",
|
|
41379
41614
|
...init.headers ?? {}
|
|
41380
41615
|
},
|
|
41381
41616
|
signal: AbortSignal.timeout(5000)
|
|
41382
41617
|
});
|
|
41383
41618
|
}
|
|
41384
|
-
async function
|
|
41385
|
-
if (!
|
|
41386
|
-
return nodesCache.nodes;
|
|
41619
|
+
async function discover(cell, fresh) {
|
|
41620
|
+
if (!fresh && cell.nodesCache && Date.now() - cell.nodesCache.at < NODES_CACHE_MS) {
|
|
41621
|
+
return cell.nodesCache.nodes;
|
|
41387
41622
|
}
|
|
41388
|
-
const response = await centralFetch("/peer/nodes", { method: "GET" });
|
|
41623
|
+
const response = await centralFetch(cell, "/peer/nodes", { method: "GET" });
|
|
41389
41624
|
const body = await response.json();
|
|
41390
41625
|
if (!response.ok || !body.ok || !Array.isArray(body.nodes)) {
|
|
41391
41626
|
throw new Error(`otium: node discovery failed: ${body.error ?? response.status}`);
|
|
41392
41627
|
}
|
|
41393
|
-
|
|
41394
|
-
|
|
41628
|
+
const nodes = body.nodes.map((node) => ({ ...node, viaCellId: cell.join.cellId }));
|
|
41629
|
+
cell.nodesCache = { nodes, workspaceId: body.workspaceId ?? "", at: Date.now() };
|
|
41630
|
+
return nodes;
|
|
41395
41631
|
}
|
|
41396
|
-
async function
|
|
41397
|
-
const
|
|
41632
|
+
async function listPeerNodesForCell(cellId, opts = {}) {
|
|
41633
|
+
const cell = cellState(cellId);
|
|
41634
|
+
if (!cell)
|
|
41635
|
+
throw new Error(`otium: not attached to cell ${cellId}`);
|
|
41636
|
+
return discover(cell, opts.fresh === true);
|
|
41637
|
+
}
|
|
41638
|
+
async function listPeerNodes(opts = {}) {
|
|
41639
|
+
if (cells.size === 0)
|
|
41640
|
+
throw new Error("otium: join credentials missing");
|
|
41641
|
+
const results = await Promise.allSettled([...cells.values()].map((cell) => discover(cell, opts.fresh === true)));
|
|
41642
|
+
const nodes = results.flatMap((result) => result.status === "fulfilled" ? result.value : []);
|
|
41643
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
41644
|
+
if (failures.length === results.length) {
|
|
41645
|
+
throw failures[0].reason;
|
|
41646
|
+
}
|
|
41647
|
+
for (const failure of failures) {
|
|
41648
|
+
logger.warn({ err: failure.reason }, "otium: discovery failed");
|
|
41649
|
+
}
|
|
41650
|
+
return nodes;
|
|
41651
|
+
}
|
|
41652
|
+
async function selfPeerNodeForCell(cellId) {
|
|
41653
|
+
const nodes = await listPeerNodesForCell(cellId);
|
|
41398
41654
|
return nodes.find((node) => node.self) ?? null;
|
|
41399
41655
|
}
|
|
41656
|
+
async function selfPeerNode() {
|
|
41657
|
+
const cell = firstCell();
|
|
41658
|
+
if (!cell)
|
|
41659
|
+
throw new Error("otium: join credentials missing");
|
|
41660
|
+
return selfPeerNodeForCell(cell.join.cellId);
|
|
41661
|
+
}
|
|
41662
|
+
async function peerWorkspaceIdForCell(cellId) {
|
|
41663
|
+
const cell = cellState(cellId);
|
|
41664
|
+
if (!cell)
|
|
41665
|
+
return null;
|
|
41666
|
+
if (!cell.nodesCache?.workspaceId)
|
|
41667
|
+
await discover(cell, true);
|
|
41668
|
+
return cell.nodesCache?.workspaceId || null;
|
|
41669
|
+
}
|
|
41400
41670
|
async function resolvePeerNodeByCellId(cellId) {
|
|
41401
41671
|
const find = (nodes) => nodes.find((node) => node.cellId === cellId) ?? null;
|
|
41402
41672
|
const cached = find(await listPeerNodes());
|
|
@@ -41404,36 +41674,39 @@ async function resolvePeerNodeByCellId(cellId) {
|
|
|
41404
41674
|
return cached;
|
|
41405
41675
|
return find(await listPeerNodes({ fresh: true }));
|
|
41406
41676
|
}
|
|
41407
|
-
async function mintPeerToken(
|
|
41408
|
-
const
|
|
41677
|
+
async function mintPeerToken(target) {
|
|
41678
|
+
const cell = cellState(target.viaCellId);
|
|
41679
|
+
if (!cell)
|
|
41680
|
+
throw new Error("otium: join credentials missing");
|
|
41681
|
+
const cached = cell.tokenCache.get(target.cellId);
|
|
41409
41682
|
if (cached && cached.expiresAtMs - Date.now() > 30000)
|
|
41410
41683
|
return cached.token;
|
|
41411
|
-
const response = await centralFetch("/peer/token", {
|
|
41684
|
+
const response = await centralFetch(cell, "/peer/token", {
|
|
41412
41685
|
method: "POST",
|
|
41413
|
-
body: JSON.stringify({ toCellId })
|
|
41686
|
+
body: JSON.stringify({ toCellId: target.cellId })
|
|
41414
41687
|
});
|
|
41415
41688
|
const body = await response.json();
|
|
41416
41689
|
if (!response.ok || !body.ok || !body.token) {
|
|
41417
41690
|
throw new Error(`otium: peer token mint failed: ${body.error ?? response.status}`);
|
|
41418
41691
|
}
|
|
41419
|
-
tokenCache.set(
|
|
41692
|
+
cell.tokenCache.set(target.cellId, {
|
|
41420
41693
|
token: body.token,
|
|
41421
41694
|
expiresAtMs: Date.parse(body.expiresAt ?? "") || Date.now() + 60000
|
|
41422
41695
|
});
|
|
41423
41696
|
return body.token;
|
|
41424
41697
|
}
|
|
41425
|
-
async function
|
|
41426
|
-
const cached = verifyCache.get(token);
|
|
41698
|
+
async function verifyAgainstCell(cell, token) {
|
|
41699
|
+
const cached = cell.verifyCache.get(token);
|
|
41427
41700
|
if (cached && Date.now() - cached.at < VERIFY_CACHE_MS)
|
|
41428
41701
|
return cached.verified;
|
|
41429
41702
|
let response;
|
|
41430
41703
|
try {
|
|
41431
|
-
response = await centralFetch("/peer/verify", {
|
|
41704
|
+
response = await centralFetch(cell, "/peer/verify", {
|
|
41432
41705
|
method: "POST",
|
|
41433
41706
|
body: JSON.stringify({ token })
|
|
41434
41707
|
});
|
|
41435
41708
|
} catch (err2) {
|
|
41436
|
-
logger.warn({ err: err2 }, "otium: central verify unreachable");
|
|
41709
|
+
logger.warn({ err: err2, cellId: cell.join.cellId }, "otium: central verify unreachable");
|
|
41437
41710
|
return null;
|
|
41438
41711
|
}
|
|
41439
41712
|
const body = await response.json().catch(() => null);
|
|
@@ -41444,25 +41717,39 @@ async function verifyPeerToken(token) {
|
|
|
41444
41717
|
fromCellId: body.fromCellId,
|
|
41445
41718
|
fromNodeName: body.fromNodeName,
|
|
41446
41719
|
fromIsPrimary: body.fromIsPrimary,
|
|
41447
|
-
expiresAt: body.expiresAt
|
|
41720
|
+
expiresAt: body.expiresAt,
|
|
41721
|
+
viaCellId: cell.join.cellId
|
|
41448
41722
|
};
|
|
41449
|
-
verifyCache.set(token, { verified, at: Date.now() });
|
|
41450
|
-
for (const [key, entry] of verifyCache) {
|
|
41723
|
+
cell.verifyCache.set(token, { verified, at: Date.now() });
|
|
41724
|
+
for (const [key, entry] of cell.verifyCache) {
|
|
41451
41725
|
if (Date.now() - entry.at > VERIFY_CACHE_MS)
|
|
41452
|
-
verifyCache.delete(key);
|
|
41726
|
+
cell.verifyCache.delete(key);
|
|
41453
41727
|
}
|
|
41454
41728
|
return verified;
|
|
41455
41729
|
}
|
|
41456
|
-
function
|
|
41457
|
-
|
|
41458
|
-
|
|
41459
|
-
|
|
41730
|
+
async function verifyPeerToken(token) {
|
|
41731
|
+
const rejectedAt = rejectCache.get(token);
|
|
41732
|
+
if (rejectedAt !== undefined && Date.now() - rejectedAt < REJECT_CACHE_MS)
|
|
41733
|
+
return null;
|
|
41734
|
+
for (const cell of cells.values()) {
|
|
41735
|
+
const verified = await verifyAgainstCell(cell, token);
|
|
41736
|
+
if (verified) {
|
|
41737
|
+
rejectCache.delete(token);
|
|
41738
|
+
return verified;
|
|
41739
|
+
}
|
|
41740
|
+
}
|
|
41741
|
+
rejectCache.set(token, Date.now());
|
|
41742
|
+
for (const [key, at] of rejectCache) {
|
|
41743
|
+
if (Date.now() - at > REJECT_CACHE_MS)
|
|
41744
|
+
rejectCache.delete(key);
|
|
41745
|
+
}
|
|
41746
|
+
return null;
|
|
41460
41747
|
}
|
|
41461
|
-
var NODES_CACHE_MS = 30000, VERIFY_CACHE_MS = 30000,
|
|
41748
|
+
var NODES_CACHE_MS = 30000, VERIFY_CACHE_MS = 30000, cells, REJECT_CACHE_MS = 2000, rejectCache;
|
|
41462
41749
|
var init_central = __esm(async () => {
|
|
41463
41750
|
await init_src();
|
|
41464
|
-
|
|
41465
|
-
|
|
41751
|
+
cells = new Map;
|
|
41752
|
+
rejectCache = new Map;
|
|
41466
41753
|
});
|
|
41467
41754
|
|
|
41468
41755
|
// ../../adapters/otium/src/protocol.ts
|
|
@@ -41537,7 +41824,7 @@ async function forwardCanonicalTool(capability, request) {
|
|
|
41537
41824
|
if (!hub?.isPrimary || hub.self)
|
|
41538
41825
|
return { error: "canonical hub is unavailable", status: 503 };
|
|
41539
41826
|
try {
|
|
41540
|
-
const peerToken = await mintPeerToken(hub
|
|
41827
|
+
const peerToken = await mintPeerToken(hub);
|
|
41541
41828
|
const response = await fetch(`${hub.baseUrl.replace(/\/+$/, "")}/api/v1/peer/bridge/canonical-mcp`, {
|
|
41542
41829
|
method: "POST",
|
|
41543
41830
|
headers: { authorization: `Bearer ${peerToken}`, "content-type": "application/json" },
|
|
@@ -41710,6 +41997,7 @@ __export(exports_join, {
|
|
|
41710
41997
|
saveJoin: () => saveJoin,
|
|
41711
41998
|
removeJoin: () => removeJoin,
|
|
41712
41999
|
parseInviteCode: () => parseInviteCode,
|
|
42000
|
+
loadJoins: () => loadJoins,
|
|
41713
42001
|
loadJoin: () => loadJoin,
|
|
41714
42002
|
joinFilePath: () => joinFilePath,
|
|
41715
42003
|
joinCredentialDigest: () => joinCredentialDigest,
|
|
@@ -41730,7 +42018,7 @@ import {
|
|
|
41730
42018
|
rmSync as rmSync10,
|
|
41731
42019
|
statSync as statSync20,
|
|
41732
42020
|
unlinkSync as unlinkSync23,
|
|
41733
|
-
writeFileSync as
|
|
42021
|
+
writeFileSync as writeFileSync24
|
|
41734
42022
|
} from "fs";
|
|
41735
42023
|
import { dirname as dirname21, resolve as resolve24 } from "path";
|
|
41736
42024
|
function joinFilePath() {
|
|
@@ -41811,7 +42099,7 @@ function withJoinCredentialLock(operation) {
|
|
|
41811
42099
|
try {
|
|
41812
42100
|
mkdirSync32(lockPath, { mode: 448 });
|
|
41813
42101
|
created = true;
|
|
41814
|
-
|
|
42102
|
+
writeFileSync24(ownerPath, `${JSON.stringify(owner)}
|
|
41815
42103
|
`, { mode: 384 });
|
|
41816
42104
|
const ownerFd = openSync5(ownerPath, "r");
|
|
41817
42105
|
try {
|
|
@@ -41881,82 +42169,60 @@ function normalizedJoin(join42) {
|
|
|
41881
42169
|
function joinCredentialDigest(join42) {
|
|
41882
42170
|
return createHash11("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
|
|
41883
42171
|
}
|
|
41884
|
-
function
|
|
42172
|
+
function readPersistedJoins(path = joinFilePath()) {
|
|
41885
42173
|
if (!existsSync37(path))
|
|
41886
|
-
return
|
|
42174
|
+
return [];
|
|
41887
42175
|
const parsed = JSON.parse(readFileSync28(path, "utf-8"));
|
|
41888
42176
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
41889
42177
|
throw new Error("persisted join credentials are not a JSON object");
|
|
41890
42178
|
}
|
|
41891
|
-
|
|
42179
|
+
const record = parsed;
|
|
42180
|
+
if (!Array.isArray(record.joins))
|
|
42181
|
+
return [normalizeJoin(record)];
|
|
42182
|
+
return record.joins.map((entry) => {
|
|
42183
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
42184
|
+
throw new Error("persisted join credentials contain a non-object entry");
|
|
42185
|
+
}
|
|
42186
|
+
return normalizeJoin(entry);
|
|
42187
|
+
});
|
|
41892
42188
|
}
|
|
41893
42189
|
function isJoinPersisted(join42) {
|
|
41894
42190
|
try {
|
|
41895
|
-
const
|
|
41896
|
-
return persisted
|
|
42191
|
+
const normalized = normalizedJoin(join42);
|
|
42192
|
+
return readPersistedJoins().some((persisted) => joinsEqual(persisted, normalized));
|
|
41897
42193
|
} catch {
|
|
41898
42194
|
return false;
|
|
41899
42195
|
}
|
|
41900
42196
|
}
|
|
41901
|
-
function
|
|
42197
|
+
function fsyncPath(target) {
|
|
42198
|
+
const fd = openSync5(target, "r");
|
|
42199
|
+
try {
|
|
42200
|
+
fsyncSync2(fd);
|
|
42201
|
+
} finally {
|
|
42202
|
+
closeSync5(fd);
|
|
42203
|
+
}
|
|
42204
|
+
}
|
|
42205
|
+
function writeJoins(joins, allowOverwrite) {
|
|
41902
42206
|
const path = joinFilePath();
|
|
41903
42207
|
const directory = dirname21(path);
|
|
41904
|
-
const normalized = normalizedJoin(join42);
|
|
41905
|
-
mkdirSync32(directory, { recursive: true });
|
|
41906
|
-
if (existsSync37(path)) {
|
|
41907
|
-
if (lstatSync(path).isSymbolicLink()) {
|
|
41908
|
-
throw new Error(`refusing to replace symlinked Otium join file at ${path}`);
|
|
41909
|
-
}
|
|
41910
|
-
let existing = null;
|
|
41911
|
-
try {
|
|
41912
|
-
existing = readPersistedJoin(path);
|
|
41913
|
-
} catch (error2) {
|
|
41914
|
-
if (!options.replaceExisting) {
|
|
41915
|
-
throw new Error(`existing Otium join file at ${path} is invalid; pass --replace to replace it`, { cause: error2 });
|
|
41916
|
-
}
|
|
41917
|
-
}
|
|
41918
|
-
if (existing && joinsEqual(existing, normalized)) {
|
|
41919
|
-
chmodSync7(path, 384);
|
|
41920
|
-
const fileFd = openSync5(path, "r");
|
|
41921
|
-
try {
|
|
41922
|
-
fsyncSync2(fileFd);
|
|
41923
|
-
} finally {
|
|
41924
|
-
closeSync5(fileFd);
|
|
41925
|
-
}
|
|
41926
|
-
const directoryFd = openSync5(directory, "r");
|
|
41927
|
-
try {
|
|
41928
|
-
fsyncSync2(directoryFd);
|
|
41929
|
-
} finally {
|
|
41930
|
-
closeSync5(directoryFd);
|
|
41931
|
-
}
|
|
41932
|
-
return path;
|
|
41933
|
-
}
|
|
41934
|
-
if (!options.replaceExisting) {
|
|
41935
|
-
throw new Error(`this node is already joined${existing ? ` as ${existing.cellId}` : " with an invalid join file"}; pass --replace to replace its credentials`);
|
|
41936
|
-
}
|
|
41937
|
-
}
|
|
41938
42208
|
const temporaryPath = resolve24(directory, `.otium-join.json.${process.pid}.${randomUUID28()}.tmp`);
|
|
42209
|
+
const payload = { v: JOIN_FILE_VERSION, joins };
|
|
41939
42210
|
let fd;
|
|
41940
42211
|
try {
|
|
41941
42212
|
fd = openSync5(temporaryPath, "wx", 384);
|
|
41942
|
-
|
|
42213
|
+
writeFileSync24(fd, `${JSON.stringify(payload, null, 2)}
|
|
41943
42214
|
`, "utf8");
|
|
41944
42215
|
fsyncSync2(fd);
|
|
41945
42216
|
closeSync5(fd);
|
|
41946
42217
|
fd = undefined;
|
|
41947
|
-
if (
|
|
42218
|
+
if (allowOverwrite) {
|
|
41948
42219
|
renameSync16(temporaryPath, path);
|
|
41949
42220
|
} else {
|
|
41950
42221
|
linkSync(temporaryPath, path);
|
|
41951
42222
|
unlinkSync23(temporaryPath);
|
|
41952
42223
|
}
|
|
41953
42224
|
chmodSync7(path, 384);
|
|
41954
|
-
|
|
41955
|
-
try {
|
|
41956
|
-
fsyncSync2(directoryFd);
|
|
41957
|
-
} finally {
|
|
41958
|
-
closeSync5(directoryFd);
|
|
41959
|
-
}
|
|
42225
|
+
fsyncPath(directory);
|
|
41960
42226
|
} catch (error2) {
|
|
41961
42227
|
if (fd !== undefined)
|
|
41962
42228
|
closeSync5(fd);
|
|
@@ -41966,10 +42232,43 @@ function saveJoinWhileLocked(join42, options = {}) {
|
|
|
41966
42232
|
}
|
|
41967
42233
|
return path;
|
|
41968
42234
|
}
|
|
42235
|
+
function saveJoinWhileLocked(join42, options = {}) {
|
|
42236
|
+
const path = joinFilePath();
|
|
42237
|
+
const directory = dirname21(path);
|
|
42238
|
+
const normalized = normalizedJoin(join42);
|
|
42239
|
+
mkdirSync32(directory, { recursive: true });
|
|
42240
|
+
if (!existsSync37(path))
|
|
42241
|
+
return writeJoins([normalized], false);
|
|
42242
|
+
if (lstatSync(path).isSymbolicLink()) {
|
|
42243
|
+
throw new Error(`refusing to replace symlinked Otium join file at ${path}`);
|
|
42244
|
+
}
|
|
42245
|
+
let existing = null;
|
|
42246
|
+
try {
|
|
42247
|
+
existing = readPersistedJoins(path);
|
|
42248
|
+
} catch (error2) {
|
|
42249
|
+
if (!options.replaceExisting) {
|
|
42250
|
+
throw new Error(`existing Otium join file at ${path} is invalid; pass --replace to replace it`, { cause: error2 });
|
|
42251
|
+
}
|
|
42252
|
+
}
|
|
42253
|
+
if (existing?.some((persisted) => joinsEqual(persisted, normalized))) {
|
|
42254
|
+
chmodSync7(path, 384);
|
|
42255
|
+
fsyncPath(path);
|
|
42256
|
+
fsyncPath(directory);
|
|
42257
|
+
return path;
|
|
42258
|
+
}
|
|
42259
|
+
if (!existing)
|
|
42260
|
+
return writeJoins([normalized], true);
|
|
42261
|
+
const conflict = existing.find((persisted) => persisted.cellId === normalized.cellId);
|
|
42262
|
+
if (conflict && !options.replaceExisting) {
|
|
42263
|
+
throw new Error(`this node is already joined as ${conflict.cellId}; pass --replace to replace its credentials`);
|
|
42264
|
+
}
|
|
42265
|
+
const next = conflict ? existing.map((persisted) => persisted.cellId === normalized.cellId ? normalized : persisted) : [...existing, normalized];
|
|
42266
|
+
return writeJoins(next, true);
|
|
42267
|
+
}
|
|
41969
42268
|
function saveJoin(join42, options = {}) {
|
|
41970
42269
|
return withJoinCredentialLock(() => saveJoinWhileLocked(join42, options));
|
|
41971
42270
|
}
|
|
41972
|
-
function removeJoin() {
|
|
42271
|
+
function removeJoin(cellId) {
|
|
41973
42272
|
return withJoinCredentialLock(() => {
|
|
41974
42273
|
const path = joinFilePath();
|
|
41975
42274
|
if (!existsSync37(path))
|
|
@@ -41977,27 +42276,36 @@ function removeJoin() {
|
|
|
41977
42276
|
if (lstatSync(path).isSymbolicLink()) {
|
|
41978
42277
|
throw new Error(`refusing to remove symlinked Otium join file at ${path}`);
|
|
41979
42278
|
}
|
|
41980
|
-
|
|
41981
|
-
|
|
41982
|
-
|
|
41983
|
-
|
|
41984
|
-
|
|
41985
|
-
|
|
42279
|
+
if (cellId) {
|
|
42280
|
+
let remaining;
|
|
42281
|
+
try {
|
|
42282
|
+
remaining = readPersistedJoins(path).filter((join42) => join42.cellId !== cellId);
|
|
42283
|
+
} catch {
|
|
42284
|
+
return false;
|
|
42285
|
+
}
|
|
42286
|
+
if (remaining.length === readPersistedJoins(path).length)
|
|
42287
|
+
return false;
|
|
42288
|
+
if (remaining.length > 0) {
|
|
42289
|
+
writeJoins(remaining, true);
|
|
42290
|
+
return true;
|
|
42291
|
+
}
|
|
41986
42292
|
}
|
|
42293
|
+
unlinkSync23(path);
|
|
42294
|
+
fsyncPath(dirname21(path));
|
|
41987
42295
|
return true;
|
|
41988
42296
|
});
|
|
41989
42297
|
}
|
|
41990
|
-
function
|
|
42298
|
+
function loadJoins() {
|
|
41991
42299
|
const central = process.env.OTIUM_CENTRAL_URL?.trim();
|
|
41992
42300
|
const cellId = process.env.OTIUM_CELL_ID?.trim();
|
|
41993
42301
|
const secret = process.env.OTIUM_CELL_SECRET?.trim();
|
|
41994
42302
|
const relay = process.env.OTIUM_RELAY_URL?.trim();
|
|
41995
42303
|
if (central && cellId && secret) {
|
|
41996
42304
|
try {
|
|
41997
|
-
return normalizeJoin({ central, relay, cellId, secret });
|
|
42305
|
+
return [normalizeJoin({ central, relay, cellId, secret })];
|
|
41998
42306
|
} catch (err2) {
|
|
41999
42307
|
logger.warn({ err: err2 }, "otium: invalid OTIUM_CENTRAL_URL/OTIUM_CELL_ID/OTIUM_CELL_SECRET env");
|
|
42000
|
-
return
|
|
42308
|
+
return [];
|
|
42001
42309
|
}
|
|
42002
42310
|
}
|
|
42003
42311
|
if (central || cellId || secret) {
|
|
@@ -42005,18 +42313,18 @@ function loadJoin() {
|
|
|
42005
42313
|
}
|
|
42006
42314
|
const path = joinFilePath();
|
|
42007
42315
|
if (!existsSync37(path))
|
|
42008
|
-
return
|
|
42316
|
+
return [];
|
|
42009
42317
|
try {
|
|
42010
|
-
|
|
42011
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
42012
|
-
return null;
|
|
42013
|
-
return normalizeJoin(parsed);
|
|
42318
|
+
return readPersistedJoins(path);
|
|
42014
42319
|
} catch (err2) {
|
|
42015
42320
|
logger.warn({ err: err2, path }, "otium: failed to read join file");
|
|
42016
|
-
return
|
|
42321
|
+
return [];
|
|
42017
42322
|
}
|
|
42018
42323
|
}
|
|
42019
|
-
|
|
42324
|
+
function loadJoin() {
|
|
42325
|
+
return loadJoins()[0] ?? null;
|
|
42326
|
+
}
|
|
42327
|
+
var JOIN_LOCK_STALE_MS = 30000, JOIN_FILE_VERSION = 2;
|
|
42020
42328
|
var init_join = __esm(async () => {
|
|
42021
42329
|
await init_src();
|
|
42022
42330
|
});
|
|
@@ -42170,7 +42478,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42170
42478
|
return errorResult("Error: Hub node is no longer attached.");
|
|
42171
42479
|
let token;
|
|
42172
42480
|
try {
|
|
42173
|
-
token = await mintPeerToken(hubNode
|
|
42481
|
+
token = await mintPeerToken(hubNode);
|
|
42174
42482
|
} catch (err2) {
|
|
42175
42483
|
return errorResult(`Error: Failed to spawn on hub: ${err2.message}`);
|
|
42176
42484
|
}
|
|
@@ -42210,7 +42518,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42210
42518
|
return errorResult("Error: Hub node is no longer attached.");
|
|
42211
42519
|
let token;
|
|
42212
42520
|
try {
|
|
42213
|
-
token = await mintPeerToken(hubNode
|
|
42521
|
+
token = await mintPeerToken(hubNode);
|
|
42214
42522
|
} catch (error2) {
|
|
42215
42523
|
return errorResult(`Error: Failed to open hub question: ${error2.message}`);
|
|
42216
42524
|
}
|
|
@@ -42266,7 +42574,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42266
42574
|
if (!hubNode)
|
|
42267
42575
|
return errorResult("Error: Hub node is no longer attached.");
|
|
42268
42576
|
try {
|
|
42269
|
-
const token = await mintPeerToken(hubNode
|
|
42577
|
+
const token = await mintPeerToken(hubNode);
|
|
42270
42578
|
const response = await fetch(`${hubNode.baseUrl.replace(/\/+$/, "")}/api/v1/peer/bridge/self-config`, {
|
|
42271
42579
|
method: "POST",
|
|
42272
42580
|
headers: {
|
|
@@ -42297,7 +42605,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42297
42605
|
return { ok: false, error: "hub node is no longer attached" };
|
|
42298
42606
|
let token;
|
|
42299
42607
|
try {
|
|
42300
|
-
token = await mintPeerToken(hubNode
|
|
42608
|
+
token = await mintPeerToken(hubNode);
|
|
42301
42609
|
} catch (error2) {
|
|
42302
42610
|
return { ok: false, error: `peer token mint failed: ${error2.message}` };
|
|
42303
42611
|
}
|
|
@@ -42359,7 +42667,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42359
42667
|
if (!hubNode)
|
|
42360
42668
|
return { ok: false, error: "hub node is no longer attached" };
|
|
42361
42669
|
try {
|
|
42362
|
-
const token = await mintPeerToken(hubNode
|
|
42670
|
+
const token = await mintPeerToken(hubNode);
|
|
42363
42671
|
const form = new FormData;
|
|
42364
42672
|
form.set("hostQueryId", request.bridge.hostQueryId);
|
|
42365
42673
|
form.set("userId", request.userId);
|
|
@@ -42526,13 +42834,81 @@ var init_store2 = __esm(async () => {
|
|
|
42526
42834
|
`);
|
|
42527
42835
|
});
|
|
42528
42836
|
|
|
42837
|
+
// ../../adapters/otium/src/workspace-scope.ts
|
|
42838
|
+
import { createHash as createHash13 } from "crypto";
|
|
42839
|
+
import { mkdirSync as mkdirSync34, readFileSync as readFileSync29, writeFileSync as writeFileSync25 } from "fs";
|
|
42840
|
+
import { dirname as dirname22, resolve as resolve25 } from "path";
|
|
42841
|
+
function surfaceScopeFor(central, workspaceId) {
|
|
42842
|
+
const digest = createHash13("sha256").update(`${central.trim().replace(/\/+$/, "")}
|
|
42843
|
+
${workspaceId.trim()}`).digest("hex");
|
|
42844
|
+
return `ws_${digest.slice(0, 24)}`;
|
|
42845
|
+
}
|
|
42846
|
+
function scopeCachePath() {
|
|
42847
|
+
return resolve25(DATA_DIR, "otium-workspace.json");
|
|
42848
|
+
}
|
|
42849
|
+
function readCache() {
|
|
42850
|
+
try {
|
|
42851
|
+
const parsed = JSON.parse(readFileSync29(scopeCachePath(), "utf-8"));
|
|
42852
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
42853
|
+
return {};
|
|
42854
|
+
return parsed;
|
|
42855
|
+
} catch {
|
|
42856
|
+
return {};
|
|
42857
|
+
}
|
|
42858
|
+
}
|
|
42859
|
+
function cachedSurfaceScope(join43) {
|
|
42860
|
+
const record = readCache()[join43.cellId];
|
|
42861
|
+
if (!record || record.central !== join43.central || !record.workspaceId)
|
|
42862
|
+
return null;
|
|
42863
|
+
return surfaceScopeFor(record.central, record.workspaceId);
|
|
42864
|
+
}
|
|
42865
|
+
function cacheSurfaceScope(join43, workspaceId) {
|
|
42866
|
+
const scope = surfaceScopeFor(join43.central, workspaceId);
|
|
42867
|
+
const path = scopeCachePath();
|
|
42868
|
+
try {
|
|
42869
|
+
mkdirSync34(dirname22(path), { recursive: true });
|
|
42870
|
+
const cache = readCache();
|
|
42871
|
+
cache[join43.cellId] = { central: join43.central, workspaceId, scope };
|
|
42872
|
+
writeFileSync25(path, `${JSON.stringify(cache, null, 2)}
|
|
42873
|
+
`, { mode: 384 });
|
|
42874
|
+
} catch (err2) {
|
|
42875
|
+
logger.warn({ err: err2, path }, "otium: could not cache the workspace scope");
|
|
42876
|
+
}
|
|
42877
|
+
return scope;
|
|
42878
|
+
}
|
|
42879
|
+
async function resolveSurfaceScope(join43) {
|
|
42880
|
+
const cached = cachedSurfaceScope(join43);
|
|
42881
|
+
if (cached)
|
|
42882
|
+
return cached;
|
|
42883
|
+
try {
|
|
42884
|
+
const workspaceId = await peerWorkspaceIdForCell(join43.cellId);
|
|
42885
|
+
if (!workspaceId)
|
|
42886
|
+
return null;
|
|
42887
|
+
return cacheSurfaceScope(join43, workspaceId);
|
|
42888
|
+
} catch (err2) {
|
|
42889
|
+
logger.warn({ err: err2 }, "otium: workspace scope unresolved (will retry on the next contact)");
|
|
42890
|
+
return null;
|
|
42891
|
+
}
|
|
42892
|
+
}
|
|
42893
|
+
function surfaceScopeForCell(cellId) {
|
|
42894
|
+
const join43 = attachedOtiumCells().find((candidate) => candidate.cellId === cellId);
|
|
42895
|
+
return join43 ? cachedSurfaceScope(join43) : null;
|
|
42896
|
+
}
|
|
42897
|
+
function unscopedRoomsAddressable() {
|
|
42898
|
+
return attachedOtiumCells().length < 2;
|
|
42899
|
+
}
|
|
42900
|
+
var init_workspace_scope = __esm(async () => {
|
|
42901
|
+
await init_src();
|
|
42902
|
+
await init_central();
|
|
42903
|
+
});
|
|
42904
|
+
|
|
42529
42905
|
// ../../adapters/otium/src/session-bridge.ts
|
|
42530
42906
|
function prunePendingRemoteAsks(now = Date.now()) {
|
|
42531
42907
|
pruneRemoteAsks(now - PENDING_ASK_TTL_MS2);
|
|
42532
42908
|
}
|
|
42533
42909
|
async function postPeer(node, path, body) {
|
|
42534
42910
|
try {
|
|
42535
|
-
const token = await mintPeerToken(node
|
|
42911
|
+
const token = await mintPeerToken(node);
|
|
42536
42912
|
const response = await fetch(`${node.baseUrl.replace(/\/+$/, "")}${path}`, {
|
|
42537
42913
|
method: "POST",
|
|
42538
42914
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -42569,7 +42945,7 @@ async function postPeerIdempotent(node, path, body) {
|
|
|
42569
42945
|
}
|
|
42570
42946
|
async function peerSupportsRemoteAsk(node) {
|
|
42571
42947
|
try {
|
|
42572
|
-
const token = await mintPeerToken(node
|
|
42948
|
+
const token = await mintPeerToken(node);
|
|
42573
42949
|
const response = await fetch(`${node.baseUrl.replace(/\/+$/, "")}/api/v1/peer/capabilities`, {
|
|
42574
42950
|
headers: { authorization: `Bearer ${token}` },
|
|
42575
42951
|
signal: AbortSignal.timeout(PEER_TIMEOUT_MS)
|
|
@@ -42580,26 +42956,50 @@ async function peerSupportsRemoteAsk(node) {
|
|
|
42580
42956
|
return false;
|
|
42581
42957
|
}
|
|
42582
42958
|
}
|
|
42583
|
-
async function findNode(nodeName) {
|
|
42584
|
-
const
|
|
42585
|
-
|
|
42959
|
+
async function findNode(nodeName, fromTopicId) {
|
|
42960
|
+
const callerScope = fromTopicId ? getTopic(fromTopicId)?.surfaceScope ?? null : null;
|
|
42961
|
+
const select = (nodes) => {
|
|
42962
|
+
let candidates = nodes.filter((node) => node.nodeName === nodeName);
|
|
42963
|
+
if (callerScope) {
|
|
42964
|
+
candidates = candidates.filter((node) => surfaceScopeForCell(node.viaCellId) === callerScope);
|
|
42965
|
+
}
|
|
42966
|
+
if (candidates.length === 0)
|
|
42967
|
+
return null;
|
|
42968
|
+
if (candidates.length > 1) {
|
|
42969
|
+
return {
|
|
42970
|
+
ok: false,
|
|
42971
|
+
error: `node "${nodeName}" is ambiguous across attached workspaces`
|
|
42972
|
+
};
|
|
42973
|
+
}
|
|
42974
|
+
return { ok: true, node: candidates[0] };
|
|
42975
|
+
};
|
|
42976
|
+
const cached = select(await listPeerNodes());
|
|
42977
|
+
if (cached)
|
|
42978
|
+
return cached;
|
|
42979
|
+
return select(await listPeerNodes({ fresh: true })) ?? {
|
|
42980
|
+
ok: false,
|
|
42981
|
+
error: `unknown remote node "${nodeName}"`
|
|
42982
|
+
};
|
|
42586
42983
|
}
|
|
42587
|
-
async function originLabel(args) {
|
|
42588
|
-
const self = await
|
|
42984
|
+
async function originLabel(args, peer) {
|
|
42985
|
+
const self = await selfPeerNodeForCell(peer.viaCellId).catch(() => null);
|
|
42589
42986
|
const local = args.fromTitle?.trim() || args.fromKey?.trim() || "peer";
|
|
42590
42987
|
return self?.nodeName ? `${self.nodeName}/${local}` : local;
|
|
42591
42988
|
}
|
|
42592
42989
|
async function forward(args) {
|
|
42593
|
-
const
|
|
42594
|
-
if (!
|
|
42990
|
+
const lookup = await findNode(args.toNode, args.fromTopicId).catch(() => ({ ok: false, error: `unknown remote node "${args.toNode}"` }));
|
|
42991
|
+
if (!lookup.ok)
|
|
42992
|
+
return lookup;
|
|
42993
|
+
const node = lookup.node;
|
|
42994
|
+
if (node.self)
|
|
42595
42995
|
return { ok: false, error: `unknown remote node "${args.toNode}"` };
|
|
42596
|
-
const self = await
|
|
42996
|
+
const self = await selfPeerNodeForCell(node.viaCellId).catch(() => null);
|
|
42597
42997
|
if (!self)
|
|
42598
42998
|
return { ok: false, error: "local peer node is not attached" };
|
|
42599
42999
|
if (!self.isPrimary && !node.isPrimary) {
|
|
42600
43000
|
return { ok: false, error: "worker peer calls must target the primary hub" };
|
|
42601
43001
|
}
|
|
42602
|
-
const fromLabel = await originLabel(args);
|
|
43002
|
+
const fromLabel = await originLabel(args, node);
|
|
42603
43003
|
const requestId = args.requestId;
|
|
42604
43004
|
if (args.action === "ask") {
|
|
42605
43005
|
if (!requestId || !args.fromTopicId || !args.fromKey || !args.message) {
|
|
@@ -42669,14 +43069,20 @@ async function forward(args) {
|
|
|
42669
43069
|
...args.sourceQueryId ? { sourceQueryId: args.sourceQueryId } : {}
|
|
42670
43070
|
});
|
|
42671
43071
|
}
|
|
42672
|
-
async function sessions(userId, sourceQueryId) {
|
|
43072
|
+
async function sessions(userId, sourceQueryId, fromTopicId) {
|
|
42673
43073
|
const nodes = await listPeerNodes().catch(() => []);
|
|
42674
|
-
const
|
|
42675
|
-
if (
|
|
43074
|
+
const selves = new Map(nodes.filter((node) => node.self).map((node) => [node.viaCellId, node]));
|
|
43075
|
+
if (selves.size === 0)
|
|
42676
43076
|
return { ok: false, nodes: [] };
|
|
43077
|
+
const callerScope = fromTopicId ? getTopic(fromTopicId)?.surfaceScope ?? null : null;
|
|
42677
43078
|
return {
|
|
42678
43079
|
ok: true,
|
|
42679
|
-
nodes: await Promise.all(nodes.filter((node) =>
|
|
43080
|
+
nodes: await Promise.all(nodes.filter((node) => {
|
|
43081
|
+
const self = selves.get(node.viaCellId);
|
|
43082
|
+
if (callerScope && surfaceScopeForCell(node.viaCellId) !== callerScope)
|
|
43083
|
+
return false;
|
|
43084
|
+
return !node.self && node.nodeName && self && (self.isPrimary || node.isPrimary);
|
|
43085
|
+
}).map(async (node) => {
|
|
42680
43086
|
const result = await postPeer(node, "/api/v1/peer/sessions", {
|
|
42681
43087
|
v: PEER_PROTOCOL_VERSION,
|
|
42682
43088
|
userId,
|
|
@@ -42714,7 +43120,7 @@ function flushPeerReplyOutbox() {
|
|
|
42714
43120
|
toTopic: "",
|
|
42715
43121
|
userId: pending2.user_id,
|
|
42716
43122
|
fromTitle: pending2.source_title
|
|
42717
|
-
});
|
|
43123
|
+
}, node);
|
|
42718
43124
|
const result = await postPeer(node, "/api/v1/peer/reply", {
|
|
42719
43125
|
v: PEER_PROTOCOL_VERSION,
|
|
42720
43126
|
requestId: pending2.request_id,
|
|
@@ -42797,6 +43203,7 @@ var init_session_bridge = __esm(async () => {
|
|
|
42797
43203
|
await init_central();
|
|
42798
43204
|
init_protocol();
|
|
42799
43205
|
await init_store2();
|
|
43206
|
+
await init_workspace_scope();
|
|
42800
43207
|
PENDING_ASK_TTL_MS2 = 15 * 60 * 1000;
|
|
42801
43208
|
otiumPeerSessionBridge = { forward, sessions, reply };
|
|
42802
43209
|
});
|
|
@@ -42884,7 +43291,7 @@ function startPeerSessionBridgeIpc(bridge) {
|
|
|
42884
43291
|
return Response.json(await bridge.forward(payload.args));
|
|
42885
43292
|
}
|
|
42886
43293
|
if (payload.action === "sessions") {
|
|
42887
|
-
return Response.json(await bridge.sessions(payload.userId, payload.sourceQueryId));
|
|
43294
|
+
return Response.json(await bridge.sessions(payload.userId, payload.sourceQueryId, payload.fromTopicId));
|
|
42888
43295
|
}
|
|
42889
43296
|
if (payload.action === "reply") {
|
|
42890
43297
|
return Response.json(await bridge.reply(payload.route, payload.sourceTitle, payload.replyText, payload.kind));
|
|
@@ -43501,14 +43908,14 @@ import {
|
|
|
43501
43908
|
existsSync as existsSync38,
|
|
43502
43909
|
fsyncSync as fsyncSync3,
|
|
43503
43910
|
linkSync as linkSync2,
|
|
43504
|
-
mkdirSync as
|
|
43911
|
+
mkdirSync as mkdirSync35,
|
|
43505
43912
|
openSync as openSync6,
|
|
43506
|
-
readFileSync as
|
|
43913
|
+
readFileSync as readFileSync30,
|
|
43507
43914
|
renameSync as renameSync17,
|
|
43508
43915
|
unlinkSync as unlinkSync24,
|
|
43509
|
-
writeFileSync as
|
|
43916
|
+
writeFileSync as writeFileSync26
|
|
43510
43917
|
} from "fs";
|
|
43511
|
-
import { dirname as
|
|
43918
|
+
import { dirname as dirname23, resolve as resolve26 } from "path";
|
|
43512
43919
|
function parseEnrollmentInvite(code) {
|
|
43513
43920
|
let parsed;
|
|
43514
43921
|
try {
|
|
@@ -43529,14 +43936,14 @@ function parseEnrollmentInvite(code) {
|
|
|
43529
43936
|
return { v: 2, central, token };
|
|
43530
43937
|
}
|
|
43531
43938
|
function pendingEnrollmentPath() {
|
|
43532
|
-
return
|
|
43939
|
+
return resolve26(DATA_DIR, "otium-enrollment-pending.json");
|
|
43533
43940
|
}
|
|
43534
43941
|
function isEnrollmentPending(invite) {
|
|
43535
43942
|
const path = pendingEnrollmentPath();
|
|
43536
43943
|
if (!existsSync38(path))
|
|
43537
43944
|
return false;
|
|
43538
43945
|
try {
|
|
43539
|
-
const saved = JSON.parse(
|
|
43946
|
+
const saved = JSON.parse(readFileSync30(path, "utf8"));
|
|
43540
43947
|
return saved.central === invite.central && saved.token === invite.token;
|
|
43541
43948
|
} catch {
|
|
43542
43949
|
return false;
|
|
@@ -43545,7 +43952,7 @@ function isEnrollmentPending(invite) {
|
|
|
43545
43952
|
function loadOrCreatePending(invite, nodeName) {
|
|
43546
43953
|
const path = pendingEnrollmentPath();
|
|
43547
43954
|
if (existsSync38(path)) {
|
|
43548
|
-
const saved = JSON.parse(
|
|
43955
|
+
const saved = JSON.parse(readFileSync30(path, "utf8"));
|
|
43549
43956
|
if (saved.central === invite.central && saved.token === invite.token)
|
|
43550
43957
|
return saved;
|
|
43551
43958
|
throw new Error(`another Otium enrollment is pending at ${path}`);
|
|
@@ -43558,12 +43965,12 @@ function loadOrCreatePending(invite, nodeName) {
|
|
|
43558
43965
|
publicKey: pair.publicKey.export({ format: "der", type: "spki" }).toString("base64url"),
|
|
43559
43966
|
...nodeName ? { nodeName } : {}
|
|
43560
43967
|
};
|
|
43561
|
-
|
|
43562
|
-
const temporaryPath =
|
|
43968
|
+
mkdirSync35(dirname23(path), { recursive: true });
|
|
43969
|
+
const temporaryPath = resolve26(dirname23(path), `.otium-enrollment-pending.json.${process.pid}.${randomUUID31()}.tmp`);
|
|
43563
43970
|
let fd;
|
|
43564
43971
|
try {
|
|
43565
43972
|
fd = openSync6(temporaryPath, "wx", 384);
|
|
43566
|
-
|
|
43973
|
+
writeFileSync26(fd, `${JSON.stringify(pending2, null, 2)}
|
|
43567
43974
|
`, "utf8");
|
|
43568
43975
|
fsyncSync3(fd);
|
|
43569
43976
|
closeSync6(fd);
|
|
@@ -43571,7 +43978,7 @@ function loadOrCreatePending(invite, nodeName) {
|
|
|
43571
43978
|
linkSync2(temporaryPath, path);
|
|
43572
43979
|
unlinkSync24(temporaryPath);
|
|
43573
43980
|
chmodSync8(path, 384);
|
|
43574
|
-
const directoryFd = openSync6(
|
|
43981
|
+
const directoryFd = openSync6(dirname23(path), "r");
|
|
43575
43982
|
try {
|
|
43576
43983
|
fsyncSync3(directoryFd);
|
|
43577
43984
|
} finally {
|
|
@@ -43583,7 +43990,7 @@ function loadOrCreatePending(invite, nodeName) {
|
|
|
43583
43990
|
if (existsSync38(temporaryPath))
|
|
43584
43991
|
unlinkSync24(temporaryPath);
|
|
43585
43992
|
if (error2.code === "EEXIST" && existsSync38(path)) {
|
|
43586
|
-
const saved = JSON.parse(
|
|
43993
|
+
const saved = JSON.parse(readFileSync30(path, "utf8"));
|
|
43587
43994
|
if (saved.central === invite.central && saved.token === invite.token)
|
|
43588
43995
|
return saved;
|
|
43589
43996
|
}
|
|
@@ -43593,12 +44000,12 @@ function loadOrCreatePending(invite, nodeName) {
|
|
|
43593
44000
|
}
|
|
43594
44001
|
function replacePendingEnrollment(pending2) {
|
|
43595
44002
|
const path = pendingEnrollmentPath();
|
|
43596
|
-
const directory =
|
|
43597
|
-
const temporaryPath =
|
|
44003
|
+
const directory = dirname23(path);
|
|
44004
|
+
const temporaryPath = resolve26(directory, `.otium-enrollment-pending.json.${process.pid}.${randomUUID31()}.tmp`);
|
|
43598
44005
|
let fd;
|
|
43599
44006
|
try {
|
|
43600
44007
|
fd = openSync6(temporaryPath, "wx", 384);
|
|
43601
|
-
|
|
44008
|
+
writeFileSync26(fd, `${JSON.stringify(pending2, null, 2)}
|
|
43602
44009
|
`, "utf8");
|
|
43603
44010
|
fsyncSync3(fd);
|
|
43604
44011
|
closeSync6(fd);
|
|
@@ -43620,7 +44027,7 @@ function replacePendingEnrollment(pending2) {
|
|
|
43620
44027
|
}
|
|
43621
44028
|
function recordClaimedCredential(pending2, join43) {
|
|
43622
44029
|
withJoinCredentialLock(() => {
|
|
43623
|
-
const current3 = JSON.parse(
|
|
44030
|
+
const current3 = JSON.parse(readFileSync30(pendingEnrollmentPath(), "utf8"));
|
|
43624
44031
|
if (current3.central !== pending2.central || current3.token !== pending2.token || current3.idempotencyKey !== pending2.idempotencyKey || current3.publicKey !== pending2.publicKey) {
|
|
43625
44032
|
throw new Error("pending Otium enrollment changed while its claim was in flight");
|
|
43626
44033
|
}
|
|
@@ -43699,7 +44106,7 @@ async function claimEnrollment(invite, nodeName) {
|
|
|
43699
44106
|
function commitEnrollment(join43, options = {}) {
|
|
43700
44107
|
return withJoinCredentialLock(() => {
|
|
43701
44108
|
const pendingPath = pendingEnrollmentPath();
|
|
43702
|
-
const pending2 = existsSync38(pendingPath) ? JSON.parse(
|
|
44109
|
+
const pending2 = existsSync38(pendingPath) ? JSON.parse(readFileSync30(pendingPath, "utf8")) : null;
|
|
43703
44110
|
const digest = joinCredentialDigest(join43);
|
|
43704
44111
|
if (pending2 && (!pending2.claimed || pending2.claimed.digest !== digest || pending2.claimed.cellId !== join43.cellId)) {
|
|
43705
44112
|
throw new Error(`pending Otium enrollment at ${pendingPath} does not match these credentials`);
|
|
@@ -43711,7 +44118,7 @@ function commitEnrollment(join43, options = {}) {
|
|
|
43711
44118
|
if (!pending2)
|
|
43712
44119
|
return path;
|
|
43713
44120
|
unlinkSync24(pendingPath);
|
|
43714
|
-
const directoryFd = openSync6(
|
|
44121
|
+
const directoryFd = openSync6(dirname23(pendingPath), "r");
|
|
43715
44122
|
try {
|
|
43716
44123
|
fsyncSync3(directoryFd);
|
|
43717
44124
|
} finally {
|
|
@@ -43750,6 +44157,8 @@ async function forwardGatewayRequest(req, options) {
|
|
|
43750
44157
|
target.search = url.search;
|
|
43751
44158
|
const headers = new Headers(req.headers);
|
|
43752
44159
|
headers.set("authorization", `Bearer ${NODE_CONTROL_TOKEN}`);
|
|
44160
|
+
headers.set(SURFACE_SCOPE_HEADER, options.surfaceScope ?? "");
|
|
44161
|
+
headers.set(SURFACE_SCOPE_STRICT_HEADER, options.strictScope ? "1" : "0");
|
|
43753
44162
|
headers.delete("transfer-encoding");
|
|
43754
44163
|
headers.delete("content-length");
|
|
43755
44164
|
headers.delete("host");
|
|
@@ -43764,7 +44173,7 @@ async function forwardGatewayRequest(req, options) {
|
|
|
43764
44173
|
signal: req.signal
|
|
43765
44174
|
}));
|
|
43766
44175
|
}
|
|
43767
|
-
var RUNTIME_CONTRACT_PATH = "/api/v1/control/runtime/v1", OTIUM_GATEWAY_FORWARD_PREFIX = "/api/v1/peer/runtime";
|
|
44176
|
+
var RUNTIME_CONTRACT_PATH = "/api/v1/control/runtime/v1", SURFACE_SCOPE_HEADER = "x-negotium-surface-scope", SURFACE_SCOPE_STRICT_HEADER = "x-negotium-surface-scope-strict", OTIUM_GATEWAY_FORWARD_PREFIX = "/api/v1/peer/runtime";
|
|
43768
44177
|
var init_gateway_forward = __esm(async () => {
|
|
43769
44178
|
await init_src();
|
|
43770
44179
|
});
|
|
@@ -43797,7 +44206,7 @@ function checkProtocol(body) {
|
|
|
43797
44206
|
return null;
|
|
43798
44207
|
}
|
|
43799
44208
|
async function requirePeer(req) {
|
|
43800
|
-
if (!
|
|
44209
|
+
if (!isOtiumCentralConfigured()) {
|
|
43801
44210
|
return { ok: false, response: jsonError2("multi-node is disabled", 403) };
|
|
43802
44211
|
}
|
|
43803
44212
|
const token = bearer(req);
|
|
@@ -43811,8 +44220,22 @@ async function requirePeer(req) {
|
|
|
43811
44220
|
function requirePrimaryOrigin(peer) {
|
|
43812
44221
|
return peer.verified.fromIsPrimary ? null : jsonError2("only the workspace hub may call this endpoint", 403);
|
|
43813
44222
|
}
|
|
43814
|
-
function
|
|
43815
|
-
|
|
44223
|
+
function peerTopicByName(toTopic, userId, peer) {
|
|
44224
|
+
const surfaceScope = surfaceScopeForCell(peer.verified.viaCellId);
|
|
44225
|
+
const scoped = getTopicByNameForUser(toTopic, userId, { surface: "otium", surfaceScope });
|
|
44226
|
+
if (scoped)
|
|
44227
|
+
return scoped;
|
|
44228
|
+
if (!unscopedRoomsAddressable())
|
|
44229
|
+
return null;
|
|
44230
|
+
return getTopicByNameForUser(toTopic, userId, { surface: "otium", surfaceScope: null });
|
|
44231
|
+
}
|
|
44232
|
+
function peerAddressable(topic, peer) {
|
|
44233
|
+
if (topic.surface !== "otium")
|
|
44234
|
+
return false;
|
|
44235
|
+
const roomScope = topic.surfaceScope ?? null;
|
|
44236
|
+
if (!roomScope)
|
|
44237
|
+
return unscopedRoomsAddressable();
|
|
44238
|
+
return roomScope === surfaceScopeForCell(peer.verified.viaCellId);
|
|
43816
44239
|
}
|
|
43817
44240
|
function localCapabilities() {
|
|
43818
44241
|
const agents = SUPPORTED_AGENTS.map((kind) => {
|
|
@@ -43882,8 +44305,8 @@ async function handleAbort(req) {
|
|
|
43882
44305
|
const toTopic = str(body, "toTopic");
|
|
43883
44306
|
if (!userId || !toTopic)
|
|
43884
44307
|
return jsonError2("userId and toTopic are required", 400);
|
|
43885
|
-
const topic =
|
|
43886
|
-
if (!topic || !peerAddressable(topic)) {
|
|
44308
|
+
const topic = peerTopicByName(toTopic, userId, peer);
|
|
44309
|
+
if (!topic || !peerAddressable(topic, peer)) {
|
|
43887
44310
|
return jsonError2(`shared topic "${toTopic}" not found on this node`, 404);
|
|
43888
44311
|
}
|
|
43889
44312
|
appendJsonlEntry(sessionInboxPath(userId, topic.id), {
|
|
@@ -43924,8 +44347,8 @@ async function handleTell(req) {
|
|
|
43924
44347
|
if (depth > MAX_TELL_DEPTH) {
|
|
43925
44348
|
return jsonError2(`tell depth limit exceeded (max ${MAX_TELL_DEPTH})`, 400);
|
|
43926
44349
|
}
|
|
43927
|
-
const topic =
|
|
43928
|
-
if (!topic || !peerAddressable(topic)) {
|
|
44350
|
+
const topic = peerTopicByName(toTopic, userId, peer);
|
|
44351
|
+
if (!topic || !peerAddressable(topic, peer)) {
|
|
43929
44352
|
return jsonError2(`shared topic "${toTopic}" not found on this node`, 404);
|
|
43930
44353
|
}
|
|
43931
44354
|
const claim = claimInboundPeerMessage({
|
|
@@ -43982,7 +44405,7 @@ async function handleSessions(req) {
|
|
|
43982
44405
|
const userId = str(body, "userId");
|
|
43983
44406
|
if (!userId)
|
|
43984
44407
|
return jsonError2("userId is required", 400);
|
|
43985
|
-
const topics = listTopics({ surface: "otium" }).filter((topic) => topic.kind !== "manager" && !topic.isSubagent && peerAddressable(topic) && topic.participants.some((p) => p.userId === userId));
|
|
44408
|
+
const topics = listTopics({ surface: "otium" }).filter((topic) => topic.kind !== "manager" && !topic.isSubagent && peerAddressable(topic, peer) && topic.participants.some((p) => p.userId === userId));
|
|
43986
44409
|
const titleCounts = new Map;
|
|
43987
44410
|
for (const topic of topics) {
|
|
43988
44411
|
const normalized = topic.title.toLowerCase();
|
|
@@ -44030,8 +44453,8 @@ async function handleAsk(req) {
|
|
|
44030
44453
|
if (!Number.isInteger(fromDepth) || fromDepth < 0) {
|
|
44031
44454
|
return jsonError2("fromDepth must be a non-negative integer", 400);
|
|
44032
44455
|
}
|
|
44033
|
-
const topic =
|
|
44034
|
-
if (!topic || !peerAddressable(topic)) {
|
|
44456
|
+
const topic = peerTopicByName(toTopic, userId, peer);
|
|
44457
|
+
if (!topic || !peerAddressable(topic, peer)) {
|
|
44035
44458
|
return jsonError2(`shared topic "${toTopic}" not found on this node`, 404);
|
|
44036
44459
|
}
|
|
44037
44460
|
if (!topic.agent)
|
|
@@ -44151,7 +44574,7 @@ async function handleOtiumPeerRequest(req) {
|
|
|
44151
44574
|
const url = new URL(req.url);
|
|
44152
44575
|
const path = url.pathname;
|
|
44153
44576
|
if (path === "/ready" && req.method === "GET") {
|
|
44154
|
-
if (!
|
|
44577
|
+
if (!isOtiumCentralConfigured())
|
|
44155
44578
|
return null;
|
|
44156
44579
|
return Response.json({ ok: true });
|
|
44157
44580
|
}
|
|
@@ -44170,7 +44593,9 @@ async function handleOtiumPeerRequest(req) {
|
|
|
44170
44593
|
return jsonError2("canonical Negotium node is unavailable", 503);
|
|
44171
44594
|
}
|
|
44172
44595
|
return forwardGatewayRequest(req, {
|
|
44173
|
-
nodeOrigin: `http://127.0.0.1:${node.info.port}
|
|
44596
|
+
nodeOrigin: `http://127.0.0.1:${node.info.port}`,
|
|
44597
|
+
surfaceScope: surfaceScopeForCell(peer.verified.viaCellId),
|
|
44598
|
+
strictScope: !unscopedRoomsAddressable()
|
|
44174
44599
|
});
|
|
44175
44600
|
}
|
|
44176
44601
|
if (req.method === "GET") {
|
|
@@ -44215,41 +44640,89 @@ var init_peer_server = __esm(async () => {
|
|
|
44215
44640
|
init_protocol();
|
|
44216
44641
|
await init_session_bridge();
|
|
44217
44642
|
await init_store2();
|
|
44643
|
+
await init_workspace_scope();
|
|
44218
44644
|
RUNTIME_VERSION = NEGOTIUM_VERSION;
|
|
44219
44645
|
});
|
|
44220
44646
|
|
|
44221
44647
|
// ../../adapters/otium/src/index.ts
|
|
44648
|
+
function acquireGlobalOtiumServices() {
|
|
44649
|
+
if (globalServices) {
|
|
44650
|
+
globalServices.refs += 1;
|
|
44651
|
+
} else {
|
|
44652
|
+
const unregisterRuntimeBridge = registerPeerRuntimeBridge(otiumPeerRuntimeBridge);
|
|
44653
|
+
const unregisterSessionBridge = registerPeerSessionBridge(otiumPeerSessionBridge);
|
|
44654
|
+
const sessionBridgeIpc = startPeerSessionBridgeIpc(otiumPeerSessionBridge);
|
|
44655
|
+
const canonicalMcpBridge = startCanonicalMcpBridge();
|
|
44656
|
+
const stopPeerReplyOutbox = startPeerReplyOutboxWorker();
|
|
44657
|
+
const uninstallFileHooks = installPeerFileHooks();
|
|
44658
|
+
const unsubscribeTopicCleanup = runtimeBus().subscribe((event) => {
|
|
44659
|
+
if (event.type !== "topic-deleted")
|
|
44660
|
+
return;
|
|
44661
|
+
const removed = cleanupPeerStateForLocalTopic(event.topicId);
|
|
44662
|
+
if (removed.inboxRequests + removed.remoteAsks > 0) {
|
|
44663
|
+
logger.info({ topicId: event.topicId, ...removed }, "otium: removed peer state for deleted local topic");
|
|
44664
|
+
}
|
|
44665
|
+
});
|
|
44666
|
+
failInterruptedRemoteAskCallbacks().then((failedAsks) => {
|
|
44667
|
+
if (failedAsks > 0) {
|
|
44668
|
+
logger.warn({ failedAsks }, "otium: failed remote asks interrupted by previous process");
|
|
44669
|
+
}
|
|
44670
|
+
});
|
|
44671
|
+
globalServices = {
|
|
44672
|
+
refs: 1,
|
|
44673
|
+
stop: () => {
|
|
44674
|
+
unsubscribeTopicCleanup();
|
|
44675
|
+
unregisterRuntimeBridge();
|
|
44676
|
+
unregisterSessionBridge();
|
|
44677
|
+
sessionBridgeIpc.stop();
|
|
44678
|
+
canonicalMcpBridge.stop();
|
|
44679
|
+
stopPeerReplyOutbox();
|
|
44680
|
+
uninstallFileHooks();
|
|
44681
|
+
}
|
|
44682
|
+
};
|
|
44683
|
+
}
|
|
44684
|
+
let released = false;
|
|
44685
|
+
return () => {
|
|
44686
|
+
if (released || !globalServices)
|
|
44687
|
+
return;
|
|
44688
|
+
released = true;
|
|
44689
|
+
globalServices.refs -= 1;
|
|
44690
|
+
if (globalServices.refs > 0)
|
|
44691
|
+
return;
|
|
44692
|
+
globalServices.stop();
|
|
44693
|
+
globalServices = null;
|
|
44694
|
+
};
|
|
44695
|
+
}
|
|
44696
|
+
function refreshDefaultSurfaceScope() {
|
|
44697
|
+
const scopes = [...mountedScopes.values()];
|
|
44698
|
+
setDefaultSurfaceScope(scopes.length === 1 ? scopes[0] ?? null : null);
|
|
44699
|
+
setSurfaceScopeRequired(scopes.length > 1);
|
|
44700
|
+
}
|
|
44222
44701
|
function startOtiumNodeRuntime(options) {
|
|
44223
44702
|
const { join: join43 } = options;
|
|
44224
|
-
|
|
44225
|
-
const
|
|
44226
|
-
const unregisterSessionBridge = registerPeerSessionBridge(otiumPeerSessionBridge);
|
|
44227
|
-
const sessionBridgeIpc = startPeerSessionBridgeIpc(otiumPeerSessionBridge);
|
|
44228
|
-
const canonicalMcpBridge = startCanonicalMcpBridge();
|
|
44229
|
-
const stopPeerReplyOutbox = startPeerReplyOutboxWorker();
|
|
44230
|
-
failInterruptedRemoteAskCallbacks().then((failedAsks) => {
|
|
44231
|
-
if (failedAsks > 0) {
|
|
44232
|
-
logger.warn({ failedAsks }, "otium: failed remote asks interrupted by previous process");
|
|
44233
|
-
}
|
|
44234
|
-
});
|
|
44235
|
-
const uninstallFileHooks = installPeerFileHooks();
|
|
44236
|
-
const unsubscribeTopicCleanup = runtimeBus().subscribe((event) => {
|
|
44237
|
-
if (event.type !== "topic-deleted")
|
|
44238
|
-
return;
|
|
44239
|
-
const removed = cleanupPeerStateForLocalTopic(event.topicId);
|
|
44240
|
-
if (removed.inboxRequests + removed.remoteAsks > 0) {
|
|
44241
|
-
logger.info({ topicId: event.topicId, ...removed }, "otium: removed peer state for deleted local topic");
|
|
44242
|
-
}
|
|
44243
|
-
});
|
|
44703
|
+
attachOtiumCentralCell(join43);
|
|
44704
|
+
const releaseGlobals = acquireGlobalOtiumServices();
|
|
44244
44705
|
let stopped = false;
|
|
44245
44706
|
logger.info({ central: join43.central, cellId: join43.cellId }, "otium: worker mode enabled");
|
|
44246
|
-
|
|
44707
|
+
mountedScopes.set(join43.cellId, cachedSurfaceScope(join43));
|
|
44708
|
+
refreshDefaultSurfaceScope();
|
|
44709
|
+
selfPeerNodeForCell(join43.cellId).then((self) => {
|
|
44247
44710
|
if (self) {
|
|
44248
44711
|
logger.info({ nodeName: self.nodeName, baseUrl: self.baseUrl }, "otium: attached to workspace");
|
|
44249
44712
|
}
|
|
44250
44713
|
}).catch((err2) => {
|
|
44251
44714
|
logger.warn({ err: err2 }, "otium: self check against central failed (will retry per request)");
|
|
44252
44715
|
});
|
|
44716
|
+
resolveSurfaceScope(join43).then((scope) => {
|
|
44717
|
+
if (!scope || stopped)
|
|
44718
|
+
return;
|
|
44719
|
+
mountedScopes.set(join43.cellId, scope);
|
|
44720
|
+
refreshDefaultSurfaceScope();
|
|
44721
|
+
if (mountedScopes.size === 1)
|
|
44722
|
+
stampUnscopedOtiumTopics(scope);
|
|
44723
|
+
}).catch((err2) => {
|
|
44724
|
+
logger.warn({ err: err2 }, "otium: workspace scope resolution failed");
|
|
44725
|
+
});
|
|
44253
44726
|
return {
|
|
44254
44727
|
name: "otium",
|
|
44255
44728
|
join: join43,
|
|
@@ -44257,14 +44730,10 @@ function startOtiumNodeRuntime(options) {
|
|
|
44257
44730
|
if (stopped)
|
|
44258
44731
|
return;
|
|
44259
44732
|
stopped = true;
|
|
44260
|
-
|
|
44261
|
-
|
|
44262
|
-
|
|
44263
|
-
|
|
44264
|
-
canonicalMcpBridge.stop();
|
|
44265
|
-
stopPeerReplyOutbox();
|
|
44266
|
-
uninstallFileHooks();
|
|
44267
|
-
configureOtiumCentral(null);
|
|
44733
|
+
mountedScopes.delete(join43.cellId);
|
|
44734
|
+
refreshDefaultSurfaceScope();
|
|
44735
|
+
detachOtiumCentralCell(join43.cellId);
|
|
44736
|
+
releaseGlobals();
|
|
44268
44737
|
}
|
|
44269
44738
|
};
|
|
44270
44739
|
}
|
|
@@ -44302,7 +44771,7 @@ function startOtiumAdapter(options) {
|
|
|
44302
44771
|
}
|
|
44303
44772
|
};
|
|
44304
44773
|
}
|
|
44305
|
-
var otiumAdapter;
|
|
44774
|
+
var globalServices = null, mountedScopes, otiumAdapter;
|
|
44306
44775
|
var init_src8 = __esm(async () => {
|
|
44307
44776
|
await init_src();
|
|
44308
44777
|
await init_canonical_mcp_bridge();
|
|
@@ -44314,6 +44783,7 @@ var init_src8 = __esm(async () => {
|
|
|
44314
44783
|
init_session_bridge_ipc();
|
|
44315
44784
|
await init_store2();
|
|
44316
44785
|
init_tunnel_client();
|
|
44786
|
+
await init_workspace_scope();
|
|
44317
44787
|
await init_central();
|
|
44318
44788
|
await init_enrollment();
|
|
44319
44789
|
await init_join();
|
|
@@ -44323,6 +44793,8 @@ var init_src8 = __esm(async () => {
|
|
|
44323
44793
|
await init_runtime_bridge();
|
|
44324
44794
|
await init_store2();
|
|
44325
44795
|
init_tunnel_client();
|
|
44796
|
+
await init_workspace_scope();
|
|
44797
|
+
mountedScopes = new Map;
|
|
44326
44798
|
otiumAdapter = defineNegotiumAdapter({
|
|
44327
44799
|
name: "otium",
|
|
44328
44800
|
capabilities: {
|
|
@@ -44342,12 +44814,59 @@ var init_src8 = __esm(async () => {
|
|
|
44342
44814
|
// ../../adapters/otium/src/node-runtime.ts
|
|
44343
44815
|
var exports_node_runtime = {};
|
|
44344
44816
|
__export(exports_node_runtime, {
|
|
44817
|
+
reconcileOtiumWorkspaces: () => reconcileOtiumWorkspaces,
|
|
44818
|
+
mountedOtiumWorkspaces: () => mountedOtiumWorkspaces,
|
|
44345
44819
|
mountConfiguredOtiumNodeRuntime: () => mountConfiguredOtiumNodeRuntime,
|
|
44346
44820
|
handleOtiumAdapterControlRequest: () => handleOtiumAdapterControlRequest,
|
|
44821
|
+
detachOtiumWorkspace: () => detachOtiumWorkspace,
|
|
44822
|
+
attachOtiumWorkspace: () => attachOtiumWorkspace,
|
|
44347
44823
|
OTIUM_ADAPTER_CONTROL_PREFIX: () => OTIUM_ADAPTER_CONTROL_PREFIX,
|
|
44348
44824
|
OTIUM_ADAPTER_CONTROL_HEADER: () => OTIUM_ADAPTER_CONTROL_HEADER,
|
|
44349
44825
|
MAX_PEER_REQUEST_BODY_BYTES: () => MAX_PEER_REQUEST_BODY_BYTES
|
|
44350
44826
|
});
|
|
44827
|
+
function sameCredentials(left, right) {
|
|
44828
|
+
return left.central === right.central && left.relay === right.relay && left.secret === right.secret;
|
|
44829
|
+
}
|
|
44830
|
+
function attachOtiumWorkspace(join43) {
|
|
44831
|
+
const current3 = mounted.get(join43.cellId);
|
|
44832
|
+
if (current3) {
|
|
44833
|
+
if (sameCredentials(current3.join, join43))
|
|
44834
|
+
return false;
|
|
44835
|
+
detachOtiumWorkspace(join43.cellId);
|
|
44836
|
+
}
|
|
44837
|
+
mounted.set(join43.cellId, startOtiumNodeRuntime({ join: join43 }));
|
|
44838
|
+
return true;
|
|
44839
|
+
}
|
|
44840
|
+
function detachOtiumWorkspace(cellId) {
|
|
44841
|
+
const runtime2 = mounted.get(cellId);
|
|
44842
|
+
if (!runtime2)
|
|
44843
|
+
return false;
|
|
44844
|
+
mounted.delete(cellId);
|
|
44845
|
+
runtime2.stop();
|
|
44846
|
+
return true;
|
|
44847
|
+
}
|
|
44848
|
+
function mountedOtiumWorkspaces() {
|
|
44849
|
+
return [...mounted.values()].map((runtime2) => runtime2.join);
|
|
44850
|
+
}
|
|
44851
|
+
function reconcileOtiumWorkspaces(joins = loadJoins()) {
|
|
44852
|
+
const wanted = new Map(joins.map((join43) => [join43.cellId, join43]));
|
|
44853
|
+
const detached = [];
|
|
44854
|
+
for (const cellId of [...mounted.keys()]) {
|
|
44855
|
+
if (wanted.has(cellId))
|
|
44856
|
+
continue;
|
|
44857
|
+
detachOtiumWorkspace(cellId);
|
|
44858
|
+
detached.push(cellId);
|
|
44859
|
+
}
|
|
44860
|
+
const attached = [];
|
|
44861
|
+
for (const [cellId, join43] of wanted) {
|
|
44862
|
+
if (attachOtiumWorkspace(join43))
|
|
44863
|
+
attached.push(cellId);
|
|
44864
|
+
}
|
|
44865
|
+
if (attached.length > 0 || detached.length > 0) {
|
|
44866
|
+
logger.info({ attached, detached }, "otium: workspace attachments reconciled");
|
|
44867
|
+
}
|
|
44868
|
+
return { attached, detached };
|
|
44869
|
+
}
|
|
44351
44870
|
async function handleOtiumAdapterControlRequest(req) {
|
|
44352
44871
|
const url = new URL(req.url);
|
|
44353
44872
|
if (!url.pathname.startsWith(`${OTIUM_ADAPTER_CONTROL_PREFIX}/`))
|
|
@@ -44356,6 +44875,25 @@ async function handleOtiumAdapterControlRequest(req) {
|
|
|
44356
44875
|
return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
|
44357
44876
|
}
|
|
44358
44877
|
const peerPath = url.pathname.slice(OTIUM_ADAPTER_CONTROL_PREFIX.length) || "/";
|
|
44878
|
+
if (peerPath === OTIUM_WORKSPACES_CONTROL_PATH) {
|
|
44879
|
+
if (req.headers.get(OTIUM_RELAYED_HEADER)) {
|
|
44880
|
+
return Response.json({ ok: false, error: "not found" }, { status: 404 });
|
|
44881
|
+
}
|
|
44882
|
+
if (req.method === "GET") {
|
|
44883
|
+
return Response.json({
|
|
44884
|
+
ok: true,
|
|
44885
|
+
workspaces: mountedOtiumWorkspaces().map((join43) => ({
|
|
44886
|
+
cellId: join43.cellId,
|
|
44887
|
+
central: join43.central
|
|
44888
|
+
}))
|
|
44889
|
+
});
|
|
44890
|
+
}
|
|
44891
|
+
if (req.method === "POST") {
|
|
44892
|
+
const result = reconcileOtiumWorkspaces();
|
|
44893
|
+
return Response.json({ ok: true, ...result });
|
|
44894
|
+
}
|
|
44895
|
+
return Response.json({ ok: false, error: "method not allowed" }, { status: 405 });
|
|
44896
|
+
}
|
|
44359
44897
|
const peerUrl = new URL(req.url);
|
|
44360
44898
|
peerUrl.pathname = peerPath;
|
|
44361
44899
|
const headers = new Headers(req.headers);
|
|
@@ -44364,29 +44902,72 @@ async function handleOtiumAdapterControlRequest(req) {
|
|
|
44364
44902
|
return await handleOtiumPeerRequest(new Request(peerUrl.toString(), { method: req.method, headers, body, signal: req.signal })) ?? Response.json({ ok: false, error: "Otium route not found" }, { status: 404 });
|
|
44365
44903
|
}
|
|
44366
44904
|
function mountConfiguredOtiumNodeRuntime() {
|
|
44367
|
-
const
|
|
44368
|
-
if (
|
|
44905
|
+
const joins = loadJoins();
|
|
44906
|
+
if (joins.length === 0)
|
|
44369
44907
|
return null;
|
|
44370
|
-
|
|
44908
|
+
for (const join43 of joins)
|
|
44909
|
+
attachOtiumWorkspace(join43);
|
|
44371
44910
|
registerNodeRequestHandler("otium-adapter-control", handleOtiumAdapterControlRequest);
|
|
44372
44911
|
let stopped = false;
|
|
44373
44912
|
return {
|
|
44374
|
-
|
|
44913
|
+
name: "otium",
|
|
44914
|
+
join: joins[0],
|
|
44375
44915
|
stop() {
|
|
44376
44916
|
if (stopped)
|
|
44377
44917
|
return;
|
|
44378
44918
|
stopped = true;
|
|
44379
44919
|
unregisterNodeRequestHandler("otium-adapter-control");
|
|
44380
|
-
|
|
44920
|
+
for (const cellId of [...mounted.keys()])
|
|
44921
|
+
detachOtiumWorkspace(cellId);
|
|
44381
44922
|
}
|
|
44382
44923
|
};
|
|
44383
44924
|
}
|
|
44925
|
+
var mounted;
|
|
44384
44926
|
var init_node_runtime = __esm(async () => {
|
|
44385
44927
|
await init_src();
|
|
44386
44928
|
await init_src8();
|
|
44387
44929
|
await init_join();
|
|
44388
44930
|
await init_peer_server();
|
|
44389
44931
|
init_protocol();
|
|
44932
|
+
mounted = new Map;
|
|
44933
|
+
});
|
|
44934
|
+
|
|
44935
|
+
// ../../adapters/otium/src/workspace-control.ts
|
|
44936
|
+
var exports_workspace_control = {};
|
|
44937
|
+
__export(exports_workspace_control, {
|
|
44938
|
+
reconcileRunningNodeWorkspaces: () => reconcileRunningNodeWorkspaces
|
|
44939
|
+
});
|
|
44940
|
+
async function reconcileRunningNodeWorkspaces() {
|
|
44941
|
+
try {
|
|
44942
|
+
const { inspectNodeDaemon: inspectNodeDaemon2 } = await init_src5().then(() => exports_src3);
|
|
44943
|
+
const status = await inspectNodeDaemon2();
|
|
44944
|
+
if (!status.running || !status.info) {
|
|
44945
|
+
return { ok: false, attached: [], detached: [], error: "node is not running" };
|
|
44946
|
+
}
|
|
44947
|
+
const response = await fetch(`http://127.0.0.1:${status.info.port}${OTIUM_ADAPTER_CONTROL_PREFIX}${OTIUM_WORKSPACES_CONTROL_PATH}`, {
|
|
44948
|
+
method: "POST",
|
|
44949
|
+
headers: {
|
|
44950
|
+
[OTIUM_ADAPTER_CONTROL_HEADER]: NODE_CONTROL_TOKEN,
|
|
44951
|
+
"content-type": "application/json"
|
|
44952
|
+
},
|
|
44953
|
+
signal: AbortSignal.timeout(5000)
|
|
44954
|
+
});
|
|
44955
|
+
const body = await response.json().catch(() => null);
|
|
44956
|
+
if (!response.ok || !body?.ok) {
|
|
44957
|
+
return {
|
|
44958
|
+
ok: false,
|
|
44959
|
+
attached: [],
|
|
44960
|
+
detached: [],
|
|
44961
|
+
error: body?.error ?? `node returned ${response.status}`
|
|
44962
|
+
};
|
|
44963
|
+
}
|
|
44964
|
+
return { ok: true, attached: body.attached ?? [], detached: body.detached ?? [] };
|
|
44965
|
+
} catch (error2) {
|
|
44966
|
+
return { ok: false, attached: [], detached: [], error: error2.message };
|
|
44967
|
+
}
|
|
44968
|
+
}
|
|
44969
|
+
var init_workspace_control = __esm(async () => {
|
|
44970
|
+
await init_src();
|
|
44390
44971
|
});
|
|
44391
44972
|
|
|
44392
44973
|
// ../../adapters/otium/src/join-cli.ts
|
|
@@ -44480,6 +45061,12 @@ async function joinCommand(args) {
|
|
|
44480
45061
|
} finally {
|
|
44481
45062
|
configureOtiumCentral(null);
|
|
44482
45063
|
}
|
|
45064
|
+
const { reconcileRunningNodeWorkspaces: reconcileRunningNodeWorkspaces2 } = await init_workspace_control().then(() => exports_workspace_control);
|
|
45065
|
+
const applied = await reconcileRunningNodeWorkspaces2();
|
|
45066
|
+
if (applied.ok && applied.attached.length > 0) {
|
|
45067
|
+
console.log("the running node attached this workspace; the others kept running");
|
|
45068
|
+
return;
|
|
45069
|
+
}
|
|
44483
45070
|
console.log("\nnext: `negotium-otium serve` (mounts the otium peer routes automatically)");
|
|
44484
45071
|
}
|
|
44485
45072
|
var init_join_cli = __esm(async () => {
|
|
@@ -44494,9 +45081,15 @@ __export(exports_sidecar, {
|
|
|
44494
45081
|
runOtiumSidecar: () => runOtiumSidecar,
|
|
44495
45082
|
proxyOtiumPeerRequest: () => proxyOtiumPeerRequest
|
|
44496
45083
|
});
|
|
45084
|
+
function isPublicPeerPath(pathname) {
|
|
45085
|
+
return pathname === "/ready" || pathname.startsWith("/api/v1/peer/");
|
|
45086
|
+
}
|
|
44497
45087
|
async function proxyOtiumPeerRequest(req, dependencies = {}) {
|
|
44498
45088
|
const inspectNode = dependencies.inspectNode ?? inspectNodeDaemon;
|
|
44499
45089
|
const fetchRequest = dependencies.fetch ?? fetch;
|
|
45090
|
+
if (!isPublicPeerPath(new URL(req.url).pathname)) {
|
|
45091
|
+
return Response.json({ ok: false, error: "not found" }, { status: 404 });
|
|
45092
|
+
}
|
|
44500
45093
|
const status = await inspectNode();
|
|
44501
45094
|
if (!status.running || !status.info) {
|
|
44502
45095
|
return Response.json({ ok: false, error: "canonical Negotium node is unavailable" }, { status: 503 });
|
|
@@ -44507,6 +45100,7 @@ async function proxyOtiumPeerRequest(req, dependencies = {}) {
|
|
|
44507
45100
|
target.search = source.search;
|
|
44508
45101
|
const headers = new Headers(req.headers);
|
|
44509
45102
|
headers.set(OTIUM_ADAPTER_CONTROL_HEADER, NODE_CONTROL_TOKEN);
|
|
45103
|
+
headers.set(OTIUM_RELAYED_HEADER, "1");
|
|
44510
45104
|
try {
|
|
44511
45105
|
const body = req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer();
|
|
44512
45106
|
headers.delete("transfer-encoding");
|
|
@@ -44563,8 +45157,8 @@ async function runOtiumSidecar(options) {
|
|
|
44563
45157
|
}) : null;
|
|
44564
45158
|
tunnel?.start();
|
|
44565
45159
|
let resolveCompleted;
|
|
44566
|
-
const completed = new Promise((
|
|
44567
|
-
resolveCompleted =
|
|
45160
|
+
const completed = new Promise((resolve27) => {
|
|
45161
|
+
resolveCompleted = resolve27;
|
|
44568
45162
|
});
|
|
44569
45163
|
onShutdown("otium-sidecar-server", 130, () => server?.stop(true));
|
|
44570
45164
|
onShutdown("otium-sidecar-tunnel", 120, () => tunnel?.stop());
|
|
@@ -44690,16 +45284,28 @@ async function runOtiumCli(args = process.argv.slice(2)) {
|
|
|
44690
45284
|
break;
|
|
44691
45285
|
}
|
|
44692
45286
|
case "leave": {
|
|
44693
|
-
|
|
44694
|
-
|
|
45287
|
+
const targetCellId = commandArgs[0]?.trim();
|
|
45288
|
+
if (commandArgs.length > 1 || targetCellId?.startsWith("-")) {
|
|
45289
|
+
throw new Error(`usage: negotium otium ${command} [<cell-id>]`);
|
|
45290
|
+
}
|
|
44695
45291
|
if (process.env.OTIUM_CENTRAL_URL || process.env.OTIUM_CELL_ID || process.env.OTIUM_CELL_SECRET) {
|
|
44696
45292
|
throw new Error("Otium join is configured by environment; remove OTIUM_CENTRAL_URL, OTIUM_CELL_ID, and OTIUM_CELL_SECRET to disconnect");
|
|
44697
45293
|
}
|
|
44698
|
-
const {
|
|
44699
|
-
|
|
45294
|
+
const { loadJoins: loadJoins2, removeJoin: removeJoin2 } = await init_join().then(() => exports_join);
|
|
45295
|
+
const joins = loadJoins2();
|
|
45296
|
+
if (joins.length === 0)
|
|
44700
45297
|
throw new Error("not joined to an Otium workspace");
|
|
44701
|
-
|
|
44702
|
-
|
|
45298
|
+
if (!targetCellId && joins.length > 1) {
|
|
45299
|
+
throw new Error(`this node is joined to ${joins.length} workspaces; name one to leave: ${joins.map((join43) => join43.cellId).join(", ")}`);
|
|
45300
|
+
}
|
|
45301
|
+
if (targetCellId && !joins.some((join43) => join43.cellId === targetCellId)) {
|
|
45302
|
+
throw new Error(`not joined as ${targetCellId}`);
|
|
45303
|
+
}
|
|
45304
|
+
removeJoin2(targetCellId);
|
|
45305
|
+
console.log(targetCellId ? `left Otium workspace ${targetCellId}; its credentials were removed` : "disconnected from Otium; workspace credentials removed");
|
|
45306
|
+
const { reconcileRunningNodeWorkspaces: reconcileRunningNodeWorkspaces2 } = await init_workspace_control().then(() => exports_workspace_control);
|
|
45307
|
+
const applied = await reconcileRunningNodeWorkspaces2();
|
|
45308
|
+
console.log(applied.ok ? "the running node has detached it; other workspaces are unaffected" : "restart the node to apply this");
|
|
44703
45309
|
break;
|
|
44704
45310
|
}
|
|
44705
45311
|
case "serve": {
|
|
@@ -44722,8 +45328,9 @@ async function runOtiumCli(args = process.argv.slice(2)) {
|
|
|
44722
45328
|
" serve [--port <port>] [--relay <url>]",
|
|
44723
45329
|
" run peer routes and an outbound relay tunnel",
|
|
44724
45330
|
"",
|
|
44725
|
-
"
|
|
44726
|
-
"
|
|
45331
|
+
"Rooms on the otium surface are discovered by the hub over the Runtime",
|
|
45332
|
+
"Gateway. There is nothing to publish or withdraw: a room's surface is",
|
|
45333
|
+
"fixed when it is created."
|
|
44727
45334
|
].join(`
|
|
44728
45335
|
`));
|
|
44729
45336
|
if (command && command !== "help" && command !== "--help")
|
|
@@ -44766,7 +45373,7 @@ __export(exports_topics, {
|
|
|
44766
45373
|
topicsCommand: () => topicsCommand
|
|
44767
45374
|
});
|
|
44768
45375
|
function topicsCommand() {
|
|
44769
|
-
const topics = getVisibleTopics();
|
|
45376
|
+
const topics = getVisibleTopics({ surface: "terminal" });
|
|
44770
45377
|
if (topics.length === 0) {
|
|
44771
45378
|
console.log("no topics yet - start `negotium` to create one in Terminal");
|
|
44772
45379
|
return;
|
|
@@ -45265,6 +45872,16 @@ function renderCliHelp() {
|
|
|
45265
45872
|
// ../cli/src/main.ts
|
|
45266
45873
|
var [, , rawCommand, ...args] = process.argv;
|
|
45267
45874
|
var command = normalizeCliCommand(rawCommand);
|
|
45875
|
+
async function runOtium(args2) {
|
|
45876
|
+
const { runOtiumCli: runOtiumCli2 } = await loadOtiumCli();
|
|
45877
|
+
try {
|
|
45878
|
+
await runOtiumCli2(args2);
|
|
45879
|
+
} catch (error2) {
|
|
45880
|
+
process.stderr.write(`negotium otium: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
45881
|
+
`);
|
|
45882
|
+
process.exitCode = 1;
|
|
45883
|
+
}
|
|
45884
|
+
}
|
|
45268
45885
|
function numericOption(values, name, fallback) {
|
|
45269
45886
|
const prefix = `--${name}=`;
|
|
45270
45887
|
const parsed = Number.parseInt(values.find((value) => value.startsWith(prefix))?.slice(prefix.length) ?? "", 10);
|
|
@@ -45290,7 +45907,7 @@ async function runCanonicalNode(port) {
|
|
|
45290
45907
|
});
|
|
45291
45908
|
console.log(`negotium node listening on 127.0.0.1:${node.port} (ctrl-c to stop)`);
|
|
45292
45909
|
await node.completed;
|
|
45293
|
-
await new Promise((
|
|
45910
|
+
await new Promise((resolve27) => setImmediate(resolve27));
|
|
45294
45911
|
process.exit(0);
|
|
45295
45912
|
}
|
|
45296
45913
|
async function stopAdapter(name) {
|
|
@@ -45322,8 +45939,7 @@ switch (command) {
|
|
|
45322
45939
|
}
|
|
45323
45940
|
case "serve": {
|
|
45324
45941
|
if (args[0] === "otium") {
|
|
45325
|
-
|
|
45326
|
-
await runOtiumCli2(["serve", ...args.slice(1)]);
|
|
45942
|
+
await runOtium(["serve", ...args.slice(1)]);
|
|
45327
45943
|
break;
|
|
45328
45944
|
}
|
|
45329
45945
|
await runCanonicalNode(numericOption(args, "port", 7777));
|
|
@@ -45407,11 +46023,10 @@ switch (command) {
|
|
|
45407
46023
|
break;
|
|
45408
46024
|
}
|
|
45409
46025
|
case "otium": {
|
|
45410
|
-
const { runOtiumCli: runOtiumCli2 } = await loadOtiumCli();
|
|
45411
46026
|
if (args[0] === "serve") {
|
|
45412
46027
|
process.stderr.write("warning: `negotium otium serve` is deprecated; use `negotium serve otium`\n");
|
|
45413
46028
|
}
|
|
45414
|
-
await
|
|
46029
|
+
await runOtium(args);
|
|
45415
46030
|
break;
|
|
45416
46031
|
}
|
|
45417
46032
|
default: {
|
|
@@ -45421,4 +46036,4 @@ switch (command) {
|
|
|
45421
46036
|
}
|
|
45422
46037
|
}
|
|
45423
46038
|
|
|
45424
|
-
//# debugId=
|
|
46039
|
+
//# debugId=EDB732002F3171B764756E2164756E21
|