@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
|
@@ -61,16 +61,20 @@ export function renderMemberIdentity(member) {
|
|
|
61
61
|
? `Team identity: you are @${member.handle}.`
|
|
62
62
|
: `Team identity: you are @${member.handle} — ${member.description}`;
|
|
63
63
|
}
|
|
64
|
+
/** The four private-memory paths plus the out-of-cwd warning; callers append their own sentence. */
|
|
65
|
+
function memoryPathsBlock(privateMemoryPath) {
|
|
66
|
+
return `Private memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd.`;
|
|
67
|
+
}
|
|
64
68
|
export function renderMemberMemory(raw, privateMemoryPath = '<private-memory-path>') {
|
|
65
69
|
const overBudget = raw.byteLength > MAX_MEMORY_BYTES;
|
|
66
70
|
const body = overBudget ? '' : raw.toString('utf8');
|
|
67
71
|
const warning = overBudget
|
|
68
72
|
? '\n\n[Maintenance warning: memory.md exceeds the 8 KiB context budget. Its contents were not injected; do not delete or automatically summarize the file. Maintain a smaller index explicitly.]'
|
|
69
73
|
: '';
|
|
70
|
-
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\
|
|
74
|
+
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\n${memoryPathsBlock(privateMemoryPath)} Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Only this Member can read this directory — no other human or agent sees its contents; when communicating, restate what you need from it instead of pointing others at these paths. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
|
|
71
75
|
}
|
|
72
76
|
function renderUnavailableMemory(privateMemoryPath, reason) {
|
|
73
|
-
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\
|
|
77
|
+
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\n${memoryPathsBlock(privateMemoryPath)} Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
|
|
74
78
|
}
|
|
75
79
|
function escape(value) {
|
|
76
80
|
return value.replaceAll(BEGIN, '[escaped begin marker]').replaceAll(END, '[escaped end marker]');
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
30
30
|
import { formatTeamDuration, formatTeamTimestamp } from "./time-format.js";
|
|
31
|
+
import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
|
|
31
32
|
export const name = 'wowyuarm-agent-team-member-time-context';
|
|
32
33
|
/** Default minimum spacing between two snapshots within one turn, in ms. */
|
|
33
34
|
export const CLOCK_REFRESH_INTERVAL_MS = 1_800_000;
|
|
@@ -90,8 +91,22 @@ export function renderClockSnapshot(input) {
|
|
|
90
91
|
+ `Elapsed since the preceding ${baseline}: ${elapsed}.\n`
|
|
91
92
|
+ 'Team collaboration timestamps use UTC+8. Sequence and revision, not wall-clock time, determine ordering and concurrency.';
|
|
92
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* The clock baseline folded incrementally over one Session's own events. A
|
|
96
|
+
* rollover starts a fresh Session log, and the cache below is keyed by Session
|
|
97
|
+
* id, so the fold never guesses elapsed across generations: a missing prior
|
|
98
|
+
* event renders `unavailable`, not a fabricated baseline.
|
|
99
|
+
*/
|
|
100
|
+
const CLOCK_FOLD = {
|
|
101
|
+
start: emptyBaseline(),
|
|
102
|
+
step: (state, event) => applyClockEvent(state, event),
|
|
103
|
+
};
|
|
93
104
|
export function apply(ctx, config = {}) {
|
|
94
105
|
const refreshIntervalMs = config.refreshIntervalMs ?? CLOCK_REFRESH_INTERVAL_MS;
|
|
106
|
+
// Per-step fold state, reused for this plugin lifecycle: one entry per
|
|
107
|
+
// Member, replaced when that Member's Session changes, so a rollover neither
|
|
108
|
+
// resumes across generations nor retains every generation it leaves behind.
|
|
109
|
+
const cursors = new Map();
|
|
95
110
|
ctx.on('agent/pre-step', async ({ agent, turn, step, signal }, next) => {
|
|
96
111
|
const decision = await next();
|
|
97
112
|
if (decision.kind === 'reject' || signal.aborted)
|
|
@@ -102,13 +117,13 @@ export function apply(ctx, config = {}) {
|
|
|
102
117
|
const host = ctx.get('agentTeam');
|
|
103
118
|
if (host === undefined)
|
|
104
119
|
return decision;
|
|
105
|
-
|
|
120
|
+
const member = host.memberForAgent(agent);
|
|
121
|
+
if (member === undefined)
|
|
106
122
|
return decision;
|
|
107
123
|
const now = Date.now();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const baseline = foldClockBaseline(agent.session.ownEvents());
|
|
124
|
+
const owned = advanceOwnedSessionEventCursor(cursors.get(member.memberId), agent.session.id, CLOCK_FOLD, agent.session.ownEvents(), agent.session.inheritedEventCount);
|
|
125
|
+
cursors.set(member.memberId, owned);
|
|
126
|
+
const baseline = owned.cursor.value;
|
|
112
127
|
// Turn-first-step always samples; later steps sample only at the refresh
|
|
113
128
|
// interval, so a quick tool-dense turn stays at one line.
|
|
114
129
|
if (!shouldSampleClock(step, now, baseline, refreshIntervalMs))
|
|
@@ -18,6 +18,26 @@ import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm';
|
|
|
18
18
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
19
19
|
import { CONTEXT_PRESSURE_NOTICE_SUMMARY } from "./context-management.js";
|
|
20
20
|
import { AGENT_TEAM_PLUGIN_ID } from "./context-source.js";
|
|
21
|
+
import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
|
|
22
|
+
/**
|
|
23
|
+
* Whether this Session's own span already carries the pressure notice: either
|
|
24
|
+
* surfaced as a `user/message` or still queued in a durable
|
|
25
|
+
* `agent/inbox/spliced` insert. Expressed as a fold rather than a scan because
|
|
26
|
+
* the answer is monotone — once delivered it stays delivered for the
|
|
27
|
+
* generation — so the cursor only ever has to read events it has not seen.
|
|
28
|
+
*/
|
|
29
|
+
export const PRESSURE_NOTICE_FOLD = {
|
|
30
|
+
start: false,
|
|
31
|
+
step: (delivered, event) => {
|
|
32
|
+
if (delivered)
|
|
33
|
+
return true;
|
|
34
|
+
if (event.type === 'user/message' && isPressureNotice(event.data))
|
|
35
|
+
return true;
|
|
36
|
+
if (event.type === 'agent/inbox/spliced' && event.data.inserted.some(isPressureNotice))
|
|
37
|
+
return true;
|
|
38
|
+
return false;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
21
41
|
/** One pressure-notice text; concise, structured, and inside the reserve. */
|
|
22
42
|
export function contextPressureNoticeText(input) {
|
|
23
43
|
const claims = input.activeClaims.length === 0 ? 'none' : input.activeClaims.join(', ');
|
|
@@ -41,6 +61,14 @@ export class PressurePolicyCoordinator {
|
|
|
41
61
|
* Process-only by design: a restart re-earns one sequence per chain.
|
|
42
62
|
*/
|
|
43
63
|
overflowRetries = new Map();
|
|
64
|
+
/**
|
|
65
|
+
* Whether the one-shot notice was already delivered, folded incrementally per
|
|
66
|
+
* Member. The scan below is a monotone "has this ever happened" fold over the
|
|
67
|
+
* Session's own events, so a cursor can replace re-scanning the whole log on
|
|
68
|
+
* every step; identity guarding falls back to a cold fold when the Member's
|
|
69
|
+
* Session changed under the entry.
|
|
70
|
+
*/
|
|
71
|
+
noticeSeen = new Map();
|
|
44
72
|
disposed = false;
|
|
45
73
|
constructor(options) {
|
|
46
74
|
this.options = options;
|
|
@@ -55,14 +83,10 @@ export class PressurePolicyCoordinator {
|
|
|
55
83
|
* rollover starts a fresh Session whose own event span has no notice yet,
|
|
56
84
|
* which is exactly the documented re-arm.
|
|
57
85
|
*/
|
|
58
|
-
noticeDelivered(agent) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (event.type === 'agent/inbox/spliced' && event.data.inserted.some(isPressureNotice))
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
return false;
|
|
86
|
+
noticeDelivered(agent, memberId) {
|
|
87
|
+
const owned = advanceOwnedSessionEventCursor(this.noticeSeen.get(memberId), agent.session.id, PRESSURE_NOTICE_FOLD, agent.session.ownEvents(), agent.session.inheritedEventCount);
|
|
88
|
+
this.noticeSeen.set(memberId, owned);
|
|
89
|
+
return owned.cursor.value;
|
|
66
90
|
}
|
|
67
91
|
dispose() {
|
|
68
92
|
this.disposed = true;
|
|
@@ -98,7 +122,7 @@ export class PressurePolicyCoordinator {
|
|
|
98
122
|
const outcome = await this.enforceHardLimit(agent, member.memberId, member.sessionId, signal);
|
|
99
123
|
return outcome ? { kind: 'continue' } : { kind: 'reject' };
|
|
100
124
|
}
|
|
101
|
-
if (usageTokens >= handoffAt && !this.noticeDelivered(agent)) {
|
|
125
|
+
if (usageTokens >= handoffAt && !this.noticeDelivered(agent, member.memberId)) {
|
|
102
126
|
const notice = createUserMessage({
|
|
103
127
|
content: [{ type: 'text', text: contextPressureNoticeText({
|
|
104
128
|
usageTokens, handoffAt, hardLimit,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental Session-log fold with an identity guard.
|
|
3
|
+
*
|
|
4
|
+
* Several Host projections are pure left folds over one Member Session's own
|
|
5
|
+
* events: context intent, the clock baseline, and the one-shot pressure
|
|
6
|
+
* notice. Folding the whole log on every event makes each of them cost
|
|
7
|
+
* O(session length) per step, so a long Session gets steadily slower. This
|
|
8
|
+
* module keeps the fold value together with how far it has consumed the log,
|
|
9
|
+
* so a step folds only the tail that arrived since — the same idiom the
|
|
10
|
+
* Harness Session itself uses for its own derived folds.
|
|
11
|
+
*
|
|
12
|
+
* The module is deliberately Session-agnostic: it takes a plain event array
|
|
13
|
+
* plus the log offset that array starts at, so a caller (including a
|
|
14
|
+
* measurement harness) can drive it without constructing a Session.
|
|
15
|
+
*
|
|
16
|
+
* Correctness rests on the log being append-only, which it is: Session logs
|
|
17
|
+
* have no truncation API, `seq` is the log index, and `seq === log.length` is
|
|
18
|
+
* a documented contiguity contract. A Session adopts a different log only by
|
|
19
|
+
* being constructed from another seed, which replaces the whole slice rather
|
|
20
|
+
* than editing one position.
|
|
21
|
+
*
|
|
22
|
+
* The guard covers the observable ways that contract can break: a changed
|
|
23
|
+
* `logFrom`, a shorter log, a different event type at the anchor. Each
|
|
24
|
+
* re-folds from `logFrom`, so a fork, resume, or rollover degrades to the cost
|
|
25
|
+
* of a cold fold and never to a wrong picture.
|
|
26
|
+
*
|
|
27
|
+
* Named residual: the guard anchors on `(seq, type)`, so an event replaced in
|
|
28
|
+
* place by another of the SAME type at the SAME seq is not detected, and the
|
|
29
|
+
* cursor would keep its old value and fold only the tail. Reaching that state
|
|
30
|
+
* requires a caller to violate the append-only contract above — reading one
|
|
31
|
+
* Session's cursor against a log someone rewrote in place. A content-sensitive
|
|
32
|
+
* anchor would not close it either: only the anchor position is re-read, so a
|
|
33
|
+
* swap at any earlier folded position passes a boundary digest just as
|
|
34
|
+
* silently, while costing a payload digest on every advance. Closing it
|
|
35
|
+
* properly means fingerprinting every folded event, which is the full refold
|
|
36
|
+
* this module exists to avoid; if the contract ever changes, that is the fix.
|
|
37
|
+
* @module @wowyuarm/dsh-agent-team/session-event-cursor
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Open a cursor for a log slice that starts at `logFrom` (a Session's
|
|
41
|
+
* `inheritedEventCount` for its own events, or 0 for a full log). Nothing is
|
|
42
|
+
* consumed yet, so the first {@link advanceSessionEventCursor} folds the whole
|
|
43
|
+
* slice.
|
|
44
|
+
*/
|
|
45
|
+
export function initSessionEventCursor(fold, logFrom = 0) {
|
|
46
|
+
return { value: fold.start, foldedThrough: logFrom, logFrom, anchor: null };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fold the log tail that arrived since the cursor stopped and return the
|
|
50
|
+
* advanced cursor; the same reference comes back when there is nothing new.
|
|
51
|
+
*
|
|
52
|
+
* `events` is the slice starting at `logFrom`, and `logEnd` is the EXCLUSIVE
|
|
53
|
+
* end offset of that slice in the owning log — the same coordinate space as
|
|
54
|
+
* `logFrom`, not `events.length`. An inherited Session's own events start at
|
|
55
|
+
* its `inheritedEventCount`, so for a 14-event log with 10 inherited events
|
|
56
|
+
* the slice is `events[0..4]` spanning `logFrom = 10` to `logEnd = 14`. This
|
|
57
|
+
* mirrors the Session's own `snapshotEvents(fromSeq, toSeqExclusive)`.
|
|
58
|
+
*
|
|
59
|
+
* Both offsets are passed explicitly rather than read from the cursor so the
|
|
60
|
+
* caller states the log it is folding, which is also what makes the fallback
|
|
61
|
+
* below decidable.
|
|
62
|
+
*/
|
|
63
|
+
export function advanceSessionEventCursor(cursor, events, logFrom, logEnd, fold) {
|
|
64
|
+
if (logEnd !== logFrom + events.length) {
|
|
65
|
+
// The slice and the log it claims to describe disagree, so the tail it is
|
|
66
|
+
// missing is indistinguishable from events that were never there. Refuse
|
|
67
|
+
// rather than fold a shape the caller did not state.
|
|
68
|
+
throw new RangeError(`session event cursor: ${events.length} events cannot span [${logFrom}, ${logEnd})`);
|
|
69
|
+
}
|
|
70
|
+
// Resume only from a position the log can still justify. Every other case
|
|
71
|
+
// re-folds from `logFrom`: one path handles truncation, a changed fork
|
|
72
|
+
// prefix, and a rebuilt log alike, so no caller has to classify them.
|
|
73
|
+
const resumable = cursor.logFrom === logFrom
|
|
74
|
+
&& cursor.foldedThrough >= logFrom
|
|
75
|
+
&& cursor.foldedThrough <= logEnd
|
|
76
|
+
&& (cursor.foldedThrough === logFrom || anchorMatches(cursor, events));
|
|
77
|
+
const from = resumable ? cursor.foldedThrough : logFrom;
|
|
78
|
+
if (from === logEnd)
|
|
79
|
+
return cursor;
|
|
80
|
+
let value = resumable ? cursor.value : fold.start;
|
|
81
|
+
for (let index = from; index < logEnd; index += 1)
|
|
82
|
+
value = fold.step(value, events[index - logFrom]);
|
|
83
|
+
return { value, foldedThrough: logEnd, logFrom, anchor: anchorOf(events[logEnd - 1 - logFrom]) };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether the event the cursor stopped on still occupies that position with
|
|
87
|
+
* the type it had. The position is checked as well as the type because `seq`
|
|
88
|
+
* is a log index: an event that moved is a different event.
|
|
89
|
+
*/
|
|
90
|
+
function anchorMatches(cursor, events) {
|
|
91
|
+
const anchor = cursor.anchor;
|
|
92
|
+
if (anchor === null)
|
|
93
|
+
return false;
|
|
94
|
+
const seen = events[anchor.seq - cursor.logFrom];
|
|
95
|
+
return seen !== undefined && seen.seq === anchor.seq && seen.type === anchor.type;
|
|
96
|
+
}
|
|
97
|
+
/** The identity a cursor anchors on when it stops at the last consumed event. */
|
|
98
|
+
function anchorOf(event) {
|
|
99
|
+
if (event === undefined)
|
|
100
|
+
return null;
|
|
101
|
+
return { seq: event.seq, type: event.type };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Advance one owner's fold and answer with the entry to store back, so every
|
|
105
|
+
* caller keeps the same one-entry-per-owner invariant.
|
|
106
|
+
*
|
|
107
|
+
* `events` is that Session's whole own-events slice starting at `logFrom` (its
|
|
108
|
+
* `inheritedEventCount`), so the slice's end is the log's end. The cursor is
|
|
109
|
+
* resumed only while `owned` still belongs to `sessionId`; every other case
|
|
110
|
+
* starts cold at `logFrom`, which is the same degradation the cursor's own
|
|
111
|
+
* guard performs for a rewritten log.
|
|
112
|
+
*/
|
|
113
|
+
export function advanceOwnedSessionEventCursor(owned, sessionId, fold, events, logFrom) {
|
|
114
|
+
const cursor = owned !== undefined && owned.sessionId === sessionId ? owned.cursor : initSessionEventCursor(fold, logFrom);
|
|
115
|
+
return { sessionId, cursor: advanceSessionEventCursor(cursor, events, logFrom, logFrom + events.length, fold) };
|
|
116
|
+
}
|
|
@@ -266,11 +266,6 @@ function physicalParentSession(rows) {
|
|
|
266
266
|
const parent = rows[0]?.parentSession;
|
|
267
267
|
return typeof parent === 'string' ? parent : undefined;
|
|
268
268
|
}
|
|
269
|
-
/**
|
|
270
|
-
* The startup remediation pass. Never throws to its caller: a failed or
|
|
271
|
-
* partial walk logs and leaves the completion cache unrecorded, so the next
|
|
272
|
-
* start retries exactly the Members that need it.
|
|
273
|
-
*/
|
|
274
269
|
export class SessionRemediation {
|
|
275
270
|
ctx;
|
|
276
271
|
persistence;
|
|
@@ -303,29 +298,43 @@ export class SessionRemediation {
|
|
|
303
298
|
for (const member of members) {
|
|
304
299
|
if (member.state !== 'enabled')
|
|
305
300
|
continue;
|
|
306
|
-
|
|
301
|
+
const outcome = await this.remediateMember(member);
|
|
302
|
+
if (outcome.cacheHit)
|
|
307
303
|
continue;
|
|
308
304
|
walked += 1;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
const summary = await this.remediateLineage(member);
|
|
313
|
-
repairedCount = summary.repaired;
|
|
314
|
-
untouchedCount = summary.untouched;
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
// Unexpected per-Member failure: log and leave the cache unrecorded.
|
|
318
|
-
this.ctx.logger.warn(`agent-team: legacy Session remediation for member '${member.handle}' did not complete: ${error instanceof Error ? error.message : String(error)}`);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
repaired += repairedCount;
|
|
322
|
-
untouched += untouchedCount;
|
|
323
|
-
await this.recordCompletion(member);
|
|
305
|
+
repaired += outcome.repaired;
|
|
306
|
+
untouched += outcome.untouched;
|
|
324
307
|
}
|
|
325
308
|
if (walked > 0) {
|
|
326
309
|
this.ctx.logger.info(`agent-team: legacy Session remediation walked ${walked} member lineage(s): ${repaired} artifact(s) repaired, ${untouched} left untouched`);
|
|
327
310
|
}
|
|
328
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Remediate one Member's lineage; the bounded in-place heal a restart
|
|
314
|
+
* performs after an activation refused on a session. `completed` with zero
|
|
315
|
+
* repairs is the deterministic nothing-to-do answer (a finished walk found
|
|
316
|
+
* nothing provably this plugin's, or the cache already covered the Member);
|
|
317
|
+
* `completed: false` means the walk itself failed and a later attempt
|
|
318
|
+
* should retry. Never throws.
|
|
319
|
+
*/
|
|
320
|
+
async remediateMember(member) {
|
|
321
|
+
if (member.state !== 'enabled' || this.cacheStillValid(member))
|
|
322
|
+
return { repaired: 0, untouched: 0, completed: true, cacheHit: true };
|
|
323
|
+
try {
|
|
324
|
+
const summary = await this.remediateLineage(member);
|
|
325
|
+
// A walk that failed a repair is not evidence of a clean lineage: it is
|
|
326
|
+
// left uncached so the next start retries exactly this Member, and it
|
|
327
|
+
// reports `completed: false` so no caller reads it as the deterministic
|
|
328
|
+
// nothing-to-do answer.
|
|
329
|
+
if (summary.failed === 0)
|
|
330
|
+
await this.recordCompletion(member);
|
|
331
|
+
return { repaired: summary.repaired, untouched: summary.untouched, completed: summary.failed === 0, cacheHit: false };
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
this.ctx.logger.warn(`agent-team: legacy Session remediation for member '${member.handle}' did not complete: ${error instanceof Error ? error.message : String(error)}`);
|
|
335
|
+
return { repaired: 0, untouched: 0, completed: false, cacheHit: false };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
329
338
|
/** Whether the cached walk still covers this Member under the current format. */
|
|
330
339
|
cacheStillValid(member) {
|
|
331
340
|
const record = this.table?.get(member.memberId);
|
|
@@ -360,6 +369,7 @@ export class SessionRemediation {
|
|
|
360
369
|
async remediateLineage(member) {
|
|
361
370
|
let repaired = 0;
|
|
362
371
|
let untouched = 0;
|
|
372
|
+
let failed = 0;
|
|
363
373
|
const seen = new Set();
|
|
364
374
|
let sessionId = member.sessionId;
|
|
365
375
|
for (let depth = 0; sessionId !== undefined && !seen.has(sessionId) && depth < MAX_LINEAGE_DEPTH; depth += 1) {
|
|
@@ -369,9 +379,11 @@ export class SessionRemediation {
|
|
|
369
379
|
repaired += 1;
|
|
370
380
|
else if (outcome.status === 'left-untouched')
|
|
371
381
|
untouched += 1;
|
|
382
|
+
else if (outcome.status === 'repair-failed')
|
|
383
|
+
failed += 1;
|
|
372
384
|
sessionId = outcome.parentSession === undefined ? undefined : outcome.parentSession;
|
|
373
385
|
}
|
|
374
|
-
return { repaired, untouched };
|
|
386
|
+
return { repaired, untouched, failed };
|
|
375
387
|
}
|
|
376
388
|
/** Remediate one lineage Session: verify readability, and repair when the refusal is this plugin's legacy source shape. */
|
|
377
389
|
async remediateOne(sessionId, member) {
|
|
@@ -409,7 +421,7 @@ export class SessionRemediation {
|
|
|
409
421
|
}
|
|
410
422
|
catch (readError) {
|
|
411
423
|
this.ctx.logger.warn(`agent-team: could not decode the stored artifact of Session '${sessionId}' (${basename(path)}): ${readError instanceof Error ? readError.message : String(readError)}`);
|
|
412
|
-
return { status: '
|
|
424
|
+
return { status: 'repair-failed', parentSession: undefined, reason: 'artifact undecodable' };
|
|
413
425
|
}
|
|
414
426
|
const parentSession = physicalParentSession(rows);
|
|
415
427
|
if (!rows.some(row => containsLegacySource(row))) {
|
|
@@ -423,7 +435,7 @@ export class SessionRemediation {
|
|
|
423
435
|
}
|
|
424
436
|
catch (proofError) {
|
|
425
437
|
this.ctx.logger.warn(`agent-team: Session '${sessionId}' of member '${member.handle}' still refuses after the legacy source rewrite (${proofError instanceof Error ? proofError.message : String(proofError)}); leaving it untouched`);
|
|
426
|
-
return { status: '
|
|
438
|
+
return { status: 'repair-failed', parentSession, reason: 'residual refusal after rewrite' };
|
|
427
439
|
}
|
|
428
440
|
try {
|
|
429
441
|
const published = await this.publishSibling(path, encodeCurrentArtifact(artifact));
|
|
@@ -434,7 +446,7 @@ export class SessionRemediation {
|
|
|
434
446
|
}
|
|
435
447
|
catch (publishError) {
|
|
436
448
|
this.ctx.logger.warn(`agent-team: could not publish the remediated sibling for Session '${sessionId}' (${basename(path)}): ${publishError instanceof Error ? publishError.message : String(publishError)}`);
|
|
437
|
-
return { status: '
|
|
449
|
+
return { status: 'repair-failed', parentSession, reason: 'publish failed' };
|
|
438
450
|
}
|
|
439
451
|
// Success is proven by the storage layer itself, not by our own writer.
|
|
440
452
|
try {
|
|
@@ -448,7 +460,7 @@ export class SessionRemediation {
|
|
|
448
460
|
}
|
|
449
461
|
catch (verifyError) {
|
|
450
462
|
this.ctx.logger.warn(`agent-team: the remediated sibling for Session '${sessionId}' was published but still refuses: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`);
|
|
451
|
-
return { status: '
|
|
463
|
+
return { status: 'repair-failed', parentSession, reason: 'sibling refused after publish' };
|
|
452
464
|
}
|
|
453
465
|
this.ctx.logger.info(`agent-team: repaired legacy source kinds in Session '${sessionId}' of member '${member.handle}' by publishing a current-format sibling; the original artifact is untouched`);
|
|
454
466
|
return { status: 'repaired', parentSession };
|
|
@@ -216,6 +216,47 @@ const claimOperation = (kind) => z.object({
|
|
|
216
216
|
inbox: inboxDeltaSchema,
|
|
217
217
|
}).strict(),
|
|
218
218
|
}).strict();
|
|
219
|
+
/**
|
|
220
|
+
* The five departure-fact collections a releasing operation persists. All four
|
|
221
|
+
* release kinds carry this snapshot under their own `kind` literal and their own
|
|
222
|
+
* identifying fields; the durable half that must stay identical lives here once,
|
|
223
|
+
* so a new release kind cannot silently drop or invent a field.
|
|
224
|
+
*/
|
|
225
|
+
const releaseSnapshotFields = {
|
|
226
|
+
claims: z.array(claimSchema),
|
|
227
|
+
activities: z.array(claimsReleasedActivitySchema),
|
|
228
|
+
tasks: z.array(taskSchema),
|
|
229
|
+
threads: z.array(threadSchema),
|
|
230
|
+
inbox: inboxDeltaSchema,
|
|
231
|
+
};
|
|
232
|
+
/** The receipt form every new read writes: the progress it made and the Inbox delta it consumed. */
|
|
233
|
+
const threadReadReceiptDataSchema = z.object({
|
|
234
|
+
workspaceId: workspaceIdSchema,
|
|
235
|
+
memberId: memberIdSchema,
|
|
236
|
+
threadRef: threadRefSchema,
|
|
237
|
+
taskRef: taskRefSchema.optional(),
|
|
238
|
+
readThroughSequence: z.number().int().nonnegative(),
|
|
239
|
+
inbox: inboxDeltaSchema,
|
|
240
|
+
}).strict().transform(omitUndefined);
|
|
241
|
+
/**
|
|
242
|
+
* The pre-receipt Thread-read snapshot. Still accepted so a ledger written
|
|
243
|
+
* before the receipt form keeps opening; both shapes are strict, so a stored
|
|
244
|
+
* record parses as exactly one of them.
|
|
245
|
+
*/
|
|
246
|
+
const threadReadSnapshotDataSchema = z.object({
|
|
247
|
+
workspaceId: workspaceIdSchema,
|
|
248
|
+
memberId: memberIdSchema,
|
|
249
|
+
task: taskSchema.optional(),
|
|
250
|
+
thread: threadSchema,
|
|
251
|
+
claims: z.array(claimSchema),
|
|
252
|
+
anchor: messageSchema,
|
|
253
|
+
anchorMentions: z.array(memberIdSchema),
|
|
254
|
+
facts: z.array(readFactSchema),
|
|
255
|
+
readThroughSequence: z.number().int().nonnegative(),
|
|
256
|
+
remainingUnreadCount: z.number().int().nonnegative(),
|
|
257
|
+
attention: attentionSchema.optional(),
|
|
258
|
+
inbox: inboxDeltaSchema,
|
|
259
|
+
}).strict().transform(omitUndefined);
|
|
219
260
|
/** Closed Agent Team operation union before occurrence stamping. */
|
|
220
261
|
const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
221
262
|
z.object({
|
|
@@ -258,11 +299,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
|
258
299
|
kind: z.literal('team/member-archived'),
|
|
259
300
|
data: z.object({
|
|
260
301
|
member: memberSchema,
|
|
261
|
-
|
|
262
|
-
activities: z.array(claimsReleasedActivitySchema),
|
|
263
|
-
tasks: z.array(taskSchema),
|
|
264
|
-
threads: z.array(threadSchema),
|
|
265
|
-
inbox: inboxDeltaSchema,
|
|
302
|
+
...releaseSnapshotFields,
|
|
266
303
|
}).strict(),
|
|
267
304
|
}).strict(),
|
|
268
305
|
z.object({
|
|
@@ -324,11 +361,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
|
324
361
|
workspaceId: workspaceIdSchema,
|
|
325
362
|
channelRef: channelRefSchema,
|
|
326
363
|
memberId: memberIdSchema,
|
|
327
|
-
|
|
328
|
-
activities: z.array(claimsReleasedActivitySchema),
|
|
329
|
-
tasks: z.array(taskSchema),
|
|
330
|
-
threads: z.array(threadSchema),
|
|
331
|
-
inbox: inboxDeltaSchema,
|
|
364
|
+
...releaseSnapshotFields,
|
|
332
365
|
}).strict(),
|
|
333
366
|
}).strict(),
|
|
334
367
|
z.object({
|
|
@@ -338,11 +371,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
|
338
371
|
data: z.object({
|
|
339
372
|
workspaceId: workspaceIdSchema,
|
|
340
373
|
channel: channelSchema,
|
|
341
|
-
|
|
342
|
-
activities: z.array(claimsReleasedActivitySchema),
|
|
343
|
-
tasks: z.array(taskSchema),
|
|
344
|
-
threads: z.array(threadSchema),
|
|
345
|
-
inbox: inboxDeltaSchema,
|
|
374
|
+
...releaseSnapshotFields,
|
|
346
375
|
}).strict(),
|
|
347
376
|
}).strict(),
|
|
348
377
|
z.object({
|
|
@@ -419,20 +448,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
|
419
448
|
...operationBase,
|
|
420
449
|
previousOperationId: operationIdSchema.nullable(),
|
|
421
450
|
kind: z.literal('team/thread-read'),
|
|
422
|
-
data: z.
|
|
423
|
-
workspaceId: workspaceIdSchema,
|
|
424
|
-
memberId: memberIdSchema,
|
|
425
|
-
task: taskSchema.optional(),
|
|
426
|
-
thread: threadSchema,
|
|
427
|
-
claims: z.array(claimSchema),
|
|
428
|
-
anchor: messageSchema,
|
|
429
|
-
anchorMentions: z.array(memberIdSchema),
|
|
430
|
-
facts: z.array(readFactSchema),
|
|
431
|
-
readThroughSequence: z.number().int().nonnegative(),
|
|
432
|
-
remainingUnreadCount: z.number().int().nonnegative(),
|
|
433
|
-
attention: attentionSchema.optional(),
|
|
434
|
-
inbox: inboxDeltaSchema,
|
|
435
|
-
}).strict().transform(omitUndefined),
|
|
451
|
+
data: z.union([threadReadReceiptDataSchema, threadReadSnapshotDataSchema]),
|
|
436
452
|
}).strict(),
|
|
437
453
|
z.object({
|
|
438
454
|
...operationBase,
|
|
@@ -440,11 +456,7 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
|
440
456
|
kind: z.literal('team/member-removed'),
|
|
441
457
|
data: z.object({
|
|
442
458
|
member: memberSchema,
|
|
443
|
-
|
|
444
|
-
activities: z.array(claimsReleasedActivitySchema),
|
|
445
|
-
tasks: z.array(taskSchema),
|
|
446
|
-
threads: z.array(threadSchema),
|
|
447
|
-
inbox: inboxDeltaSchema,
|
|
459
|
+
...releaseSnapshotFields,
|
|
448
460
|
}).strict(),
|
|
449
461
|
}).strict(),
|
|
450
462
|
z.object({
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Team's single seam for reading stored Sessions.
|
|
3
|
+
*
|
|
4
|
+
* Every per-Session read the Host performs goes through here: one
|
|
5
|
+
* `open → read → close` cycle whose handle lifecycle is guaranteed closed,
|
|
6
|
+
* and whose failures are normalized into the five categories consumers choose
|
|
7
|
+
* policy by. Callers never touch a `SessionHandle`, an artifact path, a
|
|
8
|
+
* format generation, or Harness error message text.
|
|
9
|
+
*
|
|
10
|
+
* The reader owns reading and classification only — not Member lifecycle,
|
|
11
|
+
* Agent create/resume, UI, ledger, or remediation (which reads raw artifact
|
|
12
|
+
* bytes through its own repair path).
|
|
13
|
+
*
|
|
14
|
+
* Classification notes: the shipped JSONL backend throws its
|
|
15
|
+
* `corrupt session log` family as plain `Error`s, so corruption is detected by
|
|
16
|
+
* that stable message prefix (an upstream gap; if the text ever changes the
|
|
17
|
+
* failure degrades to `unknown`, which consumers treat conservatively —
|
|
18
|
+
* blocking rather than silently skipping). Typed refusals are also matched
|
|
19
|
+
* through `error.cause` chains because the Harness agent layer wraps
|
|
20
|
+
* persistence failures during resume.
|
|
21
|
+
* @module @wowyuarm/dsh-agent-team/stored-session-reader
|
|
22
|
+
*/
|
|
23
|
+
import { SessionFormatUnsupportedError, SessionPersistenceCorruptionError, SessionPersistenceNotFoundError, } from '@deepseek-ai/dsh-session-persistence';
|
|
24
|
+
/**
|
|
25
|
+
* A call site's session-read failure, carrying the typed classification so
|
|
26
|
+
* activation can route the diagnostic without re-matching error text.
|
|
27
|
+
*/
|
|
28
|
+
export class StoredSessionReadError extends Error {
|
|
29
|
+
failure;
|
|
30
|
+
constructor(message, failure) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.failure = failure;
|
|
33
|
+
this.name = 'StoredSessionReadError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** How deep to follow `error.cause` chains looking for a typed session failure. */
|
|
37
|
+
const MAX_CAUSE_DEPTH = 4;
|
|
38
|
+
/** The JSONL backend's stable corruption-message prefix (plain `Error`s, not typed). */
|
|
39
|
+
const CORRUPT_SESSION_LOG_TEXT = /corrupt session log/;
|
|
40
|
+
function messageOf(error) {
|
|
41
|
+
return error instanceof Error ? error.message : String(error);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The deterministic session-shaped classification of one error value:
|
|
45
|
+
* typed Harness failures plus the corruption message family. A system error
|
|
46
|
+
* code is deliberately NOT matched here — outside the reader an fs error may
|
|
47
|
+
* come from unrelated work (private memory, attachment cache), so only the
|
|
48
|
+
* reader itself, where the failed operation is known to be a session read,
|
|
49
|
+
* may classify an fs error as `io`.
|
|
50
|
+
*/
|
|
51
|
+
function sessionClassOf(error) {
|
|
52
|
+
if (error instanceof SessionPersistenceNotFoundError)
|
|
53
|
+
return { kind: 'missing' };
|
|
54
|
+
if (error instanceof SessionFormatUnsupportedError) {
|
|
55
|
+
return { kind: 'refused', ...(error.location === undefined ? {} : { location: error.location }) };
|
|
56
|
+
}
|
|
57
|
+
if (error instanceof SessionPersistenceCorruptionError)
|
|
58
|
+
return { kind: 'corrupt' };
|
|
59
|
+
if (error instanceof Error && CORRUPT_SESSION_LOG_TEXT.test(error.message))
|
|
60
|
+
return { kind: 'corrupt' };
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/** The first session-shaped classification in an error's cause chain, with the matched error for its message. */
|
|
64
|
+
function sessionClassInChain(error) {
|
|
65
|
+
let current = error;
|
|
66
|
+
for (let depth = 0; current !== undefined && current !== null && depth <= MAX_CAUSE_DEPTH; depth += 1) {
|
|
67
|
+
const classified = sessionClassOf(current);
|
|
68
|
+
if (classified !== undefined)
|
|
69
|
+
return { classified, matched: current };
|
|
70
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Classify any failure as a stored-Session read failure. Total: unmatched
|
|
76
|
+
* errors become `unknown`, and a direct system error code becomes `io` —
|
|
77
|
+
* call this only where the failed operation is known to be a session read.
|
|
78
|
+
*/
|
|
79
|
+
export function classifyStoredSessionFailure(error, sessionId) {
|
|
80
|
+
const inChain = sessionClassInChain(error);
|
|
81
|
+
if (inChain !== undefined) {
|
|
82
|
+
return { kind: inChain.classified.kind, sessionId, detail: messageOf(inChain.matched), ...('location' in inChain.classified ? { location: inChain.classified.location } : {}) };
|
|
83
|
+
}
|
|
84
|
+
if (error !== null && typeof error === 'object' && typeof error.code === 'string') {
|
|
85
|
+
return { kind: 'io', sessionId, detail: messageOf(error) };
|
|
86
|
+
}
|
|
87
|
+
return { kind: 'unknown', sessionId, detail: messageOf(error) };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The session-shaped failure carried by an arbitrary error, when it carries
|
|
91
|
+
* one at all: deterministic markers (typed classes, the corruption message
|
|
92
|
+
* family) are matched through the cause chain; transient fs errors are not —
|
|
93
|
+
* outside the reader they are not provably session-related. Activation uses
|
|
94
|
+
* this to route diagnostics; `undefined` means the failure is not
|
|
95
|
+
* session-shaped.
|
|
96
|
+
*/
|
|
97
|
+
export function sessionFailureOf(error, sessionId) {
|
|
98
|
+
const inChain = sessionClassInChain(error);
|
|
99
|
+
if (inChain === undefined)
|
|
100
|
+
return undefined;
|
|
101
|
+
return { kind: inChain.classified.kind, sessionId, detail: messageOf(inChain.matched), ...('location' in inChain.classified ? { location: inChain.classified.location } : {}) };
|
|
102
|
+
}
|
|
103
|
+
/** Read stored Sessions through one seam: full validated reads with normalized failures. */
|
|
104
|
+
export class StoredSessionReader {
|
|
105
|
+
ctx;
|
|
106
|
+
constructor(ctx) {
|
|
107
|
+
this.ctx = ctx;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Read one stored Session completely. The handle is closed on every path;
|
|
111
|
+
* any failure of open, read, or close is returned as its normalized
|
|
112
|
+
* category, never thrown.
|
|
113
|
+
*/
|
|
114
|
+
async read(sessionId) {
|
|
115
|
+
try {
|
|
116
|
+
const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
|
|
117
|
+
try {
|
|
118
|
+
const { events } = await handle.read();
|
|
119
|
+
return { ok: true, inspection: { header: handle.header, inheritedEventCount: handle.inheritedEventCount, events } };
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await handle.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return { ok: false, failure: classifyStoredSessionFailure(error, sessionId) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Whether one stored Session has durable persisted content, decided through
|
|
131
|
+
* `stat`: the backend reports a still-draining session through its pending
|
|
132
|
+
* header, so this is the existence probe activation relies on. Only the
|
|
133
|
+
* missing case returns `false`; unexpected errors propagate as before.
|
|
134
|
+
*/
|
|
135
|
+
async exists(sessionId) {
|
|
136
|
+
return (await this.ctx.sessionPersistence.stat(sessionId)) !== undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|