@wowyuarm/dsh-agent-team 0.1.10 → 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 (110) hide show
  1. package/README.md +13 -0
  2. package/README.zh.md +13 -0
  3. package/package.json +52 -47
  4. package/packages/agent-team/README.md +4 -2
  5. package/packages/agent-team/README.zh.md +4 -2
  6. package/packages/agent-team/lib/context-management.js +40 -50
  7. package/packages/agent-team/lib/context-projection.js +10 -0
  8. package/packages/agent-team/lib/index.js +235 -112
  9. package/packages/agent-team/lib/invariant.js +38 -4
  10. package/packages/agent-team/lib/ledger.js +554 -271
  11. package/packages/agent-team/lib/member-context.js +6 -2
  12. package/packages/agent-team/lib/member-time-context.js +20 -5
  13. package/packages/agent-team/lib/pressure-policy.js +33 -9
  14. package/packages/agent-team/lib/session-event-cursor.js +116 -0
  15. package/packages/agent-team/lib/session-remediation.js +38 -26
  16. package/packages/agent-team/lib/spec.js +46 -34
  17. package/packages/agent-team/lib/stored-session-reader.js +138 -0
  18. package/packages/agent-team/lib/typert.host.js +107 -40
  19. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  20. package/packages/agent-team/lib/typert.remote-client.js +84 -32
  21. package/packages/agent-team/lib/types/context-management.d.ts +7 -0
  22. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
  24. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/index.d.ts +78 -2
  26. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
  28. package/packages/agent-team/lib/types/ledger.d.ts +180 -7
  29. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  31. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
  33. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  34. package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
  35. package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
  36. package/packages/agent-team/lib/types/session-remediation.d.ts +23 -0
  37. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -1
  38. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  39. package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
  40. package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
  41. package/packages/agent-team/lib/types/types/entities.d.ts +28 -2
  42. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  43. package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
  44. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  45. package/packages/agent-team/lib/types/types/requests-results.d.ts +44 -3
  46. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  47. package/packages/agent-team/preset/team-member/agent.cordis.yml +3 -1
  48. package/packages/client-agent-team/README.md +1 -1
  49. package/packages/client-agent-team/README.zh.md +1 -1
  50. package/packages/client-agent-team/lib/client.js +1068 -375
  51. package/packages/client-agent-team/lib/client.js.map +1 -1
  52. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -4
  54. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +42 -22
  56. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +13 -24
  58. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
  59. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamComposer.js +15 -9
  61. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  62. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  63. package/packages/client-agent-team/lib/types/client/TeamConversation.js +8 -1
  64. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +21 -0
  65. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
  66. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +71 -0
  67. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  68. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
  69. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
  70. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  71. package/packages/client-agent-team/lib/types/client/TeamMessage.js +11 -4
  72. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
  73. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  74. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
  75. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  76. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +102 -28
  77. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  78. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  79. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +50 -7
  80. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/index.js +17 -6
  82. package/packages/client-agent-team/lib/types/client/locales.d.ts +18 -0
  83. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  84. package/packages/client-agent-team/lib/types/client/locales.js +18 -0
  85. package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
  86. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  87. package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
  88. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
  89. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
  90. package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
  91. package/packages/client-agent-team/lib/types/client/slots.d.ts +7 -1
  92. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  93. package/packages/client-agent-team/lib/types/client/team-changes.d.ts +26 -1
  94. package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
  95. package/packages/client-agent-team/lib/types/client/team-changes.js +66 -15
  96. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
  97. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
  98. package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
  99. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
  100. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  101. package/packages/client-agent-team/lib/types/client/team-formatters.js +34 -6
  102. package/packages/tool-agent-team/README.md +1 -1
  103. package/packages/tool-agent-team/README.zh.md +1 -1
  104. package/packages/tool-agent-team/lib/context-tools.js +18 -14
  105. package/packages/tool-agent-team/lib/host-access.js +20 -0
  106. package/packages/tool-agent-team/lib/index.js +16 -17
  107. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  108. package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
  109. package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
  110. 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]');
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
30
30
  import { formatTeamDuration, formatTeamTimestamp } from "./time-format.js";
