@wowyuarm/dsh-agent-team 0.1.8 → 0.1.9

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 (66) hide show
  1. package/package.json +2 -1
  2. package/packages/agent-team/README.md +5 -3
  3. package/packages/agent-team/README.zh.md +5 -3
  4. package/packages/agent-team/lib/attachments.js +14 -4
  5. package/packages/agent-team/lib/context-management.js +379 -0
  6. package/packages/agent-team/lib/context-projection.js +574 -0
  7. package/packages/agent-team/lib/context-source.js +72 -0
  8. package/packages/agent-team/lib/index.js +1208 -62
  9. package/packages/agent-team/lib/ledger.js +222 -14
  10. package/packages/agent-team/lib/member-context.js +6 -2
  11. package/packages/agent-team/lib/member-runtime.js +159 -4
  12. package/packages/agent-team/lib/pressure-policy.js +200 -0
  13. package/packages/agent-team/lib/spec.js +18 -0
  14. package/packages/agent-team/lib/typert.host.js +122 -41
  15. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  16. package/packages/agent-team/lib/typert.remote-client.js +38 -25
  17. package/packages/agent-team/lib/types/attachments.d.ts +1 -1
  18. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  19. package/packages/agent-team/lib/types/context-management.d.ts +142 -0
  20. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
  21. package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
  22. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
  23. package/packages/agent-team/lib/types/context-source.d.ts +83 -0
  24. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
  25. package/packages/agent-team/lib/types/index.d.ts +275 -2
  26. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/ledger.d.ts +94 -6
  28. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
  31. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
  33. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
  34. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  35. package/packages/agent-team/lib/types/types/entities.d.ts +14 -1
  36. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  37. package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
  38. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  39. package/packages/agent-team/lib/types/types/requests-results.d.ts +52 -1
  40. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  41. package/packages/agent-team/preset/team-member/agent.cordis.yml +9 -1
  42. package/packages/client-agent-team/lib/client.js +314 -323
  43. package/packages/client-agent-team/lib/client.js.map +1 -1
  44. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
  45. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
  47. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  48. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  49. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  50. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  51. package/packages/client-agent-team/lib/types/client/index.js +34 -11
  52. package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -14
  53. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/locales.js +0 -14
  55. package/packages/client-agent-team/lib/types/client/slots.d.ts +5 -0
  56. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  57. package/packages/tool-agent-team/README.md +8 -5
  58. package/packages/tool-agent-team/README.zh.md +8 -5
  59. package/packages/tool-agent-team/lib/context-tools.js +179 -0
  60. package/packages/tool-agent-team/lib/index.js +135 -16
  61. package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
  62. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
  63. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  64. package/packages/agent-team/lib/auto-compaction.js +0 -201
  65. package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
  66. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +0 -1
@@ -17,7 +17,7 @@ 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
19
  orderedFacts: [], factsByThread: new Map(), channelRefByThread: new Map(), mentionsByMessage: new Map(), messageCountByThread: new Map(),
20
- attentionByThread: new Map() };
20
+ attentionByThread: new Map(), previousSessions: new Map(), rolloverSeeds: new Map() };
21
21
  }
22
22
  const EMPTY_PROGRESS_NUDGE_TARGETS = Object.freeze({
23
23
  progress: Object.freeze([]),
@@ -247,9 +247,137 @@ export class AgentTeamLedger {
247
247
  return this.committed(this.memberResult(operation));
248
248
  });
249
249
  }
