@wowyuarm/dsh-agent-team 0.1.8 → 0.1.10

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 (81) hide show
  1. package/README.md +4 -11
  2. package/README.zh.md +4 -11
  3. package/package.json +54 -44
  4. package/packages/agent-team/README.md +7 -5
  5. package/packages/agent-team/README.zh.md +7 -4
  6. package/packages/agent-team/lib/attachments.js +14 -4
  7. package/packages/agent-team/lib/context-management.js +382 -0
  8. package/packages/agent-team/lib/context-projection.js +578 -0
  9. package/packages/agent-team/lib/context-source.js +212 -0
  10. package/packages/agent-team/lib/index.js +1272 -76
  11. package/packages/agent-team/lib/ledger.js +333 -44
  12. package/packages/agent-team/lib/member-context.js +6 -2
  13. package/packages/agent-team/lib/member-runtime.js +159 -4
  14. package/packages/agent-team/lib/member-time-context.js +126 -0
  15. package/packages/agent-team/lib/pressure-policy.js +199 -0
  16. package/packages/agent-team/lib/progress-nudge.js +12 -4
  17. package/packages/agent-team/lib/session-remediation.js +481 -0
  18. package/packages/agent-team/lib/spec.js +21 -2
  19. package/packages/agent-team/lib/time-format.js +56 -0
  20. package/packages/agent-team/lib/typert.host.js +168 -54
  21. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  22. package/packages/agent-team/lib/typert.remote-client.js +59 -25
  23. package/packages/agent-team/lib/types/attachments.d.ts +1 -1
  24. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/context-management.d.ts +147 -0
  26. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
  27. package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
  28. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
  29. package/packages/agent-team/lib/types/context-source.d.ts +117 -0
  30. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
  31. package/packages/agent-team/lib/types/index.d.ts +300 -2
  32. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/ledger.d.ts +119 -6
  34. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  35. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
  37. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  38. package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
  39. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
  40. package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
  41. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
  42. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
  43. package/packages/agent-team/lib/types/session-remediation.d.ts +149 -0
  44. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
  45. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  46. package/packages/agent-team/lib/types/time-format.d.ts +28 -0
  47. package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
  48. package/packages/agent-team/lib/types/types/entities.d.ts +22 -1
  49. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  50. package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
  51. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  52. package/packages/agent-team/lib/types/types/requests-results.d.ts +58 -1
  53. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  54. package/packages/agent-team/preset/team-member/agent.cordis.yml +27 -4
  55. package/packages/client-agent-team/lib/client.js +369 -352
  56. package/packages/client-agent-team/lib/client.js.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
  58. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  59. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
  60. package/packages/client-agent-team/lib/types/client/TeamMessage.js +2 -2
  61. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +4 -4
  62. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  63. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  65. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/index.js +44 -16
  67. package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -14
  68. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  69. package/packages/client-agent-team/lib/types/client/locales.js +0 -14
  70. package/packages/client-agent-team/lib/types/client/slots.d.ts +6 -1
  71. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  72. package/packages/tool-agent-team/README.md +11 -6
  73. package/packages/tool-agent-team/README.zh.md +11 -6
  74. package/packages/tool-agent-team/lib/context-tools.js +179 -0
  75. package/packages/tool-agent-team/lib/index.js +369 -58
  76. package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
  77. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
  78. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  79. package/packages/agent-team/lib/auto-compaction.js +0 -201
  80. package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
  81. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { isDeepStrictEqual } from 'node:util';
3
+ import { formatTeamTimestamp } from "./time-format.js";
3
4
  /** Stable Human Member identity shared by every replay of one dshHome Team. */
4
5
  export const AGENT_TEAM_HUMAN_MEMBER_ID = 'member:human';
5
6
  /** Idempotency identity of the one Host bootstrap operation. */
@@ -17,7 +18,7 @@ function emptyProjection() {
17
18
  return { byRequest: new Map(), byOperation: new Map(), ordered: [], channels: new Map(), members: new Map(), memberships: new Map(),
18
19
  claims: new Map(), messages: [], tasks: new Map(), threads: new Map(), attention: new Map(), directMarkers: new Map(), activityMarkers: new Map(),
19
20
  orderedFacts: [], factsByThread: new Map(), channelRefByThread: new Map(), mentionsByMessage: new Map(), messageCountByThread: new Map(),
20
- attentionByThread: new Map() };
21
+ attentionByThread: new Map(), previousSessions: new Map(), rolloverSeeds: new Map() };
21
22
  }
22
23
  const EMPTY_PROGRESS_NUDGE_TARGETS = Object.freeze({
23
24
  progress: Object.freeze([]),
@@ -247,9 +248,137 @@ export class AgentTeamLedger {
247
248
  return this.committed(this.memberResult(operation));
248
249
  });
249
250
  }
