@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.
Files changed (90) hide show
  1. package/README.md +8 -1
  2. package/README.zh.md +9 -1
  3. package/package.json +12 -3
  4. package/packages/agent-team/README.md +3 -3
  5. package/packages/agent-team/README.zh.md +3 -3
  6. package/packages/agent-team/lib/attachments.js +11 -12
  7. package/packages/agent-team/lib/auto-compaction.js +163 -0
  8. package/packages/agent-team/lib/index.js +252 -139
  9. package/packages/agent-team/lib/ledger.js +344 -170
  10. package/packages/agent-team/lib/recovery.js +30 -35
  11. package/packages/agent-team/lib/spec.js +27 -11
  12. package/packages/agent-team/lib/typert.host.js +197 -88
  13. package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
  14. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  15. package/packages/agent-team/lib/typert.remote-client.js +139 -59
  16. package/packages/agent-team/lib/types/attachments.d.ts +2 -2
  17. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  18. package/packages/agent-team/lib/types/auto-compaction.d.ts +38 -0
  19. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -0
  20. package/packages/agent-team/lib/types/index.d.ts +52 -19
  21. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/ledger.d.ts +29 -2
  23. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  24. package/packages/agent-team/lib/types/recovery.d.ts +15 -16
  25. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
  26. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/types.d.ts +69 -30
  28. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  29. package/packages/agent-team/preset/team-member/agent.cordis.yml +7 -2
  30. package/packages/client-agent-team/README.md +2 -1
  31. package/packages/client-agent-team/README.zh.md +2 -1
  32. package/packages/client-agent-team/lib/client.js +1577 -863
  33. package/packages/client-agent-team/lib/client.js.map +1 -1
  34. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  35. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +35 -174
  36. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  37. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +20 -30
  38. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +3 -2
  40. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +6 -1
  41. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  42. package/packages/client-agent-team/lib/types/client/TeamComposer.js +4 -5
  43. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  44. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  46. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +34 -0
  47. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -0
  48. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +169 -0
  49. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts +17 -0
  50. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -0
  51. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +120 -0
  52. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +4 -1
  53. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/TeamMessage.js +94 -47
  55. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +2 -1
  56. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +81 -39
  58. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  59. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -1
  60. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  61. package/packages/client-agent-team/lib/types/client/index.js +72 -1
  62. package/packages/client-agent-team/lib/types/client/locales.d.ts +24 -0
  63. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/locales.js +24 -0
  65. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +22 -0
  66. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +1 -0
  67. package/packages/client-agent-team/lib/types/client/member-session-input.js +98 -0
  68. package/packages/client-agent-team/lib/types/client/navigation.d.ts +1 -1
  69. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/navigation.js +12 -6
  71. package/packages/client-agent-team/lib/types/client/requests.d.ts +16 -0
  72. package/packages/client-agent-team/lib/types/client/requests.d.ts.map +1 -0
  73. package/packages/client-agent-team/lib/types/client/requests.js +22 -0
  74. package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -1
  75. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  76. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +6 -1
  77. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
  78. package/packages/client-agent-team/lib/types/client/task-refs.js +19 -0
  79. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +52 -3
  80. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/team-formatters.js +74 -4
  82. package/packages/client-agent-team/lib/types/client/team-membership.d.ts.map +1 -1
  83. package/packages/client-agent-team/lib/types/client/team-membership.js +2 -1
  84. package/packages/tool-agent-team/README.md +7 -7
  85. package/packages/tool-agent-team/README.zh.md +7 -7
  86. package/packages/tool-agent-team/lib/index.js +38 -28
  87. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  88. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts +0 -13
  89. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts.map +0 -1
  90. 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(), topLevelMessages: [], mentionsByMessage: new Map(), messageCountByThread: 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 taskRef = this.ref('task');
285
+ const asTask = request.asTask !== false;
282
286
  const threadRef = this.ref('thread');
