@wowyuarm/dsh-agent-team 0.1.9 → 0.1.11

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 (119) hide show
  1. package/README.md +17 -11
  2. package/README.zh.md +17 -11
  3. package/package.json +61 -47
  4. package/packages/agent-team/README.md +6 -4
  5. package/packages/agent-team/README.zh.md +6 -3
  6. package/packages/agent-team/lib/context-management.js +48 -55
  7. package/packages/agent-team/lib/context-projection.js +22 -8
  8. package/packages/agent-team/lib/context-source.js +163 -23
  9. package/packages/agent-team/lib/index.js +349 -176
  10. package/packages/agent-team/lib/invariant.js +38 -4
  11. package/packages/agent-team/lib/ledger.js +658 -294
  12. package/packages/agent-team/lib/member-context.js +6 -2
  13. package/packages/agent-team/lib/member-time-context.js +141 -0
  14. package/packages/agent-team/lib/pressure-policy.js +34 -11
  15. package/packages/agent-team/lib/progress-nudge.js +12 -4
  16. package/packages/agent-team/lib/session-event-cursor.js +116 -0
  17. package/packages/agent-team/lib/session-remediation.js +493 -0
  18. package/packages/agent-team/lib/spec.js +49 -36
  19. package/packages/agent-team/lib/stored-session-reader.js +138 -0
  20. package/packages/agent-team/lib/time-format.js +56 -0
  21. package/packages/agent-team/lib/typert.host.js +163 -63
  22. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  23. package/packages/agent-team/lib/typert.remote-client.js +105 -32
  24. package/packages/agent-team/lib/types/context-management.d.ts +13 -1
  25. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
  26. package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
  27. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  28. package/packages/agent-team/lib/types/context-source.d.ts +70 -36
  29. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/index.d.ts +110 -9
  31. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/ledger.d.ts +205 -7
  34. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  35. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
  37. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
  39. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
  41. package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
  42. package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
  43. package/packages/agent-team/lib/types/session-remediation.d.ts +172 -0
  44. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
  45. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  46. package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
  47. package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
  48. package/packages/agent-team/lib/types/time-format.d.ts +28 -0
  49. package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
  50. package/packages/agent-team/lib/types/types/entities.d.ts +36 -2
  51. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  52. package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
  53. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  54. package/packages/agent-team/lib/types/types/requests-results.d.ts +50 -3
  55. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  56. package/packages/agent-team/preset/team-member/agent.cordis.yml +21 -4
  57. package/packages/client-agent-team/README.md +1 -1
  58. package/packages/client-agent-team/README.zh.md +1 -1
  59. package/packages/client-agent-team/lib/client.js +1121 -402
  60. package/packages/client-agent-team/lib/client.js.map +1 -1
  61. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  62. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -4
  63. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +42 -22
  65. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +13 -24
  67. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
  68. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  69. package/packages/client-agent-team/lib/types/client/TeamComposer.js +15 -9
  70. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  71. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/TeamConversation.js +8 -1
  73. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +21 -0
  74. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
  75. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +71 -0
  76. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  77. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
  78. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
  79. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  80. package/packages/client-agent-team/lib/types/client/TeamMessage.js +13 -6
  81. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
  82. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  83. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
  84. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  85. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +106 -32
  86. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  87. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  88. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +50 -7
  89. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  90. package/packages/client-agent-team/lib/types/client/index.js +25 -9
  91. package/packages/client-agent-team/lib/types/client/locales.d.ts +18 -0
  92. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  93. package/packages/client-agent-team/lib/types/client/locales.js +18 -0
  94. package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
  95. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  96. package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
  97. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
  98. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
  99. package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
  100. package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -2
  101. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  102. package/packages/client-agent-team/lib/types/client/team-changes.d.ts +26 -1
  103. package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
  104. package/packages/client-agent-team/lib/types/client/team-changes.js +66 -15
  105. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
  106. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
  107. package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
  108. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
  109. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  110. package/packages/client-agent-team/lib/types/client/team-formatters.js +34 -6
  111. package/packages/tool-agent-team/README.md +8 -6
  112. package/packages/tool-agent-team/README.zh.md +8 -6
  113. package/packages/tool-agent-team/lib/context-tools.js +18 -14
  114. package/packages/tool-agent-team/lib/host-access.js +20 -0
  115. package/packages/tool-agent-team/lib/index.js +314 -123
  116. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  117. package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
  118. package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
  119. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -61,16 +61,20 @@ export function renderMemberIdentity(member) {
61
61
  ? `Team identity: you are @${member.handle}.`
62
62
  : `Team identity: you are @${member.handle} — ${member.description}`;
63
63
  }
