@wowyuarm/dsh-agent-team 0.1.10 → 0.1.11

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 (110) hide show
  1. package/README.md +13 -0
  2. package/README.zh.md +13 -0
  3. package/package.json +52 -47
  4. package/packages/agent-team/README.md +4 -2
  5. package/packages/agent-team/README.zh.md +4 -2
  6. package/packages/agent-team/lib/context-management.js +40 -50
  7. package/packages/agent-team/lib/context-projection.js +10 -0
  8. package/packages/agent-team/lib/index.js +235 -112
  9. package/packages/agent-team/lib/invariant.js +38 -4
  10. package/packages/agent-team/lib/ledger.js +554 -271
  11. package/packages/agent-team/lib/member-context.js +6 -2
  12. package/packages/agent-team/lib/member-time-context.js +20 -5
  13. package/packages/agent-team/lib/pressure-policy.js +33 -9
  14. package/packages/agent-team/lib/session-event-cursor.js +116 -0
  15. package/packages/agent-team/lib/session-remediation.js +38 -26
  16. package/packages/agent-team/lib/spec.js +46 -34
  17. package/packages/agent-team/lib/stored-session-reader.js +138 -0
  18. package/packages/agent-team/lib/typert.host.js +107 -40
  19. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  20. package/packages/agent-team/lib/typert.remote-client.js +84 -32
  21. package/packages/agent-team/lib/types/context-management.d.ts +7 -0
  22. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
  24. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/index.d.ts +78 -2
  26. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
  28. package/packages/agent-team/lib/types/ledger.d.ts +180 -7
  29. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  31. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
  33. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  34. package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
  35. package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
  36. package/packages/agent-team/lib/types/session-remediation.d.ts +23 -0
  37. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -1
  38. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  39. package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
  40. package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
  41. package/packages/agent-team/lib/types/types/entities.d.ts +28 -2
  42. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  43. package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
  44. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  45. package/packages/agent-team/lib/types/types/requests-results.d.ts +44 -3
  46. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  47. package/packages/agent-team/preset/team-member/agent.cordis.yml +3 -1
  48. package/packages/client-agent-team/README.md +1 -1
  49. package/packages/client-agent-team/README.zh.md +1 -1
  50. package/packages/client-agent-team/lib/client.js +1068 -375
  51. package/packages/client-agent-team/lib/client.js.map +1 -1
  52. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -4
  54. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +42 -22
  56. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +13 -24
  58. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
  59. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamComposer.js +15 -9
  61. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  62. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  63. package/packages/client-agent-team/lib/types/client/TeamConversation.js +8 -1
  64. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +21 -0
  65. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
  66. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +71 -0
  67. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  68. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
  69. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
  70. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  71. package/packages/client-agent-team/lib/types/client/TeamMessage.js +11 -4
  72. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
  73. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  74. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
  75. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  76. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +102 -28
  77. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  78. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  79. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +50 -7
  80. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/index.js +17 -6
  82. package/packages/client-agent-team/lib/types/client/locales.d.ts +18 -0
  83. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  84. package/packages/client-agent-team/lib/types/client/locales.js +18 -0
  85. package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
  86. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  87. package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
  88. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
  89. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
  90. package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
  91. package/packages/client-agent-team/lib/types/client/slots.d.ts +7 -1
  92. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  93. package/packages/client-agent-team/lib/types/client/team-changes.d.ts +26 -1
  94. package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
  95. package/packages/client-agent-team/lib/types/client/team-changes.js +66 -15
  96. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
  97. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
  98. package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
  99. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
  100. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  101. package/packages/client-agent-team/lib/types/client/team-formatters.js +34 -6
  102. package/packages/tool-agent-team/README.md +1 -1
  103. package/packages/tool-agent-team/README.zh.md +1 -1
  104. package/packages/tool-agent-team/lib/context-tools.js +18 -14
  105. package/packages/tool-agent-team/lib/host-access.js +20 -0
  106. package/packages/tool-agent-team/lib/index.js +16 -17
  107. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  108. package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
  109. package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
  110. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -47,19 +47,20 @@ 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
60
  import { ProgressNudgeCoordinator } from "./progress-nudge.js";
61
61
  import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
62
62
  import { SessionRemediation, handoffAlreadyInLog } from "./session-remediation.js";
63
+ import { StoredSessionReadError, StoredSessionReader, sessionFailureOf } from "./stored-session-reader.js";
63
64
  import { agentTeamDomainSpec } from "./spec.js";
