@wowyuarm/dsh-agent-team 0.1.8 → 0.1.10

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