250
+ /**
251
+ * Move one enabled Member onto its next context generation as itself. The
252
+ * actor is the calling Member — the Host executes the transition but is
253
+ * never the business actor — and the durable data records only the
254
+ * verifiable envelope (previous/new Session anchors, the successful handoff
255
+ * tool-result seq, checkpoint seed lineage, trigger). The private handoff
256
+ * prose stays in the Member's own Session log. An exact retry resolves to
257
+ * the recorded outcome; the same requestId with different data collides.
258
+ */
259
+ rolloverMemberSession(request) {
260
+ return this.enqueue(async () => {
261
+ const existing = this.state.byRequest.get(request.requestId);
262
+ if (existing !== undefined) {
263
+ this.assertSameMemberSessionRolledOver(existing, request);
264
+ return this.resolved(this.memberResult(existing));
265
+ }
266
+ if (request.actor.kind !== 'member')
267
+ throw new Error('Member session rollover requires Member authority');
268
+ const prior = this.requireMember(request.memberId);
269
+ if (request.actor.memberId !== request.memberId)
270
+ throw new Error(`Agent Member '${prior.handle}' cannot roll over another Member's Session`);
271
+ if (prior.state !== 'enabled')
272
+ throw new Error(`Agent Member '${prior.handle}' is ${prior.state}; only enabled Members can roll over their Session`);
273
+ if (prior.sessionId === request.newSessionId)
274
+ throw new Error(`Agent Member '${prior.handle}' already runs Session '${request.newSessionId}'`);
275
+ if (prior.sessionId !== request.previousSessionId)
276
+ throw new Error(`Agent Member '${prior.handle}' is no longer bound to Session '${request.previousSessionId}'`);
277
+ if ((request.checkpointRef !== undefined) !== (request.sourceSessionId !== undefined))
278
+ throw new Error('rollover checkpoint fields must appear together');
279
+ if (request.sourceSessionId !== undefined && request.sourceThroughSeq === undefined)
280
+ throw new Error('rollover checkpoint seed requires a through sequence');
281
+ if (request.sourceThroughSeq !== undefined && request.sourceSessionId === undefined)
282
+ throw new Error('rollover through sequence requires a source Session');
283
+ const member = Object.freeze({ ...prior, sessionId: request.newSessionId });
284
+ const operation = Object.freeze({
285
+ ...this.operationBase(request, this.nextSequence()), kind: 'team/member-session-rolled-over',
286
+ data: Object.freeze({
287
+ member,
288
+ previousSessionId: request.previousSessionId,
289
+ newSessionId: request.newSessionId,
290
+ handoffEventSeq: request.handoffEventSeq,
291
+ trigger: request.trigger,
292
+ ...(request.sourceSessionId === undefined ? {} : { sourceSessionId: request.sourceSessionId }),
293
+ ...(request.sourceThroughSeq === undefined ? {} : { sourceThroughSeq: request.sourceThroughSeq }),
294
+ ...(request.checkpointRef === undefined ? {} : { checkpointRef: request.checkpointRef }),
295
+ }),
296
+ });
297
+ await this.table.put(operation.operationId, operation);
298
+ this.apply(operation);
299
+ return this.committed(this.memberResult(operation));
300
+ });
301
+ }
250
302
  getMember(memberId) {
251
303
  return this.state.members.get(memberId);
252
304
  }
