@wowyuarm/dsh-agent-team 0.1.3 → 0.1.4

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 (61) hide show
  1. package/README.md +7 -1
  2. package/README.zh.md +7 -1
  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/index.js +38 -9
  7. package/packages/agent-team/lib/ledger.js +275 -108
  8. package/packages/agent-team/lib/spec.js +27 -11
  9. package/packages/agent-team/lib/typert.host.js +185 -86
  10. package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
  11. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  12. package/packages/agent-team/lib/typert.remote-client.js +139 -59
  13. package/packages/agent-team/lib/types/index.d.ts +12 -7
  14. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  15. package/packages/agent-team/lib/types/ledger.d.ts +16 -2
  16. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  17. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  18. package/packages/agent-team/lib/types/types.d.ts +69 -30
  19. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  20. package/packages/agent-team/preset/team-member/agent.cordis.yml +1 -1
  21. package/packages/client-agent-team/README.md +2 -1
  22. package/packages/client-agent-team/README.zh.md +2 -1
  23. package/packages/client-agent-team/lib/client.js +625 -319
  24. package/packages/client-agent-team/lib/client.js.map +1 -1
  25. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +34 -12
  26. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  27. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +14 -10
  28. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +6 -1
  29. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  30. package/packages/client-agent-team/lib/types/client/TeamComposer.js +4 -4
  31. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  32. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  33. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  34. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +4 -1
  35. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamMessage.js +16 -9
  37. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +2 -1
  38. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +71 -16
  40. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -1
  42. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  43. package/packages/client-agent-team/lib/types/client/index.js +1 -0
  44. package/packages/client-agent-team/lib/types/client/locales.d.ts +24 -0
  45. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/locales.js +24 -0
  47. package/packages/client-agent-team/lib/types/client/navigation.d.ts +1 -1
  48. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  49. package/packages/client-agent-team/lib/types/client/navigation.js +12 -6
  50. package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -1
  51. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  52. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +1 -1
  53. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/task-refs.js +1 -1
  55. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
  56. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/team-formatters.js +13 -0
  58. package/packages/tool-agent-team/README.md +7 -7
  59. package/packages/tool-agent-team/README.zh.md +7 -7
  60. package/packages/tool-agent-team/lib/index.js +38 -28
  61. 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. */
@@ -282,12 +282,14 @@ export class AgentTeamLedger {
282
282
  // Human-gated (reply path).
283
283
  const sequence = this.nextSequence();
284
284
  const base = this.operationBase(request, sequence);
285
- const taskRef = this.ref('task');
285
+ const asTask = request.asTask !== false;
286
286
  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 });
287
+ const taskRef = asTask ? this.ref('task') : undefined;
288
+ const task = taskRef === undefined ? undefined
289
+ : Object.freeze({ taskRef, channelRef: channel.channelRef, threadRef, status: 'todo', resolution: 'open' });
290
+ const thread = Object.freeze({ threadRef, ...(taskRef === undefined ? {} : { taskRef }), revision: sequence });
289
291
  const message = Object.freeze({
290
- messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, taskRef,
292
+ messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, ...(taskRef === undefined ? {} : { taskRef }),
291
293
  sender: request.actor.memberId, body,
292
294
  ...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
293
295
  topLevel: true, sequence, occurredAt: base.occurredAt,
@@ -298,7 +300,8 @@ export class AgentTeamLedger {
298
300
  const inbox = this.messageInboxDelta(message, request.actor.memberId, recipients, started);
299
301
  const operation = Object.freeze({
300
302
  ...base, kind: 'team/message-sent',
301
- data: Object.freeze({ workspaceId: request.workspaceId, mentions: recipients, message, task, thread, inbox }),
303
+ data: Object.freeze({ workspaceId: request.workspaceId, mentions: recipients, message,
304
+ ...(task === undefined ? {} : { task }), thread, inbox }),
302
305
  });
303
306
  await this.table.put(operation.operationId, operation);
304
307
  this.apply(operation);
@@ -314,31 +317,31 @@ export class AgentTeamLedger {
314
317
  return this.resolved(this.replyResult(existing));
315
318
  }
316
319
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
317
- const { task, thread } = this.threadForActor(actor, request.workspaceId, request.taskRef);
320
+ const { task, thread, channelRef } = this.threadContextForActor(actor, request.workspaceId, request);
318
321
  const body = request.body.trim();
319
322
  if (body === '')
320
323
  throw new Error('message body must not be empty');
321
- this.assertMentionTargets(this.requireChannel(request.workspaceId, task.channelRef), recipients);
324
+ this.assertMentionTargets(this.requireChannel(request.workspaceId, channelRef), recipients);
322
325
  const deferred = this.deferredThreadWrite(actor.memberId, task, thread, request.baseRevision);
323
326
  if (deferred !== undefined)
324
327
  return this.resolved(deferred);
325
- if (task.resolution === 'closed')
328
+ if (task?.resolution === 'closed')
326
329
  throw new Error(`Task '${task.taskRef}' is closed; reopen it before replying`);
327
330
  const unfollowedAgents = recipients.filter(memberId => this.state.members.has(memberId) && !this.isFollowing(thread.threadRef, memberId));
328
331
  if (unfollowedAgents.length > 0 && actor.kind === 'member') {
329
- return this.resolved(this.memberNotFollowing(request.workspaceId, task.channelRef, unfollowedAgents, task, thread));
332
+ return this.resolved(this.memberNotFollowing(request.workspaceId, channelRef, unfollowedAgents, task, thread));
330
333
  }
331
334
  if (unfollowedAgents.length > 0 && request.confirmationToken === undefined) {
332
- return this.resolved(this.issueConfirmation(request.actor, request.workspaceId, task.channelRef, body, recipients, task, thread));
335
+ return this.resolved(this.issueConfirmation(request.actor, request.workspaceId, channelRef, body, recipients, task, thread));
333
336
  }
334
337
  if (request.confirmationToken !== undefined) {
335
- this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId, task.channelRef, task, thread, body, recipients);
338
+ this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId, channelRef, task, thread, body, recipients);
336
339
  }
337
340
  const sequence = this.nextSequence();
338
341
  const base = this.operationBase(request, sequence);
339
342
  const message = Object.freeze({
340
- messageRef: this.ref('message'), channelRef: task.channelRef, threadRef: task.threadRef,
341
- taskRef: task.taskRef, sender: request.actor.memberId, body,
343
+ messageRef: this.ref('message'), channelRef, threadRef: thread.threadRef,
344
+ ...(task === undefined ? {} : { taskRef: task.taskRef }), sender: request.actor.memberId, body,
342
345
  ...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
343
346
  topLevel: false, sequence, occurredAt: base.occurredAt,
344
347
  });
@@ -348,13 +351,45 @@ export class AgentTeamLedger {
348
351
  const operation = Object.freeze({
349
352
  ...base, kind: 'team/thread-replied',
350
353
  data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
351
- mentions: recipients, message, task, thread: nextThread, inbox }),
354
+ mentions: recipients, message, ...(task === undefined ? {} : { task }), thread: nextThread, inbox }),
352
355
  });
