negotium 0.1.7 → 0.1.9
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 +26 -2
- package/dist/canonical-mcp-bridge.js +14 -0
- package/dist/canonical-mcp-bridge.js.map +9 -0
- package/dist/chunk-10ct8814.js +429 -0
- package/dist/chunk-10ct8814.js.map +11 -0
- package/dist/chunk-18fzftjf.js +214 -0
- package/dist/chunk-18fzftjf.js.map +10 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-1msje2zf.js +23 -0
- package/dist/chunk-1msje2zf.js.map +10 -0
- package/dist/chunk-2dgkah1z.js +706 -0
- package/dist/chunk-2dgkah1z.js.map +14 -0
- package/dist/chunk-3t4dtm26.js +61 -0
- package/dist/chunk-3t4dtm26.js.map +10 -0
- package/dist/chunk-3vn2vnj1.js +51 -0
- package/dist/chunk-3vn2vnj1.js.map +10 -0
- package/dist/chunk-6hknphtc.js +2054 -0
- package/dist/chunk-6hknphtc.js.map +29 -0
- package/dist/chunk-6tfzpcvk.js +2149 -0
- package/dist/chunk-6tfzpcvk.js.map +15 -0
- package/dist/chunk-8g0c4as0.js +23 -0
- package/dist/chunk-8g0c4as0.js.map +10 -0
- package/dist/chunk-9spx3h9n.js +279 -0
- package/dist/chunk-9spx3h9n.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-e9b2xpff.js +147 -0
- package/dist/chunk-e9b2xpff.js.map +10 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-eexqsee7.js +368 -0
- package/dist/chunk-eexqsee7.js.map +11 -0
- package/dist/chunk-efyxk97v.js +500 -0
- package/dist/chunk-efyxk97v.js.map +13 -0
- package/dist/chunk-epqxwkvh.js +3534 -0
- package/dist/chunk-epqxwkvh.js.map +20 -0
- package/dist/chunk-f1pzh3xh.js +115 -0
- package/dist/chunk-f1pzh3xh.js.map +10 -0
- package/dist/chunk-f5xgprg1.js +59 -0
- package/dist/chunk-f5xgprg1.js.map +10 -0
- package/dist/chunk-f97qybwk.js +584 -0
- package/dist/chunk-f97qybwk.js.map +11 -0
- package/dist/chunk-fvc7ncxd.js +252 -0
- package/dist/chunk-fvc7ncxd.js.map +10 -0
- package/dist/chunk-g7pvfj5e.js +574 -0
- package/dist/chunk-g7pvfj5e.js.map +14 -0
- package/dist/chunk-h8vmb9gf.js +315 -0
- package/dist/chunk-h8vmb9gf.js.map +10 -0
- package/dist/chunk-hgw7n8cm.js +43 -0
- package/dist/chunk-hgw7n8cm.js.map +10 -0
- package/dist/chunk-hsq7j9jk.js +126 -0
- package/dist/chunk-hsq7j9jk.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j0kjscb7.js +53 -0
- package/dist/chunk-j0kjscb7.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-kbm35t7w.js +1331 -0
- package/dist/chunk-kbm35t7w.js.map +16 -0
- package/dist/chunk-m4veth0c.js +1649 -0
- package/dist/chunk-m4veth0c.js.map +17 -0
- package/dist/chunk-mhty3nna.js +150 -0
- package/dist/chunk-mhty3nna.js.map +11 -0
- package/dist/chunk-mmgh670x.js +248 -0
- package/dist/chunk-mmgh670x.js.map +11 -0
- package/dist/chunk-n0kk5xbq.js +2358 -0
- package/dist/chunk-n0kk5xbq.js.map +26 -0
- package/dist/chunk-nmv41qx9.js +2847 -0
- package/dist/chunk-nmv41qx9.js.map +23 -0
- package/dist/chunk-nqdp52aj.js +113 -0
- package/dist/chunk-nqdp52aj.js.map +12 -0
- package/dist/chunk-ns2m25dz.js +139 -0
- package/dist/chunk-ns2m25dz.js.map +10 -0
- package/dist/chunk-pkdmypdd.js +147 -0
- package/dist/chunk-pkdmypdd.js.map +11 -0
- package/dist/chunk-rmyfmrrr.js +1634 -0
- package/dist/chunk-rmyfmrrr.js.map +15 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sn51wnf1.js +3020 -0
- package/dist/chunk-sn51wnf1.js.map +32 -0
- package/dist/chunk-sqhr1a7e.js +587 -0
- package/dist/chunk-sqhr1a7e.js.map +11 -0
- package/dist/chunk-tmj5r2zv.js +2933 -0
- package/dist/chunk-tmj5r2zv.js.map +20 -0
- package/dist/chunk-vefj3880.js +56 -0
- package/dist/chunk-vefj3880.js.map +10 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-wry79q55.js +108 -0
- package/dist/chunk-wry79q55.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-z2xy5n2q.js +103 -0
- package/dist/chunk-z2xy5n2q.js.map +10 -0
- package/dist/hosted-agent.js +53 -0
- package/dist/hosted-agent.js.map +10 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +49 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +50 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +137 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +878 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +368 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +534 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2298 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +301 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +257 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +28 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { getRegistry } from "#agents/registry";
|
|
2
|
+
import { SESSION_WORKSPACE_DIR } from "#platform/config";
|
|
3
|
+
import { delay } from "#platform/delay";
|
|
4
|
+
import { logger } from "#platform/logger";
|
|
5
|
+
import { readConversation } from "#storage/conversations";
|
|
6
|
+
import { getTopicAgent } from "#storage/forum/agent-settings";
|
|
7
|
+
import { clearDmSessionId, clearSessionForTopic } from "#storage/forum/index";
|
|
8
|
+
import { type AskReplySource, markPendingAskSources } from "#storage/session-asks";
|
|
9
|
+
import { AbortReason, type HandleAgentQueryParams } from "./types";
|
|
10
|
+
|
|
11
|
+
export function topicQueryKey(userId: number, topicName: string): string {
|
|
12
|
+
return `${userId}:${topicName}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// --- Active queries (userId:topicName → abort control) ---
|
|
16
|
+
|
|
17
|
+
export interface ActiveQueryControl {
|
|
18
|
+
abortReason: AbortReason;
|
|
19
|
+
userId: number;
|
|
20
|
+
abortController: AbortController;
|
|
21
|
+
params?: HandleAgentQueryParams;
|
|
22
|
+
// Playwright instance this query is using. Set inside handleAgentQuery
|
|
23
|
+
// right after `ensurePlaywright` resolves so the playwright manager's
|
|
24
|
+
// crash callback can find which queries are tool-blocked on a dead MCP
|
|
25
|
+
// and abort them — without this the SDK waits indefinitely for a tool
|
|
26
|
+
// result the MCP will never deliver (observed 22-min stalls).
|
|
27
|
+
instanceKey?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const activeQueries = new Map<string, ActiveQueryControl>();
|
|
31
|
+
|
|
32
|
+
// --- Inter-session queue: defers inject requests while a query is running ---
|
|
33
|
+
|
|
34
|
+
class SessionInjectQueue {
|
|
35
|
+
private queue = new Map<string, HandleAgentQueryParams[]>();
|
|
36
|
+
|
|
37
|
+
/** Enqueue params if requestId is set and not a duplicate. Returns true if enqueued. */
|
|
38
|
+
enqueue(queryKey: string, params: HandleAgentQueryParams, logReason: string): boolean {
|
|
39
|
+
if (!params.requestId) {
|
|
40
|
+
// Missing requestId means the caller forgot to thread it through — this
|
|
41
|
+
// is a code bug, not a runtime condition. The inject will be silently
|
|
42
|
+
// dropped, so escalate to `error` so it surfaces in monitoring instead
|
|
43
|
+
// of hiding in warn-level noise.
|
|
44
|
+
logger.error(
|
|
45
|
+
{ queryKey, from: params.from, logReason },
|
|
46
|
+
"BUG: session inject reached enqueue without requestId — DROPPING inject",
|
|
47
|
+
);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const q = this.queue.get(queryKey) ?? [];
|
|
51
|
+
if (q.some((p) => p.requestId === params.requestId)) {
|
|
52
|
+
// Intentional dedup (e.g. retry-after-restart, replayed inbox entry).
|
|
53
|
+
// Surface it so operators can distinguish a legit dedup from a buggy
|
|
54
|
+
// collision when investigating a missing reply.
|
|
55
|
+
logger.info(
|
|
56
|
+
{ queryKey, requestId: params.requestId, from: params.from },
|
|
57
|
+
"session inject deduped (existing entry with same requestId)",
|
|
58
|
+
);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
q.push(params);
|
|
62
|
+
this.queue.set(queryKey, q);
|
|
63
|
+
markPendingAskSources({
|
|
64
|
+
userId: params.userId,
|
|
65
|
+
callerTopic: params.topicName,
|
|
66
|
+
sources: params.askReplySources,
|
|
67
|
+
state: "queued_for_caller",
|
|
68
|
+
});
|
|
69
|
+
logger.info({ queryKey, from: params.from, requestId: params.requestId }, logReason);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* True when an entry with this requestId is still queued under the key.
|
|
75
|
+
* Used by `handleAgentQuery`'s finally to detect the abort-requeue case:
|
|
76
|
+
* the dying invocation's params (and its forkHandle) were re-enqueued for a
|
|
77
|
+
* later dequeue, so resources tied to them must not be cleaned up yet.
|
|
78
|
+
*/
|
|
79
|
+
hasRequest(queryKey: string, requestId: string): boolean {
|
|
80
|
+
return this.queue.get(queryKey)?.some((p) => p.requestId === requestId) ?? false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Remove and return the next queued params for this key, or undefined if empty. */
|
|
84
|
+
dequeueNext(queryKey: string): HandleAgentQueryParams | undefined {
|
|
85
|
+
const q = this.queue.get(queryKey);
|
|
86
|
+
if (!q?.length) return undefined;
|
|
87
|
+
const next = q.shift()!;
|
|
88
|
+
if (q.length === 0) this.queue.delete(queryKey);
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove and return the longest mergeable PREFIX of the queue as a single
|
|
94
|
+
* entry, or undefined if empty. Multiple prompts are joined so they land in
|
|
95
|
+
* one turn instead of chaining N turns for N queued injects.
|
|
96
|
+
*
|
|
97
|
+
* Caller-bound ask_session replies (`askReplySources`) are safe to merge
|
|
98
|
+
* even when they came from different target topics: the merged turn is just
|
|
99
|
+
* more context for the same caller topic. Other inter-session work remains
|
|
100
|
+
* conservative: same `from`, same `silent` flag, same `sessionId`.
|
|
101
|
+
*/
|
|
102
|
+
dequeueAll(queryKey: string): HandleAgentQueryParams | undefined {
|
|
103
|
+
const q = this.queue.get(queryKey);
|
|
104
|
+
if (!q?.length) return undefined;
|
|
105
|
+
const base = q[0];
|
|
106
|
+
const isAskReplyInject = (p: HandleAgentQueryParams) =>
|
|
107
|
+
!p.silent && Boolean(p.askReplySources?.length);
|
|
108
|
+
const baseIsAskReplyInject = isAskReplyInject(base);
|
|
109
|
+
const mergeable = (p: HandleAgentQueryParams) =>
|
|
110
|
+
baseIsAskReplyInject
|
|
111
|
+
? isAskReplyInject(p)
|
|
112
|
+
: !isAskReplyInject(p) &&
|
|
113
|
+
p.from === base.from &&
|
|
114
|
+
(p.silent ?? false) === (base.silent ?? false) &&
|
|
115
|
+
p.sessionId === base.sessionId;
|
|
116
|
+
let take = 1;
|
|
117
|
+
while (take < q.length && mergeable(q[take])) take++;
|
|
118
|
+
const batch = q.splice(0, take);
|
|
119
|
+
if (q.length === 0) this.queue.delete(queryKey);
|
|
120
|
+
if (batch.length === 1) return batch[0];
|
|
121
|
+
const sources = batch.flatMap((p): AskReplySource[] => {
|
|
122
|
+
if (p.askReplySources?.length) return p.askReplySources;
|
|
123
|
+
if (!p.requestId || !p.from) return [];
|
|
124
|
+
return [{ from: p.from, requestId: p.requestId, contextId: p.contextId }];
|
|
125
|
+
});
|
|
126
|
+
const depths = batch
|
|
127
|
+
.map((p) => p.depth)
|
|
128
|
+
.filter((depth): depth is number => typeof depth === "number");
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
prompt: batch.map((p) => p.prompt).join("\n\n"),
|
|
132
|
+
from: baseIsAskReplyInject
|
|
133
|
+
? [...new Set(batch.map((p) => p.from).filter(Boolean))].join(", ")
|
|
134
|
+
: base.from,
|
|
135
|
+
depth: depths.length ? Math.min(...depths) : base.depth,
|
|
136
|
+
requestId: `merged-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
137
|
+
// Same-caller batches share one context; only drop it when the batch
|
|
138
|
+
// disagrees (a merged turn must not append to the wrong context).
|
|
139
|
+
contextId: batch.every((p) => p.contextId === base.contextId) ? base.contextId : undefined,
|
|
140
|
+
askReplySources: sources,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* All non-empty queue keys belonging to a user (queryKey starts with
|
|
146
|
+
* `${userId}:`). Used by `handleAgentQuery`'s finally to drain other
|
|
147
|
+
* topics' queued inter-session injects when the per-user concurrency cap
|
|
148
|
+
* frees up.
|
|
149
|
+
*/
|
|
150
|
+
keysForUser(userId: number): string[] {
|
|
151
|
+
const prefix = `${userId}:`;
|
|
152
|
+
const result: string[] = [];
|
|
153
|
+
for (const key of this.queue.keys()) {
|
|
154
|
+
if (key.startsWith(prefix)) result.push(key);
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const interSessionQueue = new SessionInjectQueue();
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Abort the in-flight query (if any) for `userId:topicName` and wait for
|
|
164
|
+
* `handleAgentQuery`'s finally block to clean up before returning.
|
|
165
|
+
*
|
|
166
|
+
* Used by topic lifecycle (delete/reset) so the dying query's tail writes
|
|
167
|
+
* (`setSessionForTopic`, `appendConversationEvent`, `recordUsage`) don't race
|
|
168
|
+
* against row removal — without this wait, those writes silently target a
|
|
169
|
+
* row that no longer exists, leaving orphan log files and zero-row UPDATEs.
|
|
170
|
+
*
|
|
171
|
+
* Returns true when the query exited cleanly (or none was running). Returns
|
|
172
|
+
* false on timeout — caller may still proceed but should treat the topic as
|
|
173
|
+
* potentially having stragglers.
|
|
174
|
+
*/
|
|
175
|
+
export async function abortAndWaitForQuery(
|
|
176
|
+
userId: number,
|
|
177
|
+
topicName: string,
|
|
178
|
+
reason: AbortReason = AbortReason.External,
|
|
179
|
+
timeoutMs = 5000,
|
|
180
|
+
): Promise<boolean> {
|
|
181
|
+
const queryKey = topicQueryKey(userId, topicName);
|
|
182
|
+
const running = activeQueries.get(queryKey);
|
|
183
|
+
if (!running) return true;
|
|
184
|
+
running.abortReason = reason;
|
|
185
|
+
running.abortController.abort();
|
|
186
|
+
const start = Date.now();
|
|
187
|
+
while (activeQueries.get(queryKey) === running) {
|
|
188
|
+
if (Date.now() - start > timeoutMs) {
|
|
189
|
+
logger.warn(
|
|
190
|
+
{ userId, topicName, timeoutMs },
|
|
191
|
+
"abortAndWaitForQuery: timeout — proceeding without confirmation",
|
|
192
|
+
);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
await delay(50);
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- Initializing topics: blocks message handling during /new reset ---
|
|
201
|
+
|
|
202
|
+
export const initializingTopics = new Set<string>();
|
|
203
|
+
|
|
204
|
+
// --- DM session retry guard: prevents duplicate retries on concurrent expiry ---
|
|
205
|
+
|
|
206
|
+
const dmRetryingUsers = new Set<number>();
|
|
207
|
+
export const deleteDmRetrying = (userId: number) => dmRetryingUsers.delete(userId);
|
|
208
|
+
|
|
209
|
+
// SDK does not expose a typed session-expiry error; match by message
|
|
210
|
+
const SESSION_EXPIRED_MSG = "No conversation found with session ID";
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Result of detecting (and acting on) a session-expiry error.
|
|
214
|
+
*
|
|
215
|
+
* - `{ type: "dm-retry" }`: DM expiry — caller retries with `sessionId: null`.
|
|
216
|
+
* - `{ type: "forum-retry", sessionId: null }`: forum expiry where reconstruct
|
|
217
|
+
* was not attempted or failed — caller retries with a FRESH session and the
|
|
218
|
+
* DB session_id has been cleared. Context is lost.
|
|
219
|
+
* - `{ type: "forum-retry", sessionId: "<id>" }`: forum expiry where the
|
|
220
|
+
* native SDK rollout has been REBUILT from the unified conversation log at
|
|
221
|
+
* the SAME path the SDK expects. Caller retries with the preserved
|
|
222
|
+
* sessionId. Full context (1500+ entries) is restored. DB session_id is
|
|
223
|
+
* left untouched.
|
|
224
|
+
*/
|
|
225
|
+
export type SessionExpiryResult =
|
|
226
|
+
| { type: "dm-retry" }
|
|
227
|
+
| { type: "forum-retry"; sessionId: string | null };
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Try to rebuild the native SDK rollout file for the topic from the unified
|
|
231
|
+
* conversation log. Returns true on success — the rollout is written at the
|
|
232
|
+
* exact path the SDK looks for and the DB sessionId remains valid.
|
|
233
|
+
*
|
|
234
|
+
* Falls through to `false` silently when there are no entries to encode or
|
|
235
|
+
* the registry's `writeRollout` throws (malformed entries, disk error, etc.)
|
|
236
|
+
* so the caller can fall back to a fresh-session retry.
|
|
237
|
+
*/
|
|
238
|
+
function tryReconstructForumRollout(
|
|
239
|
+
params: HandleAgentQueryParams,
|
|
240
|
+
userId: number,
|
|
241
|
+
topicName: string,
|
|
242
|
+
): boolean {
|
|
243
|
+
const sessionId = params.sessionId;
|
|
244
|
+
if (!sessionId) return false;
|
|
245
|
+
try {
|
|
246
|
+
const entries = readConversation(userId, topicName);
|
|
247
|
+
if (entries.length === 0) return false;
|
|
248
|
+
const cwd = SESSION_WORKSPACE_DIR;
|
|
249
|
+
const agent = getTopicAgent(userId, topicName);
|
|
250
|
+
const result = getRegistry(agent).writeRollout({
|
|
251
|
+
cwd,
|
|
252
|
+
entries,
|
|
253
|
+
reuseSessionId: sessionId,
|
|
254
|
+
...(params.model ? { model: params.model } : {}),
|
|
255
|
+
...(params.effort ? { effort: params.effort } : {}),
|
|
256
|
+
});
|
|
257
|
+
logger.info(
|
|
258
|
+
{
|
|
259
|
+
userId,
|
|
260
|
+
topicName,
|
|
261
|
+
agent,
|
|
262
|
+
sessionId: result.sessionId,
|
|
263
|
+
rolloutPath: result.rolloutPath,
|
|
264
|
+
entries: entries.length,
|
|
265
|
+
},
|
|
266
|
+
"Forum session expired — rollout reconstructed from unified log, sessionId preserved",
|
|
267
|
+
);
|
|
268
|
+
return true;
|
|
269
|
+
} catch (err) {
|
|
270
|
+
logger.warn(
|
|
271
|
+
{ err, userId, topicName, sessionId },
|
|
272
|
+
"Forum session reconstruct failed — falling back to fresh session",
|
|
273
|
+
);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Detects a session-expiry error and either rebuilds the rollout (preferred,
|
|
280
|
+
* keeps context) or clears the stale session (fallback, loses context).
|
|
281
|
+
* Returns the retry result, or null if not an expiry (or already retried once).
|
|
282
|
+
*/
|
|
283
|
+
export function detectSessionExpiry(
|
|
284
|
+
params: HandleAgentQueryParams,
|
|
285
|
+
errMsg: string,
|
|
286
|
+
userId: number,
|
|
287
|
+
topicName: string,
|
|
288
|
+
): SessionExpiryResult | null {
|
|
289
|
+
if (params._sessionRetried || !errMsg.includes(SESSION_EXPIRED_MSG)) return null;
|
|
290
|
+
|
|
291
|
+
if (params.sessionType === "dm") {
|
|
292
|
+
if (!dmRetryingUsers.has(userId)) {
|
|
293
|
+
clearDmSessionId(userId);
|
|
294
|
+
dmRetryingUsers.add(userId);
|
|
295
|
+
logger.info({ userId }, "DM session expired, cleared and will retry");
|
|
296
|
+
return { type: "dm-retry" };
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Forum path: try to rebuild the rollout from the unified log so the SDK
|
|
302
|
+
// can resume on the SAME sessionId with full context. Only clear when that
|
|
303
|
+
// path is unavailable (no entries, encoder error, missing sessionId).
|
|
304
|
+
if (tryReconstructForumRollout(params, userId, topicName)) {
|
|
305
|
+
return { type: "forum-retry", sessionId: params.sessionId ?? null };
|
|
306
|
+
}
|
|
307
|
+
clearSessionForTopic(userId, topicName);
|
|
308
|
+
logger.info(
|
|
309
|
+
{ userId, topicName },
|
|
310
|
+
"Forum session expired, cleared and will retry with fresh session",
|
|
311
|
+
);
|
|
312
|
+
return { type: "forum-retry", sessionId: null };
|
|
313
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Live query-control exports.
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
DeferredInject,
|
|
5
|
+
NewQueryDecision,
|
|
6
|
+
RoomQueryControl,
|
|
7
|
+
} from "./active-rooms";
|
|
8
|
+
export {
|
|
9
|
+
abortAllRooms,
|
|
10
|
+
abortRoom,
|
|
11
|
+
cancelDeferredInject,
|
|
12
|
+
clearRoomQuery,
|
|
13
|
+
decideNewQuery,
|
|
14
|
+
deferInject,
|
|
15
|
+
getRoomQuery,
|
|
16
|
+
InterSessionQueue,
|
|
17
|
+
interSessionQueue,
|
|
18
|
+
isUserOrigin,
|
|
19
|
+
setRoomQuery,
|
|
20
|
+
takeDeferredInject,
|
|
21
|
+
wsAbortReason,
|
|
22
|
+
} from "./active-rooms";
|
|
23
|
+
export type { HandleAgentQueryOutcome, HandleAgentQueryParams } from "./types";
|
|
24
|
+
export { AbortReason } from "./types";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { unlinkSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { SESSION_INBOX_DIR } from "#platform/config";
|
|
4
|
+
import { logger } from "#platform/logger";
|
|
5
|
+
import { scheduledSessionInboxPath, sessionInboxPath } from "#query/session-inbox-path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Delete pending, scheduled, and crash-claimed inbox files for a topic.
|
|
9
|
+
* Canonical files are id-keyed; title-keyed candidates cover rolling upgrades.
|
|
10
|
+
*/
|
|
11
|
+
export function cleanupSessionInboxFiles(
|
|
12
|
+
userId: string,
|
|
13
|
+
topicId: string,
|
|
14
|
+
legacyTopicTitle?: string,
|
|
15
|
+
): number {
|
|
16
|
+
const live = sessionInboxPath(userId, topicId);
|
|
17
|
+
const scheduled = scheduledSessionInboxPath(userId, topicId);
|
|
18
|
+
const candidates = new Set([live, `${live}.processing`, scheduled, `${scheduled}.processing`]);
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
legacyTopicTitle &&
|
|
22
|
+
legacyTopicTitle !== "." &&
|
|
23
|
+
legacyTopicTitle !== ".." &&
|
|
24
|
+
basename(legacyTopicTitle) === legacyTopicTitle
|
|
25
|
+
) {
|
|
26
|
+
const legacyBase = join(SESSION_INBOX_DIR, userId, legacyTopicTitle);
|
|
27
|
+
for (const suffix of [".jsonl", ".jsonl.processing", ".schedule", ".schedule.processing"]) {
|
|
28
|
+
candidates.add(`${legacyBase}${suffix}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let deleted = 0;
|
|
33
|
+
for (const path of candidates) {
|
|
34
|
+
try {
|
|
35
|
+
unlinkSync(path);
|
|
36
|
+
deleted++;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
39
|
+
logger.warn({ err, path, topicId, userId }, "topic cleanup: inbox unlink failed");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return deleted;
|
|
44
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { SESSION_INBOX_DIR } from "#platform/config";
|
|
3
|
+
|
|
4
|
+
const TOPIC_ID_FILE_PREFIX = "topic-id-";
|
|
5
|
+
const JSONL_SUFFIX = ".jsonl";
|
|
6
|
+
const SCHEDULE_SUFFIX = ".schedule";
|
|
7
|
+
|
|
8
|
+
/** Build a flat inbox path from the canonical topic id, never its title. */
|
|
9
|
+
export function sessionInboxPath(userId: string, topicId: string): string {
|
|
10
|
+
const key = Buffer.from(topicId, "utf8").toString("base64url");
|
|
11
|
+
return join(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Durable sidecar consumed by the session-inbox worker once deliverAt passes. */
|
|
15
|
+
export function scheduledSessionInboxPath(userId: string, topicId: string): string {
|
|
16
|
+
const key = Buffer.from(topicId, "utf8").toString("base64url");
|
|
17
|
+
return join(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function decodeTopicIdFileName(fileName: string, suffix: string): string | null {
|
|
21
|
+
if (!fileName.startsWith(TOPIC_ID_FILE_PREFIX) || !fileName.endsWith(suffix)) return null;
|
|
22
|
+
const encoded = fileName.slice(TOPIC_ID_FILE_PREFIX.length, -suffix.length);
|
|
23
|
+
if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded)) return null;
|
|
24
|
+
try {
|
|
25
|
+
return Buffer.from(encoded, "base64url").toString("utf8") || null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Null denotes a legacy `{topicTitle}.jsonl` filename from an older runtime. */
|
|
32
|
+
export function topicIdFromSessionInboxFileName(fileName: string): string | null {
|
|
33
|
+
return decodeTopicIdFileName(fileName, JSONL_SUFFIX);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Accept both a live .schedule file and a crash-residue .schedule.processing claim. */
|
|
37
|
+
export function topicIdFromScheduledSessionInboxFileName(fileName: string): string | null {
|
|
38
|
+
const base = fileName.endsWith(".processing")
|
|
39
|
+
? fileName.slice(0, -".processing".length)
|
|
40
|
+
: fileName;
|
|
41
|
+
return decodeTopicIdFileName(base, SCHEDULE_SUFFIX);
|
|
42
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ACTIVE_QUERY_STALE_MS, USERS_LOG_DIR } from "#platform/config";
|
|
4
|
+
import { readJsonFile } from "#platform/jsonl";
|
|
5
|
+
import { logger } from "#platform/logger";
|
|
6
|
+
import type { QueryState } from "#types";
|
|
7
|
+
|
|
8
|
+
type QueryStateUserId = number | string;
|
|
9
|
+
|
|
10
|
+
function queryStateDirPath(userId: QueryStateUserId): string {
|
|
11
|
+
return join(USERS_LOG_DIR, String(userId), "active-queries");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function queryStateFile(userId: QueryStateUserId, topicName: string): string {
|
|
15
|
+
const name = `${topicName}.json`;
|
|
16
|
+
return join(queryStateDirPath(userId), name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function writeQueryState(userId: QueryStateUserId, topicName: string, task?: string) {
|
|
20
|
+
const dir = queryStateDirPath(userId);
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
const state: QueryState = { since: new Date().toISOString() };
|
|
23
|
+
if (task) state.task = [...task.replace(/\n+/g, " ").trim()].slice(0, 100).join("");
|
|
24
|
+
const target = queryStateFile(userId, topicName);
|
|
25
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
26
|
+
writeFileSync(tmp, JSON.stringify(state));
|
|
27
|
+
renameSync(tmp, target);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function clearQueryState(userId: QueryStateUserId, topicName: string) {
|
|
31
|
+
try {
|
|
32
|
+
unlinkSync(queryStateFile(userId, topicName));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if ((e as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
35
|
+
logger.warn({ err: e, userId, topicName }, "Failed to clear query state file");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function cleanStaleQueryStates(allowedUserIds: Set<number>) {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
try {
|
|
43
|
+
const userDirs = readdirSync(USERS_LOG_DIR, { withFileTypes: true });
|
|
44
|
+
for (const dir of userDirs) {
|
|
45
|
+
if (!dir.isDirectory()) continue;
|
|
46
|
+
const userId = Number(dir.name);
|
|
47
|
+
if (!Number.isFinite(userId) || !allowedUserIds.has(userId)) continue;
|
|
48
|
+
const stateDir = join(USERS_LOG_DIR, dir.name, "active-queries");
|
|
49
|
+
let files: string[];
|
|
50
|
+
try {
|
|
51
|
+
files = readdirSync(stateDir);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
logger.warn({ err: e, stateDir }, "cleanStaleQueryStates: failed to read state dir");
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const filePath = join(stateDir, file);
|
|
58
|
+
try {
|
|
59
|
+
if (file.endsWith(".lock") || file.endsWith(".tmp")) {
|
|
60
|
+
try {
|
|
61
|
+
unlinkSync(filePath);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
logger.warn(
|
|
64
|
+
{ err: e, filePath },
|
|
65
|
+
"cleanStaleQueryStates: failed to remove legacy file",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
logger.info({ filePath }, "Removed legacy lock/tmp file");
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (file.endsWith(".json")) {
|
|
72
|
+
const state = readJsonFile<QueryState>(filePath);
|
|
73
|
+
const stale = !state || now - new Date(state.since).getTime() > ACTIVE_QUERY_STALE_MS;
|
|
74
|
+
if (stale) {
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(filePath);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
logger.warn(
|
|
79
|
+
{ err: e, filePath },
|
|
80
|
+
"cleanStaleQueryStates: failed to remove stale state file",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
logger.info({ filePath }, "Removed stale query state file");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
/* ignore individual file errors */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
logger.warn({ err: e }, "cleanStaleQueryStates: failed");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ForkHandle } from "#agents/fork";
|
|
2
|
+
import type { AskReplySource } from "#storage/session-asks";
|
|
3
|
+
import type { AgentKind, EffortLevel } from "#types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What `handleAgentQuery` did with the request. Callers that own resources
|
|
7
|
+
* tied to the params (e.g. an ask-fork rollout) use this to decide cleanup:
|
|
8
|
+
* "ran"/"queued" transfer ownership to the query layer, "dropped" returns it.
|
|
9
|
+
*/
|
|
10
|
+
export type HandleAgentQueryOutcome = "ran" | "queued" | "dropped";
|
|
11
|
+
|
|
12
|
+
// --- Parameter interfaces ---
|
|
13
|
+
|
|
14
|
+
interface BaseQueryParams {
|
|
15
|
+
chatId: number;
|
|
16
|
+
userId: number;
|
|
17
|
+
topicName: string;
|
|
18
|
+
sessionId: string | null;
|
|
19
|
+
prompt: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OutputParams {
|
|
23
|
+
messageThreadId?: number;
|
|
24
|
+
systemPrompt: string;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
sessionType?: "dm" | "forum" | "ephemeral" | "manager" | "cron";
|
|
27
|
+
model?: string;
|
|
28
|
+
silent?: boolean;
|
|
29
|
+
effort?: EffortLevel;
|
|
30
|
+
/** Override the topic-resolved agent backend. Used by callers like the
|
|
31
|
+
* archiver that run on ephemeral topics and need an explicit provider. */
|
|
32
|
+
agent?: AgentKind;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface InterSessionParams {
|
|
36
|
+
from?: string; // "user" or sender session name
|
|
37
|
+
depth?: number; // 0 = from user, 1+ = via tell_session
|
|
38
|
+
// For silent ask-reply forks: caller's depth at ask time. When the fork's
|
|
39
|
+
// reply is injected back to the caller, the caller resumes at this depth
|
|
40
|
+
// so tell chain caps remain accurate across ask hops.
|
|
41
|
+
fromDepth?: number;
|
|
42
|
+
requestId?: string; // unique ID for dedup in interSessionQueue
|
|
43
|
+
contextId?: string; // context ID for multi-turn inter-session conversations
|
|
44
|
+
/**
|
|
45
|
+
* Ask-fork rollout backing this inject's `sessionId`. Ownership travels
|
|
46
|
+
* WITH the params: while the inject sits in `interSessionQueue` the rollout
|
|
47
|
+
* must stay on disk (deleting it breaks the dequeued query's resume), and
|
|
48
|
+
* `handleAgentQuery`'s finally removes it once the consuming query actually
|
|
49
|
+
* finished (and no retry/requeue still references it).
|
|
50
|
+
*/
|
|
51
|
+
forkHandle?: ForkHandle;
|
|
52
|
+
/** Original ask_session replies represented by this inject, preserved across dequeueAll merges. */
|
|
53
|
+
askReplySources?: AskReplySource[];
|
|
54
|
+
agents?: Record<
|
|
55
|
+
string,
|
|
56
|
+
{
|
|
57
|
+
description: string;
|
|
58
|
+
prompt: string;
|
|
59
|
+
model?: string;
|
|
60
|
+
tools?: string[];
|
|
61
|
+
maxTurns?: number;
|
|
62
|
+
effort?: EffortLevel | number;
|
|
63
|
+
}
|
|
64
|
+
>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface RetryGuardParams {
|
|
68
|
+
_sessionRetried?: boolean; // guard against infinite retry on session expiry
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface HandleAgentQueryParams
|
|
72
|
+
extends BaseQueryParams,
|
|
73
|
+
OutputParams,
|
|
74
|
+
InterSessionParams,
|
|
75
|
+
RetryGuardParams {}
|
|
76
|
+
|
|
77
|
+
// --- Abort reason ---
|
|
78
|
+
|
|
79
|
+
export enum AbortReason {
|
|
80
|
+
None = "none",
|
|
81
|
+
Internal = "internal", // replaced by a newer query on the same topic
|
|
82
|
+
External = "external", // /abort command or abort_session via session-inbox
|
|
83
|
+
}
|