negotium 0.1.11 → 0.1.13

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 (165) hide show
  1. package/README.md +11 -2
  2. package/dist/canonical-mcp-bridge.js +2 -3
  3. package/dist/canonical-mcp-bridge.js.map +1 -1
  4. package/dist/{chunk-gz12qcf0.js → chunk-a9y2rwxf.js} +2 -2
  5. package/dist/{chunk-gz12qcf0.js.map → chunk-a9y2rwxf.js.map} +3 -3
  6. package/dist/{chunk-j0kjscb7.js → chunk-s2gez3wg.js} +20 -2
  7. package/dist/{chunk-j0kjscb7.js.map → chunk-s2gez3wg.js.map} +2 -2
  8. package/dist/cron.js +292 -256
  9. package/dist/cron.js.map +23 -22
  10. package/dist/hosted-agent.js +3012 -17
  11. package/dist/hosted-agent.js.map +30 -3
  12. package/dist/main.js +31323 -36
  13. package/dist/main.js.map +198 -3
  14. package/dist/prompts.js +12 -9
  15. package/dist/prompts.js.map +5 -5
  16. package/dist/registry.js +10 -6
  17. package/dist/registry.js.map +3 -3
  18. package/dist/rollout.js +1 -1
  19. package/dist/runtime/cron/store.ts +24 -1
  20. package/dist/runtime/src/application/submit-user-message.ts +3 -0
  21. package/dist/runtime/src/index.ts +7 -0
  22. package/dist/runtime/src/mcp/runtime-spec.ts +3 -0
  23. package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
  24. package/dist/runtime/src/platform/config.ts +2 -2
  25. package/dist/runtime/src/platform/mcp-config.ts +13 -0
  26. package/dist/runtime/src/prompts/builders.ts +20 -6
  27. package/dist/runtime/src/query/active-rooms.ts +2 -0
  28. package/dist/runtime/src/runtime/delivery-ack.ts +112 -0
  29. package/dist/runtime/src/runtime/errors.ts +12 -12
  30. package/dist/runtime/src/runtime/turn-runner.ts +12 -0
  31. package/dist/runtime/src/storage/api-messages.ts +7 -5
  32. package/dist/runtime/src/storage/api-topics.ts +5 -2
  33. package/dist/runtime/src/storage/forum-db.ts +4 -1
  34. package/dist/runtime/src/storage/runtime-turn-requests.ts +1 -0
  35. package/dist/runtime/src/storage/session-asks.ts +13 -5
  36. package/dist/runtime/src/storage/storage-contract.ts +54 -0
  37. package/dist/runtime/src/storage/storage-host.ts +89 -36
  38. package/dist/runtime/src/storage/storage-public-host.ts +16 -0
  39. package/dist/runtime/src/storage/storage-public.ts +21 -5
  40. package/dist/runtime/src/storage/token-stats.ts +1 -1
  41. package/dist/runtime/src/types/api.ts +2 -0
  42. package/dist/runtime/src/types.ts +2 -0
  43. package/dist/runtime/src/version.ts +1 -1
  44. package/dist/runtime-helpers.js +29 -18
  45. package/dist/runtime-helpers.js.map +8 -7
  46. package/dist/storage.js +260 -202
  47. package/dist/storage.js.map +11 -9
  48. package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +2 -0
  49. package/dist/types/packages/core/src/platform/mcp-config.d.ts +4 -0
  50. package/dist/types/packages/core/src/prompts/builders.d.ts +2 -0
  51. package/dist/types/packages/core/src/storage/api-messages.d.ts +6 -6
  52. package/dist/types/packages/core/src/storage/api-topics.d.ts +20 -2
  53. package/dist/types/packages/core/src/storage/forum-db.d.ts +2 -1
  54. package/dist/types/packages/core/src/storage/session-asks.d.ts +3 -2
  55. package/dist/types/packages/core/src/storage/storage-contract.d.ts +46 -0
  56. package/dist/types/packages/core/src/storage/storage-host.d.ts +12 -19
  57. package/dist/types/packages/core/src/storage/storage-public-host.d.ts +7 -0
  58. package/dist/types/packages/core/src/storage/storage-public.d.ts +4 -2
  59. package/dist/types/packages/core/src/types/api.d.ts +2 -0
  60. package/dist/types/packages/core/src/types.d.ts +2 -0
  61. package/dist/types/packages/core/src/version.d.ts +1 -1
  62. package/dist/types/packages/module-cron/src/store.d.ts +22 -2
  63. package/dist/vault.js +2 -2
  64. package/dist/vault.js.map +3 -3
  65. package/package.json +1 -1
  66. package/dist/chunk-01xcc856.js +0 -149
  67. package/dist/chunk-01xcc856.js.map +0 -10
  68. package/dist/chunk-1a6kv9vt.js +0 -118
  69. package/dist/chunk-1a6kv9vt.js.map +0 -10
  70. package/dist/chunk-1b62d730.js +0 -117
  71. package/dist/chunk-1b62d730.js.map +0 -10
  72. package/dist/chunk-1k7t7bnx.js +0 -97
  73. package/dist/chunk-1k7t7bnx.js.map +0 -10
  74. package/dist/chunk-28apmz4r.js +0 -431
  75. package/dist/chunk-28apmz4r.js.map +0 -11
  76. package/dist/chunk-2fvydfkx.js +0 -141
  77. package/dist/chunk-2fvydfkx.js.map +0 -10
  78. package/dist/chunk-41ygvg5k.js +0 -2387
  79. package/dist/chunk-41ygvg5k.js.map +0 -26
  80. package/dist/chunk-4q966ttx.js +0 -371
  81. package/dist/chunk-4q966ttx.js.map +0 -11
  82. package/dist/chunk-51rcsavw.js +0 -1712
  83. package/dist/chunk-51rcsavw.js.map +0 -18
  84. package/dist/chunk-59d0shyj.js +0 -103
  85. package/dist/chunk-59d0shyj.js.map +0 -10
  86. package/dist/chunk-5fzgvn79.js +0 -3536
  87. package/dist/chunk-5fzgvn79.js.map +0 -20
  88. package/dist/chunk-61px0717.js +0 -153
  89. package/dist/chunk-61px0717.js.map +0 -11
  90. package/dist/chunk-66y0hr8c.js +0 -2136
  91. package/dist/chunk-66y0hr8c.js.map +0 -29
  92. package/dist/chunk-69k45zg8.js +0 -250
  93. package/dist/chunk-69k45zg8.js.map +0 -11
  94. package/dist/chunk-6ah5farg.js +0 -130
  95. package/dist/chunk-6ah5farg.js.map +0 -10
  96. package/dist/chunk-6d4fmea5.js +0 -117
  97. package/dist/chunk-6d4fmea5.js.map +0 -12
  98. package/dist/chunk-8cahx7xc.js +0 -46
  99. package/dist/chunk-8cahx7xc.js.map +0 -10
  100. package/dist/chunk-8f20fk4n.js +0 -252
  101. package/dist/chunk-8f20fk4n.js.map +0 -10
  102. package/dist/chunk-91ckrvk8.js +0 -318
  103. package/dist/chunk-91ckrvk8.js.map +0 -10
  104. package/dist/chunk-93krf6ng.js +0 -3069
  105. package/dist/chunk-93krf6ng.js.map +0 -32
  106. package/dist/chunk-9qx1t77a.js +0 -214
  107. package/dist/chunk-9qx1t77a.js.map +0 -10
  108. package/dist/chunk-beqhdr9w.js +0 -589
  109. package/dist/chunk-beqhdr9w.js.map +0 -11
  110. package/dist/chunk-bftftsgq.js +0 -1637
  111. package/dist/chunk-bftftsgq.js.map +0 -15
  112. package/dist/chunk-c500xe9z.js +0 -33
  113. package/dist/chunk-c500xe9z.js.map +0 -10
  114. package/dist/chunk-cjsnkvq2.js +0 -23
  115. package/dist/chunk-cjsnkvq2.js.map +0 -10
  116. package/dist/chunk-cxa7yghg.js +0 -577
  117. package/dist/chunk-cxa7yghg.js.map +0 -14
  118. package/dist/chunk-edb07sjp.js +0 -567
  119. package/dist/chunk-edb07sjp.js.map +0 -14
  120. package/dist/chunk-f361dwba.js +0 -589
  121. package/dist/chunk-f361dwba.js.map +0 -11
  122. package/dist/chunk-f5xgprg1.js +0 -59
  123. package/dist/chunk-f5xgprg1.js.map +0 -10
  124. package/dist/chunk-ffefbd3r.js +0 -17
  125. package/dist/chunk-ffefbd3r.js.map +0 -10
  126. package/dist/chunk-fswhzdf5.js +0 -64
  127. package/dist/chunk-fswhzdf5.js.map +0 -10
  128. package/dist/chunk-g144wdrs.js +0 -2907
  129. package/dist/chunk-g144wdrs.js.map +0 -24
  130. package/dist/chunk-h980hb2n.js +0 -512
  131. package/dist/chunk-h980hb2n.js.map +0 -13
  132. package/dist/chunk-h9vbhhgr.js +0 -1337
  133. package/dist/chunk-h9vbhhgr.js.map +0 -16
  134. package/dist/chunk-hwy3n31w.js +0 -21
  135. package/dist/chunk-hwy3n31w.js.map +0 -9
  136. package/dist/chunk-jtrk0khr.js +0 -108
  137. package/dist/chunk-jtrk0khr.js.map +0 -10
  138. package/dist/chunk-jzz91jwh.js +0 -70
  139. package/dist/chunk-jzz91jwh.js.map +0 -10
  140. package/dist/chunk-m9e3tbr8.js +0 -708
  141. package/dist/chunk-m9e3tbr8.js.map +0 -14
  142. package/dist/chunk-mh5mexsf.js +0 -147
  143. package/dist/chunk-mh5mexsf.js.map +0 -11
  144. package/dist/chunk-npd8n03v.js +0 -2152
  145. package/dist/chunk-npd8n03v.js.map +0 -15
  146. package/dist/chunk-pg1azh6n.js +0 -2
  147. package/dist/chunk-pg1azh6n.js.map +0 -9
  148. package/dist/chunk-qwy57177.js +0 -237
  149. package/dist/chunk-qwy57177.js.map +0 -11
  150. package/dist/chunk-r31q21h5.js +0 -53
  151. package/dist/chunk-r31q21h5.js.map +0 -10
  152. package/dist/chunk-rbvctv1n.js +0 -366
  153. package/dist/chunk-rbvctv1n.js.map +0 -10
  154. package/dist/chunk-sa874j5c.js +0 -9
  155. package/dist/chunk-sa874j5c.js.map +0 -10
  156. package/dist/chunk-sctcs5zb.js +0 -281
  157. package/dist/chunk-sctcs5zb.js.map +0 -10
  158. package/dist/chunk-tsstrcfc.js +0 -122
  159. package/dist/chunk-tsstrcfc.js.map +0 -10
  160. package/dist/chunk-wk2qf31s.js +0 -58
  161. package/dist/chunk-wk2qf31s.js.map +0 -10
  162. package/dist/chunk-wx7ag4dd.js +0 -2936
  163. package/dist/chunk-wx7ag4dd.js.map +0 -20
  164. package/dist/chunk-znx1fpfd.js +0 -61
  165. package/dist/chunk-znx1fpfd.js.map +0 -10
