@wowyuarm/dsh-agent-team 0.1.7 → 0.1.9
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/package.json +2 -1
- package/packages/agent-team/README.md +5 -3
- package/packages/agent-team/README.zh.md +5 -3
- package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +2 -0
- package/packages/agent-team/lib/attachments.js +14 -4
- package/packages/agent-team/lib/context-management.js +379 -0
- package/packages/agent-team/lib/context-projection.js +574 -0
- package/packages/agent-team/lib/context-source.js +72 -0
- package/packages/agent-team/lib/index.js +1258 -54
- package/packages/agent-team/lib/ledger.js +357 -26
- package/packages/agent-team/lib/member-context.js +7 -3
- package/packages/agent-team/lib/member-runtime.js +159 -4
- package/packages/agent-team/lib/pressure-policy.js +200 -0
- package/packages/agent-team/lib/progress-nudge.js +320 -0
- package/packages/agent-team/lib/spec.js +18 -0
- package/packages/agent-team/lib/typert.host.js +124 -42
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +39 -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 +142 -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 +83 -0
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +283 -2
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +120 -7
- 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/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 +136 -0
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/entities.d.ts +32 -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 +54 -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 +10 -2
- package/packages/client-agent-team/lib/client.js +283 -398
- 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/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +3 -7
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +3 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +37 -27
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +2 -2
- package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.js +3 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +5 -0
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +2 -2
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +4 -6
- package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts +14 -0
- package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamStateDot.js +15 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +1 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +41 -16
- 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 +36 -78
- package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -16
- 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 -16
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +1 -2
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts +7 -1
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/task-refs.js +25 -3
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +11 -10
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +14 -11
- package/packages/tool-agent-team/README.md +8 -5
- package/packages/tool-agent-team/README.zh.md +8 -5
- package/packages/tool-agent-team/lib/context-tools.js +179 -0
- package/packages/tool-agent-team/lib/index.js +144 -25
- 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
- package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts +0 -12
- package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberDock.js +0 -13
- package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +0 -20
- package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/member-session-input.js +0 -110
|
@@ -39,6 +39,58 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
39
39
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
40
40
|
done = true;
|
|
41
41
|
};
|
|
42
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
43
|
+
if (value !== null && value !== void 0) {
|
|
44
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
45
|
+
var dispose, inner;
|
|
46
|
+
if (async) {
|
|
47
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
48
|
+
dispose = value[Symbol.asyncDispose];
|
|
49
|
+
}
|
|
50
|
+
if (dispose === void 0) {
|
|
51
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
52
|
+
dispose = value[Symbol.dispose];
|
|
53
|
+
if (async) inner = dispose;
|
|
54
|
+
}
|
|
55
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
56
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
57
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
58
|
+
}
|
|
59
|
+
else if (async) {
|
|
60
|
+
env.stack.push({ async: true });
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
};
|
|
64
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
65
|
+
return function (env) {
|
|
66
|
+
function fail(e) {
|
|
67
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
68
|
+
env.hasError = true;
|
|
69
|
+
}
|
|
70
|
+
var r, s = 0;
|
|
71
|
+
function next() {
|
|
72
|
+
while (r = env.stack.pop()) {
|
|
73
|
+
try {
|
|
74
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
75
|
+
if (r.dispose) {
|
|
76
|
+
var result = r.dispose.call(r.value);
|
|
77
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
78
|
+
}
|
|
79
|
+
else s |= 1;
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
fail(e);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
86
|
+
if (env.hasError) throw env.error;
|
|
87
|
+
}
|
|
88
|
+
return next();
|
|
89
|
+
};
|
|
90
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
91
|
+
var e = new Error(message);
|
|
92
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
93
|
+
});
|
|
42
94
|
import { randomUUID } from 'node:crypto';
|
|
43
95
|
import { isDeepStrictEqual } from 'node:util';
|
|
44
96
|
import { Service } from '@deepseek-ai/cordis';
|
|
@@ -46,13 +98,17 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
|
46
98
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
47
99
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
48
100
|
import { scopeOf } from '@deepseek-ai/dsh-scope';
|
|
49
|
-
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
101
|
+
import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
50
102
|
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
|
|
51
103
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
52
104
|
import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
|
|
53
|
-
import {
|
|
105
|
+
import { PressurePolicyCoordinator } from "./pressure-policy.js";
|
|
106
|
+
import { ContextManagementCoordinator } from "./context-management.js";
|
|
107
|
+
import { createHandoffMessage } from "./context-source.js";
|
|
108
|
+
import { carriedInputOf, checkpointByRef, checkpointRefFor, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
|
|
54
109
|
import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
|
|
55
|
-
import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, MemberRuntime } from "./member-runtime.js";
|
|
110
|
+
import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, memberMemoryDirectoryName, MemberRuntime } from "./member-runtime.js";
|
|
111
|
+
import { ProgressNudgeCoordinator } from "./progress-nudge.js";
|
|
56
112
|
import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
|
|
57
113
|
import { agentTeamDomainSpec } from "./spec.js";
|
|
58
114
|
export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
|
|
@@ -64,6 +120,24 @@ const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
|
|
|
64
120
|
const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
|
|
65
121
|
const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
|
|
66
122
|
const ORPHANED_MEMBER_DIAGNOSTIC = 'Member preset composition was lost after a reload; its tools are unavailable. Resume rebuilds the member in place.';
|
|
123
|
+
/** Longest accepted model-supplied checkpoint display name. */
|
|
124
|
+
const MAX_CHECKPOINT_NAME_CHARS = 120;
|
|
125
|
+
/** Default and maximum number of timeline items one query returns. */
|
|
126
|
+
const DEFAULT_TIMELINE_LIMIT = 12;
|
|
127
|
+
const MAX_TIMELINE_LIMIT = 24;
|
|
128
|
+
/** Deepest ancestor lineage the timeline and seed resolution walk. */
|
|
129
|
+
const MAX_TIMELINE_ANCESTORS = 8;
|
|
130
|
+
/** Product pressure budget constants (see docs/team-collaboration.md). */
|
|
131
|
+
const CONTEXT_HARD_LIMIT_CAP = 256_000;
|
|
132
|
+
const CONTEXT_HANDOFF_AT_CAP = 200_000;
|
|
133
|
+
const CONTEXT_HANDOFF_RESERVE = 8_000;
|
|
134
|
+
const CONTEXT_SAFE_OUTPUT_RESERVE = 16_000;
|
|
135
|
+
/**
|
|
136
|
+
* Usage at or above which an acknowledged acceptance advises a fresh
|
|
137
|
+
* rollover instead of keeping the context; capped by the route's effective
|
|
138
|
+
* handoff budget so narrow routes get a proportionally earlier boundary.
|
|
139
|
+
*/
|
|
140
|
+
const ACCEPT_TASK_BOUNDARY_THRESHOLD = 128_000;
|
|
67
141
|
function sameChangeScope(left, right) {
|
|
68
142
|
if (left.kind === 'workspace' && right.kind === 'workspace')
|
|
69
143
|
return left.workspaceId === right.workspaceId;
|
|
@@ -189,6 +263,8 @@ let AgentTeam = (() => {
|
|
|
189
263
|
domain = __runInitializers(this, _instanceExtraInitializers);
|
|
190
264
|
ledger;
|
|
191
265
|
handles = new Map();
|
|
266
|
+
/** Live Member per session id; drives the root session/event listener. */
|
|
267
|
+
memberBySessionId = new Map();
|
|
192
268
|
/** Live selection refs let model edits take effect without disposing the Session. */
|
|
193
269
|
modelSelections = new Map();
|
|
194
270
|
/** Agent ids with a turn in flight; restarts must wait for the boundary. */
|
|
@@ -214,7 +290,7 @@ let AgentTeam = (() => {
|
|
|
214
290
|
* stale keys.
|
|
215
291
|
*/
|
|
216
292
|
memberFailures = new Map();
|
|
217
|
-
|
|
293
|
+
pressurePolicy;
|
|
218
294
|
notifiedInbox = new Map();
|
|
219
295
|
attachmentGcTimer;
|
|
220
296
|
recovery = new RecoveryCoordinator({
|
|
@@ -226,34 +302,62 @@ let AgentTeam = (() => {
|
|
|
226
302
|
this.ctx.logger.warn(`agent-team: member '${this.memberLabel(memberId)}' reached ${consecutiveFailures}/${RECOVERY_MAX_CONSECUTIVE_ERRORS} consecutive recoverable failures; leaving it in error for the operator`);
|
|
227
303
|
},
|
|
228
304
|
});
|
|
305
|
+
/**
|
|
306
|
+
* Advisory progress-visibility nudges. Purely in-process: the ledger owns
|
|
307
|
+
* eligibility, this coordinator owns counting and notices, and neither ever
|
|
308
|
+
* writes Team facts. See docs/team-collaboration.md for the regimes.
|
|
309
|
+
*/
|
|
310
|
+
progressNudge = new ProgressNudgeCoordinator({
|
|
311
|
+
agentForMember: memberId => this.handles.get(memberId)?.agent,
|
|
312
|
+
targetsForMember: memberId => this.requireLedger().progressNudgeTargets(memberId),
|
|
313
|
+
sessionLogForMember: (memberId, sessionId) => {
|
|
314
|
+
const handle = this.handles.get(memberId);
|
|
315
|
+
if (handle === undefined || handle.agent.session.id !== sessionId)
|
|
316
|
+
return undefined;
|
|
317
|
+
return { events: handle.agent.session.ownEvents() };
|
|
318
|
+
},
|
|
319
|
+
log: message => { this.ctx.logger.warn(message); },
|
|
320
|
+
});
|
|
321
|
+
/**
|
|
322
|
+
* Context self-management: the one deep module that turns a Member's
|
|
323
|
+
* successful `context_rollover` tool result into its next private context
|
|
324
|
+
* generation. The ledger owns the binding audit, the Session projection
|
|
325
|
+
* owns intent, and this coordinator owns only reconstructible process
|
|
326
|
+
* state. See docs/architecture.md and docs/team-collaboration.md.
|
|
327
|
+
*/
|
|
328
|
+
contextManagement = new ContextManagementCoordinator({
|
|
329
|
+
agentForMember: memberId => this.handles.get(memberId)?.agent,
|
|
330
|
+
memberForAgent: agent => this.memberForAgent(agent),
|
|
331
|
+
projectionForMember: (memberId, sessionId) => {
|
|
332
|
+
const handle = this.handles.get(memberId);
|
|
333
|
+
if (handle === undefined || handle.agent.session.id !== sessionId)
|
|
334
|
+
return undefined;
|
|
335
|
+
return foldContextProjection(handle.agent.session.ownEvents(), handle.agent.session.inheritedEventCount, handle.agent.session.id);
|
|
336
|
+
},
|
|
337
|
+
executeTransition: (memberId, plan) => this.executeMemberTransition(memberId, plan),
|
|
338
|
+
log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
|
|
339
|
+
});
|
|
229
340
|
lifecycleTail = Promise.resolve();
|
|
230
341
|
accepting = true;
|
|
231
342
|
changeVersion = 0;
|
|
232
343
|
changeWaiters = new Set();
|
|
233
344
|
constructor(ctx) {
|
|
234
345
|
super(ctx, 'agentTeam');
|
|
235
|
-
this.
|
|
346
|
+
this.pressurePolicy = new PressurePolicyCoordinator({
|
|
236
347
|
agentForMember: memberId => this.handles.get(memberId)?.agent,
|
|
348
|
+
memberForAgent: agent => {
|
|
349
|
+
const member = this.memberForAgent(agent);
|
|
350
|
+
return member === undefined ? undefined : { memberId: member.memberId, sessionId: agent.id };
|
|
351
|
+
},
|
|
237
352
|
compactionForAgent: agent => this.ctx.agentPresets.serviceFor(agent, 'compaction'),
|
|
238
|
-
|
|
353
|
+
limitsForAgent: agent => this.routeLimitsForAgent(agent),
|
|
354
|
+
activeClaimLabels: memberId => this.activeClaimLabels(memberId),
|
|
355
|
+
runningJobLabels: memberId => this.runningJobLabels(memberId),
|
|
239
356
|
failed: (memberId, _sessionId, diagnostic) => {
|
|
240
357
|
this.setMemberFailure(memberId, 'compaction', diagnostic);
|
|
241
358
|
this.emitAutoCompactionChanged(memberId);
|
|
242
359
|
},
|
|
243
|
-
cleared: memberId => {
|
|
244
|
-
if (!this.clearMemberFailure(memberId, 'compaction'))
|
|
245
|
-
return;
|
|
246
|
-
this.emitAutoCompactionChanged(memberId);
|
|
247
|
-
},
|
|
248
360
|
log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
|
|
249
|
-
// Advisory only: the Agent decides whether anything is worth persisting.
|
|
250
|
-
steerPreCompaction: agent => {
|
|
251
|
-
const hint = createUserMessage({
|
|
252
|
-
content: [{ type: 'text', text: preCompactionNoticeText() }],
|
|
253
|
-
source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: PRE_COMPACTION_NOTICE_SUMMARY },
|
|
254
|
-
});
|
|
255
|
-
agent.steer(hint);
|
|
256
|
-
},
|
|
257
361
|
});
|
|
258
362
|
}
|
|
259
363
|
/** Open the durable ledger and restore every enabled Member independently. */
|
|
@@ -281,7 +385,12 @@ let AgentTeam = (() => {
|
|
|
281
385
|
if (recovered)
|
|
282
386
|
this.notifiedInbox.delete(member.memberId);
|
|
283
387
|
this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
|
|
284
|
-
|
|
388
|
+
// A rollover/recovery in flight delivers its own sequenced
|
|
389
|
+
// rederived Inbox after the handoff and carried input; a status-driven
|
|
390
|
+
// steer here would claim the handoff turn's next step and leapfrog
|
|
391
|
+
// the carried messages.
|
|
392
|
+
if (!this.contextManagement.isTransitioning(member.memberId))
|
|
393
|
+
this.notifyMember(agent);
|
|
285
394
|
}
|
|
286
395
|
// A turn that ends without an error closes any automatic recovery episode.
|
|
287
396
|
// The idle transition is itself presence-affecting (working → available),
|
|
@@ -295,11 +404,37 @@ let AgentTeam = (() => {
|
|
|
295
404
|
this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
|
|
296
405
|
}
|
|
297
406
|
});
|
|
407
|
+
// Progress nudges count every `tool/call` of each Member Session. The
|
|
408
|
+
// store's dispatch carrier is untagged, so a scope-tagged listener inside
|
|
409
|
+
// the Agent setup would receive nothing; one root listener that maps the
|
|
410
|
+
// session id back to its Member is the seam that works (same shape as the
|
|
411
|
+
// `agent/status` listener above). The first turn of a freshly published
|
|
412
|
+
// Member cannot race this: `handles.set()` precedes the same activation
|
|
413
|
+
// continuation that publishes the Agent, so by the time any tool call
|
|
414
|
+
// streams, the map lookup succeeds.
|
|
415
|
+
this.ctx.on('session/event', (session, event) => {
|
|
416
|
+
const memberId = this.memberBySessionId.get(session.id);
|
|
417
|
+
if (memberId === undefined)
|
|
418
|
+
return;
|
|
419
|
+
const handle = this.handles.get(memberId);
|
|
420
|
+
if (handle === undefined || handle.agent.session.id !== session.id)
|
|
421
|
+
return;
|
|
422
|
+
this.progressNudge.onSessionEvent(memberId, session.id, handle.agent, event);
|
|
423
|
+
// A successful assistant response ends any open provider-overflow
|
|
424
|
+
// recovery sequence for this Member.
|
|
425
|
+
if (event.type === 'assistant/message')
|
|
426
|
+
this.pressurePolicy.onAssistantMessage(handle.agent);
|
|
427
|
+
// Context management reacts only after a successful durable tool/result;
|
|
428
|
+
// the projection (not this listener) decides what that means.
|
|
429
|
+
this.contextManagement.onSessionEvent(memberId, handle.agent, event);
|
|
430
|
+
});
|
|
298
431
|
const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
|
|
299
432
|
this.ctx.effect(() => async () => {
|
|
300
433
|
this.accepting = false;
|
|
301
434
|
this.recovery.dispose();
|
|
302
|
-
|
|
435
|
+
this.progressNudge.dispose();
|
|
436
|
+
this.contextManagement.dispose();
|
|
437
|
+
this.pressurePolicy.dispose();
|
|
303
438
|
if (this.attachmentGcTimer !== undefined)
|
|
304
439
|
clearInterval(this.attachmentGcTimer);
|
|
305
440
|
this.attachmentGcTimer = undefined;
|
|
@@ -321,7 +456,7 @@ let AgentTeam = (() => {
|
|
|
321
456
|
this.startAttachmentGc(ledger);
|
|
322
457
|
// One metadata listing serves every Member restore; per-member list calls
|
|
323
458
|
// would repeat the same I/O linearly during startup.
|
|
324
|
-
const persistedSessions = new Set((await this.
|
|
459
|
+
const persistedSessions = new Set((await this.persistedSessionHeaders()).map(header => header.id));
|
|
325
460
|
for (const member of ledger.listMembers()) {
|
|
326
461
|
if (member.state === 'enabled')
|
|
327
462
|
await this.activateMember(member, undefined, persistedSessions);
|
|
@@ -329,6 +464,23 @@ let AgentTeam = (() => {
|
|
|
329
464
|
await this.memberRuntime.cleanupRemovedMember(member);
|
|
330
465
|
}
|
|
331
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* Whether one Member Session has durable persisted content, decided through
|
|
469
|
+
* {@link SessionPersistenceService.inspect} rather than a bare metadata
|
|
470
|
+
* listing: inspection first awaits any in-flight retirement drain for the
|
|
471
|
+
* id, so a resume racing a suspend's fire-and-forget final flush cannot
|
|
472
|
+
* mistake a still-draining persisted Session for an unpersisted one and
|
|
473
|
+
* fork a fresh generation over it.
|
|
474
|
+
*/
|
|
475
|
+
async sessionPersisted(sessionId) {
|
|
476
|
+
try {
|
|
477
|
+
await this.ctx.sessionPersistence.inspect(sessionId);
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
332
484
|
/** Resolve one exact live Agent to its durable Team Member; forks do not inherit identity. */
|
|
333
485
|
memberForAgent(agent) {
|
|
334
486
|
for (const [memberId, handle] of this.handles) {
|
|
@@ -456,7 +608,7 @@ let AgentTeam = (() => {
|
|
|
456
608
|
presetId: request.presetId,
|
|
457
609
|
...(request.model === undefined ? {} : { model: Object.freeze({ ...request.model }) }),
|
|
458
610
|
...(request.capabilities === undefined ? {} : { capabilities: Object.freeze(deepCopyCapabilities(request.capabilities)) }),
|
|
459
|
-
privateMemoryPath: dshHomePath('agent-team', 'members', memberId),
|
|
611
|
+
privateMemoryPath: dshHomePath('agent-team', 'members', memberMemoryDirectoryName(memberId)),
|
|
460
612
|
state: 'enabled',
|
|
461
613
|
});
|
|
462
614
|
const result = await this.requireLedger().addMember({ ...request, actor: agentTeamHumanActor(), member });
|
|
@@ -485,7 +637,13 @@ let AgentTeam = (() => {
|
|
|
485
637
|
if (result.committed)
|
|
486
638
|
this.emitCommitted(result.value.receipt);
|
|
487
639
|
this.clearMemberNotificationState(result.value.member.memberId);
|
|
488
|
-
|
|
640
|
+
// The suspended Session's log is durable once its retirement completes,
|
|
641
|
+
// and agents.resume() waits for exactly that retirement before loading.
|
|
642
|
+
// Consulting the persistence tree here instead would race the
|
|
643
|
+
// fire-and-forget retirement on Windows, where the JSONL backend
|
|
644
|
+
// publishes directories through transient staging entries that surface
|
|
645
|
+
// as ENOENT mid-walk — so pass the known session rather than re-listing.
|
|
646
|
+
await this.activateMember(result.value.member, undefined, new Set([result.value.member.sessionId]));
|
|
489
647
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
|
|
490
648
|
});
|
|
491
649
|
}
|
|
@@ -563,18 +721,7 @@ let AgentTeam = (() => {
|
|
|
563
721
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
|
|
564
722
|
}
|
|
565
723
|
const renewed = result.value.member;
|
|
566
|
-
|
|
567
|
-
// error markers belong to the disposed agent, not to the Member.
|
|
568
|
-
this.recovery.stopTracking(request.memberId);
|
|
569
|
-
await active.dispose();
|
|
570
|
-
this.handles.delete(request.memberId);
|
|
571
|
-
this.modelSelections.delete(request.memberId);
|
|
572
|
-
this.memberRuntime.forgetMember(request.memberId);
|
|
573
|
-
this.clearMemberFailure(request.memberId, 'activation');
|
|
574
|
-
this.clearMemberNotificationState(request.memberId);
|
|
575
|
-
// The previous log survives on disk; archiving hides it from every
|
|
576
|
-
// grouping surface so one Member keeps exactly one visible Session.
|
|
577
|
-
await this.ctx.workspaceRegistry.archiveSession(previousSessionId);
|
|
724
|
+
await this.retireMemberGeneration(request.memberId, active, previousSessionId);
|
|
578
725
|
await this.activateMember(renewed, undefined, undefined, previousSessionId);
|
|
579
726
|
const reactivated = this.handles.get(request.memberId);
|
|
580
727
|
if (reactivated === undefined) {
|
|
@@ -585,6 +732,126 @@ let AgentTeam = (() => {
|
|
|
585
732
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(renewed) });
|
|
586
733
|
});
|
|
587
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Retire one Member's previous generation after its durable binding moved
|
|
737
|
+
* onto a new Session id: drop the old handle's transient state, dispose the
|
|
738
|
+
* Agent, and archive the old Session log (which stays on disk for history).
|
|
739
|
+
* Shared by the Human clear path and the model-initiated rollover.
|
|
740
|
+
*/
|
|
741
|
+
async retireMemberGeneration(memberId, active, previousSessionId) {
|
|
742
|
+
// Drop the old handle's transient state: pending recovery episodes and
|
|
743
|
+
// error markers belong to the disposed agent, not to the Member.
|
|
744
|
+
this.recovery.stopTracking(memberId);
|
|
745
|
+
// The fresh Session may re-earn one Claim suggestion per Thread; the
|
|
746
|
+
// old Session's one-shot records must not leak into it.
|
|
747
|
+
this.progressNudge.stopTracking(memberId);
|
|
748
|
+
// The context admission gate stays armed through disposal: input racing
|
|
749
|
+
// the retire window must still be captured for the new generation, and
|
|
750
|
+
// the coordinator drops its own bookkeeping only after the swap settles.
|
|
751
|
+
this.memberBySessionId.delete(previousSessionId);
|
|
752
|
+
await active.dispose();
|
|
753
|
+
this.handles.delete(memberId);
|
|
754
|
+
this.modelSelections.delete(memberId);
|
|
755
|
+
this.memberRuntime.forgetMember(memberId);
|
|
756
|
+
this.clearMemberFailure(memberId, 'activation');
|
|
757
|
+
this.clearMemberNotificationState(memberId);
|
|
758
|
+
// The previous log survives on disk; archiving hides it from every
|
|
759
|
+
// grouping surface so one Member keeps exactly one visible Session.
|
|
760
|
+
await this.ctx.workspaceRegistry.archiveSession(previousSessionId);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Execute one prepared context rollover at a true idle boundary: commit the
|
|
764
|
+
* idempotent Member-actor operation, retire the previous generation, and
|
|
765
|
+
* activate the fresh Session whose first model-facing context is the
|
|
766
|
+
* Member's own handoff. Later non-Team input captured during the transition
|
|
767
|
+
* is delivered after the handoff; the Team Inbox is rederived from the
|
|
768
|
+
* ledger, never copied.
|
|
769
|
+
*/
|
|
770
|
+
async executeMemberTransition(memberId, plan) {
|
|
771
|
+
await this.enqueueLifecycle(async () => {
|
|
772
|
+
this.requireAccepting();
|
|
773
|
+
const stored = this.requireLedger().getMember(memberId);
|
|
774
|
+
if (stored === undefined || stored.state !== 'enabled')
|
|
775
|
+
throw new Error(`Agent Member '${memberId}' cannot roll over: not enabled`);
|
|
776
|
+
if (stored.sessionId !== plan.previousSessionId)
|
|
777
|
+
throw new Error(`Agent Member '${stored.handle}' is no longer bound to the rolled-over Session`);
|
|
778
|
+
const active = this.handles.get(memberId);
|
|
779
|
+
if (active === undefined)
|
|
780
|
+
throw new Error(`Agent Member '${stored.handle}' has no active session to roll over`);
|
|
781
|
+
// A racing turn the admission gate rejects still leaves the Agent
|
|
782
|
+
// momentarily running; wait for its convergence instead of failing the
|
|
783
|
+
// swap — the gate guarantees it spends no model request.
|
|
784
|
+
if (this.runningAgents.has(active.agent.id))
|
|
785
|
+
await active.agent.whenIdle();
|
|
786
|
+
if (this.runningAgents.has(active.agent.id))
|
|
787
|
+
throw new Error(`Agent Member '${stored.handle}' is still running; the rollover must wait for idle`);
|
|
788
|
+
// Checkpoint return: resolve the seed before committing anything. A
|
|
789
|
+
// violation found here fails the whole swap with the old generation
|
|
790
|
+
// intact — never a guessed seed over a wrong prefix.
|
|
791
|
+
const seed = plan.checkpointRef === undefined ? undefined : await this.resolveCheckpointSeed(memberId, active.agent, plan.checkpointRef);
|
|
792
|
+
// Recheck the job guard at the lifecycle commit seam: a job may have
|
|
793
|
+
// started or settled after the tool-time validation.
|
|
794
|
+
const blockingJobs = this.ownedJobsBlockingRollover(active.agent);
|
|
795
|
+
if (blockingJobs.length > 0) {
|
|
796
|
+
throw new Error(`the context rollover is refused: this Member now owns jobs that would not survive the switch (${blockingJobs.join(', ')}); collect or stop them, then retry`);
|
|
797
|
+
}
|
|
798
|
+
const rolled = await this.rolloverSessionForAgent(active.agent, {
|
|
799
|
+
requestId: plan.requestId,
|
|
800
|
+
workspaceId: stored.workspaceId,
|
|
801
|
+
memberId,
|
|
802
|
+
previousSessionId: plan.previousSessionId,
|
|
803
|
+
newSessionId: plan.newSessionId,
|
|
804
|
+
handoffEventSeq: plan.handoffEventSeq,
|
|
805
|
+
trigger: plan.trigger,
|
|
806
|
+
...(seed === undefined ? {} : { checkpointRef: seed.checkpointRef, sourceSessionId: seed.sourceSessionId, sourceThroughSeq: seed.sourceThroughSeq }),
|
|
807
|
+
});
|
|
808
|
+
// The durable old-log projection is the carried-input truth: after the
|
|
809
|
+
// old Agent retires, fold its final state and take every post-intent
|
|
810
|
+
// non-Team candidate the old generation never answered. The process
|
|
811
|
+
// capture only accelerates; it is unioned by message id, never allowed
|
|
812
|
+
// to override the fold.
|
|
813
|
+
const preRetireCapture = this.contextManagement.drainCapturedInput(memberId);
|
|
814
|
+
await this.retireMemberGeneration(memberId, active, plan.previousSessionId);
|
|
815
|
+
const finalState = foldContextProjection(active.agent.session.ownEvents(), active.agent.session.inheritedEventCount, active.agent.session.id);
|
|
816
|
+
const foldedCarried = carriedInputOf(finalState);
|
|
817
|
+
const carriedById = new Map(plan.carriedInput.map(message => [message.id, message]));
|
|
818
|
+
for (const message of foldedCarried)
|
|
819
|
+
carriedById.set(message.id, message);
|
|
820
|
+
for (const message of preRetireCapture)
|
|
821
|
+
if (!carriedById.has(message.id))
|
|
822
|
+
carriedById.set(message.id, message);
|
|
823
|
+
for (const message of this.contextManagement.drainCapturedInput(memberId))
|
|
824
|
+
if (!carriedById.has(message.id))
|
|
825
|
+
carriedById.set(message.id, message);
|
|
826
|
+
const carriedInput = [...carriedById.values()];
|
|
827
|
+
// A fresh rollover seeds nothing and points the lineage parent at the
|
|
828
|
+
// previous active Session; a checkpoint return seeds the resolved
|
|
829
|
+
// prefix and parents at the seed source Session instead. Activation
|
|
830
|
+
// defers the ordinary Inbox wake so the handoff is guaranteed to be the
|
|
831
|
+
// new generation's first model-facing context.
|
|
832
|
+
const seedEvents = seed === undefined ? undefined : seed.prefix;
|
|
833
|
+
await this.activateMember(rolled.member, undefined, undefined, seed === undefined ? plan.previousSessionId : seed.sourceSessionId, {
|
|
834
|
+
deferNotify: true,
|
|
835
|
+
// The prefix is contiguous from seq 0, so its length is exactly the
|
|
836
|
+
// inherited cut the child folds past.
|
|
837
|
+
...(seedEvents === undefined ? {} : { seed: seedEvents, inheritedEventCount: SessionLogOffset(seedEvents.length) }),
|
|
838
|
+
});
|
|
839
|
+
const reactivated = this.handles.get(memberId);
|
|
840
|
+
if (reactivated === undefined) {
|
|
841
|
+
throw new Error(`Agent Member '${stored.handle}' failed to activate its next context: ${this.memberFailures.get(memberId)?.activation ?? 'unknown error'}`);
|
|
842
|
+
}
|
|
843
|
+
// The handoff is the first model-facing context of the new generation.
|
|
844
|
+
// It rides the step-priority inbox lane (steer) so a later rederived
|
|
845
|
+
// Inbox notice queues behind it instead of preempting it; carried input
|
|
846
|
+
// follows as its own turn, and the Inbox is rederived from ledger facts.
|
|
847
|
+
reactivated.agent.steer(this.contextManagement.handoffMessageFor(plan));
|
|
848
|
+
for (const message of carriedInput)
|
|
849
|
+
reactivated.agent.followup(message);
|
|
850
|
+
const notifications = this.requireLedger().notificationFacts(memberId, { workspaceId: rolled.member.workspaceId });
|
|
851
|
+
if (notifications.length > 0)
|
|
852
|
+
this.notifyMember(reactivated.agent, carriedInput.length > 0);
|
|
853
|
+
});
|
|
854
|
+
}
|
|
588
855
|
/** Ledger handle for log lines; falls back to the raw id when unknown. */
|
|
589
856
|
memberLabel(memberId) {
|
|
590
857
|
return this.ledger?.getMember(memberId)?.handle ?? memberId;
|
|
@@ -628,6 +895,9 @@ let AgentTeam = (() => {
|
|
|
628
895
|
if (this.isInboxNotice(pending))
|
|
629
896
|
handle.agent.inbox.remove(pending.id);
|
|
630
897
|
}
|
|
898
|
+
// Recovery carries the concrete interrupted work; a queued progress nudge
|
|
899
|
+
// is stale next to it and must not survive as a second reminder.
|
|
900
|
+
this.progressNudge.revokePendingNotice(member.memberId);
|
|
631
901
|
handle.agent.steer(hint);
|
|
632
902
|
}
|
|
633
903
|
/** Automatic-recovery wakeup; throwing tells the coordinator the target is gone. */
|
|
@@ -895,7 +1165,60 @@ let AgentTeam = (() => {
|
|
|
895
1165
|
const result = await this.requireLedger().readThread({ ...request, actor });
|
|
896
1166
|
if (result.committed)
|
|
897
1167
|
this.emitCommitted(result.value.receipt);
|
|
898
|
-
|
|
1168
|
+
const value = result.value;
|
|
1169
|
+
// Private read-time enrich: an acceptance the reader just acknowledged is
|
|
1170
|
+
// a natural Task boundary, so the Host prices the reader's context once,
|
|
1171
|
+
// after the durable read has committed. Never a ledger fact, never
|
|
1172
|
+
// persisted, and a measurement failure degrades to an explicit
|
|
1173
|
+
// `unavailable` — the committed read is never reversed.
|
|
1174
|
+
const advice = await this.acceptanceContextAdvice(agent, value);
|
|
1175
|
+
return advice === undefined ? value : Object.freeze({ ...value, contextAdvice: advice });
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Context advice for one acceptance acknowledged by this read: only when
|
|
1179
|
+
* the read carried an unread accept activity AND the Task is still done.
|
|
1180
|
+
* Repeat reads (nothing unread), history-style reads, and reopened Tasks
|
|
1181
|
+
* carry no advice — the acceptance no longer stands.
|
|
1182
|
+
*/
|
|
1183
|
+
async acceptanceContextAdvice(agent, read) {
|
|
1184
|
+
if (read.task === undefined || read.task.resolution !== 'accepted' || read.task.status !== 'done')
|
|
1185
|
+
return undefined;
|
|
1186
|
+
const acknowledgedAccept = read.facts.some(entry => entry.unread && entry.fact.kind === 'activity'
|
|
1187
|
+
&& entry.fact.activity.kind === 'accept');
|
|
1188
|
+
if (!acknowledgedAccept)
|
|
1189
|
+
return undefined;
|
|
1190
|
+
return this.contextAdviceFor(agent);
|
|
1191
|
+
}
|
|
1192
|
+
/** Price the reading Member's context against its current route's budgets. */
|
|
1193
|
+
async contextAdviceFor(agent) {
|
|
1194
|
+
try {
|
|
1195
|
+
const limits = await this.routeLimitsForAgent(agent);
|
|
1196
|
+
if (limits === undefined)
|
|
1197
|
+
return this.unavailableAdvice();
|
|
1198
|
+
const taskBoundaryThreshold = Math.min(ACCEPT_TASK_BOUNDARY_THRESHOLD, limits.handoffAt);
|
|
1199
|
+
if (limits.usageTokens >= limits.handoffAt) {
|
|
1200
|
+
return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
|
|
1201
|
+
action: 'handoff-now',
|
|
1202
|
+
guidance: 'You are at or above the handoff budget. Finish the current atomic action and unsettled evidence, then call context_rollover with a fresh handoff now.' });
|
|
1203
|
+
}
|
|
1204
|
+
if (limits.usageTokens >= taskBoundaryThreshold) {
|
|
1205
|
+
return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
|
|
1206
|
+
action: 'rollover',
|
|
1207
|
+
guidance: 'Finish the acceptance closeout, persist only durable reusable conclusions, collect or stop jobs, then call context_rollover with a fresh handoff covering every other active Claim. Do not return to an old checkpoint solely because this Task was accepted.' });
|
|
1208
|
+
}
|
|
1209
|
+
return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
|
|
1210
|
+
action: 'keep',
|
|
1211
|
+
guidance: 'Keep the current context for possible acceptance follow-up. This acceptance is already a timeline boundary; do not create a redundant checkpoint. Record a checkpoint only before the next noisy or risky phase.' });
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
// Any measurement failure degrades explicitly; the read stays durable.
|
|
1215
|
+
return this.unavailableAdvice();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
unavailableAdvice() {
|
|
1219
|
+
return Object.freeze({ usageTokens: undefined, taskBoundaryThreshold: undefined, handoffAt: undefined, hardLimit: undefined,
|
|
1220
|
+
action: 'unavailable',
|
|
1221
|
+
guidance: 'Context usage could not be measured for this acceptance; manage context by your existing pressure policy.' });
|
|
899
1222
|
}
|
|
900
1223
|
/**
|
|
901
1224
|
* Agent-only direct message: append the audit-only dm-sent operation, then
|
|
@@ -958,6 +1281,663 @@ let AgentTeam = (() => {
|
|
|
958
1281
|
validateLedger() {
|
|
959
1282
|
this.requireLedger().validate();
|
|
960
1283
|
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Effective context-pressure budget for one Member's current route:
|
|
1286
|
+
* `hardLimit = min(256K, routeWindow - outputReserve)` and
|
|
1287
|
+
* `handoffAt = min(200K, hardLimit - handoffReserve)`.
|
|
1288
|
+
*/
|
|
1289
|
+
contextLimits(routeWindow) {
|
|
1290
|
+
const hardLimit = Math.min(CONTEXT_HARD_LIMIT_CAP, Math.max(0, routeWindow - CONTEXT_SAFE_OUTPUT_RESERVE));
|
|
1291
|
+
const handoffAt = Math.min(CONTEXT_HANDOFF_AT_CAP, Math.max(0, hardLimit - CONTEXT_HANDOFF_RESERVE));
|
|
1292
|
+
return { hardLimit, handoffAt };
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Budget + measurement for one Member's CURRENT routed selection — the
|
|
1296
|
+
* member-pinned model or the default selection, resolved through the LLM
|
|
1297
|
+
* service so a route change is honored at the next pre-step. The last
|
|
1298
|
+
* persisted `request/context` event wins when it matches the current
|
|
1299
|
+
* selection (no directory round-trip for an unchanged route). A selection
|
|
1300
|
+
* whose capacity cannot be resolved returns `undefined` and the pressure
|
|
1301
|
+
* policy fails closed instead of assuming an unbounded route.
|
|
1302
|
+
*/
|
|
1303
|
+
async routeLimitsForAgent(agent) {
|
|
1304
|
+
const member = this.memberForAgent(agent);
|
|
1305
|
+
if (member === undefined)
|
|
1306
|
+
return undefined;
|
|
1307
|
+
// The pressure budget must match the route the CURRENT step actually
|
|
1308
|
+
// uses: the live selection ref's `assembled` capture when the step has
|
|
1309
|
+
// entered prompt assembly, its `current` selection otherwise. Re-deriving
|
|
1310
|
+
// from the ledger or the Host default would race a concurrent default
|
|
1311
|
+
// change or live model edit and split the budget from the real route.
|
|
1312
|
+
const ref = this.modelSelections.get(member.memberId);
|
|
1313
|
+
const selection = ref?.assembled ?? ref?.current ?? member.model ?? this.ctx.agentDefaultModel.currentSelection();
|
|
1314
|
+
const meter = agent.ctx.get('tokenMeter');
|
|
1315
|
+
const usageTokens = meter?.measure(agent.session)?.totalTokens;
|
|
1316
|
+
if (usageTokens === undefined)
|
|
1317
|
+
return undefined;
|
|
1318
|
+
const persisted = agent.session.requestContext();
|
|
1319
|
+
let contextWindow;
|
|
1320
|
+
if (persisted?.provider === selection.provider && persisted.model === selection.model) {
|
|
1321
|
+
contextWindow = persisted.contextWindow;
|
|
1322
|
+
}
|
|
1323
|
+
if (contextWindow === undefined) {
|
|
1324
|
+
contextWindow = await this.resolvedContextWindow(selection);
|
|
1325
|
+
}
|
|
1326
|
+
if (contextWindow === undefined)
|
|
1327
|
+
return undefined;
|
|
1328
|
+
const { hardLimit, handoffAt } = this.contextLimits(contextWindow);
|
|
1329
|
+
return { usageTokens, hardLimit, handoffAt };
|
|
1330
|
+
}
|
|
1331
|
+
/** Cache of resolved context windows per provider/model; unknown stays unknown. */
|
|
1332
|
+
routeWindows = new Map();
|
|
1333
|
+
/**
|
|
1334
|
+
* Resolve one selection's provider-owned context capacity, cached per
|
|
1335
|
+
* route. The LLM service is resolved lazily (not a hard inject): the Host
|
|
1336
|
+
* mounts it in every production shape, while ledger-level fixtures provide
|
|
1337
|
+
* only the services the Team service itself owns. A missing or throwing
|
|
1338
|
+
* resolution is an unknown route — the pressure policy fails closed on it.
|
|
1339
|
+
*/
|
|
1340
|
+
async resolvedContextWindow(selection) {
|
|
1341
|
+
const key = `${selection.provider}::${selection.model}`;
|
|
1342
|
+
if (this.routeWindows.has(key))
|
|
1343
|
+
return this.routeWindows.get(key);
|
|
1344
|
+
let contextWindow;
|
|
1345
|
+
try {
|
|
1346
|
+
const llm = this.ctx.get('llm');
|
|
1347
|
+
const info = llm === undefined ? undefined : await llm.resolveModelInfo(selection.provider, selection.model);
|
|
1348
|
+
contextWindow = info?.context?.contextWindow;
|
|
1349
|
+
}
|
|
1350
|
+
catch {
|
|
1351
|
+
contextWindow = undefined;
|
|
1352
|
+
}
|
|
1353
|
+
this.routeWindows.set(key, contextWindow);
|
|
1354
|
+
return contextWindow;
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Resolve one checkpoint ref to its exact seed prefix before any rollover
|
|
1358
|
+
* commit. The walk covers the current generation (own events) and archived
|
|
1359
|
+
* ancestors through `sessionPersistence`; the same projection definition
|
|
1360
|
+
* folds every source. Guards fail closed: unresolved, foreign-lineage,
|
|
1361
|
+
* open-turn, or nonshrinking targets reject without any lifecycle effect.
|
|
1362
|
+
*/
|
|
1363
|
+
async resolveCheckpointSeed(memberId, agent, checkpointRef) {
|
|
1364
|
+
let sessionId = agent.session.id;
|
|
1365
|
+
let live = true;
|
|
1366
|
+
let guard = 0;
|
|
1367
|
+
while (sessionId !== undefined && guard++ < MAX_TIMELINE_ANCESTORS) {
|
|
1368
|
+
let events;
|
|
1369
|
+
let inheritedEventCount;
|
|
1370
|
+
let parentSession;
|
|
1371
|
+
// Capture this iteration's source identity BEFORE the branch advances
|
|
1372
|
+
// the lineage flag: the measurement call below keys on it.
|
|
1373
|
+
const sourceIsCurrent = live;
|
|
1374
|
+
if (live) {
|
|
1375
|
+
events = agent.session.snapshotEvents();
|
|
1376
|
+
inheritedEventCount = agent.session.inheritedEventCount;
|
|
1377
|
+
parentSession = agent.session.header.parentSession;
|
|
1378
|
+
live = false;
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
try {
|
|
1382
|
+
const inspection = await this.ctx.sessionPersistence.inspect(sessionId);
|
|
1383
|
+
events = inspection.events;
|
|
1384
|
+
inheritedEventCount = inspection.inheritedEventCount;
|
|
1385
|
+
parentSession = inspection.meta.parentSession;
|
|
1386
|
+
}
|
|
1387
|
+
catch (error) {
|
|
1388
|
+
throw new Error(`checkpoint '${checkpointRef}' could not be resolved: its source Session is unreadable (${error instanceof Error ? error.message : String(error)})`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// Fold the source with its inherited cut respected: inherited events
|
|
1392
|
+
// are resolved history in that source, never fresh intent; checkpoints
|
|
1393
|
+
// recorded in this source's own span are the selectable targets.
|
|
1394
|
+
const state = foldContextProjection(events, inheritedEventCount, sessionId);
|
|
1395
|
+
// A Team-boundary default checkpoint: the boundary's completed-turn
|
|
1396
|
+
// anchor is the seed cut, and it is selectable exactly when one
|
|
1397
|
+
// Thread's facts entered the context through it — the same proof the
|
|
1398
|
+
// timeline requires, revalidated here because the model may cite a
|
|
1399
|
+
// boundary the timeline never surfaced.
|
|
1400
|
+
const boundary = checkpointRef.startsWith('team-boundary-')
|
|
1401
|
+
? state.boundaries.find(entry => entry.key === checkpointRef)
|
|
1402
|
+
: undefined;
|
|
1403
|
+
const entry = checkpointByRef(state, checkpointRef);
|
|
1404
|
+
const anchorTurnEndSeq = boundary !== undefined ? boundary.turnEndSeq : entry?.turnEndSeq;
|
|
1405
|
+
if (anchorTurnEndSeq !== undefined && anchorTurnEndSeq !== -1) {
|
|
1406
|
+
if (boundary !== undefined && boundary.source !== 'team-boundary') {
|
|
1407
|
+
throw new Error(`checkpoint '${checkpointRef}' is not a restorable boundary`);
|
|
1408
|
+
}
|
|
1409
|
+
// The seed is the exact contiguous prefix through the anchor's
|
|
1410
|
+
// completed turn end. Balanced by construction — the turn ended.
|
|
1411
|
+
const throughSeq = anchorTurnEndSeq + 1;
|
|
1412
|
+
const prefix = events.slice(0, throughSeq);
|
|
1413
|
+
if (boundary !== undefined) {
|
|
1414
|
+
const threads = this.threadsEnteringContext(events, anchorTurnEndSeq);
|
|
1415
|
+
if (threads.length !== 1) {
|
|
1416
|
+
throw new Error(threads.length === 0
|
|
1417
|
+
? `boundary '${checkpointRef}' has no single attributable Thread; write a fresh handoff instead`
|
|
1418
|
+
: `boundary '${checkpointRef}' spans multiple Threads; write a fresh handoff instead`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
// Nonshrinking guard, priced by the SAME source-replayed measurement
|
|
1422
|
+
// the timeline shows: the seed's retained cost is the SOURCE's own
|
|
1423
|
+
// token count scaled by the anchor share — a small current generation
|
|
1424
|
+
// never disguises a large ancestor seed. An unmeasurable source fails
|
|
1425
|
+
// closed: pricing the unknown as zero would wave an oversized seed
|
|
1426
|
+
// through.
|
|
1427
|
+
const limits = await this.routeLimitsForAgent(agent);
|
|
1428
|
+
const handoffAt = limits?.handoffAt ?? CONTEXT_HANDOFF_AT_CAP;
|
|
1429
|
+
const sourceUsage = await this.sourceUsageTokens(sessionId, sourceIsCurrent, agent);
|
|
1430
|
+
if (sourceUsage === undefined) {
|
|
1431
|
+
throw new Error(`checkpoint '${checkpointRef}' could not be priced: its source Session's context cost cannot be measured; write a fresh handoff instead`);
|
|
1432
|
+
}
|
|
1433
|
+
const retained = this.retainedEstimate(sourceUsage, events.length, anchorTurnEndSeq);
|
|
1434
|
+
if (prefix.length >= events.length) {
|
|
1435
|
+
throw new Error('checkpoint return does not shrink the working set; use a fresh handoff instead');
|
|
1436
|
+
}
|
|
1437
|
+
if (retained >= handoffAt) {
|
|
1438
|
+
throw new Error('checkpoint return would retain a context at or above the handoff budget; use a fresh handoff instead');
|
|
1439
|
+
}
|
|
1440
|
+
// Single-Thread coverage guard: with more than one active Claim the
|
|
1441
|
+
// Host cannot prove a rewind stays inside one Thread's context.
|
|
1442
|
+
if (this.requireLedger().activeClaimCountForMember(memberId) > 1) {
|
|
1443
|
+
throw new Error('multiple active Claims: write a fresh handoff covering all of them instead of returning to a checkpoint');
|
|
1444
|
+
}
|
|
1445
|
+
return { checkpointRef, sourceSessionId: sessionId, sourceThroughSeq: SessionLogOffset(throughSeq), prefix };
|
|
1446
|
+
}
|
|
1447
|
+
sessionId = parentSession;
|
|
1448
|
+
}
|
|
1449
|
+
throw new Error(`checkpoint '${checkpointRef}' does not resolve in this Member's lineage`);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Rebuild the handoff for a Member whose rollover committed but whose new
|
|
1453
|
+
* Session activated without the handoff delivery (a crash between the
|
|
1454
|
+
* ledger commit and the swap's delivery step). The previous Session —
|
|
1455
|
+
* recorded in the operation, mirrored by the lineage parent, and available
|
|
1456
|
+
* from the ledger even when the new Session's own header never carried it —
|
|
1457
|
+
* holds the durable intent; fold it cold and deliver the same handoff
|
|
1458
|
+
* envelope first. Idempotent: once any handoff exists in the new Session's
|
|
1459
|
+
* own log this never runs.
|
|
1460
|
+
*/
|
|
1461
|
+
async reconstructMissingHandoff(member, agent) {
|
|
1462
|
+
const previousSessionId = agent.session.header.parentSession
|
|
1463
|
+
?? this.requireLedger().previousSessionForMember(member.memberId);
|
|
1464
|
+
if (previousSessionId === undefined)
|
|
1465
|
+
return;
|
|
1466
|
+
let inspection;
|
|
1467
|
+
try {
|
|
1468
|
+
const result = await this.ctx.sessionPersistence.inspect(previousSessionId);
|
|
1469
|
+
inspection = result;
|
|
1470
|
+
}
|
|
1471
|
+
catch (error) {
|
|
1472
|
+
this.ctx.logger.warn(`agent-team: rollover handoff reconstruction could not read the previous Session '${previousSessionId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, previousSessionId);
|
|
1476
|
+
if (state.pending === null)
|
|
1477
|
+
return;
|
|
1478
|
+
const pending = state.pending;
|
|
1479
|
+
const message = createHandoffMessage({
|
|
1480
|
+
handoff: pending.handoff,
|
|
1481
|
+
previousSessionId,
|
|
1482
|
+
newSessionId: agent.session.id,
|
|
1483
|
+
trigger: 'model',
|
|
1484
|
+
handoffEventSeq: pending.resultSeq,
|
|
1485
|
+
...(pending.checkpointRef === undefined ? {} : { checkpointRef: pending.checkpointRef }),
|
|
1486
|
+
...(pending.relatedFiles.length === 0 ? {} : { relatedFiles: pending.relatedFiles }),
|
|
1487
|
+
});
|
|
1488
|
+
agent.steer(message);
|
|
1489
|
+
this.ctx.logger.info(`agent-team: reconstructed the rollover handoff for member '${this.memberLabel(member.memberId)}' from the previous Session '${previousSessionId}'`);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Resolve the exact recorded seed prefix of one committed checkpoint
|
|
1493
|
+
* return for crash recovery: cold-read the recorded source Session, take
|
|
1494
|
+
* the contiguous prefix of the recorded exclusive length, and verify the
|
|
1495
|
+
* source's own fold still resolves the recorded anchor there — an explicit
|
|
1496
|
+
* checkpoint through `checkpointByRef`, or a default Team boundary whose
|
|
1497
|
+
* recorded ref appears among the source's own boundary keys.
|
|
1498
|
+
* Fail-closed by contract: an unreadable source or an unprovable anchor
|
|
1499
|
+
* REJECTS the activation — a committed checkpoint return may never
|
|
1500
|
+
* downgrade to a blank child.
|
|
1501
|
+
*/
|
|
1502
|
+
async recordedCheckpointPrefix(seed) {
|
|
1503
|
+
let inspection;
|
|
1504
|
+
try {
|
|
1505
|
+
inspection = await this.ctx.sessionPersistence.inspect(seed.sourceSessionId);
|
|
1506
|
+
}
|
|
1507
|
+
catch (error) {
|
|
1508
|
+
throw new Error(`the recorded checkpoint-return seed source Session '${seed.sourceSessionId}' is unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1509
|
+
}
|
|
1510
|
+
const through = Number(seed.sourceThroughSeq);
|
|
1511
|
+
if (!Number.isSafeInteger(through) || through < 0 || through > inspection.events.length) {
|
|
1512
|
+
throw new Error(`the recorded checkpoint-return seed cut ${through} is not a valid prefix of Session '${seed.sourceSessionId}'`);
|
|
1513
|
+
}
|
|
1514
|
+
const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, seed.sourceSessionId);
|
|
1515
|
+
const anchorProven = checkpointByRef(state, seed.checkpointRef) !== undefined
|
|
1516
|
+
|| state.boundaries.some(boundary => boundary.key === seed.checkpointRef);
|
|
1517
|
+
if (!anchorProven) {
|
|
1518
|
+
throw new Error(`the recorded checkpoint-return anchor '${seed.checkpointRef}' no longer resolves in Session '${seed.sourceSessionId}'`);
|
|
1519
|
+
}
|
|
1520
|
+
return { prefix: inspection.events.slice(0, through) };
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Redeliver the old generation's unconsumed post-intent input to a
|
|
1524
|
+
* generation whose durable rollover committed but whose delivery did not
|
|
1525
|
+
* finish. Bound to the ledger's recorded transition target — the CURRENT
|
|
1526
|
+
* Session being that target — never to whether the handoff itself landed,
|
|
1527
|
+
* so a crash after the handoff but before the carried enqueue still
|
|
1528
|
+
* replays. The fold is the same durable truth the live transition uses
|
|
1529
|
+
* (unconsumed, non-Team, order preserved). Idempotency keys on what is
|
|
1530
|
+
* CURRENTLY present: delivered `user/message` ids plus the live inbox's
|
|
1531
|
+
* pending next-step/next-turn ids — a historical insert that was already
|
|
1532
|
+
* claimed (and removed) but never surfaced is NOT known and must replay.
|
|
1533
|
+
* Fail-closed: an unreadable previous Session rejects the activation
|
|
1534
|
+
* rather than silently dropping the Member's input.
|
|
1535
|
+
*/
|
|
1536
|
+
async replayCarriedInput(member, agent) {
|
|
1537
|
+
const transition = this.requireLedger().lastTransitionForMember(member.memberId);
|
|
1538
|
+
if (transition === undefined || transition.targetSessionId !== agent.session.id)
|
|
1539
|
+
return 0;
|
|
1540
|
+
let inspection;
|
|
1541
|
+
try {
|
|
1542
|
+
inspection = await this.ctx.sessionPersistence.inspect(transition.previousSessionId);
|
|
1543
|
+
}
|
|
1544
|
+
catch (error) {
|
|
1545
|
+
throw new Error(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1546
|
+
}
|
|
1547
|
+
const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, transition.previousSessionId);
|
|
1548
|
+
const carried = carriedInputOf(state);
|
|
1549
|
+
if (carried.length === 0)
|
|
1550
|
+
return 0;
|
|
1551
|
+
const known = new Set();
|
|
1552
|
+
for (const event of agent.session.ownEvents()) {
|
|
1553
|
+
if (event.type !== 'user/message')
|
|
1554
|
+
continue;
|
|
1555
|
+
const id = event.data.id;
|
|
1556
|
+
if (id !== undefined)
|
|
1557
|
+
known.add(id);
|
|
1558
|
+
}
|
|
1559
|
+
for (const pending of [...agent.inbox.nextStep, ...agent.inbox.nextTurn]) {
|
|
1560
|
+
known.add(pending.id);
|
|
1561
|
+
}
|
|
1562
|
+
let redelivered = 0;
|
|
1563
|
+
for (const message of carried) {
|
|
1564
|
+
if (known.has(message.id))
|
|
1565
|
+
continue;
|
|
1566
|
+
agent.followup(message);
|
|
1567
|
+
redelivered += 1;
|
|
1568
|
+
}
|
|
1569
|
+
if (redelivered > 0)
|
|
1570
|
+
this.ctx.logger.info(`agent-team: redelivered ${redelivered} carried input message(s) for member '${this.memberLabel(member.memberId)}' after the rollover crash recovery`);
|
|
1571
|
+
return redelivered;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Agent-only checkpoint request validation: the tool calls this inside its
|
|
1575
|
+
* own running turn. Like `context_rollover`, the tool performs no side effect —
|
|
1576
|
+
* the durable checkpoint is the successful `tool/call`+`tool/result` pair
|
|
1577
|
+
* the Session projection folds; the ref returned here is deterministic
|
|
1578
|
+
* from this Member Session's identity plus the tool call id, so the model
|
|
1579
|
+
* can cite it before the result exists and a repeated provider call id in
|
|
1580
|
+
* another generation never collides with this one.
|
|
1581
|
+
*/
|
|
1582
|
+
recordCheckpointForAgent(agent, request) {
|
|
1583
|
+
const member = this.memberForAgent(agent);
|
|
1584
|
+
if (member === undefined || member.state !== 'enabled')
|
|
1585
|
+
throw new Error('context_checkpoint requires an active Team Member');
|
|
1586
|
+
if (this.runningAgents.has(agent.id) !== true)
|
|
1587
|
+
throw new Error('context_checkpoint must run inside this Member\'s own running turn');
|
|
1588
|
+
const name = request.name.trim();
|
|
1589
|
+
if (name === '')
|
|
1590
|
+
throw new Error('context_checkpoint requires a non-empty name');
|
|
1591
|
+
if (name.length > MAX_CHECKPOINT_NAME_CHARS)
|
|
1592
|
+
throw new Error(`context_checkpoint name exceeds ${MAX_CHECKPOINT_NAME_CHARS} characters`);
|
|
1593
|
+
return { checkpointRef: checkpointRefFor(agent.session.id, request.callId), name };
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Agent-only bounded structural timeline: resolved checkpoints plus Team
|
|
1597
|
+
* delivery, handoff, and compaction boundaries across the current
|
|
1598
|
+
* generation and its archived ancestor lineage. Structural only — no
|
|
1599
|
+
* transcript content. The meter prices retained/discarded tokens; entries
|
|
1600
|
+
* the Host cannot prove restorable carry a rejection reason instead of
|
|
1601
|
+
* silently disappearing.
|
|
1602
|
+
*/
|
|
1603
|
+
async contextTimelineForAgent(agent, request) {
|
|
1604
|
+
const member = this.memberForAgent(agent);
|
|
1605
|
+
if (member === undefined || member.state !== 'enabled')
|
|
1606
|
+
throw new Error('context_timeline requires an active Team Member');
|
|
1607
|
+
const limit = request.limit === undefined ? DEFAULT_TIMELINE_LIMIT : Math.trunc(request.limit);
|
|
1608
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_TIMELINE_LIMIT)
|
|
1609
|
+
throw new Error(`context_timeline limit must be between 1 and ${MAX_TIMELINE_LIMIT}`);
|
|
1610
|
+
const meter = agent.ctx.get('tokenMeter');
|
|
1611
|
+
const measurement = meter?.measure(agent.session);
|
|
1612
|
+
const usageTokens = measurement?.totalTokens ?? 0;
|
|
1613
|
+
const limits = await this.routeLimitsForAgent(agent);
|
|
1614
|
+
const hardLimit = limits?.hardLimit ?? CONTEXT_HARD_LIMIT_CAP;
|
|
1615
|
+
const handoffAt = limits?.handoffAt ?? CONTEXT_HANDOFF_AT_CAP;
|
|
1616
|
+
// Fold the current generation, then walk archived ancestors through their
|
|
1617
|
+
// persisted logs; the same projection definition folds every source.
|
|
1618
|
+
const items = [];
|
|
1619
|
+
const seen = new Set();
|
|
1620
|
+
let sessionId = member.sessionId;
|
|
1621
|
+
let live = true;
|
|
1622
|
+
let guard = 0;
|
|
1623
|
+
while (sessionId !== undefined && guard++ < MAX_TIMELINE_ANCESTORS) {
|
|
1624
|
+
let state;
|
|
1625
|
+
let sourceSessionId = sessionId;
|
|
1626
|
+
let sourceEvents = [];
|
|
1627
|
+
// Capture this iteration's source identity BEFORE the branch advances
|
|
1628
|
+
// the lineage flag: measurement and discard pricing both key on it.
|
|
1629
|
+
const sourceIsCurrent = live;
|
|
1630
|
+
if (live) {
|
|
1631
|
+
state = foldContextProjection(agent.session.ownEvents(), agent.session.inheritedEventCount, agent.session.id);
|
|
1632
|
+
sourceEvents = agent.session.snapshotEvents();
|
|
1633
|
+
sessionId = agent.session.header.parentSession;
|
|
1634
|
+
live = false;
|
|
1635
|
+
}
|
|
1636
|
+
else {
|
|
1637
|
+
try {
|
|
1638
|
+
const inspection = await this.ctx.sessionPersistence.inspect(sessionId);
|
|
1639
|
+
state = foldContextProjection(inspection.events, inspection.inheritedEventCount, sessionId);
|
|
1640
|
+
sourceEvents = inspection.events;
|
|
1641
|
+
sourceSessionId = sessionId;
|
|
1642
|
+
sessionId = inspection.meta.parentSession;
|
|
1643
|
+
}
|
|
1644
|
+
catch {
|
|
1645
|
+
// An unreadable ancestor ends the lineage walk here.
|
|
1646
|
+
sessionId = undefined;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (state === undefined)
|
|
1650
|
+
break;
|
|
1651
|
+
// Price every candidate of this source against the SOURCE's own
|
|
1652
|
+
// replayed measurement — a small current generation never shrinks a
|
|
1653
|
+
// large ancestor's real seed cost. An unmeasurable source (no meter,
|
|
1654
|
+
// unreadable ancestor) prices as UNKNOWN, never as zero: the timeline
|
|
1655
|
+
// marks its candidates unprovable and the return guard rejects them.
|
|
1656
|
+
const sourceUsage = await this.sourceUsageTokens(sourceSessionId, sourceIsCurrent, agent);
|
|
1657
|
+
for (const candidate of timelineCandidates(state, limit)) {
|
|
1658
|
+
if (seen.has(candidate.ref))
|
|
1659
|
+
continue;
|
|
1660
|
+
seen.add(candidate.ref);
|
|
1661
|
+
items.push(this.timelineItemFor(candidate, sourceUsage, usageTokens, hardLimit, handoffAt, sourceSessionId, sourceIsCurrent, sourceEvents));
|
|
1662
|
+
if (items.length >= limit)
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1665
|
+
if (items.length >= limit)
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
return { usageTokens, hardLimit, handoffAt, items };
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Replayed measurement of one lineage source: the live current Session
|
|
1672
|
+
* measures directly; an archived ancestor measures through a borrowed
|
|
1673
|
+
* prepared Session, so a seed's retained cost is priced in the SOURCE's
|
|
1674
|
+
* own tokens — never the current generation's. Returns undefined when no
|
|
1675
|
+
* meter is available or the source cannot be borrowed; callers fail
|
|
1676
|
+
* closed on the unknown.
|
|
1677
|
+
*/
|
|
1678
|
+
async sourceUsageTokens(sessionId, live, agent) {
|
|
1679
|
+
const meter = agent.ctx.get('tokenMeter');
|
|
1680
|
+
if (meter === undefined)
|
|
1681
|
+
return undefined;
|
|
1682
|
+
if (live)
|
|
1683
|
+
return meter.measure(agent.session)?.totalTokens;
|
|
1684
|
+
try {
|
|
1685
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
1686
|
+
try {
|
|
1687
|
+
const borrowed = __addDisposableResource(env_1, await this.ctx.sessionPersistence.borrowSession(sessionId)
|
|
1688
|
+
// The borrow resolves the exact Session object for both shapes: the
|
|
1689
|
+
// live instance for an attached Session, the prepared one for an
|
|
1690
|
+
// archived ancestor. Measuring through the service context's store
|
|
1691
|
+
// would need an inject this service does not declare.
|
|
1692
|
+
, false);
|
|
1693
|
+
// The borrow resolves the exact Session object for both shapes: the
|
|
1694
|
+
// live instance for an attached Session, the prepared one for an
|
|
1695
|
+
// archived ancestor. Measuring through the service context's store
|
|
1696
|
+
// would need an inject this service does not declare.
|
|
1697
|
+
const session = borrowed.source === 'live'
|
|
1698
|
+
? agent.session.id === sessionId ? agent.session : undefined
|
|
1699
|
+
: borrowed.preparedSession;
|
|
1700
|
+
return session === undefined ? undefined : meter.measure(session)?.totalTokens;
|
|
1701
|
+
}
|
|
1702
|
+
catch (e_1) {
|
|
1703
|
+
env_1.error = e_1;
|
|
1704
|
+
env_1.hasError = true;
|
|
1705
|
+
}
|
|
1706
|
+
finally {
|
|
1707
|
+
__disposeResources(env_1);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
catch {
|
|
1711
|
+
return undefined;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Monotonic anchor-share estimate of a seed's retained cost, priced in the
|
|
1716
|
+
* SOURCE Session's own measurement: the fraction of the source log the
|
|
1717
|
+
* seed prefix covers, scaled to the source's replayed token count. The
|
|
1718
|
+
* anchor position is exact and the share grows monotonically toward the
|
|
1719
|
+
* source's head (100%). A large ancestor's anchor therefore prices at the
|
|
1720
|
+
* ancestor's real size even inside a small current generation — the
|
|
1721
|
+
* timeline display and the return guard share this one estimate.
|
|
1722
|
+
*/
|
|
1723
|
+
retainedEstimate(sourceUsageTokens, sourceLength, anchorTurnEndSeq) {
|
|
1724
|
+
if (sourceLength <= 0)
|
|
1725
|
+
return sourceUsageTokens;
|
|
1726
|
+
const share = Math.min(1, Math.max(0, (anchorTurnEndSeq + 1) / sourceLength));
|
|
1727
|
+
return Math.round(sourceUsageTokens * share);
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Threads whose facts entered this Session's model context by the given
|
|
1731
|
+
* seq: delivered Team notices (their bodies quote `Thread: <ref>`
|
|
1732
|
+
* structurally) and successful Team-claim mutations (their task overlays
|
|
1733
|
+
* resolve to Threads through the ledger). Never from unread ledger
|
|
1734
|
+
* activity — a Thread the Member never saw did not enter its context.
|
|
1735
|
+
* Order-stable, deduplicated.
|
|
1736
|
+
*/
|
|
1737
|
+
threadsEnteringContext(events, throughSeq) {
|
|
1738
|
+
const refs = [];
|
|
1739
|
+
const openAttributions = new Map();
|
|
1740
|
+
const push = (ref) => {
|
|
1741
|
+
if (ref !== undefined && !refs.includes(ref))
|
|
1742
|
+
refs.push(ref);
|
|
1743
|
+
};
|
|
1744
|
+
for (const event of events) {
|
|
1745
|
+
if (event.seq > throughSeq)
|
|
1746
|
+
break;
|
|
1747
|
+
if (event.type === 'tool/call' && (event.data.name === 'team_claim' || event.data.name === 'team_message')) {
|
|
1748
|
+
openAttributions.set(event.data.callId, { name: event.data.name, arguments: event.data.arguments });
|
|
1749
|
+
}
|
|
1750
|
+
else if (event.type === 'tool/result') {
|
|
1751
|
+
const block = event.data.message.content?.[0];
|
|
1752
|
+
if (block !== undefined && block.toolCallId !== undefined) {
|
|
1753
|
+
const recorded = openAttributions.get(block.toolCallId);
|
|
1754
|
+
if (recorded !== undefined && block.isError !== true) {
|
|
1755
|
+
openAttributions.delete(block.toolCallId);
|
|
1756
|
+
try {
|
|
1757
|
+
const args = JSON.parse(recorded.arguments);
|
|
1758
|
+
// A claim mutation resolves its Task overlay through the ledger;
|
|
1759
|
+
// a team_message committed reply names its Thread in its call
|
|
1760
|
+
// arguments; a committed start's Thread is born in the result
|
|
1761
|
+
// and is read from the durable presentation meta.
|
|
1762
|
+
if (recorded.name === 'team_claim' && typeof args.taskRef === 'string' && args.taskRef !== '') {
|
|
1763
|
+
push(this.requireLedger().threadForTask(args.taskRef));
|
|
1764
|
+
}
|
|
1765
|
+
else if (recorded.name === 'team_message') {
|
|
1766
|
+
const meta = event.data.meta;
|
|
1767
|
+
if (meta !== undefined && typeof meta === 'object' && meta.kind === 'committed') {
|
|
1768
|
+
const metaThreadRef = meta.threadRef;
|
|
1769
|
+
if (typeof metaThreadRef === 'string' && metaThreadRef !== '')
|
|
1770
|
+
push(metaThreadRef);
|
|
1771
|
+
}
|
|
1772
|
+
else if (typeof args.threadRef === 'string' && args.threadRef !== '') {
|
|
1773
|
+
push(args.threadRef);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
catch {
|
|
1778
|
+
// Malformed call arguments contribute no attribution.
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
else if (event.type === 'user/message') {
|
|
1784
|
+
const source = event.data.source;
|
|
1785
|
+
if (source?.kind !== 'plugin')
|
|
1786
|
+
continue;
|
|
1787
|
+
// Reminder notices never enter attribution — a progress nudge or a
|
|
1788
|
+
// recovery instruction is not a Team fact.
|
|
1789
|
+
if (source.form === 'notice' && source.summary !== undefined && isReminderNoticeSummary(source.summary))
|
|
1790
|
+
continue;
|
|
1791
|
+
const text = event.data.content.filter(block => block.type === 'text').map(block => block.text ?? '').join('\n');
|
|
1792
|
+
for (const match of text.matchAll(/Thread: (thread:[0-9a-f-]{6,})/g)) {
|
|
1793
|
+
push(match[1]);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return refs;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Price and annotate one timeline candidate without mutating anything.
|
|
1801
|
+
* Retained prices in the SOURCE Session's own replayed measurement (the
|
|
1802
|
+
* monotonic anchor-share of the source log); discarded is what a return
|
|
1803
|
+
* replaces — for a current-generation anchor, the measured usage beyond
|
|
1804
|
+
* the anchor; for an ancestor anchor, the current generation's whole
|
|
1805
|
+
* usage (an approximation: the ancestor's own suffix is not part of this
|
|
1806
|
+
* generation). A small current child therefore discards little but may
|
|
1807
|
+
* still retain a large ancestor seed, and both numbers say so honestly.
|
|
1808
|
+
*/
|
|
1809
|
+
timelineItemFor(candidate, sourceUsage, currentUsage, _hardLimit, handoffAt, sourceSessionId, sourceIsCurrent, sourceEvents) {
|
|
1810
|
+
const retainedTokens = candidate.source === 'head'
|
|
1811
|
+
? currentUsage
|
|
1812
|
+
: this.retainedEstimate(sourceUsage ?? 0, sourceEvents.length, candidate.turnEndSeq);
|
|
1813
|
+
const discardedTokens = candidate.source === 'head'
|
|
1814
|
+
? 0
|
|
1815
|
+
: sourceIsCurrent
|
|
1816
|
+
? Math.max(0, currentUsage - retainedTokens)
|
|
1817
|
+
: currentUsage;
|
|
1818
|
+
const affectedThreads = candidate.turnEndSeq === -1 ? [] : this.threadsEnteringContext(sourceEvents, candidate.turnEndSeq);
|
|
1819
|
+
let restorable = candidate.turnEndSeq !== -1;
|
|
1820
|
+
let reason;
|
|
1821
|
+
if (candidate.source === 'head') {
|
|
1822
|
+
// The head is the current working set: returning to it discards
|
|
1823
|
+
// nothing and is never a meaningful return target.
|
|
1824
|
+
restorable = false;
|
|
1825
|
+
reason = 'the head is the current working set; returning to it discards nothing';
|
|
1826
|
+
}
|
|
1827
|
+
else if (sourceUsage === undefined) {
|
|
1828
|
+
// The source's cost cannot be measured (no meter, or the archived
|
|
1829
|
+
// ancestor cannot be borrowed): the budget cannot be proven, so the
|
|
1830
|
+
// candidate is not selectable. Never price an unknown as zero.
|
|
1831
|
+
restorable = false;
|
|
1832
|
+
reason = 'the source Session\'s context cost cannot be measured, so the return budget cannot be proven';
|
|
1833
|
+
}
|
|
1834
|
+
else if (candidate.source === 'handoff' || candidate.source === 'compaction') {
|
|
1835
|
+
// A handoff starts a generation and a compaction rewrites the visible
|
|
1836
|
+
// surface: rewinding into them is not a proven-safe V1 target.
|
|
1837
|
+
restorable = false;
|
|
1838
|
+
reason = `source '${candidate.source}' is not a restorable checkpoint`;
|
|
1839
|
+
}
|
|
1840
|
+
else if (candidate.source === 'team-boundary') {
|
|
1841
|
+
// A Team delivery is a selectable default checkpoint exactly when the
|
|
1842
|
+
// Host can prove it stays inside one Thread's context: the boundary
|
|
1843
|
+
// resolved at a completed turn AND exactly one Thread's facts entered
|
|
1844
|
+
// the Session context through it. Multi-Thread or unattributable
|
|
1845
|
+
// boundaries document why they are not selectable.
|
|
1846
|
+
if (affectedThreads.length !== 1) {
|
|
1847
|
+
restorable = false;
|
|
1848
|
+
reason = affectedThreads.length === 0
|
|
1849
|
+
? 'no single Thread is attributable to this boundary'
|
|
1850
|
+
: 'multiple Threads entered the context through this boundary; write a fresh handoff instead';
|
|
1851
|
+
}
|
|
1852
|
+
else if (retainedTokens >= handoffAt) {
|
|
1853
|
+
restorable = false;
|
|
1854
|
+
reason = 'retained context would not materially shrink the working set';
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
else if (candidate.source === 'agent' && retainedTokens >= handoffAt) {
|
|
1858
|
+
restorable = false;
|
|
1859
|
+
reason = 'retained context would be at or above the handoff budget';
|
|
1860
|
+
}
|
|
1861
|
+
return {
|
|
1862
|
+
checkpointRef: candidate.ref,
|
|
1863
|
+
name: candidate.label,
|
|
1864
|
+
source: candidate.source,
|
|
1865
|
+
retainedTokens,
|
|
1866
|
+
discardedTokens,
|
|
1867
|
+
affectedThreads,
|
|
1868
|
+
restorable,
|
|
1869
|
+
...(reason === undefined ? {} : { reason }),
|
|
1870
|
+
...(candidate.source === 'agent' ? {} : { sourceSessionId }),
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Agent-only rollover request validation: the tool calls this to check its
|
|
1875
|
+
* Member binding, exclusivity, and checkpoint ownership. It performs no
|
|
1876
|
+
* lifecycle effect — the actual transition reacts to the successful tool
|
|
1877
|
+
* result through the context-management coordinator.
|
|
1878
|
+
*/
|
|
1879
|
+
async requestNewContext(agent, request) {
|
|
1880
|
+
const member = this.memberForAgent(agent);
|
|
1881
|
+
if (member === undefined || member.state !== 'enabled')
|
|
1882
|
+
throw new Error('context_rollover requires an active Team Member');
|
|
1883
|
+
if (this.contextManagement.isTransitioning(member.memberId))
|
|
1884
|
+
throw new Error('a context rollover is already scheduled for this Member; wait for it to finish before requesting another');
|
|
1885
|
+
if (this.runningAgents.has(agent.id) !== true) {
|
|
1886
|
+
// The tool runs inside the Member's own turn, so a non-running agent at
|
|
1887
|
+
// this point is a harness anomaly; refuse rather than schedule a swap
|
|
1888
|
+
// outside the turn fence.
|
|
1889
|
+
throw new Error('context_rollover must run inside this Member\'s own running turn');
|
|
1890
|
+
}
|
|
1891
|
+
// Job ownership guard: disposing the old Agent cancels its running jobs
|
|
1892
|
+
// and orphaned terminal-but-unreported output would vanish with it. The
|
|
1893
|
+
// rejection names the jobs so the model can collect or stop them first;
|
|
1894
|
+
// the transition rechecks at the lifecycle commit seam because a job may
|
|
1895
|
+
// settle between this validation and the swap.
|
|
1896
|
+
const blocking = this.ownedJobsBlockingRollover(agent);
|
|
1897
|
+
if (blocking.length > 0) {
|
|
1898
|
+
throw new Error(`context_rollover is refused while this Member owns jobs that would not survive the switch (${blocking.join(', ')}); collect or stop them first, then retry`);
|
|
1899
|
+
}
|
|
1900
|
+
if (request.checkpointRef === undefined)
|
|
1901
|
+
return { mode: 'fresh' };
|
|
1902
|
+
if (!/^(context-checkpoint-[0-9a-f]{64}|team-boundary-[0-9a-f]{64})$/.test(request.checkpointRef))
|
|
1903
|
+
throw new Error('checkpointRef must be an opaque ref exactly as returned by context_timeline');
|
|
1904
|
+
// Full current-state prevalidation through the ONE resolver the swap
|
|
1905
|
+
// itself uses: a ref that is fabricated, unattributable, nonshrinking,
|
|
1906
|
+
// unmeasurable, over-budget, or blocked by multiple active Claims
|
|
1907
|
+
// rejects HERE — a model-visible error result instead of a fake
|
|
1908
|
+
// `scheduled` whose async swap always fails. The seed is resolved and
|
|
1909
|
+
// discarded: the transition seam resolves it again, so only the mutable
|
|
1910
|
+
// guard set (jobs, route limits, lineage growth) is revalidated there.
|
|
1911
|
+
await this.resolveCheckpointSeed(member.memberId, agent, request.checkpointRef);
|
|
1912
|
+
return { mode: 'from-checkpoint' };
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Jobs this Agent owns that cannot survive a generation swap: any
|
|
1916
|
+
* running/stopping job, and any settled job whose terminal output was
|
|
1917
|
+
* never reported (disposal would silently discard it). In-place hard
|
|
1918
|
+
* compaction is exempt — it never cancels the owner.
|
|
1919
|
+
*/
|
|
1920
|
+
ownedJobsBlockingRollover(agent) {
|
|
1921
|
+
const jobs = agent.ctx.get('jobs');
|
|
1922
|
+
if (jobs === undefined)
|
|
1923
|
+
return [];
|
|
1924
|
+
return jobs.list(agent)
|
|
1925
|
+
.filter((job) => job.status === 'running' || job.status === 'stopping' || ((job.status === 'completed' || job.status === 'killed' || job.status === 'failed') && !job.reported))
|
|
1926
|
+
.map((job) => `${job.label} (${job.id})`);
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Agent-only session rollover commit: the Member actor must be the target
|
|
1930
|
+
* Member on its currently bound live Session. The Host performs the actual
|
|
1931
|
+
* generation swap around this write; the ledger records only the durable
|
|
1932
|
+
* binding transition and rollover audit envelope.
|
|
1933
|
+
*/
|
|
1934
|
+
async rolloverSessionForAgent(agent, request) {
|
|
1935
|
+
const actor = this.memberCall(agent, request.workspaceId);
|
|
1936
|
+
const result = await this.requireLedger().rolloverMemberSession({ ...request, actor });
|
|
1937
|
+
if (result.committed)
|
|
1938
|
+
this.emitCommitted(result.value.receipt);
|
|
1939
|
+
return result.value;
|
|
1940
|
+
}
|
|
961
1941
|
emitCommittedOutcome(result) {
|
|
962
1942
|
if (result.committed && result.value.kind === 'committed' && result.value.receipt !== undefined)
|
|
963
1943
|
this.emitCommitted(result.value.receipt);
|
|
@@ -1036,16 +2016,40 @@ let AgentTeam = (() => {
|
|
|
1036
2016
|
throw error;
|
|
1037
2017
|
}
|
|
1038
2018
|
}
|
|
1039
|
-
|
|
2019
|
+
/**
|
|
2020
|
+
* Session headers currently durable in the persistence backend.
|
|
2021
|
+
*
|
|
2022
|
+
* The Host retires a disposed Session's log without awaiting it, so a
|
|
2023
|
+
* concurrent activation can observe the JSONL backend's transient win32
|
|
2024
|
+
* staging directories (.dsh-mkdir-*) as ENOENT while they rename into
|
|
2025
|
+
* place. The read is idempotent; back off briefly instead of failing the
|
|
2026
|
+
* activation on a race the publisher resolves within milliseconds.
|
|
2027
|
+
*/
|
|
2028
|
+
async persistedSessionHeaders() {
|
|
2029
|
+
for (let attempt = 0;; attempt += 1) {
|
|
2030
|
+
try {
|
|
2031
|
+
return await this.ctx.sessionPersistence.list();
|
|
2032
|
+
}
|
|
2033
|
+
catch (error) {
|
|
2034
|
+
if (attempt >= 3 || error?.code !== 'ENOENT')
|
|
2035
|
+
throw error;
|
|
2036
|
+
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 25));
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
async activateMember(member, knownWorkspacePath, knownSessions, forkedFrom, options) {
|
|
1040
2041
|
if (this.handles.has(member.memberId))
|
|
1041
2042
|
return;
|
|
1042
2043
|
let created;
|
|
1043
2044
|
try {
|
|
1044
2045
|
const workspace = this.requireWorkspace(member.workspaceId);
|
|
1045
2046
|
const workspacePath = knownWorkspacePath ?? workspace.path;
|
|
1046
|
-
|
|
2047
|
+
// Existing Members carry the pre-sanitization ledger path; activation
|
|
2048
|
+
// migrates it onto the sanitized directory before provisioning.
|
|
2049
|
+
const sanitizedMemoryPath = dshHomePath('agent-team', 'members', memberMemoryDirectoryName(member.memberId));
|
|
2050
|
+
await this.memberRuntime.initializePrivateMemory(sanitizedMemoryPath, member.privateMemoryPath);
|
|
1047
2051
|
const persisted = knownSessions !== undefined ? knownSessions.has(member.sessionId)
|
|
1048
|
-
:
|
|
2052
|
+
: await this.sessionPersisted(member.sessionId);
|
|
1049
2053
|
// AgentOptions declares only provider/model. Install the full selection
|
|
1050
2054
|
// through the public Agent model-selection seam so reasoning effort is
|
|
1051
2055
|
// applied to the next request and not lost during activation.
|
|
@@ -1061,6 +2065,46 @@ let AgentTeam = (() => {
|
|
|
1061
2065
|
this.memberRuntime.applyMemberToolPolicy(agentCtx, member);
|
|
1062
2066
|
this.validateMemberPreset(agentCtx);
|
|
1063
2067
|
installModelSelection(agentCtx, selected);
|
|
2068
|
+
// Admission gate for pending context rollovers: once a successful
|
|
2069
|
+
// context_rollover result is durable, queued input must not open another
|
|
2070
|
+
// old-generation model request. The turn-stop boundary captures the
|
|
2071
|
+
// inbox (non-Team input is carried to the new generation), and a
|
|
2072
|
+
// racing pre-step rejects instead of admitting claimed messages.
|
|
2073
|
+
agentCtx.on('agent/turn-stopping', ({ agent }) => {
|
|
2074
|
+
if (!this.contextManagement.needsAdmissionGate(agent))
|
|
2075
|
+
return;
|
|
2076
|
+
this.contextManagement.captureQueuedInput(agent);
|
|
2077
|
+
});
|
|
2078
|
+
agentCtx.on('agent/pre-step', async ({ agent, messages, signal }, next) => {
|
|
2079
|
+
// Check before AND after the waterfall: a rollover pending at either
|
|
2080
|
+
// edge must reject this old-generation step, preserving its claimed
|
|
2081
|
+
// input for the new generation instead of letting it run or drop.
|
|
2082
|
+
if (this.contextManagement.needsAdmissionGate(agent)) {
|
|
2083
|
+
this.contextManagement.captureClaimedInput(agent, messages);
|
|
2084
|
+
return { kind: 'reject' };
|
|
2085
|
+
}
|
|
2086
|
+
// Team pressure policy rides the same pre-step seam after the
|
|
2087
|
+
// admission gate: the handoff-budget notice steers into the running
|
|
2088
|
+
// turn, and the hard limit forces compaction before the request is
|
|
2089
|
+
// forwarded — failing closed blocks the step instead of submitting
|
|
2090
|
+
// over the Team limit. Missing route capacity is an explicit reject.
|
|
2091
|
+
const pressure = await this.pressurePolicy.onPreStep(agent, signal);
|
|
2092
|
+
if (pressure.kind === 'reject')
|
|
2093
|
+
return { kind: 'reject' };
|
|
2094
|
+
const decision = await next();
|
|
2095
|
+
if (decision.kind === 'reject' || !this.contextManagement.needsAdmissionGate(agent))
|
|
2096
|
+
return decision;
|
|
2097
|
+
this.contextManagement.captureClaimedInput(agent, messages);
|
|
2098
|
+
return { kind: 'reject' };
|
|
2099
|
+
});
|
|
2100
|
+
// Provider context-overflow recovery: one bounded compact-and-retry
|
|
2101
|
+
// sequence per failure chain through the Team-owned policy.
|
|
2102
|
+
agentCtx.on('agent/request-error', async ({ agent, failure, signal }, next) => {
|
|
2103
|
+
const retry = await this.pressurePolicy.onRequestError(agent, failure, signal);
|
|
2104
|
+
if (retry)
|
|
2105
|
+
return { kind: 'retry' };
|
|
2106
|
+
return next();
|
|
2107
|
+
});
|
|
1064
2108
|
return {
|
|
1065
2109
|
commit: () => {
|
|
1066
2110
|
const agent = agentCtx.agent;
|
|
@@ -1077,24 +2121,112 @@ let AgentTeam = (() => {
|
|
|
1077
2121
|
},
|
|
1078
2122
|
};
|
|
1079
2123
|
};
|
|
2124
|
+
// A renewal's create path parents at the ledger-recorded previous
|
|
2125
|
+
// Session even when the caller does not pass one: a crash between the
|
|
2126
|
+
// durable rollover commit and this activation recreates the Session
|
|
2127
|
+
// from the ledger binding alone, and the lineage parent is what makes
|
|
2128
|
+
// the missing handoff reconstructible on the NEXT failure. A recorded
|
|
2129
|
+
// checkpoint seed re-seeds the child here — the crash lost the process
|
|
2130
|
+
// memory, but the ledger envelope names the exact source prefix.
|
|
2131
|
+
let recordedSeed;
|
|
2132
|
+
let recordedSeedCut;
|
|
2133
|
+
const rolloverSeed = !persisted && options?.seed === undefined
|
|
2134
|
+
? this.requireLedger().rolloverSeedForMember(member.memberId, member.sessionId)
|
|
2135
|
+
: undefined;
|
|
2136
|
+
if (rolloverSeed !== undefined) {
|
|
2137
|
+
// Fail-closed: an unreadable or unprovable recorded seed rejects
|
|
2138
|
+
// this activation (the catch below records the diagnostic) instead
|
|
2139
|
+
// of silently downgrading a committed checkpoint return to a blank
|
|
2140
|
+
// child.
|
|
2141
|
+
const resolved = await this.recordedCheckpointPrefix(rolloverSeed);
|
|
2142
|
+
recordedSeed = resolved.prefix;
|
|
2143
|
+
recordedSeedCut = SessionLogOffset(resolved.prefix.length);
|
|
2144
|
+
}
|
|
2145
|
+
const lineageParent = forkedFrom ?? (!persisted ? this.requireLedger().previousSessionForMember(member.memberId) : undefined);
|
|
2146
|
+
let recoveryCarried = 0;
|
|
2147
|
+
const seededFromLedger = recordedSeed !== undefined;
|
|
2148
|
+
const effectiveParent = recordedSeed !== undefined ? rolloverSeed.sourceSessionId : lineageParent;
|
|
2149
|
+
const seedOptions = options?.seed !== undefined && options.inheritedEventCount !== undefined
|
|
2150
|
+
? { seed: options.seed, inheritedEventCount: options.inheritedEventCount }
|
|
2151
|
+
: recordedSeed !== undefined && recordedSeedCut !== undefined
|
|
2152
|
+
? { seed: recordedSeed, inheritedEventCount: recordedSeedCut }
|
|
2153
|
+
: {};
|
|
1080
2154
|
created = persisted
|
|
1081
2155
|
? await this.ctx.agents.resume({ resumeSessionId: member.sessionId, agentOptions, setup })
|
|
1082
2156
|
: await this.ctx.agents.create({
|
|
1083
2157
|
sessionId: member.sessionId,
|
|
1084
2158
|
// A context renewal records its fork lineage so the archived
|
|
1085
|
-
// previous Session stays discoverable from the durable header
|
|
1086
|
-
|
|
2159
|
+
// previous Session stays discoverable from the durable header; a
|
|
2160
|
+
// checkpoint return parents at the seed source and marks the
|
|
2161
|
+
// fork seeded with its exact inherited prefix length.
|
|
2162
|
+
meta: {
|
|
2163
|
+
cwd: workspacePath,
|
|
2164
|
+
agentPreset: member.presetId,
|
|
2165
|
+
...(effectiveParent === undefined ? {} : { parentSession: effectiveParent }),
|
|
2166
|
+
...(options?.seed === undefined && !seededFromLedger ? {} : { isSeeded: true }),
|
|
2167
|
+
},
|
|
2168
|
+
...seedOptions,
|
|
1087
2169
|
agentOptions,
|
|
1088
2170
|
setup,
|
|
1089
2171
|
});
|
|
1090
2172
|
this.memberRuntime.mountMemberSkillProvider(member, created.agent.ctx, skillSelection);
|
|
1091
2173
|
await workspace.attachSession(member.sessionId);
|
|
1092
2174
|
this.handles.set(member.memberId, created);
|
|
2175
|
+
this.memberBySessionId.set(member.sessionId, member.memberId);
|
|
1093
2176
|
this.modelSelections.set(member.memberId, selected);
|
|
1094
2177
|
this.clearMemberFailure(member.memberId, 'activation');
|
|
1095
2178
|
this.nameMemberSession(member, created.agent);
|
|
1096
|
-
|
|
1097
|
-
|
|
2179
|
+
// The one-shot pressure notice needs no explicit re-arm here: it latches
|
|
2180
|
+
// on durable Session evidence, and a fresh generation's own event span
|
|
2181
|
+
// starts empty — a new Session is itself the re-arm.
|
|
2182
|
+
// A restart between a durable rollover intent and its swap replays the
|
|
2183
|
+
// old Session; the projection still carries the intent, so finish the
|
|
2184
|
+
// transition (or keep waiting for the containing turn) from here.
|
|
2185
|
+
if (persisted) {
|
|
2186
|
+
const state = foldContextProjection(created.agent.session.ownEvents(), created.agent.session.inheritedEventCount, created.agent.session.id);
|
|
2187
|
+
if (state.pending !== null)
|
|
2188
|
+
this.contextManagement.recoverPendingTransition(member.memberId, created.agent, member.sessionId);
|
|
2189
|
+
// A restart between one checkpoint's durable result and its quiet
|
|
2190
|
+
// follow-up delivery repairs exactly once; delivered continuations
|
|
2191
|
+
// stay delivered through the projection's own delivery record.
|
|
2192
|
+
this.contextManagement.repairContinuations(created.agent, state);
|
|
2193
|
+
// A restart after the rollover committed but before the handoff was
|
|
2194
|
+
// delivered activates the new Session with no handoff in its own
|
|
2195
|
+
// log — never treat that as an ordinary blank Member Session. The
|
|
2196
|
+
// operation's recorded previous Session (the lineage parent) still
|
|
2197
|
+
// holds the intent; rebuild the handoff from it.
|
|
2198
|
+
if (!state.boundaries.some(boundary => boundary.source === 'handoff')) {
|
|
2199
|
+
await this.reconstructMissingHandoff(member, created.agent);
|
|
2200
|
+
}
|
|
2201
|
+
// Carried input redelivery binds to the committed transition target —
|
|
2202
|
+
// this Session — never to the handoff's presence: a crash after the
|
|
2203
|
+
// handoff landed but before the carried enqueue still replays here.
|
|
2204
|
+
recoveryCarried = await this.replayCarriedInput(member, created.agent);
|
|
2205
|
+
}
|
|
2206
|
+
else if (forkedFrom === undefined && lineageParent !== undefined) {
|
|
2207
|
+
// A restart recreated a Session that never materialized before the
|
|
2208
|
+
// crash (the rollover committed, its activation failed, and this
|
|
2209
|
+
// create path just rebuilt it from the ledger binding alone): the
|
|
2210
|
+
// same handoff reconstruction applies from the ledger's recorded
|
|
2211
|
+
// previous Session, and the old generation's unconsumed post-intent
|
|
2212
|
+
// input — the durable fold, the same truth the live transition uses —
|
|
2213
|
+
// rides behind the handoff exactly as it would have. The rollover's
|
|
2214
|
+
// own activation passes its fork parent explicitly and delivers the
|
|
2215
|
+
// handoff right after — that delivery is the plan, never this
|
|
2216
|
+
// recovery.
|
|
2217
|
+
await this.reconstructMissingHandoff(member, created.agent);
|
|
2218
|
+
recoveryCarried = await this.replayCarriedInput(member, created.agent);
|
|
2219
|
+
}
|
|
2220
|
+
// The ordinary Inbox wake runs LAST, after any recovery delivery above:
|
|
2221
|
+
// the handoff must stay the first model-facing context of a recovered
|
|
2222
|
+
// generation, with carried input behind it and rederived Team facts
|
|
2223
|
+
// behind that. When recovery redelivered carried input, the wake is
|
|
2224
|
+
// sequenced behind it (its own follow-up turn) so the steer lane can
|
|
2225
|
+
// never leapfrog the carried messages. A rollover activation defers the
|
|
2226
|
+
// wake entirely — its caller delivers the handoff (and carried input)
|
|
2227
|
+
// first and rederives the Inbox afterwards.
|
|
2228
|
+
if (options?.deferNotify !== true)
|
|
2229
|
+
this.notifyMember(created.agent, recoveryCarried > 0);
|
|
1098
2230
|
}
|
|
1099
2231
|
catch (error) {
|
|
1100
2232
|
await created?.dispose();
|
|
@@ -1180,6 +2312,13 @@ let AgentTeam = (() => {
|
|
|
1180
2312
|
const handle = this.handles.get(member.memberId);
|
|
1181
2313
|
if (handle === undefined)
|
|
1182
2314
|
return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable' });
|
|
2315
|
+
// A rollover commits its ledger binding before the old generation retires
|
|
2316
|
+
// and the new one activates; during that window the live handle still runs
|
|
2317
|
+
// the previous Session. The Member stays visible but must not report the
|
|
2318
|
+
// new binding as active — a Client following the row would otherwise open
|
|
2319
|
+
// a Session that does not exist yet.
|
|
2320
|
+
if (handle.agent.id !== member.sessionId)
|
|
2321
|
+
return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic: 'context rollover in progress' });
|
|
1183
2322
|
if (this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
|
|
1184
2323
|
return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: ORPHANED_MEMBER_DIAGNOSTIC });
|
|
1185
2324
|
}
|
|
@@ -1233,21 +2372,50 @@ let AgentTeam = (() => {
|
|
|
1233
2372
|
}
|
|
1234
2373
|
const ledger = this.requireLedger();
|
|
1235
2374
|
this.emitChanged(ledger.changeScopesOf(operation));
|
|
2375
|
+
// Nudge reconciliation precedes notifyMember: a committed reply both
|
|
2376
|
+
// resets the author's silence and cancels any of its queued nudge before
|
|
2377
|
+
// the Inbox notice (higher priority) is considered.
|
|
2378
|
+
this.progressNudge.onCommitted(operation);
|
|
1236
2379
|
for (const memberId of ledger.affectedMembersOf(operation)) {
|
|
1237
2380
|
const handle = this.handles.get(memberId);
|
|
1238
2381
|
if (handle !== undefined)
|
|
1239
2382
|
this.notifyMember(handle.agent);
|
|
1240
2383
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
2384
|
+
// Task acceptance no longer schedules standalone auto compaction: it is
|
|
2385
|
+
// a semantic checkpoint/context cue (delivered as ordinary Team
|
|
2386
|
+
// notification), and the Team pressure policy owns compaction entry.
|
|
2387
|
+
}
|
|
2388
|
+
/** Model-visible active-Claim labels for the pressure notice. */
|
|
2389
|
+
activeClaimLabels(memberId) {
|
|
2390
|
+
const ledger = this.ledger;
|
|
2391
|
+
if (ledger === undefined)
|
|
2392
|
+
return [];
|
|
2393
|
+
const labels = [];
|
|
2394
|
+
for (const claim of ledger.activeClaimsForMember(memberId)) {
|
|
2395
|
+
if (claim.state !== 'active')
|
|
2396
|
+
continue;
|
|
2397
|
+
labels.push(`${claim.claimRef} (${claim.direction})`);
|
|
2398
|
+
}
|
|
2399
|
+
return labels;
|
|
2400
|
+
}
|
|
2401
|
+
/** Model-visible running/stopping job labels for the pressure notice. */
|
|
2402
|
+
runningJobLabels(memberId) {
|
|
2403
|
+
const handle = this.handles.get(memberId);
|
|
2404
|
+
if (handle === undefined)
|
|
2405
|
+
return [];
|
|
2406
|
+
const jobs = handle.agent.ctx.get('jobs');
|
|
2407
|
+
if (jobs === undefined)
|
|
2408
|
+
return [];
|
|
2409
|
+
return jobs.list(handle.agent)
|
|
2410
|
+
.filter((job) => job.status === 'running' || job.status === 'stopping')
|
|
2411
|
+
.map((job) => `${job.name ?? job.id}`);
|
|
1244
2412
|
}
|
|
1245
2413
|
emitAutoCompactionChanged(memberId) {
|
|
1246
2414
|
const workspaceId = this.ledger?.getMember(memberId)?.workspaceId;
|
|
1247
2415
|
this.emitChanged(workspaceId === undefined ? undefined : [{ kind: 'workspace', workspaceId }]);
|
|
1248
2416
|
}
|
|
1249
2417
|
/** Wake from durable unread state with bounded facts for direct and state-changing work. */
|
|
1250
|
-
notifyMember(agent) {
|
|
2418
|
+
notifyMember(agent, sequenced = false) {
|
|
1251
2419
|
const member = this.memberForAgent(agent);
|
|
1252
2420
|
if (member === undefined || member.state !== 'enabled')
|
|
1253
2421
|
return;
|
|
@@ -1256,6 +2424,12 @@ let AgentTeam = (() => {
|
|
|
1256
2424
|
this.notifiedInbox.delete(member.memberId);
|
|
1257
2425
|
return;
|
|
1258
2426
|
}
|
|
2427
|
+
// Any ordinary next-turn input already queued (a rollover's or recovery's
|
|
2428
|
+
// carried messages) forces sequencing regardless of the caller: a steer
|
|
2429
|
+
// would claim the nearest step boundary and leapfrog the carried turn.
|
|
2430
|
+
if (!sequenced) {
|
|
2431
|
+
sequenced = agent.inbox.nextTurn.some(message => message.source.kind === 'user');
|
|
2432
|
+
}
|
|
1259
2433
|
const signature = JSON.stringify(notifications.map(({ item }) => [
|
|
1260
2434
|
item.thread.threadRef, item.thread.revision, item.unreadCount, item.directCount, item.newestSequence,
|
|
1261
2435
|
]));
|
|
@@ -1271,13 +2445,24 @@ let AgentTeam = (() => {
|
|
|
1271
2445
|
const existingInboxHint = pending.find(message => this.isInboxNotice(message));
|
|
1272
2446
|
if (existingInboxHint !== undefined)
|
|
1273
2447
|
agent.inbox.remove(existingInboxHint.id);
|
|
2448
|
+
// A durable Inbox notice outranks a queued progress nudge; revoke it so
|
|
2449
|
+
// the model reads concrete unread work instead of a generic reminder.
|
|
2450
|
+
this.progressNudge.revokePendingNotice(member.memberId);
|
|
1274
2451
|
const hint = createUserMessage({
|
|
1275
2452
|
content: [{ type: 'text', text: this.notificationText(notifications, member.memberId) }],
|
|
1276
2453
|
source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: INBOX_NOTICE_SUMMARY },
|
|
1277
2454
|
});
|
|
1278
2455
|
this.notifiedInbox.set(member.memberId, signature);
|
|
1279
2456
|
try {
|
|
1280
|
-
|
|
2457
|
+
// Steer normally claims the nearest step boundary — which would
|
|
2458
|
+
// preempt carried input that a rollover or crash recovery queued as
|
|
2459
|
+
// ordinary next-turn messages. The sequenced mode enqueues the
|
|
2460
|
+
// rederived Inbox as its own follow-up turn instead, so the delivery
|
|
2461
|
+
// order stays handoff → carried input → rederived Inbox.
|
|
2462
|
+
if (sequenced)
|
|
2463
|
+
agent.followup(hint);
|
|
2464
|
+
else
|
|
2465
|
+
agent.steer(hint);
|
|
1281
2466
|
}
|
|
1282
2467
|
catch (error) {
|
|
1283
2468
|
this.clearMemberNotificationState(member.memberId);
|
|
@@ -1349,11 +2534,28 @@ let AgentTeam = (() => {
|
|
|
1349
2534
|
activityNotification(activity, readerId) {
|
|
1350
2535
|
const actor = activity.actor === AGENT_TEAM_HUMAN_MEMBER_ID
|
|
1351
2536
|
? 'human' : this.requireLedger().getMember(activity.actor)?.handle ?? activity.actor;
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
2537
|
+
// An acceptance concludes the reader's own Claims in two ways: a Claim
|
|
2538
|
+
// already finished before the accept was accepted (its work stands), and
|
|
2539
|
+
// a still-open Claim the accept completed atomically (no further work is
|
|
2540
|
+
// needed). Both must be said plainly, or the owner keeps working on a
|
|
2541
|
+
// done Task or never learns its contribution was accepted.
|
|
2542
|
+
if (activity.kind === 'accept' && activity.actor === AGENT_TEAM_HUMAN_MEMBER_ID && readerId !== undefined) {
|
|
2543
|
+
const acceptedOwn = (activity.acceptedClaimRefs ?? []).filter(claimRef => this.requireLedger().getClaim(claimRef)?.owner === readerId);
|
|
2544
|
+
const completedOwn = (activity.completedClaimRefs ?? []).filter(claimRef => this.requireLedger().getClaim(claimRef)?.owner === readerId);
|
|
2545
|
+
// The early-accept-only reader holds no finished Claim: every own Claim
|
|
2546
|
+
// was completed atomically, so the "finished Claim" clause's ref list
|
|
2547
|
+
// would be empty. Render the completed semantics alone instead of a
|
|
2548
|
+
// sentence with a dangling empty list.
|
|
2549
|
+
const finishedOwn = acceptedOwn.filter(claimRef => !completedOwn.includes(claimRef));
|
|
2550
|
+
if (completedOwn.length > 0 && finishedOwn.length > 0) {
|
|
2551
|
+
return `Team Task update\n${actor} accepted Task ${activity.taskRef}. Your Claim ${completedOwn.join(', ')} was completed with the acceptance, and your finished Claim ${finishedOwn.join(', ')} was accepted. No further work is needed.`;
|
|
2552
|
+
}
|
|
2553
|
+
if (completedOwn.length > 0) {
|
|
2554
|
+
return `Team Task update\n${actor} accepted Task ${activity.taskRef} and your open Claim ${completedOwn.join(', ')} was completed with it. No further work is needed.`;
|
|
2555
|
+
}
|
|
2556
|
+
if (acceptedOwn.length > 0) {
|
|
2557
|
+
return `Team Task update\n${actor} accepted Task ${activity.taskRef}; your finished Claim ${acceptedOwn.join(', ')} was accepted. No further work is needed.`;
|
|
2558
|
+
}
|
|
1357
2559
|
}
|
|
1358
2560
|
if (activity.kind === 'claim' || activity.kind === 'done' || activity.kind === 'release') {
|
|
1359
2561
|
return `Team Task update\n${actor} ${activity.kind} Claim ${activity.claimRef} on Task ${activity.taskRef}.`;
|
|
@@ -1376,12 +2578,14 @@ let AgentTeam = (() => {
|
|
|
1376
2578
|
await handle.dispose();
|
|
1377
2579
|
this.handles.delete(memberId);
|
|
1378
2580
|
}
|
|
2581
|
+
this.memberBySessionId.delete(member.sessionId);
|
|
1379
2582
|
this.modelSelections.delete(memberId);
|
|
1380
2583
|
this.memberRuntime.forgetMember(memberId);
|
|
1381
2584
|
this.clearMemberFailure(memberId, 'activation');
|
|
1382
2585
|
}
|
|
1383
2586
|
clearMemberRecoveryState(member) {
|
|
1384
2587
|
this.recovery.stopTracking(member.memberId);
|
|
2588
|
+
this.progressNudge.stopTracking(member.memberId);
|
|
1385
2589
|
this.clearMemberFailure(member.memberId, 'runtime');
|
|
1386
2590
|
this.clearMemberNotificationState(member.memberId);
|
|
1387
2591
|
}
|