@wowyuarm/dsh-agent-team 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -11
- package/README.zh.md +4 -11
- package/package.json +53 -44
- package/packages/agent-team/README.md +2 -2
- package/packages/agent-team/README.zh.md +2 -1
- package/packages/agent-team/lib/context-management.js +8 -5
- package/packages/agent-team/lib/context-projection.js +12 -8
- package/packages/agent-team/lib/context-source.js +163 -23
- package/packages/agent-team/lib/index.js +174 -124
- package/packages/agent-team/lib/ledger.js +111 -30
- package/packages/agent-team/lib/member-time-context.js +126 -0
- package/packages/agent-team/lib/pressure-policy.js +1 -2
- package/packages/agent-team/lib/progress-nudge.js +12 -4
- package/packages/agent-team/lib/session-remediation.js +481 -0
- package/packages/agent-team/lib/spec.js +3 -2
- package/packages/agent-team/lib/time-format.js +56 -0
- package/packages/agent-team/lib/typert.host.js +82 -49
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +46 -25
- package/packages/agent-team/lib/types/context-management.d.ts +6 -1
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-source.d.ts +70 -36
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +32 -7
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +25 -0
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
- package/packages/agent-team/lib/types/session-remediation.d.ts +149 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/time-format.d.ts +28 -0
- package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
- package/packages/agent-team/lib/types/types/entities.d.ts +8 -0
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +6 -0
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +19 -4
- package/packages/client-agent-team/lib/client.js +80 -54
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +2 -2
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +4 -4
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +10 -5
- package/packages/client-agent-team/lib/types/client/slots.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/tool-agent-team/README.md +8 -6
- package/packages/tool-agent-team/README.zh.md +8 -6
- package/packages/tool-agent-team/lib/index.js +302 -110
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { formatTeamTimestamp } from "./time-format.js";
|
|
3
4
|
/** Stable Human Member identity shared by every replay of one dshHome Team. */
|
|
4
5
|
export const AGENT_TEAM_HUMAN_MEMBER_ID = 'member:human';
|
|
5
6
|
/** Idempotency identity of the one Host bootstrap operation. */
|
|
@@ -1009,7 +1010,7 @@ export class AgentTeamLedger {
|
|
|
1009
1010
|
// mirror: the reader sent it to the other Member → (you → them).
|
|
1010
1011
|
const direction = pair ? 'them → you' : 'you → them';
|
|
1011
1012
|
const truncated = operation.data.body.length > 160 ? `${operation.data.body.slice(0, 160)}…` : operation.data.body;
|
|
1012
|
-
return `(${direction}) ${truncated}`;
|
|
1013
|
+
return `(${direction}, at ${formatTeamTimestamp(operation.occurredAt)}) ${truncated}`;
|
|
1013
1014
|
}
|
|
1014
1015
|
return undefined;
|
|
1015
1016
|
}
|
|
@@ -1039,8 +1040,12 @@ export class AgentTeamLedger {
|
|
|
1039
1040
|
const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
|
|
1040
1041
|
const directCount = unread.filter(item => item.direct).length;
|
|
1041
1042
|
const attention = this.attentionFor(authorized.memberId, thread.threadRef);
|
|
1043
|
+
// Same snapshot, same source as newestSequence: the instant hangs off
|
|
1044
|
+
// the newest unread fact itself, never a second lookup that could
|
|
1045
|
+
// observe a different commit between the two reads.
|
|
1046
|
+
const newest = unread.at(-1).fact;
|
|
1042
1047
|
items.push(Object.freeze({ channelRef, ...(task === undefined ? {} : { task }), thread, unreadCount: unread.length, directCount,
|
|
1043
|
-
newestSequence:
|
|
1048
|
+
newestSequence: newest.sequence, newestOccurredAt: newest.occurredAt, ...(attention === undefined ? {} : { attention }) }));
|
|
1044
1049
|
}
|
|
1045
1050
|
items.sort((left, right) => right.directCount - left.directCount || right.newestSequence - left.newestSequence || left.thread.threadRef.localeCompare(right.thread.threadRef));
|
|
1046
1051
|
const selected = items.slice(0, limit);
|
|
@@ -1235,8 +1240,10 @@ export class AgentTeamLedger {
|
|
|
1235
1240
|
const message = fact.message;
|
|
1236
1241
|
const thread = this.requireThread(message.threadRef);
|
|
1237
1242
|
const task = thread.taskRef === undefined ? undefined : this.state.tasks.get(thread.taskRef);
|
|
1243
|
+
const facts = this.state.factsByThread.get(thread.threadRef) ?? [];
|
|
1238
1244
|
return Object.freeze({ message, mentions: fact.mentions, ...(task === undefined ? {} : { task, taskNumber: taskNumbers.get(task.taskRef) ?? 0 }), thread,
|
|
1239
|
-
messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0
|
|
1245
|
+
messageCount: this.state.messageCountByThread.get(thread.threadRef) ?? 0,
|
|
1246
|
+
lastActivityAt: facts.at(-1)?.occurredAt ?? '' });
|
|
1240
1247
|
});
|
|
1241
1248
|
const initialization = this.initialization();
|
|
1242
1249
|
const nextCursor = selected.length === 0 ? cursor : direction === 'before' ? selected[0].sequence : selected.at(-1).sequence;
|
|
@@ -2153,7 +2160,7 @@ export class AgentTeamLedger {
|
|
|
2153
2160
|
for (const claim of operation.data.claims)
|
|
2154
2161
|
target.claims.set(claim.claimRef, claim);
|
|
2155
2162
|
for (const activity of operation.data.activities)
|
|
2156
|
-
this.appendActivityFact(target, activity);
|
|
2163
|
+
this.appendActivityFact(target, activity, operation.occurredAt);
|
|
2157
2164
|
for (const task of operation.data.tasks)
|
|
2158
2165
|
target.tasks.set(task.taskRef, task);
|
|
2159
2166
|
for (const thread of operation.data.threads)
|
|
@@ -2169,7 +2176,7 @@ export class AgentTeamLedger {
|
|
|
2169
2176
|
for (const claim of operation.data.claims)
|
|
2170
2177
|
target.claims.set(claim.claimRef, claim);
|
|
2171
2178
|
for (const activity of operation.data.activities)
|
|
2172
|
-
this.appendActivityFact(target, activity);
|
|
2179
|
+
this.appendActivityFact(target, activity, operation.occurredAt);
|
|
2173
2180
|
for (const task of operation.data.tasks)
|
|
2174
2181
|
target.tasks.set(task.taskRef, task);
|
|
2175
2182
|
for (const thread of operation.data.threads)
|
|
@@ -2184,7 +2191,7 @@ export class AgentTeamLedger {
|
|
|
2184
2191
|
for (const claim of operation.data.claims)
|
|
2185
2192
|
target.claims.set(claim.claimRef, claim);
|
|
2186
2193
|
for (const activity of operation.data.activities)
|
|
2187
|
-
this.appendActivityFact(target, activity);
|
|
2194
|
+
this.appendActivityFact(target, activity, operation.occurredAt);
|
|
2188
2195
|
for (const task of operation.data.tasks)
|
|
2189
2196
|
target.tasks.set(task.taskRef, task);
|
|
2190
2197
|
for (const thread of operation.data.threads)
|
|
@@ -2200,7 +2207,7 @@ export class AgentTeamLedger {
|
|
|
2200
2207
|
for (const claim of operation.data.claims)
|
|
2201
2208
|
target.claims.set(claim.claimRef, claim);
|
|
2202
2209
|
for (const activity of operation.data.activities)
|
|
2203
|
-
this.appendActivityFact(target, activity);
|
|
2210
|
+
this.appendActivityFact(target, activity, operation.occurredAt);
|
|
2204
2211
|
for (const task of operation.data.tasks)
|
|
2205
2212
|
target.tasks.set(task.taskRef, task);
|
|
2206
2213
|
for (const thread of operation.data.threads)
|
|
@@ -2209,7 +2216,7 @@ export class AgentTeamLedger {
|
|
|
2209
2216
|
return;
|
|
2210
2217
|
}
|
|
2211
2218
|
if (operation.kind === 'team/thread-promoted') {
|
|
2212
|
-
this.appendActivityFact(target, operation.data.activity);
|
|
2219
|
+
this.appendActivityFact(target, operation.data.activity, operation.occurredAt);
|
|
2213
2220
|
target.tasks.set(operation.data.task.taskRef, operation.data.task);
|
|
2214
2221
|
target.threads.set(operation.data.thread.threadRef, operation.data.thread);
|
|
2215
2222
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
@@ -2219,7 +2226,7 @@ export class AgentTeamLedger {
|
|
|
2219
2226
|
const { message, mentions } = operation.data;
|
|
2220
2227
|
target.messages.push(message);
|
|
2221
2228
|
target.mentionsByMessage.set(message.messageRef, Object.freeze([...mentions]));
|
|
2222
|
-
this.appendMessageFact(target, message, mentions);
|
|
2229
|
+
this.appendMessageFact(target, message, mentions, message.occurredAt ?? operation.occurredAt);
|
|
2223
2230
|
if (operation.data.task !== undefined)
|
|
2224
2231
|
target.tasks.set(operation.data.task.taskRef, operation.data.task);
|
|
2225
2232
|
target.threads.set(operation.data.thread.threadRef, operation.data.thread);
|
|
@@ -2230,7 +2237,7 @@ export class AgentTeamLedger {
|
|
|
2230
2237
|
}
|
|
2231
2238
|
if (operation.kind === 'team/claim-created' || operation.kind === 'team/claim-done' || operation.kind === 'team/claim-released') {
|
|
2232
2239
|
target.claims.set(operation.data.claim.claimRef, operation.data.claim);
|
|
2233
|
-
this.appendActivityFact(target, operation.data.activity);
|
|
2240
|
+
this.appendActivityFact(target, operation.data.activity, operation.occurredAt);
|
|
2234
2241
|
target.tasks.set(operation.data.task.taskRef, operation.data.task);
|
|
2235
2242
|
target.threads.set(operation.data.thread.threadRef, operation.data.thread);
|
|
2236
2243
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
@@ -2239,7 +2246,7 @@ export class AgentTeamLedger {
|
|
|
2239
2246
|
if (operation.kind === 'team/task-changed') {
|
|
2240
2247
|
for (const claim of operation.data.claims)
|
|
2241
2248
|
target.claims.set(claim.claimRef, claim);
|
|
2242
|
-
this.appendActivityFact(target, operation.data.activity);
|
|
2249
|
+
this.appendActivityFact(target, operation.data.activity, operation.occurredAt);
|
|
2243
2250
|
target.tasks.set(operation.data.task.taskRef, operation.data.task);
|
|
2244
2251
|
target.threads.set(operation.data.thread.threadRef, operation.data.thread);
|
|
2245
2252
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
@@ -2257,16 +2264,16 @@ export class AgentTeamLedger {
|
|
|
2257
2264
|
assertUnhandledKind(operation);
|
|
2258
2265
|
}
|
|
2259
2266
|
/** Facts arrive in ledger sequence order, so global and per-thread lists stay sorted by append only. */
|
|
2260
|
-
appendMessageFact(target, message, mentions) {
|
|
2261
|
-
const fact = Object.freeze({ kind: 'message', sequence: message.sequence, message, mentions });
|
|
2267
|
+
appendMessageFact(target, message, mentions, occurredAt) {
|
|
2268
|
+
const fact = Object.freeze({ kind: 'message', sequence: message.sequence, message, mentions, occurredAt });
|
|
2262
2269
|
target.orderedFacts.push(fact);
|
|
2263
2270
|
const facts = target.factsByThread.get(message.threadRef) ?? [];
|
|
2264
2271
|
facts.push(fact);
|
|
2265
2272
|
target.factsByThread.set(message.threadRef, facts);
|
|
2266
2273
|
target.messageCountByThread.set(message.threadRef, (target.messageCountByThread.get(message.threadRef) ?? 0) + 1);
|
|
2267
2274
|
}
|
|
2268
|
-
appendActivityFact(target, activity) {
|
|
2269
|
-
const fact = Object.freeze({ kind: 'activity', sequence: activity.sequence, activity });
|
|
2275
|
+
appendActivityFact(target, activity, occurredAt) {
|
|
2276
|
+
const fact = Object.freeze({ kind: 'activity', sequence: activity.sequence, activity, occurredAt });
|
|
2270
2277
|
target.orderedFacts.push(fact);
|
|
2271
2278
|
const facts = target.factsByThread.get(activity.threadRef) ?? [];
|
|
2272
2279
|
facts.push(fact);
|
|
@@ -2318,7 +2325,8 @@ export class AgentTeamLedger {
|
|
|
2318
2325
|
const background = firstRead
|
|
2319
2326
|
? projection.messages.filter(message => message.threadRef === thread.threadRef && message.sequence < attention.startSequence)
|
|
2320
2327
|
.map(message => Object.freeze({ kind: 'message', sequence: message.sequence, message,
|
|
2321
|
-
mentions: projection.mentionsByMessage.get(message.messageRef) ?? []
|
|
2328
|
+
mentions: projection.mentionsByMessage.get(message.messageRef) ?? [],
|
|
2329
|
+
occurredAt: this.occurredAtForFactFrom(projection, message.sequence, message.occurredAt) }))
|
|
2322
2330
|
.filter(fact => !unreadFactKeys.has(this.threadFactKey(fact))).slice(-12)
|
|
2323
2331
|
: [];
|
|
2324
2332
|
const combined = [...background.map(fact => this.readFactFrom(projection, memberId, fact, false)), ...unreadFacts]
|
|
@@ -2393,6 +2401,23 @@ export class AgentTeamLedger {
|
|
|
2393
2401
|
threadFactsFrom(projection, threadRef) {
|
|
2394
2402
|
return projection.factsByThread.get(threadRef) ?? [];
|
|
2395
2403
|
}
|
|
2404
|
+
/**
|
|
2405
|
+
* The one per-fact instant projection: a fact's wall-clock time is the
|
|
2406
|
+
* occurredAt of the ledger operation that committed it, looked up by
|
|
2407
|
+
* sequence inside the same read snapshot. Every agent-facing surface
|
|
2408
|
+
* (Thread facts, notifications, DM history, mutation results) resolves
|
|
2409
|
+
* through this single path, never a second parallel lookup.
|
|
2410
|
+
*/
|
|
2411
|
+
occurredAtForFactFrom(projection, sequence, fallback) {
|
|
2412
|
+
if (fallback !== undefined)
|
|
2413
|
+
return fallback;
|
|
2414
|
+
// Facts arrive in ledger sequence order and `ordered` is append-only, so
|
|
2415
|
+
// the entry at index sequence - 1 is the committing operation. The index
|
|
2416
|
+
// read is guarded for hypothetical projections whose prefix has not yet
|
|
2417
|
+
// caught up; those callers always pass an explicit fallback instead.
|
|
2418
|
+
const operation = projection.ordered[sequence - 1];
|
|
2419
|
+
return operation?.occurredAt ?? '';
|
|
2420
|
+
}
|
|
2396
2421
|
messageInboxDelta(message, sender, mentions, attention) {
|
|
2397
2422
|
const markers = mentions.map(memberId => Object.freeze({ memberId, threadRef: message.threadRef,
|
|
2398
2423
|
messageRef: message.messageRef, sequence: message.sequence }));
|
|
@@ -2454,18 +2479,24 @@ export class AgentTeamLedger {
|
|
|
2454
2479
|
return this.inboxDelta([], [], [], [], markers);
|
|
2455
2480
|
}
|
|
2456
2481
|
removeMemberThreadInbox(memberId, threadRefs) {
|
|
2457
|
-
|
|
2482
|
+
return this.removeMemberThreadInboxFrom(this.state, memberId, threadRefs);
|
|
2483
|
+
}
|
|
2484
|
+
removeMemberThreadInboxFrom(projection, memberId, threadRefs) {
|
|
2485
|
+
const removed = [...projection.attention.values()].filter(attention => attention.memberId === memberId && threadRefs.has(attention.threadRef))
|
|
2458
2486
|
.map(attention => Object.freeze({ memberId, threadRef: attention.threadRef }));
|
|
2459
|
-
const markers = [...
|
|
2460
|
-
const activityMarkers = [...
|
|
2487
|
+
const markers = [...projection.directMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
|
|
2488
|
+
const activityMarkers = [...projection.activityMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
|
|
2461
2489
|
return this.inboxDelta([], removed, [], markers, [], activityMarkers);
|
|
2462
2490
|
}
|
|
2463
2491
|
/** Attention and marker cleanup for EVERY Member on the given Threads. */
|
|
2464
2492
|
channelArchivalInbox(threadRefs) {
|
|
2465
|
-
|
|
2493
|
+
return this.channelArchivalInboxFrom(this.state, threadRefs);
|
|
2494
|
+
}
|
|
2495
|
+
channelArchivalInboxFrom(projection, threadRefs) {
|
|
2496
|
+
const removed = [...projection.attention.values()].filter(attention => threadRefs.has(attention.threadRef))
|
|
2466
2497
|
.map(attention => Object.freeze({ memberId: attention.memberId, threadRef: attention.threadRef }));
|
|
2467
|
-
const markers = [...
|
|
2468
|
-
const activityMarkers = [...
|
|
2498
|
+
const markers = [...projection.directMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
|
|
2499
|
+
const activityMarkers = [...projection.activityMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
|
|
2469
2500
|
return this.inboxDelta([], removed, [], markers, [], activityMarkers);
|
|
2470
2501
|
}
|
|
2471
2502
|
inboxDelta(set = [], removed = [], addedMarkers = [], removedMarkers = [], addedActivityMarkers = [], removedActivityMarkers = []) {
|
|
@@ -2864,8 +2895,12 @@ export class AgentTeamLedger {
|
|
|
2864
2895
|
isChannelMemberFrom(projection, channelRef, memberId) {
|
|
2865
2896
|
return projection.memberships.get(channelRef)?.has(memberId) === true;
|
|
2866
2897
|
}
|
|
2898
|
+
/** Every Thread of the Channel, taskful or taskless — the scope both cleanup validators replay against. */
|
|
2867
2899
|
channelThreadRefs(channelRef) {
|
|
2868
|
-
return
|
|
2900
|
+
return this.channelThreadRefsFrom(this.state, channelRef);
|
|
2901
|
+
}
|
|
2902
|
+
channelThreadRefsFrom(projection, channelRef) {
|
|
2903
|
+
return new Set([...projection.threads.keys()].filter(threadRef => this.channelRefForThreadFrom(projection, threadRef) === channelRef));
|
|
2869
2904
|
}
|
|
2870
2905
|
assertJoinableMember(workspaceId, memberId) {
|
|
2871
2906
|
const member = this.requireMember(memberId);
|
|
@@ -3187,7 +3222,7 @@ export class AgentTeamLedger {
|
|
|
3187
3222
|
consumedDirectMarkers: operation.data.inbox.directMarkers.removed });
|
|
3188
3223
|
}
|
|
3189
3224
|
receipt(operation) {
|
|
3190
|
-
return Object.freeze({ operationId: operation.operationId, requestId: operation.requestId, sequence: operation.sequence });
|
|
3225
|
+
return Object.freeze({ operationId: operation.operationId, requestId: operation.requestId, sequence: operation.sequence, occurredAt: operation.occurredAt });
|
|
3191
3226
|
}
|
|
3192
3227
|
removalResult(operation) {
|
|
3193
3228
|
return Object.freeze({ receipt: this.receipt(operation), member: operation.data.member,
|
|
@@ -3214,24 +3249,70 @@ export class AgentTeamLedger {
|
|
|
3214
3249
|
sortedRecords() {
|
|
3215
3250
|
const records = [...this.table.entries()].sort((left, right) => left[1].sequence - right[1].sequence);
|
|
3216
3251
|
const occurrences = new Map();
|
|
3252
|
+
const instants = new Map();
|
|
3217
3253
|
for (const [, operation] of records) {
|
|
3254
|
+
instants.set(operation.sequence, operation.occurredAt);
|
|
3218
3255
|
if (operation.kind === 'team/message-sent' || operation.kind === 'team/thread-replied') {
|
|
3219
3256
|
occurrences.set(operation.data.message.messageRef, operation.data.message.occurredAt ?? operation.occurredAt);
|
|
3220
3257
|
}
|
|
3221
3258
|
}
|
|
3222
|
-
|
|
3259
|
+
// Replay against a scratch projection while normalizing, so a legacy
|
|
3260
|
+
// cleanup repair sees the same state its validator will.
|
|
3261
|
+
const projection = emptyProjection();
|
|
3262
|
+
return records.map(([id, operation]) => {
|
|
3263
|
+
const normalized = this.repairLegacyChannelCleanup(this.normalizeOperation(operation, occurrences, instants), projection);
|
|
3264
|
+
this.applyTo(projection, normalized);
|
|
3265
|
+
return [id, normalized];
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Releases up to 0.1.9 scoped Channel archival and Channel member-removal
|
|
3270
|
+
* inbox cleanup to taskful Threads only (the collector read the Task
|
|
3271
|
+
* projection), so archiving or member removal in a Channel holding a
|
|
3272
|
+
* taskless Thread with Attention or markers wrote an incomplete inbox and
|
|
3273
|
+
* every later load rejected the record, leaving the profile unable to boot.
|
|
3274
|
+
* Records carrying exactly that legacy cleanup are repaired in memory the
|
|
3275
|
+
* same way pre-envelope Thread reads are; any other inbox still fails
|
|
3276
|
+
* validation, so a forgery is not silently accepted.
|
|
3277
|
+
*/
|
|
3278
|
+
repairLegacyChannelCleanup(operation, projection) {
|
|
3279
|
+
if (operation.kind === 'team/channel-archived') {
|
|
3280
|
+
const channelRef = operation.data.channel.channelRef;
|
|
3281
|
+
const expected = this.channelArchivalInboxFrom(projection, this.channelThreadRefsFrom(projection, channelRef));
|
|
3282
|
+
if (!isDeepStrictEqual(operation.data.inbox, expected)
|
|
3283
|
+
&& isDeepStrictEqual(operation.data.inbox, this.channelArchivalInboxFrom(projection, this.legacyChannelThreadRefs(projection, channelRef)))) {
|
|
3284
|
+
return { ...operation, data: { ...operation.data, inbox: expected } };
|
|
3285
|
+
}
|
|
3286
|
+
return operation;
|
|
3287
|
+
}
|
|
3288
|
+
if (operation.kind === 'team/channel-member-removed') {
|
|
3289
|
+
const { channelRef, memberId } = operation.data;
|
|
3290
|
+
const expected = this.removeMemberThreadInboxFrom(projection, memberId, this.channelThreadRefsFrom(projection, channelRef));
|
|
3291
|
+
if (!isDeepStrictEqual(operation.data.inbox, expected)
|
|
3292
|
+
&& isDeepStrictEqual(operation.data.inbox, this.removeMemberThreadInboxFrom(projection, memberId, this.legacyChannelThreadRefs(projection, channelRef)))) {
|
|
3293
|
+
return { ...operation, data: { ...operation.data, inbox: expected } };
|
|
3294
|
+
}
|
|
3295
|
+
return operation;
|
|
3296
|
+
}
|
|
3297
|
+
return operation;
|
|
3298
|
+
}
|
|
3299
|
+
/** The pre-fix Channel Thread scope: only Threads carrying a Task. */
|
|
3300
|
+
legacyChannelThreadRefs(projection, channelRef) {
|
|
3301
|
+
return new Set([...projection.tasks.values()].filter(task => task.channelRef === channelRef).map(task => task.threadRef));
|
|
3223
3302
|
}
|
|
3224
3303
|
/** Ledgers written before message occurredAt existed store bare messages; Thread reads resolve instants from the originating operations. */
|
|
3225
|
-
normalizeOperation(operation, occurrences) {
|
|
3304
|
+
normalizeOperation(operation, occurrences, instants) {
|
|
3226
3305
|
if (operation.kind !== 'team/thread-read')
|
|
3227
3306
|
return operation;
|
|
3228
3307
|
const stamp = (message) => (message.occurredAt === undefined
|
|
3229
3308
|
? { ...message, occurredAt: occurrences.get(message.messageRef) ?? operation.occurredAt }
|
|
3230
3309
|
: { ...message, occurredAt: message.occurredAt });
|
|
3231
|
-
const
|
|
3232
|
-
? {
|
|
3233
|
-
|
|
3234
|
-
: {
|
|
3310
|
+
const stampEnvelope = (envelope) => envelope.kind === 'message'
|
|
3311
|
+
? { kind: 'message', sequence: envelope.sequence, message: stamp(envelope.message), mentions: envelope.mentions,
|
|
3312
|
+
occurredAt: envelope.occurredAt ?? envelope.message.occurredAt ?? occurrences.get(envelope.message.messageRef) ?? operation.occurredAt }
|
|
3313
|
+
: { kind: 'activity', sequence: envelope.sequence, activity: envelope.activity,
|
|
3314
|
+
occurredAt: envelope.occurredAt ?? instants.get(envelope.sequence) ?? operation.occurredAt };
|
|
3315
|
+
const facts = operation.data.facts.map((fact) => ({ ...fact, fact: stampEnvelope(fact.fact) }));
|
|
3235
3316
|
return { ...operation, data: { ...operation.data, anchor: stamp(operation.data.anchor), facts } };
|
|
3236
3317
|
}
|
|
3237
3318
|
enqueue(operation) {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team Member turn-level clock context.
|
|
3
|
+
*
|
|
4
|
+
* The first model step of every eligible Team Member turn appends one
|
|
5
|
+
* durable, source-attributed clock snapshot: the current instant in the
|
|
6
|
+
* fixed Team coordination zone (UTC+8), the elapsed time since the
|
|
7
|
+
* preceding model-visible event, and the ordering authority note. Later
|
|
8
|
+
* steps of the same turn stay quiet unless the turn runs longer than the
|
|
9
|
+
* refresh interval, in which case one snapshot lands per elapsed interval —
|
|
10
|
+
* a tool-dense turn of quick steps produces exactly one line, while a turn
|
|
11
|
+
* that outlives the interval still shows its real span. The snapshot is an
|
|
12
|
+
* observation, never ledger authority: sequence and revision, not
|
|
13
|
+
* wall-clock time, order Team facts.
|
|
14
|
+
*
|
|
15
|
+
* This plugin deliberately does not mount the shipped
|
|
16
|
+
* `@deepseek-ai/dsh-time-context`: its browser-zone policy asks the model to
|
|
17
|
+
* confirm dates with the user whenever a request carries no unique browser
|
|
18
|
+
* zone, which is the normal case for background Member wakes (Inbox, DM,
|
|
19
|
+
* recovery, continuation). The Team coordination zone is fixed instead. If
|
|
20
|
+
* the harness grows a public non-browser/canonical-zone policy, retire this
|
|
21
|
+
* row in favor of configuring that plugin.
|
|
22
|
+
*
|
|
23
|
+
* State is folded from the Member Session's own events — the same
|
|
24
|
+
* manual-fold pattern the Host's context projection uses — so restart,
|
|
25
|
+
* request reconstruction, and compaction all derive identical baselines
|
|
26
|
+
* without a second durable store.
|
|
27
|
+
* @module @wowyuarm/dsh-agent-team/member-time-context
|
|
28
|
+
*/
|
|
29
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
30
|
+
import { formatTeamDuration, formatTeamTimestamp } from "./time-format.js";
|
|
31
|
+
export const name = 'wowyuarm-agent-team-member-time-context';
|
|
32
|
+
/** Default minimum spacing between two snapshots within one turn, in ms. */
|
|
33
|
+
export const CLOCK_REFRESH_INTERVAL_MS = 1_800_000;
|
|
34
|
+
function emptyBaseline() {
|
|
35
|
+
return { lastMessageTime: null, lastInjectionTime: null, lastTurnInjectionTime: null, openTurn: -1 };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fold one session event into the clock baseline. Uninterested events return
|
|
39
|
+
* the same state reference.
|
|
40
|
+
* @internal exported for tests.
|
|
41
|
+
*/
|
|
42
|
+
export function applyClockEvent(state, event) {
|
|
43
|
+
switch (event.type) {
|
|
44
|
+
case 'turn/start':
|
|
45
|
+
return event.data.turn === state.openTurn ? state
|
|
46
|
+
: { ...state, lastTurnInjectionTime: null, openTurn: event.data.turn };
|
|
47
|
+
case 'turn/end':
|
|
48
|
+
return state.openTurn === -1 ? state : { ...state, lastTurnInjectionTime: null, openTurn: -1 };
|
|
49
|
+
case 'user/message': {
|
|
50
|
+
const source = event.data.source;
|
|
51
|
+
const injected = source.kind === 'plugin' && source.plugin === name;
|
|
52
|
+
const withMessage = state.lastMessageTime === event.time ? state : { ...state, lastMessageTime: event.time };
|
|
53
|
+
if (!injected)
|
|
54
|
+
return withMessage;
|
|
55
|
+
return { ...withMessage, lastInjectionTime: event.time, lastTurnInjectionTime: event.time };
|
|
56
|
+
}
|
|
57
|
+
case 'assistant/message':
|
|
58
|
+
case 'tool/result':
|
|
59
|
+
return state.lastMessageTime === event.time ? state : { ...state, lastMessageTime: event.time };
|
|
60
|
+
default:
|
|
61
|
+
return state;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Fold a whole event log into the clock baseline. @internal exported for tests. */
|
|
65
|
+
export function foldClockBaseline(events) {
|
|
66
|
+
let state = emptyBaseline();
|
|
67
|
+
for (const event of events)
|
|
68
|
+
state = applyClockEvent(state, event);
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether this step should append a clock snapshot: the first step of a turn
|
|
73
|
+
* always does (every wake starts with a fresh instant), and a later step
|
|
74
|
+
* does only when the turn has run longer than the refresh interval since
|
|
75
|
+
* the last landed snapshot. Skipped steps produce nothing and never
|
|
76
|
+
* backfill — their span folds into the next snapshot's elapsed.
|
|
77
|
+
* @internal exported for tests.
|
|
78
|
+
*/
|
|
79
|
+
export function shouldSampleClock(step, now, baseline, refreshIntervalMs) {
|
|
80
|
+
if (step === 1)
|
|
81
|
+
return true;
|
|
82
|
+
return baseline.lastTurnInjectionTime === null || now - baseline.lastTurnInjectionTime >= refreshIntervalMs;
|
|
83
|
+
}
|
|
84
|
+
/** Render one durable clock snapshot text. @internal exported for tests. */
|
|
85
|
+
export function renderClockSnapshot(input) {
|
|
86
|
+
// A wall-clock rollback clamps elapsed to 0s without rewriting history.
|
|
87
|
+
const elapsed = input.previous === undefined ? 'unavailable' : formatTeamDuration(input.now - input.previous);
|
|
88
|
+
const baseline = input.step === 1 ? 'model-visible event' : 'step context';
|
|
89
|
+
return `Team clock sampled while preparing turn ${input.turn}, step ${input.step}: ${formatTeamTimestamp(new Date(input.now).toISOString())}\n`
|
|
90
|
+
+ `Elapsed since the preceding ${baseline}: ${elapsed}.\n`
|
|
91
|
+
+ 'Team collaboration timestamps use UTC+8. Sequence and revision, not wall-clock time, determine ordering and concurrency.';
|
|
92
|
+
}
|
|
93
|
+
export function apply(ctx, config = {}) {
|
|
94
|
+
const refreshIntervalMs = config.refreshIntervalMs ?? CLOCK_REFRESH_INTERVAL_MS;
|
|
95
|
+
ctx.on('agent/pre-step', async ({ agent, turn, step, signal }, next) => {
|
|
96
|
+
const decision = await next();
|
|
97
|
+
if (decision.kind === 'reject' || signal.aborted)
|
|
98
|
+
return decision;
|
|
99
|
+
// The Host service is resolved at step time, never through plugin inject
|
|
100
|
+
// (same reason as member-context: the row mounts while the Host itself
|
|
101
|
+
// is still restoring Members).
|
|
102
|
+
const host = ctx.get('agentTeam');
|
|
103
|
+
if (host === undefined)
|
|
104
|
+
return decision;
|
|
105
|
+
if (host.memberForAgent(agent) === undefined)
|
|
106
|
+
return decision;
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
// A rollover starts a fresh Session log, so the fold never guesses
|
|
109
|
+
// elapsed across generations: a missing prior event renders
|
|
110
|
+
// `unavailable`, not a fabricated baseline.
|
|
111
|
+
const baseline = foldClockBaseline(agent.session.ownEvents());
|
|
112
|
+
// Turn-first-step always samples; later steps sample only at the refresh
|
|
113
|
+
// interval, so a quick tool-dense turn stays at one line.
|
|
114
|
+
if (!shouldSampleClock(step, now, baseline, refreshIntervalMs))
|
|
115
|
+
return decision;
|
|
116
|
+
const previous = step === 1
|
|
117
|
+
? baseline.lastMessageTime ?? undefined
|
|
118
|
+
: baseline.lastTurnInjectionTime ?? undefined;
|
|
119
|
+
const text = renderClockSnapshot({ now, turn, step, previous });
|
|
120
|
+
const message = createUserMessage({
|
|
121
|
+
content: [{ type: 'text', text }],
|
|
122
|
+
source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
|
|
123
|
+
});
|
|
124
|
+
return { kind: 'enter', messages: [...decision.messages, message] };
|
|
125
|
+
}, { prepend: true });
|
|
126
|
+
}
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm';
|
|
18
18
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
19
19
|
import { CONTEXT_PRESSURE_NOTICE_SUMMARY } from "./context-management.js";
|
|
20
|
-
|
|
21
|
-
const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
|
|
20
|
+
import { AGENT_TEAM_PLUGIN_ID } from "./context-source.js";
|
|
22
21
|
/** One pressure-notice text; concise, structured, and inside the reserve. */
|
|
23
22
|
export function contextPressureNoticeText(input) {
|
|
24
23
|
const claims = input.activeClaims.length === 0 ? 'none' : input.activeClaims.join(', ');
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* @module @wowyuarm/dsh-agent-team/progress-nudge
|
|
23
23
|
*/
|
|
24
24
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
25
|
-
|
|
25
|
+
import { AGENT_TEAM_PLUGIN_ID, isCheckpointContinuationMessage } from "./context-source.js";
|
|
26
26
|
/** Notice summary for every nudge this Coordinator injects. */
|
|
27
27
|
export const PROGRESS_NUDGE_NOTICE_SUMMARY = 'Progress visibility reminder';
|
|
28
28
|
export const PROGRESS_NUDGE_TOOL_CALLS_START = 20;
|
|
@@ -39,7 +39,7 @@ export function isPublicCommunicationOperationKind(kind) {
|
|
|
39
39
|
/** Whether one message is a nudge notice this module injected. */
|
|
40
40
|
export function isProgressNudgeNotice(message) {
|
|
41
41
|
const source = message.source;
|
|
42
|
-
return source.kind === 'plugin' && source.plugin ===
|
|
42
|
+
return source.kind === 'plugin' && source.plugin === AGENT_TEAM_PLUGIN_ID
|
|
43
43
|
&& source.form === 'notice' && source.summary === PROGRESS_NUDGE_NOTICE_SUMMARY;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
@@ -218,7 +218,7 @@ export class ProgressNudgeCoordinator {
|
|
|
218
218
|
const claimTargets = claimDue ? freshClaimTargets : [];
|
|
219
219
|
const notice = createUserMessage({
|
|
220
220
|
content: [{ type: 'text', text: nudgeNoticeText(state.silentToolCalls, progressTargets, claimTargets) }],
|
|
221
|
-
source: { kind: 'plugin', plugin:
|
|
221
|
+
source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: PROGRESS_NUDGE_NOTICE_SUMMARY },
|
|
222
222
|
});
|
|
223
223
|
const pending = {
|
|
224
224
|
messageId: notice.id,
|
|
@@ -268,7 +268,15 @@ export class ProgressNudgeCoordinator {
|
|
|
268
268
|
hasBlockingNotice(agent) {
|
|
269
269
|
for (const message of [...agent.inbox.nextStep, ...agent.inbox.nextTurn]) {
|
|
270
270
|
const source = message.source;
|
|
271
|
-
if (source.kind !== 'plugin' || source.plugin !==
|
|
271
|
+
if (source.kind !== 'plugin' || source.plugin !== AGENT_TEAM_PLUGIN_ID)
|
|
272
|
+
continue;
|
|
273
|
+
// A queued checkpoint continuation is a pending wake that outranks a
|
|
274
|
+
// nudge exactly as the notice families above do. It rides the snapshot
|
|
275
|
+
// form (it must carry its checkpoint ref in a section), so it cannot be
|
|
276
|
+
// recognized by the notice summary check below.
|
|
277
|
+
if (isCheckpointContinuationMessage(message))
|
|
278
|
+
return true;
|
|
279
|
+
if (source.form !== 'notice')
|
|
272
280
|
continue;
|
|
273
281
|
if (source.summary !== PROGRESS_NUDGE_NOTICE_SUMMARY)
|
|
274
282
|
return true;
|