@wowyuarm/dsh-agent-team 0.1.0 → 0.1.1
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 +14 -2
- package/README.zh.md +10 -2
- package/package.json +2 -2
- package/packages/agent-team/lib/attachments.js +157 -0
- package/packages/agent-team/lib/index.js +274 -5
- package/packages/agent-team/lib/ledger.js +65 -13
- package/packages/agent-team/lib/recovery.js +87 -0
- package/packages/agent-team/lib/spec.js +17 -1
- package/packages/agent-team/lib/typert.host.js +321 -21
- package/packages/agent-team/lib/typert.remote-client.d.ts +9 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +249 -17
- package/packages/agent-team/lib/types/attachments.d.ts +69 -0
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +66 -1
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +17 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +42 -0
- package/packages/agent-team/lib/types/recovery.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +85 -2
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/client-agent-team/README.md +2 -2
- package/packages/client-agent-team/README.zh.md +2 -2
- package/packages/client-agent-team/lib/client.js +1176 -281
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +6 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +104 -58
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +5 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +44 -19
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +23 -18
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +40 -4
- 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 +3 -3
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +7 -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 +34 -8
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +3 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +18 -11
- 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 +6 -5
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +50 -10
- package/packages/client-agent-team/lib/types/client/locales.d.ts +28 -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 +28 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
- package/packages/client-agent-team/lib/types/client/slots.d.ts +18 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +2 -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 +4 -0
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
- package/packages/tool-agent-team/lib/index.js +6 -2
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -76,8 +76,6 @@ export class AgentTeamLedger {
|
|
|
76
76
|
const description = request.description.trim();
|
|
77
77
|
if (name === '')
|
|
78
78
|
throw new Error('channel name must not be empty');
|
|
79
|
-
if (description === '')
|
|
80
|
-
throw new Error('channel description must not be empty');
|
|
81
79
|
const memberIds = this.normalizeUnique(request.memberIds, 'initial Channel members');
|
|
82
80
|
for (const memberId of memberIds)
|
|
83
81
|
this.assertJoinableMember(request.workspaceId, memberId);
|
|
@@ -107,8 +105,6 @@ export class AgentTeamLedger {
|
|
|
107
105
|
const description = request.description.trim();
|
|
108
106
|
if (name === '')
|
|
109
107
|
throw new Error('channel name must not be empty');
|
|
110
|
-
if (description === '')
|
|
111
|
-
throw new Error('channel description must not be empty');
|
|
112
108
|
const channel = Object.freeze({ ...this.requireChannel(request.workspaceId, request.channelRef), name, description });
|
|
113
109
|
const operation = Object.freeze({
|
|
114
110
|
...this.operationBase(request, this.nextSequence()), kind: 'team/channel-updated',
|
|
@@ -132,13 +128,11 @@ export class AgentTeamLedger {
|
|
|
132
128
|
const presetId = request.presetId.trim();
|
|
133
129
|
if (handle === '')
|
|
134
130
|
throw new Error('member handle must not be empty');
|
|
135
|
-
if (description === '')
|
|
136
|
-
throw new Error('member description must not be empty');
|
|
137
131
|
if (presetId === '')
|
|
138
132
|
throw new Error('member preset must not be empty');
|
|
133
|
+
// Description and initial Channels are optional: a Member with neither is
|
|
134
|
+
// still drivable through its DM view, and joins Channels later.
|
|
139
135
|
const channelRefs = this.normalizeUnique(request.channelRefs, 'initial Member Channels');
|
|
140
|
-
if (channelRefs.length === 0)
|
|
141
|
-
throw new Error('Agent Member requires at least one initial Channel');
|
|
142
136
|
for (const channelRef of channelRefs)
|
|
143
137
|
this.requireChannel(request.workspaceId, channelRef);
|
|
144
138
|
this.assertHandleAvailable(request.workspaceId, handle);
|
|
@@ -172,8 +166,6 @@ export class AgentTeamLedger {
|
|
|
172
166
|
const description = request.description.trim();
|
|
173
167
|
if (handle === '')
|
|
174
168
|
throw new Error('member handle must not be empty');
|
|
175
|
-
if (description === '')
|
|
176
|
-
throw new Error('member description must not be empty');
|
|
177
169
|
if (handle !== prior.handle)
|
|
178
170
|
this.assertHandleAvailable(prior.workspaceId, handle, prior.memberId);
|
|
179
171
|
this.assertModelSelection(request.model);
|
|
@@ -199,6 +191,31 @@ export class AgentTeamLedger {
|
|
|
199
191
|
resumeMember(request) {
|
|
200
192
|
return this.setMemberState(request, 'enabled');
|
|
201
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Audit record of one in-place Member session restart. The operation changes
|
|
196
|
+
* no projection state — identity, transcript, and memory are untouched — so
|
|
197
|
+
* replay treats it as a marker while the Host re-mints the live handle.
|
|
198
|
+
*/
|
|
199
|
+
restartMember(request) {
|
|
200
|
+
return this.enqueue(async () => {
|
|
201
|
+
const existing = this.state.byRequest.get(request.requestId);
|
|
202
|
+
if (existing !== undefined) {
|
|
203
|
+
this.assertSameMemberRestart(existing, request);
|
|
204
|
+
return this.resolved(this.memberResult(existing));
|
|
205
|
+
}
|
|
206
|
+
this.assertHumanActor(request.actor);
|
|
207
|
+
const member = this.requireMember(request.memberId);
|
|
208
|
+
if (member.state !== 'enabled')
|
|
209
|
+
throw new Error(`Agent Member '${member.handle}' is ${member.state}; only enabled Members can be restarted`);
|
|
210
|
+
const operation = Object.freeze({
|
|
211
|
+
...this.operationBase(request, this.nextSequence()), kind: 'team/member-session-restarted',
|
|
212
|
+
data: Object.freeze({ member }),
|
|
213
|
+
});
|
|
214
|
+
await this.table.put(operation.operationId, operation);
|
|
215
|
+
this.apply(operation);
|
|
216
|
+
return this.committed(this.memberResult(operation));
|
|
217
|
+
});
|
|
218
|
+
}
|
|
202
219
|
getMember(memberId) {
|
|
203
220
|
return this.state.members.get(memberId);
|
|
204
221
|
}
|
|
@@ -292,7 +309,9 @@ export class AgentTeamLedger {
|
|
|
292
309
|
const thread = Object.freeze({ threadRef, taskRef, revision: sequence });
|
|
293
310
|
const message = Object.freeze({
|
|
294
311
|
messageRef: this.ref('message'), channelRef: channel.channelRef, threadRef, taskRef,
|
|
295
|
-
sender: request.actor.memberId, body,
|
|
312
|
+
sender: request.actor.memberId, body,
|
|
313
|
+
...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
|
|
314
|
+
topLevel: true, sequence, occurredAt: base.occurredAt,
|
|
296
315
|
});
|
|
297
316
|
const started = [this.startAttention(request.actor.memberId, threadRef, sequence),
|
|
298
317
|
...recipients.filter(memberId => this.state.members.has(memberId))
|
|
@@ -345,7 +364,9 @@ export class AgentTeamLedger {
|
|
|
345
364
|
const base = this.operationBase(request, sequence);
|
|
346
365
|
const message = Object.freeze({
|
|
347
366
|
messageRef: this.ref('message'), channelRef: task.channelRef, threadRef: task.threadRef,
|
|
348
|
-
taskRef: task.taskRef, sender: request.actor.memberId, body,
|
|
367
|
+
taskRef: task.taskRef, sender: request.actor.memberId, body,
|
|
368
|
+
...(request.resolvedAttachments === undefined ? {} : { attachments: request.resolvedAttachments }),
|
|
369
|
+
topLevel: false, sequence, occurredAt: base.occurredAt,
|
|
349
370
|
});
|
|
350
371
|
const nextThread = Object.freeze({ ...thread, revision: sequence });
|
|
351
372
|
const started = unfollowedAgents.map(memberId => this.startAttention(memberId, thread.threadRef, sequence));
|
|
@@ -684,6 +705,17 @@ export class AgentTeamLedger {
|
|
|
684
705
|
return undefined;
|
|
685
706
|
}
|
|
686
707
|
}
|
|
708
|
+
/** Attachment ids referenced by any stored Message — the GC's keep-set oracle. */
|
|
709
|
+
referencedAttachmentIds() {
|
|
710
|
+
const referenced = new Set();
|
|
711
|
+
for (const fact of this.state.orderedFacts) {
|
|
712
|
+
if (fact.kind !== 'message')
|
|
713
|
+
continue;
|
|
714
|
+
for (const attachment of fact.message.attachments ?? [])
|
|
715
|
+
referenced.add(attachment.attachmentId);
|
|
716
|
+
}
|
|
717
|
+
return referenced;
|
|
718
|
+
}
|
|
687
719
|
threadHistory(actor, request) {
|
|
688
720
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
689
721
|
const task = this.requireTask(request.workspaceId, request.taskRef);
|
|
@@ -814,6 +846,7 @@ export class AgentTeamLedger {
|
|
|
814
846
|
case 'team/member-added':
|
|
815
847
|
case 'team/member-suspended':
|
|
816
848
|
case 'team/member-resumed':
|
|
849
|
+
case 'team/member-session-restarted':
|
|
817
850
|
case 'team/member-updated':
|
|
818
851
|
case 'team/member-removed':
|
|
819
852
|
return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
|
|
@@ -987,6 +1020,14 @@ export class AgentTeamLedger {
|
|
|
987
1020
|
throw new Error('invalid Member lifecycle transition');
|
|
988
1021
|
return;
|
|
989
1022
|
}
|
|
1023
|
+
if (operation.kind === 'team/member-session-restarted') {
|
|
1024
|
+
assertHuman();
|
|
1025
|
+
const prior = projection.members.get(operation.data.member.memberId);
|
|
1026
|
+
// The restart records the unchanged Member: same identity, still enabled.
|
|
1027
|
+
if (prior === undefined || prior.state !== 'enabled' || !this.sameMemberIdentity(prior, operation.data.member))
|
|
1028
|
+
throw new Error('invalid Member restart');
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
990
1031
|
if (operation.kind === 'team/channel-updated') {
|
|
991
1032
|
assertHuman();
|
|
992
1033
|
const prior = projection.channels.get(operation.data.channel.channelRef);
|
|
@@ -1367,6 +1408,11 @@ export class AgentTeamLedger {
|
|
|
1367
1408
|
target.members.set(operation.data.member.memberId, operation.data.member);
|
|
1368
1409
|
return;
|
|
1369
1410
|
}
|
|
1411
|
+
if (operation.kind === 'team/member-session-restarted') {
|
|
1412
|
+
// Audit-only: identity, transcript, and memory are untouched, so the
|
|
1413
|
+
// projection deliberately does not change.
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1370
1416
|
if (operation.kind === 'team/channel-updated') {
|
|
1371
1417
|
target.channels.set(operation.data.channel.channelRef, operation.data.channel);
|
|
1372
1418
|
return;
|
|
@@ -1941,6 +1987,11 @@ export class AgentTeamLedger {
|
|
|
1941
1987
|
|| !this.sameList(operation.data.channelRefs, this.normalizeUnique(request.channelRefs, 'initial Member Channels')))
|
|
1942
1988
|
this.throwRequestCollision(request.requestId);
|
|
1943
1989
|
}
|
|
1990
|
+
assertSameMemberRestart(operation, request) {
|
|
1991
|
+
if (operation.kind !== 'team/member-session-restarted' || !this.sameActor(operation.actor, request.actor)
|
|
1992
|
+
|| operation.data.member.memberId !== request.memberId)
|
|
1993
|
+
this.throwRequestCollision(request.requestId);
|
|
1994
|
+
}
|
|
1944
1995
|
assertSameMemberState(operation, request, state) {
|
|
1945
1996
|
const kind = state === 'suspended' ? 'team/member-suspended' : 'team/member-resumed';
|
|
1946
1997
|
if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.member.memberId !== request.memberId)
|
|
@@ -1972,7 +2023,8 @@ export class AgentTeamLedger {
|
|
|
1972
2023
|
assertSameMessage(operation, request, recipients) {
|
|
1973
2024
|
if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
|
|
1974
2025
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
|
|
1975
|
-
|| operation.data.message.body !== request.body.trim() || !this.sameList(operation.data.mentions, recipients)
|
|
2026
|
+
|| operation.data.message.body !== request.body.trim() || !this.sameList(operation.data.mentions, recipients)
|
|
2027
|
+
|| !this.sameList(operation.data.message.attachments?.map(attachment => attachment.attachmentId) ?? [], request.attachments ?? []))
|
|
1976
2028
|
this.throwRequestCollision(request.requestId);
|
|
1977
2029
|
}
|
|
1978
2030
|
assertSameReply(operation, request, recipients) {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const TRANSIENT_NETWORK_PATTERNS = [/fetch failed/i, /econnreset/i, /etimedout/i, /socket hang up/i];
|
|
2
|
+
const RATE_LIMIT_PATTERNS = [/\b429\b/, /rate limit/i, /\b503\b/, /overloaded/i];
|
|
3
|
+
export function classifyRecoverableError(message) {
|
|
4
|
+
if (RATE_LIMIT_PATTERNS.some(pattern => pattern.test(message)))
|
|
5
|
+
return 'rate limiting';
|
|
6
|
+
if (TRANSIENT_NETWORK_PATTERNS.some(pattern => pattern.test(message)))
|
|
7
|
+
return 'transient network';
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
export const RECOVERY_DELAY_MS = 120_000;
|
|
11
|
+
export const RECOVERY_MAX_ATTEMPTS = 3;
|
|
12
|
+
/**
|
|
13
|
+
* Per-member automatic recovery episodes. One episode covers a run of
|
|
14
|
+
* identical errors; a changed error string starts a fresh one, and any clean
|
|
15
|
+
* turn end clears the slate. After `maxAttempts` injections of the same error
|
|
16
|
+
* the coordinator stands down and leaves the Member in error for the operator.
|
|
17
|
+
*/
|
|
18
|
+
export class RecoveryCoordinator {
|
|
19
|
+
episodes = new Map();
|
|
20
|
+
inject;
|
|
21
|
+
onStandDown;
|
|
22
|
+
delayMs;
|
|
23
|
+
maxAttempts;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.inject = options.inject;
|
|
26
|
+
this.onStandDown = options.onStandDown;
|
|
27
|
+
this.delayMs = options.delayMs ?? RECOVERY_DELAY_MS;
|
|
28
|
+
this.maxAttempts = options.maxAttempts ?? RECOVERY_MAX_ATTEMPTS;
|
|
29
|
+
}
|
|
30
|
+
/** Observe one error occurrence for a Member. */
|
|
31
|
+
onError(memberId, errorMessage) {
|
|
32
|
+
const kind = classifyRecoverableError(errorMessage);
|
|
33
|
+
if (kind === undefined) {
|
|
34
|
+
// A non-retryable failure cancels anything pending: retrying cannot help.
|
|
35
|
+
this.stopTracking(memberId);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let episode = this.episodes.get(memberId);
|
|
39
|
+
if (episode === undefined || episode.lastError !== errorMessage) {
|
|
40
|
+
this.cancelTimer(episode);
|
|
41
|
+
episode = { kind, lastError: errorMessage, attempts: 0 };
|
|
42
|
+
this.episodes.set(memberId, episode);
|
|
43
|
+
}
|
|
44
|
+
else if (episode.attempts >= this.maxAttempts) {
|
|
45
|
+
// The same error survived every allowed injection — stand down once.
|
|
46
|
+
if (episode.stoodDown !== true) {
|
|
47
|
+
episode.stoodDown = true;
|
|
48
|
+
this.onStandDown?.(memberId, episode.attempts);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (episode.timer !== undefined)
|
|
53
|
+
return;
|
|
54
|
+
episode.timer = setTimeout(() => {
|
|
55
|
+
episode.timer = undefined;
|
|
56
|
+
episode.attempts += 1;
|
|
57
|
+
try {
|
|
58
|
+
this.inject(memberId, episode.attempts, kind);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
this.stopTracking(memberId);
|
|
62
|
+
}
|
|
63
|
+
}, this.delayMs);
|
|
64
|
+
}
|
|
65
|
+
/** A turn ended cleanly (running→idle without an error): the episode is over. */
|
|
66
|
+
onCleanTurnEnd(memberId) {
|
|
67
|
+
this.stopTracking(memberId);
|
|
68
|
+
}
|
|
69
|
+
stopTracking(memberId) {
|
|
70
|
+
const episode = this.episodes.get(memberId);
|
|
71
|
+
if (episode === undefined)
|
|
72
|
+
return;
|
|
73
|
+
this.cancelTimer(episode);
|
|
74
|
+
this.episodes.delete(memberId);
|
|
75
|
+
}
|
|
76
|
+
dispose() {
|
|
77
|
+
for (const episode of this.episodes.values())
|
|
78
|
+
this.cancelTimer(episode);
|
|
79
|
+
this.episodes.clear();
|
|
80
|
+
}
|
|
81
|
+
cancelTimer(episode) {
|
|
82
|
+
if (episode?.timer === undefined)
|
|
83
|
+
return;
|
|
84
|
+
clearTimeout(episode.timer);
|
|
85
|
+
episode.timer = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -11,6 +11,7 @@ const taskRefSchema = z.string().regex(/^task:[^:]+$/).transform(value => value)
|
|
|
11
11
|
const threadRefSchema = z.string().regex(/^thread:[^:]+$/).transform(value => value);
|
|
12
12
|
const claimRefSchema = z.string().regex(/^claim:[^:]+$/).transform(value => value);
|
|
13
13
|
const activityRefSchema = z.string().regex(/^activity:[^:]+$/).transform(value => value);
|
|
14
|
+
const attachmentIdSchema = z.string().min(1).transform(value => value);
|
|
14
15
|
const actorSchema = z.discriminatedUnion('kind', [
|
|
15
16
|
z.object({ kind: z.literal('human'), memberId: memberIdSchema, handle: z.string().min(1) }).strict(),
|
|
16
17
|
z.object({ kind: z.literal('member'), memberId: memberIdSchema, handle: z.string().min(1) }).strict(),
|
|
@@ -45,7 +46,15 @@ const channelSchema = z.object({
|
|
|
45
46
|
description: z.string().min(1),
|
|
46
47
|
createdAtSequence: z.number().int().positive(),
|
|
47
48
|
}).strict();
|
|
48
|
-
// Ledgers written before message occurredAt existed store bare
|
|
49
|
+
// Ledgers written before message occurredAt or attachments existed store bare
|
|
50
|
+
// messages; the union schema below stamps occurredAt on load and leaves absent
|
|
51
|
+
// attachment metadata undefined, so old ledgers replay unchanged.
|
|
52
|
+
const attachmentSchema = z.object({
|
|
53
|
+
attachmentId: attachmentIdSchema,
|
|
54
|
+
name: z.string().min(1),
|
|
55
|
+
byteSize: z.number().int().nonnegative(),
|
|
56
|
+
mediaType: z.string().min(1),
|
|
57
|
+
}).strict();
|
|
49
58
|
const messageSchema = z.object({
|
|
50
59
|
messageRef: messageRefSchema,
|
|
51
60
|
channelRef: channelRefSchema,
|
|
@@ -53,6 +62,7 @@ const messageSchema = z.object({
|
|
|
53
62
|
taskRef: taskRefSchema,
|
|
54
63
|
sender: memberIdSchema,
|
|
55
64
|
body: z.string().min(1),
|
|
65
|
+
attachments: z.array(attachmentSchema).optional(),
|
|
56
66
|
topLevel: z.boolean(),
|
|
57
67
|
sequence: z.number().int().positive(),
|
|
58
68
|
occurredAt: z.string().datetime().optional(),
|
|
@@ -212,6 +222,12 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
|
212
222
|
kind: z.literal('team/member-resumed'),
|
|
213
223
|
data: z.object({ member: memberSchema }).strict(),
|
|
214
224
|
}).strict(),
|
|
225
|
+
z.object({
|
|
226
|
+
...operationBase,
|
|
227
|
+
previousOperationId: operationIdSchema.nullable(),
|
|
228
|
+
kind: z.literal('team/member-session-restarted'),
|
|
229
|
+
data: z.object({ member: memberSchema }).strict(),
|
|
230
|
+
}).strict(),
|
|
215
231
|
z.object({
|
|
216
232
|
...operationBase,
|
|
217
233
|
previousOperationId: operationIdSchema.nullable(),
|