@wowyuarm/dsh-agent-team 0.1.10 → 0.1.12

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 (135) hide show
  1. package/README.md +17 -4
  2. package/README.zh.md +17 -4
  3. package/package.json +53 -47
  4. package/packages/agent-team/README.md +6 -4
  5. package/packages/agent-team/README.zh.md +6 -4
  6. package/packages/agent-team/lib/context-management.js +40 -50
  7. package/packages/agent-team/lib/context-projection.js +15 -1
  8. package/packages/agent-team/lib/index.js +244 -154
  9. package/packages/agent-team/lib/invariant.js +38 -4
  10. package/packages/agent-team/lib/ledger.js +775 -383
  11. package/packages/agent-team/lib/member-context.js +6 -2
  12. package/packages/agent-team/lib/member-runtime.js +47 -9
  13. package/packages/agent-team/lib/member-time-context.js +20 -5
  14. package/packages/agent-team/lib/mentions.js +56 -0
  15. package/packages/agent-team/lib/pressure-policy.js +33 -9
  16. package/packages/agent-team/lib/session-event-cursor.js +116 -0
  17. package/packages/agent-team/lib/session-remediation.js +38 -26
  18. package/packages/agent-team/lib/spec.js +47 -34
  19. package/packages/agent-team/lib/stored-session-reader.js +138 -0
  20. package/packages/agent-team/lib/typert.host.js +157 -41
  21. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  22. package/packages/agent-team/lib/typert.remote-client.js +129 -32
  23. package/packages/agent-team/lib/types/context-management.d.ts +7 -0
  24. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
  26. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/index.d.ts +77 -7
  28. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/ledger.d.ts +251 -20
  31. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/member-runtime.d.ts +2 -1
  34. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  35. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/mentions.d.ts +34 -0
  37. package/packages/agent-team/lib/types/mentions.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
  39. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
  41. package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
  42. package/packages/agent-team/lib/types/session-remediation.d.ts +23 -0
  43. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -1
  44. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  45. package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
  46. package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
  47. package/packages/agent-team/lib/types/types/entities.d.ts +28 -20
  48. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  49. package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
  50. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  51. package/packages/agent-team/lib/types/types/requests-results.d.ts +87 -4
  52. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  53. package/packages/agent-team/preset/team-member/agent.cordis.yml +49 -7
  54. package/packages/client-agent-team/README.md +1 -1
  55. package/packages/client-agent-team/README.zh.md +1 -1
  56. package/packages/client-agent-team/lib/client.js +2357 -1207
  57. package/packages/client-agent-team/lib/client.js.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  59. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +27 -7
  60. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +18 -0
  61. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -0
  62. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +22 -0
  63. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +3 -1
  64. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  65. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +167 -35
  66. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  67. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +23 -27
  68. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
  69. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/TeamComposer.js +26 -26
  71. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  72. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  73. package/packages/client-agent-team/lib/types/client/TeamConversation.js +9 -2
  74. package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts +20 -0
  75. package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts.map +1 -0
  76. package/packages/client-agent-team/lib/types/client/TeamCountBadge.js +25 -0
  77. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +25 -0
  78. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
  79. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +139 -0
  80. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
  82. package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts +43 -0
  83. package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts.map +1 -0
  84. package/packages/client-agent-team/lib/types/client/TeamMemberRow.js +27 -0
  85. package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts +2 -2
  86. package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts.map +1 -1
  87. package/packages/client-agent-team/lib/types/client/TeamMembersAction.js +2 -2
  88. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
  89. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  90. package/packages/client-agent-team/lib/types/client/TeamMessage.js +11 -4
  91. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
  92. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  93. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
  94. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  95. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +108 -30
  96. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  97. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  98. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +68 -7
  99. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  100. package/packages/client-agent-team/lib/types/client/index.js +19 -9
  101. package/packages/client-agent-team/lib/types/client/locales.d.ts +40 -2
  102. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  103. package/packages/client-agent-team/lib/types/client/locales.js +40 -2
  104. package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
  105. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  106. package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
  107. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
  108. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
  109. package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
  110. package/packages/client-agent-team/lib/types/client/slots.d.ts +7 -2
  111. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  112. package/packages/client-agent-team/lib/types/client/team-changes.d.ts +33 -2
  113. package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
  114. package/packages/client-agent-team/lib/types/client/team-changes.js +107 -25
  115. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
  116. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
  117. package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
  118. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +43 -1
  119. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  120. package/packages/client-agent-team/lib/types/client/team-formatters.js +81 -6
  121. package/packages/tool-agent-team/README.md +2 -2
  122. package/packages/tool-agent-team/README.zh.md +2 -2
  123. package/packages/tool-agent-team/lib/context-tools.js +18 -14
  124. package/packages/tool-agent-team/lib/host-access.js +20 -0
  125. package/packages/tool-agent-team/lib/index.js +44 -32
  126. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  127. package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
  128. package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
  129. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  130. package/packages/agent-team/lib/progress-nudge.js +0 -328
  131. package/packages/agent-team/lib/types/progress-nudge.d.ts +0 -136
  132. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +0 -1
  133. package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts +0 -3
  134. package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts.map +0 -1
  135. package/packages/client-agent-team/lib/types/client/TeamSettings.js +0 -5
@@ -47,29 +47,40 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
47
47
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
48
48
  import { scopeOf } from '@deepseek-ai/dsh-scope';