package/dist/rollout.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  repairPoisonedRollout,
11
11
  writeClaudeRollout,
12
12
  writeCodexRollout
13
- } from "./chunk-gz12qcf0.js";
13
+ } from "./chunk-a9y2rwxf.js";
14
14
 
15
15
  // src/rollout.ts
16
16
  import { fileURLToPath } from "url";
@@ -137,7 +137,30 @@ interface TableColumnRow {
137
137
 
138
138
  let schemaReady = false;
139
139
 
140
- export type CronDatabase = Pick<typeof defaultDb, "exec" | "query" | "transaction">;
140
+ export interface CronDatabaseStatement {
141
+ get(...params: any[]): unknown;
142
+ all(...params: any[]): unknown[];
143
+ run(...params: any[]): CronDatabaseRunResult;
144
+ }
145
+
146
+ export interface CronDatabaseRunResult {
147
+ changes: number | bigint;
148
+ lastInsertRowid?: number | bigint;
149
+ }
150
+
151
+ export interface CronDatabaseTransaction<Result> {
152
+ (): Result;
153
+ deferred?: () => Result;
154
+ immediate?: () => Result;
155
+ exclusive?: () => Result;
156
+ }
157
+
158
+ /** Structural SQLite surface accepted from Bun, Node, or an embedding host. */
159
+ export interface CronDatabase {
160
+ exec(sql: string): unknown;
161
+ query(sql: string): CronDatabaseStatement;
162
+ transaction<Result>(fn: () => Result): CronDatabaseTransaction<Result>;
163
+ }
141
164
 
142
165
  let db: CronDatabase = defaultDb;
143
166
 
@@ -12,6 +12,8 @@ export interface SubmitUserMessageParams {
12
12
  allowAutoContinue?: boolean;
13
13
  /** Adapter capability: only Otium should enable visual panel tools. */
14
14
  visualTools?: boolean;
15
+ /** Adapter capability: Otium and Telegram may enable file delivery. */
16
+ fileDeliveryTools?: boolean;
15
17
  onDispatched?: (queryId: string) => void;
16
18
  /** Override used by remote hosts and deterministic tests. */
17
19
  startTurn?: (params: StartAiTurnParams) => string | null;
@@ -44,6 +46,7 @@ export function submitUserMessage(params: SubmitUserMessageParams): SubmitUserMe
44
46
  prompt: params.text,
45
47
  allowAutoContinue: params.allowAutoContinue ?? true,
46
48
  visualTools: params.visualTools === true,
49
+ fileDeliveryTools: params.fileDeliveryTools === true,
47
50
  onDispatched: params.onDispatched,
48
51
  });
