negotium 0.2.24 → 0.2.26

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.
Files changed (31) hide show
  1. package/dist/agent-helpers.js +82 -29
  2. package/dist/agent-helpers.js.map +5 -5
  3. package/dist/hosted-agent.js +2 -2
  4. package/dist/hosted-agent.js.map +2 -2
  5. package/dist/main.js +167 -364
  6. package/dist/main.js.map +26 -27
  7. package/dist/mcp-factories.js +84 -30
  8. package/dist/mcp-factories.js.map +6 -6
  9. package/dist/registry.js +2 -2
  10. package/dist/registry.js.map +2 -2
  11. package/dist/runtime/src/index.ts +3 -7
  12. package/dist/runtime/src/mcp/session-comm/default-host.ts +6 -1
  13. package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +17 -1
  14. package/dist/runtime/src/mcp/session-comm/topics.ts +23 -1
  15. package/dist/runtime/src/node-host.ts +2 -3
  16. package/dist/runtime/src/storage/api-topics.ts +164 -52
  17. package/dist/runtime/src/storage/storage-public.ts +0 -1
  18. package/dist/runtime/src/topics/create.ts +12 -6
  19. package/dist/runtime/src/topics/derive.ts +20 -11
  20. package/dist/runtime/src/types/api.ts +8 -4
  21. package/dist/runtime/src/version.ts +1 -1
  22. package/dist/storage.js +92 -38
  23. package/dist/storage.js.map +3 -3
  24. package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +7 -0
  25. package/dist/types/packages/core/src/storage/api-topics.d.ts +34 -19
  26. package/dist/types/packages/core/src/storage/storage-public.d.ts +1 -1
  27. package/dist/types/packages/core/src/topics/derive.d.ts +13 -4
  28. package/dist/types/packages/core/src/types/api.d.ts +8 -4
  29. package/dist/types/packages/core/src/version.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/dist/runtime/src/application/switch-topic-access-mode.ts +0 -110
@@ -5,6 +5,8 @@ export interface SessionTopicRow {
5
5
  agent: string | null;
6
6
  sessionId: string | null;
7
7
  description: string | null;
8
+ /** Product surface the topic lives on; rows from other surfaces are not addressable. */
9
+ surface?: string | null;
8
10
  }