251
+ /**
252
+ * Move one enabled Member onto its next context generation as itself. The
253
+ * actor is the calling Member — the Host executes the transition but is
254
+ * never the business actor — and the durable data records only the
255
+ * verifiable envelope (previous/new Session anchors, the successful handoff
256
+ * tool-result seq, checkpoint seed lineage, trigger). The private handoff
257
+ * prose stays in the Member's own Session log. An exact retry resolves to
258
+ * the recorded outcome; the same requestId with different data collides.
259
+ */
260
+ rolloverMemberSession(request) {
261
+ return this.enqueue(async () => {
262
+ const existing = this.state.byRequest.get(request.requestId);
263
+ if (existing !== undefined) {
264
+ this.assertSameMemberSessionRolledOver(existing, request);
265
+ return this.resolved(this.memberResult(existing));
266
+ }
267
+ if (request.actor.kind !== 'member')
268
+ throw new Error('Member session rollover requires Member authority');
269
+ const prior = this.requireMember(request.memberId);
270
+ if (request.actor.memberId !== request.memberId)
271
+ throw new Error(`Agent Member '${prior.handle}' cannot roll over another Member's Session`);
272
+ if (prior.state !== 'enabled')
273
+ throw new Error(`Agent Member '${prior.handle}' is ${prior.state}; only enabled Members can roll over their Session`);
274
+ if (prior.sessionId === request.newSessionId)
275
+ throw new Error(`Agent Member '${prior.handle}' already runs Session '${request.newSessionId}'`);
276
+ if (prior.sessionId !== request.previousSessionId)
277
+ throw new Error(`Agent Member '${prior.handle}' is no longer bound to Session '${request.previousSessionId}'`);
278
+ if ((request.checkpointRef !== undefined) !== (request.sourceSessionId !== undefined))
279
+ throw new Error('rollover checkpoint fields must appear together');
280
+ if (request.sourceSessionId !== undefined && request.sourceThroughSeq === undefined)
281
+ throw new Error('rollover checkpoint seed requires a through sequence');
282
+ if (request.sourceThroughSeq !== undefined && request.sourceSessionId === undefined)
283
+ throw new Error('rollover through sequence requires a source Session');
284
+ const member = Object.freeze({ ...prior, sessionId: request.newSessionId });
285
+ const operation = Object.freeze({
286
+ ...this.operationBase(request, this.nextSequence()), kind: 'team/member-session-rolled-over',
287
+ data: Object.freeze({
288
+ member,
289
+ previousSessionId: request.previousSessionId,
290
+ newSessionId: request.newSessionId,
291
+ handoffEventSeq: request.handoffEventSeq,
292
+ trigger: request.trigger,
293
+ ...(request.sourceSessionId === undefined ? {} : { sourceSessionId: request.sourceSessionId }),
294
+ ...(request.sourceThroughSeq === undefined ? {} : { sourceThroughSeq: request.sourceThroughSeq }),
295
+ ...(request.checkpointRef === undefined ? {} : { checkpointRef: request.checkpointRef }),
296
+ }),
297
+ });
298
+ await this.table.put(operation.operationId, operation);
299
+ this.apply(operation);
300
+ return this.committed(this.memberResult(operation));
301
+ });
302
+ }
250
303
  getMember(memberId) {
251
304
  return this.state.members.get(memberId);
252
305
  }
