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