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

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 (127) hide show
  1. package/README.md +5 -3
  2. package/README.zh.md +5 -3
  3. package/cordis.patch.yml +2 -185
  4. package/package.json +52 -46
  5. package/packages/agent-team/README.md +1 -1
  6. package/packages/agent-team/README.zh.md +1 -1
  7. package/packages/agent-team/core-skills/member-memory-manager/SKILL.md +30 -0
  8. package/packages/agent-team/lib/context-continuity-host.js +91 -0
  9. package/packages/agent-team/lib/context-projection.js +266 -489
  10. package/packages/agent-team/lib/context-source.js +17 -106
  11. package/packages/agent-team/lib/human-avatar.js +61 -0
  12. package/packages/agent-team/lib/human-profile.js +46 -0
  13. package/packages/agent-team/lib/human-update-check.js +119 -0
  14. package/packages/agent-team/lib/index.js +408 -264
  15. package/packages/agent-team/lib/ledger.js +120 -15
  16. package/packages/agent-team/lib/member-context.js +9 -8
  17. package/packages/agent-team/lib/member-runtime.js +1 -1
  18. package/packages/agent-team/lib/member-time-context.js +2 -5
  19. package/packages/agent-team/lib/preset-roster.js +15 -0
  20. package/packages/agent-team/lib/pressure-policy.js +9 -6
  21. package/packages/agent-team/lib/session-remediation.js +493 -0
  22. package/packages/agent-team/lib/stored-session-reader.js +2 -1
  23. package/packages/agent-team/lib/typert.host.js +605 -291
  24. package/packages/agent-team/lib/typert.remote-client.d.ts +15 -6
  25. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  26. package/packages/agent-team/lib/typert.remote-client.js +367 -244
  27. package/packages/agent-team/lib/types/context-continuity-host.d.ts +84 -0
  28. package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -0
  29. package/packages/agent-team/lib/types/context-projection.d.ts +130 -185
  30. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  31. package/packages/agent-team/lib/types/context-source.d.ts +17 -81
  32. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/human-avatar.d.ts +29 -0
  34. package/packages/agent-team/lib/types/human-avatar.d.ts.map +1 -0
  35. package/packages/agent-team/lib/types/human-profile.d.ts +44 -0
  36. package/packages/agent-team/lib/types/human-profile.d.ts.map +1 -0
  37. package/packages/agent-team/lib/types/human-update-check.d.ts +73 -0
  38. package/packages/agent-team/lib/types/human-update-check.d.ts.map +1 -0
  39. package/packages/agent-team/lib/types/index.d.ts +112 -38
  40. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  41. package/packages/agent-team/lib/types/ledger.d.ts +31 -2
  42. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  43. package/packages/agent-team/lib/types/member-context.d.ts +0 -14
  44. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  45. package/packages/agent-team/lib/types/member-time-context.d.ts +0 -15
  46. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
  47. package/packages/agent-team/lib/types/preset-roster.d.ts +5 -0
  48. package/packages/agent-team/lib/types/preset-roster.d.ts.map +1 -0
  49. package/packages/agent-team/lib/types/pressure-policy.d.ts +6 -0
  50. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  51. package/packages/agent-team/lib/types/session-remediation.d.ts +172 -0
  52. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
  53. package/packages/agent-team/lib/types/stored-session-reader.d.ts +2 -1
  54. package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -1
  55. package/packages/agent-team/lib/types/types/entities.d.ts +20 -3
  56. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  57. package/packages/agent-team/lib/types/types/requests-results.d.ts +63 -0
  58. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  59. package/packages/agent-team/preset/team-member/agent.cordis.yml +166 -0
  60. package/packages/client-agent-team/lib/client.js +1667 -516
  61. package/packages/client-agent-team/lib/client.js.map +1 -1
  62. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts +15 -0
  63. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +1 -0
  64. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +115 -0
  65. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +12 -8
  67. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +22 -1
  68. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -1
  69. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +25 -2
  70. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +6 -1
  71. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +69 -31
  73. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +4 -4
  74. package/packages/client-agent-team/lib/types/client/TeamComposer.js +3 -3
  75. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  76. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  77. package/packages/client-agent-team/lib/types/client/TeamConversation.js +12 -5
  78. package/packages/client-agent-team/lib/types/client/TeamFooterAction.js +2 -2
  79. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +4 -1
  80. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +22 -7
  82. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +11 -1
  83. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  84. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +69 -8
  85. package/packages/client-agent-team/lib/types/client/TeamMembersAction.js +2 -2
  86. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +18 -3
  87. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  88. package/packages/client-agent-team/lib/types/client/TeamMessage.js +213 -67
  89. package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts.map +1 -1
  90. package/packages/client-agent-team/lib/types/client/TeamRowMenu.js +2 -2
  91. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.js +2 -2
  92. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +5 -0
  93. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  94. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +55 -29
  95. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +3 -3
  96. package/packages/client-agent-team/lib/types/client/TeamWorkspaceSelector.js +2 -2
  97. package/packages/client-agent-team/lib/types/client/avatar-image.d.ts +21 -0
  98. package/packages/client-agent-team/lib/types/client/avatar-image.d.ts.map +1 -0
  99. package/packages/client-agent-team/lib/types/client/avatar-image.js +9 -0
  100. package/packages/client-agent-team/lib/types/client/human-identity.d.ts +94 -0
  101. package/packages/client-agent-team/lib/types/client/human-identity.d.ts.map +1 -0
  102. package/packages/client-agent-team/lib/types/client/human-identity.js +117 -0
  103. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  104. package/packages/client-agent-team/lib/types/client/index.js +112 -41
  105. package/packages/client-agent-team/lib/types/client/locales.d.ts +72 -2
  106. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  107. package/packages/client-agent-team/lib/types/client/locales.js +78 -2
  108. package/packages/client-agent-team/lib/types/client/multi-menu-field.js +2 -2
  109. package/packages/client-agent-team/lib/types/client/refs.d.ts +71 -0
  110. package/packages/client-agent-team/lib/types/client/refs.d.ts.map +1 -0
  111. package/packages/client-agent-team/lib/types/client/refs.js +181 -0
  112. package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -2
  113. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  114. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +57 -9
  115. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  116. package/packages/client-agent-team/lib/types/client/team-formatters.js +85 -17
  117. package/packages/tool-agent-team/README.md +1 -1
  118. package/packages/tool-agent-team/README.zh.md +1 -1
  119. package/packages/tool-agent-team/lib/context-tools.js +20 -6
  120. package/packages/tool-agent-team/lib/index.js +2 -2
  121. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  122. package/packages/agent-team/lib/context-management.js +0 -371
  123. package/packages/agent-team/lib/types/context-management.d.ts +0 -154
  124. package/packages/agent-team/lib/types/context-management.d.ts.map +0 -1
  125. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +0 -31
  126. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +0 -1
  127. package/packages/client-agent-team/lib/types/client/task-refs.js +0 -99