31
+ import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
31
32
  export const name = 'wowyuarm-agent-team-member-time-context';
32
33
  /** Default minimum spacing between two snapshots within one turn, in ms. */
33
34
  export const CLOCK_REFRESH_INTERVAL_MS = 1_800_000;
@@ -90,8 +91,22 @@ export function renderClockSnapshot(input) {
90
91
  + `Elapsed since the preceding ${baseline}: ${elapsed}.\n`
91
92
  + 'Team collaboration timestamps use UTC+8. Sequence and revision, not wall-clock time, determine ordering and concurrency.';
92
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
+ };
93
104
  export function apply(ctx, config = {}) {
94
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();
95
110
  ctx.on('agent/pre-step', async ({ agent, turn, step, signal }, next) => {
96
111
  const decision = await next();
97
112
  if (decision.kind === 'reject' || signal.aborted)
@@ -102,13 +117,13 @@ export function apply(ctx, config = {}) {
102
117
  const host = ctx.get('agentTeam');
103
118
  if (host === undefined)
104
119
  return decision;
105
- if (host.memberForAgent(agent) === undefined)
120
+ const member = host.memberForAgent(agent);
121
+ if (member === undefined)
106
122
  return decision;
107
123
  const now = Date.now();
108
- // A rollover starts a fresh Session log, so the fold never guesses
109
- // elapsed across generations: a missing prior event renders
110
- // `unavailable`, not a fabricated baseline.
111
- const baseline = foldClockBaseline(agent.session.ownEvents());
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;
112
127
  // Turn-first-step always samples; later steps sample only at the refresh
113
128
  // interval, so a quick tool-dense turn stays at one line.
114
129
  if (!shouldSampleClock(step, now, baseline, refreshIntervalMs))
@@ -18,6 +18,26 @@ 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
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
+ };
21
41
  /** One pressure-notice text; concise, structured, and inside the reserve. */
22
42
  export function contextPressureNoticeText(input) {
23
43
  const claims = input.activeClaims.length === 0 ? 'none' : input.activeClaims.join(', ');
@@ -41,6 +61,14 @@ export class PressurePolicyCoordinator {
41
61
  * Process-only by design: a restart re-earns one sequence per chain.
42
62
  */
43
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();
44
72
  disposed = false;
45
73
  constructor(options) {
46
74
  this.options = options;
@@ -55,14 +83,10 @@ export class PressurePolicyCoordinator {
55
83
  * rollover starts a fresh Session whose own event span has no notice yet,
56
84
  * which is exactly the documented re-arm.
57
85
  */
58
- noticeDelivered(agent) {
59
- for (const event of agent.session.ownEvents()) {
60
- if (event.type === 'user/message' && isPressureNotice(event.data))
61
- return true;
62
- if (event.type === 'agent/inbox/spliced' && event.data.inserted.some(isPressureNotice))
63
- return true;
64
- }
65
- 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;
66
90
  }
67
91
  dispose() {
68
92
  this.disposed = true;
@@ -98,7 +122,7 @@ export class PressurePolicyCoordinator {
98
122
  const outcome = await this.enforceHardLimit(agent, member.memberId, member.sessionId, signal);
99
123
  return outcome ? { kind: 'continue' } : { kind: 'reject' };
100
124
  }
101
- if (usageTokens >= handoffAt && !this.noticeDelivered(agent)) {
125
+ if (usageTokens >= handoffAt && !this.noticeDelivered(agent, member.memberId)) {
102
126
  const notice = createUserMessage({
103
127
  content: [{ type: 'text', text: contextPressureNoticeText({
104
128
  usageTokens, handoffAt, hardLimit,
@@ -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
+ }
@@ -266,11 +266,6 @@ function physicalParentSession(rows) {
266
266
  const parent = rows[0]?.parentSession;
267
267
  return typeof parent === 'string' ? parent : undefined;
268
268
  }
269
- /**
270
- * The startup remediation pass. Never throws to its caller: a failed or
271
- * partial walk logs and leaves the completion cache unrecorded, so the next
272
- * start retries exactly the Members that need it.
273
- */
274
269
  export class SessionRemediation {
275
270
  ctx;
276
271
  persistence;
@@ -303,29 +298,43 @@ export class SessionRemediation {
303
298
  for (const member of members) {
304
299
  if (member.state !== 'enabled')
305
300
  continue;
306
- if (this.cacheStillValid(member))
301
+ const outcome = await this.remediateMember(member);
302
+ if (outcome.cacheHit)
307
303
  continue;
308
304
  walked += 1;
309
- let repairedCount = 0;
310
- let untouchedCount = 0;
311
- try {
312
- const summary = await this.remediateLineage(member);
313
- repairedCount = summary.repaired;
314
- untouchedCount = summary.untouched;
315
- }
316
- catch (error) {
317
- // Unexpected per-Member failure: log and leave the cache unrecorded.
318
- this.ctx.logger.warn(`agent-team: legacy Session remediation for member '${member.handle}' did not complete: ${error instanceof Error ? error.message : String(error)}`);
319
- continue;
320
- }
321
- repaired += repairedCount;
322
- untouched += untouchedCount;
323
- await this.recordCompletion(member);
305
+ repaired += outcome.repaired;
306
+ untouched += outcome.untouched;
324
307
  }
325
308
  if (walked > 0) {
326
309
  this.ctx.logger.info(`agent-team: legacy Session remediation walked ${walked} member lineage(s): ${repaired} artifact(s) repaired, ${untouched} left untouched`);
327
310
  }
328
311
  }
312
+ /**
313
+ * Remediate one Member's lineage; the bounded in-place heal a restart
314
+ * performs after an activation refused on a session. `completed` with zero
315
+ * repairs is the deterministic nothing-to-do answer (a finished walk found
316
+ * nothing provably this plugin's, or the cache already covered the Member);
317
+ * `completed: false` means the walk itself failed and a later attempt
318
+ * should retry. Never throws.
319
+ */
320
+ async remediateMember(member) {
321
+ if (member.state !== 'enabled' || this.cacheStillValid(member))
322
+ return { repaired: 0, untouched: 0, completed: true, cacheHit: true };
323
+ try {
324
+ const summary = await this.remediateLineage(member);
325
+ // A walk that failed a repair is not evidence of a clean lineage: it is
326
+ // left uncached so the next start retries exactly this Member, and it
327
+ // reports `completed: false` so no caller reads it as the deterministic
328
+ // nothing-to-do answer.
329
+ if (summary.failed === 0)
330
+ await this.recordCompletion(member);
331
+ return { repaired: summary.repaired, untouched: summary.untouched, completed: summary.failed === 0, cacheHit: false };
332
+ }
333
+ catch (error) {
334
+ this.ctx.logger.warn(`agent-team: legacy Session remediation for member '${member.handle}' did not complete: ${error instanceof Error ? error.message : String(error)}`);
335
+ return { repaired: 0, untouched: 0, completed: false, cacheHit: false };
336
+ }
337
+ }
329
338
  /** Whether the cached walk still covers this Member under the current format. */
330
339
  cacheStillValid(member) {
331
340
  const record = this.table?.get(member.memberId);
@@ -360,6 +369,7 @@ export class SessionRemediation {
360
369
  async remediateLineage(member) {
361
370
  let repaired = 0;
362
371
  let untouched = 0;
372
+ let failed = 0;
363
373
  const seen = new Set();
364
374
  let sessionId = member.sessionId;
365
375
  for (let depth = 0; sessionId !== undefined && !seen.has(sessionId) && depth < MAX_LINEAGE_DEPTH; depth += 1) {
@@ -369,9 +379,11 @@ export class SessionRemediation {
369
379
  repaired += 1;
370
380
  else if (outcome.status === 'left-untouched')
371
381
  untouched += 1;
382
+ else if (outcome.status === 'repair-failed')
383
+ failed += 1;
372
384
  sessionId = outcome.parentSession === undefined ? undefined : outcome.parentSession;
373
385
  }
374
- return { repaired, untouched };
386
+ return { repaired, untouched, failed };
375
387
  }
376
388
  /** Remediate one lineage Session: verify readability, and repair when the refusal is this plugin's legacy source shape. */
377
389
  async remediateOne(sessionId, member) {
@@ -409,7 +421,7 @@ export class SessionRemediation {
409
421
  }
410
422
  catch (readError) {
411
423
  this.ctx.logger.warn(`agent-team: could not decode the stored artifact of Session '${sessionId}' (${basename(path)}): ${readError instanceof Error ? readError.message : String(readError)}`);
412
- return { status: 'left-untouched', parentSession: undefined, reason: 'artifact undecodable' };
424
+ return { status: 'repair-failed', parentSession: undefined, reason: 'artifact undecodable' };
413
425
  }
414
426
  const parentSession = physicalParentSession(rows);
415
427
  if (!rows.some(row => containsLegacySource(row))) {
@@ -423,7 +435,7 @@ export class SessionRemediation {
423
435
  }
424
436
  catch (proofError) {
425
437
  this.ctx.logger.warn(`agent-team: Session '${sessionId}' of member '${member.handle}' still refuses after the legacy source rewrite (${proofError instanceof Error ? proofError.message : String(proofError)}); leaving it untouched`);
426
- return { status: 'left-untouched', parentSession, reason: 'residual refusal after rewrite' };
438
+ return { status: 'repair-failed', parentSession, reason: 'residual refusal after rewrite' };
427
439
  }
428
440
  try {
429
441
  const published = await this.publishSibling(path, encodeCurrentArtifact(artifact));
@@ -434,7 +446,7 @@ export class SessionRemediation {
434
446
  }
435
447
  catch (publishError) {
436
448
  this.ctx.logger.warn(`agent-team: could not publish the remediated sibling for Session '${sessionId}' (${basename(path)}): ${publishError instanceof Error ? publishError.message : String(publishError)}`);
437
- return { status: 'left-untouched', parentSession, reason: 'publish failed' };
449
+ return { status: 'repair-failed', parentSession, reason: 'publish failed' };
438
450
  }
439
451
  // Success is proven by the storage layer itself, not by our own writer.
440
452
  try {
@@ -448,7 +460,7 @@ export class SessionRemediation {
448
460
  }
449
461
  catch (verifyError) {
450
462
  this.ctx.logger.warn(`agent-team: the remediated sibling for Session '${sessionId}' was published but still refuses: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`);
451
- return { status: 'left-untouched', parentSession, reason: 'sibling refused after publish' };
463
+ return { status: 'repair-failed', parentSession, reason: 'sibling refused after publish' };
452
464
  }
453
465
  this.ctx.logger.info(`agent-team: repaired legacy source kinds in Session '${sessionId}' of member '${member.handle}' by publishing a current-format sibling; the original artifact is untouched`);
454
466
  return { status: 'repaired', parentSession };
@@ -216,6 +216,47 @@ const claimOperation = (kind) => z.object({
216
216
  inbox: inboxDeltaSchema,
217
217
  }).strict(),
218
218
  }).strict();
219
+ /**
220
+ * The five departure-fact collections a releasing operation persists. All four
221
+ * release kinds carry this snapshot under their own `kind` literal and their own
222
+ * identifying fields; the durable half that must stay identical lives here once,
223
+ * so a new release kind cannot silently drop or invent a field.
224
+ */
225
+ const releaseSnapshotFields = {
226
+ claims: z.array(claimSchema),
227
+ activities: z.array(claimsReleasedActivitySchema),
228
+ tasks: z.array(taskSchema),
229
+ threads: z.array(threadSchema),
230
+ inbox: inboxDeltaSchema,
231
+ };
232
+ /** The receipt form every new read writes: the progress it made and the Inbox delta it consumed. */
233
+ const threadReadReceiptDataSchema = z.object({
234
+ workspaceId: workspaceIdSchema,
235
+ memberId: memberIdSchema,
236
+ threadRef: threadRefSchema,
237
+ taskRef: taskRefSchema.optional(),
238
+ readThroughSequence: z.number().int().nonnegative(),
239
+ inbox: inboxDeltaSchema,
240
+ }).strict().transform(omitUndefined);
241
+ /**
242
+ * The pre-receipt Thread-read snapshot. Still accepted so a ledger written
243
+ * before the receipt form keeps opening; both shapes are strict, so a stored
244
+ * record parses as exactly one of them.
245
+ */
246
+ const threadReadSnapshotDataSchema = z.object({
247
+ workspaceId: workspaceIdSchema,
248
+ memberId: memberIdSchema,
249
+ task: taskSchema.optional(),
250
+ thread: threadSchema,
251
+ claims: z.array(claimSchema),
252
+ anchor: messageSchema,
253
+ anchorMentions: z.array(memberIdSchema),
254
+ facts: z.array(readFactSchema),
255
+ readThroughSequence: z.number().int().nonnegative(),
256
+ remainingUnreadCount: z.number().int().nonnegative(),
257
+ attention: attentionSchema.optional(),
258
+ inbox: inboxDeltaSchema,
259
+ }).strict().transform(omitUndefined);
219
260
  /** Closed Agent Team operation union before occurrence stamping. */
220
261
  const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
221
262
  z.object({
@@ -258,11 +299,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
258
299
  kind: z.literal('team/member-archived'),
259
300
  data: z.object({
260
301
  member: memberSchema,
261
- claims: z.array(claimSchema),
262
- activities: z.array(claimsReleasedActivitySchema),
263
- tasks: z.array(taskSchema),
264
- threads: z.array(threadSchema),
265
- inbox: inboxDeltaSchema,
302
+ ...releaseSnapshotFields,
266
303
  }).strict(),
267
304
  }).strict(),
268
305
  z.object({
@@ -324,11 +361,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
324
361
  workspaceId: workspaceIdSchema,
325
362
  channelRef: channelRefSchema,
326
363
  memberId: memberIdSchema,
327
- claims: z.array(claimSchema),
328
- activities: z.array(claimsReleasedActivitySchema),
329
- tasks: z.array(taskSchema),
330
- threads: z.array(threadSchema),
331
- inbox: inboxDeltaSchema,
364
+ ...releaseSnapshotFields,
332
365
  }).strict(),
333
366
  }).strict(),
334
367
  z.object({
@@ -338,11 +371,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
338
371
  data: z.object({
339
372
  workspaceId: workspaceIdSchema,
340
373
  channel: channelSchema,
341
- claims: z.array(claimSchema),
342
- activities: z.array(claimsReleasedActivitySchema),
343
- tasks: z.array(taskSchema),
344
- threads: z.array(threadSchema),
345
- inbox: inboxDeltaSchema,
374
+ ...releaseSnapshotFields,
346
375
  }).strict(),
347
376
  }).strict(),
348
377
  z.object({
@@ -419,20 +448,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
419
448
  ...operationBase,
420
449
  previousOperationId: operationIdSchema.nullable(),
421
450
  kind: z.literal('team/thread-read'),
422
- data: z.object({
423
- workspaceId: workspaceIdSchema,
424
- memberId: memberIdSchema,
425
- task: taskSchema.optional(),
426
- thread: threadSchema,
427
- claims: z.array(claimSchema),
428
- anchor: messageSchema,
429
- anchorMentions: z.array(memberIdSchema),
430
- facts: z.array(readFactSchema),
431
- readThroughSequence: z.number().int().nonnegative(),
432
- remainingUnreadCount: z.number().int().nonnegative(),
433
- attention: attentionSchema.optional(),
434
- inbox: inboxDeltaSchema,
435
- }).strict().transform(omitUndefined),
451
+ data: z.union([threadReadReceiptDataSchema, threadReadSnapshotDataSchema]),
436
452
  }).strict(),
437
453
  z.object({
438
454
  ...operationBase,
@@ -440,11 +456,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
440
456
  kind: z.literal('team/member-removed'),
441
457
  data: z.object({
442
458
  member: memberSchema,
443
- claims: z.array(claimSchema),
444
- activities: z.array(claimsReleasedActivitySchema),
445
- tasks: z.array(taskSchema),
446
- threads: z.array(threadSchema),
447
- inbox: inboxDeltaSchema,
459
+ ...releaseSnapshotFields,
448
460
  }).strict(),
449
461
  }).strict(),
450
462
  z.object({
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The Team's single seam for reading stored Sessions.
3
+ *
4
+ * Every per-Session read the Host performs goes through here: one
5
+ * `open → read → close` cycle whose handle lifecycle is guaranteed closed,
6
+ * and whose failures are normalized into the five categories consumers choose
7
+ * policy by. Callers never touch a `SessionHandle`, an artifact path, a
8
+ * format generation, or Harness error message text.
9
+ *
10
+ * The reader owns reading and classification only — not Member lifecycle,
11
+ * Agent create/resume, UI, ledger, or remediation (which reads raw artifact
12
+ * bytes through its own repair path).
13
+ *
14
+ * Classification notes: the shipped JSONL backend throws its
15
+ * `corrupt session log` family as plain `Error`s, so corruption is detected by
16
+ * that stable message prefix (an upstream gap; if the text ever changes the
17
+ * failure degrades to `unknown`, which consumers treat conservatively —
18
+ * blocking rather than silently skipping). Typed refusals are also matched
19
+ * through `error.cause` chains because the Harness agent layer wraps
20
+ * persistence failures during resume.
21
+ * @module @wowyuarm/dsh-agent-team/stored-session-reader
22
+ */
23
+ import { SessionFormatUnsupportedError, SessionPersistenceCorruptionError, SessionPersistenceNotFoundError, } from '@deepseek-ai/dsh-session-persistence';
24
+ /**
25
+ * A call site's session-read failure, carrying the typed classification so
26
+ * activation can route the diagnostic without re-matching error text.
27
+ */
28
+ export class StoredSessionReadError extends Error {
29
+ failure;
30
+ constructor(message, failure) {
31
+ super(message);
32
+ this.failure = failure;
33
+ this.name = 'StoredSessionReadError';
34
+ }
35
+ }
36
+ /** How deep to follow `error.cause` chains looking for a typed session failure. */
37
+ const MAX_CAUSE_DEPTH = 4;
38
+ /** The JSONL backend's stable corruption-message prefix (plain `Error`s, not typed). */
39
+ const CORRUPT_SESSION_LOG_TEXT = /corrupt session log/;
40
+ function messageOf(error) {
41
+ return error instanceof Error ? error.message : String(error);
42
+ }
43
+ /**
44
+ * The deterministic session-shaped classification of one error value:
45
+ * typed Harness failures plus the corruption message family. A system error
46
+ * code is deliberately NOT matched here — outside the reader an fs error may
47
+ * come from unrelated work (private memory, attachment cache), so only the
48
+ * reader itself, where the failed operation is known to be a session read,
49
+ * may classify an fs error as `io`.
50
+ */
51
+ function sessionClassOf(error) {
52
+ if (error instanceof SessionPersistenceNotFoundError)
53
+ return { kind: 'missing' };
54
+ if (error instanceof SessionFormatUnsupportedError) {
55
+ return { kind: 'refused', ...(error.location === undefined ? {} : { location: error.location }) };
56
+ }
57
+ if (error instanceof SessionPersistenceCorruptionError)
58
+ return { kind: 'corrupt' };
59
+ if (error instanceof Error && CORRUPT_SESSION_LOG_TEXT.test(error.message))
60
+ return { kind: 'corrupt' };
61
+ return undefined;
62
+ }
63
+ /** The first session-shaped classification in an error's cause chain, with the matched error for its message. */
64
+ function sessionClassInChain(error) {
65
+ let current = error;
66
+ for (let depth = 0; current !== undefined && current !== null && depth <= MAX_CAUSE_DEPTH; depth += 1) {
67
+ const classified = sessionClassOf(current);
68
+ if (classified !== undefined)
69
+ return { classified, matched: current };
70
+ current = current instanceof Error ? current.cause : undefined;
71
+ }
72
+ return undefined;
73
+ }
74
+ /**
75
+ * Classify any failure as a stored-Session read failure. Total: unmatched
76
+ * errors become `unknown`, and a direct system error code becomes `io` —
77
+ * call this only where the failed operation is known to be a session read.
78
+ */
79
+ export function classifyStoredSessionFailure(error, sessionId) {
80
+ const inChain = sessionClassInChain(error);
81
+ if (inChain !== undefined) {
82
+ return { kind: inChain.classified.kind, sessionId, detail: messageOf(inChain.matched), ...('location' in inChain.classified ? { location: inChain.classified.location } : {}) };
83
+ }
84
+ if (error !== null && typeof error === 'object' && typeof error.code === 'string') {
85
+ return { kind: 'io', sessionId, detail: messageOf(error) };
86
+ }
87
+ return { kind: 'unknown', sessionId, detail: messageOf(error) };
88
+ }
89
+ /**
90
+ * The session-shaped failure carried by an arbitrary error, when it carries
91
+ * one at all: deterministic markers (typed classes, the corruption message
92
+ * family) are matched through the cause chain; transient fs errors are not —
93
+ * outside the reader they are not provably session-related. Activation uses
94
+ * this to route diagnostics; `undefined` means the failure is not
95
+ * session-shaped.
96
+ */
97
+ export function sessionFailureOf(error, sessionId) {
98
+ const inChain = sessionClassInChain(error);
99
+ if (inChain === undefined)
100
+ return undefined;
101
+ return { kind: inChain.classified.kind, sessionId, detail: messageOf(inChain.matched), ...('location' in inChain.classified ? { location: inChain.classified.location } : {}) };
102
+ }
103
+ /** Read stored Sessions through one seam: full validated reads with normalized failures. */
104
+ export class StoredSessionReader {
105
+ ctx;
106
+ constructor(ctx) {
107
+ this.ctx = ctx;
108
+ }
109
+ /**
110
+ * Read one stored Session completely. The handle is closed on every path;
111
+ * any failure of open, read, or close is returned as its normalized
112
+ * category, never thrown.
113
+ */
114
+ async read(sessionId) {
115
+ try {
116
+ const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
117
+ try {
118
+ const { events } = await handle.read();
119
+ return { ok: true, inspection: { header: handle.header, inheritedEventCount: handle.inheritedEventCount, events } };
120
+ }
121
+ finally {
122
+ await handle.close();
123
+ }
124
+ }
125
+ catch (error) {
126
+ return { ok: false, failure: classifyStoredSessionFailure(error, sessionId) };
127
+ }
128
+ }
129
+ /**
130
+ * Whether one stored Session has durable persisted content, decided through
131
+ * `stat`: the backend reports a still-draining session through its pending
132
+ * header, so this is the existence probe activation relies on. Only the
133
+ * missing case returns `false`; unexpected errors propagate as before.
134
+ */
135
+ async exists(sessionId) {
136
+ return (await this.ctx.sessionPersistence.stat(sessionId)) !== undefined;
137
+ }
138
+ }