353
356
  await this.table.put(operation.operationId, operation);
354
357
  this.apply(operation);
355
358
  return this.committed(this.replyResult(operation));
356
359
  });
357
360
  }
361
+ promoteThread(request) {
362
+ return this.enqueue(async () => {
363
+ const existing = this.state.byRequest.get(request.requestId);
364
+ if (existing !== undefined) {
365
+ this.assertSamePromotion(existing, request);
366
+ return this.resolved(this.promotionResult(existing));
367
+ }
368
+ this.assertHumanActor(request.actor);
369
+ const { task: existingTask, thread, channelRef } = this.threadContextForActor(request.actor, request.workspaceId, request);
370
+ if (existingTask !== undefined)
371
+ throw new Error(`Thread '${thread.threadRef}' already has Task '${existingTask.taskRef}'`);
372
+ const deferred = this.deferredThreadWrite(request.actor.memberId, undefined, thread, request.baseRevision);
373
+ if (deferred !== undefined)
374
+ return this.resolved(deferred);
375
+ const sequence = this.nextSequence();
376
+ const base = this.operationBase(request, sequence);
377
+ const taskRef = this.ref('task');
378
+ const task = Object.freeze({ taskRef, channelRef, threadRef: thread.threadRef, status: 'todo', resolution: 'open' });
379
+ const nextThread = Object.freeze({ threadRef: thread.threadRef, taskRef, revision: sequence });
380
+ const activity = Object.freeze({ activityRef: this.ref('activity'), kind: 'promote',
381
+ taskRef, threadRef: thread.threadRef, actor: request.actor.memberId, sequence });
382
+ const inbox = this.promoteThreadInbox(activity, thread.threadRef);
383
+ const operation = Object.freeze({
384
+ ...base, kind: 'team/thread-promoted',
385
+ data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
386
+ activity, task, thread: nextThread, inbox }),
387
+ });
388
+ await this.table.put(operation.operationId, operation);
389
+ this.apply(operation);
390
+ return this.committed(this.promotionResult(operation));
391
+ });
392
+ }
358
393
  changeClaim(request) {
359
394
  return this.enqueue(async () => {
360
395
  const existing = this.state.byRequest.get(request.requestId);
@@ -526,10 +561,10 @@ export class AgentTeamLedger {
526
561
  return this.resolved(this.attentionResult(existing));
527
562
  }
528
563
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
529
- const { task, thread } = this.threadForActor(actor, request.workspaceId, request.taskRef);
564
+ const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
530
565
  const current = this.attentionFor(actor.memberId, thread.threadRef);
531
566
  if (request.action === 'follow') {
532
- if (task.resolution === 'closed')
567
+ if (task?.resolution === 'closed')
533
568
  throw new Error(`Task '${task.taskRef}' is closed; reopen it before following`);
534
569
  if (current !== undefined)
535
570
  throw new Error(`Member is already following Thread '${thread.threadRef}'`);
@@ -537,7 +572,7 @@ export class AgentTeamLedger {
537
572
  const operation = Object.freeze({
538
573
  ...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
539
574
  data: Object.freeze({ workspaceId: request.workspaceId, action: 'follow', memberId: actor.memberId,
540
- task, thread, inbox: this.inboxDelta([attention]) }),
575
+ ...(task === undefined ? {} : { task }), thread, inbox: this.inboxDelta([attention]) }),
541
576
  });
542
577
  await this.table.put(operation.operationId, operation);
543
578
  this.apply(operation);
@@ -545,12 +580,12 @@ export class AgentTeamLedger {
545
580
  }
546
581
  if (current === undefined)
547
582
  throw new Error(`Member is already unfollowed from Thread '${thread.threadRef}'`);
548
- if (this.hasActiveClaim(actor.memberId, task.taskRef))
583
+ if (task !== undefined && this.hasActiveClaim(actor.memberId, task.taskRef))
549
584
  throw new Error('Member cannot unfollow while owning an active Claim');
550
585
  const operation = Object.freeze({
551
586
  ...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
552
587
  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)) }),
588
+ ...(task === undefined ? {} : { task }), thread, inbox: this.inboxDelta([], [{ memberId: actor.memberId, threadRef: thread.threadRef }], [], this.directMarkersFor(actor.memberId, thread.threadRef)) }),
554
589
  });
555
590
  await this.table.put(operation.operationId, operation);
556
591
  this.apply(operation);
@@ -559,11 +594,9 @@ export class AgentTeamLedger {
559
594
  }
560
595
  attentionStatus(actor, request) {
561
596
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
562
- const { task, thread } = this.threadForActor(authorized, request.workspaceId, request.taskRef);
597
+ const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
563
598
  const attention = this.attentionFor(authorized.memberId, thread.threadRef);
564
- return attention === undefined
565
- ? Object.freeze({ task, thread })
566
- : Object.freeze({ task, thread, attention });
599
+ return Object.freeze({ ...(task === undefined ? {} : { task }), thread, ...(attention === undefined ? {} : { attention }) });
567
600
  }
