@wowyuarm/dsh-agent-team 0.1.3 → 0.1.5

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 (79) hide show
  1. package/README.md +7 -1
  2. package/README.zh.md +12 -3
  3. package/package.json +1 -1
  4. package/packages/agent-team/README.md +3 -3
  5. package/packages/agent-team/README.zh.md +3 -3
  6. package/packages/agent-team/lib/auto-compaction.js +39 -1
  7. package/packages/agent-team/lib/index.js +123 -14
  8. package/packages/agent-team/lib/ledger.js +355 -110
  9. package/packages/agent-team/lib/spec.js +39 -11
  10. package/packages/agent-team/lib/typert.host.js +257 -86
  11. package/packages/agent-team/lib/typert.remote-client.d.ts +5 -1
  12. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  13. package/packages/agent-team/lib/typert.remote-client.js +196 -59
  14. package/packages/agent-team/lib/types/auto-compaction.d.ts +9 -0
  15. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -1
  16. package/packages/agent-team/lib/types/index.d.ts +24 -7
  17. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  18. package/packages/agent-team/lib/types/ledger.d.ts +36 -2
  19. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  20. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  21. package/packages/agent-team/lib/types/types.d.ts +109 -30
  22. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  23. package/packages/agent-team/preset/team-member/agent.cordis.yml +1 -1
  24. package/packages/client-agent-team/README.md +2 -1
  25. package/packages/client-agent-team/README.zh.md +2 -1
  26. package/packages/client-agent-team/lib/client.js +1082 -380
  27. package/packages/client-agent-team/lib/client.js.map +1 -1
  28. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +2 -1
  29. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  30. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +73 -15
  31. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  32. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +34 -10
  33. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  34. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +3 -1
  35. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +6 -1
  36. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  37. package/packages/client-agent-team/lib/types/client/TeamComposer.js +76 -20
  38. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  40. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  41. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -1
  42. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +16 -1
  43. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +4 -1
  44. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamMessage.js +62 -25
  46. package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts +3 -2
  47. package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts.map +1 -1
  48. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts +5 -2
  49. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.js +2 -4
  51. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +2 -1
  52. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +92 -18
  54. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  56. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +5 -2
  57. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/index.js +16 -13
  59. package/packages/client-agent-team/lib/types/client/locales.d.ts +40 -0
  60. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  61. package/packages/client-agent-team/lib/types/client/locales.js +40 -0
  62. package/packages/client-agent-team/lib/types/client/navigation.d.ts +1 -1
  63. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/navigation.js +12 -6
  65. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts +16 -0
  66. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts.map +1 -0
  67. package/packages/client-agent-team/lib/types/client/sidebar-sections.js +102 -0
  68. package/packages/client-agent-team/lib/types/client/slots.d.ts +3 -1
  69. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +1 -1
  71. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/task-refs.js +1 -1
  73. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
  74. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  75. package/packages/client-agent-team/lib/types/client/team-formatters.js +22 -2
  76. package/packages/tool-agent-team/README.md +7 -7
  77. package/packages/tool-agent-team/README.zh.md +7 -7
  78. package/packages/tool-agent-team/lib/index.js +38 -28
  79. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -16,7 +16,7 @@ export function agentTeamHumanActor() {
16
16
  function emptyProjection() {
17
17
  return { byRequest: new Map(), byOperation: new Map(), ordered: [], channels: new Map(), members: new Map(), memberships: new Map(),
18
18
  claims: new Map(), messages: [], tasks: new Map(), threads: new Map(), attention: new Map(), directMarkers: new Map(), activityMarkers: new Map(),
19
- orderedFacts: [], factsByThread: new Map(), topLevelMessages: [], mentionsByMessage: new Map(), messageCountByThread: new Map(),
19
+ orderedFacts: [], factsByThread: new Map(), channelRefByThread: new Map(), mentionsByMessage: new Map(), messageCountByThread: new Map(),
20
20
  attentionByThread: new Map() };
21
21
  }
22
22
  /** Exhaustiveness guard for the closed operation union: adding a kind must update every dispatch. */
@@ -195,6 +195,35 @@ export class AgentTeamLedger {
195
195
  resumeMember(request) {
196
196
  return this.setMemberState(request, 'enabled');
197
197
  }
198
+ /**
199
+ * Move one enabled Member onto a fresh Session: the durable operation
200
+ * records the sessionId transition while identity, memory, and binding
201
+ * survive. The Host disposes the old handle and archives the previous
202
+ * Session log around this write, so the next turn starts empty.
203
+ */
204
+ renewMemberSession(request) {
205
+ return this.enqueue(async () => {
206
+ const existing = this.state.byRequest.get(request.requestId);
207
+ if (existing !== undefined) {
208
+ this.assertSameMemberSessionRenewed(existing, request);
209
+ return this.resolved(this.memberResult(existing));
210
+ }
211
+ this.assertHumanActor(request.actor);
212
+ const prior = this.requireMember(request.memberId);
213
+ if (prior.state !== 'enabled')
214
+ throw new Error(`Agent Member '${prior.handle}' is ${prior.state}; only enabled Members can start from a new context`);
215
+ if (prior.sessionId === request.sessionId)
216
+ throw new Error(`Agent Member '${prior.handle}' already runs Session '${request.sessionId}'`);
217
+ const member = Object.freeze({ ...prior, sessionId: request.sessionId });
218
+ const operation = Object.freeze({
219
+ ...this.operationBase(request, this.nextSequence()), kind: 'team/member-session-renewed',
220
+ data: Object.freeze({ member, previousSessionId: prior.sessionId }),
221
+ });
222
+ await this.table.put(operation.operationId, operation);
223
+ this.apply(operation);
224
+ return this.committed(this.memberResult(operation));
225
+ });
226
+ }
198
227
  getMember(memberId) {
199
228
  return this.state.members.get(memberId);
200
229
  }
@@ -282,12 +311,14 @@ export class AgentTeamLedger {
282
311
  // Human-gated (reply path).
283
312
  const sequence = this.nextSequence();
284
313
  const base = this.operationBase(request, sequence);
285
- const taskRef = this.ref('task');
314
+ const asTask = request.asTask !== false;
286
315
  const threadRef = this.ref('thread');
287
- const task = Object.freeze({ taskRef, channelRef: channel.channelRef, threadRef, status: 'todo', resolution: 'open' });
288
- const thread = Object.freeze({ threadRef, taskRef, revision: sequence });
316
+ const taskRef = asTask ? this.ref('task') : undefined;
317
+ const task = taskRef === undefined ? undefined
318
+ : Object.freeze({ taskRef, channelRef: channel.channelRef, threadRef, status: 'todo', resolution: 'open' });
319
+ const thread = Object.freeze({ threadRef, ...(taskRef === undefined ? {} : { taskRef }), revision: sequence });
289
320
  const message = Object.freeze({
290
- messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, taskRef,
321
+ messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, ...(taskRef === undefined ? {} : { taskRef }),
291
322
  sender: request.actor.memberId, body,
292
323
  ...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
293
324
  topLevel: true, sequence, occurredAt: base.occurredAt,
@@ -298,7 +329,8 @@ export class AgentTeamLedger {
298
329
  const inbox = this.messageInboxDelta(message, request.actor.memberId, recipients, started);
299
330
  const operation = Object.freeze({
300
331
  ...base, kind: 'team/message-sent',
301
- data: Object.freeze({ workspaceId: request.workspaceId, mentions: recipients, message, task, thread, inbox }),
332
+ data: Object.freeze({ workspaceId: request.workspaceId, mentions: recipients, message,
333
+ ...(task === undefined ? {} : { task }), thread, inbox }),
302
334
  });
303
335
  await this.table.put(operation.operationId, operation);
304
336
  this.apply(operation);
@@ -314,31 +346,31 @@ export class AgentTeamLedger {
314
346
  return this.resolved(this.replyResult(existing));
315
347
  }
316
348
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
317
- const { task, thread } = this.threadForActor(actor, request.workspaceId, request.taskRef);
349
+ const { task, thread, channelRef } = this.threadContextForActor(actor, request.workspaceId, request);
318
350
  const body = request.body.trim();
319
351
  if (body === '')
320
352
  throw new Error('message body must not be empty');
321
- this.assertMentionTargets(this.requireChannel(request.workspaceId, task.channelRef), recipients);
353
+ this.assertMentionTargets(this.requireChannel(request.workspaceId, channelRef), recipients);
322
354
  const deferred = this.deferredThreadWrite(actor.memberId, task, thread, request.baseRevision);
323
355
  if (deferred !== undefined)
324
356
  return this.resolved(deferred);
325
- if (task.resolution === 'closed')
357
+ if (task?.resolution === 'closed')
326
358
  throw new Error(`Task '${task.taskRef}' is closed; reopen it before replying`);
327
359
  const unfollowedAgents = recipients.filter(memberId => this.state.members.has(memberId) && !this.isFollowing(thread.threadRef, memberId));
328
360
  if (unfollowedAgents.length > 0 && actor.kind === 'member') {
329
- return this.resolved(this.memberNotFollowing(request.workspaceId, task.channelRef, unfollowedAgents, task, thread));
361
+ return this.resolved(this.memberNotFollowing(request.workspaceId, channelRef, unfollowedAgents, task, thread));
330
362
  }
331
363
  if (unfollowedAgents.length > 0 && request.confirmationToken === undefined) {
332
- return this.resolved(this.issueConfirmation(request.actor, request.workspaceId, task.channelRef, body, recipients, task, thread));
364
+ return this.resolved(this.issueConfirmation(request.actor, request.workspaceId, channelRef, body, recipients, task, thread));
333
365
  }
334
366
  if (request.confirmationToken !== undefined) {
335
- this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId, task.channelRef, task, thread, body, recipients);
367
+ this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId, channelRef, task, thread, body, recipients);
336
368
  }
337
369
  const sequence = this.nextSequence();
338
370
  const base = this.operationBase(request, sequence);
339
371
  const message = Object.freeze({
340
- messageRef: this.ref('message'), channelRef: task.channelRef, threadRef: task.threadRef,
341
- taskRef: task.taskRef, sender: request.actor.memberId, body,
372
+ messageRef: this.ref('message'), channelRef, threadRef: thread.threadRef,
373
+ ...(task === undefined ? {} : { taskRef: task.taskRef }), sender: request.actor.memberId, body,
342
374
  ...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
343
375
  topLevel: false, sequence, occurredAt: base.occurredAt,
344
376
  });
@@ -348,13 +380,45 @@ export class AgentTeamLedger {
348
380
  const operation = Object.freeze({
349
381
  ...base, kind: 'team/thread-replied',
350
382
  data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
351
- mentions: recipients, message, task, thread: nextThread, inbox }),
383
+ mentions: recipients, message, ...(task === undefined ? {} : { task }), thread: nextThread, inbox }),
352
384
  });
353
385
  await this.table.put(operation.operationId, operation);
354
386
  this.apply(operation);
355
387
  return this.committed(this.replyResult(operation));
356
388
  });