64
65
  import { formatTeamTimestamp } from "./time-format.js";
65
66
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
@@ -70,6 +71,17 @@ export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.pre
70
71
  const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
71
72
  const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
72
73
  const ORPHANED_MEMBER_DIAGNOSTIC = 'Member preset composition was lost after a reload; its tools are unavailable. Resume rebuilds the member in place.';
74
+ /**
75
+ * A preset mount/validation failure during activation, carrying its own class
76
+ * so the activation diagnostic can route preset-composition failures (the
77
+ * install/runtime split failure mode) without matching message text.
78
+ */
79
+ class PresetCompositionError extends Error {
80
+ constructor(message, options) {
81
+ super(message, options);
82
+ this.name = 'PresetCompositionError';
83
+ }
84
+ }
73
85
  /** Longest accepted model-supplied checkpoint display name. */
74
86
  const MAX_CHECKPOINT_NAME_CHARS = 120;
75
87
  /** Default and maximum number of timeline items one query returns. */
@@ -95,6 +107,8 @@ function sameChangeScope(left, right) {
95
107
  return left.channelRef === right.channelRef;
96
108
  if (left.kind === 'thread' && right.kind === 'thread')
97
109
  return left.threadRef === right.threadRef;
110
+ if (left.kind === 'presence' && right.kind === 'presence')
111
+ return left.workspaceId === right.workspaceId;
98
112
  return false;
99
113
  }
100
114
  /** Mark the preset's `team_message` definition as an Agent Team consumer. */
@@ -261,6 +275,13 @@ let AgentTeam = (() => {
261
275
  },
262
276
  runningAgents: this.runningAgents,
263
277
  });
278
+ /**
279
+ * The single seam for every per-Session stored read: handle lifecycle and
280
+ * failure normalization live here, so a DSH persistence-interface change is
281
+ * adapted once, and consumers choose policy by failure category instead of
282
+ * matching error text.
283
+ */
284
+ sessionReader = new StoredSessionReader(this.ctx);
264
285
  /**
265
286
  * Why one Member shows error presence, per failure source. Reads prefer
266
287
  * activation, then runtime, then compaction; slots clear independently, so
@@ -297,6 +318,14 @@ let AgentTeam = (() => {
297
318
  },
298
319
  log: message => { this.ctx.logger.warn(message); },
299
320
  });
321
+ /**
322
+ * Incremental fold state for {@link contextManagement}'s projection lookup:
323
+ * one entry per Member, replaced when that Member's Session changes. Keying
324
+ * by Member rather than by Session id is what bounds the map — a rollover
325
+ * that leaves the previous generation's projection behind retains it for the
326
+ * life of the process, and only the current Session can ever read it.
327
+ */
328
+ contextCursors = new Map();
300
329
  /**
301
330
  * Context self-management: the one deep module that turns a Member's
302
331
  * successful `context_rollover` tool result into its next private context
@@ -311,15 +340,34 @@ let AgentTeam = (() => {
311
340
  const handle = this.handles.get(memberId);
312
341
  if (handle === undefined || handle.agent.session.id !== sessionId)
313
342
  return undefined;
314
- return foldContextProjection(handle.agent.session.ownEvents(), handle.agent.session.inheritedEventCount, handle.agent.session.id);
343
+ const session = handle.agent.session;
344
+ // The projection is read on every successful tool result and turn end, so
345
+ // it folds incrementally. The entry is keyed by Member and carries the
346
+ // Session it was built from: this Session's log keeps growing, a rollover
347
+ // replaces the entry instead of resuming from its predecessor's cursor,
348
+ // and a rewritten log falls back to a cold fold inside the cursor.
349
+ const owned = advanceOwnedSessionEventCursor(this.contextCursors.get(memberId), session.id, contextProjectionFold(session.id), session.ownEvents(), session.inheritedEventCount);
350
+ this.contextCursors.set(memberId, owned);
351
+ return owned.cursor.value;
315
352
  },
316
353
  executeTransition: (memberId, plan) => this.executeMemberTransition(memberId, plan),
317
354
  log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
318
355
  });
319
356
  lifecycleTail = Promise.resolve();
320
357
  accepting = true;
321
- changeVersion = 0;
358
+ /**
359
+ * Presence wake epoch: Agent running/idle/failure is runtime state with no
360
+ * durable fact behind it, so the presence scope counts those edge wakes in
361
+ * process. It never takes part in a projection waiter's comparison.
362
+ */
363
+ presenceEpoch = 0;
322
364
  changeWaiters = new Set();