9
11
  export interface SessionTopicEntry<TAgent extends string = string> {
10
12
  sessionId: string;
@@ -36,6 +38,11 @@ export interface SessionTargetCatalogHost<TAgent extends string = string> {
36
38
  readonly listRows: () => SessionTopicRow[];
37
39
  readonly currentTopicId?: string;
38
40
  readonly currentTopicName?: string;
41
+ /**
42
+ * Surface of the calling topic. Sessions only converse within their own
43
+ * surface: terminal with terminal, telegram with telegram, otium with otium.
44
+ */
45
+ readonly currentSurface?: string;
39
46
  readonly isAgent: (value: string | null) => value is TAgent;
40
47
  }
41
48
  export interface SessionTargetCatalog<TAgent extends string = string> {
@@ -1,5 +1,16 @@
1
1
  import type { AgentKind, EffortLevel } from "../types";
2
- import type { AiMode, TopicAccessMode, TopicDto, TopicKind, TopicVisibility } from "../types/api";
2
+ import type { AiMode, TopicDto, TopicKind, TopicSurface, TopicVisibility } from "../types/api";
3
+ /**
4
+ * Surface used when a caller does not name one — and the value every existing
5
+ * row is backfilled with on first boot after the surface migration.
6
+ *
7
+ * Hosts that only ever serve one surface declare it once in their environment
8
+ * (`NEGOTIUM_DEFAULT_SURFACE=otium` on the Otium hub and worker); a developer
9
+ * Mac leaves it unset and gets `terminal`, with the telegram adapter
10
+ * reclassifying its own mapped rooms afterwards.
11
+ */
12
+ export declare function defaultTopicSurface(): TopicSurface;
13
+ export declare function normalizeTopicSurface(value: unknown): TopicSurface;
3
14
  /** Persist the authoritative agent and, when supplied, its normalized base defaults. */
4
15
  export declare function setApiTopicAgent(topicId: string, agent: AgentKind, defaults?: {
5
16
  model: string;
@@ -23,7 +34,7 @@ export interface TopicRow {
23
34
  is_subagent: number;
24
35
  subagent_report_mode: string | null;
25
36
  visibility: string | null;
26
- access_mode: string | null;
37
+ surface: string | null;
27
38
  browser_profile_owner: string | null;
28
39
  session_id: string | null;
29
40
  }
@@ -32,9 +43,6 @@ export interface TopicSessionLogContext {
32
43
  queryId?: string;
33
44
  agent?: AgentKind;
34
45
  }
35
- export declare function normalizeTopicAccessMode(value: unknown): TopicAccessMode;
36
- /** Otium and other non-local adapters may only address shared topics. */
37
- export declare function isTopicShared(topic: Pick<TopicDto, "accessMode">): boolean;
38
46
  export declare function normalizeTopicVisibility(value: unknown): TopicVisibility;
39
47
  /** Discovery boundary shared by user-facing adapters. */
40
48
  export declare function isTopicVisible(topic: Pick<TopicDto, "visibility">): boolean;
@@ -64,21 +72,15 @@ export declare function normalizeTopicState(input: {
64
72
  };
65
73
  export declare function upsertTopic(t: TopicDto): void;
66
74
  /**
67
- * Apply one access mode to a set of topics as a single all-or-nothing write.
75
+ * List topics, optionally restricted to one surface.
68
76
  *
69
- * A partial write is not a cosmetic glitch here: a public parent left with
70
- * private subagent children (or the reverse) is exactly the half-exposed state
71
- * the access-mode cascade exists to prevent, so every row commits or none do.
72
- *
73
- * Deliberately a narrow UPDATE instead of a loop over {@link upsertTopic}.
74
- * That helper opens its own transaction, and the node:sqlite shim in
75
- * `sqlite.ts` emulates transactions with bare BEGIN/COMMIT rather than
76
- * savepoints — nesting one inside an outer transaction would fail outright.
77
- * It also rewrites participants and browser-profile ownership, none of which
78
- * an access-mode change should touch.
77
+ * The filter lives here rather than in each adapter so a forgotten call site
78
+ * cannot leak a telegram room into the terminal picker; adapters pass their own
79
+ * surface and get a closed world back.
79
80
  */
80
- export declare function setTopicAccessModes(topicIds: readonly string[], accessMode: TopicAccessMode): void;
81
- export declare function listTopics(): TopicDto[];
81
+ export declare function listTopics(opts?: {
82
+ surface?: TopicSurface;
83
+ }): TopicDto[];
82
84
  export declare function getTopic(id: string): TopicDto | null;
83
85
  /** Return the private manager room owned by a user, excluding the retired shared General row. */
84
86
  export declare function getManagerTopicForUser(userId: string): TopicDto | null;
@@ -93,11 +95,24 @@ export declare function getTopicMemoryOrigin(id: string): TopicDto | null;
93
95
  /** Look up a topic by its user-visible title (case-insensitive exact match). */
94
96
  export declare function getTopicByName(title: string): TopicDto | null;
95
97
  export declare function getTopicByNameAndKind(title: string, kind: TopicKind): TopicDto | null;
98
+ /**
99
+ * Titles are unique **per surface**, not per node: `otium` may exist once on
100
+ * the terminal, once on telegram and once on the Otium hub.
101
+ */
96
102
  export declare function findTopicTitleConflict(title: string, kind: TopicKind, opts?: {
97
103
  excludeTopicId?: string;
104
+ surface?: TopicSurface;
98
105
  }): TopicDto | null;
106
+ /**
107
+ * Move topics onto a surface. Used by adapters that own a classification the
108
+ * canonical store cannot derive on its own (the telegram chat↔topic mapping
109
+ * lives in that adapter's database, not this one).
110
+ */
111
+ export declare function setTopicSurfaces(topicIds: readonly string[], surface: TopicSurface): number;
99
112
  /** Look up a topic by title, restricted to topics where `userId` participates. */
100
- export declare function getTopicByNameForUser(title: string, userId: string): TopicDto | null;
113
+ export declare function getTopicByNameForUser(title: string, userId: string, opts?: {
114
+ surface?: TopicSurface;
115
+ }): TopicDto | null;
101
116
  /** Persist the agent session ID for a topic after a successful turn. */
102
117
  export declare function setTopicSessionId(topicId: string, sessionId: string, context?: TopicSessionLogContext): void;
103
118
  /** Clear the session ID (e.g. on topic reset). */
@@ -41,4 +41,4 @@ export * from "./wiki";
41
41
  export * as wikiSummaryNames from "./wiki-summary-names";
42
42
  export * from "./wiki-summary-names";
43
43
  export type { AgentKind, EffortLevel } from "../types";
44
- export type { AiMode, MessageDto, ParticipantDto, ResponsePolicy, TopicAccessMode, TopicDto, TopicKind, TopicVisibility, } from "../types/api";
44
+ export type { AiMode, MessageDto, ParticipantDto, ResponsePolicy, TopicDto, TopicKind, TopicVisibility, } from "../types/api";
@@ -8,10 +8,19 @@
8
8
  */
9
9
  import { type CompactSummaryRequest } from "./session";
10
10
  import type { AgentKind } from "../types";
11
- import type { TopicDto } from "../types/api";
12
- export declare function getTopics(): TopicDto[];
13
- /** Topics adapters may show in lists and selection UIs. */
14
- export declare function getVisibleTopics(): TopicDto[];
11
+ import type { TopicDto, TopicSurface } from "../types/api";
12
+ export declare function getTopics(opts?: {
13
+ surface?: TopicSurface;
14
+ }): TopicDto[];
15
+ /**
16
+ * Topics adapters may show in lists and selection UIs.
17
+ *
18
+ * Callers that represent one product surface pass it, so a telegram room never
19
+ * appears in the terminal picker and vice versa (S-6).
20
+ */
21
+ export declare function getVisibleTopics(opts?: {
22
+ surface?: TopicSurface;
23
+ }): TopicDto[];
15
24
  export declare function updateTopic(topicId: string, patch: Partial<TopicDto>): boolean;
16
25
  export declare function isParticipant(topic: TopicDto, userId: string): boolean;
17
26
  /** Thrown when a user-supplied derived-topic name collides with an existing
@@ -12,8 +12,12 @@
12
12
  /** Agent identifier — one of the supported AI provider backends. */
13
13
  export type AgentKind = "maestro" | "claude" | "codex";
14
14
  export type TopicKind = "channel" | "agent" | "manager";
15
- /** Adapter access boundary for a user-facing topic. */
16
- export type TopicAccessMode = "private" | "shared";
15
+ /**
16
+ * The one product surface a topic lives on. A topic belongs to exactly one
17
+ * surface for its whole life; names are unique per surface, not per node.
18
+ */
19
+ export type TopicSurface = "terminal" | "telegram" | "otium";
20
+ export declare const TOPIC_SURFACES: readonly TopicSurface[];
17
21
  /** Whether adapters may expose a topic in user-facing discovery surfaces. */
18
22
  export type TopicVisibility = "visible" | "hidden";
19
23
  export type ResponsePolicy = "off" | "mention" | "always";
@@ -115,8 +119,8 @@ export interface TopicDto {
115
119
  subagentReportMode?: SubagentReportMode;
116
120
  /** Hidden topics remain executable/addressable by id but stay out of adapter pickers. */
117
121
  visibility?: TopicVisibility;
118
- /** Private stays on local adapters; shared may be exposed through Otium too. */
119
- accessMode?: TopicAccessMode;
122
+ /** The product surface that owns this topic (terminal / telegram / otium). */
123
+ surface?: TopicSurface;
120
124
  /** Stable execution placement. Absent means the hub runs this topic locally. */
121
125
  executionNode?: {
122
126
  nodeId: string;
@@ -1 +1 @@
1
- export declare const NEGOTIUM_VERSION = "0.2.24";
1
+ export declare const NEGOTIUM_VERSION = "0.2.26";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "negotium",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "type": "module",
5
5
  "description": "Install the Negotium multi-agent runtime and CLI with one package",
6
6
  "license": "Apache-2.0",
@@ -1,110 +0,0 @@
1
- import { WsHub } from "#bus";
2
- import { getTopic, listTopics, setTopicAccessModes } from "#storage/api-topics";
3
- import type { TopicAccessMode, TopicDto } from "#types/api";
4
-
5
- export type SwitchTopicAccessModeResult =
6
- | { ok: true; accessMode: TopicAccessMode; text: string; topicIds: string[] }
7
- | { ok: false; error: string };
8
-
9
- export interface SwitchTopicAccessModeParams {
10
- topicId: string;
11
- userId: string;
12
- accessMode: TopicAccessMode;
13
- }
14
-
15
- /**
16
- * Every subagent room reachable from `rootTopicId`, deepest-first order aside.
17
- *
18
- * Mirrors the traversal `deleteTopicCascade` already uses: only `isSubagent`
19
- * children are owned by their parent, while fork/spawn rooms are independent
20
- * and must keep their own privacy. `seen` guards against a parent cycle left
21
- * behind by reparenting, which would otherwise loop forever.
22
- */
23
- function collectSubagentDescendants(rootTopicId: string): TopicDto[] {
24
- const all = listTopics();
25
- const childrenByParent = new Map<string, TopicDto[]>();
26
- for (const topic of all) {
27
- if (!topic.isSubagent || !topic.parentTopicId) continue;
28
- const siblings = childrenByParent.get(topic.parentTopicId);
29
- if (siblings) siblings.push(topic);
30
- else childrenByParent.set(topic.parentTopicId, [topic]);
31
- }
32
- const descendants: TopicDto[] = [];
33
- const seen = new Set<string>([rootTopicId]);
34
- const queue = [rootTopicId];
35
- while (queue.length > 0) {
36
- const parentId = queue.shift() as string;
37
- for (const child of childrenByParent.get(parentId) ?? []) {
38
- if (seen.has(child.id)) continue;
39
- seen.add(child.id);
40
- descendants.push(child);
41
- queue.push(child.id);
42
- }
43
- }
44
- return descendants;
45
- }
46
-
47
- /** Change whether a user-owned topic is local-only or shared with connected adapters. */
48
- export function switchTopicAccessMode(
49
- params: SwitchTopicAccessModeParams,
50
- ): SwitchTopicAccessModeResult {
51
- const topic = getTopic(params.topicId);
52
- if (!topic) return { ok: false, error: "Topic not found" };
53
- const owner = topic.participants.some(
54
- (participant) => participant.userId === params.userId && participant.role === "owner",
55
- );
56
- if (!owner) return { ok: false, error: "Only topic owners can change privacy" };
57
- // A subagent room is owned by the room that spawned it, so its privacy is not
58
- // its own to change: allowing it would let a worker room escape the parent's
59
- // setting, and the next parent switch would silently overwrite it anyway.
60
- if (topic.isSubagent) {
61
- return {
62
- ok: false,
63
- error: "Subagent rooms inherit privacy from their parent topic",
64
- };
65
- }
66
-
67
- // Subagents hold the parent's conversation context, so leaving them behind on
68
- // the old mode would either expose work the user just made private or strand
69
- // half a delegation tree off the Hub. New rooms already inherit the parent's
70
- // access mode at creation (`createDerivedTopic`); this keeps existing ones in
71
- // step.
72
- const descendants = collectSubagentDescendants(topic.id);
73
- const changed = [topic, ...descendants].filter(
74
- (candidate) => (candidate.accessMode ?? "private") !== params.accessMode,
75
- );
76
- if (changed.length === 0) {
77
- return {
78
- ok: true,
79
- accessMode: params.accessMode,
80
- text:
81
- params.accessMode === "shared"
82
- ? `"${topic.title}" is already public to the connected Otium Hub.`
83
- : `"${topic.title}" is already private to this worker.`,
84
- topicIds: [],
85
- };
86
- }
87
-
88
- setTopicAccessModes(
89
- changed.map((candidate) => candidate.id),
90
- params.accessMode,
91
- );
92
- // Broadcast only after the write commits: an event emitted inside the
93
- // transaction would announce a state a rollback could still take back.
94
- for (const candidate of changed) WsHub.get().broadcastTopicUpdated(candidate.id);
95
-
96
- const subagentCount = changed.filter((candidate) => candidate.id !== topic.id).length;
97
- const suffix =
98
- subagentCount > 0
99
- ? ` (${subagentCount} subagent room${subagentCount === 1 ? "" : "s"} updated)`
100
- : "";
101
- return {
102
- ok: true,
103
- accessMode: params.accessMode,
104
- topicIds: changed.map((candidate) => candidate.id),
105
- text:
106
- (params.accessMode === "shared"
107
- ? `"${topic.title}" is public to the connected Otium Hub.`
108
- : `"${topic.title}" is private to this worker.`) + suffix,
109
- };
110
- }