@@ -1,26 +1,36 @@
1
1
  /**
2
- * Host-only Session projection for Agent Team context management.
2
+ * Team's projection contribution to the context-continuity engine: the durable
3
+ * ref naming, the Team-notice rule, and the domain judgement that decides which
4
+ * events anchor a timeline boundary and what each boundary is attributable to.
3
5
  *
4
- * One pure synchronous fold over a Member Session log derives every
5
- * context-management fact the Host needs: pending rollover intent (a
6
- * successful `context_rollover` call/result pair — under the legacy
7
- * `new_context` name for Sessions recorded before the rename), explicit
8
- * checkpoints (a
9
- * successful `context_checkpoint` pair resolved by its containing `turn/end`),
10
- * handoff/compaction boundaries, and quiet-continuation delivery state. The
11
- * fold is the single authority for these facts — no second store, and callers
12
- * never re-derive intent from raw events.
6
+ * The fold itself is the engine's (`createContextProjectionDefinition`): one
7
+ * registered unit per Host folds every Session's durable log and owns the
8
+ * universal structure — checkpoints, the pending rollover intent, quiet
9
+ * continuation delivery, carry candidates, open calls, turn cursors. This
10
+ * module supplies only what the engine deliberately refuses to know:
13
11
  *
14
- * A seeded child Session folds with `inheritedEventCount` respected: events
15
- * inherited from the fork prefix are already resolved history, never fresh
16
- * intent. The cold fold and the registered unit both start past the inherited
17
- * cut, so an inherited historical rollover call can never schedule
18
- * another transition in the child.
12
+ * - the durable refs Team's existing logs already carry
13
+ * (`context-checkpoint-<sha256>`, `team-boundary-<sha256>`), frozen because
14
+ * recorded history is read back through them;
15
+ * - which queued messages are Team-owned notices the successor rederives;
16
+ * - which successful Team calls anchor a boundary, and which Threads each
17
+ * boundary is attributable to. A claim mutation resolves its Task's Thread
18
+ * through the ledger; that binding is written once at Task creation, so a
19
+ * re-fold reproduces the same attribution, and a missing ledger or mapping
20
+ * yields no attribution instead of failing the fold.
21
+ *
22
+ * The remaining exports are read views over the engine's state that Team still
23
+ * owns: the anchor lookups the rollover guard revalidates a cited ref through,
24
+ * the accumulated Thread attribution that guard and the timeline both judge a
25
+ * boundary by, and carried-message resolution — the engine keys carry
26
+ * candidates by message id, and the durable log is where the message body
27
+ * lives.
19
28
  * @module @wowyuarm/dsh-agent-team/context-projection
20
29
  */
21
30
  import { createHash } from 'node:crypto';
22
- import { z } from 'zod';
23
- import { continuationCheckpointRefOf, handoffOf, isAgentTeamContextSource, isAgentTeamSource, isCheckpointContinuationMessage } from "./context-source.js";
31
+ import { CONTEXT_CHECKPOINT_TOOL_NAME, CONTEXT_ROLLOVER_TOOL_NAME, createContextProjectionDefinition, foldContextProjection as foldEngineContextProjection, } from '@wowyuarm/dsh-context-continuity';
32
+ import { AGENT_TEAM_PLUGIN_ID, handoffOf, isAgentTeamContextSource } from "./context-source.js";
33
+ import { TEAM_CONTEXT_CODEC } from "./context-continuity-host.js";
24
34
  /** Summary marker of the pre-compaction memory hint. */
25
35
  const PRE_COMPACTION_NOTICE_SUMMARY = 'Compaction is imminent; consider persisting key conclusions.';
26
36
  /** Team tool whose successful mutations are semantic timeline candidates. */
@@ -32,111 +42,42 @@ const TEAM_THREAD_TOOL_NAME = 'team_thread';
32
42
  const TEAM_MESSAGE_BOUNDARY_LABEL = 'Team message';
33
43
  const TEAM_ATTENTION_BOUNDARY_LABEL = 'Team attention change';
34
44
  const TEAM_CLAIM_BOUNDARY_LABEL = 'Team task claim change';
45
+ /** The handoff boundary label; every generation opens with one. */
46
+ const HANDOFF_BOUNDARY_LABEL = 'context handoff';
47
+ /** The compaction boundary label. */
48
+ const COMPACTION_BOUNDARY_LABEL = 'compaction notice';
49
+ /**
50
+ * The anchor kinds Team contributes. The engine treats a kind as opaque, but
51
+ * Team's own read views map it back to the timeline source vocabulary, so the
52
+ * three are named once here instead of as bare strings at each contribution.
53
+ */
54
+ const TEAM_BOUNDARY_KINDS = ['team-boundary', 'handoff', 'compaction'];
35
55
  /**
36
56
  * Notice summaries that are pure reminders, never semantic Team facts: a
37
57
  * recovery instruction must not become a return anchor. `Progress visibility
38
58
  * reminder` is a historical decoder — the progress-nudge system was removed,
39
59
  * but session logs recorded before its removal still carry those notices, and
40
- * the projection keeps recognizing them (same pattern as the legacy
41
- * `new_context` tool name below).
60
+ * the fold keeps recognizing them (same pattern as the legacy `new_context`
61
+ * tool name below).
42
62
  */
43
63
  const REMINDER_NOTICE_SUMMARIES = new Set(['Progress visibility reminder', 'Recovery: continue your interrupted work.']);
44
64
  /** Whether one notice summary is a pure reminder (never a semantic Team fact). */
45
65
  export function isReminderNoticeSummary(summary) {
46
66
  return REMINDER_NOTICE_SUMMARIES.has(summary);
47
67
  }
48
- /** Stable tool names the projection recognizes. */
49
- export const CONTEXT_CHECKPOINT_TOOL_NAME = 'context_checkpoint';
50
- export const CONTEXT_ROLLOVER_TOOL_NAME = 'context_rollover';
68
+ /** Stable tool names the fold recognizes under their current names. */
69
+ export { CONTEXT_CHECKPOINT_TOOL_NAME, CONTEXT_ROLLOVER_TOOL_NAME };
51
70
  /**
52
71
  * Legacy decoder name: the rollover tool was renamed `new_context` →
53
72
  * `context_rollover`, and Sessions recorded before the rename still carry
54
- * durable `new_context` call/result pairs. The projection keeps folding them
55
- * — pending rollovers and crash recovery of existing Members depend on old
73
+ * durable `new_context` call/result pairs. The fold keeps decoding them —
74
+ * pending rollovers and crash recovery of existing Members depend on old
56
75
  * events still resolving intent — but this is a log decoder, not a tool
57
76
  * alias: no new call can carry the old name.
58
77
  */
