@wowyuarm/dsh-agent-team 0.1.6 → 0.1.8

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 (108) hide show
  1. package/README.md +11 -5
  2. package/README.zh.md +11 -5
  3. package/package.json +44 -42
  4. package/packages/agent-team/README.md +2 -0
  5. package/packages/agent-team/README.zh.md +2 -0
  6. package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +2 -0
  7. package/packages/agent-team/lib/index.js +140 -204
  8. package/packages/agent-team/lib/ledger.js +436 -25
  9. package/packages/agent-team/lib/member-context.js +6 -2
  10. package/packages/agent-team/lib/member-runtime.js +229 -0
  11. package/packages/agent-team/lib/preset-roster.js +1 -0
  12. package/packages/agent-team/lib/progress-nudge.js +320 -0
  13. package/packages/agent-team/lib/spec.js +30 -1
  14. package/packages/agent-team/lib/typert.host.js +250 -70
  15. package/packages/agent-team/lib/typert.remote-client.d.ts +5 -1
  16. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  17. package/packages/agent-team/lib/typert.remote-client.js +168 -38
  18. package/packages/agent-team/lib/types/entities.js +1 -0
  19. package/packages/agent-team/lib/types/index.d.ts +28 -44
  20. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  21. package/packages/agent-team/lib/types/ledger.d.ts +60 -2
  22. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  24. package/packages/agent-team/lib/types/member-runtime.d.ts +108 -0
  25. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -0
  26. package/packages/agent-team/lib/types/operations.js +1 -0
  27. package/packages/agent-team/lib/types/preset-roster.d.ts.map +1 -1
  28. package/packages/agent-team/lib/types/progress-nudge.d.ts +136 -0
  29. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -0
  30. package/packages/agent-team/lib/types/requests-results.js +1 -0
  31. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/types/entities.d.ts +357 -0
  33. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -0
  34. package/packages/agent-team/lib/types/types/operations.d.ts +303 -0
  35. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -0
  36. package/packages/agent-team/lib/types/types/requests-results.d.ts +529 -0
  37. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/types.d.ts +10 -1070
  39. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  40. package/packages/agent-team/preset/team-member/agent.cordis.yml +6 -4
  41. package/packages/client-agent-team/README.md +3 -1
  42. package/packages/client-agent-team/README.zh.md +3 -1
  43. package/packages/client-agent-team/lib/client.js +773 -733
  44. package/packages/client-agent-team/lib/client.js.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +3 -2
  46. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  47. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +30 -7
  48. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +1 -1
  49. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +5 -9
  51. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts +2 -1
  52. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +31 -5
  54. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +3 -1
  55. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  56. package/packages/client-agent-team/lib/types/client/TeamComposer.js +39 -28
  57. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  58. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  59. package/packages/client-agent-team/lib/types/client/TeamConversation.js +2 -2
  60. package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.d.ts.map +1 -1
  61. package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.js +3 -2
  62. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  63. package/packages/client-agent-team/lib/types/client/TeamMessage.js +15 -3
  64. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +2 -2
  65. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +4 -6
  67. package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts +14 -0
  68. package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts.map +1 -0
  69. package/packages/client-agent-team/lib/types/client/TeamStateDot.js +15 -0
  70. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +2 -1
  71. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +41 -16
  73. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  74. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  75. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  76. package/packages/client-agent-team/lib/types/client/TeamWorkspaceRow.d.ts +1 -1
  77. package/packages/client-agent-team/lib/types/client/TeamWorkspaceRow.d.ts.map +1 -1
  78. package/packages/client-agent-team/lib/types/client/index.d.ts +1 -1
  79. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  80. package/packages/client-agent-team/lib/types/client/index.js +9 -77
  81. package/packages/client-agent-team/lib/types/client/locales.d.ts +26 -0
  82. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  83. package/packages/client-agent-team/lib/types/client/locales.js +26 -0
  84. package/packages/client-agent-team/lib/types/client/navigation.d.ts +2 -1
  85. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  86. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +1 -2
  87. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -1
  88. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts +1 -1
  89. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts.map +1 -1
  90. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts +1 -1
  91. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts.map +1 -1
  92. package/packages/client-agent-team/lib/types/client/slots.d.ts +5 -1
  93. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  94. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +7 -1
  95. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
  96. package/packages/client-agent-team/lib/types/client/task-refs.js +25 -3
  97. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +11 -10
  98. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  99. package/packages/client-agent-team/lib/types/client/team-formatters.js +14 -11
  100. package/packages/client-agent-team/lib/types/client/team-membership.d.ts +1 -1
  101. package/packages/client-agent-team/lib/types/client/team-membership.d.ts.map +1 -1
  102. package/packages/tool-agent-team/lib/index.js +9 -9
  103. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts +0 -17
  104. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +0 -1
  105. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +0 -135
  106. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +0 -22
  107. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +0 -1
  108. package/packages/client-agent-team/lib/types/client/member-session-input.js +0 -98
@@ -19,6 +19,10 @@ function emptyProjection() {
19
19
  orderedFacts: [], factsByThread: new Map(), channelRefByThread: new Map(), mentionsByMessage: new Map(), messageCountByThread: new Map(),
20
20
  attentionByThread: new Map() };
21
21
  }
22
+ const EMPTY_PROGRESS_NUDGE_TARGETS = Object.freeze({
23
+ progress: Object.freeze([]),
24
+ claim: Object.freeze([]),
25
+ });
22
26
  /** Exhaustiveness guard for the closed operation union: adding a kind must update every dispatch. */