306
+ /**
307
+ * The retired Session this Member most recently left through a renewal or
308
+ * rollover — the lineage parent a crash between the durable binding commit
309
+ * and the new Session's activation must reconstruct a handoff from. Read
310
+ * from the audit projection; the Member record itself only names the
311
+ * current Session.
312
+ */
313
+ previousSessionForMember(memberId) {
314
+ return this.state.previousSessions.get(memberId);
315
+ }
316
+ /**
317
+ * The latest session transition this Member committed: the retired Session
318
+ * and the target it moved onto, through either a renewal or a rollover.
319
+ * Crash recovery binds carried-input redelivery to the recorded target —
320
+ * the CURRENT Session being that target proves the old generation's
321
+ * unconsumed input still belongs here, regardless of whether the handoff
322
+ * itself already landed.
323
+ */
324
+ lastTransitionForMember(memberId) {
325
+ const previousSessionId = this.state.previousSessions.get(memberId);
326
+ const member = this.state.members.get(memberId);
327
+ if (previousSessionId === undefined || member === undefined)
328
+ return undefined;
329
+ return { previousSessionId, targetSessionId: member.sessionId };
330
+ }
331
+ /**
332
+ * The Thread a Task overlay lives on, or undefined for an unknown or
333
+ * archived-Channel Task. Read-only input for attributing claim-mutation
334
+ * boundaries to the Thread whose context they entered.
335
+ */
336
+ threadForTask(taskRef) {
337
+ const task = this.state.tasks.get(taskRef);
338
+ if (task === undefined)
339
+ return undefined;
340
+ return this.state.channels.get(task.channelRef)?.state === 'archived' ? undefined : task.threadRef;
341
+ }
342
+ /**
343
+ * The seed envelope of this Member's most recent rollover, when that
344
+ * rollover was a checkpoint return: the recorded source Session, the
345
+ * exclusive end of its seeded prefix, and the checkpoint ref. A fresh
346
+ * rollover clears the envelope. Crash recovery reads this to rebuild the
347
+ * child generation from the recorded seed instead of an empty context.
348
+ */
349
+ rolloverSeedForMember(memberId, targetSessionId) {
350
+ const seed = this.state.rolloverSeeds.get(memberId);
351
+ // Bind the envelope to its recorded target: a later renewal or fresh
352
+ // rollover that reused or replaced the binding must never consume an
353
+ // older generation's seed.
354
+ return seed?.targetSessionId === targetSessionId ? seed : undefined;
355
+ }
356
+ /**
357
+ * Count one Member's active Claims on open Tasks in active Channels — the
358
+ * read-only input to the checkpoint single-Thread coverage guard: with
359
+ * more than one, a rewind cannot be proven to stay inside one Thread.
360
+ */
361
+ activeClaimCountForMember(memberId) {
362
+ return this.activeClaimsForMember(memberId).length;
363
+ }
364
+ /**
365
+ * One Member's active Claims on open Tasks in active Channels, oldest
366
+ * first — the read-only input for the pressure notice's Claim labels.
367
+ */
368
+ activeClaimsForMember(memberId) {
369
+ const active = [];
370
+ for (const claim of this.state.claims.values()) {
371
+ if (claim.owner !== memberId || claim.state !== 'active')
372
+ continue;
373
+ const task = this.state.tasks.get(claim.taskRef);
374
+ if (task === undefined || task.resolution !== 'open')
375
+ continue;
376
+ if (this.state.channels.get(task.channelRef)?.state === 'archived')
377
+ continue;
378
+ active.push(claim);
379
+ }
380
+ return active.sort((a, b) => a.claimRef.localeCompare(b.claimRef));
381
+ }
253
382
  /**
254
383
  * Current progress-nudge candidacy for one Member: threads whose Thread
255
384
  * progress reminder applies (active Claim, or following a taskless Thread)
@@ -666,6 +795,15 @@ export class AgentTeamLedger {
666
795
  : claim);
667
796
  const releasedClaims = claims.filter(claim => claim.state === 'released' && this.state.claims.get(claim.claimRef)?.state === 'active');
668
797
  const completedClaims = claims.filter(claim => claim.state === 'done' && this.state.claims.get(claim.claimRef)?.state === 'active');
798
+ // Every done Claim at commit time — pre-finished and atomically
799
+ // completed alike — is the acceptance discriminator: the one list
800
+ // accept markers deliver from, so a normal accept no longer stays
801
+ // silent about the Members whose work it accepted. Sorted lexically
802
+ // so replay validation compares stably regardless of claim-ref
803
+ // generation order.
804
+ const acceptedClaims = request.action === 'accept'
805
+ ? claims.filter(claim => claim.state === 'done').sort((left, right) => left.claimRef.localeCompare(right.claimRef))
806
+ : [];
669
807
  const resolution = request.action === 'reopen' ? 'open' : request.action === 'accept' ? 'accepted' : 'closed';
670
808
  const status = resolution === 'accepted' ? 'done' : resolution === 'closed' ? 'closed'
671
809
  : this.deriveTaskStatus(task.taskRef, claims);
@@ -674,12 +812,13 @@ export class AgentTeamLedger {
674
812
  const activity = Object.freeze({ activityRef: this.ref('activity'), kind: request.action,
675
813
  taskRef: task.taskRef, threadRef: task.threadRef, actor: request.actor.memberId, sequence,
676
814
  ...(releasedClaims.length === 0 ? {} : { releasedClaimRefs: Object.freeze(releasedClaims.map(claim => claim.claimRef)) }),
677
- ...(completedClaims.length === 0 ? {} : { completedClaimRefs: Object.freeze(completedClaims.map(claim => claim.claimRef)) }) });
815
+ ...(completedClaims.length === 0 ? {} : { completedClaimRefs: Object.freeze(completedClaims.map(claim => claim.claimRef)) }),
816
+ ...(request.action === 'accept' ? { acceptedClaimRefs: Object.freeze(acceptedClaims.map(claim => claim.claimRef)) } : {}) });
678
817
  const inbox = request.action === 'close'
679
818
  ? this.closeThreadInbox(activity, thread.threadRef)
680
819
  : request.action === 'reopen'
681
820
  ? this.reopenThreadInbox(activity, thread.threadRef)
682
- : this.acceptThreadInbox(activity, thread.threadRef, completedClaims.map(claim => claim.owner));
821
+ : this.acceptThreadInbox(activity, thread.threadRef, acceptedClaims.map(claim => claim.owner));
683
822
  const operation = Object.freeze({
684
823
  ...this.operationBase(request, sequence), kind: 'team/task-changed',
685
824
  data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
@@ -871,7 +1010,7 @@ export class AgentTeamLedger {
871
1010
  // mirror: the reader sent it to the other Member → (you → them).
872
1011
  const direction = pair ? 'them → you' : 'you → them';
873
1012
  const truncated = operation.data.body.length > 160 ? `${operation.data.body.slice(0, 160)}…` : operation.data.body;
874
- return `(${direction}) ${truncated}`;
1013
+ return `(${direction}, at ${formatTeamTimestamp(operation.occurredAt)}) ${truncated}`;
875
1014
  }
876
1015
  return undefined;
877
1016
  }
@@ -901,8 +1040,12 @@ export class AgentTeamLedger {
901
1040
  const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
902
1041
  const directCount = unread.filter(item => item.direct).length;
903
1042
  const attention = this.attentionFor(authorized.memberId, thread.threadRef);
1043
+ // Same snapshot, same source as newestSequence: the instant hangs off
1044
+ // the newest unread fact itself, never a second lookup that could
1045
+ // observe a different commit between the two reads.
1046
+ const newest = unread.at(-1).fact;
904
1047
  items.push(Object.freeze({ channelRef, ...(task === undefined ? {} : { task }), thread, unreadCount: unread.length, directCount,
905
- newestSequence: unread.at(-1).fact.sequence, ...(attention === undefined ? {} : { attention }) }));
1048
+ newestSequence: newest.sequence, newestOccurredAt: newest.occurredAt, ...(attention === undefined ? {} : { attention }) }));
906
1049
  }
907
1050
  items.sort((left, right) => right.directCount - left.directCount || right.newestSequence - left.newestSequence || left.thread.threadRef.localeCompare(right.thread.threadRef));
908
1051
  const selected = items.slice(0, limit);
@@ -1097,8 +1240,10 @@ export class AgentTeamLedger {
1097
1240
  const message = fact.message;
1098
1241
  const thread = this.requireThread(message.threadRef);
1099
1242
  const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
1243
+ const facts = this.state.factsByThread.get(thread.threadRef) ?? [];
1100
1244
  return Object.freeze({ message, mentions: fact.mentions, ...(task === undefined ? {} : { task, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }), thread,
1101
- messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0 });
1245
+ messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0,
1246
+ lastActivityAt: facts.at(-1)?.occurredAt ?? '' });
1102
1247
  });
1103
1248
  const initialization = this.initialization();
1104
1249
  const nextCursor = selected.length === 0 ? cursor : direction === 'before' ? selected[0].sequence : selected.at(-1).sequence;
@@ -1155,6 +1300,7 @@ export class AgentTeamLedger {
1155
1300
  case 'team/member-session-restarted':
1156
1301
  case 'team/member-context-cleared':
1157
1302
  case 'team/member-session-renewed':
1303
+ case 'team/member-session-rolled-over':
1158
1304
  case 'team/member-updated':
1159
1305
  case 'team/member-removed':
1160
1306
  return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
@@ -1275,6 +1421,7 @@ export class AgentTeamLedger {
1275
1421
  case 'team/member-session-restarted':
1276
1422
  case 'team/member-context-cleared':
1277
1423
  case 'team/member-session-renewed':
1424
+ case 'team/member-session-rolled-over':
1278
1425
  case 'team/member-updated':
1279
1426
  case 'team/channel-member-added':
1280
1427
  case 'team/thread-read':
@@ -1407,6 +1554,29 @@ export class AgentTeamLedger {
1407
1554
  throw new Error('invalid Member session renewal');
1408
1555
  return;
1409
1556
  }
1557
+ if (operation.kind === 'team/member-session-rolled-over') {
1558
+ // The rollover's business actor is the calling Member itself; the Host
1559
+ // only executes the recorded transition. Authorization is self-scoped:
1560
+ // the actor must be the enabled target Member on its currently bound
1561
+ // Session, and the recorded transition moves exactly the sessionId.
1562
+ const actorMember = assertMember();
1563
+ const prior = projection.members.get(operation.data.member.memberId);
1564
+ if (actorMember.memberId !== operation.data.member.memberId)
1565
+ throw new Error('rolled-over Member must match its actor');
1566
+ if (prior === undefined || prior.state !== 'enabled'
1567
+ || prior.sessionId !== operation.data.previousSessionId
1568
+ || operation.data.member.sessionId !== operation.data.newSessionId
1569
+ || operation.data.member.sessionId === operation.data.previousSessionId
1570
+ || !this.sameMemberFacts(prior, operation.data.member))
1571
+ throw new Error('invalid Member session rollover');
1572
+ if (operation.data.sourceSessionId !== undefined && operation.data.sourceThroughSeq === undefined)
1573
+ throw new Error('rolled-over checkpoint seed requires a through sequence');
1574
+ if (operation.data.sourceThroughSeq !== undefined && operation.data.sourceSessionId === undefined)
1575
+ throw new Error('rolled-over through sequence requires a source Session');
1576
+ if ((operation.data.checkpointRef !== undefined) !== (operation.data.sourceSessionId !== undefined))
1577
+ throw new Error('rolled-over checkpoint fields must appear together');
1578
+ return;
1579
+ }
1410
1580
  if (operation.kind === 'team/channel-updated') {
1411
1581
  assertHuman();
1412
1582
  const prior = projection.channels.get(operation.data.channel.channelRef);
@@ -1640,13 +1810,24 @@ export class AgentTeamLedger {
1640
1810
  const expectedCompletedClaimRefs = operation.data.claims
1641
1811
  .filter(claim => projection.claims.get(claim.claimRef)?.state === 'active' && claim.state === 'done')
1642
1812
  .map(claim => claim.claimRef);
1813
+ // New-shape accepts carry the acceptance discriminator: every done
1814
+ // Claim at commit time, sorted. Legacy accepts predate it; the
1815
+ // recorded wake semantics for them stay completed-only (the old rule)
1816
+ // so every existing ledger replays unchanged.
1817
+ const expectedAcceptedClaimRefs = operation.data.claims
1818
+ .filter(claim => claim.state === 'done')
1819
+ .map(claim => claim.claimRef)
1820
+ .sort((left, right) => left.localeCompare(right));
1643
1821
  if (task.resolution !== expectedResolution || task.status !== expectedStatus
1644
1822
  || (activity.kind !== 'close' && activity.releasedClaimRefs !== undefined)
1645
1823
  || (activity.kind !== 'accept' && activity.completedClaimRefs !== undefined)
1824
+ || (activity.kind !== 'accept' && activity.acceptedClaimRefs !== undefined)
1646
1825
  || (activity.kind === 'close' && !this.sameList(activity.releasedClaimRefs ?? [], operation.data.claims
1647
1826
  .filter(claim => projection.claims.get(claim.claimRef)?.state === 'active' && claim.state === 'released')
1648
1827
  .map(claim => claim.claimRef)))
1649
- || !this.sameList(activity.completedClaimRefs ?? [], expectedCompletedClaimRefs)) {
1828
+ || !this.sameList(activity.completedClaimRefs ?? [], expectedCompletedClaimRefs)
1829
+ || (activity.kind === 'accept' && activity.acceptedClaimRefs !== undefined
1830
+ && !this.sameList(activity.acceptedClaimRefs, expectedAcceptedClaimRefs))) {
1650
1831
  throw new Error('invalid Task state transition');
1651
1832
  }
1652
1833
  const priorClaims = [...projection.claims.values()].filter(claim => claim.taskRef === task.taskRef).sort((left, right) => left.claimRef.localeCompare(right.claimRef));
@@ -1670,7 +1851,10 @@ export class AgentTeamLedger {
1670
1851
  ? this.closeThreadInboxFrom(projection, activity, thread.threadRef)
1671
1852
  : activity.kind === 'reopen'
1672
1853
  ? this.reopenThreadInboxFrom(projection, activity, thread.threadRef)
1673
- : this.acceptThreadInboxFrom(projection, activity, thread.threadRef, expectedCompletedClaimRefs);
1854
+ // Presence of the discriminator picks the wake rule: new accepts
1855
+ // wake every done owner; legacy records keep the completed-only
1856
+ // delta exactly as recorded.
1857
+ : this.acceptThreadInboxFrom(projection, activity, thread.threadRef, activity.acceptedClaimRefs !== undefined ? activity.acceptedClaimRefs : expectedCompletedClaimRefs);
1674
1858
  if (!isDeepStrictEqual(operation.data.inbox, expectedInbox))
1675
1859
  throw new Error('invalid Task inbox projection');
1676
1860
  this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
@@ -1938,6 +2122,25 @@ export class AgentTeamLedger {
1938
2122
  }
1939
2123
  if (operation.kind === 'team/member-session-renewed') {
1940
2124
  target.members.set(operation.data.member.memberId, operation.data.member);
2125
+ target.previousSessions.set(operation.data.member.memberId, operation.data.previousSessionId);
2126
+ // A Human-side renewal is always a fresh start: a stale checkpoint
2127
+ // seed from an earlier rollover must never re-seed it.
2128
+ target.rolloverSeeds.delete(operation.data.member.memberId);
2129
+ return;
2130
+ }
2131
+ if (operation.kind === 'team/member-session-rolled-over') {
2132
+ target.members.set(operation.data.member.memberId, operation.data.member);
2133
+ target.previousSessions.set(operation.data.member.memberId, operation.data.previousSessionId);
2134
+ // A checkpoint return records its seed envelope: a crash between this
2135
+ // commit and the new Session's activation rebuilds the child from the
2136
+ // recorded source instead of an empty context.
2137
+ const { sourceSessionId, sourceThroughSeq, checkpointRef } = operation.data;
2138
+ if (sourceSessionId !== undefined && sourceThroughSeq !== undefined && checkpointRef !== undefined) {
2139
+ target.rolloverSeeds.set(operation.data.member.memberId, { targetSessionId: operation.data.newSessionId, sourceSessionId, sourceThroughSeq, checkpointRef });
2140
+ }
2141
+ else {
2142
+ target.rolloverSeeds.delete(operation.data.member.memberId);
2143
+ }
1941
2144
  return;
1942
2145
  }
1943
2146
  if (operation.kind === 'team/channel-updated') {
@@ -1957,7 +2160,7 @@ export class AgentTeamLedger {
1957
2160
  for (const claim of operation.data.claims)
1958
2161
  target.claims.set(claim.claimRef, claim);
1959
2162
  for (const activity of operation.data.activities)
1960
- this.appendActivityFact(target, activity);
2163
+ this.appendActivityFact(target, activity, operation.occurredAt);
1961
2164
  for (const task of operation.data.tasks)
1962
2165
  target.tasks.set(task.taskRef, task);
1963
2166
  for (const thread of operation.data.threads)
@@ -1973,7 +2176,7 @@ export class AgentTeamLedger {
1973
2176
  for (const claim of operation.data.claims)
1974
2177
  target.claims.set(claim.claimRef, claim);
1975
2178
  for (const activity of operation.data.activities)
1976
- this.appendActivityFact(target, activity);
2179
+ this.appendActivityFact(target, activity, operation.occurredAt);
1977
2180
  for (const task of operation.data.tasks)
1978
2181
  target.tasks.set(task.taskRef, task);
1979
2182
  for (const thread of operation.data.threads)
@@ -1988,7 +2191,7 @@ export class AgentTeamLedger {
1988
2191
  for (const claim of operation.data.claims)
1989
2192
  target.claims.set(claim.claimRef, claim);
1990
2193
  for (const activity of operation.data.activities)
1991
- this.appendActivityFact(target, activity);
2194
+ this.appendActivityFact(target, activity, operation.occurredAt);
1992
2195
  for (const task of operation.data.tasks)
1993
2196
  target.tasks.set(task.taskRef, task);
1994
2197
  for (const thread of operation.data.threads)
@@ -2004,7 +2207,7 @@ export class AgentTeamLedger {
2004
2207
  for (const claim of operation.data.claims)
2005
2208
  target.claims.set(claim.claimRef, claim);
2006
2209
  for (const activity of operation.data.activities)
2007
- this.appendActivityFact(target, activity);
2210
+ this.appendActivityFact(target, activity, operation.occurredAt);
2008
2211
  for (const task of operation.data.tasks)
2009
2212
  target.tasks.set(task.taskRef, task);
2010
2213
  for (const thread of operation.data.threads)
@@ -2013,7 +2216,7 @@ export class AgentTeamLedger {
2013
2216
  return;
2014
2217
  }
2015
2218
  if (operation.kind === 'team/thread-promoted') {
2016
- this.appendActivityFact(target, operation.data.activity);
2219
+ this.appendActivityFact(target, operation.data.activity, operation.occurredAt);
2017
2220
  target.tasks.set(operation.data.task.taskRef, operation.data.task);
2018
2221
  target.threads.set(operation.data.thread.threadRef, operation.data.thread);
2019
2222
  this.applyInboxDelta(target, operation.data.inbox);
@@ -2023,7 +2226,7 @@ export class AgentTeamLedger {
2023
2226
  const { message, mentions } = operation.data;
2024
2227
  target.messages.push(message);
2025
2228
  target.mentionsByMessage.set(message.messageRef, Object.freeze([...mentions]));
2026
- this.appendMessageFact(target, message, mentions);
2229
+ this.appendMessageFact(target, message, mentions, message.occurredAt ?? operation.occurredAt);
2027
2230
  if (operation.data.task !== undefined)
2028
2231
  target.tasks.set(operation.data.task.taskRef, operation.data.task);
2029
2232
  target.threads.set(operation.data.thread.threadRef, operation.data.thread);
@@ -2034,7 +2237,7 @@ export class AgentTeamLedger {
2034
2237
  }
2035
2238
  if (operation.kind === 'team/claim-created' || operation.kind === 'team/claim-done' || operation.kind === 'team/claim-released') {
2036
2239
  target.claims.set(operation.data.claim.claimRef, operation.data.claim);
2037
- this.appendActivityFact(target, operation.data.activity);
2240
+ this.appendActivityFact(target, operation.data.activity, operation.occurredAt);
2038
2241
  target.tasks.set(operation.data.task.taskRef, operation.data.task);
2039
2242
  target.threads.set(operation.data.thread.threadRef, operation.data.thread);
2040
2243
  this.applyInboxDelta(target, operation.data.inbox);
@@ -2043,7 +2246,7 @@ export class AgentTeamLedger {
2043
2246
  if (operation.kind === 'team/task-changed') {
2044
2247
  for (const claim of operation.data.claims)
2045
2248
  target.claims.set(claim.claimRef, claim);
2046
- this.appendActivityFact(target, operation.data.activity);
2249
+ this.appendActivityFact(target, operation.data.activity, operation.occurredAt);
2047
2250
  target.tasks.set(operation.data.task.taskRef, operation.data.task);
2048
2251
  target.threads.set(operation.data.thread.threadRef, operation.data.thread);
2049
2252
  this.applyInboxDelta(target, operation.data.inbox);
@@ -2061,16 +2264,16 @@ export class AgentTeamLedger {
2061
2264
  assertUnhandledKind(operation);
2062
2265
  }
2063
2266
  /** Facts arrive in ledger sequence order, so global and per-thread lists stay sorted by append only. */