568
601
  inbox(actor, request) {
569
602
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
@@ -571,22 +604,22 @@ export class AgentTeamLedger {
571
604
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
572
605
  throw new Error('inbox limit must be an integer between 1 and 100');
573
606
  const items = [];
574
- for (const task of this.state.tasks.values()) {
575
- if (this.state.channels.get(task.channelRef)?.workspaceId !== request.workspaceId)
607
+ for (const thread of this.state.threads.values()) {
608
+ const channelRef = this.channelRefForThread(thread.threadRef);
609
+ if (channelRef === undefined)
576
610
  continue;
577
- if (authorized.kind === 'member' && !this.isChannelMember(task.channelRef, authorized.memberId))
611
+ if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
612
+ continue;
613
+ if (authorized.kind === 'member' && !this.isChannelMember(channelRef, authorized.memberId))
578
614
  continue;
579
- const thread = this.requireThread(task.threadRef);
580
615
  const unread = this.unreadFor(authorized.memberId, thread.threadRef);
581
616
  if (unread.length === 0)
582
617
  continue;
618
+ const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
583
619
  const directCount = unread.filter(item => item.direct).length;
584
620
  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 }));
621
+ items.push(Object.freeze({ channelRef, ...(task === undefined ? {} : { task }), thread, unreadCount: unread.length, directCount,
622
+ newestSequence: unread.at(-1).fact.sequence, ...(attention === undefined ? {} : { attention }) }));
590
623
  }
591
624
  items.sort((left, right) => right.directCount - left.directCount || right.newestSequence - left.newestSequence || left.thread.threadRef.localeCompare(right.thread.threadRef));
592
625
  const selected = items.slice(0, limit);
@@ -611,10 +644,11 @@ export class AgentTeamLedger {
611
644
  return this.resolved(this.threadReadResult(existing));
612
645
  }
