@wowyuarm/dsh-agent-team 0.1.5 → 0.1.7

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 (100) hide show
  1. package/README.md +23 -3
  2. package/README.zh.md +23 -3
  3. package/package.json +50 -43
  4. package/packages/agent-team/README.md +8 -0
  5. package/packages/agent-team/README.zh.md +8 -0
  6. package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +41 -0
  7. package/packages/agent-team/core-skills/member-skill-manager/references/auth-and-config.md +30 -0
  8. package/packages/agent-team/core-skills/member-skill-manager/references/writing-great-skills.md +17 -0
  9. package/packages/agent-team/lib/index.js +168 -37
  10. package/packages/agent-team/lib/ledger.js +440 -18
  11. package/packages/agent-team/lib/member-context.js +7 -3
  12. package/packages/agent-team/lib/member-runtime.js +229 -0
  13. package/packages/agent-team/lib/member-skills.js +94 -0
  14. package/packages/agent-team/lib/preset-roster.js +1 -0
  15. package/packages/agent-team/lib/spec.js +56 -1
  16. package/packages/agent-team/lib/typert.host.js +359 -77
  17. package/packages/agent-team/lib/typert.remote-client.d.ts +5 -1
  18. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  19. package/packages/agent-team/lib/typert.remote-client.js +248 -43
  20. package/packages/agent-team/lib/types/entities.js +1 -0
  21. package/packages/agent-team/lib/types/index.d.ts +51 -7
  22. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/ledger.d.ts +62 -1
  24. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  26. package/packages/agent-team/lib/types/member-runtime.d.ts +108 -0
  27. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -0
  28. package/packages/agent-team/lib/types/member-skills.d.ts +45 -0
  29. package/packages/agent-team/lib/types/member-skills.d.ts.map +1 -0
  30. package/packages/agent-team/lib/types/operations.js +1 -0
  31. package/packages/agent-team/lib/types/preset-roster.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/requests-results.js +1 -0
  33. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  34. package/packages/agent-team/lib/types/types/entities.d.ts +339 -0
  35. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -0
  36. package/packages/agent-team/lib/types/types/operations.d.ts +303 -0
  37. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/types/requests-results.d.ts +527 -0
  39. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -0
  40. package/packages/agent-team/lib/types/types.d.ts +10 -985
  41. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  42. package/packages/agent-team/preset/team-member/agent.cordis.yml +11 -6
  43. package/packages/client-agent-team/README.md +4 -2
  44. package/packages/client-agent-team/README.zh.md +4 -2
  45. package/packages/client-agent-team/lib/client.js +636 -572
  46. package/packages/client-agent-team/lib/client.js.map +1 -1
  47. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +3 -5
  48. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  49. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +37 -26
  50. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +1 -1
  51. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  52. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +2 -2
  53. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts +2 -1
  54. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +31 -5
  56. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamComposer.js +2 -1
  58. package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts +12 -0
  59. package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts.map +1 -0
  60. package/packages/client-agent-team/lib/types/client/TeamMemberDock.js +13 -0
  61. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +4 -8
  62. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  63. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +7 -40
  64. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  65. package/packages/client-agent-team/lib/types/client/TeamMessage.js +10 -3
  66. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +1 -1
  67. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  68. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +61 -20
  69. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  70. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  71. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  72. package/packages/client-agent-team/lib/types/client/TeamWorkspaceRow.d.ts +1 -1
  73. package/packages/client-agent-team/lib/types/client/TeamWorkspaceRow.d.ts.map +1 -1
  74. package/packages/client-agent-team/lib/types/client/index.d.ts +1 -1
  75. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  76. package/packages/client-agent-team/lib/types/client/index.js +25 -28
  77. package/packages/client-agent-team/lib/types/client/locales.d.ts +32 -16
  78. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  79. package/packages/client-agent-team/lib/types/client/locales.js +32 -16
  80. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +1 -3
  81. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +1 -1
  82. package/packages/client-agent-team/lib/types/client/member-session-input.js +18 -6
  83. package/packages/client-agent-team/lib/types/client/navigation.d.ts +2 -1
  84. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  85. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts +1 -1
  86. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts.map +1 -1
  87. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts +1 -1
  88. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts.map +1 -1
  89. package/packages/client-agent-team/lib/types/client/slots.d.ts +3 -1
  90. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  91. package/packages/client-agent-team/lib/types/client/team-membership.d.ts +1 -1
  92. package/packages/client-agent-team/lib/types/client/team-membership.d.ts.map +1 -1
  93. package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts +5 -7
  94. package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts.map +1 -1
  95. package/packages/client-agent-team/lib/types/client/timeline-scroll.js +9 -22
  96. package/packages/tool-agent-team/lib/index.js +25 -5
  97. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  98. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts +0 -17
  99. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +0 -1
  100. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +0 -135