49
52
  return { message, queryId };
@@ -206,9 +206,16 @@ export {
206
206
  listBackgroundSessionsForUser,
207
207
  registerBackgroundSessionProvider,
208
208
  } from "#runtime/background-sessions";
209
+ export type { DeliveryAckResult } from "#runtime/delivery-ack";
210
+ export {
211
+ claimDeliveryAck,
212
+ prepareDeliveryAck,
213
+ resolveDeliveryAck,
214
+ } from "#runtime/delivery-ack";
209
215
  export type { FileHooks, UploadAccess } from "#runtime/file-hooks";
210
216
  export {
211
217
  fileHooks,
218
+ resetFileHooks,
212
219
  resolveAttachmentByFileId,
213
220
  resolveUploadedFilePathByFileId,
214
221
  setFileHooks,
@@ -30,6 +30,8 @@ export interface RuntimeMcpContext {
30
30
  autoContinue?: boolean;
31
31
  /** Capability minted by the adapter. Visual tools are absent unless true. */
32
32
  visualTools?: boolean;
33
+ /** Capability minted by the adapter. File-delivery tools are absent unless true. */
34
+ fileDeliveryTools?: boolean;
33
35
  peerBridge?: PeerRuntimeBridgeContext;
34
36
  }
35
37
 
@@ -83,6 +85,7 @@ function isRuntimeMcpContext(value: unknown): value is RuntimeMcpContext {
83
85
  (ctx.currentUserPrompt === undefined || typeof ctx.currentUserPrompt === "string") &&
84
86
  (ctx.autoContinue === undefined || typeof ctx.autoContinue === "boolean") &&
85
87
  (ctx.visualTools === undefined || typeof ctx.visualTools === "boolean") &&
88
+ (ctx.fileDeliveryTools === undefined || typeof ctx.fileDeliveryTools === "boolean") &&
86
89
  (ctx.peerBridge === undefined ||
87
90
  (typeof ctx.peerBridge.hubCellId === "string" &&
88
91
  typeof ctx.peerBridge.hostTopicId === "string" &&
@@ -566,7 +566,7 @@ if (!isReplyOnly) {
566
566
 
567
567
  if (currentDepth + 1 > MAX_TELL_DEPTH) {
568
568
  return mcpError(
569
- `Error: depth 한도 도달 (현재 ${currentDepth}, 최대 ${MAX_TELL_DEPTH}). 이상 tell_session 체인을 만들 수 없습니다.`,
569
+ `Error: depth limit reached (current ${currentDepth}, max ${MAX_TELL_DEPTH}). Cannot build any more tell_session chains.`,
570
570
  );
571
571
  }
572
572
 
@@ -353,10 +353,10 @@ export const PDFTOTEXT_BIN = envText("PDFTOTEXT_BIN") ?? "pdftotext";
353
353
 
354
354
  // Max tell_session relay depth from origin user. ask_session forks reset to
355
355
  // depth=0, so this only caps tell_session chains. Override via MAX_TELL_DEPTH
356
- // (positive int); defaults to 3 when unset or invalid.
356
+ // (positive int); defaults to 20 when unset or invalid.
357
357
  const _envMaxTellDepth = Number.parseInt(process.env.MAX_TELL_DEPTH ?? "", 10);
358
358
  export const MAX_TELL_DEPTH =
359
- Number.isInteger(_envMaxTellDepth) && _envMaxTellDepth > 0 ? _envMaxTellDepth : 3;
359
+ Number.isInteger(_envMaxTellDepth) && _envMaxTellDepth > 0 ? _envMaxTellDepth : 20;
360
360
 
361
361
  /** Codex CLI auth file. 호출 시점에 env를 읽는다 — 테스트가 런타임에
362
362
  * NEGOTIUM_CODEX_AUTH_FILE을 바꾸므로 모듈 로드 상수로 만들면 안 된다. */
@@ -186,6 +186,7 @@ export interface RuntimeMcpBuildContext {
186
186
  bgBashPort?: number;
187
187
  autoContinue?: boolean;
188
188
  visualTools?: boolean;
189
+ fileDeliveryTools?: boolean;
189
190
  silent?: boolean;
190
191
  peerBridge?: PeerRuntimeBridgeContext;
191
192
  }
@@ -298,6 +299,7 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
298
299
  currentUserPrompt,
299
300
  autoContinue,
300
301
  visualTools,
302
+ fileDeliveryTools,
301
303
  peerBridge,
302
304
  }) {
303
305
  if (!topicId || !agent) return null;
@@ -312,6 +314,7 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
312
314
  currentUserPrompt,
313
315
  autoContinue,
314
316
  visualTools,
317
+ fileDeliveryTools,
315
318
  peerBridge,
316
319
  });
317
320
  },
@@ -663,6 +666,7 @@ export function getManagerMcpServers(opts: {
663
666
  playwrightCapability?: string;
664
667
  autoContinue?: boolean;
665
668
  visualTools?: boolean;
669
+ fileDeliveryTools?: boolean;
666
670
  }) {
667
671
  if (!opts.topicId) {
668
672
  throw new Error("getManagerMcpServers: private General topicId is required");
@@ -682,6 +686,7 @@ export function getManagerMcpServers(opts: {
682
686
  playwrightCapability: opts.playwrightCapability,
683
687
  autoContinue: opts.autoContinue,
684
688
  visualTools: opts.visualTools,
689
+ fileDeliveryTools: opts.fileDeliveryTools,
685
690
  });
686
691
  }
687
692
 
@@ -706,6 +711,7 @@ export function getForumMcpServers(opts: {
706
711
  bgBashPort?: number;
707
712
  autoContinue?: boolean;
708
713
  visualTools?: boolean;
714
+ fileDeliveryTools?: boolean;
709
715
  depth?: number;
710
716
  enabled?: string[] | null;
711
717
  extra?: Record<string, unknown>;
@@ -731,6 +737,7 @@ export function getForumMcpServers(opts: {
731
737
  bgBashPort,
732
738
  autoContinue,
733
739
  visualTools,
740
+ fileDeliveryTools,
734
741
  peerBridge,
735
742
  } = opts;
736
743
 
@@ -760,6 +767,7 @@ export function getForumMcpServers(opts: {
760
767
  bgBashPort,
761
768
  autoContinue,
762
769
  visualTools,
770
+ fileDeliveryTools,
763
771
  silent,
764
772
  peerBridge,
765
773
  },
@@ -787,6 +795,7 @@ export function getCronMcpServers(opts: {
787
795
  playwrightPort?: number;
788
796
  playwrightCapability?: string;
789
797
  visualTools?: boolean;
798
+ fileDeliveryTools?: boolean;
790
799
  }) {
791
800
  return buildScope("cron", {
792
801
  userId: opts.userId,
@@ -802,6 +811,7 @@ export function getCronMcpServers(opts: {
802
811
  playwrightCapability: opts.playwrightCapability,
803
812
  autoContinue: false,
804
813
  visualTools: opts.visualTools,
814
+ fileDeliveryTools: opts.fileDeliveryTools,
805
815
  });
806
816
  }
807
817
 
@@ -827,6 +837,7 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
827
837
  playwrightPort: opts.playwrightPort,
828
838
  playwrightCapability: opts.playwrightCapability,
829
839
  visualTools: opts.visualTools,
840
+ fileDeliveryTools: opts.fileDeliveryTools,
830
841
  });
831
842
  }
832
843
  if (opts.sessionType === "dm" || opts.sessionType === "ephemeral") {
@@ -852,6 +863,7 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
852
863
  playwrightCapability: opts.playwrightCapability,
853
864
  autoContinue: opts.autoContinue,
854
865
  visualTools: opts.visualTools,
866
+ fileDeliveryTools: opts.fileDeliveryTools,
855
867
  });
856
868
  }
857
869
  return getForumMcpServers({
@@ -869,6 +881,7 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
869
881
  bgBashPort: opts.bgBashPort,
870
882
  autoContinue: opts.autoContinue,
871
883
  visualTools: opts.visualTools,
884
+ fileDeliveryTools: opts.fileDeliveryTools,
872
885
  depth: opts.depth,
873
886
  enabled: opts.mcpEnabled,
874
887
  extra: opts.mcpExtra,
@@ -156,12 +156,15 @@ export interface SessionSystemPromptOpts {
156
156
  canSpawnSubagents?: boolean;
157
157
  /** True only when the current adapter renders Otium visual cards. */
158
158
  visualTools?: boolean;
159
+ /** True only when the current adapter can deliver files to its chat. */
160
+ fileDeliveryTools?: boolean;
159
161
  }
160
162
 
161
163
  function buildRuntimeToolSection(
162
164
  agentKind: AgentKind,
163
165
  canSpawnSubagents = false,
164
166
  visualTools = false,
167
+ fileDeliveryTools = false,
165
168
  ): string {
166
169
  const runtimeNamespace = "mcp__runtime";
167
170
  const taskNamespace = "mcp__task";
@@ -222,6 +225,14 @@ function buildRuntimeToolSection(
222
225
  "Visual HTML runs in a sandbox. Use inline CSS/JS only; local buttons, tabs, filters, forms with preventDefault, canvas, and SVG interactions are supported. External navigation, scripts, network fetches, form posts, popups, and parent-window access are blocked.",
223
226
  ]
224
227
  : [];
228
+ const fileDeliverySection = fileDeliveryTools
229
+ ? [
230
+ "",
231
+ "## File Delivery",
232
+ `To send a file to the user, save it under your working directory and call the ${sendFileTool} with { file_path: "<absolute path>" }.`,
233
+ "It appears as a downloadable attachment in the chat and returns success. Never claim file delivery is unavailable after a successful call.",
234
+ ]
235
+ : [];
225
236
 
226
237
  const shared = [
227
238
  "",
@@ -237,10 +248,7 @@ function buildRuntimeToolSection(
237
248
  taskToolLine,
238
249
  "Use this shared task store for plans, task progress, and checklist updates. It is visible across claude/codex/maestro turns and drives the live task panel.",
239
250
  nativeTaskPolicyLine,
240
- "",
241
- "## File Delivery",
242
- `To send a file to the user, save it under your working directory and call the ${sendFileTool} with { file_path: "<absolute path>" }.`,
243
- "It appears as a downloadable attachment in the chat and returns success. Never claim file delivery is unavailable after a successful call.",
251
+ ...fileDeliverySection,
244
252
  "",
245
253
  "## Session Communication",
246
254
  "The session-comm MCP server is the only cross-topic messaging surface. Its canonical tools are `list_sessions`, `peek_session`, `tell_session`, `ask_session`, and `abort_session`.",
@@ -292,7 +300,13 @@ export function buildTopicSystemPrompt(opts: SessionSystemPromptOpts): string {
292
300
  TOPIC_TITLE: opts.topicTitle,
293
301
  WORKSPACE_CWD: opts.workspaceCwd,
294
302
  UPLOADS_DIR: uploadsDir,
295
- }) + buildRuntimeToolSection(opts.agentKind, opts.canSpawnSubagents, opts.visualTools);
303
+ }) +
304
+ buildRuntimeToolSection(
305
+ opts.agentKind,
306
+ opts.canSpawnSubagents,
307
+ opts.visualTools,
308
+ opts.fileDeliveryTools,
309
+ );
296
310
  if (opts.description?.trim()) {
297
311
  prompt += `\n\n## Topic-Specific Instructions\n${opts.description.trim()}`;
298
312
  }
@@ -307,7 +321,7 @@ export function buildChannelSystemPrompt(opts: SessionSystemPromptOpts): string
307
321
  TOPIC_TITLE: opts.topicTitle,
308
322
  WORKSPACE_CWD: opts.workspaceCwd,
309
323
  UPLOADS_DIR: uploadsDir,
310
- }) + buildRuntimeToolSection(opts.agentKind, false, opts.visualTools)
324
+ }) + buildRuntimeToolSection(opts.agentKind, false, opts.visualTools, opts.fileDeliveryTools)
311
325
  );
