@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.
- package/package.json +2 -1
- package/packages/agent-team/README.md +5 -3
- package/packages/agent-team/README.zh.md +5 -3
- package/packages/agent-team/lib/attachments.js +14 -4
- package/packages/agent-team/lib/context-management.js +379 -0
- package/packages/agent-team/lib/context-projection.js +574 -0
- package/packages/agent-team/lib/context-source.js +72 -0
- package/packages/agent-team/lib/index.js +1208 -62
- package/packages/agent-team/lib/ledger.js +222 -14
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-runtime.js +159 -4
- package/packages/agent-team/lib/pressure-policy.js +200 -0
- package/packages/agent-team/lib/spec.js +18 -0
- package/packages/agent-team/lib/typert.host.js +122 -41
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +38 -25
- package/packages/agent-team/lib/types/attachments.d.ts +1 -1
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-management.d.ts +142 -0
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-source.d.ts +83 -0
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +275 -2
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +94 -6
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/entities.d.ts +14 -1
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +52 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +9 -1
- package/packages/client-agent-team/lib/client.js +314 -323
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +34 -11
- package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -14
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +0 -14
- package/packages/client-agent-team/lib/types/client/slots.d.ts +5 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/tool-agent-team/README.md +8 -5
- package/packages/tool-agent-team/README.zh.md +8 -5
- package/packages/tool-agent-team/lib/context-tools.js +179 -0
- package/packages/tool-agent-team/lib/index.js +135 -16
- package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/auto-compaction.js +0 -201
- package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
- 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,
|
|
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
|
-
|
|
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
|
-
*
|
|
2220
|
-
*
|
|
2221
|
-
*
|
|
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,
|
|
2224
|
-
if (
|
|
2421
|
+
acceptThreadInbox(activity, threadRef, acceptedOwners) {
|
|
2422
|
+
if (acceptedOwners.length === 0)
|
|
2225
2423
|
return this.inboxDelta();
|
|
2226
|
-
const recipients = [...new Set(
|
|
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,
|
|
2232
|
-
if (
|
|
2429
|
+
acceptThreadInboxFrom(projection, activity, threadRef, claimRefs) {
|
|
2430
|
+
if (claimRefs.length === 0)
|
|
2233
2431
|
return this.inboxDelta();
|
|
2234
|
-
const owners =
|
|
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(`${
|
|
29
|
+
memory = renderMemberMemory(await readFile(`${memoryPath}/memory.md`), memoryPath);
|
|
26
30
|
}
|
|
27
31
|
catch (error) {
|
|
28
|
-
memory = renderUnavailableMemory(
|
|
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
|
|
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(
|
|
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
|
+
}
|