@sema-agent/core 7.21.1 → 7.22.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 +123 -1
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +2 -1
- package/dist/core/ask-origin.d.ts +1 -0
- package/dist/core/ask-origin.js +3 -2
- package/dist/core/auto-mode-rebuild.js +6 -2
- package/dist/core/auto-mode.d.ts +40 -18
- package/dist/core/auto-mode.js +63 -45
- package/dist/core/checkpoint-store.d.ts +8 -8
- package/dist/core/engine-notice.d.ts +49 -1
- package/dist/core/engine-notice.js +14 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/mcp.d.ts +65 -0
- package/dist/core/mcp.js +190 -41
- package/dist/core/memory-engine/consolidation.d.ts +5 -0
- package/dist/core/memory-engine/consolidation.js +7 -3
- package/dist/core/memory-engine/engine.d.ts +8 -0
- package/dist/core/memory-engine/engine.js +25 -19
- package/dist/core/memory-engine/layout.d.ts +47 -9
- package/dist/core/memory-engine/layout.js +157 -57
- package/dist/core/memory-engine/session-incarnation.d.ts +27 -0
- package/dist/core/memory-engine/session-incarnation.js +63 -0
- package/dist/core/parent-spec-seat.d.ts +15 -3
- package/dist/core/parent-spec-seat.js +5 -0
- package/dist/core/permission-rule-model.d.ts +1 -2
- package/dist/core/retention.d.ts +8 -1
- package/dist/core/retention.js +2 -0
- package/dist/core/runner/classifier-ladder.js +1 -0
- package/dist/core/runner/contracts.d.ts +11 -0
- package/dist/core/runner/mcp-redial.d.ts +109 -0
- package/dist/core/runner/mcp-redial.js +60 -0
- package/dist/core/runner/memory-consolidation-driver.d.ts +2 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +9 -12
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -0
- package/dist/core/runner/prepare-hands-readface.js +13 -3
- package/dist/core/runner/prepare-memory.js +3 -3
- package/dist/core/runner/prepare-protocol-tools.d.ts +6 -1
- package/dist/core/runner/prepare-protocol-tools.js +20 -38
- package/dist/core/runner/prepare-task.js +3 -3
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
- package/dist/core/runner/prepare-wiring-manifest.js +2 -1
- package/dist/core/runner/run-identity-wiring.d.ts +3 -1
- package/dist/core/runner/run-identity-wiring.js +4 -1
- package/dist/core/runner/run-settle-and-teardown.d.ts +3 -1
- package/dist/core/runner/run-settle-and-teardown.js +6 -2
- package/dist/core/runner/run-terminal-adoption.d.ts +4 -1
- package/dist/core/runner/run-terminal-adoption.js +2 -6
- package/dist/core/runner/runtask.js +8 -5
- package/dist/core/runner/stream-lifecycle-verbs.d.ts +5 -1
- package/dist/core/runner/stream-lifecycle-verbs.js +23 -2
- package/dist/core/runner-deps.d.ts +16 -7
- package/dist/core/skill-listing-budget.d.ts +5 -4
- package/dist/core/task-result.d.ts +3 -2
- package/dist/core/task-spec.d.ts +2 -1
- package/dist/core/task-stream.d.ts +26 -0
- package/dist/core/tool-spec.d.ts +5 -1
- package/dist/core/trace.d.ts +5 -0
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +23 -0
- package/dist/core/wiring-manifest.js +3 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +2 -2
- package/dist/tools/fs/bash-program-position.d.ts +26 -5
- package/dist/tools/fs/bash-program-position.js +63 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +27 -17
- package/dist/tools/fs/bash-readonly-classifier.js +134 -55
- package/dist/tools/fs/fs-bash.js +43 -18
- package/dist/tools/fs/index.d.ts +4 -2
- package/dist/tools/fs/read-deny.d.ts +25 -11
- package/dist/tools/fs/read-deny.js +5 -1
- package/dist/tools/fs/safety.d.ts +1 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -1
- package/wire-changes/core@7.22.0.json +637 -0
|
@@ -102,11 +102,11 @@ export type PlatformLimitReason = Extract<ResourceLimitReason, "env_lifetime" |
|
|
|
102
102
|
export interface SafetyAxis {
|
|
103
103
|
/** design/70: the tool is egress-marked (`ToolSpec.egress` — an external write: push, open PR, send). */
|
|
104
104
|
egress?: boolean;
|
|
105
|
-
/** design/77 §4:
|
|
106
|
-
* operator's `shellGate:"always"`, the probe's structural verdict (mandated / unanswered), or the shell
|
|
107
|
-
* gate's own tighten under its `"maybe"` tier; a policy ask the doctrine's probe neither tightened nor
|
|
108
|
-
* mandated carries no bit (its park is the budgetable kind). */
|
|
105
|
+
/** design/77 §4: an irreversibility MANDATE **of the TOOL's own** (its `always`/`maybe` tier); a SHELL SEAT raise is NOT this axis, see {@link shell}. */
|
|
109
106
|
irreversible?: boolean;
|
|
107
|
+
/** The engine's SHELL SEAT raised this ask. Its question is about TEXT — not about undoing an act — so
|
|
108
|
+
* the two axes are mutually exclusive. */
|
|
109
|
+
shell?: boolean;
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
112
|
* design/80 §D-E: a DETERMINISTIC, REDACTED risk summary attached AT MINT to an escalation
|
|
@@ -948,10 +948,10 @@ export interface CheckpointState {
|
|
|
948
948
|
/**
|
|
949
949
|
* design/199 — the READ-face containment state at suspend (v9,
|
|
950
950
|
* {@link FACE_CHECKPOINT_VERSION}): the RESOLVED face plus the NORMALIZED deny-set ADDITIONS in
|
|
951
|
-
* force (deployment ∪ task layers; the built-in table is engine code
|
|
952
|
-
*
|
|
953
|
-
* non-default (face "open", or additions present) — an absent section IS the default posture and
|
|
954
|
-
* resumes as roots
|
|
951
|
+
* force (deployment ∪ task layers; the built-in table is engine code under the RESUMING deployment's
|
|
952
|
+
* own tier selection — empty unless it made one — never frozen here). Written ONLY when the posture
|
|
953
|
+
* is non-default (face "open", or additions present) — an absent section IS the default posture and
|
|
954
|
+
* resumes as roots plus whatever built-in tiers the resuming worker selected. Resume folds STRICTER-WINS: face = roots unless the
|
|
955
955
|
* section says open AND the live resolution says open; deny additions = frozen ∪ live (add-only,
|
|
956
956
|
* monotone — a resume can never widen what the cut leg could read).
|
|
957
957
|
*
|
|
@@ -109,6 +109,18 @@ export interface EngineNotice {
|
|
|
109
109
|
* its explicit {@link NOTICE_AUDIENCE} row (#433 made the registry total over the catalog —
|
|
110
110
|
* no engine-minted code is audience-defaulted any more). The refusal itself
|
|
111
111
|
* (`mcp.server_revoked`) is a tool RESULT code, not a notice.
|
|
112
|
+
* - `"mcp.server_redialed"` — a host asked this run to RE-DIAL one MCP server (the run
|
|
113
|
+
* handle's `reconnectMcpServer` verb) and this is what came of it. `detail: { server, outcome,
|
|
114
|
+
* added, removed, reason?, sessionId, runId }` — `outcome` is the result's own three-word answer
|
|
115
|
+
* (`reconnected` / `refused` / `not_declared`) and `added`/`removed` are COUNTS of namespaced
|
|
116
|
+
* tool names, never the names themselves (a server's tool names are server-authored text and
|
|
117
|
+
* this is an operator line, not a disclosure channel). ONE code rather than a success code and a
|
|
118
|
+
* failure code: the outcome is data about one event, not two kinds of event, and both would
|
|
119
|
+
* carry the same presentation tier — a consumer alerting on failures reads one field.
|
|
120
|
+
* `"operator"` audience: re-dialing is deployment plumbing and a failed one leaves the server
|
|
121
|
+
* DOWN, which only the operator can act on. Dedup unit: once per ATTEMPT — never folded per run,
|
|
122
|
+
* because two attempts on one server are two different answers and folding the second would hide
|
|
123
|
+
* the one that says the server is still down.
|
|
112
124
|
* - `"mcp.injection_dropped"` (L-167) — an entry of the user's own request-lane MCP injection
|
|
113
125
|
* was NOT mounted for the leg. The HOST owns the drop sites and supplies the facts; CORE composes
|
|
114
126
|
* the notice ({@link import("./mcp-injection-drop.js").mcpInjectionDroppedNotice}, the one mint).
|
|
@@ -707,6 +719,28 @@ export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) =>
|
|
|
707
719
|
* one repeated disclosure about one run. The account is per-RUN by its own contract sentence ("at most
|
|
708
720
|
* once per family, per run"), so it must carry the run's own id. Same omit-never-fabricate rule.
|
|
709
721
|
*/
|
|
722
|
+
/** What a settled leg still owes a person BEFORE the compensation lane has had its say: the facts the
|
|
723
|
+
* terminal-adoption lane holds, carried to the settle lane that closes the account
|
|
724
|
+
* ({@link settleOwedUserInput}). */
|
|
725
|
+
export interface OwedUserInputAccount {
|
|
726
|
+
/** The harness's at-agent_end snapshot, held because the tail still had something to say about it. */
|
|
727
|
+
readonly held: {
|
|
728
|
+
steer: number;
|
|
729
|
+
followUp: number;
|
|
730
|
+
} | undefined;
|
|
731
|
+
/** What a durable park's migration carried onto the new checkpoint, by family. */
|
|
732
|
+
readonly migrated: {
|
|
733
|
+
steer: number;
|
|
734
|
+
followUp: number;
|
|
735
|
+
};
|
|
736
|
+
/** The resumed leg's undelivered continuation frames, and how many of them the migration carried. */
|
|
737
|
+
readonly continuation: {
|
|
738
|
+
frames: ReadonlyArray<{
|
|
739
|
+
seq?: number;
|
|
740
|
+
}>;
|
|
741
|
+
carried: number;
|
|
742
|
+
};
|
|
743
|
+
}
|
|
710
744
|
/**
|
|
711
745
|
* What this leg still OWES a person, one count per family — the input to {@link undrainedUserInputNotices}.
|
|
712
746
|
* TWO lanes accept caller input and either can be holding some when a run ends: the harness's live queue
|
|
@@ -727,11 +761,25 @@ export declare function owedUserInputCounts(held: {
|
|
|
727
761
|
seq?: number;
|
|
728
762
|
}>;
|
|
729
763
|
carried: number;
|
|
730
|
-
|
|
764
|
+
rowReturned: boolean;
|
|
731
765
|
}): {
|
|
732
766
|
steer: number;
|
|
733
767
|
followUp: number;
|
|
734
768
|
};
|
|
769
|
+
/** Close the account — the ONE settlement point, called AFTER the reopen compensation with what that
|
|
770
|
+
* compensation actually did. Ordering is the whole rule: the counts are arithmetic, and the only fact they
|
|
771
|
+
* cannot be computed without is one the lane before this one does not have yet. EXACTLY ONCE per account,
|
|
772
|
+
* which is what lets the teardown call it again as a backstop for a settle lane that threw on its way here
|
|
773
|
+
* (an account nobody closes is the silence this seat exists to end; a second row would be the same loss
|
|
774
|
+
* counted twice). Keyed on the account OBJECT — one per run, minted by the lane that assembles it. NEVER
|
|
775
|
+
* THROWS: a station calls it from a teardown path where the run is already failing, and a disclosure that
|
|
776
|
+
* takes the cleanup down with it is worse than the silence it was fixing (the absent-sink arm of
|
|
777
|
+
* {@link deliverEngineNotice} ends at a bare `console.warn`, which a host-replaced console can throw from). */
|
|
778
|
+
export declare function settleOwedUserInput(account: OwedUserInputAccount, rowReturned: boolean, ids: {
|
|
779
|
+
taskId?: string;
|
|
780
|
+
sessionId?: string;
|
|
781
|
+
runId?: string;
|
|
782
|
+
}, onNotice: ((notice: EngineNotice) => void) | undefined): void;
|
|
735
783
|
export declare function undrainedUserInputNotices(counts: {
|
|
736
784
|
steer: number;
|
|
737
785
|
followUp: number;
|
|
@@ -32,12 +32,25 @@ export function deliverEngineNotice(onNotice, notice) {
|
|
|
32
32
|
console.warn(notice.message);
|
|
33
33
|
}
|
|
34
34
|
export function owedUserInputCounts(held, migrated, continuation) {
|
|
35
|
-
const owedContinuation = (continuation.
|
|
35
|
+
const owedContinuation = (continuation.rowReturned ? continuation.frames.filter((f) => f.seq === undefined).length : continuation.frames.length) - continuation.carried;
|
|
36
36
|
return {
|
|
37
37
|
steer: Math.max(0, (held?.steer ?? 0) - migrated.steer) + Math.max(0, owedContinuation),
|
|
38
38
|
followUp: Math.max(0, (held?.followUp ?? 0) - migrated.followUp),
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
export function settleOwedUserInput(account, rowReturned, ids, onNotice) {
|
|
42
|
+
if (SETTLED_OWED_ACCOUNTS.has(account))
|
|
43
|
+
return;
|
|
44
|
+
SETTLED_OWED_ACCOUNTS.add(account);
|
|
45
|
+
try {
|
|
46
|
+
const owed = owedUserInputCounts(account.held, account.migrated, { ...account.continuation, rowReturned });
|
|
47
|
+
for (const notice of undrainedUserInputNotices(owed, ids.taskId, ids.sessionId, ids.runId))
|
|
48
|
+
deliverEngineNotice(onNotice, notice);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const SETTLED_OWED_ACCOUNTS = new WeakSet();
|
|
41
54
|
export function undrainedUserInputNotices(counts, taskId, sessionId, runId) {
|
|
42
55
|
const tid = taskId !== undefined ? { taskId } : {};
|
|
43
56
|
const sid = sessionId !== undefined ? { sessionId } : {};
|
|
@@ -119,7 +119,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
119
119
|
* the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
|
|
120
120
|
* so the catalog is not written a second time here and the two cannot drift.
|
|
121
121
|
*/
|
|
122
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
|
|
122
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
|
|
123
123
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
124
124
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
125
125
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -18,6 +18,8 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
18
18
|
"memory.layout_unowned_root_content",
|
|
19
19
|
"memory.tail",
|
|
20
20
|
"memory.pollution_mark_failed",
|
|
21
|
+
"memory.session_incarnation_mismatch",
|
|
22
|
+
"memory.session_incarnation_unreadable",
|
|
21
23
|
"memory.session_polluted",
|
|
22
24
|
"memory.harvest_quarantined",
|
|
23
25
|
"memory.delegation_static_mark_waived",
|
|
@@ -132,6 +134,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
132
134
|
"delegation.ask_unresolvable",
|
|
133
135
|
"mcp.injection_dropped",
|
|
134
136
|
"mcp.revocation_probe_failed",
|
|
137
|
+
"mcp.server_redialed",
|
|
135
138
|
"workflow.governance_key_stripped",
|
|
136
139
|
"workflow.agent_option_ignored",
|
|
137
140
|
"memory.session_polluted",
|
|
@@ -214,6 +217,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
214
217
|
"checkpoint.execution_outcome_unrecorded": "operator",
|
|
215
218
|
"mcp.injection_dropped": "user",
|
|
216
219
|
"mcp.revocation_probe_failed": "operator",
|
|
220
|
+
"mcp.server_redialed": "operator",
|
|
217
221
|
"workflow.governance_key_stripped": "operator",
|
|
218
222
|
"workflow.agent_option_ignored": "operator",
|
|
219
223
|
"memory.content_class_declared": "operator",
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -190,6 +190,25 @@ export interface MaterializedMcp {
|
|
|
190
190
|
* misroute).
|
|
191
191
|
*/
|
|
192
192
|
refresh: (server?: string) => Promise<McpRefreshResult[]>;
|
|
193
|
+
/**
|
|
194
|
+
* RE-DIAL one named server — the action {@link refresh} deliberately is not. It closes
|
|
195
|
+
* whatever handle the server still has and dials it again through the SAME `connectServer` the
|
|
196
|
+
* materialization used, so the schema gate, the namespacing, the axis derivation and the metadata
|
|
197
|
+
* cache seeding cannot drift between a first connect and a re-dial: one function, not one
|
|
198
|
+
* convention.
|
|
199
|
+
*
|
|
200
|
+
* ONE SERVER, NAMED — CC 2.1.259's control-channel request carries a REQUIRED `serverName` and has
|
|
201
|
+
* no all-servers form, and an all-servers arm would close HEALTHY connections to re-dial them (a
|
|
202
|
+
* stdio server owning a device or a port is exactly the one that cannot be re-dialed while its
|
|
203
|
+
* predecessor still holds the device). CLOSE-THEN-DIAL, so a re-dial that FAILS leaves the server
|
|
204
|
+
* DISCONNECTED until another re-dial or the next leg's materialize, and calls in flight on the
|
|
205
|
+
* replaced connection fail with it — inherent to re-dialing, and CC's behavior too.
|
|
206
|
+
*
|
|
207
|
+
* NOT A MODEL FACE (CC has no such tool either): a verb that can leave a connection down is a host
|
|
208
|
+
* act, and its consumer entrance is the run handle's `reconnectMcpServer`. LEG-LOCAL like
|
|
209
|
+
* {@link refresh}: the result rides no checkpoint, and a durable resume takes a fresh listing.
|
|
210
|
+
*/
|
|
211
|
+
reconnect: (server: string) => Promise<McpReconnectResult>;
|
|
193
212
|
dispose: () => Promise<void>;
|
|
194
213
|
}
|
|
195
214
|
/** One per-server entry of {@link MaterializedMcp.refresh}. `added`/`removed` are namespaced
|
|
@@ -244,6 +263,52 @@ export interface McpRefreshResult {
|
|
|
244
263
|
*/
|
|
245
264
|
listingIncomplete?: McpListingIncomplete;
|
|
246
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* The result of {@link MaterializedMcp.reconnect} — ONE server, because the face takes one.
|
|
268
|
+
*
|
|
269
|
+
* `outcome` is the whole top level, three words, so a consumer mapping it onto a three-state wire
|
|
270
|
+
* answer needs no table: `reconnected` is the only success, and everything that did not reconnect is
|
|
271
|
+
* either `refused` (the engine declined, or the dial/first listing failed — `reason` says which) or
|
|
272
|
+
* `not_declared` (no server of that name in this leg's spec). Sub-reasons ride `reason`; a fourth
|
|
273
|
+
* word would be a new state for every downstream to learn.
|
|
274
|
+
*
|
|
275
|
+
* @contract mcp.redial.roster_assertion — a re-dial result carries `tools` IF AND ONLY IF it asserts a
|
|
276
|
+
* roster for its `prefix` domain, and that one rule is what every consumer of it reads: the fresh intake
|
|
277
|
+
* on `reconnected`, the EMPTY roster on a dial that failed after the old connection was already closed
|
|
278
|
+
* (the server is down, so its tools are withdrawn rather than left mounted and unusable), and NOTHING at
|
|
279
|
+
* all on an arm that never touched the connection — an unknown name, a revoked server, a re-dial already
|
|
280
|
+
* in flight, a disposed materialization — whose mounted roster is left exactly as it was.
|
|
281
|
+
*/
|
|
282
|
+
export interface McpReconnectResult {
|
|
283
|
+
server: string;
|
|
284
|
+
/** The consumer's splice domain, same meaning and same mint as {@link McpRefreshResult.prefix}. */
|
|
285
|
+
prefix: string;
|
|
286
|
+
outcome: "reconnected" | "refused" | "not_declared";
|
|
287
|
+
/** Why, on every non-`reconnected` outcome (neutralized + bounded when it embeds remote text).
|
|
288
|
+
* Never the only machine-readable fact: see {@link status}. */
|
|
289
|
+
reason?: string;
|
|
290
|
+
toolCount: number;
|
|
291
|
+
/** Namespaced names this re-dial added / took away relative to what the server had mounted a moment
|
|
292
|
+
* ago — a first successful dial adds its whole roster, a failed dial removes it. */
|
|
293
|
+
added: string[];
|
|
294
|
+
removed: string[];
|
|
295
|
+
/** See the type docstring: present iff this result asserts a roster for `prefix`. */
|
|
296
|
+
tools?: AgentTool[];
|
|
297
|
+
axes?: McpToolAxis[];
|
|
298
|
+
dropped?: McpDroppedTool[];
|
|
299
|
+
/** The OPERATOR lines this re-dial produced (an incomplete listing walk, a kept-but-defective tool
|
|
300
|
+
* schema, a spec naming tools a resources-only server cannot serve). Also appended to
|
|
301
|
+
* {@link MaterializedMcp.warnings}, which the runner drains ONCE at prepare — so the copy here is
|
|
302
|
+
* what the run handle's verb forwards to `onError(phase:"mcp")`. One production, two drains. */
|
|
303
|
+
warnings?: string[];
|
|
304
|
+
/** The NEW connect-time status record (also swapped into {@link MaterializedMcp.statuses}), present
|
|
305
|
+
* whenever the dial was attempted: a consumer keys its next action off `errorCode` / `httpStatus`
|
|
306
|
+
* (a 401 is a re-authorization door, a `spawn_failed` a missing binary), not off {@link reason}. */
|
|
307
|
+
status?: McpServerStatus;
|
|
308
|
+
/** The re-dial's own `tools/list` walk did not finish — {@link McpRefreshResult.listingIncomplete}
|
|
309
|
+
* minus the retention arm: a re-dial has no previous listing on this connection to retain from. */
|
|
310
|
+
listingIncomplete?: McpListingIncomplete;
|
|
311
|
+
}
|
|
247
312
|
/** design/99 §E9 — projected per-server MCP status (see {@link MaterializedMcp.statuses}). NOTE: `serverInfo`
|
|
248
313
|
* and `error` are SERVER-controlled strings (verbatim from the remote) — UNTRUSTED; a consumer rendering them
|
|
249
314
|
* into a TUI/log must treat them as such (a misbehaving server could embed ANSI/break-out sequences). `toolNames`
|
package/dist/core/mcp.js
CHANGED
|
@@ -709,7 +709,52 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
709
709
|
const droppedTools = [];
|
|
710
710
|
let disposing = false;
|
|
711
711
|
const serverHandles = [];
|
|
712
|
-
const
|
|
712
|
+
const redialsInFlight = new Set();
|
|
713
|
+
const commitConnectedServer = (spec, s, transportKind) => {
|
|
714
|
+
const raised = [];
|
|
715
|
+
clients.push(s.client);
|
|
716
|
+
const handle = { name: spec.name, spec, client: s.client, health: s.health, tools: s.tools, listedRaw: s.listedTools, transportKind, announce: s.announce, instructions: s.instructions };
|
|
717
|
+
const hIdx = serverHandles.findIndex((h) => h.name === spec.name);
|
|
718
|
+
if (hIdx === -1)
|
|
719
|
+
serverHandles.push(handle);
|
|
720
|
+
else
|
|
721
|
+
serverHandles[hIdx] = handle;
|
|
722
|
+
const hasInstructions = s.instructions !== undefined;
|
|
723
|
+
const announceClose = () => {
|
|
724
|
+
if (disposing)
|
|
725
|
+
return;
|
|
726
|
+
s.status.transportClosed = true;
|
|
727
|
+
if (hasInstructions)
|
|
728
|
+
instructionsDelta.pendingRemovals.push(spec.name);
|
|
729
|
+
};
|
|
730
|
+
s.announce.fn = announceClose;
|
|
731
|
+
if (s.health.dead)
|
|
732
|
+
announceClose();
|
|
733
|
+
if (s.resourceServer) {
|
|
734
|
+
const rIdx = resourceServers.findIndex((rs) => rs.server === spec.name);
|
|
735
|
+
if (rIdx === -1)
|
|
736
|
+
resourceServers.push(s.resourceServer);
|
|
737
|
+
else
|
|
738
|
+
resourceServers[rIdx] = s.resourceServer;
|
|
739
|
+
}
|
|
740
|
+
for (const d of s.dropped)
|
|
741
|
+
droppedTools.push({ server: inlineUntrusted(spec.name, 160), ...d });
|
|
742
|
+
for (const a of s.schemaAdvisories)
|
|
743
|
+
raised.push(schemaAdvisoryWarning(spec.name, a.tool, a.reason));
|
|
744
|
+
if (s.toolsCapabilityAbsent === true && spec.allowTools !== undefined && spec.allowTools.length > 0) {
|
|
745
|
+
raised.push(toolsCapabilityAbsentWarning(spec.name));
|
|
746
|
+
}
|
|
747
|
+
if (s.listingIncomplete)
|
|
748
|
+
raised.push(listingIncompleteWarning(spec.name, s.listingIncomplete));
|
|
749
|
+
warnings.push(...raised);
|
|
750
|
+
const sIdx = statuses.findIndex((st) => st.name === spec.name);
|
|
751
|
+
if (sIdx === -1)
|
|
752
|
+
statuses.push(s.status);
|
|
753
|
+
else
|
|
754
|
+
statuses[sIdx] = s.status;
|
|
755
|
+
return raised;
|
|
756
|
+
};
|
|
757
|
+
const readDialKind = (spec) => {
|
|
713
758
|
try {
|
|
714
759
|
const kind = spec.transport.kind;
|
|
715
760
|
return kind === "stdio" || kind === "http" ? kind : undefined;
|
|
@@ -717,7 +762,33 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
717
762
|
catch {
|
|
718
763
|
return undefined;
|
|
719
764
|
}
|
|
720
|
-
}
|
|
765
|
+
};
|
|
766
|
+
const dialKinds = entries.map(readDialKind);
|
|
767
|
+
const prefixOf = (name) => mintNamespacePrefix(MCP_NAMESPACE, name);
|
|
768
|
+
const reconcileInstructions = (name, before, after, connected) => {
|
|
769
|
+
let owedRemoval = false;
|
|
770
|
+
for (let i = instructionsDelta.pendingRemovals.length - 1; i >= 0; i--) {
|
|
771
|
+
if (instructionsDelta.pendingRemovals[i] !== name)
|
|
772
|
+
continue;
|
|
773
|
+
instructionsDelta.pendingRemovals.splice(i, 1);
|
|
774
|
+
owedRemoval = true;
|
|
775
|
+
}
|
|
776
|
+
let hadPendingAdd = false;
|
|
777
|
+
for (let i = instructionsDelta.pendingAdds.length - 1; i >= 0; i--) {
|
|
778
|
+
if (instructionsDelta.pendingAdds[i].server !== name)
|
|
779
|
+
continue;
|
|
780
|
+
instructionsDelta.pendingAdds.splice(i, 1);
|
|
781
|
+
hadPendingAdd = true;
|
|
782
|
+
}
|
|
783
|
+
if (after !== undefined) {
|
|
784
|
+
instructionsDelta.pendingAdds.push({ server: name, text: after });
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (connected)
|
|
788
|
+
return;
|
|
789
|
+
if (before !== undefined || owedRemoval || hadPendingAdd)
|
|
790
|
+
instructionsDelta.pendingRemovals.push(name);
|
|
791
|
+
};
|
|
721
792
|
const settled = await Promise.allSettled(entries.map((spec, i) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked, dialKinds[i])));
|
|
722
793
|
try {
|
|
723
794
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -725,36 +796,12 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
725
796
|
const spec = entries[i];
|
|
726
797
|
if (r.status === "fulfilled") {
|
|
727
798
|
const s = r.value;
|
|
728
|
-
clients.push(s.client);
|
|
729
|
-
serverHandles.push({ name: spec.name, spec, client: s.client, health: s.health, tools: s.tools, listedRaw: s.listedTools, transportKind: dialKinds[i] });
|
|
730
799
|
tools.push(...s.tools);
|
|
731
800
|
toolAxes.push(...s.axes);
|
|
732
801
|
if (s.instructions) {
|
|
733
802
|
serverInstructions.push({ server: spec.name, text: s.instructions });
|
|
734
803
|
}
|
|
735
|
-
|
|
736
|
-
const announceClose = () => {
|
|
737
|
-
if (disposing)
|
|
738
|
-
return;
|
|
739
|
-
s.status.transportClosed = true;
|
|
740
|
-
if (hasInstructions)
|
|
741
|
-
instructionsDelta.pendingRemovals.push(spec.name);
|
|
742
|
-
};
|
|
743
|
-
s.announce.fn = announceClose;
|
|
744
|
-
if (s.health.dead)
|
|
745
|
-
announceClose();
|
|
746
|
-
if (s.resourceServer)
|
|
747
|
-
resourceServers.push(s.resourceServer);
|
|
748
|
-
for (const d of s.dropped)
|
|
749
|
-
droppedTools.push({ server: inlineUntrusted(spec.name, 160), ...d });
|
|
750
|
-
for (const a of s.schemaAdvisories)
|
|
751
|
-
warnings.push(schemaAdvisoryWarning(spec.name, a.tool, a.reason));
|
|
752
|
-
if (s.toolsCapabilityAbsent === true && spec.allowTools !== undefined && spec.allowTools.length > 0) {
|
|
753
|
-
warnings.push(toolsCapabilityAbsentWarning(spec.name));
|
|
754
|
-
}
|
|
755
|
-
if (s.listingIncomplete)
|
|
756
|
-
warnings.push(listingIncompleteWarning(spec.name, s.listingIncomplete));
|
|
757
|
-
statuses.push(s.status);
|
|
804
|
+
commitConnectedServer(spec, s, dialKinds[i]);
|
|
758
805
|
}
|
|
759
806
|
else {
|
|
760
807
|
warnings.push(asServerWarning(spec, r.reason));
|
|
@@ -770,6 +817,89 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
770
817
|
await Promise.allSettled(settled.map((r) => (r.status === "fulfilled" ? r.value.client.close() : Promise.resolve())));
|
|
771
818
|
throw mergeErr;
|
|
772
819
|
}
|
|
820
|
+
const redialOnce = async (server) => {
|
|
821
|
+
const prefix = prefixOf(server);
|
|
822
|
+
const declined = (outcome, reason) => ({ server, prefix, outcome, reason, toolCount: 0, added: [], removed: [] });
|
|
823
|
+
if (disposing)
|
|
824
|
+
return declined("refused", "this materialization is being disposed — the task is tearing down");
|
|
825
|
+
const spec = entries.find((e) => e.name === server);
|
|
826
|
+
if (spec === undefined)
|
|
827
|
+
return declined("not_declared", `no MCP server named ${inlineUntrusted(server, 120)} is declared for this task`);
|
|
828
|
+
if (isServerRevoked(server))
|
|
829
|
+
return declined("refused", "server revoked by the operator — the re-dial did not contact it");
|
|
830
|
+
if (redialsInFlight.has(server))
|
|
831
|
+
return declined("refused", "a re-dial of this server is already in flight — wait for it to settle");
|
|
832
|
+
redialsInFlight.add(server);
|
|
833
|
+
try {
|
|
834
|
+
const hIdx = serverHandles.findIndex((h) => h.name === server);
|
|
835
|
+
const prior = hIdx === -1 ? undefined : serverHandles[hIdx];
|
|
836
|
+
const priorNames = prior ? prior.tools.map((t) => t.name) : [];
|
|
837
|
+
const priorInstructions = prior?.instructions;
|
|
838
|
+
if (prior) {
|
|
839
|
+
prior.announce.fn = undefined;
|
|
840
|
+
serverHandles.splice(hIdx, 1);
|
|
841
|
+
const cIdx = clients.indexOf(prior.client);
|
|
842
|
+
if (cIdx !== -1)
|
|
843
|
+
clients.splice(cIdx, 1);
|
|
844
|
+
await prior.client.close().catch(() => undefined);
|
|
845
|
+
}
|
|
846
|
+
const dialKind = readDialKind(spec);
|
|
847
|
+
let connected;
|
|
848
|
+
try {
|
|
849
|
+
connected = await connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked, dialKind);
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
const { kind, delivered, httpStatus } = classifyMcpFailure(err, { phase: "connect", transport: dialKind });
|
|
853
|
+
const status = { name: server, status: "failed", errorCode: kind, delivered, ...(httpStatus !== undefined ? { httpStatus } : {}), error: namedMcpFailureText(err) };
|
|
854
|
+
const sIdx = statuses.findIndex((st) => st.name === server);
|
|
855
|
+
if (sIdx === -1)
|
|
856
|
+
statuses.push(status);
|
|
857
|
+
else
|
|
858
|
+
statuses[sIdx] = status;
|
|
859
|
+
const raised = asServerWarning(spec, err);
|
|
860
|
+
warnings.push(raised);
|
|
861
|
+
reconcileInstructions(server, priorInstructions, undefined, false);
|
|
862
|
+
return {
|
|
863
|
+
server,
|
|
864
|
+
prefix,
|
|
865
|
+
outcome: "refused",
|
|
866
|
+
reason: inlineUntrusted(namedMcpFailureText(err), 240),
|
|
867
|
+
toolCount: 0,
|
|
868
|
+
added: [],
|
|
869
|
+
removed: priorNames,
|
|
870
|
+
tools: [],
|
|
871
|
+
axes: [],
|
|
872
|
+
dropped: [],
|
|
873
|
+
warnings: [raised.message],
|
|
874
|
+
status,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
if (disposing) {
|
|
878
|
+
await connected.client.close().catch(() => undefined);
|
|
879
|
+
return declined("refused", "this materialization was disposed while the re-dial was in flight — the new connection was closed");
|
|
880
|
+
}
|
|
881
|
+
const raised = commitConnectedServer(spec, connected, dialKind);
|
|
882
|
+
reconcileInstructions(server, priorInstructions, connected.instructions, true);
|
|
883
|
+
const newNames = connected.tools.map((t) => t.name);
|
|
884
|
+
return {
|
|
885
|
+
server,
|
|
886
|
+
prefix,
|
|
887
|
+
outcome: "reconnected",
|
|
888
|
+
toolCount: newNames.length,
|
|
889
|
+
added: newNames.filter((n) => !priorNames.includes(n)),
|
|
890
|
+
removed: priorNames.filter((n) => !newNames.includes(n)),
|
|
891
|
+
tools: connected.tools,
|
|
892
|
+
axes: connected.axes,
|
|
893
|
+
dropped: connected.dropped.map((d) => ({ server: inlineUntrusted(server, 160), ...d })),
|
|
894
|
+
...(raised.length > 0 ? { warnings: raised.map((w) => w.message) } : {}),
|
|
895
|
+
status: connected.status,
|
|
896
|
+
...(connected.listingIncomplete !== undefined ? { listingIncomplete: connected.listingIncomplete } : {}),
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
finally {
|
|
900
|
+
redialsInFlight.delete(server);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
773
903
|
return {
|
|
774
904
|
tools,
|
|
775
905
|
toolAxes,
|
|
@@ -779,8 +909,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
779
909
|
droppedTools,
|
|
780
910
|
statuses,
|
|
781
911
|
refresh: async (server) => {
|
|
782
|
-
const
|
|
783
|
-
const targets = server !== undefined ? serverHandles.filter((h) => h.name === server) : serverHandles;
|
|
912
|
+
const targets = server !== undefined ? serverHandles.filter((h) => h.name === server) : [...serverHandles];
|
|
784
913
|
const results = [];
|
|
785
914
|
if (server !== undefined && targets.length === 0) {
|
|
786
915
|
return [{ server, prefix: prefixOf(server), status: "not_connected", toolCount: 0, added: [], removed: [] }];
|
|
@@ -834,8 +963,25 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
834
963
|
});
|
|
835
964
|
}
|
|
836
965
|
}
|
|
966
|
+
for (let i = 0; i < results.length; i++) {
|
|
967
|
+
const entry = results[i];
|
|
968
|
+
if (entry.status !== "refreshed")
|
|
969
|
+
continue;
|
|
970
|
+
if (serverHandles.find((h) => h.name === entry.server)?.tools === entry.tools)
|
|
971
|
+
continue;
|
|
972
|
+
results[i] = {
|
|
973
|
+
server: entry.server,
|
|
974
|
+
prefix: entry.prefix,
|
|
975
|
+
status: "failed",
|
|
976
|
+
toolCount: 0,
|
|
977
|
+
added: [],
|
|
978
|
+
removed: [],
|
|
979
|
+
error: "this server was re-dialed while the refresh was reading another server — the listing is from the connection that re-dial replaced, so it was discarded",
|
|
980
|
+
};
|
|
981
|
+
}
|
|
837
982
|
return results;
|
|
838
983
|
},
|
|
984
|
+
reconnect: redialOnce,
|
|
839
985
|
dispose: async () => {
|
|
840
986
|
disposing = true;
|
|
841
987
|
await Promise.allSettled(clients.map((c) => c.close()));
|
|
@@ -1063,13 +1209,13 @@ function renderDirChildren(server, uri, children, flags) {
|
|
|
1063
1209
|
};
|
|
1064
1210
|
}
|
|
1065
1211
|
function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
1066
|
-
const listable = resourceServers.filter((rs) => rs.listAllowed);
|
|
1067
|
-
const readable = resourceServers.filter((rs) => rs.readAllowed);
|
|
1068
|
-
if (listable.length === 0 && readable.length === 0)
|
|
1212
|
+
const listable = () => resourceServers.filter((rs) => rs.listAllowed);
|
|
1213
|
+
const readable = () => resourceServers.filter((rs) => rs.readAllowed);
|
|
1214
|
+
if (listable().length === 0 && readable().length === 0)
|
|
1069
1215
|
return { tools: [], axes: [] };
|
|
1070
1216
|
const tools = [];
|
|
1071
1217
|
const axes = [];
|
|
1072
|
-
if (listable.length > 0) {
|
|
1218
|
+
if (listable().length > 0) {
|
|
1073
1219
|
tools.push({
|
|
1074
1220
|
name: LIST_MCP_RESOURCES,
|
|
1075
1221
|
description: "List the resources available from connected MCP servers (uri / name / description). Pass `server` to limit " +
|
|
@@ -1082,9 +1228,10 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
|
1082
1228
|
},
|
|
1083
1229
|
execute: async (_id, params, signal) => {
|
|
1084
1230
|
const { server } = (params ?? {});
|
|
1085
|
-
const
|
|
1231
|
+
const live = listable();
|
|
1232
|
+
const targets = server ? live.filter((rs) => rs.server === server) : live;
|
|
1086
1233
|
if (server && targets.length === 0) {
|
|
1087
|
-
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with listable resources. Available: ${serverNames(
|
|
1234
|
+
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with listable resources. Available: ${serverNames(live)}.` }], details: undefined, terminate: false };
|
|
1088
1235
|
}
|
|
1089
1236
|
const sections = [];
|
|
1090
1237
|
const all = [];
|
|
@@ -1146,7 +1293,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
|
1146
1293
|
});
|
|
1147
1294
|
axes.push({ name: LIST_MCP_RESOURCES, effect: "read" });
|
|
1148
1295
|
}
|
|
1149
|
-
if (readable.length > 0) {
|
|
1296
|
+
if (readable().length > 0) {
|
|
1150
1297
|
tools.push({
|
|
1151
1298
|
name: READ_MCP_RESOURCE,
|
|
1152
1299
|
description: "Read a specific MCP resource by `server` + `uri` (from ListMcpResourcesTool). Its content is external/untrusted data.",
|
|
@@ -1164,9 +1311,10 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
|
1164
1311
|
const { server, uri } = (params ?? {});
|
|
1165
1312
|
if (!server || !uri)
|
|
1166
1313
|
return { content: [{ type: "text", text: "Error: `server` and `uri` are required." }], details: undefined, terminate: false };
|
|
1167
|
-
const
|
|
1314
|
+
const live = readable();
|
|
1315
|
+
const rs = live.find((r) => r.server === server);
|
|
1168
1316
|
if (!rs)
|
|
1169
|
-
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with readable resources. Available: ${serverNames(
|
|
1317
|
+
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with readable resources. Available: ${serverNames(live)}.` }], details: undefined, terminate: false };
|
|
1170
1318
|
const what = `The read of resource ${inlineUntrusted(uri)}`;
|
|
1171
1319
|
if (isServerRevoked(server)) {
|
|
1172
1320
|
return {
|
|
@@ -1203,7 +1351,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
|
1203
1351
|
});
|
|
1204
1352
|
axes.push({ name: READ_MCP_RESOURCE, effect: "read" });
|
|
1205
1353
|
}
|
|
1206
|
-
if (listable.length > 0) {
|
|
1354
|
+
if (listable().length > 0) {
|
|
1207
1355
|
tools.push({
|
|
1208
1356
|
name: READ_MCP_RESOURCE_DIR,
|
|
1209
1357
|
description: "List the MCP resources UNDER a directory `uri` on `server` — its child resources (those whose uri is " +
|
|
@@ -1223,9 +1371,10 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
|
1223
1371
|
const { server, uri } = (params ?? {});
|
|
1224
1372
|
if (!server || !uri)
|
|
1225
1373
|
return { content: [{ type: "text", text: "Error: `server` and `uri` are required." }], details: undefined, terminate: false };
|
|
1226
|
-
const
|
|
1374
|
+
const live = listable();
|
|
1375
|
+
const rs = live.find((r) => r.server === server);
|
|
1227
1376
|
if (!rs)
|
|
1228
|
-
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with listable resources. Available: ${serverNames(
|
|
1377
|
+
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with listable resources. Available: ${serverNames(live)}.` }], details: undefined, terminate: false };
|
|
1229
1378
|
const what = `The directory listing of ${inlineUntrusted(uri)}`;
|
|
1230
1379
|
if (isServerRevoked(server)) {
|
|
1231
1380
|
return {
|
|
@@ -302,11 +302,16 @@ export type ConsolidationGateRead = {
|
|
|
302
302
|
* ONE source for "is a run owed", so the edge can never sit disarmed while the trigger believes nothing is owed.
|
|
303
303
|
*/
|
|
304
304
|
export declare function recommendationUnconsumed(row: Pick<ConsolidationGateRow, "lastRecommendedAt" | "lastRunAt">): boolean;
|
|
305
|
+
/** §2.2 — the ONE reading of "how many distinct sessions has this scope banked since its last completed run", for every gate comparing it against `minSessionsBetweenRuns` and every sentence reporting it. The stored `count` includes the most recent harvest's session, and that one is never material to a decision taken on its own behalf: this bookkeeping also runs on the suspend path, so it may still be running, and at a terminal harvest its notes are the ones just written. Banked is one less than stored; an absent row banks nothing.
|
|
306
|
+
* Never re-derived — two gates comparing two numbers is how a run is admitted for a crossing that was never minted, and how a refusal reports a figure the recommendation notice contradicts. */
|
|
307
|
+
export declare function sessionsSinceExcludingCurrent(row: Pick<ConsolidationGateRow, "sessions"> | undefined): number;
|
|
305
308
|
/**
|
|
306
309
|
* §2.2 — the engine-minted session count (harvest-tail bookkeeping; ≤1 row write per terminal
|
|
307
310
|
* harvest; the OFF mode never calls this — D-9a zero-write). Returns the recommendation edge:
|
|
308
311
|
* `recommended` is true exactly ONCE per threshold crossing (once-per-越线沿; a completed run
|
|
309
312
|
* resets the edge).
|
|
313
|
+
*
|
|
314
|
+
* The ring records the calling session (dedup), so `count` INCLUDES the caller; the gate here and the number beside it are {@link sessionsSinceExcludingCurrent}, which takes it back out.
|
|
310
315
|
*/
|
|
311
316
|
export declare function recordConsolidationSession(controlDir: string, scope: string, sessionId: string, opts: ScreenedConsolidationOptions, now: () => number): {
|
|
312
317
|
recommended: boolean;
|
|
@@ -150,19 +150,23 @@ export function consolidationGateFileExists(controlDir) {
|
|
|
150
150
|
export function recommendationUnconsumed(row) {
|
|
151
151
|
return row.lastRecommendedAt !== undefined && (row.lastRunAt === undefined || row.lastRecommendedAt > row.lastRunAt);
|
|
152
152
|
}
|
|
153
|
+
export function sessionsSinceExcludingCurrent(row) {
|
|
154
|
+
return Math.max(0, (row?.sessions.count ?? 0) - 1);
|
|
155
|
+
}
|
|
153
156
|
export function recordConsolidationSession(controlDir, scope, sessionId, opts, now) {
|
|
154
157
|
return updateConsolidationGate(controlDir, (file) => {
|
|
155
158
|
const row = file.scopes[scope] ?? freshRow();
|
|
156
159
|
if (row.sessions.ring.includes(sessionId)) {
|
|
157
|
-
return { result: { recommended: false, sessionsSince: row
|
|
160
|
+
return { result: { recommended: false, sessionsSince: sessionsSinceExcludingCurrent(row) } };
|
|
158
161
|
}
|
|
159
162
|
row.sessions.count += 1;
|
|
160
163
|
row.sessions.ring.push(sessionId);
|
|
161
164
|
if (row.sessions.ring.length > CONSOLIDATION_SESSION_RING_MAX)
|
|
162
165
|
row.sessions.ring.splice(0, row.sessions.ring.length - CONSOLIDATION_SESSION_RING_MAX);
|
|
163
166
|
const at = now();
|
|
167
|
+
const banked = sessionsSinceExcludingCurrent(row);
|
|
164
168
|
const timeOpen = row.lastRunAt === undefined || at - row.lastRunAt >= opts.minRunIntervalMs;
|
|
165
|
-
const sessionsOpen =
|
|
169
|
+
const sessionsOpen = banked >= opts.minSessionsBetweenRuns;
|
|
166
170
|
const edgeArmed = !recommendationUnconsumed(row);
|
|
167
171
|
let recommended = false;
|
|
168
172
|
if (timeOpen && sessionsOpen && edgeArmed) {
|
|
@@ -170,7 +174,7 @@ export function recordConsolidationSession(controlDir, scope, sessionId, opts, n
|
|
|
170
174
|
row.lastRecommendedAt = at;
|
|
171
175
|
}
|
|
172
176
|
file.scopes[scope] = row;
|
|
173
|
-
return { next: file, result: { recommended, sessionsSince:
|
|
177
|
+
return { next: file, result: { recommended, sessionsSince: banked } };
|
|
174
178
|
});
|
|
175
179
|
}
|
|
176
180
|
export function recordActiveSetBaseline(controlDir, scope, size, ratioCap, _now) {
|