@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.
- package/README.md +17 -11
- package/README.zh.md +17 -11
- package/package.json +61 -47
- package/packages/agent-team/README.md +6 -4
- package/packages/agent-team/README.zh.md +6 -3
- package/packages/agent-team/lib/context-management.js +48 -55
- package/packages/agent-team/lib/context-projection.js +22 -8
- package/packages/agent-team/lib/context-source.js +163 -23
- package/packages/agent-team/lib/index.js +349 -176
- package/packages/agent-team/lib/invariant.js +38 -4
- package/packages/agent-team/lib/ledger.js +658 -294
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-time-context.js +141 -0
- package/packages/agent-team/lib/pressure-policy.js +34 -11
- package/packages/agent-team/lib/progress-nudge.js +12 -4
- package/packages/agent-team/lib/session-event-cursor.js +116 -0
- package/packages/agent-team/lib/session-remediation.js +493 -0
- package/packages/agent-team/lib/spec.js +49 -36
- package/packages/agent-team/lib/stored-session-reader.js +138 -0
- package/packages/agent-team/lib/time-format.js +56 -0
- package/packages/agent-team/lib/typert.host.js +163 -63
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +105 -32
- package/packages/agent-team/lib/types/context-management.d.ts +13 -1
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-source.d.ts +70 -36
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +110 -9
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +205 -7
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
- package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
- package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts +172 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
- package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
- package/packages/agent-team/lib/types/time-format.d.ts +28 -0
- package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
- package/packages/agent-team/lib/types/types/entities.d.ts +36 -2
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +50 -3
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +21 -4
- package/packages/client-agent-team/README.md +1 -1
- package/packages/client-agent-team/README.zh.md +1 -1
- package/packages/client-agent-team/lib/client.js +1121 -402
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -4
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +42 -22
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +13 -24
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +15 -9
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +8 -1
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +21 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +71 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +13 -6
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +106 -32
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +50 -7
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +25 -9
- package/packages/client-agent-team/lib/types/client/locales.d.ts +18 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +18 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts +26 -1
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.js +66 -15
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +34 -6
- package/packages/tool-agent-team/README.md +8 -6
- package/packages/tool-agent-team/README.zh.md +8 -6
- package/packages/tool-agent-team/lib/context-tools.js +18 -14
- package/packages/tool-agent-team/lib/host-access.js +20 -0
- package/packages/tool-agent-team/lib/index.js +314 -123
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
- package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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({
|