@wowyuarm/dsh-agent-team 0.1.3 → 0.1.5
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 +12 -3
- 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/auto-compaction.js +39 -1
- package/packages/agent-team/lib/index.js +123 -14
- package/packages/agent-team/lib/ledger.js +355 -110
- package/packages/agent-team/lib/spec.js +39 -11
- package/packages/agent-team/lib/typert.host.js +257 -86
- package/packages/agent-team/lib/typert.remote-client.d.ts +5 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +196 -59
- package/packages/agent-team/lib/types/auto-compaction.d.ts +9 -0
- package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +24 -7
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +36 -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 +109 -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 +1082 -380
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +2 -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 +73 -15
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +34 -10
- 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 -1
- 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 +76 -20
- 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/TeamMemberSessionComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +16 -1
- 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 +62 -25
- package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts +3 -2
- package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts +5 -2
- package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamSidebarSection.js +2 -4
- 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 +92 -18
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +5 -2
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +16 -13
- package/packages/client-agent-team/lib/types/client/locales.d.ts +40 -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 +40 -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/sidebar-sections.d.ts +16 -0
- package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/sidebar-sections.js +102 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +3 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/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 +22 -2
- 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. */
|
|
@@ -195,6 +195,35 @@ export class AgentTeamLedger {
|
|
|
195
195
|
resumeMember(request) {
|
|
196
196
|
return this.setMemberState(request, 'enabled');
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Move one enabled Member onto a fresh Session: the durable operation
|
|
200
|
+
* records the sessionId transition while identity, memory, and binding
|
|
201
|
+
* survive. The Host disposes the old handle and archives the previous
|
|
202
|
+
* Session log around this write, so the next turn starts empty.
|
|
203
|
+
*/
|
|
204
|
+
renewMemberSession(request) {
|
|
205
|
+
return this.enqueue(async () => {
|
|
206
|
+
const existing = this.state.byRequest.get(request.requestId);
|
|
207
|
+
if (existing !== undefined) {
|
|
208
|
+
this.assertSameMemberSessionRenewed(existing, request);
|
|
209
|
+
return this.resolved(this.memberResult(existing));
|
|
210
|
+
}
|
|
211
|
+
this.assertHumanActor(request.actor);
|
|
212
|
+
const prior = this.requireMember(request.memberId);
|
|
213
|
+
if (prior.state !== 'enabled')
|
|
214
|
+
throw new Error(`Agent Member '${prior.handle}' is ${prior.state}; only enabled Members can start from a new context`);
|
|
215
|
+
if (prior.sessionId === request.sessionId)
|
|
216
|
+
throw new Error(`Agent Member '${prior.handle}' already runs Session '${request.sessionId}'`);
|
|
217
|
+
const member = Object.freeze({ ...prior, sessionId: request.sessionId });
|
|
218
|
+
const operation = Object.freeze({
|
|
219
|
+
...this.operationBase(request, this.nextSequence()), kind: 'team/member-session-renewed',
|
|
220
|
+
data: Object.freeze({ member, previousSessionId: prior.sessionId }),
|
|
221
|
+
});
|
|
222
|
+
await this.table.put(operation.operationId, operation);
|
|
223
|
+
this.apply(operation);
|
|
224
|
+
return this.committed(this.memberResult(operation));
|
|
225
|
+
});
|
|
226
|
+
}
|
|
198
227
|
getMember(memberId) {
|
|
199
228
|
return this.state.members.get(memberId);
|
|
200
229
|
}
|
|
@@ -282,12 +311,14 @@ export class AgentTeamLedger {
|
|
|
282
311
|
// Human-gated (reply path).
|
|
283
312
|
const sequence = this.nextSequence();
|
|
284
313
|
const base = this.operationBase(request, sequence);
|
|
285
|
-
const
|
|
314
|
+
const asTask = request.asTask !== false;
|
|
286
315
|
const threadRef = this.ref('thread');
|
|
287
|
-
const
|
|
288
|
-
const
|
|
316
|
+
const taskRef = asTask ? this.ref('task') : undefined;
|
|
317
|
+
const task = taskRef === undefined ? undefined
|
|
318
|
+
: Object.freeze({ taskRef, channelRef: channel.channelRef, threadRef, status: 'todo', resolution: 'open' });
|
|
319
|
+
const thread = Object.freeze({ threadRef, ...(taskRef === undefined ? {} : { taskRef }), revision: sequence });
|
|
289
320
|
const message = Object.freeze({
|
|
290
|
-
messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, taskRef,
|
|
321
|
+
messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, ...(taskRef === undefined ? {} : { taskRef }),
|
|
291
322
|
sender: request.actor.memberId, body,
|
|
292
323
|
...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
|
|
293
324
|
topLevel: true, sequence, occurredAt: base.occurredAt,
|
|
@@ -298,7 +329,8 @@ export class AgentTeamLedger {
|
|
|
298
329
|
const inbox = this.messageInboxDelta(message, request.actor.memberId, recipients, started);
|
|
299
330
|
const operation = Object.freeze({
|
|
300
331
|
...base, kind: 'team/message-sent',
|
|
301
|
-
data: Object.freeze({ workspaceId: request.workspaceId, mentions: recipients, message,
|
|
332
|
+
data: Object.freeze({ workspaceId: request.workspaceId, mentions: recipients, message,
|
|
333
|
+
...(task === undefined ? {} : { task }), thread, inbox }),
|
|
302
334
|
});
|
|
303
335
|
await this.table.put(operation.operationId, operation);
|
|
304
336
|
this.apply(operation);
|
|
@@ -314,31 +346,31 @@ export class AgentTeamLedger {
|
|
|
314
346
|
return this.resolved(this.replyResult(existing));
|
|
315
347
|
}
|
|
316
348
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
317
|
-
const { task, thread } = this.
|
|
349
|
+
const { task, thread, channelRef } = this.threadContextForActor(actor, request.workspaceId, request);
|
|
318
350
|
const body = request.body.trim();
|
|
319
351
|
if (body === '')
|
|
320
352
|
throw new Error('message body must not be empty');
|
|
321
|
-
this.assertMentionTargets(this.requireChannel(request.workspaceId,
|
|
353
|
+
this.assertMentionTargets(this.requireChannel(request.workspaceId, channelRef), recipients);
|
|
322
354
|
const deferred = this.deferredThreadWrite(actor.memberId, task, thread, request.baseRevision);
|
|
323
355
|
if (deferred !== undefined)
|
|
324
356
|
return this.resolved(deferred);
|
|
325
|
-
if (task
|
|
357
|
+
if (task?.resolution === 'closed')
|
|
326
358
|
throw new Error(`Task '${task.taskRef}' is closed; reopen it before replying`);
|
|
327
359
|
const unfollowedAgents = recipients.filter(memberId => this.state.members.has(memberId) && !this.isFollowing(thread.threadRef, memberId));
|
|
328
360
|
if (unfollowedAgents.length > 0 && actor.kind === 'member') {
|
|
329
|
-
return this.resolved(this.memberNotFollowing(request.workspaceId,
|
|
361
|
+
return this.resolved(this.memberNotFollowing(request.workspaceId, channelRef, unfollowedAgents, task, thread));
|
|
330
362
|
}
|
|
331
363
|
if (unfollowedAgents.length > 0 && request.confirmationToken === undefined) {
|
|
332
|
-
return this.resolved(this.issueConfirmation(request.actor, request.workspaceId,
|
|
364
|
+
return this.resolved(this.issueConfirmation(request.actor, request.workspaceId, channelRef, body, recipients, task, thread));
|
|
333
365
|
}
|
|
334
366
|
if (request.confirmationToken !== undefined) {
|
|
335
|
-
this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId,
|
|
367
|
+
this.consumeConfirmation(request.confirmationToken, request.actor, request.workspaceId, channelRef, task, thread, body, recipients);
|
|
336
368
|
}
|
|
337
369
|
const sequence = this.nextSequence();
|
|
338
370
|
const base = this.operationBase(request, sequence);
|
|
339
371
|
const message = Object.freeze({
|
|
340
|
-
messageRef: this.ref('message'), channelRef
|
|
341
|
-
taskRef: task.taskRef, sender: request.actor.memberId, body,
|
|
372
|
+
messageRef: this.ref('message'), channelRef, threadRef: thread.threadRef,
|
|
373
|
+
...(task === undefined ? {} : { taskRef: task.taskRef }), sender: request.actor.memberId, body,
|
|
342
374
|
...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
|
|
343
375
|
topLevel: false, sequence, occurredAt: base.occurredAt,
|
|
344
376
|
});
|
|
@@ -348,13 +380,45 @@ export class AgentTeamLedger {
|
|
|
348
380
|
const operation = Object.freeze({
|
|
349
381
|
...base, kind: 'team/thread-replied',
|
|
350
382
|
data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
|
|
351
|
-
mentions: recipients, message, task, thread: nextThread, inbox }),
|
|
383
|
+
mentions: recipients, message, ...(task === undefined ? {} : { task }), thread: nextThread, inbox }),
|
|
352
384
|
});
|
|
353
385
|
await this.table.put(operation.operationId, operation);
|
|
354
386
|
this.apply(operation);
|
|
355
387
|
return this.committed(this.replyResult(operation));
|
|
356
388
|
});
|
|
357
389
|
}
|
|
390
|
+
promoteThread(request) {
|
|
391
|
+
return this.enqueue(async () => {
|
|
392
|
+
const existing = this.state.byRequest.get(request.requestId);
|
|
393
|
+
if (existing !== undefined) {
|
|
394
|
+
this.assertSamePromotion(existing, request);
|
|
395
|
+
return this.resolved(this.promotionResult(existing));
|
|
396
|
+
}
|
|
397
|
+
this.assertHumanActor(request.actor);
|
|
398
|
+
const { task: existingTask, thread, channelRef } = this.threadContextForActor(request.actor, request.workspaceId, request);
|
|
399
|
+
if (existingTask !== undefined)
|
|
400
|
+
throw new Error(`Thread '${thread.threadRef}' already has Task '${existingTask.taskRef}'`);
|
|
401
|
+
const deferred = this.deferredThreadWrite(request.actor.memberId, undefined, thread, request.baseRevision);
|
|
402
|
+
if (deferred !== undefined)
|
|
403
|
+
return this.resolved(deferred);
|
|
404
|
+
const sequence = this.nextSequence();
|
|
405
|
+
const base = this.operationBase(request, sequence);
|
|
406
|
+
const taskRef = this.ref('task');
|
|
407
|
+
const task = Object.freeze({ taskRef, channelRef, threadRef: thread.threadRef, status: 'todo', resolution: 'open' });
|
|
408
|
+
const nextThread = Object.freeze({ threadRef: thread.threadRef, taskRef, revision: sequence });
|
|
409
|
+
const activity = Object.freeze({ activityRef: this.ref('activity'), kind: 'promote',
|
|
410
|
+
taskRef, threadRef: thread.threadRef, actor: request.actor.memberId, sequence });
|
|
411
|
+
const inbox = this.promoteThreadInbox(activity, thread.threadRef);
|
|
412
|
+
const operation = Object.freeze({
|
|
413
|
+
...base, kind: 'team/thread-promoted',
|
|
414
|
+
data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
|
|
415
|
+
activity, task, thread: nextThread, inbox }),
|
|
416
|
+
});
|
|
417
|
+
await this.table.put(operation.operationId, operation);
|
|
418
|
+
this.apply(operation);
|
|
419
|
+
return this.committed(this.promotionResult(operation));
|
|
420
|
+
});
|
|
421
|
+
}
|
|
358
422
|
changeClaim(request) {
|
|
359
423
|
return this.enqueue(async () => {
|
|
360
424
|
const existing = this.state.byRequest.get(request.requestId);
|
|
@@ -446,8 +510,10 @@ export class AgentTeamLedger {
|
|
|
446
510
|
// Acceptance normally waits for every Claim to finish (in_review);
|
|
447
511
|
// a Human may also accept early while work is in progress, which then
|
|
448
512
|
// completes the still-active Claims inside the same atomic operation.
|
|
449
|
-
|
|
450
|
-
|
|
513
|
+
// A never-claimed todo Task may be accepted directly as well: work
|
|
514
|
+
// finished outside the ledger is declared complete by the Human.
|
|
515
|
+
if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress' && task.status !== 'todo') {
|
|
516
|
+
throw new Error(`Task '${task.taskRef}' must be in_review, in_progress, or todo before acceptance`);
|
|
451
517
|
}
|
|
452
518
|
const priorActiveClaims = [...this.state.claims.values()].filter(claim => claim.taskRef === task.taskRef && claim.state === 'active');
|
|
453
519
|
if (request.action === 'accept' && task.status === 'in_progress' && priorActiveClaims.length === 0) {
|
|
@@ -526,10 +592,10 @@ export class AgentTeamLedger {
|
|
|
526
592
|
return this.resolved(this.attentionResult(existing));
|
|
527
593
|
}
|
|
528
594
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
529
|
-
const { task, thread } = this.
|
|
595
|
+
const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
|
|
530
596
|
const current = this.attentionFor(actor.memberId, thread.threadRef);
|
|
531
597
|
if (request.action === 'follow') {
|
|
532
|
-
if (task
|
|
598
|
+
if (task?.resolution === 'closed')
|
|
533
599
|
throw new Error(`Task '${task.taskRef}' is closed; reopen it before following`);
|
|
534
600
|
if (current !== undefined)
|
|
535
601
|
throw new Error(`Member is already following Thread '${thread.threadRef}'`);
|
|
@@ -537,7 +603,7 @@ export class AgentTeamLedger {
|
|
|
537
603
|
const operation = Object.freeze({
|
|
538
604
|
...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
|
|
539
605
|
data: Object.freeze({ workspaceId: request.workspaceId, action: 'follow', memberId: actor.memberId,
|
|
540
|
-
task, thread, inbox: this.inboxDelta([attention]) }),
|
|
606
|
+
...(task === undefined ? {} : { task }), thread, inbox: this.inboxDelta([attention]) }),
|
|
541
607
|
});
|
|
542
608
|
await this.table.put(operation.operationId, operation);
|
|
543
609
|
this.apply(operation);
|
|
@@ -545,12 +611,12 @@ export class AgentTeamLedger {
|
|
|
545
611
|
}
|
|
546
612
|
if (current === undefined)
|
|
547
613
|
throw new Error(`Member is already unfollowed from Thread '${thread.threadRef}'`);
|
|
548
|
-
if (this.hasActiveClaim(actor.memberId, task.taskRef))
|
|
614
|
+
if (task !== undefined && this.hasActiveClaim(actor.memberId, task.taskRef))
|
|
549
615
|
throw new Error('Member cannot unfollow while owning an active Claim');
|
|
550
616
|
const operation = Object.freeze({
|
|
551
617
|
...this.operationBase(request, this.nextSequence()), kind: 'team/thread-attention-changed',
|
|
552
618
|
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)) }),
|
|
619
|
+
...(task === undefined ? {} : { task }), thread, inbox: this.inboxDelta([], [{ memberId: actor.memberId, threadRef: thread.threadRef }], [], this.directMarkersFor(actor.memberId, thread.threadRef)) }),
|
|
554
620
|
});
|
|
555
621
|
await this.table.put(operation.operationId, operation);
|
|
556
622
|
this.apply(operation);
|
|
@@ -559,11 +625,9 @@ export class AgentTeamLedger {
|
|
|
559
625
|
}
|
|
560
626
|
attentionStatus(actor, request) {
|
|
561
627
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
562
|
-
const { task, thread } = this.
|
|
628
|
+
const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
|
|
563
629
|
const attention = this.attentionFor(authorized.memberId, thread.threadRef);
|
|
564
|
-
return attention === undefined
|
|
565
|
-
? Object.freeze({ task, thread })
|
|
566
|
-
: Object.freeze({ task, thread, attention });
|
|
630
|
+
return Object.freeze({ ...(task === undefined ? {} : { task }), thread, ...(attention === undefined ? {} : { attention }) });
|
|
567
631
|
}
|
|
568
632
|
inbox(actor, request) {
|
|
569
633
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
@@ -571,22 +635,22 @@ export class AgentTeamLedger {
|
|
|
571
635
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
572
636
|
throw new Error('inbox limit must be an integer between 1 and 100');
|
|
573
637
|
const items = [];
|
|
574
|
-
for (const
|
|
575
|
-
|
|
638
|
+
for (const thread of this.state.threads.values()) {
|
|
639
|
+
const channelRef = this.channelRefForThread(thread.threadRef);
|
|
640
|
+
if (channelRef === undefined)
|
|
576
641
|
continue;
|
|
577
|
-
if (
|
|
642
|
+
if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
|
|
643
|
+
continue;
|
|
644
|
+
if (authorized.kind === 'member' && !this.isChannelMember(channelRef, authorized.memberId))
|
|
578
645
|
continue;
|
|
579
|
-
const thread = this.requireThread(task.threadRef);
|
|
580
646
|
const unread = this.unreadFor(authorized.memberId, thread.threadRef);
|
|
581
647
|
if (unread.length === 0)
|
|
582
648
|
continue;
|
|
649
|
+
const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
|
|
583
650
|
const directCount = unread.filter(item => item.direct).length;
|
|
584
651
|
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 }));
|
|
652
|
+
items.push(Object.freeze({ channelRef, ...(task === undefined ? {} : { task }), thread, unreadCount: unread.length, directCount,
|
|
653
|
+
newestSequence: unread.at(-1).fact.sequence, ...(attention === undefined ? {} : { attention }) }));
|
|
590
654
|
}
|
|
591
655
|
items.sort((left, right) => right.directCount - left.directCount || right.newestSequence - left.newestSequence || left.thread.threadRef.localeCompare(right.thread.threadRef));
|
|
592
656
|
const selected = items.slice(0, limit);
|
|
@@ -611,10 +675,11 @@ export class AgentTeamLedger {
|
|
|
611
675
|
return this.resolved(this.threadReadResult(existing));
|
|
612
676
|
}
|
|
613
677
|
const actor = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
614
|
-
const prepared = this.prepareRead(actor.memberId, request.workspaceId, request
|
|
678
|
+
const prepared = this.prepareRead(actor.memberId, request.workspaceId, request);
|
|
615
679
|
const operation = Object.freeze({
|
|
616
680
|
...this.operationBase(request, this.nextSequence()), kind: 'team/thread-read',
|
|
617
|
-
data: Object.freeze({ workspaceId: request.workspaceId, memberId: actor.memberId,
|
|
681
|
+
data: Object.freeze({ workspaceId: request.workspaceId, memberId: actor.memberId,
|
|
682
|
+
...(prepared.task === undefined ? {} : { task: prepared.task }),
|
|
618
683
|
thread: prepared.thread, claims: prepared.claims, anchor: prepared.anchor, anchorMentions: prepared.anchorMentions,
|
|
619
684
|
facts: prepared.facts,
|
|
620
685
|
readThroughSequence: prepared.readThroughSequence, remainingUnreadCount: prepared.remainingUnreadCount,
|
|
@@ -627,11 +692,11 @@ export class AgentTeamLedger {
|
|
|
627
692
|
}
|
|
628
693
|
threadObservations(actor, request) {
|
|
629
694
|
this.assertHumanActor(actor);
|
|
630
|
-
const task = this.
|
|
695
|
+
const { task, thread } = this.threadContextForActor(actor, request.workspaceId, request);
|
|
631
696
|
const limit = request.limit ?? 50;
|
|
632
697
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
633
698
|
throw new Error('observation limit must be an integer between 1 and 100');
|
|
634
|
-
const threadRef =
|
|
699
|
+
const threadRef = thread.threadRef;
|
|
635
700
|
const current = new Map();
|
|
636
701
|
const observations = [];
|
|
637
702
|
for (const operation of this.state.ordered) {
|
|
@@ -643,7 +708,7 @@ export class AgentTeamLedger {
|
|
|
643
708
|
continue;
|
|
644
709
|
if (!current.delete(removed.memberId))
|
|
645
710
|
continue;
|
|
646
|
-
observations.push(Object.freeze({ sequence: operation.sequence, threadRef, taskRef: task.taskRef,
|
|
711
|
+
observations.push(Object.freeze({ sequence: operation.sequence, threadRef, ...(task === undefined ? {} : { taskRef: task.taskRef }),
|
|
647
712
|
memberId: removed.memberId, action: 'unfollow' }));
|
|
648
713
|
}
|
|
649
714
|
for (const next of delta.attention.set) {
|
|
@@ -654,7 +719,7 @@ export class AgentTeamLedger {
|
|
|
654
719
|
// Task creation establishes initial Attention; reads only advance its watermark.
|
|
655
720
|
if (operation.kind === 'team/message-sent' || (prior !== undefined && prior.startSequence === next.startSequence))
|
|
656
721
|
continue;
|
|
657
|
-
observations.push(Object.freeze({ sequence: operation.sequence, threadRef, taskRef: task.taskRef,
|
|
722
|
+
observations.push(Object.freeze({ sequence: operation.sequence, threadRef, ...(task === undefined ? {} : { taskRef: task.taskRef }),
|
|
658
723
|
memberId: next.memberId, action: 'follow' }));
|
|
659
724
|
}
|
|
660
725
|
}
|
|
@@ -677,7 +742,7 @@ export class AgentTeamLedger {
|
|
|
677
742
|
}
|
|
678
743
|
threadHistory(actor, request) {
|
|
679
744
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
680
|
-
const { task, thread } = this.
|
|
745
|
+
const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
|
|
681
746
|
const limit = request.limit ?? 20;
|
|
682
747
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
683
748
|
throw new Error('history limit must be an integer between 1 and 100');
|
|
@@ -686,9 +751,9 @@ export class AgentTeamLedger {
|
|
|
686
751
|
throw new Error('beforeSequence must be a positive integer');
|
|
687
752
|
const all = this.threadFacts(thread.threadRef).filter(fact => fact.sequence < before);
|
|
688
753
|
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),
|
|
754
|
+
const anchor = this.threadAnchor(thread.threadRef);
|
|
755
|
+
return Object.freeze({ ...(task === undefined ? {} : { task }), thread, anchor, anchorMentions: this.state.mentionsByMessage.get(anchor.messageRef) ?? [],
|
|
756
|
+
claims: task === undefined ? Object.freeze([]) : this.claimsForTask(task.taskRef), facts: Object.freeze(facts),
|
|
692
757
|
cursor: facts.length === 0 ? before : facts[0].sequence, hasMore: all.length > facts.length });
|
|
693
758
|
}
|
|
694
759
|
listClaims(actor, request) {
|
|
@@ -736,22 +801,27 @@ export class AgentTeamLedger {
|
|
|
736
801
|
}
|
|
737
802
|
if (request.threadRef !== undefined) {
|
|
738
803
|
const thread = this.requireThread(request.threadRef);
|
|
739
|
-
const
|
|
740
|
-
if (
|
|
804
|
+
const channelRef = this.channelRefForThread(thread.threadRef);
|
|
805
|
+
if (channelRef === undefined)
|
|
806
|
+
throw new Error(`unknown Thread ref '${request.threadRef}'`);
|
|
807
|
+
if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
|
|
808
|
+
throw new Error(`Thread '${request.threadRef}' does not belong to Workspace '${request.workspaceId}'`);
|
|
809
|
+
if (request.channelRef !== undefined && channelRef !== request.channelRef)
|
|
741
810
|
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 '${
|
|
811
|
+
if (memberId !== undefined && !this.isChannelMember(channelRef, memberId))
|
|
812
|
+
throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
|
|
744
813
|
}
|
|
745
814
|
const channels = [...this.state.channels.values()].filter(channel => channel.workspaceId === request.workspaceId
|
|
746
815
|
&& (memberId === undefined || this.isChannelMember(channel.channelRef, memberId)));
|
|
747
816
|
const channelRefs = new Set(channels.map(channel => channel.channelRef));
|
|
748
817
|
const allFacts = this.state.orderedFacts.filter(fact => {
|
|
749
|
-
const
|
|
750
|
-
|
|
818
|
+
const threadRef = fact.kind === 'message' ? fact.message.threadRef : fact.activity.threadRef;
|
|
819
|
+
const channelRef = this.channelRefForThread(threadRef);
|
|
820
|
+
if (channelRef === undefined || !channelRefs.has(channelRef))
|
|
751
821
|
return false;
|
|
752
|
-
if (request.channelRef !== undefined &&
|
|
822
|
+
if (request.channelRef !== undefined && channelRef !== request.channelRef)
|
|
753
823
|
return false;
|
|
754
|
-
if (request.threadRef !== undefined &&
|
|
824
|
+
if (request.threadRef !== undefined && threadRef !== request.threadRef)
|
|
755
825
|
return false;
|
|
756
826
|
if (request.topLevelOnly && (fact.kind !== 'message' || !fact.message.topLevel))
|
|
757
827
|
return false;
|
|
@@ -766,9 +836,9 @@ export class AgentTeamLedger {
|
|
|
766
836
|
const taskNumbers = this.taskNumbers(request.workspaceId);
|
|
767
837
|
const items = selected.filter((fact) => fact.kind === 'message').map(fact => {
|
|
768
838
|
const message = fact.message;
|
|
769
|
-
const task = this.requireTask(request.workspaceId, message.taskRef);
|
|
770
839
|
const thread = this.requireThread(message.threadRef);
|
|
771
|
-
|
|
840
|
+
const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
|
|
841
|
+
return Object.freeze({ message, mentions: fact.mentions, ...(task === undefined ? {} : { task, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }), thread,
|
|
772
842
|
messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0 });
|
|
773
843
|
});
|
|
774
844
|
const initialization = this.initialization();
|
|
@@ -778,7 +848,12 @@ export class AgentTeamLedger {
|
|
|
778
848
|
channels: Object.freeze(channels),
|
|
779
849
|
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
850
|
tasks: Object.freeze(visibleTasks),
|
|
781
|
-
threads: Object.freeze([...this.state.threads.values()].filter(thread =>
|
|
851
|
+
threads: Object.freeze([...this.state.threads.values()].filter(thread => {
|
|
852
|
+
const channelRef = this.channelRefForThread(thread.threadRef);
|
|
853
|
+
return channelRef !== undefined && channelRefs.has(channelRef)
|
|
854
|
+
&& (request.channelRef === undefined || channelRef === request.channelRef)
|
|
855
|
+
&& (request.threadRef === undefined || thread.threadRef === request.threadRef);
|
|
856
|
+
})),
|
|
782
857
|
taskNumbers: Object.freeze(visibleTasks.map(task => Object.freeze({ taskRef: task.taskRef, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }))),
|
|
783
858
|
items: Object.freeze(items),
|
|
784
859
|
claims: this.claimsForVisibleTasks(visibleTasks),
|
|
@@ -816,6 +891,8 @@ export class AgentTeamLedger {
|
|
|
816
891
|
case 'team/member-suspended':
|
|
817
892
|
case 'team/member-resumed':
|
|
818
893
|
case 'team/member-session-restarted':
|
|
894
|
+
case 'team/member-context-cleared':
|
|
895
|
+
case 'team/member-session-renewed':
|
|
819
896
|
case 'team/member-updated':
|
|
820
897
|
case 'team/member-removed':
|
|
821
898
|
return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
|
|
@@ -839,6 +916,8 @@ export class AgentTeamLedger {
|
|
|
839
916
|
case 'team/message-sent':
|
|
840
917
|
case 'team/thread-replied':
|
|
841
918
|
return [{ kind: 'channel', channelRef: operation.data.message.channelRef }, { kind: 'thread', threadRef: operation.data.message.threadRef }];
|
|
919
|
+
case 'team/thread-promoted':
|
|
920
|
+
return [{ kind: 'channel', channelRef: operation.data.task.channelRef }, { kind: 'thread', threadRef: operation.data.thread.threadRef }];
|
|
842
921
|
case 'team/claim-created':
|
|
843
922
|
case 'team/claim-done':
|
|
844
923
|
case 'team/claim-released':
|
|
@@ -885,6 +964,8 @@ export class AgentTeamLedger {
|
|
|
885
964
|
case 'team/message-sent':
|
|
886
965
|
case 'team/thread-replied':
|
|
887
966
|
return [operation.data.message.threadRef];
|
|
967
|
+
case 'team/thread-promoted':
|
|
968
|
+
return [operation.data.activity.threadRef];
|
|
888
969
|
case 'team/claim-created':
|
|
889
970
|
case 'team/claim-done':
|
|
890
971
|
case 'team/claim-released':
|
|
@@ -901,6 +982,8 @@ export class AgentTeamLedger {
|
|
|
901
982
|
case 'team/member-suspended':
|
|
902
983
|
case 'team/member-resumed':
|
|
903
984
|
case 'team/member-session-restarted':
|
|
985
|
+
case 'team/member-context-cleared':
|
|
986
|
+
case 'team/member-session-renewed':
|
|
904
987
|
case 'team/member-updated':
|
|
905
988
|
case 'team/channel-member-added':
|
|
906
989
|
case 'team/thread-read':
|
|
@@ -1010,6 +1093,28 @@ export class AgentTeamLedger {
|
|
|
1010
1093
|
throw new Error('invalid Member restart');
|
|
1011
1094
|
return;
|
|
1012
1095
|
}
|
|
1096
|
+
if (operation.kind === 'team/member-context-cleared') {
|
|
1097
|
+
assertHuman();
|
|
1098
|
+
const prior = projection.members.get(operation.data.member.memberId);
|
|
1099
|
+
// Legacy in-place clear: the recorded Member is unchanged, and the
|
|
1100
|
+
// projection deliberately does not change either.
|
|
1101
|
+
if (prior === undefined || prior.state !== 'enabled' || !this.sameMemberIdentity(prior, operation.data.member))
|
|
1102
|
+
throw new Error('invalid Member context clear');
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (operation.kind === 'team/member-session-renewed') {
|
|
1106
|
+
assertHuman();
|
|
1107
|
+
const prior = projection.members.get(operation.data.member.memberId);
|
|
1108
|
+
// The renewal moves exactly the sessionId: the prior Member must be
|
|
1109
|
+
// enabled on the recorded previous Session, and every other fact must
|
|
1110
|
+
// carry over unchanged.
|
|
1111
|
+
if (prior === undefined || prior.state !== 'enabled'
|
|
1112
|
+
|| prior.sessionId !== operation.data.previousSessionId
|
|
1113
|
+
|| operation.data.member.sessionId === operation.data.previousSessionId
|
|
1114
|
+
|| !this.sameMemberFacts(prior, operation.data.member))
|
|
1115
|
+
throw new Error('invalid Member session renewal');
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1013
1118
|
if (operation.kind === 'team/channel-updated') {
|
|
1014
1119
|
assertHuman();
|
|
1015
1120
|
const prior = projection.channels.get(operation.data.channel.channelRef);
|
|
@@ -1051,7 +1156,7 @@ export class AgentTeamLedger {
|
|
|
1051
1156
|
if (channel === undefined || member === undefined || operation.data.workspaceId !== channel.workspaceId
|
|
1052
1157
|
|| member.workspaceId !== channel.workspaceId || !projection.memberships.get(channel.channelRef)?.has(member.memberId))
|
|
1053
1158
|
throw new Error('invalid Channel membership removal');
|
|
1054
|
-
const threadRefs = new Set([...projection.
|
|
1159
|
+
const threadRefs = new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === channel.channelRef));
|
|
1055
1160
|
this.validateReleaseCleanup(operation.data, projection, member.memberId, threadRefs, operation.sequence, refs);
|
|
1056
1161
|
return;
|
|
1057
1162
|
}
|
|
@@ -1060,7 +1165,7 @@ export class AgentTeamLedger {
|
|
|
1060
1165
|
const prior = projection.members.get(operation.data.member.memberId);
|
|
1061
1166
|
if (prior === undefined || prior.state === 'inactive' || operation.data.member.state !== 'inactive' || !this.sameMemberIdentity(prior, operation.data.member))
|
|
1062
1167
|
throw new Error('invalid Member removal');
|
|
1063
|
-
const threadRefs = new Set(
|
|
1168
|
+
const threadRefs = new Set(projection.threads.keys());
|
|
1064
1169
|
this.validateReleaseCleanup(operation.data, projection, prior.memberId, threadRefs, operation.sequence, refs);
|
|
1065
1170
|
return;
|
|
1066
1171
|
}
|
|
@@ -1068,15 +1173,17 @@ export class AgentTeamLedger {
|
|
|
1068
1173
|
const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
|
|
1069
1174
|
if (operation.data.memberId !== operation.actor.memberId)
|
|
1070
1175
|
throw new Error('Attention operation has wrong actor');
|
|
1071
|
-
const task = projection.tasks.get(operation.data.task.taskRef);
|
|
1072
1176
|
const thread = projection.threads.get(operation.data.thread.threadRef);
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1177
|
+
const task = operation.data.task;
|
|
1178
|
+
const currentTask = thread?.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
|
|
1179
|
+
const channelRef = thread === undefined ? undefined : this.channelRefForThreadFrom(projection, thread.threadRef);
|
|
1180
|
+
if (thread === undefined || channelRef === undefined || !this.sameThread(thread, operation.data.thread)
|
|
1181
|
+
|| (task === undefined ? currentTask !== undefined : currentTask === undefined || !this.sameTask(currentTask, task) || thread.taskRef !== task.taskRef)
|
|
1182
|
+
|| (actor !== undefined && !projection.memberships.get(channelRef)?.has(actor.memberId)))
|
|
1076
1183
|
throw new Error('invalid Attention operation');
|
|
1077
1184
|
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)))) {
|
|
1185
|
+
if ((operation.data.action === 'follow' && (task?.resolution === 'closed' || current !== undefined))
|
|
1186
|
+
|| (operation.data.action === 'unfollow' && (current === undefined || (task !== undefined && this.hasActiveClaimFrom(projection, operation.data.memberId, task.taskRef))))) {
|
|
1080
1187
|
throw new Error('invalid Attention transition');
|
|
1081
1188
|
}
|
|
1082
1189
|
const expected = operation.data.action === 'follow'
|
|
@@ -1094,9 +1201,12 @@ export class AgentTeamLedger {
|
|
|
1094
1201
|
assertHuman();
|
|
1095
1202
|
if (operation.data.memberId !== operation.actor.memberId)
|
|
1096
1203
|
throw new Error('Thread read has wrong actor');
|
|
1097
|
-
const expected = this.prepareReadFrom(projection, operation.data.memberId, operation.data.workspaceId,
|
|
1204
|
+
const expected = this.prepareReadFrom(projection, operation.data.memberId, operation.data.workspaceId, {
|
|
1205
|
+
threadRef: operation.data.thread.threadRef,
|
|
1206
|
+
...(operation.data.task === undefined ? {} : { taskRef: operation.data.task.taskRef }),
|
|
1207
|
+
});
|
|
1098
1208
|
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,
|
|
1209
|
+
...(expected.task === undefined ? {} : { task: expected.task }), thread: expected.thread, claims: expected.claims, anchor: expected.anchor,
|
|
1100
1210
|
anchorMentions: expected.anchorMentions, facts: expected.facts,
|
|
1101
1211
|
readThroughSequence: expected.readThroughSequence, remainingUnreadCount: expected.remainingUnreadCount,
|
|
1102
1212
|
...(expected.attention === undefined ? {} : { attention: expected.attention }), inbox: expected.inbox });
|
|
@@ -1105,27 +1215,56 @@ export class AgentTeamLedger {
|
|
|
1105
1215
|
this.validateInboxDelta(operation.data.inbox, projection, refs);
|
|
1106
1216
|
return;
|
|
1107
1217
|
}
|
|
1218
|
+
if (operation.kind === 'team/thread-promoted') {
|
|
1219
|
+
assertHuman();
|
|
1220
|
+
const { activity, task, thread } = operation.data;
|
|
1221
|
+
const priorThread = projection.threads.get(thread.threadRef);
|
|
1222
|
+
const channelRef = priorThread === undefined ? undefined : this.channelRefForThreadFrom(projection, thread.threadRef);
|
|
1223
|
+
if (priorThread === undefined || priorThread.taskRef !== undefined || projection.tasks.has(task.taskRef)
|
|
1224
|
+
|| channelRef === undefined || task.channelRef !== channelRef
|
|
1225
|
+
|| projection.channels.get(channelRef)?.workspaceId !== operation.data.workspaceId
|
|
1226
|
+
|| operation.data.baseRevision !== priorThread.revision
|
|
1227
|
+
|| task.status !== 'todo' || task.resolution !== 'open'
|
|
1228
|
+
|| thread.revision !== operation.sequence || task.threadRef !== thread.threadRef || thread.taskRef !== task.taskRef
|
|
1229
|
+
|| !this.sameThread(thread, { ...priorThread, taskRef: task.taskRef, revision: operation.sequence })
|
|
1230
|
+
|| activity.kind !== 'promote' || activity.sequence !== operation.sequence || activity.actor !== operation.actor.memberId
|
|
1231
|
+
|| activity.taskRef !== task.taskRef || activity.threadRef !== thread.threadRef)
|
|
1232
|
+
throw new Error('invalid Thread promotion');
|
|
1233
|
+
this.addRef(refs, task.taskRef);
|
|
1234
|
+
this.addRef(refs, activity.activityRef);
|
|
1235
|
+
const expectedInbox = this.promoteThreadInboxFrom(projection, activity, thread.threadRef);
|
|
1236
|
+
if (!isDeepStrictEqual(operation.data.inbox, expectedInbox))
|
|
1237
|
+
throw new Error('invalid Promotion inbox projection');
|
|
1238
|
+
this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1108
1241
|
if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
|
|
1109
1242
|
const actor = operation.actor.kind === 'member' ? assertMember() : (assertHuman(), undefined);
|
|
1110
1243
|
const { message, task, thread } = operation.data;
|
|
1111
1244
|
const channel = projection.channels.get(message.channelRef);
|
|
1112
1245
|
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
|
|
1246
|
+
|| message.sequence !== operation.sequence || thread.revision !== operation.sequence
|
|
1247
|
+
|| message.threadRef !== thread.threadRef
|
|
1248
|
+
|| (task === undefined ? message.taskRef !== undefined || thread.taskRef !== undefined
|
|
1249
|
+
: message.taskRef !== task.taskRef || thread.taskRef !== task.taskRef || task.threadRef !== thread.threadRef || task.channelRef !== channel.channelRef)
|
|
1115
1250
|
|| (actor !== undefined && !projection.memberships.get(channel.channelRef)?.has(actor.memberId)))
|
|
1116
1251
|
throw new Error('invalid Message operation');
|
|
1117
1252
|
if (operation.kind === 'team/message-sent') {
|
|
1118
|
-
if (!message.topLevel || projection.
|
|
1253
|
+
if (!message.topLevel || projection.threads.has(thread.threadRef) || (task !== undefined && projection.tasks.has(task.taskRef))) {
|
|
1119
1254
|
throw new Error('invalid top-level Message operation');
|
|
1120
|
-
|
|
1255
|
+
}
|
|
1121
1256
|
this.addRef(refs, thread.threadRef);
|
|
1257
|
+
if (task !== undefined)
|
|
1258
|
+
this.addRef(refs, task.taskRef);
|
|
1122
1259
|
}
|
|
1123
1260
|
else {
|
|
1124
|
-
const priorTask = projection.tasks.get(task.taskRef);
|
|
1125
1261
|
const priorThread = projection.threads.get(thread.threadRef);
|
|
1126
|
-
|
|
1127
|
-
|
|
1262
|
+
const priorTask = task === undefined ? undefined : projection.tasks.get(task.taskRef);
|
|
1263
|
+
if (message.topLevel || priorThread === undefined || operation.data.baseRevision !== priorThread.revision
|
|
1264
|
+
|| priorThread.taskRef !== thread.taskRef
|
|
1265
|
+
|| (task === undefined ? priorTask !== undefined : priorTask === undefined || !this.sameTask(priorTask, task))) {
|
|
1128
1266
|
throw new Error('invalid Thread reply');
|
|
1267
|
+
}
|
|
1129
1268
|
}
|
|
1130
1269
|
this.addRef(refs, message.messageRef);
|
|
1131
1270
|
this.validateMentions(operation.data.mentions, message, projection);
|
|
@@ -1404,6 +1543,15 @@ export class AgentTeamLedger {
|
|
|
1404
1543
|
// projection deliberately does not change.
|
|
1405
1544
|
return;
|
|
1406
1545
|
}
|
|
1546
|
+
if (operation.kind === 'team/member-context-cleared') {
|
|
1547
|
+
// Legacy audit-only marker: identity, memory path, and binding were
|
|
1548
|
+
// untouched; the Host recreated the live Session empty under the same id.
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if (operation.kind === 'team/member-session-renewed') {
|
|
1552
|
+
target.members.set(operation.data.member.memberId, operation.data.member);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1407
1555
|
if (operation.kind === 'team/channel-updated') {
|
|
1408
1556
|
target.channels.set(operation.data.channel.channelRef, operation.data.channel);
|
|
1409
1557
|
return;
|
|
@@ -1444,13 +1592,23 @@ export class AgentTeamLedger {
|
|
|
1444
1592
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
1445
1593
|
return;
|
|
1446
1594
|
}
|
|
1595
|
+
if (operation.kind === 'team/thread-promoted') {
|
|
1596
|
+
this.appendActivityFact(target, operation.data.activity);
|
|
1597
|
+
target.tasks.set(operation.data.task.taskRef, operation.data.task);
|
|
1598
|
+
target.threads.set(operation.data.thread.threadRef, operation.data.thread);
|
|
1599
|
+
this.applyInboxDelta(target, operation.data.inbox);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1447
1602
|
if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
|
|
1448
1603
|
const { message, mentions } = operation.data;
|
|
1449
1604
|
target.messages.push(message);
|
|
1450
1605
|
target.mentionsByMessage.set(message.messageRef, Object.freeze([...mentions]));
|
|
1451
1606
|
this.appendMessageFact(target, message, mentions);
|
|
1452
|
-
|
|
1607
|
+
if (operation.data.task !== undefined)
|
|
1608
|
+
target.tasks.set(operation.data.task.taskRef, operation.data.task);
|
|
1453
1609
|
target.threads.set(operation.data.thread.threadRef, operation.data.thread);
|
|
1610
|
+
if (message.topLevel)
|
|
1611
|
+
target.channelRefByThread.set(message.threadRef, message.channelRef);
|
|
1454
1612
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
1455
1613
|
return;
|
|
1456
1614
|
}
|
|
@@ -1484,8 +1642,6 @@ export class AgentTeamLedger {
|
|
|
1484
1642
|
const facts = target.factsByThread.get(message.threadRef) ?? [];
|
|
1485
1643
|
facts.push(fact);
|
|
1486
1644
|
target.factsByThread.set(message.threadRef, facts);
|
|
1487
|
-
if (message.topLevel)
|
|
1488
|
-
target.topLevelMessages.push(message);
|
|
1489
1645
|
target.messageCountByThread.set(message.threadRef, (target.messageCountByThread.get(message.threadRef) ?? 0) + 1);
|
|
1490
1646
|
}
|
|
1491
1647
|
appendActivityFact(target, activity) {
|
|
@@ -1520,26 +1676,19 @@ export class AgentTeamLedger {
|
|
|
1520
1676
|
for (const marker of delta.activityMarkers.added)
|
|
1521
1677
|
target.activityMarkers.set(this.activityMarkerKey(marker), marker);
|
|
1522
1678
|
}
|
|
1523
|
-
prepareRead(memberId, workspaceId,
|
|
1524
|
-
return this.prepareReadFrom(this.state, memberId, workspaceId,
|
|
1679
|
+
prepareRead(memberId, workspaceId, request) {
|
|
1680
|
+
return this.prepareReadFrom(this.state, memberId, workspaceId, request);
|
|
1525
1681
|
}
|
|
1526
1682
|
/** 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}'`);
|
|
1683
|
+
prepareReadFrom(projection, memberId, workspaceId, request) {
|
|
1684
|
+
const { task, thread, channelRef } = this.threadContextFrom(projection, workspaceId, request);
|
|
1536
1685
|
if (memberId !== AGENT_TEAM_HUMAN_MEMBER_ID) {
|
|
1537
1686
|
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 '${
|
|
1687
|
+
if (member === undefined || !projection.memberships.get(channelRef)?.has(member.memberId)) {
|
|
1688
|
+
throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channelRef}'`);
|
|
1540
1689
|
}
|
|
1541
1690
|
}
|
|
1542
|
-
const anchor = this.threadAnchorFrom(projection,
|
|
1691
|
+
const anchor = this.threadAnchorFrom(projection, thread.threadRef);
|
|
1543
1692
|
const attention = this.attentionForFrom(projection, memberId, thread.threadRef);
|
|
1544
1693
|
const unread = this.unreadForFrom(projection, memberId, thread.threadRef);
|
|
1545
1694
|
const unreadFacts = unread.slice(0, 20);
|
|
@@ -1580,7 +1729,7 @@ export class AgentTeamLedger {
|
|
|
1580
1729
|
};
|
|
1581
1730
|
this.applyInboxDelta(nextProjection, inbox);
|
|
1582
1731
|
const remainingUnreadCount = this.unreadForFrom(nextProjection, memberId, thread.threadRef).length;
|
|
1583
|
-
return Object.freeze({ task, thread, claims: this.claimsForTaskFrom(projection, task.taskRef), anchor,
|
|
1732
|
+
return Object.freeze({ ...(task === undefined ? {} : { task }), thread, claims: task === undefined ? Object.freeze([]) : this.claimsForTaskFrom(projection, task.taskRef), anchor,
|
|
1584
1733
|
anchorMentions: projection.mentionsByMessage.get(anchor.messageRef) ?? [],
|
|
1585
1734
|
facts: Object.freeze(combined),
|
|
1586
1735
|
readThroughSequence, remainingUnreadCount, ...(attention === undefined ? {} : { attention: nextAttention[0] ?? attention }),
|
|
@@ -1670,6 +1819,17 @@ export class AgentTeamLedger {
|
|
|
1670
1819
|
activityRef: activity.activityRef, sequence: activity.sequence }));
|
|
1671
1820
|
return this.inboxDelta([], [], [], [], markers);
|
|
1672
1821
|
}
|
|
1822
|
+
/** Promotion wakes every current follower; the Human actor never follows, so no recipient filter is needed. */
|
|
1823
|
+
promoteThreadInbox(activity, threadRef) {
|
|
1824
|
+
return this.promoteThreadInboxFrom(this.state, activity, threadRef);
|
|
1825
|
+
}
|
|
1826
|
+
promoteThreadInboxFrom(projection, activity, threadRef) {
|
|
1827
|
+
const markers = [...projection.attention.values()]
|
|
1828
|
+
.filter(attention => attention.threadRef === threadRef && attention.memberId !== activity.actor)
|
|
1829
|
+
.map(attention => Object.freeze({ memberId: attention.memberId, threadRef,
|
|
1830
|
+
activityRef: activity.activityRef, sequence: activity.sequence }));
|
|
1831
|
+
return this.inboxDelta([], [], [], [], markers);
|
|
1832
|
+
}
|
|
1673
1833
|
removeMemberThreadInbox(memberId, threadRefs) {
|
|
1674
1834
|
const removed = [...this.state.attention.values()].filter(attention => attention.memberId === memberId && threadRefs.has(attention.threadRef))
|
|
1675
1835
|
.map(attention => Object.freeze({ memberId, threadRef: attention.threadRef }));
|
|
@@ -1762,11 +1922,11 @@ export class AgentTeamLedger {
|
|
|
1762
1922
|
...(task === undefined ? {} : { taskRef: task.taskRef }), ...(thread === undefined ? {} : { threadRef: thread.threadRef, revision: thread.revision }) });
|
|
1763
1923
|
}
|
|
1764
1924
|
unreadRequired(task, thread, unread) {
|
|
1765
|
-
return Object.freeze({ kind: 'unread_required', taskRef: task.taskRef, threadRef: thread.threadRef,
|
|
1925
|
+
return Object.freeze({ kind: 'unread_required', ...(task === undefined ? {} : { taskRef: task.taskRef }), threadRef: thread.threadRef,
|
|
1766
1926
|
revision: thread.revision, unreadCount: unread.length, directCount: unread.filter(item => item.direct).length });
|
|
1767
1927
|
}
|
|
1768
1928
|
staleRevision(task, thread, expectedRevision) {
|
|
1769
|
-
return Object.freeze({ kind: 'stale_revision', taskRef: task.taskRef, threadRef: thread.threadRef,
|
|
1929
|
+
return Object.freeze({ kind: 'stale_revision', ...(task === undefined ? {} : { taskRef: task.taskRef }), threadRef: thread.threadRef,
|
|
1770
1930
|
expectedRevision, revision: thread.revision });
|
|
1771
1931
|
}
|
|
1772
1932
|
releaseSummaries(claims, actor, sequence) {
|
|
@@ -1819,16 +1979,16 @@ export class AgentTeamLedger {
|
|
|
1819
1979
|
/** Commit projections shared by the Message-sent and Thread-replied results. */
|
|
1820
1980
|
committedMessageResult(operation) {
|
|
1821
1981
|
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,
|
|
1982
|
+
...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread, attention: operation.data.inbox.attention.set,
|
|
1823
1983
|
directMarkers: operation.data.inbox.directMarkers.added };
|
|
1824
1984
|
}
|
|
1825
|
-
threadAnchor(
|
|
1826
|
-
return this.threadAnchorFrom(this.state,
|
|
1985
|
+
threadAnchor(threadRef) {
|
|
1986
|
+
return this.threadAnchorFrom(this.state, threadRef);
|
|
1827
1987
|
}
|
|
1828
|
-
threadAnchorFrom(projection,
|
|
1829
|
-
const anchor = projection.messages.find(message => message.
|
|
1988
|
+
threadAnchorFrom(projection, threadRef) {
|
|
1989
|
+
const anchor = projection.messages.find(message => message.threadRef === threadRef && message.topLevel);
|
|
1830
1990
|
if (anchor === undefined)
|
|
1831
|
-
throw new Error(`
|
|
1991
|
+
throw new Error(`Thread '${threadRef}' has no anchor Message`);
|
|
1832
1992
|
return anchor;
|
|
1833
1993
|
}
|
|
1834
1994
|
threadFactKey(fact) {
|
|
@@ -1843,12 +2003,15 @@ export class AgentTeamLedger {
|
|
|
1843
2003
|
taskNumbers(workspaceId) {
|
|
1844
2004
|
const numbers = new Map();
|
|
1845
2005
|
const next = new Map();
|
|
1846
|
-
for (const
|
|
1847
|
-
|
|
2006
|
+
for (const operation of this.state.ordered) {
|
|
2007
|
+
const task = operation.kind === 'team/message-sent' || operation.kind === 'team/thread-promoted' ? operation.data.task : undefined;
|
|
2008
|
+
if (task === undefined)
|
|
1848
2009
|
continue;
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2010
|
+
if (this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
|
|
2011
|
+
continue;
|
|
2012
|
+
const ordinal = (next.get(task.channelRef) ?? 0) + 1;
|
|
2013
|
+
next.set(task.channelRef, ordinal);
|
|
2014
|
+
numbers.set(task.taskRef, ordinal);
|
|
1852
2015
|
}
|
|
1853
2016
|
return numbers;
|
|
1854
2017
|
}
|
|
@@ -1932,6 +2095,53 @@ export class AgentTeamLedger {
|
|
|
1932
2095
|
throw new Error(`unknown Thread ref '${threadRef}'${this.unknownRefHint(threadRef, 'thread', 'Thread')}`);
|
|
1933
2096
|
return thread;
|
|
1934
2097
|
}
|
|
2098
|
+
channelRefForThread(threadRef) {
|
|
2099
|
+
return this.channelRefForThreadFrom(this.state, threadRef);
|
|
2100
|
+
}
|
|
2101
|
+
channelRefForThreadFrom(projection, threadRef) {
|
|
2102
|
+
return projection.channelRefByThread.get(threadRef);
|
|
2103
|
+
}
|
|
2104
|
+
threadContextForActor(actor, workspaceId, request) {
|
|
2105
|
+
const resolved = this.threadContextFrom(this.state, workspaceId, request);
|
|
2106
|
+
if (actor.kind === 'member')
|
|
2107
|
+
this.requireMemberChannel(this.requireMember(actor.memberId), resolved.channelRef);
|
|
2108
|
+
return resolved;
|
|
2109
|
+
}
|
|
2110
|
+
threadContextFrom(projection, workspaceId, request) {
|
|
2111
|
+
if (request.threadRef === undefined && request.taskRef === undefined)
|
|
2112
|
+
throw new Error('Thread addressing requires threadRef or taskRef');
|
|
2113
|
+
let thread;
|
|
2114
|
+
let task;
|
|
2115
|
+
if (request.threadRef !== undefined) {
|
|
2116
|
+
thread = projection.threads.get(request.threadRef);
|
|
2117
|
+
if (thread === undefined)
|
|
2118
|
+
throw new Error(`unknown Thread ref '${request.threadRef}'${this.unknownRefHint(request.threadRef, 'thread', 'Thread')}`);
|
|
2119
|
+
task = thread.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
|
|
2120
|
+
}
|
|
2121
|
+
if (request.taskRef !== undefined) {
|
|
2122
|
+
const byTask = projection.tasks.get(request.taskRef);
|
|
2123
|
+
if (byTask === undefined)
|
|
2124
|
+
throw new Error(`unknown Task ref '${request.taskRef}'${this.unknownRefHint(request.taskRef, 'task', 'Task')}`);
|
|
2125
|
+
if (task !== undefined && task.taskRef !== byTask.taskRef)
|
|
2126
|
+
throw new Error(`Task '${request.taskRef}' does not belong to Thread '${request.threadRef}'`);
|
|
2127
|
+
if (thread !== undefined && byTask.threadRef !== thread.threadRef)
|
|
2128
|
+
throw new Error(`Task '${request.taskRef}' does not belong to Thread '${thread.threadRef}'`);
|
|
2129
|
+
task = byTask;
|
|
2130
|
+
thread = projection.threads.get(byTask.threadRef);
|
|
2131
|
+
if (thread === undefined)
|
|
2132
|
+
throw new Error(`unknown Thread ref '${byTask.threadRef}'`);
|
|
2133
|
+
}
|
|
2134
|
+
if (thread === undefined)
|
|
2135
|
+
throw new Error('unknown Thread');
|
|
2136
|
+
const channelRef = this.channelRefForThreadFrom(projection, thread.threadRef);
|
|
2137
|
+
if (channelRef === undefined)
|
|
2138
|
+
throw new Error(`Thread '${thread.threadRef}' has no Channel`);
|
|
2139
|
+
if (projection.channels.get(channelRef)?.workspaceId !== workspaceId)
|
|
2140
|
+
throw new Error(`Thread '${thread.threadRef}' does not belong to Workspace '${workspaceId}'`);
|
|
2141
|
+
if (task !== undefined && task.channelRef !== channelRef)
|
|
2142
|
+
throw new Error(`Task '${task.taskRef}' does not belong to Thread '${thread.threadRef}'`);
|
|
2143
|
+
return { ...(task === undefined ? {} : { task }), thread, channelRef };
|
|
2144
|
+
}
|
|
1935
2145
|
requireChannel(workspaceId, channelRef) {
|
|
1936
2146
|
const channel = this.state.channels.get(channelRef);
|
|
1937
2147
|
if (channel === undefined)
|
|
@@ -2032,6 +2242,11 @@ export class AgentTeamLedger {
|
|
|
2032
2242
|
if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.member.memberId !== request.memberId)
|
|
2033
2243
|
this.throwRequestCollision(request.requestId);
|
|
2034
2244
|
}
|
|
2245
|
+
assertSameMemberSessionRenewed(operation, request) {
|
|
2246
|
+
if (operation.kind !== 'team/member-session-renewed' || !this.sameActor(operation.actor, request.actor)
|
|
2247
|
+
|| operation.data.member.memberId !== request.memberId || operation.data.member.sessionId !== request.sessionId)
|
|
2248
|
+
this.throwRequestCollision(request.requestId);
|
|
2249
|
+
}
|
|
2035
2250
|
assertSameChannelUpdate(operation, request) {
|
|
2036
2251
|
if (operation.kind !== 'team/channel-updated' || !this.sameActor(operation.actor, request.actor)
|
|
2037
2252
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef
|
|
@@ -2059,16 +2274,28 @@ export class AgentTeamLedger {
|
|
|
2059
2274
|
if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
|
|
2060
2275
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
|
|
2061
2276
|
|| 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 ?? [])
|
|
2277
|
+
|| !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? [])
|
|
2278
|
+
|| (request.asTask !== false) !== (operation.data.task !== undefined))
|
|
2063
2279
|
this.throwRequestCollision(request.requestId);
|
|
2064
2280
|
}
|
|
2065
2281
|
assertSameReply(operation, request, recipients) {
|
|
2066
2282
|
if (operation.kind !== 'team/thread-replied' || !this.sameActor(operation.actor, request.actor)
|
|
2067
|
-
|| operation.data.workspaceId !== request.workspaceId
|
|
2283
|
+
|| operation.data.workspaceId !== request.workspaceId
|
|
2284
|
+
|| (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
|
|
2285
|
+
|| (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
|
|
2286
|
+
|| (request.threadRef === undefined && request.taskRef === undefined)
|
|
2068
2287
|
|| operation.data.message.body !== request.body.trim() || operation.data.baseRevision !== request.baseRevision
|
|
2069
2288
|
|| !this.sameList(operation.data.mentions, recipients))
|
|
2070
2289
|
this.throwRequestCollision(request.requestId);
|
|
2071
2290
|
}
|
|
2291
|
+
assertSamePromotion(operation, request) {
|
|
2292
|
+
if (operation.kind !== 'team/thread-promoted' || !this.sameActor(operation.actor, request.actor)
|
|
2293
|
+
|| operation.data.workspaceId !== request.workspaceId
|
|
2294
|
+
|| operation.data.thread.threadRef !== request.threadRef
|
|
2295
|
+
|| operation.data.baseRevision !== request.baseRevision) {
|
|
2296
|
+
this.throwRequestCollision(request.requestId);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2072
2299
|
assertSameClaim(operation, request) {
|
|
2073
2300
|
const kind = request.action === 'claim' ? 'team/claim-created' : request.action === 'done' ? 'team/claim-done' : 'team/claim-released';
|
|
2074
2301
|
if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.workspaceId !== request.workspaceId
|
|
@@ -2089,12 +2316,19 @@ export class AgentTeamLedger {
|
|
|
2089
2316
|
}
|
|
2090
2317
|
assertSameAttention(operation, request) {
|
|
2091
2318
|
if (operation.kind !== 'team/thread-attention-changed' || !this.sameActor(operation.actor, request.actor)
|
|
2092
|
-
|| operation.data.workspaceId !== request.workspaceId
|
|
2319
|
+
|| operation.data.workspaceId !== request.workspaceId
|
|
2320
|
+
|| (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
|
|
2321
|
+
|| (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
|
|
2322
|
+
|| (request.threadRef === undefined && request.taskRef === undefined)
|
|
2323
|
+
|| operation.data.action !== request.action)
|
|
2093
2324
|
this.throwRequestCollision(request.requestId);
|
|
2094
2325
|
}
|
|
2095
2326
|
assertSameThreadRead(operation, request) {
|
|
2096
2327
|
if (operation.kind !== 'team/thread-read' || !this.sameActor(operation.actor, request.actor)
|
|
2097
|
-
|| operation.data.workspaceId !== request.workspaceId
|
|
2328
|
+
|| operation.data.workspaceId !== request.workspaceId
|
|
2329
|
+
|| (request.threadRef !== undefined && operation.data.thread.threadRef !== request.threadRef)
|
|
2330
|
+
|| (request.taskRef !== undefined && operation.data.task?.taskRef !== request.taskRef)
|
|
2331
|
+
|| (request.threadRef === undefined && request.taskRef === undefined))
|
|
2098
2332
|
this.throwRequestCollision(request.requestId);
|
|
2099
2333
|
}
|
|
2100
2334
|
sameTask(left, right) {
|
|
@@ -2119,6 +2353,13 @@ export class AgentTeamLedger {
|
|
|
2119
2353
|
&& isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
|
|
2120
2354
|
&& left.privateMemoryPath === right.privateMemoryPath;
|
|
2121
2355
|
}
|
|
2356
|
+
/** sameMemberIdentity minus the sessionId: every durable Member fact a session renewal must carry over unchanged. */
|
|
2357
|
+
sameMemberFacts(left, right) {
|
|
2358
|
+
return left.memberId === right.memberId && left.workspaceId === right.workspaceId
|
|
2359
|
+
&& left.handle === right.handle && left.description === right.description && left.presetId === right.presetId
|
|
2360
|
+
&& isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
|
|
2361
|
+
&& left.privateMemoryPath === right.privateMemoryPath;
|
|
2362
|
+
}
|
|
2122
2363
|
sameActor(left, right) {
|
|
2123
2364
|
return left.kind === right.kind && left.memberId === right.memberId && left.handle === right.handle;
|
|
2124
2365
|
}
|
|
@@ -2180,13 +2421,17 @@ export class AgentTeamLedger {
|
|
|
2180
2421
|
task: operation.data.task, thread: operation.data.thread, claims: operation.data.claims });
|
|
2181
2422
|
}
|
|
2182
2423
|
attentionResult(operation) {
|
|
2183
|
-
return Object.freeze({ receipt: this.receipt(operation), task: operation.data.task, thread: operation.data.thread,
|
|
2424
|
+
return Object.freeze({ receipt: this.receipt(operation), ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread,
|
|
2184
2425
|
...(operation.data.inbox.attention.set[0] === undefined ? {} : { attention: operation.data.inbox.attention.set[0] }) });
|
|
2185
2426
|
}
|
|
2427
|
+
promotionResult(operation) {
|
|
2428
|
+
return Object.freeze({ kind: 'committed', receipt: this.receipt(operation), activity: operation.data.activity,
|
|
2429
|
+
task: operation.data.task, thread: operation.data.thread });
|
|
2430
|
+
}
|
|
2186
2431
|
threadReadResult(operation) {
|
|
2187
2432
|
// Loaded records are normalized by sortedRecords(); fresh writes always carry instants.
|
|
2188
2433
|
const { anchor, facts } = operation.data;
|
|
2189
|
-
return Object.freeze({ receipt: this.receipt(operation), task: operation.data.task, thread: operation.data.thread,
|
|
2434
|
+
return Object.freeze({ receipt: this.receipt(operation), ...(operation.data.task === undefined ? {} : { task: operation.data.task }), thread: operation.data.thread,
|
|
2190
2435
|
claims: operation.data.claims, anchor, anchorMentions: operation.data.anchorMentions, facts,
|
|
2191
2436
|
readThroughSequence: operation.data.readThroughSequence, remainingUnreadCount: operation.data.remainingUnreadCount,
|
|
2192
2437
|
...(operation.data.attention === undefined ? {} : { attention: operation.data.attention }),
|