283
- const task = Object.freeze({ taskRef, channelRef: channel.channelRef, threadRef, status: 'todo', resolution: 'open' });
284
- const thread = Object.freeze({ threadRef, taskRef, revision: sequence });
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, task, thread, inbox }),
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.requireTask(request.workspaceId, request.taskRef);
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, task.channelRef), recipients);
321
- const unread = this.unreadFor(actor.memberId, thread.threadRef);
322
- if (unread.length > 0)
323
- return this.resolved(this.unreadRequired(task, thread, unread));
324
- if (request.baseRevision !== thread.revision)
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, task.channelRef, unfollowedAgents, task, thread));
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, task.channelRef, body, recipients, task, thread));
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, task.channelRef, task, thread, body, recipients);
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: task.channelRef, threadRef: task.threadRef,
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.requireTask(request.workspaceId, request.taskRef);
368
- const thread = this.requireThread(task.threadRef);
369
- if (actor.kind === 'member')
370
- this.requireMemberChannel(this.requireMember(actor.memberId), task.channelRef);
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.requireTask(request.workspaceId, request.taskRef);
445
- const thread = this.requireThread(task.threadRef);
446
- const unread = this.unreadFor(request.actor.memberId, thread.threadRef);
447
- if (unread.length > 0)
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.requireTask(request.workspaceId, request.taskRef);
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.resolution === 'closed')
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.requireTask(request.workspaceId, request.taskRef);
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 task of this.state.tasks.values()) {
590
- if (this.state.channels.get(task.channelRef)?.workspaceId !== request.workspaceId)
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(task.channelRef, authorized.memberId))
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(attention === undefined
601
- ? Object.freeze({ channelRef: task.channelRef, task, thread, unreadCount: unread.length, directCount,
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.taskRef);
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, task: prepared.task,
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.requireTask(request.workspaceId, request.taskRef);
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 = task.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
- switch (operation.kind) {
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.requireTask(request.workspaceId, request.taskRef);
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(task);
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.requireTask(request.workspaceId, request.taskRef);
728
- if (authorized.kind === 'member')
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 task = this.requireTask(request.workspaceId, thread.taskRef);
773
- if (request.channelRef !== undefined && task.channelRef !== request.channelRef)
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(task.channelRef, memberId))
776
- throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${task.channelRef}'`);
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 task = fact.kind === 'message' ? this.state.tasks.get(fact.message.taskRef) : this.state.tasks.get(fact.activity.taskRef);
783
- if (task === undefined || !channelRefs.has(task.channelRef))
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 && task.channelRef !== request.channelRef)
791
+ if (request.channelRef !== undefined && channelRef !== request.channelRef)
786
792
  return false;
787
- if (request.threadRef !== undefined && task.threadRef !== request.threadRef)
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, request.channelRef);
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
- return Object.freeze({ message, mentions: fact.mentions, task, thread, taskNumber: taskNumbers.get(task.taskRef) ?? 0,
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 => visibleTasks.some(task => task.taskRef === thread.taskRef))),
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
- default:
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.tasks.values()].filter(task => task.channelRef === channel.channelRef).map(task => task.threadRef));
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([...projection.tasks.values()].map(task => task.threadRef));
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
- if (task === undefined || thread === undefined || !this.sameTask(task, operation.data.task)
1094
- || !this.sameThread(thread, operation.data.thread)
1095
- || (actor !== undefined && !projection.memberships.get(task.channelRef)?.has(actor.memberId)))
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.resolution === 'closed' || current !== undefined))
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, operation.data.task.taskRef);
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 || message.taskRef !== task.taskRef
1134
- || message.threadRef !== thread.threadRef || task.threadRef !== thread.threadRef || task.channelRef !== channel.channelRef
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.tasks.has(task.taskRef) || projection.threads.has(thread.threadRef))
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
- this.addRef(refs, task.taskRef);
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
- if (message.topLevel || priorTask === undefined || priorThread === undefined || !this.sameTask(priorTask, task)
1147
- || priorThread.taskRef !== thread.taskRef || operation.data.baseRevision !== priorThread.revision)
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
- target.tasks.set(operation.data.task.taskRef, operation.data.task);
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, taskRef) {
1541
- return this.prepareReadFrom(this.state, memberId, workspaceId, taskRef);
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, taskRef) {
1545
- const task = projection.tasks.get(taskRef);
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(task.channelRef)?.has(member.memberId)) {
1556
- throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${task.channelRef}'`);
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, task);
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
- threadAnchor(task) {
1820
- return this.threadAnchorFrom(this.state, task);
1821
- }
1822
- threadAnchorFrom(projection, task) {
1823
- const anchor = projection.messages.find(message => message.taskRef === task.taskRef && message.topLevel);
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(`Task '${task.taskRef}' has no anchor Message`);
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
- taskNumbers(workspaceId, channelRef) {
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
- let next = 1;
1834
- for (const message of this.state.topLevelMessages) {
1835
- if (this.state.channels.get(message.channelRef)?.workspaceId !== workspaceId)
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 (channelRef !== undefined && message.channelRef !== channelRef)
1944
+ if (this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
1838
1945
  continue;
1839
- numbers.set(message.taskRef, next);
1840
- next += 1;
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 || operation.data.message.taskRef !== request.taskRef
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 || operation.data.task.taskRef !== request.taskRef || operation.data.action !== request.action)
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 || operation.data.task.taskRef !== request.taskRef)
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({ kind: 'committed', receipt: this.receipt(operation), message: operation.data.message,
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({ kind: 'committed', receipt: this.receipt(operation), message: operation.data.message,
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 }),