59
78
  export const NEW_CONTEXT_TOOL_NAME = 'new_context';
60
- /** Whether one recorded tool name is a rollover call under the current or the legacy name. */
61
- function isRolloverToolName(name) {
62
- return name === CONTEXT_ROLLOVER_TOOL_NAME || name === NEW_CONTEXT_TOOL_NAME;
63
- }
64
- /** Whether one tool name can produce a Team-effect boundary from a successful call. */
65
- function isTeamEffectToolName(name) {
66
- return name === TEAM_CLAIM_TOOL_NAME || name === TEAM_MESSAGE_TOOL_NAME || name === TEAM_THREAD_TOOL_NAME;
67
- }
68
- /** Whether one raw team_thread arguments string is an attention mutation (follow/unfollow). */
69
- function argumentsAreAttentionMutation(raw) {
70
- let parsed;
71
- try {
72
- parsed = JSON.parse(raw);
73
- }
74
- catch {
75
- return false;
76
- }
77
- if (typeof parsed !== 'object' || parsed === null)
78
- return false;
79
- const { action } = parsed;
80
- return action === 'follow' || action === 'unfollow';
81
- }
82
- /** Whether one raw team_message arguments string is a Thread-effect attempt (start/reply — dm is not a Thread fact). */
83
- function argumentsAreMessageCommit(raw) {
84
- let parsed;
85
- try {
86
- parsed = JSON.parse(raw);
87
- }
88
- catch {
89
- return false;
90
- }
91
- if (typeof parsed !== 'object' || parsed === null)
92
- return false;
93
- const { action } = parsed;
94
- return action === 'start' || action === 'reply';
95
- }
96
- const relatedFileSchema = z.object({ path: z.string().min(1), reason: z.string() }).strict();
97
- const checkpointRefSchema = z.string().regex(/^(context-checkpoint-[0-9a-f]{64}|team-boundary-[0-9a-f]{64})$/).transform(value => value);
98
- const openCallSchema = z.object({ callId: z.string().min(1), name: z.string(), arguments: z.string() }).strict();
99
- const carriedCandidateSchema = z.object({
100
- message: z.any(),
101
- surfacedTurn: z.number().int(),
102
- consumed: z.boolean(),
103
- });
104
- const boundarySchema = z.object({
105
- key: z.string().min(1),
106
- source: z.enum(['team-boundary', 'handoff', 'compaction']),
107
- label: z.string(),
108
- seq: z.number().int().nonnegative(),
109
- turn: z.number().int().nonnegative(),
110
- turnEndSeq: z.number().int(),
111
- }).strict();
112
- const stateSchema = z.object({
113
- checkpoints: z.array(z.object({
114
- checkpointRef: checkpointRefSchema,
115
- name: z.string(),
116
- resultSeq: z.number().int().nonnegative(),
117
- turn: z.number().int().nonnegative(),
118
- turnEndSeq: z.number().int(),
119
- }).strict()),
120
- pending: z.object({
121
- toolCallId: z.string().min(1),
122
- resultSeq: z.number().int().nonnegative(),
123
- turn: z.number().int().nonnegative(),
124
- handoff: z.string(),
125
- checkpointRef: checkpointRefSchema.optional(),
126
- relatedFiles: z.array(relatedFileSchema),
127
- turnEndSeq: z.number().int(),
128
- }).strict().nullable(),
129
- continuations: z.array(z.object({
130
- checkpointRef: checkpointRefSchema,
131
- deliveredSeq: z.number().int(),
132
- }).strict()),
133
- carriedCandidates: z.array(carriedCandidateSchema),
134
- lastTurn: z.number().int().nonnegative(),
135
- openCalls: z.array(openCallSchema),
136
- boundaries: z.array(boundarySchema),
137
- seenThreads: z.array(z.string()),
138
- lastTurnEndSeq: z.number().int(),
139
- }).strict();
79
+ /** Every recorded tool name a rollover call may carry, current name first. */
80
+ export const TEAM_ROLLOVER_TOOL_NAMES = [CONTEXT_ROLLOVER_TOOL_NAME, NEW_CONTEXT_TOOL_NAME];
140
81
  /**
141
82
  * Deterministic checkpoint ref from the recording session and tool call
142
83
  * identity: a bounded, collision-resistant opaque token. Provider call ids
@@ -150,8 +91,8 @@ export function checkpointRefFor(sessionId, callId) {
150
91
  return `context-checkpoint-${createHash('sha256').update(JSON.stringify([sessionId, callId])).digest('hex')}`;
151
92
  }
152
93
  /**
153
- * Deterministic default-boundary ref for one delivered Team boundary: the
154
- * same session-scoped hash shape as checkpoint refs, keyed on the boundary's
94
+ * Deterministic boundary ref for one delivered Team boundary: the same
95
+ * session-scoped hash shape as checkpoint refs, keyed on the boundary's
155
96
  * anchoring event seq. Consecutive generations routinely repeat event seqs,
156
97
  * so the Session identity must be part of the key or two generations'
157
98
  * boundaries at the same seq collide — the timeline would silently drop the
@@ -160,129 +101,54 @@ export function checkpointRefFor(sessionId, callId) {
160
101
  export function boundaryRefFor(sessionId, seq) {
161
102
  return `team-boundary-${createHash('sha256').update(JSON.stringify([sessionId, seq])).digest('hex')}`;
162
103
  }
163
- function emptyState() {
164
- return { checkpoints: [], pending: null, continuations: [], carriedCandidates: [], lastTurn: 0, openCalls: [], boundaries: [], seenThreads: [], lastTurnEndSeq: -1 };
165
- }
166
104
  /**
167
- * Cold-fold one immutable event log into the projection state. Events at or
168
- * before `inheritedEventCount` belong to the fork prefix and are resolved
169
- * history in this Session, so they never produce fresh intent. `sessionId` is
170
- * the log's own Session identity — it keys checkpoint refs, so the same
171
- * provider call id in two different Sessions produces two distinct refs.
105
+ * Whether one queued message is a Team-owned notice the rederived Inbox
106
+ * replaces. Handoff and continuation envelopes carry the same plugin
107
+ * attribution but are ordinary delivered context the new generation keeps, so
108
+ * they are excluded rather than dropped.
172
109
  */