305
+ /**
306
+ * The retired Session this Member most recently left through a renewal or
307
+ * rollover — the lineage parent a crash between the durable binding commit
308
+ * and the new Session's activation must reconstruct a handoff from. Read
309
+ * from the audit projection; the Member record itself only names the
310
+ * current Session.
311
+ */
312
+ previousSessionForMember(memberId) {
313
+ return this.state.previousSessions.get(memberId);
314
+ }
315
+ /**
316
+ * The latest session transition this Member committed: the retired Session
317
+ * and the target it moved onto, through either a renewal or a rollover.
318
+ * Crash recovery binds carried-input redelivery to the recorded target —
319
+ * the CURRENT Session being that target proves the old generation's
320
+ * unconsumed input still belongs here, regardless of whether the handoff
321
+ * itself already landed.
322
+ */
323
+ lastTransitionForMember(memberId) {
324
+ const previousSessionId = this.state.previousSessions.get(memberId);
325
+ const member = this.state.members.get(memberId);
326
+ if (previousSessionId === undefined || member === undefined)
327
+ return undefined;
328
+ return { previousSessionId, targetSessionId: member.sessionId };
329
+ }
330
+ /**
331
+ * The Thread a Task overlay lives on, or undefined for an unknown or
332
+ * archived-Channel Task. Read-only input for attributing claim-mutation
333
+ * boundaries to the Thread whose context they entered.
334
+ */
335
+ threadForTask(taskRef) {
336
+ const task = this.state.tasks.get(taskRef);
337
+ if (task === undefined)
338
+ return undefined;
339
+ return this.state.channels.get(task.channelRef)?.state === 'archived' ? undefined : task.threadRef;
340
+ }
341
+ /**
342
+ * The seed envelope of this Member's most recent rollover, when that
343
+ * rollover was a checkpoint return: the recorded source Session, the
344
+ * exclusive end of its seeded prefix, and the checkpoint ref. A fresh
345
+ * rollover clears the envelope. Crash recovery reads this to rebuild the
346
+ * child generation from the recorded seed instead of an empty context.
347
+ */
348
+ rolloverSeedForMember(memberId, targetSessionId) {
349
+ const seed = this.state.rolloverSeeds.get(memberId);
350
+ // Bind the envelope to its recorded target: a later renewal or fresh
351
+ // rollover that reused or replaced the binding must never consume an
352
+ // older generation's seed.
353
+ return seed?.targetSessionId === targetSessionId ? seed : undefined;
354
+ }
355
+ /**
356
+ * Count one Member's active Claims on open Tasks in active Channels — the
357
+ * read-only input to the checkpoint single-Thread coverage guard: with
358
+ * more than one, a rewind cannot be proven to stay inside one Thread.
359
+ */
360
+ activeClaimCountForMember(memberId) {
361
+ return this.activeClaimsForMember(memberId).length;
362
+ }
363
+ /**
364
+ * One Member's active Claims on open Tasks in active Channels, oldest
365
+ * first — the read-only input for the pressure notice's Claim labels.
366
+ */
367
+ activeClaimsForMember(memberId) {
368
+ const active = [];
369
+ for (const claim of this.state.claims.values()) {
370
+ if (claim.owner !== memberId || claim.state !== 'active')
371
+ continue;
372
+ const task = this.state.tasks.get(claim.taskRef);
373
+ if (task === undefined || task.resolution !== 'open')
374
+ continue;
375
+ if (this.state.channels.get(task.channelRef)?.state === 'archived')
376
+ continue;
377
+ active.push(claim);
378
+ }
379
+ return active.sort((a, b) => a.claimRef.localeCompare(b.claimRef));
380
+ }
253
381
  /**
254
382
  * Current progress-nudge candidacy for one Member: threads whose Thread
255
383
  * progress reminder applies (active Claim, or following a taskless Thread)
@@ -666,6 +794,15 @@ export class AgentTeamLedger {
666
794
  : claim);
667
795
  const releasedClaims = claims.filter(claim => claim.state === 'released' && this.state.claims.get(claim.claimRef)?.state === 'active');
668
796
  const completedClaims = claims.filter(claim => claim.state === 'done' && this.state.claims.get(claim.claimRef)?.state === 'active');
797
+ // Every done Claim at commit time — pre-finished and atomically
798
+ // completed alike — is the acceptance discriminator: the one list
799
+ // accept markers deliver from, so a normal accept no longer stays
800
+ // silent about the Members whose work it accepted. Sorted lexically
801
+ // so replay validation compares stably regardless of claim-ref
802
+ // generation order.
803
+ const acceptedClaims = request.action === 'accept'
804
+ ? claims.filter(claim => claim.state === 'done').sort((left, right) => left.claimRef.localeCompare(right.claimRef))
805
+ : [];
669
806
  const resolution = request.action === 'reopen' ? 'open' : request.action === 'accept' ? 'accepted' : 'closed';
670
807
  const status = resolution === 'accepted' ? 'done' : resolution === 'closed' ? 'closed'
671
808
  : this.deriveTaskStatus(task.taskRef, claims);
@@ -674,12 +811,13 @@ export class AgentTeamLedger {
674
811
  const activity = Object.freeze({ activityRef: this.ref('activity'), kind: request.action,
675
812
  taskRef: task.taskRef, threadRef: task.threadRef, actor: request.actor.memberId, sequence,
676
813
  ...(releasedClaims.length === 0 ? {} : { releasedClaimRefs: Object.freeze(releasedClaims.map(claim => claim.claimRef)) }),
677
- ...(completedClaims.length === 0 ? {} : { completedClaimRefs: Object.freeze(completedClaims.map(claim => claim.claimRef)) }) });
814
+ ...(completedClaims.length === 0 ? {} : { completedClaimRefs: Object.freeze(completedClaims.map(claim => claim.claimRef)) }),
815
+ ...(request.action === 'accept' ? { acceptedClaimRefs: Object.freeze(acceptedClaims.map(claim => claim.claimRef)) } : {}) });
678
816
  const inbox = request.action === 'close'
679
817
  ? this.closeThreadInbox(activity, thread.threadRef)
680
818
  : request.action === 'reopen'
681
819
  ? this.reopenThreadInbox(activity, thread.threadRef)
682
- : this.acceptThreadInbox(activity, thread.threadRef, completedClaims.map(claim => claim.owner));
820
+ : this.acceptThreadInbox(activity, thread.threadRef, acceptedClaims.map(claim => claim.owner));
683
821
  const operation = Object.freeze({
684
822
  ...this.operationBase(request, sequence), kind: 'team/task-changed',
685
823
  data: Object.freeze({ workspaceId: request.workspaceId, baseRevision: request.baseRevision,
@@ -1155,6 +1293,7 @@ export class AgentTeamLedger {
1155
1293
  case 'team/member-session-restarted':
1156
1294
  case 'team/member-context-cleared':
1157
1295
  case 'team/member-session-renewed':
1296
+ case 'team/member-session-rolled-over':
1158
1297
  case 'team/member-updated':
1159
1298
  case 'team/member-removed':
1160
1299
  return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
@@ -1275,6 +1414,7 @@ export class AgentTeamLedger {
1275
1414
  case 'team/member-session-restarted':
1276
1415
  case 'team/member-context-cleared':
1277
1416
  case 'team/member-session-renewed':
1417
+ case 'team/member-session-rolled-over':
1278
1418
  case 'team/member-updated':
1279
1419
  case 'team/channel-member-added':
1280
1420
  case 'team/thread-read':
@@ -1407,6 +1547,29 @@ export class AgentTeamLedger {
1407
1547
  throw new Error('invalid Member session renewal');
1408
1548
  return;
1409
1549
  }
1550
+ if (operation.kind === 'team/member-session-rolled-over') {
1551
+ // The rollover's business actor is the calling Member itself; the Host
1552
+ // only executes the recorded transition. Authorization is self-scoped:
1553
+ // the actor must be the enabled target Member on its currently bound
1554
+ // Session, and the recorded transition moves exactly the sessionId.
1555
+ const actorMember = assertMember();
1556
+ const prior = projection.members.get(operation.data.member.memberId);
1557
+ if (actorMember.memberId !== operation.data.member.memberId)
1558
+ throw new Error('rolled-over Member must match its actor');
1559
+ if (prior === undefined || prior.state !== 'enabled'
1560
+ || prior.sessionId !== operation.data.previousSessionId
1561
+ || operation.data.member.sessionId !== operation.data.newSessionId
1562
+ || operation.data.member.sessionId === operation.data.previousSessionId
1563
+ || !this.sameMemberFacts(prior, operation.data.member))
1564
+ throw new Error('invalid Member session rollover');
1565
+ if (operation.data.sourceSessionId !== undefined && operation.data.sourceThroughSeq === undefined)
1566
+ throw new Error('rolled-over checkpoint seed requires a through sequence');
1567
+ if (operation.data.sourceThroughSeq !== undefined && operation.data.sourceSessionId === undefined)
1568
+ throw new Error('rolled-over through sequence requires a source Session');
1569
+ if ((operation.data.checkpointRef !== undefined) !== (operation.data.sourceSessionId !== undefined))
1570
+ throw new Error('rolled-over checkpoint fields must appear together');
1571
+ return;
1572
+ }
1410
1573
  if (operation.kind === 'team/channel-updated') {
1411
1574
  assertHuman();
1412
1575
  const prior = projection.channels.get(operation.data.channel.channelRef);
@@ -1640,13 +1803,24 @@ export class AgentTeamLedger {
1640
1803
  const expectedCompletedClaimRefs = operation.data.claims
1641
1804
  .filter(claim => projection.claims.get(claim.claimRef)?.state === 'active' && claim.state === 'done')
1642
1805
  .map(claim => claim.claimRef);
1806
+ // New-shape accepts carry the acceptance discriminator: every done
1807
+ // Claim at commit time, sorted. Legacy accepts predate it; the
1808
+ // recorded wake semantics for them stay completed-only (the old rule)
1809
+ // so every existing ledger replays unchanged.
1810
+ const expectedAcceptedClaimRefs = operation.data.claims
1811
+ .filter(claim => claim.state === 'done')
1812
+ .map(claim => claim.claimRef)
1813
+ .sort((left, right) => left.localeCompare(right));
1643
1814
  if (task.resolution !== expectedResolution || task.status !== expectedStatus
1644
1815
  || (activity.kind !== 'close' && activity.releasedClaimRefs !== undefined)
1645
1816
  || (activity.kind !== 'accept' && activity.completedClaimRefs !== undefined)
1817
+ || (activity.kind !== 'accept' && activity.acceptedClaimRefs !== undefined)
1646
1818
  || (activity.kind === 'close' && !this.sameList(activity.releasedClaimRefs ?? [], operation.data.claims
1647
1819
  .filter(claim => projection.claims.get(claim.claimRef)?.state === 'active' && claim.state === 'released')
1648
1820
  .map(claim => claim.claimRef)))
1649
- || !this.sameList(activity.completedClaimRefs ?? [], expectedCompletedClaimRefs)) {
1821
+ || !this.sameList(activity.completedClaimRefs ?? [], expectedCompletedClaimRefs)
1822
+ || (activity.kind === 'accept' && activity.acceptedClaimRefs !== undefined
1823
+ && !this.sameList(activity.acceptedClaimRefs, expectedAcceptedClaimRefs))) {
1650
1824
  throw new Error('invalid Task state transition');
1651
1825
  }
1652
1826
  const priorClaims = [...projection.claims.values()].filter(claim => claim.taskRef === task.taskRef).sort((left, right) => left.claimRef.localeCompare(right.claimRef));
@@ -1670,7 +1844,10 @@ export class AgentTeamLedger {
1670
1844
  ? this.closeThreadInboxFrom(projection, activity, thread.threadRef)
1671
1845
  : activity.kind === 'reopen'
1672
1846
  ? this.reopenThreadInboxFrom(projection, activity, thread.threadRef)
1673
- : this.acceptThreadInboxFrom(projection, activity, thread.threadRef, expectedCompletedClaimRefs);
1847
+ // Presence of the discriminator picks the wake rule: new accepts
1848
+ // wake every done owner; legacy records keep the completed-only
1849
+ // delta exactly as recorded.
1850
+ : this.acceptThreadInboxFrom(projection, activity, thread.threadRef, activity.acceptedClaimRefs !== undefined ? activity.acceptedClaimRefs : expectedCompletedClaimRefs);
1674
1851
  if (!isDeepStrictEqual(operation.data.inbox, expectedInbox))
1675
1852
  throw new Error('invalid Task inbox projection');
1676
1853
  this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
@@ -1938,6 +2115,25 @@ export class AgentTeamLedger {
1938
2115
  }
1939
2116
  if (operation.kind === 'team/member-session-renewed') {
1940
2117
  target.members.set(operation.data.member.memberId, operation.data.member);
2118
+ target.previousSessions.set(operation.data.member.memberId, operation.data.previousSessionId);
2119
+ // A Human-side renewal is always a fresh start: a stale checkpoint
2120
+ // seed from an earlier rollover must never re-seed it.
2121
+ target.rolloverSeeds.delete(operation.data.member.memberId);
2122
+ return;
2123
+ }
2124
+ if (operation.kind === 'team/member-session-rolled-over') {
2125
+ target.members.set(operation.data.member.memberId, operation.data.member);
2126
+ target.previousSessions.set(operation.data.member.memberId, operation.data.previousSessionId);
2127
+ // A checkpoint return records its seed envelope: a crash between this
2128
+ // commit and the new Session's activation rebuilds the child from the
2129
+ // recorded source instead of an empty context.
2130
+ const { sourceSessionId, sourceThroughSeq, checkpointRef } = operation.data;
2131
+ if (sourceSessionId !== undefined && sourceThroughSeq !== undefined && checkpointRef !== undefined) {
2132
+ target.rolloverSeeds.set(operation.data.member.memberId, { targetSessionId: operation.data.newSessionId, sourceSessionId, sourceThroughSeq, checkpointRef });
2133
+ }
2134
+ else {
2135
+ target.rolloverSeeds.delete(operation.data.member.memberId);
2136
+ }
1941
2137
  return;
1942
2138
  }
1943
2139
  if (operation.kind === 'team/channel-updated') {
@@ -2216,22 +2412,24 @@ export class AgentTeamLedger {
2216
2412
  return this.inboxDelta([], removed, [], [...projection.directMarkers.values()].filter(marker => marker.threadRef === threadRef), activityMarkers, [...projection.activityMarkers.values()].filter(marker => marker.threadRef === threadRef));
2217
2413
  }
2218
2414
  /**
2219
- * Early acceptance completes open Claims inside the accept operation; their
2220
- * owners learn about it through activity markers regardless of whether they
2221
- * still follow the Thread. Plain accepts (no completed Claims) stay silent.
2415
+ * Acceptance notifies every done Claim owner pre-finished (the normal
2416
+ * flow) and atomically completed (early acceptance) alike through
2417
+ * activity markers regardless of whether they still follow the Thread.
2418
+ * The actor never notifies itself. Legacy records without the acceptance
2419
+ * discriminator keep the old completed-only wake semantics on replay.
2222
2420
  */
