negotium 0.2.26 → 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 +200 -118
- 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 +1124 -511
- package/dist/main.js.map +43 -41
- package/dist/mcp-factories.js +294 -216
- 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 +217 -32
- 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 +141 -31
- 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(`
|
|
@@ -7832,22 +7886,24 @@ function backfillTopicSurfaces() {
|
|
|
7832
7886
|
logger.info({ surface }, "api_topics: surface backfilled");
|
|
7833
7887
|
}
|
|
7834
7888
|
function renameSurfaceTitleCollisions() {
|
|
7835
|
-
const rows = db.query("SELECT id, title, kind, surface FROM api_topics ORDER BY created_at ASC, rowid ASC").all();
|
|
7836
|
-
const
|
|
7889
|
+
const rows = db.query("SELECT id, title, kind, surface FROM api_topics WHERE kind != 'manager' ORDER BY created_at ASC, rowid ASC").all();
|
|
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,18 +8153,17 @@ 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);
|
|
8081
8160
|
if (general)
|
|
8082
8161
|
return rowToDto2(general);
|
|
8083
8162
|
}
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
params.push(kind, GENERAL_TOPIC_ID);
|
|
8089
|
-
}
|
|
8163
|
+
if (kind === "manager")
|
|
8164
|
+
return null;
|
|
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 = ?)";
|
|
8090
8167
|
if (opts.excludeTopicId) {
|
|
8091
8168
|
sql += " AND id != ?";
|
|
8092
8169
|
params.push(opts.excludeTopicId);
|
|
@@ -8113,14 +8190,16 @@ function getTopicByNameForUser(title, userId, opts = {}) {
|
|
|
8113
8190
|
const qualified = /^(agent|channel|manager):(.+)$/i.exec(trimmed);
|
|
8114
8191
|
const requestedKind = qualified ? normalizeTopicKind(qualified[1]?.toLowerCase()) : null;
|
|
8115
8192
|
const requestedTitle = qualified ? qualified[2].trim() : trimmed;
|
|
8193
|
+
const scoped = Object.hasOwn(opts, "surfaceScope");
|
|
8116
8194
|
const rows = db.query(`SELECT t.* FROM api_topics t
|
|
8117
8195
|
WHERE LOWER(t.title) = LOWER(?)
|
|
8118
8196
|
AND t.id != ?
|
|
8119
8197
|
AND t.visibility != 'hidden'
|
|
8120
8198
|
AND (? IS NULL OR t.surface = ?)
|
|
8199
|
+
${scoped ? "AND t.surface_scope IS ?" : ""}
|
|
8121
8200
|
AND EXISTS (
|
|
8122
8201
|
SELECT 1 FROM topic_members m WHERE m.topic_id = t.id AND m.user_id = ?
|
|
8123
|
-
)`).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);
|
|
8124
8203
|
const matches = requestedKind ? rows.filter((row) => row.kind === requestedKind) : rows;
|
|
8125
8204
|
return matches.length === 1 ? rowToDto2(matches[0]) : null;
|
|
8126
8205
|
}
|
|
@@ -8208,7 +8287,7 @@ function removeParticipantFromDB(topicId, userId) {
|
|
|
8208
8287
|
db.query("DELETE FROM topic_members WHERE topic_id = ? AND user_id = ?").run(topicId, userId);
|
|
8209
8288
|
return true;
|
|
8210
8289
|
}
|
|
8211
|
-
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";
|
|
8212
8291
|
var init_api_topics = __esm(async () => {
|
|
8213
8292
|
init_constants();
|
|
8214
8293
|
init_logger();
|
|
@@ -11428,8 +11507,10 @@ var init_active_rooms = __esm(async () => {
|
|
|
11428
11507
|
|
|
11429
11508
|
// ../../packages/core/src/topics/personal-general.ts
|
|
11430
11509
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
11431
|
-
function ensurePersonalGeneral(userId) {
|
|
11432
|
-
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 });
|
|
11433
11514
|
if (existing) {
|
|
11434
11515
|
if (existing.description === LEGACY_PERSONAL_GENERAL_DESCRIPTION) {
|
|
11435
11516
|
existing.description = PERSONAL_GENERAL_DESCRIPTION;
|
|
@@ -11453,6 +11534,8 @@ function ensurePersonalGeneral(userId) {
|
|
|
11453
11534
|
aiMode: "always",
|
|
11454
11535
|
aiMention: false,
|
|
11455
11536
|
participants: [{ userId, role: "owner" }],
|
|
11537
|
+
surface: scope,
|
|
11538
|
+
surfaceScope,
|
|
11456
11539
|
createdAt: now,
|
|
11457
11540
|
lastMessageAt: now
|
|
11458
11541
|
};
|
|
@@ -13440,7 +13523,8 @@ function mergeRuntimeUserTurnRequest(input) {
|
|
|
13440
13523
|
const now = Date.now();
|
|
13441
13524
|
return db.transaction(() => {
|
|
13442
13525
|
const rows = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? ORDER BY created_at ASC, rowid ASC").all(input.topicId);
|
|
13443
|
-
const
|
|
13526
|
+
const thread = input.execution.threadRootId;
|
|
13527
|
+
const previous = rows.map(rowToRequest).filter((request) => request.execution?.threadRootId === thread);
|
|
13444
13528
|
const omittedRequestIds = new Set([
|
|
13445
13529
|
...input.omitRequestIds ?? [],
|
|
13446
13530
|
...previous.filter((request) => Boolean(request.execution?.providerSessionId)).map((request) => request.requestId)
|
|
@@ -13476,7 +13560,11 @@ function mergeRuntimeUserTurnRequest(input) {
|
|
|
13476
13560
|
execution.sessionIdSpecified = true;
|
|
13477
13561
|
}
|
|
13478
13562
|
const attachments = flattenUserTurnAttachments(userMessages);
|
|
13479
|
-
|
|
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
|
+
}
|
|
13480
13568
|
db.query(`INSERT INTO runtime_user_turn_requests
|
|
13481
13569
|
(request_id, topic_id, user_id, prompt, user_messages_json, attachments_json,
|
|
13482
13570
|
allow_auto_continue, execution_json, topic_epoch, created_at,
|
|
@@ -14323,8 +14411,8 @@ function updateTopic(topicId, patch) {
|
|
|
14323
14411
|
function isParticipant(topic, userId) {
|
|
14324
14412
|
return topic.participants.some((p) => p.userId === userId);
|
|
14325
14413
|
}
|
|
14326
|
-
function nextDerivedTopicTitle(sourceTitle, kind, suffix, surface) {
|
|
14327
|
-
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()));
|
|
14328
14416
|
let n = 1;
|
|
14329
14417
|
let title = `${sourceTitle}-${suffix}-${n}`;
|
|
14330
14418
|
while (visibleTitles.has(title.toLowerCase())) {
|
|
@@ -14389,8 +14477,9 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
|
|
|
14389
14477
|
] : [{ userId, role: "owner" }];
|
|
14390
14478
|
const kind = topic.kind ?? inferTopicKind(topic);
|
|
14391
14479
|
const surface = topic.surface ?? defaultTopicSurface();
|
|
14392
|
-
const
|
|
14393
|
-
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 });
|
|
14394
14483
|
if (conflict) {
|
|
14395
14484
|
logger.info({ sourceTopicId, title, kind, conflictTopicId: conflict.id }, "createDerivedTopic: title conflict");
|
|
14396
14485
|
throw new TopicTitleConflictError(title);
|
|
@@ -14413,7 +14502,8 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
|
|
|
14413
14502
|
isFork: copyHistory,
|
|
14414
14503
|
...subagent ? { isSubagent: true } : {},
|
|
14415
14504
|
visibility: topic.visibility,
|
|
14416
|
-
surface
|
|
14505
|
+
surface,
|
|
14506
|
+
surfaceScope
|
|
14417
14507
|
};
|
|
14418
14508
|
let sessionId;
|
|
14419
14509
|
let rollbackHandle;
|
|
@@ -14521,7 +14611,10 @@ async function createDerivedTopicImpl(topic, sourceTopicId, userId, copyHistory,
|
|
|
14521
14611
|
if (!currentSource || currentSource.kind === "manager" || !isParticipant(currentSource, userId) || subagent && isRuntimeTopicMaintenance(sourceTopicId)) {
|
|
14522
14612
|
throw new TopicDeriveBusyError("Source topic changed while deriving; try again");
|
|
14523
14613
|
}
|
|
14524
|
-
const transactionalConflict = findTopicTitleConflict(title, kind, {
|
|
14614
|
+
const transactionalConflict = findTopicTitleConflict(title, kind, {
|
|
14615
|
+
surface,
|
|
14616
|
+
surfaceScope
|
|
14617
|
+
});
|
|
14525
14618
|
if (transactionalConflict)
|
|
14526
14619
|
throw new TopicTitleConflictError(title);
|
|
14527
14620
|
upsertTopic(derived);
|
|
@@ -15391,13 +15484,14 @@ async function forwardToPeer(args) {
|
|
|
15391
15484
|
error: forwarded.configured ? "remote session bridge is configured but unavailable" : "remote nodes are not connected on this negotium node (standalone mode)"
|
|
15392
15485
|
};
|
|
15393
15486
|
}
|
|
15394
|
-
async function peerSessionsForUser(userId, sourceQueryId) {
|
|
15487
|
+
async function peerSessionsForUser(userId, sourceQueryId, fromTopicId) {
|
|
15395
15488
|
if (activeBridge)
|
|
15396
|
-
return activeBridge.sessions(userId, sourceQueryId);
|
|
15489
|
+
return activeBridge.sessions(userId, sourceQueryId, fromTopicId);
|
|
15397
15490
|
const sessions = await callLoopbackBridge({
|
|
15398
15491
|
action: "sessions",
|
|
15399
15492
|
userId,
|
|
15400
|
-
sourceQueryId
|
|
15493
|
+
sourceQueryId,
|
|
15494
|
+
fromTopicId
|
|
15401
15495
|
});
|
|
15402
15496
|
if (sessions.result)
|
|
15403
15497
|
return sessions.result;
|
|
@@ -16153,6 +16247,226 @@ body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--
|
|
|
16153
16247
|
</style>`;
|
|
16154
16248
|
});
|
|
16155
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
|
+
|
|
16156
16470
|
// ../../packages/core/src/topics/lifecycle.ts
|
|
16157
16471
|
var exports_lifecycle = {};
|
|
16158
16472
|
__export(exports_lifecycle, {
|
|
@@ -16202,6 +16516,7 @@ async function cleanupParticipantResources(topic, userIds, sessionId, cwd, purge
|
|
|
16202
16516
|
cleanupSessionInboxFiles(participantUserId, topic.id, topic.title);
|
|
16203
16517
|
clearQueryState(participantUserId, topic.id, topic.title);
|
|
16204
16518
|
clearQueryUsageAlert(participantUserId, topic.id);
|
|
16519
|
+
deleteTopicStats(participantUserId, topic.id);
|
|
16205
16520
|
deletePendingAsksForTopic({ userId: participantUserId, topicName: topic.title });
|
|
16206
16521
|
}
|
|
16207
16522
|
return true;
|
|
@@ -16356,6 +16671,7 @@ var init_lifecycle = __esm(async () => {
|
|
|
16356
16671
|
await init_runtime_turn_requests();
|
|
16357
16672
|
await init_self_schedules();
|
|
16358
16673
|
await init_session_asks();
|
|
16674
|
+
await init_token_stats();
|
|
16359
16675
|
await init_topic_archive();
|
|
16360
16676
|
await init_topic_archive_state();
|
|
16361
16677
|
TopicArchiveRequiredError = class TopicArchiveRequiredError extends Error {
|
|
@@ -16390,8 +16706,8 @@ var init_lifecycle = __esm(async () => {
|
|
|
16390
16706
|
|
|
16391
16707
|
// ../../packages/core/src/runtime/attachments.ts
|
|
16392
16708
|
import { randomUUID as randomUUID14 } from "crypto";
|
|
16393
|
-
import { copyFileSync as copyFileSync2, mkdirSync as
|
|
16394
|
-
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";
|
|
16395
16711
|
function workspaceCwdFor(topicId) {
|
|
16396
16712
|
return resolveTopicWorkspaceDir(topicId);
|
|
16397
16713
|
}
|
|
@@ -16404,7 +16720,7 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
|
|
|
16404
16720
|
if (!attachmentIds?.length)
|
|
16405
16721
|
return [];
|
|
16406
16722
|
const out = [];
|
|
16407
|
-
const destDir =
|
|
16723
|
+
const destDir = join26(workspaceCwdFor(topicId), "attachments", queryId);
|
|
16408
16724
|
for (const rawId of attachmentIds) {
|
|
16409
16725
|
if (typeof rawId !== "string")
|
|
16410
16726
|
continue;
|
|
@@ -16418,10 +16734,10 @@ function materializePromptAttachments(topicId, queryId, attachmentIds) {
|
|
|
16418
16734
|
continue;
|
|
16419
16735
|
}
|
|
16420
16736
|
try {
|
|
16421
|
-
|
|
16737
|
+
mkdirSync17(destDir, { recursive: true });
|
|
16422
16738
|
const index = String(out.length + 1).padStart(2, "0");
|
|
16423
16739
|
const safeName = safeAttachmentFilename(attachment.filename, fileId);
|
|
16424
|
-
const destPath =
|
|
16740
|
+
const destPath = join26(destDir, `${index}-${fileId.slice(0, 8)}-${safeName}`);
|
|
16425
16741
|
copyFileSync2(sourcePath, destPath);
|
|
16426
16742
|
out.push({
|
|
16427
16743
|
id: attachment.id,
|
|
@@ -16451,14 +16767,14 @@ function promptWithAttachments(prompt, attachments) {
|
|
|
16451
16767
|
return composeAttachmentPrompt(prompt, attachments.map(({ filename, path }) => attachmentPromptLine(filename, path)));
|
|
16452
16768
|
}
|
|
16453
16769
|
function ingestAttachment(args) {
|
|
16454
|
-
const destDir =
|
|
16455
|
-
|
|
16770
|
+
const destDir = join26(UPLOADS_DIR, args.topicId);
|
|
16771
|
+
mkdirSync17(destDir, { recursive: true });
|
|
16456
16772
|
const safeName = safeAttachmentFilename(args.filename, "upload");
|
|
16457
|
-
const destPath =
|
|
16773
|
+
const destPath = join26(destDir, `${Date.now()}-${randomUUID14().slice(0, 8)}-${safeName}`);
|
|
16458
16774
|
if (args.sourcePath !== undefined) {
|
|
16459
16775
|
copyFileSync2(args.sourcePath, destPath);
|
|
16460
16776
|
} else if (args.bytes !== undefined) {
|
|
16461
|
-
|
|
16777
|
+
writeFileSync15(destPath, args.bytes);
|
|
16462
16778
|
} else {
|
|
16463
16779
|
throw new Error("ingestAttachment: provide sourcePath or bytes");
|
|
16464
16780
|
}
|
|
@@ -17054,204 +17370,6 @@ var init_visuals = __esm(async () => {
|
|
|
17054
17370
|
init_visual_html();
|
|
17055
17371
|
});
|
|
17056
17372
|
|
|
17057
|
-
// ../../packages/core/src/storage/token-stats.ts
|
|
17058
|
-
var exports_token_stats = {};
|
|
17059
|
-
__export(exports_token_stats, {
|
|
17060
|
-
tokenStatsFileId: () => tokenStatsFileId,
|
|
17061
|
-
recordUsage: () => recordUsage,
|
|
17062
|
-
getTopicStats: () => getTopicStats,
|
|
17063
|
-
getStats: () => getStats,
|
|
17064
|
-
calcCost: () => calcCost
|
|
17065
|
-
});
|
|
17066
|
-
import { createHash as createHash7 } from "crypto";
|
|
17067
|
-
import { mkdirSync as mkdirSync17 } from "fs";
|
|
17068
|
-
import { join as join26 } from "path";
|
|
17069
|
-
function emptyBucket() {
|
|
17070
|
-
return {
|
|
17071
|
-
inputTokens: 0,
|
|
17072
|
-
outputTokens: 0,
|
|
17073
|
-
cacheCreationInputTokens: 0,
|
|
17074
|
-
cacheReadInputTokens: 0,
|
|
17075
|
-
queries: 0,
|
|
17076
|
-
estimatedCostUsd: 0
|
|
17077
|
-
};
|
|
17078
|
-
}
|
|
17079
|
-
function tokenStatsFileId(userId) {
|
|
17080
|
-
const rawUserId = String(userId);
|
|
17081
|
-
return /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..") ? rawUserId : `sha256-${createHash7("sha256").update(rawUserId).digest("hex")}`;
|
|
17082
|
-
}
|
|
17083
|
-
function queriesPath(userId) {
|
|
17084
|
-
const fileId = tokenStatsFileId(userId);
|
|
17085
|
-
const logDir = resolveStorageLogDir();
|
|
17086
|
-
mkdirSync17(logDir, { recursive: true });
|
|
17087
|
-
return join26(logDir, `token-queries-${fileId}.jsonl`);
|
|
17088
|
-
}
|
|
17089
|
-
function loadRecords(userId) {
|
|
17090
|
-
try {
|
|
17091
|
-
return readJsonlLines(queriesPath(userId)).flatMap((line) => {
|
|
17092
|
-
try {
|
|
17093
|
-
return [JSON.parse(line)];
|
|
17094
|
-
} catch {
|
|
17095
|
-
return [];
|
|
17096
|
-
}
|
|
17097
|
-
});
|
|
17098
|
-
} catch {
|
|
17099
|
-
return [];
|
|
17100
|
-
}
|
|
17101
|
-
}
|
|
17102
|
-
function calcCost(b) {
|
|
17103
|
-
return b.estimatedCostUsd;
|
|
17104
|
-
}
|
|
17105
|
-
function estimateUsageCost(agent, model, usage) {
|
|
17106
|
-
const prices = TOKEN_PRICES[`${agent}:${model}`];
|
|
17107
|
-
if (!prices)
|
|
17108
|
-
return 0;
|
|
17109
|
-
return (usage.inputTokens * prices.input + usage.outputTokens * prices.output + (agent === "claude" ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input) : 0) + usage.cacheReadInputTokens * prices.cacheRead) / 1e6;
|
|
17110
|
-
}
|
|
17111
|
-
function isQueryRecord(value) {
|
|
17112
|
-
if (!value || typeof value !== "object")
|
|
17113
|
-
return false;
|
|
17114
|
-
const record = value;
|
|
17115
|
-
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";
|
|
17116
|
-
}
|
|
17117
|
-
function recordUsage(userId, session, usage, context) {
|
|
17118
|
-
const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
|
|
17119
|
-
const inputTokens = context.agent === "claude" ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
17120
|
-
const normalized = {
|
|
17121
|
-
inputTokens,
|
|
17122
|
-
outputTokens: usage.outputTokens,
|
|
17123
|
-
cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
|
|
17124
|
-
cacheReadInputTokens
|
|
17125
|
-
};
|
|
17126
|
-
const record = {
|
|
17127
|
-
schemaVersion: 2,
|
|
17128
|
-
timestamp: new Date().toISOString(),
|
|
17129
|
-
session,
|
|
17130
|
-
topicId: context.topicId,
|
|
17131
|
-
...context.providerSessionId ? { providerSessionId: context.providerSessionId } : {},
|
|
17132
|
-
agent: context.agent,
|
|
17133
|
-
model: context.model,
|
|
17134
|
-
...normalized,
|
|
17135
|
-
...usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {},
|
|
17136
|
-
...usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {},
|
|
17137
|
-
estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized)
|
|
17138
|
-
};
|
|
17139
|
-
try {
|
|
17140
|
-
appendJsonlEntry(queriesPath(userId), record);
|
|
17141
|
-
} catch (e) {
|
|
17142
|
-
logger.warn({ err: e, userId }, "token-stats: Failed to record");
|
|
17143
|
-
}
|
|
17144
|
-
}
|
|
17145
|
-
function getStats(userId, from, to) {
|
|
17146
|
-
const records = loadRecords(userId);
|
|
17147
|
-
const fromTs = from ? new Date(from).getTime() : 0;
|
|
17148
|
-
const toTs = to ? new Date(to).getTime() : Infinity;
|
|
17149
|
-
if (from && Number.isNaN(fromTs) || to && Number.isNaN(toTs)) {
|
|
17150
|
-
logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
|
|
17151
|
-
return {
|
|
17152
|
-
total: emptyBucket(),
|
|
17153
|
-
byHour: {},
|
|
17154
|
-
bySession: {},
|
|
17155
|
-
currentSessions: [],
|
|
17156
|
-
ignoredLegacyRecords: 0,
|
|
17157
|
-
estimatedCostUsd: 0
|
|
17158
|
-
};
|
|
17159
|
-
}
|
|
17160
|
-
const total = emptyBucket();
|
|
17161
|
-
const byHour = {};
|
|
17162
|
-
const bySession = {};
|
|
17163
|
-
const currentSessions = new Map;
|
|
17164
|
-
let ignoredLegacyRecords = 0;
|
|
17165
|
-
for (const raw of records) {
|
|
17166
|
-
if (!isQueryRecord(raw)) {
|
|
17167
|
-
ignoredLegacyRecords += 1;
|
|
17168
|
-
continue;
|
|
17169
|
-
}
|
|
17170
|
-
const r = raw;
|
|
17171
|
-
const ts = new Date(r.timestamp).getTime();
|
|
17172
|
-
if (ts < fromTs || ts > toTs)
|
|
17173
|
-
continue;
|
|
17174
|
-
const hourKey = r.timestamp.slice(0, 13);
|
|
17175
|
-
if (!byHour[hourKey])
|
|
17176
|
-
byHour[hourKey] = emptyBucket();
|
|
17177
|
-
if (!bySession[r.session])
|
|
17178
|
-
bySession[r.session] = emptyBucket();
|
|
17179
|
-
for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
|
|
17180
|
-
bucket.inputTokens += r.inputTokens;
|
|
17181
|
-
bucket.outputTokens += r.outputTokens;
|
|
17182
|
-
bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
|
|
17183
|
-
bucket.cacheReadInputTokens += r.cacheReadInputTokens;
|
|
17184
|
-
bucket.queries += 1;
|
|
17185
|
-
bucket.estimatedCostUsd += r.estimatedCostUsd;
|
|
17186
|
-
}
|
|
17187
|
-
if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
|
|
17188
|
-
currentSessions.set(r.topicId, {
|
|
17189
|
-
timestamp: r.timestamp,
|
|
17190
|
-
topicId: r.topicId,
|
|
17191
|
-
topicTitle: r.session,
|
|
17192
|
-
...r.providerSessionId ? { providerSessionId: r.providerSessionId } : {},
|
|
17193
|
-
agent: r.agent,
|
|
17194
|
-
model: r.model,
|
|
17195
|
-
contextTokens: r.contextTokens,
|
|
17196
|
-
contextWindow: r.contextWindow
|
|
17197
|
-
});
|
|
17198
|
-
}
|
|
17199
|
-
}
|
|
17200
|
-
return {
|
|
17201
|
-
total,
|
|
17202
|
-
byHour,
|
|
17203
|
-
bySession,
|
|
17204
|
-
currentSessions: [...currentSessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
|
|
17205
|
-
ignoredLegacyRecords,
|
|
17206
|
-
estimatedCostUsd: calcCost(total)
|
|
17207
|
-
};
|
|
17208
|
-
}
|
|
17209
|
-
function getTopicStats(userId, topicId) {
|
|
17210
|
-
const total = emptyBucket();
|
|
17211
|
-
let currentSession;
|
|
17212
|
-
for (const raw of loadRecords(userId)) {
|
|
17213
|
-
if (!isQueryRecord(raw) || raw.topicId !== topicId)
|
|
17214
|
-
continue;
|
|
17215
|
-
total.inputTokens += raw.inputTokens;
|
|
17216
|
-
total.outputTokens += raw.outputTokens;
|
|
17217
|
-
total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
|
|
17218
|
-
total.cacheReadInputTokens += raw.cacheReadInputTokens;
|
|
17219
|
-
total.queries += 1;
|
|
17220
|
-
total.estimatedCostUsd += raw.estimatedCostUsd;
|
|
17221
|
-
if (raw.contextTokens !== undefined && raw.contextWindow !== undefined && raw.contextWindow > 0 && (!currentSession || raw.timestamp > currentSession.timestamp)) {
|
|
17222
|
-
currentSession = {
|
|
17223
|
-
timestamp: raw.timestamp,
|
|
17224
|
-
topicId: raw.topicId,
|
|
17225
|
-
topicTitle: raw.session,
|
|
17226
|
-
...raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {},
|
|
17227
|
-
agent: raw.agent,
|
|
17228
|
-
model: raw.model,
|
|
17229
|
-
contextTokens: raw.contextTokens,
|
|
17230
|
-
contextWindow: raw.contextWindow
|
|
17231
|
-
};
|
|
17232
|
-
}
|
|
17233
|
-
}
|
|
17234
|
-
return { topicId, ...total, ...currentSession ? { currentSession } : {} };
|
|
17235
|
-
}
|
|
17236
|
-
var TOKEN_PRICES;
|
|
17237
|
-
var init_token_stats = __esm(async () => {
|
|
17238
|
-
init_jsonl();
|
|
17239
|
-
init_logger();
|
|
17240
|
-
await init_storage_host();
|
|
17241
|
-
TOKEN_PRICES = {
|
|
17242
|
-
"codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
|
|
17243
|
-
"codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
|
|
17244
|
-
"codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
|
|
17245
|
-
"claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
|
|
17246
|
-
"claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
|
|
17247
|
-
"claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
|
|
17248
|
-
"maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
|
|
17249
|
-
"maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
|
|
17250
|
-
"maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
|
|
17251
|
-
"maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 }
|
|
17252
|
-
};
|
|
17253
|
-
});
|
|
17254
|
-
|
|
17255
17373
|
// ../../packages/core/src/runtime/turn-event-stream.ts
|
|
17256
17374
|
import { randomUUID as randomUUID15 } from "crypto";
|
|
17257
17375
|
import { realpathSync as realpathSync5, statSync as statSync9 } from "fs";
|
|
@@ -17367,10 +17485,11 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
|
|
|
17367
17485
|
agentType,
|
|
17368
17486
|
model,
|
|
17369
17487
|
sourceNode: execution?.sourceNode,
|
|
17488
|
+
...execution?.threadRootId ? { threadRootId: execution.threadRootId } : {},
|
|
17370
17489
|
usage,
|
|
17371
17490
|
createdAt: new Date().toISOString()
|
|
17372
17491
|
};
|
|
17373
|
-
appendApiMessage(message);
|
|
17492
|
+
appendApiMessage(message, execution?.threadRootId ? { updateTopicLastMessageAt: false } : undefined);
|
|
17374
17493
|
hub.broadcastMessage(topicId, message);
|
|
17375
17494
|
lastVisibleMessageId = message.id;
|
|
17376
17495
|
visibleMessageIds.push(message.id);
|
|
@@ -17892,7 +18011,7 @@ __export(exports_app_settings, {
|
|
|
17892
18011
|
getGlobalAiName: () => getGlobalAiName,
|
|
17893
18012
|
DEFAULT_AI_NAME: () => DEFAULT_AI_NAME
|
|
17894
18013
|
});
|
|
17895
|
-
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";
|
|
17896
18015
|
import { dirname as dirname14, join as join27 } from "path";
|
|
17897
18016
|
function settingsFile() {
|
|
17898
18017
|
return join27(resolveStorageDataDir(), "otium-settings.json");
|
|
@@ -17922,7 +18041,7 @@ function setGlobalAiName(name) {
|
|
|
17922
18041
|
aiName = name.trim() || DEFAULT_AI_NAME;
|
|
17923
18042
|
try {
|
|
17924
18043
|
mkdirSync18(dirname14(path), { recursive: true });
|
|
17925
|
-
|
|
18044
|
+
writeFileSync16(path, JSON.stringify({ aiName }, null, 2));
|
|
17926
18045
|
} catch {}
|
|
17927
18046
|
return aiName;
|
|
17928
18047
|
}
|
|
@@ -18419,6 +18538,7 @@ async function drainOneDurableUserTurn() {
|
|
|
18419
18538
|
bridgeSessionFromHistory: execution?.bridgeSessionFromHistory,
|
|
18420
18539
|
peerBridge: execution?.peerBridge,
|
|
18421
18540
|
from: execution?.from,
|
|
18541
|
+
threadRootId: execution?.threadRootId,
|
|
18422
18542
|
_queryId: request.requestId,
|
|
18423
18543
|
_runtimeEpoch: execution?.runtimeEpoch ?? request.topicEpoch,
|
|
18424
18544
|
onSettled: () => {
|
|
@@ -18491,6 +18611,7 @@ function startAiTurn(params) {
|
|
|
18491
18611
|
let sessionId = sessionResolution.sessionId;
|
|
18492
18612
|
const deferredSessionId = params.sessionId === undefined && !sessionResolution.isolated ? undefined : sessionId;
|
|
18493
18613
|
const sourceNode = params.sourceNode;
|
|
18614
|
+
const threadRootId = params.threadRootId;
|
|
18494
18615
|
const topicId = topic.id;
|
|
18495
18616
|
const requestId = params.requestId;
|
|
18496
18617
|
const depth = params.depth;
|
|
@@ -19019,7 +19140,7 @@ function startAiTurn(params) {
|
|
|
19019
19140
|
WsHub.get().broadcastTyping(topicId, "ai");
|
|
19020
19141
|
WsHub.get().broadcastAiActive(topicId, queryId);
|
|
19021
19142
|
}
|
|
19022
|
-
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) => {
|
|
19023
19144
|
let outcome = streamOutcome;
|
|
19024
19145
|
if (outcome.kind === "session-expired") {
|
|
19025
19146
|
const retry = resolveSessionRetry({
|
|
@@ -20279,7 +20400,8 @@ function gatewayPayloadHash(params, requestId, actorUserId) {
|
|
|
20279
20400
|
params.text,
|
|
20280
20401
|
params.clientMessageId,
|
|
20281
20402
|
requestId,
|
|
20282
|
-
params.allowAutoContinue ?? true
|
|
20403
|
+
params.allowAutoContinue ?? true,
|
|
20404
|
+
params.threadRootId ?? null
|
|
20283
20405
|
])).digest("hex");
|
|
20284
20406
|
}
|
|
20285
20407
|
function submitRuntimeGatewayTurn(params) {
|
|
@@ -20299,6 +20421,7 @@ function submitRuntimeGatewayTurn(params) {
|
|
|
20299
20421
|
sourceAdapter: "runtime-gateway",
|
|
20300
20422
|
sourceMessageId: params.clientMessageId,
|
|
20301
20423
|
text: params.text,
|
|
20424
|
+
...params.threadRootId ? { threadRootId: params.threadRootId } : {},
|
|
20302
20425
|
createdAt
|
|
20303
20426
|
};
|
|
20304
20427
|
const submission = {
|
|
@@ -20333,7 +20456,8 @@ function submitRuntimeGatewayTurn(params) {
|
|
|
20333
20456
|
sessionIdSpecified: true,
|
|
20334
20457
|
conversationPrompts: [params.text],
|
|
20335
20458
|
loggedUserMessageCount: 0,
|
|
20336
|
-
vaultUserId: params.vaultUserId
|
|
20459
|
+
vaultUserId: params.vaultUserId,
|
|
20460
|
+
...params.threadRootId ? { threadRootId: params.threadRootId } : {}
|
|
20337
20461
|
}
|
|
20338
20462
|
});
|
|
20339
20463
|
const acceptedEvent = appendRuntimeEvent("runtime-gateway-ingress", {
|
|
@@ -20526,7 +20650,11 @@ function registerTopic(opts) {
|
|
|
20526
20650
|
throw new TopicValidationError("Manager rooms are system-managed");
|
|
20527
20651
|
}
|
|
20528
20652
|
const surface = normalizeTopicSurface(opts.surface ?? defaultTopicSurface());
|
|
20529
|
-
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 });
|
|
20530
20658
|
if (conflict) {
|
|
20531
20659
|
throw new TopicValidationError(`A topic named "${title}" already exists on ${surface}`);
|
|
20532
20660
|
}
|
|
@@ -20563,6 +20691,7 @@ function registerTopic(opts) {
|
|
|
20563
20691
|
aiMode,
|
|
20564
20692
|
participants: [{ userId: opts.userId, role: "owner" }],
|
|
20565
20693
|
surface,
|
|
20694
|
+
surfaceScope,
|
|
20566
20695
|
createdAt: now,
|
|
20567
20696
|
lastMessageAt: now
|
|
20568
20697
|
};
|
|
@@ -21716,7 +21845,7 @@ var init_file_ops = __esm(() => {
|
|
|
21716
21845
|
|
|
21717
21846
|
// ../../packages/core/src/runtime/inbox.ts
|
|
21718
21847
|
import { createHash as createHash9, randomUUID as randomUUID20 } from "crypto";
|
|
21719
|
-
import { readdirSync as readdirSync8, statSync as statSync13, writeFileSync as
|
|
21848
|
+
import { readdirSync as readdirSync8, statSync as statSync13, writeFileSync as writeFileSync17 } from "fs";
|
|
21720
21849
|
import { join as join32 } from "path";
|
|
21721
21850
|
async function createAskForkPlan(options) {
|
|
21722
21851
|
const snapshot = structuredClone(options.entries);
|
|
@@ -22006,7 +22135,7 @@ function sweepScheduledSessionInbox(nowMs = Date.now()) {
|
|
|
22006
22135
|
total: due.length + pending.length
|
|
22007
22136
|
}, "self-schedule: promotion interrupted; retaining only the unpromoted entries");
|
|
22008
22137
|
try {
|
|
22009
|
-
|
|
22138
|
+
writeFileSync17(drained.processingPath, `${unfinished.map((entry) => JSON.stringify(entry)).join(`
|
|
22010
22139
|
`)}
|
|
22011
22140
|
`);
|
|
22012
22141
|
} catch (rewriteErr) {
|
|
@@ -22413,13 +22542,16 @@ __export(exports_src, {
|
|
|
22413
22542
|
startBashrsCompletionsWorker: () => startBashrsCompletionsWorker,
|
|
22414
22543
|
startAskUserQuestionGateOwner: () => startAskUserQuestionGateOwner,
|
|
22415
22544
|
startAiTurn: () => startAiTurn,
|
|
22545
|
+
stampUnscopedOtiumTopics: () => stampUnscopedOtiumTopics,
|
|
22416
22546
|
setTopicSurfaces: () => setTopicSurfaces,
|
|
22417
22547
|
setTopicSessionId: () => setTopicSessionId,
|
|
22548
|
+
setSurfaceScopeRequired: () => setSurfaceScopeRequired,
|
|
22418
22549
|
setRuntimeMcpPort: () => setRuntimeMcpPort,
|
|
22419
22550
|
setRuntimeBus: () => setRuntimeBus,
|
|
22420
22551
|
setPlaywrightUnavailableNotifier: () => setPlaywrightUnavailableNotifier,
|
|
22421
22552
|
setNodeMcpServers: () => setNodeMcpServers,
|
|
22422
22553
|
setFileHooks: () => setFileHooks,
|
|
22554
|
+
setDefaultSurfaceScope: () => setDefaultSurfaceScope,
|
|
22423
22555
|
setBashrsCompletionSink: () => setBashrsCompletionSink,
|
|
22424
22556
|
setApiTopicConfig: () => setApiTopicConfig,
|
|
22425
22557
|
sessionInboxPath: () => sessionInboxPath,
|
|
@@ -22461,6 +22593,7 @@ __export(exports_src, {
|
|
|
22461
22593
|
onShutdown: () => onShutdown,
|
|
22462
22594
|
normalizeVaultKey: () => normalizeVaultKey,
|
|
22463
22595
|
normalizeTopicSurface: () => normalizeTopicSurface,
|
|
22596
|
+
normalizeSurfaceScope: () => normalizeSurfaceScope,
|
|
22464
22597
|
nodeRequestHandlerNames: () => nodeRequestHandlerNames,
|
|
22465
22598
|
modelOwner: () => modelOwner,
|
|
22466
22599
|
markPlaywrightUnavailable: () => markPlaywrightUnavailable,
|
|
@@ -22488,6 +22621,7 @@ __export(exports_src, {
|
|
|
22488
22621
|
isTranscriptionConfigured: () => isTranscriptionConfigured,
|
|
22489
22622
|
isTopicVisible: () => isTopicVisible,
|
|
22490
22623
|
isTopicRunning: () => isTopicRunning,
|
|
22624
|
+
isSurfaceScopeRequired: () => isSurfaceScopeRequired,
|
|
22491
22625
|
isSensitivePath: () => isSensitivePath,
|
|
22492
22626
|
isParticipant: () => isParticipant,
|
|
22493
22627
|
isHostedMcpSurface: () => isHostedMcpSurface,
|
|
@@ -22537,6 +22671,7 @@ __export(exports_src, {
|
|
|
22537
22671
|
deleteVaultEntry: () => deleteVaultEntry,
|
|
22538
22672
|
deleteTopicCascade: () => deleteTopicCascade,
|
|
22539
22673
|
defaultTopicSurface: () => defaultTopicSurface,
|
|
22674
|
+
defaultSurfaceScope: () => defaultSurfaceScope,
|
|
22540
22675
|
db: () => db,
|
|
22541
22676
|
createSubagentManagementToolDefinitions: () => createSubagentManagementToolDefinitions,
|
|
22542
22677
|
createSpawnSubagentToolDefinition: () => createSpawnSubagentToolDefinition,
|
|
@@ -22834,6 +22969,7 @@ __export(exports_node_host, {
|
|
|
22834
22969
|
getVisibleTopics: () => getVisibleTopics,
|
|
22835
22970
|
getTopicStats: () => getTopicStats,
|
|
22836
22971
|
getTopic: () => getTopic,
|
|
22972
|
+
getApiMessage: () => getApiMessage,
|
|
22837
22973
|
flushBashrsCompletions: () => flushBashrsCompletions,
|
|
22838
22974
|
executeVaultCommand: () => executeVaultCommand,
|
|
22839
22975
|
ensurePersonalGeneral: () => ensurePersonalGeneral,
|
|
@@ -23168,6 +23304,7 @@ __export(exports_mcp_runtime_host, {
|
|
|
23168
23304
|
dispatchPeerRuntimeFile: () => dispatchPeerRuntimeFile,
|
|
23169
23305
|
dispatchPeerRuntimeAskUser: () => dispatchPeerRuntimeAskUser,
|
|
23170
23306
|
deleteTopicCascade: () => deleteTopicCascade,
|
|
23307
|
+
defaultTopicSurface: () => defaultTopicSurface,
|
|
23171
23308
|
createSubagentManagementToolDefinitions: () => createSubagentManagementToolDefinitions,
|
|
23172
23309
|
createSpawnSubagentToolDefinition: () => createSpawnSubagentToolDefinition,
|
|
23173
23310
|
createSelfConfigToolDefinitions: () => createSelfConfigToolDefinitions,
|
|
@@ -23224,6 +23361,9 @@ var init_mcp_runtime_host = __esm(async () => {
|
|
|
23224
23361
|
|
|
23225
23362
|
// ../../packages/mcp/src/node-tools.ts
|
|
23226
23363
|
import { z as z6 } from "zod";
|
|
23364
|
+
function callerSurface(ctx) {
|
|
23365
|
+
return getTopic(ctx.topicId)?.surface ?? defaultTopicSurface();
|
|
23366
|
+
}
|
|
23227
23367
|
function resolveTopicForUser(ctx, ref) {
|
|
23228
23368
|
const trimmed = ref.trim();
|
|
23229
23369
|
if (!trimmed)
|
|
@@ -23235,7 +23375,7 @@ function resolveTopicForUser(ctx, ref) {
|
|
|
23235
23375
|
return { error: notFound };
|
|
23236
23376
|
return { topic: byId };
|
|
23237
23377
|
}
|
|
23238
|
-
const byTitle = getTopicByNameForUser(trimmed, ctx.userId);
|
|
23378
|
+
const byTitle = getTopicByNameForUser(trimmed, ctx.userId, { surface: callerSurface(ctx) });
|
|
23239
23379
|
if (byTitle)
|
|
23240
23380
|
return { topic: byTitle };
|
|
23241
23381
|
return { error: notFound };
|
|
@@ -23257,6 +23397,7 @@ function registerNodeTools(server, ctx) {
|
|
|
23257
23397
|
const topic = registerTopic({
|
|
23258
23398
|
title,
|
|
23259
23399
|
userId: ctx.userId,
|
|
23400
|
+
surface: callerSurface(ctx),
|
|
23260
23401
|
agent,
|
|
23261
23402
|
model,
|
|
23262
23403
|
effort,
|
|
@@ -23278,7 +23419,7 @@ function registerNodeTools(server, ctx) {
|
|
|
23278
23419
|
}
|
|
23279
23420
|
});
|
|
23280
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 () => {
|
|
23281
|
-
const topics = getTopics().filter((topic) => isParticipant(topic, ctx.userId));
|
|
23422
|
+
const topics = getTopics({ surface: callerSurface(ctx) }).filter((topic) => isParticipant(topic, ctx.userId));
|
|
23282
23423
|
if (topics.length === 0) {
|
|
23283
23424
|
return textResult("No topics found. Use register_topic to create one.");
|
|
23284
23425
|
}
|
|
@@ -24255,13 +24396,15 @@ function currentTopic(context) {
|
|
|
24255
24396
|
return topic;
|
|
24256
24397
|
}
|
|
24257
24398
|
function targetCatalog(context) {
|
|
24258
|
-
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;
|
|
24259
24402
|
return createSessionTargetCatalog({
|
|
24260
24403
|
currentTopicId: context.currentTopicId,
|
|
24261
24404
|
currentTopicName: context.currentTopic,
|
|
24262
24405
|
currentSurface: surface,
|
|
24263
24406
|
isAgent: isAgentKind,
|
|
24264
|
-
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) => ({
|
|
24265
24408
|
id: topic.id,
|
|
24266
24409
|
title: topic.title,
|
|
24267
24410
|
kind: topic.kind ?? null,
|
|
@@ -24313,7 +24456,7 @@ function createDefaultSessionCommMcpHost() {
|
|
|
24313
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 ? `
|
|
24314
24457
|
description: ${topic.description.slice(0, 80)}` : ""}`);
|
|
24315
24458
|
if (!identity.restricted) {
|
|
24316
|
-
const peers = await peerSessionsForUser(context.userId, context.peerHostQueryId);
|
|
24459
|
+
const peers = await peerSessionsForUser(context.userId, context.peerHostQueryId, context.currentTopicId);
|
|
24317
24460
|
for (const node of peers.nodes ?? []) {
|
|
24318
24461
|
for (const session of node.sessions ?? []) {
|
|
24319
24462
|
if (session.agent)
|
|
@@ -25158,7 +25301,7 @@ import {
|
|
|
25158
25301
|
renameSync as renameSync11,
|
|
25159
25302
|
statSync as statSync15,
|
|
25160
25303
|
unlinkSync as unlinkSync19,
|
|
25161
|
-
writeFileSync as
|
|
25304
|
+
writeFileSync as writeFileSync18
|
|
25162
25305
|
} from "fs";
|
|
25163
25306
|
import { basename as basename7, dirname as dirname16, join as join36, relative as relative2, resolve as resolve17 } from "path";
|
|
25164
25307
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -25196,7 +25339,7 @@ function acquireRecoveryGuard(lockPath) {
|
|
|
25196
25339
|
try {
|
|
25197
25340
|
const fd = openSync4(guardPath, "wx");
|
|
25198
25341
|
try {
|
|
25199
|
-
|
|
25342
|
+
writeFileSync18(fd, ownerToken, "utf-8");
|
|
25200
25343
|
} catch (error2) {
|
|
25201
25344
|
closeSync4(fd);
|
|
25202
25345
|
try {
|
|
@@ -25229,7 +25372,7 @@ function acquireFileLock(path) {
|
|
|
25229
25372
|
try {
|
|
25230
25373
|
const fd = openSync4(lockPath, "wx");
|
|
25231
25374
|
try {
|
|
25232
|
-
|
|
25375
|
+
writeFileSync18(fd, ownerToken, "utf-8");
|
|
25233
25376
|
} catch (error2) {
|
|
25234
25377
|
closeSync4(fd);
|
|
25235
25378
|
try {
|
|
@@ -25273,7 +25416,7 @@ function acquireFileLock(path) {
|
|
|
25273
25416
|
}
|
|
25274
25417
|
function atomicWriteFile(path, content) {
|
|
25275
25418
|
const temporaryPath = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
25276
|
-
|
|
25419
|
+
writeFileSync18(temporaryPath, content, "utf-8");
|
|
25277
25420
|
try {
|
|
25278
25421
|
renameSync11(temporaryPath, path);
|
|
25279
25422
|
} catch (error2) {
|
|
@@ -26153,7 +26296,7 @@ ${fresh.join(`
|
|
|
26153
26296
|
const existing = readFileSync23(skillPath, "utf-8");
|
|
26154
26297
|
finalContent = mergeGotchas(content, extractGotchas(existing));
|
|
26155
26298
|
} catch {}
|
|
26156
|
-
|
|
26299
|
+
writeFileSync18(skillPath, finalContent, "utf-8");
|
|
26157
26300
|
return {
|
|
26158
26301
|
content: [
|
|
26159
26302
|
{
|
|
@@ -26172,7 +26315,7 @@ function writeSummaryDocument(rawTopic, content, dateStr) {
|
|
|
26172
26315
|
let counter = 1;
|
|
26173
26316
|
while (true) {
|
|
26174
26317
|
try {
|
|
26175
|
-
|
|
26318
|
+
writeFileSync18(resolve17(runtime().summariesDir, summaryName), content, {
|
|
26176
26319
|
encoding: "utf-8",
|
|
26177
26320
|
flag: "wx"
|
|
26178
26321
|
});
|
|
@@ -26212,7 +26355,7 @@ function writeTopicDocument(rawTopic, content) {
|
|
|
26212
26355
|
const briefPath = resolve17(runtime().topicsDir, `${fileSlug}.md`);
|
|
26213
26356
|
ensureDir(dirname16(briefPath));
|
|
26214
26357
|
const existed = existsSync27(briefPath);
|
|
26215
|
-
|
|
26358
|
+
writeFileSync18(briefPath, content, "utf-8");
|
|
26216
26359
|
const setTopicBrief2 = runtime().host.setTopicBrief;
|
|
26217
26360
|
let sqliteUpdated = false;
|
|
26218
26361
|
if (topicId && setTopicBrief2) {
|
|
@@ -26879,7 +27022,7 @@ import {
|
|
|
26879
27022
|
renameSync as renameSync12,
|
|
26880
27023
|
statSync as statSync16,
|
|
26881
27024
|
unlinkSync as unlinkSync20,
|
|
26882
|
-
writeFileSync as
|
|
27025
|
+
writeFileSync as writeFileSync19
|
|
26883
27026
|
} from "fs";
|
|
26884
27027
|
import { basename as basename8, join as join37 } from "path";
|
|
26885
27028
|
function startLogRotation() {
|
|
@@ -26979,7 +27122,7 @@ function removeSentFilesForTopic(userId, topicName) {
|
|
|
26979
27122
|
}
|
|
26980
27123
|
});
|
|
26981
27124
|
if (remaining.length > 0) {
|
|
26982
|
-
|
|
27125
|
+
writeFileSync19(file, `${remaining.join(`
|
|
26983
27126
|
`)}
|
|
26984
27127
|
`);
|
|
26985
27128
|
} else {
|
|
@@ -27501,6 +27644,7 @@ __export(exports_storage_public, {
|
|
|
27501
27644
|
tasks: () => exports_tasks,
|
|
27502
27645
|
taskScopeKey: () => taskScopeKey,
|
|
27503
27646
|
taskFileMtimeNs: () => taskFileMtimeNs,
|
|
27647
|
+
stampUnscopedOtiumTopics: () => stampUnscopedOtiumTopics,
|
|
27504
27648
|
softDeleteApiMessagesByIdPrefix: () => softDeleteApiMessagesByIdPrefix,
|
|
27505
27649
|
softDeleteApiMessage: () => softDeleteApiMessage,
|
|
27506
27650
|
settleTopicArchiveJob: () => settleTopicArchiveJob,
|
|
@@ -27515,10 +27659,12 @@ __export(exports_storage_public, {
|
|
|
27515
27659
|
setTopicAgentAndSession: () => setTopicAgentAndSession,
|
|
27516
27660
|
setTopicAgentAndClearSession: () => setTopicAgentAndClearSession,
|
|
27517
27661
|
setTopicAgent: () => setTopicAgent,
|
|
27662
|
+
setSurfaceScopeRequired: () => setSurfaceScopeRequired,
|
|
27518
27663
|
setSessionForTopic: () => setSessionForTopic,
|
|
27519
27664
|
setLastShownConfig: () => setLastShownConfig,
|
|
27520
27665
|
setGlobalAiName: () => setGlobalAiName,
|
|
27521
27666
|
setDmSessionId: () => setDmSessionId,
|
|
27667
|
+
setDefaultSurfaceScope: () => setDefaultSurfaceScope,
|
|
27522
27668
|
setApiTopicConfig: () => setApiTopicConfig,
|
|
27523
27669
|
setApiTopicAgent: () => setApiTopicAgent,
|
|
27524
27670
|
setApiMessageReactions: () => setApiMessageReactions,
|
|
@@ -27551,6 +27697,7 @@ __export(exports_storage_public, {
|
|
|
27551
27697
|
normalizeTopicSurface: () => normalizeTopicSurface,
|
|
27552
27698
|
normalizeTopicState: () => normalizeTopicState,
|
|
27553
27699
|
normalizeTopicKind: () => normalizeTopicKind,
|
|
27700
|
+
normalizeSurfaceScope: () => normalizeSurfaceScope,
|
|
27554
27701
|
normalizeAiMode: () => normalizeAiMode,
|
|
27555
27702
|
markPendingAskState: () => markPendingAskState,
|
|
27556
27703
|
markPendingAskSources: () => markPendingAskSources,
|
|
@@ -27566,6 +27713,7 @@ __export(exports_storage_public, {
|
|
|
27566
27713
|
isTopicVisible: () => isTopicVisible,
|
|
27567
27714
|
isTopicSummaryFile: () => isTopicSummaryFile,
|
|
27568
27715
|
isTopicBriefFile: () => isTopicBriefFile,
|
|
27716
|
+
isSurfaceScopeRequired: () => isSurfaceScopeRequired,
|
|
27569
27717
|
isRuntimeProcessLeaseAlive: () => isRuntimeProcessLeaseAlive,
|
|
27570
27718
|
inferTopicKind: () => inferTopicKind,
|
|
27571
27719
|
inferAiMode: () => inferAiMode,
|
|
@@ -27618,6 +27766,7 @@ __export(exports_storage_public, {
|
|
|
27618
27766
|
findRecentUserMessage: () => findRecentUserMessage,
|
|
27619
27767
|
findLastSessionIdForAgent: () => findLastSessionIdForAgent,
|
|
27620
27768
|
describePendingAskState: () => describePendingAskState,
|
|
27769
|
+
deleteTopicStats: () => deleteTopicStats,
|
|
27621
27770
|
deleteTopicBrief: () => deleteTopicBrief,
|
|
27622
27771
|
deleteTopicArchiveState: () => deleteTopicArchiveState,
|
|
27623
27772
|
deleteTopic: () => deleteTopic,
|
|
@@ -27626,6 +27775,7 @@ __export(exports_storage_public, {
|
|
|
27626
27775
|
deleteMessagesForTopic: () => deleteMessagesForTopic,
|
|
27627
27776
|
deleteApiTopicConfig: () => deleteApiTopicConfig,
|
|
27628
27777
|
defaultTopicSurface: () => defaultTopicSurface,
|
|
27778
|
+
defaultSurfaceScope: () => defaultSurfaceScope,
|
|
27629
27779
|
db: () => db2,
|
|
27630
27780
|
createTasks: () => createTasks,
|
|
27631
27781
|
createPendingAsk: () => createPendingAsk,
|
|
@@ -28485,7 +28635,7 @@ var init_spec = __esm(() => {
|
|
|
28485
28635
|
});
|
|
28486
28636
|
|
|
28487
28637
|
// ../../packages/mcp-host/src/manifest.ts
|
|
28488
|
-
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";
|
|
28489
28639
|
import { dirname as dirname17 } from "path";
|
|
28490
28640
|
import { z as z14 } from "zod";
|
|
28491
28641
|
|
|
@@ -28552,7 +28702,7 @@ class McpManifest {
|
|
|
28552
28702
|
servers: [...this.entries.values()]
|
|
28553
28703
|
};
|
|
28554
28704
|
const tmp = `${this.file}.tmp-${process.pid}`;
|
|
28555
|
-
|
|
28705
|
+
writeFileSync20(tmp, `${JSON.stringify(payload, null, 2)}
|
|
28556
28706
|
`);
|
|
28557
28707
|
renameSync13(tmp, this.file);
|
|
28558
28708
|
}
|
|
@@ -28587,7 +28737,7 @@ import {
|
|
|
28587
28737
|
readFileSync as readFileSync25,
|
|
28588
28738
|
renameSync as renameSync14,
|
|
28589
28739
|
unlinkSync as unlinkSync21,
|
|
28590
|
-
writeFileSync as
|
|
28740
|
+
writeFileSync as writeFileSync21
|
|
28591
28741
|
} from "fs";
|
|
28592
28742
|
import { connect, createServer as createServer3 } from "net";
|
|
28593
28743
|
import { join as join38 } from "path";
|
|
@@ -28904,7 +29054,7 @@ class McpHost {
|
|
|
28904
29054
|
mkdirSync27(this.portsDir, { recursive: true });
|
|
28905
29055
|
const file = join38(this.portsDir, fileName);
|
|
28906
29056
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
28907
|
-
|
|
29057
|
+
writeFileSync21(tmp, String(port));
|
|
28908
29058
|
renameSync14(tmp, file);
|
|
28909
29059
|
} catch (e) {
|
|
28910
29060
|
this.log("warn", "Failed to write MCP port file", { fileName, port, err: String(e) });
|
|
@@ -29022,7 +29172,7 @@ import {
|
|
|
29022
29172
|
readFileSync as readFileSync26,
|
|
29023
29173
|
rmSync as rmSync9,
|
|
29024
29174
|
statSync as statSync18,
|
|
29025
|
-
writeFileSync as
|
|
29175
|
+
writeFileSync as writeFileSync22
|
|
29026
29176
|
} from "fs";
|
|
29027
29177
|
import { basename as basename10, extname as extname4, join as join39 } from "path";
|
|
29028
29178
|
function safeExtension(filename) {
|
|
@@ -29112,7 +29262,7 @@ class NodeFileStore {
|
|
|
29112
29262
|
...access.visibility ? { visibility: access.visibility } : {}
|
|
29113
29263
|
};
|
|
29114
29264
|
copyFileSync4(absPath, savedPath);
|
|
29115
|
-
|
|
29265
|
+
writeFileSync22(this.#metadataPath(fileId), JSON.stringify(metadata), { mode: 384 });
|
|
29116
29266
|
return this.#attachment(fileId, metadata);
|
|
29117
29267
|
} catch (error2) {
|
|
29118
29268
|
rmSync9(savedPath, { force: true });
|
|
@@ -29305,12 +29455,38 @@ import {
|
|
|
29305
29455
|
readFileSync as readFileSync27,
|
|
29306
29456
|
renameSync as renameSync15,
|
|
29307
29457
|
unlinkSync as unlinkSync22,
|
|
29308
|
-
writeFileSync as
|
|
29458
|
+
writeFileSync as writeFileSync23
|
|
29309
29459
|
} from "fs";
|
|
29310
29460
|
import { dirname as dirname18, resolve as resolve21 } from "path";
|
|
29311
29461
|
function jsonError(status, error2) {
|
|
29312
29462
|
return Response.json({ ok: false, error: error2 }, { status });
|
|
29313
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
|
+
}
|
|
29314
29490
|
function topicServiceError(error2) {
|
|
29315
29491
|
const status = error2.code === "TOPIC_NOT_FOUND" ? 404 : error2.code === "TOPIC_FORBIDDEN" ? 403 : 400;
|
|
29316
29492
|
return jsonError(status, error2.message);
|
|
@@ -29365,7 +29541,21 @@ function runtimeEvent(event) {
|
|
|
29365
29541
|
createdAt: event.createdAt
|
|
29366
29542
|
};
|
|
29367
29543
|
}
|
|
29368
|
-
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
|
+
};
|
|
29369
29559
|
let cursor = Math.max(0, after);
|
|
29370
29560
|
return createPollingSseStream(req, {
|
|
29371
29561
|
ready: { v: NODE_RUNTIME_CONTRACT_VERSION, cursor },
|
|
@@ -29376,7 +29566,7 @@ function createRuntimeContractEventStream(req, after, topicId) {
|
|
|
29376
29566
|
break;
|
|
29377
29567
|
for (const event of events) {
|
|
29378
29568
|
cursor = event.seq;
|
|
29379
|
-
if (!topicId || event.topicId === topicId) {
|
|
29569
|
+
if ((!topicId || event.topicId === topicId) && eventInScope(event.topicId)) {
|
|
29380
29570
|
send("runtime", runtimeEvent(event), event.seq);
|
|
29381
29571
|
}
|
|
29382
29572
|
}
|
|
@@ -29387,8 +29577,8 @@ function createRuntimeContractEventStream(req, after, topicId) {
|
|
|
29387
29577
|
}
|
|
29388
29578
|
});
|
|
29389
29579
|
}
|
|
29390
|
-
function createEventStream(req, userId, after) {
|
|
29391
|
-
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));
|
|
29392
29582
|
let cursor = Math.max(0, after);
|
|
29393
29583
|
return createPollingSseStream(req, {
|
|
29394
29584
|
ready: { protocolVersion: NODE_CONTROL_PROTOCOL_VERSION, cursor },
|
|
@@ -29401,7 +29591,8 @@ function createEventStream(req, userId, after) {
|
|
|
29401
29591
|
cursor = event.seq;
|
|
29402
29592
|
if (event.type === "topic-created" || event.type === "topic-updated") {
|
|
29403
29593
|
const topic = getTopic(event.topicId);
|
|
29404
|
-
|
|
29594
|
+
const admissible = topic && isParticipant(topic, userId) && (!surface || topic.surface === surface);
|
|
29595
|
+
if (admissible)
|
|
29405
29596
|
allowedTopics.add(event.topicId);
|
|
29406
29597
|
else
|
|
29407
29598
|
allowedTopics.delete(event.topicId);
|
|
@@ -29453,6 +29644,10 @@ function createNodeControlHandler(options) {
|
|
|
29453
29644
|
if (body.v !== NODE_RUNTIME_CONTRACT_VERSION)
|
|
29454
29645
|
return jsonError(400, "Unsupported v");
|
|
29455
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
|
+
}
|
|
29456
29651
|
const userId = requiredText(body.userId, "userId");
|
|
29457
29652
|
const actorUserId = body.actorUserId === undefined ? undefined : requiredText(body.actorUserId, "actorUserId");
|
|
29458
29653
|
const actorLabel = body.actorLabel === undefined ? undefined : requiredText(body.actorLabel, "actorLabel");
|
|
@@ -29460,12 +29655,19 @@ function createNodeControlHandler(options) {
|
|
|
29460
29655
|
const text2 = requiredText(body.text, "text");
|
|
29461
29656
|
const clientMessageId = requiredText(body.clientMessageId, "clientMessageId");
|
|
29462
29657
|
const requestId = body.requestId === undefined ? undefined : requiredText(body.requestId, "requestId");
|
|
29658
|
+
const threadRootId = body.threadRootId === undefined ? undefined : requiredText(body.threadRootId, "threadRootId");
|
|
29463
29659
|
const topic = getTopic(topicId);
|
|
29464
29660
|
if (!topic)
|
|
29465
29661
|
return jsonError(404, "Topic not found");
|
|
29466
29662
|
if (!topic.participants.some((participant) => participant.userId === userId)) {
|
|
29467
29663
|
return jsonError(404, "Topic not found");
|
|
29468
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
|
+
}
|
|
29469
29671
|
const submission = submitRuntimeGatewayTurn({
|
|
29470
29672
|
topic,
|
|
29471
29673
|
userId,
|
|
@@ -29475,7 +29677,8 @@ function createNodeControlHandler(options) {
|
|
|
29475
29677
|
text: text2,
|
|
29476
29678
|
clientMessageId,
|
|
29477
29679
|
requestId,
|
|
29478
|
-
allowAutoContinue: body.allowAutoContinue !== false
|
|
29680
|
+
allowAutoContinue: body.allowAutoContinue !== false,
|
|
29681
|
+
...threadRootId ? { threadRootId } : {}
|
|
29479
29682
|
});
|
|
29480
29683
|
return Response.json({
|
|
29481
29684
|
ok: true,
|
|
@@ -29492,13 +29695,15 @@ function createNodeControlHandler(options) {
|
|
|
29492
29695
|
if (req.method === "GET" && runtimePath === "/events") {
|
|
29493
29696
|
const parsed = Number.parseInt(url.searchParams.get("after") ?? "0", 10);
|
|
29494
29697
|
const topicId = url.searchParams.get("topicId")?.trim() || undefined;
|
|
29495
|
-
return createRuntimeContractEventStream(req, Number.isFinite(parsed) ? parsed : 0, topicId);
|
|
29698
|
+
return createRuntimeContractEventStream(req, Number.isFinite(parsed) ? parsed : 0, topicId, requestSurfaceScope(req));
|
|
29496
29699
|
}
|
|
29497
29700
|
const runtimeMessagesMatch = runtimePath.match(/^\/topics\/([^/]+)\/messages$/);
|
|
29498
29701
|
if (runtimeMessagesMatch && req.method === "GET") {
|
|
29499
29702
|
const topicId = decodeURIComponent(runtimeMessagesMatch[1]);
|
|
29500
|
-
|
|
29703
|
+
const messagesTopic = getTopic(topicId);
|
|
29704
|
+
if (!messagesTopic || !topicInRequestScope(req, messagesTopic)) {
|
|
29501
29705
|
return jsonError(404, "Topic not found");
|
|
29706
|
+
}
|
|
29502
29707
|
const cursor = url.searchParams.get("cursor");
|
|
29503
29708
|
const parsedLimit = Number.parseInt(url.searchParams.get("limit") ?? "50", 10);
|
|
29504
29709
|
const result = listApiMessages(topicId, {
|
|
@@ -29508,7 +29713,7 @@ function createNodeControlHandler(options) {
|
|
|
29508
29713
|
return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, ...result });
|
|
29509
29714
|
}
|
|
29510
29715
|
if (req.method === "GET" && runtimePath === "/topics") {
|
|
29511
|
-
const topics =
|
|
29716
|
+
const topics = gatewayVisibleTopics(req);
|
|
29512
29717
|
return Response.json({
|
|
29513
29718
|
ok: true,
|
|
29514
29719
|
v: NODE_RUNTIME_CONTRACT_VERSION,
|
|
@@ -29531,6 +29736,7 @@ function createNodeControlHandler(options) {
|
|
|
29531
29736
|
userId,
|
|
29532
29737
|
kind: "agent",
|
|
29533
29738
|
surface: "otium",
|
|
29739
|
+
...requestSurfaceScope(req),
|
|
29534
29740
|
...agent ? { agent } : {}
|
|
29535
29741
|
});
|
|
29536
29742
|
return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, topic }, { status: 201 });
|
|
@@ -29542,7 +29748,7 @@ function createNodeControlHandler(options) {
|
|
|
29542
29748
|
return jsonError(400, "Unsupported v");
|
|
29543
29749
|
const topicId = decodeURIComponent(importMatch[1]);
|
|
29544
29750
|
const topic = getTopic(topicId);
|
|
29545
|
-
if (!topic)
|
|
29751
|
+
if (!topic || !topicInRequestScope(req, topic))
|
|
29546
29752
|
return jsonError(404, "Topic not found");
|
|
29547
29753
|
if (!Array.isArray(body.messages)) {
|
|
29548
29754
|
return jsonError(400, "messages must be an array");
|
|
@@ -29587,6 +29793,8 @@ function createNodeControlHandler(options) {
|
|
|
29587
29793
|
const runtimeTopicMatch = runtimePath.match(/^\/topics\/([^/]+)$/);
|
|
29588
29794
|
if (runtimeTopicMatch && req.method === "GET") {
|
|
29589
29795
|
const topic = getTopic(decodeURIComponent(runtimeTopicMatch[1]));
|
|
29796
|
+
if (topic && !topicInRequestScope(req, topic))
|
|
29797
|
+
return jsonError(404, "Topic not found");
|
|
29590
29798
|
if (!topic)
|
|
29591
29799
|
return jsonError(404, "Topic not found");
|
|
29592
29800
|
return Response.json({ ok: true, v: NODE_RUNTIME_CONTRACT_VERSION, topic });
|
|
@@ -29610,7 +29818,7 @@ function createNodeControlHandler(options) {
|
|
|
29610
29818
|
}
|
|
29611
29819
|
if (req.method === "GET" && path === "/session") {
|
|
29612
29820
|
const userId = requiredText(url.searchParams.get("user"), "user");
|
|
29613
|
-
ensurePersonalGeneral(userId);
|
|
29821
|
+
ensurePersonalGeneral(userId, requestedSurface(url));
|
|
29614
29822
|
return Response.json({
|
|
29615
29823
|
ok: true,
|
|
29616
29824
|
protocolVersion: NODE_CONTROL_PROTOCOL_VERSION,
|
|
@@ -29681,7 +29889,7 @@ function createNodeControlHandler(options) {
|
|
|
29681
29889
|
if (req.method === "GET" && path === "/events") {
|
|
29682
29890
|
const userId = requiredText(url.searchParams.get("user"), "user");
|
|
29683
29891
|
const parsed = Number.parseInt(url.searchParams.get("after") ?? "0", 10);
|
|
29684
|
-
return createEventStream(req, userId, Number.isFinite(parsed) ? parsed : 0);
|
|
29892
|
+
return createEventStream(req, userId, Number.isFinite(parsed) ? parsed : 0, requestedSurface(url));
|
|
29685
29893
|
}
|
|
29686
29894
|
const messagesMatch = path.match(/^\/topics\/([^/]+)\/messages$/);
|
|
29687
29895
|
if (messagesMatch && req.method === "GET") {
|
|
@@ -29878,7 +30086,7 @@ function writeNodeDaemonInfo(port, startedAt) {
|
|
|
29878
30086
|
};
|
|
29879
30087
|
mkdirSync29(dirname18(NODE_DAEMON_INFO_PATH), { recursive: true });
|
|
29880
30088
|
const temporary = `${NODE_DAEMON_INFO_PATH}.${process.pid}.${randomUUID25()}.tmp`;
|
|
29881
|
-
|
|
30089
|
+
writeFileSync23(temporary, `${JSON.stringify(info, null, 2)}
|
|
29882
30090
|
`, { mode: 384 });
|
|
29883
30091
|
chmodSync6(temporary, 384);
|
|
29884
30092
|
renameSync15(temporary, NODE_DAEMON_INFO_PATH);
|
|
@@ -29966,7 +30174,7 @@ async function stopNodeDaemon(timeoutMs = 3000) {
|
|
|
29966
30174
|
throw new Error(`node shutdown returned HTTP ${response.status}`);
|
|
29967
30175
|
return true;
|
|
29968
30176
|
}
|
|
29969
|
-
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;
|
|
29970
30178
|
var init_control = __esm(async () => {
|
|
29971
30179
|
await init_node_host();
|
|
29972
30180
|
await init_files();
|
|
@@ -32895,8 +33103,10 @@ function activeContextBreakdown(state) {
|
|
|
32895
33103
|
if (!topic)
|
|
32896
33104
|
return;
|
|
32897
33105
|
const messages = activeMessages(state);
|
|
32898
|
-
const
|
|
32899
|
-
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);
|
|
32900
33110
|
const confirmed = latest?.usage.context ?? stored?.contextTokens;
|
|
32901
33111
|
const contextWindow = latest?.usage.contextWindow ?? stored?.contextWindow;
|
|
32902
33112
|
if (confirmed === undefined || !contextWindow)
|
|
@@ -34184,10 +34394,10 @@ function topicPickerHints(topicPickerRoot) {
|
|
|
34184
34394
|
`\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 ${exit}`,
|
|
34185
34395
|
`\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 ${shortExit}`,
|
|
34186
34396
|
`\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete \xB7 ${exit}`,
|
|
34187
|
-
`\u2191\u2193 select \xB7 Enter open \xB7 type to filter \xB7 Ctrl-N/D
|
|
34188
|
-
"\u2191\u2193 \xB7 Enter \xB7 type to filter \xB7 Ctrl-N/D
|
|
34189
|
-
"type to filter \xB7 Ctrl-N/D
|
|
34190
|
-
"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"
|
|
34191
34401
|
];
|
|
34192
34402
|
return [...new Set(candidates)].sort((a, b) => displayWidth(b) - displayWidth(a));
|
|
34193
34403
|
}
|
|
@@ -35394,7 +35604,7 @@ class EmbeddedNegotiumClient {
|
|
|
35394
35604
|
try {
|
|
35395
35605
|
if (this.#startNode)
|
|
35396
35606
|
this.#node = await startDefaultNode({ port: this.#port });
|
|
35397
|
-
ensurePersonalGeneral(this.#userId);
|
|
35607
|
+
ensurePersonalGeneral(this.#userId, "terminal");
|
|
35398
35608
|
} catch (error2) {
|
|
35399
35609
|
this.#unsubscribe?.();
|
|
35400
35610
|
this.#unsubscribe = null;
|
|
@@ -35778,7 +35988,7 @@ class RemoteNegotiumClient {
|
|
|
35778
35988
|
}
|
|
35779
35989
|
}
|
|
35780
35990
|
async#openEventStream(after, signal) {
|
|
35781
|
-
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`, {
|
|
35782
35992
|
signal,
|
|
35783
35993
|
headers: {
|
|
35784
35994
|
accept: "text/event-stream",
|
|
@@ -39864,7 +40074,7 @@ function startTelegramAdapter(opts) {
|
|
|
39864
40074
|
if (initialForumChatId !== undefined && !forumMode) {
|
|
39865
40075
|
logger.warn({ forumChatId: initialForumChatId }, "telegram adapter: forumChatId set but client lacks createForumTopic \u2014 forum mode disabled");
|
|
39866
40076
|
}
|
|
39867
|
-
const personalGeneral = ensurePersonalGeneral(userId);
|
|
40077
|
+
const personalGeneral = ensurePersonalGeneral(userId, "telegram");
|
|
39868
40078
|
const byKey = new Map;
|
|
39869
40079
|
const byTopic = new Map;
|
|
39870
40080
|
const targetByQueryId = new Map;
|
|
@@ -40111,7 +40321,9 @@ function startTelegramAdapter(opts) {
|
|
|
40111
40321
|
return true;
|
|
40112
40322
|
}
|
|
40113
40323
|
if (!store.isFlagSet(SURFACE_BACKFILL_FLAG)) {
|
|
40114
|
-
const mappedIds = [
|
|
40324
|
+
const mappedIds = [
|
|
40325
|
+
...new Set(store.load().map((mapping) => mapping.topicId).filter((topicId) => getTopic(topicId)?.kind !== "manager"))
|
|
40326
|
+
];
|
|
40115
40327
|
const moved = setTopicSurfaces(mappedIds, "telegram");
|
|
40116
40328
|
store.setFlag(SURFACE_BACKFILL_FLAG);
|
|
40117
40329
|
if (moved > 0) {
|
|
@@ -41360,45 +41572,101 @@ var init_join_status = __esm(() => {
|
|
|
41360
41572
|
});
|
|
41361
41573
|
|
|
41362
41574
|
// ../../adapters/otium/src/control-protocol.ts
|
|
41363
|
-
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";
|
|
41364
41576
|
|
|
41365
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
|
+
}
|
|
41366
41600
|
function configureOtiumCentral(join42) {
|
|
41367
|
-
|
|
41368
|
-
|
|
41601
|
+
cells.clear();
|
|
41602
|
+
if (join42)
|
|
41603
|
+
attachOtiumCentralCell(join42);
|
|
41369
41604
|
}
|
|
41370
|
-
function
|
|
41371
|
-
return
|
|
41605
|
+
function isOtiumCentralConfigured() {
|
|
41606
|
+
return cells.size > 0;
|
|
41372
41607
|
}
|
|
41373
|
-
function centralFetch(path, init) {
|
|
41374
|
-
|
|
41375
|
-
throw new Error("otium: join credentials missing");
|
|
41376
|
-
return fetch(`${joinConfig.central}${path}`, {
|
|
41608
|
+
function centralFetch(cell, path, init) {
|
|
41609
|
+
return fetch(`${cell.join.central}${path}`, {
|
|
41377
41610
|
...init,
|
|
41378
41611
|
headers: {
|
|
41379
|
-
authorization: `Bearer ${
|
|
41612
|
+
authorization: `Bearer ${cell.join.secret}`,
|
|
41380
41613
|
"content-type": "application/json",
|
|
41381
41614
|
...init.headers ?? {}
|
|
41382
41615
|
},
|
|
41383
41616
|
signal: AbortSignal.timeout(5000)
|
|
41384
41617
|
});
|
|
41385
41618
|
}
|
|
41386
|
-
async function
|
|
41387
|
-
if (!
|
|
41388
|
-
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;
|
|
41389
41622
|
}
|
|
41390
|
-
const response = await centralFetch("/peer/nodes", { method: "GET" });
|
|
41623
|
+
const response = await centralFetch(cell, "/peer/nodes", { method: "GET" });
|
|
41391
41624
|
const body = await response.json();
|
|
41392
41625
|
if (!response.ok || !body.ok || !Array.isArray(body.nodes)) {
|
|
41393
41626
|
throw new Error(`otium: node discovery failed: ${body.error ?? response.status}`);
|
|
41394
41627
|
}
|
|
41395
|
-
|
|
41396
|
-
|
|
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;
|
|
41397
41631
|
}
|
|
41398
|
-
async function
|
|
41399
|
-
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);
|
|
41400
41654
|
return nodes.find((node) => node.self) ?? null;
|
|
41401
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
|
+
}
|
|
41402
41670
|
async function resolvePeerNodeByCellId(cellId) {
|
|
41403
41671
|
const find = (nodes) => nodes.find((node) => node.cellId === cellId) ?? null;
|
|
41404
41672
|
const cached = find(await listPeerNodes());
|
|
@@ -41406,36 +41674,39 @@ async function resolvePeerNodeByCellId(cellId) {
|
|
|
41406
41674
|
return cached;
|
|
41407
41675
|
return find(await listPeerNodes({ fresh: true }));
|
|
41408
41676
|
}
|
|
41409
|
-
async function mintPeerToken(
|
|
41410
|
-
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);
|
|
41411
41682
|
if (cached && cached.expiresAtMs - Date.now() > 30000)
|
|
41412
41683
|
return cached.token;
|
|
41413
|
-
const response = await centralFetch("/peer/token", {
|
|
41684
|
+
const response = await centralFetch(cell, "/peer/token", {
|
|
41414
41685
|
method: "POST",
|
|
41415
|
-
body: JSON.stringify({ toCellId })
|
|
41686
|
+
body: JSON.stringify({ toCellId: target.cellId })
|
|
41416
41687
|
});
|
|
41417
41688
|
const body = await response.json();
|
|
41418
41689
|
if (!response.ok || !body.ok || !body.token) {
|
|
41419
41690
|
throw new Error(`otium: peer token mint failed: ${body.error ?? response.status}`);
|
|
41420
41691
|
}
|
|
41421
|
-
tokenCache.set(
|
|
41692
|
+
cell.tokenCache.set(target.cellId, {
|
|
41422
41693
|
token: body.token,
|
|
41423
41694
|
expiresAtMs: Date.parse(body.expiresAt ?? "") || Date.now() + 60000
|
|
41424
41695
|
});
|
|
41425
41696
|
return body.token;
|
|
41426
41697
|
}
|
|
41427
|
-
async function
|
|
41428
|
-
const cached = verifyCache.get(token);
|
|
41698
|
+
async function verifyAgainstCell(cell, token) {
|
|
41699
|
+
const cached = cell.verifyCache.get(token);
|
|
41429
41700
|
if (cached && Date.now() - cached.at < VERIFY_CACHE_MS)
|
|
41430
41701
|
return cached.verified;
|
|
41431
41702
|
let response;
|
|
41432
41703
|
try {
|
|
41433
|
-
response = await centralFetch("/peer/verify", {
|
|
41704
|
+
response = await centralFetch(cell, "/peer/verify", {
|
|
41434
41705
|
method: "POST",
|
|
41435
41706
|
body: JSON.stringify({ token })
|
|
41436
41707
|
});
|
|
41437
41708
|
} catch (err2) {
|
|
41438
|
-
logger.warn({ err: err2 }, "otium: central verify unreachable");
|
|
41709
|
+
logger.warn({ err: err2, cellId: cell.join.cellId }, "otium: central verify unreachable");
|
|
41439
41710
|
return null;
|
|
41440
41711
|
}
|
|
41441
41712
|
const body = await response.json().catch(() => null);
|
|
@@ -41446,25 +41717,39 @@ async function verifyPeerToken(token) {
|
|
|
41446
41717
|
fromCellId: body.fromCellId,
|
|
41447
41718
|
fromNodeName: body.fromNodeName,
|
|
41448
41719
|
fromIsPrimary: body.fromIsPrimary,
|
|
41449
|
-
expiresAt: body.expiresAt
|
|
41720
|
+
expiresAt: body.expiresAt,
|
|
41721
|
+
viaCellId: cell.join.cellId
|
|
41450
41722
|
};
|
|
41451
|
-
verifyCache.set(token, { verified, at: Date.now() });
|
|
41452
|
-
for (const [key, entry] of verifyCache) {
|
|
41723
|
+
cell.verifyCache.set(token, { verified, at: Date.now() });
|
|
41724
|
+
for (const [key, entry] of cell.verifyCache) {
|
|
41453
41725
|
if (Date.now() - entry.at > VERIFY_CACHE_MS)
|
|
41454
|
-
verifyCache.delete(key);
|
|
41726
|
+
cell.verifyCache.delete(key);
|
|
41455
41727
|
}
|
|
41456
41728
|
return verified;
|
|
41457
41729
|
}
|
|
41458
|
-
function
|
|
41459
|
-
|
|
41460
|
-
|
|
41461
|
-
|
|
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;
|
|
41462
41747
|
}
|
|
41463
|
-
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;
|
|
41464
41749
|
var init_central = __esm(async () => {
|
|
41465
41750
|
await init_src();
|
|
41466
|
-
|
|
41467
|
-
|
|
41751
|
+
cells = new Map;
|
|
41752
|
+
rejectCache = new Map;
|
|
41468
41753
|
});
|
|
41469
41754
|
|
|
41470
41755
|
// ../../adapters/otium/src/protocol.ts
|
|
@@ -41539,7 +41824,7 @@ async function forwardCanonicalTool(capability, request) {
|
|
|
41539
41824
|
if (!hub?.isPrimary || hub.self)
|
|
41540
41825
|
return { error: "canonical hub is unavailable", status: 503 };
|
|
41541
41826
|
try {
|
|
41542
|
-
const peerToken = await mintPeerToken(hub
|
|
41827
|
+
const peerToken = await mintPeerToken(hub);
|
|
41543
41828
|
const response = await fetch(`${hub.baseUrl.replace(/\/+$/, "")}/api/v1/peer/bridge/canonical-mcp`, {
|
|
41544
41829
|
method: "POST",
|
|
41545
41830
|
headers: { authorization: `Bearer ${peerToken}`, "content-type": "application/json" },
|
|
@@ -41712,6 +41997,7 @@ __export(exports_join, {
|
|
|
41712
41997
|
saveJoin: () => saveJoin,
|
|
41713
41998
|
removeJoin: () => removeJoin,
|
|
41714
41999
|
parseInviteCode: () => parseInviteCode,
|
|
42000
|
+
loadJoins: () => loadJoins,
|
|
41715
42001
|
loadJoin: () => loadJoin,
|
|
41716
42002
|
joinFilePath: () => joinFilePath,
|
|
41717
42003
|
joinCredentialDigest: () => joinCredentialDigest,
|
|
@@ -41732,7 +42018,7 @@ import {
|
|
|
41732
42018
|
rmSync as rmSync10,
|
|
41733
42019
|
statSync as statSync20,
|
|
41734
42020
|
unlinkSync as unlinkSync23,
|
|
41735
|
-
writeFileSync as
|
|
42021
|
+
writeFileSync as writeFileSync24
|
|
41736
42022
|
} from "fs";
|
|
41737
42023
|
import { dirname as dirname21, resolve as resolve24 } from "path";
|
|
41738
42024
|
function joinFilePath() {
|
|
@@ -41813,7 +42099,7 @@ function withJoinCredentialLock(operation) {
|
|
|
41813
42099
|
try {
|
|
41814
42100
|
mkdirSync32(lockPath, { mode: 448 });
|
|
41815
42101
|
created = true;
|
|
41816
|
-
|
|
42102
|
+
writeFileSync24(ownerPath, `${JSON.stringify(owner)}
|
|
41817
42103
|
`, { mode: 384 });
|
|
41818
42104
|
const ownerFd = openSync5(ownerPath, "r");
|
|
41819
42105
|
try {
|
|
@@ -41883,82 +42169,60 @@ function normalizedJoin(join42) {
|
|
|
41883
42169
|
function joinCredentialDigest(join42) {
|
|
41884
42170
|
return createHash11("sha256").update(JSON.stringify(normalizedJoin(join42))).digest("base64url");
|
|
41885
42171
|
}
|
|
41886
|
-
function
|
|
42172
|
+
function readPersistedJoins(path = joinFilePath()) {
|
|
41887
42173
|
if (!existsSync37(path))
|
|
41888
|
-
return
|
|
42174
|
+
return [];
|
|
41889
42175
|
const parsed = JSON.parse(readFileSync28(path, "utf-8"));
|
|
41890
42176
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
41891
42177
|
throw new Error("persisted join credentials are not a JSON object");
|
|
41892
42178
|
}
|
|
41893
|
-
|
|
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
|
+
});
|
|
41894
42188
|
}
|
|
41895
42189
|
function isJoinPersisted(join42) {
|
|
41896
42190
|
try {
|
|
41897
|
-
const
|
|
41898
|
-
return persisted
|
|
42191
|
+
const normalized = normalizedJoin(join42);
|
|
42192
|
+
return readPersistedJoins().some((persisted) => joinsEqual(persisted, normalized));
|
|
41899
42193
|
} catch {
|
|
41900
42194
|
return false;
|
|
41901
42195
|
}
|
|
41902
42196
|
}
|
|
41903
|
-
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) {
|
|
41904
42206
|
const path = joinFilePath();
|
|
41905
42207
|
const directory = dirname21(path);
|
|
41906
|
-
const normalized = normalizedJoin(join42);
|
|
41907
|
-
mkdirSync32(directory, { recursive: true });
|
|
41908
|
-
if (existsSync37(path)) {
|
|
41909
|
-
if (lstatSync(path).isSymbolicLink()) {
|
|
41910
|
-
throw new Error(`refusing to replace symlinked Otium join file at ${path}`);
|
|
41911
|
-
}
|
|
41912
|
-
let existing = null;
|
|
41913
|
-
try {
|
|
41914
|
-
existing = readPersistedJoin(path);
|
|
41915
|
-
} catch (error2) {
|
|
41916
|
-
if (!options.replaceExisting) {
|
|
41917
|
-
throw new Error(`existing Otium join file at ${path} is invalid; pass --replace to replace it`, { cause: error2 });
|
|
41918
|
-
}
|
|
41919
|
-
}
|
|
41920
|
-
if (existing && joinsEqual(existing, normalized)) {
|
|
41921
|
-
chmodSync7(path, 384);
|
|
41922
|
-
const fileFd = openSync5(path, "r");
|
|
41923
|
-
try {
|
|
41924
|
-
fsyncSync2(fileFd);
|
|
41925
|
-
} finally {
|
|
41926
|
-
closeSync5(fileFd);
|
|
41927
|
-
}
|
|
41928
|
-
const directoryFd = openSync5(directory, "r");
|
|
41929
|
-
try {
|
|
41930
|
-
fsyncSync2(directoryFd);
|
|
41931
|
-
} finally {
|
|
41932
|
-
closeSync5(directoryFd);
|
|
41933
|
-
}
|
|
41934
|
-
return path;
|
|
41935
|
-
}
|
|
41936
|
-
if (!options.replaceExisting) {
|
|
41937
|
-
throw new Error(`this node is already joined${existing ? ` as ${existing.cellId}` : " with an invalid join file"}; pass --replace to replace its credentials`);
|
|
41938
|
-
}
|
|
41939
|
-
}
|
|
41940
42208
|
const temporaryPath = resolve24(directory, `.otium-join.json.${process.pid}.${randomUUID28()}.tmp`);
|
|
42209
|
+
const payload = { v: JOIN_FILE_VERSION, joins };
|
|
41941
42210
|
let fd;
|
|
41942
42211
|
try {
|
|
41943
42212
|
fd = openSync5(temporaryPath, "wx", 384);
|
|
41944
|
-
|
|
42213
|
+
writeFileSync24(fd, `${JSON.stringify(payload, null, 2)}
|
|
41945
42214
|
`, "utf8");
|
|
41946
42215
|
fsyncSync2(fd);
|
|
41947
42216
|
closeSync5(fd);
|
|
41948
42217
|
fd = undefined;
|
|
41949
|
-
if (
|
|
42218
|
+
if (allowOverwrite) {
|
|
41950
42219
|
renameSync16(temporaryPath, path);
|
|
41951
42220
|
} else {
|
|
41952
42221
|
linkSync(temporaryPath, path);
|
|
41953
42222
|
unlinkSync23(temporaryPath);
|
|
41954
42223
|
}
|
|
41955
42224
|
chmodSync7(path, 384);
|
|
41956
|
-
|
|
41957
|
-
try {
|
|
41958
|
-
fsyncSync2(directoryFd);
|
|
41959
|
-
} finally {
|
|
41960
|
-
closeSync5(directoryFd);
|
|
41961
|
-
}
|
|
42225
|
+
fsyncPath(directory);
|
|
41962
42226
|
} catch (error2) {
|
|
41963
42227
|
if (fd !== undefined)
|
|
41964
42228
|
closeSync5(fd);
|
|
@@ -41968,10 +42232,43 @@ function saveJoinWhileLocked(join42, options = {}) {
|
|
|
41968
42232
|
}
|
|
41969
42233
|
return path;
|
|
41970
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
|
+
}
|
|
41971
42268
|
function saveJoin(join42, options = {}) {
|
|
41972
42269
|
return withJoinCredentialLock(() => saveJoinWhileLocked(join42, options));
|
|
41973
42270
|
}
|
|
41974
|
-
function removeJoin() {
|
|
42271
|
+
function removeJoin(cellId) {
|
|
41975
42272
|
return withJoinCredentialLock(() => {
|
|
41976
42273
|
const path = joinFilePath();
|
|
41977
42274
|
if (!existsSync37(path))
|
|
@@ -41979,27 +42276,36 @@ function removeJoin() {
|
|
|
41979
42276
|
if (lstatSync(path).isSymbolicLink()) {
|
|
41980
42277
|
throw new Error(`refusing to remove symlinked Otium join file at ${path}`);
|
|
41981
42278
|
}
|
|
41982
|
-
|
|
41983
|
-
|
|
41984
|
-
|
|
41985
|
-
|
|
41986
|
-
|
|
41987
|
-
|
|
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
|
+
}
|
|
41988
42292
|
}
|
|
42293
|
+
unlinkSync23(path);
|
|
42294
|
+
fsyncPath(dirname21(path));
|
|
41989
42295
|
return true;
|
|
41990
42296
|
});
|
|
41991
42297
|
}
|
|
41992
|
-
function
|
|
42298
|
+
function loadJoins() {
|
|
41993
42299
|
const central = process.env.OTIUM_CENTRAL_URL?.trim();
|
|
41994
42300
|
const cellId = process.env.OTIUM_CELL_ID?.trim();
|
|
41995
42301
|
const secret = process.env.OTIUM_CELL_SECRET?.trim();
|
|
41996
42302
|
const relay = process.env.OTIUM_RELAY_URL?.trim();
|
|
41997
42303
|
if (central && cellId && secret) {
|
|
41998
42304
|
try {
|
|
41999
|
-
return normalizeJoin({ central, relay, cellId, secret });
|
|
42305
|
+
return [normalizeJoin({ central, relay, cellId, secret })];
|
|
42000
42306
|
} catch (err2) {
|
|
42001
42307
|
logger.warn({ err: err2 }, "otium: invalid OTIUM_CENTRAL_URL/OTIUM_CELL_ID/OTIUM_CELL_SECRET env");
|
|
42002
|
-
return
|
|
42308
|
+
return [];
|
|
42003
42309
|
}
|
|
42004
42310
|
}
|
|
42005
42311
|
if (central || cellId || secret) {
|
|
@@ -42007,18 +42313,18 @@ function loadJoin() {
|
|
|
42007
42313
|
}
|
|
42008
42314
|
const path = joinFilePath();
|
|
42009
42315
|
if (!existsSync37(path))
|
|
42010
|
-
return
|
|
42316
|
+
return [];
|
|
42011
42317
|
try {
|
|
42012
|
-
|
|
42013
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
42014
|
-
return null;
|
|
42015
|
-
return normalizeJoin(parsed);
|
|
42318
|
+
return readPersistedJoins(path);
|
|
42016
42319
|
} catch (err2) {
|
|
42017
42320
|
logger.warn({ err: err2, path }, "otium: failed to read join file");
|
|
42018
|
-
return
|
|
42321
|
+
return [];
|
|
42019
42322
|
}
|
|
42020
42323
|
}
|
|
42021
|
-
|
|
42324
|
+
function loadJoin() {
|
|
42325
|
+
return loadJoins()[0] ?? null;
|
|
42326
|
+
}
|
|
42327
|
+
var JOIN_LOCK_STALE_MS = 30000, JOIN_FILE_VERSION = 2;
|
|
42022
42328
|
var init_join = __esm(async () => {
|
|
42023
42329
|
await init_src();
|
|
42024
42330
|
});
|
|
@@ -42172,7 +42478,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42172
42478
|
return errorResult("Error: Hub node is no longer attached.");
|
|
42173
42479
|
let token;
|
|
42174
42480
|
try {
|
|
42175
|
-
token = await mintPeerToken(hubNode
|
|
42481
|
+
token = await mintPeerToken(hubNode);
|
|
42176
42482
|
} catch (err2) {
|
|
42177
42483
|
return errorResult(`Error: Failed to spawn on hub: ${err2.message}`);
|
|
42178
42484
|
}
|
|
@@ -42212,7 +42518,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42212
42518
|
return errorResult("Error: Hub node is no longer attached.");
|
|
42213
42519
|
let token;
|
|
42214
42520
|
try {
|
|
42215
|
-
token = await mintPeerToken(hubNode
|
|
42521
|
+
token = await mintPeerToken(hubNode);
|
|
42216
42522
|
} catch (error2) {
|
|
42217
42523
|
return errorResult(`Error: Failed to open hub question: ${error2.message}`);
|
|
42218
42524
|
}
|
|
@@ -42268,7 +42574,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42268
42574
|
if (!hubNode)
|
|
42269
42575
|
return errorResult("Error: Hub node is no longer attached.");
|
|
42270
42576
|
try {
|
|
42271
|
-
const token = await mintPeerToken(hubNode
|
|
42577
|
+
const token = await mintPeerToken(hubNode);
|
|
42272
42578
|
const response = await fetch(`${hubNode.baseUrl.replace(/\/+$/, "")}/api/v1/peer/bridge/self-config`, {
|
|
42273
42579
|
method: "POST",
|
|
42274
42580
|
headers: {
|
|
@@ -42299,7 +42605,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42299
42605
|
return { ok: false, error: "hub node is no longer attached" };
|
|
42300
42606
|
let token;
|
|
42301
42607
|
try {
|
|
42302
|
-
token = await mintPeerToken(hubNode
|
|
42608
|
+
token = await mintPeerToken(hubNode);
|
|
42303
42609
|
} catch (error2) {
|
|
42304
42610
|
return { ok: false, error: `peer token mint failed: ${error2.message}` };
|
|
42305
42611
|
}
|
|
@@ -42361,7 +42667,7 @@ var init_runtime_bridge = __esm(async () => {
|
|
|
42361
42667
|
if (!hubNode)
|
|
42362
42668
|
return { ok: false, error: "hub node is no longer attached" };
|
|
42363
42669
|
try {
|
|
42364
|
-
const token = await mintPeerToken(hubNode
|
|
42670
|
+
const token = await mintPeerToken(hubNode);
|
|
42365
42671
|
const form = new FormData;
|
|
42366
42672
|
form.set("hostQueryId", request.bridge.hostQueryId);
|
|
42367
42673
|
form.set("userId", request.userId);
|
|
@@ -42528,13 +42834,81 @@ var init_store2 = __esm(async () => {
|
|
|
42528
42834
|
`);
|
|
42529
42835
|
});
|
|
42530
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
|
+
|
|
42531
42905
|
// ../../adapters/otium/src/session-bridge.ts
|
|
42532
42906
|
function prunePendingRemoteAsks(now = Date.now()) {
|
|
42533
42907
|
pruneRemoteAsks(now - PENDING_ASK_TTL_MS2);
|
|
42534
42908
|
}
|
|
42535
42909
|
async function postPeer(node, path, body) {
|
|
42536
42910
|
try {
|
|
42537
|
-
const token = await mintPeerToken(node
|
|
42911
|
+
const token = await mintPeerToken(node);
|
|
42538
42912
|
const response = await fetch(`${node.baseUrl.replace(/\/+$/, "")}${path}`, {
|
|
42539
42913
|
method: "POST",
|
|
42540
42914
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -42571,7 +42945,7 @@ async function postPeerIdempotent(node, path, body) {
|
|
|
42571
42945
|
}
|
|
42572
42946
|
async function peerSupportsRemoteAsk(node) {
|
|
42573
42947
|
try {
|
|
42574
|
-
const token = await mintPeerToken(node
|
|
42948
|
+
const token = await mintPeerToken(node);
|
|
42575
42949
|
const response = await fetch(`${node.baseUrl.replace(/\/+$/, "")}/api/v1/peer/capabilities`, {
|
|
42576
42950
|
headers: { authorization: `Bearer ${token}` },
|
|
42577
42951
|
signal: AbortSignal.timeout(PEER_TIMEOUT_MS)
|
|
@@ -42582,26 +42956,50 @@ async function peerSupportsRemoteAsk(node) {
|
|
|
42582
42956
|
return false;
|
|
42583
42957
|
}
|
|
42584
42958
|
}
|
|
42585
|
-
async function findNode(nodeName) {
|
|
42586
|
-
const
|
|
42587
|
-
|
|
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
|
+
};
|
|
42588
42983
|
}
|
|
42589
|
-
async function originLabel(args) {
|
|
42590
|
-
const self = await
|
|
42984
|
+
async function originLabel(args, peer) {
|
|
42985
|
+
const self = await selfPeerNodeForCell(peer.viaCellId).catch(() => null);
|
|
42591
42986
|
const local = args.fromTitle?.trim() || args.fromKey?.trim() || "peer";
|
|
42592
42987
|
return self?.nodeName ? `${self.nodeName}/${local}` : local;
|
|
42593
42988
|
}
|
|
42594
42989
|
async function forward(args) {
|
|
42595
|
-
const
|
|
42596
|
-
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)
|
|
42597
42995
|
return { ok: false, error: `unknown remote node "${args.toNode}"` };
|
|
42598
|
-
const self = await
|
|
42996
|
+
const self = await selfPeerNodeForCell(node.viaCellId).catch(() => null);
|
|
42599
42997
|
if (!self)
|
|
42600
42998
|
return { ok: false, error: "local peer node is not attached" };
|
|
42601
42999
|
if (!self.isPrimary && !node.isPrimary) {
|
|
42602
43000
|
return { ok: false, error: "worker peer calls must target the primary hub" };
|
|
42603
43001
|
}
|
|
42604
|
-
const fromLabel = await originLabel(args);
|
|
43002
|
+
const fromLabel = await originLabel(args, node);
|
|
42605
43003
|
const requestId = args.requestId;
|
|
42606
43004
|
if (args.action === "ask") {
|
|
42607
43005
|
if (!requestId || !args.fromTopicId || !args.fromKey || !args.message) {
|
|
@@ -42671,14 +43069,20 @@ async function forward(args) {
|
|
|
42671
43069
|
...args.sourceQueryId ? { sourceQueryId: args.sourceQueryId } : {}
|
|
42672
43070
|
});
|
|
42673
43071
|
}
|
|
42674
|
-
async function sessions(userId, sourceQueryId) {
|
|
43072
|
+
async function sessions(userId, sourceQueryId, fromTopicId) {
|
|
42675
43073
|
const nodes = await listPeerNodes().catch(() => []);
|
|
42676
|
-
const
|
|
42677
|
-
if (
|
|
43074
|
+
const selves = new Map(nodes.filter((node) => node.self).map((node) => [node.viaCellId, node]));
|
|
43075
|
+
if (selves.size === 0)
|
|
42678
43076
|
return { ok: false, nodes: [] };
|
|
43077
|
+
const callerScope = fromTopicId ? getTopic(fromTopicId)?.surfaceScope ?? null : null;
|
|
42679
43078
|
return {
|
|
42680
43079
|
ok: true,
|
|
42681
|
-
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) => {
|
|
42682
43086
|
const result = await postPeer(node, "/api/v1/peer/sessions", {
|
|
42683
43087
|
v: PEER_PROTOCOL_VERSION,
|
|
42684
43088
|
userId,
|
|
@@ -42716,7 +43120,7 @@ function flushPeerReplyOutbox() {
|
|
|
42716
43120
|
toTopic: "",
|
|
42717
43121
|
userId: pending2.user_id,
|
|
42718
43122
|
fromTitle: pending2.source_title
|
|
42719
|
-
});
|
|
43123
|
+
}, node);
|
|
42720
43124
|
const result = await postPeer(node, "/api/v1/peer/reply", {
|
|
42721
43125
|
v: PEER_PROTOCOL_VERSION,
|
|
42722
43126
|
requestId: pending2.request_id,
|
|
@@ -42799,6 +43203,7 @@ var init_session_bridge = __esm(async () => {
|
|
|
42799
43203
|
await init_central();
|
|
42800
43204
|
init_protocol();
|
|
42801
43205
|
await init_store2();
|
|
43206
|
+
await init_workspace_scope();
|
|
42802
43207
|
PENDING_ASK_TTL_MS2 = 15 * 60 * 1000;
|
|
42803
43208
|
otiumPeerSessionBridge = { forward, sessions, reply };
|
|
42804
43209
|
});
|
|
@@ -42886,7 +43291,7 @@ function startPeerSessionBridgeIpc(bridge) {
|
|
|
42886
43291
|
return Response.json(await bridge.forward(payload.args));
|
|
42887
43292
|
}
|
|
42888
43293
|
if (payload.action === "sessions") {
|
|
42889
|
-
return Response.json(await bridge.sessions(payload.userId, payload.sourceQueryId));
|
|
43294
|
+
return Response.json(await bridge.sessions(payload.userId, payload.sourceQueryId, payload.fromTopicId));
|
|
42890
43295
|
}
|
|
42891
43296
|
if (payload.action === "reply") {
|
|
42892
43297
|
return Response.json(await bridge.reply(payload.route, payload.sourceTitle, payload.replyText, payload.kind));
|
|
@@ -43503,14 +43908,14 @@ import {
|
|
|
43503
43908
|
existsSync as existsSync38,
|
|
43504
43909
|
fsyncSync as fsyncSync3,
|
|
43505
43910
|
linkSync as linkSync2,
|
|
43506
|
-
mkdirSync as
|
|
43911
|
+
mkdirSync as mkdirSync35,
|
|
43507
43912
|
openSync as openSync6,
|
|
43508
|
-
readFileSync as
|
|
43913
|
+
readFileSync as readFileSync30,
|
|
43509
43914
|
renameSync as renameSync17,
|
|
43510
43915
|
unlinkSync as unlinkSync24,
|
|
43511
|
-
writeFileSync as
|
|
43916
|
+
writeFileSync as writeFileSync26
|
|
43512
43917
|
} from "fs";
|
|
43513
|
-
import { dirname as
|
|
43918
|
+
import { dirname as dirname23, resolve as resolve26 } from "path";
|
|
43514
43919
|
function parseEnrollmentInvite(code) {
|
|
43515
43920
|
let parsed;
|
|
43516
43921
|
try {
|
|
@@ -43531,14 +43936,14 @@ function parseEnrollmentInvite(code) {
|
|
|
43531
43936
|
return { v: 2, central, token };
|
|
43532
43937
|
}
|
|
43533
43938
|
function pendingEnrollmentPath() {
|
|
43534
|
-
return
|
|
43939
|
+
return resolve26(DATA_DIR, "otium-enrollment-pending.json");
|
|
43535
43940
|
}
|
|
43536
43941
|
function isEnrollmentPending(invite) {
|
|
43537
43942
|
const path = pendingEnrollmentPath();
|
|
43538
43943
|
if (!existsSync38(path))
|
|
43539
43944
|
return false;
|
|
43540
43945
|
try {
|
|
43541
|
-
const saved = JSON.parse(
|
|
43946
|
+
const saved = JSON.parse(readFileSync30(path, "utf8"));
|
|
43542
43947
|
return saved.central === invite.central && saved.token === invite.token;
|
|
43543
43948
|
} catch {
|
|
43544
43949
|
return false;
|
|
@@ -43547,7 +43952,7 @@ function isEnrollmentPending(invite) {
|
|
|
43547
43952
|
function loadOrCreatePending(invite, nodeName) {
|
|
43548
43953
|
const path = pendingEnrollmentPath();
|
|
43549
43954
|
if (existsSync38(path)) {
|
|
43550
|
-
const saved = JSON.parse(
|
|
43955
|
+
const saved = JSON.parse(readFileSync30(path, "utf8"));
|
|
43551
43956
|
if (saved.central === invite.central && saved.token === invite.token)
|
|
43552
43957
|
return saved;
|
|
43553
43958
|
throw new Error(`another Otium enrollment is pending at ${path}`);
|
|
@@ -43560,12 +43965,12 @@ function loadOrCreatePending(invite, nodeName) {
|
|
|
43560
43965
|
publicKey: pair.publicKey.export({ format: "der", type: "spki" }).toString("base64url"),
|
|
43561
43966
|
...nodeName ? { nodeName } : {}
|
|
43562
43967
|
};
|
|
43563
|
-
|
|
43564
|
-
const temporaryPath =
|
|
43968
|
+
mkdirSync35(dirname23(path), { recursive: true });
|
|
43969
|
+
const temporaryPath = resolve26(dirname23(path), `.otium-enrollment-pending.json.${process.pid}.${randomUUID31()}.tmp`);
|
|
43565
43970
|
let fd;
|
|
43566
43971
|
try {
|
|
43567
43972
|
fd = openSync6(temporaryPath, "wx", 384);
|
|
43568
|
-
|
|
43973
|
+
writeFileSync26(fd, `${JSON.stringify(pending2, null, 2)}
|
|
43569
43974
|
`, "utf8");
|
|
43570
43975
|
fsyncSync3(fd);
|
|
43571
43976
|
closeSync6(fd);
|
|
@@ -43573,7 +43978,7 @@ function loadOrCreatePending(invite, nodeName) {
|
|
|
43573
43978
|
linkSync2(temporaryPath, path);
|
|
43574
43979
|
unlinkSync24(temporaryPath);
|
|
43575
43980
|
chmodSync8(path, 384);
|
|
43576
|
-
const directoryFd = openSync6(
|
|
43981
|
+
const directoryFd = openSync6(dirname23(path), "r");
|
|
43577
43982
|
try {
|
|
43578
43983
|
fsyncSync3(directoryFd);
|
|
43579
43984
|
} finally {
|
|
@@ -43585,7 +43990,7 @@ function loadOrCreatePending(invite, nodeName) {
|
|
|
43585
43990
|
if (existsSync38(temporaryPath))
|
|
43586
43991
|
unlinkSync24(temporaryPath);
|
|
43587
43992
|
if (error2.code === "EEXIST" && existsSync38(path)) {
|
|
43588
|
-
const saved = JSON.parse(
|
|
43993
|
+
const saved = JSON.parse(readFileSync30(path, "utf8"));
|
|
43589
43994
|
if (saved.central === invite.central && saved.token === invite.token)
|
|
43590
43995
|
return saved;
|
|
43591
43996
|
}
|
|
@@ -43595,12 +44000,12 @@ function loadOrCreatePending(invite, nodeName) {
|
|
|
43595
44000
|
}
|
|
43596
44001
|
function replacePendingEnrollment(pending2) {
|
|
43597
44002
|
const path = pendingEnrollmentPath();
|
|
43598
|
-
const directory =
|
|
43599
|
-
const temporaryPath =
|
|
44003
|
+
const directory = dirname23(path);
|
|
44004
|
+
const temporaryPath = resolve26(directory, `.otium-enrollment-pending.json.${process.pid}.${randomUUID31()}.tmp`);
|
|
43600
44005
|
let fd;
|
|
43601
44006
|
try {
|
|
43602
44007
|
fd = openSync6(temporaryPath, "wx", 384);
|
|
43603
|
-
|
|
44008
|
+
writeFileSync26(fd, `${JSON.stringify(pending2, null, 2)}
|
|
43604
44009
|
`, "utf8");
|
|
43605
44010
|
fsyncSync3(fd);
|
|
43606
44011
|
closeSync6(fd);
|
|
@@ -43622,7 +44027,7 @@ function replacePendingEnrollment(pending2) {
|
|
|
43622
44027
|
}
|
|
43623
44028
|
function recordClaimedCredential(pending2, join43) {
|
|
43624
44029
|
withJoinCredentialLock(() => {
|
|
43625
|
-
const current3 = JSON.parse(
|
|
44030
|
+
const current3 = JSON.parse(readFileSync30(pendingEnrollmentPath(), "utf8"));
|
|
43626
44031
|
if (current3.central !== pending2.central || current3.token !== pending2.token || current3.idempotencyKey !== pending2.idempotencyKey || current3.publicKey !== pending2.publicKey) {
|
|
43627
44032
|
throw new Error("pending Otium enrollment changed while its claim was in flight");
|
|
43628
44033
|
}
|
|
@@ -43701,7 +44106,7 @@ async function claimEnrollment(invite, nodeName) {
|
|
|
43701
44106
|
function commitEnrollment(join43, options = {}) {
|
|
43702
44107
|
return withJoinCredentialLock(() => {
|
|
43703
44108
|
const pendingPath = pendingEnrollmentPath();
|
|
43704
|
-
const pending2 = existsSync38(pendingPath) ? JSON.parse(
|
|
44109
|
+
const pending2 = existsSync38(pendingPath) ? JSON.parse(readFileSync30(pendingPath, "utf8")) : null;
|
|
43705
44110
|
const digest = joinCredentialDigest(join43);
|
|
43706
44111
|
if (pending2 && (!pending2.claimed || pending2.claimed.digest !== digest || pending2.claimed.cellId !== join43.cellId)) {
|
|
43707
44112
|
throw new Error(`pending Otium enrollment at ${pendingPath} does not match these credentials`);
|
|
@@ -43713,7 +44118,7 @@ function commitEnrollment(join43, options = {}) {
|
|
|
43713
44118
|
if (!pending2)
|
|
43714
44119
|
return path;
|
|
43715
44120
|
unlinkSync24(pendingPath);
|
|
43716
|
-
const directoryFd = openSync6(
|
|
44121
|
+
const directoryFd = openSync6(dirname23(pendingPath), "r");
|
|
43717
44122
|
try {
|
|
43718
44123
|
fsyncSync3(directoryFd);
|
|
43719
44124
|
} finally {
|
|
@@ -43752,6 +44157,8 @@ async function forwardGatewayRequest(req, options) {
|
|
|
43752
44157
|
target.search = url.search;
|
|
43753
44158
|
const headers = new Headers(req.headers);
|
|
43754
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");
|
|
43755
44162
|
headers.delete("transfer-encoding");
|
|
43756
44163
|
headers.delete("content-length");
|
|
43757
44164
|
headers.delete("host");
|
|
@@ -43766,7 +44173,7 @@ async function forwardGatewayRequest(req, options) {
|
|
|
43766
44173
|
signal: req.signal
|
|
43767
44174
|
}));
|
|
43768
44175
|
}
|
|
43769
|
-
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";
|
|
43770
44177
|
var init_gateway_forward = __esm(async () => {
|
|
43771
44178
|
await init_src();
|
|
43772
44179
|
});
|
|
@@ -43799,7 +44206,7 @@ function checkProtocol(body) {
|
|
|
43799
44206
|
return null;
|
|
43800
44207
|
}
|
|
43801
44208
|
async function requirePeer(req) {
|
|
43802
|
-
if (!
|
|
44209
|
+
if (!isOtiumCentralConfigured()) {
|
|
43803
44210
|
return { ok: false, response: jsonError2("multi-node is disabled", 403) };
|
|
43804
44211
|
}
|
|
43805
44212
|
const token = bearer(req);
|
|
@@ -43813,8 +44220,22 @@ async function requirePeer(req) {
|
|
|
43813
44220
|
function requirePrimaryOrigin(peer) {
|
|
43814
44221
|
return peer.verified.fromIsPrimary ? null : jsonError2("only the workspace hub may call this endpoint", 403);
|
|
43815
44222
|
}
|
|
43816
|
-
function
|
|
43817
|
-
|
|
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);
|
|
43818
44239
|
}
|
|
43819
44240
|
function localCapabilities() {
|
|
43820
44241
|
const agents = SUPPORTED_AGENTS.map((kind) => {
|
|
@@ -43884,8 +44305,8 @@ async function handleAbort(req) {
|
|
|
43884
44305
|
const toTopic = str(body, "toTopic");
|
|
43885
44306
|
if (!userId || !toTopic)
|
|
43886
44307
|
return jsonError2("userId and toTopic are required", 400);
|
|
43887
|
-
const topic =
|
|
43888
|
-
if (!topic || !peerAddressable(topic)) {
|
|
44308
|
+
const topic = peerTopicByName(toTopic, userId, peer);
|
|
44309
|
+
if (!topic || !peerAddressable(topic, peer)) {
|
|
43889
44310
|
return jsonError2(`shared topic "${toTopic}" not found on this node`, 404);
|
|
43890
44311
|
}
|
|
43891
44312
|
appendJsonlEntry(sessionInboxPath(userId, topic.id), {
|
|
@@ -43926,8 +44347,8 @@ async function handleTell(req) {
|
|
|
43926
44347
|
if (depth > MAX_TELL_DEPTH) {
|
|
43927
44348
|
return jsonError2(`tell depth limit exceeded (max ${MAX_TELL_DEPTH})`, 400);
|
|
43928
44349
|
}
|
|
43929
|
-
const topic =
|
|
43930
|
-
if (!topic || !peerAddressable(topic)) {
|
|
44350
|
+
const topic = peerTopicByName(toTopic, userId, peer);
|
|
44351
|
+
if (!topic || !peerAddressable(topic, peer)) {
|
|
43931
44352
|
return jsonError2(`shared topic "${toTopic}" not found on this node`, 404);
|
|
43932
44353
|
}
|
|
43933
44354
|
const claim = claimInboundPeerMessage({
|
|
@@ -43984,7 +44405,7 @@ async function handleSessions(req) {
|
|
|
43984
44405
|
const userId = str(body, "userId");
|
|
43985
44406
|
if (!userId)
|
|
43986
44407
|
return jsonError2("userId is required", 400);
|
|
43987
|
-
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));
|
|
43988
44409
|
const titleCounts = new Map;
|
|
43989
44410
|
for (const topic of topics) {
|
|
43990
44411
|
const normalized = topic.title.toLowerCase();
|
|
@@ -44032,8 +44453,8 @@ async function handleAsk(req) {
|
|
|
44032
44453
|
if (!Number.isInteger(fromDepth) || fromDepth < 0) {
|
|
44033
44454
|
return jsonError2("fromDepth must be a non-negative integer", 400);
|
|
44034
44455
|
}
|
|
44035
|
-
const topic =
|
|
44036
|
-
if (!topic || !peerAddressable(topic)) {
|
|
44456
|
+
const topic = peerTopicByName(toTopic, userId, peer);
|
|
44457
|
+
if (!topic || !peerAddressable(topic, peer)) {
|
|
44037
44458
|
return jsonError2(`shared topic "${toTopic}" not found on this node`, 404);
|
|
44038
44459
|
}
|
|
44039
44460
|
if (!topic.agent)
|
|
@@ -44153,7 +44574,7 @@ async function handleOtiumPeerRequest(req) {
|
|
|
44153
44574
|
const url = new URL(req.url);
|
|
44154
44575
|
const path = url.pathname;
|
|
44155
44576
|
if (path === "/ready" && req.method === "GET") {
|
|
44156
|
-
if (!
|
|
44577
|
+
if (!isOtiumCentralConfigured())
|
|
44157
44578
|
return null;
|
|
44158
44579
|
return Response.json({ ok: true });
|
|
44159
44580
|
}
|
|
@@ -44172,7 +44593,9 @@ async function handleOtiumPeerRequest(req) {
|
|
|
44172
44593
|
return jsonError2("canonical Negotium node is unavailable", 503);
|
|
44173
44594
|
}
|
|
44174
44595
|
return forwardGatewayRequest(req, {
|
|
44175
|
-
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()
|
|
44176
44599
|
});
|
|
44177
44600
|
}
|
|
44178
44601
|
if (req.method === "GET") {
|
|
@@ -44217,41 +44640,89 @@ var init_peer_server = __esm(async () => {
|
|
|
44217
44640
|
init_protocol();
|
|
44218
44641
|
await init_session_bridge();
|
|
44219
44642
|
await init_store2();
|
|
44643
|
+
await init_workspace_scope();
|
|
44220
44644
|
RUNTIME_VERSION = NEGOTIUM_VERSION;
|
|
44221
44645
|
});
|
|
44222
44646
|
|
|
44223
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
|
+
}
|
|
44224
44701
|
function startOtiumNodeRuntime(options) {
|
|
44225
44702
|
const { join: join43 } = options;
|
|
44226
|
-
|
|
44227
|
-
const
|
|
44228
|
-
const unregisterSessionBridge = registerPeerSessionBridge(otiumPeerSessionBridge);
|
|
44229
|
-
const sessionBridgeIpc = startPeerSessionBridgeIpc(otiumPeerSessionBridge);
|
|
44230
|
-
const canonicalMcpBridge = startCanonicalMcpBridge();
|
|
44231
|
-
const stopPeerReplyOutbox = startPeerReplyOutboxWorker();
|
|
44232
|
-
failInterruptedRemoteAskCallbacks().then((failedAsks) => {
|
|
44233
|
-
if (failedAsks > 0) {
|
|
44234
|
-
logger.warn({ failedAsks }, "otium: failed remote asks interrupted by previous process");
|
|
44235
|
-
}
|
|
44236
|
-
});
|
|
44237
|
-
const uninstallFileHooks = installPeerFileHooks();
|
|
44238
|
-
const unsubscribeTopicCleanup = runtimeBus().subscribe((event) => {
|
|
44239
|
-
if (event.type !== "topic-deleted")
|
|
44240
|
-
return;
|
|
44241
|
-
const removed = cleanupPeerStateForLocalTopic(event.topicId);
|
|
44242
|
-
if (removed.inboxRequests + removed.remoteAsks > 0) {
|
|
44243
|
-
logger.info({ topicId: event.topicId, ...removed }, "otium: removed peer state for deleted local topic");
|
|
44244
|
-
}
|
|
44245
|
-
});
|
|
44703
|
+
attachOtiumCentralCell(join43);
|
|
44704
|
+
const releaseGlobals = acquireGlobalOtiumServices();
|
|
44246
44705
|
let stopped = false;
|
|
44247
44706
|
logger.info({ central: join43.central, cellId: join43.cellId }, "otium: worker mode enabled");
|
|
44248
|
-
|
|
44707
|
+
mountedScopes.set(join43.cellId, cachedSurfaceScope(join43));
|
|
44708
|
+
refreshDefaultSurfaceScope();
|
|
44709
|
+
selfPeerNodeForCell(join43.cellId).then((self) => {
|
|
44249
44710
|
if (self) {
|
|
44250
44711
|
logger.info({ nodeName: self.nodeName, baseUrl: self.baseUrl }, "otium: attached to workspace");
|
|
44251
44712
|
}
|
|
44252
44713
|
}).catch((err2) => {
|
|
44253
44714
|
logger.warn({ err: err2 }, "otium: self check against central failed (will retry per request)");
|
|
44254
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
|
+
});
|
|
44255
44726
|
return {
|
|
44256
44727
|
name: "otium",
|
|
44257
44728
|
join: join43,
|
|
@@ -44259,14 +44730,10 @@ function startOtiumNodeRuntime(options) {
|
|
|
44259
44730
|
if (stopped)
|
|
44260
44731
|
return;
|
|
44261
44732
|
stopped = true;
|
|
44262
|
-
|
|
44263
|
-
|
|
44264
|
-
|
|
44265
|
-
|
|
44266
|
-
canonicalMcpBridge.stop();
|
|
44267
|
-
stopPeerReplyOutbox();
|
|
44268
|
-
uninstallFileHooks();
|
|
44269
|
-
configureOtiumCentral(null);
|
|
44733
|
+
mountedScopes.delete(join43.cellId);
|
|
44734
|
+
refreshDefaultSurfaceScope();
|
|
44735
|
+
detachOtiumCentralCell(join43.cellId);
|
|
44736
|
+
releaseGlobals();
|
|
44270
44737
|
}
|
|
44271
44738
|
};
|
|
44272
44739
|
}
|
|
@@ -44304,7 +44771,7 @@ function startOtiumAdapter(options) {
|
|
|
44304
44771
|
}
|
|
44305
44772
|
};
|
|
44306
44773
|
}
|
|
44307
|
-
var otiumAdapter;
|
|
44774
|
+
var globalServices = null, mountedScopes, otiumAdapter;
|
|
44308
44775
|
var init_src8 = __esm(async () => {
|
|
44309
44776
|
await init_src();
|
|
44310
44777
|
await init_canonical_mcp_bridge();
|
|
@@ -44316,6 +44783,7 @@ var init_src8 = __esm(async () => {
|
|
|
44316
44783
|
init_session_bridge_ipc();
|
|
44317
44784
|
await init_store2();
|
|
44318
44785
|
init_tunnel_client();
|
|
44786
|
+
await init_workspace_scope();
|
|
44319
44787
|
await init_central();
|
|
44320
44788
|
await init_enrollment();
|
|
44321
44789
|
await init_join();
|
|
@@ -44325,6 +44793,8 @@ var init_src8 = __esm(async () => {
|
|
|
44325
44793
|
await init_runtime_bridge();
|
|
44326
44794
|
await init_store2();
|
|
44327
44795
|
init_tunnel_client();
|
|
44796
|
+
await init_workspace_scope();
|
|
44797
|
+
mountedScopes = new Map;
|
|
44328
44798
|
otiumAdapter = defineNegotiumAdapter({
|
|
44329
44799
|
name: "otium",
|
|
44330
44800
|
capabilities: {
|
|
@@ -44344,12 +44814,59 @@ var init_src8 = __esm(async () => {
|
|
|
44344
44814
|
// ../../adapters/otium/src/node-runtime.ts
|
|
44345
44815
|
var exports_node_runtime = {};
|
|
44346
44816
|
__export(exports_node_runtime, {
|
|
44817
|
+
reconcileOtiumWorkspaces: () => reconcileOtiumWorkspaces,
|
|
44818
|
+
mountedOtiumWorkspaces: () => mountedOtiumWorkspaces,
|
|
44347
44819
|
mountConfiguredOtiumNodeRuntime: () => mountConfiguredOtiumNodeRuntime,
|
|
44348
44820
|
handleOtiumAdapterControlRequest: () => handleOtiumAdapterControlRequest,
|
|
44821
|
+
detachOtiumWorkspace: () => detachOtiumWorkspace,
|
|
44822
|
+
attachOtiumWorkspace: () => attachOtiumWorkspace,
|
|
44349
44823
|
OTIUM_ADAPTER_CONTROL_PREFIX: () => OTIUM_ADAPTER_CONTROL_PREFIX,
|
|
44350
44824
|
OTIUM_ADAPTER_CONTROL_HEADER: () => OTIUM_ADAPTER_CONTROL_HEADER,
|
|
44351
44825
|
MAX_PEER_REQUEST_BODY_BYTES: () => MAX_PEER_REQUEST_BODY_BYTES
|
|
44352
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
|
+
}
|
|
44353
44870
|
async function handleOtiumAdapterControlRequest(req) {
|
|
44354
44871
|
const url = new URL(req.url);
|
|
44355
44872
|
if (!url.pathname.startsWith(`${OTIUM_ADAPTER_CONTROL_PREFIX}/`))
|
|
@@ -44358,6 +44875,25 @@ async function handleOtiumAdapterControlRequest(req) {
|
|
|
44358
44875
|
return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
|
44359
44876
|
}
|
|
44360
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
|
+
}
|
|
44361
44897
|
const peerUrl = new URL(req.url);
|
|
44362
44898
|
peerUrl.pathname = peerPath;
|
|
44363
44899
|
const headers = new Headers(req.headers);
|
|
@@ -44366,29 +44902,72 @@ async function handleOtiumAdapterControlRequest(req) {
|
|
|
44366
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 });
|
|
44367
44903
|
}
|
|
44368
44904
|
function mountConfiguredOtiumNodeRuntime() {
|
|
44369
|
-
const
|
|
44370
|
-
if (
|
|
44905
|
+
const joins = loadJoins();
|
|
44906
|
+
if (joins.length === 0)
|
|
44371
44907
|
return null;
|
|
44372
|
-
|
|
44908
|
+
for (const join43 of joins)
|
|
44909
|
+
attachOtiumWorkspace(join43);
|
|
44373
44910
|
registerNodeRequestHandler("otium-adapter-control", handleOtiumAdapterControlRequest);
|
|
44374
44911
|
let stopped = false;
|
|
44375
44912
|
return {
|
|
44376
|
-
|
|
44913
|
+
name: "otium",
|
|
44914
|
+
join: joins[0],
|
|
44377
44915
|
stop() {
|
|
44378
44916
|
if (stopped)
|
|
44379
44917
|
return;
|
|
44380
44918
|
stopped = true;
|
|
44381
44919
|
unregisterNodeRequestHandler("otium-adapter-control");
|
|
44382
|
-
|
|
44920
|
+
for (const cellId of [...mounted.keys()])
|
|
44921
|
+
detachOtiumWorkspace(cellId);
|
|
44383
44922
|
}
|
|
44384
44923
|
};
|
|
44385
44924
|
}
|
|
44925
|
+
var mounted;
|
|
44386
44926
|
var init_node_runtime = __esm(async () => {
|
|
44387
44927
|
await init_src();
|
|
44388
44928
|
await init_src8();
|
|
44389
44929
|
await init_join();
|
|
44390
44930
|
await init_peer_server();
|
|
44391
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();
|
|
44392
44971
|
});
|
|
44393
44972
|
|
|
44394
44973
|
// ../../adapters/otium/src/join-cli.ts
|
|
@@ -44482,6 +45061,12 @@ async function joinCommand(args) {
|
|
|
44482
45061
|
} finally {
|
|
44483
45062
|
configureOtiumCentral(null);
|
|
44484
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
|
+
}
|
|
44485
45070
|
console.log("\nnext: `negotium-otium serve` (mounts the otium peer routes automatically)");
|
|
44486
45071
|
}
|
|
44487
45072
|
var init_join_cli = __esm(async () => {
|
|
@@ -44496,9 +45081,15 @@ __export(exports_sidecar, {
|
|
|
44496
45081
|
runOtiumSidecar: () => runOtiumSidecar,
|
|
44497
45082
|
proxyOtiumPeerRequest: () => proxyOtiumPeerRequest
|
|
44498
45083
|
});
|
|
45084
|
+
function isPublicPeerPath(pathname) {
|
|
45085
|
+
return pathname === "/ready" || pathname.startsWith("/api/v1/peer/");
|
|
45086
|
+
}
|
|
44499
45087
|
async function proxyOtiumPeerRequest(req, dependencies = {}) {
|
|
44500
45088
|
const inspectNode = dependencies.inspectNode ?? inspectNodeDaemon;
|
|
44501
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
|
+
}
|
|
44502
45093
|
const status = await inspectNode();
|
|
44503
45094
|
if (!status.running || !status.info) {
|
|
44504
45095
|
return Response.json({ ok: false, error: "canonical Negotium node is unavailable" }, { status: 503 });
|
|
@@ -44509,6 +45100,7 @@ async function proxyOtiumPeerRequest(req, dependencies = {}) {
|
|
|
44509
45100
|
target.search = source.search;
|
|
44510
45101
|
const headers = new Headers(req.headers);
|
|
44511
45102
|
headers.set(OTIUM_ADAPTER_CONTROL_HEADER, NODE_CONTROL_TOKEN);
|
|
45103
|
+
headers.set(OTIUM_RELAYED_HEADER, "1");
|
|
44512
45104
|
try {
|
|
44513
45105
|
const body = req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer();
|
|
44514
45106
|
headers.delete("transfer-encoding");
|
|
@@ -44565,8 +45157,8 @@ async function runOtiumSidecar(options) {
|
|
|
44565
45157
|
}) : null;
|
|
44566
45158
|
tunnel?.start();
|
|
44567
45159
|
let resolveCompleted;
|
|
44568
|
-
const completed = new Promise((
|
|
44569
|
-
resolveCompleted =
|
|
45160
|
+
const completed = new Promise((resolve27) => {
|
|
45161
|
+
resolveCompleted = resolve27;
|
|
44570
45162
|
});
|
|
44571
45163
|
onShutdown("otium-sidecar-server", 130, () => server?.stop(true));
|
|
44572
45164
|
onShutdown("otium-sidecar-tunnel", 120, () => tunnel?.stop());
|
|
@@ -44692,16 +45284,28 @@ async function runOtiumCli(args = process.argv.slice(2)) {
|
|
|
44692
45284
|
break;
|
|
44693
45285
|
}
|
|
44694
45286
|
case "leave": {
|
|
44695
|
-
|
|
44696
|
-
|
|
45287
|
+
const targetCellId = commandArgs[0]?.trim();
|
|
45288
|
+
if (commandArgs.length > 1 || targetCellId?.startsWith("-")) {
|
|
45289
|
+
throw new Error(`usage: negotium otium ${command} [<cell-id>]`);
|
|
45290
|
+
}
|
|
44697
45291
|
if (process.env.OTIUM_CENTRAL_URL || process.env.OTIUM_CELL_ID || process.env.OTIUM_CELL_SECRET) {
|
|
44698
45292
|
throw new Error("Otium join is configured by environment; remove OTIUM_CENTRAL_URL, OTIUM_CELL_ID, and OTIUM_CELL_SECRET to disconnect");
|
|
44699
45293
|
}
|
|
44700
|
-
const {
|
|
44701
|
-
|
|
45294
|
+
const { loadJoins: loadJoins2, removeJoin: removeJoin2 } = await init_join().then(() => exports_join);
|
|
45295
|
+
const joins = loadJoins2();
|
|
45296
|
+
if (joins.length === 0)
|
|
44702
45297
|
throw new Error("not joined to an Otium workspace");
|
|
44703
|
-
|
|
44704
|
-
|
|
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");
|
|
44705
45309
|
break;
|
|
44706
45310
|
}
|
|
44707
45311
|
case "serve": {
|
|
@@ -44724,8 +45328,9 @@ async function runOtiumCli(args = process.argv.slice(2)) {
|
|
|
44724
45328
|
" serve [--port <port>] [--relay <url>]",
|
|
44725
45329
|
" run peer routes and an outbound relay tunnel",
|
|
44726
45330
|
"",
|
|
44727
|
-
"
|
|
44728
|
-
"
|
|
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."
|
|
44729
45334
|
].join(`
|
|
44730
45335
|
`));
|
|
44731
45336
|
if (command && command !== "help" && command !== "--help")
|
|
@@ -44768,7 +45373,7 @@ __export(exports_topics, {
|
|
|
44768
45373
|
topicsCommand: () => topicsCommand
|
|
44769
45374
|
});
|
|
44770
45375
|
function topicsCommand() {
|
|
44771
|
-
const topics = getVisibleTopics();
|
|
45376
|
+
const topics = getVisibleTopics({ surface: "terminal" });
|
|
44772
45377
|
if (topics.length === 0) {
|
|
44773
45378
|
console.log("no topics yet - start `negotium` to create one in Terminal");
|
|
44774
45379
|
return;
|
|
@@ -45267,6 +45872,16 @@ function renderCliHelp() {
|
|
|
45267
45872
|
// ../cli/src/main.ts
|
|
45268
45873
|
var [, , rawCommand, ...args] = process.argv;
|
|
45269
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
|
+
}
|
|
45270
45885
|
function numericOption(values, name, fallback) {
|
|
45271
45886
|
const prefix = `--${name}=`;
|
|
45272
45887
|
const parsed = Number.parseInt(values.find((value) => value.startsWith(prefix))?.slice(prefix.length) ?? "", 10);
|
|
@@ -45292,7 +45907,7 @@ async function runCanonicalNode(port) {
|
|
|
45292
45907
|
});
|
|
45293
45908
|
console.log(`negotium node listening on 127.0.0.1:${node.port} (ctrl-c to stop)`);
|
|
45294
45909
|
await node.completed;
|
|
45295
|
-
await new Promise((
|
|
45910
|
+
await new Promise((resolve27) => setImmediate(resolve27));
|
|
45296
45911
|
process.exit(0);
|
|
45297
45912
|
}
|
|
45298
45913
|
async function stopAdapter(name) {
|
|
@@ -45324,8 +45939,7 @@ switch (command) {
|
|
|
45324
45939
|
}
|
|
45325
45940
|
case "serve": {
|
|
45326
45941
|
if (args[0] === "otium") {
|
|
45327
|
-
|
|
45328
|
-
await runOtiumCli2(["serve", ...args.slice(1)]);
|
|
45942
|
+
await runOtium(["serve", ...args.slice(1)]);
|
|
45329
45943
|
break;
|
|
45330
45944
|
}
|
|
45331
45945
|
await runCanonicalNode(numericOption(args, "port", 7777));
|
|
@@ -45409,11 +46023,10 @@ switch (command) {
|
|
|
45409
46023
|
break;
|
|
45410
46024
|
}
|
|
45411
46025
|
case "otium": {
|
|
45412
|
-
const { runOtiumCli: runOtiumCli2 } = await loadOtiumCli();
|
|
45413
46026
|
if (args[0] === "serve") {
|
|
45414
46027
|
process.stderr.write("warning: `negotium otium serve` is deprecated; use `negotium serve otium`\n");
|
|
45415
46028
|
}
|
|
45416
|
-
await
|
|
46029
|
+
await runOtium(args);
|
|
45417
46030
|
break;
|
|
45418
46031
|
}
|
|
45419
46032
|
default: {
|
|
@@ -45423,4 +46036,4 @@ switch (command) {
|
|
|
45423
46036
|
}
|
|
45424
46037
|
}
|
|
45425
46038
|
|
|
45426
|
-
//# debugId=
|
|
46039
|
+
//# debugId=EDB732002F3171B764756E2164756E21
|