@co0ontty/wand 4.100.0 → 4.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-team-runner.d.ts +1 -1
- package/dist/ai-team-runner.js +3 -3
- package/dist/build-info.json +3 -3
- package/dist/conversation-service.d.ts +11 -1
- package/dist/conversation-service.js +56 -9
- package/dist/conversation-sync-contract.d.ts +27 -0
- package/dist/conversation-sync-contract.js +23 -0
- package/dist/conversation-sync.d.ts +11 -0
- package/dist/conversation-sync.js +85 -0
- package/dist/conversation-types.d.ts +1 -0
- package/dist/server-conversation-routes.js +17 -0
- package/dist/server-session-context.js +1 -1
- package/dist/server-session-list-routes.js +2 -2
- package/dist/server.js +4 -1
- package/dist/session-registry.d.ts +6 -1
- package/dist/session-registry.js +29 -0
- package/dist/web-ui/content/ai-teams.js +11 -11
- package/dist/web-ui/content/module-manifest.json +1 -1
- package/dist/web-ui/content/modules/{5d4915f6e1661d23 → 08c7b047195f138d}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{621b2383696b9d7d → 09ef1bb1d195d727}/host.js +1 -1
- package/dist/web-ui/content/modules/{d1185edafb2a3fc6 → 119874145414688d}/shell-file-panel-drawer.js +1 -1
- package/dist/web-ui/content/modules/{e2263673e53a298c → 15bcc550ef9fd5b7}/host.js +1 -1
- package/dist/web-ui/content/modules/{b9b78d6b75945e66 → 1a7303b87763fd43}/employee-profile.js +1 -1
- package/dist/web-ui/content/modules/{bbe7c6f6eda87002 → 1d35739f634745f6}/task-board-host.js +1 -1
- package/dist/web-ui/content/modules/2099d37d57856cbd/brand-identity.js +1 -0
- package/dist/web-ui/content/modules/{bc56ae7841b21ca6 → 222f48948517167e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{51fc769946593cfa → 2981483671797b92}/chunk.js +1 -1
- package/dist/web-ui/content/modules/2d6339b2f6c2e2d0/host.js +1 -0
- package/dist/web-ui/content/modules/{a04036215e751a32 → 2edc19639ac1bd4b}/chunk.js +1 -1
- package/dist/web-ui/content/modules/2f88fc37c6105f7e/sidebar-peek.js +1 -0
- package/dist/web-ui/content/modules/31124fb7ee6939df/host.js +1 -0
- package/dist/web-ui/content/modules/31b28c5d0019c363/host-bridge.js +1 -0
- package/dist/web-ui/content/modules/{784eef1d52221e7d → 321bd9c755bc30fc}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{e7780cd2afeb29d9 → 32c461164a1b364c}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{3b975071451386a4 → 35a8fd2cfd31d7a0}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{ca37cc312813dd4f → 3601736ae2d678ef}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{dfb9876d670c5365 → 36bd608366c56b72}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{a919e64032fe3acc → 36ce8123a2079807}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{371e120f3773f783 → 36e5c3a6d6705358}/employee-card.js +1 -1
- package/dist/web-ui/content/modules/3700d99c54d3c400/host.js +1 -0
- package/dist/web-ui/content/modules/{3d0ec18974ad2b11 → 38917c19e2cf1702}/chunk.js +1 -1
- package/dist/web-ui/content/modules/3ae29086b227f9d9/markdown.js +1 -0
- package/dist/web-ui/content/modules/{07b7b195f2e59a16 → 42c439d3acfe22bd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{258f0487e8621dcc → 42db858e4417c702}/chunk.js +1 -1
- package/dist/web-ui/content/modules/443dc81eac29f233/host.js +1 -0
- package/dist/web-ui/content/modules/{48b00eccb3067884 → 456634525d44777d}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{96f4a041d5cf046b → 47419445e0aaa3cc}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{2409b362681d91d6 → 476ff7911bce1a38}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{950a2f28bc128f94 → 4a9a3e1f87b2c32b}/dialog-modal.js +1 -1
- package/dist/web-ui/content/modules/{405a98ab11edb2c1 → 4af10e1f8a789204}/runtime.js +54 -54
- package/dist/web-ui/content/modules/{0cb64ca968a6d25d → 4b9deeeb77570db3}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{280457daf6b6fe61 → 4d4870f20eb1c52d}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{3d050c621f552e20 → 4f30a913cc5b3f89}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{a1e9344b3fbd11ae → 5395fa57a47e9b96}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{0a04baa27e4fc9e3 → 544dc812abdfbbb8}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{9638c7ba97db8755 → 5513bcbacdf44588}/notice-view.js +1 -1
- package/dist/web-ui/content/modules/{b3c8d461cf58ea32 → 55b75ebcf6924001}/host.js +1 -1
- package/dist/web-ui/content/modules/{9a12d5f9e5fef729 → 58836e616e6d12ff}/workspaces-panel.js +1 -1
- package/dist/web-ui/content/modules/{f555ceb3720822c5 → 5c94fb2b7abcd1a5}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{a763fe780816708f → 5df1b522f5d3c99d}/host.js +1 -1
- package/dist/web-ui/content/modules/{73a96d56bc1484de → 622a8d12c4c622bf}/host.js +1 -1
- package/dist/web-ui/content/modules/626c7ce0aa1714b0/host.js +1 -0
- package/dist/web-ui/content/modules/{b0b078bcfc9085a6 → 665e81fe210f1b7a}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{26b6ff004618750f → 6f341fe3e8aee844}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{3d1f076ddfc09771 → 72b13a7486b6d437}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{a9114b269e25b3f5 → 7375574a2ea42740}/chunk.js +1 -1
- package/dist/web-ui/content/modules/76adde4019731df1/chunk.js +1 -0
- package/dist/web-ui/content/modules/{8676143ae232d8b6 → 78a46adbf9807f89}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{0bd3cebfdbc60d19 → 7925b65867b7aa4a}/queue-view-adapter.js +1 -1
- package/dist/web-ui/content/modules/{2837772e09c2b62d → 8198bfd269c0caca}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{ea7cd328cb5cf7e0 → 82e40fdf0abfb8cd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{5e50a3c3c546d824 → 84b79a33c64e146c}/chunk.js +1 -1
- package/dist/web-ui/content/modules/8a4f0ef2c0812af6/chunk.js +4 -0
- package/dist/web-ui/content/modules/{8c4258e7c35e9eca → 8a6844a4251a1271}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{da8314e267c164ed → 8bd214570df4a7a0}/host.js +1 -1
- package/dist/web-ui/content/modules/{29c912ef59696172 → 8bff85e108320aab}/panel.js +1 -1
- package/dist/web-ui/content/modules/{5444c4f9260fcb2f → 8ce71665ae55ccf1}/toast-region.js +1 -1
- package/dist/web-ui/content/modules/{ad896e0a52316ead → 8e29056fb56439f7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{d0a0f9214f3bbf71 → 905f8bc3f8a3efb8}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f248ee3b31df4034 → 96dd661559b67ebf}/chunk.js +1 -1
- package/dist/web-ui/content/modules/988e01b67e6f1883/composer-surface-adapter.js +1 -0
- package/dist/web-ui/content/modules/{c4705a6af423d824 → 98c14fc0474455a5}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{a2263c03ce67440a → 9d48d1ea3c9228bd}/library-bridge.js +1 -1
- package/dist/web-ui/content/modules/{23eea6861975478b → 9e754b01fa4207fb}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{ddcaaa62f1b7f049 → 9f33acc1c4284e03}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{b9445d806edca559 → a098c7b1aa4e2f6b}/sha256.js +1 -1
- package/dist/web-ui/content/modules/{abe3642ec4dba601 → a4af270960a39ea6}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{91f4ec7a3631129c → a4dca2ffaf656d3c}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{97faa837aeaaff64 → a975ff38c3d43ba8}/workspace-window.js +1 -1
- package/dist/web-ui/content/modules/{2575e08b75b1a192 → aab92f2990a7185f}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{e8f9f6d46ef4bb53 → acb21c7e7fd6cb67}/page.js +1 -1
- package/dist/web-ui/content/modules/{23178d02e631d065 → ad1136919a65fc6e}/host.js +1 -1
- package/dist/web-ui/content/modules/{007197d061c2a565 → b0254a2d3652a925}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4ca8f7b400b83127 → b0c4eed46c1b6ad4}/chat-render.js +1 -1
- package/dist/web-ui/content/modules/{7aab1455fc7348e1 → b1d43ed06a5d1a57}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4718bec8ce29956e → b38924f45bfa71d6}/chunk.js +1 -1
- package/dist/web-ui/content/modules/b550260fc9c5363a/dropdown-surface.js +1 -0
- package/dist/web-ui/content/modules/{c2e0b78e4a6b1a18 → b62294e53dcb3147}/host.js +1 -1
- package/dist/web-ui/content/modules/{9a4f713719354002 → b937f1a939618be9}/host.js +1 -1
- package/dist/web-ui/content/modules/{65e98a2bcd16cd79 → bce467c603299460}/chunk.js +1 -1
- package/dist/web-ui/content/modules/c138b442c1682189/panel-host.js +1 -0
- package/dist/web-ui/content/modules/{93b428761a02ca66 → c152782274c04fad}/chunk.js +1 -1
- package/dist/web-ui/content/modules/c32d14632487364f/group-editor.js +1 -0
- package/dist/web-ui/content/modules/{0b00a661913662d4 → c422f47fd13dc4bc}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{0500b57b86317246 → c6d0a1b2753a4883}/host.js +1 -1
- package/dist/web-ui/content/modules/{28795c8bb7f4e2e8 → c72c4f1d47c3726d}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{2aa0bc0eba53a5c9 → c81a3d95b14cdcd4}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{0128a9a2c361da59 → cd22aa55f6cc5276}/chunk.js +1 -1
- package/dist/web-ui/content/modules/cdff6e7f72879c88/progress.js +1 -0
- package/dist/web-ui/content/modules/{0206d2425a3eb375 → cf715621edb1d12e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/cfee7b0d79587d59/home.js +6 -0
- package/dist/web-ui/content/modules/{82f9fed7fd7ffd9f → e33f6d66a02ec26c}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{fd2a62ffb5f81f09 → e5e93fe5304838a1}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{cdb1909af2179a41 → e61136c22675ec7d}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f41d42a01dc09373 → e95b60ab18a79499}/login-visual.js +1 -1
- package/dist/web-ui/content/modules/{a120182be2ef9632 → eecaebc5482ee203}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{b0d4d1f8ff0fae38 → eece3c2d2374e8f7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4dd52ad71d002eab → ef7f76b551fdc619}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{45e8d613816cdec5 → f55031a02775c89e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{9a0704236531e38e → f6f86d43c12b0864}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{37767c3c41b8d8a8 → f90f80d7207dff1f}/host.js +1 -1
- package/dist/web-ui/content/modules/fbe35e7ddd7384fa/host.js +1 -0
- package/dist/web-ui/embedded-assets.d.ts +407 -407
- package/dist/web-ui/embedded-assets.js +4 -4
- package/dist/ws-broadcast.d.ts +2 -1
- package/dist/ws-broadcast.js +1 -1
- package/package.json +1 -1
- package/dist/web-ui/content/modules/0261c50fee52b437/host.js +0 -1
- package/dist/web-ui/content/modules/05889845b99c95dd/host.js +0 -1
- package/dist/web-ui/content/modules/19a7b932484a64ca/host-bridge.js +0 -1
- package/dist/web-ui/content/modules/1f88294162059296/home.js +0 -6
- package/dist/web-ui/content/modules/2be38bfbed614f65/host.js +0 -1
- package/dist/web-ui/content/modules/360db1263b60f857/composer-surface-adapter.js +0 -1
- package/dist/web-ui/content/modules/364b10aceecf7ab9/panel-host.js +0 -1
- package/dist/web-ui/content/modules/5f4ea7ad706b19f7/progress.js +0 -1
- package/dist/web-ui/content/modules/71205ec0b1cf554d/host.js +0 -1
- package/dist/web-ui/content/modules/76b953fe1f34c421/dropdown-surface.js +0 -1
- package/dist/web-ui/content/modules/8492b5592532e563/chunk.js +0 -1
- package/dist/web-ui/content/modules/890cdc256b49e26c/host.js +0 -1
- package/dist/web-ui/content/modules/9d7b92a76b2d54ab/sidebar-peek.js +0 -1
- package/dist/web-ui/content/modules/a48f540d6a228012/chunk.js +0 -4
- package/dist/web-ui/content/modules/c8ee697d1dd6e302/group-editor.js +0 -1
- package/dist/web-ui/content/modules/d948ac122dc34acc/markdown.js +0 -1
- package/dist/web-ui/content/modules/edb31a89f667eddf/brand-identity.js +0 -1
- package/dist/web-ui/content/modules/f9111c80a2c26147/host.js +0 -1
package/dist/ai-team-runner.d.ts
CHANGED
|
@@ -189,7 +189,7 @@ export declare class AiTeamRunner {
|
|
|
189
189
|
chatInput(sessionId: string, text: string): Promise<void>;
|
|
190
190
|
/** Explicit task target; ordinary group talk never implicitly approves/stops/starts a run. */
|
|
191
191
|
inputForRun(runId: string, text: string): Promise<AiTeamRunDetail>;
|
|
192
|
-
detail(runId: string): AiTeamRunDetail;
|
|
192
|
+
detail(runId: string, relayTurns?: ConversationTurn[]): AiTeamRunDetail;
|
|
193
193
|
listForTask(taskId: string): AiTeamRun[];
|
|
194
194
|
/**
|
|
195
195
|
* 此刻正在干活的步骤的实时文本(§4.9)。只覆盖 running 且有会话的步骤,按 seq 升序;
|
package/dist/ai-team-runner.js
CHANGED
|
@@ -531,7 +531,7 @@ export class AiTeamRunner {
|
|
|
531
531
|
this.saveRun(current);
|
|
532
532
|
});
|
|
533
533
|
}
|
|
534
|
-
detail(runId) {
|
|
534
|
+
detail(runId, relayTurns) {
|
|
535
535
|
const run = this.storage.getAiTeamRun(runId);
|
|
536
536
|
if (!run)
|
|
537
537
|
throw new Error("团队运行不存在。");
|
|
@@ -547,8 +547,8 @@ export class AiTeamRunner {
|
|
|
547
547
|
// 群聊回合只给最近这一段(§4.4);没有群聊会话的旧运行给空数组,前端不用判 null。
|
|
548
548
|
// 有对话归属时 ConversationService 已投影用户署名;旧运行只回放 relay 快照,在这里补同一口径。
|
|
549
549
|
const self = this.selfAuthorOf;
|
|
550
|
-
const messages = (run.conversationId && this.chatHistoryTurns ? this.chatHistoryTurns(run.conversationId)
|
|
551
|
-
: run.chatSessionId ? this.ops.snapshot(run.chatSessionId)?.messages ?? [] : [])
|
|
550
|
+
const messages = (relayTurns ?? (run.conversationId && this.chatHistoryTurns ? this.chatHistoryTurns(run.conversationId)
|
|
551
|
+
: run.chatSessionId ? this.ops.snapshot(run.chatSessionId)?.messages ?? [] : []))
|
|
552
552
|
.map((turn) => (turn.role === "user" && !turn.author && self ? { ...turn, author: self() } : turn));
|
|
553
553
|
const currentTeam = this.storage.getAiTeam(run.teamId);
|
|
554
554
|
// A relay belongs to a conversation, not its newest task. Always use the run's task.
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-10-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "47bb7f819edbd778640501e0c80eeec6b6f3a8cd",
|
|
3
|
+
"builtAt": "2026-10-21T13:30:56.402Z",
|
|
4
|
+
"version": "4.101.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ConversationSyncQuery } from "./conversation-sync.js";
|
|
1
2
|
import { type AiTeamRun } from "./ai-team-types.js";
|
|
2
3
|
import { type AiTeamRunner } from "./ai-team-runner.js";
|
|
3
4
|
import { type ConversationDetail, type ConversationInstance, type ConversationReceipt, type ConversationSummary, type ConversationTarget, type ConversationSessionUpdate } from "./conversation-types.js";
|
|
@@ -37,11 +38,19 @@ export declare class ConversationService {
|
|
|
37
38
|
config: WandConfig;
|
|
38
39
|
deleteSession?(id: string): void;
|
|
39
40
|
notifySession?(update: ConversationSessionUpdate): void;
|
|
41
|
+
notifyChanged?(): void;
|
|
40
42
|
});
|
|
41
43
|
private readonly removals;
|
|
42
44
|
private readonly pendingOperations;
|
|
43
45
|
private readonly previewTimers;
|
|
44
46
|
private readonly legacyChannels;
|
|
47
|
+
private liveRevision;
|
|
48
|
+
private noticePending;
|
|
49
|
+
private changed;
|
|
50
|
+
private readSnapshots;
|
|
51
|
+
private readonly projections;
|
|
52
|
+
private readonly synchronizer;
|
|
53
|
+
sync(id: string, query: ConversationSyncQuery): import("./conversation-sync-contract.js").ConversationSync;
|
|
45
54
|
/** Reuse session events; send only a bounded IM projection, never a second execution protocol. */
|
|
46
55
|
ingestSessionEvent(event: ProcessEvent): void;
|
|
47
56
|
dispose(): void;
|
|
@@ -73,7 +82,8 @@ export declare class ConversationService {
|
|
|
73
82
|
remove(id: string): Promise<void>;
|
|
74
83
|
private removeOwnedResources;
|
|
75
84
|
history(id: string): ConversationTurn[];
|
|
76
|
-
detail(id: string): ConversationDetail;
|
|
85
|
+
detail(id: string, select?: (turns: ConversationTurn[]) => ConversationTurn[]): ConversationDetail;
|
|
86
|
+
private readDetail;
|
|
77
87
|
/** Read-only, bounded projection of the exact accepted run. Never follow a group's newest task. */
|
|
78
88
|
private taskPreview;
|
|
79
89
|
private groupTeam;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { ConversationSynchronizer } from "./conversation-sync.js";
|
|
2
3
|
import { statSync, rmSync, existsSync, realpathSync } from "node:fs";
|
|
3
4
|
import { isAbsolute, resolve, sep } from "node:path";
|
|
4
5
|
import { AI_TEAM_ACTIVE_RUN_STATUSES, AI_TEAM_DEFAULT_MAX_STEPS, AI_TEAM_MAX_MEMBERS, memberAgents } from "./ai-team-types.js";
|
|
@@ -56,8 +57,21 @@ export class ConversationService {
|
|
|
56
57
|
pendingOperations = new Map();
|
|
57
58
|
previewTimers = new Map();
|
|
58
59
|
legacyChannels = new Set();
|
|
60
|
+
liveRevision = 0;
|
|
61
|
+
noticePending = false;
|
|
62
|
+
changed() {
|
|
63
|
+
if (this.noticePending)
|
|
64
|
+
return;
|
|
65
|
+
this.noticePending = true;
|
|
66
|
+
queueMicrotask(() => { this.noticePending = false; this.deps.notifyChanged?.(); });
|
|
67
|
+
}
|
|
68
|
+
readSnapshots = null;
|
|
69
|
+
projections = new Map();
|
|
70
|
+
synchronizer = new ConversationSynchronizer((id, select) => this.detail(id, select));
|
|
71
|
+
sync(id, query) { return this.synchronizer.sync(id, query); }
|
|
59
72
|
/** Reuse session events; send only a bounded IM projection, never a second execution protocol. */
|
|
60
73
|
ingestSessionEvent(event) {
|
|
74
|
+
this.liveRevision++;
|
|
61
75
|
const session = this.deps.structured.get(event.sessionId);
|
|
62
76
|
if (session)
|
|
63
77
|
this.importEmployeeSession(session);
|
|
@@ -85,10 +99,17 @@ export class ConversationService {
|
|
|
85
99
|
for (const timer of this.previewTimers.values())
|
|
86
100
|
clearTimeout(timer);
|
|
87
101
|
this.previewTimers.clear();
|
|
102
|
+
this.projections.clear();
|
|
88
103
|
}
|
|
89
104
|
get storage() { return this.deps.storage; }
|
|
90
105
|
snapshot(id) {
|
|
91
|
-
|
|
106
|
+
if (!id)
|
|
107
|
+
return null;
|
|
108
|
+
if (this.readSnapshots?.has(id))
|
|
109
|
+
return this.readSnapshots.get(id);
|
|
110
|
+
const snapshot = this.deps.structured.get(id) ?? this.storage.getSession(id);
|
|
111
|
+
this.readSnapshots?.set(id, snapshot);
|
|
112
|
+
return snapshot;
|
|
92
113
|
}
|
|
93
114
|
/** Explicit startup/event migration, never a GET repair and never an invented group. */
|
|
94
115
|
importRun(run) {
|
|
@@ -308,6 +329,7 @@ export class ConversationService {
|
|
|
308
329
|
if (patch.dissolved !== undefined && instance.kind !== "group")
|
|
309
330
|
throw new Error("只有群聊可以解散或恢复。");
|
|
310
331
|
this.storage.updateConversationListState(id, { ...patch, dissolvedBy: this.selfName() });
|
|
332
|
+
this.changed();
|
|
311
333
|
return this.summary(instance);
|
|
312
334
|
}
|
|
313
335
|
/** Delete only this conversation's owned channels and, for groups, its linked task resources. */
|
|
@@ -318,7 +340,7 @@ export class ConversationService {
|
|
|
318
340
|
const removal = this.removeOwnedResources(id);
|
|
319
341
|
this.removals.set(id, removal);
|
|
320
342
|
void removal.finally(() => { if (this.removals.get(id) === removal)
|
|
321
|
-
this.removals.delete(id); }).catch(() => { });
|
|
343
|
+
this.removals.delete(id); this.changed(); }).catch(() => { });
|
|
322
344
|
return removal;
|
|
323
345
|
}
|
|
324
346
|
async removeOwnedResources(id) {
|
|
@@ -383,17 +405,39 @@ export class ConversationService {
|
|
|
383
405
|
this.storage.deleteConversation(id);
|
|
384
406
|
}
|
|
385
407
|
history(id) { return this.turns(this.requireInstance(id)); }
|
|
386
|
-
detail(id) {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
408
|
+
detail(id, select = turns => turns) {
|
|
409
|
+
const previous = this.readSnapshots;
|
|
410
|
+
this.readSnapshots = previous ?? new Map();
|
|
411
|
+
try {
|
|
412
|
+
return this.readDetail(id, select);
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
this.readSnapshots = previous;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
readDetail(id, select) {
|
|
419
|
+
const revision = `${this.storage.projectionRevision()}:${this.liveRevision}:${JSON.stringify(this.deps.config.userProfile ?? {})}`;
|
|
420
|
+
let projection = this.projections.get(id);
|
|
421
|
+
if (projection?.revision !== revision) {
|
|
422
|
+
const instance = this.requireInstance(id);
|
|
423
|
+
const turns = this.turns(instance);
|
|
424
|
+
const summary = this.summary(instance, turns);
|
|
425
|
+
projection = { revision, instance, turns, summary, bytes: Buffer.byteLength(JSON.stringify(turns)) };
|
|
426
|
+
this.projections.delete(id);
|
|
427
|
+
if (projection.bytes <= 8 * 1024 * 1024)
|
|
428
|
+
this.projections.set(id, projection);
|
|
429
|
+
while (this.projections.size > 12 || [...this.projections.values()].reduce((n, p) => n + p.bytes, 0) > 16 * 1024 * 1024) {
|
|
430
|
+
this.projections.delete(this.projections.keys().next().value);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const { summary, turns } = projection;
|
|
434
|
+
return { ...summary, messages: select(turns).map(turn => turn.sessionLink
|
|
391
435
|
? { ...turn, sessionPreview: conversationSessionPreview(this.snapshot(turn.sessionLink.sessionId), turn.requestId ? this.receipt(turn.requestId) ?? undefined : undefined),
|
|
392
436
|
// Older clients can still open the exact source session without understanding sessionLink.
|
|
393
437
|
author: turn.author ? { ...turn.author, sessionId: turn.sessionLink.sessionId } : turn.author,
|
|
394
438
|
content: turn.content.length ? turn.content : [{ type: "text", text: "消息已接收,打开对应会话查看实时回复。" }] }
|
|
395
439
|
: this.taskPreview(turn)),
|
|
396
|
-
runDetails: summary.tasks.flatMap((task) => task.runs.map((run) => this.deps.runner.detail(run.id))) };
|
|
440
|
+
runDetails: projection.runDetails ??= summary.tasks.flatMap((task) => task.runs.map((run) => this.deps.runner.detail(run.id, turns))) };
|
|
397
441
|
}
|
|
398
442
|
/** Read-only, bounded projection of the exact accepted run. Never follow a group's newest task. */
|
|
399
443
|
taskPreview(turn) {
|
|
@@ -497,7 +541,10 @@ export class ConversationService {
|
|
|
497
541
|
return existing.receipt; // Pending survives restart: never implicitly execute twice.
|
|
498
542
|
}
|
|
499
543
|
const receipt = { requestId, state: "pending", conversationId: id };
|
|
500
|
-
const commit = () =>
|
|
544
|
+
const commit = () => {
|
|
545
|
+
this.storage.saveConversationRequest({ id: requestId, fingerprint, receipt, updatedAt: now() });
|
|
546
|
+
this.changed();
|
|
547
|
+
};
|
|
501
548
|
commit();
|
|
502
549
|
try {
|
|
503
550
|
await operation(receipt, commit);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ConversationDetail } from "./conversation-types.js";
|
|
2
|
+
import type { ConversationTurn } from "./types.js";
|
|
3
|
+
export interface ConversationHistory {
|
|
4
|
+
revision: string;
|
|
5
|
+
from: string | null;
|
|
6
|
+
hasEarlier: boolean;
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ConversationSync {
|
|
10
|
+
id: string;
|
|
11
|
+
history: ConversationHistory;
|
|
12
|
+
unchanged?: boolean;
|
|
13
|
+
reset?: boolean;
|
|
14
|
+
detail?: Omit<ConversationDetail, "messages">;
|
|
15
|
+
messages?: ConversationTurn[];
|
|
16
|
+
messageOrder?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ConversationSyncQuery {
|
|
19
|
+
since?: string;
|
|
20
|
+
from?: string;
|
|
21
|
+
older?: boolean;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}
|
|
24
|
+
/** Shared client reducer. The server's order also removes deleted or moved rows. */
|
|
25
|
+
export declare function mergeConversationSync(previous: ConversationDetail | null, update: ConversationSync): ConversationDetail & {
|
|
26
|
+
history: ConversationHistory;
|
|
27
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Shared client reducer. The server's order also removes deleted or moved rows. */
|
|
2
|
+
export function mergeConversationSync(previous, update) {
|
|
3
|
+
if (previous && previous.id !== update.id)
|
|
4
|
+
previous = null;
|
|
5
|
+
if (update.unchanged) {
|
|
6
|
+
if (!previous)
|
|
7
|
+
throw new Error("缺少对话同步基线,请重新读取。");
|
|
8
|
+
return { ...previous, history: update.history };
|
|
9
|
+
}
|
|
10
|
+
const messages = new Map((update.reset ? [] : previous?.messages ?? []).map(turn => [turn.messageId, turn]));
|
|
11
|
+
for (const turn of update.messages ?? [])
|
|
12
|
+
messages.set(turn.messageId, turn);
|
|
13
|
+
const ordered = (update.messageOrder ?? []).map(id => {
|
|
14
|
+
const turn = messages.get(id);
|
|
15
|
+
if (!turn)
|
|
16
|
+
throw new Error("对话同步缺少消息,请重新读取。");
|
|
17
|
+
return turn;
|
|
18
|
+
});
|
|
19
|
+
const detail = update.detail ?? previous;
|
|
20
|
+
if (!detail || detail.id !== update.id)
|
|
21
|
+
throw new Error("对话同步身份不一致。");
|
|
22
|
+
return { ...detail, messages: ordered, history: update.history };
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConversationDetail } from "./conversation-types.js";
|
|
2
|
+
import type { ConversationTurn } from "./types.js";
|
|
3
|
+
import type { ConversationSync, ConversationSyncQuery } from "./conversation-sync-contract.js";
|
|
4
|
+
export type { ConversationSyncQuery } from "./conversation-sync-contract.js";
|
|
5
|
+
/** Owner-scoped, bounded diff fingerprints only: no transcript copy or persistent client state. */
|
|
6
|
+
export declare class ConversationSynchronizer {
|
|
7
|
+
private readonly read;
|
|
8
|
+
private readonly revisions;
|
|
9
|
+
constructor(read: (id: string, select: (turns: ConversationTurn[]) => ConversationTurn[]) => ConversationDetail);
|
|
10
|
+
sync(id: string, query?: ConversationSyncQuery): ConversationSync;
|
|
11
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3
|
+
// Anchors survive deletion of the first loaded row. Never use an array offset as a durable cursor.
|
|
4
|
+
function key(turn) { return [turn.createdAt ?? "", turn.role === "user" ? "0" : "1", turn.messageId]; }
|
|
5
|
+
function compare(a, b) {
|
|
6
|
+
for (let i = 0; i < a.length; i++) {
|
|
7
|
+
const d = a[i].localeCompare(b[i]);
|
|
8
|
+
if (d)
|
|
9
|
+
return d;
|
|
10
|
+
}
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
function anchor(turn) { return Buffer.from(JSON.stringify(key(turn))).toString("base64url"); }
|
|
14
|
+
function decode(value) {
|
|
15
|
+
if (!value || value.length > 2048)
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString());
|
|
19
|
+
return Array.isArray(parsed) && parsed.length === 3 && parsed.every(v => typeof v === "string") ? parsed : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Owner-scoped, bounded diff fingerprints only: no transcript copy or persistent client state. */
|
|
26
|
+
export class ConversationSynchronizer {
|
|
27
|
+
read;
|
|
28
|
+
revisions = new Map();
|
|
29
|
+
constructor(read) {
|
|
30
|
+
this.read = read;
|
|
31
|
+
}
|
|
32
|
+
sync(id, query = {}) {
|
|
33
|
+
let from = null, hasEarlier = false, total = 0;
|
|
34
|
+
const limit = Math.min(80, Math.max(1, query.limit ?? 40));
|
|
35
|
+
const requestedAnchor = decode(query.from);
|
|
36
|
+
const detail = this.read(id, raw => {
|
|
37
|
+
const all = raw.map((turn, i) => turn.messageId ? turn : { ...turn, messageId: `${id}:legacy:${i}` })
|
|
38
|
+
.sort((a, b) => compare(key(a), key(b)));
|
|
39
|
+
total = all.length;
|
|
40
|
+
let start = requestedAnchor ? all.findIndex(turn => compare(key(turn), requestedAnchor) >= 0) : Math.max(0, total - limit);
|
|
41
|
+
if (start < 0)
|
|
42
|
+
start = total;
|
|
43
|
+
if (query.older && requestedAnchor)
|
|
44
|
+
start = Math.max(0, start - limit);
|
|
45
|
+
// Bound the first page by bytes too. One indivisible turn may exceed this soft budget.
|
|
46
|
+
if (!requestedAnchor) {
|
|
47
|
+
let bytes = 0;
|
|
48
|
+
for (let i = total - 1; i >= start; i--) {
|
|
49
|
+
const size = Buffer.byteLength(JSON.stringify(all[i]));
|
|
50
|
+
if (bytes + size > 96 * 1024 && i < total - 1) {
|
|
51
|
+
start = i + 1;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
bytes += size;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
hasEarlier = start > 0;
|
|
58
|
+
from = all[start] ? anchor(all[start]) : query.from ?? null;
|
|
59
|
+
return all.slice(start);
|
|
60
|
+
});
|
|
61
|
+
const { messages, ...metadata } = detail;
|
|
62
|
+
const metadataHash = hash(metadata);
|
|
63
|
+
const fingerprints = new Map(messages.map(turn => [turn.messageId, hash(turn)]));
|
|
64
|
+
const order = [...fingerprints.keys()];
|
|
65
|
+
if (order.length !== messages.length)
|
|
66
|
+
throw new Error("对话消息身份重复,无法安全同步。");
|
|
67
|
+
const revision = hash([id, metadataHash, from, hasEarlier, total, [...fingerprints]]);
|
|
68
|
+
const history = { revision, from, hasEarlier, total };
|
|
69
|
+
const previous = query.since ? this.revisions.get(query.since) : undefined;
|
|
70
|
+
this.revisions.delete(revision);
|
|
71
|
+
// Avoid a large reader retaining unbounded fingerprint maps; cache miss resets the
|
|
72
|
+
// SAME anchored window, never silently drops already loaded history.
|
|
73
|
+
if (order.length <= 10_000)
|
|
74
|
+
this.revisions.set(revision, { id, metadata: metadataHash, messages: fingerprints });
|
|
75
|
+
while (this.revisions.size > 64)
|
|
76
|
+
this.revisions.delete(this.revisions.keys().next().value);
|
|
77
|
+
if (query.since === revision)
|
|
78
|
+
return { id, history, unchanged: true };
|
|
79
|
+
const valid = previous?.id === id ? previous : undefined;
|
|
80
|
+
return { id, history, reset: !valid,
|
|
81
|
+
...(valid?.metadata === metadataHash ? {} : { detail: metadata }),
|
|
82
|
+
messages: valid ? messages.filter(turn => valid.messages.get(turn.messageId) !== fingerprints.get(turn.messageId)) : messages,
|
|
83
|
+
messageOrder: order };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -53,6 +53,7 @@ export interface ConversationSummary extends ConversationInstance {
|
|
|
53
53
|
export interface ConversationDetail extends ConversationSummary {
|
|
54
54
|
messages: ConversationTurn[];
|
|
55
55
|
runDetails: AiTeamRunDetail[];
|
|
56
|
+
history?: import("./conversation-sync-contract.js").ConversationHistory;
|
|
56
57
|
}
|
|
57
58
|
export interface ConversationReceipt {
|
|
58
59
|
requestId: string;
|
|
@@ -48,6 +48,23 @@ export function registerConversationRoutes(app, service) {
|
|
|
48
48
|
else
|
|
49
49
|
res.json(receipt);
|
|
50
50
|
});
|
|
51
|
+
app.get("/api/conversations/:id/sync", (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
for (const key of ["since", "from"])
|
|
54
|
+
if (typeof req.query[key] === "string" && String(req.query[key]).length > 2048) {
|
|
55
|
+
throw badRequest("对话同步游标无效。");
|
|
56
|
+
}
|
|
57
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
58
|
+
res.json(service.sync(req.params.id, {
|
|
59
|
+
since: typeof req.query.since === "string" ? req.query.since : undefined,
|
|
60
|
+
from: typeof req.query.from === "string" ? req.query.from : undefined,
|
|
61
|
+
older: req.query.older === "1",
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
sendRouteError(res, error, "对话同步失败。", 404);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
51
68
|
app.get("/api/conversations/:id", (req, res) => {
|
|
52
69
|
try {
|
|
53
70
|
res.json(service.detail(req.params.id));
|
|
@@ -11,7 +11,7 @@ export function createSessionRouteContext(deps) {
|
|
|
11
11
|
const { structured, storage, sessions, config, decisions } = deps;
|
|
12
12
|
const wantsCompactTools = (req) => req.get("X-Wand-Tool-Projection") === "compact" || req.query.compactTools === "1";
|
|
13
13
|
const sessionResponseDTO = (snapshot, req) => {
|
|
14
|
-
const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
|
|
14
|
+
const enriched = sessions.transportMessages?.(snapshot.id, snapshot.messages ?? []) ?? enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
|
|
15
15
|
const windowed = windowMessagesForTransport(wantsCompactTools(req) ? compactToolMessagesForTransport(enriched) : enriched, config.cardDefaults ?? {});
|
|
16
16
|
return toSessionDetailDTO(snapshot, {
|
|
17
17
|
messages: windowed.messages,
|
|
@@ -219,7 +219,7 @@ export function registerSessionListRoutes(app, ctx) {
|
|
|
219
219
|
const transcriptOutput = (snapshot.sessionKind ?? "pty") === "pty"
|
|
220
220
|
? processes.getPtyTranscript(snapshot.id) ?? snapshot.output
|
|
221
221
|
: req.query.format === "chat" && req.query.output === "omit" ? "" : snapshot.output;
|
|
222
|
-
const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
|
|
222
|
+
const enriched = sessions.transportMessages?.(snapshot.id, snapshot.messages ?? []) ?? enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
|
|
223
223
|
const messages = wantsCompactTools(req) ? compactToolMessagesForTransport(enriched) : enriched;
|
|
224
224
|
if (req.query.format === "chat") {
|
|
225
225
|
// 客户端带 blockBudget(Web/iOS)走块级窗口:只回最近 N 个块(必要时切掉最旧 turn 的头部),
|
|
@@ -260,7 +260,7 @@ export function registerSessionListRoutes(app, ctx) {
|
|
|
260
260
|
res.status(404).json({ error: "未找到该会话,可能已被删除。" });
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
|
-
const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
|
|
263
|
+
const enriched = sessions.transportMessages?.(snapshot.id, snapshot.messages ?? []) ?? enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
|
|
264
264
|
const all = wantsCompactTools(req) ? compactToolMessagesForTransport(enriched) : enriched;
|
|
265
265
|
const total = all.length;
|
|
266
266
|
// Web history: fetch at most one block window *before* the currently
|
package/dist/server.js
CHANGED
|
@@ -463,7 +463,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
463
463
|
defaultModelOf: (provider) => getDefaultModelForProvider(config, provider),
|
|
464
464
|
});
|
|
465
465
|
const conversationService = new ConversationService({ storage, config, structured: structuredSessions, runner: aiTeams,
|
|
466
|
-
notifySession: notifier.conversationSession, deleteSession: id => { sessionRegistry.deleteWithProviderHistory(id); } });
|
|
466
|
+
notifySession: notifier.conversationSession, notifyChanged: () => notifier.system({ kind: "conversations-changed" }), deleteSession: id => { sessionRegistry.deleteWithProviderHistory(id); } });
|
|
467
467
|
conversations = conversationService;
|
|
468
468
|
conversationService.migrateExistingChats();
|
|
469
469
|
const scheduledTasks = new ScheduledTaskService({ storage, conversations: conversationService,
|
|
@@ -802,6 +802,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
802
802
|
const wsManager = new WsBroadcastManager(wss, () => config.cardDefaults ?? {}, useHttps, authService, (req) => authenticateBearerAppToken(req, storage, config) !== null);
|
|
803
803
|
wsManager.setup({
|
|
804
804
|
getSession: (id) => sessionRegistry.get(id),
|
|
805
|
+
projectMessages: (id, messages) => sessionRegistry.transportMessages(id, messages),
|
|
805
806
|
getTerminalState: (id) => processes.getTerminalState(id),
|
|
806
807
|
sendPtyInput: (id, input, shortcutKey, userInput) => processes.sendInputConfirmed(id, input, "terminal", shortcutKey, userInput).then(() => { }),
|
|
807
808
|
resizePty: (id, cols, rows) => {
|
|
@@ -817,6 +818,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
817
818
|
});
|
|
818
819
|
// Wire process events to WebSocket broadcast
|
|
819
820
|
processes.on("process", (event) => {
|
|
821
|
+
sessionRegistry.invalidateMessageProjection(event.sessionId);
|
|
820
822
|
taskTitles.ingest(event);
|
|
821
823
|
sessionCompletions.ingest(event);
|
|
822
824
|
missions.ingest(event);
|
|
@@ -825,6 +827,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
825
827
|
wsManager.emitEvent(event);
|
|
826
828
|
});
|
|
827
829
|
structuredSessions.setEventEmitter((event) => {
|
|
830
|
+
sessionRegistry.invalidateMessageProjection(event.sessionId);
|
|
828
831
|
taskTitles.ingest(event);
|
|
829
832
|
storage.recordTaskCommentSessionEvent(event);
|
|
830
833
|
conversationService.ingestSessionEvent(event);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ProcessManager } from "./process-manager.js";
|
|
2
2
|
import type { WandStorage } from "./storage.js";
|
|
3
3
|
import type { StructuredSessionManager } from "./structured-session-manager.js";
|
|
4
|
-
import type { ExecutionMode, SessionSnapshot } from "./types.js";
|
|
4
|
+
import type { ExecutionMode, SessionSnapshot, ConversationTurn } from "./types.js";
|
|
5
5
|
export type SessionOwner = "structured" | "pty" | "storage";
|
|
6
6
|
export type ProviderHistoryDeleter = Pick<ProcessManager, "deleteClaudeHistoryFiles" | "deleteCodexHistoryFiles" | "deleteOpenCodeHistorySessions" | "deleteQoderHistoryFiles" | "deletePiHistoryFiles" | "deleteGrokHistoryFiles">;
|
|
7
7
|
export declare function deleteProviderNativeHistory(deleter: ProviderHistoryDeleter, provider: string, providerSessionId: string, cwd: string): void;
|
|
@@ -28,6 +28,11 @@ export declare class SessionRegistry {
|
|
|
28
28
|
private readonly processes;
|
|
29
29
|
private readonly structured;
|
|
30
30
|
private readonly storage;
|
|
31
|
+
private readonly messageProjections;
|
|
32
|
+
private readonly messageEpochs;
|
|
33
|
+
invalidateMessageProjection(id: string): void;
|
|
34
|
+
/** One semantic pass per source/revision, shared by HTTP and all WS readers. */
|
|
35
|
+
transportMessages(id: string, messages: ConversationTurn[]): ConversationTurn[];
|
|
31
36
|
/** Deleted owners can still be exiting; listSlim alone no longer sees them. */
|
|
32
37
|
private readonly nativeDeletionBarriers;
|
|
33
38
|
constructor(processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage);
|
package/dist/session-registry.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { isArchiveBlocked } from "./archive-policy.js";
|
|
2
3
|
import { logInfo } from "./runtime-log.js";
|
|
3
4
|
import { isSystemAiCall } from "./system-ai-call.js";
|
|
4
5
|
import { systemAiSessions } from "./system-ai-session-recorder.js";
|
|
5
6
|
import { cleanupWorktreeSync } from "./git-worktree.js";
|
|
6
7
|
import { inferProviderFromCommand } from "./session-provider.js";
|
|
8
|
+
import { enrichStructuredMessages } from "./structured-client-protocol.js";
|
|
7
9
|
export function deleteProviderNativeHistory(deleter, provider, providerSessionId, cwd) {
|
|
8
10
|
const id = providerSessionId.trim();
|
|
9
11
|
if (!id)
|
|
@@ -117,6 +119,33 @@ export class SessionRegistry {
|
|
|
117
119
|
processes;
|
|
118
120
|
structured;
|
|
119
121
|
storage;
|
|
122
|
+
messageProjections = new Map();
|
|
123
|
+
messageEpochs = new Map();
|
|
124
|
+
invalidateMessageProjection(id) {
|
|
125
|
+
this.messageProjections.delete(id);
|
|
126
|
+
this.messageEpochs.set(id, (this.messageEpochs.get(id) ?? 0) + 1);
|
|
127
|
+
if (this.messageEpochs.size > 2048)
|
|
128
|
+
this.messageEpochs.delete(this.messageEpochs.keys().next().value);
|
|
129
|
+
}
|
|
130
|
+
/** One semantic pass per source/revision, shared by HTTP and all WS readers. */
|
|
131
|
+
transportMessages(id, messages) {
|
|
132
|
+
// Some live adapters append in place before the debounced event boundary. A
|
|
133
|
+
// content digest prevents cached semantics from crossing that mutation window.
|
|
134
|
+
const raw = JSON.stringify(messages);
|
|
135
|
+
const digest = createHash("sha256").update(raw).digest("hex");
|
|
136
|
+
const revision = `${this.storage.projectionRevision()}:${this.messageEpochs.get(id) ?? 0}:${digest}`;
|
|
137
|
+
const cached = this.messageProjections.get(id);
|
|
138
|
+
if (cached?.revision === revision)
|
|
139
|
+
return cached.projected;
|
|
140
|
+
const projected = enrichStructuredMessages(messages, id);
|
|
141
|
+
this.messageProjections.delete(id);
|
|
142
|
+
if (raw.length <= 1024 * 1024 && messages.length <= 2000 && messages.reduce((n, turn) => n + turn.content.length, 0) <= 10000) {
|
|
143
|
+
this.messageProjections.set(id, { revision, source: messages, projected });
|
|
144
|
+
while (this.messageProjections.size > 16)
|
|
145
|
+
this.messageProjections.delete(this.messageProjections.keys().next().value);
|
|
146
|
+
}
|
|
147
|
+
return projected;
|
|
148
|
+
}
|
|
120
149
|
/** Deleted owners can still be exiting; listSlim alone no longer sees them. */
|
|
121
150
|
nativeDeletionBarriers = new Map();
|
|
122
151
|
constructor(processes, structured, storage) {
|