49
49
  import { Session, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
50
- import { SessionFormatUnsupportedError } from '@deepseek-ai/dsh-session-persistence';
51
50
  import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
52
51
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
53
52
  import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
54
53
  import { PressurePolicyCoordinator } from "./pressure-policy.js";
55
54
  import { ContextManagementCoordinator } from "./context-management.js";
56
55
  import { AGENT_TEAM_PLUGIN_ID, createHandoffMessage } from "./context-source.js";
57
- import { carriedInputOf, checkpointByRef, checkpointRefFor, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
56
+ import { carriedInputOf, checkpointByRef, checkpointRefFor, contextProjectionFold, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
57
+ import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
58
58
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
59
59
  import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, memberMemoryDirectoryName, MemberRuntime } from "./member-runtime.js";
60
- import { ProgressNudgeCoordinator } from "./progress-nudge.js";
61
60
  import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
62
61
  import { SessionRemediation, handoffAlreadyInLog } from "./session-remediation.js";
62
+ import { StoredSessionReadError, StoredSessionReader, sessionFailureOf } from "./stored-session-reader.js";
63
63
  import { agentTeamDomainSpec } from "./spec.js";
64
64
  import { formatTeamTimestamp } from "./time-format.js";
65
65
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
66
- export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
66
+ export { AGENT_TEAM_HUMAN_HANDLE, AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
67
67
  export { AGENT_TEAM_TOOL_NAMES } from "./member-runtime.js";
68
68
  /** Process-stable marker carried by the final Team message tool definition. */
69
69
  export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.preset');
70
70
  const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
71
71
  const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
72
72
  const ORPHANED_MEMBER_DIAGNOSTIC = 'Member preset composition was lost after a reload; its tools are unavailable. Resume rebuilds the member in place.';
73
+ /**
74
+ * A preset mount/validation failure during activation, carrying its own class
75
+ * so the activation diagnostic can route preset-composition failures (the
76
+ * install/runtime split failure mode) without matching message text.
77
+ */
78
+ class PresetCompositionError extends Error {
79
+ constructor(message, options) {
80
+ super(message, options);
81
+ this.name = 'PresetCompositionError';
82
+ }
83
+ }
73
84
  /** Longest accepted model-supplied checkpoint display name. */
74
85
  const MAX_CHECKPOINT_NAME_CHARS = 120;
75
86
  /** Default and maximum number of timeline items one query returns. */
@@ -95,6 +106,8 @@ function sameChangeScope(left, right) {
95
106
  return left.channelRef === right.channelRef;
96
107
  if (left.kind === 'thread' && right.kind === 'thread')
97
108
  return left.threadRef === right.threadRef;
109
+ if (left.kind === 'presence' && right.kind === 'presence')
110
+ return left.workspaceId === right.workspaceId;
98
111
  return false;
99
112
  }
100
113
  /** Mark the preset's `team_message` definition as an Agent Team consumer. */
@@ -261,6 +274,13 @@ let AgentTeam = (() => {
261
274
  },
262
275
  runningAgents: this.runningAgents,
263
276
  });
277
+ /**
278
+ * The single seam for every per-Session stored read: handle lifecycle and
279
+ * failure normalization live here, so a DSH persistence-interface change is
280
+ * adapted once, and consumers choose policy by failure category instead of
281
+ * matching error text.
282
+ */
283
+ sessionReader = new StoredSessionReader(this.ctx);
264
284
  /**
265
285
  * Why one Member shows error presence, per failure source. Reads prefer
266
286
  * activation, then runtime, then compaction; slots clear independently, so
@@ -282,21 +302,13 @@ let AgentTeam = (() => {
282
302
  },
283
303
  });
284
304
  /**
285
- * Advisory progress-visibility nudges. Purely in-process: the ledger owns
286
- * eligibility, this coordinator owns counting and notices, and neither ever
287
- * writes Team facts. See docs/team-collaboration.md for the regimes.
305
+ * Incremental fold state for {@link contextManagement}'s projection lookup:
306
+ * one entry per Member, replaced when that Member's Session changes. Keying
307
+ * by Member rather than by Session id is what bounds the map — a rollover
308
+ * that leaves the previous generation's projection behind retains it for the
309
+ * life of the process, and only the current Session can ever read it.
288
310
  */
289
- progressNudge = new ProgressNudgeCoordinator({
290
- agentForMember: memberId => this.handles.get(memberId)?.agent,
291
- targetsForMember: memberId => this.requireLedger().progressNudgeTargets(memberId),
292
- sessionLogForMember: (memberId, sessionId) => {
293
- const handle = this.handles.get(memberId);
294
- if (handle === undefined || handle.agent.session.id !== sessionId)
295
- return undefined;
296
- return { events: handle.agent.session.ownEvents() };
297
- },
298
- log: message => { this.ctx.logger.warn(message); },
299
- });
311
+ contextCursors = new Map();
300
312
  /**
301
313
  * Context self-management: the one deep module that turns a Member's
302
314
  * successful `context_rollover` tool result into its next private context
@@ -311,15 +323,34 @@ let AgentTeam = (() => {
311
323
  const handle = this.handles.get(memberId);
312
324
  if (handle === undefined || handle.agent.session.id !== sessionId)
313
325
  return undefined;
314
- return foldContextProjection(handle.agent.session.ownEvents(), handle.agent.session.inheritedEventCount, handle.agent.session.id);
326
+ const session = handle.agent.session;
327
+ // The projection is read on every successful tool result and turn end, so
328
+ // it folds incrementally. The entry is keyed by Member and carries the
329
+ // Session it was built from: this Session's log keeps growing, a rollover
330
+ // replaces the entry instead of resuming from its predecessor's cursor,
331
+ // and a rewritten log falls back to a cold fold inside the cursor.
332
+ const owned = advanceOwnedSessionEventCursor(this.contextCursors.get(memberId), session.id, contextProjectionFold(session.id), session.ownEvents(), session.inheritedEventCount);
333
+ this.contextCursors.set(memberId, owned);
334
+ return owned.cursor.value;
315
335
  },
316
336
  executeTransition: (memberId, plan) => this.executeMemberTransition(memberId, plan),
317
337
  log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
318
338
  });
319
339
  lifecycleTail = Promise.resolve();
320
340
  accepting = true;
321
- changeVersion = 0;
341
+ /**
342
+ * Presence wake epoch: Agent running/idle/failure is runtime state with no
343
+ * durable fact behind it, so the presence scope counts those edge wakes in
344
+ * process. It never takes part in a projection waiter's comparison.
345
+ */
346
+ presenceEpoch = 0;
322
347
  changeWaiters = new Set();
348
+ /**
349
+ * The startup-opened remediation instance, held for the restart heal: the
350
+ * completion-cache domain may only be opened once per plugin lifecycle, so
351
+ * the restart path reuses this instance instead of opening its own.
352
+ */
353
+ remediation;
323
354
  constructor(ctx) {
324
355
  super(ctx, 'agentTeam');
325
356
  this.pressurePolicy = new PressurePolicyCoordinator({
@@ -351,7 +382,7 @@ let AgentTeam = (() => {
351
382
  if (kind !== undefined)
352
383
  this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable ${kind} error; recording a consecutive error occurrence`);
353
384
  this.recovery.onError(member.memberId, message);
354
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
385
+ this.emitPresenceChanged(member.workspaceId);
355
386
  });
356
387
  this.ctx.on('agent/status', ({ agent, status }) => {
357
388
  const member = this.memberForAgent(agent);
@@ -363,7 +394,7 @@ let AgentTeam = (() => {
363
394
  const recovered = this.clearMemberFailure(member.memberId, 'runtime');
364
395
  if (recovered)
365
396
  this.notifiedInbox.delete(member.memberId);
366
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
397
+ this.emitPresenceChanged(member.workspaceId);
367
398
  // A rollover/recovery in flight delivers its own sequenced
368
399
  // rederived Inbox after the handoff and carried input; a status-driven
369
400
  // steer here would claim the handoff turn's next step and leapfrog
@@ -373,24 +404,23 @@ let AgentTeam = (() => {
373
404
  }
374
405
  // A turn that ends without an error closes any automatic recovery episode.
375
406
  // The idle transition is itself presence-affecting (working → available),
376
- // so it must wake workspace watchers exactly like the running transition
407
+ // so it must wake presence watchers exactly like the running transition
377
408
  // above; without this wake, cached Client member rows keep showing the
378
409
  // Member as working after every turn until an unrelated change arrives.
379
410
  if (status === 'idle' && member !== undefined) {
380
411
  if (this.memberFailures.get(member.memberId)?.runtime === undefined) {
381
412
  this.recovery.onCleanTurnEnd(member.memberId);
382
413
  }
383
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
414
+ this.emitPresenceChanged(member.workspaceId);
384
415
  }
385
416
  });
386
- // Progress nudges count every `tool/call` of each Member Session. The
387
- // store's dispatch carrier is untagged, so a scope-tagged listener inside
388
- // the Agent setup would receive nothing; one root listener that maps the
389
- // session id back to its Member is the seam that works (same shape as the
390
- // `agent/status` listener above). The first turn of a freshly published
391
- // Member cannot race this: `handles.set()` precedes the same activation
392
- // continuation that publishes the Agent, so by the time any tool call
393
- // streams, the map lookup succeeds.
417
+ // The store's dispatch carrier is untagged, so a scope-tagged listener
418
+ // inside the Agent setup would receive nothing; one root listener that
419
+ // maps the session id back to its Member is the seam that works (same
420
+ // shape as the `agent/status` listener above). The first turn of a
421
+ // freshly published Member cannot race this: `handles.set()` precedes the
422
+ // same activation continuation that publishes the Agent, so by the time
423
+ // any tool call streams, the map lookup succeeds.
394
424
  this.ctx.on('session/event', (session, event) => {
395
425
  const memberId = this.memberBySessionId.get(session.id);
396
426
  if (memberId === undefined)
@@ -398,7 +428,6 @@ let AgentTeam = (() => {
398
428
  const handle = this.handles.get(memberId);
399
429
  if (handle === undefined || handle.agent.session.id !== session.id)
400
430
  return;
401
- this.progressNudge.onSessionEvent(memberId, session.id, handle.agent, event);
402
431
  // A successful assistant response ends any open provider-overflow
403
432
  // recovery sequence for this Member.
404
433
  if (event.type === 'assistant/message')
@@ -410,8 +439,8 @@ let AgentTeam = (() => {
410
439
  const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
411
440
  this.ctx.effect(() => async () => {
412
441
  this.accepting = false;
442
+ this.remediation = undefined;
413
443
  this.recovery.dispose();
414
- this.progressNudge.dispose();
415
444
  this.contextManagement.dispose();
416
445
  this.pressurePolicy.dispose();
417
446
  if (this.attachmentGcTimer !== undefined)
@@ -439,6 +468,7 @@ let AgentTeam = (() => {
439
468
  // startup — the next start retries exactly what the cache does not cover.
440
469
  try {
441
470
  const remediation = new SessionRemediation(this.ctx, this.ctx.sessionPersistence, await SessionRemediation.open(this.ctx));
471
+ this.remediation = remediation;
442
472
  await remediation.remediateEnabledMembers(ledger.listMembers());
443
473
  }
444
474
  catch (error) {
@@ -463,7 +493,7 @@ let AgentTeam = (() => {
463
493
  * generation over it.
464
494
  */
465
495
  async sessionPersisted(sessionId) {
466
- return (await this.ctx.sessionPersistence.stat(sessionId)) !== undefined;
496
+ return this.sessionReader.exists(sessionId);
467
497
  }
468
498
  /** Resolve one exact live Agent to its durable Team Member; forks do not inherit identity. */
469
499
  memberForAgent(agent) {
@@ -501,8 +531,9 @@ let AgentTeam = (() => {
501
531
  if (!Number.isInteger(request.afterVersion) || request.afterVersion < 0)
502
532
  throw new Error('afterVersion must be a non-negative integer');
503
533
  const scope = this.validateChangeScope(request.scope);
504
- if (this.changeVersion > request.afterVersion || !this.accepting)
505
- return Object.freeze({ version: this.changeVersion });
534
+ const version = this.changeVersionOf(scope);
535
+ if (version > request.afterVersion || !this.accepting)
536
+ return Object.freeze({ version });
506
537
  return new Promise((resolve, reject) => {
507
538
  let settled = false;
508
539
  const waiter = {
@@ -521,7 +552,7 @@ let AgentTeam = (() => {
521
552
  return;
522
553
  settled = true;
523
554
  this.changeWaiters.delete(waiter);
524
- resolve(Object.freeze({ version: this.changeVersion }));
555
+ resolve(Object.freeze({ version: this.changeVersionOf(scope) }));
525
556
  }, 25_000);
526
557
  const onAbort = () => {
527
558
  if (settled)
@@ -659,6 +690,23 @@ let AgentTeam = (() => {
659
690
  if (member.state !== 'enabled')
660
691
  throw new Error(`Agent Member '${member.handle}' is ${member.state}; only enabled Members can be restarted`);
661
692
  this.ctx.logger.info(`agent-team: restarting member '${member.handle}' after a failed activation`);
693
+ // A deterministic session refusal may be repairable in place: run the
694
+ // same bounded startup remediation for this one Member before retrying
695
+ // activation, so the restart heals instead of replaying the failure.
696
+ const activation = this.memberFailures.get(request.memberId)?.activation;
697
+ if (activation !== undefined && activation.class === 'session-refused' && this.remediation !== undefined) {
698
+ const outcome = await this.remediation.remediateMember(member);
699
+ if (outcome.repaired > 0) {
700
+ this.ctx.logger.info(`agent-team: repaired ${outcome.repaired} refused Session artifact(s) for member '${member.handle}'; retrying activation`);
701
+ }
702
+ else if (outcome.completed) {
703
+ // The walk finished and nothing was provably this plugin's to fix:
704
+ // a retry would fail identically. Mark the refusal non-remediable
705
+ // so the surface stops offering restart and says why.
706
+ this.markRefusalNonRemediable(request.memberId);
707
+ return Object.freeze({ status: this.memberStatus(member) });
708
+ }
709
+ }
662
710
  await this.reactivateMember(request.memberId);
663
711
  return Object.freeze({ status: this.memberStatus(member) });
664
712
  }
@@ -726,9 +774,6 @@ let AgentTeam = (() => {
726
774
  // Drop the old handle's transient state: pending recovery episodes and
727
775
  // error markers belong to the disposed agent, not to the Member.
728
776
  this.recovery.stopTracking(memberId);
729
- // The fresh Session may re-earn one Claim suggestion per Thread; the
730
- // old Session's one-shot records must not leak into it.
731
- this.progressNudge.stopTracking(memberId);
732
777
  // The context admission gate stays armed through disposal: input racing
733
778
  // the retire window must still be captured for the new generation, and
734
779
  // the coordinator drops its own bookkeeping only after the swap settles.
@@ -879,9 +924,6 @@ let AgentTeam = (() => {
879
924
  if (this.isInboxNotice(pending))
880
925
  handle.agent.inbox.remove(pending.id);
881
926
  }
882
- // Recovery carries the concrete interrupted work; a queued progress nudge
883
- // is stale next to it and must not survive as a second reminder.
884
- this.progressNudge.revokePendingNotice(member.memberId);
885
927
  handle.agent.steer(hint);
886
928
  }
887
929
  /** Automatic-recovery wakeup; throwing tells the coordinator the target is gone. */
@@ -1078,7 +1120,7 @@ let AgentTeam = (() => {
1078
1120
  this.emitCommitted(result.value.receipt);
1079
1121
  return result.value;
1080
1122
  }
1081
- /** Host-only Human Inbox projection; the Web Client does not consume it. */
1123
+ /** Human Inbox projection; the Web Client consumes the direct-only slice as its mention queue. */
1082
1124
  inbox(request) {
1083
1125
  this.requireWorkspace(request.workspaceId);
1084
1126
  return this.requireLedger().inbox(agentTeamHumanActor(), request);
@@ -1087,8 +1129,10 @@ let AgentTeam = (() => {
1087
1129
  async readThread(request) {
1088
1130
  const actor = this.humanCall(request.workspaceId);
1089
1131
  const result = await this.requireLedger().readThread({ ...request, actor });
1090
- if (result.committed)
1091
- this.emitCommitted(result.value.receipt);
1132
+ // A read that made no progress commits no operation and carries no receipt.
1133
+ const receipt = result.committed ? result.value.receipt : undefined;
1134
+ if (receipt !== undefined)
1135
+ this.emitCommitted(receipt);
1092
1136
  return result.value;
1093
1137
  }
1094
1138
  /** Human-only durable Attention observations for one Thread. */
@@ -1147,8 +1191,10 @@ let AgentTeam = (() => {
1147
1191
  async readThreadForAgent(agent, request) {
1148
1192
  const actor = this.memberCall(agent, request.workspaceId);
1149
1193
  const result = await this.requireLedger().readThread({ ...request, actor });
1150
- if (result.committed)
1151
- this.emitCommitted(result.value.receipt);
1194
+ // A read that made no progress commits no operation and carries no receipt.
1195
+ const receipt = result.committed ? result.value.receipt : undefined;
1196
+ if (receipt !== undefined)
1197
+ this.emitCommitted(receipt);
1152
1198
  const value = result.value;
1153
1199
  // Private read-time enrich: an acceptance the reader just acknowledged is
1154
1200
  // a natural Task boundary, so the Host prices the reader's context once,
@@ -1265,6 +1311,16 @@ let AgentTeam = (() => {
1265
1311
  validateLedger() {
1266
1312
  this.requireLedger().validate();
1267
1313
  }
1314
+ /**
1315
+ * Validate the durable ledger for the invariant's mount check. The
1316
+ * constructor already re-derived every durable record against its own
1317
+ * scratch projection, so this adopts that conclusion once while nothing has
1318
+ * committed since; every other call, and every commit-driven validation,
1319
+ * replays the whole table again.
1320
+ */
1321
+ validateLedgerAtMount() {
1322
+ this.requireLedger().validateAtMount();
1323
+ }
1268
1324
  /**
1269
1325
  * Effective context-pressure budget for one Member's current route:
1270
1326
  * `hardLimit = min(256K, routeWindow - outputReserve)` and
@@ -1362,20 +1418,13 @@ let AgentTeam = (() => {
1362
1418
  live = false;
1363
1419
  }
1364
1420
  else {
1365
- try {
1366
- const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
1367
- try {
1368
- events = (await handle.read()).events;
1369
- inheritedEventCount = handle.inheritedEventCount;
1370
- parentSession = handle.header.parentSession;
1371
- }
1372
- finally {
1373
- await handle.close();
1374
- }
1375
- }
1376
- catch (error) {
1377
- throw new Error(`checkpoint '${checkpointRef}' could not be resolved: its source Session is unreadable (${error instanceof Error ? error.message : String(error)})`);
1421
+ const read = await this.sessionReader.read(sessionId);
1422
+ if (!read.ok) {
1423
+ throw new StoredSessionReadError(`checkpoint '${checkpointRef}' could not be resolved: its source Session is unreadable (${read.failure.kind}: ${read.failure.detail})`, read.failure);
1378
1424
  }
1425
+ events = read.inspection.events;
1426
+ inheritedEventCount = read.inspection.inheritedEventCount;
1427
+ parentSession = read.inspection.header.parentSession;
1379
1428
  }
1380
1429
  // Fold the source with its inherited cut respected: inherited events
1381
1430
  // are resolved history in that source, never fresh intent; checkpoints
@@ -1452,20 +1501,15 @@ let AgentTeam = (() => {
1452
1501
  ?? this.requireLedger().previousSessionForMember(member.memberId);
1453
1502
  if (previousSessionId === undefined)
1454
1503
  return;
1455
- let inspection;
1456
- try {
1457
- const handle = await this.ctx.sessionPersistence.open(previousSessionId, 'read');
1458
- try {
1459
- inspection = { events: (await handle.read()).events, inheritedEventCount: handle.inheritedEventCount };
1460
- }
1461
- finally {
1462
- await handle.close();
1463
- }
1464
- }
1465
- catch (error) {
1466
- this.ctx.logger.warn(`agent-team: rollover handoff reconstruction could not read the previous Session '${previousSessionId}': ${error instanceof Error ? error.message : String(error)}`);
1504
+ const previousRead = await this.sessionReader.read(previousSessionId);
1505
+ if (!previousRead.ok) {
1506
+ // Best-effort recovery: the handoff intent stays lost with the
1507
+ // unreadable previous Session, but the Member still activates.
1508
+ const failure = previousRead.failure;
1509
+ this.ctx.logger.warn(`agent-team: rollover handoff reconstruction could not read the previous Session '${previousSessionId}' (${failure.kind}): ${failure.detail}`);
1467
1510
  return;
1468
1511
  }
1512
+ const inspection = previousRead.inspection;
1469
1513
  const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, previousSessionId);
1470
1514
  if (state.pending === null)
1471
1515
  return;
@@ -1494,19 +1538,12 @@ let AgentTeam = (() => {
1494
1538
  * downgrade to a blank child.
1495
1539
  */
1496
1540
  async recordedCheckpointPrefix(seed) {
1497
- let inspection;
1498
- try {
1499
- const handle = await this.ctx.sessionPersistence.open(seed.sourceSessionId, 'read');
1500
- try {
1501
- inspection = { events: (await handle.read()).events, inheritedEventCount: handle.inheritedEventCount };
1502
- }
1503
- finally {
1504
- await handle.close();
1505
- }
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)}`);
1541
+ const seedRead = await this.sessionReader.read(seed.sourceSessionId);
1542
+ if (!seedRead.ok) {
1543
+ const failure = seedRead.failure;
1544
+ throw new StoredSessionReadError(`the recorded checkpoint-return seed source Session '${seed.sourceSessionId}' is unreadable (${failure.kind}): ${failure.detail}`, failure);
1509
1545
  }
1546
+ const inspection = seedRead.inspection;
1510
1547
  const through = Number(seed.sourceThroughSeq);
1511
1548
  if (!Number.isSafeInteger(through) || through < 0 || through > inspection.events.length) {
1512
1549
  throw new Error(`the recorded checkpoint-return seed cut ${through} is not a valid prefix of Session '${seed.sourceSessionId}'`);
@@ -1555,23 +1592,14 @@ let AgentTeam = (() => {
1555
1592
  }
1556
1593
  let inspectionEvents;
1557
1594
  let inspectionInherited;
1558
- try {
1559
- const handle = await this.ctx.sessionPersistence.open(transition.previousSessionId, 'read');
1560
- try {
1561
- inspectionEvents = (await handle.read()).events;
1562
- inspectionInherited = handle.inheritedEventCount;
1563
- }
1564
- finally {
1565
- await handle.close();
1566
- }
1567
- }
1568
- catch (error) {
1569
- const detail = error instanceof Error ? error.message : String(error);
1595
+ const previousRead = await this.sessionReader.read(transition.previousSessionId);
1596
+ if (!previousRead.ok) {
1597
+ const failure = previousRead.failure;
1570
1598
  // Log-corruption class: bounded fail-open. The current Session's own
1571
1599
  // fold is intact and the Host repairs the retired log's torn tail; a
1572
1600
  // corrupted retired generation must not permanently block activation.
1573
- if (/corrupt session log/.test(detail)) {
1574
- this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is corrupt: ${detail}; skipping the replay`);
1601
+ if (failure.kind === 'corrupt') {
1602
+ this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is corrupt: ${failure.detail}; skipping the replay`);
1575
1603
  return 0;
1576
1604
  }
1577
1605
  // Deterministic released-format refusal: the retired artifact is refused
@@ -1580,12 +1608,14 @@ let AgentTeam = (() => {
1580
1608
  // Member no longer runs in into permanent unavailability — the exact
1581
1609
  // failure class this hardening removes. Genuine IO and unknown failures
1582
1610
  // stay fail-closed so carried input is never dropped silently.
1583
- if (error instanceof SessionFormatUnsupportedError) {
1584
- this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is refused by the session-format migration: ${detail}; skipping the replay`);
1611
+ if (failure.kind === 'refused') {
1612
+ this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is refused by the session-format migration: ${failure.detail}; skipping the replay`);
1585
1613
  return 0;
1586
1614
  }
1587
- throw new Error(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable: ${detail}`);
1615
+ throw new StoredSessionReadError(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable (${failure.kind}): ${failure.detail}`, failure);
1588
1616
  }
1617
+ inspectionEvents = previousRead.inspection.events;
1618
+ inspectionInherited = previousRead.inspection.inheritedEventCount;
1589
1619
  const state = foldContextProjection(inspectionEvents, inspectionInherited, transition.previousSessionId);
1590
1620
  const carried = carriedInputOf(state);
1591
1621
  if (carried.length === 0)
@@ -1659,6 +1689,7 @@ let AgentTeam = (() => {
1659
1689
  // persisted logs; the same projection definition folds every source.
1660
1690
  const items = [];
1661
1691
  const seen = new Set();
1692
+ let incompleteFrom;
1662
1693
  let sessionId = member.sessionId;
1663
1694
  let live = true;
1664
1695
  let guard = 0;
@@ -1676,23 +1707,19 @@ let AgentTeam = (() => {
1676
1707
  live = false;
1677
1708
  }
1678
1709
  else {
1679
- try {
1680
- const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
1681
- try {
1682
- const { events } = await handle.read();
1683
- state = foldContextProjection(events, handle.inheritedEventCount, sessionId);
1684
- sourceEvents = events;
1685
- sourceSessionId = sessionId;
1686
- sessionId = handle.header.parentSession;
1687
- }
1688
- finally {
1689
- await handle.close();
1690
- }
1691
- }
1692
- catch {
1693
- // An unreadable ancestor ends the lineage walk here.
1710
+ const read = await this.sessionReader.read(sessionId);
1711
+ if (!read.ok) {
1712
+ // An unreadable ancestor ends the lineage walk here — recorded in
1713
+ // the result, never silent, and never a Member-availability fact.
1714
+ incompleteFrom = { sessionId, reason: `${read.failure.kind}: ${read.failure.detail}` };
1694
1715
  sessionId = undefined;
1695
1716
  }
1717
+ else {
1718
+ state = foldContextProjection(read.inspection.events, read.inspection.inheritedEventCount, sessionId);
1719
+ sourceEvents = read.inspection.events;
1720
+ sourceSessionId = sessionId;
1721
+ sessionId = read.inspection.header.parentSession;
1722
+ }
1696
1723
  }
1697
1724
  if (state === undefined)
1698
1725
  break;
@@ -1713,7 +1740,7 @@ let AgentTeam = (() => {
1713
1740
  if (items.length >= limit)
1714
1741
  break;
1715
1742
  }
1716
- return { usageTokens, hardLimit, handoffAt, items };
1743
+ return { usageTokens, hardLimit, handoffAt, items, ...(incompleteFrom === undefined ? {} : { incompleteFrom }) };
1717
1744
  }
1718
1745
  /**
1719
1746
  * Replayed measurement of one lineage source: the live current Session
@@ -1729,19 +1756,16 @@ let AgentTeam = (() => {
1729
1756
  return undefined;
1730
1757
  if (live)
1731
1758
  return meter.measure(agent.session)?.totalTokens;
1759
+ // 0.1.5 removed borrowSession: rebuild a detached Session from the
1760
+ // stored log so the seed's retained cost is priced by the SOURCE's own
1761
+ // replay, never the current generation's. An unreadable source or a
1762
+ // failing meter is unmeasurable and prices as UNKNOWN.
1763
+ const read = await this.sessionReader.read(sessionId);
1764
+ if (!read.ok)
1765
+ return undefined;
1732
1766
  try {
1733
- // 0.1.5 removed borrowSession: rebuild a detached Session from the
1734
- // stored log so the seed's retained cost is priced by the SOURCE's own
1735
- // replay, never the current generation's.
1736
- const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
1737
- try {
1738
- const { events } = await handle.read();
1739
- const session = Session.create(sessionId, events, handle.header, handle.inheritedEventCount);
1740
- return meter.measure(session)?.totalTokens;
1741
- }
1742
- finally {
1743
- await handle.close();
1744
- }
1767
+ const session = Session.create(sessionId, read.inspection.events, read.inspection.header, read.inspection.inheritedEventCount);
1768
+ return meter.measure(session)?.totalTokens;
1745
1769
  }
1746
1770
  catch {
1747
1771
  return undefined;
@@ -1820,8 +1844,9 @@ let AgentTeam = (() => {
1820
1844
  const source = event.data.source;
1821
1845
  if (source?.kind !== 'plugin')
1822
1846
  continue;
1823
- // Reminder notices never enter attribution — a progress nudge or a
1824
- // recovery instruction is not a Team fact.
1847
+ // Reminder notices never enter attribution — a recovery instruction
1848
+ // (or a historical progress-nudge notice, kept decodable in session
1849
+ // logs recorded before that system was removed) is not a Team fact.
1825
1850
  if (source.form === 'notice' && source.summary !== undefined && isReminderNoticeSummary(source.summary))
1826
1851
  continue;
1827
1852
  const text = event.data.content.filter(block => block.type === 'text').map(block => block.text ?? '').join('\n');
@@ -2097,9 +2122,17 @@ let AgentTeam = (() => {
2097
2122
  // `swap` is bound by the provider at activation (no-op until then).
2098
2123
  const skillSelection = { current: member.capabilities?.skills?.allow, swap: () => { } };
2099
2124
  const setup = async (agentCtx, agent) => {
2100
- await this.ctx.agentPresets.mount(agentCtx, member.presetId);
2101
- this.memberRuntime.applyMemberToolPolicy(agentCtx, member);
2102
- this.validateMemberPreset(agentCtx);
2125
+ try {
2126
+ await this.ctx.agentPresets.mount(agentCtx, member.presetId);
2127
+ this.memberRuntime.applyMemberToolPolicy(agentCtx, member);
2128
+ this.validateMemberPreset(agentCtx);
2129
+ }
2130
+ catch (error) {
2131
+ // Tag composition failures with their own class: the activation
2132
+ // diagnostic routes preset-composition (install/runtime split)
2133
+ // failures by type, not by matching message text.
2134
+ throw new PresetCompositionError(error instanceof Error ? error.message : String(error), { cause: error });
2135
+ }
2103
2136
  installModelSelection(agentCtx, selected);
2104
2137
  // Admission gate for pending context rollovers: once a successful
2105
2138
  // context_rollover result is durable, queued input must not open another
@@ -2276,11 +2309,11 @@ let AgentTeam = (() => {
2276
2309
  this.memberRuntime.forgetMember(member.memberId);
2277
2310
  const message = error instanceof Error ? error.message : String(error);
2278
2311
  this.ctx.logger.warn(`agent-team: activation failed for member '${this.memberLabel(member.memberId)}': ${message}`);
2279
- this.setMemberFailure(member.memberId, 'activation', message);
2312
+ this.setActivationDiagnostic(member.memberId, this.activationDiagnosticOf(error, member.sessionId));
2280
2313
  }
2281
2314
  finally {
2282
2315
  // Activation only changes this Workspace's presence projection.
2283
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
2316
+ this.emitPresenceChanged(member.workspaceId);
2284
2317
  }
2285
2318
  }
2286
2319
  /**
@@ -2368,13 +2401,13 @@ let AgentTeam = (() => {
2368
2401
  // new binding as active — a Client following the row would otherwise open
2369
2402
  // a Session that does not exist yet.
2370
2403
  if (handle.agent.id !== member.sessionId)
2371
- return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic: 'context rollover in progress' });
2404
+ return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic: { class: 'rollover', detail: 'context rollover in progress' } });
2372
2405
  if (this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
2373
- return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: ORPHANED_MEMBER_DIAGNOSTIC });
2406
+ return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: { class: 'preset-composition', detail: ORPHANED_MEMBER_DIAGNOSTIC } });
2374
2407
  }
2375
2408
  const runtimeError = failures?.runtime ?? failures?.compaction;
2376
2409
  if (runtimeError !== undefined)
2377
- return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: runtimeError });
2410
+ return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: { class: 'runtime', detail: runtimeError } });
2378
2411
  // Capability warnings are runtime-derived at activation (handles-scoped,
2379
2412
  // like failures): absent while capabilities resolve cleanly.
2380
2413
  const capabilityWarnings = this.memberRuntime.capabilityWarningsFor(member.memberId);
@@ -2388,6 +2421,37 @@ let AgentTeam = (() => {
2388
2421
  failures[slot] = message;
2389
2422
  this.memberFailures.set(memberId, failures);
2390
2423
  }
2424
+ /** Store one structured activation diagnostic; runtime/compaction slots stay plain messages. */
2425
+ setActivationDiagnostic(memberId, diagnostic) {
2426
+ const failures = this.memberFailures.get(memberId) ?? {};
2427
+ failures.activation = Object.freeze(diagnostic);
2428
+ this.memberFailures.set(memberId, failures);
2429
+ }
2430
+ /**
2431
+ * Route one activation failure to its diagnostic class: preset composition
2432
+ * failures by their own error class, session failures by the seam's typed
2433
+ * classification (our call sites carry it directly; a Harness resume
2434
+ * failure carries it through the cause chain), everything else as an
2435
+ * unclassified activation failure.
2436
+ */
2437
+ activationDiagnosticOf(error, sessionId) {
2438
+ if (error instanceof PresetCompositionError)
2439
+ return { class: 'preset-composition', detail: error.message };
2440
+ const failure = error instanceof StoredSessionReadError ? error.failure : sessionFailureOf(error, sessionId);
2441
+ if (failure !== undefined) {
2442
+ const shared = { detail: failure.detail, ...(failure.location === undefined ? {} : { location: failure.location }), sessionId: failure.sessionId };
2443
+ return failure.kind === 'refused' ? { class: 'session-refused', ...shared } : { class: 'session-unreadable', ...shared };
2444
+ }
2445
+ return { class: 'activation', detail: error instanceof Error ? error.message : String(error) };
2446
+ }
2447
+ /** Mark a session-refused activation diagnostic as proven non-remediable. */
2448
+ markRefusalNonRemediable(memberId) {
2449
+ const failures = this.memberFailures.get(memberId);
2450
+ const activation = failures?.activation;
2451
+ if (activation === undefined || activation.class !== 'session-refused')
2452
+ return;
2453
+ failures.activation = Object.freeze({ ...activation, remediable: false });
2454
+ }
2391
2455
  clearMemberFailure(memberId, slot) {
2392
2456
  const failures = this.memberFailures.get(memberId);
2393
2457
  if (failures === undefined || failures[slot] === undefined)
@@ -2422,10 +2486,6 @@ let AgentTeam = (() => {
2422
2486
  }
2423
2487
  const ledger = this.requireLedger();
2424
2488
  this.emitChanged(ledger.changeScopesOf(operation));
2425
- // Nudge reconciliation precedes notifyMember: a committed reply both
2426
- // resets the author's silence and cancels any of its queued nudge before
2427
- // the Inbox notice (higher priority) is considered.
2428
- this.progressNudge.onCommitted(operation);
2429
2489
  for (const memberId of ledger.affectedMembersOf(operation)) {
2430
2490
  const handle = this.handles.get(memberId);
2431
2491
  if (handle !== undefined)
@@ -2462,7 +2522,17 @@ let AgentTeam = (() => {
2462
2522
  }
2463
2523
  emitAutoCompactionChanged(memberId) {
2464
2524
  const workspaceId = this.ledger?.getMember(memberId)?.workspaceId;
2465
- this.emitChanged(workspaceId === undefined ? undefined : [{ kind: 'workspace', workspaceId }]);
2525
+ if (workspaceId === undefined)
2526
+ return;
2527
+ this.emitPresenceChanged(workspaceId);
2528
+ }
2529
+ /**
2530
+ * Presence-only invalidation: Agent running/idle/activation/failure changes
2531
+ * alter no durable projection, so only members/presence subscribers wake —
2532
+ * the workspace catalog and the scope-less Inbox subscriptions stay parked.
2533
+ */
2534
+ emitPresenceChanged(workspaceId) {
2535
+ this.emitChanged([{ kind: 'presence', workspaceId }]);
2466
2536
  }
2467
2537
  /** Wake from durable unread state with bounded facts for direct and state-changing work. */
2468
2538
  notifyMember(agent, sequenced = false) {
@@ -2495,9 +2565,6 @@ let AgentTeam = (() => {
2495
2565
  const existingInboxHint = pending.find(message => this.isInboxNotice(message));
2496
2566
  if (existingInboxHint !== undefined)
2497
2567
  agent.inbox.remove(existingInboxHint.id);
2498
- // A durable Inbox notice outranks a queued progress nudge; revoke it so
2499
- // the model reads concrete unread work instead of a generic reminder.
2500
- this.progressNudge.revokePendingNotice(member.memberId);
2501
2568
  const hint = createUserMessage({
2502
2569
  content: [{ type: 'text', text: this.notificationText(notifications, member.memberId) }],
2503
2570
  source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: INBOX_NOTICE_SUMMARY },
@@ -2635,7 +2702,6 @@ let AgentTeam = (() => {
2635
2702
  }
2636
2703
  clearMemberRecoveryState(member) {
2637
2704
  this.recovery.stopTracking(member.memberId);
2638
- this.progressNudge.stopTracking(member.memberId);
2639
2705
  this.clearMemberFailure(member.memberId, 'runtime');
2640
2706
  this.clearMemberNotificationState(member.memberId);
2641
2707
  }
@@ -2646,21 +2712,45 @@ let AgentTeam = (() => {
2646
2712
  * Wake waiters for one committed or lifecycle change. Undefined broadcasts
2647
2713
  * to everyone; an empty scope list invalidates nobody because no shared
2648
2714
  * projection changed; otherwise global and matching scoped waiters wake.
2715
+ * Presence-only scopes sit outside that: they change no durable projection,
2716
+ * so only matching presence waiters wake and the scope-less Inbox
2717
+ * subscriptions stay parked.
2718
+ *
2719
+ * Each woken waiter receives the version of its own scope's domain, so a
2720
+ * wake can never hand a projection waiter a presence number or the other way
2721
+ * around. Nothing here advances the projection version.
2649
2722
  */
2650
2723
  emitChanged(scopes) {
2651
- this.changeVersion += 1;
2724
+ const touchesProjection = scopes === undefined || scopes.some(scope => scope.kind !== 'presence');
2725
+ const touchesPresence = scopes !== undefined && scopes.some(scope => scope.kind === 'presence');
2726
+ if (!touchesProjection && !touchesPresence)
2727
+ return;
2728
+ if (touchesPresence)
2729
+ this.presenceEpoch += 1;
2652
2730
  for (const waiter of this.changeWaiters) {
2653
2731
  const waiterScope = waiter.scope;
2654
- if (scopes !== undefined && (waiterScope === undefined ? scopes.length === 0 : !scopes.some(scope => sameChangeScope(scope, waiterScope))))
2732
+ if (scopes !== undefined && (waiterScope === undefined ? !touchesProjection : !scopes.some(scope => sameChangeScope(scope, waiterScope))))
2655
2733
  continue;
2656
2734
  this.changeWaiters.delete(waiter);
2657
- waiter.wake(this.changeVersion);
2735
+ waiter.wake(this.changeVersionOf(waiterScope));
2658
2736
  }
2659
2737
  }
2738
+ /**
2739
+ * The cursor domain of one scope: presence scopes count process-local edge
2740
+ * wakes, every other scope compares against the durable ledger position of
2741
+ * the newest shared-projection commit. The two domains are deliberately
2742
+ * separate, so a presence edge cannot invalidate a projection subscriber and
2743
+ * a private read invalidates no one.
2744
+ */
2745
+ changeVersionOf(scope) {
2746
+ if (scope?.kind === 'presence')
2747
+ return this.presenceEpoch;
2748
+ return this.ledger?.projectionSequence() ?? 0;
2749
+ }
2660
2750
  validateChangeScope(scope) {
2661
2751
  if (scope === undefined)
2662
2752
  return undefined;
2663
- const ref = scope.kind === 'workspace' ? scope.workspaceId : scope.kind === 'channel' ? scope.channelRef : scope.threadRef;
2753
+ const ref = scope.kind === 'workspace' || scope.kind === 'presence' ? scope.workspaceId : scope.kind === 'channel' ? scope.channelRef : scope.threadRef;
2664
2754
  if (typeof ref !== 'string' || ref.length === 0)
2665
2755
  throw new Error(`change scope of kind '${scope.kind}' requires a non-empty ref`);
2666
2756
  return scope;