@wowyuarm/dsh-agent-team 0.1.1 → 0.1.3

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 (92) hide show
  1. package/README.md +1 -0
  2. package/README.zh.md +2 -0
  3. package/package.json +42 -25
  4. package/packages/agent-team/README.md +1 -1
  5. package/packages/agent-team/README.zh.md +1 -1
  6. package/packages/agent-team/lib/attachments.js +11 -12
  7. package/packages/agent-team/lib/auto-compaction.js +163 -0
  8. package/packages/agent-team/lib/index.js +262 -189
  9. package/packages/agent-team/lib/ledger.js +140 -109
  10. package/packages/agent-team/lib/recovery.js +30 -35
  11. package/packages/agent-team/lib/spec.js +10 -4
  12. package/packages/agent-team/lib/typert.host.js +68 -71
  13. package/packages/agent-team/lib/typert.remote-client.d.ts +3 -3
  14. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  15. package/packages/agent-team/lib/typert.remote-client.js +41 -56
  16. package/packages/agent-team/lib/types/attachments.d.ts +2 -2
  17. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  18. package/packages/agent-team/lib/types/auto-compaction.d.ts +38 -0
  19. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -0
  20. package/packages/agent-team/lib/types/index.d.ts +49 -24
  21. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/ledger.d.ts +24 -11
  23. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  24. package/packages/agent-team/lib/types/recovery.d.ts +15 -16
  25. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
  26. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/types.d.ts +20 -12
  28. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  29. package/packages/agent-team/preset/team-member/agent.cordis.yml +8 -3
  30. package/packages/client-agent-team/README.md +3 -2
  31. package/packages/client-agent-team/README.zh.md +3 -2
  32. package/packages/client-agent-team/lib/client.js +1507 -525
  33. package/packages/client-agent-team/lib/client.js.map +1 -1
  34. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
  35. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -182
  37. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +3 -1
  38. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +27 -5
  40. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +19 -4
  42. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  43. package/packages/client-agent-team/lib/types/client/TeamComposer.js +22 -2
  44. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  47. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +34 -0
  48. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -0
  49. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +169 -0
  50. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts +17 -0
  51. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -0
  52. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +120 -0
  53. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +7 -2
  54. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamMessage.js +170 -18
  56. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +4 -0
  57. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +70 -13
  59. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  61. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  62. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  63. package/packages/client-agent-team/lib/types/client/index.js +72 -2
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts +8 -2
  65. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/locales.js +8 -2
  67. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +22 -0
  68. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +1 -0
  69. package/packages/client-agent-team/lib/types/client/member-session-input.js +98 -0
  70. package/packages/client-agent-team/lib/types/client/requests.d.ts +16 -0
  71. package/packages/client-agent-team/lib/types/client/requests.d.ts.map +1 -0
  72. package/packages/client-agent-team/lib/types/client/requests.js +22 -0
  73. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +35 -0
  74. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -0
  75. package/packages/client-agent-team/lib/types/client/sidebar-drag.js +70 -0
  76. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts +27 -0
  77. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts.map +1 -0
  78. package/packages/client-agent-team/lib/types/client/sidebar-order.js +170 -0
  79. package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -2
  80. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +25 -0
  82. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -0
  83. package/packages/client-agent-team/lib/types/client/task-refs.js +77 -0
  84. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +54 -1
  85. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  86. package/packages/client-agent-team/lib/types/client/team-formatters.js +83 -2
  87. package/packages/client-agent-team/lib/types/client/team-membership.d.ts.map +1 -1
  88. package/packages/client-agent-team/lib/types/client/team-membership.js +2 -1
  89. package/packages/tool-agent-team/lib/index.js +4 -4
  90. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts +0 -13
  91. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts.map +0 -1
  92. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.js +0 -90
@@ -19,6 +19,10 @@ function emptyProjection() {
19
19
  orderedFacts: [], factsByThread: new Map(), topLevelMessages: [], mentionsByMessage: new Map(), messageCountByThread: new Map(),
20
20
  attentionByThread: new Map() };
21
21
  }
22
+ /** Exhaustiveness guard for the closed operation union: adding a kind must update every dispatch. */
23
+ function assertUnhandledKind(operation) {
24
+ throw new Error(`agent-team ledger does not handle operation kind '${operation.kind}'`);
25
+ }
22
26
  /** Replay and append logic behind the Agent Team service interface. */
