@wowyuarm/dsh-agent-team 0.1.12 → 0.1.13

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 (85) hide show
  1. package/README.md +2 -0
  2. package/README.zh.md +2 -0
  3. package/cordis.patch.yml +7 -3
  4. package/package.json +14 -2
  5. package/packages/agent-team/README.md +2 -2
  6. package/packages/agent-team/README.zh.md +2 -2
  7. package/packages/agent-team/lib/index.js +151 -69
  8. package/packages/agent-team/lib/ledger.js +249 -31
  9. package/packages/agent-team/lib/member-context.js +18 -1
  10. package/packages/agent-team/lib/member-runtime.js +5 -4
  11. package/packages/agent-team/lib/mentions.js +48 -24
  12. package/packages/agent-team/lib/spec.js +19 -0
  13. package/packages/agent-team/lib/typert.host.js +187 -41
  14. package/packages/agent-team/lib/typert.remote-client.d.ts +7 -3
  15. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  16. package/packages/agent-team/lib/typert.remote-client.js +132 -27
  17. package/packages/agent-team/lib/types/index.d.ts +32 -7
  18. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  19. package/packages/agent-team/lib/types/ledger.d.ts +46 -2
  20. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  21. package/packages/agent-team/lib/types/member-context.d.ts +6 -0
  22. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/mentions.d.ts +20 -7
  24. package/packages/agent-team/lib/types/mentions.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  26. package/packages/agent-team/lib/types/types/entities.d.ts +3 -2
  27. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  28. package/packages/agent-team/lib/types/types/operations.d.ts +37 -1
  29. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/types/requests-results.d.ts +50 -10
  31. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/vendor/storage-sqlite/index.d.ts +87 -0
  33. package/packages/agent-team/lib/types/vendor/storage-sqlite/index.d.ts.map +1 -0
  34. package/packages/agent-team/lib/types/vendor/storage-sqlite/schema.d.ts +59 -0
  35. package/packages/agent-team/lib/types/vendor/storage-sqlite/schema.d.ts.map +1 -0
  36. package/packages/agent-team/lib/types/vendor/storage-sqlite/unit.d.ts +57 -0
  37. package/packages/agent-team/lib/types/vendor/storage-sqlite/unit.d.ts.map +1 -0
  38. package/packages/agent-team/lib/vendor/storage-sqlite/index.js +139 -0
  39. package/packages/agent-team/lib/vendor/storage-sqlite/schema.js +120 -0
  40. package/packages/agent-team/lib/vendor/storage-sqlite/unit.js +132 -0
  41. package/packages/client-agent-team/README.md +1 -1
  42. package/packages/client-agent-team/README.zh.md +1 -1
  43. package/packages/client-agent-team/lib/client.js +603 -343
  44. package/packages/client-agent-team/lib/client.js.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamAgentImport.d.ts +12 -0
  46. package/packages/client-agent-team/lib/types/client/TeamAgentImport.d.ts.map +1 -0
  47. package/packages/client-agent-team/lib/types/client/TeamAgentImport.js +69 -0
  48. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +3 -1
  49. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +29 -17
  51. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  52. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +19 -44
  53. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +2 -2
  54. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +15 -14
  56. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  59. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +15 -29
  60. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  61. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  62. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  63. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/index.js +8 -1
  65. package/packages/client-agent-team/lib/types/client/locales.d.ts +18 -0
  66. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  67. package/packages/client-agent-team/lib/types/client/locales.js +18 -0
  68. package/packages/client-agent-team/lib/types/client/slots.d.ts +3 -1
  69. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/team-changes.d.ts +15 -31
  71. package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/team-changes.js +82 -111
  73. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +13 -11
  74. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  75. package/packages/client-agent-team/lib/types/client/team-formatters.js +29 -35
  76. package/packages/tool-agent-team/README.md +1 -1
  77. package/packages/tool-agent-team/README.zh.md +1 -1
  78. package/packages/tool-agent-team/lib/host-access.js +19 -0
  79. package/packages/tool-agent-team/lib/index.js +42 -33
  80. package/packages/tool-agent-team/lib/types/host-access.d.ts +10 -0
  81. package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -1
  82. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  83. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +0 -41
  84. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +0 -1
  85. package/packages/client-agent-team/lib/types/client/scope-coverage.js +0 -64
@@ -22,7 +22,7 @@ export function agentTeamHumanActor() {
22
22
  return HUMAN_ACTOR;
23
23
  }