2064
- appendMessageFact(target, message, mentions) {
2065
- const fact = Object.freeze({ kind: 'message', sequence: message.sequence, message, mentions });
2267
+ appendMessageFact(target, message, mentions, occurredAt) {
2268
+ const fact = Object.freeze({ kind: 'message', sequence: message.sequence, message, mentions, occurredAt });
2066
2269
  target.orderedFacts.push(fact);
2067
2270
  const facts = target.factsByThread.get(message.threadRef) ?? [];
2068
2271
  facts.push(fact);
2069
2272
  target.factsByThread.set(message.threadRef, facts);
2070
2273
  target.messageCountByThread.set(message.threadRef, (target.messageCountByThread.get(message.threadRef) ?? 0) + 1);
2071
2274
  }
2072
- appendActivityFact(target, activity) {
2073
- const fact = Object.freeze({ kind: 'activity', sequence: activity.sequence, activity });
2275
+ appendActivityFact(target, activity, occurredAt) {
2276
+ const fact = Object.freeze({ kind: 'activity', sequence: activity.sequence, activity, occurredAt });
2074
2277
  target.orderedFacts.push(fact);
2075
2278
  const facts = target.factsByThread.get(activity.threadRef) ?? [];
2076
2279
  facts.push(fact);
@@ -2122,7 +2325,8 @@ export class AgentTeamLedger {
2122
2325
  const background = firstRead
2123
2326
  ? projection.messages.filter(message => message.threadRef === thread.threadRef && message.sequence < attention.startSequence)
2124
2327
  .map(message => Object.freeze({ kind: 'message', sequence: message.sequence, message,
2125
- mentions: projection.mentionsByMessage.get(message.messageRef) ?? [] }))
2328
+ mentions: projection.mentionsByMessage.get(message.messageRef) ?? [],
2329
+ occurredAt: this.occurredAtForFactFrom(projection, message.sequence, message.occurredAt) }))
2126
2330
  .filter(fact => !unreadFactKeys.has(this.threadFactKey(fact))).slice(-12)
2127
2331
  : [];
2128
2332
  const combined = [...background.map(fact => this.readFactFrom(projection, memberId, fact, false)), ...unreadFacts]
@@ -2197,6 +2401,23 @@ export class AgentTeamLedger {
2197
2401
  threadFactsFrom(projection, threadRef) {
2198
2402
  return projection.factsByThread.get(threadRef) ?? [];
2199
2403
  }
2404
+ /**
2405
+ * The one per-fact instant projection: a fact's wall-clock time is the
2406
+ * occurredAt of the ledger operation that committed it, looked up by
2407
+ * sequence inside the same read snapshot. Every agent-facing surface
2408
+ * (Thread facts, notifications, DM history, mutation results) resolves
2409
+ * through this single path, never a second parallel lookup.
2410
+ */
2411
+ occurredAtForFactFrom(projection, sequence, fallback) {
2412
+ if (fallback !== undefined)
2413
+ return fallback;
2414
+ // Facts arrive in ledger sequence order and `ordered` is append-only, so
2415
+ // the entry at index sequence - 1 is the committing operation. The index
2416
+ // read is guarded for hypothetical projections whose prefix has not yet
2417
+ // caught up; those callers always pass an explicit fallback instead.
2418
+ const operation = projection.ordered[sequence - 1];
2419
+ return operation?.occurredAt ?? '';
2420
+ }
2200
2421
  messageInboxDelta(message, sender, mentions, attention) {
2201
2422
  const markers = mentions.map(memberId => Object.freeze({ memberId, threadRef: message.threadRef,
2202
2423
  messageRef: message.messageRef, sequence: message.sequence }));
@@ -2216,22 +2437,24 @@ export class AgentTeamLedger {
2216
2437
  return this.inboxDelta([], removed, [], [...projection.directMarkers.values()].filter(marker => marker.threadRef === threadRef), activityMarkers, [...projection.activityMarkers.values()].filter(marker => marker.threadRef === threadRef));
2217
2438
  }
2218
2439
  /**
2219
- * Early acceptance completes open Claims inside the accept operation; their
2220
- * owners learn about it through activity markers regardless of whether they
2221
- * still follow the Thread. Plain accepts (no completed Claims) stay silent.
2440
+ * Acceptance notifies every done Claim owner pre-finished (the normal
2441
+ * flow) and atomically completed (early acceptance) alike through
2442
+ * activity markers regardless of whether they still follow the Thread.
2443
+ * The actor never notifies itself. Legacy records without the acceptance
2444
+ * discriminator keep the old completed-only wake semantics on replay.
2222
2445
  */
2223
- acceptThreadInbox(activity, threadRef, completedOwners) {
2224
- if (completedOwners.length === 0)
2446
+ acceptThreadInbox(activity, threadRef, acceptedOwners) {
2447
+ if (acceptedOwners.length === 0)
2225
2448
  return this.inboxDelta();
2226
- const recipients = [...new Set(completedOwners)].filter(memberId => memberId !== activity.actor);
2449
+ const recipients = [...new Set(acceptedOwners)].filter(memberId => memberId !== activity.actor);
2227
2450
  const markers = recipients.map(memberId => Object.freeze({ memberId, threadRef,
2228
2451
  activityRef: activity.activityRef, sequence: activity.sequence }));
2229
2452
  return this.inboxDelta([], [], [], [], markers);
2230
2453
  }
2231
- acceptThreadInboxFrom(projection, activity, threadRef, completedClaimRefs) {
2232
- if (completedClaimRefs.length === 0)
2454
+ acceptThreadInboxFrom(projection, activity, threadRef, claimRefs) {
2455
+ if (claimRefs.length === 0)
2233
2456
  return this.inboxDelta();
2234
- const owners = completedClaimRefs.map(claimRef => projection.claims.get(claimRef)?.owner).filter(owner => owner !== undefined);
2457
+ const owners = claimRefs.map(claimRef => projection.claims.get(claimRef)?.owner).filter(owner => owner !== undefined);
2235
2458
  return this.acceptThreadInbox(activity, threadRef, owners);
2236
2459
  }
2237
2460
  reopenThreadInbox(activity, threadRef) {
@@ -2256,18 +2479,24 @@ export class AgentTeamLedger {
2256
2479
  return this.inboxDelta([], [], [], [], markers);
2257
2480
  }
2258
2481
  removeMemberThreadInbox(memberId, threadRefs) {
2259
- const removed = [...this.state.attention.values()].filter(attention => attention.memberId === memberId && threadRefs.has(attention.threadRef))
2482
+ return this.removeMemberThreadInboxFrom(this.state, memberId, threadRefs);
2483
+ }
2484
+ removeMemberThreadInboxFrom(projection, memberId, threadRefs) {
2485
+ const removed = [...projection.attention.values()].filter(attention => attention.memberId === memberId && threadRefs.has(attention.threadRef))
2260
2486
  .map(attention => Object.freeze({ memberId, threadRef: attention.threadRef }));
2261
- const markers = [...this.state.directMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
2262
- const activityMarkers = [...this.state.activityMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
2487
+ const markers = [...projection.directMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
2488
+ const activityMarkers = [...projection.activityMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
2263
2489
  return this.inboxDelta([], removed, [], markers, [], activityMarkers);
2264
2490
  }
2265
2491
  /** Attention and marker cleanup for EVERY Member on the given Threads. */
2266
2492
  channelArchivalInbox(threadRefs) {
2267
- const removed = [...this.state.attention.values()].filter(attention => threadRefs.has(attention.threadRef))
2493
+ return this.channelArchivalInboxFrom(this.state, threadRefs);
2494
+ }
2495
+ channelArchivalInboxFrom(projection, threadRefs) {
2496
+ const removed = [...projection.attention.values()].filter(attention => threadRefs.has(attention.threadRef))
2268
2497
  .map(attention => Object.freeze({ memberId: attention.memberId, threadRef: attention.threadRef }));
2269
- const markers = [...this.state.directMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
2270
- const activityMarkers = [...this.state.activityMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
2498
+ const markers = [...projection.directMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
2499
+ const activityMarkers = [...projection.activityMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
2271
2500
  return this.inboxDelta([], removed, [], markers, [], activityMarkers);
2272
2501
  }
2273
2502
  inboxDelta(set = [], removed = [], addedMarkers = [], removedMarkers = [], addedActivityMarkers = [], removedActivityMarkers = []) {
@@ -2666,8 +2895,12 @@ export class AgentTeamLedger {
2666
2895
  isChannelMemberFrom(projection, channelRef, memberId) {
2667
2896
  return projection.memberships.get(channelRef)?.has(memberId) === true;
2668
2897
  }
2898
+ /** Every Thread of the Channel, taskful or taskless — the scope both cleanup validators replay against. */
2669
2899
  channelThreadRefs(channelRef) {
2670
- return new Set([...this.state.tasks.values()].filter(task => task.channelRef === channelRef).map(task => task.threadRef));
2900
+ return this.channelThreadRefsFrom(this.state, channelRef);
2901
+ }
2902
+ channelThreadRefsFrom(projection, channelRef) {
2903
+ return new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === channelRef));
2671
2904
  }
2672
2905
  assertJoinableMember(workspaceId, memberId) {
2673
2906
  const member = this.requireMember(memberId);
@@ -2762,6 +2995,16 @@ export class AgentTeamLedger {
2762
2995
  || operation.data.member.memberId !== request.memberId || operation.data.member.sessionId !== request.sessionId)
2763
2996
  this.throwRequestCollision(request.requestId);
2764
2997
  }
2998
+ assertSameMemberSessionRolledOver(operation, request) {
2999
+ if (operation.kind !== 'team/member-session-rolled-over' || !this.sameActor(operation.actor, request.actor)
3000
+ || operation.data.member.memberId !== request.memberId || operation.data.member.sessionId !== request.newSessionId
3001
+ || operation.data.previousSessionId !== request.previousSessionId || operation.data.handoffEventSeq !== request.handoffEventSeq
3002
+ || operation.data.trigger !== request.trigger
3003
+ || operation.data.sourceSessionId !== request.sourceSessionId
3004
+ || operation.data.sourceThroughSeq !== request.sourceThroughSeq
3005
+ || operation.data.checkpointRef !== request.checkpointRef)
3006
+ this.throwRequestCollision(request.requestId);
3007
+ }
2765
3008
  assertSameChannelUpdate(operation, request) {
2766
3009
  if (operation.kind !== 'team/channel-updated' || !this.sameActor(operation.actor, request.actor)
2767
3010
  || operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef
@@ -2979,7 +3222,7 @@ export class AgentTeamLedger {
2979
3222
  consumedDirectMarkers: operation.data.inbox.directMarkers.removed });
2980
3223
  }
2981
3224
  receipt(operation) {
2982
- return Object.freeze({ operationId: operation.operationId, requestId: operation.requestId, sequence: operation.sequence });
3225
+ return Object.freeze({ operationId: operation.operationId, requestId: operation.requestId, sequence: operation.sequence, occurredAt: operation.occurredAt });
2983
3226
  }
2984
3227
  removalResult(operation) {
2985
3228
  return Object.freeze({ receipt: this.receipt(operation), member: operation.data.member,
@@ -3006,24 +3249,70 @@ export class AgentTeamLedger {
3006
3249
  sortedRecords() {
3007
3250
  const records = [...this.table.entries()].sort((left, right) => left[1].sequence - right[1].sequence);
3008
3251
  const occurrences = new Map();
3252
+ const instants = new Map();
3009
3253
  for (const [, operation] of records) {
3254
+ instants.set(operation.sequence, operation.occurredAt);
3010
3255
  if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
3011
3256
  occurrences.set(operation.data.message.messageRef, operation.data.message.occurredAt ?? operation.occurredAt);
3012
3257
  }
3013
3258
  }
3014
- return records.map(([id, operation]) => [id, this.normalizeOperation(operation, occurrences)]);
3259
+ // Replay against a scratch projection while normalizing, so a legacy
3260
+ // cleanup repair sees the same state its validator will.
3261
+ const projection = emptyProjection();
3262
+ return records.map(([id, operation]) => {
3263
+ const normalized = this.repairLegacyChannelCleanup(this.normalizeOperation(operation, occurrences, instants), projection);
3264
+ this.applyTo(projection, normalized);
3265
+ return [id, normalized];
3266
+ });
3267
+ }
3268
+ /**
3269
+ * Releases up to 0.1.9 scoped Channel archival and Channel member-removal
3270
+ * inbox cleanup to taskful Threads only (the collector read the Task
3271
+ * projection), so archiving or member removal in a Channel holding a
3272
+ * taskless Thread with Attention or markers wrote an incomplete inbox and
3273
+ * every later load rejected the record, leaving the profile unable to boot.
3274
+ * Records carrying exactly that legacy cleanup are repaired in memory the
3275
+ * same way pre-envelope Thread reads are; any other inbox still fails
3276
+ * validation, so a forgery is not silently accepted.
3277
+ */
3278
+ repairLegacyChannelCleanup(operation, projection) {
3279
+ if (operation.kind === 'team/channel-archived') {
3280
+ const channelRef = operation.data.channel.channelRef;
3281
+ const expected = this.channelArchivalInboxFrom(projection, this.channelThreadRefsFrom(projection, channelRef));
3282
+ if (!isDeepStrictEqual(operation.data.inbox, expected)
3283
+ && isDeepStrictEqual(operation.data.inbox, this.channelArchivalInboxFrom(projection, this.legacyChannelThreadRefs(projection, channelRef)))) {
3284
+ return { ...operation, data: { ...operation.data, inbox: expected } };
3285
+ }
3286
+ return operation;
3287
+ }
3288
+ if (operation.kind === 'team/channel-member-removed') {
3289
+ const { channelRef, memberId } = operation.data;
3290
+ const expected = this.removeMemberThreadInboxFrom(projection, memberId, this.channelThreadRefsFrom(projection, channelRef));
3291
+ if (!isDeepStrictEqual(operation.data.inbox, expected)
3292
+ && isDeepStrictEqual(operation.data.inbox, this.removeMemberThreadInboxFrom(projection, memberId, this.legacyChannelThreadRefs(projection, channelRef)))) {
3293
+ return { ...operation, data: { ...operation.data, inbox: expected } };
3294
+ }
3295
+ return operation;
3296
+ }
3297
+ return operation;
3298
+ }
3299
+ /** The pre-fix Channel Thread scope: only Threads carrying a Task. */
3300
+ legacyChannelThreadRefs(projection, channelRef) {
3301
+ return new Set([...projection.tasks.values()].filter(task => task.channelRef === channelRef).map(task => task.threadRef));
3015
3302
  }
3016
3303
  /** Ledgers written before message occurredAt existed store bare messages; Thread reads resolve instants from the originating operations. */
3017
- normalizeOperation(operation, occurrences) {
3304
+ normalizeOperation(operation, occurrences, instants) {
3018
3305
  if (operation.kind !== 'team/thread-read')
3019
3306
  return operation;
3020
3307
  const stamp = (message) => (message.occurredAt === undefined
3021
3308
  ? { ...message, occurredAt: occurrences.get(message.messageRef) ?? operation.occurredAt }
3022
3309
  : { ...message, occurredAt: message.occurredAt });
3023
- const facts = operation.data.facts.map((fact) => (fact.fact.kind === 'message'
3024
- ? { ...fact, fact: { kind: 'message', sequence: fact.fact.sequence, message: stamp(fact.fact.message),
3025
- mentions: fact.fact.mentions } }
3026
- : { ...fact, fact: fact.fact }));
3310
+ const stampEnvelope = (envelope) => envelope.kind === 'message'
3311
+ ? { kind: 'message', sequence: envelope.sequence, message: stamp(envelope.message), mentions: envelope.mentions,
3312
+ occurredAt: envelope.occurredAt ?? envelope.message.occurredAt ?? occurrences.get(envelope.message.messageRef) ?? operation.occurredAt }
3313
+ : { kind: 'activity', sequence: envelope.sequence, activity: envelope.activity,
3314
+ occurredAt: envelope.occurredAt ?? instants.get(envelope.sequence) ?? operation.occurredAt };
3315
+ const facts = operation.data.facts.map((fact) => ({ ...fact, fact: stampEnvelope(fact.fact) }));
3027
3316
  return { ...operation, data: { ...operation.data, anchor: stamp(operation.data.anchor), facts } };
3028
3317
  }
3029
3318
  enqueue(operation) {