@wowyuarm/dsh-agent-team 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/package.json +2 -1
  2. package/packages/agent-team/README.md +5 -3
  3. package/packages/agent-team/README.zh.md +5 -3
  4. package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +2 -0
  5. package/packages/agent-team/lib/attachments.js +14 -4
  6. package/packages/agent-team/lib/context-management.js +379 -0
  7. package/packages/agent-team/lib/context-projection.js +574 -0
  8. package/packages/agent-team/lib/context-source.js +72 -0
  9. package/packages/agent-team/lib/index.js +1258 -54
  10. package/packages/agent-team/lib/ledger.js +357 -26
  11. package/packages/agent-team/lib/member-context.js +7 -3
  12. package/packages/agent-team/lib/member-runtime.js +159 -4
  13. package/packages/agent-team/lib/pressure-policy.js +200 -0
  14. package/packages/agent-team/lib/progress-nudge.js +320 -0
  15. package/packages/agent-team/lib/spec.js +18 -0
  16. package/packages/agent-team/lib/typert.host.js +124 -42
  17. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  18. package/packages/agent-team/lib/typert.remote-client.js +39 -25
  19. package/packages/agent-team/lib/types/attachments.d.ts +1 -1
  20. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  21. package/packages/agent-team/lib/types/context-management.d.ts +142 -0
  22. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
  23. package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
  24. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
  25. package/packages/agent-team/lib/types/context-source.d.ts +83 -0
  26. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
  27. package/packages/agent-team/lib/types/index.d.ts +283 -2
  28. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/ledger.d.ts +120 -7
  30. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  31. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
  33. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  34. package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
  35. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
  36. package/packages/agent-team/lib/types/progress-nudge.d.ts +136 -0
  37. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  39. package/packages/agent-team/lib/types/types/entities.d.ts +32 -1
  40. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  41. package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
  42. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  43. package/packages/agent-team/lib/types/types/requests-results.d.ts +54 -1
  44. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  45. package/packages/agent-team/preset/team-member/agent.cordis.yml +10 -2
  46. package/packages/client-agent-team/lib/client.js +283 -398
  47. package/packages/client-agent-team/lib/client.js.map +1 -1
  48. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
  49. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
  51. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  52. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +3 -7
  53. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +3 -1
  54. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamComposer.js +37 -27
  56. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/TeamConversation.js +2 -2
  59. package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.js +3 -2
  61. package/packages/client-agent-team/lib/types/client/TeamMessage.js +5 -0
  62. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +2 -2
  63. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +4 -6
  65. package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts +14 -0
  66. package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts.map +1 -0
  67. package/packages/client-agent-team/lib/types/client/TeamStateDot.js +15 -0
  68. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +1 -0
  69. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +41 -16
  71. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  72. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  73. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  74. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  75. package/packages/client-agent-team/lib/types/client/index.js +36 -78
  76. package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -16
  77. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  78. package/packages/client-agent-team/lib/types/client/locales.js +0 -16
  79. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +1 -2
  80. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -1
  82. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  83. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +7 -1
  84. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
  85. package/packages/client-agent-team/lib/types/client/task-refs.js +25 -3
  86. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +11 -10
  87. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  88. package/packages/client-agent-team/lib/types/client/team-formatters.js +14 -11
  89. package/packages/tool-agent-team/README.md +8 -5
  90. package/packages/tool-agent-team/README.zh.md +8 -5
  91. package/packages/tool-agent-team/lib/context-tools.js +179 -0
  92. package/packages/tool-agent-team/lib/index.js +144 -25
  93. package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
  94. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
  95. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  96. package/packages/agent-team/lib/auto-compaction.js +0 -201
  97. package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
  98. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +0 -1
  99. package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts +0 -12
  100. package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts.map +0 -1
  101. package/packages/client-agent-team/lib/types/client/TeamMemberDock.js +0 -13
  102. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +0 -20
  103. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +0 -1
  104. package/packages/client-agent-team/lib/types/client/member-session-input.js +0 -110
@@ -39,6 +39,58 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
39
39
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
40
40
  done = true;
41
41
  };
42
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
43
+ if (value !== null && value !== void 0) {
44
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
45
+ var dispose, inner;
46
+ if (async) {
47
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
48
+ dispose = value[Symbol.asyncDispose];
49
+ }
50
+ if (dispose === void 0) {
51
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
52
+ dispose = value[Symbol.dispose];
53
+ if (async) inner = dispose;
54
+ }
55
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
56
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
57
+ env.stack.push({ value: value, dispose: dispose, async: async });
58
+ }
59
+ else if (async) {
60
+ env.stack.push({ async: true });
61
+ }
62
+ return value;
63
+ };
64
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
65
+ return function (env) {
66
+ function fail(e) {
67
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
68
+ env.hasError = true;
69
+ }
70
+ var r, s = 0;
71
+ function next() {
72
+ while (r = env.stack.pop()) {
73
+ try {
74
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
75
+ if (r.dispose) {
76
+ var result = r.dispose.call(r.value);
77
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
78
+ }
79
+ else s |= 1;
80
+ }
81
+ catch (e) {
82
+ fail(e);
83
+ }
84
+ }
85
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
86
+ if (env.hasError) throw env.error;
87
+ }
88
+ return next();
89
+ };
90
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
91
+ var e = new Error(message);
92
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
93
+ });
42
94
  import { randomUUID } from 'node:crypto';
43
95
  import { isDeepStrictEqual } from 'node:util';
44
96
  import { Service } from '@deepseek-ai/cordis';
@@ -46,13 +98,17 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
46
98
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
47
99
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
48
100
  import { scopeOf } from '@deepseek-ai/dsh-scope';
49
- import { SessionId } from '@deepseek-ai/dsh-session';
101
+ import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
50
102
  import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
51
103
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
52
104
  import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
53
- import { acceptedTaskCompactionMembers, AutoCompactionCoordinator, PRE_COMPACTION_NOTICE_SUMMARY, preCompactionNoticeText } from "./auto-compaction.js";
105
+ import { PressurePolicyCoordinator } from "./pressure-policy.js";
106
+ import { ContextManagementCoordinator } from "./context-management.js";
107
+ import { createHandoffMessage } from "./context-source.js";
108
+ import { carriedInputOf, checkpointByRef, checkpointRefFor, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
54
109
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
55
- import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, MemberRuntime } from "./member-runtime.js";
110
+ import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, memberMemoryDirectoryName, MemberRuntime } from "./member-runtime.js";
111
+ import { ProgressNudgeCoordinator } from "./progress-nudge.js";
56
112
  import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
57
113
  import { agentTeamDomainSpec } from "./spec.js";
58
114
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
@@ -64,6 +120,24 @@ const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
64
120
  const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
65
121
  const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
66
122
  const ORPHANED_MEMBER_DIAGNOSTIC = 'Member preset composition was lost after a reload; its tools are unavailable. Resume rebuilds the member in place.';
