@sema-agent/core 5.47.0 → 5.49.0
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/CHANGELOG.md +115 -0
- package/dist/agents/agent-transcript-tool.d.ts +4 -0
- package/dist/agents/agent-transcript-tool.js +10 -3
- package/dist/agents/send-message-tool.d.ts +43 -1
- package/dist/agents/send-message-tool.js +50 -11
- package/dist/agents/subagent.d.ts +18 -0
- package/dist/agents/subagent.js +102 -2
- package/dist/agents/teacher.d.ts +25 -1
- package/dist/agents/teacher.js +85 -12
- package/dist/config/defaults.d.ts +20 -0
- package/dist/config/defaults.js +5 -0
- package/dist/core/background-agent-store.d.ts +1 -0
- package/dist/core/background-agent-store.js +14 -0
- package/dist/core/mcp.d.ts +6 -1
- package/dist/core/mcp.js +34 -7
- package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
- package/dist/core/memory-engine/delegation-settlement.js +31 -4
- package/dist/core/memory-engine/dual-root.js +11 -0
- package/dist/core/memory-engine/engine.d.ts +6 -1
- package/dist/core/memory-engine/engine.js +136 -21
- package/dist/core/memory-engine/memory-backend-contract.js +33 -0
- package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
- package/dist/core/memory-engine/origin-clearance.js +10 -0
- package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
- package/dist/core/memory-engine/provenance-wording.js +1 -0
- package/dist/core/memory-engine/tools.js +6 -4
- package/dist/core/reminder-disclosure.d.ts +90 -0
- package/dist/core/reminder-disclosure.js +64 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-task.d.ts +15 -0
- package/dist/core/runner/prepare-task.js +51 -33
- package/dist/core/runner/runtask.d.ts +26 -1
- package/dist/core/runner/runtask.js +21 -3
- package/dist/core/session-store.d.ts +59 -1
- package/dist/core/session-store.js +82 -14
- package/dist/core/session.d.ts +83 -1
- package/dist/core/strategy-store.d.ts +180 -3
- package/dist/core/strategy-store.js +172 -23
- package/dist/core/task-registry-agent.d.ts +28 -0
- package/dist/core/task-registry-agent.js +63 -2
- package/dist/core/task-registry.d.ts +21 -0
- package/dist/core/task-registry.js +4 -1
- package/dist/core/types.d.ts +66 -0
- package/dist/core/untrusted-text.d.ts +63 -0
- package/dist/core/untrusted-text.js +48 -0
- package/dist/core/wiring-manifest.d.ts +35 -0
- package/dist/core/wiring-manifest.js +21 -1
- package/dist/engine/harness/types.d.ts +36 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/internal/harness-types.d.ts +1 -0
- package/dist/stores/file/file-snapshot-store.js +7 -1
- package/dist/stores/file/index.d.ts +27 -3
- package/dist/stores/file/index.js +36 -1
- package/dist/stores/file/session-policy-store.d.ts +0 -13
- package/dist/stores/file/session-policy-store.js +7 -1
- package/dist/stores/file/session-store.d.ts +22 -5
- package/dist/stores/file/session-store.js +80 -13
- package/dist/stores/file/strategy-store.d.ts +97 -0
- package/dist/stores/file/strategy-store.js +340 -0
- package/dist/tools/fs/fs-pdf.d.ts +12 -1
- package/dist/tools/fs/fs-pdf.js +17 -3
- package/dist/tools/fs/fs-read.d.ts +2 -1
- package/dist/tools/fs/fs-read.js +33 -5
- package/dist/tools/fs/fs-shared.d.ts +6 -2
- package/dist/tools/fs/index.d.ts +7 -0
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +21 -2
- package/package.json +3 -2
- package/test/export-surface.snapshot.json +22 -1
package/dist/agents/teacher.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_PROBLEM_BYTES, MAX_STRATEGY_TEXT_BYTES } from "../core/strategy-store.js";
|
|
3
|
+
import { createSafeNotifier, observeThenableRejection } from "../core/safe-notify.js";
|
|
2
4
|
import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
|
|
3
5
|
import { isDefineToolProduct, stampDefineToolBrand } from "../core/tools.js";
|
|
4
6
|
import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
|
|
@@ -46,31 +48,102 @@ export function parseTeacherAdvice(text) {
|
|
|
46
48
|
export async function runWithTeacher(runner, studentSpec, teacher = {}) {
|
|
47
49
|
const store = teacher.strategyStore;
|
|
48
50
|
const scope = teacher.scope;
|
|
51
|
+
const incidentNotifier = createSafeNotifier();
|
|
52
|
+
const warnedIncidentOps = new Set();
|
|
53
|
+
const discloseStoreIncident = (i) => {
|
|
54
|
+
const seat = teacher.onStrategyStoreIncident;
|
|
55
|
+
if (seat) {
|
|
56
|
+
const site = `teacher.onStrategyStoreIncident.${i.op}`;
|
|
57
|
+
incidentNotifier.notify(() => observeThenableRejection(seat(i), incidentNotifier, site), site);
|
|
58
|
+
}
|
|
59
|
+
else if (!warnedIncidentOps.has(i.op)) {
|
|
60
|
+
warnedIncidentOps.add(i.op);
|
|
61
|
+
console.warn(`runWithTeacher: strategy store ${i.op} failed (contained — the run continues): ${i.error}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const isConfigCoded = (e) => typeof e?.code === "string" && (e.code).startsWith("config.");
|
|
49
65
|
let spec = studentSpec;
|
|
66
|
+
let injected;
|
|
50
67
|
if (store && scope && teacher.injectStrategies !== false) {
|
|
51
|
-
|
|
68
|
+
let found = [];
|
|
69
|
+
try {
|
|
70
|
+
const raw = await store.find(scope, studentSpec.objective, teacher.retrieveK ?? 3);
|
|
71
|
+
if (!Array.isArray(raw)) {
|
|
72
|
+
discloseStoreIncident({ op: "find", error: "store.find returned a non-array result — injecting nothing" });
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
let dropped = 0;
|
|
76
|
+
for (const e of raw) {
|
|
77
|
+
const c = e;
|
|
78
|
+
const id = c?.id;
|
|
79
|
+
const ts = c?.ts;
|
|
80
|
+
const strategy = c?.strategy;
|
|
81
|
+
if (typeof id !== "string" || typeof ts !== "string" || typeof strategy !== "string") {
|
|
82
|
+
dropped++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const origin = c?.origin;
|
|
86
|
+
found.push({ id, ts, strategy, ...(origin === "earned" || origin === "seeded" ? { origin } : {}) });
|
|
87
|
+
}
|
|
88
|
+
if (dropped > 0) {
|
|
89
|
+
discloseStoreIncident({ op: "find", error: `${dropped} malformed retrieval entries dropped (id/ts/strategy must be strings)` });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (isConfigCoded(e))
|
|
95
|
+
throw e;
|
|
96
|
+
discloseStoreIncident({ op: "find", error: String(e?.message ?? e) });
|
|
97
|
+
found = [];
|
|
98
|
+
}
|
|
99
|
+
let budget = 0;
|
|
100
|
+
for (let i = 0; i < found.length; i++) {
|
|
101
|
+
budget += Buffer.byteLength(found[i].strategy, "utf8");
|
|
102
|
+
if (budget > MAX_STRATEGY_INJECTION_TOTAL_BYTES) {
|
|
103
|
+
discloseStoreIncident({
|
|
104
|
+
op: "find",
|
|
105
|
+
error: `injection truncated to ${i} of ${found.length} retrieved strategies — combined text exceeded ${MAX_STRATEGY_INJECTION_TOTAL_BYTES} bytes`,
|
|
106
|
+
});
|
|
107
|
+
found = found.slice(0, i);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
52
111
|
if (found.length > 0) {
|
|
53
112
|
const block = `[STRATEGIES from PAST, DIFFERENT tasks — they may NOT apply. If unsure, IGNORE them and proceed normally.]\n` +
|
|
54
|
-
found.map((s) => `- ${s.strategy}`).join("\n");
|
|
113
|
+
delimitUntrusted("PAST STRATEGIES (from DIFFERENT tasks; may NOT apply — verify before use)", found.map((s) => `- ${s.strategy}`).join("\n"));
|
|
55
114
|
spec = { ...studentSpec, objective: `${block}\n\n${studentSpec.objective}` };
|
|
115
|
+
injected = found.map((s) => ({ id: s.id, ts: s.ts, ...(s.origin !== undefined ? { origin: s.origin } : {}) }));
|
|
56
116
|
}
|
|
57
117
|
}
|
|
58
118
|
const result = await runTeacherCore(runner, spec, teacher);
|
|
59
119
|
if (store && scope && result.status === "completed" && result.escalations.length > 0) {
|
|
60
120
|
const advice = result.escalations[result.escalations.length - 1].teacher;
|
|
61
121
|
if (advice.strategy && advice.confidence >= (teacher.minConfidenceToStore ?? 2)) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
122
|
+
try {
|
|
123
|
+
const clip = (text, maxBytes) => {
|
|
124
|
+
let out = text;
|
|
125
|
+
while (Buffer.byteLength(out) > maxBytes)
|
|
126
|
+
out = out.slice(0, -1);
|
|
127
|
+
return out;
|
|
128
|
+
};
|
|
129
|
+
await store.save({
|
|
130
|
+
id: randomUUID(),
|
|
131
|
+
problem: clip(studentSpec.objective, MAX_STRATEGY_PROBLEM_BYTES),
|
|
132
|
+
strategy: clip(advice.strategy, MAX_STRATEGY_TEXT_BYTES),
|
|
133
|
+
confidence: Math.min(3, Math.max(0, advice.confidence)),
|
|
134
|
+
scope,
|
|
135
|
+
ts: new Date().toISOString(),
|
|
136
|
+
teacherModel: typeof teacher.model === "string" ? teacher.model : teacher.model?.id,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
if (isConfigCoded(e))
|
|
141
|
+
throw e;
|
|
142
|
+
discloseStoreIncident({ op: "save", error: String(e?.message ?? e) });
|
|
143
|
+
}
|
|
71
144
|
}
|
|
72
145
|
}
|
|
73
|
-
return result;
|
|
146
|
+
return injected !== undefined && injected.length > 0 ? { ...result, strategiesInjected: injected } : result;
|
|
74
147
|
}
|
|
75
148
|
async function runTeacherCore(runner, studentSpec, teacher) {
|
|
76
149
|
const maxEscalations = teacher.maxEscalations ?? 3;
|
|
@@ -21,6 +21,26 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export declare const RETAIN_DEFAULT_TTL_MS: number;
|
|
23
23
|
export declare const RETAIN_DEFAULT_MAX = 16;
|
|
24
|
+
/** Subagent transcript persistence — the delegation ENTRY caps (CC values: 20 concurrent /
|
|
25
|
+
* 200 per session tree). The INLET bound family (`RunnerDeps.delegationEntryCaps`), as opposed to
|
|
26
|
+
* the retain pair above, which is a pure in-memory fast-lane handle bound (outlet — armed
|
|
27
|
+
* deployments lose nothing when it evicts). Concurrent = running/pending a* handles under one
|
|
28
|
+
* (scope, rootSessionId) key in THIS process's registry; cumulative = retained-window count under
|
|
29
|
+
* the same key (see the enforcement site for the exact retained-window semantics). */
|
|
30
|
+
export declare const DELEGATION_MAX_CONCURRENT_DEFAULT = 20;
|
|
31
|
+
export declare const DELEGATION_MAX_PER_SESSION_DEFAULT = 200;
|
|
32
|
+
/** Subagent transcript persistence — orphan adoption (CC parity: after a restart a deployment
|
|
33
|
+
* auto-adopts at most this many stale rows whose transcript mtime is inside the window; older ones
|
|
34
|
+
* stay MANUALLY continuable for the whole retention period — never deleted by the window). The
|
|
35
|
+
* TRIGGER is deployment-owned (core has no daemon and never revives runs nobody asked for); these
|
|
36
|
+
* constants are the shared vocabulary so every deployment adopts by the same numbers. */
|
|
37
|
+
export declare const ORPHAN_ADOPT_WINDOW_MS_DEFAULT: number;
|
|
38
|
+
export declare const ORPHAN_ADOPT_MAX_DEFAULT = 20;
|
|
39
|
+
/** Subagent transcript persistence — the default transcript retention period (CC
|
|
40
|
+
* cleanupPeriodDays parity). Core ships NO sweeper (design/151 ruling 2): the blessed path is the
|
|
41
|
+
* deployment calling `TaskRegistry.reapDurableAgents` with `maxAgeMs` derived from this (boot +
|
|
42
|
+
* every 24h is the reference cadence). */
|
|
43
|
+
export declare const SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT = 30;
|
|
24
44
|
/** Default idle TTL (days) before a cached session is evicted (config-catalog `session.idleTtlDays`). */
|
|
25
45
|
export declare const SESSION_DEFAULT_TTL_DAYS = 7;
|
|
26
46
|
/**
|
package/dist/config/defaults.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export const RETAIN_DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
2
2
|
export const RETAIN_DEFAULT_MAX = 16;
|
|
3
|
+
export const DELEGATION_MAX_CONCURRENT_DEFAULT = 20;
|
|
4
|
+
export const DELEGATION_MAX_PER_SESSION_DEFAULT = 200;
|
|
5
|
+
export const ORPHAN_ADOPT_WINDOW_MS_DEFAULT = 48 * 60 * 60 * 1000;
|
|
6
|
+
export const ORPHAN_ADOPT_MAX_DEFAULT = 20;
|
|
7
|
+
export const SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT = 30;
|
|
3
8
|
export const SESSION_DEFAULT_TTL_DAYS = 7;
|
|
4
9
|
export const RUNNING_AGENT_OBSERVE_EVERY_BEATS = 4;
|
|
@@ -170,6 +170,7 @@ export interface BackgroundAgentRecord {
|
|
|
170
170
|
export declare const REVIVED_ROW_CLEARED_FIELDS: readonly ["settledAt", "stoppedBy", "completionId", "finalOutput", "finalOutputFull", "error", "errorCode", "errorRetryable", "errorKind", "errorRetryAfterMs", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"];
|
|
171
171
|
/** Erase {@link REVIVED_ROW_CLEARED_FIELDS} from a record a revival is about to write back. */
|
|
172
172
|
export declare function clearRevivedRowTerminalPayload(record: BackgroundAgentRecord): void;
|
|
173
|
+
export declare function announceTranscriptIntegrityGapOnce(handle: string, scope: string | undefined, sink: ((handle: string, scope: string | undefined) => void) | undefined): void;
|
|
173
174
|
/** Content-free projection for list reads (design/151 HIGH-1: `summary`/`finalOutput`/`recentSteps`
|
|
174
175
|
* and friends NEVER ride a list — content is get-by-handle only, behind the full predicate). */
|
|
175
176
|
export interface BackgroundAgentRowSummary {
|
|
@@ -21,6 +21,20 @@ export function clearRevivedRowTerminalPayload(record) {
|
|
|
21
21
|
for (const field of REVIVED_ROW_CLEARED_FIELDS)
|
|
22
22
|
delete record[field];
|
|
23
23
|
}
|
|
24
|
+
const transcriptIntegrityAnnounced = new Set();
|
|
25
|
+
export function announceTranscriptIntegrityGapOnce(handle, scope, sink) {
|
|
26
|
+
if (sink === undefined)
|
|
27
|
+
return;
|
|
28
|
+
const key = `${scope ?? ""}\u0000${handle}`;
|
|
29
|
+
if (transcriptIntegrityAnnounced.has(key))
|
|
30
|
+
return;
|
|
31
|
+
transcriptIntegrityAnnounced.add(key);
|
|
32
|
+
try {
|
|
33
|
+
sink(handle, scope);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
24
38
|
export class BackgroundAgentStoreError extends Error {
|
|
25
39
|
code;
|
|
26
40
|
constructor(code, message) {
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ import type { AgentTool } from "../internal/harness-types.js";
|
|
|
36
36
|
import type { ImageContent, TextContent } from "../internal/llm.js";
|
|
37
37
|
import { type McpImageResizer } from "./image-downsample.js";
|
|
38
38
|
import type { McpServerSpec, OnElicit, ToolEffect } from "./types.js";
|
|
39
|
+
import { type ReminderDisclosureCounts } from "./reminder-disclosure.js";
|
|
39
40
|
/**
|
|
40
41
|
* The safety axes (design/77 §4 irreversibility, design/70 egress) derived from one materialized MCP
|
|
41
42
|
* tool's server-advertised `annotations`. These ride alongside the {@link AgentTool} (which is vendored
|
|
@@ -462,7 +463,11 @@ export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormal
|
|
|
462
463
|
* combinator's own structure is legal JSON Schema ⇒ passes validateJsonSchemaShape too).
|
|
463
464
|
*/
|
|
464
465
|
export declare function mcpToolSchemaProblem(schema: unknown): string | undefined;
|
|
465
|
-
export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer
|
|
466
|
+
export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer, // design/116 CONFIRM-1 seam: deployment-injected; default = auto-detected sharp
|
|
467
|
+
reminderDisclosure?: {
|
|
468
|
+
reminderMark?: string;
|
|
469
|
+
counts?: ReminderDisclosureCounts;
|
|
470
|
+
}): Promise<MaterializedMcp>;
|
|
466
471
|
/**
|
|
467
472
|
* Fold the caller's AUTHORITATIVE per-tool override (design F: caller = trust root) over the server-hint axis.
|
|
468
473
|
* Unlike server hints, the caller may RAISE or LOWER any axis: `effect` sets the repeat-safety class (lower to
|
package/dist/core/mcp.js
CHANGED
|
@@ -11,6 +11,7 @@ import { MCP_IMAGE_MAX_BASE64, sharpImageResizer } from "./image-downsample.js";
|
|
|
11
11
|
import { sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js";
|
|
12
12
|
import { truncateError } from "./tool-errors.js";
|
|
13
13
|
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
14
|
+
import { discloseReminderShaped } from "./reminder-disclosure.js";
|
|
14
15
|
import { withContentOrigin } from "./memory-engine/content-origin.js";
|
|
15
16
|
import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
|
|
16
17
|
export const MCP_PREFIX = MCP_NAMESPACE.prefix;
|
|
@@ -649,7 +650,10 @@ export function mcpToolSchemaProblem(schema) {
|
|
|
649
650
|
}
|
|
650
651
|
return undefined;
|
|
651
652
|
}
|
|
652
|
-
export async function materializeMcpTools(specs, principal, onElicit, imageResizer) {
|
|
653
|
+
export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure) {
|
|
654
|
+
const mcpDisclosure = reminderDisclosure?.reminderMark !== undefined
|
|
655
|
+
? { mark: reminderDisclosure.reminderMark, windows: new Map(), ...(reminderDisclosure.counts !== undefined ? { counts: reminderDisclosure.counts } : {}) }
|
|
656
|
+
: undefined;
|
|
653
657
|
const clients = [];
|
|
654
658
|
const tools = [];
|
|
655
659
|
const toolAxes = [];
|
|
@@ -661,7 +665,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
661
665
|
const droppedTools = [];
|
|
662
666
|
let disposing = false;
|
|
663
667
|
const serverHandles = [];
|
|
664
|
-
const settled = await Promise.allSettled(specs.map((spec) => connectServer(spec, principal, onElicit, imageResizer)));
|
|
668
|
+
const settled = await Promise.allSettled(specs.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure)));
|
|
665
669
|
try {
|
|
666
670
|
for (let i = 0; i < specs.length; i++) {
|
|
667
671
|
const r = settled[i];
|
|
@@ -728,7 +732,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
728
732
|
try {
|
|
729
733
|
const listed = await listToolsLenient(h.client);
|
|
730
734
|
cacheMcpToolMetadata(h.client, listed.tools);
|
|
731
|
-
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, h.spec, h.client, h.health, imageResizer);
|
|
735
|
+
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, h.spec, h.client, h.health, imageResizer, mcpDisclosure);
|
|
732
736
|
const newNames = serverTools.map((t) => t.name);
|
|
733
737
|
const added = newNames.filter((n) => !h.toolNames.includes(n));
|
|
734
738
|
const removed = h.toolNames.filter((n) => !newNames.includes(n));
|
|
@@ -1119,7 +1123,7 @@ const LENIENT_CALL_TOOL_RESULT_SCHEMA = { safeParse: parseCallToolResultLenient
|
|
|
1119
1123
|
async function listToolsLenient(client, options) {
|
|
1120
1124
|
return client.request({ method: "tools/list", params: {} }, LenientListToolsResultSchema, options);
|
|
1121
1125
|
}
|
|
1122
|
-
async function connectServer(spec, principal, onElicit, imageResizer) {
|
|
1126
|
+
async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure) {
|
|
1123
1127
|
const elicitOn = spec.elicitation === true && onElicit !== undefined;
|
|
1124
1128
|
const health = { dead: false, pendingElicitations: 0, lastElicitationClosedAt: 0 };
|
|
1125
1129
|
const client = new Client({ name: `sema-core/${spec.name}`, version: "0.1.0" }, { capabilities: elicitOn ? { elicitation: { form: {} } } : {} });
|
|
@@ -1155,7 +1159,7 @@ async function connectServer(spec, principal, onElicit, imageResizer) {
|
|
|
1155
1159
|
};
|
|
1156
1160
|
const listed = await listToolsLenient(client, startupOpts);
|
|
1157
1161
|
cacheMcpToolMetadata(client, listed.tools);
|
|
1158
|
-
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer);
|
|
1162
|
+
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure);
|
|
1159
1163
|
const caps = client.getServerCapabilities();
|
|
1160
1164
|
const resourceInfo = caps?.resources
|
|
1161
1165
|
? {
|
|
@@ -1188,7 +1192,7 @@ async function connectServer(spec, principal, onElicit, imageResizer) {
|
|
|
1188
1192
|
throw err;
|
|
1189
1193
|
}
|
|
1190
1194
|
}
|
|
1191
|
-
function intakeListedTools(listed, spec, client, health, imageResizer) {
|
|
1195
|
+
function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure) {
|
|
1192
1196
|
const serverTools = [];
|
|
1193
1197
|
const serverAxes = [];
|
|
1194
1198
|
const dropped = [];
|
|
@@ -1276,8 +1280,31 @@ function intakeListedTools(listed, spec, client, health, imageResizer) {
|
|
|
1276
1280
|
type: "text",
|
|
1277
1281
|
text: `[structuredContent] JSON with schema: ${inferCompactSchema(sc)}\n${truncateError(JSON.stringify(sc))}`,
|
|
1278
1282
|
});
|
|
1279
|
-
return { content, details: { type: "mcp", structuredContent: sc }, terminate: false };
|
|
1280
1283
|
}
|
|
1284
|
+
if (reminderDisclosure !== undefined) {
|
|
1285
|
+
const textIdx = [];
|
|
1286
|
+
const segments = [];
|
|
1287
|
+
content.forEach((b, i) => {
|
|
1288
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
1289
|
+
textIdx.push(i);
|
|
1290
|
+
segments.push(b.text);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
const d = discloseReminderShaped({
|
|
1294
|
+
segments,
|
|
1295
|
+
mark: reminderDisclosure.mark,
|
|
1296
|
+
outlet: "mcp",
|
|
1297
|
+
defuseExactMark: true,
|
|
1298
|
+
throttle: { key: `${spec.name}:${remoteName}`, windows: reminderDisclosure.windows },
|
|
1299
|
+
...(reminderDisclosure.counts !== undefined ? { counts: reminderDisclosure.counts } : {}),
|
|
1300
|
+
});
|
|
1301
|
+
if (d.defused)
|
|
1302
|
+
textIdx.forEach((ci, si) => (content[ci] = { type: "text", text: d.segments[si] }));
|
|
1303
|
+
if (d.trailer !== undefined)
|
|
1304
|
+
content.push({ type: "text", text: d.trailer });
|
|
1305
|
+
}
|
|
1306
|
+
if (sc !== undefined)
|
|
1307
|
+
return { content, details: { type: "mcp", structuredContent: sc }, terminate: false };
|
|
1281
1308
|
return { content, details: res, terminate: false };
|
|
1282
1309
|
},
|
|
1283
1310
|
});
|
|
@@ -221,6 +221,11 @@ export interface HoldRow {
|
|
|
221
221
|
};
|
|
222
222
|
/** Host valve verdict awaiting the next harvest ("release" commits with cause "static"). */
|
|
223
223
|
resolved?: "release" | "discard";
|
|
224
|
+
/** The verdict's GENERATION (strictly increases on every re-resolution): the settle leg's
|
|
225
|
+
* consumed-verdict exit clears only the exact generation it read, so a host re-resolution
|
|
226
|
+
* racing a failing release attempt is never erased by the stale attempt's cleanup. Absent on
|
|
227
|
+
* rows resolved before the member existed (a generation-less clear matches those). */
|
|
228
|
+
resolvedAt?: number;
|
|
224
229
|
}
|
|
225
230
|
export declare function readHolds(controlDir: string): HoldRow[];
|
|
226
231
|
/** ATOMIC no-replace restore of a staged file onto a plane path (adversarial round 2: an
|
|
@@ -308,6 +313,28 @@ export declare function resolveHoldRecord(controlDir: string, input: {
|
|
|
308
313
|
requestId: string;
|
|
309
314
|
now: () => number;
|
|
310
315
|
}): void;
|
|
316
|
+
/**
|
|
317
|
+
* Consume a standing valve verdict WITHOUT any other transition — the settle leg's exit for a
|
|
318
|
+
* valve release that FAILED on an already-disposed row (an expired hold whose release hit a
|
|
319
|
+
* judgment refusal: lost custody, re-scan refusal, CAS conflict). `disposeHold` is a no-op there
|
|
320
|
+
* (the row is already terminal), so leaving `resolved` standing would re-run the identical failing
|
|
321
|
+
* release — and re-announce a disposal that never happened — on every subsequent harvest, forever.
|
|
322
|
+
* Clearing it returns the row to its plain disposed/expired state: the failure is disclosed by the
|
|
323
|
+
* caller, custody/quarantine bytes stay addressable, and `resolveHold(holdId, "release")` remains
|
|
324
|
+
* admissible after the host reconciles. GENERATION-KEYED: only the exact verdict generation the
|
|
325
|
+
* caller read is cleared — a host re-resolution that landed while the failing attempt was in
|
|
326
|
+
* flight is a NEWER generation and stands (the stale attempt's cleanup must not erase a live host
|
|
327
|
+
* action). A generation-less verdict (a row written before the member existed) is consumable
|
|
328
|
+
* against a generation-less read — every CURRENT writer mints a generation, so the only verdict
|
|
329
|
+
* such a clear can erase is one written by a PRE-generation writer racing on the same control
|
|
330
|
+
* plane: the standing mixed-version-writer class the upgrade-order duty prices, kept consumable
|
|
331
|
+
* on purpose (refusing it would revive the forever-re-firing valve loop for every verdict minted
|
|
332
|
+
* before the upgrade). Idempotent; unknown/verdict-less/other-generation rows are a no-op.
|
|
333
|
+
*/
|
|
334
|
+
export declare function clearHoldResolution(controlDir: string, input: {
|
|
335
|
+
holdId: string;
|
|
336
|
+
ifResolvedAt: number | undefined;
|
|
337
|
+
}): void;
|
|
311
338
|
/** Read one hold's custody bytes (release leg + expired-release valve). Returns undefined when
|
|
312
339
|
* the custody file is gone or fails its digest — the caller records capture_lost, never commits
|
|
313
340
|
* unverified bytes. For an expired-released row the bytes may already sit in quarantine
|
|
@@ -346,6 +346,7 @@ function coerceHolds(raw) {
|
|
|
346
346
|
!HOLD_STATUSES.has(row.status) ||
|
|
347
347
|
(row.disposition !== undefined && (typeof row.disposition !== "object" || row.disposition === null || !HOLD_TERMINALS.has(row.disposition.terminal) || (row.disposition.quarantineName !== undefined && !safeBasename(row.disposition.quarantineName)))) ||
|
|
348
348
|
(row.resolved !== undefined && row.resolved !== "release" && row.resolved !== "discard") ||
|
|
349
|
+
(row.resolvedAt !== undefined && (typeof row.resolvedAt !== "number" || !Number.isSafeInteger(row.resolvedAt) || row.resolvedAt < 0 || row.resolvedAt >= 2 ** 50 || row.resolved === undefined)) ||
|
|
349
350
|
(row.entryId !== undefined && typeof row.entryId !== "string") ||
|
|
350
351
|
(row.baseRev !== undefined && typeof row.baseRev !== "string") ||
|
|
351
352
|
(row.op === "update" && (!reqStr(row.entryId) || !reqStr(row.baseRev))) ||
|
|
@@ -401,6 +402,14 @@ export function openInstructionHold(controlDir, input) {
|
|
|
401
402
|
const digest = sha256(buf);
|
|
402
403
|
const captureName = `${holdId}.md`;
|
|
403
404
|
const holdDir = join(controlDir, HOLD_DIR);
|
|
405
|
+
const refuse = (reason, terminal) => {
|
|
406
|
+
try {
|
|
407
|
+
disposeHold(controlDir, { holdId, terminal, now: input.now });
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
}
|
|
411
|
+
return { ok: false, holdId, reason };
|
|
412
|
+
};
|
|
404
413
|
try {
|
|
405
414
|
lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
|
|
406
415
|
if (rec.rows.some((r) => r.holdId === holdId))
|
|
@@ -431,10 +440,10 @@ export function openInstructionHold(controlDir, input) {
|
|
|
431
440
|
writeFileSync(join(holdDir, captureName), buf, { flag: "wx" });
|
|
432
441
|
const back = readFileSync(join(holdDir, captureName));
|
|
433
442
|
if (sha256(back) !== digest)
|
|
434
|
-
return
|
|
443
|
+
return refuse("hold capture verification failed (written bytes do not hash to the captured digest)", "capture_lost");
|
|
435
444
|
}
|
|
436
445
|
catch (err) {
|
|
437
|
-
return
|
|
446
|
+
return refuse(`hold capture failed: ${err instanceof Error ? err.message : String(err)}`, "capture_lost");
|
|
438
447
|
}
|
|
439
448
|
const stagingName = uniqueStagingName(holdId, input.now);
|
|
440
449
|
const stagingPath = join(holdDir, stagingName);
|
|
@@ -447,7 +456,7 @@ export function openInstructionHold(controlDir, input) {
|
|
|
447
456
|
if (code === "ENOENT") {
|
|
448
457
|
}
|
|
449
458
|
else {
|
|
450
|
-
return
|
|
459
|
+
return refuse(`hold removal failed (${code ?? "rename error"}): the plane file was not moved`, "discarded");
|
|
451
460
|
}
|
|
452
461
|
}
|
|
453
462
|
if (existsSync(stagingPath)) {
|
|
@@ -475,7 +484,7 @@ export function openInstructionHold(controlDir, input) {
|
|
|
475
484
|
});
|
|
476
485
|
}
|
|
477
486
|
catch (err) {
|
|
478
|
-
return
|
|
487
|
+
return refuse(`hold ledger flip refused: ${err instanceof Error ? err.message : String(err)}`, "discarded");
|
|
479
488
|
}
|
|
480
489
|
return { ok: true, holdId, ...(thirdWriterStranded !== undefined ? { thirdWriterStranded } : {}) };
|
|
481
490
|
}
|
|
@@ -604,6 +613,7 @@ export function markHoldReleased(controlDir, input) {
|
|
|
604
613
|
return { result: undefined };
|
|
605
614
|
r.status = "released";
|
|
606
615
|
delete r.resolved;
|
|
616
|
+
delete r.resolvedAt;
|
|
607
617
|
return { next: rec, result: r.captureName };
|
|
608
618
|
});
|
|
609
619
|
if (captureName === undefined)
|
|
@@ -638,6 +648,23 @@ export function resolveHoldRecord(controlDir, input) {
|
|
|
638
648
|
throw e;
|
|
639
649
|
}
|
|
640
650
|
r.resolved = input.action;
|
|
651
|
+
const nextGeneration = Math.max(input.now(), 0, (r.resolvedAt ?? 0) + 1);
|
|
652
|
+
if (!Number.isSafeInteger(nextGeneration) || nextGeneration < 0 || nextGeneration >= 2 ** 50) {
|
|
653
|
+
const e = new Error(`resolveHold: hold ${JSON.stringify(input.holdId)} cannot mint a fresh verdict generation (${String(nextGeneration)} — an exhausted window, a deranged clock, or a corrupt ledger row) — refused, never written.`);
|
|
654
|
+
e.code = "memory.hold_resolve_invalid_state";
|
|
655
|
+
throw e;
|
|
656
|
+
}
|
|
657
|
+
r.resolvedAt = nextGeneration;
|
|
658
|
+
return { next: rec, result: undefined };
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
export function clearHoldResolution(controlDir, input) {
|
|
662
|
+
lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
|
|
663
|
+
const r = rec.rows.find((x) => x.holdId === input.holdId);
|
|
664
|
+
if (r === undefined || r.resolved === undefined || r.resolvedAt !== input.ifResolvedAt)
|
|
665
|
+
return { result: undefined };
|
|
666
|
+
delete r.resolved;
|
|
667
|
+
delete r.resolvedAt;
|
|
641
668
|
return { next: rec, result: undefined };
|
|
642
669
|
});
|
|
643
670
|
}
|
|
@@ -75,5 +75,16 @@ export function mergeHarvestReports(a, b) {
|
|
|
75
75
|
const qf = [...(a.quarantineFailures ?? []), ...(b.quarantineFailures ?? [])];
|
|
76
76
|
if (qf.length > 0)
|
|
77
77
|
merged.quarantineFailures = qf;
|
|
78
|
+
const ac = a.containment;
|
|
79
|
+
const bc = b.containment;
|
|
80
|
+
if (ac !== undefined || bc !== undefined) {
|
|
81
|
+
merged.containment = {
|
|
82
|
+
indexRolledBack: ac?.indexRolledBack === true || bc?.indexRolledBack === true,
|
|
83
|
+
quarantinedInstruction: [...(ac?.quarantinedInstruction ?? []), ...(bc?.quarantinedInstruction ?? [])],
|
|
84
|
+
heldInstruction: [...(ac?.heldInstruction ?? []), ...(bc?.heldInstruction ?? [])],
|
|
85
|
+
releasedHolds: [...(ac?.releasedHolds ?? []), ...(bc?.releasedHolds ?? [])],
|
|
86
|
+
disposedHolds: [...(ac?.disposedHolds ?? []), ...(bc?.disposedHolds ?? [])],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
78
89
|
return merged;
|
|
79
90
|
}
|
|
@@ -803,7 +803,12 @@ export declare class MemoryEngine {
|
|
|
803
803
|
* and a re-call RESUMES it idempotently. Refusals (#123 coded, caller-shape):
|
|
804
804
|
* unattributed/invalid input, unknown/unmarked entry, and a CHALLENGED entry — the clear valve
|
|
805
805
|
* is not a challenge exit (adjudicate the challenge first; clearing a challenged entry would
|
|
806
|
-
* launder the exclusion through the weaker credential)
|
|
806
|
+
* launder the exclusion through the weaker credential); the challenged refusal re-judges on the
|
|
807
|
+
* RESUME path too while the marked entry stands. Two effect-half refusals (terminal, both
|
|
808
|
+
* states untouched): an on-disk projection diverging from the committed state (the clear never
|
|
809
|
+
* overwrites unadopted plane bytes — adopt/reconcile first), and a resumed row whose entry is
|
|
810
|
+
* ABSENT without a recorded committed tombstone (that absence is an independent deletion the
|
|
811
|
+
* clear never resurrects; the custody bytes stay disclosed on the terminal row).
|
|
807
812
|
*/
|
|
808
813
|
clearEntryOrigin(entryId: string, input: {
|
|
809
814
|
requestId: string;
|