64
+ /** The four private-memory paths plus the out-of-cwd warning; callers append their own sentence. */
65
+ function memoryPathsBlock(privateMemoryPath) {
66
+ return `Private memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd.`;
67
+ }
64
68
  export function renderMemberMemory(raw, privateMemoryPath = '<private-memory-path>') {
65
69
  const overBudget = raw.byteLength > MAX_MEMORY_BYTES;
66
70
  const body = overBudget ? '' : raw.toString('utf8');
67
71
  const warning = overBudget
68
72
  ? '\n\n[Maintenance warning: memory.md exceeds the 8 KiB context budget. Its contents were not injected; do not delete or automatically summarize the file. Maintain a smaller index explicitly.]'
69
73
  : '';
70
- return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Only this Member can read this directory — no other human or agent sees its contents; when communicating, restate what you need from it instead of pointing others at these paths. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
74
+ return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\n${memoryPathsBlock(privateMemoryPath)} Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Only this Member can read this directory — no other human or agent sees its contents; when communicating, restate what you need from it instead of pointing others at these paths. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
71
75
  }
72
76
  function renderUnavailableMemory(privateMemoryPath, reason) {
73
- return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
77
+ return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\n${memoryPathsBlock(privateMemoryPath)} Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
74
78
  }
75
79
  function escape(value) {
76
80
  return value.replaceAll(BEGIN, '[escaped begin marker]').replaceAll(END, '[escaped end marker]');
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Team Member turn-level clock context.
3
+ *
4
+ * The first model step of every eligible Team Member turn appends one
5
+ * durable, source-attributed clock snapshot: the current instant in the
6
+ * fixed Team coordination zone (UTC+8), the elapsed time since the
7
+ * preceding model-visible event, and the ordering authority note. Later
8
+ * steps of the same turn stay quiet unless the turn runs longer than the
9
+ * refresh interval, in which case one snapshot lands per elapsed interval —
10
+ * a tool-dense turn of quick steps produces exactly one line, while a turn
11
+ * that outlives the interval still shows its real span. The snapshot is an
12
+ * observation, never ledger authority: sequence and revision, not
13
+ * wall-clock time, order Team facts.
14
+ *
15
+ * This plugin deliberately does not mount the shipped
16
+ * `@deepseek-ai/dsh-time-context`: its browser-zone policy asks the model to
17
+ * confirm dates with the user whenever a request carries no unique browser
18
+ * zone, which is the normal case for background Member wakes (Inbox, DM,
19
+ * recovery, continuation). The Team coordination zone is fixed instead. If
20
+ * the harness grows a public non-browser/canonical-zone policy, retire this
21
+ * row in favor of configuring that plugin.
22
+ *
23
+ * State is folded from the Member Session's own events — the same
24
+ * manual-fold pattern the Host's context projection uses — so restart,
25
+ * request reconstruction, and compaction all derive identical baselines
26
+ * without a second durable store.
27
+ * @module @wowyuarm/dsh-agent-team/member-time-context
28
+ */
29
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
30
+ import { formatTeamDuration, formatTeamTimestamp } from "./time-format.js";
31
+ import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
32
+ export const name = 'wowyuarm-agent-team-member-time-context';
33
+ /** Default minimum spacing between two snapshots within one turn, in ms. */
34
+ export const CLOCK_REFRESH_INTERVAL_MS = 1_800_000;
35
+ function emptyBaseline() {
36
+ return { lastMessageTime: null, lastInjectionTime: null, lastTurnInjectionTime: null, openTurn: -1 };
37
+ }
38
+ /**
39
+ * Fold one session event into the clock baseline. Uninterested events return
40
+ * the same state reference.
41
+ * @internal exported for tests.
42
+ */
43
+ export function applyClockEvent(state, event) {
44
+ switch (event.type) {
45
+ case 'turn/start':
46
+ return event.data.turn === state.openTurn ? state
47
+ : { ...state, lastTurnInjectionTime: null, openTurn: event.data.turn };
48
+ case 'turn/end':
49
+ return state.openTurn === -1 ? state : { ...state, lastTurnInjectionTime: null, openTurn: -1 };
50
+ case 'user/message': {
51
+ const source = event.data.source;
52
+ const injected = source.kind === 'plugin' && source.plugin === name;
53
+ const withMessage = state.lastMessageTime === event.time ? state : { ...state, lastMessageTime: event.time };
54
+ if (!injected)
55
+ return withMessage;
56
+ return { ...withMessage, lastInjectionTime: event.time, lastTurnInjectionTime: event.time };
57
+ }
58
+ case 'assistant/message':
59
+ case 'tool/result':
60
+ return state.lastMessageTime === event.time ? state : { ...state, lastMessageTime: event.time };
61
+ default:
62
+ return state;
63
+ }
64
+ }
65
+ /** Fold a whole event log into the clock baseline. @internal exported for tests. */
66
+ export function foldClockBaseline(events) {
67
+ let state = emptyBaseline();
68
+ for (const event of events)
69
+ state = applyClockEvent(state, event);
70
+ return state;
71
+ }
72
+ /**
73
+ * Whether this step should append a clock snapshot: the first step of a turn
74
+ * always does (every wake starts with a fresh instant), and a later step
75
+ * does only when the turn has run longer than the refresh interval since
76
+ * the last landed snapshot. Skipped steps produce nothing and never
77
+ * backfill — their span folds into the next snapshot's elapsed.
78
+ * @internal exported for tests.
79
+ */
80
+ export function shouldSampleClock(step, now, baseline, refreshIntervalMs) {
81
+ if (step === 1)
82
+ return true;
83
+ return baseline.lastTurnInjectionTime === null || now - baseline.lastTurnInjectionTime >= refreshIntervalMs;
84
+ }
85
+ /** Render one durable clock snapshot text. @internal exported for tests. */
86
+ export function renderClockSnapshot(input) {
87
+ // A wall-clock rollback clamps elapsed to 0s without rewriting history.
88
+ const elapsed = input.previous === undefined ? 'unavailable' : formatTeamDuration(input.now - input.previous);
89
+ const baseline = input.step === 1 ? 'model-visible event' : 'step context';
90
+ return `Team clock sampled while preparing turn ${input.turn}, step ${input.step}: ${formatTeamTimestamp(new Date(input.now).toISOString())}\n`
91
+ + `Elapsed since the preceding ${baseline}: ${elapsed}.\n`
92
+ + 'Team collaboration timestamps use UTC+8. Sequence and revision, not wall-clock time, determine ordering and concurrency.';
93
+ }
94
+ /**
95
+ * The clock baseline folded incrementally over one Session's own events. A
96
+ * rollover starts a fresh Session log, and the cache below is keyed by Session
97
+ * id, so the fold never guesses elapsed across generations: a missing prior
98
+ * event renders `unavailable`, not a fabricated baseline.
99
+ */
100
+ const CLOCK_FOLD = {
101
+ start: emptyBaseline(),
102
+ step: (state, event) => applyClockEvent(state, event),
103
+ };
104
+ export function apply(ctx, config = {}) {
105
+ const refreshIntervalMs = config.refreshIntervalMs ?? CLOCK_REFRESH_INTERVAL_MS;
106
+ // Per-step fold state, reused for this plugin lifecycle: one entry per
107
+ // Member, replaced when that Member's Session changes, so a rollover neither
108
+ // resumes across generations nor retains every generation it leaves behind.
109
+ const cursors = new Map();
110
+ ctx.on('agent/pre-step', async ({ agent, turn, step, signal }, next) => {
111
+ const decision = await next();
112
+ if (decision.kind === 'reject' || signal.aborted)
113
+ return decision;
114
+ // The Host service is resolved at step time, never through plugin inject
115
+ // (same reason as member-context: the row mounts while the Host itself
116
+ // is still restoring Members).
117
+ const host = ctx.get('agentTeam');
118
+ if (host === undefined)
119
+ return decision;
120
+ const member = host.memberForAgent(agent);
121
+ if (member === undefined)
122
+ return decision;
123
+ const now = Date.now();
124
+ const owned = advanceOwnedSessionEventCursor(cursors.get(member.memberId), agent.session.id, CLOCK_FOLD, agent.session.ownEvents(), agent.session.inheritedEventCount);
125
+ cursors.set(member.memberId, owned);
126
+ const baseline = owned.cursor.value;
127
+ // Turn-first-step always samples; later steps sample only at the refresh
128
+ // interval, so a quick tool-dense turn stays at one line.
129
+ if (!shouldSampleClock(step, now, baseline, refreshIntervalMs))
130
+ return decision;
131
+ const previous = step === 1
132
+ ? baseline.lastMessageTime ?? undefined
133
+ : baseline.lastTurnInjectionTime ?? undefined;
134
+ const text = renderClockSnapshot({ now, turn, step, previous });
135
+ const message = createUserMessage({
136
+ content: [{ type: 'text', text }],
137
+ source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
138
+ });
139
+ return { kind: 'enter', messages: [...decision.messages, message] };
140
+ }, { prepend: true });
141
+ }
@@ -17,8 +17,27 @@
17
17
  import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm';
18
18
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
19
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';
20
+ import { AGENT_TEAM_PLUGIN_ID } from "./context-source.js";
21
+ import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
22
+ /**
23
+ * Whether this Session's own span already carries the pressure notice: either
24
+ * surfaced as a `user/message` or still queued in a durable
25
+ * `agent/inbox/spliced` insert. Expressed as a fold rather than a scan because
26
+ * the answer is monotone — once delivered it stays delivered for the
27
+ * generation — so the cursor only ever has to read events it has not seen.
28
+ */
29
+ export const PRESSURE_NOTICE_FOLD = {
30
+ start: false,
31
+ step: (delivered, event) => {
32
+ if (delivered)
33
+ return true;
34
+ if (event.type === 'user/message' && isPressureNotice(event.data))
35
+ return true;
36
+ if (event.type === 'agent/inbox/spliced' && event.data.inserted.some(isPressureNotice))
37
+ return true;
38
+ return false;
39
+ },
40
+ };
22
41
  /** One pressure-notice text; concise, structured, and inside the reserve. */
23
42
  export function contextPressureNoticeText(input) {
24
43
  const claims = input.activeClaims.length === 0 ? 'none' : input.activeClaims.join(', ');
@@ -42,6 +61,14 @@ export class PressurePolicyCoordinator {
42
61
  * Process-only by design: a restart re-earns one sequence per chain.
43
62
  */
44
63
  overflowRetries = new Map();
64
+ /**
65
+ * Whether the one-shot notice was already delivered, folded incrementally per
66
+ * Member. The scan below is a monotone "has this ever happened" fold over the
67
+ * Session's own events, so a cursor can replace re-scanning the whole log on
68
+ * every step; identity guarding falls back to a cold fold when the Member's
69
+ * Session changed under the entry.
70
+ */
71
+ noticeSeen = new Map();
45
72
  disposed = false;
46
73
  constructor(options) {
47
74
  this.options = options;
@@ -56,14 +83,10 @@ export class PressurePolicyCoordinator {
56
83
  * rollover starts a fresh Session whose own event span has no notice yet,
57
84
  * which is exactly the documented re-arm.
58
85
  */
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;
86
+ noticeDelivered(agent, memberId) {
87
+ const owned = advanceOwnedSessionEventCursor(this.noticeSeen.get(memberId), agent.session.id, PRESSURE_NOTICE_FOLD, agent.session.ownEvents(), agent.session.inheritedEventCount);
88
+ this.noticeSeen.set(memberId, owned);
89
+ return owned.cursor.value;
67
90
  }
68
91
  dispose() {
69
92
  this.disposed = true;
@@ -99,7 +122,7 @@ export class PressurePolicyCoordinator {
99
122
  const outcome = await this.enforceHardLimit(agent, member.memberId, member.sessionId, signal);
100
123
  return outcome ? { kind: 'continue' } : { kind: 'reject' };
101
124
  }
102
- if (usageTokens >= handoffAt && !this.noticeDelivered(agent)) {
125
+ if (usageTokens >= handoffAt && !this.noticeDelivered(agent, member.memberId)) {
103
126
  const notice = createUserMessage({
104
127
  content: [{ type: 'text', text: contextPressureNoticeText({
105
128
  usageTokens, handoffAt, hardLimit,
@@ -22,7 +22,7 @@
22
22
  * @module @wowyuarm/dsh-agent-team/progress-nudge
23
23
  */
24
24
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
25
- const PROGRESS_NUDGE_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
25
+ import { AGENT_TEAM_PLUGIN_ID, isCheckpointContinuationMessage } from "./context-source.js";
26
26
  /** Notice summary for every nudge this Coordinator injects. */
27
27
  export const PROGRESS_NUDGE_NOTICE_SUMMARY = 'Progress visibility reminder';
28
28
  export const PROGRESS_NUDGE_TOOL_CALLS_START = 20;
@@ -39,7 +39,7 @@ export function isPublicCommunicationOperationKind(kind) {
39
39
  /** Whether one message is a nudge notice this module injected. */
40
40
  export function isProgressNudgeNotice(message) {
41
41
  const source = message.source;
42
- return source.kind === 'plugin' && source.plugin === PROGRESS_NUDGE_PLUGIN_ID
42
+ return source.kind === 'plugin' && source.plugin === AGENT_TEAM_PLUGIN_ID
43
43
  && source.form === 'notice' && source.summary === PROGRESS_NUDGE_NOTICE_SUMMARY;
44
44
  }
45
45
  /**
@@ -218,7 +218,7 @@ export class ProgressNudgeCoordinator {
218
218
  const claimTargets = claimDue ? freshClaimTargets : [];
219
219
  const notice = createUserMessage({
220
220
  content: [{ type: 'text', text: nudgeNoticeText(state.silentToolCalls, progressTargets, claimTargets) }],
221
- source: { kind: 'plugin', plugin: PROGRESS_NUDGE_PLUGIN_ID, form: 'notice', summary: PROGRESS_NUDGE_NOTICE_SUMMARY },
221
+ source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: PROGRESS_NUDGE_NOTICE_SUMMARY },
222
222
  });
223
223
  const pending = {
224
224
  messageId: notice.id,
@@ -268,7 +268,15 @@ export class ProgressNudgeCoordinator {
268
268
  hasBlockingNotice(agent) {
269
269
  for (const message of [...agent.inbox.nextStep, ...agent.inbox.nextTurn]) {
270
270
  const source = message.source;
271
- if (source.kind !== 'plugin' || source.plugin !== PROGRESS_NUDGE_PLUGIN_ID || source.form !== 'notice')
271
+ if (source.kind !== 'plugin' || source.plugin !== AGENT_TEAM_PLUGIN_ID)
272
+ continue;
273
+ // A queued checkpoint continuation is a pending wake that outranks a
274
+ // nudge exactly as the notice families above do. It rides the snapshot
275
+ // form (it must carry its checkpoint ref in a section), so it cannot be
276
+ // recognized by the notice summary check below.
277
+ if (isCheckpointContinuationMessage(message))
278
+ return true;
279
+ if (source.form !== 'notice')
272
280
  continue;
273
281
  if (source.summary !== PROGRESS_NUDGE_NOTICE_SUMMARY)
274
282
  return true;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Incremental Session-log fold with an identity guard.
3
+ *
4
+ * Several Host projections are pure left folds over one Member Session's own
5
+ * events: context intent, the clock baseline, and the one-shot pressure
6
+ * notice. Folding the whole log on every event makes each of them cost
7
+ * O(session length) per step, so a long Session gets steadily slower. This
8
+ * module keeps the fold value together with how far it has consumed the log,
9
+ * so a step folds only the tail that arrived since — the same idiom the
10
+ * Harness Session itself uses for its own derived folds.
11
+ *
12
+ * The module is deliberately Session-agnostic: it takes a plain event array
13
+ * plus the log offset that array starts at, so a caller (including a
14
+ * measurement harness) can drive it without constructing a Session.
15
+ *
16
+ * Correctness rests on the log being append-only, which it is: Session logs
17
+ * have no truncation API, `seq` is the log index, and `seq === log.length` is
18
+ * a documented contiguity contract. A Session adopts a different log only by
19
+ * being constructed from another seed, which replaces the whole slice rather
20
+ * than editing one position.
21
+ *
22
+ * The guard covers the observable ways that contract can break: a changed
23
+ * `logFrom`, a shorter log, a different event type at the anchor. Each
24
+ * re-folds from `logFrom`, so a fork, resume, or rollover degrades to the cost
25
+ * of a cold fold and never to a wrong picture.
26
+ *
27
+ * Named residual: the guard anchors on `(seq, type)`, so an event replaced in
28
+ * place by another of the SAME type at the SAME seq is not detected, and the
29
+ * cursor would keep its old value and fold only the tail. Reaching that state
30
+ * requires a caller to violate the append-only contract above — reading one
31
+ * Session's cursor against a log someone rewrote in place. A content-sensitive
32
+ * anchor would not close it either: only the anchor position is re-read, so a
33
+ * swap at any earlier folded position passes a boundary digest just as
34
+ * silently, while costing a payload digest on every advance. Closing it
35
+ * properly means fingerprinting every folded event, which is the full refold
36
+ * this module exists to avoid; if the contract ever changes, that is the fix.
37
+ * @module @wowyuarm/dsh-agent-team/session-event-cursor
38
+ */
39
+ /**
40
+ * Open a cursor for a log slice that starts at `logFrom` (a Session's
41
+ * `inheritedEventCount` for its own events, or 0 for a full log). Nothing is
42
+ * consumed yet, so the first {@link advanceSessionEventCursor} folds the whole
43
+ * slice.
44
+ */
45
+ export function initSessionEventCursor(fold, logFrom = 0) {
46
+ return { value: fold.start, foldedThrough: logFrom, logFrom, anchor: null };
47
+ }
48
+ /**
49
+ * Fold the log tail that arrived since the cursor stopped and return the
50
+ * advanced cursor; the same reference comes back when there is nothing new.
51
+ *
52
+ * `events` is the slice starting at `logFrom`, and `logEnd` is the EXCLUSIVE
53
+ * end offset of that slice in the owning log — the same coordinate space as
54
+ * `logFrom`, not `events.length`. An inherited Session's own events start at
55
+ * its `inheritedEventCount`, so for a 14-event log with 10 inherited events
56
+ * the slice is `events[0..4]` spanning `logFrom = 10` to `logEnd = 14`. This
57
+ * mirrors the Session's own `snapshotEvents(fromSeq, toSeqExclusive)`.
58
+ *
59
+ * Both offsets are passed explicitly rather than read from the cursor so the
60
+ * caller states the log it is folding, which is also what makes the fallback
61
+ * below decidable.
62
+ */
63
+ export function advanceSessionEventCursor(cursor, events, logFrom, logEnd, fold) {
64
+ if (logEnd !== logFrom + events.length) {
65
+ // The slice and the log it claims to describe disagree, so the tail it is
66
+ // missing is indistinguishable from events that were never there. Refuse
67
+ // rather than fold a shape the caller did not state.
68
+ throw new RangeError(`session event cursor: ${events.length} events cannot span [${logFrom}, ${logEnd})`);
69
+ }
70
+ // Resume only from a position the log can still justify. Every other case
71
+ // re-folds from `logFrom`: one path handles truncation, a changed fork
72
+ // prefix, and a rebuilt log alike, so no caller has to classify them.
73
+ const resumable = cursor.logFrom === logFrom
74
+ && cursor.foldedThrough >= logFrom
75
+ && cursor.foldedThrough <= logEnd
76
+ && (cursor.foldedThrough === logFrom || anchorMatches(cursor, events));
77
+ const from = resumable ? cursor.foldedThrough : logFrom;
78
+ if (from === logEnd)
79
+ return cursor;
80
+ let value = resumable ? cursor.value : fold.start;
81
+ for (let index = from; index < logEnd; index += 1)
82
+ value = fold.step(value, events[index - logFrom]);
83
+ return { value, foldedThrough: logEnd, logFrom, anchor: anchorOf(events[logEnd - 1 - logFrom]) };
84
+ }
85
+ /**
86
+ * Whether the event the cursor stopped on still occupies that position with
87
+ * the type it had. The position is checked as well as the type because `seq`
88
+ * is a log index: an event that moved is a different event.
89
+ */
90
+ function anchorMatches(cursor, events) {
91
+ const anchor = cursor.anchor;
92
+ if (anchor === null)
93
+ return false;
94
+ const seen = events[anchor.seq - cursor.logFrom];
95
+ return seen !== undefined && seen.seq === anchor.seq && seen.type === anchor.type;
96
+ }
97
+ /** The identity a cursor anchors on when it stops at the last consumed event. */
98
+ function anchorOf(event) {
99
+ if (event === undefined)
100
+ return null;
101
+ return { seq: event.seq, type: event.type };
102
+ }
103
+ /**
104
+ * Advance one owner's fold and answer with the entry to store back, so every
105
+ * caller keeps the same one-entry-per-owner invariant.
106
+ *
107
+ * `events` is that Session's whole own-events slice starting at `logFrom` (its
108
+ * `inheritedEventCount`), so the slice's end is the log's end. The cursor is
109
+ * resumed only while `owned` still belongs to `sessionId`; every other case
110
+ * starts cold at `logFrom`, which is the same degradation the cursor's own
111
+ * guard performs for a rewritten log.
112
+ */
113
+ export function advanceOwnedSessionEventCursor(owned, sessionId, fold, events, logFrom) {
114
+ const cursor = owned !== undefined && owned.sessionId === sessionId ? owned.cursor : initSessionEventCursor(fold, logFrom);
115
+ return { sessionId, cursor: advanceSessionEventCursor(cursor, events, logFrom, logFrom + events.length, fold) };
116
+ }