@@ -23,6 +23,18 @@ function emptyProjection() {
23
23
  function assertUnhandledKind(operation) {
24
24
  throw new Error(`agent-team ledger does not handle operation kind '${operation.kind}'`);
25
25
  }
26
+ /** Deep-freeze a Member capability overlay; absent stays absent. */
27
+ function freezeCapabilities(capabilities) {
28
+ if (capabilities === undefined)
29
+ return {};
30
+ const freezeAllow = (allow) => allow === undefined ? {} : { allow: Object.freeze([...allow]) };
31
+ return {
32
+ capabilities: Object.freeze({
33
+ ...(capabilities.tools === undefined ? {} : { tools: Object.freeze(freezeAllow(capabilities.tools.allow)) }),
34
+ ...(capabilities.skills === undefined ? {} : { skills: Object.freeze(freezeAllow(capabilities.skills.allow)) }),
35
+ }),
36
+ };
37
+ }
26
38
  /** Replay and append logic behind the Agent Team service interface. */
27
39
  export class AgentTeamLedger {
28
40
  table;
@@ -85,7 +97,7 @@ export class AgentTeamLedger {
85
97
  this.assertJoinableMember(request.workspaceId, memberId);
86
98
  const sequence = this.nextSequence();
87
99
  const channel = Object.freeze({
88
- channelRef: this.ref('channel'), workspaceId: request.workspaceId, name, description, createdAtSequence: sequence,
100
+ channelRef: this.ref('channel'), workspaceId: request.workspaceId, name, description, createdAtSequence: sequence, state: 'active',
89
101
  });
90
102
  const operation = Object.freeze({
91
103
  ...this.operationBase(request, sequence), kind: 'team/channel-created',
@@ -109,7 +121,7 @@ export class AgentTeamLedger {
109
121
  const description = request.description.trim();
110
122
  if (name === '')
111
123
  throw new Error('channel name must not be empty');
112
- const channel = Object.freeze({ ...this.requireChannel(request.workspaceId, request.channelRef), name, description });
124
+ const channel = Object.freeze({ ...this.requireActiveChannel(request.workspaceId, request.channelRef), name, description });
113
125
  const operation = Object.freeze({
114
126
  ...this.operationBase(request, this.nextSequence()), kind: 'team/channel-updated',
115
127
  data: Object.freeze({ workspaceId: request.workspaceId, channel }),
@@ -138,12 +150,14 @@ export class AgentTeamLedger {
138
150
  // still drivable through its DM view, and joins Channels later.
139
151
  const channelRefs = this.normalizeUnique(request.channelRefs, 'initial Member Channels');
140
152
  for (const channelRef of channelRefs)
141
- this.requireChannel(request.workspaceId, channelRef);
153
+ this.requireActiveChannel(request.workspaceId, channelRef);
142
154
  this.assertHandleAvailable(request.workspaceId, handle);
143
155
  this.assertModelSelection(request.member.model);
156
+ this.assertCapabilities(request.member.capabilities);
144
157
  const member = Object.freeze({
145
158
  ...request.member, handle, description, presetId, state: 'enabled',
146
159
  ...(request.member.model === undefined ? {} : { model: Object.freeze({ ...request.member.model }) }),
160
+ ...freezeCapabilities(request.member.capabilities),
147
161
  });
148
162
  const operation = Object.freeze({
149
163
  ...this.operationBase(request, this.nextSequence()), kind: 'team/member-added',
@@ -166,6 +180,8 @@ export class AgentTeamLedger {
166
180
  const prior = this.requireMember(request.memberId);
167
181
  if (prior.state === 'inactive')
168
182
  throw new Error(`Agent Member '${prior.memberId}' is inactive and can no longer be edited`);
183
+ if (prior.state === 'archived')
184
+ throw new Error(`Agent Member '${prior.memberId}' is archived and can no longer be edited`);
169
185
  const handle = request.handle.trim();
170
186
  const description = request.description.trim();
171
187
  if (handle === '')
@@ -173,12 +189,15 @@ export class AgentTeamLedger {
173
189
  if (handle !== prior.handle)
174
190
  this.assertHandleAvailable(prior.workspaceId, handle, prior.memberId);
175
191
  this.assertModelSelection(request.model);
176
- // An absent model must CLEAR any override (inherit the Host default);
177
- // spreading `prior` verbatim would silently keep the pinned selection.
178
- const { model: _priorModel, ...priorWithoutModel } = prior;
192
+ this.assertCapabilities(request.capabilities);
193
+ // An absent model or capabilities field must CLEAR any override
194
+ // (inherit the Host default / full standard capability surface);
195
+ // spreading `prior` verbatim would silently keep the pinned value.
196
+ const { model: _priorModel, capabilities: _priorCapabilities, ...priorWithoutOverlays } = prior;
179
197
  const member = Object.freeze({
180
- ...priorWithoutModel, handle, description,
198
+ ...priorWithoutOverlays, handle, description,
181
199
  ...(request.model === undefined ? {} : { model: Object.freeze({ ...request.model }) }),
200
+ ...freezeCapabilities(request.capabilities),
182
201
  });
183
202
  const operation = Object.freeze({
184
203
  ...this.operationBase(request, this.nextSequence()), kind: 'team/member-updated',
@@ -238,7 +257,7 @@ export class AgentTeamLedger {
238
257
  return this.resolved(this.joinResult(existing));
239
258
  }
240
259
  this.assertHumanActor(request.actor);
241
- const channel = this.requireChannel(request.workspaceId, request.channelRef);
260
+ const channel = this.requireActiveChannel(request.workspaceId, request.channelRef);
242
261
  const member = this.requireMember(request.memberId);
243
262
  if (member.workspaceId !== request.workspaceId)
244
263
  throw new Error('Member and Channel must belong to one Workspace');
@@ -290,6 +309,48 @@ export class AgentTeamLedger {
290
309
  return this.committed(this.channelMemberRemovalResult(operation));
291
310
  });
292
311
  }
312
+ /**
313
+ * Archive one Channel: hidden from every surface, facts kept recoverable.
314
+ * Every active Claim on the Channel's Threads releases with one public
315
+ * Activity per (owner, Thread), and every Member's Attention and markers
316
+ * for those Threads clear — a hidden Channel must not leave Tasks stuck in
317
+ * progress behind it or phantom unread counts.
318
+ */
319
+ archiveChannel(request) {
320
+ return this.enqueue(async () => {
321
+ const existing = this.state.byRequest.get(request.requestId);
322
+ if (existing !== undefined) {
323
+ this.assertSameChannelArchival(existing, request);
324
+ return this.resolved(this.channelArchivalResult(existing));
325
+ }
326
+ this.assertHumanActor(request.actor);
327
+ const channel = this.requireChannel(request.workspaceId, request.channelRef);
328
+ if (channel.state === 'archived')
329
+ throw new Error(`Channel '${channel.channelRef}' is already archived`);
330
+ const nextChannel = Object.freeze({ ...channel, state: 'archived' });
331
+ const threadRefs = this.channelThreadRefs(channel.channelRef);
332
+ const releasedClaims = [...this.state.claims.values()]
333
+ .filter(claim => claim.state === 'active' && threadRefs.has(claim.threadRef))
334
+ .map(claim => Object.freeze({ ...claim, state: 'released' }));
335
+ const nextClaims = new Map(this.state.claims);
336
+ for (const claim of releasedClaims)
337
+ nextClaims.set(claim.claimRef, claim);
338
+ const sequence = this.nextSequence();
339
+ const owners = [...new Set(releasedClaims.map(claim => claim.owner))].sort();
340
+ const activities = Object.freeze(owners.flatMap(owner => this.releaseSummaries(releasedClaims.filter(claim => claim.owner === owner), owner, sequence)));
341
+ const threads = this.threadsForActivities(activities);
342
+ const tasks = this.tasksForClaims(releasedClaims, nextClaims);
343
+ const inbox = this.channelArchivalInbox(threadRefs);
344
+ const operation = Object.freeze({
345
+ ...this.operationBase(request, sequence), kind: 'team/channel-archived',
346
+ data: Object.freeze({ workspaceId: request.workspaceId, channel: nextChannel,
347
+ claims: Object.freeze(releasedClaims), activities, tasks, threads, inbox }),
348
+ });
349
+ await this.table.put(operation.operationId, operation);
350
+ this.apply(operation);
351
+ return this.committed(this.channelArchivalResult(operation));
352
+ });
353
+ }
293
354
  sendMessage(request) {
294
355
  return this.enqueue(async () => {
295
356
  const recipients = this.normalizeRecipients(request.actor, request.recipients);
@@ -299,7 +360,7 @@ export class AgentTeamLedger {
299
360
  return this.resolved(this.messageResult(existing));
300
361
  }
301
362
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
302
- const channel = this.requireChannel(request.workspaceId, request.channelRef);
363
+ const channel = this.requireActiveChannel(request.workspaceId, request.channelRef);
303
364
  if (actor.kind === 'member')
304
365
  this.requireMemberChannel(this.requireMember(actor.memberId), channel.channelRef);
305
366
  const body = request.body.trim();
@@ -572,7 +633,9 @@ export class AgentTeamLedger {
572
633
  const activities = this.releaseSummaries(releasedClaims, member.memberId, sequence);
573
634
  const threads = this.threadsForActivities(activities);
574
635
  const tasks = this.tasksForClaims(releasedClaims, projectedClaims);
575
- const threadRefs = new Set([...this.state.tasks.values()].map(task => task.threadRef));
636
+ // The Member leaves entirely, so Attention and markers clear on every
637
+ // Thread — taskless ones too — matching the replay validator's scope.
638
+ const threadRefs = new Set(this.state.threads.keys());
576
639
  const inbox = this.removeMemberThreadInbox(member.memberId, threadRefs);
577
640
  const operation = Object.freeze({
578
641
  ...this.operationBase(request, sequence), kind: 'team/member-removed',
@@ -584,6 +647,49 @@ export class AgentTeamLedger {
584
647
  return this.committed(this.removalResult(operation));
585
648
  });
586
649
  }
650
+ /**
651
+ * Archive one Member: hidden from every surface, data kept recoverable.
652
+ * The release shape matches removal — active Claims release with public
653
+ * Activities and the Member's Attention/markers clear — because a hidden
654
+ * Member must not leave Tasks stuck in progress or phantom unread counts.
655
+ */
656
+ archiveMember(request) {
657
+ return this.enqueue(async () => {
658
+ const existing = this.state.byRequest.get(request.requestId);
659
+ if (existing !== undefined) {
660
+ this.assertSameArchival(existing, request);
661
+ return this.resolved(this.archivalResult(existing));
662
+ }
663
+ this.assertHumanActor(request.actor);
664
+ const member = this.requireMember(request.memberId);
665
+ if (member.state === 'inactive')
666
+ throw new Error(`Agent Member '${member.memberId}' is already inactive`);
667
+ if (member.state === 'archived')
668
+ throw new Error(`Agent Member '${member.memberId}' is already archived`);
669
+ const nextMember = Object.freeze({ ...member, state: 'archived' });
670
+ const releasedClaims = [...this.state.claims.values()].filter(claim => claim.owner === member.memberId && claim.state === 'active')
671
+ .map(claim => Object.freeze({ ...claim, state: 'released' }));
672
+ const projectedClaims = new Map(this.state.claims);
673
+ for (const claim of releasedClaims)
674
+ projectedClaims.set(claim.claimRef, claim);
675
+ const sequence = this.nextSequence();
676
+ const activities = this.releaseSummaries(releasedClaims, member.memberId, sequence);
677
+ const threads = this.threadsForActivities(activities);
678
+ const tasks = this.tasksForClaims(releasedClaims, projectedClaims);
679
+ // The Member leaves entirely, so Attention and markers clear on every
680
+ // Thread — taskless ones too — matching the replay validator's scope.
681
+ const threadRefs = new Set(this.state.threads.keys());
682
+ const inbox = this.removeMemberThreadInbox(member.memberId, threadRefs);
683
+ const operation = Object.freeze({
684
+ ...this.operationBase(request, sequence), kind: 'team/member-archived',
685
+ data: Object.freeze({ member: nextMember, claims: Object.freeze(releasedClaims), activities, tasks, threads, inbox }),
686
+ });
687
+ await this.table.put(operation.operationId, operation);
688
+ this.apply(operation);
689
+ this.confirmations.clear();
690
+ return this.committed(this.archivalResult(operation));
691
+ });
692
+ }
587
693
  changeAttention(request) {
588
694
  return this.enqueue(async () => {
589
695
  const existing = this.state.byRequest.get(request.requestId);
@@ -623,6 +729,74 @@ export class AgentTeamLedger {
623
729
  return this.committed(this.attentionResult(operation));
624
730
  });
625
731
  }
732
+ /**
733
+ * Append one Member-to-Member direct message as an audit-only operation.
734
+ * A DM is pure delivery: no Channel, Thread, revision, attention, or
735
+ * markers change, so apply() is a marker and no projection state moves.
736
+ */
737
+ sendDm(request) {
738
+ return this.enqueue(async () => {
739
+ const existing = this.state.byRequest.get(request.requestId);
740
+ if (existing !== undefined) {
741
+ this.assertSameDm(existing, request);
742
+ return this.resolved(this.dmResult(existing));
743
+ }
744
+ // Only an enabled Member in this Workspace may send a DM; the Human is
745
+ // not a sendable peer, and Members in other Workspaces are unreachable.
746
+ const sender = this.assertActorForWorkspace(request.actor, request.workspaceId);
747
+ if (sender.kind !== 'member')
748
+ throw new Error('agent-team DM requires Member authority');
749
+ const body = request.body.trim();
750
+ if (body === '')
751
+ throw new Error('DM body must not be empty');
752
+ const recipient = this.requireMember(request.recipientMemberId);
753
+ if (recipient.workspaceId !== request.workspaceId)
754
+ throw new Error(`Agent Member '${recipient.memberId}' is not in Workspace '${request.workspaceId}'`);
755
+ if (recipient.state !== 'enabled')
756
+ throw new Error(`Agent Member '${recipient.memberId}' is ${recipient.state}; DM delivery requires an enabled Member`);
757
+ if (recipient.memberId === AGENT_TEAM_HUMAN_MEMBER_ID || !recipient.sessionId)
758
+ throw new Error('DM recipient must be an Agent Member');
759
+ if (recipient.memberId === sender.memberId)
760
+ throw new Error('Members cannot DM themselves');
761
+ const operation = Object.freeze({
762
+ ...this.operationBase(request, this.nextSequence()), kind: 'team/dm-sent',
763
+ data: Object.freeze({ workspaceId: request.workspaceId, senderMemberId: sender.memberId,
764
+ recipientMemberId: recipient.memberId, body }),
765
+ });
766
+ await this.table.put(operation.operationId, operation);
767
+ this.apply(operation);
768
+ return this.committed(this.dmResult(operation));
769
+ });
770
+ }
771
+ /**
772
+ * Bounded adjacent context for a DM relay: the truncated body of the most
773
+ * recent earlier DM between the two sessions, newest first. Returns
774
+ * undefined when this is their first exchange. `excluding` skips the
775
+ * operation id of the DM being delivered right now, so the context line is
776
+ * the adjacent prior exchange rather than a self-reference. Direction
777
+ * labels are from the reader's perspective: the reader is the recipient of
778
+ * the DM being delivered, so messages the reader sent are `you → them`.
779
+ */
780
+ dmHistoryBetween(senderSessionId, recipientMemberId, excluding) {
781
+ const sender = [...this.state.members.values()].find(member => member.sessionId === senderSessionId);
782
+ if (sender === undefined)
783
+ return undefined;
784
+ for (let index = this.state.ordered.length - 1; index >= 0; index -= 1) {
785
+ const operation = this.state.ordered[index];
786
+ if (operation.kind !== 'team/dm-sent' || operation.operationId === excluding)
787
+ continue;
788
+ const pair = operation.data.senderMemberId === sender.memberId && operation.data.recipientMemberId === recipientMemberId;
789
+ const mirror = operation.data.senderMemberId === recipientMemberId && operation.data.recipientMemberId === sender.memberId;
790
+ if (!pair && !mirror)
791
+ continue;
792
+ // pair: the other Member sent it to the reader → (them → you);
793
+ // mirror: the reader sent it to the other Member → (you → them).
794
+ const direction = pair ? 'them → you' : 'you → them';
795
+ const truncated = operation.data.body.length > 160 ? `${operation.data.body.slice(0, 160)}…` : operation.data.body;
796
+ return `(${direction}) ${truncated}`;
797
+ }
798
+ return undefined;
799
+ }
626
800
  attentionStatus(actor, request) {
627
801
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
628
802
  const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
@@ -773,7 +947,11 @@ export class AgentTeamLedger {
773
947
  const resolved = [];
774
948
  for (const taskRef of taskRefs) {
775
949
  const task = this.state.tasks.get(taskRef);
776
- if (task === undefined || this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
950
+ const channel = task === undefined ? undefined : this.state.channels.get(task.channelRef);
951
+ // Archived Channels do not exist on Team API surfaces: their Tasks stop
952
+ // resolving, so live message bodies render the refs as plain
953
+ // non-navigable text through the existing unknown-ref path.
954
+ if (task === undefined || channel?.workspaceId !== workspaceId || channel?.state === 'archived')
777
955
  continue;
778
956
  resolved.push(Object.freeze({ taskRef: task.taskRef, channelRef: task.channelRef, threadRef: task.threadRef, taskNumber: numbers.get(task.taskRef) ?? 0 }));
779
957
  }
@@ -795,7 +973,7 @@ export class AgentTeamLedger {
795
973
  throw new Error('Member cannot view another Workspace');
796
974
  }
797
975
  if (request.channelRef !== undefined) {
798
- this.requireChannel(request.workspaceId, request.channelRef);
976
+ this.requireActiveChannel(request.workspaceId, request.channelRef);
799
977
  if (memberId !== undefined && !this.isChannelMember(request.channelRef, memberId))
800
978
  throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${request.channelRef}'`);
801
979
  }
@@ -806,12 +984,14 @@ export class AgentTeamLedger {
806
984
  throw new Error(`unknown Thread ref '${request.threadRef}'`);
807
985
  if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
808
986
  throw new Error(`Thread '${request.threadRef}' does not belong to Workspace '${request.workspaceId}'`);
987
+ this.assertThreadChannelActive(channelRef);
809
988
  if (request.channelRef !== undefined && channelRef !== request.channelRef)
810
989
  throw new Error(`Thread '${request.threadRef}' does not belong to Channel '${request.channelRef}'`);
811
990
  if (memberId !== undefined && !this.isChannelMember(channelRef, memberId))
812
991
  throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
813
992
  }
814
993
  const channels = [...this.state.channels.values()].filter(channel => channel.workspaceId === request.workspaceId
994
+ && channel.state !== 'archived'
815
995
  && (memberId === undefined || this.isChannelMember(channel.channelRef, memberId)));
816
996
  const channelRefs = new Set(channels.map(channel => channel.channelRef));
817
997
  const allFacts = this.state.orderedFacts.filter(fact => {
@@ -846,7 +1026,10 @@ export class AgentTeamLedger {
846
1026
  return Object.freeze({
847
1027
  humanMemberId: initialization.data.humanMemberId,
848
1028
  channels: Object.freeze(channels),
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 })))),
1029
+ members: Object.freeze([...this.state.memberships.entries()].filter(([channelRef]) => channelRefs.has(channelRef)).flatMap(([channelRef, ids]) => [...ids].filter(id => {
1030
+ const state = this.state.members.get(id)?.state;
1031
+ return state !== 'inactive' && state !== 'archived';
1032
+ }).sort().map(memberId => Object.freeze({ channelRef, memberId })))),
850
1033
  tasks: Object.freeze(visibleTasks),
851
1034
  threads: Object.freeze([...this.state.threads.values()].filter(thread => {
852
1035
  const channelRef = this.channelRefForThread(thread.threadRef);
@@ -896,6 +1079,20 @@ export class AgentTeamLedger {
896
1079
  case 'team/member-updated':
897
1080
  case 'team/member-removed':
898
1081
  return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
1082
+ case 'team/member-archived': {
1083
+ // Claim releases are thread-visible facts: open Channel and Thread
1084
+ // pages refetch alongside the workspace-wide roster change.
1085
+ const channelByTask = new Map(operation.data.tasks.map(task => [task.taskRef, task.channelRef]));
1086
+ const scopes = [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
1087
+ for (const activity of operation.data.activities) {
1088
+ scopes.push({ kind: 'thread', threadRef: activity.threadRef });
1089
+ const channelRef = channelByTask.get(activity.taskRef);
1090
+ if (channelRef !== undefined && !scopes.some(scope => scope.kind === 'channel' && scope.channelRef === channelRef)) {
1091
+ scopes.push({ kind: 'channel', channelRef });
1092
+ }
1093
+ }
1094
+ return scopes;
1095
+ }
899
1096
  case 'team/channel-member-added':
900
1097
  return [{ kind: 'workspace', workspaceId: operation.data.workspaceId }, { kind: 'channel', channelRef: operation.data.channelRef }];
901
1098
  case 'team/channel-member-removed': {
@@ -913,6 +1110,15 @@ export class AgentTeamLedger {
913
1110
  }
914
1111
  return scopes;
915
1112
  }
1113
+ case 'team/channel-archived': {
1114
+ const scopes = [
1115
+ { kind: 'workspace', workspaceId: operation.data.workspaceId },
1116
+ { kind: 'channel', channelRef: operation.data.channel.channelRef },
1117
+ ];
1118
+ for (const activity of operation.data.activities)
1119
+ scopes.push({ kind: 'thread', threadRef: activity.threadRef });
1120
+ return scopes;
1121
+ }
916
1122
  case 'team/message-sent':
917
1123
  case 'team/thread-replied':
918
1124
  return [{ kind: 'channel', channelRef: operation.data.message.channelRef }, { kind: 'thread', threadRef: operation.data.message.threadRef }];
@@ -929,6 +1135,10 @@ export class AgentTeamLedger {
929
1135
  // A read advances only the reader's private watermark; no projection
930
1136
  // visible to other participants changes, so nobody is woken.
931
1137
  return [];
1138
+ case 'team/dm-sent':
1139
+ // A DM changes no shared projection: delivery is a session-level
1140
+ // runtime effect, so no change waiter has anything to refetch.
1141
+ return [];
932
1142
  default:
933
1143
  return assertUnhandledKind(operation);
934
1144
  }
@@ -973,7 +1183,9 @@ export class AgentTeamLedger {
973
1183
  case 'team/thread-attention-changed':
974
1184
  return [operation.data.thread.threadRef];
975
1185
  case 'team/channel-member-removed':
1186
+ case 'team/channel-archived':
976
1187
  case 'team/member-removed':
1188
+ case 'team/member-archived':
977
1189
  return operation.data.activities.map(activity => activity.threadRef);
978
1190
  case 'team/initialized':
979
1191
  case 'team/channel-created':
@@ -987,6 +1199,7 @@ export class AgentTeamLedger {
987
1199
  case 'team/member-updated':
988
1200
  case 'team/channel-member-added':
989
1201
  case 'team/thread-read':
1202
+ case 'team/dm-sent':
990
1203
  return [];
991
1204
  default:
992
1205
  return assertUnhandledKind(operation);
@@ -1119,7 +1332,8 @@ export class AgentTeamLedger {
1119
1332
  assertHuman();
1120
1333
  const prior = projection.channels.get(operation.data.channel.channelRef);
1121
1334
  if (prior === undefined || prior.workspaceId !== operation.data.workspaceId
1122
- || operation.data.channel.workspaceId !== prior.workspaceId || operation.data.channel.createdAtSequence !== prior.createdAtSequence) {
1335
+ || operation.data.channel.workspaceId !== prior.workspaceId || operation.data.channel.createdAtSequence !== prior.createdAtSequence
1336
+ || operation.data.channel.state !== prior.state) {
1123
1337
  throw new Error('invalid Channel update');
1124
1338
  }
1125
1339
  return;
@@ -1127,7 +1341,7 @@ export class AgentTeamLedger {
1127
1341
  if (operation.kind === 'team/member-updated') {
1128
1342
  assertHuman();
1129
1343
  const prior = projection.members.get(operation.data.member.memberId);
1130
- if (prior === undefined || prior.state === 'inactive' || operation.data.member.state !== prior.state
1344
+ if (prior === undefined || prior.state === 'inactive' || prior.state === 'archived' || operation.data.member.state !== prior.state
1131
1345
  || operation.data.member.sessionId !== prior.sessionId || operation.data.member.workspaceId !== prior.workspaceId
1132
1346
  || operation.data.member.presetId !== prior.presetId
1133
1347
  || operation.data.member.privateMemoryPath !== prior.privateMemoryPath)
@@ -1160,6 +1374,18 @@ export class AgentTeamLedger {
1160
1374
  this.validateReleaseCleanup(operation.data, projection, member.memberId, threadRefs, operation.sequence, refs);
1161
1375
  return;
1162
1376
  }
1377
+ if (operation.kind === 'team/channel-archived') {
1378
+ assertHuman();
1379
+ const prior = projection.channels.get(operation.data.channel.channelRef);
1380
+ if (prior === undefined || prior.state !== 'active' || operation.data.channel.state !== 'archived'
1381
+ || operation.data.channel.workspaceId !== prior.workspaceId || operation.data.workspaceId !== prior.workspaceId
1382
+ || operation.data.channel.name !== prior.name || operation.data.channel.description !== prior.description
1383
+ || operation.data.channel.createdAtSequence !== prior.createdAtSequence)
1384
+ throw new Error('invalid Channel archival');
1385
+ const threadRefs = new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === prior.channelRef));
1386
+ this.validateChannelArchivalCleanup(operation.data, projection, threadRefs, operation.sequence, refs);
1387
+ return;
1388
+ }
1163
1389
  if (operation.kind === 'team/member-removed') {
1164
1390
  assertHuman();
1165
1391
  const prior = projection.members.get(operation.data.member.memberId);
@@ -1169,6 +1395,15 @@ export class AgentTeamLedger {
1169
1395
  this.validateReleaseCleanup(operation.data, projection, prior.memberId, threadRefs, operation.sequence, refs);
1170
1396
  return;
1171
1397
  }
1398
+ if (operation.kind === 'team/member-archived') {
1399
+ assertHuman();
1400
+ const prior = projection.members.get(operation.data.member.memberId);
1401
+ if (prior === undefined || (prior.state !== 'enabled' && prior.state !== 'suspended') || operation.data.member.state !== 'archived' || !this.sameMemberIdentity(prior, operation.data.member))
1402
+ throw new Error('invalid Member archival');
1403
+ const threadRefs = new Set(projection.threads.keys());
1404
+ this.validateReleaseCleanup(operation.data, projection, prior.memberId, threadRefs, operation.sequence, refs);
1405
+ return;
1406
+ }
1172
1407
  if (operation.kind === 'team/thread-attention-changed') {
1173
1408
  const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
1174
1409
  if (operation.data.memberId !== operation.actor.memberId)
@@ -1362,6 +1597,18 @@ export class AgentTeamLedger {
1362
1597
  this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
1363
1598
  return;
1364
1599
  }
1600
+ if (operation.kind === 'team/dm-sent') {
1601
+ const sender = assertMember();
1602
+ const recipient = projection.members.get(operation.data.recipientMemberId);
1603
+ if (recipient === undefined || recipient.workspaceId !== operation.data.workspaceId
1604
+ || recipient.state !== 'enabled' || recipient.memberId === AGENT_TEAM_HUMAN_MEMBER_ID
1605
+ || operation.data.senderMemberId !== sender.memberId
1606
+ || operation.data.recipientMemberId === sender.memberId
1607
+ || operation.data.body.trim() === '' || operation.data.body !== operation.data.body.trim()) {
1608
+ throw new Error('invalid DM operation');
1609
+ }
1610
+ return;
1611
+ }
1365
1612
  assertUnhandledKind(operation);
1366
1613
  }
1367
1614
  validateInboxDelta(delta, projection, _refs, additionalThreadRefs = [], additionalMessages = [], additionalActivities = []) {
@@ -1479,6 +1726,68 @@ export class AgentTeamLedger {
1479
1726
  throw new Error('invalid Member inbox cleanup');
1480
1727
  this.validateInboxDelta(data.inbox, projection, refs);
1481
1728
  }
1729
+ /** Replay validation of one Channel archival's release snapshot across every owner. */
1730
+ validateChannelArchivalCleanup(data, projection, threadRefs, sequence, refs) {
1731
+ const releasedClaims = [...projection.claims.values()]
1732
+ .filter(claim => claim.state === 'active' && threadRefs.has(claim.threadRef))
1733
+ .map(claim => Object.freeze({ ...claim, state: 'released' }));
1734
+ if (data.claims.length !== releasedClaims.length || data.claims.some((claim, index) => {
1735
+ const expected = releasedClaims[index];
1736
+ return expected === undefined || !this.sameClaim(expected, claim);
1737
+ }))
1738
+ throw new Error('invalid released Claim projection');
1739
+ // One activity per (owner, Thread), owners sorted like the commit path.
1740
+ const byOwner = new Map();
1741
+ for (const claim of releasedClaims) {
1742
+ const byThread = byOwner.get(claim.owner) ?? new Map();
1743
+ byOwner.set(claim.owner, byThread);
1744
+ byThread.set(claim.threadRef, [...(byThread.get(claim.threadRef) ?? []), claim]);
1745
+ }
1746
+ const expectedActivities = [...byOwner.keys()].sort().flatMap(owner => [...byOwner.get(owner).entries()].map(([threadRef, claims]) => ({
1747
+ kind: 'claims_released', taskRef: claims[0].taskRef, threadRef, actor: owner, sequence,
1748
+ claimRefs: claims.map(claim => claim.claimRef).sort(),
1749
+ })));
1750
+ if (data.activities.length !== expectedActivities.length || data.activities.some((activity, index) => {
1751
+ const expected = expectedActivities[index];
1752
+ return expected === undefined || activity.kind !== expected.kind || activity.taskRef !== expected.taskRef
1753
+ || activity.threadRef !== expected.threadRef || activity.actor !== expected.actor || activity.sequence !== expected.sequence
1754
+ || !this.sameList(activity.claimRefs, expected.claimRefs);
1755
+ }))
1756
+ throw new Error('invalid released Claim activities');
1757
+ for (const claim of data.claims) {
1758
+ const prior = projection.claims.get(claim.claimRef);
1759
+ if (prior === undefined)
1760
+ throw new Error('invalid released Claim reference');
1761
+ }
1762
+ for (const activity of data.activities)
1763
+ this.addRef(refs, activity.activityRef);
1764
+ const projectedClaims = new Map(projection.claims);
1765
+ for (const claim of releasedClaims)
1766
+ projectedClaims.set(claim.claimRef, claim);
1767
+ const expectedTasks = [...new Set(releasedClaims.map(claim => claim.taskRef))].map(taskRef => {
1768
+ const task = projection.tasks.get(taskRef);
1769
+ return Object.freeze({ ...task, status: this.deriveResolvedTaskStatus(task, projectedClaims.values()) });
1770
+ });
1771
+ const expectedThreads = expectedActivities.map(activity => {
1772
+ const thread = projection.threads.get(activity.threadRef);
1773
+ return Object.freeze({ ...thread, revision: sequence });
1774
+ });
1775
+ if (!isDeepStrictEqual(data.tasks, expectedTasks) || !isDeepStrictEqual(data.threads, expectedThreads)) {
1776
+ throw new Error('invalid released Claim Task or Thread projection');
1777
+ }
1778
+ // Attention and marker cleanup covers EVERY Member on the archived Threads.
1779
+ const expectedAttention = [...projection.attention.values()]
1780
+ .filter(attention => threadRefs.has(attention.threadRef))
1781
+ .map(attention => ({ memberId: attention.memberId, threadRef: attention.threadRef }));
1782
+ const expectedMarkers = [...projection.directMarkers.values()]
1783
+ .filter(marker => threadRefs.has(marker.threadRef));
1784
+ const expectedActivityMarkers = [...projection.activityMarkers.values()]
1785
+ .filter(marker => threadRefs.has(marker.threadRef));
1786
+ const expectedInbox = this.inboxDelta([], expectedAttention, [], expectedMarkers, [], expectedActivityMarkers);
1787
+ if (!isDeepStrictEqual(data.inbox, expectedInbox))
1788
+ throw new Error('invalid Channel archival inbox cleanup');
1789
+ this.validateInboxDelta(data.inbox, projection, refs);
1790
+ }
1482
1791
  validateMessageInbox(operation, projection) {
1483
1792
  const { message, mentions } = operation.data;
1484
1793
  const mentionedAgents = mentions.filter(memberId => projection.members.has(memberId));
@@ -1510,7 +1819,7 @@ export class AgentTeamLedger {
1510
1819
  if (memberId === AGENT_TEAM_HUMAN_MEMBER_ID)
1511
1820
  return true;
1512
1821
  const member = projection.members.get(memberId);
1513
- return member !== undefined && member.state !== 'inactive'
1822
+ return member !== undefined && member.state !== 'inactive' && member.state !== 'archived'
1514
1823
  && projection.channels.get(channelRef)?.workspaceId === member.workspaceId
1515
1824
  && this.isChannelMemberFrom(projection, channelRef, memberId);
1516
1825
  }
@@ -1577,6 +1886,22 @@ export class AgentTeamLedger {
1577
1886
  this.applyInboxDelta(target, operation.data.inbox);
1578
1887
  return;
1579
1888
  }
1889
+ if (operation.kind === 'team/channel-archived') {
1890
+ // Archival keeps Memberships: hidden state, not departure — a future
1891
+ // restore returns every Member to the Channel. Visibility filters by
1892
+ // channel state instead.
1893
+ target.channels.set(operation.data.channel.channelRef, operation.data.channel);
1894
+ for (const claim of operation.data.claims)
1895
+ target.claims.set(claim.claimRef, claim);
1896
+ for (const activity of operation.data.activities)
1897
+ this.appendActivityFact(target, activity);
1898
+ for (const task of operation.data.tasks)
1899
+ target.tasks.set(task.taskRef, task);
1900
+ for (const thread of operation.data.threads)
1901
+ target.threads.set(thread.threadRef, thread);
1902
+ this.applyInboxDelta(target, operation.data.inbox);
1903
+ return;
1904
+ }
1580
1905
  if (operation.kind === 'team/member-removed') {
1581
1906
  target.members.set(operation.data.member.memberId, operation.data.member);
1582
1907
  for (const membership of target.memberships.values())
@@ -1592,6 +1917,22 @@ export class AgentTeamLedger {
1592
1917
  this.applyInboxDelta(target, operation.data.inbox);
1593
1918
  return;
1594
1919
  }
1920
+ if (operation.kind === 'team/member-archived') {
1921
+ // Archival keeps Memberships: hidden state, not departure — a future
1922
+ // restore returns the Member to its Channels. Visibility filters by
1923
+ // member state instead.
1924
+ target.members.set(operation.data.member.memberId, operation.data.member);
1925
+ for (const claim of operation.data.claims)
1926
+ target.claims.set(claim.claimRef, claim);
1927
+ for (const activity of operation.data.activities)
1928
+ this.appendActivityFact(target, activity);
1929
+ for (const task of operation.data.tasks)
1930
+ target.tasks.set(task.taskRef, task);
1931
+ for (const thread of operation.data.threads)
1932
+ target.threads.set(thread.threadRef, thread);
1933
+ this.applyInboxDelta(target, operation.data.inbox);
1934
+ return;
1935
+ }
1595
1936
  if (operation.kind === 'team/thread-promoted') {
1596
1937
  this.appendActivityFact(target, operation.data.activity);
1597
1938
  target.tasks.set(operation.data.task.taskRef, operation.data.task);
@@ -1633,6 +1974,11 @@ export class AgentTeamLedger {
1633
1974
  this.applyInboxDelta(target, operation.data.inbox);
1634
1975
  return;
1635
1976
  }
1977
+ if (operation.kind === 'team/dm-sent') {
1978
+ // Audit-only: delivery is a transient runtime effect, so the durable
1979
+ // projection deliberately does not change.
1980
+ return;
1981
+ }
1636
1982
  assertUnhandledKind(operation);
1637
1983
  }
1638
1984
  /** Facts arrive in ledger sequence order, so global and per-thread lists stay sorted by append only. */
@@ -1837,6 +2183,14 @@ export class AgentTeamLedger {
1837
2183
  const activityMarkers = [...this.state.activityMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
1838
2184
  return this.inboxDelta([], removed, [], markers, [], activityMarkers);
1839
2185
  }
2186
+ /** Attention and marker cleanup for EVERY Member on the given Threads. */
2187
+ channelArchivalInbox(threadRefs) {
2188
+ const removed = [...this.state.attention.values()].filter(attention => threadRefs.has(attention.threadRef))
2189
+ .map(attention => Object.freeze({ memberId: attention.memberId, threadRef: attention.threadRef }));
2190
+ const markers = [...this.state.directMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
2191
+ const activityMarkers = [...this.state.activityMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
2192
+ return this.inboxDelta([], removed, [], markers, [], activityMarkers);
2193
+ }
1840
2194
  inboxDelta(set = [], removed = [], addedMarkers = [], removedMarkers = [], addedActivityMarkers = [], removedActivityMarkers = []) {
1841
2195
  return Object.freeze({
1842
2196
  attention: Object.freeze({ set: Object.freeze(set), removed: Object.freeze(removed) }),
@@ -1912,7 +2266,7 @@ export class AgentTeamLedger {
1912
2266
  if (memberId === AGENT_TEAM_HUMAN_MEMBER_ID)
1913
2267
  continue;
1914
2268
  const member = this.requireMember(memberId);
1915
- if (member.state === 'inactive' || member.workspaceId !== channel.workspaceId || !this.isChannelMember(channel.channelRef, memberId)) {
2269
+ if (member.state === 'inactive' || member.state === 'archived' || member.workspaceId !== channel.workspaceId || !this.isChannelMember(channel.channelRef, memberId)) {
1916
2270
  throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channel.channelRef}'`);
1917
2271
  }
1918
2272
  }
@@ -1963,6 +2317,9 @@ export class AgentTeamLedger {
1963
2317
  threadForActor(actor, workspaceId, taskRef) {
1964
2318
  const task = this.requireTask(workspaceId, taskRef);
1965
2319
  const thread = this.requireThread(task.threadRef);
2320
+ // Archived Channels do not exist on Team API surfaces: their Tasks and
2321
+ // Claims are unreachable for reads (listClaims) as for writes.
2322
+ this.assertThreadChannelActive(task.channelRef);
1966
2323
  if (actor.kind === 'member')
1967
2324
  this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
1968
2325
  return { task, thread };
@@ -2138,6 +2495,13 @@ export class AgentTeamLedger {
2138
2495
  throw new Error(`Thread '${thread.threadRef}' has no Channel`);
2139
2496
  if (projection.channels.get(channelRef)?.workspaceId !== workspaceId)
2140
2497
  throw new Error(`Thread '${thread.threadRef}' does not belong to Workspace '${workspaceId}'`);
2498
+ // Archived Channels do not exist on Team API surfaces: their Threads are
2499
+ // unreachable by ref, for reads as for writes. Replay stays honest —
2500
+ // operations predating the archival resolve against the then-active
2501
+ // Channel because replay applies in sequence.
2502
+ if (projection.channels.get(channelRef)?.state === 'archived') {
2503
+ throw new Error(`Channel '${channelRef}' is archived and no longer accepts Team work`);
2504
+ }
2141
2505
  if (task !== undefined && task.channelRef !== channelRef)
2142
2506
  throw new Error(`Task '${task.taskRef}' does not belong to Thread '${thread.threadRef}'`);
2143
2507
  return { ...(task === undefined ? {} : { task }), thread, channelRef };
@@ -2150,6 +2514,19 @@ export class AgentTeamLedger {
2150
2514
  throw new Error(`Channel '${channelRef}' does not belong to Workspace '${workspaceId}'`);
2151
2515
  return channel;
2152
2516
  }
2517
+ /** Guard for every mutating or Channel-scoped surface flow: archived Channels reject. */
2518
+ requireActiveChannel(workspaceId, channelRef) {
2519
+ const channel = this.requireChannel(workspaceId, channelRef);
2520
+ if (channel.state === 'archived')
2521
+ throw new Error(`Channel '${channelRef}' is archived and no longer accepts Team work`);
2522
+ return channel;
2523
+ }
2524
+ /** Thread-mutation guard: the Channel owning the Thread must still be active. */
2525
+ assertThreadChannelActive(channelRef) {
2526
+ if (this.state.channels.get(channelRef)?.state === 'archived') {
2527
+ throw new Error(`Channel '${channelRef}' is archived and no longer accepts Team work`);
2528
+ }
2529
+ }
2153
2530
  requireMember(memberId) {
2154
2531
  const member = this.state.members.get(memberId);
2155
2532
  if (member === undefined)
@@ -2206,6 +2583,20 @@ export class AgentTeamLedger {
2206
2583
  if (model.provider.trim() === '' || model.model.trim() === '')
2207
2584
  throw new Error('member model selection must name a provider route and a model id');
2208
2585
  }
2586
+ /**
2587
+ * Capability allow-list entries are exact identifiers (skill names,
2588
+ * reserved tool names); only whitespace-only values are rejected. Unknown
2589
+ * names are intent, not errors — committing must stay replayable across
2590
+ * Harness upgrades, so divergence is derived at activation instead.
2591
+ */
2592
+ assertCapabilities(capabilities) {
2593
+ for (const surface of [capabilities?.tools?.allow, capabilities?.skills?.allow]) {
2594
+ if (surface === undefined)
2595
+ continue;
2596
+ if (surface.some(name => name.trim() === ''))
2597
+ throw new Error('capability allow-list names must not be empty');
2598
+ }
2599
+ }
2209
2600
  initialization() {
2210
2601
  const operation = this.state.ordered[0];
2211
2602
  if (operation === undefined)
@@ -2234,6 +2625,7 @@ export class AgentTeamLedger {
2234
2625
  || operation.data.member.workspaceId !== request.workspaceId || operation.data.member.handle !== request.handle.trim()
2235
2626
  || operation.data.member.description !== request.description.trim() || operation.data.member.presetId !== request.presetId.trim()
2236
2627
  || !isDeepStrictEqual(operation.data.member.model ?? undefined, request.member.model ?? undefined)
2628
+ || !isDeepStrictEqual(operation.data.member.capabilities ?? undefined, request.member.capabilities ?? undefined)
2237
2629
  || !this.sameList(operation.data.channelRefs, this.normalizeUnique(request.channelRefs, 'initial Member Channels')))
2238
2630
  this.throwRequestCollision(request.requestId);
2239
2631
  }
@@ -2257,7 +2649,8 @@ export class AgentTeamLedger {
2257
2649
  if (operation.kind !== 'team/member-updated' || !this.sameActor(operation.actor, request.actor)
2258
2650
  || operation.data.member.memberId !== request.memberId || operation.data.member.handle !== request.handle.trim()
2259
2651
  || operation.data.member.description !== request.description.trim()
2260
- || !isDeepStrictEqual(operation.data.member.model ?? undefined, request.model ?? undefined))
2652
+ || !isDeepStrictEqual(operation.data.member.model ?? undefined, request.model ?? undefined)
2653
+ || !isDeepStrictEqual(operation.data.member.capabilities ?? undefined, request.capabilities ?? undefined))
2261
2654
  this.throwRequestCollision(request.requestId);
2262
2655
  }
2263
2656
  assertSameChannelJoin(operation, request) {
@@ -2270,6 +2663,22 @@ export class AgentTeamLedger {
2270
2663
  || operation.data.workspaceId !== request.workspaceId || operation.data.channelRef !== request.channelRef || operation.data.memberId !== request.memberId)
2271
2664
  this.throwRequestCollision(request.requestId);
2272
2665
  }
2666
+ assertSameChannelArchival(operation, request) {
2667
+ if (operation.kind !== 'team/channel-archived' || !this.sameActor(operation.actor, request.actor)
2668
+ || operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef)
2669
+ this.throwRequestCollision(request.requestId);
2670
+ }
2671
+ assertSameDm(operation, request) {
2672
+ if (operation.kind !== 'team/dm-sent' || !this.sameActor(operation.actor, request.actor)
2673
+ || operation.data.workspaceId !== request.workspaceId
2674
+ || operation.data.senderMemberId !== request.actor.memberId
2675
+ || operation.data.recipientMemberId !== request.recipientMemberId
2676
+ || operation.data.body !== request.body.trim())
2677
+ this.throwRequestCollision(request.requestId);
2678
+ }
2679
+ dmResult(operation) {
2680
+ return Object.freeze({ receipt: this.receipt(operation), recipient: this.requireMember(operation.data.recipientMemberId) });
2681
+ }
2273
2682
  assertSameMessage(operation, request, recipients) {
2274
2683
  if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
2275
2684
  || operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
@@ -2314,6 +2723,11 @@ export class AgentTeamLedger {
2314
2723
  || operation.data.member.memberId !== request.memberId)
2315
2724
  this.throwRequestCollision(request.requestId);
2316
2725
  }
2726
+ assertSameArchival(operation, request) {
2727
+ if (operation.kind !== 'team/member-archived' || !this.sameActor(operation.actor, request.actor)
2728
+ || operation.data.member.memberId !== request.memberId)
2729
+ this.throwRequestCollision(request.requestId);
2730
+ }
2317
2731
  assertSameAttention(operation, request) {
2318
2732
  if (operation.kind !== 'team/thread-attention-changed' || !this.sameActor(operation.actor, request.actor)
2319
2733
  || operation.data.workspaceId !== request.workspaceId
@@ -2404,6 +2818,10 @@ export class AgentTeamLedger {
2404
2818
  return Object.freeze({ receipt: this.receipt(operation), channelRef: operation.data.channelRef, memberId: operation.data.memberId,
2405
2819
  releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
2406
2820
  }
2821
+ channelArchivalResult(operation) {
2822
+ return Object.freeze({ receipt: this.receipt(operation), channel: operation.data.channel,
2823
+ releasedClaims: operation.data.claims });
2824
+ }
2407
2825
  messageResult(operation) {
2408
2826
  return Object.freeze(this.committedMessageResult(operation));
2409
2827
  }
@@ -2444,6 +2862,10 @@ export class AgentTeamLedger {
2444
2862
  return Object.freeze({ receipt: this.receipt(operation), member: operation.data.member,
2445
2863
  releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
2446
2864
  }
2865
+ archivalResult(operation) {
2866
+ return Object.freeze({ receipt: this.receipt(operation), member: operation.data.member,
2867
+ releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
2868
+ }
2447
2869
  operationBase(request, sequence) {
2448
2870
  return { sequence, operationId: this.createOperationId(), requestId: request.requestId, occurredAt: this.createOccurredAt(),
2449
2871
  actor: Object.freeze({ ...request.actor }), previousOperationId: this.state.ordered.at(-1)?.operationId ?? null };