23
27
  export class AgentTeamLedger {
24
28
  table;
@@ -191,31 +195,6 @@ export class AgentTeamLedger {
191
195
  resumeMember(request) {
192
196
  return this.setMemberState(request, 'enabled');
193
197
  }
194
- /**
195
- * Audit record of one in-place Member session restart. The operation changes
196
- * no projection state — identity, transcript, and memory are untouched — so
197
- * replay treats it as a marker while the Host re-mints the live handle.
198
- */
199
- restartMember(request) {
200
- return this.enqueue(async () => {
201
- const existing = this.state.byRequest.get(request.requestId);
202
- if (existing !== undefined) {
203
- this.assertSameMemberRestart(existing, request);
204
- return this.resolved(this.memberResult(existing));
205
- }
206
- this.assertHumanActor(request.actor);
207
- const member = this.requireMember(request.memberId);
208
- if (member.state !== 'enabled')
209
- throw new Error(`Agent Member '${member.handle}' is ${member.state}; only enabled Members can be restarted`);
210
- const operation = Object.freeze({
211
- ...this.operationBase(request, this.nextSequence()), kind: 'team/member-session-restarted',
212
- data: Object.freeze({ member }),
213
- });
214
- await this.table.put(operation.operationId, operation);
215
- this.apply(operation);
216
- return this.committed(this.memberResult(operation));
217
- });
218
- }
219
198
  getMember(memberId) {
220
199
  return this.state.members.get(memberId);
221
200
  }
@@ -335,19 +314,14 @@ export class AgentTeamLedger {
335
314
  return this.resolved(this.replyResult(existing));
336
315
  }
337
316
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
338
- const task = this.requireTask(request.workspaceId, request.taskRef);
339
- const thread = this.requireThread(task.threadRef);
340
- if (actor.kind === 'member')
341
- this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
317
+ const { task, thread } = this.threadForActor(actor, request.workspaceId, request.taskRef);
342
318
  const body = request.body.trim();
343
319
  if (body === '')
344
320
  throw new Error('message body must not be empty');
345
321
  this.assertMentionTargets(this.requireChannel(request.workspaceId, task.channelRef), recipients);
346
- const unread = this.unreadFor(actor.memberId, thread.threadRef);
347
- if (unread.length > 0)
348
- return this.resolved(this.unreadRequired(task, thread, unread));
349
- if (request.baseRevision !== thread.revision)
350
- return this.resolved(this.staleRevision(task, thread, request.baseRevision));
322
+ const deferred = this.deferredThreadWrite(actor.memberId, task, thread, request.baseRevision);
323
+ if (deferred !== undefined)
324
+ return this.resolved(deferred);
351
325
  if (task.resolution === 'closed')
352
326
  throw new Error(`Task '${task.taskRef}' is closed; reopen it before replying`);
353
327
  const unfollowedAgents = recipients.filter(memberId => this.state.members.has(memberId) && !this.isFollowing(thread.threadRef, memberId));
@@ -389,15 +363,10 @@ export class AgentTeamLedger {
389
363
  return this.resolved(this.claimResult(existing));
390
364
  }
391
365
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
392
- const task = this.requireTask(request.workspaceId, request.taskRef);
393
- const thread = this.requireThread(task.threadRef);
394
- if (actor.kind === 'member')
395
- this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
396
- const unread = this.unreadFor(actor.memberId, thread.threadRef);
397
- if (unread.length > 0)
398
- return this.resolved(this.unreadRequired(task, thread, unread));
399
- if (request.baseRevision !== thread.revision)
400
- return this.resolved(this.staleRevision(task, thread, request.baseRevision));
366
+ const { task, thread } = this.threadForActor(actor, request.workspaceId, request.taskRef);
367
+ const deferred = this.deferredThreadWrite(actor.memberId, task, thread, request.baseRevision);
368
+ if (deferred !== undefined)
369
+ return this.resolved(deferred);
401
370
  if (task.resolution !== 'open')
402
371
  throw new Error(`Task '${task.taskRef}' is ${task.status}; reopen it before changing Claims`);
403
372
  if (actor.kind !== 'member')
@@ -466,24 +435,32 @@ export class AgentTeamLedger {
466
435
  return this.resolved(this.taskResult(existing));
467
436
  }
468
437
  this.assertHumanActor(request.actor);
469
- const task = this.requireTask(request.workspaceId, request.taskRef);
470
- const thread = this.requireThread(task.threadRef);
471
- const unread = this.unreadFor(request.actor.memberId, thread.threadRef);
472
- if (unread.length > 0)
473
- return this.resolved(this.unreadRequired(task, thread, unread));
474
- if (request.baseRevision !== thread.revision)
475
- return this.resolved(this.staleRevision(task, thread, request.baseRevision));
438
+ const { task, thread } = this.threadForActor(request.actor, request.workspaceId, request.taskRef);
439
+ const deferred = this.deferredThreadWrite(request.actor.memberId, task, thread, request.baseRevision);
440
+ if (deferred !== undefined)
441
+ return this.resolved(deferred);
476
442
  if (request.action === 'accept' && task.resolution !== 'open')
477
443
  throw new Error(`Task '${task.taskRef}' is already ${task.resolution}`);
478
444
  if (request.action === 'close' && task.resolution === 'closed')
479
445
  throw new Error(`Task '${task.taskRef}' is already closed`);
480
- if (request.action === 'accept' && task.status !== 'in_review')
481
- throw new Error(`Task '${task.taskRef}' must be in_review before acceptance`);
446
+ // Acceptance normally waits for every Claim to finish (in_review);
447
+ // a Human may also accept early while work is in progress, which then
448
+ // completes the still-active Claims inside the same atomic operation.
449
+ if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress') {
450
+ throw new Error(`Task '${task.taskRef}' must be in_review or in_progress before acceptance`);
451
+ }
452
+ const priorActiveClaims = [...this.state.claims.values()].filter(claim => claim.taskRef === task.taskRef && claim.state === 'active');
453
+ if (request.action === 'accept' && task.status === 'in_progress' && priorActiveClaims.length === 0) {
454
+ throw new Error(`Task '${task.taskRef}' has no active Claims to complete for early acceptance`);
455
+ }
482
456
  if (request.action === 'reopen' && task.resolution === 'open')
483
457
  throw new Error(`Task '${task.taskRef}' is already open`);
484
458
  const sequence = this.nextSequence();
485
- const claims = [...this.state.claims.values()].filter(claim => claim.taskRef === task.taskRef).map(claim => request.action === 'close' && claim.state === 'active' ? Object.freeze({ ...claim, state: 'released' }) : claim);
459
+ const claims = [...this.state.claims.values()].filter(claim => claim.taskRef === task.taskRef).map(claim => request.action === 'close' && claim.state === 'active' ? Object.freeze({ ...claim, state: 'released' })
460
+ : request.action === 'accept' && claim.state === 'active' ? Object.freeze({ ...claim, state: 'done' })
461
+ : claim);
486
462
  const releasedClaims = claims.filter(claim => claim.state === 'released' && this.state.claims.get(claim.claimRef)?.state === 'active');
463
+ const completedClaims = claims.filter(claim => claim.state === 'done' && this.state.claims.get(claim.claimRef)?.state === 'active');
487
464
  const resolution = request.action === 'reopen' ? 'open' : request.action === 'accept' ? 'accepted' : 'closed';
488
465
  const status = resolution === 'accepted' ? 'done' : resolution === 'closed' ? 'closed'
489
466
  : this.deriveTaskStatus(task.taskRef, claims);
@@ -491,12 +468,13 @@ export class AgentTeamLedger {
491
468
  const nextThread = Object.freeze({ ...thread, revision: sequence });
492
469
  const activity = Object.freeze({ activityRef: this.ref('activity'), kind: request.action,
493
470
  taskRef: task.taskRef, threadRef: task.threadRef, actor: request.actor.memberId, sequence,
494
- ...(releasedClaims.length === 0 ? {} : { releasedClaimRefs: Object.freeze(releasedClaims.map(claim => claim.claimRef)) }) });
471
+ ...(releasedClaims.length === 0 ? {} : { releasedClaimRefs: Object.freeze(releasedClaims.map(claim => claim.claimRef)) }),
472
+ ...(completedClaims.length === 0 ? {} : { completedClaimRefs: Object.freeze(completedClaims.map(claim => claim.claimRef)) }) });
495
473
  const inbox = request.action === 'close'