613
646
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
614
- const prepared = this.prepareRead(actor.memberId, request.workspaceId, request.taskRef);
647
+ const prepared = this.prepareRead(actor.memberId, request.workspaceId, request);
615
648
  const operation = Object.freeze({
616
649
  ...this.operationBase(request, this.nextSequence()), kind: 'team/thread-read',
617
- data: Object.freeze({ workspaceId: request.workspaceId, memberId: actor.memberId, task: prepared.task,
650
+ data: Object.freeze({ workspaceId: request.workspaceId, memberId: actor.memberId,
651
+ ...(prepared.task === undefined ? {} : { task: prepared.task }),
618
652
  thread: prepared.thread, claims: prepared.claims, anchor: prepared.anchor, anchorMentions: prepared.anchorMentions,
619
653
  facts: prepared.facts,
620
654
  readThroughSequence: prepared.readThroughSequence, remainingUnreadCount: prepared.remainingUnreadCount,
@@ -627,11 +661,11 @@ export class AgentTeamLedger {
627
661
  }
628
662
  threadObservations(actor, request) {
629
663
  this.assertHumanActor(actor);
630
- const task = this.requireTask(request.workspaceId, request.taskRef);
664
+ const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
631
665
  const limit = request.limit ?? 50;
632
666
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
633
667
  throw new Error('observation limit must be an integer between 1 and 100');
634
- const threadRef = task.threadRef;
668
+ const threadRef = thread.threadRef;
635
669
  const current = new Map();
636
670
  const observations = [];
637
671
  for (const operation of this.state.ordered) {
@@ -643,7 +677,7 @@ export class AgentTeamLedger {
643
677
  continue;
644
678
  if (!current.delete(removed.memberId))
645
679
  continue;
646
- observations.push(Object.freeze({ sequence: operation.sequence, threadRef, taskRef: task.taskRef,
680
+ observations.push(Object.freeze({ sequence: operation.sequence, threadRef, ...(task === undefined ? {} : { taskRef: task.taskRef }),
647
681
  memberId: removed.memberId, action: 'unfollow' }));
648
682
  }
649
683
  for (const next of delta.attention.set) {
@@ -654,7 +688,7 @@ export class AgentTeamLedger {
654
688
  // Task creation establishes initial Attention; reads only advance its watermark.
655
689
  if (operation.kind === 'team/message-sent' || (prior !== undefined && prior.startSequence === next.startSequence))
656
690
  continue;
657
- observations.push(Object.freeze({ sequence: operation.sequence, threadRef, taskRef: task.taskRef,
691
+ observations.push(Object.freeze({ sequence: operation.sequence, threadRef, ...(task === undefined ? {} : { taskRef: task.taskRef }),
658
692
  memberId: next.memberId, action: 'follow' }));
659
693
  }
660
694
  }
@@ -677,7 +711,7 @@ export class AgentTeamLedger {
677
711
  }
678
712
  threadHistory(actor, request) {
679
713
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
680
- const { task, thread } = this.threadForActor(authorized, request.workspaceId, request.taskRef);
714
+ const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
681
715
  const limit = request.limit ?? 20;
682
716
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
683
717
  throw new Error('history limit must be an integer between 1 and 100');
@@ -686,9 +720,9 @@ export class AgentTeamLedger {
686
720
  throw new Error('beforeSequence must be a positive integer');
687
721
  const all = this.threadFacts(thread.threadRef).filter(fact => fact.sequence < before);
688
722
  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),
723
+ const anchor = this.threadAnchor(thread.threadRef);
724
+ return Object.freeze({ ...(task === undefined ? {} : { task }), thread, anchor, anchorMentions: this.state.mentionsByMessage.get(anchor.messageRef) ?? [],
725
+ claims: task === undefined ? Object.freeze([]) : this.claimsForTask(task.taskRef), facts: Object.freeze(facts),
692
726
  cursor: facts.length === 0 ? before : facts[0].sequence, hasMore: all.length > facts.length });
693
727
  }
694
728
  listClaims(actor, request) {
@@ -736,22 +770,27 @@ export class AgentTeamLedger {
736
770
  }
737
771
  if (request.threadRef !== undefined) {
738
772
  const thread = this.requireThread(request.threadRef);
739
- const task = this.requireTask(request.workspaceId, thread.taskRef);
740
- if (request.channelRef !== undefined && task.channelRef !== request.channelRef)
773
+ const channelRef = this.channelRefForThread(thread.threadRef);
774
+ if (channelRef === undefined)
775
+ throw new Error(`unknown Thread ref '${request.threadRef}'`);
776
+ if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
777
+ throw new Error(`Thread '${request.threadRef}' does not belong to Workspace '${request.workspaceId}'`);
778
+ if (request.channelRef !== undefined && channelRef !== request.channelRef)
741
779
  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}'`);
780
+ if (memberId !== undefined && !this.isChannelMember(channelRef, memberId))
781
+ throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
744
782
  }
745
783
  const channels = [...this.state.channels.values()].filter(channel => channel.workspaceId === request.workspaceId
746
784
  && (memberId === undefined || this.isChannelMember(channel.channelRef, memberId)));
747
785
  const channelRefs = new Set(channels.map(channel => channel.channelRef));
748
786
  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))
787
+ const threadRef = fact.kind === 'message' ? fact.message.threadRef : fact.activity.threadRef;
788
+ const channelRef = this.channelRefForThread(threadRef);
789
+ if (channelRef === undefined || !channelRefs.has(channelRef))
751
790
  return false;
752
- if (request.channelRef !== undefined && task.channelRef !== request.channelRef)
791
+ if (request.channelRef !== undefined && channelRef !== request.channelRef)
753
792
  return false;
754
- if (request.threadRef !== undefined && task.threadRef !== request.threadRef)
793
+ if (request.threadRef !== undefined && threadRef !== request.threadRef)
755
794
  return false;
756
795
  if (request.topLevelOnly && (fact.kind !== 'message' || !fact.message.topLevel))
757
796
  return false;
@@ -766,9 +805,9 @@ export class AgentTeamLedger {
766
805
  const taskNumbers = this.taskNumbers(request.workspaceId);
767
806
  const items = selected.filter((fact) => fact.kind === 'message').map(fact => {
768
807
  const message = fact.message;
769
- const task = this.requireTask(request.workspaceId, message.taskRef);
770
808
  const thread = this.requireThread(message.threadRef);
771
- return Object.freeze({ message, mentions: fact.mentions, task, thread, taskNumber: taskNumbers.get(task.taskRef) ?? 0,
809
+ const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
810
+ return Object.freeze({ message, mentions: fact.mentions, ...(task === undefined ? {} : { task, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }), thread,
772
811
  messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0 });
773
812
  });
774
813
  const initialization = this.initialization();
@@ -778,7 +817,12 @@ export class AgentTeamLedger {
778
817
  channels: Object.freeze(channels),
779
818
  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
819
  tasks: Object.freeze(visibleTasks),
781
- threads: Object.freeze([...this.state.threads.values()].filter(thread => visibleTasks.some(task => task.taskRef === thread.taskRef))),
820
+ threads: Object.freeze([...this.state.threads.values()].filter(thread => {
821
+ const channelRef = this.channelRefForThread(thread.threadRef);
822
+ return channelRef !== undefined && channelRefs.has(channelRef)
823
+ && (request.channelRef === undefined || channelRef === request.channelRef)
824
+ && (request.threadRef === undefined || thread.threadRef === request.threadRef);
825
+ })),
782
826
  taskNumbers: Object.freeze(visibleTasks.map(task => Object.freeze({ taskRef: task.taskRef, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }))),
783
827
  items: Object.freeze(items),
784
828
  claims: this.claimsForVisibleTasks(visibleTasks),
@@ -839,6 +883,8 @@ export class AgentTeamLedger {
839
883
  case 'team/message-sent':
840
884
  case 'team/thread-replied':
841
885
  return [{ kind: 'channel', channelRef: operation.data.message.channelRef }, { kind: 'thread', threadRef: operation.data.message.threadRef }];
886
+ case 'team/thread-promoted':
887
+ return [{ kind: 'channel', channelRef: operation.data.task.channelRef }, { kind: 'thread', threadRef: operation.data.thread.threadRef }];
842
888
  case 'team/claim-created':
843
889
  case 'team/claim-done':
844
890
  case 'team/claim-released':
@@ -885,6 +931,8 @@ export class AgentTeamLedger {
885
931
  case 'team/message-sent':
886
932
  case 'team/thread-replied':
887
933
  return [operation.data.message.threadRef];
934
+ case 'team/thread-promoted':
935
+ return [operation.data.activity.threadRef];
888
936
  case 'team/claim-created':
889
937
  case 'team/claim-done':
890
938
  case 'team/claim-released':
@@ -1051,7 +1099,7 @@ export class AgentTeamLedger {
1051
1099
  if (channel === undefined || member === undefined || operation.data.workspaceId !== channel.workspaceId
1052
1100
  || member.workspaceId !== channel.workspaceId || !projection.memberships.get(channel.channelRef)?.has(member.memberId))
1053
1101
  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));
1102
+ const threadRefs = new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === channel.channelRef));
1055
1103
  this.validateReleaseCleanup(operation.data, projection, member.memberId, threadRefs, operation.sequence, refs);
1056
1104
  return;
1057
1105
  }
@@ -1060,7 +1108,7 @@ export class AgentTeamLedger {
1060
1108
  const prior = projection.members.get(operation.data.member.memberId);
1061
1109
  if (prior === undefined || prior.state === 'inactive' || operation.data.member.state !== 'inactive' || !this.sameMemberIdentity(prior, operation.data.member))
1062
1110
  throw new Error('invalid Member removal');
1063
- const threadRefs = new Set([...projection.tasks.values()].map(task => task.threadRef));
1111
+ const threadRefs = new Set(projection.threads.keys());
1064
1112
  this.validateReleaseCleanup(operation.data, projection, prior.memberId, threadRefs, operation.sequence, refs);
1065
1113
  return;
1066
1114
  }
@@ -1068,15 +1116,17 @@ export class AgentTeamLedger {
1068
1116
  const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
1069
1117
  if (operation.data.memberId !== operation.actor.memberId)
1070
1118
  throw new Error('Attention operation has wrong actor');
1071
- const task = projection.tasks.get(operation.data.task.taskRef);
1072
1119
  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)))
1120
+ const task = operation.data.task;
1121
+ const currentTask = thread?.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
1122
+ const channelRef = thread === undefined ? undefined : this.channelRefForThreadFrom(projection, thread.threadRef);
1123
+ if (thread === undefined || channelRef === undefined || !this.sameThread(thread, operation.data.thread)
1124
+ || (task === undefined ? currentTask !== undefined : currentTask === undefined || !this.sameTask(currentTask, task) || thread.taskRef !== task.taskRef)
1125
+ || (actor !== undefined && !projection.memberships.get(channelRef)?.has(actor.memberId)))
1076
1126
  throw new Error('invalid Attention operation');
1077
1127
  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)))) {
1128
+ if ((operation.data.action === 'follow' && (task?.resolution === 'closed' || current !== undefined))
1129
+ || (operation.data.action === 'unfollow' && (current === undefined || (task !== undefined && this.hasActiveClaimFrom(projection, operation.data.memberId, task.taskRef))))) {
1080
1130
  throw new Error('invalid Attention transition');
1081
1131
  }
1082
1132
  const expected = operation.data.action === 'follow'
@@ -1094,9 +1144,12 @@ export class AgentTeamLedger {
1094
1144
  assertHuman();
1095
1145
  if (operation.data.memberId !== operation.actor.memberId)
1096
1146
  throw new Error('Thread read has wrong actor');
1097
- const expected = this.prepareReadFrom(projection, operation.data.memberId, operation.data.workspaceId, operation.data.task.taskRef);
1147
+ const expected = this.prepareReadFrom(projection, operation.data.memberId, operation.data.workspaceId, {
1148
+ threadRef: operation.data.thread.threadRef,
1149
+ ...(operation.data.task === undefined ? {} : { taskRef: operation.data.task.taskRef }),
1150
+ });
1098
1151
  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,
1152
+ ...(expected.task === undefined ? {} : { task: expected.task }), thread: expected.thread, claims: expected.claims, anchor: expected.anchor,
1100
1153
  anchorMentions: expected.anchorMentions, facts: expected.facts,
1101
1154
  readThroughSequence: expected.readThroughSequence, remainingUnreadCount: expected.remainingUnreadCount,
1102
1155
  ...(expected.attention === undefined ? {} : { attention: expected.attention }), inbox: expected.inbox });
@@ -1105,27 +1158,56 @@ export class AgentTeamLedger {
1105
1158
  this.validateInboxDelta(operation.data.inbox, projection, refs);
1106
1159
  return;
1107
1160
  }
1161
+ if (operation.kind === 'team/thread-promoted') {
1162
+ assertHuman();
1163
+ const { activity, task, thread } = operation.data;
1164
+ const priorThread = projection.threads.get(thread.threadRef);
1165
+ const channelRef = priorThread === undefined ? undefined : this.channelRefForThreadFrom(projection, thread.threadRef);
1166
+ if (priorThread === undefined || priorThread.taskRef !== undefined || projection.tasks.has(task.taskRef)
1167
+ || channelRef === undefined || task.channelRef !== channelRef
1168
+ || projection.channels.get(channelRef)?.workspaceId !== operation.data.workspaceId
1169
+ || operation.data.baseRevision !== priorThread.revision
1170
+ || task.status !== 'todo' || task.resolution !== 'open'
1171
+ || thread.revision !== operation.sequence || task.threadRef !== thread.threadRef || thread.taskRef !== task.taskRef
1172
+ || !this.sameThread(thread, { ...priorThread, taskRef: task.taskRef, revision: operation.sequence })
1173
+ || activity.kind !== 'promote' || activity.sequence !== operation.sequence || activity.actor !== operation.actor.memberId
1174
+ || activity.taskRef !== task.taskRef || activity.threadRef !== thread.threadRef)
1175
+ throw new Error('invalid Thread promotion');
1176
+ this.addRef(refs, task.taskRef);
1177
+ this.addRef(refs, activity.activityRef);
1178
+ const expectedInbox = this.promoteThreadInboxFrom(projection, activity, thread.threadRef);
1179
+ if (!isDeepStrictEqual(operation.data.inbox, expectedInbox))
1180
+ throw new Error('invalid Promotion inbox projection');
1181
+ this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
1182
+ return;
1183
+ }
1108
1184
  if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
1109
1185
  const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
1110
1186
  const { message, task, thread } = operation.data;
1111
1187
  const channel = projection.channels.get(message.channelRef);
1112
1188
  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
1189
+ || message.sequence !== operation.sequence || thread.revision !== operation.sequence
1190
+ || message.threadRef !== thread.threadRef
1191
+ || (task === undefined ? message.taskRef !== undefined || thread.taskRef !== undefined
1192
+ : message.taskRef !== task.taskRef || thread.taskRef !== task.taskRef || task.threadRef !== thread.threadRef || task.channelRef !== channel.channelRef)
1115
1193
  || (actor !== undefined && !projection.memberships.get(channel.channelRef)?.has(actor.memberId)))
1116
1194
  throw new Error('invalid Message operation');
1117
1195
  if (operation.kind === 'team/message-sent') {
1118
- if (!message.topLevel || projection.tasks.has(task.taskRef) || projection.threads.has(thread.threadRef))
1196
+ if (!message.topLevel || projection.threads.has(thread.threadRef) || (task !== undefined && projection.tasks.has(task.taskRef))) {
1119
1197
  throw new Error('invalid top-level Message operation');
1120
- this.addRef(refs, task.taskRef);
1198
+ }
1121
1199
  this.addRef(refs, thread.threadRef);
1200
+ if (task !== undefined)
1201
+ this.addRef(refs, task.taskRef);
1122
1202
  }
1123
1203
  else {
1124
- const priorTask = projection.tasks.get(task.taskRef);
1125
1204
  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)
1205
+ const priorTask = task === undefined ? undefined : projection.tasks.get(task.taskRef);
1206
+ if (message.topLevel || priorThread === undefined || operation.data.baseRevision !== priorThread.revision
1207
+ || priorThread.taskRef !== thread.taskRef
1208
+ || (task === undefined ? priorTask !== undefined : priorTask === undefined || !this.sameTask(priorTask, task))) {
1128
1209
  throw new Error('invalid Thread reply');
1210
+ }
1129
1211
  }
1130
1212
  this.addRef(refs, message.messageRef);
1131
1213
  this.validateMentions(operation.data.mentions, message, projection);
@@ -1444,13 +1526,23 @@ export class AgentTeamLedger {
1444
1526
  this.applyInboxDelta(target, operation.data.inbox);
1445
1527
  return;
1446
1528
  }
1529
+ if (operation.kind === 'team/thread-promoted') {
1530
+ this.appendActivityFact(target, operation.data.activity);
1531
+ target.tasks.set(operation.data.task.taskRef, operation.data.task);
1532
+ target.threads.set(operation.data.thread.threadRef, operation.data.thread);
1533
+ this.applyInboxDelta(target, operation.data.inbox);
1534
+ return;
1535
+ }
1447
1536
  if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
1448
1537
  const { message, mentions } = operation.data;
1449
1538
  target.messages.push(message);
1450
1539
  target.mentionsByMessage.set(message.messageRef, Object.freeze([...mentions]));
1451
1540
  this.appendMessageFact(target, message, mentions);
1452
- target.tasks.set(operation.data.task.taskRef, operation.data.task);
1541
+ if (operation.data.task !== undefined)
1542
+ target.tasks.set(operation.data.task.taskRef, operation.data.task);
1453
1543
  target.threads.set(operation.data.thread.threadRef, operation.data.thread);
1544
+ if (message.topLevel)
1545
+ target.channelRefByThread.set(message.threadRef, message.channelRef);
1454
1546
  this.applyInboxDelta(target, operation.data.inbox);
1455
1547
  return;
1456
1548
  }
@@ -1484,8 +1576,6 @@ export class AgentTeamLedger {
1484
1576
  const facts = target.factsByThread.get(message.threadRef) ?? [];
1485
1577
  facts.push(fact);
1486
1578
  target.factsByThread.set(message.threadRef, facts);
1487
- if (message.topLevel)
1488
- target.topLevelMessages.push(message);
1489
1579
  target.messageCountByThread.set(message.threadRef, (target.messageCountByThread.get(message.threadRef) ?? 0) + 1);
1490
1580
  }
1491
1581
  appendActivityFact(target, activity) {
@@ -1520,26 +1610,19 @@ export class AgentTeamLedger {
1520
1610
  for (const marker of delta.activityMarkers.added)
1521
1611
  target.activityMarkers.set(this.activityMarkerKey(marker), marker);
1522
1612
  }
1523
- prepareRead(memberId, workspaceId, taskRef) {
1524
- return this.prepareReadFrom(this.state, memberId, workspaceId, taskRef);
1613
+ prepareRead(memberId, workspaceId, request) {
1614
+ return this.prepareReadFrom(this.state, memberId, workspaceId, request);
1525
1615
  }
1526
1616
  /** 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}'`);
1617
+ prepareReadFrom(projection, memberId, workspaceId, request) {
1618
+ const { task, thread, channelRef } = this.threadContextFrom(projection, workspaceId, request);
1536
1619
  if (memberId !== AGENT_TEAM_HUMAN_MEMBER_ID) {
1537
1620
  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}'`);
1621
+ if (member === undefined || !projection.memberships.get(channelRef)?.has(member.memberId)) {
1622
+ throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
1540
1623
  }
1541
1624
  }
1542
- const anchor = this.threadAnchorFrom(projection, task);
1625
+ const anchor = this.threadAnchorFrom(projection, thread.threadRef);
1543
1626
  const attention = this.attentionForFrom(projection, memberId, thread.threadRef);
1544
1627
  const unread = this.unreadForFrom(projection, memberId, thread.threadRef);
1545
1628
  const unreadFacts = unread.slice(0, 20);
@@ -1580,7 +1663,7 @@ export class AgentTeamLedger {
1580
1663
  };
1581
1664
  this.applyInboxDelta(nextProjection, inbox);
1582
1665
  const remainingUnreadCount = this.unreadForFrom(nextProjection, memberId, thread.threadRef).length;
1583
- return Object.freeze({ task, thread, claims: this.claimsForTaskFrom(projection, task.taskRef), anchor,
1666
+ return Object.freeze({ ...(task === undefined ? {} : { task }), thread, claims: task === undefined ? Object.freeze([]) : this.claimsForTaskFrom(projection, task.taskRef), anchor,
1584
1667
  anchorMentions: projection.mentionsByMessage.get(anchor.messageRef) ?? [],
1585
1668
  facts: Object.freeze(combined),
1586
1669
  readThroughSequence, remainingUnreadCount, ...(attention === undefined ? {} : { attention: nextAttention[0] ?? attention }),
@@ -1670,6 +1753,17 @@ export class AgentTeamLedger {
1670
1753
  activityRef: activity.activityRef, sequence: activity.sequence }));
1671
1754
  return this.inboxDelta([], [], [], [], markers);
1672
1755
  }
1756
+ /** Promotion wakes every current follower; the Human actor never follows, so no recipient filter is needed. */
1757
+ promoteThreadInbox(activity, threadRef) {
1758
+ return this.promoteThreadInboxFrom(this.state, activity, threadRef);
1759
+ }
1760
+ promoteThreadInboxFrom(projection, activity, threadRef) {
1761
+ const markers = [...projection.attention.values()]
1762
+ .filter(attention => attention.threadRef === threadRef && attention.memberId !== activity.actor)
1763
+ .map(attention => Object.freeze({ memberId: attention.memberId, threadRef,
1764
+ activityRef: activity.activityRef, sequence: activity.sequence }));
1765
+ return this.inboxDelta([], [], [], [], markers);
1766
+ }
1673
1767
  removeMemberThreadInbox(memberId, threadRefs) {
1674
1768
  const removed = [...this.state.attention.values()].filter(attention => attention.memberId === memberId && threadRefs.has(attention.threadRef))
1675
1769
  .map(attention => Object.freeze({ memberId, threadRef: attention.threadRef }));
@@ -1762,11 +1856,11 @@ export class AgentTeamLedger {
1762
1856
  ...(task === undefined ? {} : { taskRef: task.taskRef }), ...(thread === undefined ? {} : { threadRef: thread.threadRef, revision: thread.revision }) });
1763
1857
  }
1764
1858
  unreadRequired(task, thread, unread) {
1765
- return Object.freeze({ kind: 'unread_required', taskRef: task.taskRef, threadRef: thread.threadRef,
1859
+ return Object.freeze({ kind: 'unread_required', ...(task === undefined ? {} : { taskRef: task.taskRef }), threadRef: thread.threadRef,
1766
1860
  revision: thread.revision, unreadCount: unread.length, directCount: unread.filter(item => item.direct).length });
1767
1861
  }
1768
1862
  staleRevision(task, thread, expectedRevision) {
1769
- return Object.freeze({ kind: 'stale_revision', taskRef: task.taskRef, threadRef: thread.threadRef,
1863
+ return Object.freeze({ kind: 'stale_revision', ...(task === undefined ? {} : { taskRef: task.taskRef }), threadRef: thread.threadRef,
1770
1864
  expectedRevision, revision: thread.revision });
1771
1865
  }
1772
1866
  releaseSummaries(claims, actor, sequence) {
@@ -1819,16 +1913,16 @@ export class AgentTeamLedger {
1819
1913
  /** Commit projections shared by the Message-sent and Thread-replied results. */
1820
1914
  committedMessageResult(operation) {
1821
1915
  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,
1916
+ ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread, attention: operation.data.inbox.attention.set,
1823
1917
  directMarkers: operation.data.inbox.directMarkers.added };
1824
1918
  }
1825
- threadAnchor(task) {
1826
- return this.threadAnchorFrom(this.state, task);
1919
+ threadAnchor(threadRef) {
1920
+ return this.threadAnchorFrom(this.state, threadRef);
1827
1921
  }
1828
- threadAnchorFrom(projection, task) {
1829
- const anchor = projection.messages.find(message => message.taskRef === task.taskRef && message.topLevel);
1922
+ threadAnchorFrom(projection, threadRef) {
1923
+ const anchor = projection.messages.find(message => message.threadRef === threadRef && message.topLevel);
1830
1924
  if (anchor === undefined)
1831
- throw new Error(`Task '${task.taskRef}' has no anchor Message`);
1925
+ throw new Error(`Thread '${threadRef}' has no anchor Message`);
1832
1926
  return anchor;
1833
1927
  }
1834
1928
  threadFactKey(fact) {
@@ -1843,12 +1937,15 @@ export class AgentTeamLedger {
1843
1937
  taskNumbers(workspaceId) {
1844
1938
  const numbers = new Map();
1845
1939
  const next = new Map();
1846
- for (const message of this.state.topLevelMessages) {
1847
- if (this.state.channels.get(message.channelRef)?.workspaceId !== workspaceId)
1940
+ for (const operation of this.state.ordered) {
1941
+ const task = operation.kind === 'team/message-sent' || operation.kind === 'team/thread-promoted' ? operation.data.task : undefined;
1942
+ if (task === undefined)
1943
+ continue;
1944
+ if (this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
1848
1945
  continue;
1849
- const ordinal = (next.get(message.channelRef) ?? 0) + 1;
1850
- next.set(message.channelRef, ordinal);
1851
- numbers.set(message.taskRef, ordinal);
1946
+ const ordinal = (next.get(task.channelRef) ?? 0) + 1;
1947
+ next.set(task.channelRef, ordinal);
1948
+ numbers.set(task.taskRef, ordinal);
1852
1949
  }
1853
1950
  return numbers;
1854
1951
  }
@@ -1932,6 +2029,53 @@ export class AgentTeamLedger {
1932
2029
  throw new Error(`unknown Thread ref '${threadRef}'${this.unknownRefHint(threadRef, 'thread', 'Thread')}`);
1933
2030
  return thread;
1934
2031
  }
2032
+ channelRefForThread(threadRef) {
2033
+ return this.channelRefForThreadFrom(this.state, threadRef);
2034
+ }
2035
+ channelRefForThreadFrom(projection, threadRef) {
2036
+ return projection.channelRefByThread.get(threadRef);
2037
+ }
2038
+ threadContextForActor(actor, workspaceId, request) {
2039
+ const resolved = this.threadContextFrom(this.state, workspaceId, request);
2040
+ if (actor.kind === 'member')
2041
+ this.requireMemberChannel(this.requireMember(actor.memberId), resolved.channelRef);
2042
+ return resolved;
2043
+ }
2044
+ threadContextFrom(projection, workspaceId, request) {
2045
+ if (request.threadRef === undefined && request.taskRef === undefined)
2046
+ throw new Error('Thread addressing requires threadRef or taskRef');
2047
+ let thread;
2048
+ let task;
2049
+ if (request.threadRef !== undefined) {
2050
+ thread = projection.threads.get(request.threadRef);
2051
+ if (thread === undefined)
2052
+ throw new Error(`unknown Thread ref '${request.threadRef}'${this.unknownRefHint(request.threadRef, 'thread', 'Thread')}`);
2053
+ task = thread.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
2054
+ }
2055
+ if (request.taskRef !== undefined) {
2056
+ const byTask = projection.tasks.get(request.taskRef);
2057
+ if (byTask === undefined)
2058
+ throw new Error(`unknown Task ref '${request.taskRef}'${this.unknownRefHint(request.taskRef, 'task', 'Task')}`);
2059
+ if (task !== undefined && task.taskRef !== byTask.taskRef)
2060
+ throw new Error(`Task '${request.taskRef}' does not belong to Thread '${request.threadRef}'`);
2061
+ if (thread !== undefined && byTask.threadRef !== thread.threadRef)
2062
+ throw new Error(`Task '${request.taskRef}' does not belong to Thread '${thread.threadRef}'`);
2063
+ task = byTask;
2064
+ thread = projection.threads.get(byTask.threadRef);
2065
+ if (thread === undefined)
2066
+ throw new Error(`unknown Thread ref '${byTask.threadRef}'`);
2067
+ }
2068
+ if (thread === undefined)
2069
+ throw new Error('unknown Thread');
2070
+ const channelRef = this.channelRefForThreadFrom(projection, thread.threadRef);
2071
+ if (channelRef === undefined)
2072
+ throw new Error(`Thread '${thread.threadRef}' has no Channel`);
2073
+ if (projection.channels.get(channelRef)?.workspaceId !== workspaceId)
2074
+ throw new Error(`Thread '${thread.threadRef}' does not belong to Workspace '${workspaceId}'`);
2075
+ if (task !== undefined && task.channelRef !== channelRef)
2076
+ throw new Error(`Task '${task.taskRef}' does not belong to Thread '${thread.threadRef}'`);
2077
+ return { ...(task === undefined ? {} : { task }), thread, channelRef };
2078
+ }
1935
2079
  requireChannel(workspaceId, channelRef) {
1936
2080
  const channel = this.state.channels.get(channelRef);
1937
2081
  if (channel === undefined)
@@ -2059,16 +2203,28 @@ export class AgentTeamLedger {
2059
2203
  if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
2060
2204
  || operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
2061
2205
  || 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 ?? []))
2206
+ || !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? [])
2207
+ || (request.asTask !== false) !== (operation.data.task !== undefined))
2063
2208
  this.throwRequestCollision(request.requestId);
2064
2209
  }
2065
2210
  assertSameReply(operation, request, recipients) {
2066
2211
  if (operation.kind !== 'team/thread-replied' || !this.sameActor(operation.actor, request.actor)
2067
- || operation.data.workspaceId !== request.workspaceId || operation.data.message.taskRef !== request.taskRef
2212
+ || operation.data.workspaceId !== request.workspaceId
2213
+ || (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
2214
+ || (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
2215
+ || (request.threadRef === undefined && request.taskRef === undefined)
2068
2216
  || operation.data.message.body !== request.body.trim() || operation.data.baseRevision !== request.baseRevision
2069
2217
  || !this.sameList(operation.data.mentions, recipients))
2070
2218
  this.throwRequestCollision(request.requestId);
2071
2219
  }
2220
+ assertSamePromotion(operation, request) {
2221
+ if (operation.kind !== 'team/thread-promoted' || !this.sameActor(operation.actor, request.actor)
2222
+ || operation.data.workspaceId !== request.workspaceId
2223
+ || operation.data.thread.threadRef !== request.threadRef
2224
+ || operation.data.baseRevision !== request.baseRevision) {
2225
+ this.throwRequestCollision(request.requestId);
2226
+ }
2227
+ }
2072
2228
  assertSameClaim(operation, request) {
2073
2229
  const kind = request.action === 'claim' ? 'team/claim-created' : request.action === 'done' ? 'team/claim-done' : 'team/claim-released';
2074
2230
  if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.workspaceId !== request.workspaceId
@@ -2089,12 +2245,19 @@ export class AgentTeamLedger {
2089
2245
  }
2090
2246
  assertSameAttention(operation, request) {
2091
2247
  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)
2248
+ || operation.data.workspaceId !== request.workspaceId
2249
+ || (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
2250
+ || (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
2251
+ || (request.threadRef === undefined && request.taskRef === undefined)
2252
+ || operation.data.action !== request.action)
2093
2253
  this.throwRequestCollision(request.requestId);
2094
2254
  }
2095
2255
  assertSameThreadRead(operation, request) {
2096
2256
  if (operation.kind !== 'team/thread-read' || !this.sameActor(operation.actor, request.actor)
2097
- || operation.data.workspaceId !== request.workspaceId || operation.data.task.taskRef !== request.taskRef)
2257
+ || operation.data.workspaceId !== request.workspaceId
2258
+ || (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
2259
+ || (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
2260
+ || (request.threadRef === undefined && request.taskRef === undefined))
2098
2261
  this.throwRequestCollision(request.requestId);
2099
2262
  }
2100
2263
  sameTask(left, right) {
@@ -2180,13 +2343,17 @@ export class AgentTeamLedger {
2180
2343
  task: operation.data.task, thread: operation.data.thread, claims: operation.data.claims });
2181
2344
  }
2182
2345
  attentionResult(operation) {
2183
- return Object.freeze({ receipt: this.receipt(operation), task: operation.data.task, thread: operation.data.thread,
2346
+ return Object.freeze({ receipt: this.receipt(operation), ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread,
2184
2347
  ...(operation.data.inbox.attention.set[0] === undefined ? {} : { attention: operation.data.inbox.attention.set[0] }) });
2185
2348
  }
2349
+ promotionResult(operation) {
2350
+ return Object.freeze({ kind: 'committed', receipt: this.receipt(operation), activity: operation.data.activity,
2351
+ task: operation.data.task, thread: operation.data.thread });
2352
+ }
2186
2353
  threadReadResult(operation) {
2187
2354
  // Loaded records are normalized by sortedRecords(); fresh writes always carry instants.
2188
2355
  const { anchor, facts } = operation.data;
2189
- return Object.freeze({ receipt: this.receipt(operation), task: operation.data.task, thread: operation.data.thread,
2356
+ return Object.freeze({ receipt: this.receipt(operation), ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread,
2190
2357
  claims: operation.data.claims, anchor, anchorMentions: operation.data.anchorMentions, facts,
2191
2358
  readThroughSequence: operation.data.readThroughSequence, remainingUnreadCount: operation.data.remainingUnreadCount,
2192
2359
  ...(operation.data.attention === undefined ? {} : { attention: operation.data.attention }),