@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.
- package/README.md +4 -11
- package/README.zh.md +4 -11
- package/package.json +54 -44
- package/packages/agent-team/README.md +7 -5
- package/packages/agent-team/README.zh.md +7 -4
- package/packages/agent-team/lib/attachments.js +14 -4
- package/packages/agent-team/lib/context-management.js +382 -0
- package/packages/agent-team/lib/context-projection.js +578 -0
- package/packages/agent-team/lib/context-source.js +212 -0
- package/packages/agent-team/lib/index.js +1272 -76
- package/packages/agent-team/lib/ledger.js +333 -44
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-runtime.js +159 -4
- package/packages/agent-team/lib/member-time-context.js +126 -0
- package/packages/agent-team/lib/pressure-policy.js +199 -0
- package/packages/agent-team/lib/progress-nudge.js +12 -4
- package/packages/agent-team/lib/session-remediation.js +481 -0
- package/packages/agent-team/lib/spec.js +21 -2
- package/packages/agent-team/lib/time-format.js +56 -0
- package/packages/agent-team/lib/typert.host.js +168 -54
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +59 -25
- package/packages/agent-team/lib/types/attachments.d.ts +1 -1
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-management.d.ts +147 -0
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-source.d.ts +117 -0
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +300 -2
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +119 -6
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
- package/packages/agent-team/lib/types/session-remediation.d.ts +149 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/time-format.d.ts +28 -0
- package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
- package/packages/agent-team/lib/types/types/entities.d.ts +22 -1
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +58 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +27 -4
- package/packages/client-agent-team/lib/client.js +369 -352
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +2 -2
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +4 -4
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +44 -16
- package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -14
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +0 -14
- package/packages/client-agent-team/lib/types/client/slots.d.ts +6 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/tool-agent-team/README.md +11 -6
- package/packages/tool-agent-team/README.zh.md +11 -6
- package/packages/tool-agent-team/lib/context-tools.js +179 -0
- package/packages/tool-agent-team/lib/index.js +369 -58
- package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/auto-compaction.js +0 -201
- package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
- package/packages/agent-team/lib/types/auto-compaction.d.ts.map +0 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-management coordinator: the one deep module that turns a Member's
|
|
3
|
+
* successful `context_rollover` tool result into its next private context
|
|
4
|
+
* generation.
|
|
5
|
+
*
|
|
6
|
+
* Authority split (see docs/architecture.md):
|
|
7
|
+
* - the Team ledger owns the Member→Session binding and rollover audit;
|
|
8
|
+
* - the Session log projection owns intent, checkpoints, and delivery state;
|
|
9
|
+
* - this coordinator owns only process locks and is always reconstructible.
|
|
10
|
+
*
|
|
11
|
+
* The coordinator reacts exclusively after the successful `tool/result` is
|
|
12
|
+
* durably appended — never inside a tool body — so a render/finalize failure
|
|
13
|
+
* can never outrun result durability. The actual swap waits for the
|
|
14
|
+
* containing turn to end and the Agent to be idle, captures later input so no
|
|
15
|
+
* old-generation model request opens, and then reuses the Member lifecycle:
|
|
16
|
+
* commit rollover operation → dispose old Agent → archive old Session →
|
|
17
|
+
* create/activate the new generation → deliver the handoff first.
|
|
18
|
+
* @module @wowyuarm/dsh-agent-team/context-management
|
|
19
|
+
*/
|
|
20
|
+
import { SessionId as SessionIdBrand } from '@deepseek-ai/dsh-session';
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { AGENT_TEAM_PLUGIN_ID, createCheckpointContinuationMessage, createHandoffMessage, isAgentTeamContextSource } from "./context-source.js";
|
|
23
|
+
import { CONTEXT_CHECKPOINT_TOOL_NAME, CONTEXT_ROLLOVER_TOOL_NAME, continuationDelivered, foldContextProjection, } from "./context-projection.js";
|
|
24
|
+
/** Stable summary of the one-shot rollover pressure notice (ticket 03 wires delivery). */
|
|
25
|
+
export const CONTEXT_PRESSURE_NOTICE_SUMMARY = 'Context pressure: prepare a handoff';
|
|
26
|
+
export class ContextManagementCoordinator {
|
|
27
|
+
options;
|
|
28
|
+
members = new Map();
|
|
29
|
+
capturedInput = new Map();
|
|
30
|
+
/** Per-member latch for the in-process scheduling→delivery window. */
|
|
31
|
+
scheduledContinuations = new Set();
|
|
32
|
+
disposed = false;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
}
|
|
36
|
+
/** Whether one Member has a pending or in-flight rollover; tools use this to reject. */
|
|
37
|
+
isTransitioning(memberId) {
|
|
38
|
+
return this.members.has(memberId);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Root `session/event` observer for Member Sessions. The store's dispatch
|
|
42
|
+
* carrier is untagged, so the Host maps session ids to Members and calls
|
|
43
|
+
* this for every Member event. All reactions are gated on the projection
|
|
44
|
+
* state, which itself only records successful durable pairs.
|
|
45
|
+
*/
|
|
46
|
+
onSessionEvent(memberId, agent, event) {
|
|
47
|
+
if (this.disposed)
|
|
48
|
+
return;
|
|
49
|
+
if (event.type === 'tool/result') {
|
|
50
|
+
this.onToolResult(memberId, agent, event);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (event.type === 'user/message') {
|
|
54
|
+
this.onUserMessage(memberId, agent, event);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (event.type === 'turn/end') {
|
|
58
|
+
this.onTurnEnd(memberId, agent);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Build the first handoff message of one generation; the lifecycle delivers it. */
|
|
62
|
+
handoffMessageFor(plan) {
|
|
63
|
+
return createHandoffMessage({
|
|
64
|
+
handoff: plan.handoff,
|
|
65
|
+
previousSessionId: plan.previousSessionId,
|
|
66
|
+
newSessionId: plan.newSessionId,
|
|
67
|
+
trigger: plan.trigger,
|
|
68
|
+
handoffEventSeq: plan.handoffEventSeq,
|
|
69
|
+
...(plan.checkpointRef === undefined ? {} : { checkpointRef: plan.checkpointRef }),
|
|
70
|
+
...(plan.relatedFiles.length === 0 ? {} : { relatedFiles: plan.relatedFiles }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether one Agent's pending transition requires the admission gate: a
|
|
75
|
+
* pending rollover must stop old-generation turns from admitting queued
|
|
76
|
+
* input. The gate arms only for the old-generation Agent instance — the
|
|
77
|
+
* new generation activates mid-swap and must be free to consume the
|
|
78
|
+
* handoff and carried input immediately.
|
|
79
|
+
*/
|
|
80
|
+
needsAdmissionGate(agent) {
|
|
81
|
+
const memberId = this.options.memberForAgent(agent)?.memberId;
|
|
82
|
+
if (memberId === undefined)
|
|
83
|
+
return false;
|
|
84
|
+
return this.members.get(memberId)?.agent === agent;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Capture the inbox messages queued for an old generation at its turn-stop
|
|
88
|
+
* boundary: non-Team input is preserved verbatim for delivery after the
|
|
89
|
+
* handoff; stale Team notices are dropped because the new generation
|
|
90
|
+
* rederives the Inbox from ledger facts. Removing them from the inbox lets
|
|
91
|
+
* the turn close cleanly instead of admitting another old-generation step.
|
|
92
|
+
*/
|
|
93
|
+
captureQueuedInput(agent) {
|
|
94
|
+
const memberId = this.options.memberForAgent(agent)?.memberId;
|
|
95
|
+
if (memberId === undefined)
|
|
96
|
+
return [];
|
|
97
|
+
if (this.members.get(memberId)?.agent !== agent)
|
|
98
|
+
return [];
|
|
99
|
+
const removed = [...agent.inbox.nextStep, ...agent.inbox.nextTurn];
|
|
100
|
+
for (const message of removed)
|
|
101
|
+
agent.inbox.remove(message.id);
|
|
102
|
+
return this.captureInput(agent, removed);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Capture messages a racing pre-step already claimed from the inbox before
|
|
106
|
+
* rejecting that old-generation step. A rejected step's claimed message is
|
|
107
|
+
* otherwise neither discarded nor re-emitted, so the gate preserves it here.
|
|
108
|
+
*/
|
|
109
|
+
captureClaimedInput(agent, messages) {
|
|
110
|
+
return this.captureInput(agent, messages);
|
|
111
|
+
}
|
|
112
|
+
captureInput(agent, messages) {
|
|
113
|
+
const memberId = this.options.memberForAgent(agent)?.memberId;
|
|
114
|
+
if (memberId === undefined)
|
|
115
|
+
return [];
|
|
116
|
+
if (this.members.get(memberId)?.agent !== agent)
|
|
117
|
+
return [];
|
|
118
|
+
const preserved = [];
|
|
119
|
+
for (const message of messages) {
|
|
120
|
+
if (this.isTeamNotice(message))
|
|
121
|
+
continue;
|
|
122
|
+
preserved.push(message);
|
|
123
|
+
}
|
|
124
|
+
if (preserved.length > 0)
|
|
125
|
+
this.capturedInput.set(memberId, [...(this.capturedInput.get(memberId) ?? []), ...preserved]);
|
|
126
|
+
return preserved;
|
|
127
|
+
}
|
|
128
|
+
/** Drain the captured input of one Member for delivery after the handoff. */
|
|
129
|
+
drainCapturedInput(memberId) {
|
|
130
|
+
const captured = this.capturedInput.get(memberId) ?? [];
|
|
131
|
+
this.capturedInput.delete(memberId);
|
|
132
|
+
return captured;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether one queued message is a Team-owned notice the rederived Inbox
|
|
136
|
+
* replaces. Handoff and continuation envelopes carry the same plugin
|
|
137
|
+
* attribution but are ordinary delivered context the new generation keeps, so
|
|
138
|
+
* they are excluded rather than dropped.
|
|
139
|
+
*/
|
|
140
|
+
isTeamNotice(message) {
|
|
141
|
+
const source = message.source;
|
|
142
|
+
return source.kind === 'plugin' && source.plugin === AGENT_TEAM_PLUGIN_ID && !isAgentTeamContextSource(message);
|
|
143
|
+
}
|
|
144
|
+
/** Drop one Member's bookkeeping; the Host calls this on dispose/removal. */
|
|
145
|
+
stopTracking(memberId) {
|
|
146
|
+
this.members.delete(memberId);
|
|
147
|
+
this.capturedInput.delete(memberId);
|
|
148
|
+
}
|
|
149
|
+
dispose() {
|
|
150
|
+
this.disposed = true;
|
|
151
|
+
this.members.clear();
|
|
152
|
+
this.capturedInput.clear();
|
|
153
|
+
}
|
|
154
|
+
onToolResult(memberId, agent, event) {
|
|
155
|
+
if (event.data.error !== undefined)
|
|
156
|
+
return;
|
|
157
|
+
const member = this.options.memberForAgent(agent);
|
|
158
|
+
if (member === undefined)
|
|
159
|
+
return;
|
|
160
|
+
const state = this.options.projectionForMember(memberId, member.sessionId);
|
|
161
|
+
if (state?.pending === null || state === undefined)
|
|
162
|
+
return;
|
|
163
|
+
const pending = state.pending;
|
|
164
|
+
// React only to the intent's own result landing durably, and only once.
|
|
165
|
+
if (pending.resultSeq !== event.seq)
|
|
166
|
+
return;
|
|
167
|
+
if (this.members.has(memberId))
|
|
168
|
+
return;
|
|
169
|
+
this.members.set(memberId, {
|
|
170
|
+
agent,
|
|
171
|
+
intent: {
|
|
172
|
+
toolCallId: pending.toolCallId,
|
|
173
|
+
resultSeq: pending.resultSeq,
|
|
174
|
+
turn: pending.turn,
|
|
175
|
+
handoff: pending.handoff,
|
|
176
|
+
...(pending.checkpointRef === undefined ? {} : { checkpointRef: pending.checkpointRef }),
|
|
177
|
+
relatedFiles: pending.relatedFiles,
|
|
178
|
+
},
|
|
179
|
+
turnEnded: false,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
onUserMessage(memberId, _agent, event) {
|
|
183
|
+
// Quiet-continuation delivery bookkeeping is projection-owned; the
|
|
184
|
+
// coordinator only repairs a crash gap after restart, which the Host
|
|
185
|
+
// performs through repairContinuations during activation.
|
|
186
|
+
void memberId;
|
|
187
|
+
void event;
|
|
188
|
+
}
|
|
189
|
+
onTurnEnd(memberId, agent) {
|
|
190
|
+
// Checkpoint continuations first: a turn that resolved checkpoints owes
|
|
191
|
+
// each of them exactly one quiet next-turn follow-up. The projection's
|
|
192
|
+
// continuations state deduplicates across restarts; this in-memory latch
|
|
193
|
+
// closes the same-process window between scheduling and the delivery
|
|
194
|
+
// event landing in the log.
|
|
195
|
+
this.scheduleCheckpointContinuations(memberId, agent);
|
|
196
|
+
const transition = this.members.get(memberId);
|
|
197
|
+
if (transition === undefined || transition.turnEnded)
|
|
198
|
+
return;
|
|
199
|
+
const member = this.options.memberForAgent(agent);
|
|
200
|
+
if (member === undefined || transition.agent !== agent)
|
|
201
|
+
return;
|
|
202
|
+
transition.turnEnded = true;
|
|
203
|
+
// Wait for true idle (the turn-end event fires before the driver fully
|
|
204
|
+
// converges), then perform the swap off the session-event dispatch path.
|
|
205
|
+
void agent.whenIdle().then(() => {
|
|
206
|
+
if (this.disposed)
|
|
207
|
+
return;
|
|
208
|
+
const current = this.members.get(memberId);
|
|
209
|
+
if (current === undefined || current !== transition)
|
|
210
|
+
return;
|
|
211
|
+
if (this.options.agentForMember(memberId) !== agent)
|
|
212
|
+
return;
|
|
213
|
+
void this.performTransition(memberId, member, transition);
|
|
214
|
+
}, error => {
|
|
215
|
+
this.options.log(`context rollover idle wait failed: ${error instanceof Error ? error.message : String(error)} (member ${memberId})`);
|
|
216
|
+
this.members.delete(memberId);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Quiet follow-ups for checkpoints resolved by the turn that just ended.
|
|
221
|
+
* A successful `context_checkpoint` result concludes its turn; the Host
|
|
222
|
+
* continues work in the next turn with one host-generated notice. The
|
|
223
|
+
* projection folds delivery (the continuation user/message event), so a
|
|
224
|
+
* restart repairs a missing follow-up through repairContinuations without
|
|
225
|
+
* duplicating a delivered one; this live path latches per checkpoint in
|
|
226
|
+
* memory for the scheduling window.
|
|
227
|
+
*/
|
|
228
|
+
scheduleCheckpointContinuations(memberId, agent) {
|
|
229
|
+
const member = this.options.memberForAgent(agent);
|
|
230
|
+
if (member === undefined)
|
|
231
|
+
return;
|
|
232
|
+
const state = this.options.projectionForMember(memberId, member.sessionId);
|
|
233
|
+
if (state === undefined)
|
|
234
|
+
return;
|
|
235
|
+
for (const checkpoint of state.checkpoints) {
|
|
236
|
+
if (checkpoint.turnEndSeq === -1)
|
|
237
|
+
continue;
|
|
238
|
+
if (continuationDelivered(state, checkpoint.checkpointRef))
|
|
239
|
+
continue;
|
|
240
|
+
const latch = `${memberId}:${checkpoint.checkpointRef}`;
|
|
241
|
+
if (this.scheduledContinuations.has(latch))
|
|
242
|
+
continue;
|
|
243
|
+
this.scheduledContinuations.add(latch);
|
|
244
|
+
// The turn/end observer fires inside the session append publication
|
|
245
|
+
// (a synchronous followup would reenter the publishing append), and a
|
|
246
|
+
// next-turn message queued while the driver is still converging never
|
|
247
|
+
// latches a wake — the loop replays only maintenance/abort latches. So
|
|
248
|
+
// the continuation waits for true idle, then queues its own turn: the
|
|
249
|
+
// same discipline the rollover path uses.
|
|
250
|
+
void agent.whenIdle().then(() => {
|
|
251
|
+
if (this.disposed)
|
|
252
|
+
return;
|
|
253
|
+
try {
|
|
254
|
+
agent.followup(createCheckpointContinuationMessage(checkpoint.checkpointRef));
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
this.scheduledContinuations.delete(latch);
|
|
258
|
+
this.options.log(`context continuation scheduling failed: ${error instanceof Error ? error.message : String(error)} (member ${memberId})`);
|
|
259
|
+
}
|
|
260
|
+
}, () => {
|
|
261
|
+
this.scheduledContinuations.delete(latch);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async performTransition(memberId, member, transition) {
|
|
266
|
+
// Stable rollover identity derives from the previous Session and the
|
|
267
|
+
// successful tool call: a result seq is session-local and would collide
|
|
268
|
+
// across generations. Both inputs are unconstrained strings (provider
|
|
269
|
+
// call ids carry arbitrary lengths and characters; Session ids are
|
|
270
|
+
// caller-branded), so the pair is JSON-encoded — no delimiter can alias
|
|
271
|
+
// across the two fields — and hashed to a fixed-length url-safe hex
|
|
272
|
+
// digest instead of being embedded verbatim: the derived Session id and
|
|
273
|
+
// request id stay bounded and collision-resistant.
|
|
274
|
+
const stableKey = createHash('sha256').update(JSON.stringify([member.sessionId, transition.intent.toolCallId])).digest('hex');
|
|
275
|
+
const newSessionId = SessionIdBrand(`agent-team-rollover-${stableKey}`);
|
|
276
|
+
const plan = {
|
|
277
|
+
previousSessionId: member.sessionId,
|
|
278
|
+
newSessionId,
|
|
279
|
+
handoff: transition.intent.handoff,
|
|
280
|
+
handoffEventSeq: transition.intent.resultSeq,
|
|
281
|
+
trigger: 'model',
|
|
282
|
+
relatedFiles: transition.intent.relatedFiles,
|
|
283
|
+
...(transition.intent.checkpointRef === undefined ? {} : { checkpointRef: transition.intent.checkpointRef }),
|
|
284
|
+
requestId: `agent-team:rollover:${stableKey}`,
|
|
285
|
+
carriedInput: this.drainCapturedInput(memberId),
|
|
286
|
+
};
|
|
287
|
+
const swap = this.options.executeTransition(memberId, plan);
|
|
288
|
+
transition.swapping = swap;
|
|
289
|
+
try {
|
|
290
|
+
await swap;
|
|
291
|
+
this.members.delete(memberId);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
this.options.log(`context rollover failed, leaving the previous generation recoverable: ${error instanceof Error ? error.message : String(error)} (member ${memberId})`);
|
|
295
|
+
this.members.delete(memberId);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Crash-recovery hook the Host runs during Member activation: re-derive
|
|
300
|
+
* pending intent from the projection and finish a transition that a restart
|
|
301
|
+
* interrupted after the successful result was durable.
|
|
302
|
+
*/
|
|
303
|
+
recoverPendingTransition(memberId, agent, sessionId) {
|
|
304
|
+
if (this.disposed || this.members.has(memberId))
|
|
305
|
+
return;
|
|
306
|
+
const state = this.options.projectionForMember(memberId, sessionId);
|
|
307
|
+
if (state?.pending === null || state === undefined)
|
|
308
|
+
return;
|
|
309
|
+
const pending = state.pending;
|
|
310
|
+
if (pending.turnEndSeq === -1) {
|
|
311
|
+
// The containing turn never ended durably; treat the intent as still
|
|
312
|
+
// waiting and observe the live events from here.
|
|
313
|
+
this.members.set(memberId, {
|
|
314
|
+
agent,
|
|
315
|
+
intent: {
|
|
316
|
+
toolCallId: pending.toolCallId,
|
|
317
|
+
resultSeq: pending.resultSeq,
|
|
318
|
+
turn: pending.turn,
|
|
319
|
+
handoff: pending.handoff,
|
|
320
|
+
...(pending.checkpointRef === undefined ? {} : { checkpointRef: pending.checkpointRef }),
|
|
321
|
+
relatedFiles: pending.relatedFiles,
|
|
322
|
+
},
|
|
323
|
+
turnEnded: false,
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// The turn already ended before the crash; the Agent is idle at
|
|
328
|
+
// activation, so the swap can proceed directly.
|
|
329
|
+
const member = this.options.memberForAgent(agent);
|
|
330
|
+
if (member === undefined)
|
|
331
|
+
return;
|
|
332
|
+
const transition = {
|
|
333
|
+
agent,
|
|
334
|
+
intent: {
|
|
335
|
+
toolCallId: pending.toolCallId,
|
|
336
|
+
resultSeq: pending.resultSeq,
|
|
337
|
+
turn: pending.turn,
|
|
338
|
+
handoff: pending.handoff,
|
|
339
|
+
...(pending.checkpointRef === undefined ? {} : { checkpointRef: pending.checkpointRef }),
|
|
340
|
+
relatedFiles: pending.relatedFiles,
|
|
341
|
+
},
|
|
342
|
+
turnEnded: true,
|
|
343
|
+
};
|
|
344
|
+
this.members.set(memberId, transition);
|
|
345
|
+
void this.performTransition(memberId, member, transition);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Crash-recovery for quiet continuations: schedule the follow-up for one
|
|
349
|
+
* resolved checkpoint exactly once when the result was durable but the
|
|
350
|
+
* delivery never landed. The projection's continuations state is the
|
|
351
|
+
* durable delivery record — a checkpoint whose delivery event exists in the
|
|
352
|
+
* log is never re-scheduled.
|
|
353
|
+
*/
|
|
354
|
+
repairContinuations(agent, state) {
|
|
355
|
+
if (this.disposed)
|
|
356
|
+
return;
|
|
357
|
+
const memberId = this.options.memberForAgent(agent)?.memberId;
|
|
358
|
+
if (memberId === undefined)
|
|
359
|
+
return;
|
|
360
|
+
for (const checkpoint of state.checkpoints) {
|
|
361
|
+
if (checkpoint.turnEndSeq === -1)
|
|
362
|
+
continue;
|
|
363
|
+
if (continuationDelivered(state, checkpoint.checkpointRef))
|
|
364
|
+
continue;
|
|
365
|
+
const latch = `${memberId}:${checkpoint.checkpointRef}`;
|
|
366
|
+
if (this.scheduledContinuations.has(latch))
|
|
367
|
+
continue;
|
|
368
|
+
this.scheduledContinuations.add(latch);
|
|
369
|
+
try {
|
|
370
|
+
agent.followup(createCheckpointContinuationMessage(checkpoint.checkpointRef));
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
this.scheduledContinuations.delete(latch);
|
|
374
|
+
this.options.log(`context continuation repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** Tool names this module owns; the preset validation requires all of them. */
|
|
380
|
+
export const CONTEXT_TOOL_NAMES = Object.freeze([CONTEXT_CHECKPOINT_TOOL_NAME, CONTEXT_ROLLOVER_TOOL_NAME, 'context_timeline']);
|
|
381
|
+
/** Re-exported for Host wiring: cold-fold helper for archived ancestors. */
|
|
382
|
+
export { foldContextProjection };
|