496
474
  ? this.closeThreadInbox(activity, thread.threadRef)
497
475
  : request.action === 'reopen'
498
476
  ? this.reopenThreadInbox(activity, thread.threadRef)
499
- : this.inboxDelta();
477
+ : this.acceptThreadInbox(activity, thread.threadRef, completedClaims.map(claim => claim.owner));
500
478
  const operation = Object.freeze({
501
479
  ...this.operationBase(request, sequence), kind: 'team/task-changed',
502
480
  data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
@@ -548,10 +526,7 @@ export class AgentTeamLedger {
548
526
  return this.resolved(this.attentionResult(existing));
549
527
  }
550
528
  const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
551
- const task = this.requireTask(request.workspaceId, request.taskRef);
552
- const thread = this.requireThread(task.threadRef);
553
- if (actor.kind === 'member')
554
- this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
529
+ const { task, thread } = this.threadForActor(actor, request.workspaceId, request.taskRef);
555
530
  const current = this.attentionFor(actor.memberId, thread.threadRef);
556
531
  if (request.action === 'follow') {
557
532
  if (task.resolution === 'closed')
@@ -584,10 +559,7 @@ export class AgentTeamLedger {
584
559
  }
585
560
  attentionStatus(actor, request) {
586
561
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
587
- const task = this.requireTask(request.workspaceId, request.taskRef);
588
- if (authorized.kind === 'member')
589
- this.requireMemberChannel(this.requireMember(authorized.memberId), task.channelRef);
590
- const thread = this.requireThread(task.threadRef);
562
+ const { task, thread } = this.threadForActor(authorized, request.workspaceId, request.taskRef);
591
563
  const attention = this.attentionFor(authorized.memberId, thread.threadRef);
592
564
  return attention === undefined
593
565
  ? Object.freeze({ task, thread })
@@ -688,22 +660,9 @@ export class AgentTeamLedger {
688
660
  }
689
661
  return Object.freeze({ items: Object.freeze(observations.slice(-limit)) });
690
662
  }
663
+ /** Every operation carrying an inbox delta drives the Inbox projection; the payload shape decides, not a per-kind list. */
691
664
  attentionDelta(operation) {
692
- switch (operation.kind) {
693
- case 'team/channel-member-removed':
694
- case 'team/message-sent':
695
- case 'team/thread-replied':
696
- case 'team/claim-created':
697
- case 'team/claim-done':
698
- case 'team/claim-released':
699
- case 'team/task-changed':
700
- case 'team/thread-attention-changed':
701
- case 'team/thread-read':
702
- case 'team/member-removed':
703
- return operation.data.inbox;
704
- default:
705
- return undefined;
706
- }
665
+ return 'inbox' in operation.data ? operation.data.inbox : undefined;
707
666
  }
708
667
  /** Attachment ids referenced by any stored Message — the GC's keep-set oracle. */
709
668
  referencedAttachmentIds() {
@@ -718,10 +677,7 @@ export class AgentTeamLedger {
718
677
  }
719
678
  threadHistory(actor, request) {
720
679
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
721
- const task = this.requireTask(request.workspaceId, request.taskRef);
722
- if (authorized.kind === 'member')
723
- this.requireMemberChannel(this.requireMember(authorized.memberId), task.channelRef);
724
- const thread = this.requireThread(task.threadRef);
680
+ const { task, thread } = this.threadForActor(authorized, request.workspaceId, request.taskRef);
725
681
  const limit = request.limit ?? 20;
726
682
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
727
683
  throw new Error('history limit must be an integer between 1 and 100');
@@ -737,14 +693,27 @@ export class AgentTeamLedger {
737
693
  }
738
694
  listClaims(actor, request) {
739
695
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
740
- const task = this.requireTask(request.workspaceId, request.taskRef);
741
- if (authorized.kind === 'member')
742
- this.requireMemberChannel(this.requireMember(authorized.memberId), task.channelRef);
743
- return Object.freeze({ task, thread: this.requireThread(task.threadRef), claims: this.claimsForTask(task.taskRef) });
696
+ const { task, thread } = this.threadForActor(authorized, request.workspaceId, request.taskRef);
697
+ return Object.freeze({ task, thread, claims: this.claimsForTask(task.taskRef) });
744
698
  }
745
699
  getTask(taskRef) {
746
700
  return this.state.tasks.get(taskRef);
747
701
  }
702
+ getClaim(claimRef) {
703
+ return this.state.claims.get(claimRef);
704
+ }
705
+ /** Navigation facts for message-body Task refs; unknown refs are omitted. */
706
+ resolveTaskRefs(workspaceId, taskRefs) {
707
+ const numbers = this.taskNumbers(workspaceId);
708
+ const resolved = [];
709
+ for (const taskRef of taskRefs) {
710
+ const task = this.state.tasks.get(taskRef);
711
+ if (task === undefined || this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
712
+ continue;
713
+ resolved.push(Object.freeze({ taskRef: task.taskRef, channelRef: task.channelRef, threadRef: task.threadRef, taskNumber: numbers.get(task.taskRef) ?? 0 }));
714
+ }
715
+ return resolved;
716
+ }
748
717
  view(request, memberId) {
749
718
  const limit = request.limit ?? 20;
750
719
  const direction = request.direction ?? 'after';
@@ -794,7 +763,7 @@ export class AgentTeamLedger {
794
763
  const visibleTasks = [...this.state.tasks.values()].filter(task => channelRefs.has(task.channelRef)
795
764
  && (request.channelRef === undefined || task.channelRef === request.channelRef)
796
765
  && (request.threadRef === undefined || task.threadRef === request.threadRef));
797
- const taskNumbers = this.taskNumbers(request.workspaceId, request.channelRef);
766
+ const taskNumbers = this.taskNumbers(request.workspaceId);
798
767
  const items = selected.filter((fact) => fact.kind === 'message').map(fact => {
799
768
  const message = fact.message;
800
769
  const task = this.requireTask(request.workspaceId, message.taskRef);
@@ -881,6 +850,8 @@ export class AgentTeamLedger {
881
850
  // A read advances only the reader's private watermark; no projection
882
851
  // visible to other participants changes, so nobody is woken.
883
852
  return [];
853
+ default:
854
+ return assertUnhandledKind(operation);
884
855
  }
885
856
  }
886
857
  /** Members whose Inbox projection may have changed; the Host notifies only live ones. */
@@ -923,8 +894,19 @@ export class AgentTeamLedger {
923
894
  case 'team/channel-member-removed':
924
895
  case 'team/member-removed':
925
896
  return operation.data.activities.map(activity => activity.threadRef);
926
- default:
897
+ case 'team/initialized':
898
+ case 'team/channel-created':
899
+ case 'team/channel-updated':
900
+ case 'team/member-added':
901
+ case 'team/member-suspended':
902
+ case 'team/member-resumed':
903
+ case 'team/member-session-restarted':
904
+ case 'team/member-updated':
905
+ case 'team/channel-member-added':
906
+ case 'team/thread-read':
927
907
  return [];
908
+ default:
909
+ return assertUnhandledKind(operation);
928
910
  }
929
911
  }
930
912
  replay() {
@@ -1001,7 +983,7 @@ export class AgentTeamLedger {
1001
983
  if (operation.kind === 'team/member-added') {
1002
984
  assertHuman();
1003
985
  const { member, channelRefs } = operation.data;
1004
- if (projection.members.has(member.memberId) || channelRefs.length === 0 || new Set(channelRefs).size !== channelRefs.length)
986
+ if (projection.members.has(member.memberId) || new Set(channelRefs).size !== channelRefs.length)
1005
987
  throw new Error('invalid Member creation');
1006
988
  this.addRef(refs, member.memberId);
1007
989
  for (const channelRef of channelRefs) {
@@ -1202,12 +1184,18 @@ export class AgentTeamLedger {
1202
1184
  const expectedResolution = activity.kind === 'accept' ? 'accepted' : activity.kind === 'close' ? 'closed' : 'open';
1203
1185
  const expectedStatus = expectedResolution === 'accepted' ? 'done' : expectedResolution === 'closed' ? 'closed'
1204
1186
  : this.deriveTaskStatus(task.taskRef, operation.data.claims);
1187
+ const expectedCompletedClaimRefs = operation.data.claims
1188
+ .filter(claim => projection.claims.get(claim.claimRef)?.state === 'active' && claim.state === 'done')
1189
+ .map(claim => claim.claimRef);
1205
1190
  if (task.resolution !== expectedResolution || task.status !== expectedStatus
1206
1191
  || (activity.kind !== 'close' && activity.releasedClaimRefs !== undefined)
1192
+ || (activity.kind !== 'accept' && activity.completedClaimRefs !== undefined)
1207
1193
  || (activity.kind === 'close' && !this.sameList(activity.releasedClaimRefs ?? [], operation.data.claims
1208
1194
  .filter(claim => projection.claims.get(claim.claimRef)?.state === 'active' && claim.state === 'released')
1209
- .map(claim => claim.claimRef))))
1195
+ .map(claim => claim.claimRef)))
1196
+ || !this.sameList(activity.completedClaimRefs ?? [], expectedCompletedClaimRefs)) {
1210
1197
  throw new Error('invalid Task state transition');
1198
+ }
1211
1199
  const priorClaims = [...projection.claims.values()].filter(claim => claim.taskRef === task.taskRef).sort((left, right) => left.claimRef.localeCompare(right.claimRef));
1212
1200
  const operationClaims = [...operation.data.claims].sort((left, right) => left.claimRef.localeCompare(right.claimRef));
1213
1201
  if (priorClaims.length !== operationClaims.length || priorClaims.some((claim, index) => claim.claimRef !== operationClaims[index]?.claimRef))
@@ -1218,7 +1206,9 @@ export class AgentTeamLedger {
1218
1206
  throw new Error('invalid Task Claim identity');
1219
1207
  const legal = activity.kind === 'close'
1220
1208
  ? (previousClaim.state === 'active' ? claim.state === 'released' : claim.state === previousClaim.state)
1221
- : claim.state === previousClaim.state;
1209
+ : activity.kind === 'accept'
1210
+ ? (previousClaim.state === 'active' ? claim.state === 'done' : claim.state === previousClaim.state)
1211
+ : claim.state === previousClaim.state;
1222
1212
  if (!legal)
1223
1213
  throw new Error('invalid Task Claim transition');
1224
1214
  }
@@ -1227,12 +1217,13 @@ export class AgentTeamLedger {
1227
1217
  ? this.closeThreadInboxFrom(projection, activity, thread.threadRef)
1228
1218
  : activity.kind === 'reopen'
1229
1219
  ? this.reopenThreadInboxFrom(projection, activity, thread.threadRef)
1230
- : this.inboxDelta();
1220
+ : this.acceptThreadInboxFrom(projection, activity, thread.threadRef, expectedCompletedClaimRefs);
1231
1221
  if (!isDeepStrictEqual(operation.data.inbox, expectedInbox))
1232
1222
  throw new Error('invalid Task inbox projection');
1233
1223
  this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
1234
1224
  return;
1235
1225
  }
1226
+ assertUnhandledKind(operation);
1236
1227
  }
1237
1228
  validateInboxDelta(delta, projection, _refs, additionalThreadRefs = [], additionalMessages = [], additionalActivities = []) {
1238
1229
  const attentionKeys = new Set();
@@ -1482,7 +1473,9 @@ export class AgentTeamLedger {
1482
1473
  }
1483
1474
  if (operation.kind === 'team/thread-attention-changed' || operation.kind === 'team/thread-read') {
1484
1475
  this.applyInboxDelta(target, operation.data.inbox);
1476
+ return;
1485
1477
  }
1478
+ assertUnhandledKind(operation);
1486
1479
  }
1487
1480
  /** Facts arrive in ledger sequence order, so global and per-thread lists stay sorted by append only. */
1488
1481
  appendMessageFact(target, message, mentions) {
@@ -1648,6 +1641,25 @@ export class AgentTeamLedger {
1648
1641
  activityRef: activity.activityRef, sequence: activity.sequence }));
1649
1642
  return this.inboxDelta([], removed, [], [...projection.directMarkers.values()].filter(marker => marker.threadRef === threadRef), activityMarkers, [...projection.activityMarkers.values()].filter(marker => marker.threadRef === threadRef));
1650
1643
  }
1644
+ /**
1645
+ * Early acceptance completes open Claims inside the accept operation; their
1646
+ * owners learn about it through activity markers regardless of whether they
1647
+ * still follow the Thread. Plain accepts (no completed Claims) stay silent.
1648
+ */
1649
+ acceptThreadInbox(activity, threadRef, completedOwners) {
1650
+ if (completedOwners.length === 0)
1651
+ return this.inboxDelta();
1652
+ const recipients = [...new Set(completedOwners)].filter(memberId => memberId !== activity.actor);
1653
+ const markers = recipients.map(memberId => Object.freeze({ memberId, threadRef,
1654
+ activityRef: activity.activityRef, sequence: activity.sequence }));
1655
+ return this.inboxDelta([], [], [], [], markers);
1656
+ }
1657
+ acceptThreadInboxFrom(projection, activity, threadRef, completedClaimRefs) {
1658
+ if (completedClaimRefs.length === 0)
1659
+ return this.inboxDelta();
1660
+ const owners = completedClaimRefs.map(claimRef => projection.claims.get(claimRef)?.owner).filter(owner => owner !== undefined);
1661
+ return this.acceptThreadInbox(activity, threadRef, owners);
1662
+ }
1651
1663
  reopenThreadInbox(activity, threadRef) {
1652
1664
  return this.reopenThreadInboxFrom(this.state, activity, threadRef);
1653
1665
  }
@@ -1787,6 +1799,29 @@ export class AgentTeamLedger {
1787
1799
  return Object.freeze({ ...task, status: this.deriveResolvedTaskStatus(task, projected.values()) });
1788
1800
  }));
1789
1801
  }
1802
+ /** Resolve one Task's Thread for a workspace-authorized actor; Member actors must belong to the Task's Channel. */
1803
+ threadForActor(actor, workspaceId, taskRef) {
1804
+ const task = this.requireTask(workspaceId, taskRef);
1805
+ const thread = this.requireThread(task.threadRef);
1806
+ if (actor.kind === 'member')
1807
+ this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
1808
+ return { task, thread };
1809
+ }
1810
+ /** Business outcomes that defer a thread write: unread work first, then a stale revision. */
1811
+ deferredThreadWrite(actorMemberId, task, thread, baseRevision) {
1812
+ const unread = this.unreadFor(actorMemberId, thread.threadRef);
1813
+ if (unread.length > 0)
1814
+ return this.unreadRequired(task, thread, unread);
1815
+ if (baseRevision !== thread.revision)
1816
+ return this.staleRevision(task, thread, baseRevision);
1817
+ return undefined;
1818
+ }
1819
+ /** Commit projections shared by the Message-sent and Thread-replied results. */
1820
+ committedMessageResult(operation) {
1821
+ return { kind: 'committed', receipt: this.receipt(operation), message: operation.data.message,
1822
+ task: operation.data.task, thread: operation.data.thread, attention: operation.data.inbox.attention.set,
1823
+ directMarkers: operation.data.inbox.directMarkers.added };
1824
+ }
1790
1825
  threadAnchor(task) {
1791
1826
  return this.threadAnchorFrom(this.state, task);
1792
1827
  }
@@ -1799,16 +1834,21 @@ export class AgentTeamLedger {
1799
1834
  threadFactKey(fact) {
1800
1835
  return fact.kind === 'message' ? `message:${fact.message.messageRef}` : `activity:${fact.activity.activityRef}`;
1801
1836
  }
1802
- taskNumbers(workspaceId, channelRef) {
1837
+ /**
1838
+ * Display numbers for Tasks: one counter per home Channel, in creation
1839
+ * order. This is the single numbering authority — Channel cards, Thread
1840
+ * headings, cross-channel ref resolution, and inbox renders all show the
1841
+ * ordinal the Task holds inside its own Channel.
1842
+ */
1843
+ taskNumbers(workspaceId) {
1803
1844
  const numbers = new Map();
1804
- let next = 1;
1845
+ const next = new Map();
1805
1846
  for (const message of this.state.topLevelMessages) {
1806
1847
  if (this.state.channels.get(message.channelRef)?.workspaceId !== workspaceId)
1807
1848
  continue;
1808
- if (channelRef !== undefined && message.channelRef !== channelRef)
1809
- continue;
1810
- numbers.set(message.taskRef, next);
1811
- next += 1;
1849
+ const ordinal = (next.get(message.channelRef) ?? 0) + 1;
1850
+ next.set(message.channelRef, ordinal);
1851
+ numbers.set(message.taskRef, ordinal);
1812
1852
  }
1813
1853
  return numbers;
1814
1854
  }
@@ -1987,11 +2027,6 @@ export class AgentTeamLedger {
1987
2027
  || !this.sameList(operation.data.channelRefs, this.normalizeUnique(request.channelRefs, 'initial Member Channels')))
1988
2028
  this.throwRequestCollision(request.requestId);
1989
2029
  }
1990
- assertSameMemberRestart(operation, request) {
1991
- if (operation.kind !== 'team/member-session-restarted' || !this.sameActor(operation.actor, request.actor)
1992
- || operation.data.member.memberId !== request.memberId)
1993
- this.throwRequestCollision(request.requestId);
1994
- }
1995
2030
  assertSameMemberState(operation, request, state) {
1996
2031
  const kind = state === 'suspended' ? 'team/member-suspended' : 'team/member-resumed';
1997
2032
  if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.member.memberId !== request.memberId)
@@ -2129,14 +2164,10 @@ export class AgentTeamLedger {
2129
2164
  releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
2130
2165
  }
2131
2166
  messageResult(operation) {
2132
- return Object.freeze({ kind: 'committed', receipt: this.receipt(operation), message: operation.data.message,
2133
- task: operation.data.task, thread: operation.data.thread, attention: operation.data.inbox.attention.set,
2134
- directMarkers: operation.data.inbox.directMarkers.added });
2167
+ return Object.freeze(this.committedMessageResult(operation));
2135
2168
  }
2136
2169
  replyResult(operation) {
2137
- return Object.freeze({ kind: 'committed', receipt: this.receipt(operation), message: operation.data.message,
2138
- task: operation.data.task, thread: operation.data.thread, attention: operation.data.inbox.attention.set,
2139
- directMarkers: operation.data.inbox.directMarkers.added });
2170
+ return Object.freeze(this.committedMessageResult(operation));
2140
2171
  }
2141
2172
  claimResult(operation) {
2142
2173
  const attention = operation.data.inbox.attention.set.find(candidate => candidate.memberId === operation.data.claim.owner);
@@ -8,59 +8,55 @@ export function classifyRecoverableError(message) {
8
8
  return undefined;
9
9
  }
10
10
  export const RECOVERY_DELAY_MS = 120_000;
11
- export const RECOVERY_MAX_ATTEMPTS = 3;
11
+ export const RECOVERY_MAX_CONSECUTIVE_ERRORS = 3;
12
12
  /**
13
- * Per-member automatic recovery episodes. One episode covers a run of
14
- * identical errors; a changed error string starts a fresh one, and any clean
15
- * turn end clears the slate. After `maxAttempts` injections of the same error
16
- * the coordinator stands down and leaves the Member in error for the operator.
13
+ * Per-member automatic recovery episodes. An episode is every recoverable
14
+ * `agent/error` occurrence until a clean turn end, regardless of error text
15
+ * or family. Each of the first two occurrences schedules its own wakeup.
17
16
  */
18
17
  export class RecoveryCoordinator {
19
18
  episodes = new Map();
20
- inject;
19
+ wake;
21
20
  onStandDown;
22
21
  delayMs;
23
- maxAttempts;
22
+ maxConsecutiveErrors;
24
23
  constructor(options) {
25
- this.inject = options.inject;
24
+ this.wake = options.wake;
26
25
  this.onStandDown = options.onStandDown;
27
26
  this.delayMs = options.delayMs ?? RECOVERY_DELAY_MS;
28
- this.maxAttempts = options.maxAttempts ?? RECOVERY_MAX_ATTEMPTS;
27
+ this.maxConsecutiveErrors = options.maxConsecutiveErrors ?? RECOVERY_MAX_CONSECUTIVE_ERRORS;
29
28
  }
30
- /** Observe one error occurrence for a Member. */
29
+ /** Observe one `agent/error` occurrence for a Member. */
31
30
  onError(memberId, errorMessage) {
32
- const kind = classifyRecoverableError(errorMessage);
33
- if (kind === undefined) {
34
- // A non-retryable failure cancels anything pending: retrying cannot help.
31
+ if (classifyRecoverableError(errorMessage) === undefined) {
32
+ // A non-recoverable failure cancels anything pending: retrying cannot help.
35
33
  this.stopTracking(memberId);
36
34
  return;
37
35
  }
38
36
  let episode = this.episodes.get(memberId);
39
- if (episode === undefined || episode.lastError !== errorMessage) {
40
- this.cancelTimer(episode);
41
- episode = { kind, lastError: errorMessage, attempts: 0 };
37
+ if (episode === undefined) {
38
+ episode = { consecutiveFailures: 0, timers: new Set() };
42
39
  this.episodes.set(memberId, episode);
43
40
  }
44
- else if (episode.attempts >= this.maxAttempts) {
45
- // The same error survived every allowed injection — stand down once.
46
- if (episode.stoodDown !== true) {
47
- episode.stoodDown = true;
48
- this.onStandDown?.(memberId, episode.attempts);
49
- }
41
+ if (episode.stoodDown === true)
50
42
  return;
51
- }
52
- if (episode.timer !== undefined)
43
+ episode.consecutiveFailures += 1;
44
+ if (episode.consecutiveFailures >= this.maxConsecutiveErrors) {
45
+ this.cancelTimers(episode);
46
+ episode.stoodDown = true;
47
+ this.onStandDown?.(memberId, episode.consecutiveFailures);
53
48
  return;
54
- episode.timer = setTimeout(() => {
55
- episode.timer = undefined;
56
- episode.attempts += 1;
49
+ }
50
+ const timer = setTimeout(() => {
51
+ episode.timers.delete(timer);
57
52
  try {
58
- this.inject(memberId, episode.attempts, kind);
53
+ this.wake(memberId);
59
54
  }
60
55
  catch {
61
56
  this.stopTracking(memberId);
62
57
  }
63
58
  }, this.delayMs);
59
+ episode.timers.add(timer);
64
60
  }
65
61
  /** A turn ended cleanly (running→idle without an error): the episode is over. */
66
62
  onCleanTurnEnd(memberId) {
@@ -70,18 +66,17 @@ export class RecoveryCoordinator {
70
66
  const episode = this.episodes.get(memberId);
71
67
  if (episode === undefined)
72
68
  return;
73
- this.cancelTimer(episode);
69
+ this.cancelTimers(episode);
74
70
  this.episodes.delete(memberId);
75
71
  }
76
72
  dispose() {
77
73
  for (const episode of this.episodes.values())
78
- this.cancelTimer(episode);
74
+ this.cancelTimers(episode);
79
75
  this.episodes.clear();
80
76
  }
81
- cancelTimer(episode) {
82
- if (episode?.timer === undefined)
83
- return;
84
- clearTimeout(episode.timer);
85
- episode.timer = undefined;
77
+ cancelTimers(episode) {
78
+ for (const timer of episode.timers)
79
+ clearTimeout(timer);
80
+ episode.timers.clear();
86
81
  }
87
82
  }
@@ -26,13 +26,18 @@ const operationBase = {
26
26
  const modelSelectionSchema = z.object({
27
27
  provider: z.string().min(1),
28
28
  model: z.string().min(1),
29
- }).strict();
29
+ reasoningEffort: z.string().min(1).transform(value => value).optional(),
30
+ }).strict().transform(({ provider, model, reasoningEffort }) => ({
31
+ provider,
32
+ model,
33
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
34
+ }));
30
35
  const memberSchema = z.object({
31
36
  memberId: memberIdSchema,
32
37
  sessionId: sessionIdSchema,
33
38
  workspaceId: workspaceIdSchema,
34
39
  handle: z.string().min(1),
35
- description: z.string().min(1),
40
+ description: z.string(),
36
41
  presetId: z.string().min(1),
37
42
  // Ledgers written before per-Member model selection existed omit the field.
38
43
  model: modelSelectionSchema.optional(),
@@ -43,7 +48,7 @@ const channelSchema = z.object({
43
48
  channelRef: channelRefSchema,
44
49
  workspaceId: workspaceIdSchema,
45
50
  name: z.string().min(1),
46
- description: z.string().min(1),
51
+ description: z.string(),
47
52
  createdAtSequence: z.number().int().positive(),
48
53
  }).strict();
49
54
  // Ledgers written before message occurredAt or attachments existed store bare
@@ -155,6 +160,7 @@ const taskActivitySchema = z.object({
155
160
  ...activityBase,
156
161
  kind: z.union([z.literal('accept'), z.literal('close'), z.literal('reopen')]),
157
162
  releasedClaimRefs: z.array(claimRefSchema).min(1).optional(),
163
+ completedClaimRefs: z.array(claimRefSchema).min(1).optional(),
158
164
  }).strict();
159
165
  const claimsReleasedActivitySchema = z.object({
160
166
  ...activityBase,
@@ -208,7 +214,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
208
214
  ...operationBase,
209
215
  previousOperationId: operationIdSchema.nullable(),
210
216
  kind: z.literal('team/member-added'),
211
- data: z.object({ member: memberSchema, channelRefs: z.array(channelRefSchema).min(1) }).strict(),
217
+ data: z.object({ member: memberSchema, channelRefs: z.array(channelRefSchema) }).strict(),
212
218
  }).strict(),
213
219
  z.object({
214
220
  ...operationBase,