365
+ /**
366
+ * The startup-opened remediation instance, held for the restart heal: the
367
+ * completion-cache domain may only be opened once per plugin lifecycle, so
368
+ * the restart path reuses this instance instead of opening its own.
369
+ */
370
+ remediation;
323
371
  constructor(ctx) {
324
372
  super(ctx, 'agentTeam');
325
373
  this.pressurePolicy = new PressurePolicyCoordinator({
@@ -351,7 +399,7 @@ let AgentTeam = (() => {
351
399
  if (kind !== undefined)
352
400
  this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable ${kind} error; recording a consecutive error occurrence`);
353
401
  this.recovery.onError(member.memberId, message);
354
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
402
+ this.emitPresenceChanged(member.workspaceId);
355
403
  });
356
404
  this.ctx.on('agent/status', ({ agent, status }) => {
357
405
  const member = this.memberForAgent(agent);
@@ -363,7 +411,7 @@ let AgentTeam = (() => {
363
411
  const recovered = this.clearMemberFailure(member.memberId, 'runtime');
364
412
  if (recovered)
365
413
  this.notifiedInbox.delete(member.memberId);
366
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
414
+ this.emitPresenceChanged(member.workspaceId);
367
415
  // A rollover/recovery in flight delivers its own sequenced
368
416
  // rederived Inbox after the handoff and carried input; a status-driven
369
417
  // steer here would claim the handoff turn's next step and leapfrog
@@ -373,14 +421,14 @@ let AgentTeam = (() => {
373
421
  }
374
422
  // A turn that ends without an error closes any automatic recovery episode.
375
423
  // The idle transition is itself presence-affecting (working → available),
376
- // so it must wake workspace watchers exactly like the running transition
424
+ // so it must wake presence watchers exactly like the running transition
377
425
  // above; without this wake, cached Client member rows keep showing the
378
426
  // Member as working after every turn until an unrelated change arrives.
379
427
  if (status === 'idle' && member !== undefined) {
380
428
  if (this.memberFailures.get(member.memberId)?.runtime === undefined) {
381
429
  this.recovery.onCleanTurnEnd(member.memberId);
382
430
  }
383
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
431
+ this.emitPresenceChanged(member.workspaceId);
384
432
  }
385
433
  });
386
434
  // Progress nudges count every `tool/call` of each Member Session. The
@@ -410,6 +458,7 @@ let AgentTeam = (() => {
410
458
  const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
411
459
  this.ctx.effect(() => async () => {
412
460
  this.accepting = false;
461
+ this.remediation = undefined;
413
462
  this.recovery.dispose();
414
463
  this.progressNudge.dispose();
415
464
  this.contextManagement.dispose();
@@ -439,6 +488,7 @@ let AgentTeam = (() => {
439
488
  // startup — the next start retries exactly what the cache does not cover.
440
489
  try {
441
490
  const remediation = new SessionRemediation(this.ctx, this.ctx.sessionPersistence, await SessionRemediation.open(this.ctx));
491
+ this.remediation = remediation;
442
492
  await remediation.remediateEnabledMembers(ledger.listMembers());
443
493
  }
444
494
  catch (error) {
@@ -463,7 +513,7 @@ let AgentTeam = (() => {
463
513
  * generation over it.
464
514
  */
465
515
  async sessionPersisted(sessionId) {
466
- return (await this.ctx.sessionPersistence.stat(sessionId)) !== undefined;
516
+ return this.sessionReader.exists(sessionId);
467
517
  }
468
518
  /** Resolve one exact live Agent to its durable Team Member; forks do not inherit identity. */
469
519
  memberForAgent(agent) {
@@ -501,8 +551,9 @@ let AgentTeam = (() => {
501
551
  if (!Number.isInteger(request.afterVersion) || request.afterVersion < 0)
502
552
  throw new Error('afterVersion must be a non-negative integer');
503
553
  const scope = this.validateChangeScope(request.scope);
504
- if (this.changeVersion > request.afterVersion || !this.accepting)
505
- return Object.freeze({ version: this.changeVersion });
554
+ const version = this.changeVersionOf(scope);
555
+ if (version > request.afterVersion || !this.accepting)
556
+ return Object.freeze({ version });
506
557
  return new Promise((resolve, reject) => {
507
558
  let settled = false;
508
559
  const waiter = {
@@ -521,7 +572,7 @@ let AgentTeam = (() => {
521
572
  return;
522
573
  settled = true;
523
574
  this.changeWaiters.delete(waiter);
524
- resolve(Object.freeze({ version: this.changeVersion }));
575
+ resolve(Object.freeze({ version: this.changeVersionOf(scope) }));
525
576
  }, 25_000);
526
577
  const onAbort = () => {
527
578
  if (settled)
@@ -659,6 +710,23 @@ let AgentTeam = (() => {
659
710
  if (member.state !== 'enabled')
660
711
  throw new Error(`Agent Member '${member.handle}' is ${member.state}; only enabled Members can be restarted`);
661
712
  this.ctx.logger.info(`agent-team: restarting member '${member.handle}' after a failed activation`);
713
+ // A deterministic session refusal may be repairable in place: run the
714
+ // same bounded startup remediation for this one Member before retrying
715
+ // activation, so the restart heals instead of replaying the failure.
716
+ const activation = this.memberFailures.get(request.memberId)?.activation;
717
+ if (activation !== undefined && activation.class === 'session-refused' && this.remediation !== undefined) {
718
+ const outcome = await this.remediation.remediateMember(member);
719
+ if (outcome.repaired > 0) {
720
+ this.ctx.logger.info(`agent-team: repaired ${outcome.repaired} refused Session artifact(s) for member '${member.handle}'; retrying activation`);
721
+ }
722
+ else if (outcome.completed) {
723
+ // The walk finished and nothing was provably this plugin's to fix:
724
+ // a retry would fail identically. Mark the refusal non-remediable
725
+ // so the surface stops offering restart and says why.
726
+ this.markRefusalNonRemediable(request.memberId);
727
+ return Object.freeze({ status: this.memberStatus(member) });
728
+ }
729
+ }
662
730
  await this.reactivateMember(request.memberId);
663
731
  return Object.freeze({ status: this.memberStatus(member) });
664
732
  }
@@ -1078,7 +1146,7 @@ let AgentTeam = (() => {
1078
1146
  this.emitCommitted(result.value.receipt);
1079
1147
  return result.value;
1080
1148
  }
1081
- /** Host-only Human Inbox projection; the Web Client does not consume it. */
1149
+ /** Human Inbox projection; the Web Client consumes the direct-only slice as its mention queue. */
1082
1150
  inbox(request) {
1083
1151
  this.requireWorkspace(request.workspaceId);
1084
1152
  return this.requireLedger().inbox(agentTeamHumanActor(), request);
@@ -1087,8 +1155,10 @@ let AgentTeam = (() => {
1087
1155
  async readThread(request) {
1088
1156
  const actor = this.humanCall(request.workspaceId);
1089
1157
  const result = await this.requireLedger().readThread({ ...request, actor });
1090
- if (result.committed)
1091
- this.emitCommitted(result.value.receipt);
1158
+ // A read that made no progress commits no operation and carries no receipt.
1159
+ const receipt = result.committed ? result.value.receipt : undefined;
1160
+ if (receipt !== undefined)
1161
+ this.emitCommitted(receipt);
1092
1162
  return result.value;
1093
1163
  }
1094
1164
  /** Human-only durable Attention observations for one Thread. */
@@ -1147,8 +1217,10 @@ let AgentTeam = (() => {
1147
1217
  async readThreadForAgent(agent, request) {
1148
1218
  const actor = this.memberCall(agent, request.workspaceId);
1149
1219
  const result = await this.requireLedger().readThread({ ...request, actor });
1150
- if (result.committed)
1151
- this.emitCommitted(result.value.receipt);
1220
+ // A read that made no progress commits no operation and carries no receipt.
1221
+ const receipt = result.committed ? result.value.receipt : undefined;
1222
+ if (receipt !== undefined)
1223
+ this.emitCommitted(receipt);
1152
1224
  const value = result.value;
1153
1225
  // Private read-time enrich: an acceptance the reader just acknowledged is
1154
1226
  // a natural Task boundary, so the Host prices the reader's context once,
@@ -1265,6 +1337,16 @@ let AgentTeam = (() => {
1265
1337
  validateLedger() {
1266
1338
  this.requireLedger().validate();
1267
1339
  }
1340
+ /**
1341
+ * Validate the durable ledger for the invariant's mount check. The
1342
+ * constructor already re-derived every durable record against its own
1343
+ * scratch projection, so this adopts that conclusion once while nothing has
1344
+ * committed since; every other call, and every commit-driven validation,
1345
+ * replays the whole table again.
1346
+ */
1347
+ validateLedgerAtMount() {
1348
+ this.requireLedger().validateAtMount();
1349
+ }
1268
1350
  /**
1269
1351
  * Effective context-pressure budget for one Member's current route:
1270
1352
  * `hardLimit = min(256K, routeWindow - outputReserve)` and
@@ -1362,20 +1444,13 @@ let AgentTeam = (() => {
1362
1444
  live = false;
1363
1445
  }
1364
1446
  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)})`);
1447
+ const read = await this.sessionReader.read(sessionId);
1448
+ if (!read.ok) {
1449
+ throw new StoredSessionReadError(`checkpoint '${checkpointRef}' could not be resolved: its source Session is unreadable (${read.failure.kind}: ${read.failure.detail})`, read.failure);
1378
1450
  }
1451
+ events = read.inspection.events;
1452
+ inheritedEventCount = read.inspection.inheritedEventCount;
1453
+ parentSession = read.inspection.header.parentSession;
1379
1454
  }
1380
1455
  // Fold the source with its inherited cut respected: inherited events
1381
1456
  // are resolved history in that source, never fresh intent; checkpoints
@@ -1452,20 +1527,15 @@ let AgentTeam = (() => {
1452
1527
  ?? this.requireLedger().previousSessionForMember(member.memberId);
1453
1528
  if (previousSessionId === undefined)
1454
1529
  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)}`);
1530
+ const previousRead = await this.sessionReader.read(previousSessionId);
1531
+ if (!previousRead.ok) {
1532
+ // Best-effort recovery: the handoff intent stays lost with the
1533
+ // unreadable previous Session, but the Member still activates.
1534
+ const failure = previousRead.failure;
1535
+ this.ctx.logger.warn(`agent-team: rollover handoff reconstruction could not read the previous Session '${previousSessionId}' (${failure.kind}): ${failure.detail}`);
1467
1536
  return;
1468
1537
  }
1538
+ const inspection = previousRead.inspection;
1469
1539
  const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, previousSessionId);
1470
1540
  if (state.pending === null)
1471
1541
  return;
@@ -1494,19 +1564,12 @@ let AgentTeam = (() => {
1494
1564
  * downgrade to a blank child.
1495
1565
  */
1496
1566
  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)}`);
1567
+ const seedRead = await this.sessionReader.read(seed.sourceSessionId);
1568
+ if (!seedRead.ok) {
1569
+ const failure = seedRead.failure;
1570
+ throw new StoredSessionReadError(`the recorded checkpoint-return seed source Session '${seed.sourceSessionId}' is unreadable (${failure.kind}): ${failure.detail}`, failure);
1509
1571
  }
1572
+ const inspection = seedRead.inspection;
1510
1573
  const through = Number(seed.sourceThroughSeq);
1511
1574
  if (!Number.isSafeInteger(through) || through < 0 || through > inspection.events.length) {
1512
1575
  throw new Error(`the recorded checkpoint-return seed cut ${through} is not a valid prefix of Session '${seed.sourceSessionId}'`);
@@ -1555,23 +1618,14 @@ let AgentTeam = (() => {
1555
1618
  }
1556
1619
  let inspectionEvents;
1557
1620
  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);
1621
+ const previousRead = await this.sessionReader.read(transition.previousSessionId);
1622
+ if (!previousRead.ok) {
1623
+ const failure = previousRead.failure;
1570
1624
  // Log-corruption class: bounded fail-open. The current Session's own
1571
1625
  // fold is intact and the Host repairs the retired log's torn tail; a
1572
1626
  // 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`);
1627
+ if (failure.kind === 'corrupt') {
1628
+ 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
1629
  return 0;
1576
1630
  }
1577
1631
  // Deterministic released-format refusal: the retired artifact is refused
@@ -1580,12 +1634,14 @@ let AgentTeam = (() => {
1580
1634
  // Member no longer runs in into permanent unavailability — the exact
1581
1635
  // failure class this hardening removes. Genuine IO and unknown failures
1582
1636
  // 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`);
1637
+ if (failure.kind === 'refused') {
1638
+ 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
1639
  return 0;
1586
1640
  }
1587
- throw new Error(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable: ${detail}`);
1641
+ throw new StoredSessionReadError(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable (${failure.kind}): ${failure.detail}`, failure);
1588
1642
  }
1643
+ inspectionEvents = previousRead.inspection.events;
1644
+ inspectionInherited = previousRead.inspection.inheritedEventCount;
1589
1645
  const state = foldContextProjection(inspectionEvents, inspectionInherited, transition.previousSessionId);
1590
1646
  const carried = carriedInputOf(state);
1591
1647
  if (carried.length === 0)
@@ -1659,6 +1715,7 @@ let AgentTeam = (() => {
1659
1715
  // persisted logs; the same projection definition folds every source.
1660
1716
  const items = [];
1661
1717
  const seen = new Set();
1718
+ let incompleteFrom;
1662
1719
  let sessionId = member.sessionId;
1663
1720
  let live = true;
1664
1721
  let guard = 0;
@@ -1676,23 +1733,19 @@ let AgentTeam = (() => {
1676
1733
  live = false;
1677
1734
  }
1678
1735
  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.
1736
+ const read = await this.sessionReader.read(sessionId);
1737
+ if (!read.ok) {
1738
+ // An unreadable ancestor ends the lineage walk here — recorded in
1739
+ // the result, never silent, and never a Member-availability fact.
1740
+ incompleteFrom = { sessionId, reason: `${read.failure.kind}: ${read.failure.detail}` };
1694
1741
  sessionId = undefined;
1695
1742
  }
1743
+ else {
1744
+ state = foldContextProjection(read.inspection.events, read.inspection.inheritedEventCount, sessionId);
1745
+ sourceEvents = read.inspection.events;
1746
+ sourceSessionId = sessionId;
1747
+ sessionId = read.inspection.header.parentSession;
1748
+ }
1696
1749
  }
1697
1750
  if (state === undefined)
1698
1751
  break;
@@ -1713,7 +1766,7 @@ let AgentTeam = (() => {
1713
1766
  if (items.length >= limit)
1714
1767
  break;
1715
1768
  }
1716
- return { usageTokens, hardLimit, handoffAt, items };
1769
+ return { usageTokens, hardLimit, handoffAt, items, ...(incompleteFrom === undefined ? {} : { incompleteFrom }) };
1717
1770
  }
1718
1771
  /**
1719
1772
  * Replayed measurement of one lineage source: the live current Session
@@ -1729,19 +1782,16 @@ let AgentTeam = (() => {
1729
1782
  return undefined;
1730
1783
  if (live)
1731
1784
  return meter.measure(agent.session)?.totalTokens;
1785
+ // 0.1.5 removed borrowSession: rebuild a detached Session from the
1786
+ // stored log so the seed's retained cost is priced by the SOURCE's own
1787
+ // replay, never the current generation's. An unreadable source or a
1788
+ // failing meter is unmeasurable and prices as UNKNOWN.
1789
+ const read = await this.sessionReader.read(sessionId);
1790
+ if (!read.ok)
1791
+ return undefined;
1732
1792
  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
- }
1793
+ const session = Session.create(sessionId, read.inspection.events, read.inspection.header, read.inspection.inheritedEventCount);
1794
+ return meter.measure(session)?.totalTokens;
1745
1795
  }
1746
1796
  catch {
1747
1797
  return undefined;
@@ -2097,9 +2147,17 @@ let AgentTeam = (() => {
2097
2147
  // `swap` is bound by the provider at activation (no-op until then).
2098
2148
  const skillSelection = { current: member.capabilities?.skills?.allow, swap: () => { } };
2099
2149
  const setup = async (agentCtx, agent) => {
2100
- await this.ctx.agentPresets.mount(agentCtx, member.presetId);
2101
- this.memberRuntime.applyMemberToolPolicy(agentCtx, member);
2102
- this.validateMemberPreset(agentCtx);
2150
+ try {
2151
+ await this.ctx.agentPresets.mount(agentCtx, member.presetId);
2152
+ this.memberRuntime.applyMemberToolPolicy(agentCtx, member);
2153
+ this.validateMemberPreset(agentCtx);
2154
+ }
2155
+ catch (error) {
2156
+ // Tag composition failures with their own class: the activation
2157
+ // diagnostic routes preset-composition (install/runtime split)
2158
+ // failures by type, not by matching message text.
2159
+ throw new PresetCompositionError(error instanceof Error ? error.message : String(error), { cause: error });
2160
+ }
2103
2161
  installModelSelection(agentCtx, selected);
2104
2162
  // Admission gate for pending context rollovers: once a successful
2105
2163
  // context_rollover result is durable, queued input must not open another
@@ -2276,11 +2334,11 @@ let AgentTeam = (() => {
2276
2334
  this.memberRuntime.forgetMember(member.memberId);
2277
2335
  const message = error instanceof Error ? error.message : String(error);
2278
2336
  this.ctx.logger.warn(`agent-team: activation failed for member '${this.memberLabel(member.memberId)}': ${message}`);
2279
- this.setMemberFailure(member.memberId, 'activation', message);
2337
+ this.setActivationDiagnostic(member.memberId, this.activationDiagnosticOf(error, member.sessionId));
2280
2338
  }
2281
2339
  finally {
2282
2340
  // Activation only changes this Workspace's presence projection.
2283
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
2341
+ this.emitPresenceChanged(member.workspaceId);
2284
2342
  }
2285
2343
  }
2286
2344
  /**
@@ -2368,13 +2426,13 @@ let AgentTeam = (() => {
2368
2426
  // new binding as active — a Client following the row would otherwise open
2369
2427
  // a Session that does not exist yet.
2370
2428
  if (handle.agent.id !== member.sessionId)
2371
- return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic: 'context rollover in progress' });
2429
+ return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic: { class: 'rollover', detail: 'context rollover in progress' } });
2372
2430
  if (this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
2373
- return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: ORPHANED_MEMBER_DIAGNOSTIC });
2431
+ return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: { class: 'preset-composition', detail: ORPHANED_MEMBER_DIAGNOSTIC } });
2374
2432
  }
2375
2433
  const runtimeError = failures?.runtime ?? failures?.compaction;
2376
2434
  if (runtimeError !== undefined)
2377
- return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: runtimeError });
2435
+ return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: { class: 'runtime', detail: runtimeError } });
2378
2436
  // Capability warnings are runtime-derived at activation (handles-scoped,
2379
2437
  // like failures): absent while capabilities resolve cleanly.
2380
2438
  const capabilityWarnings = this.memberRuntime.capabilityWarningsFor(member.memberId);
@@ -2388,6 +2446,37 @@ let AgentTeam = (() => {
2388
2446
  failures[slot] = message;
2389
2447
  this.memberFailures.set(memberId, failures);
2390
2448
  }
2449
+ /** Store one structured activation diagnostic; runtime/compaction slots stay plain messages. */
2450
+ setActivationDiagnostic(memberId, diagnostic) {
2451
+ const failures = this.memberFailures.get(memberId) ?? {};
2452
+ failures.activation = Object.freeze(diagnostic);
2453
+ this.memberFailures.set(memberId, failures);
2454
+ }
2455
+ /**
2456
+ * Route one activation failure to its diagnostic class: preset composition
2457
+ * failures by their own error class, session failures by the seam's typed
2458
+ * classification (our call sites carry it directly; a Harness resume
2459
+ * failure carries it through the cause chain), everything else as an
2460
+ * unclassified activation failure.
2461
+ */
2462
+ activationDiagnosticOf(error, sessionId) {
2463
+ if (error instanceof PresetCompositionError)
2464
+ return { class: 'preset-composition', detail: error.message };
2465
+ const failure = error instanceof StoredSessionReadError ? error.failure : sessionFailureOf(error, sessionId);
2466
+ if (failure !== undefined) {
2467
+ const shared = { detail: failure.detail, ...(failure.location === undefined ? {} : { location: failure.location }), sessionId: failure.sessionId };
2468
+ return failure.kind === 'refused' ? { class: 'session-refused', ...shared } : { class: 'session-unreadable', ...shared };
2469
+ }
2470
+ return { class: 'activation', detail: error instanceof Error ? error.message : String(error) };
2471
+ }
2472
+ /** Mark a session-refused activation diagnostic as proven non-remediable. */
2473
+ markRefusalNonRemediable(memberId) {
2474
+ const failures = this.memberFailures.get(memberId);
2475
+ const activation = failures?.activation;
2476
+ if (activation === undefined || activation.class !== 'session-refused')
2477
+ return;
2478
+ failures.activation = Object.freeze({ ...activation, remediable: false });
2479
+ }
2391
2480
  clearMemberFailure(memberId, slot) {
2392
2481
  const failures = this.memberFailures.get(memberId);
2393
2482
  if (failures === undefined || failures[slot] === undefined)
@@ -2462,7 +2551,17 @@ let AgentTeam = (() => {
2462
2551
  }
2463
2552
  emitAutoCompactionChanged(memberId) {
2464
2553
  const workspaceId = this.ledger?.getMember(memberId)?.workspaceId;
2465
- this.emitChanged(workspaceId === undefined ? undefined : [{ kind: 'workspace', workspaceId }]);
2554
+ if (workspaceId === undefined)
2555
+ return;
2556
+ this.emitPresenceChanged(workspaceId);
2557
+ }
2558
+ /**
2559
+ * Presence-only invalidation: Agent running/idle/activation/failure changes
2560
+ * alter no durable projection, so only members/presence subscribers wake —
2561
+ * the workspace catalog and the scope-less Inbox subscriptions stay parked.
2562
+ */
2563
+ emitPresenceChanged(workspaceId) {
2564
+ this.emitChanged([{ kind: 'presence', workspaceId }]);
2466
2565
  }
2467
2566
  /** Wake from durable unread state with bounded facts for direct and state-changing work. */
2468
2567
  notifyMember(agent, sequenced = false) {
@@ -2646,21 +2745,45 @@ let AgentTeam = (() => {
2646
2745
  * Wake waiters for one committed or lifecycle change. Undefined broadcasts
2647
2746
  * to everyone; an empty scope list invalidates nobody because no shared
2648
2747
  * projection changed; otherwise global and matching scoped waiters wake.
2748
+ * Presence-only scopes sit outside that: they change no durable projection,
2749
+ * so only matching presence waiters wake and the scope-less Inbox
2750
+ * subscriptions stay parked.
2751
+ *
2752
+ * Each woken waiter receives the version of its own scope's domain, so a
2753
+ * wake can never hand a projection waiter a presence number or the other way
2754
+ * around. Nothing here advances the projection version.
2649
2755
  */
2650
2756
  emitChanged(scopes) {
2651
- this.changeVersion += 1;
2757
+ const touchesProjection = scopes === undefined || scopes.some(scope => scope.kind !== 'presence');
2758
+ const touchesPresence = scopes !== undefined && scopes.some(scope => scope.kind === 'presence');
2759
+ if (!touchesProjection && !touchesPresence)
2760
+ return;
2761
+ if (touchesPresence)
2762
+ this.presenceEpoch += 1;
2652
2763
  for (const waiter of this.changeWaiters) {
2653
2764
  const waiterScope = waiter.scope;
2654
- if (scopes !== undefined && (waiterScope === undefined ? scopes.length === 0 : !scopes.some(scope => sameChangeScope(scope, waiterScope))))
2765
+ if (scopes !== undefined && (waiterScope === undefined ? !touchesProjection : !scopes.some(scope => sameChangeScope(scope, waiterScope))))
2655
2766
  continue;
2656
2767
  this.changeWaiters.delete(waiter);
2657
- waiter.wake(this.changeVersion);
2768
+ waiter.wake(this.changeVersionOf(waiterScope));
2658
2769
  }
2659
2770
  }
2771
+ /**
2772
+ * The cursor domain of one scope: presence scopes count process-local edge
2773
+ * wakes, every other scope compares against the durable ledger position of
2774
+ * the newest shared-projection commit. The two domains are deliberately
2775
+ * separate, so a presence edge cannot invalidate a projection subscriber and
2776
+ * a private read invalidates no one.
2777
+ */
2778
+ changeVersionOf(scope) {
2779
+ if (scope?.kind === 'presence')
2780
+ return this.presenceEpoch;
2781
+ return this.ledger?.projectionSequence() ?? 0;
2782
+ }
2660
2783
  validateChangeScope(scope) {
2661
2784
  if (scope === undefined)
2662
2785
  return undefined;
2663
- const ref = scope.kind === 'workspace' ? scope.workspaceId : scope.kind === 'channel' ? scope.channelRef : scope.threadRef;
2786
+ const ref = scope.kind === 'workspace' || scope.kind === 'presence' ? scope.workspaceId : scope.kind === 'channel' ? scope.channelRef : scope.threadRef;
2664
2787
  if (typeof ref !== 'string' || ref.length === 0)
2665
2788
  throw new Error(`change scope of kind '${scope.kind}' requires a non-empty ref`);
2666
2789
  return scope;