shennian 0.3.86 → 0.4.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.
Files changed (98) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/shennian.js +1 -1
  3. package/dist/publish-build-manifest.json +32 -27
  4. package/dist/src/agents/adapter.js +1 -19
  5. package/dist/src/agents/claude-managed-permissions.js +1 -248
  6. package/dist/src/agents/claude.js +8 -393
  7. package/dist/src/agents/codex-managed-permissions.js +1 -294
  8. package/dist/src/agents/codex-managed-room-tools.js +1 -249
  9. package/dist/src/agents/codex-utils.js +7 -200
  10. package/dist/src/agents/codex.d.ts +7 -1
  11. package/dist/src/agents/codex.js +17 -1352
  12. package/dist/src/agents/config-status.js +1 -226
  13. package/dist/src/agents/cursor-managed-permissions.js +2 -299
  14. package/dist/src/agents/cursor.js +1 -314
  15. package/dist/src/agents/custom.js +4 -418
  16. package/dist/src/agents/external-channel-instructions.js +11 -94
  17. package/dist/src/agents/gemini.js +1 -180
  18. package/dist/src/agents/manager.js +18 -250
  19. package/dist/src/agents/model-registry/cache.js +1 -37
  20. package/dist/src/agents/model-registry/discovery.js +2 -188
  21. package/dist/src/agents/model-registry/parsers.js +4 -521
  22. package/dist/src/agents/model-registry/runner.js +1 -30
  23. package/dist/src/agents/model-registry/service.js +1 -115
  24. package/dist/src/agents/model-registry/types.js +1 -8
  25. package/dist/src/agents/model-registry.js +1 -19
  26. package/dist/src/agents/native-capabilities.js +1 -237
  27. package/dist/src/agents/openclaw.js +2 -293
  28. package/dist/src/agents/opencode-managed-permissions.js +2 -187
  29. package/dist/src/agents/opencode.js +1 -298
  30. package/dist/src/agents/platform-instructions.js +17 -46
  31. package/dist/src/agents/runtime-compatibility.js +1 -44
  32. package/dist/src/agents/workbuddy-managed-permissions.js +2 -329
  33. package/dist/src/agents/workbuddy.js +3 -444
  34. package/dist/src/commands/logout.d.ts +3 -0
  35. package/dist/src/commands/logout.js +1 -0
  36. package/dist/src/daemon-ipc/paths.js +1 -1
  37. package/dist/src/fs/boundary.js +1 -185
  38. package/dist/src/fs/handler.js +1 -130
  39. package/dist/src/fs/security.js +1 -32
  40. package/dist/src/fs/text-decoder.js +1 -110
  41. package/dist/src/index.js +3 -587
  42. package/dist/src/manager/prompt.js +29 -34
  43. package/dist/src/manager/registry.js +1 -317
  44. package/dist/src/manager/retired-external-channel-compat.js +1 -40
  45. package/dist/src/manager/runtime.js +23 -1514
  46. package/dist/src/native-fusion/codex-parser.d.ts +3 -1
  47. package/dist/src/native-fusion/codex-parser.js +4 -660
  48. package/dist/src/native-fusion/codex-user-input.js +5 -102
  49. package/dist/src/native-fusion/config.js +1 -5
  50. package/dist/src/native-fusion/file-watcher.js +1 -191
  51. package/dist/src/native-fusion/local-repository.js +2 -569
  52. package/dist/src/native-fusion/opencode-parser.js +3 -130
  53. package/dist/src/native-fusion/parser-common.d.ts +6 -1
  54. package/dist/src/native-fusion/parser-common.js +4 -4
  55. package/dist/src/native-fusion/parsers.d.ts +4 -2
  56. package/dist/src/native-fusion/parsers.js +3 -140
  57. package/dist/src/native-fusion/service.js +2 -739
  58. package/dist/src/native-fusion/session-header.js +2 -101
  59. package/dist/src/native-fusion/state.js +2 -104
  60. package/dist/src/native-fusion/types.js +1 -1
  61. package/dist/src/personal-sync/machine-authority.js +1 -34
  62. package/dist/src/room/activation-audit.js +2 -45
  63. package/dist/src/room/activation-policy.js +1 -30
  64. package/dist/src/room/activation-service.js +8 -408
  65. package/dist/src/room/invite-bootstrap.js +1 -156
  66. package/dist/src/room/managed-prompt-composer.d.ts +1 -1
  67. package/dist/src/room/managed-prompt-composer.js +17 -16
  68. package/dist/src/room/managed-room-binding.js +1 -167
  69. package/dist/src/room/managed-room-ipc.d.ts +4 -0
  70. package/dist/src/room/managed-room-ipc.js +2 -2
  71. package/dist/src/room/mirror-sync-service.d.ts +1 -0
  72. package/dist/src/room/mirror-sync-service.js +1 -183
  73. package/dist/src/room/room-binding-client.js +1 -151
  74. package/dist/src/session/archive-zip.js +1 -220
  75. package/dist/src/session/deletion-sync-state.js +2 -136
  76. package/dist/src/session/handlers/agent-capabilities.js +1 -14
  77. package/dist/src/session/handlers/agent-config.js +1 -150
  78. package/dist/src/session/handlers/agents.js +1 -55
  79. package/dist/src/session/handlers/chat.js +2 -1030
  80. package/dist/src/session/handlers/control.js +1 -61
  81. package/dist/src/session/handlers/fs.js +1 -594
  82. package/dist/src/session/handlers/message-status.js +1 -15
  83. package/dist/src/session/handlers/remote-access.js +1 -43
  84. package/dist/src/session/handlers/room-managed.js +1 -101
  85. package/dist/src/session/handlers/session-refresh.js +1 -35
  86. package/dist/src/session/handlers/title.js +1 -79
  87. package/dist/src/session/handlers/tool-detail.js +1 -149
  88. package/dist/src/session/handlers/voice-window.js +1 -62
  89. package/dist/src/session/history-subscriptions.js +1 -470
  90. package/dist/src/session/manager.d.ts +1 -0
  91. package/dist/src/session/manager.js +1 -658
  92. package/dist/src/session/native-cleanup-outbox.js +2 -287
  93. package/dist/src/session/native-session-lifecycle.js +6 -327
  94. package/dist/src/session/queue.d.ts +8 -0
  95. package/dist/src/session/queue.js +6 -6
  96. package/dist/src/upgrade/scheduler.js +1 -126
  97. package/node_modules/@shennian/wire/dist/agent-workspace.js +2 -2
  98. package/package.json +1 -1
