@wowyuarm/dsh-agent-team 0.1.14-alpha.2 → 0.1.15

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 (106) hide show
  1. package/README.md +6 -4
  2. package/README.zh.md +6 -4
  3. package/cordis.patch.yml +3 -1
  4. package/package.json +51 -46
  5. package/packages/agent-team/README.md +2 -2
  6. package/packages/agent-team/README.zh.md +2 -2
  7. package/packages/agent-team/core-skills/member-memory-manager/SKILL.md +30 -0
  8. package/packages/agent-team/lib/context-continuity-host.js +90 -0
  9. package/packages/agent-team/lib/context-projection.js +266 -489
  10. package/packages/agent-team/lib/context-source.js +4 -72
  11. package/packages/agent-team/lib/human-avatar.js +61 -0
  12. package/packages/agent-team/lib/human-profile.js +157 -0
  13. package/packages/agent-team/lib/human-update-check.js +119 -0
  14. package/packages/agent-team/lib/index.js +464 -254
  15. package/packages/agent-team/lib/ledger.js +120 -15
  16. package/packages/agent-team/lib/member-context.js +6 -3
  17. package/packages/agent-team/lib/member-runtime.js +1 -1
  18. package/packages/agent-team/lib/pressure-policy.js +6 -1
  19. package/packages/agent-team/lib/typert.host.js +489 -33
  20. package/packages/agent-team/lib/typert.remote-client.d.ts +15 -3
  21. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  22. package/packages/agent-team/lib/typert.remote-client.js +250 -28
  23. package/packages/agent-team/lib/types/context-continuity-host.d.ts +84 -0
  24. package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -0
  25. package/packages/agent-team/lib/types/context-projection.d.ts +130 -185
  26. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/context-source.d.ts +4 -35
  28. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/human-avatar.d.ts +29 -0
  30. package/packages/agent-team/lib/types/human-avatar.d.ts.map +1 -0
  31. package/packages/agent-team/lib/types/human-profile.d.ts +116 -0
  32. package/packages/agent-team/lib/types/human-profile.d.ts.map +1 -0
  33. package/packages/agent-team/lib/types/human-update-check.d.ts +73 -0
  34. package/packages/agent-team/lib/types/human-update-check.d.ts.map +1 -0
  35. package/packages/agent-team/lib/types/index.d.ts +153 -41
  36. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  37. package/packages/agent-team/lib/types/ledger.d.ts +31 -2
  38. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  39. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/pressure-policy.d.ts +6 -0
  41. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  42. package/packages/agent-team/lib/types/types/entities.d.ts +17 -0
  43. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  44. package/packages/agent-team/lib/types/types/requests-results.d.ts +81 -0
  45. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  46. package/packages/client-agent-team/README.md +1 -1
  47. package/packages/client-agent-team/README.zh.md +1 -1
  48. package/packages/client-agent-team/lib/client.js +1545 -303
  49. package/packages/client-agent-team/lib/client.js.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts +15 -0
  51. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +1 -0
  52. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +115 -0
  53. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +6 -2
  55. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +22 -1
  56. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +25 -2
  58. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +6 -1
  59. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +67 -29
  61. package/packages/client-agent-team/lib/types/client/TeamComposer.js +2 -2
  62. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  63. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/TeamConversation.js +12 -5
  65. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +4 -1
  66. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -1
  67. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +22 -7
  68. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +11 -1
  69. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +66 -5
  71. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +18 -3
  72. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  73. package/packages/client-agent-team/lib/types/client/TeamMessage.js +213 -67
  74. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +5 -0
  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 +52 -26
  77. package/packages/client-agent-team/lib/types/client/avatar-image.d.ts +21 -0
  78. package/packages/client-agent-team/lib/types/client/avatar-image.d.ts.map +1 -0
  79. package/packages/client-agent-team/lib/types/client/avatar-image.js +9 -0
  80. package/packages/client-agent-team/lib/types/client/human-identity.d.ts +90 -0
  81. package/packages/client-agent-team/lib/types/client/human-identity.d.ts.map +1 -0
  82. package/packages/client-agent-team/lib/types/client/human-identity.js +96 -0
  83. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  84. package/packages/client-agent-team/lib/types/client/index.js +76 -4
  85. package/packages/client-agent-team/lib/types/client/locales.d.ts +72 -2
  86. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  87. package/packages/client-agent-team/lib/types/client/locales.js +78 -2
  88. package/packages/client-agent-team/lib/types/client/refs.d.ts +71 -0
  89. package/packages/client-agent-team/lib/types/client/refs.d.ts.map +1 -0
  90. package/packages/client-agent-team/lib/types/client/refs.js +181 -0
  91. package/packages/client-agent-team/lib/types/client/slots.d.ts +9 -3
  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-formatters.d.ts +57 -9
  94. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  95. package/packages/client-agent-team/lib/types/client/team-formatters.js +85 -17
  96. package/packages/tool-agent-team/README.md +2 -2
  97. package/packages/tool-agent-team/README.zh.md +2 -2
  98. package/packages/tool-agent-team/lib/context-tools.js +20 -6
  99. package/packages/tool-agent-team/lib/index.js +2 -2
  100. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  101. package/packages/agent-team/lib/context-management.js +0 -371
  102. package/packages/agent-team/lib/types/context-management.d.ts +0 -154
  103. package/packages/agent-team/lib/types/context-management.d.ts.map +0 -1
  104. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +0 -31
  105. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +0 -1
  106. package/packages/client-agent-team/lib/types/client/task-refs.js +0 -99
@@ -40,6 +40,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
40
40
  done = true;
41
41
  };
42
42
  import { randomUUID } from 'node:crypto';
43
+ import { readFileSync } from 'node:fs';
44
+ import { join } from 'node:path';
43
45
  import { isDeepStrictEqual } from 'node:util';
44
46
  import { Service } from '@deepseek-ai/cordis';
45
47
  import { installModelSelection } from '@deepseek-ai/dsh-agent';
@@ -50,11 +52,14 @@ import { Session, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
50
52
  import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
51
53
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
52
54
  import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
55
+ import { HUMAN_PROFILE_DEFAULT_NAME, HUMAN_PROFILE_REPO_URL, HUMAN_PROFILE_SETTINGS_NAMESPACE, HUMAN_PROFILE_SETTINGS_SCHEMA, HUMAN_PROFILE_VERSION, assertValidHumanName, normalizeHumanName, parseLegacyHumanProfile, planLegacyAdoption } from "./human-profile.js";
56
+ import { humanAvatarsRoot, readHumanAvatar, removeHumanAvatar, writeHumanAvatar } from "./human-avatar.js";
57
+ import { createHumanUpdateChecker } from "./human-update-check.js";
53
58
  import { PressurePolicyCoordinator } from "./pressure-policy.js";
54
- import { ContextManagementCoordinator } from "./context-management.js";
55
- import { AGENT_TEAM_PLUGIN_ID, createHandoffMessage, isAgentTeamSource, isAgentTeamSourceKind } 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";
59
+ import { CONTEXT_CONTINUITY_PROJECTION_KEY, readContextTimeline } from '@wowyuarm/dsh-context-continuity';
60
+ import { createTeamContextManagement, TEAM_CONTEXT_CODEC } from "./context-continuity-host.js";
61
+ import { AGENT_TEAM_PLUGIN_ID, isAgentTeamSource } from "./context-source.js";
62
+ import { boundaryByRef, carriedInputOf, checkpointByRef, checkpointRefFor, createTeamContextProjectionConfig, createTeamContextProjectionDefinition, foldTeamContextProjection, retainedTopicsThrough, TeamContextProjectionHost } from "./context-projection.js";
58
63
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
59
64
  import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, memberMemoryDirectoryName, MemberRuntime } from "./member-runtime.js";
60
65
  import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
@@ -63,6 +68,8 @@ import { agentTeamDomainSpec } from "./spec.js";
63
68
  import { formatTeamTimestamp } from "./time-format.js";
