@wowyuarm/dsh-agent-team 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/package.json +2 -1
  2. package/packages/agent-team/README.md +5 -3
  3. package/packages/agent-team/README.zh.md +5 -3
  4. package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +2 -0
  5. package/packages/agent-team/lib/attachments.js +14 -4
  6. package/packages/agent-team/lib/context-management.js +379 -0
  7. package/packages/agent-team/lib/context-projection.js +574 -0
  8. package/packages/agent-team/lib/context-source.js +72 -0
  9. package/packages/agent-team/lib/index.js +1258 -54
  10. package/packages/agent-team/lib/ledger.js +357 -26
  11. package/packages/agent-team/lib/member-context.js +7 -3
  12. package/packages/agent-team/lib/member-runtime.js +159 -4
  13. package/packages/agent-team/lib/pressure-policy.js +200 -0
  14. package/packages/agent-team/lib/progress-nudge.js +320 -0
  15. package/packages/agent-team/lib/spec.js +18 -0
  16. package/packages/agent-team/lib/typert.host.js +124 -42
  17. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  18. package/packages/agent-team/lib/typert.remote-client.js +39 -25
  19. package/packages/agent-team/lib/types/attachments.d.ts +1 -1
  20. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  21. package/packages/agent-team/lib/types/context-management.d.ts +142 -0
  22. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
  23. package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
  24. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
  25. package/packages/agent-team/lib/types/context-source.d.ts +83 -0
  26. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
  27. package/packages/agent-team/lib/types/index.d.ts +283 -2
  28. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/ledger.d.ts +120 -7
  30. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  31. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
  33. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  34. package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
  35. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
  36. package/packages/agent-team/lib/types/progress-nudge.d.ts +136 -0
  37. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  39. package/packages/agent-team/lib/types/types/entities.d.ts +32 -1
  40. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  41. package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
  42. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  43. package/packages/agent-team/lib/types/types/requests-results.d.ts +54 -1
  44. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  45. package/packages/agent-team/preset/team-member/agent.cordis.yml +10 -2
  46. package/packages/client-agent-team/lib/client.js +283 -398
  47. package/packages/client-agent-team/lib/client.js.map +1 -1
  48. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
  49. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
  51. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  52. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +3 -7
  53. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +3 -1
  54. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamComposer.js +37 -27
  56. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/TeamConversation.js +2 -2
  59. package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.js +3 -2
  61. package/packages/client-agent-team/lib/types/client/TeamMessage.js +5 -0
  62. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +2 -2
  63. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +4 -6
  65. package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts +14 -0
  66. package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts.map +1 -0
  67. package/packages/client-agent-team/lib/types/client/TeamStateDot.js +15 -0
  68. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +1 -0
  69. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +41 -16
  71. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  72. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  73. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  74. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  75. package/packages/client-agent-team/lib/types/client/index.js +36 -78
  76. package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -16
  77. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  78. package/packages/client-agent-team/lib/types/client/locales.js +0 -16
  79. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +1 -2
  80. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -1
  82. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  83. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +7 -1
  84. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
  85. package/packages/client-agent-team/lib/types/client/task-refs.js +25 -3
  86. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +11 -10
  87. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  88. package/packages/client-agent-team/lib/types/client/team-formatters.js +14 -11
  89. package/packages/tool-agent-team/README.md +8 -5
  90. package/packages/tool-agent-team/README.zh.md +8 -5
  91. package/packages/tool-agent-team/lib/context-tools.js +179 -0
  92. package/packages/tool-agent-team/lib/index.js +144 -25
  93. package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
  94. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
  95. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  96. package/packages/agent-team/lib/auto-compaction.js +0 -201
  97. package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
  98. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +0 -1
  99. package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts +0 -12
  100. package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts.map +0 -1
  101. package/packages/client-agent-team/lib/types/client/TeamMemberDock.js +0 -13
  102. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +0 -20
  103. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +0 -1
  104. package/packages/client-agent-team/lib/types/client/member-session-input.js +0 -110
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * @module @wowyuarm/dsh-agent-team/member-runtime
15
15
  */
