@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.
Files changed (99) hide show
  1. package/README.md +8 -4
  2. package/README.zh.md +8 -4
  3. package/cordis.patch.yml +26 -12
  4. package/package.json +52 -47
  5. package/packages/agent-team/lib/attachments.js +63 -4
  6. package/packages/agent-team/lib/compaction-engine.js +18 -0
  7. package/packages/agent-team/lib/context-continuity-host.js +198 -4
  8. package/packages/agent-team/lib/context-judge.js +150 -0
  9. package/packages/agent-team/lib/context-source.js +11 -6
  10. package/packages/agent-team/lib/environment-check.js +102 -0
  11. package/packages/agent-team/lib/human-profile.js +23 -37
  12. package/packages/agent-team/lib/index.js +510 -99
  13. package/packages/agent-team/lib/installed-manifest.js +32 -0
  14. package/packages/agent-team/lib/invariant.js +36 -12
  15. package/packages/agent-team/lib/ledger.js +37 -1
  16. package/packages/agent-team/lib/member-context.js +2 -4
  17. package/packages/agent-team/lib/member-runtime.js +2 -1
  18. package/packages/agent-team/lib/member-time-context.js +2 -4
  19. package/packages/agent-team/lib/recovery.js +53 -4
  20. package/packages/agent-team/lib/typert.host.js +225 -54
  21. package/packages/agent-team/lib/typert.remote-client.d.ts +5 -1
  22. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  23. package/packages/agent-team/lib/typert.remote-client.js +127 -39
  24. package/packages/agent-team/lib/types/attachments.d.ts +32 -2
  25. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  26. package/packages/agent-team/lib/types/compaction-engine.d.ts +19 -0
  27. package/packages/agent-team/lib/types/compaction-engine.d.ts.map +1 -0
  28. package/packages/agent-team/lib/types/context-continuity-host.d.ts +131 -4
  29. package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/context-judge.d.ts +82 -0
  31. package/packages/agent-team/lib/types/context-judge.d.ts.map +1 -0
  32. package/packages/agent-team/lib/types/context-source.d.ts +7 -4
  33. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  34. package/packages/agent-team/lib/types/environment-check.d.ts +66 -0
  35. package/packages/agent-team/lib/types/environment-check.d.ts.map +1 -0
  36. package/packages/agent-team/lib/types/human-profile.d.ts +21 -27
  37. package/packages/agent-team/lib/types/human-profile.d.ts.map +1 -1
  38. package/packages/agent-team/lib/types/index.d.ts +281 -18
  39. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/installed-manifest.d.ts +16 -0
  41. package/packages/agent-team/lib/types/installed-manifest.d.ts.map +1 -0
  42. package/packages/agent-team/lib/types/invariant.d.ts +27 -5
  43. package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
  44. package/packages/agent-team/lib/types/ledger.d.ts +12 -0
  45. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  46. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  47. package/packages/agent-team/lib/types/member-runtime.d.ts +1 -1
  48. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  49. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
  50. package/packages/agent-team/lib/types/recovery.d.ts +19 -6
  51. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
  52. package/packages/agent-team/lib/types/types/entities.d.ts +1 -8
  53. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  54. package/packages/agent-team/lib/types/types/requests-results.d.ts +94 -2
  55. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  56. package/packages/agent-team/lib/types/types.d.ts +9 -0
  57. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/client.js +1264 -383
  59. package/packages/client-agent-team/lib/client.js.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/EnvironmentCheck.d.ts +18 -0
  61. package/packages/client-agent-team/lib/types/client/EnvironmentCheck.d.ts.map +1 -0
  62. package/packages/client-agent-team/lib/types/client/EnvironmentCheck.js +58 -0
  63. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +0 -5
  64. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +1 -1
  65. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +2 -7
  67. package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts +26 -0
  68. package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts.map +1 -0
  69. package/packages/client-agent-team/lib/types/client/TeamSettingsSection.js +195 -0
  70. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +7 -11
  71. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/attachment-preview.js +13 -3
  73. package/packages/client-agent-team/lib/types/client/context-judge.d.ts +64 -0
  74. package/packages/client-agent-team/lib/types/client/context-judge.d.ts.map +1 -0
  75. package/packages/client-agent-team/lib/types/client/context-judge.js +78 -0
  76. package/packages/client-agent-team/lib/types/client/environment-check.d.ts +65 -0
  77. package/packages/client-agent-team/lib/types/client/environment-check.d.ts.map +1 -0
  78. package/packages/client-agent-team/lib/types/client/environment-check.js +78 -0
  79. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  80. package/packages/client-agent-team/lib/types/client/index.js +84 -11
  81. package/packages/client-agent-team/lib/types/client/judge-form.d.ts +99 -0
  82. package/packages/client-agent-team/lib/types/client/judge-form.d.ts.map +1 -0
  83. package/packages/client-agent-team/lib/types/client/judge-form.js +208 -0
  84. package/packages/client-agent-team/lib/types/client/locales.d.ts +70 -14
  85. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  86. package/packages/client-agent-team/lib/types/client/locales.js +76 -14
  87. package/packages/tool-agent-team/README.md +6 -3
  88. package/packages/tool-agent-team/README.zh.md +6 -3
  89. package/packages/tool-agent-team/lib/context-tools.js +148 -164
  90. package/packages/tool-agent-team/lib/index.js +17 -2
  91. package/packages/tool-agent-team/lib/types/context-tools.d.ts +32 -7
  92. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  93. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  94. package/packages/agent-team/lib/pressure-policy.js +0 -230
  95. package/packages/agent-team/lib/types/pressure-policy.d.ts +0 -130
  96. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +0 -1
  97. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts +0 -15
  98. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +0 -1
  99. 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 { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
54
- import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
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 { PressurePolicyCoordinator } from "./pressure-policy.js";
59
- import { CONTEXT_CONTINUITY_PROJECTION_KEY, readContextTimeline } from '@wowyuarm/dsh-context-continuity';
60
- import { createTeamContextManagement, TEAM_CONTEXT_CODEC } from "./context-continuity-host.js";
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
- const MAX_TIMELINE_LIMIT = 24;
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 = HUMAN_PROFILE_SETTINGS_SCHEMA;
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.pressurePolicy = new PressurePolicyCoordinator({
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
- failed: (memberId, _sessionId, diagnostic) => {
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', () => { this.syncHumanHandle(); });
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(message);
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, message);
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(handle.agent);
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
- await this.activateMember(member, undefined, persistedSessions);
640
- else if (member.state === 'inactive')
641
- await this.memberRuntime.cleanupRemovedMember(member);
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 wait for the owned AgentHandle to become quiescent. */
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.disposeMemberSession(request.memberId, result.value.member);
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.emitCommitted(result.value.receipt);
930
- else {
931
- // A retried identical request already renewed this Member; report the
932
- // recorded outcome without another dispose/reactivate cycle.
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
- await this.retireMemberGeneration(request.memberId, active, previousSessionId);
937
- await this.activateMember(renewed, undefined, undefined, previousSessionId);
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.disposeMemberSession(request.memberId, result.value.member);
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.disposeMemberSession(request.memberId, result.value.member);
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
- /** Upload one composer attachment into the cache; bytes are immutable once written. */
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
- const attachmentId = newAttachmentId();
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
- /** Read one cached attachment back for client display; gone entries throw and the UI degrades to a chip. */
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 Error(`attachment '${request.attachmentId}' is no longer cached`);
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: 'handoff-now',
1519
- 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.' });
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('context_timeline requires an active Team Member');
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(`context_timeline limit must be between 1 and ${MAX_TIMELINE_LIMIT}`);
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 context_timeline');
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
- const metadata = await this.resolveMessageAttachments(request);
2256
- const result = await this.requireLedger().sendMessage({
2257
- ...request, body: this.appendAttachmentLines(request.body, metadata),
2258
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
2259
- actor,
2260
- });
2261
- this.emitCommittedOutcome(result);
2262
- return result.value;
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
- const metadata = await this.resolveMessageAttachments(request);
2267
- const result = await this.requireLedger().reply({
2268
- ...request, body: this.appendAttachmentLines(request.body, metadata),
2269
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
2270
- actor,
2271
- });
2272
- this.emitCommittedOutcome(result);
2273
- return result.value;
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
- // Team pressure policy rides the same pre-step seam after the
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, and the hard limit forces compaction before the request is
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 Team limit. Missing route capacity is an explicit reject.
2399
- const pressure = await this.pressurePolicy.onPreStep(agent, signal);
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 compact-and-retry
2409
- // sequence per failure chain through the Team-owned policy.
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 retry = await this.pressurePolicy.onRequestError(agent, failure, signal);
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
- this.ctx.emit('agent-team/committed', { receipt });
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
- const agent = this.handles.get(operation.data.memberId)?.agent;
2722
- if (agent !== undefined) {
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
- try {
2728
- if (agent.status === 'idle' || agent.inbox.nextTurn.some(message => message.source.kind === 'user'))
2729
- agent.followup(notice);
2730
- else
2731
- agent.steer(notice);
2732
- }
2733
- catch (error) {
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;