@wowyuarm/dsh-agent-team 0.1.9 → 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 (119) hide show
  1. package/README.md +17 -11
  2. package/README.zh.md +17 -11
  3. package/package.json +61 -47
  4. package/packages/agent-team/README.md +6 -4
  5. package/packages/agent-team/README.zh.md +6 -3
  6. package/packages/agent-team/lib/context-management.js +48 -55
  7. package/packages/agent-team/lib/context-projection.js +22 -8
  8. package/packages/agent-team/lib/context-source.js +163 -23
  9. package/packages/agent-team/lib/index.js +349 -176
  10. package/packages/agent-team/lib/invariant.js +38 -4
  11. package/packages/agent-team/lib/ledger.js +658 -294
  12. package/packages/agent-team/lib/member-context.js +6 -2
  13. package/packages/agent-team/lib/member-time-context.js +141 -0
  14. package/packages/agent-team/lib/pressure-policy.js +34 -11
  15. package/packages/agent-team/lib/progress-nudge.js +12 -4
  16. package/packages/agent-team/lib/session-event-cursor.js +116 -0
  17. package/packages/agent-team/lib/session-remediation.js +493 -0
  18. package/packages/agent-team/lib/spec.js +49 -36
  19. package/packages/agent-team/lib/stored-session-reader.js +138 -0
  20. package/packages/agent-team/lib/time-format.js +56 -0
  21. package/packages/agent-team/lib/typert.host.js +163 -63
  22. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  23. package/packages/agent-team/lib/typert.remote-client.js +105 -32
  24. package/packages/agent-team/lib/types/context-management.d.ts +13 -1
  25. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
  26. package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
  27. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  28. package/packages/agent-team/lib/types/context-source.d.ts +70 -36
  29. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/index.d.ts +110 -9
  31. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/ledger.d.ts +205 -7
  34. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  35. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
  37. package/packages/agent-team/lib/types/member-time-context.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/progress-nudge.d.ts.map +1 -1
  41. package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
  42. package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
  43. package/packages/agent-team/lib/types/session-remediation.d.ts +172 -0
  44. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
  45. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  46. package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
  47. package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
  48. package/packages/agent-team/lib/types/time-format.d.ts +28 -0
  49. package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
  50. package/packages/agent-team/lib/types/types/entities.d.ts +36 -2
  51. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  52. package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
  53. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  54. package/packages/agent-team/lib/types/types/requests-results.d.ts +50 -3
  55. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  56. package/packages/agent-team/preset/team-member/agent.cordis.yml +21 -4
  57. package/packages/client-agent-team/README.md +1 -1
  58. package/packages/client-agent-team/README.zh.md +1 -1
  59. package/packages/client-agent-team/lib/client.js +1121 -402
  60. package/packages/client-agent-team/lib/client.js.map +1 -1
  61. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  62. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -4
  63. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +42 -22
  65. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +13 -24
  67. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
  68. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  69. package/packages/client-agent-team/lib/types/client/TeamComposer.js +15 -9
  70. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  71. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/TeamConversation.js +8 -1
  73. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +21 -0
  74. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
  75. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +71 -0
  76. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  77. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
  78. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
  79. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  80. package/packages/client-agent-team/lib/types/client/TeamMessage.js +13 -6
  81. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
  82. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  83. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
  84. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  85. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +106 -32
  86. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  87. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  88. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +50 -7
  89. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  90. package/packages/client-agent-team/lib/types/client/index.js +25 -9
  91. package/packages/client-agent-team/lib/types/client/locales.d.ts +18 -0
  92. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  93. package/packages/client-agent-team/lib/types/client/locales.js +18 -0
  94. package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
  95. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  96. package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
  97. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
  98. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
  99. package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
  100. package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -2
  101. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  102. package/packages/client-agent-team/lib/types/client/team-changes.d.ts +26 -1
  103. package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
  104. package/packages/client-agent-team/lib/types/client/team-changes.js +66 -15
  105. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
  106. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
  107. package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
  108. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
  109. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  110. package/packages/client-agent-team/lib/types/client/team-formatters.js +34 -6
  111. package/packages/tool-agent-team/README.md +8 -6
  112. package/packages/tool-agent-team/README.zh.md +8 -6
  113. package/packages/tool-agent-team/lib/context-tools.js +18 -14
  114. package/packages/tool-agent-team/lib/host-access.js +20 -0
  115. package/packages/tool-agent-team/lib/index.js +314 -123
  116. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  117. package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
  118. package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
  119. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -39,58 +39,6 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
39
39
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
40
40
  done = true;
41
41
  };
42
- var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
43
- if (value !== null && value !== void 0) {
44
- if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
45
- var dispose, inner;
46
- if (async) {
47
- if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
48
- dispose = value[Symbol.asyncDispose];
49
- }
50
- if (dispose === void 0) {
51
- if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
52
- dispose = value[Symbol.dispose];
53
- if (async) inner = dispose;
54
- }
55
- if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
56
- if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
57
- env.stack.push({ value: value, dispose: dispose, async: async });
58
- }
59
- else if (async) {
60
- env.stack.push({ async: true });
61
- }
62
- return value;
63
- };
64
- var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
65
- return function (env) {
66
- function fail(e) {
67
- env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
68
- env.hasError = true;
69
- }
70
- var r, s = 0;
71
- function next() {
72
- while (r = env.stack.pop()) {
73
- try {
74
- if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
75
- if (r.dispose) {
76
- var result = r.dispose.call(r.value);
77
- if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
78
- }
79
- else s |= 1;
80
- }
81
- catch (e) {
82
- fail(e);
83
- }
84
- }
85
- if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
86
- if (env.hasError) throw env.error;
87
- }
88
- return next();
89
- };
90
- })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
91
- var e = new Error(message);
92
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
93
- });
94
42
  import { randomUUID } from 'node:crypto';
