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.
- package/README.md +11 -2
- package/dist/canonical-mcp-bridge.js +2 -3
- package/dist/canonical-mcp-bridge.js.map +1 -1
- package/dist/{chunk-gz12qcf0.js → chunk-a9y2rwxf.js} +2 -2
- package/dist/{chunk-gz12qcf0.js.map → chunk-a9y2rwxf.js.map} +3 -3
- package/dist/{chunk-j0kjscb7.js → chunk-s2gez3wg.js} +20 -2
- package/dist/{chunk-j0kjscb7.js.map → chunk-s2gez3wg.js.map} +2 -2
- package/dist/cron.js +292 -256
- package/dist/cron.js.map +23 -22
- package/dist/hosted-agent.js +3012 -17
- package/dist/hosted-agent.js.map +30 -3
- package/dist/main.js +31323 -36
- package/dist/main.js.map +198 -3
- package/dist/prompts.js +12 -9
- package/dist/prompts.js.map +5 -5
- package/dist/registry.js +10 -6
- package/dist/registry.js.map +3 -3
- package/dist/rollout.js +1 -1
- package/dist/runtime/cron/store.ts +24 -1
- package/dist/runtime/src/application/submit-user-message.ts +3 -0
- package/dist/runtime/src/index.ts +7 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +3 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
- package/dist/runtime/src/platform/config.ts +2 -2
- package/dist/runtime/src/platform/mcp-config.ts +13 -0
- package/dist/runtime/src/prompts/builders.ts +20 -6
- package/dist/runtime/src/query/active-rooms.ts +2 -0
- package/dist/runtime/src/runtime/delivery-ack.ts +112 -0
- package/dist/runtime/src/runtime/errors.ts +12 -12
- package/dist/runtime/src/runtime/turn-runner.ts +12 -0
- package/dist/runtime/src/storage/api-messages.ts +7 -5
- package/dist/runtime/src/storage/api-topics.ts +5 -2
- package/dist/runtime/src/storage/forum-db.ts +4 -1
- package/dist/runtime/src/storage/runtime-turn-requests.ts +1 -0
- package/dist/runtime/src/storage/session-asks.ts +13 -5
- package/dist/runtime/src/storage/storage-contract.ts +54 -0
- package/dist/runtime/src/storage/storage-host.ts +89 -36
- package/dist/runtime/src/storage/storage-public-host.ts +16 -0
- package/dist/runtime/src/storage/storage-public.ts +21 -5
- package/dist/runtime/src/storage/token-stats.ts +1 -1
- package/dist/runtime/src/types/api.ts +2 -0
- package/dist/runtime/src/types.ts +2 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +29 -18
- package/dist/runtime-helpers.js.map +8 -7
- package/dist/storage.js +260 -202
- package/dist/storage.js.map +11 -9
- package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +2 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +4 -0
- package/dist/types/packages/core/src/prompts/builders.d.ts +2 -0
- package/dist/types/packages/core/src/storage/api-messages.d.ts +6 -6
- package/dist/types/packages/core/src/storage/api-topics.d.ts +20 -2
- package/dist/types/packages/core/src/storage/forum-db.d.ts +2 -1
- package/dist/types/packages/core/src/storage/session-asks.d.ts +3 -2
- package/dist/types/packages/core/src/storage/storage-contract.d.ts +46 -0
- package/dist/types/packages/core/src/storage/storage-host.d.ts +12 -19
- package/dist/types/packages/core/src/storage/storage-public-host.d.ts +7 -0
- package/dist/types/packages/core/src/storage/storage-public.d.ts +4 -2
- package/dist/types/packages/core/src/types/api.d.ts +2 -0
- package/dist/types/packages/core/src/types.d.ts +2 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/types/packages/module-cron/src/store.d.ts +22 -2
- package/dist/vault.js +2 -2
- package/dist/vault.js.map +3 -3
- package/package.json +1 -1
- package/dist/chunk-01xcc856.js +0 -149
- package/dist/chunk-01xcc856.js.map +0 -10
- package/dist/chunk-1a6kv9vt.js +0 -118
- package/dist/chunk-1a6kv9vt.js.map +0 -10
- package/dist/chunk-1b62d730.js +0 -117
- package/dist/chunk-1b62d730.js.map +0 -10
- package/dist/chunk-1k7t7bnx.js +0 -97
- package/dist/chunk-1k7t7bnx.js.map +0 -10
- package/dist/chunk-28apmz4r.js +0 -431
- package/dist/chunk-28apmz4r.js.map +0 -11
- package/dist/chunk-2fvydfkx.js +0 -141
- package/dist/chunk-2fvydfkx.js.map +0 -10
- package/dist/chunk-41ygvg5k.js +0 -2387
- package/dist/chunk-41ygvg5k.js.map +0 -26
- package/dist/chunk-4q966ttx.js +0 -371
- package/dist/chunk-4q966ttx.js.map +0 -11
- package/dist/chunk-51rcsavw.js +0 -1712
- package/dist/chunk-51rcsavw.js.map +0 -18
- package/dist/chunk-59d0shyj.js +0 -103
- package/dist/chunk-59d0shyj.js.map +0 -10
- package/dist/chunk-5fzgvn79.js +0 -3536
- package/dist/chunk-5fzgvn79.js.map +0 -20
- package/dist/chunk-61px0717.js +0 -153
- package/dist/chunk-61px0717.js.map +0 -11
- package/dist/chunk-66y0hr8c.js +0 -2136
- package/dist/chunk-66y0hr8c.js.map +0 -29
- package/dist/chunk-69k45zg8.js +0 -250
- package/dist/chunk-69k45zg8.js.map +0 -11
- package/dist/chunk-6ah5farg.js +0 -130
- package/dist/chunk-6ah5farg.js.map +0 -10
- package/dist/chunk-6d4fmea5.js +0 -117
- package/dist/chunk-6d4fmea5.js.map +0 -12
- package/dist/chunk-8cahx7xc.js +0 -46
- package/dist/chunk-8cahx7xc.js.map +0 -10
- package/dist/chunk-8f20fk4n.js +0 -252
- package/dist/chunk-8f20fk4n.js.map +0 -10
- package/dist/chunk-91ckrvk8.js +0 -318
- package/dist/chunk-91ckrvk8.js.map +0 -10
- package/dist/chunk-93krf6ng.js +0 -3069
- package/dist/chunk-93krf6ng.js.map +0 -32
- package/dist/chunk-9qx1t77a.js +0 -214
- package/dist/chunk-9qx1t77a.js.map +0 -10
- package/dist/chunk-beqhdr9w.js +0 -589
- package/dist/chunk-beqhdr9w.js.map +0 -11
- package/dist/chunk-bftftsgq.js +0 -1637
- package/dist/chunk-bftftsgq.js.map +0 -15
- package/dist/chunk-c500xe9z.js +0 -33
- package/dist/chunk-c500xe9z.js.map +0 -10
- package/dist/chunk-cjsnkvq2.js +0 -23
- package/dist/chunk-cjsnkvq2.js.map +0 -10
- package/dist/chunk-cxa7yghg.js +0 -577
- package/dist/chunk-cxa7yghg.js.map +0 -14
- package/dist/chunk-edb07sjp.js +0 -567
- package/dist/chunk-edb07sjp.js.map +0 -14
- package/dist/chunk-f361dwba.js +0 -589
- package/dist/chunk-f361dwba.js.map +0 -11
- package/dist/chunk-f5xgprg1.js +0 -59
- package/dist/chunk-f5xgprg1.js.map +0 -10
- package/dist/chunk-ffefbd3r.js +0 -17
- package/dist/chunk-ffefbd3r.js.map +0 -10
- package/dist/chunk-fswhzdf5.js +0 -64
- package/dist/chunk-fswhzdf5.js.map +0 -10
- package/dist/chunk-g144wdrs.js +0 -2907
- package/dist/chunk-g144wdrs.js.map +0 -24
- package/dist/chunk-h980hb2n.js +0 -512
- package/dist/chunk-h980hb2n.js.map +0 -13
- package/dist/chunk-h9vbhhgr.js +0 -1337
- package/dist/chunk-h9vbhhgr.js.map +0 -16
- package/dist/chunk-hwy3n31w.js +0 -21
- package/dist/chunk-hwy3n31w.js.map +0 -9
- package/dist/chunk-jtrk0khr.js +0 -108
- package/dist/chunk-jtrk0khr.js.map +0 -10
- package/dist/chunk-jzz91jwh.js +0 -70
- package/dist/chunk-jzz91jwh.js.map +0 -10
- package/dist/chunk-m9e3tbr8.js +0 -708
- package/dist/chunk-m9e3tbr8.js.map +0 -14
- package/dist/chunk-mh5mexsf.js +0 -147
- package/dist/chunk-mh5mexsf.js.map +0 -11
- package/dist/chunk-npd8n03v.js +0 -2152
- package/dist/chunk-npd8n03v.js.map +0 -15
- package/dist/chunk-pg1azh6n.js +0 -2
- package/dist/chunk-pg1azh6n.js.map +0 -9
- package/dist/chunk-qwy57177.js +0 -237
- package/dist/chunk-qwy57177.js.map +0 -11
- package/dist/chunk-r31q21h5.js +0 -53
- package/dist/chunk-r31q21h5.js.map +0 -10
- package/dist/chunk-rbvctv1n.js +0 -366
- package/dist/chunk-rbvctv1n.js.map +0 -10
- package/dist/chunk-sa874j5c.js +0 -9
- package/dist/chunk-sa874j5c.js.map +0 -10
- package/dist/chunk-sctcs5zb.js +0 -281
- package/dist/chunk-sctcs5zb.js.map +0 -10
- package/dist/chunk-tsstrcfc.js +0 -122
- package/dist/chunk-tsstrcfc.js.map +0 -10
- package/dist/chunk-wk2qf31s.js +0 -58
- package/dist/chunk-wk2qf31s.js.map +0 -10
- package/dist/chunk-wx7ag4dd.js +0 -2936
- package/dist/chunk-wx7ag4dd.js.map +0 -20
- package/dist/chunk-znx1fpfd.js +0 -61
- package/dist/chunk-znx1fpfd.js.map +0 -10
package/dist/rollout.js
CHANGED
|
@@ -137,7 +137,30 @@ interface TableColumnRow {
|
|
|
137
137
|
|
|
138
138
|
let schemaReady = false;
|
|
139
139
|
|
|
140
|
-
export
|
|
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
|
|
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
|
|
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 :
|
|
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
|
-
}) +
|
|
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`
|
|
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}
|
|
34
|
+
if (ctor === "AuthenticationError") return `${name} authentication expired. ${hint}. (401)`;
|
|
35
35
|
if (ctor === "RateLimitError")
|
|
36
|
-
return `${name}
|
|
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}
|
|
41
|
-
: `${name}
|
|
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}
|
|
50
|
+
return `${name} authentication expired. ${hint}. (401)`;
|
|
51
51
|
}
|
|
52
52
|
if (/429|rate.limit/i.test(s))
|
|
53
|
-
return `${name}
|
|
53
|
+
return `${name} request limit exceeded. Please try again in a moment. (429)`;
|
|
54
54
|
if (/529|overloaded/i.test(s))
|
|
55
|
-
return `${name}
|
|
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}
|
|
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}
|
|
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
|
-
|
|
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
|
|
42
|
+
export interface PendingAskKey {
|
|
43
43
|
userId: PendingAskUserId;
|
|
44
44
|
from: string;
|
|
45
45
|
to: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
interface
|
|
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 =
|
|
55
|
-
|
|
56
|
-
|
|
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
|