@wowyuarm/dsh-agent-team 0.1.15 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/README.zh.md +8 -4
- package/cordis.patch.yml +26 -12
- package/package.json +52 -47
- package/packages/agent-team/lib/attachments.js +63 -4
- package/packages/agent-team/lib/compaction-engine.js +18 -0
- package/packages/agent-team/lib/context-continuity-host.js +198 -4
- package/packages/agent-team/lib/context-judge.js +150 -0
- package/packages/agent-team/lib/context-source.js +11 -6
- package/packages/agent-team/lib/environment-check.js +102 -0
- package/packages/agent-team/lib/human-profile.js +23 -37
- package/packages/agent-team/lib/index.js +510 -99
- package/packages/agent-team/lib/installed-manifest.js +32 -0
- package/packages/agent-team/lib/invariant.js +36 -12
- package/packages/agent-team/lib/ledger.js +37 -1
- package/packages/agent-team/lib/member-context.js +2 -4
- package/packages/agent-team/lib/member-runtime.js +2 -1
- package/packages/agent-team/lib/member-time-context.js +2 -4
- package/packages/agent-team/lib/recovery.js +53 -4
- package/packages/agent-team/lib/typert.host.js +225 -54
- package/packages/agent-team/lib/typert.remote-client.d.ts +5 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +127 -39
- package/packages/agent-team/lib/types/attachments.d.ts +32 -2
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/compaction-engine.d.ts +19 -0
- package/packages/agent-team/lib/types/compaction-engine.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-continuity-host.d.ts +131 -4
- package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-judge.d.ts +82 -0
- package/packages/agent-team/lib/types/context-judge.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-source.d.ts +7 -4
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
- package/packages/agent-team/lib/types/environment-check.d.ts +66 -0
- package/packages/agent-team/lib/types/environment-check.d.ts.map +1 -0
- package/packages/agent-team/lib/types/human-profile.d.ts +21 -27
- package/packages/agent-team/lib/types/human-profile.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +281 -18
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/installed-manifest.d.ts +16 -0
- package/packages/agent-team/lib/types/installed-manifest.d.ts.map +1 -0
- package/packages/agent-team/lib/types/invariant.d.ts +27 -5
- package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +12 -0
- 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 +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +19 -6
- package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/entities.d.ts +1 -8
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +94 -2
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +9 -0
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/client-agent-team/lib/client.js +1264 -383
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/EnvironmentCheck.d.ts +18 -0
- package/packages/client-agent-team/lib/types/client/EnvironmentCheck.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/EnvironmentCheck.js +58 -0
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +0 -5
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +2 -7
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts +26 -0
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.js +195 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +7 -11
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/attachment-preview.js +13 -3
- package/packages/client-agent-team/lib/types/client/context-judge.d.ts +64 -0
- package/packages/client-agent-team/lib/types/client/context-judge.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/context-judge.js +78 -0
- package/packages/client-agent-team/lib/types/client/environment-check.d.ts +65 -0
- package/packages/client-agent-team/lib/types/client/environment-check.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/environment-check.js +78 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +84 -11
- package/packages/client-agent-team/lib/types/client/judge-form.d.ts +99 -0
- package/packages/client-agent-team/lib/types/client/judge-form.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/judge-form.js +208 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts +70 -14
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +76 -14
- package/packages/tool-agent-team/README.md +6 -3
- package/packages/tool-agent-team/README.zh.md +6 -3
- package/packages/tool-agent-team/lib/context-tools.js +148 -164
- package/packages/tool-agent-team/lib/index.js +17 -2
- package/packages/tool-agent-team/lib/types/context-tools.d.ts +32 -7
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/pressure-policy.js +0 -230
- package/packages/agent-team/lib/types/pressure-policy.d.ts +0 -130
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts +0 -15
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +0 -115
|
@@ -50,14 +50,17 @@ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
|
50
50
|
import { scopeOf } from '@deepseek-ai/dsh-scope';
|
|
51
51
|
import { Session, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
52
52
|
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
|
|
53
|
-
import
|
|
54
|
-
import {
|
|
53
|
+
import z from '@deepseek-ai/schemastery';
|
|
54
|
+
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
55
|
+
import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, pathAttachmentId, readAttachment, removeAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeRequestScopedAttachment } from "./attachments.js";
|
|
56
|
+
import { reportEnvironment } from "./environment-check.js";
|
|
55
57
|
import { HUMAN_PROFILE_DEFAULT_NAME, HUMAN_PROFILE_REPO_URL, HUMAN_PROFILE_SETTINGS_NAMESPACE, HUMAN_PROFILE_SETTINGS_SCHEMA, HUMAN_PROFILE_VERSION, assertValidHumanName, normalizeHumanName, parseLegacyHumanProfile, planLegacyAdoption } from "./human-profile.js";
|
|
56
58
|
import { humanAvatarsRoot, readHumanAvatar, removeHumanAvatar, writeHumanAvatar } from "./human-avatar.js";
|
|
57
59
|
import { createHumanUpdateChecker } from "./human-update-check.js";
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
60
|
+
import { AgentTeamInvariantError } from "./invariant.js";
|
|
61
|
+
import { createTeamContextManagement, TeamPressurePolicy, TEAM_CONTEXT_CODEC, TEAM_PRESSURE_GATE_SCHEMA } from "./context-continuity-host.js";
|
|
62
|
+
import { CONTEXT_CONTINUITY_PROJECTION_KEY, DEFAULT_GATE_IDLE_MS, DEFAULT_GATE_TOKENS, readContextTimeline } from '@wowyuarm/dsh-context-continuity';
|
|
63
|
+
import { TeamContextJudge } from "./context-judge.js";
|
|
61
64
|
import { AGENT_TEAM_PLUGIN_ID, isAgentTeamSource } from "./context-source.js";
|
|
62
65
|
import { boundaryByRef, carriedInputOf, checkpointByRef, checkpointRefFor, createTeamContextProjectionConfig, createTeamContextProjectionDefinition, foldTeamContextProjection, retainedTopicsThrough, TeamContextProjectionHost } from "./context-projection.js";
|
|
63
66
|
import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
|
|
@@ -91,7 +94,8 @@ class PresetCompositionError extends Error {
|
|
|
91
94
|
const MAX_CHECKPOINT_NAME_CHARS = 120;
|
|
92
95
|
/** Default and maximum number of timeline items one query returns. */
|
|
93
96
|
const DEFAULT_TIMELINE_LIMIT = 12;
|
|
94
|
-
|
|
97
|
+
/** The widest timeline window there is; the ref gate reads at this width so no printed ref is unreachable. */
|
|
98
|
+
export const MAX_TIMELINE_LIMIT = 24;
|
|
95
99
|
/**
|
|
96
100
|
* Archived generations the timeline and seed resolution walk: the timeline
|
|
97
101
|
* reads this many ancestors behind the current generation, and the seed guard
|
|
@@ -104,6 +108,19 @@ const CONTEXT_HARD_LIMIT_CAP = 256_000;
|
|
|
104
108
|
const CONTEXT_HANDOFF_AT_CAP = 200_000;
|
|
105
109
|
const CONTEXT_HANDOFF_RESERVE = 8_000;
|
|
106
110
|
const CONTEXT_SAFE_OUTPUT_RESERVE = 16_000;
|
|
111
|
+
/**
|
|
112
|
+
* The budget Team gives the long-gap gate's judge when the row states none.
|
|
113
|
+
*
|
|
114
|
+
* The engine's own default is five seconds, which is sized for a judge beside
|
|
115
|
+
* the Host. A hosted endpoint answers in up to about four seconds, and the gate
|
|
116
|
+
* hands the judge one second less than this deadline, so that default turns a
|
|
117
|
+
* slow-but-working judge into a coin flip — and the deployment that meets this
|
|
118
|
+
* problem is the one least able to diagnose it. Ten seconds leaves room for the
|
|
119
|
+
* slow tail while still bounding the wait on the path that starts a turn. A
|
|
120
|
+
* deployment that wants another number still sets `gate.judgeTimeoutMs`, which
|
|
121
|
+
* stays a row field.
|
|
122
|
+
*/
|
|
123
|
+
const TEAM_JUDGE_TIMEOUT_DEFAULT_MS = 10_000;
|
|
107
124
|
/**
|
|
108
125
|
* Usage at or above which an acknowledged acceptance advises a fresh
|
|
109
126
|
* rollover instead of keeping the context; capped by the route's effective
|
|
@@ -171,6 +188,29 @@ export class AgentTeamDmDeliveryError extends Error {
|
|
|
171
188
|
this.name = 'AgentTeamDmDeliveryError';
|
|
172
189
|
}
|
|
173
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The judge's fields as one row form. Every one is volatile, because that is
|
|
193
|
+
* what makes the settings service offer them and what lets a key written in the
|
|
194
|
+
* Team settings surface mount the judge without restarting the Host; the key
|
|
195
|
+
* alone is a secret, so a read reports that it is set and never its value.
|
|
196
|
+
*/
|
|
197
|
+
const TEAM_JUDGE_SETTINGS_SCHEMA = z.object({
|
|
198
|
+
apiKey: z.string().role('secret').volatile().description('API key for the relatedness judge. Setting one enables jev-backed context management; clearing it disables it.'),
|
|
199
|
+
apiKeyEnv: z.string().volatile().description('Environment variable holding the key, when the key is not stated here. Default: TYPESAFE_API_KEY'),
|
|
200
|
+
model: z.string().volatile().description('Model id sent with every judgement. Default: the jev plugin\'s own default.'),
|
|
201
|
+
apiBase: z.string().volatile().description('Endpoint base the judgements go to. Default: the jev plugin\'s own default.'),
|
|
202
|
+
});
|
|
203
|
+
/**
|
|
204
|
+
* The Host row's whole schema: the profile fields verbatim, so their volatility
|
|
205
|
+
* and defaults stay owned by {@link HUMAN_PROFILE_SETTINGS_SCHEMA}, plus the
|
|
206
|
+
* gate and the judge. A patch layer that targets this row replaces its whole
|
|
207
|
+
* config, so the parts are declared together here and nowhere else.
|
|
208
|
+
*/
|
|
209
|
+
export const TEAM_HOST_ROW_SETTINGS_SCHEMA = z.object({
|
|
210
|
+
...HUMAN_PROFILE_SETTINGS_SCHEMA.dict,
|
|
211
|
+
gate: TEAM_PRESSURE_GATE_SCHEMA,
|
|
212
|
+
jev: TEAM_JUDGE_SETTINGS_SCHEMA,
|
|
213
|
+
});
|
|
174
214
|
let AgentTeam = (() => {
|
|
175
215
|
let _classSuper = TypertRemoteService;
|
|
176
216
|
let _instanceExtraInitializers = [];
|
|
@@ -195,6 +235,8 @@ let AgentTeam = (() => {
|
|
|
195
235
|
let _sendMessage_decorators;
|
|
196
236
|
let _putAttachment_decorators;
|
|
197
237
|
let _getAttachment_decorators;
|
|
238
|
+
let _contextJudgeForClient_decorators;
|
|
239
|
+
let _environmentForClient_decorators;
|
|
198
240
|
let _humanProfileForClient_decorators;
|
|
199
241
|
let _putHumanAvatar_decorators;
|
|
200
242
|
let _getHumanAvatar_decorators;
|
|
@@ -231,6 +273,8 @@ let AgentTeam = (() => {
|
|
|
231
273
|
_sendMessage_decorators = [Remote('sendMessage')];
|
|
232
274
|
_putAttachment_decorators = [Remote('putAttachment')];
|
|
233
275
|
_getAttachment_decorators = [Remote('getAttachment')];
|
|
276
|
+
_contextJudgeForClient_decorators = [Remote('contextJudge')];
|
|
277
|
+
_environmentForClient_decorators = [Remote('environment')];
|
|
234
278
|
_humanProfileForClient_decorators = [Remote('humanProfile')];
|
|
235
279
|
_putHumanAvatar_decorators = [Remote('putHumanAvatar')];
|
|
236
280
|
_getHumanAvatar_decorators = [Remote('getHumanAvatar')];
|
|
@@ -264,6 +308,8 @@ let AgentTeam = (() => {
|
|
|
264
308
|
__esDecorate(this, null, _sendMessage_decorators, { kind: "method", name: "sendMessage", static: false, private: false, access: { has: obj => "sendMessage" in obj, get: obj => obj.sendMessage }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
265
309
|
__esDecorate(this, null, _putAttachment_decorators, { kind: "method", name: "putAttachment", static: false, private: false, access: { has: obj => "putAttachment" in obj, get: obj => obj.putAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
266
310
|
__esDecorate(this, null, _getAttachment_decorators, { kind: "method", name: "getAttachment", static: false, private: false, access: { has: obj => "getAttachment" in obj, get: obj => obj.getAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
311
|
+
__esDecorate(this, null, _contextJudgeForClient_decorators, { kind: "method", name: "contextJudgeForClient", static: false, private: false, access: { has: obj => "contextJudgeForClient" in obj, get: obj => obj.contextJudgeForClient }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
312
|
+
__esDecorate(this, null, _environmentForClient_decorators, { kind: "method", name: "environmentForClient", static: false, private: false, access: { has: obj => "environmentForClient" in obj, get: obj => obj.environmentForClient }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
267
313
|
__esDecorate(this, null, _humanProfileForClient_decorators, { kind: "method", name: "humanProfileForClient", static: false, private: false, access: { has: obj => "humanProfileForClient" in obj, get: obj => obj.humanProfileForClient }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
268
314
|
__esDecorate(this, null, _putHumanAvatar_decorators, { kind: "method", name: "putHumanAvatar", static: false, private: false, access: { has: obj => "putHumanAvatar" in obj, get: obj => obj.putHumanAvatar }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
269
315
|
__esDecorate(this, null, _getHumanAvatar_decorators, { kind: "method", name: "getHumanAvatar", static: false, private: false, access: { has: obj => "getHumanAvatar" in obj, get: obj => obj.getHumanAvatar }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -279,7 +325,7 @@ let AgentTeam = (() => {
|
|
|
279
325
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
280
326
|
}
|
|
281
327
|
config = __runInitializers(this, _instanceExtraInitializers);
|
|
282
|
-
static Config =
|
|
328
|
+
static Config = TEAM_HOST_ROW_SETTINGS_SCHEMA;
|
|
283
329
|
static inject = [
|
|
284
330
|
'storageDomain',
|
|
285
331
|
'workspaceRegistry',
|
|
@@ -328,6 +374,16 @@ let AgentTeam = (() => {
|
|
|
328
374
|
*/
|
|
329
375
|
memberFailures = new Map();
|
|
330
376
|
pressurePolicy;
|
|
377
|
+
/**
|
|
378
|
+
* The gate thresholds the pressure policy reads, and the only copy of them:
|
|
379
|
+
* refreshed in place from the row's volatile config, never replaced, so the
|
|
380
|
+
* engine's per-step read sees a settings edit without a remount. It starts at
|
|
381
|
+
* Team's own judge budget and the constructor's refresh replaces that with the
|
|
382
|
+
* row's, so the budget is never absent at any point a step could read it.
|
|
383
|
+
*/
|
|
384
|
+
gate = { judgeTimeoutMs: TEAM_JUDGE_TIMEOUT_DEFAULT_MS };
|
|
385
|
+
/** The long-gap gate's judge: this Host's own `jev` mount, reconciled with the row. */
|
|
386
|
+
contextJudge;
|
|
331
387
|
notifiedInbox = new Map();
|
|
332
388
|
attachmentGcTimer;
|
|
333
389
|
/**
|
|
@@ -393,21 +449,38 @@ let AgentTeam = (() => {
|
|
|
393
449
|
constructor(ctx, config) {
|
|
394
450
|
super(ctx, 'agentTeam');
|
|
395
451
|
this.config = config;
|
|
396
|
-
this.
|
|
452
|
+
this.refreshGate();
|
|
453
|
+
this.contextJudge = new TeamContextJudge(this.ctx, {
|
|
454
|
+
config: () => this.judgeConfig(),
|
|
455
|
+
deadlineMs: () => this.gate.judgeTimeoutMs ?? TEAM_JUDGE_TIMEOUT_DEFAULT_MS,
|
|
456
|
+
});
|
|
457
|
+
this.pressurePolicy = new TeamPressurePolicy({
|
|
397
458
|
agentForMember: memberId => this.handles.get(memberId)?.agent,
|
|
398
|
-
memberForAgent: agent => {
|
|
399
|
-
const member = this.memberForAgent(agent);
|
|
400
|
-
return member === undefined ? undefined : { memberId: member.memberId, sessionId: agent.id };
|
|
401
|
-
},
|
|
402
|
-
compactionForAgent: agent => this.ctx.agentPresets.serviceFor(agent, 'compaction'),
|
|
403
459
|
limitsForAgent: agent => this.routeLimitsForAgent(agent),
|
|
460
|
+
compactionForAgent: agent => {
|
|
461
|
+
const engine = this.compactionForAgent(agent);
|
|
462
|
+
return engine === undefined ? undefined : { reduce: (reason, signal) => engine.compactIfNeeded(agent, reason, signal) };
|
|
463
|
+
},
|
|
404
464
|
activeClaimLabels: memberId => this.activeClaimLabels(memberId),
|
|
405
465
|
runningJobLabels: memberId => this.runningJobLabels(memberId),
|
|
406
|
-
|
|
466
|
+
// Deployment policy: the row's own gate config, whose omitted fields the
|
|
467
|
+
// engine answers with its own defaults. This object is this Host's one
|
|
468
|
+
// copy, refreshed in place on every settings write, so an edit reaches the
|
|
469
|
+
// next step judged instead of the next mount.
|
|
470
|
+
gate: this.gate,
|
|
471
|
+
/**
|
|
472
|
+
* The relatedness judge of the long-gap gate: the service this Host
|
|
473
|
+
* mounts from its own row's key, or one a deployment mounted itself. No
|
|
474
|
+
* key and no other mount leaves the gate off — a missing judge is a
|
|
475
|
+
* deployment choice, never a failure — and the read is lazy, so a key
|
|
476
|
+
* written after this Host mounted still answers on the next step.
|
|
477
|
+
*/
|
|
478
|
+
judgeForAgent: () => this.contextJudge.judge(),
|
|
479
|
+
failed: (memberId, diagnostic) => {
|
|
407
480
|
this.setMemberFailure(memberId, 'compaction', diagnostic);
|
|
408
481
|
this.emitAutoCompactionChanged(memberId);
|
|
409
482
|
},
|
|
410
|
-
log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
|
|
483
|
+
log: (message, memberId) => { this.ctx.logger.warn(`agent-team: ${message} (member ${this.memberLabel(memberId)})`); },
|
|
411
484
|
});
|
|
412
485
|
// Human profile: this Host row's own Config (name + avatarRef, both
|
|
413
486
|
// volatile), so the settings service derives its form from the schema and
|
|
@@ -420,7 +493,41 @@ let AgentTeam = (() => {
|
|
|
420
493
|
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, this.ctx.fiber));
|
|
421
494
|
this.adoptLegacyHumanProfile();
|
|
422
495
|
});
|
|
423
|
-
this.ctx.on('loader/volatile-update', () => {
|
|
496
|
+
this.ctx.on('loader/volatile-update', () => {
|
|
497
|
+
this.syncHumanHandle();
|
|
498
|
+
// The whole row is volatile, so every write the settings surface makes is
|
|
499
|
+
// one reconciliation. The gate comes first: the judge's own budget is
|
|
500
|
+
// derived from the gate's deadline, so a write that raises both must not
|
|
501
|
+
// be reconciled against the deadline it replaced.
|
|
502
|
+
this.refreshGate();
|
|
503
|
+
void this.contextJudge.sync().catch(error => {
|
|
504
|
+
this.ctx.logger.warn('agent-team: the relatedness judge could not be reconciled: %s', error instanceof Error ? error.message : String(error));
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
/** The Host row's judge settings, as one plain config for the mount. */
|
|
509
|
+
judgeConfig() {
|
|
510
|
+
return {
|
|
511
|
+
apiKey: this.config.jev.apiKey.get(),
|
|
512
|
+
apiKeyEnv: this.config.jev.apiKeyEnv.get(),
|
|
513
|
+
model: this.config.jev.model.get(),
|
|
514
|
+
apiBase: this.config.jev.apiBase.get(),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Copy the row's gate thresholds into the one gate object the pressure policy
|
|
519
|
+
* holds. Called once at construction and again on every volatile update, so
|
|
520
|
+
* the policy always reads the thresholds the settings document states.
|
|
521
|
+
*
|
|
522
|
+
* Two fields pass through as the row states them, and the engine answers an
|
|
523
|
+
* omitted one with its own default. The judge's budget does not: a judge is
|
|
524
|
+
* reached over a network, so Team answers for that number itself rather than
|
|
525
|
+
* making every deployment discover the engine's local-call default.
|
|
526
|
+
*/
|
|
527
|
+
refreshGate() {
|
|
528
|
+
this.gate.tokens = this.config.gate.tokens.get();
|
|
529
|
+
this.gate.idleMs = this.config.gate.idleMs.get();
|
|
530
|
+
this.gate.judgeTimeoutMs = this.config.gate.judgeTimeoutMs.get() ?? TEAM_JUDGE_TIMEOUT_DEFAULT_MS;
|
|
424
531
|
}
|
|
425
532
|
/** Current human display name; the single source for team_view and @ matching. */
|
|
426
533
|
humanHandle() {
|
|
@@ -445,7 +552,8 @@ let AgentTeam = (() => {
|
|
|
445
552
|
* The two judgements the Config schema cannot make about a Human name: the
|
|
446
553
|
* same non-empty floor as Member handles, plus global uniqueness against live
|
|
447
554
|
* Members. `setHumanProfile` runs it before the write, so a colliding rename
|
|
448
|
-
* rejects instead of persisting.
|
|
555
|
+
* rejects instead of persisting. It reads the one field it judges, so the
|
|
556
|
+
* caller passes the name alone rather than a whole profile shape.
|
|
449
557
|
*/
|
|
450
558
|
validateHumanProfile(value) {
|
|
451
559
|
const name = assertValidHumanName(value.name);
|
|
@@ -530,6 +638,10 @@ let AgentTeam = (() => {
|
|
|
530
638
|
}
|
|
531
639
|
/** Open the durable ledger and restore every enabled Member independently. */
|
|
532
640
|
async [Service.init]() {
|
|
641
|
+
// The judge is mounted from the row's own key before the first step can be
|
|
642
|
+
// judged: a deployment that states one gets the gate from its first turn,
|
|
643
|
+
// and one that states none keeps the gate off without a failure.
|
|
644
|
+
await this.contextJudge.sync();
|
|
533
645
|
// The continuity projection registers once per Host: the framework keeps
|
|
534
646
|
// one unit per projection key and drives it for every Session, so Team's
|
|
535
647
|
// fold is session-agnostic and the state carries the identity it folds.
|
|
@@ -542,10 +654,10 @@ let AgentTeam = (() => {
|
|
|
542
654
|
return;
|
|
543
655
|
const message = error instanceof Error ? error.message : String(error);
|
|
544
656
|
this.setMemberFailure(member.memberId, 'runtime', message);
|
|
545
|
-
const kind = classifyRecoverableError(
|
|
657
|
+
const kind = classifyRecoverableError(error);
|
|
546
658
|
if (kind !== undefined)
|
|
547
659
|
this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable ${kind} error; recording a consecutive error occurrence`);
|
|
548
|
-
this.recovery.onError(member.memberId,
|
|
660
|
+
this.recovery.onError(member.memberId, error);
|
|
549
661
|
this.emitMemberPresenceChanged(member);
|
|
550
662
|
});
|
|
551
663
|
this.ctx.on('agent/status', ({ agent, status }) => {
|
|
@@ -595,7 +707,7 @@ let AgentTeam = (() => {
|
|
|
595
707
|
// A successful assistant response ends any open provider-overflow
|
|
596
708
|
// recovery sequence for this Member.
|
|
597
709
|
if (event.type === 'assistant/message')
|
|
598
|
-
this.pressurePolicy.onAssistantMessage(
|
|
710
|
+
this.pressurePolicy.onAssistantMessage(memberId);
|
|
599
711
|
// Context management reacts only after a successful durable tool/result;
|
|
600
712
|
// the projection (not this listener) decides what that means.
|
|
601
713
|
this.contextManagement.onSessionEvent(memberId, handle.agent, event);
|
|
@@ -635,10 +747,35 @@ let AgentTeam = (() => {
|
|
|
635
747
|
const persistedSessions = new Set((await this.persistedSessionHeaders()).map(snapshot => snapshot.header.id));
|
|
636
748
|
await this.sweepWorkspaceParticipations(ledger);
|
|
637
749
|
for (const member of ledger.listMembers()) {
|
|
638
|
-
if (member.state === 'enabled')
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
750
|
+
if (member.state === 'enabled') {
|
|
751
|
+
// A renewal or rollover that committed but never finished leaves no
|
|
752
|
+
// live handle to reveal it: startup derives the outstanding effects
|
|
753
|
+
// from the ledger and the registry's archive set. A Workspace fault
|
|
754
|
+
// must not keep the Member from running on its recorded binding, so
|
|
755
|
+
// the ordinary activation path still runs when the retire half cannot
|
|
756
|
+
// be finished here.
|
|
757
|
+
try {
|
|
758
|
+
await this.finishMemberTransition(member.memberId, { knownSessions: persistedSessions });
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
this.ctx.logger.warn(`agent-team: could not finish the recorded Session transition for member '${member.handle}': ${error instanceof Error ? error.message : String(error)}`);
|
|
762
|
+
}
|
|
763
|
+
if (!this.handles.has(member.memberId))
|
|
764
|
+
await this.activateMember(member, undefined, persistedSessions);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
// Every other durable state converges through the same effect table
|
|
768
|
+
// its operation runs after the commit, so a crash or a failed call
|
|
769
|
+
// between commit and effect is finished here. One Member's fault
|
|
770
|
+
// must not fail the boot: it is logged and retried at the next
|
|
771
|
+
// startup, where the call that failed may have healed.
|
|
772
|
+
try {
|
|
773
|
+
await this.convergeMemberEffects(member);
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
this.ctx.logger.warn(`agent-team: could not converge member '${member.handle}' to its '${member.state}' state: ${error instanceof Error ? error.message : String(error)}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
642
779
|
}
|
|
643
780
|
}
|
|
644
781
|
/**
|
|
@@ -828,13 +965,13 @@ let AgentTeam = (() => {
|
|
|
828
965
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored), workspaceIds: ledger.workspacesOf(stored.memberId) });
|
|
829
966
|
});
|
|
830
967
|
}
|
|
831
|
-
/** Commit suspended intent, then
|
|
968
|
+
/** Commit suspended intent, then run the suspended row of the effect table: the owned AgentHandle is disposed, the Session stays. */
|
|
832
969
|
async suspendMember(request) {
|
|
833
970
|
return this.enqueueLifecycle(async () => {
|
|
834
971
|
const result = await this.requireLedger().suspendMember({ ...request, actor: agentTeamHumanActor() });
|
|
835
972
|
if (result.committed)
|
|
836
973
|
this.emitCommitted(result.value.receipt);
|
|
837
|
-
await this.
|
|
974
|
+
await this.convergeMemberEffects(result.value.member);
|
|
838
975
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
|
|
839
976
|
});
|
|
840
977
|
}
|
|
@@ -860,7 +997,9 @@ let AgentTeam = (() => {
|
|
|
860
997
|
* continuation prompt into its live session, rebuild it after an orphaned
|
|
861
998
|
* preset composition, or re-run activation when no live session exists.
|
|
862
999
|
* Runtime-only — no ledger operation, no suspend. Taking over manually also
|
|
863
|
-
* cancels any pending automatic recovery episode.
|
|
1000
|
+
* cancels any pending automatic recovery episode. A non-enabled Member
|
|
1001
|
+
* still holding a live handle is an interrupted suspend/archive/remove,
|
|
1002
|
+
* not a stalled one; recovery finishes that cleanup instead of nudging it.
|
|
864
1003
|
*/
|
|
865
1004
|
async recoverMember(request) {
|
|
866
1005
|
this.requireAccepting();
|
|
@@ -868,9 +1007,36 @@ let AgentTeam = (() => {
|
|
|
868
1007
|
if (member === undefined || !this.requireLedger().participatesIn(request.memberId, request.workspaceId))
|
|
869
1008
|
throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
|
|
870
1009
|
this.recovery.stopTracking(request.memberId);
|
|
1010
|
+
const handle = this.handles.get(request.memberId);
|
|
1011
|
+
// A committed renewal or rollover whose retire or activation never finished
|
|
1012
|
+
// is what leaves a Member with no live generation on its binding, or with
|
|
1013
|
+
// the retired generation still live: this explicit recovery path finishes
|
|
1014
|
+
// that recorded transition rather than restarting a Session the ledger has
|
|
1015
|
+
// already moved off. Every other stranded case keeps its old behavior.
|
|
1016
|
+
const stranded = handle === undefined || handle.agent.id !== member.sessionId;
|
|
1017
|
+
if (stranded && member.state === 'enabled' && this.requireLedger().lastTransitionForMember(request.memberId) !== undefined) {
|
|
1018
|
+
this.ctx.logger.info(`agent-team: finishing the recorded Session transition for member '${member.handle}'`);
|
|
1019
|
+
const settled = await this.enqueueLifecycle(async () => {
|
|
1020
|
+
await this.finishMemberTransition(request.memberId);
|
|
1021
|
+
return this.requireLedger().getMember(request.memberId) ?? member;
|
|
1022
|
+
});
|
|
1023
|
+
return Object.freeze({ status: this.memberStatus(settled) });
|
|
1024
|
+
}
|
|
1025
|
+
// A live handle on a non-enabled Member is the residue of a suspend,
|
|
1026
|
+
// archive, or removal whose disposal failed after its commit: the
|
|
1027
|
+
// durable state already says no live generation, so explicit recovery
|
|
1028
|
+
// re-runs that state's effects rather than feeding the stray generation
|
|
1029
|
+
// a resume prompt or rebuilding it as if it were enabled.
|
|
1030
|
+
if (member.state !== 'enabled' && handle !== undefined) {
|
|
1031
|
+
this.ctx.logger.info(`agent-team: finishing the interrupted ${member.state} cleanup for member '${member.handle}'`);
|
|
1032
|
+
const settled = await this.enqueueLifecycle(async () => {
|
|
1033
|
+
await this.convergeMemberEffects(member);
|
|
1034
|
+
return this.requireLedger().getMember(request.memberId) ?? member;
|
|
1035
|
+
});
|
|
1036
|
+
return Object.freeze({ status: this.memberStatus(settled) });
|
|
1037
|
+
}
|
|
871
1038
|
// An orphaned composition cannot be steered: its tools are gone, so a
|
|
872
1039
|
// continuation prompt reaches an inert Member. Rebuild the Agent in place.
|
|
873
|
-
const handle = this.handles.get(request.memberId);
|
|
874
1040
|
if (handle !== undefined && this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
|
|
875
1041
|
this.ctx.logger.info(`agent-team: rebuilding member '${member.handle}' after its preset composition was orphaned by a reload`);
|
|
876
1042
|
await this.reactivateMember(request.memberId);
|
|
@@ -914,27 +1080,50 @@ let AgentTeam = (() => {
|
|
|
914
1080
|
throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
|
|
915
1081
|
if (stored.state !== 'enabled')
|
|
916
1082
|
throw new Error(`Agent Member '${stored.handle}' is ${stored.state}; only enabled Members can start from a new context`);
|
|
917
|
-
const active = this.handles.get(request.memberId);
|
|
918
|
-
if (active === undefined)
|
|
919
|
-
throw new Error(`Agent Member '${stored.handle}' has no active session to clear`);
|
|
920
|
-
if (this.runningAgents.has(active.agent.id))
|
|
921
|
-
throw new Error(`Agent Member '${stored.handle}' is still running; wait for the current turn to end before starting from a new context`);
|
|
922
|
-
const previousSessionId = stored.sessionId;
|
|
923
1083
|
// The fresh id derives from the requestId, so a retried identical
|
|
924
1084
|
// request mints the same id and the ledger dedupes it instead of
|
|
925
1085
|
// colliding; the format matches addMember's `agent-team-<uuid>`.
|
|
926
1086
|
const sessionId = SessionId(`agent-team-${request.requestId}`);
|
|
1087
|
+
// A renewal this request already committed IS the desired binding: its
|
|
1088
|
+
// retire or activation effects may still be outstanding, so finish them
|
|
1089
|
+
// first, then answer with the recorded result either way. A repeat never
|
|
1090
|
+
// commits a second binding, and never reports an effect that did not run
|
|
1091
|
+
// as though it had — the status below carries that truth.
|
|
1092
|
+
if (this.requireLedger().hasCommitted(request.requestId)) {
|
|
1093
|
+
try {
|
|
1094
|
+
await this.finishMemberTransition(request.memberId);
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
this.ctx.logger.warn(`agent-team: could not finish the recorded context renewal for member '${stored.handle}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1098
|
+
}
|
|
1099
|
+
const recorded = await this.requireLedger().renewMemberSession({ ...request, sessionId, actor: agentTeamHumanActor() });
|
|
1100
|
+
const current = this.requireLedger().getMember(request.memberId) ?? stored;
|
|
1101
|
+
return Object.freeze({ receipt: recorded.value.receipt, status: this.memberStatus(current) });
|
|
1102
|
+
}
|
|
1103
|
+
const active = this.handles.get(request.memberId);
|
|
1104
|
+
if (active === undefined) {
|
|
1105
|
+
// With no live generation to retire, an earlier transition may be what
|
|
1106
|
+
// left the Member here; finishing it is what lets this request run at
|
|
1107
|
+
// all. With none, the ordinary no-session refusal below stands.
|
|
1108
|
+
await this.finishMemberTransition(request.memberId);
|
|
1109
|
+
}
|
|
1110
|
+
const retiring = this.handles.get(request.memberId);
|
|
1111
|
+
if (retiring === undefined)
|
|
1112
|
+
throw new Error(`Agent Member '${stored.handle}' has no active session to clear`);
|
|
1113
|
+
if (this.runningAgents.has(retiring.agent.id))
|
|
1114
|
+
throw new Error(`Agent Member '${stored.handle}' is still running; wait for the current turn to end before starting from a new context`);
|
|
927
1115
|
const result = await this.requireLedger().renewMemberSession({ ...request, sessionId, actor: agentTeamHumanActor() });
|
|
928
|
-
if (result.committed)
|
|
929
|
-
this
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
|
|
1116
|
+
if (!result.committed) {
|
|
1117
|
+
// Another caller recorded this requestId between the check above and
|
|
1118
|
+
// the write; report its recorded outcome as the retry path does.
|
|
1119
|
+
const current = this.requireLedger().getMember(request.memberId) ?? stored;
|
|
1120
|
+
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(current) });
|
|
934
1121
|
}
|
|
1122
|
+
this.emitCommitted(result.value.receipt);
|
|
935
1123
|
const renewed = result.value.member;
|
|
936
|
-
|
|
937
|
-
|
|
1124
|
+
// This request retired the previous generation itself, so the activation
|
|
1125
|
+
// is planned rather than recovered: pass the fork parent it just left.
|
|
1126
|
+
await this.finishMemberTransition(renewed.memberId, { forkedFrom: stored.sessionId });
|
|
938
1127
|
const reactivated = this.handles.get(request.memberId);
|
|
939
1128
|
if (reactivated === undefined) {
|
|
940
1129
|
// Reactivation failed; the activation diagnostic carries the reason and
|
|
@@ -944,6 +1133,79 @@ let AgentTeam = (() => {
|
|
|
944
1133
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(renewed) });
|
|
945
1134
|
});
|
|
946
1135
|
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Finish one Member's committed Session transition, whichever half a failure
|
|
1138
|
+
* interrupted. The ledger already records the binding the Member should run
|
|
1139
|
+
* and the Session it left, so only external effects remain and each is
|
|
1140
|
+
* derived fresh: a generation still live on the old Session is retired, the
|
|
1141
|
+
* retired Session is archived once the registry does not yet report it
|
|
1142
|
+
* archived, and the target activates when no live generation runs it.
|
|
1143
|
+
*
|
|
1144
|
+
* A live handle alone never decides completion — disposal can succeed and
|
|
1145
|
+
* leave the archive half owed — so the registry's own archive set is the
|
|
1146
|
+
* evidence for that step.
|
|
1147
|
+
*
|
|
1148
|
+
* `forkedFrom` marks a PLANNED in-process activation (the caller just
|
|
1149
|
+
* retired that Session in this same request): `activateMember` reads its
|
|
1150
|
+
* absence as a recovered Session that may never have materialized and runs
|
|
1151
|
+
* the handoff-reconstruction path. Startup, retry, and explicit recovery
|
|
1152
|
+
* therefore leave it unset; only the request that committed the transition
|
|
1153
|
+
* supplies it.
|
|
1154
|
+
*
|
|
1155
|
+
* Callers must already hold the lifecycle queue (or run before any other
|
|
1156
|
+
* lifecycle work): this method does not re-enter it.
|
|
1157
|
+
*/
|
|
1158
|
+
async finishMemberTransition(memberId, options) {
|
|
1159
|
+
const member = this.requireLedger().getMember(memberId);
|
|
1160
|
+
if (member === undefined || member.state !== 'enabled')
|
|
1161
|
+
return;
|
|
1162
|
+
const transition = this.requireLedger().lastTransitionForMember(memberId);
|
|
1163
|
+
if (transition === undefined)
|
|
1164
|
+
return;
|
|
1165
|
+
const live = this.handles.get(memberId);
|
|
1166
|
+
if (live !== undefined && live.agent.id !== member.sessionId) {
|
|
1167
|
+
await this.retireMemberGeneration(memberId, live, live.agent.id);
|
|
1168
|
+
}
|
|
1169
|
+
if (!this.ctx.workspaceRegistry.archivedSessionIds.includes(transition.previousSessionId)) {
|
|
1170
|
+
await this.ctx.workspaceRegistry.archiveSession(transition.previousSessionId);
|
|
1171
|
+
}
|
|
1172
|
+
if (this.handles.get(memberId) === undefined) {
|
|
1173
|
+
await this.activateMember(member, undefined, options?.knownSessions, options?.forkedFrom);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Converge one Member's external effects onto its durable lifecycle state
|
|
1178
|
+
* — the single effect table every lifecycle operation runs right after its
|
|
1179
|
+
* commit and startup re-runs for every non-enabled Member:
|
|
1180
|
+
*
|
|
1181
|
+
* - `suspended`: no live handle; the Session and private memory stay.
|
|
1182
|
+
* - `archived`: no live handle; the Session is hidden from grouping
|
|
1183
|
+
* surfaces while its log and private memory stay on disk.
|
|
1184
|
+
* - `inactive`: no live handle; the Session is archived and the private
|
|
1185
|
+
* memory deleted — removeMember's cleanup primitives.
|
|
1186
|
+
*
|
|
1187
|
+
* `enabled` has no entry here: its effects belong to the recorded Session
|
|
1188
|
+
* transition and activation. Every step is derived from the durable state
|
|
1189
|
+
* and safe to repeat — the registry's archive set decides whether the
|
|
1190
|
+
* hide half is owed, disposal tolerates an absent handle, and removal
|
|
1191
|
+
* cleanup force-tolerates an already-cleaned target — so a repeat after a
|
|
1192
|
+
* crash or a failed call runs exactly the owed steps and never a second
|
|
1193
|
+
* durable write. Callers must already hold the lifecycle queue (or run
|
|
1194
|
+
* before any other lifecycle work, as startup does).
|
|
1195
|
+
*/
|
|
1196
|
+
async convergeMemberEffects(member) {
|
|
1197
|
+
if (member.state === 'enabled')
|
|
1198
|
+
return;
|
|
1199
|
+
await this.disposeMemberSession(member.memberId, member);
|
|
1200
|
+
if (member.state === 'archived') {
|
|
1201
|
+
if (!this.ctx.workspaceRegistry.archivedSessionIds.includes(member.sessionId)) {
|
|
1202
|
+
await this.ctx.workspaceRegistry.archiveSession(member.sessionId);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
else if (member.state === 'inactive') {
|
|
1206
|
+
await this.memberRuntime.cleanupRemovedMember(member);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
947
1209
|
/**
|
|
948
1210
|
* Retire one Member's previous generation after its durable binding moved
|
|
949
1211
|
* onto a new Session id: drop the old handle's transient state, dispose the
|
|
@@ -1173,8 +1435,7 @@ let AgentTeam = (() => {
|
|
|
1173
1435
|
const result = await this.requireLedger().removeMember({ ...request, actor: agentTeamHumanActor() });
|
|
1174
1436
|
if (result.committed)
|
|
1175
1437
|
this.emitCommitted(result.value.receipt);
|
|
1176
|
-
await this.
|
|
1177
|
-
await this.memberRuntime.cleanupRemovedMember(result.value.member);
|
|
1438
|
+
await this.convergeMemberEffects(result.value.member);
|
|
1178
1439
|
return result.value;
|
|
1179
1440
|
});
|
|
1180
1441
|
}
|
|
@@ -1189,8 +1450,7 @@ let AgentTeam = (() => {
|
|
|
1189
1450
|
const result = await this.requireLedger().archiveMember({ ...request, actor: agentTeamHumanActor() });
|
|
1190
1451
|
if (result.committed)
|
|
1191
1452
|
this.emitCommitted(result.value.receipt);
|
|
1192
|
-
await this.
|
|
1193
|
-
await this.ctx.workspaceRegistry.archiveSession(result.value.member.sessionId);
|
|
1453
|
+
await this.convergeMemberEffects(result.value.member);
|
|
1194
1454
|
return result.value;
|
|
1195
1455
|
});
|
|
1196
1456
|
}
|
|
@@ -1252,15 +1512,18 @@ let AgentTeam = (() => {
|
|
|
1252
1512
|
/**
|
|
1253
1513
|
* Resolve uploaded ids and agent-supplied absolute paths into one attachment
|
|
1254
1514
|
* metadata list. Paths are all validated before anything is copied, so one
|
|
1255
|
-
* rejection leaves the cache untouched and the message uncommitted.
|
|
1515
|
+
* rejection leaves the cache untouched and the message uncommitted. Each copy
|
|
1516
|
+
* takes the id its request derives, so a retry converges on the entry the
|
|
1517
|
+
* first attempt prepared instead of writing an orphan.
|
|
1256
1518
|
*/
|
|
1257
1519
|
async resolveMessageAttachments(request) {
|
|
1258
1520
|
const fromPaths = [];
|
|
1259
1521
|
if (request.attachmentPaths !== undefined && request.attachmentPaths.length > 0) {
|
|
1260
1522
|
for (const absolutePath of request.attachmentPaths)
|
|
1261
1523
|
await validatePathAttachment(absolutePath);
|
|
1262
|
-
for (const absolutePath of request.attachmentPaths)
|
|
1263
|
-
fromPaths.push(Object.freeze(await copyPathAttachment(attachmentsRoot(), absolutePath)));
|
|
1524
|
+
for (const [index, absolutePath] of request.attachmentPaths.entries()) {
|
|
1525
|
+
fromPaths.push(Object.freeze(await copyPathAttachment(attachmentsRoot(), absolutePath, pathAttachmentId(request.requestId, index), request.requestId)));
|
|
1526
|
+
}
|
|
1264
1527
|
}
|
|
1265
1528
|
return [...fromPaths, ...await this.prepareAttachments(request.attachments)];
|
|
1266
1529
|
}
|
|
@@ -1288,7 +1551,12 @@ let AgentTeam = (() => {
|
|
|
1288
1551
|
const lines = metadata.map(attachment => `[attachment] ${attachmentPayloadPath(attachment.attachmentId, attachment.name)}`);
|
|
1289
1552
|
return `${trimmed}\n${lines.join('\n')}`;
|
|
1290
1553
|
}
|
|
1291
|
-
/**
|
|
1554
|
+
/**
|
|
1555
|
+
* Upload one composer attachment into the cache; bytes are immutable once
|
|
1556
|
+
* written. The requestId is the idempotency key: a retried upload derives
|
|
1557
|
+
* the same cache id and replays the original result, while reusing that key
|
|
1558
|
+
* for a different payload is refused as a request collision.
|
|
1559
|
+
*/
|
|
1292
1560
|
async putAttachment(request) {
|
|
1293
1561
|
this.requireAccepting();
|
|
1294
1562
|
this.requireWorkspace(request.workspaceId);
|
|
@@ -1298,16 +1566,45 @@ let AgentTeam = (() => {
|
|
|
1298
1566
|
if (bytes.byteLength > ATTACHMENT_MAX_BYTES)
|
|
1299
1567
|
throw new Error(`attachment exceeds the ${ATTACHMENT_MAX_BYTES} byte limit`);
|
|
1300
1568
|
const mediaType = sanitizeMediaType(request.mediaType);
|
|
1301
|
-
|
|
1302
|
-
return Object.freeze(await writeAttachment(attachmentsRoot(), attachmentId, request.name, mediaType, bytes));
|
|
1569
|
+
return Object.freeze(await writeRequestScopedAttachment(attachmentsRoot(), request.requestId, request.name, mediaType, bytes));
|
|
1303
1570
|
}
|
|
1304
|
-
/**
|
|
1571
|
+
/**
|
|
1572
|
+
* Read one cached attachment back for client display. A gone entry is the
|
|
1573
|
+
* one expected failure on this path, so it carries the stable
|
|
1574
|
+
* `team/attachment-not-found` code the Client branches on; anything else
|
|
1575
|
+
* (unreadable metadata, filesystem trouble) stays an unknown Host error.
|
|
1576
|
+
*/
|
|
1305
1577
|
async getAttachment(request) {
|
|
1306
1578
|
const stored = await readAttachment(attachmentsRoot(), request.attachmentId);
|
|
1307
1579
|
if (stored === undefined)
|
|
1308
|
-
throw new
|
|
1580
|
+
throw new RemoteError('team/attachment-not-found', `attachment '${request.attachmentId}' is no longer cached`, { attachmentId: request.attachmentId });
|
|
1309
1581
|
return Object.freeze({ name: stored.name, mediaType: stored.mediaType, byteSize: stored.byteSize, bytesBase64: stored.bytes.toString('base64') });
|
|
1310
1582
|
}
|
|
1583
|
+
/**
|
|
1584
|
+
* The long-gap gate's judge, as the Team settings surface states it: whether
|
|
1585
|
+
* a judge is reachable, why not when none is, and the thresholds in force.
|
|
1586
|
+
* The judge's own fields (key presence, endpoint, model) ride the settings
|
|
1587
|
+
* document this row already publishes, so nothing here restates them; the
|
|
1588
|
+
* gate's thresholds are row configuration the settings document deliberately
|
|
1589
|
+
* omits, which is why the Host resolves an omitted field here rather than
|
|
1590
|
+
* letting a second copy of those numbers exist. Two of the three fall back to
|
|
1591
|
+
* the engine's own default; the judge's budget falls back to Team's, because
|
|
1592
|
+
* that is the number the gate itself is given.
|
|
1593
|
+
*/
|
|
1594
|
+
contextJudgeForClient(_request) {
|
|
1595
|
+
const status = this.contextJudge.status();
|
|
1596
|
+
const gate = this.gate;
|
|
1597
|
+
return Object.freeze({
|
|
1598
|
+
...status,
|
|
1599
|
+
gate: Object.freeze({
|
|
1600
|
+
tokens: gate.tokens ?? DEFAULT_GATE_TOKENS,
|
|
1601
|
+
idleMs: gate.idleMs ?? DEFAULT_GATE_IDLE_MS,
|
|
1602
|
+
// No fallback here: this number is the one the gate is given, so a
|
|
1603
|
+
// second default could only ever disagree with the gate in force.
|
|
1604
|
+
judgeTimeoutMs: gate.judgeTimeoutMs,
|
|
1605
|
+
}),
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1311
1608
|
/**
|
|
1312
1609
|
* Human profile read for the settings page and footnote: name + avatar
|
|
1313
1610
|
* reference from this Host row's live Config plus version facts. Human-scoped
|
|
@@ -1316,6 +1613,26 @@ let AgentTeam = (() => {
|
|
|
1316
1613
|
* `updateAvailable` is false until a background refresh actually observes a
|
|
1317
1614
|
* newer published release.
|
|
1318
1615
|
*/
|
|
1616
|
+
/**
|
|
1617
|
+
* Local environment check for the settings page: which DSH line this Host
|
|
1618
|
+
* runs against, and whether that line is inside the range this bundle
|
|
1619
|
+
* declares. Human-scoped and read-only — the Client renders the verdict and
|
|
1620
|
+
* states the range in words, and nothing here is written back.
|
|
1621
|
+
*
|
|
1622
|
+
* Synchronous like `humanProfile`: both read the installed manifest once and
|
|
1623
|
+
* settle immediately, and the verdict comes from the Harness's own
|
|
1624
|
+
* compatibility evaluator rather than a second version comparison written
|
|
1625
|
+
* here.
|
|
1626
|
+
*
|
|
1627
|
+
* The wire type and `EnvironmentReport` keep separate top-level field lists on
|
|
1628
|
+
* purpose: the contract states every fact as optional because a Transport may
|
|
1629
|
+
* withhold any of them, while the report is the stricter thing the checker
|
|
1630
|
+
* guarantees. This assignment is where the compiler holds the two together,
|
|
1631
|
+
* and a new required contract field fails it here.
|
|
1632
|
+
*/
|
|
1633
|
+
environmentForClient(_request) {
|
|
1634
|
+
return Object.freeze(reportEnvironment());
|
|
1635
|
+
}
|
|
1319
1636
|
humanProfileForClient(_request) {
|
|
1320
1637
|
const profile = this.humanProfile();
|
|
1321
1638
|
const update = this.humanUpdateCheck.snapshot();
|
|
@@ -1515,8 +1832,8 @@ let AgentTeam = (() => {
|
|
|
1515
1832
|
const taskBoundaryThreshold = Math.min(ACCEPT_TASK_BOUNDARY_THRESHOLD, limits.handoffAt);
|
|
1516
1833
|
if (limits.usageTokens >= limits.handoffAt) {
|
|
1517
1834
|
return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
|
|
1518
|
-
action: '
|
|
1519
|
-
guidance: 'You are at or above the handoff budget. Finish the current atomic action and unsettled evidence, then call
|
|
1835
|
+
action: 'compact-now',
|
|
1836
|
+
guidance: 'You are at or above the handoff budget. Finish the current atomic action and unsettled evidence, then call context_compact with a summary you write yourself — that shortens this generation in place, keeps your recent work verbatim, and is the default. Roll over only when the work has turned a page.' });
|
|
1520
1837
|
}
|
|
1521
1838
|
if (limits.usageTokens >= taskBoundaryThreshold) {
|
|
1522
1839
|
return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
|
|
@@ -1946,6 +2263,16 @@ let AgentTeam = (() => {
|
|
|
1946
2263
|
this.ctx.logger.info(`agent-team: redelivered ${redelivered} carried input message(s) for member '${this.memberLabel(member.memberId)}' after the rollover crash recovery`);
|
|
1947
2264
|
return redelivered;
|
|
1948
2265
|
}
|
|
2266
|
+
/**
|
|
2267
|
+
* The compaction capability in one Member Agent's preset scope, or undefined
|
|
2268
|
+
* when its composition mounts none. Both the pressure policy (the hard limit)
|
|
2269
|
+
* and the engine's `context_compact` tool ask through here, so which realm
|
|
2270
|
+
* holds the engine is stated once: a preset revision publishes services
|
|
2271
|
+
* behind its own isolate, which `agent.ctx.get` cannot see from outside.
|
|
2272
|
+
*/
|
|
2273
|
+
compactionForAgent(agent) {
|
|
2274
|
+
return this.ctx.agentPresets.serviceFor(agent, 'compaction');
|
|
2275
|
+
}
|
|
1949
2276
|
/**
|
|
1950
2277
|
* Agent-only checkpoint request validation: the tool calls this inside its
|
|
1951
2278
|
* own running turn. Like `context_rollover`, the tool performs no side effect —
|
|
@@ -1981,10 +2308,10 @@ let AgentTeam = (() => {
|
|
|
1981
2308
|
async contextTimelineForAgent(agent, request) {
|
|
1982
2309
|
const member = this.memberForAgent(agent);
|
|
1983
2310
|
if (member === undefined || member.state !== 'enabled')
|
|
1984
|
-
throw new Error('
|
|
2311
|
+
throw new Error('context_status requires an active Team Member');
|
|
1985
2312
|
const limit = request.limit === undefined ? DEFAULT_TIMELINE_LIMIT : Math.trunc(request.limit);
|
|
1986
2313
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_TIMELINE_LIMIT)
|
|
1987
|
-
throw new Error(`
|
|
2314
|
+
throw new Error(`context_status limit must be between 1 and ${MAX_TIMELINE_LIMIT}`);
|
|
1988
2315
|
const meter = agent.ctx.get('tokenMeter');
|
|
1989
2316
|
const measurement = meter?.measure(agent.session);
|
|
1990
2317
|
const usageTokens = measurement?.totalTokens ?? 0;
|
|
@@ -2199,7 +2526,7 @@ let AgentTeam = (() => {
|
|
|
2199
2526
|
if (request.checkpointRef === undefined)
|
|
2200
2527
|
return { mode: 'fresh' };
|
|
2201
2528
|
if (!/^(context-checkpoint-[0-9a-f]{64}|team-boundary-[0-9a-f]{64})$/.test(request.checkpointRef))
|
|
2202
|
-
throw new Error('checkpointRef must be an opaque ref exactly as returned by
|
|
2529
|
+
throw new Error('checkpointRef must be an opaque ref exactly as returned by context_status');
|
|
2203
2530
|
// Full current-state prevalidation through the ONE resolver the swap
|
|
2204
2531
|
// itself uses: a ref that is fabricated, unattributable, nonshrinking,
|
|
2205
2532
|
// unmeasurable, over-budget, or blocked by multiple active Claims
|
|
@@ -2252,25 +2579,69 @@ let AgentTeam = (() => {
|
|
|
2252
2579
|
}
|
|
2253
2580
|
/** Shared Task-creation commit: resolve uploads into metadata lines and append through the ledger. */
|
|
2254
2581
|
async sendMessageAs(actor, request) {
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2582
|
+
try {
|
|
2583
|
+
const metadata = await this.resolveMessageAttachments(request);
|
|
2584
|
+
const result = await this.requireLedger().sendMessage({
|
|
2585
|
+
...request, body: this.appendAttachmentLines(request.body, metadata),
|
|
2586
|
+
...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
|
|
2587
|
+
actor,
|
|
2588
|
+
});
|
|
2589
|
+
this.emitCommittedOutcome(result);
|
|
2590
|
+
return result.value;
|
|
2591
|
+
}
|
|
2592
|
+
catch (error) {
|
|
2593
|
+
await this.dropUnreferencedPrepared(request, error);
|
|
2594
|
+
throw error;
|
|
2595
|
+
}
|
|
2263
2596
|
}
|
|
2264
2597
|
/** Shared existing-Thread reply commit: same upload resolution and outcome emission. */
|
|
2265
2598
|
async replyAs(actor, request) {
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2599
|
+
try {
|
|
2600
|
+
const metadata = await this.resolveMessageAttachments(request);
|
|
2601
|
+
const result = await this.requireLedger().reply({
|
|
2602
|
+
...request, body: this.appendAttachmentLines(request.body, metadata),
|
|
2603
|
+
...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
|
|
2604
|
+
actor,
|
|
2605
|
+
});
|
|
2606
|
+
this.emitCommittedOutcome(result);
|
|
2607
|
+
return result.value;
|
|
2608
|
+
}
|
|
2609
|
+
catch (error) {
|
|
2610
|
+
await this.dropUnreferencedPrepared(request, error);
|
|
2611
|
+
throw error;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* After a failed message or reply attempt, remove the cache entries this
|
|
2616
|
+
* attempt could have prepared — the request-stable path copies — unless a
|
|
2617
|
+
* committed Message references them. The ledger's own reference set is the
|
|
2618
|
+
* guard, so a post-commit failure or a colliding retry can never delete bytes
|
|
2619
|
+
* a stored Message still points at; uploads are untouched, because an
|
|
2620
|
+
* unreferenced upload is a legitimate state the GC owns. Cleanup failures are
|
|
2621
|
+
* logged and never mask the original error.
|
|
2622
|
+
*/
|
|
2623
|
+
async dropUnreferencedPrepared(request, cause) {
|
|
2624
|
+
const prepared = (request.attachmentPaths ?? []).map((_, index) => pathAttachmentId(request.requestId, index));
|
|
2625
|
+
if (prepared.length === 0)
|
|
2626
|
+
return;
|
|
2627
|
+
let referenced;
|
|
2628
|
+
try {
|
|
2629
|
+
referenced = this.requireLedger().referencedAttachmentIds();
|
|
2630
|
+
}
|
|
2631
|
+
catch (failure) {
|
|
2632
|
+
this.ctx.logger.warn(`agent-team: could not consult the attachment reference set after a failed message attempt (${String(cause)}): ${String(failure)}`);
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
for (const attachmentId of prepared) {
|
|
2636
|
+
if (referenced.has(attachmentId))
|
|
2637
|
+
continue;
|
|
2638
|
+
try {
|
|
2639
|
+
await removeAttachment(attachmentsRoot(), attachmentId);
|
|
2640
|
+
}
|
|
2641
|
+
catch (failure) {
|
|
2642
|
+
this.ctx.logger.warn(`agent-team: could not drop unreferenced prepared attachment '${attachmentId}' after a failed message attempt: ${String(failure)}`);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2274
2645
|
}
|
|
2275
2646
|
/** Fence one Human Remote call: accepting Host, known Workspace, Human actor. */
|
|
2276
2647
|
humanCall(workspaceId) {
|
|
@@ -2391,12 +2762,26 @@ let AgentTeam = (() => {
|
|
|
2391
2762
|
this.contextManagement.captureClaimedInput(agent, messages);
|
|
2392
2763
|
return { kind: 'reject' };
|
|
2393
2764
|
}
|
|
2394
|
-
//
|
|
2765
|
+
// The engine's pressure policy rides the same pre-step seam after the
|
|
2395
2766
|
// admission gate: the handoff-budget notice steers into the running
|
|
2396
|
-
// turn,
|
|
2767
|
+
// turn, the hard limit forces a reduction before the request is
|
|
2397
2768
|
// forwarded — failing closed blocks the step instead of submitting
|
|
2398
|
-
// over the
|
|
2399
|
-
|
|
2769
|
+
// over the limit — and a step arriving into a large context after a
|
|
2770
|
+
// long gap may be held instead of admitted.
|
|
2771
|
+
const member = this.memberForAgent(agent);
|
|
2772
|
+
const pressure = member === undefined
|
|
2773
|
+
? { kind: 'continue' }
|
|
2774
|
+
: await this.pressurePolicy.onPreStep(member.memberId, messages, signal);
|
|
2775
|
+
if (pressure.kind === 'hold') {
|
|
2776
|
+
// The gate kept the messages this step had claimed, and its one
|
|
2777
|
+
// rollover instruction is already steered into their place. Arming
|
|
2778
|
+
// the hold and keeping those messages are one call; the coordinator
|
|
2779
|
+
// owns both exits from here — the input comes back once the driver
|
|
2780
|
+
// converges, or rides into the next generation if the rollover
|
|
2781
|
+
// lands — so this turn ends with nothing further to do.
|
|
2782
|
+
this.contextManagement.holdClaimedInput(agent, messages);
|
|
2783
|
+
return { kind: 'reject' };
|
|
2784
|
+
}
|
|
2400
2785
|
if (pressure.kind === 'reject')
|
|
2401
2786
|
return { kind: 'reject' };
|
|
2402
2787
|
const decision = await next();
|
|
@@ -2405,10 +2790,11 @@ let AgentTeam = (() => {
|
|
|
2405
2790
|
this.contextManagement.captureClaimedInput(agent, messages);
|
|
2406
2791
|
return { kind: 'reject' };
|
|
2407
2792
|
});
|
|
2408
|
-
// Provider context-overflow recovery: one bounded
|
|
2409
|
-
// sequence per failure chain through the
|
|
2793
|
+
// Provider context-overflow recovery: one bounded reduce-and-retry
|
|
2794
|
+
// sequence per failure chain through the engine's policy.
|
|
2410
2795
|
agentCtx.on('agent/request-error', async ({ agent, failure, signal }, next) => {
|
|
2411
|
-
const
|
|
2796
|
+
const member = this.memberForAgent(agent);
|
|
2797
|
+
const retry = member !== undefined && await this.pressurePolicy.onRequestError(member.memberId, failure, signal);
|
|
2412
2798
|
if (retry)
|
|
2413
2799
|
return { kind: 'retry' };
|
|
2414
2800
|
return next();
|
|
@@ -2709,42 +3095,67 @@ let AgentTeam = (() => {
|
|
|
2709
3095
|
throw new Error('agent-team service is shutting down');
|
|
2710
3096
|
}
|
|
2711
3097
|
emitCommitted(receipt) {
|
|
2712
|
-
|
|
3098
|
+
// Everything below runs after the ledger append, so nothing here may turn
|
|
3099
|
+
// a committed operation into a failed Remote call. Each effect that runs
|
|
3100
|
+
// third-party code — Cordis listeners, change waiters, agent wakes — goes
|
|
3101
|
+
// through `afterCommit`; the ordering itself is unchanged: listeners,
|
|
3102
|
+
// then Client invalidation, then the participation notice, then wakes.
|
|
3103
|
+
this.afterCommit('commit listener dispatch', () => this.ctx.emit('agent-team/committed', { receipt }));
|
|
2713
3104
|
const operation = this.ledger?.getOperation(receipt.operationId);
|
|
2714
3105
|
if (operation === undefined) {
|
|
2715
|
-
this.emitChanged();
|
|
3106
|
+
this.afterCommit('change invalidation', () => this.emitChanged());
|
|
2716
3107
|
return;
|
|
2717
3108
|
}
|
|
2718
3109
|
const ledger = this.requireLedger();
|
|
2719
|
-
this.emitChanged(ledger.changeScopesOf(operation));
|
|
3110
|
+
this.afterCommit('change invalidation', () => this.emitChanged(ledger.changeScopesOf(operation)));
|
|
2720
3111
|
if (operation.kind === 'team/member-workspace-joined' || operation.kind === 'team/member-workspace-left') {
|
|
2721
|
-
|
|
2722
|
-
|
|
3112
|
+
this.afterCommit('participation notice', () => {
|
|
3113
|
+
const agent = this.handles.get(operation.data.memberId)?.agent;
|
|
3114
|
+
if (agent === undefined)
|
|
3115
|
+
return;
|
|
2723
3116
|
const path = this.ctx.workspaceRegistry.get(operation.data.workspaceId)?.path;
|
|
2724
3117
|
const text = `Team participation changed: you have ${operation.kind === 'team/member-workspace-joined' ? 'joined' : 'left'} Workspace ${operation.data.workspaceId}${path === undefined ? ' (path unavailable)' : ` (${JSON.stringify(path)})`}.\nCurrent Workspace ids: ${ledger.workspacesOf(operation.data.memberId).join(', ')}. This replaces earlier participation information. Your Session and cwd have not moved.`;
|
|
2725
3118
|
const notice = createUserMessage({ content: [{ type: 'text', text }],
|
|
2726
3119
|
source: { kind: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: 'Team Workspace participation changed' } });
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
// Notification delivery cannot roll back a committed participation.
|
|
2735
|
-
this.ctx.logger.warn(`agent-team: participation notice not delivered: ${error instanceof Error ? error.message : String(error)}`);
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
3120
|
+
// Delivered from durable facts; a failed wake cannot roll back the
|
|
3121
|
+
// committed participation — the next notice rederives it.
|
|
3122
|
+
if (agent.status === 'idle' || agent.inbox.nextTurn.some(message => message.source.kind === 'user'))
|
|
3123
|
+
agent.followup(notice);
|
|
3124
|
+
else
|
|
3125
|
+
agent.steer(notice);
|
|
3126
|
+
});
|
|
2738
3127
|
}
|
|
2739
3128
|
for (const memberId of ledger.affectedMembersOf(operation)) {
|
|
2740
3129
|
const handle = this.handles.get(memberId);
|
|
2741
3130
|
if (handle !== undefined)
|
|
2742
|
-
this.notifyMember(handle.agent);
|
|
3131
|
+
this.afterCommit(`notification for '${this.memberLabel(memberId)}'`, () => this.notifyMember(handle.agent));
|
|
2743
3132
|
}
|
|
2744
3133
|
// Task acceptance no longer schedules standalone auto compaction: it is
|
|
2745
3134
|
// a semantic checkpoint/context cue (delivered as ordinary Team
|
|
2746
3135
|
// notification), and the Team pressure policy owns compaction entry.
|
|
2747
3136
|
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Run one effect that follows the ledger append. The operation is durable by
|
|
3139
|
+
* the time this runs, so a delivery or listener failure degrades to a log
|
|
3140
|
+
* line: the Remote answer stays the committed result instead of a false
|
|
3141
|
+
* rollback, and an Inbox wake that failed clears its own signature, so the
|
|
3142
|
+
* next commit touching that Member rederives the same durable facts and
|
|
3143
|
+
* retries without appending a second operation.
|
|
3144
|
+
*
|
|
3145
|
+
* An invariant divergence is the deliberate exception — the invariant
|
|
3146
|
+
* companion re-raises it on the caller's frame precisely to stay loud, and
|
|
3147
|
+
* swallowing it would silence a failed integrity check.
|
|
3148
|
+
*/
|
|
3149
|
+
afterCommit(label, effect) {
|
|
3150
|
+
try {
|
|
3151
|
+
effect();
|
|
3152
|
+
}
|
|
3153
|
+
catch (error) {
|
|
3154
|
+
if (error instanceof AgentTeamInvariantError)
|
|
3155
|
+
throw error;
|
|
3156
|
+
this.ctx.logger.warn(`agent-team: post-commit ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
2748
3159
|
/** Model-visible active-Claim labels for the pressure notice. */
|
|
2749
3160
|
activeClaimLabels(memberId) {
|
|
2750
3161
|
const ledger = this.ledger;
|