95
43
  import { isDeepStrictEqual } from 'node:util';
96
44
  import { Service } from '@deepseek-ai/cordis';
@@ -98,28 +46,42 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
98
46
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
99
47
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
100
48
  import { scopeOf } from '@deepseek-ai/dsh-scope';
101
- import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
49
+ import { Session, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
102
50
  import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
103
51
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
104
52
  import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
105
53
  import { PressurePolicyCoordinator } from "./pressure-policy.js";
106
54
  import { ContextManagementCoordinator } from "./context-management.js";
107
- import { createHandoffMessage } from "./context-source.js";
108
- import { carriedInputOf, checkpointByRef, checkpointRefFor, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
55
+ import { AGENT_TEAM_PLUGIN_ID, createHandoffMessage } from "./context-source.js";
56
+ import { carriedInputOf, checkpointByRef, checkpointRefFor, contextProjectionFold, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
57
+ import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
109
58
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
110
59
  import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, memberMemoryDirectoryName, MemberRuntime } from "./member-runtime.js";
111
60
  import { ProgressNudgeCoordinator } from "./progress-nudge.js";
112
61
  import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
62
+ import { SessionRemediation, handoffAlreadyInLog } from "./session-remediation.js";
63
+ import { StoredSessionReadError, StoredSessionReader, sessionFailureOf } from "./stored-session-reader.js";
113
64
  import { agentTeamDomainSpec } from "./spec.js";
65
+ import { formatTeamTimestamp } from "./time-format.js";
114
66
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
115
67
  export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
116
68
  export { AGENT_TEAM_TOOL_NAMES } from "./member-runtime.js";
117
69
  /** Process-stable marker carried by the final Team message tool definition. */
118
70
  export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.preset');
119
- const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
120
71
  const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
121
72
  const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
122
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
+ }
123
85
  /** Longest accepted model-supplied checkpoint display name. */
124
86
  const MAX_CHECKPOINT_NAME_CHARS = 120;
125
87
  /** Default and maximum number of timeline items one query returns. */
@@ -145,6 +107,8 @@ function sameChangeScope(left, right) {
145
107
  return left.channelRef === right.channelRef;
146
108
  if (left.kind === 'thread' && right.kind === 'thread')
147
109
  return left.threadRef === right.threadRef;
110
+ if (left.kind === 'presence' && right.kind === 'presence')
111
+ return left.workspaceId === right.workspaceId;
148
112
  return false;
149
113
  }
150
114
  /** Mark the preset's `team_message` definition as an Agent Team consumer. */
@@ -152,6 +116,35 @@ export function markAgentTeamPreset(definition) {
152
116
  Object.defineProperty(definition, AGENT_TEAM_PRESET_MARKER, { value: true });
153
117
  return definition;
154
118
  }
119
+ /**
120
+ * Whether the running dsh loads workspace packages from source via tsx.
121
+ *
122
+ * The Harness `tsconfig.base.json` maps `@deepseek-ai/*` package names onto
123
+ * `src/` directories; tsx honors those paths, so a CLI launched with
124
+ * `node --import tsx/esm apps/cli/src/bin.ts` imports `@deepseek-ai/dsh-scope`
125
+ * from `src/index.ts` while a profile-installed bundle resolves the compiled
126
+ * `lib/index.js` — two module instances with independent scope keys.
127
+ */
128
+ export function isTsxDevMode() {
129
+ const flags = [...process.execArgv];
130
+ const nodeOptions = process.env.NODE_OPTIONS;
131
+ if (nodeOptions !== undefined)
132
+ flags.push(...nodeOptions.split(/\s+/));
133
+ return flags.some(flag => flag.includes('tsx'));
134
+ }
135
+ /**
136
+ * The activation diagnostic for a dsh-scope module-instance mismatch.
137
+ *
138
+ * `agentPresets.mount` already rejected an unscoped context, so a scope key
139
+ * the harness sees but this bundle does not can only mean the two sides
140
+ * loaded different physical copies of `@deepseek-ai/dsh-scope`.
141
+ */
142
+ export function teamPresetScopeMismatchMessage(tsxDevMode) {
143
+ if (tsxDevMode) {
144
+ return 'selected preset is not team-enabled: the dsh CLI is running from source via tsx (tsconfig paths resolve @deepseek-ai/dsh-scope to src/), so the harness and this bundle load different module instances; start dsh with the compiled CLI instead (pnpm dsh, node apps/cli/lib/bin.js, or npx @deepseek-ai/dsh)';
145
+ }
146
+ return 'selected preset is not team-enabled: this bundle and the harness resolved different physical copies of @deepseek-ai/dsh-scope; run pnpm install in the profile directory so node_modules matches the lockfile, then restart';
147
+ }
155
148
  /**
156
149
  * A DM was durably recorded but its session injection could not run (no live
157
150
  * handle, or the wake itself failed). The recorded DM stays durable; the
@@ -282,6 +275,13 @@ let AgentTeam = (() => {
282
275
  },
283
276
  runningAgents: this.runningAgents,
284
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);
285
285
  /**
286
286
  * Why one Member shows error presence, per failure source. Reads prefer
287
287
  * activation, then runtime, then compaction; slots clear independently, so
@@ -318,6 +318,14 @@ let AgentTeam = (() => {
318
318
  },
319
319
  log: message => { this.ctx.logger.warn(message); },
320
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();
321
329
  /**
322
330
  * Context self-management: the one deep module that turns a Member's
323
331
  * successful `context_rollover` tool result into its next private context
@@ -332,15 +340,34 @@ let AgentTeam = (() => {
332
340
  const handle = this.handles.get(memberId);
333
341
  if (handle === undefined || handle.agent.session.id !== sessionId)
334
342
  return undefined;
335
- 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;
336
352
  },
337
353
  executeTransition: (memberId, plan) => this.executeMemberTransition(memberId, plan),
338
354
  log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
339
355
  });
340
356
  lifecycleTail = Promise.resolve();
341
357
  accepting = true;
342
- 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;
343
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;
344
371
  constructor(ctx) {
345
372
  super(ctx, 'agentTeam');
346
373
  this.pressurePolicy = new PressurePolicyCoordinator({
@@ -372,7 +399,7 @@ let AgentTeam = (() => {
372
399
  if (kind !== undefined)
373
400
  this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable ${kind} error; recording a consecutive error occurrence`);
374
401
  this.recovery.onError(member.memberId, message);
375
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
402
+ this.emitPresenceChanged(member.workspaceId);
376
403
  });
377
404
  this.ctx.on('agent/status', ({ agent, status }) => {
378
405
  const member = this.memberForAgent(agent);
@@ -384,7 +411,7 @@ let AgentTeam = (() => {
384
411
  const recovered = this.clearMemberFailure(member.memberId, 'runtime');
385
412
  if (recovered)
386
413
  this.notifiedInbox.delete(member.memberId);
387
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
414
+ this.emitPresenceChanged(member.workspaceId);
388
415
  // A rollover/recovery in flight delivers its own sequenced
389
416
  // rederived Inbox after the handoff and carried input; a status-driven
390
417
  // steer here would claim the handoff turn's next step and leapfrog
@@ -394,14 +421,14 @@ let AgentTeam = (() => {
394
421
  }
395
422
  // A turn that ends without an error closes any automatic recovery episode.
396
423
  // The idle transition is itself presence-affecting (working → available),
397
- // so it must wake workspace watchers exactly like the running transition
424
+ // so it must wake presence watchers exactly like the running transition
398
425
  // above; without this wake, cached Client member rows keep showing the
399
426
  // Member as working after every turn until an unrelated change arrives.
400
427
  if (status === 'idle' && member !== undefined) {
401
428
  if (this.memberFailures.get(member.memberId)?.runtime === undefined) {
402
429
  this.recovery.onCleanTurnEnd(member.memberId);
403
430
  }
404
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
431
+ this.emitPresenceChanged(member.workspaceId);
405
432
  }
406
433
  });
407
434
  // Progress nudges count every `tool/call` of each Member Session. The
@@ -431,6 +458,7 @@ let AgentTeam = (() => {
431
458
  const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
432
459
  this.ctx.effect(() => async () => {
433
460
  this.accepting = false;
461
+ this.remediation = undefined;
434
462
  this.recovery.dispose();
435
463
  this.progressNudge.dispose();
436
464
  this.contextManagement.dispose();
@@ -454,9 +482,21 @@ let AgentTeam = (() => {
454
482
  if (initialization.committed)
455
483
  this.emitCommitted(initialization.value);
456
484
  this.startAttachmentGc(ledger);
485
+ // Legacy-artifact remediation runs before any Member activation: no write
486
+ // lease exists yet, so publishing sibling generations for refused Session
487
+ // logs cannot race a live writer. Remediation failure never blocks
488
+ // startup — the next start retries exactly what the cache does not cover.
489
+ try {
490
+ const remediation = new SessionRemediation(this.ctx, this.ctx.sessionPersistence, await SessionRemediation.open(this.ctx));
491
+ this.remediation = remediation;
492
+ await remediation.remediateEnabledMembers(ledger.listMembers());
493
+ }
494
+ catch (error) {
495
+ this.ctx.logger.warn(`agent-team: legacy Session remediation did not run to completion (it will retry on the next start): ${error instanceof Error ? error.message : String(error)}`);
496
+ }
457
497
  // One metadata listing serves every Member restore; per-member list calls
458
498
  // would repeat the same I/O linearly during startup.
459
- const persistedSessions = new Set((await this.persistedSessionHeaders()).map(header => header.id));
499
+ const persistedSessions = new Set((await this.persistedSessionHeaders()).map(snapshot => snapshot.header.id));
460
500
  for (const member of ledger.listMembers()) {
461
501
  if (member.state === 'enabled')
462
502
  await this.activateMember(member, undefined, persistedSessions);
@@ -466,20 +506,14 @@ let AgentTeam = (() => {
466
506
  }
467
507
  /**
468
508
  * Whether one Member Session has durable persisted content, decided through
469
- * {@link SessionPersistenceService.inspect} rather than a bare metadata
470
- * listing: inspection first awaits any in-flight retirement drain for the
471
- * id, so a resume racing a suspend's fire-and-forget final flush cannot
472
- * mistake a still-draining persisted Session for an unpersisted one and
473
- * fork a fresh generation over it.
509
+ * {@link SessionPersistence.stat} rather than a bare metadata listing: the
510
+ * backend reports a still-draining session through its pending header, so a
511
+ * resume racing a suspend's fire-and-forget final flush cannot mistake a
512
+ * still-draining persisted Session for an unpersisted one and fork a fresh
513
+ * generation over it.
474
514
  */
475
515
  async sessionPersisted(sessionId) {
476
- try {
477
- await this.ctx.sessionPersistence.inspect(sessionId);
478
- return true;
479
- }
480
- catch {
481
- return false;
482
- }
516
+ return this.sessionReader.exists(sessionId);
483
517
  }
484
518
  /** Resolve one exact live Agent to its durable Team Member; forks do not inherit identity. */
485
519
  memberForAgent(agent) {
@@ -517,8 +551,9 @@ let AgentTeam = (() => {
517
551
  if (!Number.isInteger(request.afterVersion) || request.afterVersion < 0)
518
552
  throw new Error('afterVersion must be a non-negative integer');
519
553
  const scope = this.validateChangeScope(request.scope);
520
- if (this.changeVersion > request.afterVersion || !this.accepting)
521
- return Object.freeze({ version: this.changeVersion });
554
+ const version = this.changeVersionOf(scope);
555
+ if (version > request.afterVersion || !this.accepting)
556
+ return Object.freeze({ version });
522
557
  return new Promise((resolve, reject) => {
523
558
  let settled = false;
524
559
  const waiter = {
@@ -537,7 +572,7 @@ let AgentTeam = (() => {
537
572
  return;
538
573
  settled = true;
539
574
  this.changeWaiters.delete(waiter);
540
- resolve(Object.freeze({ version: this.changeVersion }));
575
+ resolve(Object.freeze({ version: this.changeVersionOf(scope) }));
541
576
  }, 25_000);
542
577
  const onAbort = () => {
543
578
  if (settled)
@@ -675,6 +710,23 @@ let AgentTeam = (() => {
675
710
  if (member.state !== 'enabled')
676
711
  throw new Error(`Agent Member '${member.handle}' is ${member.state}; only enabled Members can be restarted`);
677
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
+ }
678
730
  await this.reactivateMember(request.memberId);
679
731
  return Object.freeze({ status: this.memberStatus(member) });
680
732
  }
@@ -1094,7 +1146,7 @@ let AgentTeam = (() => {
1094
1146
  this.emitCommitted(result.value.receipt);
1095
1147
  return result.value;
1096
1148
  }
1097
- /** 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. */
1098
1150
  inbox(request) {
1099
1151
  this.requireWorkspace(request.workspaceId);
1100
1152
  return this.requireLedger().inbox(agentTeamHumanActor(), request);
@@ -1103,8 +1155,10 @@ let AgentTeam = (() => {
1103
1155
  async readThread(request) {
1104
1156
  const actor = this.humanCall(request.workspaceId);
1105
1157
  const result = await this.requireLedger().readThread({ ...request, actor });
1106
- if (result.committed)
1107
- 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);
1108
1162
  return result.value;
1109
1163
  }
1110
1164
  /** Human-only durable Attention observations for one Thread. */
@@ -1163,8 +1217,10 @@ let AgentTeam = (() => {
1163
1217
  async readThreadForAgent(agent, request) {
1164
1218
  const actor = this.memberCall(agent, request.workspaceId);
1165
1219
  const result = await this.requireLedger().readThread({ ...request, actor });
1166
- if (result.committed)
1167
- 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);
1168
1224
  const value = result.value;
1169
1225
  // Private read-time enrich: an acceptance the reader just acknowledged is
1170
1226
  // a natural Task boundary, so the Host prices the reader's context once,
@@ -1240,7 +1296,7 @@ let AgentTeam = (() => {
1240
1296
  }
1241
1297
  try {
1242
1298
  const message = createUserMessage({
1243
- content: [{ type: 'text', text: this.dmRelayText(agent, recipient, request.body.trim(), result.value.receipt.operationId) }],
1299
+ content: [{ type: 'text', text: this.dmRelayText(agent, recipient, request.body.trim(), result.value.receipt.occurredAt, result.value.receipt.operationId) }],
1244
1300
  source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'relay' },
1245
1301
  });
1246
1302
  // An idle recipient gets one ordinary turn; a busy one is steered into
@@ -1256,10 +1312,10 @@ let AgentTeam = (() => {
1256
1312
  return result.value;
1257
1313
  }
1258
1314
  /** Relay body: the DM itself plus one bounded line of adjacent context. */
1259
- dmRelayText(senderAgent, recipient, body, excluding) {
1315
+ dmRelayText(senderAgent, recipient, body, occurredAt, excluding) {
1260
1316
  const sender = this.memberForAgent(senderAgent);
1261
1317
  const prior = this.requireLedger().dmHistoryBetween(senderAgent.id, recipient.memberId, excluding);
1262
- const header = `Direct message from @${sender?.handle ?? 'a Team Member'}:`;
1318
+ const header = `Direct message from @${sender?.handle ?? 'a Team Member'} at ${formatTeamTimestamp(occurredAt)}:`;
1263
1319
  const context = prior === undefined ? '' : `\n\n[most recent prior DM between you: ${prior}]`;
1264
1320
  return `${header}\n\n${body}${context}`;
1265
1321
  }
@@ -1281,6 +1337,16 @@ let AgentTeam = (() => {
1281
1337
  validateLedger() {
1282
1338
  this.requireLedger().validate();
1283
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
+ }
1284
1350
  /**
1285
1351
  * Effective context-pressure budget for one Member's current route:
1286
1352
  * `hardLimit = min(256K, routeWindow - outputReserve)` and
@@ -1378,15 +1444,13 @@ let AgentTeam = (() => {
1378
1444
  live = false;
1379
1445
  }
1380
1446
  else {
1381
- try {
1382
- const inspection = await this.ctx.sessionPersistence.inspect(sessionId);
1383
- events = inspection.events;
1384
- inheritedEventCount = inspection.inheritedEventCount;
1385
- parentSession = inspection.meta.parentSession;
1386
- }
1387
- catch (error) {
1388
- throw new Error(`checkpoint '${checkpointRef}' could not be resolved: its source Session is unreadable (${error instanceof Error ? error.message : String(error)})`);
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);
1389
1450
  }
1451
+ events = read.inspection.events;
1452
+ inheritedEventCount = read.inspection.inheritedEventCount;
1453
+ parentSession = read.inspection.header.parentSession;
1390
1454
  }
1391
1455
  // Fold the source with its inherited cut respected: inherited events
1392
1456
  // are resolved history in that source, never fresh intent; checkpoints
@@ -1463,15 +1527,15 @@ let AgentTeam = (() => {
1463
1527
  ?? this.requireLedger().previousSessionForMember(member.memberId);
1464
1528
  if (previousSessionId === undefined)
1465
1529
  return;
1466
- let inspection;
1467
- try {
1468
- const result = await this.ctx.sessionPersistence.inspect(previousSessionId);
1469
- inspection = result;
1470
- }
1471
- catch (error) {
1472
- this.ctx.logger.warn(`agent-team: rollover handoff reconstruction could not read the previous Session '${previousSessionId}': ${error instanceof Error ? error.message : String(error)}`);
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}`);
1473
1536
  return;
1474
1537
  }
1538
+ const inspection = previousRead.inspection;
1475
1539
  const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, previousSessionId);
1476
1540
  if (state.pending === null)
1477
1541
  return;
@@ -1500,13 +1564,12 @@ let AgentTeam = (() => {
1500
1564
  * downgrade to a blank child.
1501
1565
  */
1502
1566
  async recordedCheckpointPrefix(seed) {
1503
- let inspection;
1504
- try {
1505
- inspection = await this.ctx.sessionPersistence.inspect(seed.sourceSessionId);
1506
- }
1507
- catch (error) {
1508
- throw new Error(`the recorded checkpoint-return seed source Session '${seed.sourceSessionId}' is unreadable: ${error instanceof Error ? error.message : String(error)}`);
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}'`);
@@ -1530,21 +1593,56 @@ let AgentTeam = (() => {
1530
1593
  * CURRENTLY present: delivered `user/message` ids plus the live inbox's
1531
1594
  * pending next-step/next-turn ids — a historical insert that was already
1532
1595
  * claimed (and removed) but never surfaced is NOT known and must replay.
1533
- * Fail-closed: an unreadable previous Session rejects the activation
1534
- * rather than silently dropping the Member's input.
1596
+ * Fail-closed for genuinely unreadable previous Sessions (missing/IO) so
1597
+ * the Member's input is never dropped silently, with three bounded
1598
+ * exceptions: a generation that already started its own turns needs no
1599
+ * replay (its carried input was delivered or superseded while it ran), a
1600
+ * log-corruption class error skips with a warning (the Host repairs torn
1601
+ * tails; a retired generation's corrupt log must not permanently block the
1602
+ * Member's activation), and a deterministic released-format refusal skips
1603
+ * with a warning (the candidate's own migration audit refuses that
1604
+ * artifact, so no retry can ever read it).
1535
1605
  */
1536
- async replayCarriedInput(member, agent) {
1606
+ async replayCarriedInput(member, agent, generationStarted) {
1537
1607
  const transition = this.requireLedger().lastTransitionForMember(member.memberId);
1538
1608
  if (transition === undefined || transition.targetSessionId !== agent.session.id)
1539
1609
  return 0;
1540
- let inspection;
1541
- try {
1542
- inspection = await this.ctx.sessionPersistence.inspect(transition.previousSessionId);
1610
+ // A generation that already started its own turns needs no previous-Session
1611
+ // replay: its carried input was delivered with the handoff (or superseded)
1612
+ // before any own turn could run. Skipping the inspect also keeps a later
1613
+ // corruption or loss of the retired Session from re-blocking this Member
1614
+ // on every restart of the current generation.
1615
+ if (generationStarted) {
1616
+ this.ctx.logger.info(`agent-team: skipping carried-input replay for member '${this.memberLabel(member.memberId)}': the current Session '${agent.session.id}' already started a generation`);
1617
+ return 0;
1543
1618
  }
1544
- catch (error) {
1545
- throw new Error(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable: ${error instanceof Error ? error.message : String(error)}`);
1619
+ let inspectionEvents;
1620
+ let inspectionInherited;
1621
+ const previousRead = await this.sessionReader.read(transition.previousSessionId);
1622
+ if (!previousRead.ok) {
1623
+ const failure = previousRead.failure;
1624
+ // Log-corruption class: bounded fail-open. The current Session's own
1625
+ // fold is intact and the Host repairs the retired log's torn tail; a
1626
+ // corrupted retired generation must not permanently block activation.
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`);
1629
+ return 0;
1630
+ }
1631
+ // Deterministic released-format refusal: the retired artifact is refused
1632
+ // by the candidate's own migration audit, which no retry can change.
1633
+ // Failing activation here would turn a data problem in a Session the
1634
+ // Member no longer runs in into permanent unavailability — the exact
1635
+ // failure class this hardening removes. Genuine IO and unknown failures
1636
+ // stay fail-closed so carried input is never dropped silently.
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`);
1639
+ return 0;
1640
+ }
1641
+ throw new StoredSessionReadError(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable (${failure.kind}): ${failure.detail}`, failure);
1546
1642
  }
1547
- const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, transition.previousSessionId);
1643
+ inspectionEvents = previousRead.inspection.events;
1644
+ inspectionInherited = previousRead.inspection.inheritedEventCount;
1645
+ const state = foldContextProjection(inspectionEvents, inspectionInherited, transition.previousSessionId);
1548
1646
  const carried = carriedInputOf(state);
1549
1647
  if (carried.length === 0)
1550
1648
  return 0;
@@ -1617,6 +1715,7 @@ let AgentTeam = (() => {
1617
1715
  // persisted logs; the same projection definition folds every source.
1618
1716
  const items = [];
1619
1717
  const seen = new Set();
1718
+ let incompleteFrom;
1620
1719
  let sessionId = member.sessionId;
1621
1720
  let live = true;
1622
1721
  let guard = 0;
@@ -1634,17 +1733,19 @@ let AgentTeam = (() => {
1634
1733
  live = false;
1635
1734
  }
1636
1735
  else {
1637
- try {
1638
- const inspection = await this.ctx.sessionPersistence.inspect(sessionId);
1639
- state = foldContextProjection(inspection.events, inspection.inheritedEventCount, sessionId);
1640
- sourceEvents = inspection.events;
1641
- sourceSessionId = sessionId;
1642
- sessionId = inspection.meta.parentSession;
1643
- }
1644
- catch {
1645
- // An unreadable ancestor ends the lineage walk here.
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}` };
1646
1741
  sessionId = undefined;
1647
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
+ }
1648
1749
  }
1649
1750
  if (state === undefined)
1650
1751
  break;
@@ -1665,7 +1766,7 @@ let AgentTeam = (() => {
1665
1766
  if (items.length >= limit)
1666
1767
  break;
1667
1768
  }
1668
- return { usageTokens, hardLimit, handoffAt, items };
1769
+ return { usageTokens, hardLimit, handoffAt, items, ...(incompleteFrom === undefined ? {} : { incompleteFrom }) };
1669
1770
  }
1670
1771
  /**
1671
1772
  * Replayed measurement of one lineage source: the live current Session
@@ -1681,31 +1782,16 @@ let AgentTeam = (() => {
1681
1782
  return undefined;
1682
1783
  if (live)
1683
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;
1684
1792
  try {
1685
- const env_1 = { stack: [], error: void 0, hasError: false };
1686
- try {
1687
- const borrowed = __addDisposableResource(env_1, await this.ctx.sessionPersistence.borrowSession(sessionId)
1688
- // The borrow resolves the exact Session object for both shapes: the
1689
- // live instance for an attached Session, the prepared one for an
1690
- // archived ancestor. Measuring through the service context's store
1691
- // would need an inject this service does not declare.
1692
- , false);
1693
- // The borrow resolves the exact Session object for both shapes: the
1694
- // live instance for an attached Session, the prepared one for an
1695
- // archived ancestor. Measuring through the service context's store
1696
- // would need an inject this service does not declare.
1697
- const session = borrowed.source === 'live'
1698
- ? agent.session.id === sessionId ? agent.session : undefined
1699
- : borrowed.preparedSession;
1700
- return session === undefined ? undefined : meter.measure(session)?.totalTokens;
1701
- }
1702
- catch (e_1) {
1703
- env_1.error = e_1;
1704
- env_1.hasError = true;
1705
- }
1706
- finally {
1707
- __disposeResources(env_1);
1708
- }
1793
+ const session = Session.create(sessionId, read.inspection.events, read.inspection.header, read.inspection.inheritedEventCount);
1794
+ return meter.measure(session)?.totalTokens;
1709
1795
  }
1710
1796
  catch {
1711
1797
  return undefined;
@@ -2060,10 +2146,18 @@ let AgentTeam = (() => {
2060
2146
  // allow-list filters the catalog by name through the live ref below.
2061
2147
  // `swap` is bound by the provider at activation (no-op until then).
2062
2148
  const skillSelection = { current: member.capabilities?.skills?.allow, swap: () => { } };
2063
- const setup = async (agentCtx) => {
2064
- await this.ctx.agentPresets.mount(agentCtx, member.presetId);
2065
- this.memberRuntime.applyMemberToolPolicy(agentCtx, member);
2066
- this.validateMemberPreset(agentCtx);
2149
+ const setup = async (agentCtx, agent) => {
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
+ }
2067
2161
  installModelSelection(agentCtx, selected);
2068
2162
  // Admission gate for pending context rollovers: once a successful
2069
2163
  // context_rollover result is durable, queued input must not open another
@@ -2107,9 +2201,6 @@ let AgentTeam = (() => {
2107
2201
  });
2108
2202
  return {
2109
2203
  commit: () => {
2110
- const agent = agentCtx.agent;
2111
- if (agent === undefined)
2112
- throw new Error('agent-team setup has no unpublished Agent');
2113
2204
  // The member scope composes no sandbox-policy service (the preset
2114
2205
  // owns no sandbox row), so read the last logged mode straight from
2115
2206
  // the session log; `sandbox/mode` is log-only and never joins the
@@ -2179,6 +2270,11 @@ let AgentTeam = (() => {
2179
2270
  // The one-shot pressure notice needs no explicit re-arm here: it latches
2180
2271
  // on durable Session evidence, and a fresh generation's own event span
2181
2272
  // starts empty — a new Session is itself the re-arm.
2273
+ // Snapshot whether this generation had already started its own turns
2274
+ // BEFORE any recovery delivery below: the handoff steer and the Inbox
2275
+ // wake legitimately append `turn/start` to this Session, and those must
2276
+ // never be mistaken for the generation having run on its own.
2277
+ const generationStarted = created.agent.session.ownEvents().some(event => event.type === 'turn/start');
2182
2278
  // A restart between a durable rollover intent and its swap replays the
2183
2279
  // old Session; the projection still carries the intent, so finish the
2184
2280
  // transition (or keep waiting for the containing turn) from here.
@@ -2194,14 +2290,18 @@ let AgentTeam = (() => {
2194
2290
  // delivered activates the new Session with no handoff in its own
2195
2291
  // log — never treat that as an ordinary blank Member Session. The
2196
2292
  // operation's recorded previous Session (the lineage parent) still
2197
- // holds the intent; rebuild the handoff from it.
2198
- if (!state.boundaries.some(boundary => boundary.source === 'handoff')) {
2293
+ // holds the intent; rebuild the handoff from it. "No handoff in its
2294
+ // own log" is judged over both shapes: a generation rescued from the
2295
+ // retired custom kinds carries its handoff as a source the projection
2296
+ // does not classify, and rebuilding on top of it would inject the same
2297
+ // handoff twice.
2298
+ if (!handoffAlreadyInLog(state.boundaries, created.agent.session.ownEvents())) {
2199
2299
  await this.reconstructMissingHandoff(member, created.agent);
2200
2300
  }
2201
2301
  // Carried input redelivery binds to the committed transition target —
2202
2302
  // this Session — never to the handoff's presence: a crash after the
2203
2303
  // handoff landed but before the carried enqueue still replays here.
2204
- recoveryCarried = await this.replayCarriedInput(member, created.agent);
2304
+ recoveryCarried = await this.replayCarriedInput(member, created.agent, generationStarted);
2205
2305
  }
2206
2306
  else if (forkedFrom === undefined && lineageParent !== undefined) {
2207
2307
  // A restart recreated a Session that never materialized before the
@@ -2215,7 +2315,7 @@ let AgentTeam = (() => {
2215
2315
  // handoff right after — that delivery is the plan, never this
2216
2316
  // recovery.
2217
2317
  await this.reconstructMissingHandoff(member, created.agent);
2218
- recoveryCarried = await this.replayCarriedInput(member, created.agent);
2318
+ recoveryCarried = await this.replayCarriedInput(member, created.agent, generationStarted);
2219
2319
  }
2220
2320
  // The ordinary Inbox wake runs LAST, after any recovery delivery above:
2221
2321
  // the handoff must stay the first model-facing context of a recovered
@@ -2232,11 +2332,13 @@ let AgentTeam = (() => {
2232
2332
  await created?.dispose();
2233
2333
  this.modelSelections.delete(member.memberId);
2234
2334
  this.memberRuntime.forgetMember(member.memberId);
2235
- this.setMemberFailure(member.memberId, 'activation', error instanceof Error ? error.message : String(error));
2335
+ const message = error instanceof Error ? error.message : String(error);
2336
+ this.ctx.logger.warn(`agent-team: activation failed for member '${this.memberLabel(member.memberId)}': ${message}`);
2337
+ this.setActivationDiagnostic(member.memberId, this.activationDiagnosticOf(error, member.sessionId));
2236
2338
  }
2237
2339
  finally {
2238
2340
  // Activation only changes this Workspace's presence projection.
2239
- this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
2341
+ this.emitPresenceChanged(member.workspaceId);
2240
2342
  }
2241
2343
  }
2242
2344
  /**
@@ -2292,6 +2394,12 @@ let AgentTeam = (() => {
2292
2394
  const scope = scopeOf(agentCtx);
2293
2395
  const teamMessage = this.ctx.tools.get('team_message', scope);
2294
2396
  if (teamMessage?.[AGENT_TEAM_PRESET_MARKER] !== true) {
2397
+ // `agentPresets.mount` already rejected an unscoped context, so a scope
2398
+ // key the harness sees but this bundle does not means the two sides
2399
+ // loaded different module instances of @deepseek-ai/dsh-scope (the
2400
+ // common trigger is running the CLI from source via tsx).
2401
+ if (scope === undefined)
2402
+ throw new Error(teamPresetScopeMismatchMessage(isTsxDevMode()));
2295
2403
  throw new Error('selected preset is not team-enabled');
2296
2404
  }
2297
2405
  const available = new Set(this.ctx.tools.schemas(scope).map(tool => tool.name));
@@ -2318,13 +2426,13 @@ let AgentTeam = (() => {
2318
2426
  // new binding as active — a Client following the row would otherwise open
2319
2427
  // a Session that does not exist yet.
2320
2428
  if (handle.agent.id !== member.sessionId)
2321
- 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' } });
2322
2430
  if (this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
2323
- 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 } });
2324
2432
  }
2325
2433
  const runtimeError = failures?.runtime ?? failures?.compaction;
2326
2434
  if (runtimeError !== undefined)
2327
- return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: runtimeError });
2435
+ return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: { class: 'runtime', detail: runtimeError } });
2328
2436
  // Capability warnings are runtime-derived at activation (handles-scoped,
2329
2437
  // like failures): absent while capabilities resolve cleanly.
2330
2438
  const capabilityWarnings = this.memberRuntime.capabilityWarningsFor(member.memberId);
@@ -2338,6 +2446,37 @@ let AgentTeam = (() => {
2338
2446
  failures[slot] = message;
2339
2447
  this.memberFailures.set(memberId, failures);
2340
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
+ }
2341
2480
  clearMemberFailure(memberId, slot) {
2342
2481
  const failures = this.memberFailures.get(memberId);
2343
2482
  if (failures === undefined || failures[slot] === undefined)
@@ -2412,7 +2551,17 @@ let AgentTeam = (() => {
2412
2551
  }
2413
2552
  emitAutoCompactionChanged(memberId) {
2414
2553
  const workspaceId = this.ledger?.getMember(memberId)?.workspaceId;
2415
- 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 }]);
2416
2565
  }
2417
2566
  /** Wake from durable unread state with bounded facts for direct and state-changing work. */
2418
2567
  notifyMember(agent, sequenced = false) {
@@ -2503,7 +2652,7 @@ let AgentTeam = (() => {
2503
2652
  if (direct && fact.kind === 'message') {
2504
2653
  const sender = fact.message.sender === AGENT_TEAM_HUMAN_MEMBER_ID
2505
2654
  ? 'human' : this.requireLedger().getMember(fact.message.sender)?.handle ?? fact.message.sender;
2506
- const detail = ['Direct Team mention', `From: ${sender}`, `Channel: ${item.channelRef}`,
2655
+ const detail = ['Direct Team mention', `Occurred at: ${formatTeamTimestamp(fact.occurredAt)}`, `From: ${sender}`, `Channel: ${item.channelRef}`,
2507
2656
  ...(item.task === undefined ? [] : [`Task: ${item.task.taskRef}`]),
2508
2657
  `Thread: ${item.thread.threadRef}`, `Message ref: ${fact.message.messageRef}`,
2509
2658
  `Message: ${this.boundedNotificationBody(fact.message.body)}`].join('\n');
@@ -2511,15 +2660,15 @@ let AgentTeam = (() => {
2511
2660
  detailedFactCount += 1;
2512
2661
  }
2513
2662
  else if (fact.kind === 'activity') {
2514
- const detail = `${this.activityNotification(fact.activity, readerId)}\nThread: ${item.thread.threadRef}`;
2663
+ const detail = `${this.activityNotification(fact.activity, readerId)}\nOccurred at: ${formatTeamTimestamp(fact.occurredAt)}\nThread: ${item.thread.threadRef}`;
2515
2664
  if (append(detail))
2516
2665
  detailedFactCount += 1;
2517
2666
  }
2518
2667
  }
2519
- const ordinaryCount = facts.filter(entry => entry.fact.kind === 'message' && !entry.direct).length;
2520
- if (ordinaryCount > 0) {
2668
+ const ordinary = facts.filter(entry => entry.fact.kind === 'message' && !entry.direct);
2669
+ if (ordinary.length > 0) {
2521
2670
  const route = item.task === undefined ? `Thread ${item.thread.threadRef}` : `Task ${item.task.taskRef}`;
2522
- append(`${route}: ${ordinaryCount} unread update${ordinaryCount === 1 ? '' : 's'}.`);
2671
+ append(`${route}: ${ordinary.length} unread update${ordinary.length === 1 ? '' : 's'} · newest at ${formatTeamTimestamp(ordinary.at(-1).fact.occurredAt)}.`);
2523
2672
  }
2524
2673
  }
2525
2674
  if (omitted)
@@ -2596,21 +2745,45 @@ let AgentTeam = (() => {
2596
2745
  * Wake waiters for one committed or lifecycle change. Undefined broadcasts
2597
2746
  * to everyone; an empty scope list invalidates nobody because no shared
2598
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.
2599
2755
  */
2600
2756
  emitChanged(scopes) {
2601
- 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;
2602
2763
  for (const waiter of this.changeWaiters) {
2603
2764
  const waiterScope = waiter.scope;
2604
- 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))))
2605
2766
  continue;
2606
2767
  this.changeWaiters.delete(waiter);
2607
- waiter.wake(this.changeVersion);
2768
+ waiter.wake(this.changeVersionOf(waiterScope));
2608
2769
  }
2609
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
+ }
2610
2783
  validateChangeScope(scope) {
2611
2784
  if (scope === undefined)
2612
2785
  return undefined;
2613
- 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;
2614
2787
  if (typeof ref !== 'string' || ref.length === 0)
2615
2788
  throw new Error(`change scope of kind '${scope.kind}' requires a non-empty ref`);
2616
2789
  return scope;