@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.
Files changed (54) hide show
  1. package/README.md +4 -11
  2. package/README.zh.md +4 -11
  3. package/package.json +53 -44
  4. package/packages/agent-team/README.md +2 -2
  5. package/packages/agent-team/README.zh.md +2 -1
  6. package/packages/agent-team/lib/context-management.js +8 -5
  7. package/packages/agent-team/lib/context-projection.js +12 -8
  8. package/packages/agent-team/lib/context-source.js +163 -23
  9. package/packages/agent-team/lib/index.js +174 -124
  10. package/packages/agent-team/lib/ledger.js +111 -30
  11. package/packages/agent-team/lib/member-time-context.js +126 -0
  12. package/packages/agent-team/lib/pressure-policy.js +1 -2
  13. package/packages/agent-team/lib/progress-nudge.js +12 -4
  14. package/packages/agent-team/lib/session-remediation.js +481 -0
  15. package/packages/agent-team/lib/spec.js +3 -2
  16. package/packages/agent-team/lib/time-format.js +56 -0
  17. package/packages/agent-team/lib/typert.host.js +82 -49
  18. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  19. package/packages/agent-team/lib/typert.remote-client.js +46 -25
  20. package/packages/agent-team/lib/types/context-management.d.ts +6 -1
  21. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/context-source.d.ts +70 -36
  24. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/index.d.ts +32 -7
  26. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/ledger.d.ts +25 -0
  28. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
  30. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
  31. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/session-remediation.d.ts +149 -0
  34. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
  35. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/time-format.d.ts +28 -0
  37. package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/types/entities.d.ts +8 -0
  39. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/types/requests-results.d.ts +6 -0
  41. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  42. package/packages/agent-team/preset/team-member/agent.cordis.yml +19 -4
  43. package/packages/client-agent-team/lib/client.js +80 -54
  44. package/packages/client-agent-team/lib/client.js.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamMessage.js +2 -2
  46. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +4 -4
  47. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  48. package/packages/client-agent-team/lib/types/client/index.js +10 -5
  49. package/packages/client-agent-team/lib/types/client/slots.d.ts +1 -1
  50. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  51. package/packages/tool-agent-team/README.md +8 -6
  52. package/packages/tool-agent-team/README.zh.md +8 -6
  53. package/packages/tool-agent-team/lib/index.js +302 -110
  54. 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: unread.at(-1).fact.sequence, ...(attention === undefined ? {} : { attention }) }));
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
- const removed = [...this.state.attention.values()].filter(attention => attention.memberId === memberId && threadRefs.has(attention.threadRef))
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 = [...this.state.directMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
2460
- const activityMarkers = [...this.state.activityMarkers.values()].filter(marker => marker.memberId === memberId && threadRefs.has(marker.threadRef));
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
- const removed = [...this.state.attention.values()].filter(attention => threadRefs.has(attention.threadRef))
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 = [...this.state.directMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
2468
- const activityMarkers = [...this.state.activityMarkers.values()].filter(marker => threadRefs.has(marker.threadRef));
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 new Set([...this.state.tasks.values()].filter(task => task.channelRef === channelRef).map(task => task.threadRef));
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
- return records.map(([id, operation]) => [id, this.normalizeOperation(operation, occurrences)]);
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 facts = operation.data.facts.map((fact) => (fact.fact.kind === 'message'
3232
- ? { ...fact, fact: { kind: 'message', sequence: fact.fact.sequence, message: stamp(fact.fact.message),
3233
- mentions: fact.fact.mentions } }
3234
- : { ...fact, fact: fact.fact }));
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
- /** Plugin identity of the Agent Team Host, for the notice source. */
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
- const PROGRESS_NUDGE_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
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 === PROGRESS_NUDGE_PLUGIN_ID
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: PROGRESS_NUDGE_PLUGIN_ID, form: 'notice', summary: PROGRESS_NUDGE_NOTICE_SUMMARY },
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 !== PROGRESS_NUDGE_PLUGIN_ID || source.form !== 'notice')
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;