16
- import { mkdir, rm, writeFile } from 'node:fs/promises';
16
+ import { mkdir, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
17
17
  import { dirname, join, resolve } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { scopeOf } from '@deepseek-ai/dsh-scope';
@@ -32,6 +32,9 @@ export const AGENT_TEAM_TOOL_NAMES = Object.freeze([
32
32
  'team_message',
33
33
  'team_claim',
34
34
  'team_view',
35
+ 'context_rollover',
36
+ 'context_checkpoint',
37
+ 'context_timeline',
35
38
  ]);
36
39
  /** Copy a Remote-supplied capability overlay into owned frozen storage. */
37
40
  export function deepCopyCapabilities(capabilities) {
@@ -41,6 +44,31 @@ export function deepCopyCapabilities(capabilities) {
41
44
  ...(capabilities.skills === undefined ? {} : { skills: copyAllow(capabilities.skills.allow) }),
42
45
  };
43
46
  }
47
+ /**
48
+ * Filesystem directory segment for one Member's private memory namespace.
49
+ * The `member:<uuid>` ref is a durable ledger identity and must never appear
50
+ * in a path: Windows rejects `:` in a path segment (NTFS parses it as an
51
+ * Alternate Data Stream separator), which made Member activation fail at its
52
+ * first `mkdir` on Windows (issue #7).
53
+ */
54
+ export function memberMemoryDirectoryName(memberId) {
55
+ return memberId.replaceAll(':', '-');
56
+ }
57
+ /** The sanitized absolute private-memory path for one Member, regardless of what the ledger recorded. */
58
+ export function memberMemoryDirectoryPath(member) {
59
+ // Sanitize per segment: only a segment that actually contains the ledger's
60
+ // member identity colon needs rewriting, and the recorded final segment is
61
+ // the only place it can appear. The drive-letter colon of a Windows
62
+ // absolute prefix is not a member identity and must survive untouched, so
63
+ // a colon-free final segment keeps the recorded path verbatim even when
64
+ // earlier segments carry colons.
65
+ const separator = Math.max(member.privateMemoryPath.lastIndexOf('/'), member.privateMemoryPath.lastIndexOf('\\'));
66
+ const finalSegment = separator === -1 ? member.privateMemoryPath : member.privateMemoryPath.slice(separator + 1);
67
+ if (!finalSegment.includes(':'))
68
+ return member.privateMemoryPath;
69
+ const prefix = separator === -1 ? '' : member.privateMemoryPath.slice(0, separator + 1);
70
+ return prefix + memberMemoryDirectoryName(member.memberId);
71
+ }
44
72
  /** Per-Member runtime state; the four maps dispose together with each Member. */
45
73
  export class MemberRuntime {
46
74
  deps;
@@ -165,10 +193,12 @@ export class MemberRuntime {
165
193
  * scope layer (the traceable-service seam, like the tool restriction):
166
194
  * bundled read-only core skills plus this Member's own private directory,
167
195
  * with the live selection ref that later capability edits swap in place.
196
+ * The sanitized directory is authoritative: activateMember migrated any
197
+ * legacy colon directory onto it before the provider mounted.
168
198
  */
169
199
  mountMemberSkillProvider(member, agentCtx, selection) {
170
200
  this.skillProviderDisposals.set(member.memberId, memberSkills.mountMemberSkillProvider(agentCtx, {
171
- skillsDirectory: join(member.privateMemoryPath, 'skills'),
201
+ skillsDirectory: join(memberMemoryDirectoryPath(member), 'skills'),
172
202
  bundledSkillsDirectory: BUNDLED_SKILLS_DIRECTORY,
173
203
  selection,
174
204
  }));
@@ -183,8 +213,29 @@ export class MemberRuntime {
183
213
  * first-run memory.md scaffold. The Member-private skills directory starts
184
214
  * empty; the per-Member provider scans exactly this root (default roots
185
215
  * excluded).
216
+ *
217
+ * Existing installs recorded the pre-fix colon directory in the ledger, and
218
+ * `privateMemoryPath` is a durable Member fact the renewal path cannot
219
+ * rewrite: when the legacy directory exists it is renamed onto the sanitized
220
+ * path once (same-parent rename, atomic), so existing private memory
221
+ * survives instead of being silently orphaned.
222
+ *
223
+ * A colon-form twin directory is also merged when it exists: a Member may
224
+ * have written files under the ledger identity spelling (the branded
225
+ * `member:<uuid>` ref is what every Team tool result shows, so a
226
+ * hand-assembled path carries the colon). Linux accepts the segment
227
+ * silently — two directories for one Member — while Windows would have
228
+ * failed the write outright. The twin is never a durable-Member-Fact
229
+ * candidate: only the files the Member actually wrote there are worth
230
+ * keeping, and `memory.md` cannot merge, so the sanitized copy always wins
231
+ * and twin-only notes/skills are moved in without overwriting.
186
232
  */
187
- async initializePrivateMemory(path) {
233
+ async initializePrivateMemory(path, legacyPath) {
234
+ if (legacyPath !== undefined && legacyPath !== path)
235
+ await migrateLegacyMemoryDirectory(legacyPath, path);
236
+ const twinPath = twinMemoryDirectoryPath(path);
237
+ if (twinPath !== undefined)
238
+ await mergeTwinMemoryDirectory(twinPath, path);
188
239
  await mkdir(join(path, 'notes'), { recursive: true });
189
240
  await mkdir(join(path, 'skills'), { recursive: true });
190
241
  try {
@@ -197,9 +248,14 @@ export class MemberRuntime {
197
248
  }
198
249
  /** Irreversibly remove one Member: archive its Session and delete its private namespace. */
199
250
  async cleanupRemovedMember(member) {
251
+ // The ledger path may still name the legacy colon directory (never
252
+ // activated after the fix): remove both spellings; rm is force-tolerant
253
+ // of the one that does not exist.
254
+ const sanitized = memberMemoryDirectoryPath(member);
200
255
  const results = await Promise.allSettled([
201
256
  this.deps.ctx.workspaceRegistry.archiveSession(member.sessionId),
202
- rm(member.privateMemoryPath, { recursive: true, force: true }),
257
+ rm(sanitized, { recursive: true, force: true }),
258
+ ...(sanitized === member.privateMemoryPath ? [] : [rm(member.privateMemoryPath, { recursive: true, force: true })]),
203
259
  ]);
204
260
  const failures = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
205
261
  if (failures.length > 0)
@@ -227,3 +283,102 @@ export class MemberRuntime {
227
283
  this.capabilityWarnings.clear();
228
284
  }
229
285
  }
286
+ /**
287
+ * One-time in-place migration of a pre-fix colon-named private memory
288
+ * directory onto its sanitized path. A sanitized target that already exists
289
+ * wins (idempotent across restarts and partially migrated installs); a legacy
290
+ * source that never existed is simply the fresh-install case.
291
+ */
292
+ async function migrateLegacyMemoryDirectory(legacyPath, path) {
293
+ let legacy;
294
+ try {
295
+ legacy = await stat(legacyPath);
296
+ }
297
+ catch (error) {
298
+ if (error.code === 'ENOENT')
299
+ return;
300
+ throw error;
301
+ }
302
+ if (!legacy.isDirectory())
303
+ throw new Error(`legacy Member memory path '${legacyPath}' exists but is not a directory`);
304
+ try {
305
+ await stat(path);
306
+ return; // Sanitized directory already present: migration already done or a new install.
307
+ }
308
+ catch (error) {
309
+ if (error.code !== 'ENOENT')
310
+ throw error;
311
+ }
312
+ await rename(legacyPath, path);
313
+ }
314
+ /** The colon-form twin path of one sanitized Member memory directory, when the segment form admits one. */
315
+ function twinMemoryDirectoryPath(sanitizedPath) {
316
+ const separator = Math.max(sanitizedPath.lastIndexOf('/'), sanitizedPath.lastIndexOf('\\'));
317
+ const prefix = separator === -1 ? '' : sanitizedPath.slice(0, separator + 1);
318
+ const finalSegment = separator === -1 ? sanitizedPath : sanitizedPath.slice(separator + 1);
319
+ // Only the exact sanitized identity segment (`member-<uuid>`) has a colon
320
+ // twin (`member:<uuid>`); any other segment shape has no identity mapping.
321
+ if (!/^member-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(finalSegment))
322
+ return undefined;
323
+ return `${prefix}member:${finalSegment.slice('member-'.length)}`;
324
+ }
325
+ /** The colon-twin trace name for one conflicting file: `note.md` → `note.colon-twin.md`. */
326
+ function colonTwinTraceName(name) {
327
+ const dot = name.lastIndexOf('.');
328
+ return dot <= 0 ? `${name}.colon-twin` : `${name.slice(0, dot)}.colon-twin${name.slice(dot)}`;
329
+ }
330
+ /**
331
+ * Merge a hand-created colon-form twin directory into the sanitized Member
332
+ * memory directory. Unlike the ledger legacy migration this is a merge, not a
333
+ * rename: the sanitized directory is the Member's live root (the injected
334
+ * paths and the skill provider point at it), so on a same-path conflict the
335
+ * live root's copy wins and the twin's losing copy is preserved beside it
336
+ * under a `.colon-twin` name instead of being silently discarded. Twin-only
337
+ * files move in under their own names; the emptied twin directory is then
338
+ * removed so the drift cannot silently recur.
339
+ */
340
+ async function mergeTwinMemoryDirectory(twinPath, path) {
341
+ let twin;
342
+ try {
343
+ twin = await stat(twinPath);
344
+ }
345
+ catch (error) {
346
+ if (error.code !== 'ENOENT')
347
+ throw error;
348
+ return;
349
+ }
350
+ if (!twin.isDirectory())
351
+ return;
352
+ await mergeDirectoryContents(twinPath, path);
353
+ await rm(twinPath, { recursive: true, force: true });
354
+ }
355
+ /** Recursively move every twin file into the live root; an existing live file wins, its twin copy traced beside it. */
356
+ async function mergeDirectoryContents(source, target) {
357
+ await mkdir(target, { recursive: true });
358
+ for (const entry of await readdir(source, { withFileTypes: true })) {
359
+ const sourcePath = join(source, entry.name);
360
+ const targetPath = join(target, entry.name);
361
+ if (entry.isDirectory()) {
362
+ await mergeDirectoryContents(sourcePath, targetPath);
363
+ }
364
+ else if (entry.isFile()) {
365
+ // rename(2) silently REPLACES an existing target on POSIX (the EEXIST
366
+ // error only exists on Windows, where the colon twin cannot exist at
367
+ // all), so a collision must be detected, not caught: probe the target
368
+ // first. Activation is the single serialized writer, so the
369
+ // probe-to-rename window has no concurrent writer to race.
370
+ let liveFilePresent = true;
371
+ try {
372
+ await stat(targetPath);
373
+ }
374
+ catch (error) {
375
+ if (error.code !== 'ENOENT')
376
+ throw error;
377
+ liveFilePresent = false;
378
+ }
379
+ // The live root stays authoritative; the twin's losing copy stays
380
+ // traceable beside it instead of being silently discarded.
381
+ await rename(sourcePath, liveFilePresent ? join(target, colonTwinTraceName(entry.name)) : targetPath);
382
+ }
383
+ }
384
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Team-owned context pressure policy.
3
+ *
4
+ * The Team preset mounts `compaction-basic` with `auto: false`; this
5
+ * coordinator owns both policy entry points instead — the proactive handoff
6
+ * notice near the effective handoff budget and the forced CompactionEngine
7
+ * reduction before any request at or above the effective hard limit — while
8
+ * reusing the public CompactionEngine implementation. The hard-limit
9
+ * translation lives in exactly one method; no caller re-derives it.
10
+ *
11
+ * Fail-closed rule: at the hard limit, a compaction that no-ops, throws, is
12
+ * cancelled, or does not advance the durable surface must block the model
13
+ * request (reject the step) rather than knowingly submit over the Team limit.
14
+ * A blocked Member keeps its log and reports a recoverable error.
15
+ * @module @wowyuarm/dsh-agent-team/pressure-policy
16
+ */
17
+ import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm';
18
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
19
+ import { CONTEXT_PRESSURE_NOTICE_SUMMARY } from "./context-management.js";
20
+ /** Plugin identity of the Agent Team Host, for the notice source. */
21
+ const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
22
+ /** One pressure-notice text; concise, structured, and inside the reserve. */
23
+ export function contextPressureNoticeText(input) {
24
+ const claims = input.activeClaims.length === 0 ? 'none' : input.activeClaims.join(', ');
25
+ const jobs = input.runningJobs.length === 0 ? 'none' : `${input.runningJobs.length} running (collect or stop them before switching)`;
26
+ return [
27
+ `Context pressure: ${input.usageTokens} tokens measured; the handoff budget is ${input.handoffAt} and the hard limit is ${input.hardLimit}.`,
28
+ `Active Claims: ${claims}. Owner jobs: ${jobs}.`,
29
+ 'Finish the current atomic action, then call context_rollover with a handoff covering your objective, verified facts, and external side effects — a fresh context is the default path. Record anything durable in your private memory/notes first.',
30
+ ].join(' ');
31
+ }
32
+ /** Whether one user message is this policy's one-shot pressure notice. */
33
+ function isPressureNotice(message) {
34
+ const source = message.source;
35
+ return source?.plugin === AGENT_TEAM_PLUGIN_ID
36
+ && source?.summary === CONTEXT_PRESSURE_NOTICE_SUMMARY;
37
+ }
38
+ export class PressurePolicyCoordinator {
39
+ options;
40
+ /**
41
+ * Retry budget per agent for the current provider-overflow sequence.
42
+ * Process-only by design: a restart re-earns one sequence per chain.
43
+ */
44
+ overflowRetries = new Map();
45
+ disposed = false;
46
+ constructor(options) {
47
+ this.options = options;
48
+ }
49
+ /**
50
+ * The one-shot pressure notice is durable Session evidence, not process
51
+ * state: a `CONTEXT_PRESSURE_NOTICE_SUMMARY` notice already surfaced as a
52
+ * `user/message`, or still queued in a durable `agent/inbox/spliced`
53
+ * insert (a steered notice surfaces only at the next step boundary, and a
54
+ * Host restart replays the splice before surfacing), marks the current
55
+ * generation as already notified. A resume or restart stays quiet; a
56
+ * rollover starts a fresh Session whose own event span has no notice yet,
57
+ * which is exactly the documented re-arm.
58
+ */
59
+ noticeDelivered(agent) {
60
+ for (const event of agent.session.ownEvents()) {
61
+ if (event.type === 'user/message' && isPressureNotice(event.data))
62
+ return true;
63
+ if (event.type === 'agent/inbox/spliced' && event.data.inserted.some(isPressureNotice))
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ dispose() {
69
+ this.disposed = true;
70
+ this.overflowRetries.clear();
71
+ }
72
+ /** A successful assistant response ends any open overflow-recovery sequence. */
73
+ onAssistantMessage(agent) {
74
+ this.overflowRetries.delete(agent);
75
+ }
76
+ /**
77
+ * Pre-step policy for one Member agent: below the handoff budget nothing
78
+ * happens; at the handoff budget one structured notice per generation is
79
+ * steered into the running turn; at the hard limit the request is forced
80
+ * through compaction first and fails closed when that cannot be proven.
81
+ * Returns the decision for the step: `continue` forwards, `reject` blocks.
82
+ */
83
+ async onPreStep(agent, signal) {
84
+ if (this.disposed || signal.aborted)
85
+ return { kind: 'continue' };
86
+ const member = this.options.memberForAgent(agent);
87
+ if (member === undefined)
88
+ return { kind: 'continue' };
89
+ const limits = await this.options.limitsForAgent(agent);
90
+ if (limits === undefined) {
91
+ // A missing route capacity must be explicit, never an accidental
92
+ // unlimited policy: reject the step with a recoverable diagnostic.
93
+ const diagnostic = 'context pressure policy: the routed model capacity is unknown; refusing to forward a request without a bounded context budget';
94
+ this.options.failed(member.memberId, member.sessionId, diagnostic);
95
+ return { kind: 'reject' };
96
+ }
97
+ const { usageTokens, hardLimit, handoffAt } = limits;
98
+ if (usageTokens >= hardLimit) {
99
+ const outcome = await this.enforceHardLimit(agent, member.memberId, member.sessionId, signal);
100
+ return outcome ? { kind: 'continue' } : { kind: 'reject' };
101
+ }
102
+ if (usageTokens >= handoffAt && !this.noticeDelivered(agent)) {
103
+ const notice = createUserMessage({
104
+ content: [{ type: 'text', text: contextPressureNoticeText({
105
+ usageTokens, handoffAt, hardLimit,
106
+ activeClaims: this.options.activeClaimLabels(member.memberId),
107
+ runningJobs: this.options.runningJobLabels(member.memberId),
108
+ }) }],
109
+ source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: CONTEXT_PRESSURE_NOTICE_SUMMARY },
110
+ });
111
+ try {
112
+ agent.steer(notice);
113
+ }
114
+ catch (error) {
115
+ this.options.log(`context pressure notice failed: ${error instanceof Error ? error.message : String(error)} (member ${member.memberId})`);
116
+ }
117
+ return { kind: 'notice' };
118
+ }
119
+ return { kind: 'continue' };
120
+ }
121
+ /**
122
+ * Provider-overflow recovery: one bounded compact-and-retry sequence per
123
+ * open failure chain. Returns whether the request may retry once.
124
+ */
125
+ async onRequestError(agent, failure, signal) {
126
+ if (this.disposed || signal.aborted)
127
+ return false;
128
+ if (failure.code !== CONTEXT_WINDOW_EXCEEDED_CODE)
129
+ return false;
130
+ const member = this.options.memberForAgent(agent);
131
+ if (member === undefined)
132
+ return false;
133
+ const retries = this.overflowRetries.get(agent) ?? 0;
134
+ if (retries >= 1)
135
+ return false;
136
+ const engine = this.options.compactionForAgent(agent);
137
+ if (engine === undefined)
138
+ return false;
139
+ const generation = agent.session.surface.replaceGeneration;
140
+ try {
141
+ await engine.compactIfNeeded(agent, 'context-overflow', signal);
142
+ }
143
+ catch (error) {
144
+ // Durable prune progress before a later summary failure justifies the
145
+ // single retry; cancellation never does.
146
+ if (!signal.aborted && agent.session.surface.replaceGeneration > generation) {
147
+ this.overflowRetries.set(agent, retries + 1);
148
+ return true;
149
+ }
150
+ this.options.log(`context-overflow recovery failed: ${error instanceof Error ? error.message : String(error)} (member ${member.memberId})`);
151
+ return false;
152
+ }
153
+ if (signal.aborted || agent.session.surface.replaceGeneration <= generation)
154
+ return false;
155
+ this.overflowRetries.set(agent, retries + 1);
156
+ return true;
157
+ }
158
+ /**
159
+ * The one Team hard-limit translation: force a CompactionEngine reduction
160
+ * in the current Agent/Session and prove it advanced the durable surface
161
+ * (or measurably reduced pressure) before continuing. Background jobs are
162
+ * untouched — compaction never cancels or discards them.
163
+ * @returns whether the request may proceed.
164
+ */
165
+ async enforceHardLimit(agent, memberId, sessionId, signal) {
166
+ const engine = this.options.compactionForAgent(agent);
167
+ if (engine === undefined) {
168
+ const diagnostic = 'context hard limit reached and compaction is unavailable in the Member scope; the request was blocked';
169
+ this.options.failed(memberId, sessionId, diagnostic);
170
+ return false;
171
+ }
172
+ const meter = agent.ctx.get('tokenMeter');
173
+ const before = meter?.measure(agent.session)?.totalTokens ?? Number.POSITIVE_INFINITY;
174
+ const generation = agent.session.surface.replaceGeneration;
175
+ let result;
176
+ try {
177
+ result = await engine.compactIfNeeded(agent, 'context-overflow', signal);
178
+ }
179
+ catch (error) {
180
+ const diagnostic = `context hard limit compaction failed: ${error instanceof Error ? error.message : String(error)}; the request was blocked`;
181
+ this.options.failed(memberId, sessionId, diagnostic);
182
+ return false;
183
+ }
184
+ if (signal.aborted)
185
+ return false;
186
+ const after = meter?.measure(agent.session)?.totalTokens ?? Number.POSITIVE_INFINITY;
187
+ const surfaceAdvanced = agent.session.surface.replaceGeneration > generation;
188
+ const pressureReduced = meter === undefined ? false : after < before;
189
+ if (!surfaceAdvanced && !pressureReduced) {
190
+ // No-op or unchanged replacement generation: fail closed rather than
191
+ // knowingly submit over the Team limit.
192
+ const diagnostic = result === null
193
+ ? 'context hard limit reached and no compactable range exists; the request was blocked'
194
+ : 'context hard limit compaction produced no measurable reduction; the request was blocked';
195
+ this.options.failed(memberId, sessionId, diagnostic);
196
+ return false;
197
+ }
198
+ return true;
199
+ }
200
+ }