64
69
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
65
70
  export { AGENT_TEAM_HUMAN_HANDLE, AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
71
+ export { HUMAN_PROFILE_DEFAULT_NAME, HUMAN_PROFILE_REPO_URL, HUMAN_PROFILE_SETTINGS_NAMESPACE, HUMAN_PROFILE_SETTINGS_SCHEMA, HUMAN_PROFILE_VERSION, assertValidHumanName, normalizeHumanName } from "./human-profile.js";
72
+ export { humanAvatarsRoot } from "./human-avatar.js";
66
73
  export { AGENT_TEAM_TOOL_NAMES } from "./member-runtime.js";
67
74
  /** Process-stable marker carried by the final Team message tool definition. */
68
75
  export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.preset');
@@ -85,9 +92,14 @@ const MAX_CHECKPOINT_NAME_CHARS = 120;
85
92
  /** Default and maximum number of timeline items one query returns. */
86
93
  const DEFAULT_TIMELINE_LIMIT = 12;
87
94
  const MAX_TIMELINE_LIMIT = 24;
88
- /** Deepest ancestor lineage the timeline and seed resolution walk. */
95
+ /**
96
+ * Archived generations the timeline and seed resolution walk: the timeline
97
+ * reads this many ancestors behind the current generation, and the seed guard
98
+ * resolves a cited ref through the same depth, so the two surfaces cannot
99
+ * disagree about where history ends.
100
+ */
89
101
  const MAX_TIMELINE_ANCESTORS = 8;
90
- /** Product pressure budget constants (see docs/team-collaboration.md). */
102
+ /** Product pressure budget constants (see docs/team-collaboration/README.md). */
91
103
  const CONTEXT_HARD_LIMIT_CAP = 256_000;
92
104
  const CONTEXT_HANDOFF_AT_CAP = 200_000;
93
105
  const CONTEXT_HANDOFF_RESERVE = 8_000;
@@ -163,6 +175,7 @@ let AgentTeam = (() => {
163
175
  let _classSuper = TypertRemoteService;
164
176
  let _instanceExtraInitializers = [];
165
177
  let _resolveTaskRefs_decorators;
178
+ let _resolveThreadRefs_decorators;
166
179
  let _membersForClient_decorators;
167
180
  let _changes_decorators;
168
181
  let _createChannel_decorators;
@@ -182,6 +195,11 @@ let AgentTeam = (() => {
182
195
  let _sendMessage_decorators;
183
196
  let _putAttachment_decorators;
184
197
  let _getAttachment_decorators;
198
+ let _humanProfileForClient_decorators;
199
+ let _putHumanAvatar_decorators;
200
+ let _getHumanAvatar_decorators;
201
+ let _removeHumanAvatar_decorators;
202
+ let _setHumanProfile_decorators;
185
203
  let _reply_decorators;
186
204
  let _changeAttention_decorators;
187
205
  let _inbox_decorators;
@@ -193,6 +211,7 @@ let AgentTeam = (() => {
193
211
  static {
194
212
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
195
213
  _resolveTaskRefs_decorators = [Remote('resolveTaskRefs')];
214
+ _resolveThreadRefs_decorators = [Remote('resolveThreadRefs')];
196
215
  _membersForClient_decorators = [Remote('members')];
197
216
  _changes_decorators = [Remote({ mode: 'stream' })];
198
217
  _createChannel_decorators = [Remote('createChannel')];
@@ -212,6 +231,11 @@ let AgentTeam = (() => {
212
231
  _sendMessage_decorators = [Remote('sendMessage')];
213
232
  _putAttachment_decorators = [Remote('putAttachment')];
214
233
  _getAttachment_decorators = [Remote('getAttachment')];
234
+ _humanProfileForClient_decorators = [Remote('humanProfile')];
235
+ _putHumanAvatar_decorators = [Remote('putHumanAvatar')];
236
+ _getHumanAvatar_decorators = [Remote('getHumanAvatar')];
237
+ _removeHumanAvatar_decorators = [Remote('removeHumanAvatar')];
238
+ _setHumanProfile_decorators = [Remote('setHumanProfile')];
215
239
  _reply_decorators = [Remote('reply')];
216
240
  _changeAttention_decorators = [Remote('changeAttention')];
217
241
  _inbox_decorators = [Remote('inbox')];
@@ -220,6 +244,7 @@ let AgentTeam = (() => {
220
244
  _threadHistory_decorators = [Remote('threadHistory')];
221
245
  _view_decorators = [Remote('view')];
222
246
  __esDecorate(this, null, _resolveTaskRefs_decorators, { kind: "method", name: "resolveTaskRefs", static: false, private: false, access: { has: obj => "resolveTaskRefs" in obj, get: obj => obj.resolveTaskRefs }, metadata: _metadata }, null, _instanceExtraInitializers);
247
+ __esDecorate(this, null, _resolveThreadRefs_decorators, { kind: "method", name: "resolveThreadRefs", static: false, private: false, access: { has: obj => "resolveThreadRefs" in obj, get: obj => obj.resolveThreadRefs }, metadata: _metadata }, null, _instanceExtraInitializers);
223
248
  __esDecorate(this, null, _membersForClient_decorators, { kind: "method", name: "membersForClient", static: false, private: false, access: { has: obj => "membersForClient" in obj, get: obj => obj.membersForClient }, metadata: _metadata }, null, _instanceExtraInitializers);
224
249
  __esDecorate(this, null, _changes_decorators, { kind: "method", name: "changes", static: false, private: false, access: { has: obj => "changes" in obj, get: obj => obj.changes }, metadata: _metadata }, null, _instanceExtraInitializers);
225
250
  __esDecorate(this, null, _createChannel_decorators, { kind: "method", name: "createChannel", static: false, private: false, access: { has: obj => "createChannel" in obj, get: obj => obj.createChannel }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -239,6 +264,11 @@ let AgentTeam = (() => {
239
264
  __esDecorate(this, null, _sendMessage_decorators, { kind: "method", name: "sendMessage", static: false, private: false, access: { has: obj => "sendMessage" in obj, get: obj => obj.sendMessage }, metadata: _metadata }, null, _instanceExtraInitializers);
240
265
  __esDecorate(this, null, _putAttachment_decorators, { kind: "method", name: "putAttachment", static: false, private: false, access: { has: obj => "putAttachment" in obj, get: obj => obj.putAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
241
266
  __esDecorate(this, null, _getAttachment_decorators, { kind: "method", name: "getAttachment", static: false, private: false, access: { has: obj => "getAttachment" in obj, get: obj => obj.getAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
267
+ __esDecorate(this, null, _humanProfileForClient_decorators, { kind: "method", name: "humanProfileForClient", static: false, private: false, access: { has: obj => "humanProfileForClient" in obj, get: obj => obj.humanProfileForClient }, metadata: _metadata }, null, _instanceExtraInitializers);
268
+ __esDecorate(this, null, _putHumanAvatar_decorators, { kind: "method", name: "putHumanAvatar", static: false, private: false, access: { has: obj => "putHumanAvatar" in obj, get: obj => obj.putHumanAvatar }, metadata: _metadata }, null, _instanceExtraInitializers);
269
+ __esDecorate(this, null, _getHumanAvatar_decorators, { kind: "method", name: "getHumanAvatar", static: false, private: false, access: { has: obj => "getHumanAvatar" in obj, get: obj => obj.getHumanAvatar }, metadata: _metadata }, null, _instanceExtraInitializers);
270
+ __esDecorate(this, null, _removeHumanAvatar_decorators, { kind: "method", name: "removeHumanAvatar", static: false, private: false, access: { has: obj => "removeHumanAvatar" in obj, get: obj => obj.removeHumanAvatar }, metadata: _metadata }, null, _instanceExtraInitializers);
271
+ __esDecorate(this, null, _setHumanProfile_decorators, { kind: "method", name: "setHumanProfile", static: false, private: false, access: { has: obj => "setHumanProfile" in obj, get: obj => obj.setHumanProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
242
272
  __esDecorate(this, null, _reply_decorators, { kind: "method", name: "reply", static: false, private: false, access: { has: obj => "reply" in obj, get: obj => obj.reply }, metadata: _metadata }, null, _instanceExtraInitializers);
243
273
  __esDecorate(this, null, _changeAttention_decorators, { kind: "method", name: "changeAttention", static: false, private: false, access: { has: obj => "changeAttention" in obj, get: obj => obj.changeAttention }, metadata: _metadata }, null, _instanceExtraInitializers);
244
274
  __esDecorate(this, null, _inbox_decorators, { kind: "method", name: "inbox", static: false, private: false, access: { has: obj => "inbox" in obj, get: obj => obj.inbox }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -248,6 +278,8 @@ let AgentTeam = (() => {
248
278
  __esDecorate(this, null, _view_decorators, { kind: "method", name: "view", static: false, private: false, access: { has: obj => "view" in obj, get: obj => obj.view }, metadata: _metadata }, null, _instanceExtraInitializers);
249
279
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
250
280
  }
281
+ config = __runInitializers(this, _instanceExtraInitializers);
282
+ static Config = HUMAN_PROFILE_SETTINGS_SCHEMA;
251
283
  static inject = [
252
284
  'storageDomain',
253
285
  'workspaceRegistry',
@@ -256,8 +288,9 @@ let AgentTeam = (() => {
256
288
  'agentPresets',
257
289
  'tools',
258
290
  'sessionPersistence',
291
+ 'sessionProjections',
259
292
  ];
260
- domain = __runInitializers(this, _instanceExtraInitializers);
293
+ domain;
261
294
  ledger;
262
295
  handles = new Map();
263
296
  /** Live Member per session id; drives the root session/event listener. */
@@ -297,6 +330,12 @@ let AgentTeam = (() => {
297
330
  pressurePolicy;
298
331
  notifiedInbox = new Map();
299
332
  attachmentGcTimer;
333
+ /**
334
+ * New-release check behind the settings footnote. Memory-only and
335
+ * background-refreshed, so the profile read path never waits on the
336
+ * network and every failure settles as "no update known".
337
+ */
338
+ humanUpdateCheck = createHumanUpdateChecker({ currentVersion: HUMAN_PROFILE_VERSION });
300
339
  recovery = new RecoveryCoordinator({
301
340
  wake: memberId => {
302
341
  this.ctx.logger.info(`agent-team: automatic recovery wakeup for member '${this.memberLabel(memberId)}' after consecutive recoverable failures`);
@@ -307,42 +346,43 @@ let AgentTeam = (() => {
307
346
  },
308
347
  });
309
348
  /**
310
- * Incremental fold state for {@link contextManagement}'s projection lookup:
311
- * one entry per Member, replaced when that Member's Session changes. Keying
312
- * by Member rather than by Session id is what bounds the map — a rollover
313
- * that leaves the previous generation's projection behind retains it for the
314
- * life of the process, and only the current Session can ever read it.
349
+ * Team's domain half of the continuity projection: the durable ref naming,
350
+ * the Team-notice rule, and the boundary judgement (committed messages,
351
+ * claim changes, first Thread arrivals). Its claim attribution resolves the
352
+ * Task's Thread through the ledger, which is why the resolver reads
353
+ * `this.ledger` lazily — the fold may run before the domain is open, and an
354
+ * unattributed boundary is still a valid anchor.
315
355
  */
316
- contextCursors = new Map();
356
+ contextProjectionHost = new TeamContextProjectionHost({
357
+ threadForTask: taskRef => this.ledger?.threadForTask(taskRef),
358
+ });
317
359
  /**
318
360
  * Context self-management: the one deep module that turns a Member's
319
361
  * successful `context_rollover` tool result into its next private context
320
- * generation. The ledger owns the binding audit, the Session projection
321
- * owns intent, and this coordinator owns only reconstructible process
322
- * state. See docs/architecture.md and docs/team-collaboration.md.
362
+ * generation. The ledger owns the binding audit, the engine's projection
363
+ * unit owns intent, and this coordinator owns only reconstructible process
364
+ * state. See docs/architecture/README.md and docs/team-collaboration/README.md.
323
365
  */
324
- contextManagement = new ContextManagementCoordinator({
366
+ contextManagement = createTeamContextManagement({
325
367
  agentForMember: memberId => this.handles.get(memberId)?.agent,
326
368
  memberForAgent: agent => this.memberForAgent(agent),
327
369
  projectionForMember: (memberId, sessionId) => {
328
370
  const handle = this.handles.get(memberId);
329
371
  if (handle === undefined || handle.agent.session.id !== sessionId)
330
372
  return undefined;
331
- const session = handle.agent.session;
332
- // The projection is read on every successful tool result and turn end, so
333
- // it folds incrementally. The entry is keyed by Member and carries the
334
- // Session it was built from: this Session's log keeps growing, a rollover
335
- // replaces the entry instead of resuming from its predecessor's cursor,
336
- // and a rewritten log falls back to a cold fold inside the cursor.
337
- const owned = advanceOwnedSessionEventCursor(this.contextCursors.get(memberId), session.id, contextProjectionFold(session.id), session.ownEvents(), session.inheritedEventCount);
338
- this.contextCursors.set(memberId, owned);
339
- return owned.cursor.value;
373
+ // The engine's registered unit folds this Session's durable log (replay
374
+ // on attach, then incrementally per committed event) and keys every ref
375
+ // to the Session that recorded it; the registry materializes the cell
376
+ // lazily, so a Member that has not folded yet is built on this read.
377
+ return this.ctx.sessionProjections.stateOf(handle.agent.session, CONTEXT_CONTINUITY_PROJECTION_KEY);
340
378
  },
341
379
  executeTransition: (memberId, plan) => this.executeMemberTransition(memberId, plan),
342
380
  log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
343
381
  });
344
382
  lifecycleTail = Promise.resolve();
345
383
  accepting = true;
384
+ /** One adoption attempt per boot: the two readiness edges fire once each, and this keeps their attempt single. */
385
+ legacyAdoptionStarted = false;
346
386
  /**
347
387
  * Presence wake epoch: Agent running/idle/failure is runtime state with no
348
388
  * durable fact behind it, so the presence scope counts those edge wakes in
@@ -350,8 +390,9 @@ let AgentTeam = (() => {
350
390
  */
351
391
  presenceEpoch = 0;
352
392
  changeWaiters = new Set();
353
- constructor(ctx) {
393
+ constructor(ctx, config) {
354
394
  super(ctx, 'agentTeam');
395
+ this.config = config;
355
396
  this.pressurePolicy = new PressurePolicyCoordinator({
356
397
  agentForMember: memberId => this.handles.get(memberId)?.agent,
357
398
  memberForAgent: agent => {
@@ -368,9 +409,133 @@ let AgentTeam = (() => {
368
409
  },
369
410
  log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
370
411
  });
412
+ // Human profile: this Host row's own Config (name + avatarRef, both
413
+ // volatile), so the settings service derives its form from the schema and
414
+ // an edit lands in the running plugin without a remount. Team's own page
415
+ // owns that surface, so the row declares itself presentation-owned rather
416
+ // than schema-page owned. Reads go straight to the live Config reference;
417
+ // this subscription is the change signal the retired section hook used to
418
+ // be — the ledger's runtime @ handle follows every edit.
419
+ this.ctx.inject(['settings'], settingsCtx => {
420
+ settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, this.ctx.fiber));
421
+ this.adoptLegacyHumanProfile();
422
+ });
423
+ this.ctx.on('loader/volatile-update', () => { this.syncHumanHandle(); });
424
+ }
425
+ /** Current human display name; the single source for team_view and @ matching. */
426
+ humanHandle() {
427
+ const name = normalizeHumanName(this.config.name.get());
428
+ return name === '' ? HUMAN_PROFILE_DEFAULT_NAME : name;
429
+ }
430
+ /** Current human profile reference held in this Host row's Config (name + avatarRef). */
431
+ humanProfile() {
432
+ const avatarRef = this.config.avatarRef.get();
433
+ return Object.freeze({ name: this.humanHandle(), ...(avatarRef === undefined ? {} : { avatarRef }) });
434
+ }
435
+ /** Push the current Config name into the ledger's runtime @ handle. */
436
+ syncHumanHandle() {
437
+ try {
438
+ this.ledger?.setHumanDisplayHandle(this.humanHandle());
439
+ }
440
+ catch {
441
+ // The ledger is absent before Service.init; the post-open sync covers it.
442
+ }
443
+ }
444
+ /**
445
+ * The two judgements the Config schema cannot make about a Human name: the
446
+ * same non-empty floor as Member handles, plus global uniqueness against live
447
+ * Members. `setHumanProfile` runs it before the write, so a colliding rename
448
+ * rejects instead of persisting.
449
+ */
450
+ validateHumanProfile(value) {
451
+ const name = assertValidHumanName(value.name);
452
+ const ledger = this.ledger;
453
+ if (ledger === undefined)
454
+ return;
455
+ const normalized = name.normalize('NFKC').trim().toLowerCase();
456
+ for (const member of ledger.listMembers()) {
457
+ if (member.state === 'inactive' || member.state === 'archived')
458
+ continue;
459
+ if (member.handle.normalize('NFKC').trim().toLowerCase() === normalized) {
460
+ throw new Error(`human name '${name}' collides with an existing Member handle`);
461
+ }
462
+ }
463
+ }
464
+ /**
465
+ * One-time adoption of the profile facts the retired `agent-team-human`
466
+ * settings section held (see `LEGACY_HUMAN_PROFILE_SECTION` for why the
467
+ * upstream importer cannot carry them over). The values land in this Host
468
+ * row's own Config through the same Remote the profile page writes, so the
469
+ * page, the ledger's @ handle, and every avatar seat follow as after any
470
+ * edit. Readiness has two one-way edges — the settings service arrives, the
471
+ * ledger opens — and whichever fires second starts the attempt; only a
472
+ * pristine profile is adopted, and one attempt per boot is its whole
473
+ * lifetime, so adoption never loops and never re-writes.
474
+ */
475
+ adoptLegacyHumanProfile() {
476
+ if (this.legacyAdoptionStarted)
477
+ return;
478
+ if (this.ctx.get('settings') === undefined || this.ledger === undefined)
479
+ return;
480
+ this.legacyAdoptionStarted = true;
481
+ void this.adoptLegacyHumanProfileNow().catch((error) => {
482
+ this.ctx.logger.warn('agent-team: legacy human profile adoption failed: %s', String(error));
483
+ });
484
+ }
485
+ /** Carry the first legacy document that still has the section into a pristine profile. */
486
+ async adoptLegacyHumanProfileNow() {
487
+ // A profile-launched Host always carries the profile context; a Host booted
488
+ // without one still adopts from the DSH home, which is where a pre-rc.1
489
+ // install kept the document. Read through `get`: cordis refuses a direct
490
+ // `ctx.profileContext` for a service this plugin does not declare, and
491
+ // adoption may not gate the Host on app-boot.
492
+ const profileHome = this.ctx.get('profileContext')?.home;
493
+ const homes = profileHome === undefined ? [dshHomePath()] : [dshHomePath(), profileHome];
494
+ const candidates = new Set(homes.flatMap(home => [join(home, 'settings.yaml'), join(home, 'settings.yaml.imported')]));
495
+ let legacy;
496
+ let source;
497
+ for (const candidate of candidates) {
498
+ let text;
499
+ try {
500
+ text = readFileSync(candidate, 'utf8');
501
+ }
502
+ catch {
503
+ // Absent candidates are the steady state once adoption settles; the
504
+ // live document is one only because the first boot after the upgrade
505
+ // can race the importer that renames it.
506
+ continue;
507
+ }
508
+ const parsed = parseLegacyHumanProfile(text);
509
+ if (parsed !== undefined) {
510
+ legacy = parsed;
511
+ source = candidate;
512
+ break;
513
+ }
514
+ }
515
+ if (legacy === undefined || source === undefined)
516
+ return;
517
+ // The reference is carried only while its bytes still draw: the avatar
518
+ // store is the authority for whether this avatar exists at all.
519
+ let avatarRef = legacy.avatarRef;
520
+ if (avatarRef !== undefined && await readHumanAvatar(humanAvatarsRoot(), avatarRef) === undefined)
521
+ avatarRef = undefined;
522
+ const plan = planLegacyAdoption(this.humanProfile(), {
523
+ ...(legacy.name === undefined ? {} : { name: legacy.name }),
524
+ ...(avatarRef === undefined ? {} : { avatarRef }),
525
+ });
526
+ if (plan === undefined)
527
+ return;
528
+ await this.setHumanProfile(plan);
529
+ this.ctx.logger.info('agent-team: adopted the legacy human profile from %s', source);
371
530
  }
372
531
  /** Open the durable ledger and restore every enabled Member independently. */
373
532
  async [Service.init]() {
533
+ // The continuity projection registers once per Host: the framework keeps
534
+ // one unit per projection key and drives it for every Session, so Team's
535
+ // fold is session-agnostic and the state carries the identity it folds.
536
+ // The registration rides this plugin's fiber — unloading Team removes the
537
+ // key (and its cached cells) from later drives and snapshots.
538
+ this.ctx.effect(() => this.ctx.root.sessionProjections.register(createTeamContextProjectionDefinition(this.contextProjectionHost)), 'agentTeam.contextProjection');
374
539
  this.ctx.on('agent/error', ({ agent, error }) => {
375
540
  const member = this.memberForAgent(agent);
376
541
  if (member === undefined)
@@ -456,6 +621,11 @@ let AgentTeam = (() => {
456
621
  this.domain = domain;
457
622
  const ledger = new AgentTeamLedger(domain.table('operations'));
458
623
  this.ledger = ledger;
624
+ // A live Config edit in the constructor's window may have arrived before
625
+ // the ledger existed; sync once here so @ matching starts from the stored
626
+ // name.
627
+ this.syncHumanHandle();
628
+ this.adoptLegacyHumanProfile();
459
629
  const initialization = await ledger.initialize();
460
630
  if (initialization.committed)
461
631
  this.emitCommitted(initialization.value);
@@ -531,6 +701,18 @@ let AgentTeam = (() => {
531
701
  });
532
702
  return Object.freeze({ resolved: Object.freeze(this.requireLedger().resolveTaskRefs(request.workspaceId, taskRefs)) });
533
703
  }
704
+ /** Read-only navigation lookup for branded Thread refs found in message bodies. */
705
+ resolveThreadRefs(request) {
706
+ this.requireWorkspace(request.workspaceId);
707
+ const seen = new Set();
708
+ const threadRefs = request.threadRefs.filter(threadRef => {
709
+ if (seen.has(threadRef))
710
+ return false;
711
+ seen.add(threadRef);
712
+ return true;
713
+ });
714
+ return Object.freeze({ resolved: Object.freeze(this.requireLedger().resolveThreadRefs(request.workspaceId, threadRefs)) });
715
+ }
534
716
  /** Browser-safe Human roster, optionally filtered to one participation. */
535
717
  membersForClient(request) {
536
718
  if (request.workspaceId !== undefined)
@@ -634,13 +816,16 @@ let AgentTeam = (() => {
634
816
  privateMemoryPath: dshHomePath('agent-team', 'members', memberMemoryDirectoryName(memberId)),
635
817
  state: 'enabled',
636
818
  });
637
- const result = await this.requireLedger().addMember({ ...request, actor: agentTeamHumanActor(), member });
819
+ const ledger = this.requireLedger();
820
+ const result = await ledger.addMember({ ...request, actor: agentTeamHumanActor(), member });
638
821
  if (result.committed)
639
822
  this.emitCommitted(result.value.receipt);
640
823
  const stored = result.value.member;
641
824
  if (!this.handles.has(stored.memberId))
642
825
  await this.activateMember(stored, workspace.path);
643
- return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored) });
826
+ // Creation seeds exactly the creation Workspace participation; attach it
827
+ // here (like membersForClient) so the Client never synthesizes it.
828
+ return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored), workspaceIds: ledger.workspacesOf(stored.memberId) });
644
829
  });
645
830
  }
646
831
  /** Commit suspended intent, then wait for the owned AgentHandle to become quiescent. */
@@ -820,6 +1005,9 @@ let AgentTeam = (() => {
820
1005
  throw new Error(`the context rollover is refused: this Member now owns jobs that would not survive the switch (${blockingJobs.join(', ')}); collect or stop them, then retry`);
821
1006
  }
822
1007
  const rolled = await this.rolloverSessionForAgent(active.agent, {
1008
+ // The engine plan carries the host-derived request id as a plain
1009
+ // string; Team's rollover operation vocabulary brands it, and the
1010
+ // context-continuity host adapter is its only producer.
823
1011
  requestId: plan.requestId,
824
1012
  workspaceId: stored.workspaceId,
825
1013
  memberId,
@@ -836,8 +1024,9 @@ let AgentTeam = (() => {
836
1024
  // to override the fold.
837
1025
  const preRetireCapture = this.contextManagement.drainCapturedInput(memberId);
838
1026
  await this.retireMemberGeneration(memberId, active, plan.previousSessionId);
839
- const finalState = foldContextProjection(active.agent.session.ownEvents(), active.agent.session.inheritedEventCount, active.agent.session.id);
840
- const foldedCarried = carriedInputOf(finalState);
1027
+ const finalEvents = active.agent.session.ownEvents();
1028
+ const finalState = foldTeamContextProjection(finalEvents, { sessionId: active.agent.session.id, inheritedEventCount: active.agent.session.inheritedEventCount }, this.contextProjectionHost);
1029
+ const foldedCarried = carriedInputOf(finalState, finalEvents);
841
1030
  const carriedById = new Map(plan.carriedInput.map(message => [message.id, message]));
842
1031
  for (const message of foldedCarried)
843
1032
  carriedById.set(message.id, message);
@@ -1119,6 +1308,91 @@ let AgentTeam = (() => {
1119
1308
  throw new Error(`attachment '${request.attachmentId}' is no longer cached`);
1120
1309
  return Object.freeze({ name: stored.name, mediaType: stored.mediaType, byteSize: stored.byteSize, bytesBase64: stored.bytes.toString('base64') });
1121
1310
  }
1311
+ /**
1312
+ * Human profile read for the settings page and footnote: name + avatar
1313
+ * reference from this Host row's live Config plus version facts. Human-scoped
1314
+ * (the Web Client calls it); agent tools never receive avatar bytes, only the
1315
+ * name through team_view. The new-release check stays best-effort and cached —
1316
+ * `updateAvailable` is false until a background refresh actually observes a
1317
+ * newer published release.
1318
+ */
1319
+ humanProfileForClient(_request) {
1320
+ const profile = this.humanProfile();
1321
+ const update = this.humanUpdateCheck.snapshot();
1322
+ return Object.freeze({
1323
+ name: profile.name,
1324
+ ...(profile.avatarRef === undefined ? {} : { avatarRef: profile.avatarRef }),
1325
+ version: HUMAN_PROFILE_VERSION,
1326
+ repoUrl: HUMAN_PROFILE_REPO_URL,
1327
+ updateAvailable: update.updateAvailable,
1328
+ ...(update.latestVersion === undefined ? {} : { latestVersion: update.latestVersion }),
1329
+ });
1330
+ }
1331
+ /**
1332
+ * Upload one human avatar image into the persistent store. Human-only by
1333
+ * construction: only the Web Client calls this Remote, never agent tools.
1334
+ * The caller stores the returned ref in settings; bytes never enter the
1335
+ * TTL-bound attachment cache.
1336
+ */
1337
+ async putHumanAvatar(request) {
1338
+ this.requireAccepting();
1339
+ const bytes = Buffer.from(request.bytesBase64, 'base64');
1340
+ return Object.freeze(await writeHumanAvatar(humanAvatarsRoot(), request.name, request.mediaType ?? 'application/octet-stream', bytes));
1341
+ }
1342
+ /** Read one human avatar back; removed entries throw and the UI falls back to hue/initial. */
1343
+ async getHumanAvatar(request) {
1344
+ const stored = await readHumanAvatar(humanAvatarsRoot(), request.avatarRef);
1345
+ if (stored === undefined)
1346
+ throw new Error(`human avatar '${request.avatarRef}' is no longer stored`);
1347
+ return Object.freeze({ name: stored.name, mediaType: stored.mediaType, byteSize: stored.byteSize, bytesBase64: stored.bytes.toString('base64') });
1348
+ }
1349
+ /** Remove one human avatar entry; the UI falls back to hue/initial afterwards. */
1350
+ async removeHumanAvatar(request) {
1351
+ this.requireAccepting();
1352
+ await removeHumanAvatar(humanAvatarsRoot(), request.avatarRef);
1353
+ return Object.freeze({ removed: true });
1354
+ }
1355
+ /**
1356
+ * Overwrite the Human profile fields the caller supplies. The Host owns this
1357
+ * write because the profile is the Host row's own Config: the schema supplies
1358
+ * the shape, this method supplies the two judgements the schema cannot make
1359
+ * (a non-empty name, a name no live Member already answers to), and the
1360
+ * settings service persists the result into the active profile's patch
1361
+ * document and applies it to the running plugin live.
1362
+ *
1363
+ * No expected revision accompanies the write: the profile is two scalar
1364
+ * fields written from the Human's own pages, where the last write wins. This
1365
+ * is not a hard boundary around a user-editable document — the settings
1366
+ * service's own document opener (and a text editor) can change the stored
1367
+ * name without passing here, exactly as the retired section validator could
1368
+ * not stop it.
1369
+ */
1370
+ async setHumanProfile(request) {
1371
+ this.requireAccepting();
1372
+ const settings = this.ctx.get('settings');
1373
+ if (settings === undefined)
1374
+ throw new Error('the settings service is unavailable');
1375
+ const name = request.name === undefined ? undefined : assertValidHumanName(request.name);
1376
+ if (name !== undefined)
1377
+ this.validateHumanProfile({ name });
1378
+ const ops = [];
1379
+ if (name !== undefined)
1380
+ ops.push({ op: 'set', path: ['name'], value: name });
1381
+ if (request.avatarRef === null)
1382
+ ops.push({ op: 'unset', path: ['avatarRef'] });
1383
+ else if (request.avatarRef !== undefined)
1384
+ ops.push({ op: 'set', path: ['avatarRef'], value: request.avatarRef });
1385
+ if (ops.length !== 0) {
1386
+ // The settings namespace IS this Host row's id in the composition, and it
1387
+ // must be addressed by that name rather than by `ctx.fiber.entry` here: a
1388
+ // Remote call runs under its caller's context, whose fiber entry is the
1389
+ // RPC gateway's row (`typert-gateway`), which the settings service then
1390
+ // looks up as an unrelated plugin. shipping.spec.ts pins the constant to
1391
+ // the row `cordis.patch.yml` declares.
1392
+ await settings.mutate(HUMAN_PROFILE_SETTINGS_NAMESPACE, ops);
1393
+ }
1394
+ return Object.freeze({ ...this.humanProfile() });
1395
+ }
1122
1396
  /** Human existing-Thread reply; unread and revision conflicts are business outcomes. */
1123
1397
  async reply(request) {
1124
1398
  return this.replyAs(this.humanCall(request.workspaceId), request);
@@ -1429,7 +1703,7 @@ let AgentTeam = (() => {
1429
1703
  let sessionId = agent.session.id;
1430
1704
  let live = true;
1431
1705
  let guard = 0;
1432
- while (sessionId !== undefined && guard++ < MAX_TIMELINE_ANCESTORS) {
1706
+ while (sessionId !== undefined && guard++ <= MAX_TIMELINE_ANCESTORS) {
1433
1707
  let events;
1434
1708
  let inheritedEventCount;
1435
1709
  let parentSession;
@@ -1454,19 +1728,19 @@ let AgentTeam = (() => {
1454
1728
  // Fold the source with its inherited cut respected: inherited events
1455
1729
  // are resolved history in that source, never fresh intent; checkpoints
1456
1730
  // recorded in this source's own span are the selectable targets.
1457
- const state = foldContextProjection(events, inheritedEventCount, sessionId);
1731
+ const state = foldTeamContextProjection(events, { sessionId, inheritedEventCount }, this.contextProjectionHost);
1458
1732
  // A Team-boundary default checkpoint: the boundary's completed-turn
1459
1733
  // anchor is the seed cut, and it is selectable exactly when one
1460
1734
  // Thread's facts entered the context through it — the same proof the
1461
1735
  // timeline requires, revalidated here because the model may cite a
1462
1736
  // boundary the timeline never surfaced.
1463
1737
  const boundary = checkpointRef.startsWith('team-boundary-')
1464
- ? state.boundaries.find(entry => entry.key === checkpointRef)
1738
+ ? boundaryByRef(state, checkpointRef)
1465
1739
  : undefined;
1466
1740
  const entry = checkpointByRef(state, checkpointRef);
1467
1741
  const anchorTurnEndSeq = boundary !== undefined ? boundary.turnEndSeq : entry?.turnEndSeq;
1468
1742
  if (anchorTurnEndSeq !== undefined && anchorTurnEndSeq !== -1) {
1469
- if (boundary !== undefined && boundary.source !== 'team-boundary') {
1743
+ if (boundary !== undefined && boundary.kind !== 'team-boundary') {
1470
1744
  throw new Error(`checkpoint '${checkpointRef}' is not a restorable boundary`);
1471
1745
  }
1472
1746
  // The seed is the exact contiguous prefix through the anchor's
@@ -1474,7 +1748,12 @@ let AgentTeam = (() => {
1474
1748
  const throughSeq = anchorTurnEndSeq + 1;
1475
1749
  const prefix = events.slice(0, throughSeq);
1476
1750
  if (boundary !== undefined) {
1477
- const threads = this.threadsEnteringContext(events, anchorTurnEndSeq);
1751
+ // The seed must stay inside one Thread's context: the retained
1752
+ // prefix through the anchor holds exactly one Thread's facts, read
1753
+ // from the fold's own boundary attributions — the same accumulated
1754
+ // set the timeline shows, so a ref the timeline offered is never
1755
+ // refused here for a reason it did not state.
1756
+ const threads = retainedTopicsThrough(state, anchorTurnEndSeq);
1478
1757
  if (threads.length !== 1) {
1479
1758
  throw new Error(threads.length === 0
1480
1759
  ? `boundary '${checkpointRef}' has no single attributable Thread; write a fresh handoff instead`
@@ -1535,11 +1814,11 @@ let AgentTeam = (() => {
1535
1814
  return;
1536
1815
  }
1537
1816
  const inspection = previousRead.inspection;
1538
- const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, previousSessionId);
1817
+ const state = foldTeamContextProjection(inspection.events, { sessionId: previousSessionId, inheritedEventCount: inspection.inheritedEventCount }, this.contextProjectionHost);
1539
1818
  if (state.pending === null)
1540
1819
  return;
1541
1820
  const pending = state.pending;
1542
- const message = createHandoffMessage({
1821
+ const message = TEAM_CONTEXT_CODEC.createHandoffMessage({
1543
1822
  handoff: pending.handoff,
1544
1823
  previousSessionId,
1545
1824
  newSessionId: agent.session.id,
@@ -1573,9 +1852,9 @@ let AgentTeam = (() => {
1573
1852
  if (!Number.isSafeInteger(through) || through < 0 || through > inspection.events.length) {
1574
1853
  throw new Error(`the recorded checkpoint-return seed cut ${through} is not a valid prefix of Session '${seed.sourceSessionId}'`);
1575
1854
  }
1576
- const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, seed.sourceSessionId);
1855
+ const state = foldTeamContextProjection(inspection.events, { sessionId: seed.sourceSessionId, inheritedEventCount: inspection.inheritedEventCount }, this.contextProjectionHost);
1577
1856
  const anchorProven = checkpointByRef(state, seed.checkpointRef) !== undefined
1578
- || state.boundaries.some(boundary => boundary.key === seed.checkpointRef);
1857
+ || boundaryByRef(state, seed.checkpointRef) !== undefined;
1579
1858
  if (!anchorProven) {
1580
1859
  throw new Error(`the recorded checkpoint-return anchor '${seed.checkpointRef}' no longer resolves in Session '${seed.sourceSessionId}'`);
1581
1860
  }
@@ -1641,8 +1920,8 @@ let AgentTeam = (() => {
1641
1920
  }
1642
1921
  inspectionEvents = previousRead.inspection.events;
1643
1922
  inspectionInherited = previousRead.inspection.inheritedEventCount;
1644
- const state = foldContextProjection(inspectionEvents, inspectionInherited, transition.previousSessionId);
1645
- const carried = carriedInputOf(state);
1923
+ const state = foldTeamContextProjection(inspectionEvents, { sessionId: transition.previousSessionId, inheritedEventCount: inspectionInherited }, this.contextProjectionHost);
1924
+ const carried = carriedInputOf(state, inspectionEvents);
1646
1925
  if (carried.length === 0)
1647
1926
  return 0;
1648
1927
  const known = new Set();
@@ -1693,9 +1972,11 @@ let AgentTeam = (() => {
1693
1972
  * Agent-only bounded structural timeline: resolved checkpoints plus Team
1694
1973
  * delivery, handoff, and compaction boundaries across the current
1695
1974
  * generation and its archived ancestor lineage. Structural only — no
1696
- * transcript content. The meter prices retained/discarded tokens; entries
1697
- * the Host cannot prove restorable carry a rejection reason instead of
1698
- * silently disappearing.
1975
+ * transcript content. The walk, the per-source folds, the pricing, and the
1976
+ * anchor rules are the engine's `readContextTimeline`; Team contributes the
1977
+ * fold configuration, the measurement, and the one judgement the engine
1978
+ * leaves to its host — which Threads a boundary's retained prefix holds — so
1979
+ * a ref this list offers is a ref `context_rollover` accepts.
1699
1980
  */
1700
1981
  async contextTimelineForAgent(agent, request) {
1701
1982
  const member = this.memberForAgent(agent);
@@ -1710,70 +1991,111 @@ let AgentTeam = (() => {
1710
1991
  const limits = await this.routeLimitsForAgent(agent);
1711
1992
  const hardLimit = limits?.hardLimit ?? CONTEXT_HARD_LIMIT_CAP;
1712
1993
  const handoffAt = limits?.handoffAt ?? CONTEXT_HANDOFF_AT_CAP;
1713
- // Fold the current generation, then walk archived ancestors through their
1714
- // persisted logs; the same projection definition folds every source.
1715
- const items = [];
1716
- const seen = new Set();
1717
- let incompleteFrom;
1718
- let sessionId = member.sessionId;
1719
- let live = true;
1720
- let guard = 0;
1721
- while (sessionId !== undefined && guard++ < MAX_TIMELINE_ANCESTORS) {
1722
- let state;
1723
- let sourceSessionId = sessionId;
1724
- let sourceEvents = [];
1725
- // Capture this iteration's source identity BEFORE the branch advances
1726
- // the lineage flag: measurement and discard pricing both key on it.
1727
- const sourceIsCurrent = live;
1728
- if (live) {
1729
- state = foldContextProjection(agent.session.ownEvents(), agent.session.inheritedEventCount, agent.session.id);
1730
- sourceEvents = agent.session.snapshotEvents();
1731
- sessionId = agent.session.header.parentSession;
1732
- live = false;
1994
+ // One measurement per lineage source, memoized by Session: the engine
1995
+ // measures a source once before pricing its anchors, and the boundary
1996
+ // overlay below reads the same map to tell an unprovable budget (never
1997
+ // priced as free) from a real budget rejection.
1998
+ const measurements = new Map();
1999
+ const timeline = await readContextTimeline({
2000
+ current: {
2001
+ sessionId: agent.session.id,
2002
+ header: agent.session.header,
2003
+ inheritedEventCount: agent.session.inheritedEventCount,
2004
+ events: agent.session.snapshotEvents(),
2005
+ },
2006
+ config: this.contextFoldConfig(),
2007
+ readAncestor: sessionId => this.sessionReader.read(sessionId),
2008
+ measureSource: source => {
2009
+ const key = String(source.sessionId);
2010
+ if (!measurements.has(key))
2011
+ measurements.set(key, this.measureContextSourceForAgent(agent, source));
2012
+ return measurements.get(key);
2013
+ },
2014
+ currentUsageTokens: usageTokens,
2015
+ handoffAt,
2016
+ limit,
2017
+ // Archived ancestors the engine walks; the seed guard resolves a cited
2018
+ // ref through the same depth, so the timeline never offers a ref the
2019
+ // guard cannot reach.
2020
+ maxAncestors: MAX_TIMELINE_ANCESTORS,
2021
+ });
2022
+ return {
2023
+ usageTokens,
2024
+ hardLimit,
2025
+ handoffAt,
2026
+ items: timeline.items.map(item => this.teamTimelineItemFor(item, agent.session.id, handoffAt, measurements)),
2027
+ ...(timeline.incompleteFrom === undefined ? {} : { incompleteFrom: timeline.incompleteFrom }),
2028
+ };
2029
+ }
2030
+ /**
2031
+ * Map one engine timeline item onto the model-facing Team item. The engine
2032
+ * decided the walk, the fold, the pricing, and the head, checkpoint, and
2033
+ * measurable-source verdicts; Team restates exactly one policy of its own: a
2034
+ * Team boundary is selectable only when the RETAINED PREFIX through it stays
2035
+ * inside one Thread — the same proof the seed guard revalidates before it
2036
+ * swaps a generation. The engine judges a boundary by its OWN attribution
2037
+ * instead, so a boundary that arrived after a second Thread's facts would be
2038
+ * offered here and refused by `context_rollover`; Team's stricter rule is
2039
+ * what keeps the two surfaces answering one question.
2040
+ */
2041
+ teamTimelineItemFor(item, currentSessionId, handoffAt, measurements) {
2042
+ const source = item.source === 'boundary'
2043
+ ? item.kind === 'handoff' || item.kind === 'compaction' ? item.kind : 'team-boundary'
2044
+ : item.source === 'checkpoint' ? 'agent' : 'head';
2045
+ const affectedThreads = item.affectedTopics;
2046
+ let restorable = item.restorable;
2047
+ let reason = item.reason;
2048
+ // A boundary's verdict is Team's to make, but only once its source is
2049
+ // measurable: "the budget cannot be proven" is the engine's first
2050
+ // rejection and stays first — an unmeasurable boundary is not selectable
2051
+ // even when its prefix holds exactly one Thread.
2052
+ if (item.source === 'boundary' && measurements.get(String(item.sourceSessionId ?? currentSessionId)) !== undefined) {
2053
+ if (source === 'handoff' || source === 'compaction') {
2054
+ // A handoff opens a generation and a compaction rewrites the visible
2055
+ // surface: rewinding into either is not a proven-safe target. The
2056
+ // engine has no vocabulary for that and would answer "no single topic
2057
+ // is attributable", which misdescribes why.
2058
+ restorable = false;
2059
+ reason = `source '${source}' is not a restorable checkpoint`;
1733
2060
  }
1734
- else {
1735
- const read = await this.sessionReader.read(sessionId);
1736
- if (!read.ok) {
1737
- // An unreadable ancestor ends the lineage walk here — recorded in
1738
- // the result, never silent, and never a Member-availability fact.
1739
- incompleteFrom = { sessionId, reason: `${read.failure.kind}: ${read.failure.detail}` };
1740
- sessionId = undefined;
1741
- }
1742
- else {
1743
- state = foldContextProjection(read.inspection.events, read.inspection.inheritedEventCount, sessionId);
1744
- sourceEvents = read.inspection.events;
1745
- sourceSessionId = sessionId;
1746
- sessionId = read.inspection.header.parentSession;
1747
- }
2061
+ else if (affectedThreads.length !== 1) {
2062
+ restorable = false;
2063
+ reason = affectedThreads.length === 0
2064
+ ? 'no single Thread is attributable to this boundary'
2065
+ : 'multiple Threads entered the context through this boundary; write a fresh handoff instead';
1748
2066
  }
1749
- if (state === undefined)
1750
- break;
1751
- // Price every candidate of this source against the SOURCE's own
1752
- // replayed measurement — a small current generation never shrinks a
1753
- // large ancestor's real seed cost. An unmeasurable source (no meter,
1754
- // unreadable ancestor) prices as UNKNOWN, never as zero: the timeline
1755
- // marks its candidates unprovable and the return guard rejects them.
1756
- const sourceUsage = await this.sourceUsageTokens(sourceSessionId, sourceIsCurrent, agent);
1757
- for (const candidate of timelineCandidates(state, limit)) {
1758
- if (seen.has(candidate.ref))
1759
- continue;
1760
- seen.add(candidate.ref);
1761
- items.push(this.timelineItemFor(candidate, sourceUsage, usageTokens, hardLimit, handoffAt, sourceSessionId, sourceIsCurrent, sourceEvents));
1762
- if (items.length >= limit)
1763
- break;
2067
+ else if (item.retainedTokens >= handoffAt) {
2068
+ restorable = false;
2069
+ reason = 'retained context would not materially shrink the working set';
2070
+ }
2071
+ else {
2072
+ restorable = true;
2073
+ reason = undefined;
1764
2074
  }
1765
- if (items.length >= limit)
1766
- break;
1767
2075
  }
1768
- return { usageTokens, hardLimit, handoffAt, items, ...(incompleteFrom === undefined ? {} : { incompleteFrom }) };
2076
+ return {
2077
+ checkpointRef: item.ref,
2078
+ name: item.label,
2079
+ source,
2080
+ retainedTokens: item.retainedTokens,
2081
+ discardedTokens: item.discardedTokens,
2082
+ affectedThreads,
2083
+ restorable,
2084
+ ...(reason === undefined ? {} : { reason }),
2085
+ // Team's field semantics, unchanged: every item that is not a
2086
+ // checkpoint names the Session it anchors in — an ancestor generation's
2087
+ // boundary is how a lineage reads — and a checkpoint is keyed to its own
2088
+ // Session by its ref already.
2089
+ ...(source === 'agent' ? {} : { sourceSessionId: item.sourceSessionId ?? currentSessionId }),
2090
+ };
1769
2091
  }
1770
2092
  /**
1771
2093
  * Replayed measurement of one lineage source: the live current Session
1772
- * measures directly; an archived ancestor measures through a borrowed
1773
- * prepared Session, so a seed's retained cost is priced in the SOURCE's
1774
- * own tokens — never the current generation's. Returns undefined when no
1775
- * meter is available or the source cannot be borrowed; callers fail
1776
- * closed on the unknown.
2094
+ * measures directly; an archived ancestor measures through a detached
2095
+ * Session rebuilt from the source's own log, so a seed's retained cost is
2096
+ * priced in the SOURCE's own tokens — never the current generation's.
2097
+ * Returns undefined when no meter is available or the source cannot be
2098
+ * replayed; callers fail closed on the unknown.
1777
2099
  */
1778
2100
  async sourceUsageTokens(sessionId, live, agent) {
1779
2101
  const meter = agent.ctx.get('tokenMeter');
@@ -1788,14 +2110,50 @@ let AgentTeam = (() => {
1788
2110
  const read = await this.sessionReader.read(sessionId);
1789
2111
  if (!read.ok)
1790
2112
  return undefined;
2113
+ return this.measureDetachedSource(agent, {
2114
+ sessionId,
2115
+ header: read.inspection.header,
2116
+ inheritedEventCount: read.inspection.inheritedEventCount,
2117
+ events: read.inspection.events,
2118
+ });
2119
+ }
2120
+ /**
2121
+ * One already-read source's replayed measurement: an archived generation is
2122
+ * rebuilt as a detached Session and measured in its own tokens. Undefined
2123
+ * means unmeasurable, never free.
2124
+ */
2125
+ measureDetachedSource(agent, source) {
2126
+ const meter = agent.ctx.get('tokenMeter');
2127
+ if (meter === undefined)
2128
+ return undefined;
1791
2129
  try {
1792
- const session = Session.create(sessionId, read.inspection.events, read.inspection.header, read.inspection.inheritedEventCount);
2130
+ const session = Session.create(source.sessionId, source.events, source.header, source.inheritedEventCount);
1793
2131
  return meter.measure(session)?.totalTokens;
1794
2132
  }
1795
2133
  catch {
1796
2134
  return undefined;
1797
2135
  }
1798
2136
  }
2137
+ /**
2138
+ * One source's replayed measurement, in that source's own tokens, for a
2139
+ * caller that already holds the source: the live generation measures
2140
+ * directly, an archived one is rebuilt from the log it came with. Undefined
2141
+ * means unmeasurable — a caller must never price an unknown source as free.
2142
+ */
2143
+ measureContextSourceForAgent(agent, source) {
2144
+ if (String(source.sessionId) === String(agent.session.id)) {
2145
+ return agent.ctx.get('tokenMeter')?.measure(agent.session)?.totalTokens;
2146
+ }
2147
+ return this.measureDetachedSource(agent, source);
2148
+ }
2149
+ /**
2150
+ * The one fold configuration the registered projection unit and every cold
2151
+ * fold use, so a generation read back for the timeline reads exactly as the
2152
+ * projection folded it — same codec, same boundary attribution.
2153
+ */
2154
+ contextFoldConfig() {
2155
+ return createTeamContextProjectionConfig(this.contextProjectionHost);
2156
+ }
1799
2157
  /**
1800
2158
  * Monotonic anchor-share estimate of a seed's retained cost, priced in the
1801
2159
  * SOURCE Session's own measurement: the fraction of the source log the
@@ -1811,155 +2169,6 @@ let AgentTeam = (() => {
1811
2169
  const share = Math.min(1, Math.max(0, (anchorTurnEndSeq + 1) / sourceLength));
1812
2170
  return Math.round(sourceUsageTokens * share);
1813
2171
  }
1814
- /**
1815
- * Threads whose facts entered this Session's model context by the given
1816
- * seq: delivered Team notices (their bodies quote `Thread: <ref>`
1817
- * structurally) and successful Team-claim mutations (their task overlays
1818
- * resolve to Threads through the ledger). Never from unread ledger
1819
- * activity — a Thread the Member never saw did not enter its context.
1820
- * Order-stable, deduplicated.
1821
- */
1822
- threadsEnteringContext(events, throughSeq) {
1823
- const refs = [];
1824
- const openAttributions = new Map();
1825
- const push = (ref) => {
1826
- if (ref !== undefined && !refs.includes(ref))
1827
- refs.push(ref);
1828
- };
1829
- for (const event of events) {
1830
- if (event.seq > throughSeq)
1831
- break;
1832
- if (event.type === 'tool/call' && (event.data.name === 'team_claim' || event.data.name === 'team_message')) {
1833
- openAttributions.set(event.data.callId, { name: event.data.name, arguments: event.data.arguments });
1834
- }
1835
- else if (event.type === 'tool/result') {
1836
- const result = event.data.message;
1837
- if (result.isError !== true) {
1838
- const recorded = openAttributions.get(result.toolCallId);
1839
- if (recorded !== undefined) {
1840
- openAttributions.delete(result.toolCallId);
1841
- try {
1842
- const args = JSON.parse(recorded.arguments);
1843
- // A claim mutation resolves its Task overlay through the ledger;
1844
- // a team_message committed reply names its Thread in its call
1845
- // arguments; a committed start's Thread is born in the result
1846
- // and is read from the durable presentation meta.
1847
- if (recorded.name === 'team_claim' && typeof args.taskRef === 'string' && args.taskRef !== '') {
1848
- push(this.requireLedger().threadForTask(args.taskRef));
1849
- }
1850
- else if (recorded.name === 'team_message') {
1851
- const meta = event.data.meta;
1852
- if (meta !== undefined && typeof meta === 'object' && meta.kind === 'committed') {
1853
- const metaThreadRef = meta.threadRef;
1854
- if (typeof metaThreadRef === 'string' && metaThreadRef !== '')
1855
- push(metaThreadRef);
1856
- }
1857
- else if (typeof args.threadRef === 'string' && args.threadRef !== '') {
1858
- push(args.threadRef);
1859
- }
1860
- }
1861
- }
1862
- catch {
1863
- // Malformed call arguments contribute no attribution.
1864
- }
1865
- }
1866
- }
1867
- }
1868
- else if (event.type === 'user/message') {
1869
- const source = event.data.source;
1870
- // Only this plugin's own delivered notices attribute Threads, by exact
1871
- // kind identity — the shape written now and the read-time conversion's
1872
- // rename of the released V3 history. A `plugin:` prefix test would
1873
- // claim third-party producers' rows as Team facts.
1874
- if (source === undefined || !isAgentTeamSourceKind(source.kind))
1875
- continue;
1876
- // Reminder notices never enter attribution — a recovery instruction
1877
- // (or a historical progress-nudge notice, kept decodable in session
1878
- // logs recorded before that system was removed) is not a Team fact.
1879
- if (source.form === 'notice' && source.summary !== undefined && isReminderNoticeSummary(source.summary))
1880
- continue;
1881
- const text = event.data.content.filter(block => block.type === 'text').map(block => block.text ?? '').join('\n');
1882
- for (const match of text.matchAll(/Thread: (thread:[0-9a-f-]{6,})/g)) {
1883
- push(match[1]);
1884
- }
1885
- }
1886
- }
1887
- return refs;
1888
- }
1889
- /**
1890
- * Price and annotate one timeline candidate without mutating anything.
1891
- * Retained prices in the SOURCE Session's own replayed measurement (the
1892
- * monotonic anchor-share of the source log); discarded is what a return
1893
- * replaces — for a current-generation anchor, the measured usage beyond
1894
- * the anchor; for an ancestor anchor, the current generation's whole
1895
- * usage (an approximation: the ancestor's own suffix is not part of this
1896
- * generation). A small current child therefore discards little but may
1897
- * still retain a large ancestor seed, and both numbers say so honestly.
1898
- */
1899
- timelineItemFor(candidate, sourceUsage, currentUsage, _hardLimit, handoffAt, sourceSessionId, sourceIsCurrent, sourceEvents) {
1900
- const retainedTokens = candidate.source === 'head'
1901
- ? currentUsage
1902
- : this.retainedEstimate(sourceUsage ?? 0, sourceEvents.length, candidate.turnEndSeq);
1903
- const discardedTokens = candidate.source === 'head'
1904
- ? 0
1905
- : sourceIsCurrent
1906
- ? Math.max(0, currentUsage - retainedTokens)
1907
- : currentUsage;
1908
- const affectedThreads = candidate.turnEndSeq === -1 ? [] : this.threadsEnteringContext(sourceEvents, candidate.turnEndSeq);
1909
- let restorable = candidate.turnEndSeq !== -1;
1910
- let reason;
1911
- if (candidate.source === 'head') {
1912
- // The head is the current working set: returning to it discards
1913
- // nothing and is never a meaningful return target.
1914
- restorable = false;
1915
- reason = 'the head is the current working set; returning to it discards nothing';
1916
- }
1917
- else if (sourceUsage === undefined) {
1918
- // The source's cost cannot be measured (no meter, or the archived
1919
- // ancestor cannot be borrowed): the budget cannot be proven, so the
1920
- // candidate is not selectable. Never price an unknown as zero.
1921
- restorable = false;
1922
- reason = 'the source Session\'s context cost cannot be measured, so the return budget cannot be proven';
1923
- }
1924
- else if (candidate.source === 'handoff' || candidate.source === 'compaction') {
1925
- // A handoff starts a generation and a compaction rewrites the visible
1926
- // surface: rewinding into them is not a proven-safe V1 target.
1927
- restorable = false;
1928
- reason = `source '${candidate.source}' is not a restorable checkpoint`;
1929
- }
1930
- else if (candidate.source === 'team-boundary') {
1931
- // A Team delivery is a selectable default checkpoint exactly when the
1932
- // Host can prove it stays inside one Thread's context: the boundary
1933
- // resolved at a completed turn AND exactly one Thread's facts entered
1934
- // the Session context through it. Multi-Thread or unattributable
1935
- // boundaries document why they are not selectable.
1936
- if (affectedThreads.length !== 1) {
1937
- restorable = false;
1938
- reason = affectedThreads.length === 0
1939
- ? 'no single Thread is attributable to this boundary'
1940
- : 'multiple Threads entered the context through this boundary; write a fresh handoff instead';
1941
- }
1942
- else if (retainedTokens >= handoffAt) {
1943
- restorable = false;
1944
- reason = 'retained context would not materially shrink the working set';
1945
- }
1946
- }
1947
- else if (candidate.source === 'agent' && retainedTokens >= handoffAt) {
1948
- restorable = false;
1949
- reason = 'retained context would be at or above the handoff budget';
1950
- }
1951
- return {
1952
- checkpointRef: candidate.ref,
1953
- name: candidate.label,
1954
- source: candidate.source,
1955
- retainedTokens,
1956
- discardedTokens,
1957
- affectedThreads,
1958
- restorable,
1959
- ...(reason === undefined ? {} : { reason }),
1960
- ...(candidate.source === 'agent' ? {} : { sourceSessionId }),
1961
- };
1962
- }
1963
2172
  /**
1964
2173
  * Agent-only rollover request validation: the tool calls this to check its
1965
2174
  * Member binding, exclusivity, and checkpoint ownership. It performs no
@@ -2284,7 +2493,8 @@ let AgentTeam = (() => {
2284
2493
  // old Session; the projection still carries the intent, so finish the
2285
2494
  // transition (or keep waiting for the containing turn) from here.
2286
2495
  if (persisted) {
2287
- const state = foldContextProjection(created.agent.session.ownEvents(), created.agent.session.inheritedEventCount, created.agent.session.id);
2496
+ const inheritedEventCount = created.agent.session.inheritedEventCount;
2497
+ const state = foldTeamContextProjection(created.agent.session.ownEvents(), { sessionId: created.agent.session.id, inheritedEventCount }, this.contextProjectionHost);
2288
2498
  if (state.pending !== null)
2289
2499
  this.contextManagement.recoverPendingTransition(member.memberId, created.agent, member.sessionId);
2290
2500
  // A restart between one checkpoint's durable result and its quiet
@@ -2299,7 +2509,7 @@ let AgentTeam = (() => {
2299
2509
  // by the projection boundary alone: every handoff this Host ever
2300
2510
  // published classifies under the context source's recognizer,
2301
2511
  // including the history the read-time conversion renamed.
2302
- if (!state.boundaries.some(boundary => boundary.source === 'handoff')) {
2512
+ if (!state.boundaries.some(boundary => boundary.kind === 'handoff')) {
2303
2513
  await this.reconstructMissingHandoff(member, created.agent);
2304
2514
  }
2305
2515
  // Carried input redelivery binds to the committed transition target —