@wowyuarm/dsh-agent-team 0.1.0 → 0.1.2

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 (91) hide show
  1. package/README.md +14 -2
  2. package/README.zh.md +10 -2
  3. package/package.json +32 -24
  4. package/packages/agent-team/lib/attachments.js +157 -0
  5. package/packages/agent-team/lib/index.js +280 -22
  6. package/packages/agent-team/lib/ledger.js +98 -22
  7. package/packages/agent-team/lib/recovery.js +87 -0
  8. package/packages/agent-team/lib/spec.js +27 -5
  9. package/packages/agent-team/lib/typert.host.js +313 -26
  10. package/packages/agent-team/lib/typert.remote-client.d.ts +9 -1
  11. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  12. package/packages/agent-team/lib/typert.remote-client.js +234 -17
  13. package/packages/agent-team/lib/types/attachments.d.ts +69 -0
  14. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -0
  15. package/packages/agent-team/lib/types/index.d.ts +68 -6
  16. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  17. package/packages/agent-team/lib/types/ledger.d.ts +17 -1
  18. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  19. package/packages/agent-team/lib/types/recovery.d.ts +42 -0
  20. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -0
  21. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/types.d.ts +94 -3
  23. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  24. package/packages/agent-team/preset/team-member/agent.cordis.yml +2 -2
  25. package/packages/client-agent-team/README.md +4 -3
  26. package/packages/client-agent-team/README.zh.md +4 -3
  27. package/packages/client-agent-team/lib/client.js +1791 -322
  28. package/packages/client-agent-team/lib/client.js.map +1 -1
  29. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +5 -2
  30. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  31. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +108 -59
  32. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +7 -1
  33. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  34. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +80 -19
  35. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +38 -19
  37. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
  38. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamComposer.js +61 -4
  40. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  42. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  43. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +12 -2
  44. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamMessage.js +152 -14
  46. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
  47. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
  48. package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
  49. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +7 -0
  50. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  51. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +93 -16
  52. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +6 -5
  55. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
  56. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
  57. package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
  58. package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
  59. package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
  60. package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
  61. package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
  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 +50 -10
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts +34 -0
  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 +34 -0
  67. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
  68. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
  69. package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
  70. package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
  71. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
  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 +18 -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 +20 -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 +58 -0
  84. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +15 -0
  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 +30 -2
  87. package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
  88. package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
  89. package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
  90. package/packages/tool-agent-team/lib/index.js +10 -6
  91. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -76,8 +76,6 @@ export class AgentTeamLedger {
76
76
  const description = request.description.trim();
77
77
  if (name === '')
78
78
  throw new Error('channel name must not be empty');
79
- if (description === '')
80
- throw new Error('channel description must not be empty');
81
79
  const memberIds = this.normalizeUnique(request.memberIds, 'initial Channel members');
82
80
  for (const memberId of memberIds)
83
81
  this.assertJoinableMember(request.workspaceId, memberId);
@@ -107,8 +105,6 @@ export class AgentTeamLedger {
107
105
  const description = request.description.trim();
108
106
  if (name === '')
109
107
  throw new Error('channel name must not be empty');
110
- if (description === '')
111
- throw new Error('channel description must not be empty');
112
108
  const channel = Object.freeze({ ...this.requireChannel(request.workspaceId, request.channelRef), name, description });
113
109
  const operation = Object.freeze({
114
110
  ...this.operationBase(request, this.nextSequence()), kind: 'team/channel-updated',
@@ -132,13 +128,11 @@ export class AgentTeamLedger {
132
128
  const presetId = request.presetId.trim();
133
129
  if (handle === '')
134
130
  throw new Error('member handle must not be empty');
135
- if (description === '')
136
- throw new Error('member description must not be empty');
137
131
  if (presetId === '')
138
132
  throw new Error('member preset must not be empty');
133
+ // Description and initial Channels are optional: a Member with neither is
134
+ // still drivable through its DM view, and joins Channels later.
139
135
  const channelRefs = this.normalizeUnique(request.channelRefs, 'initial Member Channels');
140
- if (channelRefs.length === 0)
141
- throw new Error('Agent Member requires at least one initial Channel');
142
136
  for (const channelRef of channelRefs)
143
137
  this.requireChannel(request.workspaceId, channelRef);
144
138
  this.assertHandleAvailable(request.workspaceId, handle);
@@ -172,8 +166,6 @@ export class AgentTeamLedger {
172
166
  const description = request.description.trim();
173
167
  if (handle === '')
174
168
  throw new Error('member handle must not be empty');
175
- if (description === '')
176
- throw new Error('member description must not be empty');
177
169
  if (handle !== prior.handle)
178
170
  this.assertHandleAvailable(prior.workspaceId, handle, prior.memberId);
179
171
  this.assertModelSelection(request.model);
@@ -292,7 +284,9 @@ export class AgentTeamLedger {
292
284
  const thread = Object.freeze({ threadRef, taskRef, revision: sequence });
293
285
  const message = Object.freeze({
294
286
  messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, taskRef,
295
- sender: request.actor.memberId, body, topLevel: true, sequence, occurredAt: base.occurredAt,
287
+ sender: request.actor.memberId, body,
288
+ ...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
289
+ topLevel: true, sequence, occurredAt: base.occurredAt,
296
290
  });
297
291
  const started = [this.startAttention(request.actor.memberId, threadRef, sequence),
298
292
  ...recipients.filter(memberId => this.state.members.has(memberId))
@@ -345,7 +339,9 @@ export class AgentTeamLedger {
345
339
  const base = this.operationBase(request, sequence);
346
340
  const message = Object.freeze({
347
341
  messageRef: this.ref('message'), channelRef: task.channelRef, threadRef: task.threadRef,
348
- taskRef: task.taskRef, sender: request.actor.memberId, body, topLevel: false, sequence, occurredAt: base.occurredAt,
342
+ taskRef: task.taskRef, sender: request.actor.memberId, body,
343
+ ...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
344
+ topLevel: false, sequence, occurredAt: base.occurredAt,
349
345
  });
350
346
  const nextThread = Object.freeze({ ...thread, revision: sequence });
351
347
  const started = unfollowedAgents.map(memberId => this.startAttention(memberId, thread.threadRef, sequence));
@@ -456,13 +452,24 @@ export class AgentTeamLedger {
456
452
  throw new Error(`Task '${task.taskRef}' is already ${task.resolution}`);
457
453
  if (request.action === 'close' && task.resolution === 'closed')
458
454
  throw new Error(`Task '${task.taskRef}' is already closed`);
459
- if (request.action === 'accept' && task.status !== 'in_review')
460
- throw new Error(`Task '${task.taskRef}' must be in_review before acceptance`);
455
+ // Acceptance normally waits for every Claim to finish (in_review);
456
+ // a Human may also accept early while work is in progress, which then
457
+ // completes the still-active Claims inside the same atomic operation.
458
+ if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress') {
459
+ throw new Error(`Task '${task.taskRef}' must be in_review or in_progress before acceptance`);
460
+ }
461
+ const priorActiveClaims = [...this.state.claims.values()].filter(claim => claim.taskRef === task.taskRef && claim.state === 'active');
462
+ if (request.action === 'accept' && task.status === 'in_progress' && priorActiveClaims.length === 0) {
463
+ throw new Error(`Task '${task.taskRef}' has no active Claims to complete for early acceptance`);
464
+ }
461
465
  if (request.action === 'reopen' && task.resolution === 'open')
462
466
  throw new Error(`Task '${task.taskRef}' is already open`);
463
467
  const sequence = this.nextSequence();
464
- 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);
468
+ 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' })
469
+ : request.action === 'accept' && claim.state === 'active' ? Object.freeze({ ...claim, state: 'done' })
470
+ : claim);
465
471
  const releasedClaims = claims.filter(claim => claim.state === 'released' && this.state.claims.get(claim.claimRef)?.state === 'active');
472
+ const completedClaims = claims.filter(claim => claim.state === 'done' && this.state.claims.get(claim.claimRef)?.state === 'active');
466
473
  const resolution = request.action === 'reopen' ? 'open' : request.action === 'accept' ? 'accepted' : 'closed';
467
474
  const status = resolution === 'accepted' ? 'done' : resolution === 'closed' ? 'closed'
468
475
  : this.deriveTaskStatus(task.taskRef, claims);
@@ -470,12 +477,13 @@ export class AgentTeamLedger {
470
477
  const nextThread = Object.freeze({ ...thread, revision: sequence });
471
478
  const activity = Object.freeze({ activityRef: this.ref('activity'), kind: request.action,
472
479
  taskRef: task.taskRef, threadRef: task.threadRef, actor: request.actor.memberId, sequence,
473
- ...(releasedClaims.length === 0 ? {} : { releasedClaimRefs: Object.freeze(releasedClaims.map(claim => claim.claimRef)) }) });
480
+ ...(releasedClaims.length === 0 ? {} : { releasedClaimRefs: Object.freeze(releasedClaims.map(claim => claim.claimRef)) }),
481
+ ...(completedClaims.length === 0 ? {} : { completedClaimRefs: Object.freeze(completedClaims.map(claim => claim.claimRef)) }) });
474
482
  const inbox = request.action === 'close'
475
483
  ? this.closeThreadInbox(activity, thread.threadRef)
476
484
  : request.action === 'reopen'
477
485
  ? this.reopenThreadInbox(activity, thread.threadRef)
478
- : this.inboxDelta();
486
+ : this.acceptThreadInbox(activity, thread.threadRef, completedClaims.map(claim => claim.owner));
479
487
  const operation = Object.freeze({
480
488
  ...this.operationBase(request, sequence), kind: 'team/task-changed',
481
489
  data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
@@ -684,6 +692,17 @@ export class AgentTeamLedger {
684
692
  return undefined;
685
693
  }
686
694
  }
695
+ /** Attachment ids referenced by any stored Message — the GC's keep-set oracle. */
696
+ referencedAttachmentIds() {
697
+ const referenced = new Set();
698
+ for (const fact of this.state.orderedFacts) {
699
+ if (fact.kind !== 'message')
700
+ continue;
701
+ for (const attachment of fact.message.attachments ?? [])
702
+ referenced.add(attachment.attachmentId);
703
+ }
704
+ return referenced;
705
+ }
687
706
  threadHistory(actor, request) {
688
707
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
689
708
  const task = this.requireTask(request.workspaceId, request.taskRef);
@@ -713,6 +732,21 @@ export class AgentTeamLedger {
713
732
  getTask(taskRef) {
714
733
  return this.state.tasks.get(taskRef);
715
734
  }
735
+ getClaim(claimRef) {
736
+ return this.state.claims.get(claimRef);
737
+ }
738
+ /** Navigation facts for message-body Task refs; unknown refs are omitted. */
739
+ resolveTaskRefs(workspaceId, taskRefs) {
740
+ const numbers = this.taskNumbers(workspaceId);
741
+ const resolved = [];
742
+ for (const taskRef of taskRefs) {
743
+ const task = this.state.tasks.get(taskRef);
744
+ if (task === undefined || this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
745
+ continue;
746
+ resolved.push(Object.freeze({ taskRef: task.taskRef, channelRef: task.channelRef, threadRef: task.threadRef, taskNumber: numbers.get(task.taskRef) ?? 0 }));
747
+ }
748
+ return resolved;
749
+ }
716
750
  view(request, memberId) {
717
751
  const limit = request.limit ?? 20;
718
752
  const direction = request.direction ?? 'after';
@@ -814,6 +848,7 @@ export class AgentTeamLedger {
814
848
  case 'team/member-added':
815
849
  case 'team/member-suspended':
816
850
  case 'team/member-resumed':
851
+ case 'team/member-session-restarted':
817
852
  case 'team/member-updated':
818
853
  case 'team/member-removed':
819
854
  return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
@@ -968,7 +1003,7 @@ export class AgentTeamLedger {
968
1003
  if (operation.kind === 'team/member-added') {
969
1004
  assertHuman();
970
1005
  const { member, channelRefs } = operation.data;
971
- if (projection.members.has(member.memberId) || channelRefs.length === 0 || new Set(channelRefs).size !== channelRefs.length)
1006
+ if (projection.members.has(member.memberId) || new Set(channelRefs).size !== channelRefs.length)
972
1007
  throw new Error('invalid Member creation');
973
1008
  this.addRef(refs, member.memberId);
974
1009
  for (const channelRef of channelRefs) {
@@ -987,6 +1022,14 @@ export class AgentTeamLedger {
987
1022
  throw new Error('invalid Member lifecycle transition');
988
1023
  return;
989
1024
  }
1025
+ if (operation.kind === 'team/member-session-restarted') {
1026
+ assertHuman();
1027
+ const prior = projection.members.get(operation.data.member.memberId);
1028
+ // The restart records the unchanged Member: same identity, still enabled.
1029
+ if (prior === undefined || prior.state !== 'enabled' || !this.sameMemberIdentity(prior, operation.data.member))
1030
+ throw new Error('invalid Member restart');
1031
+ return;
1032
+ }
990
1033
  if (operation.kind === 'team/channel-updated') {
991
1034
  assertHuman();
992
1035
  const prior = projection.channels.get(operation.data.channel.channelRef);
@@ -1161,12 +1204,18 @@ export class AgentTeamLedger {
1161
1204
  const expectedResolution = activity.kind === 'accept' ? 'accepted' : activity.kind === 'close' ? 'closed' : 'open';
1162
1205
  const expectedStatus = expectedResolution === 'accepted' ? 'done' : expectedResolution === 'closed' ? 'closed'
1163
1206
  : this.deriveTaskStatus(task.taskRef, operation.data.claims);
1207
+ const expectedCompletedClaimRefs = operation.data.claims
1208
+ .filter(claim => projection.claims.get(claim.claimRef)?.state === 'active' && claim.state === 'done')
1209
+ .map(claim => claim.claimRef);
1164
1210
  if (task.resolution !== expectedResolution || task.status !== expectedStatus
1165
1211
  || (activity.kind !== 'close' && activity.releasedClaimRefs !== undefined)
1212
+ || (activity.kind !== 'accept' && activity.completedClaimRefs !== undefined)
1166
1213
  || (activity.kind === 'close' && !this.sameList(activity.releasedClaimRefs ?? [], operation.data.claims
1167
1214
  .filter(claim => projection.claims.get(claim.claimRef)?.state === 'active' && claim.state === 'released')
1168
- .map(claim => claim.claimRef))))
1215
+ .map(claim => claim.claimRef)))
1216
+ || !this.sameList(activity.completedClaimRefs ?? [], expectedCompletedClaimRefs)) {
1169
1217
  throw new Error('invalid Task state transition');
1218
+ }
1170
1219
  const priorClaims = [...projection.claims.values()].filter(claim => claim.taskRef === task.taskRef).sort((left, right) => left.claimRef.localeCompare(right.claimRef));
1171
1220
  const operationClaims = [...operation.data.claims].sort((left, right) => left.claimRef.localeCompare(right.claimRef));
1172
1221
  if (priorClaims.length !== operationClaims.length || priorClaims.some((claim, index) => claim.claimRef !== operationClaims[index]?.claimRef))
@@ -1177,7 +1226,9 @@ export class AgentTeamLedger {
1177
1226
  throw new Error('invalid Task Claim identity');
1178
1227
  const legal = activity.kind === 'close'
1179
1228
  ? (previousClaim.state === 'active' ? claim.state === 'released' : claim.state === previousClaim.state)
1180
- : claim.state === previousClaim.state;
1229
+ : activity.kind === 'accept'
1230
+ ? (previousClaim.state === 'active' ? claim.state === 'done' : claim.state === previousClaim.state)
1231
+ : claim.state === previousClaim.state;
1181
1232
  if (!legal)
1182
1233
  throw new Error('invalid Task Claim transition');
1183
1234
  }
@@ -1186,7 +1237,7 @@ export class AgentTeamLedger {
1186
1237
  ? this.closeThreadInboxFrom(projection, activity, thread.threadRef)
1187
1238
  : activity.kind === 'reopen'
1188
1239
  ? this.reopenThreadInboxFrom(projection, activity, thread.threadRef)
1189
- : this.inboxDelta();
1240
+ : this.acceptThreadInboxFrom(projection, activity, thread.threadRef, expectedCompletedClaimRefs);
1190
1241
  if (!isDeepStrictEqual(operation.data.inbox, expectedInbox))
1191
1242
  throw new Error('invalid Task inbox projection');
1192
1243
  this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
@@ -1367,6 +1418,11 @@ export class AgentTeamLedger {
1367
1418
  target.members.set(operation.data.member.memberId, operation.data.member);
1368
1419
  return;
1369
1420
  }
1421
+ if (operation.kind === 'team/member-session-restarted') {
1422
+ // Audit-only: identity, transcript, and memory are untouched, so the
1423
+ // projection deliberately does not change.
1424
+ return;
1425
+ }
1370
1426
  if (operation.kind === 'team/channel-updated') {
1371
1427
  target.channels.set(operation.data.channel.channelRef, operation.data.channel);
1372
1428
  return;
@@ -1602,6 +1658,25 @@ export class AgentTeamLedger {
1602
1658
  activityRef: activity.activityRef, sequence: activity.sequence }));
1603
1659
  return this.inboxDelta([], removed, [], [...projection.directMarkers.values()].filter(marker => marker.threadRef === threadRef), activityMarkers, [...projection.activityMarkers.values()].filter(marker => marker.threadRef === threadRef));
1604
1660
  }
1661
+ /**
1662
+ * Early acceptance completes open Claims inside the accept operation; their
1663
+ * owners learn about it through activity markers regardless of whether they
1664
+ * still follow the Thread. Plain accepts (no completed Claims) stay silent.
1665
+ */
1666
+ acceptThreadInbox(activity, threadRef, completedOwners) {
1667
+ if (completedOwners.length === 0)
1668
+ return this.inboxDelta();
1669
+ const recipients = [...new Set(completedOwners)].filter(memberId => memberId !== activity.actor);
1670
+ const markers = recipients.map(memberId => Object.freeze({ memberId, threadRef,
1671
+ activityRef: activity.activityRef, sequence: activity.sequence }));
1672
+ return this.inboxDelta([], [], [], [], markers);
1673
+ }
1674
+ acceptThreadInboxFrom(projection, activity, threadRef, completedClaimRefs) {
1675
+ if (completedClaimRefs.length === 0)
1676
+ return this.inboxDelta();
1677
+ const owners = completedClaimRefs.map(claimRef => projection.claims.get(claimRef)?.owner).filter(owner => owner !== undefined);
1678
+ return this.acceptThreadInbox(activity, threadRef, owners);
1679
+ }
1605
1680
  reopenThreadInbox(activity, threadRef) {
1606
1681
  return this.reopenThreadInboxFrom(this.state, activity, threadRef);
1607
1682
  }
@@ -1972,7 +2047,8 @@ export class AgentTeamLedger {
1972
2047
  assertSameMessage(operation, request, recipients) {
1973
2048
  if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
1974
2049
  || operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
1975
- || operation.data.message.body !== request.body.trim() || !this.sameList(operation.data.mentions, recipients))
2050
+ || operation.data.message.body !== request.body.trim() || !this.sameList(operation.data.mentions, recipients)
2051
+ || !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? []))
1976
2052
  this.throwRequestCollision(request.requestId);
1977
2053
  }
1978
2054
  assertSameReply(operation, request, recipients) {
@@ -0,0 +1,87 @@
1
+ const TRANSIENT_NETWORK_PATTERNS = [/fetch failed/i, /econnreset/i, /etimedout/i, /socket hang up/i];
2
+ const RATE_LIMIT_PATTERNS = [/\b429\b/, /rate limit/i, /\b503\b/, /overloaded/i];
3
+ export function classifyRecoverableError(message) {
4
+ if (RATE_LIMIT_PATTERNS.some(pattern => pattern.test(message)))
5
+ return 'rate limiting';
6
+ if (TRANSIENT_NETWORK_PATTERNS.some(pattern => pattern.test(message)))
7
+ return 'transient network';
8
+ return undefined;
9
+ }
10
+ export const RECOVERY_DELAY_MS = 120_000;
11
+ export const RECOVERY_MAX_ATTEMPTS = 3;
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.
17
+ */
18
+ export class RecoveryCoordinator {
19
+ episodes = new Map();
20
+ inject;
21
+ onStandDown;
22
+ delayMs;
23
+ maxAttempts;
24
+ constructor(options) {
25
+ this.inject = options.inject;
26
+ this.onStandDown = options.onStandDown;
27
+ this.delayMs = options.delayMs ?? RECOVERY_DELAY_MS;
28
+ this.maxAttempts = options.maxAttempts ?? RECOVERY_MAX_ATTEMPTS;
29
+ }
30
+ /** Observe one error occurrence for a Member. */
31
+ onError(memberId, errorMessage) {
32
+ const kind = classifyRecoverableError(errorMessage);
33
+ if (kind === undefined) {
34
+ // A non-retryable failure cancels anything pending: retrying cannot help.
35
+ this.stopTracking(memberId);
36
+ return;
37
+ }
38
+ let episode = this.episodes.get(memberId);
39
+ if (episode === undefined || episode.lastError !== errorMessage) {
40
+ this.cancelTimer(episode);
41
+ episode = { kind, lastError: errorMessage, attempts: 0 };
42
+ this.episodes.set(memberId, episode);
43
+ }
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
+ }
50
+ return;
51
+ }
52
+ if (episode.timer !== undefined)
53
+ return;
54
+ episode.timer = setTimeout(() => {
55
+ episode.timer = undefined;
56
+ episode.attempts += 1;
57
+ try {
58
+ this.inject(memberId, episode.attempts, kind);
59
+ }
60
+ catch {
61
+ this.stopTracking(memberId);
62
+ }
63
+ }, this.delayMs);
64
+ }
65
+ /** A turn ended cleanly (running→idle without an error): the episode is over. */
66
+ onCleanTurnEnd(memberId) {
67
+ this.stopTracking(memberId);
68
+ }
69
+ stopTracking(memberId) {
70
+ const episode = this.episodes.get(memberId);
71
+ if (episode === undefined)
72
+ return;
73
+ this.cancelTimer(episode);
74
+ this.episodes.delete(memberId);
75
+ }
76
+ dispose() {
77
+ for (const episode of this.episodes.values())
78
+ this.cancelTimer(episode);
79
+ this.episodes.clear();
80
+ }
81
+ cancelTimer(episode) {
82
+ if (episode?.timer === undefined)
83
+ return;
84
+ clearTimeout(episode.timer);
85
+ episode.timer = undefined;
86
+ }
87
+ }
@@ -11,6 +11,7 @@ const taskRefSchema = z.string().regex(/^task:[^:]+$/).transform(value => value)
11
11
  const threadRefSchema = z.string().regex(/^thread:[^:]+$/).transform(value => value);
12
12
  const claimRefSchema = z.string().regex(/^claim:[^:]+$/).transform(value => value);
13
13
  const activityRefSchema = z.string().regex(/^activity:[^:]+$/).transform(value => value);
14
+ const attachmentIdSchema = z.string().min(1).transform(value => value);
14
15
  const actorSchema = z.discriminatedUnion('kind', [
15
16
  z.object({ kind: z.literal('human'), memberId: memberIdSchema, handle: z.string().min(1) }).strict(),
16
17
  z.object({ kind: z.literal('member'), memberId: memberIdSchema, handle: z.string().min(1) }).strict(),
@@ -25,13 +26,18 @@ const operationBase = {
25
26
  const modelSelectionSchema = z.object({
26
27
  provider: z.string().min(1),
27
28
  model: z.string().min(1),
28
- }).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
+ }));
29
35
  const memberSchema = z.object({
30
36
  memberId: memberIdSchema,
31
37
  sessionId: sessionIdSchema,
32
38
  workspaceId: workspaceIdSchema,
33
39
  handle: z.string().min(1),
34
- description: z.string().min(1),
40
+ description: z.string(),
35
41
  presetId: z.string().min(1),
36
42
  // Ledgers written before per-Member model selection existed omit the field.
37
43
  model: modelSelectionSchema.optional(),
@@ -42,10 +48,18 @@ const channelSchema = z.object({
42
48
  channelRef: channelRefSchema,
43
49
  workspaceId: workspaceIdSchema,
44
50
  name: z.string().min(1),
45
- description: z.string().min(1),
51
+ description: z.string(),
46
52
  createdAtSequence: z.number().int().positive(),
47
53
  }).strict();
48
- // Ledgers written before message occurredAt existed store bare messages; the union schema below stamps them on load.
54
+ // Ledgers written before message occurredAt or attachments existed store bare
55
+ // messages; the union schema below stamps occurredAt on load and leaves absent
56
+ // attachment metadata undefined, so old ledgers replay unchanged.
57
+ const attachmentSchema = z.object({
58
+ attachmentId: attachmentIdSchema,
59
+ name: z.string().min(1),
60
+ byteSize: z.number().int().nonnegative(),
61
+ mediaType: z.string().min(1),
62
+ }).strict();
49
63
  const messageSchema = z.object({
50
64
  messageRef: messageRefSchema,
51
65
  channelRef: channelRefSchema,
@@ -53,6 +67,7 @@ const messageSchema = z.object({
53
67
  taskRef: taskRefSchema,
54
68
  sender: memberIdSchema,
55
69
  body: z.string().min(1),
70
+ attachments: z.array(attachmentSchema).optional(),
56
71
  topLevel: z.boolean(),
57
72
  sequence: z.number().int().positive(),
58
73
  occurredAt: z.string().datetime().optional(),
@@ -145,6 +160,7 @@ const taskActivitySchema = z.object({
145
160
  ...activityBase,
146
161
  kind: z.union([z.literal('accept'), z.literal('close'), z.literal('reopen')]),
147
162
  releasedClaimRefs: z.array(claimRefSchema).min(1).optional(),
163
+ completedClaimRefs: z.array(claimRefSchema).min(1).optional(),
148
164
  }).strict();
149
165
  const claimsReleasedActivitySchema = z.object({
150
166
  ...activityBase,
@@ -198,7 +214,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
198
214
  ...operationBase,
199
215
  previousOperationId: operationIdSchema.nullable(),
200
216
  kind: z.literal('team/member-added'),
201
- data: z.object({ member: memberSchema, channelRefs: z.array(channelRefSchema).min(1) }).strict(),
217
+ data: z.object({ member: memberSchema, channelRefs: z.array(channelRefSchema) }).strict(),
202
218
  }).strict(),
203
219
  z.object({
204
220
  ...operationBase,
@@ -212,6 +228,12 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
212
228
  kind: z.literal('team/member-resumed'),
213
229
  data: z.object({ member: memberSchema }).strict(),
214
230
  }).strict(),
231
+ z.object({
232
+ ...operationBase,
233
+ previousOperationId: operationIdSchema.nullable(),
234
+ kind: z.literal('team/member-session-restarted'),
235
+ data: z.object({ member: memberSchema }).strict(),
236
+ }).strict(),
215
237
  z.object({
216
238
  ...operationBase,
217
239
  previousOperationId: operationIdSchema.nullable(),