@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
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Startup remediation of legacy Member Session artifacts.
3
+ *
4
+ * Released dsh-agent-team 0.1.9 wrote its rollover handoff and checkpoint
5
+ * continuation into Member Session logs under bespoke message source kinds.
6
+ * dsh 0.1.5's session-format migration chain admits a closed source-kind
7
+ * vocabulary and refuses those artifacts fail-closed — the bytes are intact,
8
+ * but no reader (activation, session search, lineage timeline) can open them,
9
+ * which surfaces as Members going `unavailable`.
10
+ *
11
+ * This module restores readability without touching a byte of the refused
12
+ * artifact: it decodes the stored v0 log physically (frame-walking the
13
+ * concatenated zstd container), rewrites only this plugin's legacy source
14
+ * kinds into the admitted `plugin` shape the current writer already uses,
15
+ * re-validates the whole stream through the format catalog's own migration
16
+ * chain in memory, and publishes the migrated log as a sibling current-format
17
+ * artifact (`session.v3.jsonl.zstd`) — the storage layer's designated slot for
18
+ * a published current generation, atomically created and never overwritten.
19
+ * The v0 original remains the rollback path: delete the sibling.
20
+ *
21
+ * Scope is deliberately narrow: artifacts refused for any other reason
22
+ * (structural turn defects, foreign legacy fields) are left untouched with a
23
+ * diagnostic log — rewriting history a plugin did not author is out of
24
+ * bounds. Member availability for those rests on the activation recovery
25
+ * path's bounded fail-open, not on this migration.
26
+ *
27
+ * The walk runs once per plugin start, before any Member activation (no
28
+ * write lease exists yet) and records completion per Member in a separate
29
+ * durable domain — never in the `agent_team` ledger domain, whose version is
30
+ * the operation schema's compatibility contract, not a cache stamp.
31
+ * @module @wowyuarm/dsh-agent-team/session-remediation
32
+ */
33
+ import { randomUUID } from 'node:crypto';
34
+ import { link, readFile, rm, stat, writeFile } from 'node:fs/promises';
35
+ import { basename, dirname, join } from 'node:path';
36
+ import { constants as zstdConstants, zstdCompressSync, zstdDecompressSync } from 'node:zlib';
37
+ import { z } from 'zod';
38
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
39
+ import { sessionFormatCatalog } from '@deepseek-ai/dsh-session-format-catalog';
40
+ import { SessionFormatUnsupportedError } from '@deepseek-ai/dsh-session-persistence';
41
+ import { AGENT_TEAM_PLUGIN_ID, CHECKPOINT_SECTION_NAME, HANDOFF_CHECKPOINT, HANDOFF_EVENT_SEQ, HANDOFF_NEW_SESSION, HANDOFF_PREVIOUS_SESSION, HANDOFF_RELATED_FILES, HANDOFF_TRIGGER, } from "./context-source.js";
42
+ /** The rollover-handoff kind released 0.1.9 wrote; it is also handoff evidence in a log. */
43
+ export const LEGACY_HANDOFF_SOURCE_KIND = 'agent-team-context-handoff';
44
+ /** The two bespoke source kinds released 0.1.9 wrote; the migration's whole scope. */
45
+ const LEGACY_SOURCE_KINDS = new Set([LEGACY_HANDOFF_SOURCE_KIND, 'agent-team-context-continuation']);
46
+ /** The handoff kind alone, for handoff-presence questions. */
47
+ const LEGACY_HANDOFF_SOURCE_KINDS = new Set([LEGACY_HANDOFF_SOURCE_KIND]);
48
+ /** Sibling filename the storage layer reads in preference when present. */
49
+ const CURRENT_GENERATION_FILENAME = 'session.v3.jsonl.zstd';
50
+ /** Lineage walks stop at this depth; a chain this long is a defect, not history. */
51
+ const MAX_LINEAGE_DEPTH = 64;
52
+ /**
53
+ * Durable completion cache. A separate domain on purpose: the `agent_team`
54
+ * domain's version gates the ledger schema, and stamping migration bookkeeping
55
+ * there would turn a cache refresh into a ledger-format break. Records are
56
+ * disposable derived state — losing them only re-runs the (idempotent) walk.
57
+ */
58
+ const sessionRemediationDomainSpec = defineDomain({
59
+ name: 'agent_team_remediation',
60
+ version: 1,
61
+ tables: {
62
+ lineages: domainTable(z.object({
63
+ memberId: z.string().min(1),
64
+ formatVersion: z.number().int().nonnegative(),
65
+ currentSessionId: z.string().min(1),
66
+ verifiedAt: z.number().int().nonnegative(),
67
+ }).strict()),
68
+ },
69
+ });
70
+ /**
71
+ * Rewrite one legacy source onto the admitted shape the current writer uses.
72
+ * The original `sections` array is preserved verbatim (it carries the
73
+ * model-authored handoff prose); envelope fields ride additional named
74
+ * sections exactly as `handoffSections` composes them, added only when the
75
+ * field is present and no section of that name exists.
76
+ */
77
+ function admitLegacySource(source) {
78
+ if (source.kind === 'agent-team-context-handoff') {
79
+ const sections = Array.isArray(source.sections)
80
+ ? source.sections.filter((section) => section !== null && typeof section === 'object')
81
+ : [];
82
+ const envelope = [
83
+ [HANDOFF_PREVIOUS_SESSION, source.previousSessionId],
84
+ [HANDOFF_NEW_SESSION, source.newSessionId],
85
+ [HANDOFF_TRIGGER, source.trigger],
86
+ [HANDOFF_EVENT_SEQ, source.handoffEventSeq],
87
+ [HANDOFF_CHECKPOINT, source.checkpointRef],
88
+ [HANDOFF_RELATED_FILES, Array.isArray(source.relatedFiles)
89
+ ? source.relatedFiles.filter((file) => typeof file === 'string').join(', ')
90
+ : undefined],
91
+ ];
92
+ for (const [name, value] of envelope) {
93
+ if (value === undefined || value === null || value === '')
94
+ continue;
95
+ if (sections.some(section => section.name === name))
96
+ continue;
97
+ sections.push({ name, text: String(value) });
98
+ }
99
+ return { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'snapshot', sections };
100
+ }
101
+ // A continuation carried its checkpoint ref at the top level; the admitted
102
+ // shape reads it back from a single-name section, same as the writer.
103
+ return {
104
+ kind: 'plugin',
105
+ plugin: AGENT_TEAM_PLUGIN_ID,
106
+ form: 'snapshot',
107
+ sections: [{ name: CHECKPOINT_SECTION_NAME, text: String(source.checkpointRef ?? '') }],
108
+ };
109
+ }
110
+ /**
111
+ * Deep-rewrite every legacy Team source in one physical row tree. The kinds
112
+ * are globally unique to this plugin, so a structural match anywhere in the
113
+ * row (message sources ride several payload positions) is exactly the set to
114
+ * rewrite; everything else passes through untouched.
115
+ */
116
+ export function admitLegacyRow(value) {
117
+ if (value === null || typeof value !== 'object')
118
+ return value;
119
+ if (Array.isArray(value))
120
+ return value.map(admitLegacyRow);
121
+ const out = {};
122
+ for (const [key, item] of Object.entries(value))
123
+ out[key] = admitLegacyRow(item);
124
+ if (typeof out.kind === 'string' && LEGACY_SOURCE_KINDS.has(out.kind))
125
+ return admitLegacySource(out);
126
+ return out;
127
+ }
128
+ /** Whether one value tree still contains a legacy Team source kind. */
129
+ export function containsLegacySource(value) {
130
+ return containsLegacySourceKind(value, LEGACY_SOURCE_KINDS);
131
+ }
132
+ /**
133
+ * Whether one value tree carries the retired rollover-handoff kind. A
134
+ * continuation is deliberately excluded: it never carried a handoff, so it is
135
+ * not evidence that one already arrived.
136
+ */
137
+ export function carriesLegacyHandoffSource(value) {
138
+ return containsLegacySourceKind(value, LEGACY_HANDOFF_SOURCE_KINDS);
139
+ }
140
+ /**
141
+ * Whether a generation's log already holds a rollover handoff, in either
142
+ * shape. The admitted shape surfaces as a projection boundary; a generation
143
+ * rescued from the retired kinds carries the very same handoff as a source the
144
+ * projection does not classify, so recovery that keys on the boundary alone
145
+ * would deliver that handoff a second time.
146
+ */
147
+ export function handoffAlreadyInLog(boundaries, ownEvents) {
148
+ if (boundaries.some(boundary => boundary.source === 'handoff'))
149
+ return true;
150
+ return ownEvents.some(carriesLegacyHandoffSource);
151
+ }
152
+ function containsLegacySourceKind(value, kinds) {
153
+ if (value === null || typeof value !== 'object')
154
+ return false;
155
+ if (Array.isArray(value))
156
+ return value.some(item => containsLegacySourceKind(item, kinds));
157
+ const kind = value.kind;
158
+ if (typeof kind === 'string' && kinds.has(kind))
159
+ return true;
160
+ return Object.values(value).some(item => containsLegacySourceKind(item, kinds));
161
+ }
162
+ const ZSTD_MAGIC = 0xfd2fb528;
163
+ const SKIPPABLE_FROM = 0x184d2a50;
164
+ const SKIPPABLE_TO = 0x184d2a5f;
165
+ /**
166
+ * Byte ranges of every complete zstd frame in a concatenated container, plus
167
+ * the offset of a torn final frame. Node's zstd bindings decode only the
168
+ * first frame of their input, so multi-frame containers must be walked
169
+ * structurally; scanning for the magic alone is unsound because the magic
170
+ * sequence can occur inside compressed payloads.
171
+ */
172
+ export function scanZstdFrames(buffer) {
173
+ const frames = [];
174
+ let at = 0;
175
+ let tornStart;
176
+ while (at < buffer.length) {
177
+ if (at + 4 > buffer.length) {
178
+ tornStart = at;
179
+ break;
180
+ }
181
+ const magic = buffer.readUInt32LE(at);
182
+ if (magic >= SKIPPABLE_FROM && magic <= SKIPPABLE_TO) {
183
+ if (at + 8 > buffer.length) {
184
+ tornStart = at;
185
+ break;
186
+ }
187
+ const size = buffer.readUInt32LE(at + 4);
188
+ if (at + 8 + size > buffer.length) {
189
+ tornStart = at;
190
+ break;
191
+ }
192
+ frames.push({ start: at, end: at + 8 + size, skippable: true });
193
+ at += 8 + size;
194
+ continue;
195
+ }
196
+ if (magic !== ZSTD_MAGIC)
197
+ throw new Error(`not a zstd frame at byte ${at}`);
198
+ if (at + 5 > buffer.length) {
199
+ tornStart = at;
200
+ break;
201
+ }
202
+ const descriptor = buffer.readUInt8(at + 4);
203
+ const contentSizeFlag = descriptor >> 6;
204
+ const singleSegment = (descriptor >> 5) & 1;
205
+ const hasChecksum = (descriptor >> 2) & 1;
206
+ const dictionaryFlag = descriptor & 3;
207
+ let cursor = at + 5;
208
+ if (singleSegment === 0)
209
+ cursor += 1;
210
+ const dictionaryLengths = [0, 1, 2, 4];
211
+ const contentSizeLengths = [0, 2, 4, 8];
212
+ cursor += dictionaryLengths[dictionaryFlag] ?? 0;
213
+ cursor += contentSizeFlag === 0 ? (singleSegment === 1 ? 1 : 0) : (contentSizeLengths[contentSizeFlag] ?? 0);
214
+ for (;;) {
215
+ if (cursor + 3 > buffer.length)
216
+ throw new Error(`torn zstd block header at byte ${cursor}`);
217
+ const header = buffer.readUInt8(cursor) | (buffer.readUInt8(cursor + 1) << 8) | (buffer.readUInt8(cursor + 2) << 16);
218
+ const last = header & 1;
219
+ const blockType = (header >> 1) & 3;
220
+ const size = header >> 3;
221
+ if (blockType === 3)
222
+ throw new Error(`reserved zstd block type at byte ${cursor}`);
223
+ cursor += 3 + (blockType === 1 ? 1 : size);
224
+ if (last === 1)
225
+ break;
226
+ }
227
+ if (hasChecksum === 1)
228
+ cursor += 4;
229
+ if (cursor > buffer.length)
230
+ throw new Error(`torn zstd frame at byte ${at}`);
231
+ frames.push({ start: at, end: cursor, skippable: false });
232
+ at = cursor;
233
+ }
234
+ return { frames, tornStart };
235
+ }
236
+ /** Decode one stored zstd container to its physical JSONL rows. */
237
+ export function readPhysicalRows(buffer) {
238
+ const { frames } = scanZstdFrames(buffer);
239
+ const chunks = frames.map(frame => frame.skippable ? Buffer.alloc(0) : zstdDecompressSync(buffer.subarray(frame.start, frame.end)));
240
+ const lines = Buffer.concat(chunks).toString('utf8').split('\n').filter(line => line.length > 0);
241
+ return lines.map(line => JSON.parse(line));
242
+ }
243
+ /** Encode the migrated artifact as one checksummed zstd frame per JSONL row. */
244
+ function encodeCurrentArtifact(artifact) {
245
+ const checksum = { params: { [zstdConstants.ZSTD_c_checksumFlag]: 1 } };
246
+ const lines = [
247
+ JSON.stringify(sessionFormatCatalog.encodeCurrentHeader(artifact.header, artifact.inheritedEventCount)),
248
+ ...artifact.events.map(event => JSON.stringify(sessionFormatCatalog.encodeCurrentEvent(event))),
249
+ ];
250
+ return Buffer.concat(lines.map(line => zstdCompressSync(Buffer.from(line + '\n'), checksum)));
251
+ }
252
+ /** Run the format catalog's own migration chain over transformed rows; throws on any residual refusal. */
253
+ function migrateForProof(rows) {
254
+ const [header, ...events] = rows;
255
+ const handle = sessionFormatCatalog.createRestore(header, { recovery: 'recoverable', validation: 'current' });
256
+ for (const row of events)
257
+ handle.decodeRow(row);
258
+ return handle.finish();
259
+ }
260
+ /** The JSONL artifact path a format refusal reports, when it reports one. */
261
+ function artifactPath(error) {
262
+ return error.location?.kind === 'jsonl' ? error.location.path : undefined;
263
+ }
264
+ /** The recorded parent Session of one physical header row. */
265
+ function physicalParentSession(rows) {
266
+ const parent = rows[0]?.parentSession;
267
+ return typeof parent === 'string' ? parent : undefined;
268
+ }
269
+ export class SessionRemediation {
270
+ ctx;
271
+ persistence;
272
+ table;
273
+ constructor(ctx, persistence, domain) {
274
+ this.ctx = ctx;
275
+ this.persistence = persistence;
276
+ this.table = domain?.table('lineages');
277
+ }
278
+ /** Open the completion-cache domain; a failure only costs re-walking. */
279
+ static async open(ctx) {
280
+ try {
281
+ return await ctx.storageDomain.open(sessionRemediationDomainSpec);
282
+ }
283
+ catch (error) {
284
+ ctx.logger.warn(`agent-team: session remediation cache unavailable (walks will re-run): ${error instanceof Error ? error.message : String(error)}`);
285
+ return undefined;
286
+ }
287
+ }
288
+ /**
289
+ * Remediate every enabled Member's lineage once, before activation. A
290
+ * Member is skipped when its cache record still matches the current format
291
+ * version and Session binding — a rollover or a dsh format change is what
292
+ * re-opens the walk.
293
+ */
294
+ async remediateEnabledMembers(members) {
295
+ let repaired = 0;
296
+ let untouched = 0;
297
+ let walked = 0;
298
+ for (const member of members) {
299
+ if (member.state !== 'enabled')
300
+ continue;
301
+ const outcome = await this.remediateMember(member);
302
+ if (outcome.cacheHit)
303
+ continue;
304
+ walked += 1;
305
+ repaired += outcome.repaired;
306
+ untouched += outcome.untouched;
307
+ }
308
+ if (walked > 0) {
309
+ this.ctx.logger.info(`agent-team: legacy Session remediation walked ${walked} member lineage(s): ${repaired} artifact(s) repaired, ${untouched} left untouched`);
310
+ }
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
+ }
338
+ /** Whether the cached walk still covers this Member under the current format. */
339
+ cacheStillValid(member) {
340
+ const record = this.table?.get(member.memberId);
341
+ return record !== undefined
342
+ && record.formatVersion === sessionFormatCatalog.currentVersion
343
+ && record.currentSessionId === member.sessionId;
344
+ }
345
+ async recordCompletion(member) {
346
+ if (this.table === undefined)
347
+ return;
348
+ try {
349
+ await this.table.put(member.memberId, {
350
+ memberId: member.memberId,
351
+ formatVersion: sessionFormatCatalog.currentVersion,
352
+ currentSessionId: member.sessionId,
353
+ verifiedAt: Date.now(),
354
+ });
355
+ }
356
+ catch (error) {
357
+ this.ctx.logger.warn(`agent-team: could not record session remediation completion for member '${member.handle}' (the walk will re-run): ${error instanceof Error ? error.message : String(error)}`);
358
+ }
359
+ }
360
+ /**
361
+ * Walk one Member's lineage from its bound Session up through recorded
362
+ * parent Sessions, remediating refused artifacts in place. Readable
363
+ * artifacts still carrying legacy kinds inside a current-format generation
364
+ * (an artifact of the one-off 2026-09-10 manual rescue) are counted and
365
+ * logged but not rewritten: they are readable, and rewriting a generation
366
+ * the storage layer may still append to is a write-path risk this pass
367
+ * does not take.
368
+ */
369
+ async remediateLineage(member) {
370
+ let repaired = 0;
371
+ let untouched = 0;
372
+ let failed = 0;
373
+ const seen = new Set();
374
+ let sessionId = member.sessionId;
375
+ for (let depth = 0; sessionId !== undefined && !seen.has(sessionId) && depth < MAX_LINEAGE_DEPTH; depth += 1) {
376
+ seen.add(sessionId);
377
+ const outcome = await this.remediateOne(sessionId, member);
378
+ if (outcome.status === 'repaired')
379
+ repaired += 1;
380
+ else if (outcome.status === 'left-untouched')
381
+ untouched += 1;
382
+ else if (outcome.status === 'repair-failed')
383
+ failed += 1;
384
+ sessionId = outcome.parentSession === undefined ? undefined : outcome.parentSession;
385
+ }
386
+ return { repaired, untouched, failed };
387
+ }
388
+ /** Remediate one lineage Session: verify readability, and repair when the refusal is this plugin's legacy source shape. */
389
+ async remediateOne(sessionId, member) {
390
+ try {
391
+ const handle = await this.persistence.open(sessionId, 'read');
392
+ try {
393
+ const { events } = await handle.read();
394
+ if (events.some(event => containsLegacySource(event))) {
395
+ this.ctx.logger.info(`agent-team: Session '${sessionId}' of member '${member.handle}' reads fine but still carries legacy envelope kinds; its history metadata stays degraded until the next rollover`);
396
+ }
397
+ return { status: 'already-readable', parentSession: handle.header.parentSession };
398
+ }
399
+ finally {
400
+ await handle.close();
401
+ }
402
+ }
403
+ catch (error) {
404
+ if (!(error instanceof SessionFormatUnsupportedError)) {
405
+ this.ctx.logger.warn(`agent-team: Session '${sessionId}' of member '${member.handle}' is unreadable for a non-format reason: ${error instanceof Error ? error.message : String(error)}`);
406
+ return { status: 'left-untouched', parentSession: undefined, reason: 'unreadable for a non-format reason' };
407
+ }
408
+ const path = artifactPath(error);
409
+ if (path === undefined) {
410
+ this.ctx.logger.warn(`agent-team: refused Session '${sessionId}' of member '${member.handle}' did not report a JSONL artifact path: ${error.message}`);
411
+ return { status: 'left-untouched', parentSession: undefined, reason: 'no artifact path reported' };
412
+ }
413
+ return await this.repairArtifact(sessionId, path, member, error);
414
+ }
415
+ }
416
+ /** Transform, prove, and publish the sibling current-format artifact for one refused Session. */
417
+ async repairArtifact(sessionId, path, member, refusal) {
418
+ let rows;
419
+ try {
420
+ rows = readPhysicalRows(await readFile(path));
421
+ }
422
+ catch (readError) {
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)}`);
424
+ return { status: 'repair-failed', parentSession: undefined, reason: 'artifact undecodable' };
425
+ }
426
+ const parentSession = physicalParentSession(rows);
427
+ if (!rows.some(row => containsLegacySource(row))) {
428
+ this.ctx.logger.warn(`agent-team: Session '${sessionId}' of member '${member.handle}' is refused for a reason outside this plugin's writes: ${refusal.message.split('\n')[0]}; leaving it untouched`);
429
+ return { status: 'left-untouched', parentSession, reason: 'refusal outside this plugin writes' };
430
+ }
431
+ const admitted = rows.map(admitLegacyRow);
432
+ let artifact;
433
+ try {
434
+ artifact = migrateForProof(admitted.map(row => structuredClone(row)));
435
+ }
436
+ catch (proofError) {
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`);
438
+ return { status: 'repair-failed', parentSession, reason: 'residual refusal after rewrite' };
439
+ }
440
+ try {
441
+ const published = await this.publishSibling(path, encodeCurrentArtifact(artifact));
442
+ if (published === 'already-present') {
443
+ this.ctx.logger.warn(`agent-team: Session '${sessionId}' already has a current-format sibling; leaving both untouched`);
444
+ return { status: 'left-untouched', parentSession, reason: 'sibling already present' };
445
+ }
446
+ }
447
+ catch (publishError) {
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)}`);
449
+ return { status: 'repair-failed', parentSession, reason: 'publish failed' };
450
+ }
451
+ // Success is proven by the storage layer itself, not by our own writer.
452
+ try {
453
+ const handle = await this.persistence.open(sessionId, 'read');
454
+ try {
455
+ await handle.read();
456
+ }
457
+ finally {
458
+ await handle.close();
459
+ }
460
+ }
461
+ catch (verifyError) {
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)}`);
463
+ return { status: 'repair-failed', parentSession, reason: 'sibling refused after publish' };
464
+ }
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`);
466
+ return { status: 'repaired', parentSession };
467
+ }
468
+ /**
469
+ * Atomically place the sibling file: stage in the same directory, hardlink
470
+ * into place (an existing target refuses, never overwrites), remove the
471
+ * stage. The link dance matches the storage layer's own exclusive publish.
472
+ */
473
+ async publishSibling(sourcePath, bytes) {
474
+ const directory = dirname(sourcePath);
475
+ const target = join(directory, CURRENT_GENERATION_FILENAME);
476
+ if (await stat(target).then(() => true, () => false))
477
+ return 'already-present';
478
+ const staged = join(directory, `.remediation-${randomUUID()}.tmp`);
479
+ await writeFile(staged, bytes, { flag: 'wx' });
480
+ try {
481
+ await link(staged, target);
482
+ }
483
+ catch (error) {
484
+ if (await stat(target).then(() => true, () => false))
485
+ return 'already-present';
486
+ throw error;
487
+ }
488
+ finally {
489
+ await rm(staged, { force: true });
490
+ }
491
+ return 'published';
492
+ }
493
+ }
@@ -193,8 +193,9 @@ const claimsReleasedActivitySchema = z.object({
193
193
  const activitySchema = z.discriminatedUnion('kind', [claimActivitySchema, taskActivitySchema, claimsReleasedActivitySchema]);
194
194
  const threadFactSchema = z.discriminatedUnion('kind', [
195
195
  z.object({ kind: z.literal('message'), sequence: z.number().int().positive(), message: messageSchema,
196
- mentions: z.array(memberIdSchema) }).strict(),
197
- z.object({ kind: z.literal('activity'), sequence: z.number().int().positive(), activity: activitySchema }).strict(),
196
+ mentions: z.array(memberIdSchema), occurredAt: z.string().datetime().optional() }).strict(),
197
+ z.object({ kind: z.literal('activity'), sequence: z.number().int().positive(), activity: activitySchema,
198
+ occurredAt: z.string().datetime().optional() }).strict(),
198
199
  ]);