24
24
  function emptyProjection() {
25
- return { byRequest: new Map(), byOperation: new Map(), ordered: [], channels: new Map(), members: new Map(), memberships: new Map(),
25
+ return { byRequest: new Map(), byOperation: new Map(), ordered: [], channels: new Map(), members: new Map(), participations: new Map(), memberships: new Map(),
26
26
  claims: new Map(), tasks: new Map(), threads: new Map(), attention: new Map(), directMarkers: new Map(), activityMarkers: new Map(),
27
27
  orderedFacts: [], factsByThread: new Map(), channelRefByThread: new Map(), mentionsByMessage: new Map(), messageCountByThread: new Map(),
28
28
  messagesByRef: new Map(),
@@ -265,7 +265,8 @@ export class AgentTeamLedger {
265
265
  if (handle === '')
266
266
  throw new Error('member handle must not be empty');
267
267
  if (handle !== prior.handle)
268
- this.assertHandleAvailable(prior.workspaceId, handle, prior.memberId);
268
+ for (const workspaceId of this.workspacesOf(prior.memberId))
269
+ this.assertHandleAvailable(workspaceId, handle, prior.memberId);
269
270
  this.assertModelSelection(request.model);
270
271
  this.assertCapabilities(request.capabilities);
271
272
  // An absent model or capabilities field must CLEAR any override
@@ -465,8 +466,8 @@ export class AgentTeamLedger {
465
466
  this.assertHumanActor(request.actor);
466
467
  const channel = this.requireActiveChannel(request.workspaceId, request.channelRef);
467
468
  const member = this.requireMember(request.memberId);
468
- if (member.workspaceId !== request.workspaceId)
469
- throw new Error('Member and Channel must belong to one Workspace');
469
+ if (!this.participatesIn(member.memberId, request.workspaceId))
470
+ throw new Error('Agent Member does not participate in the Channel\'s Workspace');
470
471
  if (member.state !== 'enabled')
471
472
  throw new Error(`Agent Member '${member.memberId}' is ${member.state}; only enabled Members can join a Channel`);
472
473
  if (this.isChannelMember(channel.channelRef, member.memberId))
@@ -490,7 +491,7 @@ export class AgentTeamLedger {
490
491
  this.assertHumanActor(request.actor);
491
492
  const channel = this.requireChannel(request.workspaceId, request.channelRef);
492
493
  const member = this.requireMember(request.memberId);
493
- if (member.workspaceId !== channel.workspaceId || !this.isChannelMember(channel.channelRef, member.memberId)) {
494
+ if (!this.participatesIn(member.memberId, channel.workspaceId) || !this.isChannelMember(channel.channelRef, member.memberId)) {
494
495
  throw new Error(`Agent Member '${member.memberId}' is not a member of Channel '${channel.channelRef}'`);
495
496
  }
496
497
  const threadRefs = this.channelThreadRefs(channel.channelRef);
@@ -515,6 +516,86 @@ export class AgentTeamLedger {
515
516
  return this.committed(this.channelMemberRemovalResult(operation));
516
517
  });
517
518
  }
519
+ /**
520
+ * Join one Agent Member to one additional Workspace. Participation is a
521
+ * pure relation — no Session is created or moved; the Member's Session
522
+ * stays rooted in its default Workspace and collaboration in the joined
523
+ * Workspace is ledger work addressed by the workspaceId.
524
+ */
525
+ joinWorkspace(request) {
526
+ return this.enqueue(async () => {
527
+ const existing = this.state.byRequest.get(request.requestId);
528
+ if (existing !== undefined) {
529
+ this.assertSameWorkspaceJoin(existing, request);
530
+ return this.resolved(this.workspaceJoinResult(existing));
531
+ }
532
+ this.assertHumanActor(request.actor);
533
+ const member = this.requireMember(request.memberId);
534
+ if (member.state === 'inactive')
535
+ throw new Error(`Agent Member '${member.memberId}' is inactive and cannot join a Workspace`);
536
+ if (member.state === 'archived')
537
+ throw new Error(`Agent Member '${member.memberId}' is archived and cannot join a Workspace`);
538
+ if (this.participatesIn(member.memberId, request.workspaceId))
539
+ throw new Error(`Agent Member '${member.memberId}' already participates in Workspace '${request.workspaceId}'`);
540
+ // Handles resolve per Workspace: a second live Member with the same
541
+ // handle in the target would make mentions ambiguous there.
542
+ this.assertHandleAvailable(request.workspaceId, member.handle);
543
+ const operation = Object.freeze({
544
+ ...this.operationBase(request, this.nextSequence()), kind: 'team/member-workspace-joined',
545
+ data: Object.freeze({ workspaceId: request.workspaceId, memberId: member.memberId }),
546
+ });
547
+ await this.table.put(operation.operationId, operation);
548
+ this.apply(operation);
549
+ return this.committed(this.workspaceJoinResult(operation));
550
+ });
551
+ }
552
+ /**
553
+ * Withdraw one Agent Member from one of its non-default Workspaces. The
554
+ * default Workspace cannot be left — ending it is the archive path. Every
555
+ * active Claim the Member holds on the Workspace's Threads releases with
556
+ * public Activities, its Attention and markers on those Threads clear, and
557
+ * its Channel memberships there end; identity, Session, and remaining
558
+ * participations are untouched.
559
+ */
560
+ leaveWorkspace(request) {
561
+ return this.enqueue(async () => {
562
+ const existing = this.state.byRequest.get(request.requestId);
563
+ if (existing !== undefined) {
564
+ this.assertSameWorkspaceLeave(existing, request);
565
+ return this.resolved(this.workspaceLeaveResult(existing));
566
+ }
567
+ this.assertHumanActor(request.actor);
568
+ const member = this.requireMember(request.memberId);
569
+ if (member.state === 'inactive')
570
+ throw new Error(`Agent Member '${member.memberId}' is already inactive`);
571
+ if (member.state === 'archived')
572
+ throw new Error(`Agent Member '${member.memberId}' is already archived`);
573
+ if (request.workspaceId === member.workspaceId)
574
+ throw new Error(`Agent Member '${member.memberId}' cannot leave its default Workspace '${request.workspaceId}'; archive the Member instead`);
575
+ if (!this.participatesIn(member.memberId, request.workspaceId))
576
+ throw new Error(`Agent Member '${member.memberId}' does not participate in Workspace '${request.workspaceId}'`);
577
+ const threadRefs = this.workspaceThreadRefs(request.workspaceId);
578
+ const releasedClaims = [...this.state.claims.values()]
579
+ .filter(claim => claim.owner === member.memberId && claim.state === 'active' && threadRefs.has(claim.threadRef))
580
+ .map(claim => Object.freeze({ ...claim, state: 'released' }));
581
+ const projectedClaims = new Map(this.state.claims);
582
+ for (const claim of releasedClaims)
583
+ projectedClaims.set(claim.claimRef, claim);
584
+ const sequence = this.nextSequence();
585
+ const activities = this.releaseSummaries(releasedClaims, member.memberId, sequence);
586
+ const threads = this.threadsForActivities(activities);
587
+ const tasks = this.tasksForClaims(releasedClaims, projectedClaims);
588
+ const inbox = this.removeMemberThreadInbox(member.memberId, threadRefs);
589
+ const operation = Object.freeze({
590
+ ...this.operationBase(request, sequence), kind: 'team/member-workspace-left',
591
+ data: Object.freeze({ workspaceId: request.workspaceId, memberId: member.memberId,
592
+ claims: Object.freeze(releasedClaims), activities, tasks, threads, inbox }),
593
+ });
594
+ await this.table.put(operation.operationId, operation);
595
+ this.apply(operation);
596
+ return this.committed(this.workspaceLeaveResult(operation));
597
+ });
598
+ }
518
599
  /**
519
600
  * Archive one Channel: hidden from every surface, facts kept recoverable.
520
601
  * Every active Claim on the Channel's Threads releases with one public
@@ -975,7 +1056,7 @@ export class AgentTeamLedger {
975
1056
  if (body === '')
976
1057
  throw new Error('DM body must not be empty');
977
1058
  const recipient = this.requireMember(request.recipientMemberId);
978
- if (recipient.workspaceId !== request.workspaceId)
1059
+ if (!this.participatesIn(recipient.memberId, request.workspaceId))
979
1060
  throw new Error(`Agent Member '${recipient.memberId}' is not in Workspace '${request.workspaceId}'`);
980
1061
  if (recipient.state !== 'enabled')
981
1062
  throw new Error(`Agent Member '${recipient.memberId}' is ${recipient.state}; DM delivery requires an enabled Member`);
@@ -1030,7 +1111,16 @@ export class AgentTeamLedger {
1030
1111
  }
1031
1112
  inbox(actor, request) {
1032
1113
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
1033
- const limit = request.limit ?? 50;
1114
+ return this.inboxForWorkspaces(authorized, [request.workspaceId], request.limit);
1115
+ }
1116
+ memberInbox(actor, request) {
1117
+ const workspaces = this.workspacesOf(actor.memberId);
1118
+ for (const workspaceId of workspaces)
1119
+ this.assertActorForWorkspace(actor, workspaceId);
1120
+ return this.inboxForWorkspaces(actor, workspaces, request.limit);
1121
+ }
1122
+ inboxForWorkspaces(authorized, workspaceIds, requestedLimit) {
1123
+ const limit = requestedLimit ?? 50;
1034
1124
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
1035
1125
  throw new Error('inbox limit must be an integer between 1 and 100');
1036
1126
  // One unread slice serves every reader: Threads the reader follows or
@@ -1044,7 +1134,7 @@ export class AgentTeamLedger {
1044
1134
  // A Human reader additionally receives the separate 「最近活跃」 slice below;
1045
1135
  // `items` stays exactly the unread queue for every reader, so the badge
1046
1136
  // totals and the agent-facing `team_inbox` result cannot drift with it.
1047
- const taskNumbers = this.taskNumbers(request.workspaceId);
1137
+ const taskNumbers = new Map(workspaceIds.flatMap(workspaceId => [...this.taskNumbers(workspaceId)]));
1048
1138
  const items = [];
1049
1139
  const unreadThreads = new Set();
1050
1140
  for (const threadRef of this.inboxCandidateThreads(authorized.memberId)) {
@@ -1054,8 +1144,14 @@ export class AgentTeamLedger {
1054
1144
  const channelRef = this.channelRefForThread(thread.threadRef);
1055
1145
  if (channelRef === undefined)
1056
1146
  continue;
1057
- if (this.state.channels.get(channelRef)?.workspaceId !== request.workspaceId)
1147
+ const channel = this.state.channels.get(channelRef);
1148
+ if (channel === undefined || !workspaceIds.includes(channel.workspaceId))
1149
+ continue;
1150
+ // Archived Channels do not exist on Team API surfaces: their Threads
1151
+ // reach neither Inbox slice, matching ref resolution and the feed.
1152
+ if (channel.state === 'archived')
1058
1153
  continue;
1154
+ const workspaceId = channel.workspaceId;
1059
1155
  if (authorized.kind === 'member' && !this.isChannelMember(channelRef, authorized.memberId))
1060
1156
  continue;
1061
1157
  const unread = this.unreadFor(authorized.memberId, thread.threadRef);
@@ -1070,7 +1166,7 @@ export class AgentTeamLedger {
1070
1166
  // observe a different commit between the two reads.
1071
1167
  const newest = unread.at(-1).fact;
1072
1168
  const taskNumber = task === undefined ? undefined : taskNumbers.get(task.taskRef);
1073
- items.push(Object.freeze({ channelRef,
1169
+ items.push(Object.freeze({ workspaceId, channelRef,
1074
1170
  channelName: this.state.channels.get(channelRef)?.name ?? '',
1075
1171
  ...(task === undefined ? {} : { task }), ...(taskNumber === undefined ? {} : { taskNumber }), thread,
1076
1172
  unreadCount: unread.length, directCount,
@@ -1082,7 +1178,7 @@ export class AgentTeamLedger {
1082
1178
  items.sort((left, right) => right.directCount - left.directCount || right.newestSequence - left.newestSequence || left.thread.threadRef.localeCompare(right.thread.threadRef));
1083
1179
  const selected = items.slice(0, limit);
1084
1180
  const recent = authorized.kind === 'human'
1085
- ? this.recentInboxItems(authorized.memberId, request.workspaceId, unreadThreads, taskNumbers)
1181
+ ? this.recentInboxItems(authorized.memberId, workspaceIds[0], unreadThreads, taskNumbers)
1086
1182
  : Object.freeze([]);
1087
1183
  return Object.freeze({ items: Object.freeze(selected), recent,
1088
1184
  totalUnreadCount: items.reduce((sum, item) => sum + item.unreadCount, 0),
@@ -1147,7 +1243,12 @@ export class AgentTeamLedger {
1147
1243
  const channelRef = this.channelRefForThread(thread.threadRef);
1148
1244
  if (channelRef === undefined)
1149
1245
  continue;
1150
- if (this.state.channels.get(channelRef)?.workspaceId !== workspaceId)
1246
+ const channel = this.state.channels.get(channelRef);
1247
+ if (channel === undefined || channel.workspaceId !== workspaceId)
1248
+ continue;
1249
+ // Archived Channels do not exist on Team API surfaces, so their Threads
1250
+ // are no way back into work either — participation ends at archival.
1251
+ if (channel.state === 'archived')
1151
1252
  continue;
1152
1253
  const facts = this.state.factsByThread.get(thread.threadRef) ?? [];
1153
1254
  const newest = facts.at(-1);
@@ -1155,7 +1256,7 @@ export class AgentTeamLedger {
1155
1256
  continue;
1156
1257
  const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
1157
1258
  const taskNumber = task === undefined ? undefined : taskNumbers.get(task.taskRef);
1158
- recent.push(Object.freeze({ channelRef,
1259
+ recent.push(Object.freeze({ workspaceId, channelRef,
1159
1260
  channelName: this.state.channels.get(channelRef)?.name ?? '',
1160
1261
  ...(task === undefined ? {} : { task }), ...(taskNumber === undefined ? {} : { taskNumber }), thread,
1161
1262
  unreadCount: 0, directCount: 0,
@@ -1183,9 +1284,10 @@ export class AgentTeamLedger {
1183
1284
  /** Model-visible notification material derived from the recipient's current durable unread state. */
1184
1285
  notificationFacts(memberId, request) {
1185
1286
  const member = this.requireMember(memberId);
1186
- if (member.workspaceId !== request.workspaceId)
1287
+ if (request !== undefined && !this.participatesIn(memberId, request.workspaceId))
1187
1288
  throw new Error('Member cannot inspect another Workspace');
1188
- const inbox = this.inbox({ kind: 'member', memberId, handle: member.handle }, request);
1289
+ const actor = { kind: 'member', memberId, handle: member.handle };
1290
+ const inbox = request === undefined ? this.memberInbox(actor, {}) : this.inbox(actor, request);
1189
1291
  return Object.freeze(inbox.items.map(item => Object.freeze({ item,
1190
1292
  facts: this.unreadFor(memberId, item.thread.threadRef) })));
1191
1293
  }
@@ -1307,7 +1409,7 @@ export class AgentTeamLedger {
1307
1409
  throw new Error('cursor must be a non-negative integer sequence');
1308
1410
  if (memberId !== undefined) {
1309
1411
  const member = this.requireMember(memberId);
1310
- if (member.workspaceId !== request.workspaceId)
1412
+ if (!this.participatesIn(member.memberId, request.workspaceId))
1311
1413
  throw new Error('Member cannot view another Workspace');
1312
1414
  }
1313
1415
  if (request.channelRef !== undefined) {
@@ -1390,6 +1492,7 @@ export class AgentTeamLedger {
1390
1492
  const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
1391
1493
  const facts = this.state.factsByThread.get(thread.threadRef) ?? [];
1392
1494
  return Object.freeze({ message, mentions: fact.mentions, ...(task === undefined ? {} : { task, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }), thread,
1495
+ claimOwners: this.liveClaimOwners(task),
1393
1496
  messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0,
1394
1497
  lastActivityAt: facts.at(-1)?.occurredAt ?? '' });
1395
1498
  });
@@ -1397,6 +1500,8 @@ export class AgentTeamLedger {
1397
1500
  const nextCursor = selected.length === 0 ? cursor : direction === 'before' ? selected[0].sequence : selected.at(-1).sequence;
1398
1501
  return Object.freeze({
1399
1502
  humanMemberId: initialization.data.humanMemberId,
1503
+ workspaces: Object.freeze(memberId === undefined ? [] : this.workspacesOfFrom(this.state, memberId)
1504
+ .map(workspaceId => Object.freeze({ workspaceId, default: workspaceId === this.state.members.get(memberId)?.workspaceId }))),
1400
1505
  channels: Object.freeze(channels),
1401
1506
  members: Object.freeze([...this.state.memberships.entries()].filter(([channelRef]) => channelRefs.has(channelRef)).flatMap(([channelRef, ids]) => [...ids].filter(id => {
1402
1507
  const state = this.state.members.get(id)?.state;
@@ -1475,6 +1580,10 @@ export class AgentTeamLedger {
1475
1580
  }
1476
1581
  return scopes;
1477
1582
  }
1583
+ /** One workspace change scope per Workspace the Member participates in — member-level commits wake every panel that lists it. */
1584
+ memberWorkspaceScopes(memberId) {
1585
+ return [...(this.state.participations.get(memberId) ?? [])].map(workspaceId => ({ kind: 'workspace', workspaceId }));
1586
+ }
1478
1587
  /**
1479
1588
  * Durable position of the newest shared-projection commit: the version every
1480
1589
  * change waiter outside the presence scope observes. It only moves when a
@@ -1503,9 +1612,13 @@ export class AgentTeamLedger {
1503
1612
  case 'team/member-session-rolled-over':
1504
1613
  case 'team/member-updated':
1505
1614
  case 'team/member-removed':
1506
- return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
1615
+ return this.memberWorkspaceScopes(operation.data.member.memberId);
1507
1616
  case 'team/member-archived':
1508
- return this.withReleasedActivityScopes([{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }], operation.data.tasks, operation.data.activities);
1617
+ return this.withReleasedActivityScopes(this.memberWorkspaceScopes(operation.data.member.memberId), operation.data.tasks, operation.data.activities);
1618
+ case 'team/member-workspace-joined':
1619
+ return [{ kind: 'workspace', workspaceId: operation.data.workspaceId }];
1620
+ case 'team/member-workspace-left':
1621
+ return this.withReleasedActivityScopes([{ kind: 'workspace', workspaceId: operation.data.workspaceId }], operation.data.tasks, operation.data.activities);
1509
1622
  case 'team/channel-member-added':
1510
1623
  return [{ kind: 'workspace', workspaceId: operation.data.workspaceId }, { kind: 'channel', channelRef: operation.data.channelRef }];
1511
1624
  case 'team/channel-member-removed':
@@ -1568,6 +1681,8 @@ export class AgentTeamLedger {
1568
1681
  for (const follower of this.state.attentionByThread.get(threadRef) ?? [])
1569
1682
  members.add(follower);
1570
1683
  }
1684
+ if (operation.kind === 'team/member-workspace-joined' || operation.kind === 'team/member-workspace-left')
1685
+ members.add(operation.data.memberId);
1571
1686
  if (operation.actor.kind === 'member')
1572
1687
  members.add(operation.actor.memberId);
1573
1688
  return [...members];
@@ -1589,11 +1704,13 @@ export class AgentTeamLedger {
1589
1704
  case 'team/channel-archived':
1590
1705
  case 'team/member-removed':
1591
1706
  case 'team/member-archived':
1707
+ case 'team/member-workspace-left':
1592
1708
  return operation.data.activities.map(activity => activity.threadRef);
1593
1709
  case 'team/initialized':
1594
1710
  case 'team/channel-created':
1595
1711
  case 'team/channel-updated':
1596
1712
  case 'team/member-added':
1713
+ case 'team/member-workspace-joined':
1597
1714
  case 'team/member-suspended':
1598
1715
  case 'team/member-resumed':
1599
1716
  case 'team/member-session-restarted':
@@ -1675,7 +1792,7 @@ export class AgentTeamLedger {
1675
1792
  throw new Error('invalid initial Channel members');
1676
1793
  for (const memberId of memberIds) {
1677
1794
  const member = projection.members.get(memberId);
1678
- if (member === undefined || member.workspaceId !== channel.workspaceId || member.state !== 'enabled')
1795
+ if (member === undefined || !this.participatesInFrom(projection, member.memberId, channel.workspaceId) || member.state !== 'enabled')
1679
1796
  throw new Error('invalid initial Channel Member');
1680
1797
  }
1681
1798
  return;
@@ -1773,10 +1890,10 @@ export class AgentTeamLedger {
1773
1890
  || operation.data.member.presetId !== prior.presetId
1774
1891
  || operation.data.member.privateMemoryPath !== prior.privateMemoryPath)
1775
1892
  throw new Error('invalid Member update');
1776
- // The renamed handle must stay unique among the workspace's other live Members.
1893
+ // The renamed handle must stay unique among the live Members sharing any Workspace participation.
1777
1894
  const normalized = operation.data.member.handle.normalize('NFKC').trim().toLowerCase();
1778
1895
  for (const other of projection.members.values()) {
1779
- if (other.memberId !== prior.memberId && other.state !== 'inactive' && other.workspaceId === prior.workspaceId
1896
+ if (other.memberId !== prior.memberId && other.state !== 'inactive' && this.participationOverlapFrom(projection, other.memberId, prior.memberId)
1780
1897
  && other.handle.normalize('NFKC').trim().toLowerCase() === normalized)
1781
1898
  throw new Error('invalid Member update handle');
1782
1899
  }
@@ -1786,7 +1903,7 @@ export class AgentTeamLedger {
1786
1903
  assertHuman();
1787
1904
  const channel = projection.channels.get(operation.data.channelRef);
1788
1905
  const member = projection.members.get(operation.data.memberId);
1789
- if (channel === undefined || member === undefined || member.workspaceId !== channel.workspaceId || operation.data.workspaceId !== channel.workspaceId || projection.memberships.get(channel.channelRef)?.has(member.memberId))
1906
+ if (channel === undefined || member === undefined || !this.participatesInFrom(projection, member.memberId, channel.workspaceId) || operation.data.workspaceId !== channel.workspaceId || projection.memberships.get(channel.channelRef)?.has(member.memberId))
1790
1907
  throw new Error('invalid Channel membership');
1791
1908
  return;
1792
1909
  }
@@ -1795,12 +1912,32 @@ export class AgentTeamLedger {
1795
1912
  const channel = projection.channels.get(operation.data.channelRef);
1796
1913
  const member = projection.members.get(operation.data.memberId);
1797
1914
  if (channel === undefined || member === undefined || operation.data.workspaceId !== channel.workspaceId
1798
- || member.workspaceId !== channel.workspaceId || !projection.memberships.get(channel.channelRef)?.has(member.memberId))
1915
+ || !this.participatesInFrom(projection, member.memberId, channel.workspaceId) || !projection.memberships.get(channel.channelRef)?.has(member.memberId))
1799
1916
  throw new Error('invalid Channel membership removal');
1800
1917
  const threadRefs = new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === channel.channelRef));
1801
1918
  this.validateReleaseCleanup(operation.data, projection, member.memberId, threadRefs, operation.sequence, refs);
1802
1919
  return;
1803
1920
  }
1921
+ if (operation.kind === 'team/member-workspace-joined') {
1922
+ assertHuman();
1923
+ const member = projection.members.get(operation.data.memberId);
1924
+ if (member === undefined || member.state === 'inactive' || member.state === 'archived'
1925
+ || this.participatesInFrom(projection, member.memberId, operation.data.workspaceId))
1926
+ throw new Error('invalid Workspace join');
1927
+ this.assertHandleAvailableFrom(projection, operation.data.workspaceId, member.handle);
1928
+ return;
1929
+ }
1930
+ if (operation.kind === 'team/member-workspace-left') {
1931
+ assertHuman();
1932
+ const member = projection.members.get(operation.data.memberId);
1933
+ if (member === undefined || member.state === 'inactive' || member.state === 'archived'
1934
+ || operation.data.workspaceId === member.workspaceId
1935
+ || !this.participatesInFrom(projection, member.memberId, operation.data.workspaceId))
1936
+ throw new Error('invalid Workspace leave');
1937
+ const threadRefs = this.workspaceThreadRefsFrom(projection, operation.data.workspaceId);
1938
+ this.validateReleaseCleanup(operation.data, projection, member.memberId, threadRefs, operation.sequence, refs);
1939
+ return;
1940
+ }
1804
1941
  if (operation.kind === 'team/channel-archived') {
1805
1942
  assertHuman();
1806
1943
  const prior = projection.channels.get(operation.data.channel.channelRef);
@@ -2062,7 +2199,7 @@ export class AgentTeamLedger {
2062
2199
  if (operation.kind === 'team/dm-sent') {
2063
2200
  const sender = assertMember();
2064
2201
  const recipient = projection.members.get(operation.data.recipientMemberId);
2065
- if (recipient === undefined || recipient.workspaceId !== operation.data.workspaceId
2202
+ if (recipient === undefined || !this.participatesInFrom(projection, recipient.memberId, operation.data.workspaceId)
2066
2203
  || recipient.state !== 'enabled' || recipient.memberId === AGENT_TEAM_HUMAN_MEMBER_ID
2067
2204
  || operation.data.senderMemberId !== sender.memberId
2068
2205
  || operation.data.recipientMemberId === sender.memberId
@@ -2253,8 +2390,9 @@ export class AgentTeamLedger {
2253
2390
  if (memberId === AGENT_TEAM_HUMAN_MEMBER_ID)
2254
2391
  return true;
2255
2392
  const member = projection.members.get(memberId);
2393
+ const channelWorkspaceId = projection.channels.get(channelRef)?.workspaceId;
2256
2394
  return member !== undefined && member.state !== 'inactive' && member.state !== 'archived'
2257
- && projection.channels.get(channelRef)?.workspaceId === member.workspaceId
2395
+ && channelWorkspaceId !== undefined && this.participatesInFrom(projection, memberId, channelWorkspaceId)
2258
2396
  && this.isChannelMemberFrom(projection, channelRef, memberId);
2259
2397
  }
2260
2398
  apply(operation) {
@@ -2279,6 +2417,9 @@ export class AgentTeamLedger {
2279
2417
  }
2280
2418
  if (operation.kind === 'team/member-added') {
2281
2419
  target.members.set(operation.data.member.memberId, operation.data.member);
2420
+ // Creation seeds the first Workspace participation; member.workspaceId
2421
+ // stays the default Workspace the Session roots in, immutable after add.
2422
+ target.participations.set(operation.data.member.memberId, new Set([operation.data.member.workspaceId]));
2282
2423
  for (const channelRef of operation.data.channelRefs)
2283
2424
  this.addMembership(target, channelRef, operation.data.member.memberId);
2284
2425
  return;
@@ -2337,6 +2478,23 @@ export class AgentTeamLedger {
2337
2478
  this.applyReleaseSnapshot(target, operation, operation.data, operation.occurredAt);
2338
2479
  return;
2339
2480
  }
2481
+ if (operation.kind === 'team/member-workspace-joined') {
2482
+ const workspaces = target.participations.get(operation.data.memberId) ?? new Set();
2483
+ workspaces.add(operation.data.workspaceId);
2484
+ target.participations.set(operation.data.memberId, workspaces);
2485
+ return;
2486
+ }
2487
+ if (operation.kind === 'team/member-workspace-left') {
2488
+ target.participations.get(operation.data.memberId)?.delete(operation.data.workspaceId);
2489
+ // Departure ends Channel memberships inside the left Workspace — unlike
2490
+ // archival, which keeps them hidden for a possible restore.
2491
+ for (const channel of target.channels.values()) {
2492
+ if (channel.workspaceId === operation.data.workspaceId)
2493
+ target.memberships.get(channel.channelRef)?.delete(operation.data.memberId);
2494
+ }
2495
+ this.applyReleaseSnapshot(target, operation, operation.data, operation.occurredAt);
2496
+ return;
2497
+ }
2340
2498
  if (operation.kind === 'team/channel-archived') {
2341
2499
  // Archival keeps Memberships: hidden state, not departure — a future
2342
2500
  // restore returns every Member to the Channel. Visibility filters by
@@ -2898,7 +3056,7 @@ export class AgentTeamLedger {
2898
3056
  if (memberId === AGENT_TEAM_HUMAN_MEMBER_ID)
2899
3057
  continue;
2900
3058
  const member = this.requireMember(memberId);
2901
- if (member.state === 'inactive' || member.state === 'archived' || member.workspaceId !== channel.workspaceId || !this.isChannelMember(channel.channelRef, memberId)) {
3059
+ if (member.state === 'inactive' || member.state === 'archived' || !this.participatesIn(member.memberId, channel.workspaceId) || !this.isChannelMember(channel.channelRef, memberId)) {
2902
3060
  throw new Error(`Agent Member '${memberId}' is not authorized for Channel '${channel.channelRef}'`);
2903
3061
  }
2904
3062
  }
@@ -3043,7 +3201,7 @@ export class AgentTeamLedger {
3043
3201
  return actor;
3044
3202
  }
3045
3203
  const member = this.assertMemberActor(actor);
3046
- if (member.workspaceId !== workspaceId)
3204
+ if (!this.participatesIn(member.memberId, workspaceId))
3047
3205
  throw new Error('Member cannot mutate another Workspace');
3048
3206
  return actor;
3049
3207
  }
@@ -3220,10 +3378,50 @@ export class AgentTeamLedger {
3220
3378
  channelThreadRefsFrom(projection, channelRef) {
3221
3379
  return new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === channelRef));
3222
3380
  }
3381
+ /** The Workspace authorization question: does this Member participate in this Workspace. */
3382
+ participatesIn(memberId, workspaceId) {
3383
+ return this.participatesInFrom(this.state, memberId, workspaceId);
3384
+ }
3385
+ participatesInFrom(projection, memberId, workspaceId) {
3386
+ return projection.participations.get(memberId)?.has(workspaceId) === true;
3387
+ }
3388
+ /** Every Workspace the Member participates in: the default first, then the rest sorted. */
3389
+ workspacesOf(memberId) {
3390
+ return Object.freeze(this.workspacesOfFrom(this.state, memberId));
3391
+ }
3392
+ workspacesOfFrom(projection, memberId) {
3393
+ const member = projection.members.get(memberId);
3394
+ if (member === undefined)
3395
+ return [];
3396
+ const rest = [...(projection.participations.get(memberId) ?? new Set())]
3397
+ .filter(workspaceId => workspaceId !== member.workspaceId).sort();
3398
+ return [member.workspaceId, ...rest];
3399
+ }
3400
+ /** Whether two Members share at least one Workspace participation — the handle-uniqueness scope. */
3401
+ participationOverlapFrom(projection, leftMemberId, rightMemberId) {
3402
+ const left = projection.participations.get(leftMemberId);
3403
+ const right = projection.participations.get(rightMemberId);
3404
+ if (left === undefined || right === undefined)
3405
+ return false;
3406
+ for (const workspaceId of left)
3407
+ if (right.has(workspaceId))
3408
+ return true;
3409
+ return false;
3410
+ }
3411
+ /** Every Thread in the Workspace — the scope a Workspace leave replays its cleanup against. */
3412
+ workspaceThreadRefs(workspaceId) {
3413
+ return this.workspaceThreadRefsFrom(this.state, workspaceId);
3414
+ }
3415
+ workspaceThreadRefsFrom(projection, workspaceId) {
3416
+ return new Set([...projection.threads.keys()].filter(threadRef => {
3417
+ const channelRef = this.channelRefForThreadFrom(projection, threadRef);
3418
+ return channelRef !== undefined && projection.channels.get(channelRef)?.workspaceId === workspaceId;
3419
+ }));
3420
+ }
3223
3421
  assertJoinableMember(workspaceId, memberId) {
3224
3422
  const member = this.requireMember(memberId);
3225
- if (member.workspaceId !== workspaceId)
3226
- throw new Error(`Agent Member '${memberId}' does not belong to Workspace '${workspaceId}'`);
3423
+ if (!this.participatesIn(memberId, workspaceId))
3424
+ throw new Error(`Agent Member '${memberId}' does not participate in Workspace '${workspaceId}'`);
3227
3425
  if (member.state !== 'enabled')
3228
3426
  throw new Error(`Agent Member '${memberId}' is ${member.state}; only enabled Members can join a Channel`);
3229
3427
  }
@@ -3305,9 +3503,12 @@ export class AgentTeamLedger {
3305
3503
  return direction.normalize('NFKC').trim().replace(/\s+/gu, ' ').toLowerCase();
3306
3504
  }
3307
3505
  assertHandleAvailable(workspaceId, handle, exceptMemberId) {
3506
+ this.assertHandleAvailableFrom(this.state, workspaceId, handle, exceptMemberId);
3507
+ }
3508
+ assertHandleAvailableFrom(projection, workspaceId, handle, exceptMemberId) {
3308
3509
  const normalized = handle.normalize('NFKC').trim().toLowerCase();
3309
- if ([...this.state.members.values()].some(member => member.memberId !== exceptMemberId && member.state !== 'inactive'
3310
- && member.workspaceId === workspaceId
3510
+ if ([...projection.members.values()].some(member => member.memberId !== exceptMemberId && member.state !== 'inactive'
3511
+ && this.participatesInFrom(projection, member.memberId, workspaceId)
3311
3512
  && member.handle.normalize('NFKC').trim().toLowerCase() === normalized)) {
3312
3513
  throw new Error(`Agent Member handle '${handle}' is already active in Workspace '${workspaceId}'`);
3313
3514
  }
@@ -3409,6 +3610,16 @@ export class AgentTeamLedger {
3409
3610
  || operation.data.workspaceId !== request.workspaceId || operation.data.channelRef !== request.channelRef || operation.data.memberId !== request.memberId)
3410
3611
  this.throwRequestCollision(request.requestId);
3411
3612
  }
3613
+ assertSameWorkspaceJoin(operation, request) {
3614
+ if (operation.kind !== 'team/member-workspace-joined' || !this.sameActor(operation.actor, request.actor)
3615
+ || operation.data.workspaceId !== request.workspaceId || operation.data.memberId !== request.memberId)
3616
+ this.throwRequestCollision(request.requestId);
3617
+ }
3618
+ assertSameWorkspaceLeave(operation, request) {
3619
+ if (operation.kind !== 'team/member-workspace-left' || !this.sameActor(operation.actor, request.actor)
3620
+ || operation.data.workspaceId !== request.workspaceId || operation.data.memberId !== request.memberId)
3621
+ this.throwRequestCollision(request.requestId);
3622
+ }
3412
3623
  assertSameChannelArchival(operation, request) {
3413
3624
  if (operation.kind !== 'team/channel-archived' || !this.sameActor(operation.actor, request.actor)
3414
3625
  || operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef)
@@ -3564,6 +3775,13 @@ export class AgentTeamLedger {
3564
3775
  return Object.freeze({ receipt: this.receipt(operation), channelRef: operation.data.channelRef, memberId: operation.data.memberId,
3565
3776
  releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
3566
3777
  }
3778
+ workspaceJoinResult(operation) {
3779
+ return Object.freeze({ receipt: this.receipt(operation), memberId: operation.data.memberId, workspaceId: operation.data.workspaceId });
3780
+ }
3781
+ workspaceLeaveResult(operation) {
3782
+ return Object.freeze({ receipt: this.receipt(operation), memberId: operation.data.memberId, workspaceId: operation.data.workspaceId,
3783
+ releasedClaims: operation.data.claims, removedAttention: operation.data.inbox.attention.removed });
3784
+ }
3567
3785
  channelArchivalResult(operation) {
3568
3786
  return Object.freeze({ receipt: this.receipt(operation), channel: operation.data.channel,
3569
3787
  releasedClaims: operation.data.claims });
@@ -1,4 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
2
3
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
3
4
  import { SessionLogOffset } from '@deepseek-ai/dsh-session';
4
5
  import { memberMemoryDirectoryPath } from "./member-runtime.js";
@@ -33,7 +34,8 @@ export function apply(ctx) {
33
34
  ? 'memory.md is absent; the private memory index is empty.'
34
35
  : 'memory.md is currently unreadable; do not use any earlier private memory context.');
35
36
  }
36
- const text = `${renderMemberIdentity(member)}\n\n${memory}`;
37
+ const workspaces = renderMemberWorkspaces(host.workspacesForAgent(agent));
38
+ const text = `${renderMemberIdentity(member)}\n\n${memory}${workspaces === '' ? '' : `\n\n${workspaces}`}`;
37
39
  const latestText = agent.session.surface.nodes.toReversed().flatMap(sequence => {
38
40
  // `nodes` are event identities (SessionSeq); snapshotEvents takes log
39
41
  // offsets. Re-entering through the validating constructor keeps the two
@@ -61,6 +63,21 @@ export function renderMemberIdentity(member) {
61
63
  ? `Team identity: you are @${member.handle}.`
62
64
  : `Team identity: you are @${member.handle} — ${member.description}`;
63
65
  }
66
+ /** A current address list, not a copy of instructions from another checkout. */
67
+ export function renderMemberWorkspaces(workspaces) {
68
+ if (workspaces.length <= 1)
69
+ return '';
70
+ return [
71
+ 'Team Workspace participation — this list replaces all earlier participation context.',
72
+ ...workspaces.map(workspace => `${workspace.workspaceId}${workspace.default ? ' (default; Session cwd)' : ''}: ${workspace.path === undefined
73
+ ? 'Workspace path unavailable; ask @human before filesystem work.'
74
+ : `${JSON.stringify(workspace.path)}; instructions: ${JSON.stringify(join(workspace.path, 'AGENTS.md'))}`}`),
75
+ 'Your single Session and cwd stay in the default Workspace. Joining another Workspace does not move them.',
76
+ 'For filesystem work in another Workspace, use absolute paths (or an explicit command cwd). Before working there, read its AGENTS.md and applicable directory instructions yourself; their contents are not injected here.',
77
+ 'Pass workspace explicitly to team_view, team_thread, team_message and team_claim. team_inbox is the cross-Workspace triage entry point.',
78
+ 'If an instruction is ambiguous about which Workspace it concerns, ask @human before acting. Participation does not itself join Channels.',
79
+ ].join('\n');
80
+ }
64
81
  /** The four private-memory paths plus the out-of-cwd warning; callers append their own sentence. */
65
82
  function memoryPathsBlock(privateMemoryPath) {
66
83
  return `Private memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd.`;
@@ -311,7 +311,8 @@ export class MemberRuntime {
311
311
  * moved home) holds a sanitized target in ITS tree while the record still names
312
312
  * the real one. Renaming across trees would take a Member's whole private
313
313
  * memory out of its home — silently, since only the next session notices an
314
- * empty index. Such a source is left exactly where it is.
314
+ * empty index. Such a source is left exactly where it is, and only that
315
+ * refusal is reported back: every other outcome needs nothing from the caller.
315
316
  */
316
317
  async function migrateLegacyMemoryDirectory(legacyPath, path) {
317
318
  let legacy;
@@ -320,7 +321,7 @@ async function migrateLegacyMemoryDirectory(legacyPath, path) {
320
321
  }
321
322
  catch (error) {
322
323
  if (error.code === 'ENOENT')
323
- return 'absent';
324
+ return undefined;
324
325
  throw error;
325
326
  }
326
327
  if (!legacy.isDirectory())
@@ -329,14 +330,14 @@ async function migrateLegacyMemoryDirectory(legacyPath, path) {
329
330
  return 'refused-foreign-home';
330
331
  try {
331
332
  await stat(path);
332
- return 'target-present'; // Sanitized directory already present: migration already done or a new install.
333
+ return undefined; // Sanitized directory already present: migration already done or a new install.
333
334
  }
334
335
  catch (error) {
335
336
  if (error.code !== 'ENOENT')
336
337
  throw error;
337
338
  }
338
339
  await rename(legacyPath, path);
339
- return 'migrated';
340
+ return undefined;
340
341
  }
341
342
  /**
342
343
  * Whether one absolute path names a Member directory inside THIS process's