@wowyuarm/dsh-agent-team 0.1.3 → 0.1.4
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 +7 -1
- package/README.zh.md +7 -1
- package/package.json +1 -1
- package/packages/agent-team/README.md +3 -3
- package/packages/agent-team/README.zh.md +3 -3
- package/packages/agent-team/lib/index.js +38 -9
- package/packages/agent-team/lib/ledger.js +275 -108
- package/packages/agent-team/lib/spec.js +27 -11
- package/packages/agent-team/lib/typert.host.js +185 -86
- package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +139 -59
- package/packages/agent-team/lib/types/index.d.ts +12 -7
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +16 -2
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +69 -30
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +1 -1
- package/packages/client-agent-team/README.md +2 -1
- package/packages/client-agent-team/README.zh.md +2 -1
- package/packages/client-agent-team/lib/client.js +625 -319
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +34 -12
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +14 -10
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +6 -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 +4 -4
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +4 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +16 -9
- 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 +71 -16
- 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 -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 +1 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts +24 -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 +24 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +12 -6
- package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -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 +1 -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 +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +13 -0
- package/packages/tool-agent-team/README.md +7 -7
- package/packages/tool-agent-team/README.zh.md +7 -7
- package/packages/tool-agent-team/lib/index.js +38 -28
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -16,7 +16,7 @@ export function agentTeamHumanActor() {
|
|
|
16
16
|
function emptyProjection() {
|
|
17
17
|
return { byRequest: new Map(), byOperation: new Map(), ordered: [], channels: new Map(), members: new Map(), memberships: new Map(),
|
|
18
18
|
claims: new Map(), messages: [], tasks: new Map(), threads: new Map(), attention: new Map(), directMarkers: new Map(), activityMarkers: new Map(),
|
|
19
|
-
orderedFacts: [], factsByThread: new Map(),
|
|
19
|
+
orderedFacts: [], factsByThread: new Map(), channelRefByThread: new Map(), mentionsByMessage: new Map(), messageCountByThread: new Map(),
|
|
20
20
|
attentionByThread: new Map() };
|
|
21
21
|
}
|
|
22
22
|
/** Exhaustiveness guard for the closed operation union: adding a kind must update every dispatch. */
|
|
@@ -282,12 +282,14 @@ export class AgentTeamLedger {
|
|
|
282
282
|
// Human-gated (reply path).
|
|
283
283
|
const sequence = this.nextSequence();
|
|
284
284
|
const base = this.operationBase(request, sequence);
|
|
285
|
-
const
|
|
285
|
+
const asTask = request.asTask !== false;
|
|
286
286
|
const threadRef = this.ref('thread');
|
|
287
|
-
const
|
|
288
|
-
const
|
|
287
|
+
const taskRef = asTask ? this.ref('task') : undefined;
|
|
288
|
+
const task = taskRef === undefined ? undefined
|
|
289
|
+
: Object.freeze({ taskRef, channelRef: channel.channelRef, threadRef, status: 'todo', resolution: 'open' });
|
|
290
|
+
const thread = Object.freeze({ threadRef, ...(taskRef === undefined ? {} : { taskRef }), revision: sequence });
|
|
289
291
|
const message = Object.freeze({
|
|
290
|
-
messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, taskRef,
|
|
292
|
+
messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, ...(taskRef === undefined ? {} : { taskRef }),
|
|
291
293
|
sender: request.actor.memberId, body,
|
|
292
294
|
...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
|
|
293
295
|
topLevel: true, sequence, occurredAt: base.occurredAt,
|
|
@@ -298,7 +300,8 @@ export class AgentTeamLedger {
|
|
|
298
300
|
const inbox = this.messageInboxDelta(message, request.actor.memberId, recipients, started);
|
|
299
301
|
const operation = Object.freeze({
|
|
300
302
|
...base, kind: 'team/message-sent',
|
|
301
|
-
data: Object.freeze({ workspaceId: request.workspaceId, mentions: recipients, message,
|
|
303
|
+
data: Object.freeze({ workspaceId: request.workspaceId, mentions: recipients, message,
|
|
304
|
+
...(task === undefined ? {} : { task }), thread, inbox }),
|
|
302
305
|
});
|
|
303
306
|
await this.table.put(operation.operationId, operation);
|
|
304
307
|
this.apply(operation);
|
|
@@ -314,31 +317,31 @@ export class AgentTeamLedger {
|
|
|
314
317
|
return this.resolved(this.replyResult(existing));
|
|
315
318
|
}
|
|
316
319
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
317
|
-
const { task, thread } = this.
|
|
320
|
+
const { task, thread, channelRef } = this.threadContextForActor(actor, request.workspaceId, request);
|
|
318
321
|
const body = request.body.trim();
|
|
319
322
|
if (body === '')
|
|
320
323
|
throw new Error('message body must not be empty');
|
|
321
|
-
this.assertMentionTargets(this.requireChannel(request.workspaceId,
|
|
324
|
+
this.assertMentionTargets(this.requireChannel(request.workspaceId, channelRef), recipients);
|
|
322
325
|
const deferred = this.deferredThreadWrite(actor.memberId, task, thread, request.baseRevision);
|
|
323
326
|
if (deferred !== undefined)
|
|
324
327
|
return this.resolved(deferred);
|
|
325
|
-
if (task
|
|
328
|
+
if (task?.resolution === 'closed')
|
|
326
329
|
throw new Error(`Task '${task.taskRef}' is closed; reopen it before replying`);
|
|
327
330
|
const unfollowedAgents = recipients.filter(memberId => this.state.members.has(memberId) && !this.isFollowing(thread.threadRef, memberId));
|
|
328
331
|
if (unfollowedAgents.length > 0 && actor.kind === 'member') {
|
|
329
|
-
return this.resolved(this.memberNotFollowing(request.workspaceId,
|
|
332
|
+
return this.resolved(this.memberNotFollowing(request.workspaceId, channelRef, unfollowedAgents, task, thread));
|
|
330
333
|
}
|
|
331
334
|
if (unfollowedAgents.length > 0 && request.confirmationToken === undefined) {
|
|
332
|
-
return this.resolved(this.issueConfirmation(request.actor, request.workspaceId,
|
|
335
|
+
return this.resolved(this.issueConfirmation(request.actor, request.workspaceId, channelRef, body, recipients, task, thread));
|
|
333
336
|
}
|
|
334
337
|
if (request.confirmationToken !== undefined) {
|
|
335
|
-
this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId,
|
|
338
|
+
this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId, channelRef, task, thread, body, recipients);
|
|
336
339
|
}
|
|
337
340
|
const sequence = this.nextSequence();
|
|
338
341
|
const base = this.operationBase(request, sequence);
|
|
339
342
|
const message = Object.freeze({
|
|
340
|
-
messageRef: this.ref('message'), channelRef
|
|
341
|
-
taskRef: task.taskRef, sender: request.actor.memberId, body,
|
|
343
|
+
messageRef: this.ref('message'), channelRef, threadRef: thread.threadRef,
|
|
344
|
+
...(task === undefined ? {} : { taskRef: task.taskRef }), sender: request.actor.memberId, body,
|
|
342
345
|
...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
|
|
343
346
|
topLevel: false, sequence, occurredAt: base.occurredAt,
|
|
344
347
|
});
|
|
@@ -348,13 +351,45 @@ export class AgentTeamLedger {
|
|
|
348
351
|
const operation = Object.freeze({
|
|
349
352
|
...base, kind: 'team/thread-replied',
|
|
350
353
|
data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
|
|
351
|
-
mentions: recipients, message, task, thread: nextThread, inbox }),
|
|
354
|
+
mentions: recipients, message, ...(task === undefined ? {} : { task }), thread: nextThread, inbox }),
|
|
352
355
|
});
|
|
353
356
|
await this.table.put(operation.operationId, operation);
|
|
354
357
|
this.apply(operation);
|
|
355
358
|
return this.committed(this.replyResult(operation));
|
|
356
359
|
});
|
|
357
360
|
}
|
|
361
|
+
promoteThread(request) {
|
|
362
|
+
return this.enqueue(async () => {
|
|
363
|
+
const existing = this.state.byRequest.get(request.requestId);
|
|
364
|
+
if (existing !== undefined) {
|
|
365
|
+
this.assertSamePromotion(existing, request);
|
|
366
|
+
return this.resolved(this.promotionResult(existing));
|
|
367
|
+
}
|
|
368
|
+
this.assertHumanActor(request.actor);
|
|
369
|
+
const { task: existingTask, thread, channelRef } = this.threadContextForActor(request.actor, request.workspaceId, request);
|
|
370
|
+
if (existingTask !== undefined)
|
|
371
|
+
throw new Error(`Thread '${thread.threadRef}' already has Task '${existingTask.taskRef}'`);
|
|
372
|
+
const deferred = this.deferredThreadWrite(request.actor.memberId, undefined, thread, request.baseRevision);
|
|
373
|
+
if (deferred !== undefined)
|
|
374
|
+
return this.resolved(deferred);
|
|
375
|
+
const sequence = this.nextSequence();
|
|
376
|
+
const base = this.operationBase(request, sequence);
|
|
377
|
+
const taskRef = this.ref('task');
|
|
378
|
+
const task = Object.freeze({ taskRef, channelRef, threadRef: thread.threadRef, status: 'todo', resolution: 'open' });
|
|
379
|
+
const nextThread = Object.freeze({ threadRef: thread.threadRef, taskRef, revision: sequence });
|
|
380
|
+
const activity = Object.freeze({ activityRef: this.ref('activity'), kind: 'promote',
|
|
381
|
+
taskRef, threadRef: thread.threadRef, actor: request.actor.memberId, sequence });
|
|
382
|
+
const inbox = this.promoteThreadInbox(activity, thread.threadRef);
|
|
383
|
+
const operation = Object.freeze({
|
|
384
|
+
...base, kind: 'team/thread-promoted',
|
|
385
|
+
data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
|
|
386
|
+
activity, task, thread: nextThread, inbox }),
|
|
387
|
+
});
|
|
388
|
+
await this.table.put(operation.operationId, operation);
|
|
389
|
+
this.apply(operation);
|
|
390
|
+
return this.committed(this.promotionResult(operation));
|
|
391
|
+
});
|
|
392
|
+
}
|
|
358
393
|
changeClaim(request) {
|
|
359
394
|
return this.enqueue(async () => {
|
|
360
395
|
const existing = this.state.byRequest.get(request.requestId);
|
|
@@ -526,10 +561,10 @@ export class AgentTeamLedger {
|
|
|
526
561
|
return this.resolved(this.attentionResult(existing));
|
|
527
562
|
}
|
|
528
563
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
529
|
-
const { task, thread } = this.
|
|
564
|
+
const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
|
|
530
565
|
const current = this.attentionFor(actor.memberId, thread.threadRef);
|
|
531
566
|
if (request.action === 'follow') {
|
|
532
|
-
if (task
|
|
567
|
+
if (task?.resolution === 'closed')
|
|
533
568
|
throw new Error(`Task '${task.taskRef}' is closed; reopen it before following`);
|
|
534
569
|
if (current !== undefined)
|
|
535
570
|
throw new Error(`Member is already following Thread '${thread.threadRef}'`);
|
|
@@ -537,7 +572,7 @@ export class AgentTeamLedger {
|
|
|
537
572
|
const operation = Object.freeze({
|
|
538
573
|
...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
|
|
539
574
|
data: Object.freeze({ workspaceId: request.workspaceId, action: 'follow', memberId: actor.memberId,
|
|
540
|
-
task, thread, inbox: this.inboxDelta([attention]) }),
|
|
575
|
+
...(task === undefined ? {} : { task }), thread, inbox: this.inboxDelta([attention]) }),
|
|
541
576
|
});
|
|
542
577
|
await this.table.put(operation.operationId, operation);
|
|
543
578
|
this.apply(operation);
|
|
@@ -545,12 +580,12 @@ export class AgentTeamLedger {
|
|
|
545
580
|
}
|
|
546
581
|
if (current === undefined)
|
|
547
582
|
throw new Error(`Member is already unfollowed from Thread '${thread.threadRef}'`);
|
|
548
|
-
if (this.hasActiveClaim(actor.memberId, task.taskRef))
|
|
583
|
+
if (task !== undefined && this.hasActiveClaim(actor.memberId, task.taskRef))
|
|
549
584
|
throw new Error('Member cannot unfollow while owning an active Claim');
|
|
550
585
|
const operation = Object.freeze({
|
|
551
586
|
...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
|
|
552
587
|
data: Object.freeze({ workspaceId: request.workspaceId, action: 'unfollow', memberId: actor.memberId,
|
|
553
|
-
task, thread, inbox: this.inboxDelta([], [{ memberId: actor.memberId, threadRef: thread.threadRef }], [], this.directMarkersFor(actor.memberId, thread.threadRef)) }),
|
|
588
|
+
...(task === undefined ? {} : { task }), thread, inbox: this.inboxDelta([], [{ memberId: actor.memberId, threadRef: thread.threadRef }], [], this.directMarkersFor(actor.memberId, thread.threadRef)) }),
|
|
554
589
|
});
|
|
555
590
|
await this.table.put(operation.operationId, operation);
|
|
556
591
|
this.apply(operation);
|
|
@@ -559,11 +594,9 @@ export class AgentTeamLedger {
|
|
|
559
594
|
}
|
|
560
595
|
attentionStatus(actor, request) {
|
|
561
596
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
562
|
-
const { task, thread } = this.
|
|
597
|
+
const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
|
|
563
598
|
const attention = this.attentionFor(authorized.memberId, thread.threadRef);
|
|
564
|
-
return attention === undefined
|
|
565
|
-
? Object.freeze({ task, thread })
|
|
566
|
-
: Object.freeze({ task, thread, attention });
|
|
599
|
+
return Object.freeze({ ...(task === undefined ? {} : { task }), thread, ...(attention === undefined ? {} : { attention }) });
|
|
567
600
|
}
|
|
568
601
|
inbox(actor, request) {
|
|
569
602
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
@@ -571,22 +604,22 @@ export class AgentTeamLedger {
|
|
|
571
604
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
572
605
|
throw new Error('inbox limit must be an integer between 1 and 100');
|
|
573
606
|
const items = [];
|
|
574
|
-
for (const
|
|
575
|
-
|
|
607
|
+
for (const thread of this.state.threads.values()) {
|
|
608
|
+
const channelRef = this.channelRefForThread(thread.threadRef);
|
|
609
|
+
if (channelRef === undefined)
|
|
576
610
|
continue;
|
|
577
|
-
if (
|
|
611
|
+
if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
|
|
612
|
+
continue;
|
|
613
|
+
if (authorized.kind === 'member' && !this.isChannelMember(channelRef, authorized.memberId))
|
|
578
614
|
continue;
|
|
579
|
-
const thread = this.requireThread(task.threadRef);
|
|
580
615
|
const unread = this.unreadFor(authorized.memberId, thread.threadRef);
|
|
581
616
|
if (unread.length === 0)
|
|
582
617
|
continue;
|
|
618
|
+
const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
|
|
583
619
|
const directCount = unread.filter(item => item.direct).length;
|
|
584
620
|
const attention = this.attentionFor(authorized.memberId, thread.threadRef);
|
|
585
|
-
items.push(
|
|
586
|
-
|
|
587
|
-
newestSequence: unread.at(-1).fact.sequence })
|
|
588
|
-
: Object.freeze({ channelRef: task.channelRef, task, thread, unreadCount: unread.length, directCount,
|
|
589
|
-
newestSequence: unread.at(-1).fact.sequence, attention }));
|
|
621
|
+
items.push(Object.freeze({ channelRef, ...(task === undefined ? {} : { task }), thread, unreadCount: unread.length, directCount,
|
|
622
|
+
newestSequence: unread.at(-1).fact.sequence, ...(attention === undefined ? {} : { attention }) }));
|
|
590
623
|
}
|
|
591
624
|
items.sort((left, right) => right.directCount - left.directCount || right.newestSequence - left.newestSequence || left.thread.threadRef.localeCompare(right.thread.threadRef));
|
|
592
625
|
const selected = items.slice(0, limit);
|
|
@@ -611,10 +644,11 @@ export class AgentTeamLedger {
|
|
|
611
644
|
return this.resolved(this.threadReadResult(existing));
|
|
612
645
|
}
|
|
613
646
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
614
|
-
const prepared = this.prepareRead(actor.memberId, request.workspaceId, request
|
|
647
|
+
const prepared = this.prepareRead(actor.memberId, request.workspaceId, request);
|
|
615
648
|
const operation = Object.freeze({
|
|
616
649
|
...this.operationBase(request, this.nextSequence()), kind: 'team/thread-read',
|
|
617
|
-
data: Object.freeze({ workspaceId: request.workspaceId, memberId: actor.memberId,
|
|
650
|
+
data: Object.freeze({ workspaceId: request.workspaceId, memberId: actor.memberId,
|
|
651
|
+
...(prepared.task === undefined ? {} : { task: prepared.task }),
|
|
618
652
|
thread: prepared.thread, claims: prepared.claims, anchor: prepared.anchor, anchorMentions: prepared.anchorMentions,
|
|
619
653
|
facts: prepared.facts,
|
|
620
654
|
readThroughSequence: prepared.readThroughSequence, remainingUnreadCount: prepared.remainingUnreadCount,
|
|
@@ -627,11 +661,11 @@ export class AgentTeamLedger {
|
|
|
627
661
|
}
|
|
628
662
|
threadObservations(actor, request) {
|
|
629
663
|
this.assertHumanActor(actor);
|
|
630
|
-
const task = this.
|
|
664
|
+
const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
|
|
631
665
|
const limit = request.limit ?? 50;
|
|
632
666
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
633
667
|
throw new Error('observation limit must be an integer between 1 and 100');
|
|
634
|
-
const threadRef =
|
|
668
|
+
const threadRef = thread.threadRef;
|
|
635
669
|
const current = new Map();
|
|
636
670
|
const observations = [];
|
|
637
671
|
for (const operation of this.state.ordered) {
|
|
@@ -643,7 +677,7 @@ export class AgentTeamLedger {
|
|
|
643
677
|
continue;
|
|
644
678
|
if (!current.delete(removed.memberId))
|
|
645
679
|
continue;
|
|
646
|
-
observations.push(Object.freeze({ sequence: operation.sequence, threadRef, taskRef: task.taskRef,
|
|
680
|
+
observations.push(Object.freeze({ sequence: operation.sequence, threadRef, ...(task === undefined ? {} : { taskRef: task.taskRef }),
|
|
647
681
|
memberId: removed.memberId, action: 'unfollow' }));
|
|
648
682
|
}
|
|
649
683
|
for (const next of delta.attention.set) {
|
|
@@ -654,7 +688,7 @@ export class AgentTeamLedger {
|
|
|
654
688
|
// Task creation establishes initial Attention; reads only advance its watermark.
|
|
655
689
|
if (operation.kind === 'team/message-sent' || (prior !== undefined && prior.startSequence === next.startSequence))
|
|
656
690
|
continue;
|
|
657
|
-
observations.push(Object.freeze({ sequence: operation.sequence, threadRef, taskRef: task.taskRef,
|
|
691
|
+
observations.push(Object.freeze({ sequence: operation.sequence, threadRef, ...(task === undefined ? {} : { taskRef: task.taskRef }),
|
|
658
692
|
memberId: next.memberId, action: 'follow' }));
|
|
659
693
|
}
|
|
660
694
|
}
|
|
@@ -677,7 +711,7 @@ export class AgentTeamLedger {
|
|
|
677
711
|
}
|
|
678
712
|
threadHistory(actor, request) {
|
|
679
713
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
680
|
-
const { task, thread } = this.
|
|
714
|
+
const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
|
|
681
715
|
const limit = request.limit ?? 20;
|
|
682
716
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
683
717
|
throw new Error('history limit must be an integer between 1 and 100');
|
|
@@ -686,9 +720,9 @@ export class AgentTeamLedger {
|
|
|
686
720
|
throw new Error('beforeSequence must be a positive integer');
|
|
687
721
|
const all = this.threadFacts(thread.threadRef).filter(fact => fact.sequence < before);
|
|
688
722
|
const facts = all.slice(-limit);
|
|
689
|
-
const anchor = this.threadAnchor(
|
|
690
|
-
return Object.freeze({ task, thread, anchor, anchorMentions: this.state.mentionsByMessage.get(anchor.messageRef) ?? [],
|
|
691
|
-
claims: this.claimsForTask(task.taskRef), facts: Object.freeze(facts),
|
|
723
|
+
const anchor = this.threadAnchor(thread.threadRef);
|
|
724
|
+
return Object.freeze({ ...(task === undefined ? {} : { task }), thread, anchor, anchorMentions: this.state.mentionsByMessage.get(anchor.messageRef) ?? [],
|
|
725
|
+
claims: task === undefined ? Object.freeze([]) : this.claimsForTask(task.taskRef), facts: Object.freeze(facts),
|
|
692
726
|
cursor: facts.length === 0 ? before : facts[0].sequence, hasMore: all.length > facts.length });
|
|
693
727
|
}
|
|
694
728
|
listClaims(actor, request) {
|
|
@@ -736,22 +770,27 @@ export class AgentTeamLedger {
|
|
|
736
770
|
}
|
|
737
771
|
if (request.threadRef !== undefined) {
|
|
738
772
|
const thread = this.requireThread(request.threadRef);
|
|
739
|
-
const
|
|
740
|
-
if (
|
|
773
|
+
const channelRef = this.channelRefForThread(thread.threadRef);
|
|
774
|
+
if (channelRef === undefined)
|
|
775
|
+
throw new Error(`unknown Thread ref '${request.threadRef}'`);
|
|
776
|
+
if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
|
|
777
|
+
throw new Error(`Thread '${request.threadRef}' does not belong to Workspace '${request.workspaceId}'`);
|
|
778
|
+
if (request.channelRef !== undefined && channelRef !== request.channelRef)
|
|
741
779
|
throw new Error(`Thread '${request.threadRef}' does not belong to Channel '${request.channelRef}'`);
|
|
742
|
-
if (memberId !== undefined && !this.isChannelMember(
|
|
743
|
-
throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${
|
|
780
|
+
if (memberId !== undefined && !this.isChannelMember(channelRef, memberId))
|
|
781
|
+
throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
|
|
744
782
|
}
|
|
745
783
|
const channels = [...this.state.channels.values()].filter(channel => channel.workspaceId === request.workspaceId
|
|
746
784
|
&& (memberId === undefined || this.isChannelMember(channel.channelRef, memberId)));
|
|
747
785
|
const channelRefs = new Set(channels.map(channel => channel.channelRef));
|
|
748
786
|
const allFacts = this.state.orderedFacts.filter(fact => {
|
|
749
|
-
const
|
|
750
|
-
|
|
787
|
+
const threadRef = fact.kind === 'message' ? fact.message.threadRef : fact.activity.threadRef;
|
|
788
|
+
const channelRef = this.channelRefForThread(threadRef);
|
|
789
|
+
if (channelRef === undefined || !channelRefs.has(channelRef))
|
|
751
790
|
return false;
|
|
752
|
-
if (request.channelRef !== undefined &&
|
|
791
|
+
if (request.channelRef !== undefined && channelRef !== request.channelRef)
|
|
753
792
|
return false;
|
|
754
|
-
if (request.threadRef !== undefined &&
|
|
793
|
+
if (request.threadRef !== undefined && threadRef !== request.threadRef)
|
|
755
794
|
return false;
|
|
756
795
|
if (request.topLevelOnly && (fact.kind !== 'message' || !fact.message.topLevel))
|
|
757
796
|
return false;
|
|
@@ -766,9 +805,9 @@ export class AgentTeamLedger {
|
|
|
766
805
|
const taskNumbers = this.taskNumbers(request.workspaceId);
|
|
767
806
|
const items = selected.filter((fact) => fact.kind === 'message').map(fact => {
|
|
768
807
|
const message = fact.message;
|
|
769
|
-
const task = this.requireTask(request.workspaceId, message.taskRef);
|
|
770
808
|
const thread = this.requireThread(message.threadRef);
|
|
771
|
-
|
|
809
|
+
const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
|
|
810
|
+
return Object.freeze({ message, mentions: fact.mentions, ...(task === undefined ? {} : { task, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }), thread,
|
|
772
811
|
messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0 });
|
|
773
812
|
});
|
|
774
813
|
const initialization = this.initialization();
|
|
@@ -778,7 +817,12 @@ export class AgentTeamLedger {
|
|
|
778
817
|
channels: Object.freeze(channels),
|
|
779
818
|
members: Object.freeze([...this.state.memberships.entries()].filter(([channelRef]) => channelRefs.has(channelRef)).flatMap(([channelRef, ids]) => [...ids].filter(id => this.state.members.get(id)?.state !== 'inactive').sort().map(memberId => Object.freeze({ channelRef, memberId })))),
|
|
780
819
|
tasks: Object.freeze(visibleTasks),
|
|
781
|
-
threads: Object.freeze([...this.state.threads.values()].filter(thread =>
|
|
820
|
+
threads: Object.freeze([...this.state.threads.values()].filter(thread => {
|
|
821
|
+
const channelRef = this.channelRefForThread(thread.threadRef);
|
|
822
|
+
return channelRef !== undefined && channelRefs.has(channelRef)
|
|
823
|
+
&& (request.channelRef === undefined || channelRef === request.channelRef)
|
|
824
|
+
&& (request.threadRef === undefined || thread.threadRef === request.threadRef);
|
|
825
|
+
})),
|
|
782
826
|
taskNumbers: Object.freeze(visibleTasks.map(task => Object.freeze({ taskRef: task.taskRef, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }))),
|
|
783
827
|
items: Object.freeze(items),
|
|
784
828
|
claims: this.claimsForVisibleTasks(visibleTasks),
|
|
@@ -839,6 +883,8 @@ export class AgentTeamLedger {
|
|
|
839
883
|
case 'team/message-sent':
|
|
840
884
|
case 'team/thread-replied':
|
|
841
885
|
return [{ kind: 'channel', channelRef: operation.data.message.channelRef }, { kind: 'thread', threadRef: operation.data.message.threadRef }];
|
|
886
|
+
case 'team/thread-promoted':
|
|
887
|
+
return [{ kind: 'channel', channelRef: operation.data.task.channelRef }, { kind: 'thread', threadRef: operation.data.thread.threadRef }];
|
|
842
888
|
case 'team/claim-created':
|
|
843
889
|
case 'team/claim-done':
|
|
844
890
|
case 'team/claim-released':
|
|
@@ -885,6 +931,8 @@ export class AgentTeamLedger {
|
|
|
885
931
|
case 'team/message-sent':
|
|
886
932
|
case 'team/thread-replied':
|
|
887
933
|
return [operation.data.message.threadRef];
|
|
934
|
+
case 'team/thread-promoted':
|
|
935
|
+
return [operation.data.activity.threadRef];
|
|
888
936
|
case 'team/claim-created':
|
|
889
937
|
case 'team/claim-done':
|
|
890
938
|
case 'team/claim-released':
|
|
@@ -1051,7 +1099,7 @@ export class AgentTeamLedger {
|
|
|
1051
1099
|
if (channel === undefined || member === undefined || operation.data.workspaceId !== channel.workspaceId
|
|
1052
1100
|
|| member.workspaceId !== channel.workspaceId || !projection.memberships.get(channel.channelRef)?.has(member.memberId))
|
|
1053
1101
|
throw new Error('invalid Channel membership removal');
|
|
1054
|
-
const threadRefs = new Set([...projection.
|
|
1102
|
+
const threadRefs = new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === channel.channelRef));
|
|
1055
1103
|
this.validateReleaseCleanup(operation.data, projection, member.memberId, threadRefs, operation.sequence, refs);
|
|
1056
1104
|
return;
|
|
1057
1105
|
}
|
|
@@ -1060,7 +1108,7 @@ export class AgentTeamLedger {
|
|
|
1060
1108
|
const prior = projection.members.get(operation.data.member.memberId);
|
|
1061
1109
|
if (prior === undefined || prior.state === 'inactive' || operation.data.member.state !== 'inactive' || !this.sameMemberIdentity(prior, operation.data.member))
|
|
1062
1110
|
throw new Error('invalid Member removal');
|
|
1063
|
-
const threadRefs = new Set(
|
|
1111
|
+
const threadRefs = new Set(projection.threads.keys());
|
|
1064
1112
|
this.validateReleaseCleanup(operation.data, projection, prior.memberId, threadRefs, operation.sequence, refs);
|
|
1065
1113
|
return;
|
|
1066
1114
|
}
|
|
@@ -1068,15 +1116,17 @@ export class AgentTeamLedger {
|
|
|
1068
1116
|
const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
|
|
1069
1117
|
if (operation.data.memberId !== operation.actor.memberId)
|
|
1070
1118
|
throw new Error('Attention operation has wrong actor');
|
|
1071
|
-
const task = projection.tasks.get(operation.data.task.taskRef);
|
|
1072
1119
|
const thread = projection.threads.get(operation.data.thread.threadRef);
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1120
|
+
const task = operation.data.task;
|
|
1121
|
+
const currentTask = thread?.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
|
|
1122
|
+
const channelRef = thread === undefined ? undefined : this.channelRefForThreadFrom(projection, thread.threadRef);
|
|
1123
|
+
if (thread === undefined || channelRef === undefined || !this.sameThread(thread, operation.data.thread)
|
|
1124
|
+
|| (task === undefined ? currentTask !== undefined : currentTask === undefined || !this.sameTask(currentTask, task) || thread.taskRef !== task.taskRef)
|
|
1125
|
+
|| (actor !== undefined && !projection.memberships.get(channelRef)?.has(actor.memberId)))
|
|
1076
1126
|
throw new Error('invalid Attention operation');
|
|
1077
1127
|
const current = this.attentionForFrom(projection, operation.data.memberId, thread.threadRef);
|
|
1078
|
-
if ((operation.data.action === 'follow' && (task
|
|
1079
|
-
|| (operation.data.action === 'unfollow' && (current === undefined || this.hasActiveClaimFrom(projection, operation.data.memberId, task.taskRef)))) {
|
|
1128
|
+
if ((operation.data.action === 'follow' && (task?.resolution === 'closed' || current !== undefined))
|
|
1129
|
+
|| (operation.data.action === 'unfollow' && (current === undefined || (task !== undefined && this.hasActiveClaimFrom(projection, operation.data.memberId, task.taskRef))))) {
|
|
1080
1130
|
throw new Error('invalid Attention transition');
|
|
1081
1131
|
}
|
|
1082
1132
|
const expected = operation.data.action === 'follow'
|
|
@@ -1094,9 +1144,12 @@ export class AgentTeamLedger {
|
|
|
1094
1144
|
assertHuman();
|
|
1095
1145
|
if (operation.data.memberId !== operation.actor.memberId)
|
|
1096
1146
|
throw new Error('Thread read has wrong actor');
|
|
1097
|
-
const expected = this.prepareReadFrom(projection, operation.data.memberId, operation.data.workspaceId,
|
|
1147
|
+
const expected = this.prepareReadFrom(projection, operation.data.memberId, operation.data.workspaceId, {
|
|
1148
|
+
threadRef: operation.data.thread.threadRef,
|
|
1149
|
+
...(operation.data.task === undefined ? {} : { taskRef: operation.data.task.taskRef }),
|
|
1150
|
+
});
|
|
1098
1151
|
const expectedData = Object.freeze({ workspaceId: operation.data.workspaceId, memberId: operation.data.memberId,
|
|
1099
|
-
task: expected.task, thread: expected.thread, claims: expected.claims, anchor: expected.anchor,
|
|
1152
|
+
...(expected.task === undefined ? {} : { task: expected.task }), thread: expected.thread, claims: expected.claims, anchor: expected.anchor,
|
|
1100
1153
|
anchorMentions: expected.anchorMentions, facts: expected.facts,
|
|
1101
1154
|
readThroughSequence: expected.readThroughSequence, remainingUnreadCount: expected.remainingUnreadCount,
|
|
1102
1155
|
...(expected.attention === undefined ? {} : { attention: expected.attention }), inbox: expected.inbox });
|
|
@@ -1105,27 +1158,56 @@ export class AgentTeamLedger {
|
|
|
1105
1158
|
this.validateInboxDelta(operation.data.inbox, projection, refs);
|
|
1106
1159
|
return;
|
|
1107
1160
|
}
|
|
1161
|
+
if (operation.kind === 'team/thread-promoted') {
|
|
1162
|
+
assertHuman();
|
|
1163
|
+
const { activity, task, thread } = operation.data;
|
|
1164
|
+
const priorThread = projection.threads.get(thread.threadRef);
|
|
1165
|
+
const channelRef = priorThread === undefined ? undefined : this.channelRefForThreadFrom(projection, thread.threadRef);
|
|
1166
|
+
if (priorThread === undefined || priorThread.taskRef !== undefined || projection.tasks.has(task.taskRef)
|
|
1167
|
+
|| channelRef === undefined || task.channelRef !== channelRef
|
|
1168
|
+
|| projection.channels.get(channelRef)?.workspaceId !== operation.data.workspaceId
|
|
1169
|
+
|| operation.data.baseRevision !== priorThread.revision
|
|
1170
|
+
|| task.status !== 'todo' || task.resolution !== 'open'
|
|
1171
|
+
|| thread.revision !== operation.sequence || task.threadRef !== thread.threadRef || thread.taskRef !== task.taskRef
|
|
1172
|
+
|| !this.sameThread(thread, { ...priorThread, taskRef: task.taskRef, revision: operation.sequence })
|
|
1173
|
+
|| activity.kind !== 'promote' || activity.sequence !== operation.sequence || activity.actor !== operation.actor.memberId
|
|
1174
|
+
|| activity.taskRef !== task.taskRef || activity.threadRef !== thread.threadRef)
|
|
1175
|
+
throw new Error('invalid Thread promotion');
|
|
1176
|
+
this.addRef(refs, task.taskRef);
|
|
1177
|
+
this.addRef(refs, activity.activityRef);
|
|
1178
|
+
const expectedInbox = this.promoteThreadInboxFrom(projection, activity, thread.threadRef);
|
|
1179
|
+
if (!isDeepStrictEqual(operation.data.inbox, expectedInbox))
|
|
1180
|
+
throw new Error('invalid Promotion inbox projection');
|
|
1181
|
+
this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1108
1184
|
if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
|
|
1109
1185
|
const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
|
|
1110
1186
|
const { message, task, thread } = operation.data;
|
|
1111
1187
|
const channel = projection.channels.get(message.channelRef);
|
|
1112
1188
|
if (channel === undefined || channel.workspaceId !== operation.data.workspaceId || message.sender !== operation.actor.memberId
|
|
1113
|
-
|| message.sequence !== operation.sequence || thread.revision !== operation.sequence
|
|
1114
|
-
|| message.threadRef !== thread.threadRef
|
|
1189
|
+
|| message.sequence !== operation.sequence || thread.revision !== operation.sequence
|
|
1190
|
+
|| message.threadRef !== thread.threadRef
|
|
1191
|
+
|| (task === undefined ? message.taskRef !== undefined || thread.taskRef !== undefined
|
|
1192
|
+
: message.taskRef !== task.taskRef || thread.taskRef !== task.taskRef || task.threadRef !== thread.threadRef || task.channelRef !== channel.channelRef)
|
|
1115
1193
|
|| (actor !== undefined && !projection.memberships.get(channel.channelRef)?.has(actor.memberId)))
|
|
1116
1194
|
throw new Error('invalid Message operation');
|
|
1117
1195
|
if (operation.kind === 'team/message-sent') {
|
|
1118
|
-
if (!message.topLevel || projection.
|
|
1196
|
+
if (!message.topLevel || projection.threads.has(thread.threadRef) || (task !== undefined && projection.tasks.has(task.taskRef))) {
|
|
1119
1197
|
throw new Error('invalid top-level Message operation');
|
|
1120
|
-
|
|
1198
|
+
}
|
|
1121
1199
|
this.addRef(refs, thread.threadRef);
|
|
1200
|
+
if (task !== undefined)
|
|
1201
|
+
this.addRef(refs, task.taskRef);
|
|
1122
1202
|
}
|
|
1123
1203
|
else {
|
|
1124
|
-
const priorTask = projection.tasks.get(task.taskRef);
|
|
1125
1204
|
const priorThread = projection.threads.get(thread.threadRef);
|
|
1126
|
-
|
|
1127
|
-
|
|
1205
|
+
const priorTask = task === undefined ? undefined : projection.tasks.get(task.taskRef);
|
|
1206
|
+
if (message.topLevel || priorThread === undefined || operation.data.baseRevision !== priorThread.revision
|
|
1207
|
+
|| priorThread.taskRef !== thread.taskRef
|
|
1208
|
+
|| (task === undefined ? priorTask !== undefined : priorTask === undefined || !this.sameTask(priorTask, task))) {
|
|
1128
1209
|
throw new Error('invalid Thread reply');
|
|
1210
|
+
}
|
|
1129
1211
|
}
|
|
1130
1212
|
this.addRef(refs, message.messageRef);
|
|
1131
1213
|
this.validateMentions(operation.data.mentions, message, projection);
|
|
@@ -1444,13 +1526,23 @@ export class AgentTeamLedger {
|
|
|
1444
1526
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
1445
1527
|
return;
|
|
1446
1528
|
}
|
|
1529
|
+
if (operation.kind === 'team/thread-promoted') {
|
|
1530
|
+
this.appendActivityFact(target, operation.data.activity);
|
|
1531
|
+
target.tasks.set(operation.data.task.taskRef, operation.data.task);
|
|
1532
|
+
target.threads.set(operation.data.thread.threadRef, operation.data.thread);
|
|
1533
|
+
this.applyInboxDelta(target, operation.data.inbox);
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1447
1536
|
if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
|
|
1448
1537
|
const { message, mentions } = operation.data;
|
|
1449
1538
|
target.messages.push(message);
|
|
1450
1539
|
target.mentionsByMessage.set(message.messageRef, Object.freeze([...mentions]));
|
|
1451
1540
|
this.appendMessageFact(target, message, mentions);
|
|
1452
|
-
|
|
1541
|
+
if (operation.data.task !== undefined)
|
|
1542
|
+
target.tasks.set(operation.data.task.taskRef, operation.data.task);
|
|
1453
1543
|
target.threads.set(operation.data.thread.threadRef, operation.data.thread);
|
|
1544
|
+
if (message.topLevel)
|
|
1545
|
+
target.channelRefByThread.set(message.threadRef, message.channelRef);
|
|
1454
1546
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
1455
1547
|
return;
|
|
1456
1548
|
}
|
|
@@ -1484,8 +1576,6 @@ export class AgentTeamLedger {
|
|
|
1484
1576
|
const facts = target.factsByThread.get(message.threadRef) ?? [];
|
|
1485
1577
|
facts.push(fact);
|
|
1486
1578
|
target.factsByThread.set(message.threadRef, facts);
|
|
1487
|
-
if (message.topLevel)
|
|
1488
|
-
target.topLevelMessages.push(message);
|
|
1489
1579
|
target.messageCountByThread.set(message.threadRef, (target.messageCountByThread.get(message.threadRef) ?? 0) + 1);
|
|
1490
1580
|
}
|
|
1491
1581
|
appendActivityFact(target, activity) {
|
|
@@ -1520,26 +1610,19 @@ export class AgentTeamLedger {
|
|
|
1520
1610
|
for (const marker of delta.activityMarkers.added)
|
|
1521
1611
|
target.activityMarkers.set(this.activityMarkerKey(marker), marker);
|
|
1522
1612
|
}
|
|
1523
|
-
prepareRead(memberId, workspaceId,
|
|
1524
|
-
return this.prepareReadFrom(this.state, memberId, workspaceId,
|
|
1613
|
+
prepareRead(memberId, workspaceId, request) {
|
|
1614
|
+
return this.prepareReadFrom(this.state, memberId, workspaceId, request);
|
|
1525
1615
|
}
|
|
1526
1616
|
/** Derive the only legal durable result of one Thread read from a prior projection. */
|
|
1527
|
-
prepareReadFrom(projection, memberId, workspaceId,
|
|
1528
|
-
const task =
|
|
1529
|
-
if (task === undefined)
|
|
1530
|
-
throw new Error(`unknown Task ref '${taskRef}'${this.unknownRefHint(taskRef, 'task', 'Task')}`);
|
|
1531
|
-
if (projection.channels.get(task.channelRef)?.workspaceId !== workspaceId)
|
|
1532
|
-
throw new Error(`Task '${taskRef}' does not belong to Workspace '${workspaceId}'`);
|
|
1533
|
-
const thread = projection.threads.get(task.threadRef);
|
|
1534
|
-
if (thread === undefined)
|
|
1535
|
-
throw new Error(`unknown Thread ref '${task.threadRef}'`);
|
|
1617
|
+
prepareReadFrom(projection, memberId, workspaceId, request) {
|
|
1618
|
+
const { task, thread, channelRef } = this.threadContextFrom(projection, workspaceId, request);
|
|
1536
1619
|
if (memberId !== AGENT_TEAM_HUMAN_MEMBER_ID) {
|
|
1537
1620
|
const member = projection.members.get(memberId);
|
|
1538
|
-
if (member === undefined || !projection.memberships.get(
|
|
1539
|
-
throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${
|
|
1621
|
+
if (member === undefined || !projection.memberships.get(channelRef)?.has(member.memberId)) {
|
|
1622
|
+
throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
|
|
1540
1623
|
}
|
|
1541
1624
|
}
|
|
1542
|
-
const anchor = this.threadAnchorFrom(projection,
|
|
1625
|
+
const anchor = this.threadAnchorFrom(projection, thread.threadRef);
|
|
1543
1626
|
const attention = this.attentionForFrom(projection, memberId, thread.threadRef);
|
|
1544
1627
|
const unread = this.unreadForFrom(projection, memberId, thread.threadRef);
|
|
1545
1628
|
const unreadFacts = unread.slice(0, 20);
|
|
@@ -1580,7 +1663,7 @@ export class AgentTeamLedger {
|
|
|
1580
1663
|
};
|
|
1581
1664
|
this.applyInboxDelta(nextProjection, inbox);
|
|
1582
1665
|
const remainingUnreadCount = this.unreadForFrom(nextProjection, memberId, thread.threadRef).length;
|
|
1583
|
-
return Object.freeze({ task, thread, claims: this.claimsForTaskFrom(projection, task.taskRef), anchor,
|
|
1666
|
+
return Object.freeze({ ...(task === undefined ? {} : { task }), thread, claims: task === undefined ? Object.freeze([]) : this.claimsForTaskFrom(projection, task.taskRef), anchor,
|
|
1584
1667
|
anchorMentions: projection.mentionsByMessage.get(anchor.messageRef) ?? [],
|
|
1585
1668
|
facts: Object.freeze(combined),
|
|
1586
1669
|
readThroughSequence, remainingUnreadCount, ...(attention === undefined ? {} : { attention: nextAttention[0] ?? attention }),
|
|
@@ -1670,6 +1753,17 @@ export class AgentTeamLedger {
|
|
|
1670
1753
|
activityRef: activity.activityRef, sequence: activity.sequence }));
|
|
1671
1754
|
return this.inboxDelta([], [], [], [], markers);
|
|
1672
1755
|
}
|
|
1756
|
+
/** Promotion wakes every current follower; the Human actor never follows, so no recipient filter is needed. */
|
|
1757
|
+
promoteThreadInbox(activity, threadRef) {
|
|
1758
|
+
return this.promoteThreadInboxFrom(this.state, activity, threadRef);
|
|
1759
|
+
}
|
|
1760
|
+
promoteThreadInboxFrom(projection, activity, threadRef) {
|
|
1761
|
+
const markers = [...projection.attention.values()]
|
|
1762
|
+
.filter(attention => attention.threadRef === threadRef && attention.memberId !== activity.actor)
|
|
1763
|
+
.map(attention => Object.freeze({ memberId: attention.memberId, threadRef,
|
|
1764
|
+
activityRef: activity.activityRef, sequence: activity.sequence }));
|
|
1765
|
+
return this.inboxDelta([], [], [], [], markers);
|
|
1766
|
+
}
|
|
1673
1767
|
removeMemberThreadInbox(memberId, threadRefs) {
|
|
1674
1768
|
const removed = [...this.state.attention.values()].filter(attention => attention.memberId === memberId && threadRefs.has(attention.threadRef))
|
|
1675
1769
|
.map(attention => Object.freeze({ memberId, threadRef: attention.threadRef }));
|
|
@@ -1762,11 +1856,11 @@ export class AgentTeamLedger {
|
|
|
1762
1856
|
...(task === undefined ? {} : { taskRef: task.taskRef }), ...(thread === undefined ? {} : { threadRef: thread.threadRef, revision: thread.revision }) });
|
|
1763
1857
|
}
|
|
1764
1858
|
unreadRequired(task, thread, unread) {
|
|
1765
|
-
return Object.freeze({ kind: 'unread_required', taskRef: task.taskRef, threadRef: thread.threadRef,
|
|
1859
|
+
return Object.freeze({ kind: 'unread_required', ...(task === undefined ? {} : { taskRef: task.taskRef }), threadRef: thread.threadRef,
|
|
1766
1860
|
revision: thread.revision, unreadCount: unread.length, directCount: unread.filter(item => item.direct).length });
|
|
1767
1861
|
}
|
|
1768
1862
|
staleRevision(task, thread, expectedRevision) {
|
|
1769
|
-
return Object.freeze({ kind: 'stale_revision', taskRef: task.taskRef, threadRef: thread.threadRef,
|
|
1863
|
+
return Object.freeze({ kind: 'stale_revision', ...(task === undefined ? {} : { taskRef: task.taskRef }), threadRef: thread.threadRef,
|
|
1770
1864
|
expectedRevision, revision: thread.revision });
|
|
1771
1865
|
}
|
|
1772
1866
|
releaseSummaries(claims, actor, sequence) {
|
|
@@ -1819,16 +1913,16 @@ export class AgentTeamLedger {
|
|
|
1819
1913
|
/** Commit projections shared by the Message-sent and Thread-replied results. */
|
|
1820
1914
|
committedMessageResult(operation) {
|
|
1821
1915
|
return { kind: 'committed', receipt: this.receipt(operation), message: operation.data.message,
|
|
1822
|
-
task: operation.data.task, thread: operation.data.thread, attention: operation.data.inbox.attention.set,
|
|
1916
|
+
...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread, attention: operation.data.inbox.attention.set,
|
|
1823
1917
|
directMarkers: operation.data.inbox.directMarkers.added };
|
|
1824
1918
|
}
|
|
1825
|
-
threadAnchor(
|
|
1826
|
-
return this.threadAnchorFrom(this.state,
|
|
1919
|
+
threadAnchor(threadRef) {
|
|
1920
|
+
return this.threadAnchorFrom(this.state, threadRef);
|
|
1827
1921
|
}
|
|
1828
|
-
threadAnchorFrom(projection,
|
|
1829
|
-
const anchor = projection.messages.find(message => message.
|
|
1922
|
+
threadAnchorFrom(projection, threadRef) {
|
|
1923
|
+
const anchor = projection.messages.find(message => message.threadRef === threadRef && message.topLevel);
|
|
1830
1924
|
if (anchor === undefined)
|
|
1831
|
-
throw new Error(`
|
|
1925
|
+
throw new Error(`Thread '${threadRef}' has no anchor Message`);
|
|
1832
1926
|
return anchor;
|
|
1833
1927
|
}
|
|
1834
1928
|
threadFactKey(fact) {
|
|
@@ -1843,12 +1937,15 @@ export class AgentTeamLedger {
|
|
|
1843
1937
|
taskNumbers(workspaceId) {
|
|
1844
1938
|
const numbers = new Map();
|
|
1845
1939
|
const next = new Map();
|
|
1846
|
-
for (const
|
|
1847
|
-
|
|
1940
|
+
for (const operation of this.state.ordered) {
|
|
1941
|
+
const task = operation.kind === 'team/message-sent' || operation.kind === 'team/thread-promoted' ? operation.data.task : undefined;
|
|
1942
|
+
if (task === undefined)
|
|
1943
|
+
continue;
|
|
1944
|
+
if (this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
|
|
1848
1945
|
continue;
|
|
1849
|
-
const ordinal = (next.get(
|
|
1850
|
-
next.set(
|
|
1851
|
-
numbers.set(
|
|
1946
|
+
const ordinal = (next.get(task.channelRef) ?? 0) + 1;
|
|
1947
|
+
next.set(task.channelRef, ordinal);
|
|
1948
|
+
numbers.set(task.taskRef, ordinal);
|
|
1852
1949
|
}
|
|
1853
1950
|
return numbers;
|
|
1854
1951
|
}
|
|
@@ -1932,6 +2029,53 @@ export class AgentTeamLedger {
|
|
|
1932
2029
|
throw new Error(`unknown Thread ref '${threadRef}'${this.unknownRefHint(threadRef, 'thread', 'Thread')}`);
|
|
1933
2030
|
return thread;
|
|
1934
2031
|
}
|
|
2032
|
+
channelRefForThread(threadRef) {
|
|
2033
|
+
return this.channelRefForThreadFrom(this.state, threadRef);
|
|
2034
|
+
}
|
|
2035
|
+
channelRefForThreadFrom(projection, threadRef) {
|
|
2036
|
+
return projection.channelRefByThread.get(threadRef);
|
|
2037
|
+
}
|
|
2038
|
+
threadContextForActor(actor, workspaceId, request) {
|
|
2039
|
+
const resolved = this.threadContextFrom(this.state, workspaceId, request);
|
|
2040
|
+
if (actor.kind === 'member')
|
|
2041
|
+
this.requireMemberChannel(this.requireMember(actor.memberId), resolved.channelRef);
|
|
2042
|
+
return resolved;
|
|
2043
|
+
}
|
|
2044
|
+
threadContextFrom(projection, workspaceId, request) {
|
|
2045
|
+
if (request.threadRef === undefined && request.taskRef === undefined)
|
|
2046
|
+
throw new Error('Thread addressing requires threadRef or taskRef');
|
|
2047
|
+
let thread;
|
|
2048
|
+
let task;
|
|
2049
|
+
if (request.threadRef !== undefined) {
|
|
2050
|
+
thread = projection.threads.get(request.threadRef);
|
|
2051
|
+
if (thread === undefined)
|
|
2052
|
+
throw new Error(`unknown Thread ref '${request.threadRef}'${this.unknownRefHint(request.threadRef, 'thread', 'Thread')}`);
|
|
2053
|
+
task = thread.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
|
|
2054
|
+
}
|
|
2055
|
+
if (request.taskRef !== undefined) {
|
|
2056
|
+
const byTask = projection.tasks.get(request.taskRef);
|
|
2057
|
+
if (byTask === undefined)
|
|
2058
|
+
throw new Error(`unknown Task ref '${request.taskRef}'${this.unknownRefHint(request.taskRef, 'task', 'Task')}`);
|
|
2059
|
+
if (task !== undefined && task.taskRef !== byTask.taskRef)
|
|
2060
|
+
throw new Error(`Task '${request.taskRef}' does not belong to Thread '${request.threadRef}'`);
|
|
2061
|
+
if (thread !== undefined && byTask.threadRef !== thread.threadRef)
|
|
2062
|
+
throw new Error(`Task '${request.taskRef}' does not belong to Thread '${thread.threadRef}'`);
|
|
2063
|
+
task = byTask;
|
|
2064
|
+
thread = projection.threads.get(byTask.threadRef);
|
|
2065
|
+
if (thread === undefined)
|
|
2066
|
+
throw new Error(`unknown Thread ref '${byTask.threadRef}'`);
|
|
2067
|
+
}
|
|
2068
|
+
if (thread === undefined)
|
|
2069
|
+
throw new Error('unknown Thread');
|
|
2070
|
+
const channelRef = this.channelRefForThreadFrom(projection, thread.threadRef);
|
|
2071
|
+
if (channelRef === undefined)
|
|
2072
|
+
throw new Error(`Thread '${thread.threadRef}' has no Channel`);
|
|
2073
|
+
if (projection.channels.get(channelRef)?.workspaceId !== workspaceId)
|
|
2074
|
+
throw new Error(`Thread '${thread.threadRef}' does not belong to Workspace '${workspaceId}'`);
|
|
2075
|
+
if (task !== undefined && task.channelRef !== channelRef)
|
|
2076
|
+
throw new Error(`Task '${task.taskRef}' does not belong to Thread '${thread.threadRef}'`);
|
|
2077
|
+
return { ...(task === undefined ? {} : { task }), thread, channelRef };
|
|
2078
|
+
}
|
|
1935
2079
|
requireChannel(workspaceId, channelRef) {
|
|
1936
2080
|
const channel = this.state.channels.get(channelRef);
|
|
1937
2081
|
if (channel === undefined)
|
|
@@ -2059,16 +2203,28 @@ export class AgentTeamLedger {
|
|
|
2059
2203
|
if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
|
|
2060
2204
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
|
|
2061
2205
|
|| operation.data.message.body !== request.body.trim() || !this.sameList(operation.data.mentions, recipients)
|
|
2062
|
-
|| !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? [])
|
|
2206
|
+
|| !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? [])
|
|
2207
|
+
|| (request.asTask !== false) !== (operation.data.task !== undefined))
|
|
2063
2208
|
this.throwRequestCollision(request.requestId);
|
|
2064
2209
|
}
|
|
2065
2210
|
assertSameReply(operation, request, recipients) {
|
|
2066
2211
|
if (operation.kind !== 'team/thread-replied' || !this.sameActor(operation.actor, request.actor)
|
|
2067
|
-
|| operation.data.workspaceId !== request.workspaceId
|
|
2212
|
+
|| operation.data.workspaceId !== request.workspaceId
|
|
2213
|
+
|| (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
|
|
2214
|
+
|| (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
|
|
2215
|
+
|| (request.threadRef === undefined && request.taskRef === undefined)
|
|
2068
2216
|
|| operation.data.message.body !== request.body.trim() || operation.data.baseRevision !== request.baseRevision
|
|
2069
2217
|
|| !this.sameList(operation.data.mentions, recipients))
|
|
2070
2218
|
this.throwRequestCollision(request.requestId);
|
|
2071
2219
|
}
|
|
2220
|
+
assertSamePromotion(operation, request) {
|
|
2221
|
+
if (operation.kind !== 'team/thread-promoted' || !this.sameActor(operation.actor, request.actor)
|
|
2222
|
+
|| operation.data.workspaceId !== request.workspaceId
|
|
2223
|
+
|| operation.data.thread.threadRef !== request.threadRef
|
|
2224
|
+
|| operation.data.baseRevision !== request.baseRevision) {
|
|
2225
|
+
this.throwRequestCollision(request.requestId);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2072
2228
|
assertSameClaim(operation, request) {
|
|
2073
2229
|
const kind = request.action === 'claim' ? 'team/claim-created' : request.action === 'done' ? 'team/claim-done' : 'team/claim-released';
|
|
2074
2230
|
if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.workspaceId !== request.workspaceId
|
|
@@ -2089,12 +2245,19 @@ export class AgentTeamLedger {
|
|
|
2089
2245
|
}
|
|
2090
2246
|
assertSameAttention(operation, request) {
|
|
2091
2247
|
if (operation.kind !== 'team/thread-attention-changed' || !this.sameActor(operation.actor, request.actor)
|
|
2092
|
-
|| operation.data.workspaceId !== request.workspaceId
|
|
2248
|
+
|| operation.data.workspaceId !== request.workspaceId
|
|
2249
|
+
|| (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
|
|
2250
|
+
|| (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
|
|
2251
|
+
|| (request.threadRef === undefined && request.taskRef === undefined)
|
|
2252
|
+
|| operation.data.action !== request.action)
|
|
2093
2253
|
this.throwRequestCollision(request.requestId);
|
|
2094
2254
|
}
|
|
2095
2255
|
assertSameThreadRead(operation, request) {
|
|
2096
2256
|
if (operation.kind !== 'team/thread-read' || !this.sameActor(operation.actor, request.actor)
|
|
2097
|
-
|| operation.data.workspaceId !== request.workspaceId
|
|
2257
|
+
|| operation.data.workspaceId !== request.workspaceId
|
|
2258
|
+
|| (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
|
|
2259
|
+
|| (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
|
|
2260
|
+
|| (request.threadRef === undefined && request.taskRef === undefined))
|
|
2098
2261
|
this.throwRequestCollision(request.requestId);
|
|
2099
2262
|
}
|
|
2100
2263
|
sameTask(left, right) {
|
|
@@ -2180,13 +2343,17 @@ export class AgentTeamLedger {
|
|
|
2180
2343
|
task: operation.data.task, thread: operation.data.thread, claims: operation.data.claims });
|
|
2181
2344
|
}
|
|
2182
2345
|
attentionResult(operation) {
|
|
2183
|
-
return Object.freeze({ receipt: this.receipt(operation), task: operation.data.task, thread: operation.data.thread,
|
|
2346
|
+
return Object.freeze({ receipt: this.receipt(operation), ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread,
|
|
2184
2347
|
...(operation.data.inbox.attention.set[0] === undefined ? {} : { attention: operation.data.inbox.attention.set[0] }) });
|
|
2185
2348
|
}
|
|
2349
|
+
promotionResult(operation) {
|
|
2350
|
+
return Object.freeze({ kind: 'committed', receipt: this.receipt(operation), activity: operation.data.activity,
|
|
2351
|
+
task: operation.data.task, thread: operation.data.thread });
|
|
2352
|
+
}
|
|
2186
2353
|
threadReadResult(operation) {
|
|
2187
2354
|
// Loaded records are normalized by sortedRecords(); fresh writes always carry instants.
|
|
2188
2355
|
const { anchor, facts } = operation.data;
|
|
2189
|
-
return Object.freeze({ receipt: this.receipt(operation), task: operation.data.task, thread: operation.data.thread,
|
|
2356
|
+
return Object.freeze({ receipt: this.receipt(operation), ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread,
|
|
2190
2357
|
claims: operation.data.claims, anchor, anchorMentions: operation.data.anchorMentions, facts,
|
|
2191
2358
|
readThroughSequence: operation.data.readThroughSequence, remainingUnreadCount: operation.data.remainingUnreadCount,
|
|
2192
2359
|
...(operation.data.attention === undefined ? {} : { attention: operation.data.attention }),
|