173
- export function foldContextProjection(events, inheritedEventCount = 0, sessionId = '') {
174
- let state = emptyState();
175
- const inherited = Number(inheritedEventCount);
176
- for (const event of events) {
177
- if (event.seq < inherited)
178
- continue;
179
- state = applyContextEvent(state, event, sessionId);
180
- }
181
- return state;
110
+ export function isTeamNotice(message) {
111
+ const source = message.source;
112
+ return source.kind === 'plugin' && source.plugin === AGENT_TEAM_PLUGIN_ID && !isAgentTeamContextSource(message);
182
113
  }
183
- /**
184
- * The same fold as {@link foldContextProjection}, as the `{ start, step }` pair
185
- * the incremental Session cursor consumes. The cursor route and the cold fold
186
- * therefore share one transition function: a Session's projection cannot
187
- * depend on which of the two read it.
188
- */
189
- export const contextProjectionFold = (sessionId) => ({
190
- start: emptyState(),
191
- step: (state, event) => applyContextEvent(state, event, sessionId),
192
- });
193
- /**
194
- * The host-only projection unit; no wire view is published. The definition is
195
- * a factory: each Session folds with its own identity so checkpoint refs
196
- * derive from that Session's exact `(sessionId, callId)` pairs.
197
- */
198
- export const agentTeamContextProjectionDefinition = (sessionId) => ({
199
- key: 'agentTeamContext',
200
- // v4: first-arrival boundary labels — notice-class boundaries label the
201
- // Threads they first introduce (`First arrival: …`) instead of the
202
- // notice's own summary, and the claim boundary label names its Task
203
- // surface (`Team task claim change`). Labels are fold-time synthesized,
204
- // not stored facts, so a persisted v3 row must not survive next to the
205
- // new vocabulary: the ver mismatch refolds it from the full log.
206
- stateVersion: 4,
207
- stateSchema,
208
- init: (_header, _inheritedEventCount) => emptyState(),
209
- apply: (state, event) => applyContextEvent(state, event, sessionId),
210
- });
211
- /**
212
- * Pure transition: previous state + one committed event → next state. Returns
213
- * the same reference when the event is not this unit's.
214
- */
215
- function applyContextEvent(state, event, sessionId) {
216
- if (event.type === 'tool/call') {
217
- if (!isRolloverToolName(event.data.name) && event.data.name !== CONTEXT_CHECKPOINT_TOOL_NAME && !isTeamEffectToolName(event.data.name))
218
- return state;
219
- // Only Team-effect calls that can produce a boundary are tracked; `list`
220
- // and other non-mutations are not semantic timeline candidates.
221
- if (event.data.name === TEAM_CLAIM_TOOL_NAME && !argumentsAreClaimMutation(event.data.arguments))
222
- return state;
223
- if (event.data.name === TEAM_THREAD_TOOL_NAME && !argumentsAreAttentionMutation(event.data.arguments))
224
- return state;
225
- if (event.data.name === TEAM_MESSAGE_TOOL_NAME && !argumentsAreMessageCommit(event.data.arguments))
226
- return state;
227
- return { ...state, openCalls: [...state.openCalls, { callId: event.data.callId, name: event.data.name, arguments: event.data.arguments }] };
228
- }
229
- if (event.type === 'tool/result') {
230
- return applyToolResult(state, event.seq, event.data.turn, event.data.message, event.data.error !== undefined, sessionId, event.data.meta);
231
- }
232
- if (event.type === 'turn/end') {
233
- return applyTurnEnd(state, event.seq, event.data.turn);
234
- }
235
- if (event.type === 'user/message') {
236
- return applyUserMessage(state, event.seq, event.data, sessionId);
237
- }
238
- if (event.type === 'agent/inbox/spliced') {
239
- return applyInboxSpliced(state, event.data);
240
- }
241
- if (event.type === 'assistant/message') {
242
- return applyAssistantMessage(state, event);
243
- }
244
- if (event.type === 'turn/start') {
245
- return event.data.turn === state.lastTurn ? state : { ...state, lastTurn: event.data.turn };
246
- }
247
- return state;
114
+ /** Whether one tool name can produce a Team-effect boundary from a successful call. */
115
+ function isTeamEffectToolName(name) {
116
+ return name === TEAM_CLAIM_TOOL_NAME || name === TEAM_MESSAGE_TOOL_NAME || name === TEAM_THREAD_TOOL_NAME;
248
117
  }
249
- /**
250
- * Structural timeline boundary from one delivered user message: a rollover
251
- * handoff starts a generation; a compaction notice rewrites the visible
252
- * surface. A structured Team notification is a boundary ONLY on the first
253
- * arrival of each Thread's facts into this Session — the preserved
254
- * "work just arrived" anchor; every later re-delivery of the same Thread is
255
- * noise. Reminder notices (progress nudges, recovery instructions) never
256
- * anchor. All anchor to the containing completed turn. Plain Human/agent
257
- * prose and quiet checkpoint continuations are not boundaries.
258
- */
259
- function boundaryFromUserMessage(sessionId, seq, message, seenThreads) {
260
- const source = message.source;
261
- if (handoffOf(message) !== undefined) {
262
- return { key: `handoff:${seq}`, source: 'handoff', label: 'context handoff', seq, turn: -1, turnEndSeq: -1 };
118
+ /** One parsed JSON argument object, or undefined when the raw string carries none. */
119
+ function parseArguments(raw) {
120
+ let parsed;
121
+ try {
122
+ parsed = JSON.parse(raw);
263
123
  }
264
- if (!isAgentTeamSource(source))
265
- return undefined;
266
- if (source.form === 'relay')
124
+ catch {
267
125
  return undefined;
268
- if (source.form === 'notice' && source.summary === PRE_COMPACTION_NOTICE_SUMMARY) {
269
- return { key: `compaction:${seq}`, source: 'compaction', label: 'compaction notice', seq, turn: -1, turnEndSeq: -1 };
270
- }
271
- if (source.form === 'notice' && source.summary !== undefined && REMINDER_NOTICE_SUMMARIES.has(source.summary)) {
272
- return { reminder: true };
273
126
  }
274
- // The Threads this delivery first introduces anchor the boundary; a
275
- // delivery that introduces none (pure re-delivery) is noise.
276
- const firstArrivals = threadsQuotedInMessage(message).filter(ref => !seenThreads.includes(ref));
277
- if (firstArrivals.length === 0)
278
- return undefined;
279
- // The label states WHAT first arrived — the Thread refs this delivery
280
- // introduced into the context — never the notice's own generic account
281
- // (e.g. "unread work"): the timeline's decision surface needs the
282
- // attribution before a checkpointRef pick, and it is only knowable here,
283
- // at the fold, where first arrival is computed.
284
- const label = `First arrival: ${firstArrivals.join(', ')}`;
285
- return { key: boundaryRefFor(sessionId, seq), source: 'team-boundary', label, seq, turn: -1, turnEndSeq: -1, firstArrival: firstArrivals };
127
+ return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
128
+ }
129
+ /** The `action` one raw tool-call argument string declares, when it declares one. */
130
+ function actionOf(raw) {
131
+ return parseArguments(raw)?.action;
132
+ }
133
+ /** Whether one raw Team-claim arguments string is a mutation (not `list`). */
134
+ function argumentsAreClaimMutation(raw) {
135
+ const action = actionOf(raw);
136
+ return action === 'claim' || action === 'done' || action === 'release';
137
+ }
138
+ /** Whether one raw team_thread arguments string is an attention mutation (follow/unfollow). */
139
+ function argumentsAreAttentionMutation(raw) {
140
+ const action = actionOf(raw);
141
+ return action === 'follow' || action === 'unfollow';
142
+ }
143
+ /** Whether one raw team_message arguments string is a Thread-effect attempt (start/reply — dm is not a Thread fact). */
144
+ function argumentsAreMessageCommit(raw) {
145
+ const action = actionOf(raw);
146
+ return action === 'start' || action === 'reply';
147
+ }
148
+ /** The Thread ref one raw argument string names, when it names one. */
149
+ function threadRefIn(raw) {
150
+ const threadRef = parseArguments(raw)?.threadRef;
151
+ return typeof threadRef === 'string' && threadRef !== '' ? threadRef : undefined;
286
152
  }
287
153
  /** Thread refs structurally quoted in one delivered message body (`Thread: <ref>` lines). */
288
154
  function threadsQuotedInMessage(message) {
@@ -295,294 +161,205 @@ function threadsQuotedInMessage(message) {
295
161
  return refs;
296
162
  }
297
163
  /**
298
- * Fold one durable inbox splice. After a pending intent exists, every
299
- * non-Team message inserted into the inbox is a carry candidate; the
300
- * durable log is the truth, so a claim, removal, or cancel can never make
301
- * the message vanish silently — only surfacing it onto the model-visible
302
- * `user/message` surface consumes it.
164
+ * Team's domain half of the context-continuity projection. Every method is a
165
+ * pure judgement over the event the engine hands it plus Team's own durable
166
+ * vocabulary; the engine keeps the state machine.
303
167
  */
304
- function applyInboxSpliced(state, data) {
305
- if (state.pending === null || data.inserted.length === 0)
306
- return state;
307
- const fresh = data.inserted
308
- .filter(message => !isTeamNotice(message))
309
- .filter(message => !state.carriedCandidates.some(candidate => candidate.message.id === message.id))
310
- .map(message => ({ message, surfacedTurn: -1, consumed: false }));
311
- if (fresh.length === 0)
312
- return state;
313
- return { ...state, carriedCandidates: [...state.carriedCandidates, ...fresh] };
314
- }
315
- function applyToolResult(state, seq, turn, message, internalFailure, sessionId, meta) {
316
- // A landed result — success or failure — consumes its paired open call.
317
- // Leaving a failed result's call open would dangle it forever, and a
318
- // provider retry reusing the callId would pair its fresh success result
319
- // with these stale arguments. An unpaired result (no matching open call)
320
- // touches nothing.
321
- const index = state.openCalls.findIndex(call => call.callId === message.toolCallId);
322
- if (index === -1)
323
- return state;
324
- const recorded = state.openCalls[index];
325
- const openCalls = state.openCalls.filter(call => call.callId !== message.toolCallId);
326
- // A successful pair only: model-visible errors and internal failures carry
327
- // neither checkpoint, rollover intent, nor an effect boundary — the
328
- // failure-face contract also holds for a team_message whose presentation
329
- // meta projection failed (that lands as a ToolOutputError result).
330
- if (message.isError === true || internalFailure)
331
- return { ...state, openCalls };
332
- if (isRolloverToolName(recorded.name)) {
333
- const parsed = parseRolloverArguments(recorded.arguments);
334
- if (parsed === undefined)
335
- return { ...state, openCalls };
336
- // One pending intent per unresolved turn: a second successful call inside
337
- // the SAME turn replaces nothing — the first owns the swap. A pending
338
- // whose turn ended is the ready (or, after a restart, recoverable)
339
- // intent, NOT spent: the coordinator's process lock normally rejects a
340
- // later call before it can reach this fold. Only once that lock is gone
341
- // (the transition failed or never ran) can a later-turn successful
342
- // rollover replace the ended intent — the retry path that recovers the
343
- // Member when the tool kept answering `scheduled` but no swap came.
344
- if (state.pending !== null && state.pending.turnEndSeq === -1)
345
- return { ...state, openCalls };
346
- return { ...state, openCalls, pending: { ...parsed, toolCallId: recorded.callId, resultSeq: seq, turn, turnEndSeq: -1 } };
347
- }
348
- if (recorded.name === CONTEXT_CHECKPOINT_TOOL_NAME) {
349
- const parsed = parseCheckpointArguments(recorded.arguments);
350
- if (parsed === undefined)
351
- return { ...state, openCalls };
352
- const checkpointRef = checkpointRefFor(sessionId, message.toolCallId);
353
- return {
354
- ...state,
355
- openCalls,
356
- checkpoints: [...state.checkpoints, { checkpointRef, name: parsed.name, resultSeq: seq, turn, turnEndSeq: -1 }],
357
- };
358
- }
359
- // Effect boundaries — the attribution matrix:
360
- // claim mutation → the Task ref from its call arguments (Task → Thread
361
- // resolution stays a query-time ledger concern);
362
- // team_message committed → reply/taskRef from args, start from the
363
- // structured presentation meta (`kind === 'committed'` guards every
364
- // typed rejection; no meta means no boundary — old logs refold without
365
- // start anchors by design, never by parsing render text);
366
- // team_thread follow/unfollow → the threadRef from its call arguments.
367
- // A boundary without a resolvable ref is not produced.
368
- const boundary = effectBoundaryFor(recorded, sessionId, seq, turn, meta);
369
- if (boundary === undefined)
370
- return { ...state, openCalls };
371
- return { ...state, openCalls, boundaries: [...state.boundaries, boundary] };
372
- }
373
- /** The effect boundary one successful Team-effect call produces, if its attribution resolves. */
374
- function effectBoundaryFor(recorded, sessionId, seq, turn, meta) {
375
- if (recorded.name === TEAM_CLAIM_TOOL_NAME) {
376
- return { key: boundaryRefFor(sessionId, seq), source: 'team-boundary', label: TEAM_CLAIM_BOUNDARY_LABEL, seq, turn, turnEndSeq: -1 };
377
- }
378
- if (recorded.name === TEAM_THREAD_TOOL_NAME) {
379
- return { key: boundaryRefFor(sessionId, seq), source: 'team-boundary', label: TEAM_ATTENTION_BOUNDARY_LABEL, seq, turn, turnEndSeq: -1 };
380
- }
381
- if (recorded.name === TEAM_MESSAGE_TOOL_NAME) {
382
- // The structured meta projection is the single attribution source for a
383
- // start (the Thread is born in the result); replies carry their ref in
384
- // the call arguments already, but the committed guard is meta-side for
385
- // every action — typed rejections are successful calls, not errors.
386
- if (meta === undefined || typeof meta !== 'object' || meta === null)
168
+ export class TeamContextProjectionHost {
169
+ options;
170
+ constructor(options = {}) {
171
+ this.options = options;
172
+ }
173
+ /** Durable checkpoint ref: Team's existing log vocabulary, unchanged. */
174
+ checkpointRefFor(sessionId, toolCallId) {
175
+ return checkpointRefFor(sessionId, toolCallId);
176
+ }
177
+ /** Durable boundary ref: the same session-scoped hash shape. */
178
+ boundaryRefFor(sessionId, seq) {
179
+ return boundaryRefFor(sessionId, seq);
180
+ }
181
+ /** A Team-owned notice the successor generation rederives from ledger facts. */
182
+ isEphemeralNotice(message) {
183
+ return isTeamNotice(message);
184
+ }
185
+ /**
186
+ * Only a Team-effect call that can produce a boundary is tracked at all:
187
+ * `list` on a claim, `read` on a Thread, and `dm` are not semantic timeline
188
+ * candidates, so their results must never open an anchor.
189
+ */
190
+ tracksCall(name, raw) {
191
+ if (!isTeamEffectToolName(name))
192
+ return false;
193
+ if (name === TEAM_CLAIM_TOOL_NAME)
194
+ return argumentsAreClaimMutation(raw);
195
+ if (name === TEAM_THREAD_TOOL_NAME)
196
+ return argumentsAreAttentionMutation(raw);
197
+ return argumentsAreMessageCommit(raw);
198
+ }
199
+ /** Team's boundary for one event, or undefined when the event anchors nothing. */
200
+ domainBoundaryOf(input) {
201
+ return input.source === 'user-message'
202
+ ? this.boundaryFromUserMessage(input)
203
+ : this.boundaryFromToolResult(input);
204
+ }
205
+ /**
206
+ * Structural boundary from one delivered user message: a rollover handoff
207
+ * starts a generation; a compaction notice rewrites the visible surface. A
208
+ * structured Team notification is a boundary ONLY on the first arrival of
209
+ * each Thread's facts into this Session — the preserved "work just arrived"
210
+ * anchor; every later re-delivery of the same Thread is noise. Reminder
211
+ * notices (progress nudges, recovery instructions) never anchor. Plain
212
+ * Human/agent prose and quiet checkpoint continuations are not boundaries.
213
+ */
214
+ boundaryFromUserMessage(input) {
215
+ const { message, seenTopics } = input;
216
+ if (handoffOf(message) !== undefined) {
217
+ return { kind: 'handoff', label: HANDOFF_BOUNDARY_LABEL, topics: [] };
218
+ }
219
+ const source = message.source;
220
+ if (source.kind !== 'plugin' || source.plugin !== AGENT_TEAM_PLUGIN_ID)
387
221
  return undefined;
388
- const { kind, threadRef } = meta;
389
- if (kind !== 'committed' || typeof threadRef !== 'string' || threadRef === '')
222
+ if (source.form === 'relay')
390
223
  return undefined;
391
- return { key: boundaryRefFor(sessionId, seq), source: 'team-boundary', label: TEAM_MESSAGE_BOUNDARY_LABEL, seq, turn, turnEndSeq: -1 };
392
- }
393
- return undefined;
394
- }
395
- function applyTurnEnd(state, seq, _turn) {
396
- let changed = false;
397
- const checkpoints = state.checkpoints.map(entry => {
398
- if (entry.turnEndSeq !== -1)
399
- return entry;
400
- changed = true;
401
- return { ...entry, turnEndSeq: seq };
402
- });
403
- let pending = state.pending;
404
- if (pending !== null && pending.turnEndSeq === -1) {
405
- pending = { ...pending, turnEndSeq: seq };
406
- changed = true;
407
- }
408
- const boundaries = state.boundaries.map(entry => {
409
- if (entry.turnEndSeq !== -1)
410
- return entry;
411
- changed = true;
412
- return { ...entry, turnEndSeq: seq, turn: entry.turn === -1 ? _turn : entry.turn };
413
- });
414
- if (state.lastTurnEndSeq !== seq)
415
- changed = true;
416
- return changed ? { ...state, checkpoints, pending, boundaries, lastTurnEndSeq: seq } : state;
417
- }
418
- function applyUserMessage(state, seq, message, sessionId) {
419
- let next = state;
420
- // A surfaced candidate is NOT consumed yet: the loop appends user/message
421
- // before the step runs, so cancellation can still land between them. Only
422
- // a completed, uninterrupted assistant answer for the same turn proves the
423
- // old generation handled it; anything else stays carried, so an aborted
424
- // partial request re-delivers the input instead of silently dropping it.
425
- const surfaced = next.carriedCandidates.some(candidate => !candidate.consumed && candidate.message.id === message.id);
426
- if (surfaced) {
427
- // The user/message event carries no turn field, but the projection folds
428
- // in log order: the containing step/start preceded this message, and
429
- // the fold tracks the current turn from turn/start. Candidates surface
430
- // inside the turn the inbox claimed them for.
431
- next = { ...next, carriedCandidates: next.carriedCandidates.map(candidate => candidate.message.id === message.id && !candidate.consumed ? { ...candidate, surfacedTurn: next.lastTurn } : candidate) };
432
- }
433
- // A structural boundary delivery (handoff start, first-arrival Team notice,
434
- // compaction notice) becomes a timeline candidate anchored to the containing
435
- // turn; it resolves when that turn ends. Threads the delivery first
436
- // introduces join seenThreads so later re-deliveries produce nothing.
437
- const boundary = boundaryFromUserMessage(sessionId, seq, message, next.seenThreads);
438
- if (boundary !== undefined && !('reminder' in boundary)) {
439
- if ('firstArrival' in boundary) {
440
- const { firstArrival, ...timelineBoundary } = boundary;
441
- next = {
442
- ...next,
443
- boundaries: [...next.boundaries, timelineBoundary],
444
- seenThreads: [...next.seenThreads, ...firstArrival],
445
- };
224
+ if (source.form === 'notice' && source.summary === PRE_COMPACTION_NOTICE_SUMMARY) {
225
+ return { kind: 'compaction', label: COMPACTION_BOUNDARY_LABEL, topics: [] };
446
226
  }
447
- else {
448
- next = { ...next, boundaries: [...next.boundaries, boundary] };
227
+ if (source.form === 'notice' && source.summary !== undefined && isReminderNoticeSummary(source.summary))
228
+ return undefined;
229
+ // The Threads this delivery first introduces anchor the boundary; a
230
+ // delivery that introduces none (pure re-delivery) is noise.
231
+ const firstArrivals = threadsQuotedInMessage(message).filter(ref => !seenTopics.includes(ref));
232
+ if (firstArrivals.length === 0)
233
+ return undefined;
234
+ // The label states WHAT first arrived — the Thread refs this delivery
235
+ // introduced into the context — never the notice's own generic account
236
+ // (e.g. "unread work"): the timeline's decision surface needs the
237
+ // attribution before a checkpointRef pick, and it is only knowable here,
238
+ // at the fold, where first arrival is computed.
239
+ return { kind: 'team-boundary', label: `First arrival: ${firstArrivals.join(', ')}`, topics: firstArrivals };
240
+ }
241
+ /**
242
+ * Effect boundary from one successful Team-effect call — the attribution
243
+ * matrix: claim mutation → the Thread its Task was created in (resolved
244
+ * through the ledger, whose binding is immutable); team_message committed →
245
+ * the Thread from the call arguments or, for a start, from the structured
246
+ * presentation meta (`kind === 'committed'` guards every typed rejection; no
247
+ * meta means no boundary — old logs refold without start anchors by design,
248
+ * never by parsing render text); team_thread follow/unfollow → the threadRef
249
+ * from its call arguments. An unresolvable attribution leaves the boundary
250
+ * anchored but not single-Thread selectable.
251
+ */
252
+ boundaryFromToolResult(input) {
253
+ const { name, arguments: raw, meta } = input;
254
+ if (name === TEAM_CLAIM_TOOL_NAME) {
255
+ const taskRef = parseArguments(raw)?.taskRef;
256
+ const threadRef = typeof taskRef === 'string' && taskRef !== ''
257
+ ? this.options.threadForTask?.(taskRef)
258
+ : undefined;
259
+ return { kind: 'team-boundary', label: TEAM_CLAIM_BOUNDARY_LABEL, topics: threadRef === undefined ? [] : [threadRef] };
449
260
  }
261
+ if (name === TEAM_THREAD_TOOL_NAME) {
262
+ const threadRef = threadRefIn(raw);
263
+ return { kind: 'team-boundary', label: TEAM_ATTENTION_BOUNDARY_LABEL, topics: threadRef === undefined ? [] : [threadRef] };
264
+ }
265
+ if (name === TEAM_MESSAGE_TOOL_NAME) {
266
+ // The structured meta projection is the single attribution source for a
267
+ // start (the Thread is born in the result); replies carry their ref in
268
+ // the call arguments already, but the committed guard is meta-side for
269
+ // every action — typed rejections are successful calls, not errors.
270
+ if (meta === undefined || typeof meta !== 'object' || meta === null)
271
+ return undefined;
272
+ const { kind, threadRef } = meta;
273
+ if (kind !== 'committed' || typeof threadRef !== 'string' || threadRef === '')
274
+ return undefined;
275
+ return { kind: 'team-boundary', label: TEAM_MESSAGE_BOUNDARY_LABEL, topics: [threadRef] };
276
+ }
277
+ return undefined;
450
278
  }
451
- // The quiet continuation notice delivered for one checkpoint completes its
452
- // delivery state; replay repair reads this to avoid re-scheduling it.
453
- const continuation = continuationCheckpointRefOf(message);
454
- if (continuation === undefined)
455
- return next;
456
- const checkpointRef = continuation;
457
- const existing = next.continuations.find(entry => entry.checkpointRef === checkpointRef);
458
- if (existing !== undefined) {
459
- if (existing.deliveredSeq !== -1)
460
- return next;
461
- return { ...next, continuations: next.continuations.map(entry => entry === existing ? { ...entry, deliveredSeq: seq } : entry) };
462
- }
463
- return { ...next, continuations: [...next.continuations, { checkpointRef, deliveredSeq: seq }] };
464
- }
465
- /** A completed, uninterrupted assistant turn answers every candidate surfaced into it. */
466
- function applyAssistantMessage(state, event) {
467
- if (state.carriedCandidates.length === 0 || event.data.interrupted === true)
468
- return state;
469
- let changed = false;
470
- const carriedCandidates = state.carriedCandidates.map(candidate => {
471
- if (candidate.consumed || candidate.surfacedTurn !== event.data.turn)
472
- return candidate;
473
- changed = true;
474
- return { ...candidate, consumed: true };
475
- });
476
- return changed ? { ...state, carriedCandidates } : state;
477
- }
478
- /** The carried input one transition must deliver: unconsumed post-intent candidates, in queue order. */
479
- export function carriedInputOf(state) {
480
- return state.carriedCandidates.filter(candidate => !candidate.consumed).map(candidate => candidate.message);
481
279
  }
482
280
  /**
483
- * Whether one queued message is a Team-owned notice the rederived Inbox
484
- * replaces. Handoff and continuation envelopes carry the same plugin
485
- * attribution but are ordinary delivered context the new generation keeps, so
486
- * they are excluded rather than dropped.
281
+ * The engine fold configured for Team: the codec Team's durable envelopes are
282
+ * written with, Team's projection host, and every recorded rollover name
283
+ * (current and legacy). One config drives both the registered unit and every
284
+ * cold fold, so a replayed log and the live unit converge on one state.
487
285
  */
488
- function isTeamNotice(message) {
489
- return isAgentTeamSource(message.source) && !isAgentTeamContextSource(message);
490
- }
491
- /** Whether a quiet continuation for one checkpoint was already delivered in this Session. */
492
- export function continuationDelivered(state, checkpointRef) {
493
- return state.continuations.some(entry => entry.checkpointRef === checkpointRef && entry.deliveredSeq !== -1);
494
- }
495
- /** Record that a continuation for one checkpoint is scheduled (delivery not yet seen). */
496
- export function withScheduledContinuation(state, checkpointRef) {
497
- if (state.continuations.some(entry => entry.checkpointRef === checkpointRef))
498
- return state;
499
- return { ...state, continuations: [...state.continuations, { checkpointRef, deliveredSeq: -1 }] };
500
- }
501
- /** Whether one raw Team-claim arguments string is a mutation (not `list`). */
502
- function argumentsAreClaimMutation(raw) {
503
- let parsed;
504
- try {
505
- parsed = JSON.parse(raw);
506
- }
507
- catch {
508
- return false;
509
- }
510
- if (typeof parsed !== 'object' || parsed === null)
511
- return false;
512
- const { action } = parsed;
513
- return action === 'claim' || action === 'done' || action === 'release';
514
- }
515
- function parseRolloverArguments(raw) {
516
- let parsed;
517
- try {
518
- parsed = JSON.parse(raw);
519
- }
520
- catch {
521
- return undefined;
522
- }
523
- if (typeof parsed !== 'object' || parsed === null)
524
- return undefined;
525
- const { handoff, checkpointRef, relatedFiles } = parsed;
526
- if (typeof handoff !== 'string' || handoff.trim() === '')
527
- return undefined;
528
- if (checkpointRef !== undefined && typeof checkpointRef !== 'string')
529
- return undefined;
530
- if (relatedFiles !== undefined && !isRelatedFiles(relatedFiles))
531
- return undefined;
286
+ export function createTeamContextProjectionConfig(host) {
532
287
  return {
533
- handoff,
534
- ...(checkpointRef === undefined ? {} : { checkpointRef: checkpointRef }),
535
- relatedFiles: relatedFiles === undefined ? [] : relatedFiles,
288
+ codec: TEAM_CONTEXT_CODEC,
289
+ host,
290
+ rolloverToolNames: TEAM_ROLLOVER_TOOL_NAMES,
291
+ checkpointToolName: CONTEXT_CHECKPOINT_TOOL_NAME,
536
292
  };
537
293
  }
538
- function parseCheckpointArguments(raw) {
539
- let parsed;
540
- try {
541
- parsed = JSON.parse(raw);
542
- }
543
- catch {
544
- return undefined;
545
- }
546
- if (typeof parsed !== 'object' || parsed === null)
547
- return undefined;
548
- const { name } = parsed;
549
- if (typeof name !== 'string' || name.trim() === '')
550
- return undefined;
551
- return { name };
294
+ /**
295
+ * Cold-fold one immutable Team Session log through the engine's fold with
296
+ * Team's config. The registered unit uses the same config, so the two agree.
297
+ */
298
+ export function foldTeamContextProjection(events, target, host = new TeamContextProjectionHost()) {
299
+ return foldEngineContextProjection(events, createTeamContextProjectionConfig(host), target);
552
300
  }
553
- function isRelatedFiles(value) {
554
- return Array.isArray(value) && value.every(file => typeof file === 'object' && file !== null
555
- && typeof file.path === 'string' && typeof file.reason === 'string');
301
+ /**
302
+ * The host-only projection unit for Team Members. Register it **once per
303
+ * Host**: the framework keeps one unit per projection key and drives it for
304
+ * every Session, so the definition closes over no Session identity.
305
+ */
306
+ export function createTeamContextProjectionDefinition(host) {
307
+ return createContextProjectionDefinition(createTeamContextProjectionConfig(host));
556
308
  }
557
- // Re-exported for callers that only want the predicate view of continuation
558
- // delivery without importing the source module's message constructors.
559
- export { isCheckpointContinuationMessage };
560
309
  /**
561
- * Bounded structural timeline candidates, newest first: explicit checkpoints
562
- * (resolved only), structural boundaries, then the current head. The Host
563
- * prices retained/discarded tokens and applies restorability guards on top;
564
- * this view is the single source of candidate anchors and labels.
310
+ * The Threads Team's own fold attributed to boundaries resolved by one
311
+ * completed turn, order-stable and deduplicated: a delivered notice's first
312
+ * arrival, a claim mutation's Task→Thread binding, a committed Thread effect.
313
+ *
314
+ * This is the accumulated attribution of the RETAINED PREFIX through that
315
+ * turn, and it is one set with two readers — the timeline publishes it as the
316
+ * item's affected Threads, and the rollover guard refuses a boundary whose
317
+ * prefix spans more than one Thread, so a ref the timeline offers is a ref
318
+ * `context_rollover` accepts.
565
319
  */
566
- export function timelineCandidates(state, limit) {
567
- const candidates = [];
568
- for (const checkpoint of state.checkpoints) {
569
- if (checkpoint.turnEndSeq === -1)
570
- continue;
571
- candidates.push({ ref: checkpoint.checkpointRef, label: checkpoint.name, source: 'agent', seq: checkpoint.resultSeq, turnEndSeq: checkpoint.turnEndSeq });
572
- }
320
+ export function retainedTopicsThrough(state, turnEndSeq) {
321
+ const topics = [];
573
322
  for (const boundary of state.boundaries) {
574
- if (boundary.turnEndSeq === -1)
323
+ if (boundary.turnEndSeq === -1 || boundary.turnEndSeq > turnEndSeq)
575
324
  continue;
576
- candidates.push({ ref: boundary.key, label: boundary.label, source: boundary.source, seq: boundary.seq, turnEndSeq: boundary.turnEndSeq });
577
- }
578
- if (state.lastTurnEndSeq !== -1) {
579
- candidates.push({ ref: `head:${state.lastTurnEndSeq}`, label: 'current head', source: 'head', seq: state.lastTurnEndSeq, turnEndSeq: state.lastTurnEndSeq });
325
+ for (const topic of boundary.attributions) {
326
+ if (!topics.includes(topic))
327
+ topics.push(topic);
328
+ }
580
329
  }
581
- return candidates
582
- .sort((a, b) => b.turnEndSeq - a.turnEndSeq || b.seq - a.seq)
583
- .slice(0, Math.max(1, Math.trunc(limit)));
330
+ return topics;
584
331
  }
585
332
  /** Find one resolved checkpoint entry by its stable ref, if it exists. */
586
333
  export function checkpointByRef(state, checkpointRef) {
587
334
  return state.checkpoints.find(entry => entry.checkpointRef === checkpointRef && entry.turnEndSeq !== -1);
588
335
  }
336
+ /** Whether one engine-held boundary is the recorded anchor of `ref` in this state's Session. */
337
+ export function boundaryByRef(state, ref) {
338
+ return state.boundaries.find(boundary => boundaryRefFor(state.sessionId, boundary.resultSeq) === ref);
339
+ }
340
+ /**
341
+ * The carried input one transition must deliver: unconsumed post-intent
342
+ * candidates in queue order, resolved to their durable message bodies. The
343
+ * engine keys carry candidates by message id (that is what the transition
344
+ * dedupes on); the `agent/inbox/spliced` events that recorded them are where
345
+ * the message itself lives, so the log stays the only authority.
346
+ */
347
+ export function carriedInputOf(state, events) {
348
+ const outstanding = state.carriedCandidates.filter(candidate => !candidate.consumed);
349
+ if (outstanding.length === 0)
350
+ return [];
351
+ const byId = new Map();
352
+ for (const event of events) {
353
+ if (event.type !== 'agent/inbox/spliced')
354
+ continue;
355
+ for (const message of event.data.inserted)
356
+ byId.set(message.id, message);
357
+ }
358
+ const carried = [];
359
+ for (const candidate of outstanding) {
360
+ const message = byId.get(candidate.messageId);
361
+ if (message !== undefined)
362
+ carried.push(message);
363
+ }
364
+ return carried;
365
+ }