357
389
  }
390
+ promoteThread(request) {
391
+ return this.enqueue(async () => {
392
+ const existing = this.state.byRequest.get(request.requestId);
393
+ if (existing !== undefined) {
394
+ this.assertSamePromotion(existing, request);
395
+ return this.resolved(this.promotionResult(existing));
396
+ }
397
+ this.assertHumanActor(request.actor);
398
+ const { task: existingTask, thread, channelRef } = this.threadContextForActor(request.actor, request.workspaceId, request);
399
+ if (existingTask !== undefined)
400
+ throw new Error(`Thread '${thread.threadRef}' already has Task '${existingTask.taskRef}'`);
401
+ const deferred = this.deferredThreadWrite(request.actor.memberId, undefined, thread, request.baseRevision);
402
+ if (deferred !== undefined)
403
+ return this.resolved(deferred);
404
+ const sequence = this.nextSequence();
405
+ const base = this.operationBase(request, sequence);
406
+ const taskRef = this.ref('task');
407
+ const task = Object.freeze({ taskRef, channelRef, threadRef: thread.threadRef, status: 'todo', resolution: 'open' });
408
+ const nextThread = Object.freeze({ threadRef: thread.threadRef, taskRef, revision: sequence });
409
+ const activity = Object.freeze({ activityRef: this.ref('activity'), kind: 'promote',
410
+ taskRef, threadRef: thread.threadRef, actor: request.actor.memberId, sequence });
411
+ const inbox = this.promoteThreadInbox(activity, thread.threadRef);
412
+ const operation = Object.freeze({
413
+ ...base, kind: 'team/thread-promoted',
414
+ data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
415
+ activity, task, thread: nextThread, inbox }),
416
+ });
417
+ await this.table.put(operation.operationId, operation);
418
+ this.apply(operation);
419
+ return this.committed(this.promotionResult(operation));
420
+ });
421
+ }
358
422
  changeClaim(request) {
359
423
  return this.enqueue(async () => {
360
424
  const existing = this.state.byRequest.get(request.requestId);
@@ -446,8 +510,10 @@ export class AgentTeamLedger {
446
510
  // Acceptance normally waits for every Claim to finish (in_review);
447
511
  // a Human may also accept early while work is in progress, which then
448
512
  // completes the still-active Claims inside the same atomic operation.
449
- if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress') {
450
- throw new Error(`Task '${task.taskRef}' must be in_review or in_progress before acceptance`);
513
+ // A never-claimed todo Task may be accepted directly as well: work
514
+ // finished outside the ledger is declared complete by the Human.
515
+ if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress' && task.status !== 'todo') {
516
+ throw new Error(`Task '${task.taskRef}' must be in_review, in_progress, or todo before acceptance`);
451
517
  }
452
518
  const priorActiveClaims = [...this.state.claims.values()].filter(claim => claim.taskRef === task.taskRef && claim.state === 'active');
453
519
  if (request.action === 'accept' && task.status === 'in_progress' && priorActiveClaims.length === 0) {
@@ -526,10 +592,10 @@ export class AgentTeamLedger {
526
592
  return this.resolved(this.attentionResult(existing));
527
593
  }
528
594
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
529
- const { task, thread } = this.threadForActor(actor, request.workspaceId, request.taskRef);
595
+ const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
530
596
  const current = this.attentionFor(actor.memberId, thread.threadRef);
531
597
  if (request.action === 'follow') {
532
- if (task.resolution === 'closed')
598
+ if (task?.resolution === 'closed')
533
599
  throw new Error(`Task '${task.taskRef}' is closed; reopen it before following`);
534
600
  if (current !== undefined)
535
601
  throw new Error(`Member is already following Thread '${thread.threadRef}'`);
@@ -537,7 +603,7 @@ export class AgentTeamLedger {
537
603
  const operation = Object.freeze({
538
604
  ...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
539
605
  data: Object.freeze({ workspaceId: request.workspaceId, action: 'follow', memberId: actor.memberId,
540
- task, thread, inbox: this.inboxDelta([attention]) }),
606
+ ...(task === undefined ? {} : { task }), thread, inbox: this.inboxDelta([attention]) }),
541
607
  });
542
608
  await this.table.put(operation.operationId, operation);
543
609
  this.apply(operation);
@@ -545,12 +611,12 @@ export class AgentTeamLedger {
545
611
  }
546
612
  if (current === undefined)
547
613
  throw new Error(`Member is already unfollowed from Thread '${thread.threadRef}'`);
548
- if (this.hasActiveClaim(actor.memberId, task.taskRef))
614
+ if (task !== undefined && this.hasActiveClaim(actor.memberId, task.taskRef))
549
615
  throw new Error('Member cannot unfollow while owning an active Claim');
550
616
  const operation = Object.freeze({
551
617
  ...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
552
618
  data: Object.freeze({ workspaceId: request.workspaceId, action: 'unfollow', memberId: actor.memberId,
553
- task, thread, inbox: this.inboxDelta([], [{ memberId: actor.memberId, threadRef: thread.threadRef }], [], this.directMarkersFor(actor.memberId, thread.threadRef)) }),
619
+ ...(task === undefined ? {} : { task }), thread, inbox: this.inboxDelta([], [{ memberId: actor.memberId, threadRef: thread.threadRef }], [], this.directMarkersFor(actor.memberId, thread.threadRef)) }),
554
620
  });
555
621
  await this.table.put(operation.operationId, operation);
556
622
  this.apply(operation);
@@ -559,11 +625,9 @@ export class AgentTeamLedger {
559
625
  }
560
626
  attentionStatus(actor, request) {
561
627
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
562
- const { task, thread } = this.threadForActor(authorized, request.workspaceId, request.taskRef);
628
+ const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
563
629
  const attention = this.attentionFor(authorized.memberId, thread.threadRef);
564
- return attention === undefined
565
- ? Object.freeze({ task, thread })
566
- : Object.freeze({ task, thread, attention });
630
+ return Object.freeze({ ...(task === undefined ? {} : { task }), thread, ...(attention === undefined ? {} : { attention }) });
567
631
  }
568
632
  inbox(actor, request) {
569
633
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
@@ -571,22 +635,22 @@ export class AgentTeamLedger {
571
635
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
572
636
  throw new Error('inbox limit must be an integer between 1 and 100');
573
637
  const items = [];
574
- for (const task of this.state.tasks.values()) {
575
- if (this.state.channels.get(task.channelRef)?.workspaceId !== request.workspaceId)
638
+ for (const thread of this.state.threads.values()) {
639
+ const channelRef = this.channelRefForThread(thread.threadRef);
640
+ if (channelRef === undefined)
576
641
  continue;
577
- if (authorized.kind === 'member' && !this.isChannelMember(task.channelRef, authorized.memberId))
642
+ if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
643
+ continue;
644
+ if (authorized.kind === 'member' && !this.isChannelMember(channelRef, authorized.memberId))
578
645
  continue;
579
- const thread = this.requireThread(task.threadRef);
580
646
  const unread = this.unreadFor(authorized.memberId, thread.threadRef);
581
647
  if (unread.length === 0)
582
648
  continue;
649
+ const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
583
650
  const directCount = unread.filter(item => item.direct).length;
584
651
  const attention = this.attentionFor(authorized.memberId, thread.threadRef);
585
- items.push(attention === undefined
586
- ? Object.freeze({ channelRef: task.channelRef, task, thread, unreadCount: unread.length, directCount,
587
- newestSequence: unread.at(-1).fact.sequence })
588
- : Object.freeze({ channelRef: task.channelRef, task, thread, unreadCount: unread.length, directCount,
589
- newestSequence: unread.at(-1).fact.sequence, attention }));
652
+ items.push(Object.freeze({ channelRef, ...(task === undefined ? {} : { task }), thread, unreadCount: unread.length, directCount,
653
+ newestSequence: unread.at(-1).fact.sequence, ...(attention === undefined ? {} : { attention }) }));
590
654
  }
591
655
  items.sort((left, right) => right.directCount - left.directCount || right.newestSequence - left.newestSequence || left.thread.threadRef.localeCompare(right.thread.threadRef));
592
656
  const selected = items.slice(0, limit);
@@ -611,10 +675,11 @@ export class AgentTeamLedger {
611
675
  return this.resolved(this.threadReadResult(existing));
612
676
  }
613
677
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
614
- const prepared = this.prepareRead(actor.memberId, request.workspaceId, request.taskRef);
678
+ const prepared = this.prepareRead(actor.memberId, request.workspaceId, request);
615
679
  const operation = Object.freeze({
616
680
  ...this.operationBase(request, this.nextSequence()), kind: 'team/thread-read',
617
- data: Object.freeze({ workspaceId: request.workspaceId, memberId: actor.memberId, task: prepared.task,
681
+ data: Object.freeze({ workspaceId: request.workspaceId, memberId: actor.memberId,
682
+ ...(prepared.task === undefined ? {} : { task: prepared.task }),
618
683
  thread: prepared.thread, claims: prepared.claims, anchor: prepared.anchor, anchorMentions: prepared.anchorMentions,
619
684
  facts: prepared.facts,
620
685
  readThroughSequence: prepared.readThroughSequence, remainingUnreadCount: prepared.remainingUnreadCount,
@@ -627,11 +692,11 @@ export class AgentTeamLedger {
627
692
  }
628
693
  threadObservations(actor, request) {
629
694
  this.assertHumanActor(actor);
630
- const task = this.requireTask(request.workspaceId, request.taskRef);
695
+ const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
631
696
  const limit = request.limit ?? 50;
632
697
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
633
698
  throw new Error('observation limit must be an integer between 1 and 100');
634
- const threadRef = task.threadRef;
699
+ const threadRef = thread.threadRef;
635
700
  const current = new Map();
636
701
  const observations = [];
637
702
  for (const operation of this.state.ordered) {
@@ -643,7 +708,7 @@ export class AgentTeamLedger {
643
708
  continue;
644
709
  if (!current.delete(removed.memberId))
645
710
  continue;
646
- observations.push(Object.freeze({ sequence: operation.sequence, threadRef, taskRef: task.taskRef,
711
+ observations.push(Object.freeze({ sequence: operation.sequence, threadRef, ...(task === undefined ? {} : { taskRef: task.taskRef }),
647
712
  memberId: removed.memberId, action: 'unfollow' }));
648
713
  }
649
714
  for (const next of delta.attention.set) {
@@ -654,7 +719,7 @@ export class AgentTeamLedger {
654
719
  // Task creation establishes initial Attention; reads only advance its watermark.
655
720
  if (operation.kind === 'team/message-sent' || (prior !== undefined && prior.startSequence === next.startSequence))
656
721
  continue;
657
- observations.push(Object.freeze({ sequence: operation.sequence, threadRef, taskRef: task.taskRef,
722
+ observations.push(Object.freeze({ sequence: operation.sequence, threadRef, ...(task === undefined ? {} : { taskRef: task.taskRef }),
658
723
  memberId: next.memberId, action: 'follow' }));
659
724
  }
660
725
  }
@@ -677,7 +742,7 @@ export class AgentTeamLedger {
677
742
  }
678
743
  threadHistory(actor, request) {
679
744
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
680
- const { task, thread } = this.threadForActor(authorized, request.workspaceId, request.taskRef);
745
+ const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
681
746
  const limit = request.limit ?? 20;
682
747
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
683
748
  throw new Error('history limit must be an integer between 1 and 100');
@@ -686,9 +751,9 @@ export class AgentTeamLedger {
686
751
  throw new Error('beforeSequence must be a positive integer');
687
752
  const all = this.threadFacts(thread.threadRef).filter(fact => fact.sequence < before);
688
753
  const facts = all.slice(-limit);
689
- const anchor = this.threadAnchor(task);
690
- return Object.freeze({ task, thread, anchor, anchorMentions: this.state.mentionsByMessage.get(anchor.messageRef) ?? [],
691
- claims: this.claimsForTask(task.taskRef), facts: Object.freeze(facts),
754
+ const anchor = this.threadAnchor(thread.threadRef);
755
+ return Object.freeze({ ...(task === undefined ? {} : { task }), thread, anchor, anchorMentions: this.state.mentionsByMessage.get(anchor.messageRef) ?? [],
756
+ claims: task === undefined ? Object.freeze([]) : this.claimsForTask(task.taskRef), facts: Object.freeze(facts),
692
757
  cursor: facts.length === 0 ? before : facts[0].sequence, hasMore: all.length > facts.length });
693
758
  }
694
759
  listClaims(actor, request) {
@@ -736,22 +801,27 @@ export class AgentTeamLedger {
736
801
  }
737
802
  if (request.threadRef !== undefined) {
738
803
  const thread = this.requireThread(request.threadRef);
739
- const task = this.requireTask(request.workspaceId, thread.taskRef);
740
- if (request.channelRef !== undefined && task.channelRef !== request.channelRef)
804
+ const channelRef = this.channelRefForThread(thread.threadRef);
805
+ if (channelRef === undefined)
806
+ throw new Error(`unknown Thread ref '${request.threadRef}'`);
807
+ if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
808
+ throw new Error(`Thread '${request.threadRef}' does not belong to Workspace '${request.workspaceId}'`);
809
+ if (request.channelRef !== undefined && channelRef !== request.channelRef)
741
810
  throw new Error(`Thread '${request.threadRef}' does not belong to Channel '${request.channelRef}'`);
742
- if (memberId !== undefined && !this.isChannelMember(task.channelRef, memberId))
743
- throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${task.channelRef}'`);
811
+ if (memberId !== undefined && !this.isChannelMember(channelRef, memberId))
812
+ throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
744
813
  }
745
814
  const channels = [...this.state.channels.values()].filter(channel => channel.workspaceId === request.workspaceId
746
815
  && (memberId === undefined || this.isChannelMember(channel.channelRef, memberId)));
747
816
  const channelRefs = new Set(channels.map(channel => channel.channelRef));
748
817
  const allFacts = this.state.orderedFacts.filter(fact => {
749
- const task = fact.kind === 'message' ? this.state.tasks.get(fact.message.taskRef) : this.state.tasks.get(fact.activity.taskRef);
750
- if (task === undefined || !channelRefs.has(task.channelRef))
818
+ const threadRef = fact.kind === 'message' ? fact.message.threadRef : fact.activity.threadRef;
819
+ const channelRef = this.channelRefForThread(threadRef);
820
+ if (channelRef === undefined || !channelRefs.has(channelRef))
751
821
  return false;
752
- if (request.channelRef !== undefined && task.channelRef !== request.channelRef)
822
+ if (request.channelRef !== undefined && channelRef !== request.channelRef)
753
823
  return false;
754
- if (request.threadRef !== undefined && task.threadRef !== request.threadRef)
824
+ if (request.threadRef !== undefined && threadRef !== request.threadRef)
755
825
  return false;
756
826
  if (request.topLevelOnly && (fact.kind !== 'message' || !fact.message.topLevel))
757
827
  return false;
@@ -766,9 +836,9 @@ export class AgentTeamLedger {
766
836
  const taskNumbers = this.taskNumbers(request.workspaceId);
767
837
  const items = selected.filter((fact) => fact.kind === 'message').map(fact => {
768
838
  const message = fact.message;
769
- const task = this.requireTask(request.workspaceId, message.taskRef);
770
839
  const thread = this.requireThread(message.threadRef);
771
- return Object.freeze({ message, mentions: fact.mentions, task, thread, taskNumber: taskNumbers.get(task.taskRef) ?? 0,
840
+ const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
841
+ return Object.freeze({ message, mentions: fact.mentions, ...(task === undefined ? {} : { task, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }), thread,
772
842
  messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0 });
773
843
  });
774
844
  const initialization = this.initialization();
@@ -778,7 +848,12 @@ export class AgentTeamLedger {
778
848
  channels: Object.freeze(channels),
779
849
  members: Object.freeze([...this.state.memberships.entries()].filter(([channelRef]) => channelRefs.has(channelRef)).flatMap(([channelRef, ids]) => [...ids].filter(id => this.state.members.get(id)?.state !== 'inactive').sort().map(memberId => Object.freeze({ channelRef, memberId })))),
780
850
  tasks: Object.freeze(visibleTasks),
781
- threads: Object.freeze([...this.state.threads.values()].filter(thread => visibleTasks.some(task => task.taskRef === thread.taskRef))),
851
+ threads: Object.freeze([...this.state.threads.values()].filter(thread => {
852
+ const channelRef = this.channelRefForThread(thread.threadRef);
853
+ return channelRef !== undefined && channelRefs.has(channelRef)
854
+ && (request.channelRef === undefined || channelRef === request.channelRef)
855
+ && (request.threadRef === undefined || thread.threadRef === request.threadRef);
856
+ })),
782
857
  taskNumbers: Object.freeze(visibleTasks.map(task => Object.freeze({ taskRef: task.taskRef, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }))),
783
858
  items: Object.freeze(items),
784
859
  claims: this.claimsForVisibleTasks(visibleTasks),
@@ -816,6 +891,8 @@ export class AgentTeamLedger {
816
891
  case 'team/member-suspended':
817
892
  case 'team/member-resumed':
818
893
  case 'team/member-session-restarted':
894
+ case 'team/member-context-cleared':
895
+ case 'team/member-session-renewed':
819
896
  case 'team/member-updated':
820
897
  case 'team/member-removed':
821
898
  return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
@@ -839,6 +916,8 @@ export class AgentTeamLedger {
839
916
  case 'team/message-sent':
840
917
  case 'team/thread-replied':
841
918
  return [{ kind: 'channel', channelRef: operation.data.message.channelRef }, { kind: 'thread', threadRef: operation.data.message.threadRef }];
919
+ case 'team/thread-promoted':
920
+ return [{ kind: 'channel', channelRef: operation.data.task.channelRef }, { kind: 'thread', threadRef: operation.data.thread.threadRef }];
842
921
  case 'team/claim-created':
843
922
  case 'team/claim-done':
844
923
  case 'team/claim-released':
@@ -885,6 +964,8 @@ export class AgentTeamLedger {
885
964
  case 'team/message-sent':
886
965
  case 'team/thread-replied':
887
966
  return [operation.data.message.threadRef];
967
+ case 'team/thread-promoted':
968
+ return [operation.data.activity.threadRef];
888
969
  case 'team/claim-created':
889
970
  case 'team/claim-done':
890
971
  case 'team/claim-released':
@@ -901,6 +982,8 @@ export class AgentTeamLedger {
901
982
  case 'team/member-suspended':
902
983
  case 'team/member-resumed':
903
984
  case 'team/member-session-restarted':
985
+ case 'team/member-context-cleared':
986
+ case 'team/member-session-renewed':
904
987
  case 'team/member-updated':
905
988
  case 'team/channel-member-added':
906
989
  case 'team/thread-read':
@@ -1010,6 +1093,28 @@ export class AgentTeamLedger {
1010
1093
  throw new Error('invalid Member restart');
1011
1094
  return;
1012
1095
  }
1096
+ if (operation.kind === 'team/member-context-cleared') {
1097
+ assertHuman();
1098
+ const prior = projection.members.get(operation.data.member.memberId);
1099
+ // Legacy in-place clear: the recorded Member is unchanged, and the
1100
+ // projection deliberately does not change either.
1101
+ if (prior === undefined || prior.state !== 'enabled' || !this.sameMemberIdentity(prior, operation.data.member))
1102
+ throw new Error('invalid Member context clear');
1103
+ return;
1104
+ }
1105
+ if (operation.kind === 'team/member-session-renewed') {
1106
+ assertHuman();
1107
+ const prior = projection.members.get(operation.data.member.memberId);
1108
+ // The renewal moves exactly the sessionId: the prior Member must be
1109
+ // enabled on the recorded previous Session, and every other fact must
1110
+ // carry over unchanged.
1111
+ if (prior === undefined || prior.state !== 'enabled'
1112
+ || prior.sessionId !== operation.data.previousSessionId
1113
+ || operation.data.member.sessionId === operation.data.previousSessionId
1114
+ || !this.sameMemberFacts(prior, operation.data.member))
1115
+ throw new Error('invalid Member session renewal');
1116
+ return;
1117
+ }
1013
1118
  if (operation.kind === 'team/channel-updated') {
1014
1119
  assertHuman();
1015
1120
  const prior = projection.channels.get(operation.data.channel.channelRef);
@@ -1051,7 +1156,7 @@ export class AgentTeamLedger {
1051
1156
  if (channel === undefined || member === undefined || operation.data.workspaceId !== channel.workspaceId
1052
1157
  || member.workspaceId !== channel.workspaceId || !projection.memberships.get(channel.channelRef)?.has(member.memberId))
1053
1158
  throw new Error('invalid Channel membership removal');
1054
- const threadRefs = new Set([...projection.tasks.values()].filter(task => task.channelRef === channel.channelRef).map(task => task.threadRef));
1159
+ const threadRefs = new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === channel.channelRef));
1055
1160
  this.validateReleaseCleanup(operation.data, projection, member.memberId, threadRefs, operation.sequence, refs);
1056
1161
  return;
1057
1162
  }
@@ -1060,7 +1165,7 @@ export class AgentTeamLedger {
1060
1165
  const prior = projection.members.get(operation.data.member.memberId);
1061
1166
  if (prior === undefined || prior.state === 'inactive' || operation.data.member.state !== 'inactive' || !this.sameMemberIdentity(prior, operation.data.member))
1062
1167
  throw new Error('invalid Member removal');
1063
- const threadRefs = new Set([...projection.tasks.values()].map(task => task.threadRef));
1168
+ const threadRefs = new Set(projection.threads.keys());
1064
1169
  this.validateReleaseCleanup(operation.data, projection, prior.memberId, threadRefs, operation.sequence, refs);
1065
1170
  return;
1066
1171
  }
@@ -1068,15 +1173,17 @@ export class AgentTeamLedger {
1068
1173
  const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
1069
1174
  if (operation.data.memberId !== operation.actor.memberId)
1070
1175
  throw new Error('Attention operation has wrong actor');
1071
- const task = projection.tasks.get(operation.data.task.taskRef);
1072
1176
  const thread = projection.threads.get(operation.data.thread.threadRef);
1073
- if (task === undefined || thread === undefined || !this.sameTask(task, operation.data.task)
1074
- || !this.sameThread(thread, operation.data.thread)
1075
- || (actor !== undefined && !projection.memberships.get(task.channelRef)?.has(actor.memberId)))
1177
+ const task = operation.data.task;
1178
+ const currentTask = thread?.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
1179
+ const channelRef = thread === undefined ? undefined : this.channelRefForThreadFrom(projection, thread.threadRef);
1180
+ if (thread === undefined || channelRef === undefined || !this.sameThread(thread, operation.data.thread)
1181
+ || (task === undefined ? currentTask !== undefined : currentTask === undefined || !this.sameTask(currentTask, task) || thread.taskRef !== task.taskRef)
1182
+ || (actor !== undefined && !projection.memberships.get(channelRef)?.has(actor.memberId)))
1076
1183
  throw new Error('invalid Attention operation');
1077
1184
  const current = this.attentionForFrom(projection, operation.data.memberId, thread.threadRef);
1078
- if ((operation.data.action === 'follow' && (task.resolution === 'closed' || current !== undefined))
1079
- || (operation.data.action === 'unfollow' && (current === undefined || this.hasActiveClaimFrom(projection, operation.data.memberId, task.taskRef)))) {
1185
+ if ((operation.data.action === 'follow' && (task?.resolution === 'closed' || current !== undefined))
1186
+ || (operation.data.action === 'unfollow' && (current === undefined || (task !== undefined && this.hasActiveClaimFrom(projection, operation.data.memberId, task.taskRef))))) {
1080
1187
  throw new Error('invalid Attention transition');
1081
1188
  }
1082
1189
  const expected = operation.data.action === 'follow'
@@ -1094,9 +1201,12 @@ export class AgentTeamLedger {
1094
1201
  assertHuman();
1095
1202
  if (operation.data.memberId !== operation.actor.memberId)
1096
1203
  throw new Error('Thread read has wrong actor');
1097
- const expected = this.prepareReadFrom(projection, operation.data.memberId, operation.data.workspaceId, operation.data.task.taskRef);
1204
+ const expected = this.prepareReadFrom(projection, operation.data.memberId, operation.data.workspaceId, {
1205
+ threadRef: operation.data.thread.threadRef,
1206
+ ...(operation.data.task === undefined ? {} : { taskRef: operation.data.task.taskRef }),
1207
+ });
1098
1208
  const expectedData = Object.freeze({ workspaceId: operation.data.workspaceId, memberId: operation.data.memberId,
1099
- task: expected.task, thread: expected.thread, claims: expected.claims, anchor: expected.anchor,
1209
+ ...(expected.task === undefined ? {} : { task: expected.task }), thread: expected.thread, claims: expected.claims, anchor: expected.anchor,
1100
1210
  anchorMentions: expected.anchorMentions, facts: expected.facts,
1101
1211
  readThroughSequence: expected.readThroughSequence, remainingUnreadCount: expected.remainingUnreadCount,
1102
1212
  ...(expected.attention === undefined ? {} : { attention: expected.attention }), inbox: expected.inbox });
@@ -1105,27 +1215,56 @@ export class AgentTeamLedger {
1105
1215
  this.validateInboxDelta(operation.data.inbox, projection, refs);
1106
1216
  return;
1107
1217
  }
1218
+ if (operation.kind === 'team/thread-promoted') {
1219
+ assertHuman();
1220
+ const { activity, task, thread } = operation.data;
1221
+ const priorThread = projection.threads.get(thread.threadRef);
1222
+ const channelRef = priorThread === undefined ? undefined : this.channelRefForThreadFrom(projection, thread.threadRef);
1223
+ if (priorThread === undefined || priorThread.taskRef !== undefined || projection.tasks.has(task.taskRef)
1224
+ || channelRef === undefined || task.channelRef !== channelRef
1225
+ || projection.channels.get(channelRef)?.workspaceId !== operation.data.workspaceId
1226
+ || operation.data.baseRevision !== priorThread.revision
1227
+ || task.status !== 'todo' || task.resolution !== 'open'
1228
+ || thread.revision !== operation.sequence || task.threadRef !== thread.threadRef || thread.taskRef !== task.taskRef
1229
+ || !this.sameThread(thread, { ...priorThread, taskRef: task.taskRef, revision: operation.sequence })
1230
+ || activity.kind !== 'promote' || activity.sequence !== operation.sequence || activity.actor !== operation.actor.memberId
1231
+ || activity.taskRef !== task.taskRef || activity.threadRef !== thread.threadRef)
1232
+ throw new Error('invalid Thread promotion');
1233
+ this.addRef(refs, task.taskRef);
1234
+ this.addRef(refs, activity.activityRef);
1235
+ const expectedInbox = this.promoteThreadInboxFrom(projection, activity, thread.threadRef);
1236
+ if (!isDeepStrictEqual(operation.data.inbox, expectedInbox))
1237
+ throw new Error('invalid Promotion inbox projection');
1238
+ this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
1239
+ return;
1240
+ }
1108
1241
  if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
1109
1242
  const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
1110
1243
  const { message, task, thread } = operation.data;
1111
1244
  const channel = projection.channels.get(message.channelRef);
1112
1245
  if (channel === undefined || channel.workspaceId !== operation.data.workspaceId || message.sender !== operation.actor.memberId
1113
- || message.sequence !== operation.sequence || thread.revision !== operation.sequence || message.taskRef !== task.taskRef
1114
- || message.threadRef !== thread.threadRef || task.threadRef !== thread.threadRef || task.channelRef !== channel.channelRef
1246
+ || message.sequence !== operation.sequence || thread.revision !== operation.sequence
1247
+ || message.threadRef !== thread.threadRef
1248
+ || (task === undefined ? message.taskRef !== undefined || thread.taskRef !== undefined
1249
+ : message.taskRef !== task.taskRef || thread.taskRef !== task.taskRef || task.threadRef !== thread.threadRef || task.channelRef !== channel.channelRef)
1115
1250
  || (actor !== undefined && !projection.memberships.get(channel.channelRef)?.has(actor.memberId)))
1116
1251
  throw new Error('invalid Message operation');
1117
1252
  if (operation.kind === 'team/message-sent') {
1118
- if (!message.topLevel || projection.tasks.has(task.taskRef) || projection.threads.has(thread.threadRef))
1253
+ if (!message.topLevel || projection.threads.has(thread.threadRef) || (task !== undefined && projection.tasks.has(task.taskRef))) {
1119
1254
  throw new Error('invalid top-level Message operation');
1120
- this.addRef(refs, task.taskRef);
1255
+ }
1121
1256
  this.addRef(refs, thread.threadRef);
1257
+ if (task !== undefined)
1258
+ this.addRef(refs, task.taskRef);
1122
1259
  }
1123
1260
  else {
1124
- const priorTask = projection.tasks.get(task.taskRef);
1125
1261
  const priorThread = projection.threads.get(thread.threadRef);
1126
- if (message.topLevel || priorTask === undefined || priorThread === undefined || !this.sameTask(priorTask, task)
1127
- || priorThread.taskRef !== thread.taskRef || operation.data.baseRevision !== priorThread.revision)
1262
+ const priorTask = task === undefined ? undefined : projection.tasks.get(task.taskRef);
1263
+ if (message.topLevel || priorThread === undefined || operation.data.baseRevision !== priorThread.revision
1264
+ || priorThread.taskRef !== thread.taskRef
1265
+ || (task === undefined ? priorTask !== undefined : priorTask === undefined || !this.sameTask(priorTask, task))) {
1128
1266
  throw new Error('invalid Thread reply');
1267
+ }
1129
1268
  }
1130
1269
  this.addRef(refs, message.messageRef);
1131
1270
  this.validateMentions(operation.data.mentions, message, projection);
@@ -1404,6 +1543,15 @@ export class AgentTeamLedger {
1404
1543
  // projection deliberately does not change.
1405
1544
  return;
1406
1545
  }
1546
+ if (operation.kind === 'team/member-context-cleared') {
1547
+ // Legacy audit-only marker: identity, memory path, and binding were
1548
+ // untouched; the Host recreated the live Session empty under the same id.
1549
+ return;
1550
+ }
1551
+ if (operation.kind === 'team/member-session-renewed') {
1552
+ target.members.set(operation.data.member.memberId, operation.data.member);
1553
+ return;
1554
+ }
1407
1555
  if (operation.kind === 'team/channel-updated') {
1408
1556
  target.channels.set(operation.data.channel.channelRef, operation.data.channel);
1409
1557
  return;
@@ -1444,13 +1592,23 @@ export class AgentTeamLedger {
1444
1592
  this.applyInboxDelta(target, operation.data.inbox);
1445
1593
  return;
1446
1594
  }
1595
+ if (operation.kind === 'team/thread-promoted') {
1596
+ this.appendActivityFact(target, operation.data.activity);
1597
+ target.tasks.set(operation.data.task.taskRef, operation.data.task);
1598
+ target.threads.set(operation.data.thread.threadRef, operation.data.thread);
1599
+ this.applyInboxDelta(target, operation.data.inbox);
1600
+ return;
1601
+ }
1447
1602
  if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
1448
1603
  const { message, mentions } = operation.data;
1449
1604
  target.messages.push(message);
1450
1605
  target.mentionsByMessage.set(message.messageRef, Object.freeze([...mentions]));
1451
1606
  this.appendMessageFact(target, message, mentions);
1452
- target.tasks.set(operation.data.task.taskRef, operation.data.task);
1607
+ if (operation.data.task !== undefined)
1608
+ target.tasks.set(operation.data.task.taskRef, operation.data.task);
1453
1609
  target.threads.set(operation.data.thread.threadRef, operation.data.thread);
1610
+ if (message.topLevel)
1611
+ target.channelRefByThread.set(message.threadRef, message.channelRef);
1454
1612
  this.applyInboxDelta(target, operation.data.inbox);
1455
1613
  return;
1456
1614
  }
@@ -1484,8 +1642,6 @@ export class AgentTeamLedger {
1484
1642
  const facts = target.factsByThread.get(message.threadRef) ?? [];
1485
1643
  facts.push(fact);
1486
1644
  target.factsByThread.set(message.threadRef, facts);
1487
- if (message.topLevel)
1488
- target.topLevelMessages.push(message);
1489
1645
  target.messageCountByThread.set(message.threadRef, (target.messageCountByThread.get(message.threadRef) ?? 0) + 1);
1490
1646
  }
1491
1647
  appendActivityFact(target, activity) {
@@ -1520,26 +1676,19 @@ export class AgentTeamLedger {
1520
1676
  for (const marker of delta.activityMarkers.added)
1521
1677
  target.activityMarkers.set(this.activityMarkerKey(marker), marker);
1522
1678
  }
1523
- prepareRead(memberId, workspaceId, taskRef) {
1524
- return this.prepareReadFrom(this.state, memberId, workspaceId, taskRef);
1679
+ prepareRead(memberId, workspaceId, request) {
1680
+ return this.prepareReadFrom(this.state, memberId, workspaceId, request);
1525
1681
  }
1526
1682
  /** Derive the only legal durable result of one Thread read from a prior projection. */
1527
- prepareReadFrom(projection, memberId, workspaceId, taskRef) {
1528
- const task = projection.tasks.get(taskRef);
1529
- if (task === undefined)
1530
- throw new Error(`unknown Task ref '${taskRef}'${this.unknownRefHint(taskRef, 'task', 'Task')}`);
1531
- if (projection.channels.get(task.channelRef)?.workspaceId !== workspaceId)
1532
- throw new Error(`Task '${taskRef}' does not belong to Workspace '${workspaceId}'`);
1533
- const thread = projection.threads.get(task.threadRef);
1534
- if (thread === undefined)
1535
- throw new Error(`unknown Thread ref '${task.threadRef}'`);
1683
+ prepareReadFrom(projection, memberId, workspaceId, request) {
1684
+ const { task, thread, channelRef } = this.threadContextFrom(projection, workspaceId, request);
1536
1685
  if (memberId !== AGENT_TEAM_HUMAN_MEMBER_ID) {
1537
1686
  const member = projection.members.get(memberId);
1538
- if (member === undefined || !projection.memberships.get(task.channelRef)?.has(member.memberId)) {
1539
- throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${task.channelRef}'`);
1687
+ if (member === undefined || !projection.memberships.get(channelRef)?.has(member.memberId)) {
1688
+ throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
1540
1689
  }
1541
1690
  }
1542
- const anchor = this.threadAnchorFrom(projection, task);
1691
+ const anchor = this.threadAnchorFrom(projection, thread.threadRef);
1543
1692
  const attention = this.attentionForFrom(projection, memberId, thread.threadRef);
1544
1693
  const unread = this.unreadForFrom(projection, memberId, thread.threadRef);
1545
1694
  const unreadFacts = unread.slice(0, 20);
@@ -1580,7 +1729,7 @@ export class AgentTeamLedger {
1580
1729
  };
1581
1730
  this.applyInboxDelta(nextProjection, inbox);
1582
1731
  const remainingUnreadCount = this.unreadForFrom(nextProjection, memberId, thread.threadRef).length;
1583
- return Object.freeze({ task, thread, claims: this.claimsForTaskFrom(projection, task.taskRef), anchor,
1732
+ return Object.freeze({ ...(task === undefined ? {} : { task }), thread, claims: task === undefined ? Object.freeze([]) : this.claimsForTaskFrom(projection, task.taskRef), anchor,
1584
1733
  anchorMentions: projection.mentionsByMessage.get(anchor.messageRef) ?? [],
1585
1734
  facts: Object.freeze(combined),
1586
1735
  readThroughSequence, remainingUnreadCount, ...(attention === undefined ? {} : { attention: nextAttention[0] ?? attention }),
@@ -1670,6 +1819,17 @@ export class AgentTeamLedger {
1670
1819
  activityRef: activity.activityRef, sequence: activity.sequence }));
1671
1820
  return this.inboxDelta([], [], [], [], markers);
1672
1821
  }
1822
+ /** Promotion wakes every current follower; the Human actor never follows, so no recipient filter is needed. */
1823
+ promoteThreadInbox(activity, threadRef) {
1824
+ return this.promoteThreadInboxFrom(this.state, activity, threadRef);
1825
+ }
1826
+ promoteThreadInboxFrom(projection, activity, threadRef) {
1827
+ const markers = [...projection.attention.values()]
1828
+ .filter(attention => attention.threadRef === threadRef && attention.memberId !== activity.actor)
1829
+ .map(attention => Object.freeze({ memberId: attention.memberId, threadRef,
1830
+ activityRef: activity.activityRef, sequence: activity.sequence }));
1831
+ return this.inboxDelta([], [], [], [], markers);
1832
+ }
1673
1833
  removeMemberThreadInbox(memberId, threadRefs) {
1674
1834
  const removed = [...this.state.attention.values()].filter(attention => attention.memberId === memberId && threadRefs.has(attention.threadRef))
1675
1835
  .map(attention => Object.freeze({ memberId, threadRef: attention.threadRef }));
@@ -1762,11 +1922,11 @@ export class AgentTeamLedger {
1762
1922
  ...(task === undefined ? {} : { taskRef: task.taskRef }), ...(thread === undefined ? {} : { threadRef: thread.threadRef, revision: thread.revision }) });
1763
1923
  }
1764
1924
  unreadRequired(task, thread, unread) {
1765
- return Object.freeze({ kind: 'unread_required', taskRef: task.taskRef, threadRef: thread.threadRef,
1925
+ return Object.freeze({ kind: 'unread_required', ...(task === undefined ? {} : { taskRef: task.taskRef }), threadRef: thread.threadRef,
1766
1926
  revision: thread.revision, unreadCount: unread.length, directCount: unread.filter(item => item.direct).length });
1767
1927
  }
1768
1928
  staleRevision(task, thread, expectedRevision) {
1769
- return Object.freeze({ kind: 'stale_revision', taskRef: task.taskRef, threadRef: thread.threadRef,
1929
+ return Object.freeze({ kind: 'stale_revision', ...(task === undefined ? {} : { taskRef: task.taskRef }), threadRef: thread.threadRef,
1770
1930
  expectedRevision, revision: thread.revision });
1771
1931
  }
1772
1932
  releaseSummaries(claims, actor, sequence) {
@@ -1819,16 +1979,16 @@ export class AgentTeamLedger {
1819
1979
  /** Commit projections shared by the Message-sent and Thread-replied results. */
1820
1980
  committedMessageResult(operation) {
1821
1981
  return { kind: 'committed', receipt: this.receipt(operation), message: operation.data.message,
1822
- task: operation.data.task, thread: operation.data.thread, attention: operation.data.inbox.attention.set,
1982
+ ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread, attention: operation.data.inbox.attention.set,
1823
1983
  directMarkers: operation.data.inbox.directMarkers.added };
1824
1984
  }
1825
- threadAnchor(task) {
1826
- return this.threadAnchorFrom(this.state, task);
1985
+ threadAnchor(threadRef) {
1986
+ return this.threadAnchorFrom(this.state, threadRef);
1827
1987
  }
1828
- threadAnchorFrom(projection, task) {
1829
- const anchor = projection.messages.find(message => message.taskRef === task.taskRef && message.topLevel);
1988
+ threadAnchorFrom(projection, threadRef) {
1989
+ const anchor = projection.messages.find(message => message.threadRef === threadRef && message.topLevel);
1830
1990
  if (anchor === undefined)
1831
- throw new Error(`Task '${task.taskRef}' has no anchor Message`);
1991
+ throw new Error(`Thread '${threadRef}' has no anchor Message`);
1832
1992
  return anchor;
1833
1993
  }
1834
1994
  threadFactKey(fact) {
@@ -1843,12 +2003,15 @@ export class AgentTeamLedger {
1843
2003
  taskNumbers(workspaceId) {
1844
2004
  const numbers = new Map();
1845
2005
  const next = new Map();
1846
- for (const message of this.state.topLevelMessages) {
1847
- if (this.state.channels.get(message.channelRef)?.workspaceId !== workspaceId)
2006
+ for (const operation of this.state.ordered) {
2007
+ const task = operation.kind === 'team/message-sent' || operation.kind === 'team/thread-promoted' ? operation.data.task : undefined;
2008
+ if (task === undefined)
1848
2009
  continue;
1849
- const ordinal = (next.get(message.channelRef) ?? 0) + 1;
1850
- next.set(message.channelRef, ordinal);
1851
- numbers.set(message.taskRef, ordinal);
2010
+ if (this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
2011
+ continue;
2012
+ const ordinal = (next.get(task.channelRef) ?? 0) + 1;
2013
+ next.set(task.channelRef, ordinal);
2014
+ numbers.set(task.taskRef, ordinal);
1852
2015
  }
1853
2016
  return numbers;
1854
2017
  }
@@ -1932,6 +2095,53 @@ export class AgentTeamLedger {
1932
2095
  throw new Error(`unknown Thread ref '${threadRef}'${this.unknownRefHint(threadRef, 'thread', 'Thread')}`);
1933
2096
  return thread;
1934
2097
  }
2098
+ channelRefForThread(threadRef) {
2099
+ return this.channelRefForThreadFrom(this.state, threadRef);
2100
+ }
2101
+ channelRefForThreadFrom(projection, threadRef) {
2102
+ return projection.channelRefByThread.get(threadRef);
2103
+ }
2104
+ threadContextForActor(actor, workspaceId, request) {
2105
+ const resolved = this.threadContextFrom(this.state, workspaceId, request);
2106
+ if (actor.kind === 'member')
2107
+ this.requireMemberChannel(this.requireMember(actor.memberId), resolved.channelRef);
2108
+ return resolved;
2109
+ }
2110
+ threadContextFrom(projection, workspaceId, request) {
2111
+ if (request.threadRef === undefined && request.taskRef === undefined)
2112
+ throw new Error('Thread addressing requires threadRef or taskRef');
2113
+ let thread;
2114
+ let task;
2115
+ if (request.threadRef !== undefined) {
2116
+ thread = projection.threads.get(request.threadRef);
2117
+ if (thread === undefined)
2118
+ throw new Error(`unknown Thread ref '${request.threadRef}'${this.unknownRefHint(request.threadRef, 'thread', 'Thread')}`);
2119
+ task = thread.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
2120
+ }
2121
+ if (request.taskRef !== undefined) {
2122
+ const byTask = projection.tasks.get(request.taskRef);
2123
+ if (byTask === undefined)
2124
+ throw new Error(`unknown Task ref '${request.taskRef}'${this.unknownRefHint(request.taskRef, 'task', 'Task')}`);
2125
+ if (task !== undefined && task.taskRef !== byTask.taskRef)
2126
+ throw new Error(`Task '${request.taskRef}' does not belong to Thread '${request.threadRef}'`);
2127
+ if (thread !== undefined && byTask.threadRef !== thread.threadRef)
2128
+ throw new Error(`Task '${request.taskRef}' does not belong to Thread '${thread.threadRef}'`);
2129
+ task = byTask;
2130
+ thread = projection.threads.get(byTask.threadRef);
2131
+ if (thread === undefined)
2132
+ throw new Error(`unknown Thread ref '${byTask.threadRef}'`);
2133
+ }
2134
+ if (thread === undefined)
2135
+ throw new Error('unknown Thread');
2136
+ const channelRef = this.channelRefForThreadFrom(projection, thread.threadRef);
2137
+ if (channelRef === undefined)
2138
+ throw new Error(`Thread '${thread.threadRef}' has no Channel`);
2139
+ if (projection.channels.get(channelRef)?.workspaceId !== workspaceId)
2140
+ throw new Error(`Thread '${thread.threadRef}' does not belong to Workspace '${workspaceId}'`);
2141
+ if (task !== undefined && task.channelRef !== channelRef)
2142
+ throw new Error(`Task '${task.taskRef}' does not belong to Thread '${thread.threadRef}'`);
2143
+ return { ...(task === undefined ? {} : { task }), thread, channelRef };
2144
+ }
1935
2145
  requireChannel(workspaceId, channelRef) {
1936
2146
  const channel = this.state.channels.get(channelRef);
1937
2147
  if (channel === undefined)
@@ -2032,6 +2242,11 @@ export class AgentTeamLedger {
2032
2242
  if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.member.memberId !== request.memberId)
2033
2243
  this.throwRequestCollision(request.requestId);
2034
2244
  }
2245
+ assertSameMemberSessionRenewed(operation, request) {
2246
+ if (operation.kind !== 'team/member-session-renewed' || !this.sameActor(operation.actor, request.actor)
2247
+ || operation.data.member.memberId !== request.memberId || operation.data.member.sessionId !== request.sessionId)
2248
+ this.throwRequestCollision(request.requestId);
2249
+ }
2035
2250
  assertSameChannelUpdate(operation, request) {
2036
2251
  if (operation.kind !== 'team/channel-updated' || !this.sameActor(operation.actor, request.actor)
2037
2252
  || operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef
@@ -2059,16 +2274,28 @@ export class AgentTeamLedger {
2059
2274
  if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
2060
2275
  || operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
2061
2276
  || operation.data.message.body !== request.body.trim() || !this.sameList(operation.data.mentions, recipients)
2062
- || !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? []))
2277
+ || !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? [])
2278
+ || (request.asTask !== false) !== (operation.data.task !== undefined))
2063
2279
  this.throwRequestCollision(request.requestId);
2064
2280
  }
2065
2281
  assertSameReply(operation, request, recipients) {
2066
2282
  if (operation.kind !== 'team/thread-replied' || !this.sameActor(operation.actor, request.actor)
2067
- || operation.data.workspaceId !== request.workspaceId || operation.data.message.taskRef !== request.taskRef
2283
+ || operation.data.workspaceId !== request.workspaceId
2284
+ || (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
2285
+ || (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
2286
+ || (request.threadRef === undefined && request.taskRef === undefined)
2068
2287
  || operation.data.message.body !== request.body.trim() || operation.data.baseRevision !== request.baseRevision
2069
2288
  || !this.sameList(operation.data.mentions, recipients))
2070
2289
  this.throwRequestCollision(request.requestId);
2071
2290
  }
2291
+ assertSamePromotion(operation, request) {
2292
+ if (operation.kind !== 'team/thread-promoted' || !this.sameActor(operation.actor, request.actor)
2293
+ || operation.data.workspaceId !== request.workspaceId
2294
+ || operation.data.thread.threadRef !== request.threadRef
2295
+ || operation.data.baseRevision !== request.baseRevision) {
2296
+ this.throwRequestCollision(request.requestId);
2297
+ }
2298
+ }
2072
2299
  assertSameClaim(operation, request) {
2073
2300
  const kind = request.action === 'claim' ? 'team/claim-created' : request.action === 'done' ? 'team/claim-done' : 'team/claim-released';
2074
2301
  if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.workspaceId !== request.workspaceId
@@ -2089,12 +2316,19 @@ export class AgentTeamLedger {
2089
2316
  }
2090
2317
  assertSameAttention(operation, request) {
2091
2318
  if (operation.kind !== 'team/thread-attention-changed' || !this.sameActor(operation.actor, request.actor)
2092
- || operation.data.workspaceId !== request.workspaceId || operation.data.task.taskRef !== request.taskRef || operation.data.action !== request.action)
2319
+ || operation.data.workspaceId !== request.workspaceId
2320
+ || (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
2321
+ || (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
2322
+ || (request.threadRef === undefined && request.taskRef === undefined)
2323
+ || operation.data.action !== request.action)
2093
2324
  this.throwRequestCollision(request.requestId);
2094
2325
  }
2095
2326
  assertSameThreadRead(operation, request) {
2096
2327
  if (operation.kind !== 'team/thread-read' || !this.sameActor(operation.actor, request.actor)
2097
- || operation.data.workspaceId !== request.workspaceId || operation.data.task.taskRef !== request.taskRef)
2328
+ || operation.data.workspaceId !== request.workspaceId
2329
+ || (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
2330
+ || (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
2331
+ || (request.threadRef === undefined && request.taskRef === undefined))
2098
2332
  this.throwRequestCollision(request.requestId);
2099
2333
  }
2100
2334
  sameTask(left, right) {
@@ -2119,6 +2353,13 @@ export class AgentTeamLedger {
2119
2353
  && isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
2120
2354
  && left.privateMemoryPath === right.privateMemoryPath;
2121
2355
  }
2356
+ /** sameMemberIdentity minus the sessionId: every durable Member fact a session renewal must carry over unchanged. */
2357
+ sameMemberFacts(left, right) {
2358
+ return left.memberId === right.memberId && left.workspaceId === right.workspaceId
2359
+ && left.handle === right.handle && left.description === right.description && left.presetId === right.presetId
2360
+ && isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
2361
+ && left.privateMemoryPath === right.privateMemoryPath;
2362
+ }
2122
2363
  sameActor(left, right) {
2123
2364
  return left.kind === right.kind && left.memberId === right.memberId && left.handle === right.handle;
2124
2365
  }
@@ -2180,13 +2421,17 @@ export class AgentTeamLedger {
2180
2421
  task: operation.data.task, thread: operation.data.thread, claims: operation.data.claims });
2181
2422
  }
2182
2423
  attentionResult(operation) {
2183
- return Object.freeze({ receipt: this.receipt(operation), task: operation.data.task, thread: operation.data.thread,
2424
+ return Object.freeze({ receipt: this.receipt(operation), ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread,
2184
2425
  ...(operation.data.inbox.attention.set[0] === undefined ? {} : { attention: operation.data.inbox.attention.set[0] }) });
2185
2426
  }
2427
+ promotionResult(operation) {
2428
+ return Object.freeze({ kind: 'committed', receipt: this.receipt(operation), activity: operation.data.activity,
2429
+ task: operation.data.task, thread: operation.data.thread });
2430
+ }
2186
2431
  threadReadResult(operation) {
2187
2432
  // Loaded records are normalized by sortedRecords(); fresh writes always carry instants.
2188
2433
  const { anchor, facts } = operation.data;
2189
- return Object.freeze({ receipt: this.receipt(operation), task: operation.data.task, thread: operation.data.thread,
2434
+ return Object.freeze({ receipt: this.receipt(operation), ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread,
2190
2435
  claims: operation.data.claims, anchor, anchorMentions: operation.data.anchorMentions, facts,
2191
2436
  readThroughSequence: operation.data.readThroughSequence, remainingUnreadCount: operation.data.remainingUnreadCount,
2192
2437
  ...(operation.data.attention === undefined ? {} : { attention: operation.data.attention }),