2223
- acceptThreadInbox(activity, threadRef, completedOwners) {
2224
- if (completedOwners.length === 0)
2421
+ acceptThreadInbox(activity, threadRef, acceptedOwners) {
2422
+ if (acceptedOwners.length === 0)
2225
2423
  return this.inboxDelta();
2226
- const recipients = [...new Set(completedOwners)].filter(memberId => memberId !== activity.actor);
2424
+ const recipients = [...new Set(acceptedOwners)].filter(memberId => memberId !== activity.actor);
2227
2425
  const markers = recipients.map(memberId => Object.freeze({ memberId, threadRef,
2228
2426
  activityRef: activity.activityRef, sequence: activity.sequence }));
2229
2427
  return this.inboxDelta([], [], [], [], markers);
2230
2428
  }
2231
- acceptThreadInboxFrom(projection, activity, threadRef, completedClaimRefs) {
2232
- if (completedClaimRefs.length === 0)
2429
+ acceptThreadInboxFrom(projection, activity, threadRef, claimRefs) {
2430
+ if (claimRefs.length === 0)
2233
2431
  return this.inboxDelta();
2234
- const owners = completedClaimRefs.map(claimRef => projection.claims.get(claimRef)?.owner).filter(owner => owner !== undefined);
2432
+ const owners = claimRefs.map(claimRef => projection.claims.get(claimRef)?.owner).filter(owner => owner !== undefined);
2235
2433
  return this.acceptThreadInbox(activity, threadRef, owners);
2236
2434
  }
2237
2435
  reopenThreadInbox(activity, threadRef) {
@@ -2762,6 +2960,16 @@ export class AgentTeamLedger {
2762
2960
  || operation.data.member.memberId !== request.memberId || operation.data.member.sessionId !== request.sessionId)
2763
2961
  this.throwRequestCollision(request.requestId);
2764
2962
  }
2963
+ assertSameMemberSessionRolledOver(operation, request) {
2964
+ if (operation.kind !== 'team/member-session-rolled-over' || !this.sameActor(operation.actor, request.actor)
2965
+ || operation.data.member.memberId !== request.memberId || operation.data.member.sessionId !== request.newSessionId
2966
+ || operation.data.previousSessionId !== request.previousSessionId || operation.data.handoffEventSeq !== request.handoffEventSeq
2967
+ || operation.data.trigger !== request.trigger
2968
+ || operation.data.sourceSessionId !== request.sourceSessionId
2969
+ || operation.data.sourceThroughSeq !== request.sourceThroughSeq
2970
+ || operation.data.checkpointRef !== request.checkpointRef)
2971
+ this.throwRequestCollision(request.requestId);
2972
+ }
2765
2973
  assertSameChannelUpdate(operation, request) {
2766
2974
  if (operation.kind !== 'team/channel-updated' || !this.sameActor(operation.actor, request.actor)
2767
2975
  || operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
3
3
  import { SessionLogOffset } from '@deepseek-ai/dsh-session';
4
+ import { memberMemoryDirectoryPath } from "./member-runtime.js";
4
5
  export const name = 'wowyuarm-agent-team-member-context';
5
6
  const MAX_MEMORY_BYTES = 8 * 1024;
6
7
  const BEGIN = '<team-member-private-memory>';
@@ -20,12 +21,15 @@ export function apply(ctx) {
20
21
  const member = host.memberForAgent(agent);
21
22
  if (member === undefined)
22
23
  return decision;
24
+ // The sanitized path is authoritative: activation migrated any legacy
25
+ // colon directory onto it before this member could run a step.
26
+ const memoryPath = memberMemoryDirectoryPath(member);
23
27
  let memory;
24
28
  try {
25
- memory = renderMemberMemory(await readFile(`${member.privateMemoryPath}/memory.md`), member.privateMemoryPath);
29
+ memory = renderMemberMemory(await readFile(`${memoryPath}/memory.md`), memoryPath);
26
30
  }
27
31
  catch (error) {
28
- memory = renderUnavailableMemory(member.privateMemoryPath, error.code === 'ENOENT'
32
+ memory = renderUnavailableMemory(memoryPath, error.code === 'ENOENT'
29
33
  ? 'memory.md is absent; the private memory index is empty.'
30
34
  : 'memory.md is currently unreadable; do not use any earlier private memory context.');
31
35
  }
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * @module @wowyuarm/dsh-agent-team/member-runtime
15
15
  */
16
- import { mkdir, rm, writeFile } from 'node:fs/promises';
16
+ import { mkdir, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
17
17
  import { dirname, join, resolve } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { scopeOf } from '@deepseek-ai/dsh-scope';
@@ -32,6 +32,9 @@ export const AGENT_TEAM_TOOL_NAMES = Object.freeze([
32
32
  'team_message',
33
33
  'team_claim',
34
34
  'team_view',
35
+ 'context_rollover',
36
+ 'context_checkpoint',
37
+ 'context_timeline',
35
38
  ]);
36
39
  /** Copy a Remote-supplied capability overlay into owned frozen storage. */
37
40
  export function deepCopyCapabilities(capabilities) {
@@ -41,6 +44,31 @@ export function deepCopyCapabilities(capabilities) {
41
44
  ...(capabilities.skills === undefined ? {} : { skills: copyAllow(capabilities.skills.allow) }),
42
45
  };
43
46
  }
47
+ /**
48
+ * Filesystem directory segment for one Member's private memory namespace.
49
+ * The `member:<uuid>` ref is a durable ledger identity and must never appear
50
+ * in a path: Windows rejects `:` in a path segment (NTFS parses it as an
51
+ * Alternate Data Stream separator), which made Member activation fail at its
52
+ * first `mkdir` on Windows (issue #7).
53
+ */
54
+ export function memberMemoryDirectoryName(memberId) {
55
+ return memberId.replaceAll(':', '-');
56
+ }
57
+ /** The sanitized absolute private-memory path for one Member, regardless of what the ledger recorded. */
58
+ export function memberMemoryDirectoryPath(member) {
59
+ // Sanitize per segment: only a segment that actually contains the ledger's
60
+ // member identity colon needs rewriting, and the recorded final segment is
61
+ // the only place it can appear. The drive-letter colon of a Windows
62
+ // absolute prefix is not a member identity and must survive untouched, so
63
+ // a colon-free final segment keeps the recorded path verbatim even when
64
+ // earlier segments carry colons.
65
+ const separator = Math.max(member.privateMemoryPath.lastIndexOf('/'), member.privateMemoryPath.lastIndexOf('\\'));
66
+ const finalSegment = separator === -1 ? member.privateMemoryPath : member.privateMemoryPath.slice(separator + 1);
67
+ if (!finalSegment.includes(':'))
68
+ return member.privateMemoryPath;
69
+ const prefix = separator === -1 ? '' : member.privateMemoryPath.slice(0, separator + 1);
70
+ return prefix + memberMemoryDirectoryName(member.memberId);
71
+ }
44
72
  /** Per-Member runtime state; the four maps dispose together with each Member. */
45
73
  export class MemberRuntime {
46
74
  deps;
@@ -165,10 +193,12 @@ export class MemberRuntime {
165
193
  * scope layer (the traceable-service seam, like the tool restriction):
166
194
  * bundled read-only core skills plus this Member's own private directory,
167
195
  * with the live selection ref that later capability edits swap in place.
196
+ * The sanitized directory is authoritative: activateMember migrated any
197
+ * legacy colon directory onto it before the provider mounted.
168
198
  */
169
199
  mountMemberSkillProvider(member, agentCtx, selection) {
170
200
  this.skillProviderDisposals.set(member.memberId, memberSkills.mountMemberSkillProvider(agentCtx, {
171
- skillsDirectory: join(member.privateMemoryPath, 'skills'),
201
+ skillsDirectory: join(memberMemoryDirectoryPath(member), 'skills'),
172
202
  bundledSkillsDirectory: BUNDLED_SKILLS_DIRECTORY,
173
203
  selection,
174
204
  }));
@@ -183,8 +213,29 @@ export class MemberRuntime {
183
213
  * first-run memory.md scaffold. The Member-private skills directory starts
184
214
  * empty; the per-Member provider scans exactly this root (default roots
185
215
  * excluded).
216
+ *
217
+ * Existing installs recorded the pre-fix colon directory in the ledger, and
218
+ * `privateMemoryPath` is a durable Member fact the renewal path cannot
219
+ * rewrite: when the legacy directory exists it is renamed onto the sanitized
220
+ * path once (same-parent rename, atomic), so existing private memory
221
+ * survives instead of being silently orphaned.
222
+ *
223
+ * A colon-form twin directory is also merged when it exists: a Member may
224
+ * have written files under the ledger identity spelling (the branded
225
+ * `member:<uuid>` ref is what every Team tool result shows, so a
226
+ * hand-assembled path carries the colon). Linux accepts the segment
227
+ * silently — two directories for one Member — while Windows would have
228
+ * failed the write outright. The twin is never a durable-Member-Fact
229
+ * candidate: only the files the Member actually wrote there are worth
230
+ * keeping, and `memory.md` cannot merge, so the sanitized copy always wins
231
+ * and twin-only notes/skills are moved in without overwriting.
186
232
  */
187
- async initializePrivateMemory(path) {
233
+ async initializePrivateMemory(path, legacyPath) {
234
+ if (legacyPath !== undefined && legacyPath !== path)
235
+ await migrateLegacyMemoryDirectory(legacyPath, path);
236
+ const twinPath = twinMemoryDirectoryPath(path);
237
+ if (twinPath !== undefined)
238
+ await mergeTwinMemoryDirectory(twinPath, path);
188
239
  await mkdir(join(path, 'notes'), { recursive: true });
189
240
  await mkdir(join(path, 'skills'), { recursive: true });
190
241
  try {
@@ -197,9 +248,14 @@ export class MemberRuntime {
197
248
  }
198
249
  /** Irreversibly remove one Member: archive its Session and delete its private namespace. */
199
250
  async cleanupRemovedMember(member) {
251
+ // The ledger path may still name the legacy colon directory (never
252
+ // activated after the fix): remove both spellings; rm is force-tolerant
253
+ // of the one that does not exist.
254
+ const sanitized = memberMemoryDirectoryPath(member);
200
255
  const results = await Promise.allSettled([
201
256
  this.deps.ctx.workspaceRegistry.archiveSession(member.sessionId),
202
- rm(member.privateMemoryPath, { recursive: true, force: true }),
257
+ rm(sanitized, { recursive: true, force: true }),
258
+ ...(sanitized === member.privateMemoryPath ? [] : [rm(member.privateMemoryPath, { recursive: true, force: true })]),
203
259
  ]);
204
260
  const failures = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
205
261
  if (failures.length > 0)
@@ -227,3 +283,102 @@ export class MemberRuntime {
227
283
  this.capabilityWarnings.clear();
228
284
  }
229
285
  }
286
+ /**
287
+ * One-time in-place migration of a pre-fix colon-named private memory
288
+ * directory onto its sanitized path. A sanitized target that already exists
289
+ * wins (idempotent across restarts and partially migrated installs); a legacy
290
+ * source that never existed is simply the fresh-install case.
291
+ */
292
+ async function migrateLegacyMemoryDirectory(legacyPath, path) {
293
+ let legacy;
294
+ try {
295
+ legacy = await stat(legacyPath);
296
+ }
297
+ catch (error) {
298
+ if (error.code === 'ENOENT')
299
+ return;
300
+ throw error;
301
+ }
302
+ if (!legacy.isDirectory())
303
+ throw new Error(`legacy Member memory path '${legacyPath}' exists but is not a directory`);
304
+ try {
305
+ await stat(path);
306
+ return; // Sanitized directory already present: migration already done or a new install.
307
+ }
308
+ catch (error) {
309
+ if (error.code !== 'ENOENT')
310
+ throw error;
311
+ }
312
+ await rename(legacyPath, path);
313
+ }
314
+ /** The colon-form twin path of one sanitized Member memory directory, when the segment form admits one. */
315
+ function twinMemoryDirectoryPath(sanitizedPath) {
316
+ const separator = Math.max(sanitizedPath.lastIndexOf('/'), sanitizedPath.lastIndexOf('\\'));
317
+ const prefix = separator === -1 ? '' : sanitizedPath.slice(0, separator + 1);
318
+ const finalSegment = separator === -1 ? sanitizedPath : sanitizedPath.slice(separator + 1);
319
+ // Only the exact sanitized identity segment (`member-<uuid>`) has a colon
320
+ // twin (`member:<uuid>`); any other segment shape has no identity mapping.
321
+ if (!/^member-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(finalSegment))
322
+ return undefined;
323
+ return `${prefix}member:${finalSegment.slice('member-'.length)}`;
324
+ }
325
+ /** The colon-twin trace name for one conflicting file: `note.md` → `note.colon-twin.md`. */
326
+ function colonTwinTraceName(name) {
327
+ const dot = name.lastIndexOf('.');
328
+ return dot <= 0 ? `${name}.colon-twin` : `${name.slice(0, dot)}.colon-twin${name.slice(dot)}`;
329
+ }
330
+ /**
331
+ * Merge a hand-created colon-form twin directory into the sanitized Member
332
+ * memory directory. Unlike the ledger legacy migration this is a merge, not a
333
+ * rename: the sanitized directory is the Member's live root (the injected
334
+ * paths and the skill provider point at it), so on a same-path conflict the
335
+ * live root's copy wins and the twin's losing copy is preserved beside it
336
+ * under a `.colon-twin` name instead of being silently discarded. Twin-only
337
+ * files move in under their own names; the emptied twin directory is then
338
+ * removed so the drift cannot silently recur.
339
+ */
340
+ async function mergeTwinMemoryDirectory(twinPath, path) {
341
+ let twin;
342
+ try {
343
+ twin = await stat(twinPath);
344
+ }
345
+ catch (error) {
346
+ if (error.code !== 'ENOENT')
347
+ throw error;
348
+ return;
349
+ }
350
+ if (!twin.isDirectory())
351
+ return;
352
+ await mergeDirectoryContents(twinPath, path);
353
+ await rm(twinPath, { recursive: true, force: true });
354
+ }
355
+ /** Recursively move every twin file into the live root; an existing live file wins, its twin copy traced beside it. */
356
+ async function mergeDirectoryContents(source, target) {
357
+ await mkdir(target, { recursive: true });
358
+ for (const entry of await readdir(source, { withFileTypes: true })) {
359
+ const sourcePath = join(source, entry.name);
360
+ const targetPath = join(target, entry.name);
361
+ if (entry.isDirectory()) {
362
+ await mergeDirectoryContents(sourcePath, targetPath);
363
+ }
364
+ else if (entry.isFile()) {
365
+ // rename(2) silently REPLACES an existing target on POSIX (the EEXIST
366
+ // error only exists on Windows, where the colon twin cannot exist at
367
+ // all), so a collision must be detected, not caught: probe the target
368
+ // first. Activation is the single serialized writer, so the
369
+ // probe-to-rename window has no concurrent writer to race.
370
+ let liveFilePresent = true;
371
+ try {
372
+ await stat(targetPath);
373
+ }
374
+ catch (error) {
375
+ if (error.code !== 'ENOENT')
376
+ throw error;
377
+ liveFilePresent = false;
378
+ }
379
+ // The live root stays authoritative; the twin's losing copy stays
380
+ // traceable beside it instead of being silently discarded.
381
+ await rename(sourcePath, liveFilePresent ? join(target, colonTwinTraceName(entry.name)) : targetPath);
382
+ }
383
+ }
384
+ }