@wowyuarm/dsh-agent-team 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/README.zh.md +13 -0
- package/package.json +52 -47
- package/packages/agent-team/README.md +4 -2
- package/packages/agent-team/README.zh.md +4 -2
- package/packages/agent-team/lib/context-management.js +40 -50
- package/packages/agent-team/lib/context-projection.js +10 -0
- package/packages/agent-team/lib/index.js +235 -112
- package/packages/agent-team/lib/invariant.js +38 -4
- package/packages/agent-team/lib/ledger.js +554 -271
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-time-context.js +20 -5
- package/packages/agent-team/lib/pressure-policy.js +33 -9
- package/packages/agent-team/lib/session-event-cursor.js +116 -0
- package/packages/agent-team/lib/session-remediation.js +38 -26
- package/packages/agent-team/lib/spec.js +46 -34
- package/packages/agent-team/lib/stored-session-reader.js +138 -0
- package/packages/agent-team/lib/typert.host.js +107 -40
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +84 -32
- package/packages/agent-team/lib/types/context-management.d.ts +7 -0
- 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/index.d.ts +78 -2
- 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 +180 -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.map +1 -1
- 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/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 +23 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -1
- 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/types/entities.d.ts +28 -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 +44 -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 +3 -1
- 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 +1068 -375
- 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 +11 -4
- 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 +102 -28
- 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 +17 -6
- 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 +7 -1
- 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 +1 -1
- package/packages/tool-agent-team/README.zh.md +1 -1
- 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 +16 -17
- 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
|
@@ -47,19 +47,20 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
|
47
47
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
48
48
|
import { scopeOf } from '@deepseek-ai/dsh-scope';
|
|
49
49
|
import { Session, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
50
|
-
import { SessionFormatUnsupportedError } from '@deepseek-ai/dsh-session-persistence';
|
|
51
50
|
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
|
|
52
51
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
53
52
|
import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
|
|
54
53
|
import { PressurePolicyCoordinator } from "./pressure-policy.js";
|
|
55
54
|
import { ContextManagementCoordinator } from "./context-management.js";
|
|
56
55
|
import { AGENT_TEAM_PLUGIN_ID, createHandoffMessage } from "./context-source.js";
|
|
57
|
-
import { carriedInputOf, checkpointByRef, checkpointRefFor, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
|
|
56
|
+
import { carriedInputOf, checkpointByRef, checkpointRefFor, contextProjectionFold, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
|
|
57
|
+
import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
|
|
58
58
|
import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
|
|
59
59
|
import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, memberMemoryDirectoryName, MemberRuntime } from "./member-runtime.js";
|
|
60
60
|
import { ProgressNudgeCoordinator } from "./progress-nudge.js";
|
|
61
61
|
import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
|
|
62
62
|
import { SessionRemediation, handoffAlreadyInLog } from "./session-remediation.js";
|
|
63
|
+
import { StoredSessionReadError, StoredSessionReader, sessionFailureOf } from "./stored-session-reader.js";
|
|
63
64
|
import { agentTeamDomainSpec } from "./spec.js";
|
|
64
65
|
import { formatTeamTimestamp } from "./time-format.js";
|
|
65
66
|
export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
|
|
@@ -70,6 +71,17 @@ export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.pre
|
|
|
70
71
|
const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
|
|
71
72
|
const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
|
|
72
73
|
const ORPHANED_MEMBER_DIAGNOSTIC = 'Member preset composition was lost after a reload; its tools are unavailable. Resume rebuilds the member in place.';
|
|
74
|
+
/**
|
|
75
|
+
* A preset mount/validation failure during activation, carrying its own class
|
|
76
|
+
* so the activation diagnostic can route preset-composition failures (the
|
|
77
|
+
* install/runtime split failure mode) without matching message text.
|
|
78
|
+
*/
|
|
79
|
+
class PresetCompositionError extends Error {
|
|
80
|
+
constructor(message, options) {
|
|
81
|
+
super(message, options);
|
|
82
|
+
this.name = 'PresetCompositionError';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
73
85
|
/** Longest accepted model-supplied checkpoint display name. */
|
|
74
86
|
const MAX_CHECKPOINT_NAME_CHARS = 120;
|
|
75
87
|
/** Default and maximum number of timeline items one query returns. */
|
|
@@ -95,6 +107,8 @@ function sameChangeScope(left, right) {
|
|
|
95
107
|
return left.channelRef === right.channelRef;
|
|
96
108
|
if (left.kind === 'thread' && right.kind === 'thread')
|
|
97
109
|
return left.threadRef === right.threadRef;
|
|
110
|
+
if (left.kind === 'presence' && right.kind === 'presence')
|
|
111
|
+
return left.workspaceId === right.workspaceId;
|
|
98
112
|
return false;
|
|
99
113
|
}
|
|
100
114
|
/** Mark the preset's `team_message` definition as an Agent Team consumer. */
|
|
@@ -261,6 +275,13 @@ let AgentTeam = (() => {
|
|
|
261
275
|
},
|
|
262
276
|
runningAgents: this.runningAgents,
|
|
263
277
|
});
|
|
278
|
+
/**
|
|
279
|
+
* The single seam for every per-Session stored read: handle lifecycle and
|
|
280
|
+
* failure normalization live here, so a DSH persistence-interface change is
|
|
281
|
+
* adapted once, and consumers choose policy by failure category instead of
|
|
282
|
+
* matching error text.
|
|
283
|
+
*/
|
|
284
|
+
sessionReader = new StoredSessionReader(this.ctx);
|
|
264
285
|
/**
|
|
265
286
|
* Why one Member shows error presence, per failure source. Reads prefer
|
|
266
287
|
* activation, then runtime, then compaction; slots clear independently, so
|
|
@@ -297,6 +318,14 @@ let AgentTeam = (() => {
|
|
|
297
318
|
},
|
|
298
319
|
log: message => { this.ctx.logger.warn(message); },
|
|
299
320
|
});
|
|
321
|
+
/**
|
|
322
|
+
* Incremental fold state for {@link contextManagement}'s projection lookup:
|
|
323
|
+
* one entry per Member, replaced when that Member's Session changes. Keying
|
|
324
|
+
* by Member rather than by Session id is what bounds the map — a rollover
|
|
325
|
+
* that leaves the previous generation's projection behind retains it for the
|
|
326
|
+
* life of the process, and only the current Session can ever read it.
|
|
327
|
+
*/
|
|
328
|
+
contextCursors = new Map();
|
|
300
329
|
/**
|
|
301
330
|
* Context self-management: the one deep module that turns a Member's
|
|
302
331
|
* successful `context_rollover` tool result into its next private context
|
|
@@ -311,15 +340,34 @@ let AgentTeam = (() => {
|
|
|
311
340
|
const handle = this.handles.get(memberId);
|
|
312
341
|
if (handle === undefined || handle.agent.session.id !== sessionId)
|
|
313
342
|
return undefined;
|
|
314
|
-
|
|
343
|
+
const session = handle.agent.session;
|
|
344
|
+
// The projection is read on every successful tool result and turn end, so
|
|
345
|
+
// it folds incrementally. The entry is keyed by Member and carries the
|
|
346
|
+
// Session it was built from: this Session's log keeps growing, a rollover
|
|
347
|
+
// replaces the entry instead of resuming from its predecessor's cursor,
|
|
348
|
+
// and a rewritten log falls back to a cold fold inside the cursor.
|
|
349
|
+
const owned = advanceOwnedSessionEventCursor(this.contextCursors.get(memberId), session.id, contextProjectionFold(session.id), session.ownEvents(), session.inheritedEventCount);
|
|
350
|
+
this.contextCursors.set(memberId, owned);
|
|
351
|
+
return owned.cursor.value;
|
|
315
352
|
},
|
|
316
353
|
executeTransition: (memberId, plan) => this.executeMemberTransition(memberId, plan),
|
|
317
354
|
log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
|
|
318
355
|
});
|
|
319
356
|
lifecycleTail = Promise.resolve();
|
|
320
357
|
accepting = true;
|
|
321
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Presence wake epoch: Agent running/idle/failure is runtime state with no
|
|
360
|
+
* durable fact behind it, so the presence scope counts those edge wakes in
|
|
361
|
+
* process. It never takes part in a projection waiter's comparison.
|
|
362
|
+
*/
|
|
363
|
+
presenceEpoch = 0;
|
|
322
364
|
changeWaiters = new Set();
|
|
365
|
+
/**
|
|
366
|
+
* The startup-opened remediation instance, held for the restart heal: the
|
|
367
|
+
* completion-cache domain may only be opened once per plugin lifecycle, so
|
|
368
|
+
* the restart path reuses this instance instead of opening its own.
|
|
369
|
+
*/
|
|
370
|
+
remediation;
|
|
323
371
|
constructor(ctx) {
|
|
324
372
|
super(ctx, 'agentTeam');
|
|
325
373
|
this.pressurePolicy = new PressurePolicyCoordinator({
|
|
@@ -351,7 +399,7 @@ let AgentTeam = (() => {
|
|
|
351
399
|
if (kind !== undefined)
|
|
352
400
|
this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable ${kind} error; recording a consecutive error occurrence`);
|
|
353
401
|
this.recovery.onError(member.memberId, message);
|
|
354
|
-
this.
|
|
402
|
+
this.emitPresenceChanged(member.workspaceId);
|
|
355
403
|
});
|
|
356
404
|
this.ctx.on('agent/status', ({ agent, status }) => {
|
|
357
405
|
const member = this.memberForAgent(agent);
|
|
@@ -363,7 +411,7 @@ let AgentTeam = (() => {
|
|
|
363
411
|
const recovered = this.clearMemberFailure(member.memberId, 'runtime');
|
|
364
412
|
if (recovered)
|
|
365
413
|
this.notifiedInbox.delete(member.memberId);
|
|
366
|
-
this.
|
|
414
|
+
this.emitPresenceChanged(member.workspaceId);
|
|
367
415
|
// A rollover/recovery in flight delivers its own sequenced
|
|
368
416
|
// rederived Inbox after the handoff and carried input; a status-driven
|
|
369
417
|
// steer here would claim the handoff turn's next step and leapfrog
|
|
@@ -373,14 +421,14 @@ let AgentTeam = (() => {
|
|
|
373
421
|
}
|
|
374
422
|
// A turn that ends without an error closes any automatic recovery episode.
|
|
375
423
|
// The idle transition is itself presence-affecting (working → available),
|
|
376
|
-
// so it must wake
|
|
424
|
+
// so it must wake presence watchers exactly like the running transition
|
|
377
425
|
// above; without this wake, cached Client member rows keep showing the
|
|
378
426
|
// Member as working after every turn until an unrelated change arrives.
|
|
379
427
|
if (status === 'idle' && member !== undefined) {
|
|
380
428
|
if (this.memberFailures.get(member.memberId)?.runtime === undefined) {
|
|
381
429
|
this.recovery.onCleanTurnEnd(member.memberId);
|
|
382
430
|
}
|
|
383
|
-
this.
|
|
431
|
+
this.emitPresenceChanged(member.workspaceId);
|
|
384
432
|
}
|
|
385
433
|
});
|
|
386
434
|
// Progress nudges count every `tool/call` of each Member Session. The
|
|
@@ -410,6 +458,7 @@ let AgentTeam = (() => {
|
|
|
410
458
|
const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
|
|
411
459
|
this.ctx.effect(() => async () => {
|
|
412
460
|
this.accepting = false;
|
|
461
|
+
this.remediation = undefined;
|
|
413
462
|
this.recovery.dispose();
|
|
414
463
|
this.progressNudge.dispose();
|
|
415
464
|
this.contextManagement.dispose();
|
|
@@ -439,6 +488,7 @@ let AgentTeam = (() => {
|
|
|
439
488
|
// startup — the next start retries exactly what the cache does not cover.
|
|
440
489
|
try {
|
|
441
490
|
const remediation = new SessionRemediation(this.ctx, this.ctx.sessionPersistence, await SessionRemediation.open(this.ctx));
|
|
491
|
+
this.remediation = remediation;
|
|
442
492
|
await remediation.remediateEnabledMembers(ledger.listMembers());
|
|
443
493
|
}
|
|
444
494
|
catch (error) {
|
|
@@ -463,7 +513,7 @@ let AgentTeam = (() => {
|
|
|
463
513
|
* generation over it.
|
|
464
514
|
*/
|
|
465
515
|
async sessionPersisted(sessionId) {
|
|
466
|
-
return
|
|
516
|
+
return this.sessionReader.exists(sessionId);
|
|
467
517
|
}
|
|
468
518
|
/** Resolve one exact live Agent to its durable Team Member; forks do not inherit identity. */
|
|
469
519
|
memberForAgent(agent) {
|
|
@@ -501,8 +551,9 @@ let AgentTeam = (() => {
|
|
|
501
551
|
if (!Number.isInteger(request.afterVersion) || request.afterVersion < 0)
|
|
502
552
|
throw new Error('afterVersion must be a non-negative integer');
|
|
503
553
|
const scope = this.validateChangeScope(request.scope);
|
|
504
|
-
|
|
505
|
-
|
|
554
|
+
const version = this.changeVersionOf(scope);
|
|
555
|
+
if (version > request.afterVersion || !this.accepting)
|
|
556
|
+
return Object.freeze({ version });
|
|
506
557
|
return new Promise((resolve, reject) => {
|
|
507
558
|
let settled = false;
|
|
508
559
|
const waiter = {
|
|
@@ -521,7 +572,7 @@ let AgentTeam = (() => {
|
|
|
521
572
|
return;
|
|
522
573
|
settled = true;
|
|
523
574
|
this.changeWaiters.delete(waiter);
|
|
524
|
-
resolve(Object.freeze({ version: this.
|
|
575
|
+
resolve(Object.freeze({ version: this.changeVersionOf(scope) }));
|
|
525
576
|
}, 25_000);
|
|
526
577
|
const onAbort = () => {
|
|
527
578
|
if (settled)
|
|
@@ -659,6 +710,23 @@ let AgentTeam = (() => {
|
|
|
659
710
|
if (member.state !== 'enabled')
|
|
660
711
|
throw new Error(`Agent Member '${member.handle}' is ${member.state}; only enabled Members can be restarted`);
|
|
661
712
|
this.ctx.logger.info(`agent-team: restarting member '${member.handle}' after a failed activation`);
|
|
713
|
+
// A deterministic session refusal may be repairable in place: run the
|
|
714
|
+
// same bounded startup remediation for this one Member before retrying
|
|
715
|
+
// activation, so the restart heals instead of replaying the failure.
|
|
716
|
+
const activation = this.memberFailures.get(request.memberId)?.activation;
|
|
717
|
+
if (activation !== undefined && activation.class === 'session-refused' && this.remediation !== undefined) {
|
|
718
|
+
const outcome = await this.remediation.remediateMember(member);
|
|
719
|
+
if (outcome.repaired > 0) {
|
|
720
|
+
this.ctx.logger.info(`agent-team: repaired ${outcome.repaired} refused Session artifact(s) for member '${member.handle}'; retrying activation`);
|
|
721
|
+
}
|
|
722
|
+
else if (outcome.completed) {
|
|
723
|
+
// The walk finished and nothing was provably this plugin's to fix:
|
|
724
|
+
// a retry would fail identically. Mark the refusal non-remediable
|
|
725
|
+
// so the surface stops offering restart and says why.
|
|
726
|
+
this.markRefusalNonRemediable(request.memberId);
|
|
727
|
+
return Object.freeze({ status: this.memberStatus(member) });
|
|
728
|
+
}
|
|
729
|
+
}
|
|
662
730
|
await this.reactivateMember(request.memberId);
|
|
663
731
|
return Object.freeze({ status: this.memberStatus(member) });
|
|
664
732
|
}
|
|
@@ -1078,7 +1146,7 @@ let AgentTeam = (() => {
|
|
|
1078
1146
|
this.emitCommitted(result.value.receipt);
|
|
1079
1147
|
return result.value;
|
|
1080
1148
|
}
|
|
1081
|
-
/**
|
|
1149
|
+
/** Human Inbox projection; the Web Client consumes the direct-only slice as its mention queue. */
|
|
1082
1150
|
inbox(request) {
|
|
1083
1151
|
this.requireWorkspace(request.workspaceId);
|
|
1084
1152
|
return this.requireLedger().inbox(agentTeamHumanActor(), request);
|
|
@@ -1087,8 +1155,10 @@ let AgentTeam = (() => {
|
|
|
1087
1155
|
async readThread(request) {
|
|
1088
1156
|
const actor = this.humanCall(request.workspaceId);
|
|
1089
1157
|
const result = await this.requireLedger().readThread({ ...request, actor });
|
|
1090
|
-
|
|
1091
|
-
|
|
1158
|
+
// A read that made no progress commits no operation and carries no receipt.
|
|
1159
|
+
const receipt = result.committed ? result.value.receipt : undefined;
|
|
1160
|
+
if (receipt !== undefined)
|
|
1161
|
+
this.emitCommitted(receipt);
|
|
1092
1162
|
return result.value;
|
|
1093
1163
|
}
|
|
1094
1164
|
/** Human-only durable Attention observations for one Thread. */
|
|
@@ -1147,8 +1217,10 @@ let AgentTeam = (() => {
|
|
|
1147
1217
|
async readThreadForAgent(agent, request) {
|
|
1148
1218
|
const actor = this.memberCall(agent, request.workspaceId);
|
|
1149
1219
|
const result = await this.requireLedger().readThread({ ...request, actor });
|
|
1150
|
-
|
|
1151
|
-
|
|
1220
|
+
// A read that made no progress commits no operation and carries no receipt.
|
|
1221
|
+
const receipt = result.committed ? result.value.receipt : undefined;
|
|
1222
|
+
if (receipt !== undefined)
|
|
1223
|
+
this.emitCommitted(receipt);
|
|
1152
1224
|
const value = result.value;
|
|
1153
1225
|
// Private read-time enrich: an acceptance the reader just acknowledged is
|
|
1154
1226
|
// a natural Task boundary, so the Host prices the reader's context once,
|
|
@@ -1265,6 +1337,16 @@ let AgentTeam = (() => {
|
|
|
1265
1337
|
validateLedger() {
|
|
1266
1338
|
this.requireLedger().validate();
|
|
1267
1339
|
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Validate the durable ledger for the invariant's mount check. The
|
|
1342
|
+
* constructor already re-derived every durable record against its own
|
|
1343
|
+
* scratch projection, so this adopts that conclusion once while nothing has
|
|
1344
|
+
* committed since; every other call, and every commit-driven validation,
|
|
1345
|
+
* replays the whole table again.
|
|
1346
|
+
*/
|
|
1347
|
+
validateLedgerAtMount() {
|
|
1348
|
+
this.requireLedger().validateAtMount();
|
|
1349
|
+
}
|
|
1268
1350
|
/**
|
|
1269
1351
|
* Effective context-pressure budget for one Member's current route:
|
|
1270
1352
|
* `hardLimit = min(256K, routeWindow - outputReserve)` and
|
|
@@ -1362,20 +1444,13 @@ let AgentTeam = (() => {
|
|
|
1362
1444
|
live = false;
|
|
1363
1445
|
}
|
|
1364
1446
|
else {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
events = (await handle.read()).events;
|
|
1369
|
-
inheritedEventCount = handle.inheritedEventCount;
|
|
1370
|
-
parentSession = handle.header.parentSession;
|
|
1371
|
-
}
|
|
1372
|
-
finally {
|
|
1373
|
-
await handle.close();
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
catch (error) {
|
|
1377
|
-
throw new Error(`checkpoint '${checkpointRef}' could not be resolved: its source Session is unreadable (${error instanceof Error ? error.message : String(error)})`);
|
|
1447
|
+
const read = await this.sessionReader.read(sessionId);
|
|
1448
|
+
if (!read.ok) {
|
|
1449
|
+
throw new StoredSessionReadError(`checkpoint '${checkpointRef}' could not be resolved: its source Session is unreadable (${read.failure.kind}: ${read.failure.detail})`, read.failure);
|
|
1378
1450
|
}
|
|
1451
|
+
events = read.inspection.events;
|
|
1452
|
+
inheritedEventCount = read.inspection.inheritedEventCount;
|
|
1453
|
+
parentSession = read.inspection.header.parentSession;
|
|
1379
1454
|
}
|
|
1380
1455
|
// Fold the source with its inherited cut respected: inherited events
|
|
1381
1456
|
// are resolved history in that source, never fresh intent; checkpoints
|
|
@@ -1452,20 +1527,15 @@ let AgentTeam = (() => {
|
|
|
1452
1527
|
?? this.requireLedger().previousSessionForMember(member.memberId);
|
|
1453
1528
|
if (previousSessionId === undefined)
|
|
1454
1529
|
return;
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
}
|
|
1461
|
-
finally {
|
|
1462
|
-
await handle.close();
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
catch (error) {
|
|
1466
|
-
this.ctx.logger.warn(`agent-team: rollover handoff reconstruction could not read the previous Session '${previousSessionId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1530
|
+
const previousRead = await this.sessionReader.read(previousSessionId);
|
|
1531
|
+
if (!previousRead.ok) {
|
|
1532
|
+
// Best-effort recovery: the handoff intent stays lost with the
|
|
1533
|
+
// unreadable previous Session, but the Member still activates.
|
|
1534
|
+
const failure = previousRead.failure;
|
|
1535
|
+
this.ctx.logger.warn(`agent-team: rollover handoff reconstruction could not read the previous Session '${previousSessionId}' (${failure.kind}): ${failure.detail}`);
|
|
1467
1536
|
return;
|
|
1468
1537
|
}
|
|
1538
|
+
const inspection = previousRead.inspection;
|
|
1469
1539
|
const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, previousSessionId);
|
|
1470
1540
|
if (state.pending === null)
|
|
1471
1541
|
return;
|
|
@@ -1494,19 +1564,12 @@ let AgentTeam = (() => {
|
|
|
1494
1564
|
* downgrade to a blank child.
|
|
1495
1565
|
*/
|
|
1496
1566
|
async recordedCheckpointPrefix(seed) {
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
inspection = { events: (await handle.read()).events, inheritedEventCount: handle.inheritedEventCount };
|
|
1502
|
-
}
|
|
1503
|
-
finally {
|
|
1504
|
-
await handle.close();
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
catch (error) {
|
|
1508
|
-
throw new Error(`the recorded checkpoint-return seed source Session '${seed.sourceSessionId}' is unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1567
|
+
const seedRead = await this.sessionReader.read(seed.sourceSessionId);
|
|
1568
|
+
if (!seedRead.ok) {
|
|
1569
|
+
const failure = seedRead.failure;
|
|
1570
|
+
throw new StoredSessionReadError(`the recorded checkpoint-return seed source Session '${seed.sourceSessionId}' is unreadable (${failure.kind}): ${failure.detail}`, failure);
|
|
1509
1571
|
}
|
|
1572
|
+
const inspection = seedRead.inspection;
|
|
1510
1573
|
const through = Number(seed.sourceThroughSeq);
|
|
1511
1574
|
if (!Number.isSafeInteger(through) || through < 0 || through > inspection.events.length) {
|
|
1512
1575
|
throw new Error(`the recorded checkpoint-return seed cut ${through} is not a valid prefix of Session '${seed.sourceSessionId}'`);
|
|
@@ -1555,23 +1618,14 @@ let AgentTeam = (() => {
|
|
|
1555
1618
|
}
|
|
1556
1619
|
let inspectionEvents;
|
|
1557
1620
|
let inspectionInherited;
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
inspectionEvents = (await handle.read()).events;
|
|
1562
|
-
inspectionInherited = handle.inheritedEventCount;
|
|
1563
|
-
}
|
|
1564
|
-
finally {
|
|
1565
|
-
await handle.close();
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
catch (error) {
|
|
1569
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1621
|
+
const previousRead = await this.sessionReader.read(transition.previousSessionId);
|
|
1622
|
+
if (!previousRead.ok) {
|
|
1623
|
+
const failure = previousRead.failure;
|
|
1570
1624
|
// Log-corruption class: bounded fail-open. The current Session's own
|
|
1571
1625
|
// fold is intact and the Host repairs the retired log's torn tail; a
|
|
1572
1626
|
// corrupted retired generation must not permanently block activation.
|
|
1573
|
-
if (
|
|
1574
|
-
this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is corrupt: ${detail}; skipping the replay`);
|
|
1627
|
+
if (failure.kind === 'corrupt') {
|
|
1628
|
+
this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is corrupt: ${failure.detail}; skipping the replay`);
|
|
1575
1629
|
return 0;
|
|
1576
1630
|
}
|
|
1577
1631
|
// Deterministic released-format refusal: the retired artifact is refused
|
|
@@ -1580,12 +1634,14 @@ let AgentTeam = (() => {
|
|
|
1580
1634
|
// Member no longer runs in into permanent unavailability — the exact
|
|
1581
1635
|
// failure class this hardening removes. Genuine IO and unknown failures
|
|
1582
1636
|
// stay fail-closed so carried input is never dropped silently.
|
|
1583
|
-
if (
|
|
1584
|
-
this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is refused by the session-format migration: ${detail}; skipping the replay`);
|
|
1637
|
+
if (failure.kind === 'refused') {
|
|
1638
|
+
this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is refused by the session-format migration: ${failure.detail}; skipping the replay`);
|
|
1585
1639
|
return 0;
|
|
1586
1640
|
}
|
|
1587
|
-
throw new
|
|
1641
|
+
throw new StoredSessionReadError(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable (${failure.kind}): ${failure.detail}`, failure);
|
|
1588
1642
|
}
|
|
1643
|
+
inspectionEvents = previousRead.inspection.events;
|
|
1644
|
+
inspectionInherited = previousRead.inspection.inheritedEventCount;
|
|
1589
1645
|
const state = foldContextProjection(inspectionEvents, inspectionInherited, transition.previousSessionId);
|
|
1590
1646
|
const carried = carriedInputOf(state);
|
|
1591
1647
|
if (carried.length === 0)
|
|
@@ -1659,6 +1715,7 @@ let AgentTeam = (() => {
|
|
|
1659
1715
|
// persisted logs; the same projection definition folds every source.
|
|
1660
1716
|
const items = [];
|
|
1661
1717
|
const seen = new Set();
|
|
1718
|
+
let incompleteFrom;
|
|
1662
1719
|
let sessionId = member.sessionId;
|
|
1663
1720
|
let live = true;
|
|
1664
1721
|
let guard = 0;
|
|
@@ -1676,23 +1733,19 @@ let AgentTeam = (() => {
|
|
|
1676
1733
|
live = false;
|
|
1677
1734
|
}
|
|
1678
1735
|
else {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
sourceEvents = events;
|
|
1685
|
-
sourceSessionId = sessionId;
|
|
1686
|
-
sessionId = handle.header.parentSession;
|
|
1687
|
-
}
|
|
1688
|
-
finally {
|
|
1689
|
-
await handle.close();
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
catch {
|
|
1693
|
-
// An unreadable ancestor ends the lineage walk here.
|
|
1736
|
+
const read = await this.sessionReader.read(sessionId);
|
|
1737
|
+
if (!read.ok) {
|
|
1738
|
+
// An unreadable ancestor ends the lineage walk here — recorded in
|
|
1739
|
+
// the result, never silent, and never a Member-availability fact.
|
|
1740
|
+
incompleteFrom = { sessionId, reason: `${read.failure.kind}: ${read.failure.detail}` };
|
|
1694
1741
|
sessionId = undefined;
|
|
1695
1742
|
}
|
|
1743
|
+
else {
|
|
1744
|
+
state = foldContextProjection(read.inspection.events, read.inspection.inheritedEventCount, sessionId);
|
|
1745
|
+
sourceEvents = read.inspection.events;
|
|
1746
|
+
sourceSessionId = sessionId;
|
|
1747
|
+
sessionId = read.inspection.header.parentSession;
|
|
1748
|
+
}
|
|
1696
1749
|
}
|
|
1697
1750
|
if (state === undefined)
|
|
1698
1751
|
break;
|
|
@@ -1713,7 +1766,7 @@ let AgentTeam = (() => {
|
|
|
1713
1766
|
if (items.length >= limit)
|
|
1714
1767
|
break;
|
|
1715
1768
|
}
|
|
1716
|
-
return { usageTokens, hardLimit, handoffAt, items };
|
|
1769
|
+
return { usageTokens, hardLimit, handoffAt, items, ...(incompleteFrom === undefined ? {} : { incompleteFrom }) };
|
|
1717
1770
|
}
|
|
1718
1771
|
/**
|
|
1719
1772
|
* Replayed measurement of one lineage source: the live current Session
|
|
@@ -1729,19 +1782,16 @@ let AgentTeam = (() => {
|
|
|
1729
1782
|
return undefined;
|
|
1730
1783
|
if (live)
|
|
1731
1784
|
return meter.measure(agent.session)?.totalTokens;
|
|
1785
|
+
// 0.1.5 removed borrowSession: rebuild a detached Session from the
|
|
1786
|
+
// stored log so the seed's retained cost is priced by the SOURCE's own
|
|
1787
|
+
// replay, never the current generation's. An unreadable source or a
|
|
1788
|
+
// failing meter is unmeasurable and prices as UNKNOWN.
|
|
1789
|
+
const read = await this.sessionReader.read(sessionId);
|
|
1790
|
+
if (!read.ok)
|
|
1791
|
+
return undefined;
|
|
1732
1792
|
try {
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
// replay, never the current generation's.
|
|
1736
|
-
const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
|
|
1737
|
-
try {
|
|
1738
|
-
const { events } = await handle.read();
|
|
1739
|
-
const session = Session.create(sessionId, events, handle.header, handle.inheritedEventCount);
|
|
1740
|
-
return meter.measure(session)?.totalTokens;
|
|
1741
|
-
}
|
|
1742
|
-
finally {
|
|
1743
|
-
await handle.close();
|
|
1744
|
-
}
|
|
1793
|
+
const session = Session.create(sessionId, read.inspection.events, read.inspection.header, read.inspection.inheritedEventCount);
|
|
1794
|
+
return meter.measure(session)?.totalTokens;
|
|
1745
1795
|
}
|
|
1746
1796
|
catch {
|
|
1747
1797
|
return undefined;
|
|
@@ -2097,9 +2147,17 @@ let AgentTeam = (() => {
|
|
|
2097
2147
|
// `swap` is bound by the provider at activation (no-op until then).
|
|
2098
2148
|
const skillSelection = { current: member.capabilities?.skills?.allow, swap: () => { } };
|
|
2099
2149
|
const setup = async (agentCtx, agent) => {
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2150
|
+
try {
|
|
2151
|
+
await this.ctx.agentPresets.mount(agentCtx, member.presetId);
|
|
2152
|
+
this.memberRuntime.applyMemberToolPolicy(agentCtx, member);
|
|
2153
|
+
this.validateMemberPreset(agentCtx);
|
|
2154
|
+
}
|
|
2155
|
+
catch (error) {
|
|
2156
|
+
// Tag composition failures with their own class: the activation
|
|
2157
|
+
// diagnostic routes preset-composition (install/runtime split)
|
|
2158
|
+
// failures by type, not by matching message text.
|
|
2159
|
+
throw new PresetCompositionError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
2160
|
+
}
|
|
2103
2161
|
installModelSelection(agentCtx, selected);
|
|
2104
2162
|
// Admission gate for pending context rollovers: once a successful
|
|
2105
2163
|
// context_rollover result is durable, queued input must not open another
|
|
@@ -2276,11 +2334,11 @@ let AgentTeam = (() => {
|
|
|
2276
2334
|
this.memberRuntime.forgetMember(member.memberId);
|
|
2277
2335
|
const message = error instanceof Error ? error.message : String(error);
|
|
2278
2336
|
this.ctx.logger.warn(`agent-team: activation failed for member '${this.memberLabel(member.memberId)}': ${message}`);
|
|
2279
|
-
this.
|
|
2337
|
+
this.setActivationDiagnostic(member.memberId, this.activationDiagnosticOf(error, member.sessionId));
|
|
2280
2338
|
}
|
|
2281
2339
|
finally {
|
|
2282
2340
|
// Activation only changes this Workspace's presence projection.
|
|
2283
|
-
this.
|
|
2341
|
+
this.emitPresenceChanged(member.workspaceId);
|
|
2284
2342
|
}
|
|
2285
2343
|
}
|
|
2286
2344
|
/**
|
|
@@ -2368,13 +2426,13 @@ let AgentTeam = (() => {
|
|
|
2368
2426
|
// new binding as active — a Client following the row would otherwise open
|
|
2369
2427
|
// a Session that does not exist yet.
|
|
2370
2428
|
if (handle.agent.id !== member.sessionId)
|
|
2371
|
-
return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic: 'context rollover in progress' });
|
|
2429
|
+
return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic: { class: 'rollover', detail: 'context rollover in progress' } });
|
|
2372
2430
|
if (this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
|
|
2373
|
-
return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: ORPHANED_MEMBER_DIAGNOSTIC });
|
|
2431
|
+
return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: { class: 'preset-composition', detail: ORPHANED_MEMBER_DIAGNOSTIC } });
|
|
2374
2432
|
}
|
|
2375
2433
|
const runtimeError = failures?.runtime ?? failures?.compaction;
|
|
2376
2434
|
if (runtimeError !== undefined)
|
|
2377
|
-
return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: runtimeError });
|
|
2435
|
+
return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: { class: 'runtime', detail: runtimeError } });
|
|
2378
2436
|
// Capability warnings are runtime-derived at activation (handles-scoped,
|
|
2379
2437
|
// like failures): absent while capabilities resolve cleanly.
|
|
2380
2438
|
const capabilityWarnings = this.memberRuntime.capabilityWarningsFor(member.memberId);
|
|
@@ -2388,6 +2446,37 @@ let AgentTeam = (() => {
|
|
|
2388
2446
|
failures[slot] = message;
|
|
2389
2447
|
this.memberFailures.set(memberId, failures);
|
|
2390
2448
|
}
|
|
2449
|
+
/** Store one structured activation diagnostic; runtime/compaction slots stay plain messages. */
|
|
2450
|
+
setActivationDiagnostic(memberId, diagnostic) {
|
|
2451
|
+
const failures = this.memberFailures.get(memberId) ?? {};
|
|
2452
|
+
failures.activation = Object.freeze(diagnostic);
|
|
2453
|
+
this.memberFailures.set(memberId, failures);
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Route one activation failure to its diagnostic class: preset composition
|
|
2457
|
+
* failures by their own error class, session failures by the seam's typed
|
|
2458
|
+
* classification (our call sites carry it directly; a Harness resume
|
|
2459
|
+
* failure carries it through the cause chain), everything else as an
|
|
2460
|
+
* unclassified activation failure.
|
|
2461
|
+
*/
|
|
2462
|
+
activationDiagnosticOf(error, sessionId) {
|
|
2463
|
+
if (error instanceof PresetCompositionError)
|
|
2464
|
+
return { class: 'preset-composition', detail: error.message };
|
|
2465
|
+
const failure = error instanceof StoredSessionReadError ? error.failure : sessionFailureOf(error, sessionId);
|
|
2466
|
+
if (failure !== undefined) {
|
|
2467
|
+
const shared = { detail: failure.detail, ...(failure.location === undefined ? {} : { location: failure.location }), sessionId: failure.sessionId };
|
|
2468
|
+
return failure.kind === 'refused' ? { class: 'session-refused', ...shared } : { class: 'session-unreadable', ...shared };
|
|
2469
|
+
}
|
|
2470
|
+
return { class: 'activation', detail: error instanceof Error ? error.message : String(error) };
|
|
2471
|
+
}
|
|
2472
|
+
/** Mark a session-refused activation diagnostic as proven non-remediable. */
|
|
2473
|
+
markRefusalNonRemediable(memberId) {
|
|
2474
|
+
const failures = this.memberFailures.get(memberId);
|
|
2475
|
+
const activation = failures?.activation;
|
|
2476
|
+
if (activation === undefined || activation.class !== 'session-refused')
|
|
2477
|
+
return;
|
|
2478
|
+
failures.activation = Object.freeze({ ...activation, remediable: false });
|
|
2479
|
+
}
|
|
2391
2480
|
clearMemberFailure(memberId, slot) {
|
|
2392
2481
|
const failures = this.memberFailures.get(memberId);
|
|
2393
2482
|
if (failures === undefined || failures[slot] === undefined)
|
|
@@ -2462,7 +2551,17 @@ let AgentTeam = (() => {
|
|
|
2462
2551
|
}
|
|
2463
2552
|
emitAutoCompactionChanged(memberId) {
|
|
2464
2553
|
const workspaceId = this.ledger?.getMember(memberId)?.workspaceId;
|
|
2465
|
-
|
|
2554
|
+
if (workspaceId === undefined)
|
|
2555
|
+
return;
|
|
2556
|
+
this.emitPresenceChanged(workspaceId);
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Presence-only invalidation: Agent running/idle/activation/failure changes
|
|
2560
|
+
* alter no durable projection, so only members/presence subscribers wake —
|
|
2561
|
+
* the workspace catalog and the scope-less Inbox subscriptions stay parked.
|
|
2562
|
+
*/
|
|
2563
|
+
emitPresenceChanged(workspaceId) {
|
|
2564
|
+
this.emitChanged([{ kind: 'presence', workspaceId }]);
|
|
2466
2565
|
}
|
|
2467
2566
|
/** Wake from durable unread state with bounded facts for direct and state-changing work. */
|
|
2468
2567
|
notifyMember(agent, sequenced = false) {
|
|
@@ -2646,21 +2745,45 @@ let AgentTeam = (() => {
|
|
|
2646
2745
|
* Wake waiters for one committed or lifecycle change. Undefined broadcasts
|
|
2647
2746
|
* to everyone; an empty scope list invalidates nobody because no shared
|
|
2648
2747
|
* projection changed; otherwise global and matching scoped waiters wake.
|
|
2748
|
+
* Presence-only scopes sit outside that: they change no durable projection,
|
|
2749
|
+
* so only matching presence waiters wake and the scope-less Inbox
|
|
2750
|
+
* subscriptions stay parked.
|
|
2751
|
+
*
|
|
2752
|
+
* Each woken waiter receives the version of its own scope's domain, so a
|
|
2753
|
+
* wake can never hand a projection waiter a presence number or the other way
|
|
2754
|
+
* around. Nothing here advances the projection version.
|
|
2649
2755
|
*/
|
|
2650
2756
|
emitChanged(scopes) {
|
|
2651
|
-
|
|
2757
|
+
const touchesProjection = scopes === undefined || scopes.some(scope => scope.kind !== 'presence');
|
|
2758
|
+
const touchesPresence = scopes !== undefined && scopes.some(scope => scope.kind === 'presence');
|
|
2759
|
+
if (!touchesProjection && !touchesPresence)
|
|
2760
|
+
return;
|
|
2761
|
+
if (touchesPresence)
|
|
2762
|
+
this.presenceEpoch += 1;
|
|
2652
2763
|
for (const waiter of this.changeWaiters) {
|
|
2653
2764
|
const waiterScope = waiter.scope;
|
|
2654
|
-
if (scopes !== undefined && (waiterScope === undefined ?
|
|
2765
|
+
if (scopes !== undefined && (waiterScope === undefined ? !touchesProjection : !scopes.some(scope => sameChangeScope(scope, waiterScope))))
|
|
2655
2766
|
continue;
|
|
2656
2767
|
this.changeWaiters.delete(waiter);
|
|
2657
|
-
waiter.wake(this.
|
|
2768
|
+
waiter.wake(this.changeVersionOf(waiterScope));
|
|
2658
2769
|
}
|
|
2659
2770
|
}
|
|
2771
|
+
/**
|
|
2772
|
+
* The cursor domain of one scope: presence scopes count process-local edge
|
|
2773
|
+
* wakes, every other scope compares against the durable ledger position of
|
|
2774
|
+
* the newest shared-projection commit. The two domains are deliberately
|
|
2775
|
+
* separate, so a presence edge cannot invalidate a projection subscriber and
|
|
2776
|
+
* a private read invalidates no one.
|
|
2777
|
+
*/
|
|
2778
|
+
changeVersionOf(scope) {
|
|
2779
|
+
if (scope?.kind === 'presence')
|
|
2780
|
+
return this.presenceEpoch;
|
|
2781
|
+
return this.ledger?.projectionSequence() ?? 0;
|
|
2782
|
+
}
|
|
2660
2783
|
validateChangeScope(scope) {
|
|
2661
2784
|
if (scope === undefined)
|
|
2662
2785
|
return undefined;
|
|
2663
|
-
const ref = scope.kind === 'workspace' ? scope.workspaceId : scope.kind === 'channel' ? scope.channelRef : scope.threadRef;
|
|
2786
|
+
const ref = scope.kind === 'workspace' || scope.kind === 'presence' ? scope.workspaceId : scope.kind === 'channel' ? scope.channelRef : scope.threadRef;
|
|
2664
2787
|
if (typeof ref !== 'string' || ref.length === 0)
|
|
2665
2788
|
throw new Error(`change scope of kind '${scope.kind}' requires a non-empty ref`);
|
|
2666
2789
|
return scope;
|