23
27
  function assertUnhandledKind(operation) {
24
28
  throw new Error(`agent-team ledger does not handle operation kind '${operation.kind}'`);
@@ -97,7 +101,7 @@ export class AgentTeamLedger {
97
101
  this.assertJoinableMember(request.workspaceId, memberId);
98
102
  const sequence = this.nextSequence();
99
103
  const channel = Object.freeze({
100
- channelRef: this.ref('channel'), workspaceId: request.workspaceId, name, description, createdAtSequence: sequence,
104
+ channelRef: this.ref('channel'), workspaceId: request.workspaceId, name, description, createdAtSequence: sequence, state: 'active',
101
105
  });
102
106
  const operation = Object.freeze({
103
107
  ...this.operationBase(request, sequence), kind: 'team/channel-created',
@@ -121,7 +125,7 @@ export class AgentTeamLedger {
121
125
  const description = request.description.trim();
122
126
  if (name === '')
123
127
  throw new Error('channel name must not be empty');
124
- const channel = Object.freeze({ ...this.requireChannel(request.workspaceId, request.channelRef), name, description });
128
+ const channel = Object.freeze({ ...this.requireActiveChannel(request.workspaceId, request.channelRef), name, description });
125
129
  const operation = Object.freeze({
126
130
  ...this.operationBase(request, this.nextSequence()), kind: 'team/channel-updated',
127
131
  data: Object.freeze({ workspaceId: request.workspaceId, channel }),
@@ -150,7 +154,7 @@ export class AgentTeamLedger {
150
154
  // still drivable through its DM view, and joins Channels later.
151
155
  const channelRefs = this.normalizeUnique(request.channelRefs, 'initial Member Channels');
152
156
  for (const channelRef of channelRefs)
153
- this.requireChannel(request.workspaceId, channelRef);
157
+ this.requireActiveChannel(request.workspaceId, channelRef);
154
158
  this.assertHandleAvailable(request.workspaceId, handle);
155
159
  this.assertModelSelection(request.member.model);
156
160
  this.assertCapabilities(request.member.capabilities);
@@ -180,6 +184,8 @@ export class AgentTeamLedger {
180
184
  const prior = this.requireMember(request.memberId);
181
185
  if (prior.state === 'inactive')
182
186
  throw new Error(`Agent Member '${prior.memberId}' is inactive and can no longer be edited`);
187
+ if (prior.state === 'archived')
188
+ throw new Error(`Agent Member '${prior.memberId}' is archived and can no longer be edited`);
183
189
  const handle = request.handle.trim();
184
190
  const description = request.description.trim();
185
191
  if (handle === '')
@@ -244,6 +250,80 @@ export class AgentTeamLedger {
244
250
  getMember(memberId) {
245
251
  return this.state.members.get(memberId);
246
252
  }
253
+ /**
254
+ * Current progress-nudge candidacy for one Member: threads whose Thread
255
+ * progress reminder applies (active Claim, or following a taskless Thread)
256
+ * and tasks where the Claim reminder applies (following a still-`todo` Task
257
+ * with no Claim history for this Member). A deep read-only projection — the
258
+ * caller never re-derives attention, claim, task-resolution, or channel
259
+ * archive state. Archived channels and resolved tasks produce no candidacy;
260
+ * results are ordered by Thread revision descending, then threadRef.
261
+ */
262
+ progressNudgeTargets(memberId) {
263
+ const member = this.state.members.get(memberId);
264
+ if (member === undefined || member.state !== 'enabled')
265
+ return EMPTY_PROGRESS_NUDGE_TARGETS;
266
+ const progress = [];
267
+ const claim = [];
268
+ const seenProgress = new Set();
269
+ for (const claimRecord of this.state.claims.values()) {
270
+ if (claimRecord.owner !== memberId || claimRecord.state !== 'active')
271
+ continue;
272
+ const task = this.state.tasks.get(claimRecord.taskRef);
273
+ const thread = this.state.threads.get(claimRecord.threadRef);
274
+ if (task === undefined || thread === undefined || task.resolution !== 'open')
275
+ continue;
276
+ if (this.state.channels.get(task.channelRef)?.state === 'archived')
277
+ continue;
278
+ if (seenProgress.has(claimRecord.threadRef))
279
+ continue;
280
+ seenProgress.add(claimRecord.threadRef);
281
+ progress.push({ reason: 'active-claim', threadRef: claimRecord.threadRef, taskRef: claimRecord.taskRef });
282
+ }
283
+ for (const [threadRef, followers] of this.state.attentionByThread) {
284
+ if (!followers.has(memberId))
285
+ continue;
286
+ const thread = this.state.threads.get(threadRef);
287
+ if (thread === undefined)
288
+ continue;
289
+ const channelRef = this.state.channelRefByThread.get(threadRef);
290
+ if (channelRef === undefined || this.state.channels.get(channelRef)?.state === 'archived')
291
+ continue;
292
+ if (thread.taskRef === undefined) {
293
+ if (!seenProgress.has(threadRef)) {
294
+ seenProgress.add(threadRef);
295
+ progress.push({ reason: 'taskless-follower', threadRef });
296
+ }
297
+ continue;
298
+ }
299
+ // Claim reminder: only still-`todo` tasks with zero lifetime claims by
300
+ // this Member; once claimed (any state) the Member moved into the
301
+ // progress-reminder regime.
302
+ if (this.hasLifetimeClaimOnTask(memberId, thread.taskRef))
303
+ continue;
304
+ const task = this.state.tasks.get(thread.taskRef);
305
+ if (task === undefined || task.resolution !== 'open' || this.deriveTaskStatus(task.taskRef, this.state.claims.values()) !== 'todo')
306
+ continue;
307
+ claim.push({ threadRef, taskRef: thread.taskRef });
308
+ }
309
+ const byThreadRevision = (left, right) => {
310
+ const leftRevision = this.state.threads.get(left.threadRef)?.revision ?? 0;
311
+ const rightRevision = this.state.threads.get(right.threadRef)?.revision ?? 0;
312
+ return rightRevision - leftRevision || (left.threadRef < right.threadRef ? -1 : left.threadRef > right.threadRef ? 1 : 0);
313
+ };
314
+ return Object.freeze({
315
+ progress: Object.freeze(progress.sort(byThreadRevision)),
316
+ claim: Object.freeze(claim.sort(byThreadRevision)),
317
+ });
318
+ }
319
+ /** Whether this Member has ever held a Claim on the Task, in any state. */
320
+ hasLifetimeClaimOnTask(memberId, taskRef) {
321
+ for (const claim of this.state.claims.values()) {
322
+ if (claim.owner === memberId && claim.taskRef === taskRef)
323
+ return true;
324
+ }
325
+ return false;
326
+ }
247
327
  listMembers() {
248
328
  return Object.freeze([...this.state.members.values()]);
249
329
  }
@@ -255,7 +335,7 @@ export class AgentTeamLedger {
255
335
  return this.resolved(this.joinResult(existing));
256
336
  }
257
337
  this.assertHumanActor(request.actor);
258
- const channel = this.requireChannel(request.workspaceId, request.channelRef);
338
+ const channel = this.requireActiveChannel(request.workspaceId, request.channelRef);
259
339
  const member = this.requireMember(request.memberId);
260
340
  if (member.workspaceId !== request.workspaceId)
261
341
  throw new Error('Member and Channel must belong to one Workspace');
@@ -307,6 +387,48 @@ export class AgentTeamLedger {
307
387
  return this.committed(this.channelMemberRemovalResult(operation));
308
388
  });
309
389
  }
390
+ /**
391
+ * Archive one Channel: hidden from every surface, facts kept recoverable.
392
+ * Every active Claim on the Channel's Threads releases with one public
393
+ * Activity per (owner, Thread), and every Member's Attention and markers
394
+ * for those Threads clear — a hidden Channel must not leave Tasks stuck in
395
+ * progress behind it or phantom unread counts.
396
+ */
397
+ archiveChannel(request) {
398
+ return this.enqueue(async () => {
399
+ const existing = this.state.byRequest.get(request.requestId);
400
+ if (existing !== undefined) {
401
+ this.assertSameChannelArchival(existing, request);
402
+ return this.resolved(this.channelArchivalResult(existing));
403
+ }
404
+ this.assertHumanActor(request.actor);
405
+ const channel = this.requireChannel(request.workspaceId, request.channelRef);
406
+ if (channel.state === 'archived')
407
+ throw new Error(`Channel '${channel.channelRef}' is already archived`);
408
+ const nextChannel = Object.freeze({ ...channel, state: 'archived' });
409
+ const threadRefs = this.channelThreadRefs(channel.channelRef);
410
+ const releasedClaims = [...this.state.claims.values()]
411
+ .filter(claim => claim.state === 'active' && threadRefs.has(claim.threadRef))
412
+ .map(claim => Object.freeze({ ...claim, state: 'released' }));
413
+ const nextClaims = new Map(this.state.claims);
414
+ for (const claim of releasedClaims)
415
+ nextClaims.set(claim.claimRef, claim);
416
+ const sequence = this.nextSequence();
417
+ const owners = [...new Set(releasedClaims.map(claim => claim.owner))].sort();
418
+ const activities = Object.freeze(owners.flatMap(owner => this.releaseSummaries(releasedClaims.filter(claim => claim.owner === owner), owner, sequence)));
419
+ const threads = this.threadsForActivities(activities);
420
+ const tasks = this.tasksForClaims(releasedClaims, nextClaims);
421
+ const inbox = this.channelArchivalInbox(threadRefs);
422
+ const operation = Object.freeze({
423
+ ...this.operationBase(request, sequence), kind: 'team/channel-archived',
424
+ data: Object.freeze({ workspaceId: request.workspaceId, channel: nextChannel,
425
+ claims: Object.freeze(releasedClaims), activities, tasks, threads, inbox }),
426
+ });
427
+ await this.table.put(operation.operationId, operation);
428
+ this.apply(operation);
429
+ return this.committed(this.channelArchivalResult(operation));
430
+ });
431
+ }
310
432
  sendMessage(request) {
311
433
  return this.enqueue(async () => {
312
434
  const recipients = this.normalizeRecipients(request.actor, request.recipients);
@@ -316,7 +438,7 @@ export class AgentTeamLedger {
316
438
  return this.resolved(this.messageResult(existing));
317
439
  }
318
440
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
319
- const channel = this.requireChannel(request.workspaceId, request.channelRef);
441
+ const channel = this.requireActiveChannel(request.workspaceId, request.channelRef);
320
442
  if (actor.kind === 'member')
321
443
  this.requireMemberChannel(this.requireMember(actor.memberId), channel.channelRef);
322
444
  const body = request.body.trim();
@@ -474,7 +596,7 @@ export class AgentTeamLedger {
474
596
  else {
475
597
  if (request.direction !== undefined)
476
598
  throw new Error(`${request.action} action does not accept direction`);
477
- const previous = request.claimRef === undefined ? undefined : this.state.claims.get(request.claimRef);
599
+ const previous = request.claimRef === undefined ? undefined : this.state.claims.get(this.requireRefKey(this.state.claims, request.claimRef, 'claim', 'Claim'));
478
600
  if (previous === undefined) {
479
601
  throw new Error(`unknown Claim '${request.claimRef ?? ''}'${request.claimRef === undefined ? '' : this.unknownRefHint(request.claimRef, 'claim', 'Claim')}`);
480
602
  }
@@ -589,7 +711,9 @@ export class AgentTeamLedger {
589
711
  const activities = this.releaseSummaries(releasedClaims, member.memberId, sequence);
590
712
  const threads = this.threadsForActivities(activities);
591
713
  const tasks = this.tasksForClaims(releasedClaims, projectedClaims);
592
- const threadRefs = new Set([...this.state.tasks.values()].map(task => task.threadRef));
714
+ // The Member leaves entirely, so Attention and markers clear on every
715
+ // Thread — taskless ones too — matching the replay validator's scope.
716
+ const threadRefs = new Set(this.state.threads.keys());
593
717
  const inbox = this.removeMemberThreadInbox(member.memberId, threadRefs);
594
718
  const operation = Object.freeze({
595
719
  ...this.operationBase(request, sequence), kind: 'team/member-removed',
@@ -601,6 +725,49 @@ export class AgentTeamLedger {
601
725
  return this.committed(this.removalResult(operation));
602
726
  });
603
727
  }
728
+ /**
729
+ * Archive one Member: hidden from every surface, data kept recoverable.
730
+ * The release shape matches removal — active Claims release with public
731
+ * Activities and the Member's Attention/markers clear — because a hidden
732
+ * Member must not leave Tasks stuck in progress or phantom unread counts.
733
+ */
734
+ archiveMember(request) {
735
+ return this.enqueue(async () => {
736
+ const existing = this.state.byRequest.get(request.requestId);
737
+ if (existing !== undefined) {
738
+ this.assertSameArchival(existing, request);
739
+ return this.resolved(this.archivalResult(existing));
740
+ }
741
+ this.assertHumanActor(request.actor);
742
+ const member = this.requireMember(request.memberId);
743
+ if (member.state === 'inactive')
744
+ throw new Error(`Agent Member '${member.memberId}' is already inactive`);
745
+ if (member.state === 'archived')
746
+ throw new Error(`Agent Member '${member.memberId}' is already archived`);
747
+ const nextMember = Object.freeze({ ...member, state: 'archived' });
748
+ const releasedClaims = [...this.state.claims.values()].filter(claim => claim.owner === member.memberId && claim.state === 'active')
749
+ .map(claim => Object.freeze({ ...claim, state: 'released' }));
750
+ const projectedClaims = new Map(this.state.claims);
751
+ for (const claim of releasedClaims)
752
+ projectedClaims.set(claim.claimRef, claim);
753
+ const sequence = this.nextSequence();
754
+ const activities = this.releaseSummaries(releasedClaims, member.memberId, sequence);
755
+ const threads = this.threadsForActivities(activities);
756
+ const tasks = this.tasksForClaims(releasedClaims, projectedClaims);
757
+ // The Member leaves entirely, so Attention and markers clear on every
758
+ // Thread — taskless ones too — matching the replay validator's scope.
759
+ const threadRefs = new Set(this.state.threads.keys());
760
+ const inbox = this.removeMemberThreadInbox(member.memberId, threadRefs);
761
+ const operation = Object.freeze({
762
+ ...this.operationBase(request, sequence), kind: 'team/member-archived',
763
+ data: Object.freeze({ member: nextMember, claims: Object.freeze(releasedClaims), activities, tasks, threads, inbox }),
764
+ });
765
+ await this.table.put(operation.operationId, operation);
766
+ this.apply(operation);
767
+ this.confirmations.clear();
768
+ return this.committed(this.archivalResult(operation));
769
+ });
770
+ }
604
771
  changeAttention(request) {
605
772
  return this.enqueue(async () => {
606
773
  const existing = this.state.byRequest.get(request.requestId);
@@ -808,7 +975,7 @@ export class AgentTeamLedger {
808
975
  memberId: next.memberId, action: 'follow' }));
809
976
  }
810
977
  }
811
- return Object.freeze({ items: Object.freeze(observations.slice(-limit)) });
978
+ return Object.freeze({ items: Object.freeze(observations.slice(-limit)), followers: Object.freeze([...current.keys()]) });
812
979
  }
813
980
  /** Every operation carrying an inbox delta drives the Inbox projection; the payload shape decides, not a per-kind list. */
814
981
  attentionDelta(operation) {
@@ -857,8 +1024,13 @@ export class AgentTeamLedger {
857
1024
  const numbers = this.taskNumbers(workspaceId);
858
1025
  const resolved = [];
859
1026
  for (const taskRef of taskRefs) {
860
- const task = this.state.tasks.get(taskRef);
861
- if (task === undefined || this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
1027
+ const key = this.uniqueRefKey(this.state.tasks, taskRef, 'task');
1028
+ const task = key === undefined ? undefined : this.state.tasks.get(key);
1029
+ const channel = task === undefined ? undefined : this.state.channels.get(task.channelRef);
1030
+ // Archived Channels do not exist on Team API surfaces: their Tasks stop
1031
+ // resolving, so live message bodies render the refs as plain
1032
+ // non-navigable text through the existing unknown-ref path.
1033
+ if (task === undefined || channel?.workspaceId !== workspaceId || channel?.state === 'archived')
862
1034
  continue;
863
1035
  resolved.push(Object.freeze({ taskRef: task.taskRef, channelRef: task.channelRef, threadRef: task.threadRef, taskNumber: numbers.get(task.taskRef) ?? 0 }));
864
1036
  }
@@ -880,7 +1052,7 @@ export class AgentTeamLedger {
880
1052
  throw new Error('Member cannot view another Workspace');
881
1053
  }
882
1054
  if (request.channelRef !== undefined) {
883
- this.requireChannel(request.workspaceId, request.channelRef);
1055
+ this.requireActiveChannel(request.workspaceId, request.channelRef);
884
1056
  if (memberId !== undefined && !this.isChannelMember(request.channelRef, memberId))
885
1057
  throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${request.channelRef}'`);
886
1058
  }
@@ -891,12 +1063,14 @@ export class AgentTeamLedger {
891
1063
  throw new Error(`unknown Thread ref '${request.threadRef}'`);
892
1064
  if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
893
1065
  throw new Error(`Thread '${request.threadRef}' does not belong to Workspace '${request.workspaceId}'`);
1066
+ this.assertThreadChannelActive(channelRef);
894
1067
  if (request.channelRef !== undefined && channelRef !== request.channelRef)
895
1068
  throw new Error(`Thread '${request.threadRef}' does not belong to Channel '${request.channelRef}'`);
896
1069
  if (memberId !== undefined && !this.isChannelMember(channelRef, memberId))
897
1070
  throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
898
1071
  }
899
1072
  const channels = [...this.state.channels.values()].filter(channel => channel.workspaceId === request.workspaceId
1073
+ && channel.state !== 'archived'
900
1074
  && (memberId === undefined || this.isChannelMember(channel.channelRef, memberId)));
901
1075
  const channelRefs = new Set(channels.map(channel => channel.channelRef));
902
1076
  const allFacts = this.state.orderedFacts.filter(fact => {
@@ -931,7 +1105,10 @@ export class AgentTeamLedger {
931
1105
  return Object.freeze({
932
1106
  humanMemberId: initialization.data.humanMemberId,
933
1107
  channels: Object.freeze(channels),
934
- 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 })))),
1108
+ members: Object.freeze([...this.state.memberships.entries()].filter(([channelRef]) => channelRefs.has(channelRef)).flatMap(([channelRef, ids]) => [...ids].filter(id => {
1109
+ const state = this.state.members.get(id)?.state;
1110
+ return state !== 'inactive' && state !== 'archived';
1111
+ }).sort().map(memberId => Object.freeze({ channelRef, memberId })))),
935
1112
  tasks: Object.freeze(visibleTasks),
936
1113
  threads: Object.freeze([...this.state.threads.values()].filter(thread => {
937
1114
  const channelRef = this.channelRefForThread(thread.threadRef);
@@ -981,6 +1158,20 @@ export class AgentTeamLedger {
981
1158
  case 'team/member-updated':
982
1159
  case 'team/member-removed':
983
1160
  return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
1161
+ case 'team/member-archived': {
1162
+ // Claim releases are thread-visible facts: open Channel and Thread
1163
+ // pages refetch alongside the workspace-wide roster change.
1164
+ const channelByTask = new Map(operation.data.tasks.map(task => [task.taskRef, task.channelRef]));
1165
+ const scopes = [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
1166
+ for (const activity of operation.data.activities) {
1167
+ scopes.push({ kind: 'thread', threadRef: activity.threadRef });
1168
+ const channelRef = channelByTask.get(activity.taskRef);
1169
+ if (channelRef !== undefined && !scopes.some(scope => scope.kind === 'channel' && scope.channelRef === channelRef)) {
1170
+ scopes.push({ kind: 'channel', channelRef });
1171
+ }
1172
+ }
1173
+ return scopes;
1174
+ }
984
1175
  case 'team/channel-member-added':
985
1176
  return [{ kind: 'workspace', workspaceId: operation.data.workspaceId }, { kind: 'channel', channelRef: operation.data.channelRef }];
986
1177
  case 'team/channel-member-removed': {
@@ -998,6 +1189,15 @@ export class AgentTeamLedger {
998
1189
  }
999
1190
  return scopes;
1000
1191
  }
1192
+ case 'team/channel-archived': {
1193
+ const scopes = [
1194
+ { kind: 'workspace', workspaceId: operation.data.workspaceId },
1195
+ { kind: 'channel', channelRef: operation.data.channel.channelRef },
1196
+ ];
1197
+ for (const activity of operation.data.activities)
1198
+ scopes.push({ kind: 'thread', threadRef: activity.threadRef });
1199
+ return scopes;
1200
+ }
1001
1201
  case 'team/message-sent':
1002
1202
  case 'team/thread-replied':
1003
1203
  return [{ kind: 'channel', channelRef: operation.data.message.channelRef }, { kind: 'thread', threadRef: operation.data.message.threadRef }];
@@ -1062,7 +1262,9 @@ export class AgentTeamLedger {
1062
1262
  case 'team/thread-attention-changed':
1063
1263
  return [operation.data.thread.threadRef];
1064
1264
  case 'team/channel-member-removed':
1265
+ case 'team/channel-archived':
1065
1266
  case 'team/member-removed':
1267
+ case 'team/member-archived':
1066
1268
  return operation.data.activities.map(activity => activity.threadRef);
1067
1269
  case 'team/initialized':
1068
1270
  case 'team/channel-created':
@@ -1209,7 +1411,8 @@ export class AgentTeamLedger {
1209
1411
  assertHuman();
1210
1412
  const prior = projection.channels.get(operation.data.channel.channelRef);
1211
1413
  if (prior === undefined || prior.workspaceId !== operation.data.workspaceId
1212
- || operation.data.channel.workspaceId !== prior.workspaceId || operation.data.channel.createdAtSequence !== prior.createdAtSequence) {
1414
+ || operation.data.channel.workspaceId !== prior.workspaceId || operation.data.channel.createdAtSequence !== prior.createdAtSequence
1415
+ || operation.data.channel.state !== prior.state) {
1213
1416
  throw new Error('invalid Channel update');
1214
1417
  }
1215
1418
  return;
@@ -1217,7 +1420,7 @@ export class AgentTeamLedger {
1217
1420
  if (operation.kind === 'team/member-updated') {
1218
1421
  assertHuman();
1219
1422
  const prior = projection.members.get(operation.data.member.memberId);
1220
- if (prior === undefined || prior.state === 'inactive' || operation.data.member.state !== prior.state
1423
+ if (prior === undefined || prior.state === 'inactive' || prior.state === 'archived' || operation.data.member.state !== prior.state
1221
1424
  || operation.data.member.sessionId !== prior.sessionId || operation.data.member.workspaceId !== prior.workspaceId
1222
1425
  || operation.data.member.presetId !== prior.presetId
1223
1426
  || operation.data.member.privateMemoryPath !== prior.privateMemoryPath)
@@ -1250,6 +1453,18 @@ export class AgentTeamLedger {
1250
1453
  this.validateReleaseCleanup(operation.data, projection, member.memberId, threadRefs, operation.sequence, refs);
1251
1454
  return;
1252
1455
  }
1456
+ if (operation.kind === 'team/channel-archived') {
1457
+ assertHuman();
1458
+ const prior = projection.channels.get(operation.data.channel.channelRef);
1459
+ if (prior === undefined || prior.state !== 'active' || operation.data.channel.state !== 'archived'
1460
+ || operation.data.channel.workspaceId !== prior.workspaceId || operation.data.workspaceId !== prior.workspaceId
1461
+ || operation.data.channel.name !== prior.name || operation.data.channel.description !== prior.description
1462
+ || operation.data.channel.createdAtSequence !== prior.createdAtSequence)
1463
+ throw new Error('invalid Channel archival');
1464
+ const threadRefs = new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === prior.channelRef));
1465
+ this.validateChannelArchivalCleanup(operation.data, projection, threadRefs, operation.sequence, refs);
1466
+ return;
1467
+ }
1253
1468
  if (operation.kind === 'team/member-removed') {
1254
1469
  assertHuman();
1255
1470
  const prior = projection.members.get(operation.data.member.memberId);
@@ -1259,6 +1474,15 @@ export class AgentTeamLedger {
1259
1474
  this.validateReleaseCleanup(operation.data, projection, prior.memberId, threadRefs, operation.sequence, refs);
1260
1475
  return;
1261
1476
  }
1477
+ if (operation.kind === 'team/member-archived') {
1478
+ assertHuman();
1479
+ const prior = projection.members.get(operation.data.member.memberId);
1480
+ if (prior === undefined || (prior.state !== 'enabled' && prior.state !== 'suspended') || operation.data.member.state !== 'archived' || !this.sameMemberIdentity(prior, operation.data.member))
1481
+ throw new Error('invalid Member archival');
1482
+ const threadRefs = new Set(projection.threads.keys());
1483
+ this.validateReleaseCleanup(operation.data, projection, prior.memberId, threadRefs, operation.sequence, refs);
1484
+ return;
1485
+ }
1262
1486
  if (operation.kind === 'team/thread-attention-changed') {
1263
1487
  const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
1264
1488
  if (operation.data.memberId !== operation.actor.memberId)
@@ -1581,6 +1805,68 @@ export class AgentTeamLedger {
1581
1805
  throw new Error('invalid Member inbox cleanup');
1582
1806
  this.validateInboxDelta(data.inbox, projection, refs);
1583
1807
  }
1808
+ /** Replay validation of one Channel archival's release snapshot across every owner. */
1809
+ validateChannelArchivalCleanup(data, projection, threadRefs, sequence, refs) {
1810
+ const releasedClaims = [...projection.claims.values()]
1811
+ .filter(claim => claim.state === 'active' && threadRefs.has(claim.threadRef))
1812
+ .map(claim => Object.freeze({ ...claim, state: 'released' }));
1813
+ if (data.claims.length !== releasedClaims.length || data.claims.some((claim, index) => {
1814
+ const expected = releasedClaims[index];
1815
+ return expected === undefined || !this.sameClaim(expected, claim);
1816
+ }))
1817
+ throw new Error('invalid released Claim projection');
1818
+ // One activity per (owner, Thread), owners sorted like the commit path.
1819
+ const byOwner = new Map();
1820
+ for (const claim of releasedClaims) {
1821
+ const byThread = byOwner.get(claim.owner) ?? new Map();
1822
+ byOwner.set(claim.owner, byThread);
1823
+ byThread.set(claim.threadRef, [...(byThread.get(claim.threadRef) ?? []), claim]);
1824
+ }
1825
+ const expectedActivities = [...byOwner.keys()].sort().flatMap(owner => [...byOwner.get(owner).entries()].map(([threadRef, claims]) => ({
1826
+ kind: 'claims_released', taskRef: claims[0].taskRef, threadRef, actor: owner, sequence,
1827
+ claimRefs: claims.map(claim => claim.claimRef).sort(),
1828
+ })));
1829
+ if (data.activities.length !== expectedActivities.length || data.activities.some((activity, index) => {
1830
+ const expected = expectedActivities[index];
1831
+ return expected === undefined || activity.kind !== expected.kind || activity.taskRef !== expected.taskRef
1832
+ || activity.threadRef !== expected.threadRef || activity.actor !== expected.actor || activity.sequence !== expected.sequence
1833
+ || !this.sameList(activity.claimRefs, expected.claimRefs);
1834
+ }))
1835
+ throw new Error('invalid released Claim activities');
1836
+ for (const claim of data.claims) {
1837
+ const prior = projection.claims.get(claim.claimRef);
1838
+ if (prior === undefined)
1839
+ throw new Error('invalid released Claim reference');
1840
+ }
1841
+ for (const activity of data.activities)
1842
+ this.addRef(refs, activity.activityRef);
1843
+ const projectedClaims = new Map(projection.claims);
1844
+ for (const claim of releasedClaims)
1845
+ projectedClaims.set(claim.claimRef, claim);
1846
+ const expectedTasks = [...new Set(releasedClaims.map(claim => claim.taskRef))].map(taskRef => {
1847
+ const task = projection.tasks.get(taskRef);
1848
+ return Object.freeze({ ...task, status: this.deriveResolvedTaskStatus(task, projectedClaims.values()) });
1849
+ });
1850
+ const expectedThreads = expectedActivities.map(activity => {
1851
+ const thread = projection.threads.get(activity.threadRef);
1852
+ return Object.freeze({ ...thread, revision: sequence });
1853
+ });
1854
+ if (!isDeepStrictEqual(data.tasks, expectedTasks) || !isDeepStrictEqual(data.threads, expectedThreads)) {
1855
+ throw new Error('invalid released Claim Task or Thread projection');
1856
+ }
1857
+ // Attention and marker cleanup covers EVERY Member on the archived Threads.
1858
+ const expectedAttention = [...projection.attention.values()]
1859
+ .filter(attention => threadRefs.has(attention.threadRef))
1860
+ .map(attention => ({ memberId: attention.memberId, threadRef: attention.threadRef }));
1861
+ const expectedMarkers = [...projection.directMarkers.values()]
1862
+ .filter(marker => threadRefs.has(marker.threadRef));
1863
+ const expectedActivityMarkers = [...projection.activityMarkers.values()]
1864
+ .filter(marker => threadRefs.has(marker.threadRef));
1865
+ const expectedInbox = this.inboxDelta([], expectedAttention, [], expectedMarkers, [], expectedActivityMarkers);
1866
+ if (!isDeepStrictEqual(data.inbox, expectedInbox))
1867
+ throw new Error('invalid Channel archival inbox cleanup');
1868
+ this.validateInboxDelta(data.inbox, projection, refs);
1869
+ }
1584
1870
  validateMessageInbox(operation, projection) {
1585
1871
  const { message, mentions } = operation.data;
1586
1872
  const mentionedAgents = mentions.filter(memberId => projection.members.has(memberId));
@@ -1612,7 +1898,7 @@ export class AgentTeamLedger {
1612
1898
  if (memberId === AGENT_TEAM_HUMAN_MEMBER_ID)
1613
1899
  return true;
1614
1900
  const member = projection.members.get(memberId);
1615
- return member !== undefined && member.state !== 'inactive'
1901
+ return member !== undefined && member.state !== 'inactive' && member.state !== 'archived'
1616
1902
  && projection.channels.get(channelRef)?.workspaceId === member.workspaceId
1617
1903
  && this.isChannelMemberFrom(projection, channelRef, memberId);
1618
1904
  }
@@ -1679,6 +1965,22 @@ export class AgentTeamLedger {
1679
1965
  this.applyInboxDelta(target, operation.data.inbox);
1680
1966
  return;
1681
1967
  }
1968
+ if (operation.kind === 'team/channel-archived') {
1969
+ // Archival keeps Memberships: hidden state, not departure — a future
1970
+ // restore returns every Member to the Channel. Visibility filters by
1971
+ // channel state instead.
1972
+ target.channels.set(operation.data.channel.channelRef, operation.data.channel);
1973
+ for (const claim of operation.data.claims)
1974
+ target.claims.set(claim.claimRef, claim);
1975
+ for (const activity of operation.data.activities)
1976
+ this.appendActivityFact(target, activity);
1977
+ for (const task of operation.data.tasks)
1978
+ target.tasks.set(task.taskRef, task);
1979
+ for (const thread of operation.data.threads)
1980
+ target.threads.set(thread.threadRef, thread);
1981
+ this.applyInboxDelta(target, operation.data.inbox);
1982
+ return;
1983
+ }
1682
1984
  if (operation.kind === 'team/member-removed') {
1683
1985
  target.members.set(operation.data.member.memberId, operation.data.member);
1684
1986
  for (const membership of target.memberships.values())
@@ -1694,6 +1996,22 @@ export class AgentTeamLedger {
1694
1996
  this.applyInboxDelta(target, operation.data.inbox);
1695
1997
  return;
1696
1998
  }
1999
+ if (operation.kind === 'team/member-archived') {
2000
+ // Archival keeps Memberships: hidden state, not departure — a future
2001
+ // restore returns the Member to its Channels. Visibility filters by
2002
+ // member state instead.
2003
+ target.members.set(operation.data.member.memberId, operation.data.member);
2004
+ for (const claim of operation.data.claims)
2005
+ target.claims.set(claim.claimRef, claim);
2006
+ for (const activity of operation.data.activities)
2007
+ this.appendActivityFact(target, activity);
2008
+ for (const task of operation.data.tasks)
2009
+ target.tasks.set(task.taskRef, task);
2010
+ for (const thread of operation.data.threads)
2011
+ target.threads.set(thread.threadRef, thread);
2012
+ this.applyInboxDelta(target, operation.data.inbox);
2013
+ return;
2014
+ }
1697
2015
  if (operation.kind === 'team/thread-promoted') {
1698
2016
  this.appendActivityFact(target, operation.data.activity);
1699
2017
  target.tasks.set(operation.data.task.taskRef, operation.data.task);
@@ -1944,6 +2262,14 @@ export class AgentTeamLedger {
1944
2262
  const activityMarkers = [...this.state.activityMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
1945
2263
  return this.inboxDelta([], removed, [], markers, [], activityMarkers);
1946
2264
  }
2265
+ /** Attention and marker cleanup for EVERY Member on the given Threads. */
2266
+ channelArchivalInbox(threadRefs) {
2267
+ const removed = [...this.state.attention.values()].filter(attention => threadRefs.has(attention.threadRef))
2268
+ .map(attention => Object.freeze({ memberId: attention.memberId, threadRef: attention.threadRef }));
2269
+ const markers = [...this.state.directMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
2270
+ const activityMarkers = [...this.state.activityMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
2271
+ return this.inboxDelta([], removed, [], markers, [], activityMarkers);
2272
+ }
1947
2273
  inboxDelta(set = [], removed = [], addedMarkers = [], removedMarkers = [], addedActivityMarkers = [], removedActivityMarkers = []) {
1948
2274
  return Object.freeze({
1949
2275
  attention: Object.freeze({ set: Object.freeze(set), removed: Object.freeze(removed) }),
@@ -2019,7 +2345,7 @@ export class AgentTeamLedger {
2019
2345
  if (memberId === AGENT_TEAM_HUMAN_MEMBER_ID)
2020
2346
  continue;
2021
2347
  const member = this.requireMember(memberId);
2022
- if (member.state === 'inactive' || member.workspaceId !== channel.workspaceId || !this.isChannelMember(channel.channelRef, memberId)) {
2348
+ if (member.state === 'inactive' || member.state === 'archived' || member.workspaceId !== channel.workspaceId || !this.isChannelMember(channel.channelRef, memberId)) {
2023
2349
  throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channel.channelRef}'`);
2024
2350
  }
2025
2351
  }
@@ -2070,6 +2396,9 @@ export class AgentTeamLedger {
2070
2396
  threadForActor(actor, workspaceId, taskRef) {
2071
2397
  const task = this.requireTask(workspaceId, taskRef);
2072
2398
  const thread = this.requireThread(task.threadRef);
2399
+ // Archived Channels do not exist on Team API surfaces: their Tasks and
2400
+ // Claims are unreachable for reads (listClaims) as for writes.
2401
+ this.assertThreadChannelActive(task.channelRef);
2073
2402
  if (actor.kind === 'member')
2074
2403
  this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
2075
2404
  return { task, thread };
@@ -2183,13 +2512,55 @@ export class AgentTeamLedger {
2183
2512
  throw new Error('agent-team operation lacks enabled Member authority');
2184
2513
  return member;
2185
2514
  }
2186
- /** Agents sometimes strip the branded prefix when echoing refs; point at the fix instead of a bare lookup failure. */
2515
+ /** Shortest accepted UUID abbreviation after the branded prefix. */
2516
+ static MIN_REF_UUID_PREFIX = 6;
2517
+ isRefAbbreviationTail(tail) {
2518
+ return tail.length >= AgentTeamLedger.MIN_REF_UUID_PREFIX && /^[0-9a-f]+$/i.test(tail);
2519
+ }
2520
+ /**
2521
+ * Full map key for one branded ref, tolerating a unique UUID abbreviation.
2522
+ * Exact refs win; an abbreviated ref (prefix plus at least 6 hex chars,
2523
+ * hyphens ignored) resolves only when it matches exactly one key. Returns
2524
+ * undefined for unknown or ambiguous refs so lenient callers can degrade
2525
+ * without throwing.
2526
+ */
2527
+ uniqueRefKey(map, ref, prefix) {
2528
+ if (map.has(ref))
2529
+ return ref;
2530
+ const tail = ref.startsWith(`${prefix}:`) ? ref.slice(prefix.length + 1).replaceAll('-', '') : undefined;
2531
+ if (tail === undefined || !this.isRefAbbreviationTail(tail))
2532
+ return undefined;
2533
+ const lower = tail.toLowerCase();
2534
+ const candidates = [...map.keys()].filter(key => key.slice(prefix.length + 1).replaceAll('-', '').startsWith(lower));
2535
+ return candidates.length === 1 ? candidates[0] : undefined;
2536
+ }
2537
+ /** uniqueRefKey with the established unknown/ambiguous error contract and hints. */
2538
+ requireRefKey(map, ref, prefix, label) {
2539
+ const key = this.uniqueRefKey(map, ref, prefix);
2540
+ if (key !== undefined)
2541
+ return key;
2542
+ const tail = ref.startsWith(`${prefix}:`) ? ref.slice(prefix.length + 1).replaceAll('-', '') : undefined;
2543
+ if (tail !== undefined && this.isRefAbbreviationTail(tail)) {
2544
+ const lower = tail.toLowerCase();
2545
+ const candidates = [...map.keys()].filter(candidate => candidate.slice(prefix.length + 1).replaceAll('-', '').startsWith(lower));
2546
+ if (candidates.length > 1) {
2547
+ throw new Error(`ambiguous ${label} ref '${ref}' matches ${candidates.map(candidate => `'${candidate}'`).join(', ')}; reuse a longer prefix or the full ref exactly as returned by Team tools`);
2548
+ }
2549
+ }
2550
+ throw new Error(`unknown ${label} ref '${ref}'${this.unknownRefHint(ref, prefix, label)}`);
2551
+ }
2552
+ /** Agents strip the branded prefix or abbreviate UUIDs when echoing refs; point at the fix instead of a bare lookup failure. */
2187
2553
  unknownRefHint(ref, prefix, label) {
2188
- return ref.startsWith(`${prefix}:`) ? ''
2189
- : ` A ${label} ref must start with '${prefix}:'; reuse the full ref exactly as returned by Team tools ('${prefix}:${ref}').`;
2554
+ if (!ref.startsWith(`${prefix}:`)) {
2555
+ return ` A ${label} ref must start with '${prefix}:'; reuse the full ref exactly as returned by Team tools ('${prefix}:${ref}').`;
2556
+ }
2557
+ const tail = ref.slice(prefix.length + 1).replaceAll('-', '');
2558
+ return this.isRefAbbreviationTail(tail)
2559
+ ? ` No ${label} matches this UUID prefix; reuse the full ref exactly as returned by Team tools.`
2560
+ : ` A ${label} ref needs at least 6 hex characters after '${prefix}:', or the full ref exactly as returned by Team tools.`;
2190
2561
  }
2191
2562
  requireTask(workspaceId, taskRef) {
2192
- const task = this.state.tasks.get(taskRef);
2563
+ const task = this.state.tasks.get(this.requireRefKey(this.state.tasks, taskRef, 'task', 'Task'));
2193
2564
  if (task === undefined)
2194
2565
  throw new Error(`unknown Task ref '${taskRef}'${this.unknownRefHint(taskRef, 'task', 'Task')}`);
2195
2566
  if (this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
@@ -2197,7 +2568,7 @@ export class AgentTeamLedger {
2197
2568
  return task;
2198
2569
  }
2199
2570
  requireThread(threadRef) {
2200
- const thread = this.state.threads.get(threadRef);
2571
+ const thread = this.state.threads.get(this.requireRefKey(this.state.threads, threadRef, 'thread', 'Thread'));
2201
2572
  if (thread === undefined)
2202
2573
  throw new Error(`unknown Thread ref '${threadRef}'${this.unknownRefHint(threadRef, 'thread', 'Thread')}`);
2203
2574
  return thread;
@@ -2220,13 +2591,15 @@ export class AgentTeamLedger {
2220
2591
  let thread;
2221
2592
  let task;
2222
2593
  if (request.threadRef !== undefined) {
2223
- thread = projection.threads.get(request.threadRef);
2594
+ const threadKey = this.requireRefKey(projection.threads, request.threadRef, 'thread', 'Thread');
2595
+ thread = projection.threads.get(threadKey);
2224
2596
  if (thread === undefined)
2225
2597
  throw new Error(`unknown Thread ref '${request.threadRef}'${this.unknownRefHint(request.threadRef, 'thread', 'Thread')}`);
2226
2598
  task = thread.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
2227
2599
  }
2228
2600
  if (request.taskRef !== undefined) {
2229
- const byTask = projection.tasks.get(request.taskRef);
2601
+ const taskKey = this.requireRefKey(projection.tasks, request.taskRef, 'task', 'Task');
2602
+ const byTask = projection.tasks.get(taskKey);
2230
2603
  if (byTask === undefined)
2231
2604
  throw new Error(`unknown Task ref '${request.taskRef}'${this.unknownRefHint(request.taskRef, 'task', 'Task')}`);
2232
2605
  if (task !== undefined && task.taskRef !== byTask.taskRef)
@@ -2245,20 +2618,40 @@ export class AgentTeamLedger {
2245
2618
  throw new Error(`Thread '${thread.threadRef}' has no Channel`);
2246
2619
  if (projection.channels.get(channelRef)?.workspaceId !== workspaceId)
2247
2620
  throw new Error(`Thread '${thread.threadRef}' does not belong to Workspace '${workspaceId}'`);
2621
+ // Archived Channels do not exist on Team API surfaces: their Threads are
2622
+ // unreachable by ref, for reads as for writes. Replay stays honest —
2623
+ // operations predating the archival resolve against the then-active
2624
+ // Channel because replay applies in sequence.
2625
+ if (projection.channels.get(channelRef)?.state === 'archived') {
2626
+ throw new Error(`Channel '${channelRef}' is archived and no longer accepts Team work`);
2627
+ }
2248
2628
  if (task !== undefined && task.channelRef !== channelRef)
2249
2629
  throw new Error(`Task '${task.taskRef}' does not belong to Thread '${thread.threadRef}'`);
2250
2630
  return { ...(task === undefined ? {} : { task }), thread, channelRef };
2251
2631
  }
2252
2632
  requireChannel(workspaceId, channelRef) {
2253
- const channel = this.state.channels.get(channelRef);
2633
+ const channel = this.state.channels.get(this.requireRefKey(this.state.channels, channelRef, 'channel', 'Channel'));
2254
2634
  if (channel === undefined)
2255
2635
  throw new Error(`unknown Channel ref '${channelRef}'${this.unknownRefHint(channelRef, 'channel', 'Channel')}`);
2256
2636
  if (channel.workspaceId !== workspaceId)
2257
2637
  throw new Error(`Channel '${channelRef}' does not belong to Workspace '${workspaceId}'`);
2258
2638
  return channel;
2259
2639
  }
2640
+ /** Guard for every mutating or Channel-scoped surface flow: archived Channels reject. */
2641
+ requireActiveChannel(workspaceId, channelRef) {
2642
+ const channel = this.requireChannel(workspaceId, channelRef);
2643
+ if (channel.state === 'archived')
2644
+ throw new Error(`Channel '${channelRef}' is archived and no longer accepts Team work`);
2645
+ return channel;
2646
+ }
2647
+ /** Thread-mutation guard: the Channel owning the Thread must still be active. */
2648
+ assertThreadChannelActive(channelRef) {
2649
+ if (this.state.channels.get(channelRef)?.state === 'archived') {
2650
+ throw new Error(`Channel '${channelRef}' is archived and no longer accepts Team work`);
2651
+ }
2652
+ }
2260
2653
  requireMember(memberId) {
2261
- const member = this.state.members.get(memberId);
2654
+ const member = this.state.members.get(this.requireRefKey(this.state.members, memberId, 'member', 'Agent Member'));
2262
2655
  if (member === undefined)
2263
2656
  throw new Error(`unknown Agent Member '${memberId}'`);
2264
2657
  return member;
@@ -2393,6 +2786,11 @@ export class AgentTeamLedger {
2393
2786
  || operation.data.workspaceId !== request.workspaceId || operation.data.channelRef !== request.channelRef || operation.data.memberId !== request.memberId)
2394
2787
  this.throwRequestCollision(request.requestId);
2395
2788
  }
2789
+ assertSameChannelArchival(operation, request) {
2790
+ if (operation.kind !== 'team/channel-archived' || !this.sameActor(operation.actor, request.actor)
2791
+ || operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef)
2792
+ this.throwRequestCollision(request.requestId);
2793
+ }
2396
2794
  assertSameDm(operation, request) {
2397
2795
  if (operation.kind !== 'team/dm-sent' || !this.sameActor(operation.actor, request.actor)
2398
2796
  || operation.data.workspaceId !== request.workspaceId
@@ -2448,6 +2846,11 @@ export class AgentTeamLedger {
2448
2846
  || operation.data.member.memberId !== request.memberId)
2449
2847
  this.throwRequestCollision(request.requestId);
2450
2848
  }
2849
+ assertSameArchival(operation, request) {
2850
+ if (operation.kind !== 'team/member-archived' || !this.sameActor(operation.actor, request.actor)
2851
+ || operation.data.member.memberId !== request.memberId)
2852
+ this.throwRequestCollision(request.requestId);
2853
+ }
2451
2854
  assertSameAttention(operation, request) {
2452
2855
  if (operation.kind !== 'team/thread-attention-changed' || !this.sameActor(operation.actor, request.actor)
2453
2856
  || operation.data.workspaceId !== request.workspaceId
@@ -2538,6 +2941,10 @@ export class AgentTeamLedger {
2538
2941
  return Object.freeze({ receipt: this.receipt(operation), channelRef: operation.data.channelRef, memberId: operation.data.memberId,
2539
2942
  releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
2540
2943
  }
2944
+ channelArchivalResult(operation) {
2945
+ return Object.freeze({ receipt: this.receipt(operation), channel: operation.data.channel,
2946
+ releasedClaims: operation.data.claims });
2947
+ }
2541
2948
  messageResult(operation) {
2542
2949
  return Object.freeze(this.committedMessageResult(operation));
2543
2950
  }
@@ -2578,6 +2985,10 @@ export class AgentTeamLedger {
2578
2985
  return Object.freeze({ receipt: this.receipt(operation), member: operation.data.member,
2579
2986
  releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
2580
2987
  }
2988
+ archivalResult(operation) {
2989
+ return Object.freeze({ receipt: this.receipt(operation), member: operation.data.member,
2990
+ releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
2991
+ }
2581
2992
  operationBase(request, sequence) {
2582
2993
  return { sequence, operationId: this.createOperationId(), requestId: request.requestId, occurredAt: this.createOccurredAt(),
2583
2994
  actor: Object.freeze({ ...request.actor }), previousOperationId: this.state.ordered.at(-1)?.operationId ?? null };