199
200
  const readFactSchema = z.object({
200
201
  fact: threadFactSchema,
@@ -215,6 +216,47 @@ const claimOperation = (kind) => z.object({
215
216
  inbox: inboxDeltaSchema,
216
217
  }).strict(),
217
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);
218
260
  /** Closed Agent Team operation union before occurrence stamping. */
219
261
  const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
220
262
  z.object({
@@ -257,11 +299,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
257
299
  kind: z.literal('team/member-archived'),
258
300
  data: z.object({
259
301
  member: memberSchema,
260
- claims: z.array(claimSchema),
261
- activities: z.array(claimsReleasedActivitySchema),
262
- tasks: z.array(taskSchema),
263
- threads: z.array(threadSchema),
264
- inbox: inboxDeltaSchema,
302
+ ...releaseSnapshotFields,
265
303
  }).strict(),
266
304
  }).strict(),
267
305
  z.object({
@@ -323,11 +361,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
323
361
  workspaceId: workspaceIdSchema,
324
362
  channelRef: channelRefSchema,
325
363
  memberId: memberIdSchema,
326
- claims: z.array(claimSchema),
327
- activities: z.array(claimsReleasedActivitySchema),
328
- tasks: z.array(taskSchema),
329
- threads: z.array(threadSchema),
330
- inbox: inboxDeltaSchema,
364
+ ...releaseSnapshotFields,
331
365
  }).strict(),
332
366
  }).strict(),
