@wowyuarm/dsh-agent-team 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/README.zh.md +17 -11
- package/package.json +61 -47
- package/packages/agent-team/README.md +6 -4
- package/packages/agent-team/README.zh.md +6 -3
- package/packages/agent-team/lib/context-management.js +48 -55
- package/packages/agent-team/lib/context-projection.js +22 -8
- package/packages/agent-team/lib/context-source.js +163 -23
- package/packages/agent-team/lib/index.js +349 -176
- package/packages/agent-team/lib/invariant.js +38 -4
- package/packages/agent-team/lib/ledger.js +658 -294
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-time-context.js +141 -0
- package/packages/agent-team/lib/pressure-policy.js +34 -11
- package/packages/agent-team/lib/progress-nudge.js +12 -4
- package/packages/agent-team/lib/session-event-cursor.js +116 -0
- package/packages/agent-team/lib/session-remediation.js +493 -0
- package/packages/agent-team/lib/spec.js +49 -36
- package/packages/agent-team/lib/stored-session-reader.js +138 -0
- package/packages/agent-team/lib/time-format.js +56 -0
- package/packages/agent-team/lib/typert.host.js +163 -63
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +105 -32
- package/packages/agent-team/lib/types/context-management.d.ts +13 -1
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-source.d.ts +70 -36
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +110 -9
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +205 -7
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
- package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
- package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts +172 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
- package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
- package/packages/agent-team/lib/types/time-format.d.ts +28 -0
- package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
- package/packages/agent-team/lib/types/types/entities.d.ts +36 -2
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +50 -3
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +21 -4
- package/packages/client-agent-team/README.md +1 -1
- package/packages/client-agent-team/README.zh.md +1 -1
- package/packages/client-agent-team/lib/client.js +1121 -402
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -4
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +42 -22
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +13 -24
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +15 -9
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +8 -1
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +21 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +71 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +13 -6
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +106 -32
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +50 -7
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +25 -9
- package/packages/client-agent-team/lib/types/client/locales.d.ts +18 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +18 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts +26 -1
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.js +66 -15
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +34 -6
- package/packages/tool-agent-team/README.md +8 -6
- package/packages/tool-agent-team/README.zh.md +8 -6
- package/packages/tool-agent-team/lib/context-tools.js +18 -14
- package/packages/tool-agent-team/lib/host-access.js +20 -0
- package/packages/tool-agent-team/lib/index.js +314 -123
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
- package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -39,58 +39,6 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
39
39
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
40
40
|
done = true;
|
|
41
41
|
};
|
|
42
|
-
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
43
|
-
if (value !== null && value !== void 0) {
|
|
44
|
-
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
45
|
-
var dispose, inner;
|
|
46
|
-
if (async) {
|
|
47
|
-
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
48
|
-
dispose = value[Symbol.asyncDispose];
|
|
49
|
-
}
|
|
50
|
-
if (dispose === void 0) {
|
|
51
|
-
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
52
|
-
dispose = value[Symbol.dispose];
|
|
53
|
-
if (async) inner = dispose;
|
|
54
|
-
}
|
|
55
|
-
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
56
|
-
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
57
|
-
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
58
|
-
}
|
|
59
|
-
else if (async) {
|
|
60
|
-
env.stack.push({ async: true });
|
|
61
|
-
}
|
|
62
|
-
return value;
|
|
63
|
-
};
|
|
64
|
-
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
65
|
-
return function (env) {
|
|
66
|
-
function fail(e) {
|
|
67
|
-
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
68
|
-
env.hasError = true;
|
|
69
|
-
}
|
|
70
|
-
var r, s = 0;
|
|
71
|
-
function next() {
|
|
72
|
-
while (r = env.stack.pop()) {
|
|
73
|
-
try {
|
|
74
|
-
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
75
|
-
if (r.dispose) {
|
|
76
|
-
var result = r.dispose.call(r.value);
|
|
77
|
-
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
78
|
-
}
|
|
79
|
-
else s |= 1;
|
|
80
|
-
}
|
|
81
|
-
catch (e) {
|
|
82
|
-
fail(e);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
86
|
-
if (env.hasError) throw env.error;
|
|
87
|
-
}
|
|
88
|
-
return next();
|
|
89
|
-
};
|
|
90
|
-
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
91
|
-
var e = new Error(message);
|
|
92
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
93
|
-
});
|
|
94
42
|
import { randomUUID } from 'node:crypto';
|
|
95
43
|
import { isDeepStrictEqual } from 'node:util';
|
|
96
44
|
import { Service } from '@deepseek-ai/cordis';
|
|
@@ -98,28 +46,42 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
|
98
46
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
99
47
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
100
48
|
import { scopeOf } from '@deepseek-ai/dsh-scope';
|
|
101
|
-
import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
49
|
+
import { Session, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
102
50
|
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
|
|
103
51
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
104
52
|
import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
|
|
105
53
|
import { PressurePolicyCoordinator } from "./pressure-policy.js";
|
|
106
54
|
import { ContextManagementCoordinator } from "./context-management.js";
|
|
107
|
-
import { createHandoffMessage } from "./context-source.js";
|
|
108
|
-
import { carriedInputOf, checkpointByRef, checkpointRefFor, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
|
|
55
|
+
import { AGENT_TEAM_PLUGIN_ID, createHandoffMessage } from "./context-source.js";
|
|
56
|
+
import { carriedInputOf, checkpointByRef, checkpointRefFor, contextProjectionFold, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
|
|
57
|
+
import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
|
|
109
58
|
import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
|
|
110
59
|
import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, memberMemoryDirectoryName, MemberRuntime } from "./member-runtime.js";
|
|
111
60
|
import { ProgressNudgeCoordinator } from "./progress-nudge.js";
|
|
112
61
|
import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
|
|
62
|
+
import { SessionRemediation, handoffAlreadyInLog } from "./session-remediation.js";
|
|
63
|
+
import { StoredSessionReadError, StoredSessionReader, sessionFailureOf } from "./stored-session-reader.js";
|
|
113
64
|
import { agentTeamDomainSpec } from "./spec.js";
|
|
65
|
+
import { formatTeamTimestamp } from "./time-format.js";
|
|
114
66
|
export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
|
|
115
67
|
export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
|
|
116
68
|
export { AGENT_TEAM_TOOL_NAMES } from "./member-runtime.js";
|
|
117
69
|
/** Process-stable marker carried by the final Team message tool definition. */
|
|
118
70
|
export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.preset');
|
|
119
|
-
const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
|
|
120
71
|
const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
|
|
121
72
|
const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
|
|
122
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
|
+
}
|
|
123
85
|
/** Longest accepted model-supplied checkpoint display name. */
|
|
124
86
|
const MAX_CHECKPOINT_NAME_CHARS = 120;
|
|
125
87
|
/** Default and maximum number of timeline items one query returns. */
|
|
@@ -145,6 +107,8 @@ function sameChangeScope(left, right) {
|
|
|
145
107
|
return left.channelRef === right.channelRef;
|
|
146
108
|
if (left.kind === 'thread' && right.kind === 'thread')
|
|
147
109
|
return left.threadRef === right.threadRef;
|
|
110
|
+
if (left.kind === 'presence' && right.kind === 'presence')
|
|
111
|
+
return left.workspaceId === right.workspaceId;
|
|
148
112
|
return false;
|
|
149
113
|
}
|
|
150
114
|
/** Mark the preset's `team_message` definition as an Agent Team consumer. */
|
|
@@ -152,6 +116,35 @@ export function markAgentTeamPreset(definition) {
|
|
|
152
116
|
Object.defineProperty(definition, AGENT_TEAM_PRESET_MARKER, { value: true });
|
|
153
117
|
return definition;
|
|
154
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Whether the running dsh loads workspace packages from source via tsx.
|
|
121
|
+
*
|
|
122
|
+
* The Harness `tsconfig.base.json` maps `@deepseek-ai/*` package names onto
|
|
123
|
+
* `src/` directories; tsx honors those paths, so a CLI launched with
|
|
124
|
+
* `node --import tsx/esm apps/cli/src/bin.ts` imports `@deepseek-ai/dsh-scope`
|
|
125
|
+
* from `src/index.ts` while a profile-installed bundle resolves the compiled
|
|
126
|
+
* `lib/index.js` — two module instances with independent scope keys.
|
|
127
|
+
*/
|
|
128
|
+
export function isTsxDevMode() {
|
|
129
|
+
const flags = [...process.execArgv];
|
|
130
|
+
const nodeOptions = process.env.NODE_OPTIONS;
|
|
131
|
+
if (nodeOptions !== undefined)
|
|
132
|
+
flags.push(...nodeOptions.split(/\s+/));
|
|
133
|
+
return flags.some(flag => flag.includes('tsx'));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The activation diagnostic for a dsh-scope module-instance mismatch.
|
|
137
|
+
*
|
|
138
|
+
* `agentPresets.mount` already rejected an unscoped context, so a scope key
|
|
139
|
+
* the harness sees but this bundle does not can only mean the two sides
|
|
140
|
+
* loaded different physical copies of `@deepseek-ai/dsh-scope`.
|
|
141
|
+
*/
|
|
142
|
+
export function teamPresetScopeMismatchMessage(tsxDevMode) {
|
|
143
|
+
if (tsxDevMode) {
|
|
144
|
+
return 'selected preset is not team-enabled: the dsh CLI is running from source via tsx (tsconfig paths resolve @deepseek-ai/dsh-scope to src/), so the harness and this bundle load different module instances; start dsh with the compiled CLI instead (pnpm dsh, node apps/cli/lib/bin.js, or npx @deepseek-ai/dsh)';
|
|
145
|
+
}
|
|
146
|
+
return 'selected preset is not team-enabled: this bundle and the harness resolved different physical copies of @deepseek-ai/dsh-scope; run pnpm install in the profile directory so node_modules matches the lockfile, then restart';
|
|
147
|
+
}
|
|
155
148
|
/**
|
|
156
149
|
* A DM was durably recorded but its session injection could not run (no live
|
|
157
150
|
* handle, or the wake itself failed). The recorded DM stays durable; the
|
|
@@ -282,6 +275,13 @@ let AgentTeam = (() => {
|
|
|
282
275
|
},
|
|
283
276
|
runningAgents: this.runningAgents,
|
|
284
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);
|
|
285
285
|
/**
|
|
286
286
|
* Why one Member shows error presence, per failure source. Reads prefer
|
|
287
287
|
* activation, then runtime, then compaction; slots clear independently, so
|
|
@@ -318,6 +318,14 @@ let AgentTeam = (() => {
|
|
|
318
318
|
},
|
|
319
319
|
log: message => { this.ctx.logger.warn(message); },
|
|
320
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();
|
|
321
329
|
/**
|
|
322
330
|
* Context self-management: the one deep module that turns a Member's
|
|
323
331
|
* successful `context_rollover` tool result into its next private context
|
|
@@ -332,15 +340,34 @@ let AgentTeam = (() => {
|
|
|
332
340
|
const handle = this.handles.get(memberId);
|
|
333
341
|
if (handle === undefined || handle.agent.session.id !== sessionId)
|
|
334
342
|
return undefined;
|
|
335
|
-
|
|
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;
|
|
336
352
|
},
|
|
337
353
|
executeTransition: (memberId, plan) => this.executeMemberTransition(memberId, plan),
|
|
338
354
|
log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
|
|
339
355
|
});
|
|
340
356
|
lifecycleTail = Promise.resolve();
|
|
341
357
|
accepting = true;
|
|
342
|
-
|
|
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;
|
|
343
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;
|
|
344
371
|
constructor(ctx) {
|
|
345
372
|
super(ctx, 'agentTeam');
|
|
346
373
|
this.pressurePolicy = new PressurePolicyCoordinator({
|
|
@@ -372,7 +399,7 @@ let AgentTeam = (() => {
|
|
|
372
399
|
if (kind !== undefined)
|
|
373
400
|
this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable ${kind} error; recording a consecutive error occurrence`);
|
|
374
401
|
this.recovery.onError(member.memberId, message);
|
|
375
|
-
this.
|
|
402
|
+
this.emitPresenceChanged(member.workspaceId);
|
|
376
403
|
});
|
|
377
404
|
this.ctx.on('agent/status', ({ agent, status }) => {
|
|
378
405
|
const member = this.memberForAgent(agent);
|
|
@@ -384,7 +411,7 @@ let AgentTeam = (() => {
|
|
|
384
411
|
const recovered = this.clearMemberFailure(member.memberId, 'runtime');
|
|
385
412
|
if (recovered)
|
|
386
413
|
this.notifiedInbox.delete(member.memberId);
|
|
387
|
-
this.
|
|
414
|
+
this.emitPresenceChanged(member.workspaceId);
|
|
388
415
|
// A rollover/recovery in flight delivers its own sequenced
|
|
389
416
|
// rederived Inbox after the handoff and carried input; a status-driven
|
|
390
417
|
// steer here would claim the handoff turn's next step and leapfrog
|
|
@@ -394,14 +421,14 @@ let AgentTeam = (() => {
|
|
|
394
421
|
}
|
|
395
422
|
// A turn that ends without an error closes any automatic recovery episode.
|
|
396
423
|
// The idle transition is itself presence-affecting (working → available),
|
|
397
|
-
// so it must wake
|
|
424
|
+
// so it must wake presence watchers exactly like the running transition
|
|
398
425
|
// above; without this wake, cached Client member rows keep showing the
|
|
399
426
|
// Member as working after every turn until an unrelated change arrives.
|
|
400
427
|
if (status === 'idle' && member !== undefined) {
|
|
401
428
|
if (this.memberFailures.get(member.memberId)?.runtime === undefined) {
|
|
402
429
|
this.recovery.onCleanTurnEnd(member.memberId);
|
|
403
430
|
}
|
|
404
|
-
this.
|
|
431
|
+
this.emitPresenceChanged(member.workspaceId);
|
|
405
432
|
}
|
|
406
433
|
});
|
|
407
434
|
// Progress nudges count every `tool/call` of each Member Session. The
|
|
@@ -431,6 +458,7 @@ let AgentTeam = (() => {
|
|
|
431
458
|
const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
|
|
432
459
|
this.ctx.effect(() => async () => {
|
|
433
460
|
this.accepting = false;
|
|
461
|
+
this.remediation = undefined;
|
|
434
462
|
this.recovery.dispose();
|
|
435
463
|
this.progressNudge.dispose();
|
|
436
464
|
this.contextManagement.dispose();
|
|
@@ -454,9 +482,21 @@ let AgentTeam = (() => {
|
|
|
454
482
|
if (initialization.committed)
|
|
455
483
|
this.emitCommitted(initialization.value);
|
|
456
484
|
this.startAttachmentGc(ledger);
|
|
485
|
+
// Legacy-artifact remediation runs before any Member activation: no write
|
|
486
|
+
// lease exists yet, so publishing sibling generations for refused Session
|
|
487
|
+
// logs cannot race a live writer. Remediation failure never blocks
|
|
488
|
+
// startup — the next start retries exactly what the cache does not cover.
|
|
489
|
+
try {
|
|
490
|
+
const remediation = new SessionRemediation(this.ctx, this.ctx.sessionPersistence, await SessionRemediation.open(this.ctx));
|
|
491
|
+
this.remediation = remediation;
|
|
492
|
+
await remediation.remediateEnabledMembers(ledger.listMembers());
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
this.ctx.logger.warn(`agent-team: legacy Session remediation did not run to completion (it will retry on the next start): ${error instanceof Error ? error.message : String(error)}`);
|
|
496
|
+
}
|
|
457
497
|
// One metadata listing serves every Member restore; per-member list calls
|
|
458
498
|
// would repeat the same I/O linearly during startup.
|
|
459
|
-
const persistedSessions = new Set((await this.persistedSessionHeaders()).map(
|
|
499
|
+
const persistedSessions = new Set((await this.persistedSessionHeaders()).map(snapshot => snapshot.header.id));
|
|
460
500
|
for (const member of ledger.listMembers()) {
|
|
461
501
|
if (member.state === 'enabled')
|
|
462
502
|
await this.activateMember(member, undefined, persistedSessions);
|
|
@@ -466,20 +506,14 @@ let AgentTeam = (() => {
|
|
|
466
506
|
}
|
|
467
507
|
/**
|
|
468
508
|
* Whether one Member Session has durable persisted content, decided through
|
|
469
|
-
* {@link
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
509
|
+
* {@link SessionPersistence.stat} rather than a bare metadata listing: the
|
|
510
|
+
* backend reports a still-draining session through its pending header, so a
|
|
511
|
+
* resume racing a suspend's fire-and-forget final flush cannot mistake a
|
|
512
|
+
* still-draining persisted Session for an unpersisted one and fork a fresh
|
|
513
|
+
* generation over it.
|
|
474
514
|
*/
|
|
475
515
|
async sessionPersisted(sessionId) {
|
|
476
|
-
|
|
477
|
-
await this.ctx.sessionPersistence.inspect(sessionId);
|
|
478
|
-
return true;
|
|
479
|
-
}
|
|
480
|
-
catch {
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
516
|
+
return this.sessionReader.exists(sessionId);
|
|
483
517
|
}
|
|
484
518
|
/** Resolve one exact live Agent to its durable Team Member; forks do not inherit identity. */
|
|
485
519
|
memberForAgent(agent) {
|
|
@@ -517,8 +551,9 @@ let AgentTeam = (() => {
|
|
|
517
551
|
if (!Number.isInteger(request.afterVersion) || request.afterVersion < 0)
|
|
518
552
|
throw new Error('afterVersion must be a non-negative integer');
|
|
519
553
|
const scope = this.validateChangeScope(request.scope);
|
|
520
|
-
|
|
521
|
-
|
|
554
|
+
const version = this.changeVersionOf(scope);
|
|
555
|
+
if (version > request.afterVersion || !this.accepting)
|
|
556
|
+
return Object.freeze({ version });
|
|
522
557
|
return new Promise((resolve, reject) => {
|
|
523
558
|
let settled = false;
|
|
524
559
|
const waiter = {
|
|
@@ -537,7 +572,7 @@ let AgentTeam = (() => {
|
|
|
537
572
|
return;
|
|
538
573
|
settled = true;
|
|
539
574
|
this.changeWaiters.delete(waiter);
|
|
540
|
-
resolve(Object.freeze({ version: this.
|
|
575
|
+
resolve(Object.freeze({ version: this.changeVersionOf(scope) }));
|
|
541
576
|
}, 25_000);
|
|
542
577
|
const onAbort = () => {
|
|
543
578
|
if (settled)
|
|
@@ -675,6 +710,23 @@ let AgentTeam = (() => {
|
|
|
675
710
|
if (member.state !== 'enabled')
|
|
676
711
|
throw new Error(`Agent Member '${member.handle}' is ${member.state}; only enabled Members can be restarted`);
|
|
677
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
|
+
}
|
|
678
730
|
await this.reactivateMember(request.memberId);
|
|
679
731
|
return Object.freeze({ status: this.memberStatus(member) });
|
|
680
732
|
}
|
|
@@ -1094,7 +1146,7 @@ let AgentTeam = (() => {
|
|
|
1094
1146
|
this.emitCommitted(result.value.receipt);
|
|
1095
1147
|
return result.value;
|
|
1096
1148
|
}
|
|
1097
|
-
/**
|
|
1149
|
+
/** Human Inbox projection; the Web Client consumes the direct-only slice as its mention queue. */
|
|
1098
1150
|
inbox(request) {
|
|
1099
1151
|
this.requireWorkspace(request.workspaceId);
|
|
1100
1152
|
return this.requireLedger().inbox(agentTeamHumanActor(), request);
|
|
@@ -1103,8 +1155,10 @@ let AgentTeam = (() => {
|
|
|
1103
1155
|
async readThread(request) {
|
|
1104
1156
|
const actor = this.humanCall(request.workspaceId);
|
|
1105
1157
|
const result = await this.requireLedger().readThread({ ...request, actor });
|
|
1106
|
-
|
|
1107
|
-
|
|
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);
|
|
1108
1162
|
return result.value;
|
|
1109
1163
|
}
|
|
1110
1164
|
/** Human-only durable Attention observations for one Thread. */
|
|
@@ -1163,8 +1217,10 @@ let AgentTeam = (() => {
|
|
|
1163
1217
|
async readThreadForAgent(agent, request) {
|
|
1164
1218
|
const actor = this.memberCall(agent, request.workspaceId);
|
|
1165
1219
|
const result = await this.requireLedger().readThread({ ...request, actor });
|
|
1166
|
-
|
|
1167
|
-
|
|
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);
|
|
1168
1224
|
const value = result.value;
|
|
1169
1225
|
// Private read-time enrich: an acceptance the reader just acknowledged is
|
|
1170
1226
|
// a natural Task boundary, so the Host prices the reader's context once,
|
|
@@ -1240,7 +1296,7 @@ let AgentTeam = (() => {
|
|
|
1240
1296
|
}
|
|
1241
1297
|
try {
|
|
1242
1298
|
const message = createUserMessage({
|
|
1243
|
-
content: [{ type: 'text', text: this.dmRelayText(agent, recipient, request.body.trim(), result.value.receipt.operationId) }],
|
|
1299
|
+
content: [{ type: 'text', text: this.dmRelayText(agent, recipient, request.body.trim(), result.value.receipt.occurredAt, result.value.receipt.operationId) }],
|
|
1244
1300
|
source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'relay' },
|
|
1245
1301
|
});
|
|
1246
1302
|
// An idle recipient gets one ordinary turn; a busy one is steered into
|
|
@@ -1256,10 +1312,10 @@ let AgentTeam = (() => {
|
|
|
1256
1312
|
return result.value;
|
|
1257
1313
|
}
|
|
1258
1314
|
/** Relay body: the DM itself plus one bounded line of adjacent context. */
|
|
1259
|
-
dmRelayText(senderAgent, recipient, body, excluding) {
|
|
1315
|
+
dmRelayText(senderAgent, recipient, body, occurredAt, excluding) {
|
|
1260
1316
|
const sender = this.memberForAgent(senderAgent);
|
|
1261
1317
|
const prior = this.requireLedger().dmHistoryBetween(senderAgent.id, recipient.memberId, excluding);
|
|
1262
|
-
const header = `Direct message from @${sender?.handle ?? 'a Team Member'}:`;
|
|
1318
|
+
const header = `Direct message from @${sender?.handle ?? 'a Team Member'} at ${formatTeamTimestamp(occurredAt)}:`;
|
|
1263
1319
|
const context = prior === undefined ? '' : `\n\n[most recent prior DM between you: ${prior}]`;
|
|
1264
1320
|
return `${header}\n\n${body}${context}`;
|
|
1265
1321
|
}
|
|
@@ -1281,6 +1337,16 @@ let AgentTeam = (() => {
|
|
|
1281
1337
|
validateLedger() {
|
|
1282
1338
|
this.requireLedger().validate();
|
|
1283
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
|
+
}
|
|
1284
1350
|
/**
|
|
1285
1351
|
* Effective context-pressure budget for one Member's current route:
|
|
1286
1352
|
* `hardLimit = min(256K, routeWindow - outputReserve)` and
|
|
@@ -1378,15 +1444,13 @@ let AgentTeam = (() => {
|
|
|
1378
1444
|
live = false;
|
|
1379
1445
|
}
|
|
1380
1446
|
else {
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
inheritedEventCount = inspection.inheritedEventCount;
|
|
1385
|
-
parentSession = inspection.meta.parentSession;
|
|
1386
|
-
}
|
|
1387
|
-
catch (error) {
|
|
1388
|
-
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);
|
|
1389
1450
|
}
|
|
1451
|
+
events = read.inspection.events;
|
|
1452
|
+
inheritedEventCount = read.inspection.inheritedEventCount;
|
|
1453
|
+
parentSession = read.inspection.header.parentSession;
|
|
1390
1454
|
}
|
|
1391
1455
|
// Fold the source with its inherited cut respected: inherited events
|
|
1392
1456
|
// are resolved history in that source, never fresh intent; checkpoints
|
|
@@ -1463,15 +1527,15 @@ let AgentTeam = (() => {
|
|
|
1463
1527
|
?? this.requireLedger().previousSessionForMember(member.memberId);
|
|
1464
1528
|
if (previousSessionId === undefined)
|
|
1465
1529
|
return;
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
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}`);
|
|
1473
1536
|
return;
|
|
1474
1537
|
}
|
|
1538
|
+
const inspection = previousRead.inspection;
|
|
1475
1539
|
const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, previousSessionId);
|
|
1476
1540
|
if (state.pending === null)
|
|
1477
1541
|
return;
|
|
@@ -1500,13 +1564,12 @@ let AgentTeam = (() => {
|
|
|
1500
1564
|
* downgrade to a blank child.
|
|
1501
1565
|
*/
|
|
1502
1566
|
async recordedCheckpointPrefix(seed) {
|
|
1503
|
-
|
|
1504
|
-
|
|
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}'`);
|
|
@@ -1530,21 +1593,56 @@ let AgentTeam = (() => {
|
|
|
1530
1593
|
* CURRENTLY present: delivered `user/message` ids plus the live inbox's
|
|
1531
1594
|
* pending next-step/next-turn ids — a historical insert that was already
|
|
1532
1595
|
* claimed (and removed) but never surfaced is NOT known and must replay.
|
|
1533
|
-
* Fail-closed
|
|
1534
|
-
*
|
|
1596
|
+
* Fail-closed for genuinely unreadable previous Sessions (missing/IO) so
|
|
1597
|
+
* the Member's input is never dropped silently, with three bounded
|
|
1598
|
+
* exceptions: a generation that already started its own turns needs no
|
|
1599
|
+
* replay (its carried input was delivered or superseded while it ran), a
|
|
1600
|
+
* log-corruption class error skips with a warning (the Host repairs torn
|
|
1601
|
+
* tails; a retired generation's corrupt log must not permanently block the
|
|
1602
|
+
* Member's activation), and a deterministic released-format refusal skips
|
|
1603
|
+
* with a warning (the candidate's own migration audit refuses that
|
|
1604
|
+
* artifact, so no retry can ever read it).
|
|
1535
1605
|
*/
|
|
1536
|
-
async replayCarriedInput(member, agent) {
|
|
1606
|
+
async replayCarriedInput(member, agent, generationStarted) {
|
|
1537
1607
|
const transition = this.requireLedger().lastTransitionForMember(member.memberId);
|
|
1538
1608
|
if (transition === undefined || transition.targetSessionId !== agent.session.id)
|
|
1539
1609
|
return 0;
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1610
|
+
// A generation that already started its own turns needs no previous-Session
|
|
1611
|
+
// replay: its carried input was delivered with the handoff (or superseded)
|
|
1612
|
+
// before any own turn could run. Skipping the inspect also keeps a later
|
|
1613
|
+
// corruption or loss of the retired Session from re-blocking this Member
|
|
1614
|
+
// on every restart of the current generation.
|
|
1615
|
+
if (generationStarted) {
|
|
1616
|
+
this.ctx.logger.info(`agent-team: skipping carried-input replay for member '${this.memberLabel(member.memberId)}': the current Session '${agent.session.id}' already started a generation`);
|
|
1617
|
+
return 0;
|
|
1543
1618
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1619
|
+
let inspectionEvents;
|
|
1620
|
+
let inspectionInherited;
|
|
1621
|
+
const previousRead = await this.sessionReader.read(transition.previousSessionId);
|
|
1622
|
+
if (!previousRead.ok) {
|
|
1623
|
+
const failure = previousRead.failure;
|
|
1624
|
+
// Log-corruption class: bounded fail-open. The current Session's own
|
|
1625
|
+
// fold is intact and the Host repairs the retired log's torn tail; a
|
|
1626
|
+
// corrupted retired generation must not permanently block activation.
|
|
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`);
|
|
1629
|
+
return 0;
|
|
1630
|
+
}
|
|
1631
|
+
// Deterministic released-format refusal: the retired artifact is refused
|
|
1632
|
+
// by the candidate's own migration audit, which no retry can change.
|
|
1633
|
+
// Failing activation here would turn a data problem in a Session the
|
|
1634
|
+
// Member no longer runs in into permanent unavailability — the exact
|
|
1635
|
+
// failure class this hardening removes. Genuine IO and unknown failures
|
|
1636
|
+
// stay fail-closed so carried input is never dropped silently.
|
|
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`);
|
|
1639
|
+
return 0;
|
|
1640
|
+
}
|
|
1641
|
+
throw new StoredSessionReadError(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable (${failure.kind}): ${failure.detail}`, failure);
|
|
1546
1642
|
}
|
|
1547
|
-
|
|
1643
|
+
inspectionEvents = previousRead.inspection.events;
|
|
1644
|
+
inspectionInherited = previousRead.inspection.inheritedEventCount;
|
|
1645
|
+
const state = foldContextProjection(inspectionEvents, inspectionInherited, transition.previousSessionId);
|
|
1548
1646
|
const carried = carriedInputOf(state);
|
|
1549
1647
|
if (carried.length === 0)
|
|
1550
1648
|
return 0;
|
|
@@ -1617,6 +1715,7 @@ let AgentTeam = (() => {
|
|
|
1617
1715
|
// persisted logs; the same projection definition folds every source.
|
|
1618
1716
|
const items = [];
|
|
1619
1717
|
const seen = new Set();
|
|
1718
|
+
let incompleteFrom;
|
|
1620
1719
|
let sessionId = member.sessionId;
|
|
1621
1720
|
let live = true;
|
|
1622
1721
|
let guard = 0;
|
|
@@ -1634,17 +1733,19 @@ let AgentTeam = (() => {
|
|
|
1634
1733
|
live = false;
|
|
1635
1734
|
}
|
|
1636
1735
|
else {
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
sessionId = inspection.meta.parentSession;
|
|
1643
|
-
}
|
|
1644
|
-
catch {
|
|
1645
|
-
// 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}` };
|
|
1646
1741
|
sessionId = undefined;
|
|
1647
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
|
+
}
|
|
1648
1749
|
}
|
|
1649
1750
|
if (state === undefined)
|
|
1650
1751
|
break;
|
|
@@ -1665,7 +1766,7 @@ let AgentTeam = (() => {
|
|
|
1665
1766
|
if (items.length >= limit)
|
|
1666
1767
|
break;
|
|
1667
1768
|
}
|
|
1668
|
-
return { usageTokens, hardLimit, handoffAt, items };
|
|
1769
|
+
return { usageTokens, hardLimit, handoffAt, items, ...(incompleteFrom === undefined ? {} : { incompleteFrom }) };
|
|
1669
1770
|
}
|
|
1670
1771
|
/**
|
|
1671
1772
|
* Replayed measurement of one lineage source: the live current Session
|
|
@@ -1681,31 +1782,16 @@ let AgentTeam = (() => {
|
|
|
1681
1782
|
return undefined;
|
|
1682
1783
|
if (live)
|
|
1683
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;
|
|
1684
1792
|
try {
|
|
1685
|
-
const
|
|
1686
|
-
|
|
1687
|
-
const borrowed = __addDisposableResource(env_1, await this.ctx.sessionPersistence.borrowSession(sessionId)
|
|
1688
|
-
// The borrow resolves the exact Session object for both shapes: the
|
|
1689
|
-
// live instance for an attached Session, the prepared one for an
|
|
1690
|
-
// archived ancestor. Measuring through the service context's store
|
|
1691
|
-
// would need an inject this service does not declare.
|
|
1692
|
-
, false);
|
|
1693
|
-
// The borrow resolves the exact Session object for both shapes: the
|
|
1694
|
-
// live instance for an attached Session, the prepared one for an
|
|
1695
|
-
// archived ancestor. Measuring through the service context's store
|
|
1696
|
-
// would need an inject this service does not declare.
|
|
1697
|
-
const session = borrowed.source === 'live'
|
|
1698
|
-
? agent.session.id === sessionId ? agent.session : undefined
|
|
1699
|
-
: borrowed.preparedSession;
|
|
1700
|
-
return session === undefined ? undefined : meter.measure(session)?.totalTokens;
|
|
1701
|
-
}
|
|
1702
|
-
catch (e_1) {
|
|
1703
|
-
env_1.error = e_1;
|
|
1704
|
-
env_1.hasError = true;
|
|
1705
|
-
}
|
|
1706
|
-
finally {
|
|
1707
|
-
__disposeResources(env_1);
|
|
1708
|
-
}
|
|
1793
|
+
const session = Session.create(sessionId, read.inspection.events, read.inspection.header, read.inspection.inheritedEventCount);
|
|
1794
|
+
return meter.measure(session)?.totalTokens;
|
|
1709
1795
|
}
|
|
1710
1796
|
catch {
|
|
1711
1797
|
return undefined;
|
|
@@ -2060,10 +2146,18 @@ let AgentTeam = (() => {
|
|
|
2060
2146
|
// allow-list filters the catalog by name through the live ref below.
|
|
2061
2147
|
// `swap` is bound by the provider at activation (no-op until then).
|
|
2062
2148
|
const skillSelection = { current: member.capabilities?.skills?.allow, swap: () => { } };
|
|
2063
|
-
const setup = async (agentCtx) => {
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2149
|
+
const setup = async (agentCtx, agent) => {
|
|
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
|
+
}
|
|
2067
2161
|
installModelSelection(agentCtx, selected);
|
|
2068
2162
|
// Admission gate for pending context rollovers: once a successful
|
|
2069
2163
|
// context_rollover result is durable, queued input must not open another
|
|
@@ -2107,9 +2201,6 @@ let AgentTeam = (() => {
|
|
|
2107
2201
|
});
|
|
2108
2202
|
return {
|
|
2109
2203
|
commit: () => {
|
|
2110
|
-
const agent = agentCtx.agent;
|
|
2111
|
-
if (agent === undefined)
|
|
2112
|
-
throw new Error('agent-team setup has no unpublished Agent');
|
|
2113
2204
|
// The member scope composes no sandbox-policy service (the preset
|
|
2114
2205
|
// owns no sandbox row), so read the last logged mode straight from
|
|
2115
2206
|
// the session log; `sandbox/mode` is log-only and never joins the
|
|
@@ -2179,6 +2270,11 @@ let AgentTeam = (() => {
|
|
|
2179
2270
|
// The one-shot pressure notice needs no explicit re-arm here: it latches
|
|
2180
2271
|
// on durable Session evidence, and a fresh generation's own event span
|
|
2181
2272
|
// starts empty — a new Session is itself the re-arm.
|
|
2273
|
+
// Snapshot whether this generation had already started its own turns
|
|
2274
|
+
// BEFORE any recovery delivery below: the handoff steer and the Inbox
|
|
2275
|
+
// wake legitimately append `turn/start` to this Session, and those must
|
|
2276
|
+
// never be mistaken for the generation having run on its own.
|
|
2277
|
+
const generationStarted = created.agent.session.ownEvents().some(event => event.type === 'turn/start');
|
|
2182
2278
|
// A restart between a durable rollover intent and its swap replays the
|
|
2183
2279
|
// old Session; the projection still carries the intent, so finish the
|
|
2184
2280
|
// transition (or keep waiting for the containing turn) from here.
|
|
@@ -2194,14 +2290,18 @@ let AgentTeam = (() => {
|
|
|
2194
2290
|
// delivered activates the new Session with no handoff in its own
|
|
2195
2291
|
// log — never treat that as an ordinary blank Member Session. The
|
|
2196
2292
|
// operation's recorded previous Session (the lineage parent) still
|
|
2197
|
-
// holds the intent; rebuild the handoff from it.
|
|
2198
|
-
|
|
2293
|
+
// holds the intent; rebuild the handoff from it. "No handoff in its
|
|
2294
|
+
// own log" is judged over both shapes: a generation rescued from the
|
|
2295
|
+
// retired custom kinds carries its handoff as a source the projection
|
|
2296
|
+
// does not classify, and rebuilding on top of it would inject the same
|
|
2297
|
+
// handoff twice.
|
|
2298
|
+
if (!handoffAlreadyInLog(state.boundaries, created.agent.session.ownEvents())) {
|
|
2199
2299
|
await this.reconstructMissingHandoff(member, created.agent);
|
|
2200
2300
|
}
|
|
2201
2301
|
// Carried input redelivery binds to the committed transition target —
|
|
2202
2302
|
// this Session — never to the handoff's presence: a crash after the
|
|
2203
2303
|
// handoff landed but before the carried enqueue still replays here.
|
|
2204
|
-
recoveryCarried = await this.replayCarriedInput(member, created.agent);
|
|
2304
|
+
recoveryCarried = await this.replayCarriedInput(member, created.agent, generationStarted);
|
|
2205
2305
|
}
|
|
2206
2306
|
else if (forkedFrom === undefined && lineageParent !== undefined) {
|
|
2207
2307
|
// A restart recreated a Session that never materialized before the
|
|
@@ -2215,7 +2315,7 @@ let AgentTeam = (() => {
|
|
|
2215
2315
|
// handoff right after — that delivery is the plan, never this
|
|
2216
2316
|
// recovery.
|
|
2217
2317
|
await this.reconstructMissingHandoff(member, created.agent);
|
|
2218
|
-
recoveryCarried = await this.replayCarriedInput(member, created.agent);
|
|
2318
|
+
recoveryCarried = await this.replayCarriedInput(member, created.agent, generationStarted);
|
|
2219
2319
|
}
|
|
2220
2320
|
// The ordinary Inbox wake runs LAST, after any recovery delivery above:
|
|
2221
2321
|
// the handoff must stay the first model-facing context of a recovered
|
|
@@ -2232,11 +2332,13 @@ let AgentTeam = (() => {
|
|
|
2232
2332
|
await created?.dispose();
|
|
2233
2333
|
this.modelSelections.delete(member.memberId);
|
|
2234
2334
|
this.memberRuntime.forgetMember(member.memberId);
|
|
2235
|
-
|
|
2335
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2336
|
+
this.ctx.logger.warn(`agent-team: activation failed for member '${this.memberLabel(member.memberId)}': ${message}`);
|
|
2337
|
+
this.setActivationDiagnostic(member.memberId, this.activationDiagnosticOf(error, member.sessionId));
|
|
2236
2338
|
}
|
|
2237
2339
|
finally {
|
|
2238
2340
|
// Activation only changes this Workspace's presence projection.
|
|
2239
|
-
this.
|
|
2341
|
+
this.emitPresenceChanged(member.workspaceId);
|
|
2240
2342
|
}
|
|
2241
2343
|
}
|
|
2242
2344
|
/**
|
|
@@ -2292,6 +2394,12 @@ let AgentTeam = (() => {
|
|
|
2292
2394
|
const scope = scopeOf(agentCtx);
|
|
2293
2395
|
const teamMessage = this.ctx.tools.get('team_message', scope);
|
|
2294
2396
|
if (teamMessage?.[AGENT_TEAM_PRESET_MARKER] !== true) {
|
|
2397
|
+
// `agentPresets.mount` already rejected an unscoped context, so a scope
|
|
2398
|
+
// key the harness sees but this bundle does not means the two sides
|
|
2399
|
+
// loaded different module instances of @deepseek-ai/dsh-scope (the
|
|
2400
|
+
// common trigger is running the CLI from source via tsx).
|
|
2401
|
+
if (scope === undefined)
|
|
2402
|
+
throw new Error(teamPresetScopeMismatchMessage(isTsxDevMode()));
|
|
2295
2403
|
throw new Error('selected preset is not team-enabled');
|
|
2296
2404
|
}
|
|
2297
2405
|
const available = new Set(this.ctx.tools.schemas(scope).map(tool => tool.name));
|
|
@@ -2318,13 +2426,13 @@ let AgentTeam = (() => {
|
|
|
2318
2426
|
// new binding as active — a Client following the row would otherwise open
|
|
2319
2427
|
// a Session that does not exist yet.
|
|
2320
2428
|
if (handle.agent.id !== member.sessionId)
|
|
2321
|
-
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' } });
|
|
2322
2430
|
if (this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
|
|
2323
|
-
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 } });
|
|
2324
2432
|
}
|
|
2325
2433
|
const runtimeError = failures?.runtime ?? failures?.compaction;
|
|
2326
2434
|
if (runtimeError !== undefined)
|
|
2327
|
-
return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: runtimeError });
|
|
2435
|
+
return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: { class: 'runtime', detail: runtimeError } });
|
|
2328
2436
|
// Capability warnings are runtime-derived at activation (handles-scoped,
|
|
2329
2437
|
// like failures): absent while capabilities resolve cleanly.
|
|
2330
2438
|
const capabilityWarnings = this.memberRuntime.capabilityWarningsFor(member.memberId);
|
|
@@ -2338,6 +2446,37 @@ let AgentTeam = (() => {
|
|
|
2338
2446
|
failures[slot] = message;
|
|
2339
2447
|
this.memberFailures.set(memberId, failures);
|
|
2340
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
|
+
}
|
|
2341
2480
|
clearMemberFailure(memberId, slot) {
|
|
2342
2481
|
const failures = this.memberFailures.get(memberId);
|
|
2343
2482
|
if (failures === undefined || failures[slot] === undefined)
|
|
@@ -2412,7 +2551,17 @@ let AgentTeam = (() => {
|
|
|
2412
2551
|
}
|
|
2413
2552
|
emitAutoCompactionChanged(memberId) {
|
|
2414
2553
|
const workspaceId = this.ledger?.getMember(memberId)?.workspaceId;
|
|
2415
|
-
|
|
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 }]);
|
|
2416
2565
|
}
|
|
2417
2566
|
/** Wake from durable unread state with bounded facts for direct and state-changing work. */
|
|
2418
2567
|
notifyMember(agent, sequenced = false) {
|
|
@@ -2503,7 +2652,7 @@ let AgentTeam = (() => {
|
|
|
2503
2652
|
if (direct && fact.kind === 'message') {
|
|
2504
2653
|
const sender = fact.message.sender === AGENT_TEAM_HUMAN_MEMBER_ID
|
|
2505
2654
|
? 'human' : this.requireLedger().getMember(fact.message.sender)?.handle ?? fact.message.sender;
|
|
2506
|
-
const detail = ['Direct Team mention', `From: ${sender}`, `Channel: ${item.channelRef}`,
|
|
2655
|
+
const detail = ['Direct Team mention', `Occurred at: ${formatTeamTimestamp(fact.occurredAt)}`, `From: ${sender}`, `Channel: ${item.channelRef}`,
|
|
2507
2656
|
...(item.task === undefined ? [] : [`Task: ${item.task.taskRef}`]),
|
|
2508
2657
|
`Thread: ${item.thread.threadRef}`, `Message ref: ${fact.message.messageRef}`,
|
|
2509
2658
|
`Message: ${this.boundedNotificationBody(fact.message.body)}`].join('\n');
|
|
@@ -2511,15 +2660,15 @@ let AgentTeam = (() => {
|
|
|
2511
2660
|
detailedFactCount += 1;
|
|
2512
2661
|
}
|
|
2513
2662
|
else if (fact.kind === 'activity') {
|
|
2514
|
-
const detail = `${this.activityNotification(fact.activity, readerId)}\nThread: ${item.thread.threadRef}`;
|
|
2663
|
+
const detail = `${this.activityNotification(fact.activity, readerId)}\nOccurred at: ${formatTeamTimestamp(fact.occurredAt)}\nThread: ${item.thread.threadRef}`;
|
|
2515
2664
|
if (append(detail))
|
|
2516
2665
|
detailedFactCount += 1;
|
|
2517
2666
|
}
|
|
2518
2667
|
}
|
|
2519
|
-
const
|
|
2520
|
-
if (
|
|
2668
|
+
const ordinary = facts.filter(entry => entry.fact.kind === 'message' && !entry.direct);
|
|
2669
|
+
if (ordinary.length > 0) {
|
|
2521
2670
|
const route = item.task === undefined ? `Thread ${item.thread.threadRef}` : `Task ${item.task.taskRef}`;
|
|
2522
|
-
append(`${route}: ${
|
|
2671
|
+
append(`${route}: ${ordinary.length} unread update${ordinary.length === 1 ? '' : 's'} · newest at ${formatTeamTimestamp(ordinary.at(-1).fact.occurredAt)}.`);
|
|
2523
2672
|
}
|
|
2524
2673
|
}
|
|
2525
2674
|
if (omitted)
|
|
@@ -2596,21 +2745,45 @@ let AgentTeam = (() => {
|
|
|
2596
2745
|
* Wake waiters for one committed or lifecycle change. Undefined broadcasts
|
|
2597
2746
|
* to everyone; an empty scope list invalidates nobody because no shared
|
|
2598
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.
|
|
2599
2755
|
*/
|
|
2600
2756
|
emitChanged(scopes) {
|
|
2601
|
-
|
|
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;
|
|
2602
2763
|
for (const waiter of this.changeWaiters) {
|
|
2603
2764
|
const waiterScope = waiter.scope;
|
|
2604
|
-
if (scopes !== undefined && (waiterScope === undefined ?
|
|
2765
|
+
if (scopes !== undefined && (waiterScope === undefined ? !touchesProjection : !scopes.some(scope => sameChangeScope(scope, waiterScope))))
|
|
2605
2766
|
continue;
|
|
2606
2767
|
this.changeWaiters.delete(waiter);
|
|
2607
|
-
waiter.wake(this.
|
|
2768
|
+
waiter.wake(this.changeVersionOf(waiterScope));
|
|
2608
2769
|
}
|
|
2609
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
|
+
}
|
|
2610
2783
|
validateChangeScope(scope) {
|
|
2611
2784
|
if (scope === undefined)
|
|
2612
2785
|
return undefined;
|
|
2613
|
-
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;
|
|
2614
2787
|
if (typeof ref !== 'string' || ref.length === 0)
|
|
2615
2788
|
throw new Error(`change scope of kind '${scope.kind}' requires a non-empty ref`);
|
|
2616
2789
|
return scope;
|