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