333
367
  z.object({
@@ -337,11 +371,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
337
371
  data: z.object({
338
372
  workspaceId: workspaceIdSchema,
339
373
  channel: channelSchema,
340
- claims: z.array(claimSchema),
341
- activities: z.array(claimsReleasedActivitySchema),
342
- tasks: z.array(taskSchema),
343
- threads: z.array(threadSchema),
344
- inbox: inboxDeltaSchema,
374
+ ...releaseSnapshotFields,
345
375
  }).strict(),
346
376
  }).strict(),
347
377
  z.object({
@@ -418,20 +448,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
418
448
  ...operationBase,
419
449
  previousOperationId: operationIdSchema.nullable(),
420
450
  kind: z.literal('team/thread-read'),
421
- data: z.object({
422
- workspaceId: workspaceIdSchema,
423
- memberId: memberIdSchema,
424
- task: taskSchema.optional(),
425
- thread: threadSchema,
426
- claims: z.array(claimSchema),
427
- anchor: messageSchema,
428
- anchorMentions: z.array(memberIdSchema),
429
- facts: z.array(readFactSchema),
430
- readThroughSequence: z.number().int().nonnegative(),
431
- remainingUnreadCount: z.number().int().nonnegative(),
432
- attention: attentionSchema.optional(),
433
- inbox: inboxDeltaSchema,
434
- }).strict().transform(omitUndefined),
451
+ data: z.union([threadReadReceiptDataSchema, threadReadSnapshotDataSchema]),
435
452
  }).strict(),
436
453
  z.object({
437
454
  ...operationBase,
@@ -439,11 +456,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
439
456
  kind: z.literal('team/member-removed'),
440
457
  data: z.object({
441
458
  member: memberSchema,
442
- claims: z.array(claimSchema),
443
- activities: z.array(claimsReleasedActivitySchema),
444
- tasks: z.array(taskSchema),
445
- threads: z.array(threadSchema),
446
- inbox: inboxDeltaSchema,
459
+ ...releaseSnapshotFields,
447
460
  }).strict(),
448
461
  }).strict(),
449
462
  z.object({