312
326
  }
313
327
 
@@ -73,6 +73,8 @@ export interface DeferredInject {
73
73
  sessionType?: "dm" | "forum" | "ephemeral" | "manager" | "cron";
74
74
  /** Adapter-granted access to Otium's visual panel tools. */
75
75
  visualTools?: boolean;
76
+ /** Adapter-granted access to chat file-delivery tools. */
77
+ fileDeliveryTools?: boolean;
76
78
  /** Optional session owner; when present the topic's main session is not replaced. */
77
79
  onSessionId?: (sessionId: string) => void;
78
80
  /** Clear an externally-owned session after an unrecoverable expiry. */
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Cross-process delivery acknowledgements for files rendered by an external
3
+ * channel adapter. Signals ride the durable RuntimeBus so the runtime MCP and
4
+ * adapter may live in separate processes (the default CLI topology).
5
+ */
6
+
7
+ import { runtimeBus } from "#bus";
8
+
9
+ export interface DeliveryAckResult {
10
+ ok: boolean;
11
+ error?: string;
12
+ }
13
+
14
+ interface DeliveryAckSignal {
15
+ kind: "delivery_ack";
16
+ messageId: string;
17
+ phase: "claimed" | "settled";
18
+ result?: DeliveryAckResult;
19
+ }
20
+
21
+ export interface DeliveryAckWaiter {
22
+ promise: Promise<DeliveryAckResult | null>;
23
+ cancel(): void;
24
+ }
25
+
26
+ function signalForMessage(payload: unknown, messageId: string): DeliveryAckSignal | null {
27
+ if (!payload || typeof payload !== "object") return null;
28
+ const value = payload as Partial<DeliveryAckSignal>;
29
+ if (
30
+ value.kind !== "delivery_ack" ||
31
+ value.messageId !== messageId ||
32
+ (value.phase !== "claimed" && value.phase !== "settled")
33
+ ) {
34
+ return null;
35
+ }
36
+ return value as DeliveryAckSignal;
37
+ }
38
+
39
+ /**
40
+ * Install the listener before publishing the attachment message. If no
41
+ * adapter claims the message within `claimTimeoutMs`, no external delivery
42
+ * surface was interested and the caller may keep the host-storage result.
43
+ * Once claimed, lack of a final result is a delivery failure, never success.
44
+ */
45
+ export function prepareDeliveryAck(
46
+ messageId: string,
47
+ claimTimeoutMs: number,
48
+ deliveryTimeoutMs: number,
49
+ ): DeliveryAckWaiter {
50
+ let settled = false;
51
+ let claimed = false;
52
+ let finishPromise: (result: DeliveryAckResult | null) => void = () => {};
53
+ let timer: ReturnType<typeof setTimeout> | undefined;
54
+ let unsubscribe = () => {};
55
+
56
+ const promise = new Promise<DeliveryAckResult | null>((resolve) => {
57
+ finishPromise = resolve;
58
+ });
59
+
60
+ unsubscribe = runtimeBus().subscribe((event) => {
61
+ if (event.type !== "ai-status") return;
62
+ const signal = signalForMessage(event.payload, messageId);
63
+ if (!signal) return;
64
+ if (signal.phase === "claimed") {
65
+ if (claimed || settled) return;
66
+ claimed = true;
67
+ if (timer) clearTimeout(timer);
68
+ timer = setTimeout(() => {
69
+ finish({ ok: false, error: "channel delivery confirmation timed out" });
70
+ }, deliveryTimeoutMs);
71
+ timer.unref?.();
72
+ return;
73
+ }
74
+ if (signal.result) finish(signal.result);
75
+ });
76
+
77
+ function finish(result: DeliveryAckResult | null): void {
78
+ if (settled) return;
79
+ settled = true;
80
+ if (timer) clearTimeout(timer);
81
+ unsubscribe();
82
+ finishPromise(result);
83
+ }
84
+
85
+ timer = setTimeout(() => finish(null), claimTimeoutMs);
86
+ timer.unref?.();
87
+
88
+ return { promise, cancel: () => finish(null) };
89
+ }
90
+
91
+ /** Announce that an adapter accepted this message for external delivery. */
92
+ export function claimDeliveryAck(topicId: string, messageId: string): void {
93
+ runtimeBus().broadcastAiStatus(topicId, {
94
+ kind: "delivery_ack",
95
+ messageId,
96
+ phase: "claimed",
97
+ } satisfies DeliveryAckSignal);
98
+ }
99
+
100
+ /** Publish the adapter's one aggregated result for this message. */
101
+ export function resolveDeliveryAck(
102
+ topicId: string,
103
+ messageId: string,
104
+ result: DeliveryAckResult,
105
+ ): void {
106
+ runtimeBus().broadcastAiStatus(topicId, {
107
+ kind: "delivery_ack",
108
+ messageId,
109
+ phase: "settled",
110
+ result,
111
+ } satisfies DeliveryAckSignal);
112
+ }
@@ -16,11 +16,11 @@ export function stringifyError(err: unknown): string {
16
16
  export function authRecoveryHint(agent: AgentKind): string {
17
17
  switch (agent) {
18
18
  case "claude":
19
- return "Claude Code 로그인을 갱신해주세요";
19
+ return "Please refresh your Claude Code login";
20
20
  case "codex":
21
- return "`codex login` 으로 다시 로그인해주세요 (~/.codex/auth.json)";
21
+ return "Please log in again with `codex login` (~/.codex/auth.json)";
22
22
  case "maestro":
23
- return "DEEPSEEK_API_KEY 환경변수를 확인해주세요";
23
+ return "Please check the DEEPSEEK_API_KEY environment variable";
24
24
  }
25
25
  }
26
26
 
@@ -31,14 +31,14 @@ export function classifyAgentError(err: unknown, agent: AgentKind): string {
31
31
  typeof err === "object" && err !== null
32
32
  ? (err as Record<string, unknown>).constructor?.name
33
33
  : undefined;
34
- if (ctor === "AuthenticationError") return `${name} 인증이 만료되었습니다. ${hint}. (401)`;
34
+ if (ctor === "AuthenticationError") return `${name} authentication expired. ${hint}. (401)`;
35
35
  if (ctor === "RateLimitError")
36
- return `${name} 요청 한도를 초과했습니다. 잠시 다시 시도해주세요. (429)`;
36
+ return `${name} request limit exceeded. Please try again in a moment. (429)`;
37
37
  if (ctor === "InternalServerError") {
38
38
  const status = (err as Record<string, unknown>).status;
39
39
  return status === 529
40
- ? `${name} 서버가 과부하 상태입니다. 잠시 다시 시도해주세요. (529)`
41
- : `${name} 서버 오류가 발생했습니다. 잠시 다시 시도해주세요. (500)`;
40
+ ? `${name} server is overloaded. Please try again in a moment. (529)`
41
+ : `${name} server error occurred. Please try again in a moment. (500)`;
42
42
  }
43
43
 
44
44
  const s = stringifyError(err);
@@ -47,17 +47,17 @@ export function classifyAgentError(err: unknown, agent: AgentKind): string {
47
47
  s,
48
48
  )
49
49
  ) {
50
- return `${name} 인증이 만료되었습니다. ${hint}. (401)`;
50
+ return `${name} authentication expired. ${hint}. (401)`;
51
51
  }
52
52
  if (/429|rate.limit/i.test(s))
53
- return `${name} 요청 한도를 초과했습니다. 잠시 다시 시도해주세요. (429)`;
53
+ return `${name} request limit exceeded. Please try again in a moment. (429)`;
54
54
  if (/529|overloaded/i.test(s))
55
- return `${name} 서버가 과부하 상태입니다. 잠시 다시 시도해주세요. (529)`;
55
+ return `${name} server is overloaded. Please try again in a moment. (529)`;
56
56
  if (/500|internal.server/i.test(s))
57
- return `${name} 서버 오류가 발생했습니다. 잠시 다시 시도해주세요. (500)`;
57
+ return `${name} server error occurred. Please try again in a moment. (500)`;
58
58
 
59
59
  const snippet = s.length > 200 ? `${s.slice(0, 200)}...` : s;
60
- return `${name} 오류: ${snippet}`;
60
+ return `${name} error: ${snippet}`;
61
61
  }
62
62
 
63
63
  export function isSessionExpiredError(message: string): boolean {
@@ -946,6 +946,7 @@ function redispatchInject(inject: DeferredInject): void {
946
946
  sessionName: inject.sessionName,
947
947
  sessionType: inject.sessionType,
948
948
  visualTools: inject.visualTools,
949
+ fileDeliveryTools: inject.fileDeliveryTools,
949
950
  onSessionId: inject.onSessionId,
950
951
  onSessionReset: inject.onSessionReset,
951
952
  bridgeSessionFromHistory: inject.bridgeSessionFromHistory,
@@ -1221,6 +1222,8 @@ export interface AiTurnExecutionOptions {
1221
1222
  sessionType?: "dm" | "forum" | "ephemeral" | "manager" | "cron";
1222
1223
  /** Adapter-granted access to Otium's visual panel tools. Default-deny. */
1223
1224
  visualTools?: boolean;
1225
+ /** Adapter-granted access to chat file-delivery tools. Default-deny. */
1226
+ fileDeliveryTools?: boolean;
1224
1227
  /** Own this turn's provider session without replacing the topic's main session. */
1225
1228
  onSessionId?: (sessionId: string) => void;
1226
1229
  /** Clear the externally-owned provider session after recovery fails. */
@@ -1366,6 +1369,7 @@ function serializableUserTurnExecution(params: StartAiTurnParams): RuntimeUserTu
1366
1369
  sessionName: params.sessionName,
1367
1370
  sessionType: params.sessionType,
1368
1371
  visualTools: params.visualTools,
1372
+ fileDeliveryTools: params.fileDeliveryTools,
1369
1373
  bridgeSessionFromHistory: params.bridgeSessionFromHistory,
1370
1374
  peerBridge: params.peerBridge,
1371
1375
  from: params.from,
@@ -1451,6 +1455,7 @@ async function drainOneDurableUserTurn(): Promise<void> {
1451
1455
  sessionName: execution?.sessionName,
1452
1456
  sessionType: execution?.sessionType,
1453
1457
  visualTools: execution?.visualTools,
1458
+ fileDeliveryTools: execution?.fileDeliveryTools,
1454
1459
  bridgeSessionFromHistory: execution?.bridgeSessionFromHistory,
1455
1460
  peerBridge: execution?.peerBridge,
1456
1461
  from: execution?.from,
@@ -1555,6 +1560,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
1555
1560
  const sessionName = params.sessionName ?? topic.title;
1556
1561
  const sessionType = params.sessionType;
1557
1562
  const visualTools = params.visualTools === true;
1563
+ const fileDeliveryTools = params.fileDeliveryTools === true;
1558
1564
  const onSessionId = params.onSessionId;
1559
1565
  const onSessionReset = params.onSessionReset;
1560
1566
  const bridgeSessionFromHistory = params.bridgeSessionFromHistory === true;
@@ -1606,6 +1612,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
1606
1612
  sessionName,
1607
1613
  sessionType,
1608
1614
  visualTools,
1615
+ fileDeliveryTools,
1609
1616
  onSessionId,
1610
1617
  onSessionReset,
1611
1618
  bridgeSessionFromHistory,
@@ -1743,6 +1750,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
1743
1750
  sessionName,
1744
1751
  sessionType,
1745
1752
  visualTools,
1753
+ fileDeliveryTools,
1746
1754
  onSessionId,
1747
1755
  onSessionReset,
1748
1756
  bridgeSessionFromHistory,
@@ -1872,6 +1880,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
1872
1880
  canSpawnSubagents:
1873
1881
  peerBridge?.canSpawnSubagents ?? (topicRecord?.kind === "agent" && !topicRecord.isSubagent),
1874
1882
  visualTools,
1883
+ fileDeliveryTools,
1875
1884
  };
1876
1885
  const isManager = topicRecord?.kind === "manager";
1877
1886
  const isMentionOnlyChannel = topic.aiMode === "mention" && !isManager;
@@ -2063,6 +2072,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
2063
2072
  wikiTopicId: memoryTopic.id,
2064
2073
  autoContinue: allowAutoContinue && !silent,
2065
2074
  visualTools,
2075
+ fileDeliveryTools,
2066
2076
  peerBridge,
2067
2077
  });
2068
2078
  } finally {
@@ -2155,6 +2165,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
2155
2165
  sessionName,
2156
2166
  sessionType,
2157
2167
  visualTools,
2168
+ fileDeliveryTools,
2158
2169
  onSessionId,
2159
2170
  onSessionReset,
2160
2171
  bridgeSessionFromHistory,
@@ -2337,6 +2348,7 @@ export function triggerTopicAiTurn(
2337
2348
  sessionName: opts?.sessionName,
2338
2349
  sessionType: opts?.sessionType,
2339
2350
  visualTools: opts?.visualTools,
2351
+ fileDeliveryTools: opts?.fileDeliveryTools,
2340
2352
  onSessionId: opts?.onSessionId,
2341
2353
  onSessionReset: opts?.onSessionReset,
2342
2354
  bridgeSessionFromHistory: opts?.bridgeSessionFromHistory,
@@ -118,7 +118,7 @@ export interface ApiMessageRow {
118
118
  rowid?: number;
119
119
  }
120
120
 
121
- type ApiMessageAppendHook = (msg: MessageDto) => void | Promise<void>;
121
+ export type ApiMessageAppendHook = (msg: MessageDto) => void | Promise<void>;
122
122
  const appendHooks = new Set<ApiMessageAppendHook>();
123
123
 
124
124
  export interface AppendApiMessageOptions {
@@ -366,6 +366,11 @@ export interface MessagePage {
366
366
  hasMore: boolean;
367
367
  }
368
368
 
369
+ export interface ListApiMessagesOptions {
370
+ cursor?: string | null;
371
+ limit?: number;
372
+ }
373
+
369
374
  /**
370
375
  * List messages for a topic, NEWEST-FIRST by default (bug G).
371
376
  *
@@ -378,10 +383,7 @@ export interface MessagePage {
378
383
  * (the oldest id currently loaded) to fetch the page of messages *older* than
379
384
  * it. `hasMore` therefore means "there are older messages above this page".
380
385
  */
381
- export function listApiMessages(
382
- topicId: string,
383
- options?: { cursor?: string | null; limit?: number },
384
- ): MessagePage {
386
+ export function listApiMessages(topicId: string, options?: ListApiMessagesOptions): MessagePage {
385
387
  const limit = Math.max(1, Math.min(options?.limit ?? 50, 200));
386
388
 
387
389
  let anchorRowid: number | null = null;
@@ -370,11 +370,14 @@ function initializeApiTopicsSchema(): void {
370
370
  )
371
371
  WHERE browser_profile_owner IS NULL
372
372
  `);
373
+ db.exec(
374
+ "CREATE INDEX IF NOT EXISTS idx_api_topics_last_message ON api_topics(last_message_at DESC)",
375
+ );
373
376
  }
374
377
 
375
378
  registerStorageSchemaInitializer(initializeApiTopicsSchema, 20);
376
379
 
377
- interface TopicRow {
380
+ export interface TopicRow {
378
381
  id: string;
379
382
  title: string;
380
383
  kind: string | null;
@@ -394,7 +397,7 @@ interface TopicRow {
394
397
  session_id: string | null;
395
398
  }
396
399
 
397
- interface TopicSessionLogContext {
400
+ export interface TopicSessionLogContext {
398
401
  reason?: string;
399
402
  queryId?: string;
400
403
  agent?: AgentKind;
@@ -1 +1,4 @@
1
- export { storageDatabase as db } from "#storage/storage-host";
1
+ import type { StorageDatabase } from "#storage/storage-contract";
2
+ import { internalStorageDatabase } from "#storage/storage-host";
3
+
4
+ export const db = internalStorageDatabase as unknown as StorageDatabase;
@@ -20,6 +20,7 @@ export interface RuntimeUserTurnExecution {
20
20
  sessionName?: string;
21
21
  sessionType?: "dm" | "forum" | "ephemeral" | "manager" | "cron";
22
22
  visualTools?: boolean;
23
+ fileDeliveryTools?: boolean;
23
24
  bridgeSessionFromHistory?: boolean;
24
25
  peerBridge?: PeerRuntimeBridgeContext;
25
26
  from?: string;
@@ -39,21 +39,25 @@ export interface AskReplySource {
39
39
  contextId?: string;
40
40
  }
41
41
 
42
- interface AskKey {
42
+ export interface PendingAskKey {
43
43
  userId: PendingAskUserId;
44
44
  from: string;
45
45
  to: string;
46
46
  }
47
47
 
48
- interface AskIdentity extends AskKey {
48
+ export interface PendingAskIdentity extends PendingAskKey {
49
49
  requestId?: string;
50
50
  }
51
51
 
52
+ type AskKey = PendingAskKey;
53
+ type AskIdentity = PendingAskIdentity;
54
+
52
55
  function pendingAskDir(userId: PendingAskUserId): string {
53
56
  const rawUserId = String(userId);
54
- const safeUserId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId)
55
- ? rawUserId
56
- : `sha256-${createHash("sha256").update(rawUserId).digest("hex")}`;
57
+ const safeUserId =
58
+ /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId) && !rawUserId.includes("..")
59
+ ? rawUserId
60
+ : `sha256-${createHash("sha256").update(rawUserId).digest("hex")}`;
57
61
  return join(resolveStorageSessionAsksDir(), safeUserId);
58
62
  }
59
63
 
@@ -135,6 +139,10 @@ function readPendingAskFile(path: string, fallback: AskKey): PendingAskRecord |
135
139
  }
136
140
  const parsed = JSON.parse(raw) as Partial<PendingAskRecord>;
137
141
  if (!parsed.requestId || !parsed.from || !parsed.to) return null;
142
+ if (parsed.userId !== undefined && String(parsed.userId) !== String(fallback.userId))
143
+ return null;
144
+ if (fallback.from && parsed.from !== fallback.from) return null;
145
+ if (fallback.to && parsed.to !== fallback.to) return null;
138
146
  const now = new Date().toISOString();
139
147
  return {
140
148
  // The directory selected by the caller is authoritative. Never let a