@@ -1,470 +1 @@
1
- // @arch docs/architecture/data-sync.md#8-打开会话快照与实时订阅必须无缝衔接
2
- // @test src/__tests__/history-subscriptions.test.ts
3
- import { SESSION_HISTORY_LIMITS, } from '@shennian/wire';
4
- import { getCanonicalSessionStore, listSessionRecords } from './store.js';
5
- import { recordPersonalSyncLocalMetric, setPersonalSyncLocalSubscriptions, } from '../personal-sync/metrics.js';
6
- import { assertPersonalSyncIdentity, getPersonalSyncIdentity } from '../personal-sync/epoch.js';
7
- export const PERSONAL_HISTORY_DELTA_BATCH_WINDOW_MS = 100;
8
- export const PERSONAL_HISTORY_DELTA_BATCH_MAX_MUTATIONS = SESSION_HISTORY_LIMITS.maxDeltaMutations;
9
- const PERSONAL_HISTORY_DELTA_BATCH_MAX_BYTES = SESSION_HISTORY_LIMITS.maxMutationBytes - 4_096;
10
- const PERSONAL_HISTORY_REPLICA_PAGE_MAX_BYTES = SESSION_HISTORY_LIMITS.maxReplicaPageBytes - 16_384;
11
- export class PersonalHistorySubscriptions {
12
- client;
13
- store;
14
- hasLocalSession;
15
- subscriptions = new Map();
16
- replicaStreams = new Map();
17
- unsubscribeStore;
18
- constructor(client, store = getCanonicalSessionStore(), hasLocalSession = (sessionId) => listSessionRecords().some((record) => record.sessionId === sessionId)) {
19
- this.client = client;
20
- this.store = store;
21
- this.hasLocalSession = hasLocalSession;
22
- this.unsubscribeStore = store.subscribe((mutation) => this.handleMutation(mutation));
23
- }
24
- handleOpen(req) {
25
- const params = req.params;
26
- assertPersonalSyncIdentity(params);
27
- validateIdentity(params.sessionId, params.subscriptionId);
28
- // A missing canonical manifest is valid only for a locally known, newly
29
- // created empty session. Server metadata from an earlier hard-cut epoch
30
- // must never be projected as a successful empty transcript.
31
- if (!this.hasLocalSession(params.sessionId) && !this.store.getManifest(params.sessionId)) {
32
- throw new Error('history_unavailable');
33
- }
34
- if (this.subscriptions.has(params.subscriptionId))
35
- throw new Error('subscription already exists');
36
- const sameSessionCount = [...this.subscriptions.values()]
37
- .filter((item) => item.sessionId === params.sessionId).length;
38
- if (sameSessionCount >= SESSION_HISTORY_LIMITS.maxSubscriptionsPerSession) {
39
- this.client.sendRes({
40
- type: 'res',
41
- id: req.id,
42
- ok: false,
43
- error: 'subscription_limit_exceeded',
44
- rejection: {
45
- code: 'subscription_limit_exceeded',
46
- retryable: true,
47
- requiredAction: 'retry',
48
- },
49
- });
50
- return;
51
- }
52
- const subscription = {
53
- sessionId: params.sessionId,
54
- subscriptionId: params.subscriptionId,
55
- snapshotRevision: 0,
56
- lastSentRevision: 0,
57
- active: false,
58
- buffered: [],
59
- pending: [],
60
- pendingBytes: 0,
61
- flushTimer: null,
62
- lastQueuedRevision: 0,
63
- };
64
- this.subscriptions.set(params.subscriptionId, subscription);
65
- this.updateSubscriptionMetric();
66
- try {
67
- const page = this.store.readPage(params.sessionId, {
68
- limit: normalizeLimit(params.limit, SESSION_HISTORY_LIMITS.defaultSnapshotMessages),
69
- ...(params.beforeSequence === undefined ? {} : { beforeSequence: params.beforeSequence }),
70
- });
71
- subscription.snapshotRevision = page.bodyRevision;
72
- subscription.lastSentRevision = page.bodyRevision;
73
- subscription.lastQueuedRevision = page.bodyRevision;
74
- this.client.sendRes({
75
- type: 'res', id: req.id, ok: true,
76
- payload: {
77
- ...getPersonalSyncIdentity(this.store.daemonInstallationId),
78
- sessionId: params.sessionId,
79
- subscriptionId: params.subscriptionId,
80
- storageMode: 'machine_local',
81
- snapshotRevision: page.bodyRevision,
82
- firstAvailableSeq: page.firstAvailableSeq,
83
- lastAvailableSeq: page.lastAvailableSeq,
84
- hasOlder: page.hasOlder,
85
- messages: page.entries,
86
- },
87
- });
88
- recordPersonalSyncLocalMetric('historySnapshots');
89
- recordPersonalSyncLocalMetric('historyBytesServed', Buffer.byteLength(JSON.stringify(page.entries), 'utf8'));
90
- subscription.active = true;
91
- for (const mutation of subscription.buffered)
92
- this.queueMutation(subscription, mutation);
93
- subscription.buffered = [];
94
- }
95
- catch (error) {
96
- this.subscriptions.delete(params.subscriptionId);
97
- this.updateSubscriptionMetric();
98
- throw error;
99
- }
100
- }
101
- handleSync(req) {
102
- const params = req.params;
103
- assertPersonalSyncIdentity(params);
104
- validateIdentity(params.sessionId, params.syncId);
105
- if (!Number.isSafeInteger(params.afterRevision) || params.afterRevision < 0) {
106
- throw new Error('invalid history sync revision');
107
- }
108
- if (!this.hasLocalSession(params.sessionId) && !this.store.getManifest(params.sessionId)) {
109
- throw new Error('history_unavailable');
110
- }
111
- const targetRevision = this.store.getManifest(params.sessionId)?.bodyRevision ?? 0;
112
- if (params.afterRevision > targetRevision)
113
- throw new Error('revision_gap');
114
- const limit = normalizeSyncLimit(params.limit);
115
- const available = this.store.readDeltas(params.sessionId, params.afterRevision, limit);
116
- const mutations = [];
117
- let bytes = 0;
118
- for (const mutation of available) {
119
- const projected = { ...mutation, subscriptionId: params.syncId };
120
- const mutationBytes = Buffer.byteLength(JSON.stringify(projected), 'utf8') + 1;
121
- if (mutations.length > 0 && bytes + mutationBytes > PERSONAL_HISTORY_REPLICA_PAGE_MAX_BYTES)
122
- break;
123
- if (mutationBytes > PERSONAL_HISTORY_REPLICA_PAGE_MAX_BYTES) {
124
- this.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'response_too_large' });
125
- return;
126
- }
127
- mutations.push(projected);
128
- bytes += mutationBytes;
129
- }
130
- const toRevision = mutations.at(-1)?.revision ?? params.afterRevision;
131
- this.client.sendRes({
132
- type: 'res', id: req.id, ok: true,
133
- payload: {
134
- ...getPersonalSyncIdentity(this.store.daemonInstallationId),
135
- sessionId: params.sessionId,
136
- syncId: params.syncId,
137
- fromRevision: params.afterRevision,
138
- toRevision,
139
- targetRevision,
140
- hasMore: toRevision < targetRevision,
141
- mutations,
142
- },
143
- });
144
- recordPersonalSyncLocalMetric('historyDeltas');
145
- recordPersonalSyncLocalMetric('historyBytesServed', bytes);
146
- }
147
- handleReplicaSubscribe(req) {
148
- const params = req.params;
149
- assertPersonalSyncIdentity(params);
150
- validateStreamIdentity(params.appInstallationId, params.streamId, params.generation);
151
- if (!this.replicaStreams.has(params.streamId)
152
- && this.replicaStreams.size >= SESSION_HISTORY_LIMITS.maxReplicaStreamsPerDaemon) {
153
- this.client.sendRes({
154
- type: 'res', id: req.id, ok: false, error: 'subscription_limit_exceeded',
155
- rejection: {
156
- code: 'subscription_limit_exceeded', retryable: true, requiredAction: 'retry',
157
- },
158
- });
159
- return;
160
- }
161
- const previous = this.replicaStreams.get(params.streamId);
162
- if (previous && params.generation < previous.generation) {
163
- throw new Error('invalid body replica generation');
164
- }
165
- if (previous)
166
- this.flushReplicaStream(previous);
167
- const stream = {
168
- appInstallationId: params.appInstallationId,
169
- streamId: params.streamId,
170
- generation: params.generation,
171
- active: false,
172
- buffered: [],
173
- highWaterBySession: new Map(),
174
- lastSentBySession: new Map(),
175
- lastQueuedBySession: new Map(),
176
- pendingBySession: new Map(),
177
- pendingCount: 0,
178
- pendingBytes: 0,
179
- flushTimer: null,
180
- };
181
- // Register before capturing high-water marks. Commits racing this scan are
182
- // buffered, then filtered against the captured revision after the response.
183
- this.replicaStreams.set(params.streamId, stream);
184
- this.updateSubscriptionMetric();
185
- const highWatermarks = this.store.listSessionIds().map((sessionId) => {
186
- const bodyRevision = this.store.getManifest(sessionId)?.bodyRevision ?? 0;
187
- stream.highWaterBySession.set(sessionId, bodyRevision);
188
- stream.lastSentBySession.set(sessionId, bodyRevision);
189
- stream.lastQueuedBySession.set(sessionId, bodyRevision);
190
- return { sessionId, bodyRevision };
191
- });
192
- this.client.sendRes({
193
- type: 'res', id: req.id, ok: true,
194
- payload: {
195
- ...getPersonalSyncIdentity(this.store.daemonInstallationId),
196
- appInstallationId: params.appInstallationId,
197
- streamId: params.streamId,
198
- generation: params.generation,
199
- highWatermarks,
200
- },
201
- });
202
- stream.active = true;
203
- for (const mutation of stream.buffered)
204
- this.queueReplicaMutation(stream, mutation);
205
- stream.buffered = [];
206
- }
207
- handleReplicaUnsubscribe(req) {
208
- const params = req.params;
209
- assertPersonalSyncIdentity(params);
210
- validateStreamIdentity(params.appInstallationId, params.streamId, params.generation);
211
- const stream = this.replicaStreams.get(params.streamId);
212
- if (stream && stream.generation === params.generation) {
213
- this.flushReplicaStream(stream);
214
- this.replicaStreams.delete(params.streamId);
215
- this.updateSubscriptionMetric();
216
- }
217
- this.client.sendRes({
218
- type: 'res', id: req.id, ok: true,
219
- payload: {
220
- ...getPersonalSyncIdentity(this.store.daemonInstallationId),
221
- appInstallationId: params.appInstallationId,
222
- streamId: params.streamId,
223
- generation: params.generation,
224
- closed: true,
225
- },
226
- });
227
- }
228
- handlePage(req) {
229
- const params = req.params;
230
- assertPersonalSyncIdentity(params);
231
- const subscription = this.requireSubscription(params.sessionId, params.subscriptionId);
232
- let page;
233
- try {
234
- page = this.store.readPageAtRevision(params.sessionId, {
235
- beforeSequence: params.beforeSequence,
236
- limit: normalizeLimit(params.limit, SESSION_HISTORY_LIMITS.defaultPageMessages),
237
- revision: subscription.snapshotRevision,
238
- });
239
- }
240
- catch {
241
- throw new Error('snapshot_compacted');
242
- }
243
- this.client.sendRes({
244
- type: 'res', id: req.id, ok: true,
245
- payload: {
246
- ...getPersonalSyncIdentity(this.store.daemonInstallationId),
247
- sessionId: params.sessionId,
248
- subscriptionId: params.subscriptionId,
249
- snapshotRevision: subscription.snapshotRevision,
250
- firstAvailableSeq: page.firstAvailableSeq,
251
- hasOlder: page.hasOlder,
252
- messages: page.entries,
253
- },
254
- });
255
- recordPersonalSyncLocalMetric('historyPages');
256
- recordPersonalSyncLocalMetric('historyBytesServed', Buffer.byteLength(JSON.stringify(page.entries), 'utf8'));
257
- }
258
- handleClose(req) {
259
- const params = req.params;
260
- assertPersonalSyncIdentity(params);
261
- validateIdentity(params.sessionId, params.subscriptionId);
262
- const subscription = this.subscriptions.get(params.subscriptionId);
263
- if (subscription?.sessionId === params.sessionId) {
264
- this.flushSubscription(subscription);
265
- this.subscriptions.delete(params.subscriptionId);
266
- this.updateSubscriptionMetric();
267
- }
268
- this.client.sendRes({
269
- type: 'res', id: req.id, ok: true,
270
- payload: {
271
- ...getPersonalSyncIdentity(this.store.daemonInstallationId),
272
- sessionId: params.sessionId, subscriptionId: params.subscriptionId, closed: true,
273
- },
274
- });
275
- }
276
- closeAll() {
277
- for (const subscription of this.subscriptions.values())
278
- this.flushSubscription(subscription);
279
- for (const stream of this.replicaStreams.values())
280
- this.flushReplicaStream(stream);
281
- this.subscriptions.clear();
282
- this.replicaStreams.clear();
283
- setPersonalSyncLocalSubscriptions(0);
284
- this.unsubscribeStore();
285
- }
286
- closeSession(sessionId) {
287
- for (const [subscriptionId, subscription] of this.subscriptions.entries()) {
288
- if (subscription.sessionId !== sessionId)
289
- continue;
290
- this.flushSubscription(subscription);
291
- this.subscriptions.delete(subscriptionId);
292
- }
293
- this.updateSubscriptionMetric();
294
- }
295
- handleMutation(mutation) {
296
- for (const subscription of this.subscriptions.values()) {
297
- if (subscription.sessionId !== mutation.sessionId)
298
- continue;
299
- if (!subscription.active)
300
- subscription.buffered.push(mutation);
301
- else
302
- this.queueMutation(subscription, mutation);
303
- }
304
- for (const stream of this.replicaStreams.values()) {
305
- if (!stream.active)
306
- stream.buffered.push(mutation);
307
- else
308
- this.queueReplicaMutation(stream, mutation);
309
- }
310
- }
311
- queueReplicaMutation(stream, mutation) {
312
- const lastQueued = stream.lastQueuedBySession.get(mutation.sessionId) ?? 0;
313
- if (mutation.revision <= lastQueued)
314
- return;
315
- const mutationBytes = Buffer.byteLength(JSON.stringify({
316
- ...mutation,
317
- subscriptionId: stream.streamId,
318
- }), 'utf8') + 1;
319
- if (stream.pendingCount > 0
320
- && stream.pendingBytes + mutationBytes > PERSONAL_HISTORY_DELTA_BATCH_MAX_BYTES) {
321
- this.flushReplicaStream(stream);
322
- }
323
- const pending = stream.pendingBySession.get(mutation.sessionId) ?? [];
324
- pending.push(mutation);
325
- stream.pendingBySession.set(mutation.sessionId, pending);
326
- stream.pendingCount++;
327
- stream.pendingBytes += mutationBytes;
328
- stream.lastQueuedBySession.set(mutation.sessionId, mutation.revision);
329
- if (stream.pendingCount >= PERSONAL_HISTORY_DELTA_BATCH_MAX_MUTATIONS) {
330
- this.flushReplicaStream(stream);
331
- return;
332
- }
333
- if (stream.flushTimer)
334
- return;
335
- stream.flushTimer = setTimeout(() => this.flushReplicaStream(stream), PERSONAL_HISTORY_DELTA_BATCH_WINDOW_MS);
336
- }
337
- flushReplicaStream(stream) {
338
- if (stream.flushTimer) {
339
- clearTimeout(stream.flushTimer);
340
- stream.flushTimer = null;
341
- }
342
- if (stream.pendingCount === 0)
343
- return;
344
- const batches = [...stream.pendingBySession.entries()].flatMap(([sessionId, pending]) => {
345
- const lastSent = stream.lastSentBySession.get(sessionId) ?? 0;
346
- const mutations = pending.filter((mutation) => mutation.revision > lastSent);
347
- if (mutations.length === 0)
348
- return [];
349
- const toRevision = mutations[mutations.length - 1].revision;
350
- stream.lastSentBySession.set(sessionId, toRevision);
351
- return [{
352
- sessionId,
353
- fromRevision: lastSent,
354
- toRevision,
355
- mutations: mutations.map((mutation) => ({
356
- ...mutation,
357
- subscriptionId: stream.streamId,
358
- })),
359
- }];
360
- });
361
- stream.pendingBySession.clear();
362
- stream.pendingCount = 0;
363
- stream.pendingBytes = 0;
364
- if (batches.length === 0)
365
- return;
366
- this.client.sendEvent({
367
- type: 'event',
368
- event: 'session.body.replica.delta',
369
- payload: {
370
- ...getPersonalSyncIdentity(this.store.daemonInstallationId),
371
- appInstallationId: stream.appInstallationId,
372
- streamId: stream.streamId,
373
- generation: stream.generation,
374
- batches,
375
- },
376
- });
377
- recordPersonalSyncLocalMetric('historyDeltas');
378
- recordPersonalSyncLocalMetric('historyBytesServed', Buffer.byteLength(JSON.stringify(batches), 'utf8'));
379
- }
380
- queueMutation(subscription, mutation) {
381
- if (mutation.revision <= subscription.lastQueuedRevision)
382
- return;
383
- const mutationBytes = Buffer.byteLength(JSON.stringify({
384
- ...mutation,
385
- subscriptionId: subscription.subscriptionId,
386
- }), 'utf8') + 1;
387
- if (subscription.pending.length > 0 &&
388
- subscription.pendingBytes + mutationBytes > PERSONAL_HISTORY_DELTA_BATCH_MAX_BYTES)
389
- this.flushSubscription(subscription);
390
- subscription.pending.push(mutation);
391
- subscription.pendingBytes += mutationBytes;
392
- subscription.lastQueuedRevision = mutation.revision;
393
- if (subscription.pending.length >= PERSONAL_HISTORY_DELTA_BATCH_MAX_MUTATIONS) {
394
- this.flushSubscription(subscription);
395
- return;
396
- }
397
- if (subscription.flushTimer)
398
- return;
399
- subscription.flushTimer = setTimeout(() => this.flushSubscription(subscription), PERSONAL_HISTORY_DELTA_BATCH_WINDOW_MS);
400
- }
401
- flushSubscription(subscription) {
402
- if (subscription.flushTimer) {
403
- clearTimeout(subscription.flushTimer);
404
- subscription.flushTimer = null;
405
- }
406
- if (subscription.pending.length === 0)
407
- return;
408
- const mutations = subscription.pending
409
- .filter((mutation) => mutation.revision > subscription.lastSentRevision);
410
- subscription.pending = [];
411
- subscription.pendingBytes = 0;
412
- if (mutations.length === 0)
413
- return;
414
- const toRevision = mutations[mutations.length - 1].revision;
415
- this.client.sendEvent({
416
- type: 'event',
417
- event: 'session.body.delta',
418
- payload: {
419
- ...getPersonalSyncIdentity(this.store.daemonInstallationId),
420
- sessionId: subscription.sessionId,
421
- subscriptionId: subscription.subscriptionId,
422
- fromRevision: subscription.lastSentRevision,
423
- toRevision,
424
- mutations: mutations.map((mutation) => ({
425
- ...mutation,
426
- subscriptionId: subscription.subscriptionId,
427
- })),
428
- },
429
- });
430
- recordPersonalSyncLocalMetric('historyDeltas');
431
- recordPersonalSyncLocalMetric('historyBytesServed', Buffer.byteLength(JSON.stringify(mutations), 'utf8'));
432
- subscription.lastSentRevision = toRevision;
433
- }
434
- requireSubscription(sessionId, subscriptionId) {
435
- validateIdentity(sessionId, subscriptionId);
436
- const subscription = this.subscriptions.get(subscriptionId);
437
- if (!subscription || subscription.sessionId !== sessionId)
438
- throw new Error('subscription_not_found');
439
- return subscription;
440
- }
441
- updateSubscriptionMetric() {
442
- setPersonalSyncLocalSubscriptions(this.subscriptions.size + this.replicaStreams.size);
443
- }
444
- }
445
- function validateIdentity(sessionId, subscriptionId) {
446
- if (typeof sessionId !== 'string' || !sessionId || typeof subscriptionId !== 'string' || !subscriptionId) {
447
- throw new Error('invalid history subscription identity');
448
- }
449
- }
450
- function validateStreamIdentity(appInstallationId, streamId, generation) {
451
- if (typeof appInstallationId !== 'string' || !appInstallationId
452
- || typeof streamId !== 'string' || !streamId
453
- || !Number.isSafeInteger(generation) || generation < 1) {
454
- throw new Error('invalid body replica stream identity');
455
- }
456
- }
457
- function normalizeLimit(value, fallback) {
458
- const limit = value ?? fallback;
459
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > SESSION_HISTORY_LIMITS.maxPageMessages) {
460
- throw new Error('invalid history limit');
461
- }
462
- return limit;
463
- }
464
- function normalizeSyncLimit(value) {
465
- const limit = value ?? SESSION_HISTORY_LIMITS.maxDeltaMutations;
466
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > SESSION_HISTORY_LIMITS.maxDeltaMutations) {
467
- throw new Error('invalid history sync limit');
468
- }
469
- return limit;
470
- }
1
+ import{SESSION_HISTORY_LIMITS as l}from"@shennian/wire";import{getCanonicalSessionStore as M,listSessionRecords as _}from"./store.js";import{recordPersonalSyncLocalMetric as d,setPersonalSyncLocalSubscriptions as I}from"../personal-sync/metrics.js";import{assertPersonalSyncIdentity as p,getPersonalSyncIdentity as u}from"../personal-sync/epoch.js";const y=100,b=l.maxDeltaMutations,g=l.maxMutationBytes-4096,m=l.maxReplicaPageBytes-16384;class O{client;store;hasLocalSession;subscriptions=new Map;replicaStreams=new Map;unsubscribeStore;constructor(s,e=M(),i=t=>_().some(n=>n.sessionId===t)){this.client=s,this.store=e,this.hasLocalSession=i,this.unsubscribeStore=e.subscribe(t=>this.handleMutation(t))}handleOpen(s){const e=s.params;if(p(e),h(e.sessionId,e.subscriptionId),!this.hasLocalSession(e.sessionId)&&!this.store.getManifest(e.sessionId))throw new Error("history_unavailable");if(this.subscriptions.has(e.subscriptionId))throw new Error("subscription already exists");if([...this.subscriptions.values()].filter(n=>n.sessionId===e.sessionId).length>=l.maxSubscriptionsPerSession){this.client.sendRes({type:"res",id:s.id,ok:!1,error:"subscription_limit_exceeded",rejection:{code:"subscription_limit_exceeded",retryable:!0,requiredAction:"retry"}});return}const t={sessionId:e.sessionId,subscriptionId:e.subscriptionId,snapshotRevision:0,lastSentRevision:0,active:!1,buffered:[],pending:[],pendingBytes:0,flushTimer:null,lastQueuedRevision:0};this.subscriptions.set(e.subscriptionId,t),this.updateSubscriptionMetric();try{const n=this.store.readPage(e.sessionId,{limit:R(e.limit,l.defaultSnapshotMessages),...e.beforeSequence===void 0?{}:{beforeSequence:e.beforeSequence}});t.snapshotRevision=n.bodyRevision,t.lastSentRevision=n.bodyRevision,t.lastQueuedRevision=n.bodyRevision,this.client.sendRes({type:"res",id:s.id,ok:!0,payload:{...u(this.store.daemonInstallationId),sessionId:e.sessionId,subscriptionId:e.subscriptionId,storageMode:"machine_local",snapshotRevision:n.bodyRevision,firstAvailableSeq:n.firstAvailableSeq,lastAvailableSeq:n.lastAvailableSeq,hasOlder:n.hasOlder,messages:n.entries}}),d("historySnapshots"),d("historyBytesServed",Buffer.byteLength(JSON.stringify(n.entries),"utf8")),t.active=!0;for(const o of t.buffered)this.queueMutation(t,o);t.buffered=[]}catch(n){throw this.subscriptions.delete(e.subscriptionId),this.updateSubscriptionMetric(),n}}handleSync(s){const e=s.params;if(p(e),h(e.sessionId,e.syncId),!Number.isSafeInteger(e.afterRevision)||e.afterRevision<0)throw new Error("invalid history sync revision");if(!this.hasLocalSession(e.sessionId)&&!this.store.getManifest(e.sessionId))throw new Error("history_unavailable");const i=this.store.getManifest(e.sessionId)?.bodyRevision??0;if(e.afterRevision>i)throw new Error("revision_gap");const t=T(e.limit),n=this.store.readDeltas(e.sessionId,e.afterRevision,t),o=[];let r=0;for(const B of n){const S={...B,subscriptionId:e.syncId},f=Buffer.byteLength(JSON.stringify(S),"utf8")+1;if(o.length>0&&r+f>m)break;if(f>m){this.client.sendRes({type:"res",id:s.id,ok:!1,error:"response_too_large"});return}o.push(S),r+=f}const c=o.at(-1)?.revision??e.afterRevision;this.client.sendRes({type:"res",id:s.id,ok:!0,payload:{...u(this.store.daemonInstallationId),sessionId:e.sessionId,syncId:e.syncId,fromRevision:e.afterRevision,toRevision:c,targetRevision:i,hasMore:c<i,mutations:o}}),d("historyDeltas"),d("historyBytesServed",r)}handleReplicaSubscribe(s){const e=s.params;if(p(e),v(e.appInstallationId,e.streamId,e.generation),!this.replicaStreams.has(e.streamId)&&this.replicaStreams.size>=l.maxReplicaStreamsPerDaemon){this.client.sendRes({type:"res",id:s.id,ok:!1,error:"subscription_limit_exceeded",rejection:{code:"subscription_limit_exceeded",retryable:!0,requiredAction:"retry"}});return}const i=this.replicaStreams.get(e.streamId);if(i&&e.generation<i.generation)throw new Error("invalid body replica generation");i&&this.flushReplicaStream(i);const t={appInstallationId:e.appInstallationId,streamId:e.streamId,generation:e.generation,active:!1,buffered:[],highWaterBySession:new Map,lastSentBySession:new Map,lastQueuedBySession:new Map,pendingBySession:new Map,pendingCount:0,pendingBytes:0,flushTimer:null};this.replicaStreams.set(e.streamId,t),this.updateSubscriptionMetric();const n=this.store.listSessionIds().map(o=>{const r=this.store.getManifest(o)?.bodyRevision??0;return t.highWaterBySession.set(o,r),t.lastSentBySession.set(o,r),t.lastQueuedBySession.set(o,r),{sessionId:o,bodyRevision:r}});this.client.sendRes({type:"res",id:s.id,ok:!0,payload:{...u(this.store.daemonInstallationId),appInstallationId:e.appInstallationId,streamId:e.streamId,generation:e.generation,highWatermarks:n}}),t.active=!0;for(const o of t.buffered)this.queueReplicaMutation(t,o);t.buffered=[]}handleReplicaUnsubscribe(s){const e=s.params;p(e),v(e.appInstallationId,e.streamId,e.generation);const i=this.replicaStreams.get(e.streamId);i&&i.generation===e.generation&&(this.flushReplicaStream(i),this.replicaStreams.delete(e.streamId),this.updateSubscriptionMetric()),this.client.sendRes({type:"res",id:s.id,ok:!0,payload:{...u(this.store.daemonInstallationId),appInstallationId:e.appInstallationId,streamId:e.streamId,generation:e.generation,closed:!0}})}handlePage(s){const e=s.params;p(e);const i=this.requireSubscription(e.sessionId,e.subscriptionId);let t;try{t=this.store.readPageAtRevision(e.sessionId,{beforeSequence:e.beforeSequence,limit:R(e.limit,l.defaultPageMessages),revision:i.snapshotRevision})}catch{throw new Error("snapshot_compacted")}this.client.sendRes({type:"res",id:s.id,ok:!0,payload:{...u(this.store.daemonInstallationId),sessionId:e.sessionId,subscriptionId:e.subscriptionId,snapshotRevision:i.snapshotRevision,firstAvailableSeq:t.firstAvailableSeq,hasOlder:t.hasOlder,messages:t.entries}}),d("historyPages"),d("historyBytesServed",Buffer.byteLength(JSON.stringify(t.entries),"utf8"))}handleClose(s){const e=s.params;p(e),h(e.sessionId,e.subscriptionId);const i=this.subscriptions.get(e.subscriptionId);i?.sessionId===e.sessionId&&(this.flushSubscription(i),this.subscriptions.delete(e.subscriptionId),this.updateSubscriptionMetric()),this.client.sendRes({type:"res",id:s.id,ok:!0,payload:{...u(this.store.daemonInstallationId),sessionId:e.sessionId,subscriptionId:e.subscriptionId,closed:!0}})}closeAll(){for(const s of this.subscriptions.values())this.flushSubscription(s);for(const s of this.replicaStreams.values())this.flushReplicaStream(s);this.subscriptions.clear(),this.replicaStreams.clear(),I(0),this.unsubscribeStore()}closeSession(s){for(const[e,i]of this.subscriptions.entries())i.sessionId===s&&(this.flushSubscription(i),this.subscriptions.delete(e));this.updateSubscriptionMetric()}handleMutation(s){for(const e of this.subscriptions.values())e.sessionId===s.sessionId&&(e.active?this.queueMutation(e,s):e.buffered.push(s));for(const e of this.replicaStreams.values())e.active?this.queueReplicaMutation(e,s):e.buffered.push(s)}queueReplicaMutation(s,e){const i=s.lastQueuedBySession.get(e.sessionId)??0;if(e.revision<=i)return;const t=Buffer.byteLength(JSON.stringify({...e,subscriptionId:s.streamId}),"utf8")+1;s.pendingCount>0&&s.pendingBytes+t>g&&this.flushReplicaStream(s);const n=s.pendingBySession.get(e.sessionId)??[];if(n.push(e),s.pendingBySession.set(e.sessionId,n),s.pendingCount++,s.pendingBytes+=t,s.lastQueuedBySession.set(e.sessionId,e.revision),s.pendingCount>=b){this.flushReplicaStream(s);return}s.flushTimer||(s.flushTimer=setTimeout(()=>this.flushReplicaStream(s),y))}flushReplicaStream(s){if(s.flushTimer&&(clearTimeout(s.flushTimer),s.flushTimer=null),s.pendingCount===0)return;const e=[...s.pendingBySession.entries()].flatMap(([i,t])=>{const n=s.lastSentBySession.get(i)??0,o=t.filter(c=>c.revision>n);if(o.length===0)return[];const r=o[o.length-1].revision;return s.lastSentBySession.set(i,r),[{sessionId:i,fromRevision:n,toRevision:r,mutations:o.map(c=>({...c,subscriptionId:s.streamId}))}]});s.pendingBySession.clear(),s.pendingCount=0,s.pendingBytes=0,e.length!==0&&(this.client.sendEvent({type:"event",event:"session.body.replica.delta",payload:{...u(this.store.daemonInstallationId),appInstallationId:s.appInstallationId,streamId:s.streamId,generation:s.generation,batches:e}}),d("historyDeltas"),d("historyBytesServed",Buffer.byteLength(JSON.stringify(e),"utf8")))}queueMutation(s,e){if(e.revision<=s.lastQueuedRevision)return;const i=Buffer.byteLength(JSON.stringify({...e,subscriptionId:s.subscriptionId}),"utf8")+1;if(s.pending.length>0&&s.pendingBytes+i>g&&this.flushSubscription(s),s.pending.push(e),s.pendingBytes+=i,s.lastQueuedRevision=e.revision,s.pending.length>=b){this.flushSubscription(s);return}s.flushTimer||(s.flushTimer=setTimeout(()=>this.flushSubscription(s),y))}flushSubscription(s){if(s.flushTimer&&(clearTimeout(s.flushTimer),s.flushTimer=null),s.pending.length===0)return;const e=s.pending.filter(t=>t.revision>s.lastSentRevision);if(s.pending=[],s.pendingBytes=0,e.length===0)return;const i=e[e.length-1].revision;this.client.sendEvent({type:"event",event:"session.body.delta",payload:{...u(this.store.daemonInstallationId),sessionId:s.sessionId,subscriptionId:s.subscriptionId,fromRevision:s.lastSentRevision,toRevision:i,mutations:e.map(t=>({...t,subscriptionId:s.subscriptionId}))}}),d("historyDeltas"),d("historyBytesServed",Buffer.byteLength(JSON.stringify(e),"utf8")),s.lastSentRevision=i}requireSubscription(s,e){h(s,e);const i=this.subscriptions.get(e);if(!i||i.sessionId!==s)throw new Error("subscription_not_found");return i}updateSubscriptionMetric(){I(this.subscriptions.size+this.replicaStreams.size)}}function h(a,s){if(typeof a!="string"||!a||typeof s!="string"||!s)throw new Error("invalid history subscription identity")}function v(a,s,e){if(typeof a!="string"||!a||typeof s!="string"||!s||!Number.isSafeInteger(e)||e<1)throw new Error("invalid body replica stream identity")}function R(a,s){const e=a??s;if(!Number.isSafeInteger(e)||e<1||e>l.maxPageMessages)throw new Error("invalid history limit");return e}function T(a){const s=a??l.maxDeltaMutations;if(!Number.isSafeInteger(s)||s<1||s>l.maxDeltaMutations)throw new Error("invalid history sync limit");return s}export{b as PERSONAL_HISTORY_DELTA_BATCH_MAX_MUTATIONS,y as PERSONAL_HISTORY_DELTA_BATCH_WINDOW_MS,O as PersonalHistorySubscriptions};
@@ -45,6 +45,7 @@ export declare class SessionManager {
45
45
  params: ManagedRoomEnqueueParams;
46
46
  context: ManagedRoomQueueContext;
47
47
  }>): Promise<void>;
48
+ discardPendingManagedRoomMessages(sessionId: string): number;
48
49
  private dispatchQueuedChat;
49
50
  synchronizeSessionDeletions(): Promise<void>;
50
51
  private synchronizeSessionDeletionsOnce;