@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.
- package/README.md +14 -2
- package/README.zh.md +10 -2
- package/package.json +32 -24
- package/packages/agent-team/lib/attachments.js +157 -0
- package/packages/agent-team/lib/index.js +280 -22
- package/packages/agent-team/lib/ledger.js +98 -22
- package/packages/agent-team/lib/recovery.js +87 -0
- package/packages/agent-team/lib/spec.js +27 -5
- package/packages/agent-team/lib/typert.host.js +313 -26
- package/packages/agent-team/lib/typert.remote-client.d.ts +9 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +234 -17
- package/packages/agent-team/lib/types/attachments.d.ts +69 -0
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +68 -6
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +17 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +42 -0
- package/packages/agent-team/lib/types/recovery.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +94 -3
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +2 -2
- package/packages/client-agent-team/README.md +4 -3
- package/packages/client-agent-team/README.zh.md +4 -3
- package/packages/client-agent-team/lib/client.js +1791 -322
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +5 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +108 -59
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +7 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +80 -19
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +38 -19
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +61 -4
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +12 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +152 -14
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +7 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +93 -16
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +6 -5
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +50 -10
- package/packages/client-agent-team/lib/types/client/locales.d.ts +34 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +34 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +35 -0
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/sidebar-drag.js +70 -0
- package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts +27 -0
- package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/sidebar-order.js +170 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +18 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/task-refs.js +58 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +15 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +30 -2
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
- package/packages/tool-agent-team/lib/index.js +10 -6
- 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,
|
|
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,
|
|
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
|
-
|
|
460
|
-
|
|
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' })
|
|
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.
|
|
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) ||
|
|
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
|
-
:
|
|
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.
|
|
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
|
-
|
|
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()
|
|
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()
|
|
51
|
+
description: z.string(),
|
|
46
52
|
createdAtSequence: z.number().int().positive(),
|
|
47
53
|
}).strict();
|
|
48
|
-
// Ledgers written before message occurredAt existed store bare
|
|
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)
|
|
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(),
|