123
+ /** Longest accepted model-supplied checkpoint display name. */
124
+ const MAX_CHECKPOINT_NAME_CHARS = 120;
125
+ /** Default and maximum number of timeline items one query returns. */
126
+ const DEFAULT_TIMELINE_LIMIT = 12;
127
+ const MAX_TIMELINE_LIMIT = 24;
128
+ /** Deepest ancestor lineage the timeline and seed resolution walk. */
129
+ const MAX_TIMELINE_ANCESTORS = 8;
130
+ /** Product pressure budget constants (see docs/team-collaboration.md). */
131
+ const CONTEXT_HARD_LIMIT_CAP = 256_000;
132
+ const CONTEXT_HANDOFF_AT_CAP = 200_000;
133
+ const CONTEXT_HANDOFF_RESERVE = 8_000;
134
+ const CONTEXT_SAFE_OUTPUT_RESERVE = 16_000;
135
+ /**
136
+ * Usage at or above which an acknowledged acceptance advises a fresh
137
+ * rollover instead of keeping the context; capped by the route's effective
138
+ * handoff budget so narrow routes get a proportionally earlier boundary.
139
+ */
140
+ const ACCEPT_TASK_BOUNDARY_THRESHOLD = 128_000;
67
141
  function sameChangeScope(left, right) {
68
142
  if (left.kind === 'workspace' && right.kind === 'workspace')
69
143
  return left.workspaceId === right.workspaceId;
@@ -189,6 +263,8 @@ let AgentTeam = (() => {
189
263
  domain = __runInitializers(this, _instanceExtraInitializers);
190
264
  ledger;
191
265
  handles = new Map();
266
+ /** Live Member per session id; drives the root session/event listener. */
267
+ memberBySessionId = new Map();
192
268
  /** Live selection refs let model edits take effect without disposing the Session. */
193
269
  modelSelections = new Map();
194
270
  /** Agent ids with a turn in flight; restarts must wait for the boundary. */
@@ -214,7 +290,7 @@ let AgentTeam = (() => {
214
290
  * stale keys.
215
291
  */
216
292
  memberFailures = new Map();
217
- autoCompaction;
293
+ pressurePolicy;
218
294
  notifiedInbox = new Map();
219
295
  attachmentGcTimer;
220
296
  recovery = new RecoveryCoordinator({
@@ -226,34 +302,62 @@ let AgentTeam = (() => {
226
302
  this.ctx.logger.warn(`agent-team: member '${this.memberLabel(memberId)}' reached ${consecutiveFailures}/${RECOVERY_MAX_CONSECUTIVE_ERRORS} consecutive recoverable failures; leaving it in error for the operator`);
227
303
  },
228
304
  });
305
+ /**
306
+ * Advisory progress-visibility nudges. Purely in-process: the ledger owns
307
+ * eligibility, this coordinator owns counting and notices, and neither ever
308
+ * writes Team facts. See docs/team-collaboration.md for the regimes.
309
+ */
310
+ progressNudge = new ProgressNudgeCoordinator({
311
+ agentForMember: memberId => this.handles.get(memberId)?.agent,
312
+ targetsForMember: memberId => this.requireLedger().progressNudgeTargets(memberId),
313
+ sessionLogForMember: (memberId, sessionId) => {
314
+ const handle = this.handles.get(memberId);
315
+ if (handle === undefined || handle.agent.session.id !== sessionId)
316
+ return undefined;
317
+ return { events: handle.agent.session.ownEvents() };
318
+ },
319
+ log: message => { this.ctx.logger.warn(message); },
320
+ });
321
+ /**
322
+ * Context self-management: the one deep module that turns a Member's
323
+ * successful `context_rollover` tool result into its next private context
324
+ * generation. The ledger owns the binding audit, the Session projection
325
+ * owns intent, and this coordinator owns only reconstructible process
326
+ * state. See docs/architecture.md and docs/team-collaboration.md.
327
+ */
328
+ contextManagement = new ContextManagementCoordinator({
329
+ agentForMember: memberId => this.handles.get(memberId)?.agent,
330
+ memberForAgent: agent => this.memberForAgent(agent),
331
+ projectionForMember: (memberId, sessionId) => {
332
+ const handle = this.handles.get(memberId);
333
+ if (handle === undefined || handle.agent.session.id !== sessionId)
334
+ return undefined;
335
+ return foldContextProjection(handle.agent.session.ownEvents(), handle.agent.session.inheritedEventCount, handle.agent.session.id);
336
+ },
337
+ executeTransition: (memberId, plan) => this.executeMemberTransition(memberId, plan),
338
+ log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
339
+ });
229
340
  lifecycleTail = Promise.resolve();
230
341
  accepting = true;
231
342
  changeVersion = 0;
232
343
  changeWaiters = new Set();
233
344
  constructor(ctx) {
234
345
  super(ctx, 'agentTeam');
235
- this.autoCompaction = new AutoCompactionCoordinator({
346
+ this.pressurePolicy = new PressurePolicyCoordinator({
236
347
  agentForMember: memberId => this.handles.get(memberId)?.agent,
348
+ memberForAgent: agent => {
349
+ const member = this.memberForAgent(agent);
350
+ return member === undefined ? undefined : { memberId: member.memberId, sessionId: agent.id };
351
+ },
237
352
  compactionForAgent: agent => this.ctx.agentPresets.serviceFor(agent, 'compaction'),
238
- reactivate: memberId => this.reactivateMember(memberId),
353
+ limitsForAgent: agent => this.routeLimitsForAgent(agent),
354
+ activeClaimLabels: memberId => this.activeClaimLabels(memberId),
355
+ runningJobLabels: memberId => this.runningJobLabels(memberId),
239
356
  failed: (memberId, _sessionId, diagnostic) => {
240
357
  this.setMemberFailure(memberId, 'compaction', diagnostic);
241
358
  this.emitAutoCompactionChanged(memberId);
242
359
  },
243
- cleared: memberId => {
244
- if (!this.clearMemberFailure(memberId, 'compaction'))
245
- return;
246
- this.emitAutoCompactionChanged(memberId);
247
- },
248
360
  log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
249
- // Advisory only: the Agent decides whether anything is worth persisting.
250
- steerPreCompaction: agent => {
251
- const hint = createUserMessage({
252
- content: [{ type: 'text', text: preCompactionNoticeText() }],
253
- source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: PRE_COMPACTION_NOTICE_SUMMARY },
254
- });
255
- agent.steer(hint);
256
- },
257
361
  });
258
362
  }
259
363
  /** Open the durable ledger and restore every enabled Member independently. */
@@ -281,7 +385,12 @@ let AgentTeam = (() => {
281
385
  if (recovered)
282
386
  this.notifiedInbox.delete(member.memberId);
283
387
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
284
- this.notifyMember(agent);
388
+ // A rollover/recovery in flight delivers its own sequenced
389
+ // rederived Inbox after the handoff and carried input; a status-driven
390
+ // steer here would claim the handoff turn's next step and leapfrog
391
+ // the carried messages.
392
+ if (!this.contextManagement.isTransitioning(member.memberId))
393
+ this.notifyMember(agent);
285
394
  }
286
395
  // A turn that ends without an error closes any automatic recovery episode.
287
396
  // The idle transition is itself presence-affecting (working → available),
@@ -295,11 +404,37 @@ let AgentTeam = (() => {
295
404
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
296
405
  }
297
406
  });
407
+ // Progress nudges count every `tool/call` of each Member Session. The
408
+ // store's dispatch carrier is untagged, so a scope-tagged listener inside
409
+ // the Agent setup would receive nothing; one root listener that maps the
410
+ // session id back to its Member is the seam that works (same shape as the
411
+ // `agent/status` listener above). The first turn of a freshly published
412
+ // Member cannot race this: `handles.set()` precedes the same activation
413
+ // continuation that publishes the Agent, so by the time any tool call
414
+ // streams, the map lookup succeeds.
415
+ this.ctx.on('session/event', (session, event) => {
416
+ const memberId = this.memberBySessionId.get(session.id);
417
+ if (memberId === undefined)
418
+ return;
419
+ const handle = this.handles.get(memberId);
420
+ if (handle === undefined || handle.agent.session.id !== session.id)
421
+ return;
422
+ this.progressNudge.onSessionEvent(memberId, session.id, handle.agent, event);
423
+ // A successful assistant response ends any open provider-overflow
424
+ // recovery sequence for this Member.
425
+ if (event.type === 'assistant/message')
426
+ this.pressurePolicy.onAssistantMessage(handle.agent);
427
+ // Context management reacts only after a successful durable tool/result;
428
+ // the projection (not this listener) decides what that means.
429
+ this.contextManagement.onSessionEvent(memberId, handle.agent, event);
430
+ });
298
431
  const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
299
432
  this.ctx.effect(() => async () => {
300
433
  this.accepting = false;
301
434
  this.recovery.dispose();
302
- await this.autoCompaction.dispose();
435
+ this.progressNudge.dispose();
436
+ this.contextManagement.dispose();
437
+ this.pressurePolicy.dispose();
303
438
  if (this.attachmentGcTimer !== undefined)
304
439
  clearInterval(this.attachmentGcTimer);
305
440
  this.attachmentGcTimer = undefined;
@@ -321,7 +456,7 @@ let AgentTeam = (() => {
321
456
  this.startAttachmentGc(ledger);
322
457
  // One metadata listing serves every Member restore; per-member list calls
323
458
  // would repeat the same I/O linearly during startup.
324
- const persistedSessions = new Set((await this.ctx.sessionPersistence.list()).map(header => header.id));
459
+ const persistedSessions = new Set((await this.persistedSessionHeaders()).map(header => header.id));
325
460
  for (const member of ledger.listMembers()) {
326
461
  if (member.state === 'enabled')
327
462
  await this.activateMember(member, undefined, persistedSessions);
@@ -329,6 +464,23 @@ let AgentTeam = (() => {
329
464
  await this.memberRuntime.cleanupRemovedMember(member);
330
465
  }
331
466
  }
467
+ /**
468
+ * Whether one Member Session has durable persisted content, decided through
469
+ * {@link SessionPersistenceService.inspect} rather than a bare metadata
470
+ * listing: inspection first awaits any in-flight retirement drain for the
471
+ * id, so a resume racing a suspend's fire-and-forget final flush cannot
472
+ * mistake a still-draining persisted Session for an unpersisted one and
473
+ * fork a fresh generation over it.
474
+ */
475
+ async sessionPersisted(sessionId) {
476
+ try {
477
+ await this.ctx.sessionPersistence.inspect(sessionId);
478
+ return true;
479
+ }
480
+ catch {
481
+ return false;
482
+ }
483
+ }
332
484
  /** Resolve one exact live Agent to its durable Team Member; forks do not inherit identity. */
333
485
  memberForAgent(agent) {
334
486
  for (const [memberId, handle] of this.handles) {
@@ -456,7 +608,7 @@ let AgentTeam = (() => {
456
608
  presetId: request.presetId,
457
609
  ...(request.model === undefined ? {} : { model: Object.freeze({ ...request.model }) }),
458
610
  ...(request.capabilities === undefined ? {} : { capabilities: Object.freeze(deepCopyCapabilities(request.capabilities)) }),
459
- privateMemoryPath: dshHomePath('agent-team', 'members', memberId),
611
+ privateMemoryPath: dshHomePath('agent-team', 'members', memberMemoryDirectoryName(memberId)),
460
612
  state: 'enabled',
461
613
  });
462
614
  const result = await this.requireLedger().addMember({ ...request, actor: agentTeamHumanActor(), member });
@@ -485,7 +637,13 @@ let AgentTeam = (() => {
485
637
  if (result.committed)
486
638
  this.emitCommitted(result.value.receipt);
487
639
  this.clearMemberNotificationState(result.value.member.memberId);
488
- await this.activateMember(result.value.member);
640
+ // The suspended Session's log is durable once its retirement completes,
641
+ // and agents.resume() waits for exactly that retirement before loading.
642
+ // Consulting the persistence tree here instead would race the
643
+ // fire-and-forget retirement on Windows, where the JSONL backend
644
+ // publishes directories through transient staging entries that surface
645
+ // as ENOENT mid-walk — so pass the known session rather than re-listing.
646
+ await this.activateMember(result.value.member, undefined, new Set([result.value.member.sessionId]));
489
647
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
490
648
  });
491
649
  }
@@ -563,18 +721,7 @@ let AgentTeam = (() => {
563
721
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
564
722
  }
565
723
  const renewed = result.value.member;
566
- // Drop the old handle's transient state: pending recovery episodes and
567
- // error markers belong to the disposed agent, not to the Member.
568
- this.recovery.stopTracking(request.memberId);
569
- await active.dispose();
570
- this.handles.delete(request.memberId);
571
- this.modelSelections.delete(request.memberId);
572
- this.memberRuntime.forgetMember(request.memberId);
573
- this.clearMemberFailure(request.memberId, 'activation');
574
- this.clearMemberNotificationState(request.memberId);
575
- // The previous log survives on disk; archiving hides it from every
576
- // grouping surface so one Member keeps exactly one visible Session.
577
- await this.ctx.workspaceRegistry.archiveSession(previousSessionId);
724
+ await this.retireMemberGeneration(request.memberId, active, previousSessionId);
578
725
  await this.activateMember(renewed, undefined, undefined, previousSessionId);
579
726
  const reactivated = this.handles.get(request.memberId);
580
727
  if (reactivated === undefined) {
@@ -585,6 +732,126 @@ let AgentTeam = (() => {
585
732
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(renewed) });
586
733
  });
587
734
  }
735
+ /**
736
+ * Retire one Member's previous generation after its durable binding moved
737
+ * onto a new Session id: drop the old handle's transient state, dispose the
738
+ * Agent, and archive the old Session log (which stays on disk for history).
739
+ * Shared by the Human clear path and the model-initiated rollover.
740
+ */
741
+ async retireMemberGeneration(memberId, active, previousSessionId) {
742
+ // Drop the old handle's transient state: pending recovery episodes and
743
+ // error markers belong to the disposed agent, not to the Member.
744
+ this.recovery.stopTracking(memberId);
745
+ // The fresh Session may re-earn one Claim suggestion per Thread; the
746
+ // old Session's one-shot records must not leak into it.
747
+ this.progressNudge.stopTracking(memberId);
748
+ // The context admission gate stays armed through disposal: input racing
749
+ // the retire window must still be captured for the new generation, and
750
+ // the coordinator drops its own bookkeeping only after the swap settles.
751
+ this.memberBySessionId.delete(previousSessionId);
752
+ await active.dispose();
753
+ this.handles.delete(memberId);
754
+ this.modelSelections.delete(memberId);
755
+ this.memberRuntime.forgetMember(memberId);
756
+ this.clearMemberFailure(memberId, 'activation');
757
+ this.clearMemberNotificationState(memberId);
758
+ // The previous log survives on disk; archiving hides it from every
759
+ // grouping surface so one Member keeps exactly one visible Session.
760
+ await this.ctx.workspaceRegistry.archiveSession(previousSessionId);
761
+ }
762
+ /**
763
+ * Execute one prepared context rollover at a true idle boundary: commit the
764
+ * idempotent Member-actor operation, retire the previous generation, and
765
+ * activate the fresh Session whose first model-facing context is the
766
+ * Member's own handoff. Later non-Team input captured during the transition
767
+ * is delivered after the handoff; the Team Inbox is rederived from the
768
+ * ledger, never copied.
769
+ */
770
+ async executeMemberTransition(memberId, plan) {
771
+ await this.enqueueLifecycle(async () => {
772
+ this.requireAccepting();
773
+ const stored = this.requireLedger().getMember(memberId);
774
+ if (stored === undefined || stored.state !== 'enabled')
775
+ throw new Error(`Agent Member '${memberId}' cannot roll over: not enabled`);
776
+ if (stored.sessionId !== plan.previousSessionId)
777
+ throw new Error(`Agent Member '${stored.handle}' is no longer bound to the rolled-over Session`);
778
+ const active = this.handles.get(memberId);
779
+ if (active === undefined)
780
+ throw new Error(`Agent Member '${stored.handle}' has no active session to roll over`);
781
+ // A racing turn the admission gate rejects still leaves the Agent
782
+ // momentarily running; wait for its convergence instead of failing the
783
+ // swap — the gate guarantees it spends no model request.
784
+ if (this.runningAgents.has(active.agent.id))
785
+ await active.agent.whenIdle();
786
+ if (this.runningAgents.has(active.agent.id))
787
+ throw new Error(`Agent Member '${stored.handle}' is still running; the rollover must wait for idle`);
788
+ // Checkpoint return: resolve the seed before committing anything. A
789
+ // violation found here fails the whole swap with the old generation
790
+ // intact — never a guessed seed over a wrong prefix.
791
+ const seed = plan.checkpointRef === undefined ? undefined : await this.resolveCheckpointSeed(memberId, active.agent, plan.checkpointRef);
792
+ // Recheck the job guard at the lifecycle commit seam: a job may have
793
+ // started or settled after the tool-time validation.
794
+ const blockingJobs = this.ownedJobsBlockingRollover(active.agent);
795
+ if (blockingJobs.length > 0) {
796
+ throw new Error(`the context rollover is refused: this Member now owns jobs that would not survive the switch (${blockingJobs.join(', ')}); collect or stop them, then retry`);
797
+ }
798
+ const rolled = await this.rolloverSessionForAgent(active.agent, {
799
+ requestId: plan.requestId,
800
+ workspaceId: stored.workspaceId,
801
+ memberId,
802
+ previousSessionId: plan.previousSessionId,
803
+ newSessionId: plan.newSessionId,
804
+ handoffEventSeq: plan.handoffEventSeq,
805
+ trigger: plan.trigger,
806
+ ...(seed === undefined ? {} : { checkpointRef: seed.checkpointRef, sourceSessionId: seed.sourceSessionId, sourceThroughSeq: seed.sourceThroughSeq }),
807
+ });
808
+ // The durable old-log projection is the carried-input truth: after the
809
+ // old Agent retires, fold its final state and take every post-intent
810
+ // non-Team candidate the old generation never answered. The process
811
+ // capture only accelerates; it is unioned by message id, never allowed
812
+ // to override the fold.
813
+ const preRetireCapture = this.contextManagement.drainCapturedInput(memberId);
814
+ await this.retireMemberGeneration(memberId, active, plan.previousSessionId);
815
+ const finalState = foldContextProjection(active.agent.session.ownEvents(), active.agent.session.inheritedEventCount, active.agent.session.id);
816
+ const foldedCarried = carriedInputOf(finalState);
817
+ const carriedById = new Map(plan.carriedInput.map(message => [message.id, message]));
818
+ for (const message of foldedCarried)
819
+ carriedById.set(message.id, message);
820
+ for (const message of preRetireCapture)
821
+ if (!carriedById.has(message.id))
822
+ carriedById.set(message.id, message);
823
+ for (const message of this.contextManagement.drainCapturedInput(memberId))
824
+ if (!carriedById.has(message.id))
825
+ carriedById.set(message.id, message);
826
+ const carriedInput = [...carriedById.values()];
827
+ // A fresh rollover seeds nothing and points the lineage parent at the
828
+ // previous active Session; a checkpoint return seeds the resolved
829
+ // prefix and parents at the seed source Session instead. Activation
830
+ // defers the ordinary Inbox wake so the handoff is guaranteed to be the
831
+ // new generation's first model-facing context.
832
+ const seedEvents = seed === undefined ? undefined : seed.prefix;
833
+ await this.activateMember(rolled.member, undefined, undefined, seed === undefined ? plan.previousSessionId : seed.sourceSessionId, {
834
+ deferNotify: true,
835
+ // The prefix is contiguous from seq 0, so its length is exactly the
836
+ // inherited cut the child folds past.
837
+ ...(seedEvents === undefined ? {} : { seed: seedEvents, inheritedEventCount: SessionLogOffset(seedEvents.length) }),
838
+ });
839
+ const reactivated = this.handles.get(memberId);
840
+ if (reactivated === undefined) {
841
+ throw new Error(`Agent Member '${stored.handle}' failed to activate its next context: ${this.memberFailures.get(memberId)?.activation ?? 'unknown error'}`);
842
+ }
843
+ // The handoff is the first model-facing context of the new generation.
844
+ // It rides the step-priority inbox lane (steer) so a later rederived
845
+ // Inbox notice queues behind it instead of preempting it; carried input
846
+ // follows as its own turn, and the Inbox is rederived from ledger facts.
847
+ reactivated.agent.steer(this.contextManagement.handoffMessageFor(plan));
848
+ for (const message of carriedInput)
849
+ reactivated.agent.followup(message);
850
+ const notifications = this.requireLedger().notificationFacts(memberId, { workspaceId: rolled.member.workspaceId });
851
+ if (notifications.length > 0)
852
+ this.notifyMember(reactivated.agent, carriedInput.length > 0);
853
+ });
854
+ }
588
855
  /** Ledger handle for log lines; falls back to the raw id when unknown. */
589
856
  memberLabel(memberId) {
590
857
  return this.ledger?.getMember(memberId)?.handle ?? memberId;
@@ -628,6 +895,9 @@ let AgentTeam = (() => {
628
895
  if (this.isInboxNotice(pending))
629
896
  handle.agent.inbox.remove(pending.id);
630
897
  }
898
+ // Recovery carries the concrete interrupted work; a queued progress nudge
899
+ // is stale next to it and must not survive as a second reminder.
900
+ this.progressNudge.revokePendingNotice(member.memberId);
631
901
  handle.agent.steer(hint);
632
902
  }
633
903
  /** Automatic-recovery wakeup; throwing tells the coordinator the target is gone. */
@@ -895,7 +1165,60 @@ let AgentTeam = (() => {
895
1165
  const result = await this.requireLedger().readThread({ ...request, actor });
896
1166
  if (result.committed)
897
1167
  this.emitCommitted(result.value.receipt);
898
- return result.value;
1168
+ const value = result.value;
1169
+ // Private read-time enrich: an acceptance the reader just acknowledged is
1170
+ // a natural Task boundary, so the Host prices the reader's context once,
1171
+ // after the durable read has committed. Never a ledger fact, never
1172
+ // persisted, and a measurement failure degrades to an explicit
1173
+ // `unavailable` — the committed read is never reversed.
1174
+ const advice = await this.acceptanceContextAdvice(agent, value);
1175
+ return advice === undefined ? value : Object.freeze({ ...value, contextAdvice: advice });
1176
+ }
1177
+ /**
1178
+ * Context advice for one acceptance acknowledged by this read: only when
1179
+ * the read carried an unread accept activity AND the Task is still done.
1180
+ * Repeat reads (nothing unread), history-style reads, and reopened Tasks
1181
+ * carry no advice — the acceptance no longer stands.
1182
+ */
1183
+ async acceptanceContextAdvice(agent, read) {
1184
+ if (read.task === undefined || read.task.resolution !== 'accepted' || read.task.status !== 'done')
1185
+ return undefined;
1186
+ const acknowledgedAccept = read.facts.some(entry => entry.unread && entry.fact.kind === 'activity'
1187
+ && entry.fact.activity.kind === 'accept');
1188
+ if (!acknowledgedAccept)
1189
+ return undefined;
1190
+ return this.contextAdviceFor(agent);
1191
+ }
1192
+ /** Price the reading Member's context against its current route's budgets. */
1193
+ async contextAdviceFor(agent) {
1194
+ try {
1195
+ const limits = await this.routeLimitsForAgent(agent);
1196
+ if (limits === undefined)
1197
+ return this.unavailableAdvice();
1198
+ const taskBoundaryThreshold = Math.min(ACCEPT_TASK_BOUNDARY_THRESHOLD, limits.handoffAt);
1199
+ if (limits.usageTokens >= limits.handoffAt) {
1200
+ return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
1201
+ action: 'handoff-now',
1202
+ guidance: 'You are at or above the handoff budget. Finish the current atomic action and unsettled evidence, then call context_rollover with a fresh handoff now.' });
1203
+ }
1204
+ if (limits.usageTokens >= taskBoundaryThreshold) {
1205
+ return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
1206
+ action: 'rollover',
1207
+ guidance: 'Finish the acceptance closeout, persist only durable reusable conclusions, collect or stop jobs, then call context_rollover with a fresh handoff covering every other active Claim. Do not return to an old checkpoint solely because this Task was accepted.' });
1208
+ }
1209
+ return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
1210
+ action: 'keep',
1211
+ guidance: 'Keep the current context for possible acceptance follow-up. This acceptance is already a timeline boundary; do not create a redundant checkpoint. Record a checkpoint only before the next noisy or risky phase.' });
1212
+ }
1213
+ catch {
1214
+ // Any measurement failure degrades explicitly; the read stays durable.
1215
+ return this.unavailableAdvice();
1216
+ }
1217
+ }
1218
+ unavailableAdvice() {
1219
+ return Object.freeze({ usageTokens: undefined, taskBoundaryThreshold: undefined, handoffAt: undefined, hardLimit: undefined,
1220
+ action: 'unavailable',
1221
+ guidance: 'Context usage could not be measured for this acceptance; manage context by your existing pressure policy.' });
899
1222
  }
900
1223
  /**
901
1224
  * Agent-only direct message: append the audit-only dm-sent operation, then
@@ -958,6 +1281,663 @@ let AgentTeam = (() => {
958
1281
  validateLedger() {
959
1282
  this.requireLedger().validate();
960
1283
  }
1284
+ /**
1285
+ * Effective context-pressure budget for one Member's current route:
1286
+ * `hardLimit = min(256K, routeWindow - outputReserve)` and
1287
+ * `handoffAt = min(200K, hardLimit - handoffReserve)`.
1288
+ */
1289
+ contextLimits(routeWindow) {
1290
+ const hardLimit = Math.min(CONTEXT_HARD_LIMIT_CAP, Math.max(0, routeWindow - CONTEXT_SAFE_OUTPUT_RESERVE));
1291
+ const handoffAt = Math.min(CONTEXT_HANDOFF_AT_CAP, Math.max(0, hardLimit - CONTEXT_HANDOFF_RESERVE));
1292
+ return { hardLimit, handoffAt };
1293
+ }
1294
+ /**
1295
+ * Budget + measurement for one Member's CURRENT routed selection — the
1296
+ * member-pinned model or the default selection, resolved through the LLM
1297
+ * service so a route change is honored at the next pre-step. The last
1298
+ * persisted `request/context` event wins when it matches the current
1299
+ * selection (no directory round-trip for an unchanged route). A selection
1300
+ * whose capacity cannot be resolved returns `undefined` and the pressure
1301
+ * policy fails closed instead of assuming an unbounded route.
1302
+ */
1303
+ async routeLimitsForAgent(agent) {
1304
+ const member = this.memberForAgent(agent);
1305
+ if (member === undefined)
1306
+ return undefined;
1307
+ // The pressure budget must match the route the CURRENT step actually
1308
+ // uses: the live selection ref's `assembled` capture when the step has
1309
+ // entered prompt assembly, its `current` selection otherwise. Re-deriving
1310
+ // from the ledger or the Host default would race a concurrent default
1311
+ // change or live model edit and split the budget from the real route.
1312
+ const ref = this.modelSelections.get(member.memberId);
1313
+ const selection = ref?.assembled ?? ref?.current ?? member.model ?? this.ctx.agentDefaultModel.currentSelection();
1314
+ const meter = agent.ctx.get('tokenMeter');
1315
+ const usageTokens = meter?.measure(agent.session)?.totalTokens;
1316
+ if (usageTokens === undefined)
1317
+ return undefined;
1318
+ const persisted = agent.session.requestContext();
1319
+ let contextWindow;
1320
+ if (persisted?.provider === selection.provider && persisted.model === selection.model) {
1321
+ contextWindow = persisted.contextWindow;
1322
+ }
1323
+ if (contextWindow === undefined) {
1324
+ contextWindow = await this.resolvedContextWindow(selection);
1325
+ }
1326
+ if (contextWindow === undefined)
1327
+ return undefined;
1328
+ const { hardLimit, handoffAt } = this.contextLimits(contextWindow);
1329
+ return { usageTokens, hardLimit, handoffAt };
1330
+ }
1331
+ /** Cache of resolved context windows per provider/model; unknown stays unknown. */
1332
+ routeWindows = new Map();
1333
+ /**
1334
+ * Resolve one selection's provider-owned context capacity, cached per
1335
+ * route. The LLM service is resolved lazily (not a hard inject): the Host
1336
+ * mounts it in every production shape, while ledger-level fixtures provide
1337
+ * only the services the Team service itself owns. A missing or throwing
1338
+ * resolution is an unknown route — the pressure policy fails closed on it.
1339
+ */
1340
+ async resolvedContextWindow(selection) {
1341
+ const key = `${selection.provider}::${selection.model}`;
1342
+ if (this.routeWindows.has(key))
1343
+ return this.routeWindows.get(key);
1344
+ let contextWindow;
1345
+ try {
1346
+ const llm = this.ctx.get('llm');
1347
+ const info = llm === undefined ? undefined : await llm.resolveModelInfo(selection.provider, selection.model);
1348
+ contextWindow = info?.context?.contextWindow;
1349
+ }
1350
+ catch {
1351
+ contextWindow = undefined;
1352
+ }
1353
+ this.routeWindows.set(key, contextWindow);
1354
+ return contextWindow;
1355
+ }
1356
+ /**
1357
+ * Resolve one checkpoint ref to its exact seed prefix before any rollover
1358
+ * commit. The walk covers the current generation (own events) and archived
1359
+ * ancestors through `sessionPersistence`; the same projection definition
1360
+ * folds every source. Guards fail closed: unresolved, foreign-lineage,
1361
+ * open-turn, or nonshrinking targets reject without any lifecycle effect.
1362
+ */
1363
+ async resolveCheckpointSeed(memberId, agent, checkpointRef) {
1364
+ let sessionId = agent.session.id;
1365
+ let live = true;
1366
+ let guard = 0;
1367
+ while (sessionId !== undefined && guard++ < MAX_TIMELINE_ANCESTORS) {
1368
+ let events;
1369
+ let inheritedEventCount;
1370
+ let parentSession;
1371
+ // Capture this iteration's source identity BEFORE the branch advances
1372
+ // the lineage flag: the measurement call below keys on it.
1373
+ const sourceIsCurrent = live;
1374
+ if (live) {
1375
+ events = agent.session.snapshotEvents();
1376
+ inheritedEventCount = agent.session.inheritedEventCount;
1377
+ parentSession = agent.session.header.parentSession;
1378
+ live = false;
1379
+ }
1380
+ else {
1381
+ try {
1382
+ const inspection = await this.ctx.sessionPersistence.inspect(sessionId);
1383
+ events = inspection.events;
1384
+ inheritedEventCount = inspection.inheritedEventCount;
1385
+ parentSession = inspection.meta.parentSession;
1386
+ }
1387
+ catch (error) {
1388
+ throw new Error(`checkpoint '${checkpointRef}' could not be resolved: its source Session is unreadable (${error instanceof Error ? error.message : String(error)})`);
1389
+ }
1390
+ }
1391
+ // Fold the source with its inherited cut respected: inherited events
1392
+ // are resolved history in that source, never fresh intent; checkpoints
1393
+ // recorded in this source's own span are the selectable targets.
1394
+ const state = foldContextProjection(events, inheritedEventCount, sessionId);
1395
+ // A Team-boundary default checkpoint: the boundary's completed-turn
1396
+ // anchor is the seed cut, and it is selectable exactly when one
1397
+ // Thread's facts entered the context through it — the same proof the
1398
+ // timeline requires, revalidated here because the model may cite a
1399
+ // boundary the timeline never surfaced.
1400
+ const boundary = checkpointRef.startsWith('team-boundary-')
1401
+ ? state.boundaries.find(entry => entry.key === checkpointRef)
1402
+ : undefined;
1403
+ const entry = checkpointByRef(state, checkpointRef);
1404
+ const anchorTurnEndSeq = boundary !== undefined ? boundary.turnEndSeq : entry?.turnEndSeq;
1405
+ if (anchorTurnEndSeq !== undefined && anchorTurnEndSeq !== -1) {
1406
+ if (boundary !== undefined && boundary.source !== 'team-boundary') {
1407
+ throw new Error(`checkpoint '${checkpointRef}' is not a restorable boundary`);
1408
+ }
1409
+ // The seed is the exact contiguous prefix through the anchor's
1410
+ // completed turn end. Balanced by construction — the turn ended.
1411
+ const throughSeq = anchorTurnEndSeq + 1;
1412
+ const prefix = events.slice(0, throughSeq);
1413
+ if (boundary !== undefined) {
1414
+ const threads = this.threadsEnteringContext(events, anchorTurnEndSeq);
1415
+ if (threads.length !== 1) {
1416
+ throw new Error(threads.length === 0
1417
+ ? `boundary '${checkpointRef}' has no single attributable Thread; write a fresh handoff instead`
1418
+ : `boundary '${checkpointRef}' spans multiple Threads; write a fresh handoff instead`);
1419
+ }
1420
+ }
1421
+ // Nonshrinking guard, priced by the SAME source-replayed measurement
1422
+ // the timeline shows: the seed's retained cost is the SOURCE's own
1423
+ // token count scaled by the anchor share — a small current generation
1424
+ // never disguises a large ancestor seed. An unmeasurable source fails
1425
+ // closed: pricing the unknown as zero would wave an oversized seed
1426
+ // through.
1427
+ const limits = await this.routeLimitsForAgent(agent);
1428
+ const handoffAt = limits?.handoffAt ?? CONTEXT_HANDOFF_AT_CAP;
1429
+ const sourceUsage = await this.sourceUsageTokens(sessionId, sourceIsCurrent, agent);
1430
+ if (sourceUsage === undefined) {
1431
+ throw new Error(`checkpoint '${checkpointRef}' could not be priced: its source Session's context cost cannot be measured; write a fresh handoff instead`);
1432
+ }
1433
+ const retained = this.retainedEstimate(sourceUsage, events.length, anchorTurnEndSeq);
1434
+ if (prefix.length >= events.length) {
1435
+ throw new Error('checkpoint return does not shrink the working set; use a fresh handoff instead');
1436
+ }
1437
+ if (retained >= handoffAt) {
1438
+ throw new Error('checkpoint return would retain a context at or above the handoff budget; use a fresh handoff instead');
1439
+ }
1440
+ // Single-Thread coverage guard: with more than one active Claim the
1441
+ // Host cannot prove a rewind stays inside one Thread's context.
1442
+ if (this.requireLedger().activeClaimCountForMember(memberId) > 1) {
1443
+ throw new Error('multiple active Claims: write a fresh handoff covering all of them instead of returning to a checkpoint');
1444
+ }
1445
+ return { checkpointRef, sourceSessionId: sessionId, sourceThroughSeq: SessionLogOffset(throughSeq), prefix };
1446
+ }
1447
+ sessionId = parentSession;
1448
+ }
1449
+ throw new Error(`checkpoint '${checkpointRef}' does not resolve in this Member's lineage`);
1450
+ }
1451
+ /**
1452
+ * Rebuild the handoff for a Member whose rollover committed but whose new
1453
+ * Session activated without the handoff delivery (a crash between the
1454
+ * ledger commit and the swap's delivery step). The previous Session —
1455
+ * recorded in the operation, mirrored by the lineage parent, and available
1456
+ * from the ledger even when the new Session's own header never carried it —
1457
+ * holds the durable intent; fold it cold and deliver the same handoff
1458
+ * envelope first. Idempotent: once any handoff exists in the new Session's
1459
+ * own log this never runs.
1460
+ */
1461
+ async reconstructMissingHandoff(member, agent) {
1462
+ const previousSessionId = agent.session.header.parentSession
1463
+ ?? this.requireLedger().previousSessionForMember(member.memberId);
1464
+ if (previousSessionId === undefined)
1465
+ return;
1466
+ let inspection;
1467
+ try {
1468
+ const result = await this.ctx.sessionPersistence.inspect(previousSessionId);
1469
+ inspection = result;
1470
+ }
1471
+ catch (error) {
1472
+ this.ctx.logger.warn(`agent-team: rollover handoff reconstruction could not read the previous Session '${previousSessionId}': ${error instanceof Error ? error.message : String(error)}`);
1473
+ return;
1474
+ }
1475
+ const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, previousSessionId);
1476
+ if (state.pending === null)
1477
+ return;
1478
+ const pending = state.pending;
1479
+ const message = createHandoffMessage({
1480
+ handoff: pending.handoff,
1481
+ previousSessionId,
1482
+ newSessionId: agent.session.id,
1483
+ trigger: 'model',
1484
+ handoffEventSeq: pending.resultSeq,
1485
+ ...(pending.checkpointRef === undefined ? {} : { checkpointRef: pending.checkpointRef }),
1486
+ ...(pending.relatedFiles.length === 0 ? {} : { relatedFiles: pending.relatedFiles }),
1487
+ });
1488
+ agent.steer(message);
1489
+ this.ctx.logger.info(`agent-team: reconstructed the rollover handoff for member '${this.memberLabel(member.memberId)}' from the previous Session '${previousSessionId}'`);
1490
+ }
1491
+ /**
1492
+ * Resolve the exact recorded seed prefix of one committed checkpoint
1493
+ * return for crash recovery: cold-read the recorded source Session, take
1494
+ * the contiguous prefix of the recorded exclusive length, and verify the
1495
+ * source's own fold still resolves the recorded anchor there — an explicit
1496
+ * checkpoint through `checkpointByRef`, or a default Team boundary whose
1497
+ * recorded ref appears among the source's own boundary keys.
1498
+ * Fail-closed by contract: an unreadable source or an unprovable anchor
1499
+ * REJECTS the activation — a committed checkpoint return may never
1500
+ * downgrade to a blank child.
1501
+ */
1502
+ async recordedCheckpointPrefix(seed) {
1503
+ let inspection;
1504
+ try {
1505
+ inspection = await this.ctx.sessionPersistence.inspect(seed.sourceSessionId);
1506
+ }
1507
+ catch (error) {
1508
+ throw new Error(`the recorded checkpoint-return seed source Session '${seed.sourceSessionId}' is unreadable: ${error instanceof Error ? error.message : String(error)}`);
1509
+ }
1510
+ const through = Number(seed.sourceThroughSeq);
1511
+ if (!Number.isSafeInteger(through) || through < 0 || through > inspection.events.length) {
1512
+ throw new Error(`the recorded checkpoint-return seed cut ${through} is not a valid prefix of Session '${seed.sourceSessionId}'`);
1513
+ }
1514
+ const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, seed.sourceSessionId);
1515
+ const anchorProven = checkpointByRef(state, seed.checkpointRef) !== undefined
1516
+ || state.boundaries.some(boundary => boundary.key === seed.checkpointRef);
1517
+ if (!anchorProven) {
1518
+ throw new Error(`the recorded checkpoint-return anchor '${seed.checkpointRef}' no longer resolves in Session '${seed.sourceSessionId}'`);
1519
+ }
1520
+ return { prefix: inspection.events.slice(0, through) };
1521
+ }
1522
+ /**
1523
+ * Redeliver the old generation's unconsumed post-intent input to a
1524
+ * generation whose durable rollover committed but whose delivery did not
1525
+ * finish. Bound to the ledger's recorded transition target — the CURRENT
1526
+ * Session being that target — never to whether the handoff itself landed,
1527
+ * so a crash after the handoff but before the carried enqueue still
1528
+ * replays. The fold is the same durable truth the live transition uses
1529
+ * (unconsumed, non-Team, order preserved). Idempotency keys on what is
1530
+ * CURRENTLY present: delivered `user/message` ids plus the live inbox's
1531
+ * pending next-step/next-turn ids — a historical insert that was already
1532
+ * claimed (and removed) but never surfaced is NOT known and must replay.
1533
+ * Fail-closed: an unreadable previous Session rejects the activation
1534
+ * rather than silently dropping the Member's input.
1535
+ */
1536
+ async replayCarriedInput(member, agent) {
1537
+ const transition = this.requireLedger().lastTransitionForMember(member.memberId);
1538
+ if (transition === undefined || transition.targetSessionId !== agent.session.id)
1539
+ return 0;
1540
+ let inspection;
1541
+ try {
1542
+ inspection = await this.ctx.sessionPersistence.inspect(transition.previousSessionId);
1543
+ }
1544
+ catch (error) {
1545
+ throw new Error(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable: ${error instanceof Error ? error.message : String(error)}`);
1546
+ }
1547
+ const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, transition.previousSessionId);
1548
+ const carried = carriedInputOf(state);
1549
+ if (carried.length === 0)
1550
+ return 0;
1551
+ const known = new Set();
1552
+ for (const event of agent.session.ownEvents()) {
1553
+ if (event.type !== 'user/message')
1554
+ continue;
1555
+ const id = event.data.id;
1556
+ if (id !== undefined)
1557
+ known.add(id);
1558
+ }
1559
+ for (const pending of [...agent.inbox.nextStep, ...agent.inbox.nextTurn]) {
1560
+ known.add(pending.id);
1561
+ }
1562
+ let redelivered = 0;
1563
+ for (const message of carried) {
1564
+ if (known.has(message.id))
1565
+ continue;
1566
+ agent.followup(message);
1567
+ redelivered += 1;
1568
+ }
1569
+ if (redelivered > 0)
1570
+ this.ctx.logger.info(`agent-team: redelivered ${redelivered} carried input message(s) for member '${this.memberLabel(member.memberId)}' after the rollover crash recovery`);
1571
+ return redelivered;
1572
+ }
1573
+ /**
1574
+ * Agent-only checkpoint request validation: the tool calls this inside its
1575
+ * own running turn. Like `context_rollover`, the tool performs no side effect —
1576
+ * the durable checkpoint is the successful `tool/call`+`tool/result` pair
1577
+ * the Session projection folds; the ref returned here is deterministic
1578
+ * from this Member Session's identity plus the tool call id, so the model
1579
+ * can cite it before the result exists and a repeated provider call id in
1580
+ * another generation never collides with this one.
1581
+ */
1582
+ recordCheckpointForAgent(agent, request) {
1583
+ const member = this.memberForAgent(agent);
1584
+ if (member === undefined || member.state !== 'enabled')
1585
+ throw new Error('context_checkpoint requires an active Team Member');
1586
+ if (this.runningAgents.has(agent.id) !== true)
1587
+ throw new Error('context_checkpoint must run inside this Member\'s own running turn');
1588
+ const name = request.name.trim();
1589
+ if (name === '')
1590
+ throw new Error('context_checkpoint requires a non-empty name');
1591
+ if (name.length > MAX_CHECKPOINT_NAME_CHARS)
1592
+ throw new Error(`context_checkpoint name exceeds ${MAX_CHECKPOINT_NAME_CHARS} characters`);
1593
+ return { checkpointRef: checkpointRefFor(agent.session.id, request.callId), name };
1594
+ }
1595
+ /**
1596
+ * Agent-only bounded structural timeline: resolved checkpoints plus Team
1597
+ * delivery, handoff, and compaction boundaries across the current
1598
+ * generation and its archived ancestor lineage. Structural only — no
1599
+ * transcript content. The meter prices retained/discarded tokens; entries
1600
+ * the Host cannot prove restorable carry a rejection reason instead of
1601
+ * silently disappearing.
1602
+ */
1603
+ async contextTimelineForAgent(agent, request) {
1604
+ const member = this.memberForAgent(agent);
1605
+ if (member === undefined || member.state !== 'enabled')
1606
+ throw new Error('context_timeline requires an active Team Member');
1607
+ const limit = request.limit === undefined ? DEFAULT_TIMELINE_LIMIT : Math.trunc(request.limit);
1608
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_TIMELINE_LIMIT)
1609
+ throw new Error(`context_timeline limit must be between 1 and ${MAX_TIMELINE_LIMIT}`);
1610
+ const meter = agent.ctx.get('tokenMeter');
1611
+ const measurement = meter?.measure(agent.session);
1612
+ const usageTokens = measurement?.totalTokens ?? 0;
1613
+ const limits = await this.routeLimitsForAgent(agent);
1614
+ const hardLimit = limits?.hardLimit ?? CONTEXT_HARD_LIMIT_CAP;
1615
+ const handoffAt = limits?.handoffAt ?? CONTEXT_HANDOFF_AT_CAP;
1616
+ // Fold the current generation, then walk archived ancestors through their
1617
+ // persisted logs; the same projection definition folds every source.
1618
+ const items = [];
1619
+ const seen = new Set();
1620
+ let sessionId = member.sessionId;
1621
+ let live = true;
1622
+ let guard = 0;
1623
+ while (sessionId !== undefined && guard++ < MAX_TIMELINE_ANCESTORS) {
1624
+ let state;
1625
+ let sourceSessionId = sessionId;
1626
+ let sourceEvents = [];
1627
+ // Capture this iteration's source identity BEFORE the branch advances
1628
+ // the lineage flag: measurement and discard pricing both key on it.
1629
+ const sourceIsCurrent = live;
1630
+ if (live) {
1631
+ state = foldContextProjection(agent.session.ownEvents(), agent.session.inheritedEventCount, agent.session.id);
1632
+ sourceEvents = agent.session.snapshotEvents();
1633
+ sessionId = agent.session.header.parentSession;
1634
+ live = false;
1635
+ }
1636
+ else {
1637
+ try {
1638
+ const inspection = await this.ctx.sessionPersistence.inspect(sessionId);
1639
+ state = foldContextProjection(inspection.events, inspection.inheritedEventCount, sessionId);
1640
+ sourceEvents = inspection.events;
1641
+ sourceSessionId = sessionId;
1642
+ sessionId = inspection.meta.parentSession;
1643
+ }
1644
+ catch {
1645
+ // An unreadable ancestor ends the lineage walk here.
1646
+ sessionId = undefined;
1647
+ }
1648
+ }
1649
+ if (state === undefined)
1650
+ break;
1651
+ // Price every candidate of this source against the SOURCE's own
1652
+ // replayed measurement — a small current generation never shrinks a
1653
+ // large ancestor's real seed cost. An unmeasurable source (no meter,
1654
+ // unreadable ancestor) prices as UNKNOWN, never as zero: the timeline
1655
+ // marks its candidates unprovable and the return guard rejects them.
1656
+ const sourceUsage = await this.sourceUsageTokens(sourceSessionId, sourceIsCurrent, agent);
1657
+ for (const candidate of timelineCandidates(state, limit)) {
1658
+ if (seen.has(candidate.ref))
1659
+ continue;
1660
+ seen.add(candidate.ref);
1661
+ items.push(this.timelineItemFor(candidate, sourceUsage, usageTokens, hardLimit, handoffAt, sourceSessionId, sourceIsCurrent, sourceEvents));
1662
+ if (items.length >= limit)
1663
+ break;
1664
+ }
1665
+ if (items.length >= limit)
1666
+ break;
1667
+ }
1668
+ return { usageTokens, hardLimit, handoffAt, items };
1669
+ }
1670
+ /**
1671
+ * Replayed measurement of one lineage source: the live current Session
1672
+ * measures directly; an archived ancestor measures through a borrowed
1673
+ * prepared Session, so a seed's retained cost is priced in the SOURCE's
1674
+ * own tokens — never the current generation's. Returns undefined when no
1675
+ * meter is available or the source cannot be borrowed; callers fail
1676
+ * closed on the unknown.
1677
+ */
1678
+ async sourceUsageTokens(sessionId, live, agent) {
1679
+ const meter = agent.ctx.get('tokenMeter');
1680
+ if (meter === undefined)
1681
+ return undefined;
1682
+ if (live)
1683
+ return meter.measure(agent.session)?.totalTokens;
1684
+ try {
1685
+ const env_1 = { stack: [], error: void 0, hasError: false };
1686
+ try {
1687
+ const borrowed = __addDisposableResource(env_1, await this.ctx.sessionPersistence.borrowSession(sessionId)
1688
+ // The borrow resolves the exact Session object for both shapes: the
1689
+ // live instance for an attached Session, the prepared one for an
1690
+ // archived ancestor. Measuring through the service context's store
1691
+ // would need an inject this service does not declare.
1692
+ , false);
1693
+ // The borrow resolves the exact Session object for both shapes: the
1694
+ // live instance for an attached Session, the prepared one for an
1695
+ // archived ancestor. Measuring through the service context's store
1696
+ // would need an inject this service does not declare.
1697
+ const session = borrowed.source === 'live'
1698
+ ? agent.session.id === sessionId ? agent.session : undefined
1699
+ : borrowed.preparedSession;
1700
+ return session === undefined ? undefined : meter.measure(session)?.totalTokens;
1701
+ }
1702
+ catch (e_1) {
1703
+ env_1.error = e_1;
1704
+ env_1.hasError = true;
1705
+ }
1706
+ finally {
1707
+ __disposeResources(env_1);
1708
+ }
1709
+ }
1710
+ catch {
1711
+ return undefined;
1712
+ }
1713
+ }
1714
+ /**
1715
+ * Monotonic anchor-share estimate of a seed's retained cost, priced in the
1716
+ * SOURCE Session's own measurement: the fraction of the source log the
1717
+ * seed prefix covers, scaled to the source's replayed token count. The
1718
+ * anchor position is exact and the share grows monotonically toward the
1719
+ * source's head (100%). A large ancestor's anchor therefore prices at the
1720
+ * ancestor's real size even inside a small current generation — the
1721
+ * timeline display and the return guard share this one estimate.
1722
+ */
1723
+ retainedEstimate(sourceUsageTokens, sourceLength, anchorTurnEndSeq) {
1724
+ if (sourceLength <= 0)
1725
+ return sourceUsageTokens;
1726
+ const share = Math.min(1, Math.max(0, (anchorTurnEndSeq + 1) / sourceLength));
1727
+ return Math.round(sourceUsageTokens * share);
1728
+ }
1729
+ /**
1730
+ * Threads whose facts entered this Session's model context by the given
1731
+ * seq: delivered Team notices (their bodies quote `Thread: <ref>`
1732
+ * structurally) and successful Team-claim mutations (their task overlays
1733
+ * resolve to Threads through the ledger). Never from unread ledger
1734
+ * activity — a Thread the Member never saw did not enter its context.
1735
+ * Order-stable, deduplicated.
1736
+ */
1737
+ threadsEnteringContext(events, throughSeq) {
1738
+ const refs = [];
1739
+ const openAttributions = new Map();
1740
+ const push = (ref) => {
1741
+ if (ref !== undefined && !refs.includes(ref))
1742
+ refs.push(ref);
1743
+ };
1744
+ for (const event of events) {
1745
+ if (event.seq > throughSeq)
1746
+ break;
1747
+ if (event.type === 'tool/call' && (event.data.name === 'team_claim' || event.data.name === 'team_message')) {
1748
+ openAttributions.set(event.data.callId, { name: event.data.name, arguments: event.data.arguments });
1749
+ }
1750
+ else if (event.type === 'tool/result') {
1751
+ const block = event.data.message.content?.[0];
1752
+ if (block !== undefined && block.toolCallId !== undefined) {
1753
+ const recorded = openAttributions.get(block.toolCallId);
1754
+ if (recorded !== undefined && block.isError !== true) {
1755
+ openAttributions.delete(block.toolCallId);
1756
+ try {
1757
+ const args = JSON.parse(recorded.arguments);
1758
+ // A claim mutation resolves its Task overlay through the ledger;
1759
+ // a team_message committed reply names its Thread in its call
1760
+ // arguments; a committed start's Thread is born in the result
1761
+ // and is read from the durable presentation meta.
1762
+ if (recorded.name === 'team_claim' && typeof args.taskRef === 'string' && args.taskRef !== '') {
1763
+ push(this.requireLedger().threadForTask(args.taskRef));
1764
+ }
1765
+ else if (recorded.name === 'team_message') {
1766
+ const meta = event.data.meta;
1767
+ if (meta !== undefined && typeof meta === 'object' && meta.kind === 'committed') {
1768
+ const metaThreadRef = meta.threadRef;
1769
+ if (typeof metaThreadRef === 'string' && metaThreadRef !== '')
1770
+ push(metaThreadRef);
1771
+ }
1772
+ else if (typeof args.threadRef === 'string' && args.threadRef !== '') {
1773
+ push(args.threadRef);
1774
+ }
1775
+ }
1776
+ }
1777
+ catch {
1778
+ // Malformed call arguments contribute no attribution.
1779
+ }
1780
+ }
1781
+ }
1782
+ }
1783
+ else if (event.type === 'user/message') {
1784
+ const source = event.data.source;
1785
+ if (source?.kind !== 'plugin')
1786
+ continue;
1787
+ // Reminder notices never enter attribution — a progress nudge or a
1788
+ // recovery instruction is not a Team fact.
1789
+ if (source.form === 'notice' && source.summary !== undefined && isReminderNoticeSummary(source.summary))
1790
+ continue;
1791
+ const text = event.data.content.filter(block => block.type === 'text').map(block => block.text ?? '').join('\n');
1792
+ for (const match of text.matchAll(/Thread: (thread:[0-9a-f-]{6,})/g)) {
1793
+ push(match[1]);
1794
+ }
1795
+ }
1796
+ }
1797
+ return refs;
1798
+ }
1799
+ /**
1800
+ * Price and annotate one timeline candidate without mutating anything.
1801
+ * Retained prices in the SOURCE Session's own replayed measurement (the
1802
+ * monotonic anchor-share of the source log); discarded is what a return
1803
+ * replaces — for a current-generation anchor, the measured usage beyond
1804
+ * the anchor; for an ancestor anchor, the current generation's whole
1805
+ * usage (an approximation: the ancestor's own suffix is not part of this
1806
+ * generation). A small current child therefore discards little but may
1807
+ * still retain a large ancestor seed, and both numbers say so honestly.
1808
+ */
1809
+ timelineItemFor(candidate, sourceUsage, currentUsage, _hardLimit, handoffAt, sourceSessionId, sourceIsCurrent, sourceEvents) {
1810
+ const retainedTokens = candidate.source === 'head'
1811
+ ? currentUsage
1812
+ : this.retainedEstimate(sourceUsage ?? 0, sourceEvents.length, candidate.turnEndSeq);
1813
+ const discardedTokens = candidate.source === 'head'
1814
+ ? 0
1815
+ : sourceIsCurrent
1816
+ ? Math.max(0, currentUsage - retainedTokens)
1817
+ : currentUsage;
1818
+ const affectedThreads = candidate.turnEndSeq === -1 ? [] : this.threadsEnteringContext(sourceEvents, candidate.turnEndSeq);
1819
+ let restorable = candidate.turnEndSeq !== -1;
1820
+ let reason;
1821
+ if (candidate.source === 'head') {
1822
+ // The head is the current working set: returning to it discards
1823
+ // nothing and is never a meaningful return target.
1824
+ restorable = false;
1825
+ reason = 'the head is the current working set; returning to it discards nothing';
1826
+ }
1827
+ else if (sourceUsage === undefined) {
1828
+ // The source's cost cannot be measured (no meter, or the archived
1829
+ // ancestor cannot be borrowed): the budget cannot be proven, so the
1830
+ // candidate is not selectable. Never price an unknown as zero.
1831
+ restorable = false;
1832
+ reason = 'the source Session\'s context cost cannot be measured, so the return budget cannot be proven';
1833
+ }
1834
+ else if (candidate.source === 'handoff' || candidate.source === 'compaction') {
1835
+ // A handoff starts a generation and a compaction rewrites the visible
1836
+ // surface: rewinding into them is not a proven-safe V1 target.
1837
+ restorable = false;
1838
+ reason = `source '${candidate.source}' is not a restorable checkpoint`;
1839
+ }
1840
+ else if (candidate.source === 'team-boundary') {
1841
+ // A Team delivery is a selectable default checkpoint exactly when the
1842
+ // Host can prove it stays inside one Thread's context: the boundary
1843
+ // resolved at a completed turn AND exactly one Thread's facts entered
1844
+ // the Session context through it. Multi-Thread or unattributable
1845
+ // boundaries document why they are not selectable.
1846
+ if (affectedThreads.length !== 1) {
1847
+ restorable = false;
1848
+ reason = affectedThreads.length === 0
1849
+ ? 'no single Thread is attributable to this boundary'
1850
+ : 'multiple Threads entered the context through this boundary; write a fresh handoff instead';
1851
+ }
1852
+ else if (retainedTokens >= handoffAt) {
1853
+ restorable = false;
1854
+ reason = 'retained context would not materially shrink the working set';
1855
+ }
1856
+ }
1857
+ else if (candidate.source === 'agent' && retainedTokens >= handoffAt) {
1858
+ restorable = false;
1859
+ reason = 'retained context would be at or above the handoff budget';
1860
+ }
1861
+ return {
1862
+ checkpointRef: candidate.ref,
1863
+ name: candidate.label,
1864
+ source: candidate.source,
1865
+ retainedTokens,
1866
+ discardedTokens,
1867
+ affectedThreads,
1868
+ restorable,
1869
+ ...(reason === undefined ? {} : { reason }),
1870
+ ...(candidate.source === 'agent' ? {} : { sourceSessionId }),
1871
+ };
1872
+ }
1873
+ /**
1874
+ * Agent-only rollover request validation: the tool calls this to check its
1875
+ * Member binding, exclusivity, and checkpoint ownership. It performs no
1876
+ * lifecycle effect — the actual transition reacts to the successful tool
1877
+ * result through the context-management coordinator.
1878
+ */
1879
+ async requestNewContext(agent, request) {
1880
+ const member = this.memberForAgent(agent);
1881
+ if (member === undefined || member.state !== 'enabled')
1882
+ throw new Error('context_rollover requires an active Team Member');
1883
+ if (this.contextManagement.isTransitioning(member.memberId))
1884
+ throw new Error('a context rollover is already scheduled for this Member; wait for it to finish before requesting another');
1885
+ if (this.runningAgents.has(agent.id) !== true) {
1886
+ // The tool runs inside the Member's own turn, so a non-running agent at
1887
+ // this point is a harness anomaly; refuse rather than schedule a swap
1888
+ // outside the turn fence.
1889
+ throw new Error('context_rollover must run inside this Member\'s own running turn');
1890
+ }
1891
+ // Job ownership guard: disposing the old Agent cancels its running jobs
1892
+ // and orphaned terminal-but-unreported output would vanish with it. The
1893
+ // rejection names the jobs so the model can collect or stop them first;
1894
+ // the transition rechecks at the lifecycle commit seam because a job may
1895
+ // settle between this validation and the swap.
1896
+ const blocking = this.ownedJobsBlockingRollover(agent);
1897
+ if (blocking.length > 0) {
1898
+ throw new Error(`context_rollover is refused while this Member owns jobs that would not survive the switch (${blocking.join(', ')}); collect or stop them first, then retry`);
1899
+ }
1900
+ if (request.checkpointRef === undefined)
1901
+ return { mode: 'fresh' };
1902
+ if (!/^(context-checkpoint-[0-9a-f]{64}|team-boundary-[0-9a-f]{64})$/.test(request.checkpointRef))
1903
+ throw new Error('checkpointRef must be an opaque ref exactly as returned by context_timeline');
1904
+ // Full current-state prevalidation through the ONE resolver the swap
1905
+ // itself uses: a ref that is fabricated, unattributable, nonshrinking,
1906
+ // unmeasurable, over-budget, or blocked by multiple active Claims
1907
+ // rejects HERE — a model-visible error result instead of a fake
1908
+ // `scheduled` whose async swap always fails. The seed is resolved and
1909
+ // discarded: the transition seam resolves it again, so only the mutable
1910
+ // guard set (jobs, route limits, lineage growth) is revalidated there.
1911
+ await this.resolveCheckpointSeed(member.memberId, agent, request.checkpointRef);
1912
+ return { mode: 'from-checkpoint' };
1913
+ }
1914
+ /**
1915
+ * Jobs this Agent owns that cannot survive a generation swap: any
1916
+ * running/stopping job, and any settled job whose terminal output was
1917
+ * never reported (disposal would silently discard it). In-place hard
1918
+ * compaction is exempt — it never cancels the owner.
1919
+ */
1920
+ ownedJobsBlockingRollover(agent) {
1921
+ const jobs = agent.ctx.get('jobs');
1922
+ if (jobs === undefined)
1923
+ return [];
1924
+ return jobs.list(agent)
1925
+ .filter((job) => job.status === 'running' || job.status === 'stopping' || ((job.status === 'completed' || job.status === 'killed' || job.status === 'failed') && !job.reported))
1926
+ .map((job) => `${job.label} (${job.id})`);
1927
+ }
1928
+ /**
1929
+ * Agent-only session rollover commit: the Member actor must be the target
1930
+ * Member on its currently bound live Session. The Host performs the actual
1931
+ * generation swap around this write; the ledger records only the durable
1932
+ * binding transition and rollover audit envelope.
1933
+ */
1934
+ async rolloverSessionForAgent(agent, request) {
1935
+ const actor = this.memberCall(agent, request.workspaceId);
1936
+ const result = await this.requireLedger().rolloverMemberSession({ ...request, actor });
1937
+ if (result.committed)
1938
+ this.emitCommitted(result.value.receipt);
1939
+ return result.value;
1940
+ }
961
1941
  emitCommittedOutcome(result) {
962
1942
  if (result.committed && result.value.kind === 'committed' && result.value.receipt !== undefined)
963
1943
  this.emitCommitted(result.value.receipt);
@@ -1036,16 +2016,40 @@ let AgentTeam = (() => {
1036
2016
  throw error;
1037
2017
  }
1038
2018
  }
1039
- async activateMember(member, knownWorkspacePath, knownSessions, forkedFrom) {
2019
+ /**
2020
+ * Session headers currently durable in the persistence backend.
2021
+ *
2022
+ * The Host retires a disposed Session's log without awaiting it, so a
2023
+ * concurrent activation can observe the JSONL backend's transient win32
2024
+ * staging directories (.dsh-mkdir-*) as ENOENT while they rename into
2025
+ * place. The read is idempotent; back off briefly instead of failing the
2026
+ * activation on a race the publisher resolves within milliseconds.
2027
+ */
2028
+ async persistedSessionHeaders() {
2029
+ for (let attempt = 0;; attempt += 1) {
2030
+ try {
2031
+ return await this.ctx.sessionPersistence.list();
2032
+ }
2033
+ catch (error) {
2034
+ if (attempt >= 3 || error?.code !== 'ENOENT')
2035
+ throw error;
2036
+ await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 25));
2037
+ }
2038
+ }
2039
+ }
2040
+ async activateMember(member, knownWorkspacePath, knownSessions, forkedFrom, options) {
1040
2041
  if (this.handles.has(member.memberId))
1041
2042
  return;
1042
2043
  let created;
1043
2044
  try {
1044
2045
  const workspace = this.requireWorkspace(member.workspaceId);
1045
2046
  const workspacePath = knownWorkspacePath ?? workspace.path;
1046
- await this.memberRuntime.initializePrivateMemory(member.privateMemoryPath);
2047
+ // Existing Members carry the pre-sanitization ledger path; activation
2048
+ // migrates it onto the sanitized directory before provisioning.
2049
+ const sanitizedMemoryPath = dshHomePath('agent-team', 'members', memberMemoryDirectoryName(member.memberId));
2050
+ await this.memberRuntime.initializePrivateMemory(sanitizedMemoryPath, member.privateMemoryPath);
1047
2051
  const persisted = knownSessions !== undefined ? knownSessions.has(member.sessionId)
1048
- : (await this.ctx.sessionPersistence.list()).some(header => header.id === member.sessionId);
2052
+ : await this.sessionPersisted(member.sessionId);
1049
2053
  // AgentOptions declares only provider/model. Install the full selection
1050
2054
  // through the public Agent model-selection seam so reasoning effort is
1051
2055
  // applied to the next request and not lost during activation.
@@ -1061,6 +2065,46 @@ let AgentTeam = (() => {
1061
2065
  this.memberRuntime.applyMemberToolPolicy(agentCtx, member);
1062
2066
  this.validateMemberPreset(agentCtx);
1063
2067
  installModelSelection(agentCtx, selected);
2068
+ // Admission gate for pending context rollovers: once a successful
2069
+ // context_rollover result is durable, queued input must not open another
2070
+ // old-generation model request. The turn-stop boundary captures the
2071
+ // inbox (non-Team input is carried to the new generation), and a
2072
+ // racing pre-step rejects instead of admitting claimed messages.
2073
+ agentCtx.on('agent/turn-stopping', ({ agent }) => {
2074
+ if (!this.contextManagement.needsAdmissionGate(agent))
2075
+ return;
2076
+ this.contextManagement.captureQueuedInput(agent);
2077
+ });
2078
+ agentCtx.on('agent/pre-step', async ({ agent, messages, signal }, next) => {
2079
+ // Check before AND after the waterfall: a rollover pending at either
2080
+ // edge must reject this old-generation step, preserving its claimed
2081
+ // input for the new generation instead of letting it run or drop.
2082
+ if (this.contextManagement.needsAdmissionGate(agent)) {
2083
+ this.contextManagement.captureClaimedInput(agent, messages);
2084
+ return { kind: 'reject' };
2085
+ }
2086
+ // Team pressure policy rides the same pre-step seam after the
2087
+ // admission gate: the handoff-budget notice steers into the running
2088
+ // turn, and the hard limit forces compaction before the request is
2089
+ // forwarded — failing closed blocks the step instead of submitting
2090
+ // over the Team limit. Missing route capacity is an explicit reject.
2091
+ const pressure = await this.pressurePolicy.onPreStep(agent, signal);
2092
+ if (pressure.kind === 'reject')
2093
+ return { kind: 'reject' };
2094
+ const decision = await next();
2095
+ if (decision.kind === 'reject' || !this.contextManagement.needsAdmissionGate(agent))
2096
+ return decision;
2097
+ this.contextManagement.captureClaimedInput(agent, messages);
2098
+ return { kind: 'reject' };
2099
+ });
2100
+ // Provider context-overflow recovery: one bounded compact-and-retry
2101
+ // sequence per failure chain through the Team-owned policy.
2102
+ agentCtx.on('agent/request-error', async ({ agent, failure, signal }, next) => {
2103
+ const retry = await this.pressurePolicy.onRequestError(agent, failure, signal);
2104
+ if (retry)
2105
+ return { kind: 'retry' };
2106
+ return next();
2107
+ });
1064
2108
  return {
1065
2109
  commit: () => {
1066
2110
  const agent = agentCtx.agent;
@@ -1077,24 +2121,112 @@ let AgentTeam = (() => {
1077
2121
  },
1078
2122
  };
1079
2123
  };
2124
+ // A renewal's create path parents at the ledger-recorded previous
2125
+ // Session even when the caller does not pass one: a crash between the
2126
+ // durable rollover commit and this activation recreates the Session
2127
+ // from the ledger binding alone, and the lineage parent is what makes
2128
+ // the missing handoff reconstructible on the NEXT failure. A recorded
2129
+ // checkpoint seed re-seeds the child here — the crash lost the process
2130
+ // memory, but the ledger envelope names the exact source prefix.
2131
+ let recordedSeed;
2132
+ let recordedSeedCut;
2133
+ const rolloverSeed = !persisted && options?.seed === undefined
2134
+ ? this.requireLedger().rolloverSeedForMember(member.memberId, member.sessionId)
2135
+ : undefined;
2136
+ if (rolloverSeed !== undefined) {
2137
+ // Fail-closed: an unreadable or unprovable recorded seed rejects
2138
+ // this activation (the catch below records the diagnostic) instead
2139
+ // of silently downgrading a committed checkpoint return to a blank
2140
+ // child.
2141
+ const resolved = await this.recordedCheckpointPrefix(rolloverSeed);
2142
+ recordedSeed = resolved.prefix;
2143
+ recordedSeedCut = SessionLogOffset(resolved.prefix.length);
2144
+ }
2145
+ const lineageParent = forkedFrom ?? (!persisted ? this.requireLedger().previousSessionForMember(member.memberId) : undefined);
2146
+ let recoveryCarried = 0;
2147
+ const seededFromLedger = recordedSeed !== undefined;
2148
+ const effectiveParent = recordedSeed !== undefined ? rolloverSeed.sourceSessionId : lineageParent;
2149
+ const seedOptions = options?.seed !== undefined && options.inheritedEventCount !== undefined
2150
+ ? { seed: options.seed, inheritedEventCount: options.inheritedEventCount }
2151
+ : recordedSeed !== undefined && recordedSeedCut !== undefined
2152
+ ? { seed: recordedSeed, inheritedEventCount: recordedSeedCut }
2153
+ : {};
1080
2154
  created = persisted
1081
2155
  ? await this.ctx.agents.resume({ resumeSessionId: member.sessionId, agentOptions, setup })
1082
2156
  : await this.ctx.agents.create({
1083
2157
  sessionId: member.sessionId,
1084
2158
  // A context renewal records its fork lineage so the archived
1085
- // previous Session stays discoverable from the durable header.
1086
- meta: { cwd: workspacePath, agentPreset: member.presetId, ...(forkedFrom === undefined ? {} : { parentSession: forkedFrom }) },
2159
+ // previous Session stays discoverable from the durable header; a
2160
+ // checkpoint return parents at the seed source and marks the
2161
+ // fork seeded with its exact inherited prefix length.
2162
+ meta: {
2163
+ cwd: workspacePath,
2164
+ agentPreset: member.presetId,
2165
+ ...(effectiveParent === undefined ? {} : { parentSession: effectiveParent }),
2166
+ ...(options?.seed === undefined && !seededFromLedger ? {} : { isSeeded: true }),
2167
+ },
2168
+ ...seedOptions,
1087
2169
  agentOptions,
1088
2170
  setup,
1089
2171
  });
1090
2172
  this.memberRuntime.mountMemberSkillProvider(member, created.agent.ctx, skillSelection);
1091
2173
  await workspace.attachSession(member.sessionId);
1092
2174
  this.handles.set(member.memberId, created);
2175
+ this.memberBySessionId.set(member.sessionId, member.memberId);
1093
2176
  this.modelSelections.set(member.memberId, selected);
1094
2177
  this.clearMemberFailure(member.memberId, 'activation');
1095
2178
  this.nameMemberSession(member, created.agent);
1096
- this.notifyMember(created.agent);
1097
- this.autoCompaction.activated(member.memberId);
2179
+ // The one-shot pressure notice needs no explicit re-arm here: it latches
2180
+ // on durable Session evidence, and a fresh generation's own event span
2181
+ // starts empty — a new Session is itself the re-arm.
2182
+ // A restart between a durable rollover intent and its swap replays the
2183
+ // old Session; the projection still carries the intent, so finish the
2184
+ // transition (or keep waiting for the containing turn) from here.
2185
+ if (persisted) {
2186
+ const state = foldContextProjection(created.agent.session.ownEvents(), created.agent.session.inheritedEventCount, created.agent.session.id);
2187
+ if (state.pending !== null)
2188
+ this.contextManagement.recoverPendingTransition(member.memberId, created.agent, member.sessionId);
2189
+ // A restart between one checkpoint's durable result and its quiet
2190
+ // follow-up delivery repairs exactly once; delivered continuations
2191
+ // stay delivered through the projection's own delivery record.
2192
+ this.contextManagement.repairContinuations(created.agent, state);
2193
+ // A restart after the rollover committed but before the handoff was
2194
+ // delivered activates the new Session with no handoff in its own
2195
+ // log — never treat that as an ordinary blank Member Session. The
2196
+ // operation's recorded previous Session (the lineage parent) still
2197
+ // holds the intent; rebuild the handoff from it.
2198
+ if (!state.boundaries.some(boundary => boundary.source === 'handoff')) {
2199
+ await this.reconstructMissingHandoff(member, created.agent);
2200
+ }
2201
+ // Carried input redelivery binds to the committed transition target —
2202
+ // this Session — never to the handoff's presence: a crash after the
2203
+ // handoff landed but before the carried enqueue still replays here.
2204
+ recoveryCarried = await this.replayCarriedInput(member, created.agent);
2205
+ }
2206
+ else if (forkedFrom === undefined && lineageParent !== undefined) {
2207
+ // A restart recreated a Session that never materialized before the
2208
+ // crash (the rollover committed, its activation failed, and this
2209
+ // create path just rebuilt it from the ledger binding alone): the
2210
+ // same handoff reconstruction applies from the ledger's recorded
2211
+ // previous Session, and the old generation's unconsumed post-intent
2212
+ // input — the durable fold, the same truth the live transition uses —
2213
+ // rides behind the handoff exactly as it would have. The rollover's
2214
+ // own activation passes its fork parent explicitly and delivers the
2215
+ // handoff right after — that delivery is the plan, never this
2216
+ // recovery.
2217
+ await this.reconstructMissingHandoff(member, created.agent);
2218
+ recoveryCarried = await this.replayCarriedInput(member, created.agent);
2219
+ }
2220
+ // The ordinary Inbox wake runs LAST, after any recovery delivery above:
2221
+ // the handoff must stay the first model-facing context of a recovered
2222
+ // generation, with carried input behind it and rederived Team facts
2223
+ // behind that. When recovery redelivered carried input, the wake is
2224
+ // sequenced behind it (its own follow-up turn) so the steer lane can
2225
+ // never leapfrog the carried messages. A rollover activation defers the
2226
+ // wake entirely — its caller delivers the handoff (and carried input)
2227
+ // first and rederives the Inbox afterwards.
2228
+ if (options?.deferNotify !== true)
2229
+ this.notifyMember(created.agent, recoveryCarried > 0);
1098
2230
  }
1099
2231
  catch (error) {
1100
2232
  await created?.dispose();
@@ -1180,6 +2312,13 @@ let AgentTeam = (() => {
1180
2312
  const handle = this.handles.get(member.memberId);
1181
2313
  if (handle === undefined)
1182
2314
  return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable' });
2315
+ // A rollover commits its ledger binding before the old generation retires
2316
+ // and the new one activates; during that window the live handle still runs
2317
+ // the previous Session. The Member stays visible but must not report the
2318
+ // new binding as active — a Client following the row would otherwise open
2319
+ // a Session that does not exist yet.
2320
+ if (handle.agent.id !== member.sessionId)
2321
+ return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic: 'context rollover in progress' });
1183
2322
  if (this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
1184
2323
  return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: ORPHANED_MEMBER_DIAGNOSTIC });
1185
2324
  }
@@ -1233,21 +2372,50 @@ let AgentTeam = (() => {
1233
2372
  }
1234
2373
  const ledger = this.requireLedger();
1235
2374
  this.emitChanged(ledger.changeScopesOf(operation));
2375
+ // Nudge reconciliation precedes notifyMember: a committed reply both
2376
+ // resets the author's silence and cancels any of its queued nudge before
2377
+ // the Inbox notice (higher priority) is considered.
2378
+ this.progressNudge.onCommitted(operation);
1236
2379
  for (const memberId of ledger.affectedMembersOf(operation)) {
1237
2380
  const handle = this.handles.get(memberId);
1238
2381
  if (handle !== undefined)
1239
2382
  this.notifyMember(handle.agent);
1240
2383
  }
1241
- const compactionMembers = acceptedTaskCompactionMembers(operation);
1242
- if (compactionMembers !== undefined)
1243
- this.autoCompaction.schedule(compactionMembers);
2384
+ // Task acceptance no longer schedules standalone auto compaction: it is
2385
+ // a semantic checkpoint/context cue (delivered as ordinary Team
2386
+ // notification), and the Team pressure policy owns compaction entry.
2387
+ }
2388
+ /** Model-visible active-Claim labels for the pressure notice. */
2389
+ activeClaimLabels(memberId) {
2390
+ const ledger = this.ledger;
2391
+ if (ledger === undefined)
2392
+ return [];
2393
+ const labels = [];
2394
+ for (const claim of ledger.activeClaimsForMember(memberId)) {
2395
+ if (claim.state !== 'active')
2396
+ continue;
2397
+ labels.push(`${claim.claimRef} (${claim.direction})`);
2398
+ }
2399
+ return labels;
2400
+ }
2401
+ /** Model-visible running/stopping job labels for the pressure notice. */
2402
+ runningJobLabels(memberId) {
2403
+ const handle = this.handles.get(memberId);
2404
+ if (handle === undefined)
2405
+ return [];
2406
+ const jobs = handle.agent.ctx.get('jobs');
2407
+ if (jobs === undefined)
2408
+ return [];
2409
+ return jobs.list(handle.agent)
2410
+ .filter((job) => job.status === 'running' || job.status === 'stopping')
2411
+ .map((job) => `${job.name ?? job.id}`);
1244
2412
  }
1245
2413
  emitAutoCompactionChanged(memberId) {
1246
2414
  const workspaceId = this.ledger?.getMember(memberId)?.workspaceId;
1247
2415
  this.emitChanged(workspaceId === undefined ? undefined : [{ kind: 'workspace', workspaceId }]);
1248
2416
  }
1249
2417
  /** Wake from durable unread state with bounded facts for direct and state-changing work. */
1250
- notifyMember(agent) {
2418
+ notifyMember(agent, sequenced = false) {
1251
2419
  const member = this.memberForAgent(agent);
1252
2420
  if (member === undefined || member.state !== 'enabled')
1253
2421
  return;
@@ -1256,6 +2424,12 @@ let AgentTeam = (() => {
1256
2424
  this.notifiedInbox.delete(member.memberId);
1257
2425
  return;
1258
2426
  }
2427
+ // Any ordinary next-turn input already queued (a rollover's or recovery's
2428
+ // carried messages) forces sequencing regardless of the caller: a steer
2429
+ // would claim the nearest step boundary and leapfrog the carried turn.
2430
+ if (!sequenced) {
2431
+ sequenced = agent.inbox.nextTurn.some(message => message.source.kind === 'user');
2432
+ }
1259
2433
  const signature = JSON.stringify(notifications.map(({ item }) => [
1260
2434
  item.thread.threadRef, item.thread.revision, item.unreadCount, item.directCount, item.newestSequence,
1261
2435
  ]));
@@ -1271,13 +2445,24 @@ let AgentTeam = (() => {
1271
2445
  const existingInboxHint = pending.find(message => this.isInboxNotice(message));
1272
2446
  if (existingInboxHint !== undefined)
1273
2447
  agent.inbox.remove(existingInboxHint.id);
2448
+ // A durable Inbox notice outranks a queued progress nudge; revoke it so
2449
+ // the model reads concrete unread work instead of a generic reminder.
2450
+ this.progressNudge.revokePendingNotice(member.memberId);
1274
2451
  const hint = createUserMessage({
1275
2452
  content: [{ type: 'text', text: this.notificationText(notifications, member.memberId) }],
1276
2453
  source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: INBOX_NOTICE_SUMMARY },
1277
2454
  });
1278
2455
  this.notifiedInbox.set(member.memberId, signature);
1279
2456
  try {
1280
- agent.steer(hint);
2457
+ // Steer normally claims the nearest step boundary — which would
2458
+ // preempt carried input that a rollover or crash recovery queued as
2459
+ // ordinary next-turn messages. The sequenced mode enqueues the
2460
+ // rederived Inbox as its own follow-up turn instead, so the delivery
2461
+ // order stays handoff → carried input → rederived Inbox.
2462
+ if (sequenced)
2463
+ agent.followup(hint);
2464
+ else
2465
+ agent.steer(hint);
1281
2466
  }
1282
2467
  catch (error) {
1283
2468
  this.clearMemberNotificationState(member.memberId);
@@ -1349,11 +2534,28 @@ let AgentTeam = (() => {
1349
2534
  activityNotification(activity, readerId) {
1350
2535
  const actor = activity.actor === AGENT_TEAM_HUMAN_MEMBER_ID
1351
2536
  ? 'human' : this.requireLedger().getMember(activity.actor)?.handle ?? activity.actor;
1352
- // Early acceptance completes the reader's own open Claim inside the accept
1353
- // operation: say so plainly, or the owner keeps working on a done Task.
1354
- if (activity.kind === 'accept' && activity.actor === AGENT_TEAM_HUMAN_MEMBER_ID && readerId !== undefined
1355
- && activity.completedClaimRefs?.some(claimRef => this.requireLedger().getClaim(claimRef)?.owner === readerId)) {
1356
- return `Team Task update\n${actor} accepted Task ${activity.taskRef} and your open Claim was completed with it. No further work is needed.`;
2537
+ // An acceptance concludes the reader's own Claims in two ways: a Claim
2538
+ // already finished before the accept was accepted (its work stands), and
2539
+ // a still-open Claim the accept completed atomically (no further work is
2540
+ // needed). Both must be said plainly, or the owner keeps working on a
2541
+ // done Task or never learns its contribution was accepted.
2542
+ if (activity.kind === 'accept' && activity.actor === AGENT_TEAM_HUMAN_MEMBER_ID && readerId !== undefined) {
2543
+ const acceptedOwn = (activity.acceptedClaimRefs ?? []).filter(claimRef => this.requireLedger().getClaim(claimRef)?.owner === readerId);
2544
+ const completedOwn = (activity.completedClaimRefs ?? []).filter(claimRef => this.requireLedger().getClaim(claimRef)?.owner === readerId);
2545
+ // The early-accept-only reader holds no finished Claim: every own Claim
2546
+ // was completed atomically, so the "finished Claim" clause's ref list
2547
+ // would be empty. Render the completed semantics alone instead of a
2548
+ // sentence with a dangling empty list.
2549
+ const finishedOwn = acceptedOwn.filter(claimRef => !completedOwn.includes(claimRef));
2550
+ if (completedOwn.length > 0 && finishedOwn.length > 0) {
2551
+ return `Team Task update\n${actor} accepted Task ${activity.taskRef}. Your Claim ${completedOwn.join(', ')} was completed with the acceptance, and your finished Claim ${finishedOwn.join(', ')} was accepted. No further work is needed.`;
2552
+ }
2553
+ if (completedOwn.length > 0) {
2554
+ return `Team Task update\n${actor} accepted Task ${activity.taskRef} and your open Claim ${completedOwn.join(', ')} was completed with it. No further work is needed.`;
2555
+ }
2556
+ if (acceptedOwn.length > 0) {
2557
+ return `Team Task update\n${actor} accepted Task ${activity.taskRef}; your finished Claim ${acceptedOwn.join(', ')} was accepted. No further work is needed.`;
2558
+ }
1357
2559
  }
1358
2560
  if (activity.kind === 'claim' || activity.kind === 'done' || activity.kind === 'release') {
1359
2561
  return `Team Task update\n${actor} ${activity.kind} Claim ${activity.claimRef} on Task ${activity.taskRef}.`;
@@ -1376,12 +2578,14 @@ let AgentTeam = (() => {
1376
2578
  await handle.dispose();
1377
2579
  this.handles.delete(memberId);
1378
2580
  }
2581
+ this.memberBySessionId.delete(member.sessionId);
1379
2582
  this.modelSelections.delete(memberId);
1380
2583
  this.memberRuntime.forgetMember(memberId);
1381
2584
  this.clearMemberFailure(memberId, 'activation');
1382
2585
  }
1383
2586
  clearMemberRecoveryState(member) {
1384
2587
  this.recovery.stopTracking(member.memberId);
2588
+ this.progressNudge.stopTracking(member.memberId);
1385
2589
  this.clearMemberFailure(member.memberId, 'runtime');
1386
2590
  this.clearMemberNotificationState(member.memberId);
1387
2591
  }