@wowyuarm/dsh-agent-team 0.1.2 → 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 +8 -1
- package/README.zh.md +9 -1
- package/package.json +12 -3
- package/packages/agent-team/README.md +3 -3
- package/packages/agent-team/README.zh.md +3 -3
- package/packages/agent-team/lib/attachments.js +11 -12
- package/packages/agent-team/lib/auto-compaction.js +163 -0
- package/packages/agent-team/lib/index.js +252 -139
- package/packages/agent-team/lib/ledger.js +344 -170
- package/packages/agent-team/lib/recovery.js +30 -35
- package/packages/agent-team/lib/spec.js +27 -11
- package/packages/agent-team/lib/typert.host.js +197 -88
- 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/attachments.d.ts +2 -2
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/auto-compaction.d.ts +38 -0
- package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +52 -19
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +29 -2
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +15 -16
- package/packages/agent-team/lib/types/recovery.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 +7 -2
- 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 +1577 -863
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +35 -174
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +20 -30
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +3 -2
- 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 -5
- 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/TeamMemberEditor.d.ts +34 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +169 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts +17 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +120 -0
- 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 +94 -47
- 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 +81 -39
- 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 +72 -1
- 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/member-session-input.d.ts +22 -0
- package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/member-session-input.js +98 -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/requests.d.ts +16 -0
- package/packages/client-agent-team/lib/types/client/requests.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/requests.js +22 -0
- 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 +6 -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 +19 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +52 -3
- 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 +74 -4
- package/packages/client-agent-team/lib/types/client/team-membership.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-membership.js +2 -1
- 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
- package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts +0 -13
- package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/TeamInboxPanel.js +0 -90
|
@@ -16,9 +16,13 @@ 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
|
+
/** Exhaustiveness guard for the closed operation union: adding a kind must update every dispatch. */
|
|
23
|
+
function assertUnhandledKind(operation) {
|
|
24
|
+
throw new Error(`agent-team ledger does not handle operation kind '${operation.kind}'`);
|
|
25
|
+
}
|
|
22
26
|
/** Replay and append logic behind the Agent Team service interface. */
|
|
23
27
|
export class AgentTeamLedger {
|
|
24
28
|
table;
|
|
@@ -278,12 +282,14 @@ export class AgentTeamLedger {
|
|
|
278
282
|
// Human-gated (reply path).
|
|
279
283
|
const sequence = this.nextSequence();
|
|
280
284
|
const base = this.operationBase(request, sequence);
|
|
281
|
-
const
|
|
285
|
+
const asTask = request.asTask !== false;
|
|
282
286
|
const threadRef = this.ref('thread');
|
|
283
|
-
const
|
|
284
|
-
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 });
|
|
285
291
|
const message = Object.freeze({
|
|
286
|
-
messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, taskRef,
|
|
292
|
+
messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, ...(taskRef === undefined ? {} : { taskRef }),
|
|
287
293
|
sender: request.actor.memberId, body,
|
|
288
294
|
...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
|
|
289
295
|
topLevel: true, sequence, occurredAt: base.occurredAt,
|
|
@@ -294,7 +300,8 @@ export class AgentTeamLedger {
|
|
|
294
300
|
const inbox = this.messageInboxDelta(message, request.actor.memberId, recipients, started);
|
|
295
301
|
const operation = Object.freeze({
|
|
296
302
|
...base, kind: 'team/message-sent',
|
|
297
|
-
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 }),
|
|
298
305
|
});
|
|
299
306
|
await this.table.put(operation.operationId, operation);
|
|
300
307
|
this.apply(operation);
|
|
@@ -310,36 +317,31 @@ export class AgentTeamLedger {
|
|
|
310
317
|
return this.resolved(this.replyResult(existing));
|
|
311
318
|
}
|
|
312
319
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
313
|
-
const task = this.
|
|
314
|
-
const thread = this.requireThread(task.threadRef);
|
|
315
|
-
if (actor.kind === 'member')
|
|
316
|
-
this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
|
|
320
|
+
const { task, thread, channelRef } = this.threadContextForActor(actor, request.workspaceId, request);
|
|
317
321
|
const body = request.body.trim();
|
|
318
322
|
if (body === '')
|
|
319
323
|
throw new Error('message body must not be empty');
|
|
320
|
-
this.assertMentionTargets(this.requireChannel(request.workspaceId,
|
|
321
|
-
const
|
|
322
|
-
if (
|
|
323
|
-
return this.resolved(
|
|
324
|
-
if (
|
|
325
|
-
return this.resolved(this.staleRevision(task, thread, request.baseRevision));
|
|
326
|
-
if (task.resolution === 'closed')
|
|
324
|
+
this.assertMentionTargets(this.requireChannel(request.workspaceId, channelRef), recipients);
|
|
325
|
+
const deferred = this.deferredThreadWrite(actor.memberId, task, thread, request.baseRevision);
|
|
326
|
+
if (deferred !== undefined)
|
|
327
|
+
return this.resolved(deferred);
|
|
328
|
+
if (task?.resolution === 'closed')
|
|
327
329
|
throw new Error(`Task '${task.taskRef}' is closed; reopen it before replying`);
|
|
328
330
|
const unfollowedAgents = recipients.filter(memberId => this.state.members.has(memberId) && !this.isFollowing(thread.threadRef, memberId));
|
|
329
331
|
if (unfollowedAgents.length > 0 && actor.kind === 'member') {
|
|
330
|
-
return this.resolved(this.memberNotFollowing(request.workspaceId,
|
|
332
|
+
return this.resolved(this.memberNotFollowing(request.workspaceId, channelRef, unfollowedAgents, task, thread));
|
|
331
333
|
}
|
|
332
334
|
if (unfollowedAgents.length > 0 && request.confirmationToken === undefined) {
|
|
333
|
-
return this.resolved(this.issueConfirmation(request.actor, request.workspaceId,
|
|
335
|
+
return this.resolved(this.issueConfirmation(request.actor, request.workspaceId, channelRef, body, recipients, task, thread));
|
|
334
336
|
}
|
|
335
337
|
if (request.confirmationToken !== undefined) {
|
|
336
|
-
this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId,
|
|
338
|
+
this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId, channelRef, task, thread, body, recipients);
|
|
337
339
|
}
|
|
338
340
|
const sequence = this.nextSequence();
|
|
339
341
|
const base = this.operationBase(request, sequence);
|
|
340
342
|
const message = Object.freeze({
|
|
341
|
-
messageRef: this.ref('message'), channelRef
|
|
342
|
-
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,
|
|
343
345
|
...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
|
|
344
346
|
topLevel: false, sequence, occurredAt: base.occurredAt,
|
|
345
347
|
});
|
|
@@ -349,13 +351,45 @@ export class AgentTeamLedger {
|
|
|
349
351
|
const operation = Object.freeze({
|
|
350
352
|
...base, kind: 'team/thread-replied',
|
|
351
353
|
data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
|
|
352
|
-
mentions: recipients, message, task, thread: nextThread, inbox }),
|
|
354
|
+
mentions: recipients, message, ...(task === undefined ? {} : { task }), thread: nextThread, inbox }),
|
|
353
355
|
});
|
|
354
356
|
await this.table.put(operation.operationId, operation);
|
|
355
357
|
this.apply(operation);
|
|
356
358
|
return this.committed(this.replyResult(operation));
|
|
357
359
|
});
|
|
358
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
|
+
}
|
|
359
393
|
changeClaim(request) {
|
|
360
394
|
return this.enqueue(async () => {
|
|
361
395
|
const existing = this.state.byRequest.get(request.requestId);
|
|
@@ -364,15 +398,10 @@ export class AgentTeamLedger {
|
|
|
364
398
|
return this.resolved(this.claimResult(existing));
|
|
365
399
|
}
|
|
366
400
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
367
|
-
const task = this.
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
370
|
-
this.
|
|
371
|
-
const unread = this.unreadFor(actor.memberId, thread.threadRef);
|
|
372
|
-
if (unread.length > 0)
|
|
373
|
-
return this.resolved(this.unreadRequired(task, thread, unread));
|
|
374
|
-
if (request.baseRevision !== thread.revision)
|
|
375
|
-
return this.resolved(this.staleRevision(task, thread, request.baseRevision));
|
|
401
|
+
const { task, thread } = this.threadForActor(actor, request.workspaceId, request.taskRef);
|
|
402
|
+
const deferred = this.deferredThreadWrite(actor.memberId, task, thread, request.baseRevision);
|
|
403
|
+
if (deferred !== undefined)
|
|
404
|
+
return this.resolved(deferred);
|
|
376
405
|
if (task.resolution !== 'open')
|
|
377
406
|
throw new Error(`Task '${task.taskRef}' is ${task.status}; reopen it before changing Claims`);
|
|
378
407
|
if (actor.kind !== 'member')
|
|
@@ -441,13 +470,10 @@ export class AgentTeamLedger {
|
|
|
441
470
|
return this.resolved(this.taskResult(existing));
|
|
442
471
|
}
|
|
443
472
|
this.assertHumanActor(request.actor);
|
|
444
|
-
const task = this.
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return this.resolved(this.unreadRequired(task, thread, unread));
|
|
449
|
-
if (request.baseRevision !== thread.revision)
|
|
450
|
-
return this.resolved(this.staleRevision(task, thread, request.baseRevision));
|
|
473
|
+
const { task, thread } = this.threadForActor(request.actor, request.workspaceId, request.taskRef);
|
|
474
|
+
const deferred = this.deferredThreadWrite(request.actor.memberId, task, thread, request.baseRevision);
|
|
475
|
+
if (deferred !== undefined)
|
|
476
|
+
return this.resolved(deferred);
|
|
451
477
|
if (request.action === 'accept' && task.resolution !== 'open')
|
|
452
478
|
throw new Error(`Task '${task.taskRef}' is already ${task.resolution}`);
|
|
453
479
|
if (request.action === 'close' && task.resolution === 'closed')
|
|
@@ -535,13 +561,10 @@ export class AgentTeamLedger {
|
|
|
535
561
|
return this.resolved(this.attentionResult(existing));
|
|
536
562
|
}
|
|
537
563
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
538
|
-
const task = this.
|
|
539
|
-
const thread = this.requireThread(task.threadRef);
|
|
540
|
-
if (actor.kind === 'member')
|
|
541
|
-
this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
|
|
564
|
+
const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
|
|
542
565
|
const current = this.attentionFor(actor.memberId, thread.threadRef);
|
|
543
566
|
if (request.action === 'follow') {
|
|
544
|
-
if (task
|
|
567
|
+
if (task?.resolution === 'closed')
|
|
545
568
|
throw new Error(`Task '${task.taskRef}' is closed; reopen it before following`);
|
|
546
569
|
if (current !== undefined)
|
|
547
570
|
throw new Error(`Member is already following Thread '${thread.threadRef}'`);
|
|
@@ -549,7 +572,7 @@ export class AgentTeamLedger {
|
|
|
549
572
|
const operation = Object.freeze({
|
|
550
573
|
...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
|
|
551
574
|
data: Object.freeze({ workspaceId: request.workspaceId, action: 'follow', memberId: actor.memberId,
|
|
552
|
-
task, thread, inbox: this.inboxDelta([attention]) }),
|
|
575
|
+
...(task === undefined ? {} : { task }), thread, inbox: this.inboxDelta([attention]) }),
|
|
553
576
|
});
|
|
554
577
|
await this.table.put(operation.operationId, operation);
|
|
555
578
|
this.apply(operation);
|
|
@@ -557,12 +580,12 @@ export class AgentTeamLedger {
|
|
|
557
580
|
}
|
|
558
581
|
if (current === undefined)
|
|
559
582
|
throw new Error(`Member is already unfollowed from Thread '${thread.threadRef}'`);
|
|
560
|
-
if (this.hasActiveClaim(actor.memberId, task.taskRef))
|
|
583
|
+
if (task !== undefined && this.hasActiveClaim(actor.memberId, task.taskRef))
|
|
561
584
|
throw new Error('Member cannot unfollow while owning an active Claim');
|
|
562
585
|
const operation = Object.freeze({
|
|
563
586
|
...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
|
|
564
587
|
data: Object.freeze({ workspaceId: request.workspaceId, action: 'unfollow', memberId: actor.memberId,
|
|
565
|
-
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)) }),
|
|
566
589
|
});
|
|
567
590
|
await this.table.put(operation.operationId, operation);
|
|
568
591
|
this.apply(operation);
|
|
@@ -571,14 +594,9 @@ export class AgentTeamLedger {
|
|
|
571
594
|
}
|
|
572
595
|
attentionStatus(actor, request) {
|
|
573
596
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
574
|
-
const task = this.
|
|
575
|
-
if (authorized.kind === 'member')
|
|
576
|
-
this.requireMemberChannel(this.requireMember(authorized.memberId), task.channelRef);
|
|
577
|
-
const thread = this.requireThread(task.threadRef);
|
|
597
|
+
const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
|
|
578
598
|
const attention = this.attentionFor(authorized.memberId, thread.threadRef);
|
|
579
|
-
return attention === undefined
|
|
580
|
-
? Object.freeze({ task, thread })
|
|
581
|
-
: Object.freeze({ task, thread, attention });
|
|
599
|
+
return Object.freeze({ ...(task === undefined ? {} : { task }), thread, ...(attention === undefined ? {} : { attention }) });
|
|
582
600
|
}
|
|
583
601
|
inbox(actor, request) {
|
|
584
602
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
@@ -586,22 +604,22 @@ export class AgentTeamLedger {
|
|
|
586
604
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
587
605
|
throw new Error('inbox limit must be an integer between 1 and 100');
|
|
588
606
|
const items = [];
|
|
589
|
-
for (const
|
|
590
|
-
|
|
607
|
+
for (const thread of this.state.threads.values()) {
|
|
608
|
+
const channelRef = this.channelRefForThread(thread.threadRef);
|
|
609
|
+
if (channelRef === undefined)
|
|
610
|
+
continue;
|
|
611
|
+
if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
|
|
591
612
|
continue;
|
|
592
|
-
if (authorized.kind === 'member' && !this.isChannelMember(
|
|
613
|
+
if (authorized.kind === 'member' && !this.isChannelMember(channelRef, authorized.memberId))
|
|
593
614
|
continue;
|
|
594
|
-
const thread = this.requireThread(task.threadRef);
|
|
595
615
|
const unread = this.unreadFor(authorized.memberId, thread.threadRef);
|
|
596
616
|
if (unread.length === 0)
|
|
597
617
|
continue;
|
|
618
|
+
const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
|
|
598
619
|
const directCount = unread.filter(item => item.direct).length;
|
|
599
620
|
const attention = this.attentionFor(authorized.memberId, thread.threadRef);
|
|
600
|
-
items.push(
|
|
601
|
-
|
|
602
|
-
newestSequence: unread.at(-1).fact.sequence })
|
|
603
|
-
: Object.freeze({ channelRef: task.channelRef, task, thread, unreadCount: unread.length, directCount,
|
|
604
|
-
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 }) }));
|
|
605
623
|
}
|
|
606
624
|
items.sort((left, right) => right.directCount - left.directCount || right.newestSequence - left.newestSequence || left.thread.threadRef.localeCompare(right.thread.threadRef));
|
|
607
625
|
const selected = items.slice(0, limit);
|
|
@@ -626,10 +644,11 @@ export class AgentTeamLedger {
|
|
|
626
644
|
return this.resolved(this.threadReadResult(existing));
|
|
627
645
|
}
|
|
628
646
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
629
|
-
const prepared = this.prepareRead(actor.memberId, request.workspaceId, request
|
|
647
|
+
const prepared = this.prepareRead(actor.memberId, request.workspaceId, request);
|
|
630
648
|
const operation = Object.freeze({
|
|
631
649
|
...this.operationBase(request, this.nextSequence()), kind: 'team/thread-read',
|
|
632
|
-
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 }),
|
|
633
652
|
thread: prepared.thread, claims: prepared.claims, anchor: prepared.anchor, anchorMentions: prepared.anchorMentions,
|
|
634
653
|
facts: prepared.facts,
|
|
635
654
|
readThroughSequence: prepared.readThroughSequence, remainingUnreadCount: prepared.remainingUnreadCount,
|
|
@@ -642,11 +661,11 @@ export class AgentTeamLedger {
|
|
|
642
661
|
}
|
|
643
662
|
threadObservations(actor, request) {
|
|
644
663
|
this.assertHumanActor(actor);
|
|
645
|
-
const task = this.
|
|
664
|
+
const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
|
|
646
665
|
const limit = request.limit ?? 50;
|
|
647
666
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
648
667
|
throw new Error('observation limit must be an integer between 1 and 100');
|
|
649
|
-
const threadRef =
|
|
668
|
+
const threadRef = thread.threadRef;
|
|
650
669
|
const current = new Map();
|
|
651
670
|
const observations = [];
|
|
652
671
|
for (const operation of this.state.ordered) {
|
|
@@ -658,7 +677,7 @@ export class AgentTeamLedger {
|
|
|
658
677
|
continue;
|
|
659
678
|
if (!current.delete(removed.memberId))
|
|
660
679
|
continue;
|
|
661
|
-
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 }),
|
|
662
681
|
memberId: removed.memberId, action: 'unfollow' }));
|
|
663
682
|
}
|
|
664
683
|
for (const next of delta.attention.set) {
|
|
@@ -669,28 +688,15 @@ export class AgentTeamLedger {
|
|
|
669
688
|
// Task creation establishes initial Attention; reads only advance its watermark.
|
|
670
689
|
if (operation.kind === 'team/message-sent' || (prior !== undefined && prior.startSequence === next.startSequence))
|
|
671
690
|
continue;
|
|
672
|
-
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 }),
|
|
673
692
|
memberId: next.memberId, action: 'follow' }));
|
|
674
693
|
}
|
|
675
694
|
}
|
|
676
695
|
return Object.freeze({ items: Object.freeze(observations.slice(-limit)) });
|
|
677
696
|
}
|
|
697
|
+
/** Every operation carrying an inbox delta drives the Inbox projection; the payload shape decides, not a per-kind list. */
|
|
678
698
|
attentionDelta(operation) {
|
|
679
|
-
|
|
680
|
-
case 'team/channel-member-removed':
|
|
681
|
-
case 'team/message-sent':
|
|
682
|
-
case 'team/thread-replied':
|
|
683
|
-
case 'team/claim-created':
|
|
684
|
-
case 'team/claim-done':
|
|
685
|
-
case 'team/claim-released':
|
|
686
|
-
case 'team/task-changed':
|
|
687
|
-
case 'team/thread-attention-changed':
|
|
688
|
-
case 'team/thread-read':
|
|
689
|
-
case 'team/member-removed':
|
|
690
|
-
return operation.data.inbox;
|
|
691
|
-
default:
|
|
692
|
-
return undefined;
|
|
693
|
-
}
|
|
699
|
+
return 'inbox' in operation.data ? operation.data.inbox : undefined;
|
|
694
700
|
}
|
|
695
701
|
/** Attachment ids referenced by any stored Message — the GC's keep-set oracle. */
|
|
696
702
|
referencedAttachmentIds() {
|
|
@@ -705,10 +711,7 @@ export class AgentTeamLedger {
|
|
|
705
711
|
}
|
|
706
712
|
threadHistory(actor, request) {
|
|
707
713
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
708
|
-
const task = this.
|
|
709
|
-
if (authorized.kind === 'member')
|
|
710
|
-
this.requireMemberChannel(this.requireMember(authorized.memberId), task.channelRef);
|
|
711
|
-
const thread = this.requireThread(task.threadRef);
|
|
714
|
+
const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
|
|
712
715
|
const limit = request.limit ?? 20;
|
|
713
716
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
714
717
|
throw new Error('history limit must be an integer between 1 and 100');
|
|
@@ -717,17 +720,15 @@ export class AgentTeamLedger {
|
|
|
717
720
|
throw new Error('beforeSequence must be a positive integer');
|
|
718
721
|
const all = this.threadFacts(thread.threadRef).filter(fact => fact.sequence < before);
|
|
719
722
|
const facts = all.slice(-limit);
|
|
720
|
-
const anchor = this.threadAnchor(
|
|
721
|
-
return Object.freeze({ task, thread, anchor, anchorMentions: this.state.mentionsByMessage.get(anchor.messageRef) ?? [],
|
|
722
|
-
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),
|
|
723
726
|
cursor: facts.length === 0 ? before : facts[0].sequence, hasMore: all.length > facts.length });
|
|
724
727
|
}
|
|
725
728
|
listClaims(actor, request) {
|
|
726
729
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
727
|
-
const task = this.
|
|
728
|
-
|
|
729
|
-
this.requireMemberChannel(this.requireMember(authorized.memberId), task.channelRef);
|
|
730
|
-
return Object.freeze({ task, thread: this.requireThread(task.threadRef), claims: this.claimsForTask(task.taskRef) });
|
|
730
|
+
const { task, thread } = this.threadForActor(authorized, request.workspaceId, request.taskRef);
|
|
731
|
+
return Object.freeze({ task, thread, claims: this.claimsForTask(task.taskRef) });
|
|
731
732
|
}
|
|
732
733
|
getTask(taskRef) {
|
|
733
734
|
return this.state.tasks.get(taskRef);
|
|
@@ -769,22 +770,27 @@ export class AgentTeamLedger {
|
|
|
769
770
|
}
|
|
770
771
|
if (request.threadRef !== undefined) {
|
|
771
772
|
const thread = this.requireThread(request.threadRef);
|
|
772
|
-
const
|
|
773
|
-
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)
|
|
774
779
|
throw new Error(`Thread '${request.threadRef}' does not belong to Channel '${request.channelRef}'`);
|
|
775
|
-
if (memberId !== undefined && !this.isChannelMember(
|
|
776
|
-
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}'`);
|
|
777
782
|
}
|
|
778
783
|
const channels = [...this.state.channels.values()].filter(channel => channel.workspaceId === request.workspaceId
|
|
779
784
|
&& (memberId === undefined || this.isChannelMember(channel.channelRef, memberId)));
|
|
780
785
|
const channelRefs = new Set(channels.map(channel => channel.channelRef));
|
|
781
786
|
const allFacts = this.state.orderedFacts.filter(fact => {
|
|
782
|
-
const
|
|
783
|
-
|
|
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))
|
|
784
790
|
return false;
|
|
785
|
-
if (request.channelRef !== undefined &&
|
|
791
|
+
if (request.channelRef !== undefined && channelRef !== request.channelRef)
|
|
786
792
|
return false;
|
|
787
|
-
if (request.threadRef !== undefined &&
|
|
793
|
+
if (request.threadRef !== undefined && threadRef !== request.threadRef)
|
|
788
794
|
return false;
|
|
789
795
|
if (request.topLevelOnly && (fact.kind !== 'message' || !fact.message.topLevel))
|
|
790
796
|
return false;
|
|
@@ -796,12 +802,12 @@ export class AgentTeamLedger {
|
|
|
796
802
|
const visibleTasks = [...this.state.tasks.values()].filter(task => channelRefs.has(task.channelRef)
|
|
797
803
|
&& (request.channelRef === undefined || task.channelRef === request.channelRef)
|
|
798
804
|
&& (request.threadRef === undefined || task.threadRef === request.threadRef));
|
|
799
|
-
const taskNumbers = this.taskNumbers(request.workspaceId
|
|
805
|
+
const taskNumbers = this.taskNumbers(request.workspaceId);
|
|
800
806
|
const items = selected.filter((fact) => fact.kind === 'message').map(fact => {
|
|
801
807
|
const message = fact.message;
|
|
802
|
-
const task = this.requireTask(request.workspaceId, message.taskRef);
|
|
803
808
|
const thread = this.requireThread(message.threadRef);
|
|
804
|
-
|
|
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,
|
|
805
811
|
messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0 });
|
|
806
812
|
});
|
|
807
813
|
const initialization = this.initialization();
|
|
@@ -811,7 +817,12 @@ export class AgentTeamLedger {
|
|
|
811
817
|
channels: Object.freeze(channels),
|
|
812
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 })))),
|
|
813
819
|
tasks: Object.freeze(visibleTasks),
|
|
814
|
-
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
|
+
})),
|
|
815
826
|
taskNumbers: Object.freeze(visibleTasks.map(task => Object.freeze({ taskRef: task.taskRef, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }))),
|
|
816
827
|
items: Object.freeze(items),
|
|
817
828
|
claims: this.claimsForVisibleTasks(visibleTasks),
|
|
@@ -872,6 +883,8 @@ export class AgentTeamLedger {
|
|
|
872
883
|
case 'team/message-sent':
|
|
873
884
|
case 'team/thread-replied':
|
|
874
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 }];
|
|
875
888
|
case 'team/claim-created':
|
|
876
889
|
case 'team/claim-done':
|
|
877
890
|
case 'team/claim-released':
|
|
@@ -883,6 +896,8 @@ export class AgentTeamLedger {
|
|
|
883
896
|
// A read advances only the reader's private watermark; no projection
|
|
884
897
|
// visible to other participants changes, so nobody is woken.
|
|
885
898
|
return [];
|
|
899
|
+
default:
|
|
900
|
+
return assertUnhandledKind(operation);
|
|
886
901
|
}
|
|
887
902
|
}
|
|
888
903
|
/** Members whose Inbox projection may have changed; the Host notifies only live ones. */
|
|
@@ -916,6 +931,8 @@ export class AgentTeamLedger {
|
|
|
916
931
|
case 'team/message-sent':
|
|
917
932
|
case 'team/thread-replied':
|
|
918
933
|
return [operation.data.message.threadRef];
|
|
934
|
+
case 'team/thread-promoted':
|
|
935
|
+
return [operation.data.activity.threadRef];
|
|
919
936
|
case 'team/claim-created':
|
|
920
937
|
case 'team/claim-done':
|
|
921
938
|
case 'team/claim-released':
|
|
@@ -925,8 +942,19 @@ export class AgentTeamLedger {
|
|
|
925
942
|
case 'team/channel-member-removed':
|
|
926
943
|
case 'team/member-removed':
|
|
927
944
|
return operation.data.activities.map(activity => activity.threadRef);
|
|
928
|
-
|
|
945
|
+
case 'team/initialized':
|
|
946
|
+
case 'team/channel-created':
|
|
947
|
+
case 'team/channel-updated':
|
|
948
|
+
case 'team/member-added':
|
|
949
|
+
case 'team/member-suspended':
|
|
950
|
+
case 'team/member-resumed':
|
|
951
|
+
case 'team/member-session-restarted':
|
|
952
|
+
case 'team/member-updated':
|
|
953
|
+
case 'team/channel-member-added':
|
|
954
|
+
case 'team/thread-read':
|
|
929
955
|
return [];
|
|
956
|
+
default:
|
|
957
|
+
return assertUnhandledKind(operation);
|
|
930
958
|
}
|
|
931
959
|
}
|
|
932
960
|
replay() {
|
|
@@ -1071,7 +1099,7 @@ export class AgentTeamLedger {
|
|
|
1071
1099
|
if (channel === undefined || member === undefined || operation.data.workspaceId !== channel.workspaceId
|
|
1072
1100
|
|| member.workspaceId !== channel.workspaceId || !projection.memberships.get(channel.channelRef)?.has(member.memberId))
|
|
1073
1101
|
throw new Error('invalid Channel membership removal');
|
|
1074
|
-
const threadRefs = new Set([...projection.
|
|
1102
|
+
const threadRefs = new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === channel.channelRef));
|
|
1075
1103
|
this.validateReleaseCleanup(operation.data, projection, member.memberId, threadRefs, operation.sequence, refs);
|
|
1076
1104
|
return;
|
|
1077
1105
|
}
|
|
@@ -1080,7 +1108,7 @@ export class AgentTeamLedger {
|
|
|
1080
1108
|
const prior = projection.members.get(operation.data.member.memberId);
|
|
1081
1109
|
if (prior === undefined || prior.state === 'inactive' || operation.data.member.state !== 'inactive' || !this.sameMemberIdentity(prior, operation.data.member))
|
|
1082
1110
|
throw new Error('invalid Member removal');
|
|
1083
|
-
const threadRefs = new Set(
|
|
1111
|
+
const threadRefs = new Set(projection.threads.keys());
|
|
1084
1112
|
this.validateReleaseCleanup(operation.data, projection, prior.memberId, threadRefs, operation.sequence, refs);
|
|
1085
1113
|
return;
|
|
1086
1114
|
}
|
|
@@ -1088,15 +1116,17 @@ export class AgentTeamLedger {
|
|
|
1088
1116
|
const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
|
|
1089
1117
|
if (operation.data.memberId !== operation.actor.memberId)
|
|
1090
1118
|
throw new Error('Attention operation has wrong actor');
|
|
1091
|
-
const task = projection.tasks.get(operation.data.task.taskRef);
|
|
1092
1119
|
const thread = projection.threads.get(operation.data.thread.threadRef);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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)))
|
|
1096
1126
|
throw new Error('invalid Attention operation');
|
|
1097
1127
|
const current = this.attentionForFrom(projection, operation.data.memberId, thread.threadRef);
|
|
1098
|
-
if ((operation.data.action === 'follow' && (task
|
|
1099
|
-
|| (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))))) {
|
|
1100
1130
|
throw new Error('invalid Attention transition');
|
|
1101
1131
|
}
|
|
1102
1132
|
const expected = operation.data.action === 'follow'
|
|
@@ -1114,9 +1144,12 @@ export class AgentTeamLedger {
|
|
|
1114
1144
|
assertHuman();
|
|
1115
1145
|
if (operation.data.memberId !== operation.actor.memberId)
|
|
1116
1146
|
throw new Error('Thread read has wrong actor');
|
|
1117
|
-
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
|
+
});
|
|
1118
1151
|
const expectedData = Object.freeze({ workspaceId: operation.data.workspaceId, memberId: operation.data.memberId,
|
|
1119
|
-
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,
|
|
1120
1153
|
anchorMentions: expected.anchorMentions, facts: expected.facts,
|
|
1121
1154
|
readThroughSequence: expected.readThroughSequence, remainingUnreadCount: expected.remainingUnreadCount,
|
|
1122
1155
|
...(expected.attention === undefined ? {} : { attention: expected.attention }), inbox: expected.inbox });
|
|
@@ -1125,27 +1158,56 @@ export class AgentTeamLedger {
|
|
|
1125
1158
|
this.validateInboxDelta(operation.data.inbox, projection, refs);
|
|
1126
1159
|
return;
|
|
1127
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
|
+
}
|
|
1128
1184
|
if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
|
|
1129
1185
|
const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
|
|
1130
1186
|
const { message, task, thread } = operation.data;
|
|
1131
1187
|
const channel = projection.channels.get(message.channelRef);
|
|
1132
1188
|
if (channel === undefined || channel.workspaceId !== operation.data.workspaceId || message.sender !== operation.actor.memberId
|
|
1133
|
-
|| message.sequence !== operation.sequence || thread.revision !== operation.sequence
|
|
1134
|
-
|| 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)
|
|
1135
1193
|
|| (actor !== undefined && !projection.memberships.get(channel.channelRef)?.has(actor.memberId)))
|
|
1136
1194
|
throw new Error('invalid Message operation');
|
|
1137
1195
|
if (operation.kind === 'team/message-sent') {
|
|
1138
|
-
if (!message.topLevel || projection.
|
|
1196
|
+
if (!message.topLevel || projection.threads.has(thread.threadRef) || (task !== undefined && projection.tasks.has(task.taskRef))) {
|
|
1139
1197
|
throw new Error('invalid top-level Message operation');
|
|
1140
|
-
|
|
1198
|
+
}
|
|
1141
1199
|
this.addRef(refs, thread.threadRef);
|
|
1200
|
+
if (task !== undefined)
|
|
1201
|
+
this.addRef(refs, task.taskRef);
|
|
1142
1202
|
}
|
|
1143
1203
|
else {
|
|
1144
|
-
const priorTask = projection.tasks.get(task.taskRef);
|
|
1145
1204
|
const priorThread = projection.threads.get(thread.threadRef);
|
|
1146
|
-
|
|
1147
|
-
|
|
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))) {
|
|
1148
1209
|
throw new Error('invalid Thread reply');
|
|
1210
|
+
}
|
|
1149
1211
|
}
|
|
1150
1212
|
this.addRef(refs, message.messageRef);
|
|
1151
1213
|
this.validateMentions(operation.data.mentions, message, projection);
|
|
@@ -1243,6 +1305,7 @@ export class AgentTeamLedger {
|
|
|
1243
1305
|
this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
|
|
1244
1306
|
return;
|
|
1245
1307
|
}
|
|
1308
|
+
assertUnhandledKind(operation);
|
|
1246
1309
|
}
|
|
1247
1310
|
validateInboxDelta(delta, projection, _refs, additionalThreadRefs = [], additionalMessages = [], additionalActivities = []) {
|
|
1248
1311
|
const attentionKeys = new Set();
|
|
@@ -1463,13 +1526,23 @@ export class AgentTeamLedger {
|
|
|
1463
1526
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
1464
1527
|
return;
|
|
1465
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
|
+
}
|
|
1466
1536
|
if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
|
|
1467
1537
|
const { message, mentions } = operation.data;
|
|
1468
1538
|
target.messages.push(message);
|
|
1469
1539
|
target.mentionsByMessage.set(message.messageRef, Object.freeze([...mentions]));
|
|
1470
1540
|
this.appendMessageFact(target, message, mentions);
|
|
1471
|
-
|
|
1541
|
+
if (operation.data.task !== undefined)
|
|
1542
|
+
target.tasks.set(operation.data.task.taskRef, operation.data.task);
|
|
1472
1543
|
target.threads.set(operation.data.thread.threadRef, operation.data.thread);
|
|
1544
|
+
if (message.topLevel)
|
|
1545
|
+
target.channelRefByThread.set(message.threadRef, message.channelRef);
|
|
1473
1546
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
1474
1547
|
return;
|
|
1475
1548
|
}
|
|
@@ -1492,7 +1565,9 @@ export class AgentTeamLedger {
|
|
|
1492
1565
|
}
|
|
1493
1566
|
if (operation.kind === 'team/thread-attention-changed' || operation.kind === 'team/thread-read') {
|
|
1494
1567
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
1568
|
+
return;
|
|
1495
1569
|
}
|
|
1570
|
+
assertUnhandledKind(operation);
|
|
1496
1571
|
}
|
|
1497
1572
|
/** Facts arrive in ledger sequence order, so global and per-thread lists stay sorted by append only. */
|
|
1498
1573
|
appendMessageFact(target, message, mentions) {
|
|
@@ -1501,8 +1576,6 @@ export class AgentTeamLedger {
|
|
|
1501
1576
|
const facts = target.factsByThread.get(message.threadRef) ?? [];
|
|
1502
1577
|
facts.push(fact);
|
|
1503
1578
|
target.factsByThread.set(message.threadRef, facts);
|
|
1504
|
-
if (message.topLevel)
|
|
1505
|
-
target.topLevelMessages.push(message);
|
|
1506
1579
|
target.messageCountByThread.set(message.threadRef, (target.messageCountByThread.get(message.threadRef) ?? 0) + 1);
|
|
1507
1580
|
}
|
|
1508
1581
|
appendActivityFact(target, activity) {
|
|
@@ -1537,26 +1610,19 @@ export class AgentTeamLedger {
|
|
|
1537
1610
|
for (const marker of delta.activityMarkers.added)
|
|
1538
1611
|
target.activityMarkers.set(this.activityMarkerKey(marker), marker);
|
|
1539
1612
|
}
|
|
1540
|
-
prepareRead(memberId, workspaceId,
|
|
1541
|
-
return this.prepareReadFrom(this.state, memberId, workspaceId,
|
|
1613
|
+
prepareRead(memberId, workspaceId, request) {
|
|
1614
|
+
return this.prepareReadFrom(this.state, memberId, workspaceId, request);
|
|
1542
1615
|
}
|
|
1543
1616
|
/** Derive the only legal durable result of one Thread read from a prior projection. */
|
|
1544
|
-
prepareReadFrom(projection, memberId, workspaceId,
|
|
1545
|
-
const task =
|
|
1546
|
-
if (task === undefined)
|
|
1547
|
-
throw new Error(`unknown Task ref '${taskRef}'${this.unknownRefHint(taskRef, 'task', 'Task')}`);
|
|
1548
|
-
if (projection.channels.get(task.channelRef)?.workspaceId !== workspaceId)
|
|
1549
|
-
throw new Error(`Task '${taskRef}' does not belong to Workspace '${workspaceId}'`);
|
|
1550
|
-
const thread = projection.threads.get(task.threadRef);
|
|
1551
|
-
if (thread === undefined)
|
|
1552
|
-
throw new Error(`unknown Thread ref '${task.threadRef}'`);
|
|
1617
|
+
prepareReadFrom(projection, memberId, workspaceId, request) {
|
|
1618
|
+
const { task, thread, channelRef } = this.threadContextFrom(projection, workspaceId, request);
|
|
1553
1619
|
if (memberId !== AGENT_TEAM_HUMAN_MEMBER_ID) {
|
|
1554
1620
|
const member = projection.members.get(memberId);
|
|
1555
|
-
if (member === undefined || !projection.memberships.get(
|
|
1556
|
-
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}'`);
|
|
1557
1623
|
}
|
|
1558
1624
|
}
|
|
1559
|
-
const anchor = this.threadAnchorFrom(projection,
|
|
1625
|
+
const anchor = this.threadAnchorFrom(projection, thread.threadRef);
|
|
1560
1626
|
const attention = this.attentionForFrom(projection, memberId, thread.threadRef);
|
|
1561
1627
|
const unread = this.unreadForFrom(projection, memberId, thread.threadRef);
|
|
1562
1628
|
const unreadFacts = unread.slice(0, 20);
|
|
@@ -1597,7 +1663,7 @@ export class AgentTeamLedger {
|
|
|
1597
1663
|
};
|
|
1598
1664
|
this.applyInboxDelta(nextProjection, inbox);
|
|
1599
1665
|
const remainingUnreadCount = this.unreadForFrom(nextProjection, memberId, thread.threadRef).length;
|
|
1600
|
-
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,
|
|
1601
1667
|
anchorMentions: projection.mentionsByMessage.get(anchor.messageRef) ?? [],
|
|
1602
1668
|
facts: Object.freeze(combined),
|
|
1603
1669
|
readThroughSequence, remainingUnreadCount, ...(attention === undefined ? {} : { attention: nextAttention[0] ?? attention }),
|
|
@@ -1687,6 +1753,17 @@ export class AgentTeamLedger {
|
|
|
1687
1753
|
activityRef: activity.activityRef, sequence: activity.sequence }));
|
|
1688
1754
|
return this.inboxDelta([], [], [], [], markers);
|
|
1689
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
|
+
}
|
|
1690
1767
|
removeMemberThreadInbox(memberId, threadRefs) {
|
|
1691
1768
|
const removed = [...this.state.attention.values()].filter(attention => attention.memberId === memberId && threadRefs.has(attention.threadRef))
|
|
1692
1769
|
.map(attention => Object.freeze({ memberId, threadRef: attention.threadRef }));
|
|
@@ -1779,11 +1856,11 @@ export class AgentTeamLedger {
|
|
|
1779
1856
|
...(task === undefined ? {} : { taskRef: task.taskRef }), ...(thread === undefined ? {} : { threadRef: thread.threadRef, revision: thread.revision }) });
|
|
1780
1857
|
}
|
|
1781
1858
|
unreadRequired(task, thread, unread) {
|
|
1782
|
-
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,
|
|
1783
1860
|
revision: thread.revision, unreadCount: unread.length, directCount: unread.filter(item => item.direct).length });
|
|
1784
1861
|
}
|
|
1785
1862
|
staleRevision(task, thread, expectedRevision) {
|
|
1786
|
-
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,
|
|
1787
1864
|
expectedRevision, revision: thread.revision });
|
|
1788
1865
|
}
|
|
1789
1866
|
releaseSummaries(claims, actor, sequence) {
|
|
@@ -1816,28 +1893,59 @@ export class AgentTeamLedger {
|
|
|
1816
1893
|
return Object.freeze({ ...task, status: this.deriveResolvedTaskStatus(task, projected.values()) });
|
|
1817
1894
|
}));
|
|
1818
1895
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1896
|
+
/** Resolve one Task's Thread for a workspace-authorized actor; Member actors must belong to the Task's Channel. */
|
|
1897
|
+
threadForActor(actor, workspaceId, taskRef) {
|
|
1898
|
+
const task = this.requireTask(workspaceId, taskRef);
|
|
1899
|
+
const thread = this.requireThread(task.threadRef);
|
|
1900
|
+
if (actor.kind === 'member')
|
|
1901
|
+
this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
|
|
1902
|
+
return { task, thread };
|
|
1903
|
+
}
|
|
1904
|
+
/** Business outcomes that defer a thread write: unread work first, then a stale revision. */
|
|
1905
|
+
deferredThreadWrite(actorMemberId, task, thread, baseRevision) {
|
|
1906
|
+
const unread = this.unreadFor(actorMemberId, thread.threadRef);
|
|
1907
|
+
if (unread.length > 0)
|
|
1908
|
+
return this.unreadRequired(task, thread, unread);
|
|
1909
|
+
if (baseRevision !== thread.revision)
|
|
1910
|
+
return this.staleRevision(task, thread, baseRevision);
|
|
1911
|
+
return undefined;
|
|
1912
|
+
}
|
|
1913
|
+
/** Commit projections shared by the Message-sent and Thread-replied results. */
|
|
1914
|
+
committedMessageResult(operation) {
|
|
1915
|
+
return { kind: 'committed', receipt: this.receipt(operation), message: operation.data.message,
|
|
1916
|
+
...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread, attention: operation.data.inbox.attention.set,
|
|
1917
|
+
directMarkers: operation.data.inbox.directMarkers.added };
|
|
1918
|
+
}
|
|
1919
|
+
threadAnchor(threadRef) {
|
|
1920
|
+
return this.threadAnchorFrom(this.state, threadRef);
|
|
1921
|
+
}
|
|
1922
|
+
threadAnchorFrom(projection, threadRef) {
|
|
1923
|
+
const anchor = projection.messages.find(message => message.threadRef === threadRef && message.topLevel);
|
|
1824
1924
|
if (anchor === undefined)
|
|
1825
|
-
throw new Error(`
|
|
1925
|
+
throw new Error(`Thread '${threadRef}' has no anchor Message`);
|
|
1826
1926
|
return anchor;
|
|
1827
1927
|
}
|
|
1828
1928
|
threadFactKey(fact) {
|
|
1829
1929
|
return fact.kind === 'message' ? `message:${fact.message.messageRef}` : `activity:${fact.activity.activityRef}`;
|
|
1830
1930
|
}
|
|
1831
|
-
|
|
1931
|
+
/**
|
|
1932
|
+
* Display numbers for Tasks: one counter per home Channel, in creation
|
|
1933
|
+
* order. This is the single numbering authority — Channel cards, Thread
|
|
1934
|
+
* headings, cross-channel ref resolution, and inbox renders all show the
|
|
1935
|
+
* ordinal the Task holds inside its own Channel.
|
|
1936
|
+
*/
|
|
1937
|
+
taskNumbers(workspaceId) {
|
|
1832
1938
|
const numbers = new Map();
|
|
1833
|
-
|
|
1834
|
-
for (const
|
|
1835
|
-
|
|
1939
|
+
const next = new Map();
|
|
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)
|
|
1836
1943
|
continue;
|
|
1837
|
-
if (
|
|
1944
|
+
if (this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
|
|
1838
1945
|
continue;
|
|
1839
|
-
|
|
1840
|
-
next
|
|
1946
|
+
const ordinal = (next.get(task.channelRef) ?? 0) + 1;
|
|
1947
|
+
next.set(task.channelRef, ordinal);
|
|
1948
|
+
numbers.set(task.taskRef, ordinal);
|
|
1841
1949
|
}
|
|
1842
1950
|
return numbers;
|
|
1843
1951
|
}
|
|
@@ -1921,6 +2029,53 @@ export class AgentTeamLedger {
|
|
|
1921
2029
|
throw new Error(`unknown Thread ref '${threadRef}'${this.unknownRefHint(threadRef, 'thread', 'Thread')}`);
|
|
1922
2030
|
return thread;
|
|
1923
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
|
+
}
|
|
1924
2079
|
requireChannel(workspaceId, channelRef) {
|
|
1925
2080
|
const channel = this.state.channels.get(channelRef);
|
|
1926
2081
|
if (channel === undefined)
|
|
@@ -2048,16 +2203,28 @@ export class AgentTeamLedger {
|
|
|
2048
2203
|
if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
|
|
2049
2204
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
|
|
2050
2205
|
|| operation.data.message.body !== request.body.trim() || !this.sameList(operation.data.mentions, recipients)
|
|
2051
|
-
|| !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? [])
|
|
2206
|
+
|| !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? [])
|
|
2207
|
+
|| (request.asTask !== false) !== (operation.data.task !== undefined))
|
|
2052
2208
|
this.throwRequestCollision(request.requestId);
|
|
2053
2209
|
}
|
|
2054
2210
|
assertSameReply(operation, request, recipients) {
|
|
2055
2211
|
if (operation.kind !== 'team/thread-replied' || !this.sameActor(operation.actor, request.actor)
|
|
2056
|
-
|| 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)
|
|
2057
2216
|
|| operation.data.message.body !== request.body.trim() || operation.data.baseRevision !== request.baseRevision
|
|
2058
2217
|
|| !this.sameList(operation.data.mentions, recipients))
|
|
2059
2218
|
this.throwRequestCollision(request.requestId);
|
|
2060
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
|
+
}
|
|
2061
2228
|
assertSameClaim(operation, request) {
|
|
2062
2229
|
const kind = request.action === 'claim' ? 'team/claim-created' : request.action === 'done' ? 'team/claim-done' : 'team/claim-released';
|
|
2063
2230
|
if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.workspaceId !== request.workspaceId
|
|
@@ -2078,12 +2245,19 @@ export class AgentTeamLedger {
|
|
|
2078
2245
|
}
|
|
2079
2246
|
assertSameAttention(operation, request) {
|
|
2080
2247
|
if (operation.kind !== 'team/thread-attention-changed' || !this.sameActor(operation.actor, request.actor)
|
|
2081
|
-
|| 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)
|
|
2082
2253
|
this.throwRequestCollision(request.requestId);
|
|
2083
2254
|
}
|
|
2084
2255
|
assertSameThreadRead(operation, request) {
|
|
2085
2256
|
if (operation.kind !== 'team/thread-read' || !this.sameActor(operation.actor, request.actor)
|
|
2086
|
-
|| 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))
|
|
2087
2261
|
this.throwRequestCollision(request.requestId);
|
|
2088
2262
|
}
|
|
2089
2263
|
sameTask(left, right) {
|
|
@@ -2153,14 +2327,10 @@ export class AgentTeamLedger {
|
|
|
2153
2327
|
releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
|
|
2154
2328
|
}
|
|
2155
2329
|
messageResult(operation) {
|
|
2156
|
-
return Object.freeze(
|
|
2157
|
-
task: operation.data.task, thread: operation.data.thread, attention: operation.data.inbox.attention.set,
|
|
2158
|
-
directMarkers: operation.data.inbox.directMarkers.added });
|
|
2330
|
+
return Object.freeze(this.committedMessageResult(operation));
|
|
2159
2331
|
}
|
|
2160
2332
|
replyResult(operation) {
|
|
2161
|
-
return Object.freeze(
|
|
2162
|
-
task: operation.data.task, thread: operation.data.thread, attention: operation.data.inbox.attention.set,
|
|
2163
|
-
directMarkers: operation.data.inbox.directMarkers.added });
|
|
2333
|
+
return Object.freeze(this.committedMessageResult(operation));
|
|
2164
2334
|
}
|
|
2165
2335
|
claimResult(operation) {
|
|
2166
2336
|
const attention = operation.data.inbox.attention.set.find(candidate => candidate.memberId === operation.data.claim.owner);
|
|
@@ -2173,13 +2343,17 @@ export class AgentTeamLedger {
|
|
|
2173
2343
|
task: operation.data.task, thread: operation.data.thread, claims: operation.data.claims });
|
|
2174
2344
|
}
|
|
2175
2345
|
attentionResult(operation) {
|
|
2176
|
-
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,
|
|
2177
2347
|
...(operation.data.inbox.attention.set[0] === undefined ? {} : { attention: operation.data.inbox.attention.set[0] }) });
|
|
2178
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
|
+
}
|
|
2179
2353
|
threadReadResult(operation) {
|
|
2180
2354
|
// Loaded records are normalized by sortedRecords(); fresh writes always carry instants.
|
|
2181
2355
|
const { anchor, facts } = operation.data;
|
|
2182
|
-
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,
|
|
2183
2357
|
claims: operation.data.claims, anchor, anchorMentions: operation.data.anchorMentions, facts,
|
|
2184
2358
|
readThroughSequence: operation.data.readThroughSequence, remainingUnreadCount: operation.data.remainingUnreadCount,
|
|
2185
2359
|
...(operation.data.attention === undefined ? {} : { attention: operation.data.attention }),
|