cursor-opencode-provider 0.7.5 → 0.8.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/README.md +9 -7
- package/dist/agent-url.js +2 -2
- package/dist/auth-renewal.d.ts +85 -0
- package/dist/auth-renewal.js +284 -0
- package/dist/auth.d.ts +38 -22
- package/dist/auth.js +79 -96
- package/dist/classic-tools.d.ts +15 -0
- package/dist/classic-tools.js +45 -0
- package/dist/context/build.d.ts +42 -5
- package/dist/context/build.js +96 -92
- package/dist/context/dynamic-catalog.d.ts +3 -28
- package/dist/context/dynamic-catalog.js +25 -130
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +37 -9
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +7 -4
- package/dist/context/paths.js +13 -9
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +4 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +27 -3
- package/dist/host-agent-mode.js +2 -2
- package/dist/image-save.js +3 -3
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +59 -8
- package/dist/language-model.js +458 -118
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/plan-execution-kickoff.js +2 -2
- package/dist/plugin-opencode2.js +10 -16
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +138 -109
- package/dist/protocol/blob-store.js +3 -2
- package/dist/protocol/conversation-bind.js +1 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +11 -1
- package/dist/protocol/conversation-state.d.ts +2 -0
- package/dist/protocol/conversation-state.js +19 -0
- package/dist/protocol/create-plan.js +2 -1
- package/dist/protocol/messages.js +22 -4
- package/dist/protocol/request.d.ts +11 -9
- package/dist/protocol/request.js +15 -25
- package/dist/protocol/tools.d.ts +17 -10
- package/dist/protocol/tools.js +53 -93
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/session.d.ts +9 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/package.json +1 -1
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/context/skills.d.ts +0 -11
- package/dist/context/skills.js +0 -110
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
export type AgentSkillLike = {
|
|
2
|
-
/** OpenCode skill id (frontmatter `name`, else directory name). */
|
|
3
|
-
id?: unknown;
|
|
4
|
-
full_path?: unknown;
|
|
5
|
-
description?: unknown;
|
|
6
|
-
content?: unknown;
|
|
7
|
-
};
|
|
8
1
|
/**
|
|
9
2
|
* Configured MCP servers that own at least one advertised tool. Uses the same
|
|
10
3
|
* server resolution as the RequestContext descriptors, so guidance never names
|
|
@@ -12,32 +5,14 @@ export type AgentSkillLike = {
|
|
|
12
5
|
* `sourceName` when present).
|
|
13
6
|
*/
|
|
14
7
|
export declare function listAdvertisedMcpServers(toolNames: Iterable<string>, knownMcpServers?: Iterable<string>): string[];
|
|
15
|
-
/** Skill id OpenCode's `skill` tool expects: the discovered id, else `…/skills/<id>/SKILL.md`. */
|
|
16
|
-
export declare function skillNameFromAgentSkill(skill: AgentSkillLike): string | undefined;
|
|
17
8
|
/**
|
|
18
9
|
* Shared system-guidance line: name advertised dynamic-catalog tools and prefer
|
|
19
10
|
* them over Grep/Shell when skills or project rules apply. Concrete skill ids
|
|
20
|
-
*
|
|
21
|
-
*
|
|
11
|
+
* stay out of the frozen baseline (they live in the host system prompt / skill
|
|
12
|
+
* tool, and in host `<system-update>` when the catalog changes — same split as
|
|
13
|
+
* OpenCode's SkillGuidance / SkillInstructions).
|
|
22
14
|
*/
|
|
23
15
|
export declare function buildDynamicCatalogRoutingInstruction(options: {
|
|
24
16
|
toolNames: Iterable<string>;
|
|
25
17
|
knownMcpServers?: Iterable<string>;
|
|
26
18
|
}): string | undefined;
|
|
27
|
-
/**
|
|
28
|
-
* Mid-Conversation nudge after RequestContext skills are known. Prefer skills
|
|
29
|
-
* that match the user turn; otherwise stay quiet unless the user asked about
|
|
30
|
-
* skills explicitly (avoids dumping dozens of firecrawl-* ids every turn).
|
|
31
|
-
* `hasSkillTool` must reflect what the host permits this turn, not the
|
|
32
|
-
* epoch-held advertisement.
|
|
33
|
-
*/
|
|
34
|
-
export declare function buildSkillCatalogNudge(options: {
|
|
35
|
-
hasSkillTool: boolean;
|
|
36
|
-
skills: readonly AgentSkillLike[];
|
|
37
|
-
userText?: string;
|
|
38
|
-
/**
|
|
39
|
-
* Host skill parameter key from the advertised schema.
|
|
40
|
-
* OpenCode 1.x: `name`. OpenCode 2.0: `id`.
|
|
41
|
-
*/
|
|
42
|
-
skillArgKey?: "name" | "id";
|
|
43
|
-
}): string | undefined;
|
|
@@ -2,43 +2,22 @@ import { resolveToolServerIdentity } from "../protocol/tools.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Issue #29: Cursor keeps OpenCode `skill` and MCP tools off its native
|
|
4
4
|
* top-level function list. Both OpenCode 1.x and OpenCode 2.0 reach the model
|
|
5
|
-
* as
|
|
6
|
-
* (exec `mcp_args`). This module builds the shared routing
|
|
7
|
-
*
|
|
5
|
+
* as names in RequestContext `mcp_meta_tool_options`, then GetDynamicTools /
|
|
6
|
+
* CallDynamicTool (exec `mcp_args`). This module builds the shared routing
|
|
7
|
+
* guidance for both hosts.
|
|
8
|
+
*
|
|
9
|
+
* Skill names and descriptions are not repeated here: OpenCode puts them in
|
|
10
|
+
* its own system prompt (`SystemPrompt.skills` / `SkillGuidance` /
|
|
11
|
+
* `SkillInstructions`) and announces catalog changes itself (OpenCode 2
|
|
12
|
+
* `<system-update>`).
|
|
8
13
|
*
|
|
9
14
|
* OpenCode 2 still needs `exposeDirectMcpTools` so MCP tools leave Code Mode
|
|
10
15
|
* and enter the AI SDK catalog; without that step they never reach this
|
|
11
16
|
* advertisement path at all.
|
|
12
17
|
*/
|
|
13
|
-
const MAX_NAMED_SKILLS = 8;
|
|
14
18
|
const MAX_MCP_SERVERS_IN_GUIDANCE = 8;
|
|
15
19
|
/** Synthetic server builtins and unknown tools are advertised under (`toolsToMcpDescriptors`). */
|
|
16
20
|
const DEFAULT_TOOL_SERVER = "opencode";
|
|
17
|
-
/** Common words that carry no skill-matching signal. */
|
|
18
|
-
const STOPWORDS = new Set([
|
|
19
|
-
"about", "all", "and", "any", "are", "but", "can", "for", "from", "has", "have",
|
|
20
|
-
"how", "into", "its", "not", "one", "only", "that", "the", "their", "them",
|
|
21
|
-
"then", "there", "these", "this", "use", "used", "uses", "using", "via", "was",
|
|
22
|
-
"what", "when", "which", "who", "why", "will", "with", "you", "your",
|
|
23
|
-
// Host/MCP instruction boilerplate — otherwise a server-instruction dump
|
|
24
|
-
// matches every firecrawl/search skill on "search"/"tools"/"server".
|
|
25
|
-
"tool", "tools", "server", "servers", "call", "calls", "fetch", "search",
|
|
26
|
-
"documentation", "library", "framework", "query", "please", "through",
|
|
27
|
-
]);
|
|
28
|
-
/** An explicit question/request about skills, not incidental prose ("has the skills to"). */
|
|
29
|
-
const SKILL_INQUIRY = /\b(?:which|what|list|show|available|any|use|using|call|invoke|load|run)\b[^.?!\n]{0,40}\bskills?\b|\bskills?\b[^.?!\n]{0,40}\b(?:available|installed|loaded|exist|do you have|can you)\b/i;
|
|
30
|
-
/** Description-only hits above this look like instruction-dump false positives. */
|
|
31
|
-
const MAX_DESCRIPTION_ONLY_MATCHES = 2;
|
|
32
|
-
/**
|
|
33
|
-
* Match against the live user utterance only. Mid-conversation / MCP instruction
|
|
34
|
-
* injections and prior reminders must not re-trigger skill dumps.
|
|
35
|
-
*/
|
|
36
|
-
function skillMatchCorpus(userText) {
|
|
37
|
-
return userText
|
|
38
|
-
.replace(/<system_reminder\b[^>]*>[\s\S]*?<\/system_reminder>/gi, " ")
|
|
39
|
-
.replace(/<system-update\b[^>]*>[\s\S]*?<\/system-update>/gi, " ")
|
|
40
|
-
.replace(/New MCP server instructions[\s\S]*?(?=\n[A-Z]|\n\n|$)/gi, " ");
|
|
41
|
-
}
|
|
42
21
|
function listWithOverflow(items, limit) {
|
|
43
22
|
const shown = items.slice(0, limit).map((item) => `\`${item}\``).join(", ");
|
|
44
23
|
const hidden = items.length - limit;
|
|
@@ -62,22 +41,12 @@ export function listAdvertisedMcpServers(toolNames, knownMcpServers = []) {
|
|
|
62
41
|
}
|
|
63
42
|
return [...servers];
|
|
64
43
|
}
|
|
65
|
-
/** Skill id OpenCode's `skill` tool expects: the discovered id, else `…/skills/<id>/SKILL.md`. */
|
|
66
|
-
export function skillNameFromAgentSkill(skill) {
|
|
67
|
-
const id = typeof skill.id === "string" ? skill.id.trim() : "";
|
|
68
|
-
if (id)
|
|
69
|
-
return id;
|
|
70
|
-
const fullPath = typeof skill.full_path === "string" ? skill.full_path.trim() : "";
|
|
71
|
-
const parts = fullPath.split(/[\\/]+/).filter(Boolean);
|
|
72
|
-
if (parts.length < 2 || parts.at(-1).toLowerCase() !== "skill.md")
|
|
73
|
-
return undefined;
|
|
74
|
-
return parts.at(-2);
|
|
75
|
-
}
|
|
76
44
|
/**
|
|
77
45
|
* Shared system-guidance line: name advertised dynamic-catalog tools and prefer
|
|
78
46
|
* them over Grep/Shell when skills or project rules apply. Concrete skill ids
|
|
79
|
-
*
|
|
80
|
-
*
|
|
47
|
+
* stay out of the frozen baseline (they live in the host system prompt / skill
|
|
48
|
+
* tool, and in host `<system-update>` when the catalog changes — same split as
|
|
49
|
+
* OpenCode's SkillGuidance / SkillInstructions).
|
|
81
50
|
*/
|
|
82
51
|
export function buildDynamicCatalogRoutingInstruction(options) {
|
|
83
52
|
const names = [...options.toolNames];
|
|
@@ -91,93 +60,19 @@ export function buildDynamicCatalogRoutingInstruction(options) {
|
|
|
91
60
|
if (mcpServers.length > 0) {
|
|
92
61
|
extras.push(`MCP servers such as ${listWithOverflow(mcpServers, MAX_MCP_SERVERS_IN_GUIDANCE)}`);
|
|
93
62
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
let distinctive = false;
|
|
108
|
-
for (const token of wordTokens(description)) {
|
|
109
|
-
if (!userTokens.has(token))
|
|
110
|
-
continue;
|
|
111
|
-
hits += 1;
|
|
112
|
-
if (token.length >= 8)
|
|
113
|
-
distinctive = true;
|
|
114
|
-
if (hits >= 2)
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
// Single distinctive token (library names, skill ids) is enough.
|
|
118
|
-
return distinctive;
|
|
119
|
-
}
|
|
120
|
-
function nameMatchesUserText(name, userText) {
|
|
121
|
-
const haystack = userText.toLowerCase();
|
|
122
|
-
const candidates = [name.toLowerCase(), ...name.toLowerCase().split(/[-_]+/).filter((part) => part.length >= 5)];
|
|
123
|
-
for (const candidate of [...new Set(candidates)]) {
|
|
124
|
-
const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
125
|
-
if (new RegExp(`(?<![a-z0-9_-])${escaped}(?![a-z0-9_-])`).test(haystack))
|
|
126
|
-
return true;
|
|
63
|
+
const lines = [
|
|
64
|
+
`- OpenCode host tools that are not in Cursor's native top-level list (including ${extras.join(" and ")}) ` +
|
|
65
|
+
"are reached through GetDynamicTools / CallDynamicTool (or the host's equivalent dynamic catalog). " +
|
|
66
|
+
"When a skill matches or project rules name an MCP server, discover and call those tools that way " +
|
|
67
|
+
"before Grep/Shell fallbacks. Do not narrate that they are unavailable.",
|
|
68
|
+
];
|
|
69
|
+
if (hasSkill) {
|
|
70
|
+
// Mirror OpenCode 1 SystemPrompt.skills / OC2 SkillInstructions.render.
|
|
71
|
+
lines.push("- Skills provide specialized instructions and workflows for specific tasks. " +
|
|
72
|
+
"Use the `skill` tool to load a skill when a task matches its description " +
|
|
73
|
+
"(the host system prompt and `skill` tool carry names and descriptions). " +
|
|
74
|
+
"A skill that is already present in the conversation as a `<skill_content>` block " +
|
|
75
|
+
"does not need to be invoked again.");
|
|
127
76
|
}
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Mid-Conversation nudge after RequestContext skills are known. Prefer skills
|
|
132
|
-
* that match the user turn; otherwise stay quiet unless the user asked about
|
|
133
|
-
* skills explicitly (avoids dumping dozens of firecrawl-* ids every turn).
|
|
134
|
-
* `hasSkillTool` must reflect what the host permits this turn, not the
|
|
135
|
-
* epoch-held advertisement.
|
|
136
|
-
*/
|
|
137
|
-
export function buildSkillCatalogNudge(options) {
|
|
138
|
-
if (!options.hasSkillTool || options.skills.length === 0)
|
|
139
|
-
return undefined;
|
|
140
|
-
const userText = skillMatchCorpus(options.userText ?? "");
|
|
141
|
-
const userTokens = wordTokens(userText);
|
|
142
|
-
const skillInquiry = SKILL_INQUIRY.test(userText);
|
|
143
|
-
const named = options.skills
|
|
144
|
-
.map((skill) => {
|
|
145
|
-
const name = skillNameFromAgentSkill(skill);
|
|
146
|
-
if (!name)
|
|
147
|
-
return undefined;
|
|
148
|
-
const description = typeof skill.description === "string" ? skill.description : "";
|
|
149
|
-
const nameHit = nameMatchesUserText(name, userText);
|
|
150
|
-
const descHit = !!description && descriptionMatchesUserText(description, userTokens);
|
|
151
|
-
return { name, nameHit, descHit };
|
|
152
|
-
})
|
|
153
|
-
.filter((row) => !!row);
|
|
154
|
-
if (named.length === 0)
|
|
155
|
-
return undefined;
|
|
156
|
-
const nameMatched = named.filter((row) => row.nameHit);
|
|
157
|
-
const descOnly = named.filter((row) => row.descHit && !row.nameHit);
|
|
158
|
-
// Mass description-only hits are almost always instruction-dump noise
|
|
159
|
-
// (MCP server blurbs sharing "search"/"tools" with firecrawl skills).
|
|
160
|
-
const descMatched = descOnly.length <= MAX_DESCRIPTION_ONLY_MATCHES ? descOnly : [];
|
|
161
|
-
const selected = nameMatched.length > 0
|
|
162
|
-
? [...nameMatched, ...descMatched]
|
|
163
|
-
: skillInquiry
|
|
164
|
-
? named
|
|
165
|
-
: descMatched;
|
|
166
|
-
if (selected.length === 0)
|
|
167
|
-
return undefined;
|
|
168
|
-
const ids = [...new Set(selected.map((row) => row.name))];
|
|
169
|
-
// Prefer the advertised schema key; fall back to naming both so a stale
|
|
170
|
-
// Mid-Conversation hint cannot contradict GetDynamicTools on either host.
|
|
171
|
-
const argKey = options.skillArgKey;
|
|
172
|
-
const callHint = argKey
|
|
173
|
-
? `Call \`skill\` with \`{ "${argKey}": "<skill-id>" }\` (host schema key is \`${argKey}\`) `
|
|
174
|
-
: "Call `skill` with the skill identifier using the parameter key from GetDynamicTools " +
|
|
175
|
-
"(`name` on OpenCode 1.x, `id` on OpenCode 2.0) ";
|
|
176
|
-
return ("<system_reminder>\n" +
|
|
177
|
-
"OpenCode skills are invoked with the host `skill` tool through GetDynamicTools / CallDynamicTool " +
|
|
178
|
-
`(MCP server \`${DEFAULT_TOOL_SERVER}\`, tool \`skill\`), not by Grep/Read of SKILL.md. ` +
|
|
179
|
-
`Relevant skill id(s) this turn: ${listWithOverflow(ids, MAX_NAMED_SKILLS)}. ` +
|
|
180
|
-
callHint +
|
|
181
|
-
"before answering from memory or falling back to shell/search tools.\n" +
|
|
182
|
-
"</system_reminder>");
|
|
77
|
+
return lines.join("\n");
|
|
183
78
|
}
|
package/dist/context/epoch.d.ts
CHANGED
|
@@ -7,10 +7,10 @@ export type ContextSourceSnapshot = {
|
|
|
7
7
|
};
|
|
8
8
|
export type ContextEpoch = {
|
|
9
9
|
conversationId: string;
|
|
10
|
-
/** Exact system text
|
|
10
|
+
/** Exact system text at epoch start. Empty if a legacy checkpoint has no persisted rule. */
|
|
11
11
|
baselineSystemPrompt: string;
|
|
12
12
|
baselineHash: string;
|
|
13
|
-
/** True when restart hydrated past a checkpoint without the
|
|
13
|
+
/** True when restart hydrated past a checkpoint without the last source snapshot. */
|
|
14
14
|
recovered: boolean;
|
|
15
15
|
snapshot: ContextSourceSnapshot;
|
|
16
16
|
};
|
|
@@ -27,6 +27,8 @@ export type AdmitContextEpochInput = {
|
|
|
27
27
|
* never folded into the frozen baseline.
|
|
28
28
|
*/
|
|
29
29
|
oneShotReminders?: readonly string[];
|
|
30
|
+
/** Original system rule restored with RequestContext after a restart. */
|
|
31
|
+
recoveredBaseline?: string;
|
|
30
32
|
};
|
|
31
33
|
export type AdmitContextEpochResult = {
|
|
32
34
|
action: "initialize" | "unchanged" | "updated" | "recovered";
|
|
@@ -42,8 +44,8 @@ export type AdmitContextEpochResult = {
|
|
|
42
44
|
* - First seed: freeze baseline, return it as seedSystemPrompt; one-shots → mid.
|
|
43
45
|
* - Checkpoint turn: never returns seedSystemPrompt; source diffs + one-shots → mid.
|
|
44
46
|
* - Reseed same epoch: return frozen baseline bytes (not live host text).
|
|
45
|
-
* - Recovered
|
|
46
|
-
*
|
|
47
|
+
* - Recovered: restore the persisted rule and reassert live context once on the
|
|
48
|
+
* user turn. A legacy checkpoint without a rule has no original bytes.
|
|
47
49
|
*/
|
|
48
50
|
export declare function admitContextEpoch(input: AdmitContextEpochInput): AdmitContextEpochResult;
|
|
49
51
|
export declare function getContextEpoch(conversationId: string): ContextEpoch | undefined;
|
package/dist/context/epoch.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* Mirrors OpenCode research CONTEXT.md:
|
|
5
5
|
* - One Baseline System Context per conversation_id (provider-cache prefix)
|
|
6
6
|
* - Later Context Source changes → Mid-Conversation System Message on the
|
|
7
|
-
* user turn
|
|
7
|
+
* user turn; the baseline itself never changes within the epoch
|
|
8
8
|
* - Epoch ends on conversation remint (compaction / post-compaction rebase)
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The baseline reaches Cursor as the frozen system-instructions rule in
|
|
11
|
+
* RequestContext (`systemInstructionsRule`), on every Run. `seedSystemPrompt`
|
|
12
|
+
* marks the Runs that start a conversation; it is not sent as a `system`
|
|
13
|
+
* message, which Cursor does not follow.
|
|
12
14
|
*/
|
|
13
15
|
import { createHash } from "node:crypto";
|
|
14
16
|
import { trace } from "../debug.js";
|
|
@@ -80,8 +82,8 @@ function combineMessages(parts) {
|
|
|
80
82
|
* - First seed: freeze baseline, return it as seedSystemPrompt; one-shots → mid.
|
|
81
83
|
* - Checkpoint turn: never returns seedSystemPrompt; source diffs + one-shots → mid.
|
|
82
84
|
* - Reseed same epoch: return frozen baseline bytes (not live host text).
|
|
83
|
-
* - Recovered
|
|
84
|
-
*
|
|
85
|
+
* - Recovered: restore the persisted rule and reassert live context once on the
|
|
86
|
+
* user turn. A legacy checkpoint without a rule has no original bytes.
|
|
85
87
|
*/
|
|
86
88
|
export function admitContextEpoch(input) {
|
|
87
89
|
const conversationId = input.conversationId;
|
|
@@ -90,17 +92,27 @@ export function admitContextEpoch(input) {
|
|
|
90
92
|
const existing = byConversationId.get(conversationId);
|
|
91
93
|
if (!existing) {
|
|
92
94
|
if (input.hasCheckpoint) {
|
|
93
|
-
// Restart / soft-evict recovery:
|
|
94
|
-
//
|
|
95
|
+
// Restart / soft-evict recovery: keep the original rule and reassert the
|
|
96
|
+
// live instructions chronologically without changing the cache prefix.
|
|
97
|
+
const baselineSystemPrompt = input.recoveredBaseline ?? "";
|
|
95
98
|
const epoch = {
|
|
96
99
|
conversationId,
|
|
97
|
-
baselineSystemPrompt
|
|
98
|
-
baselineHash: "",
|
|
100
|
+
baselineSystemPrompt,
|
|
101
|
+
baselineHash: baselineSystemPrompt ? sha(baselineSystemPrompt) : "",
|
|
99
102
|
recovered: true,
|
|
100
103
|
snapshot: nextSnap,
|
|
101
104
|
};
|
|
102
105
|
touch(epoch);
|
|
103
|
-
|
|
106
|
+
// The persisted rule is the original prefix, not necessarily the current
|
|
107
|
+
// host instructions. Reconcile live context once before recording its
|
|
108
|
+
// snapshot; otherwise a changed prompt is silently treated as admitted.
|
|
109
|
+
// Even equality with the original baseline needs an update: the last
|
|
110
|
+
// checkpoint may contain an intervening mode/instruction change.
|
|
111
|
+
const liveBaseline = buildBaseline(input);
|
|
112
|
+
const recoveryUpdate = baselineSystemPrompt && liveBaseline
|
|
113
|
+
? wrapReminder("Current host system instructions after session recovery:\n\n" + liveBaseline)
|
|
114
|
+
: "";
|
|
115
|
+
const midConversationMessage = combineMessages([recoveryUpdate, ...oneShots]);
|
|
104
116
|
trace(`context epoch: recovered conversationId=${conversationId} ` +
|
|
105
117
|
`oneShots=${oneShots.length}`);
|
|
106
118
|
return { action: "recovered", midConversationMessage, epoch };
|
package/dist/context/frozen.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, } from "./build.js";
|
|
1
|
+
import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, withSystemInstructions, } from "./build.js";
|
|
2
2
|
import { clearContextEpoch, endContextEpoch, resetContextEpochsForTests } from "./epoch.js";
|
|
3
3
|
import { clearOverlayHold, resetOverlayHoldsForTests, transferOverlayHold, } from "./overlay.js";
|
|
4
4
|
import { trace } from "../debug.js";
|
|
@@ -10,10 +10,10 @@ import { encodeMessage } from "../protocol/messages.js";
|
|
|
10
10
|
* base plus live plugin/tool overlays. Rebuilding volatile git/layout data on
|
|
11
11
|
* every Run shifts the prompt prefix and tanks prompt-cache hits.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* each Run, then epoch-held (equal ids keep frozen bytes; new ids
|
|
15
|
-
* overlaid on that base. If the encoded overlay bytes did not
|
|
16
|
-
* prior materialized object is reused.
|
|
13
|
+
* Host-advertised subagents, plugin metadata, and tool/MCP capabilities are
|
|
14
|
+
* rediscovered each Run, then epoch-held (equal ids keep frozen bytes; new ids
|
|
15
|
+
* append) and overlaid on that base. If the encoded overlay bytes did not
|
|
16
|
+
* change, the exact prior materialized object is reused.
|
|
17
17
|
*/
|
|
18
18
|
const byConversationId = new Map();
|
|
19
19
|
const materializedByConversationId = new Map();
|
|
@@ -84,7 +84,7 @@ export function transferFrozenRequestContext(previousConversationId, nextConvers
|
|
|
84
84
|
const base = byConversationId.get(previousConversationId);
|
|
85
85
|
const materialized = materializedByConversationId.get(previousConversationId);
|
|
86
86
|
// System Context epoch does not transfer — compaction starts a fresh baseline.
|
|
87
|
-
// Overlay hold does transfer: same workspace, same advertised
|
|
87
|
+
// Overlay hold does transfer: same workspace, same advertised agent/plugin
|
|
88
88
|
// bytes, so the comparison seed can still match. clearFrozenRequestContext
|
|
89
89
|
// also drops epoch state for each id.
|
|
90
90
|
endContextEpoch(previousConversationId, nextConversationId);
|
|
@@ -120,6 +120,23 @@ function sameBytes(a, b) {
|
|
|
120
120
|
return false;
|
|
121
121
|
return true;
|
|
122
122
|
}
|
|
123
|
+
function advertisedMetaToolCount(context) {
|
|
124
|
+
const meta = context.mcp_meta_tool_options;
|
|
125
|
+
if (!meta || typeof meta !== "object")
|
|
126
|
+
return 0;
|
|
127
|
+
const descriptors = meta.mcp_descriptors;
|
|
128
|
+
if (!Array.isArray(descriptors))
|
|
129
|
+
return 0;
|
|
130
|
+
let count = 0;
|
|
131
|
+
for (const descriptor of descriptors) {
|
|
132
|
+
if (!descriptor || typeof descriptor !== "object")
|
|
133
|
+
continue;
|
|
134
|
+
const tools = descriptor.tools;
|
|
135
|
+
if (Array.isArray(tools))
|
|
136
|
+
count += tools.length;
|
|
137
|
+
}
|
|
138
|
+
return count;
|
|
139
|
+
}
|
|
123
140
|
function rememberMaterialized(conversationId, context) {
|
|
124
141
|
const bytes = encodeMessage("RequestContext", context);
|
|
125
142
|
const previous = materializedByConversationId.get(conversationId);
|
|
@@ -143,12 +160,23 @@ export async function getOrBuildRequestContext(conversationId, input, opts) {
|
|
|
143
160
|
if (opts?.refresh && conversationId)
|
|
144
161
|
clearOverlayHold(conversationId);
|
|
145
162
|
if (!opts?.refresh && conversationId) {
|
|
146
|
-
|
|
163
|
+
let base = getFrozenRequestContext(conversationId);
|
|
147
164
|
if (base) {
|
|
165
|
+
// The system-instructions rule is frozen with the base. A new epoch
|
|
166
|
+
// baseline (compaction rebase, binding reset) replaces it; a recovered
|
|
167
|
+
// epoch only fills a base persisted without one.
|
|
168
|
+
const instructed = withSystemInstructions(base, input.systemInstructions);
|
|
169
|
+
if (instructed !== base) {
|
|
170
|
+
setFrozenRequestContext(conversationId, instructed);
|
|
171
|
+
base = getFrozenRequestContext(conversationId);
|
|
172
|
+
trace(`request_context: system instructions frozen conversationId=${conversationId} ` +
|
|
173
|
+
`len=${input.systemInstructions?.text.length ?? 0} ` +
|
|
174
|
+
`authoritative=${input.systemInstructions?.authoritative ?? false}`);
|
|
175
|
+
}
|
|
148
176
|
const dynamic = await buildDynamicRequestContext(scoped);
|
|
149
177
|
const materialized = rememberMaterialized(conversationId, materializeRequestContext(base, dynamic));
|
|
150
178
|
trace(`request_context: materialized conversationId=${conversationId} ` +
|
|
151
|
-
`tools=${
|
|
179
|
+
`tools=${advertisedMetaToolCount(materialized.context)} ` +
|
|
152
180
|
`reused=${materialized.reused}`);
|
|
153
181
|
return materialized;
|
|
154
182
|
}
|
|
@@ -182,7 +210,7 @@ export async function getOrBuildRequestContext(conversationId, input, opts) {
|
|
|
182
210
|
? rememberMaterialized(conversationId, context)
|
|
183
211
|
: { context: freezeSnapshot(structuredClone(context)), reused: false };
|
|
184
212
|
trace(`request_context: built+frozen conversationId=${conversationId || "(none)"} ` +
|
|
185
|
-
`tools=${
|
|
213
|
+
`tools=${advertisedMetaToolCount(materialized.context)} ` +
|
|
186
214
|
`refresh=${!!opts?.refresh}`);
|
|
187
215
|
return { context: materialized.context, reused: false };
|
|
188
216
|
}
|
package/dist/context/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, type BuildRequestContextInput, } from "./build.js";
|
|
2
|
-
export { buildDynamicCatalogRoutingInstruction,
|
|
2
|
+
export { buildDynamicCatalogRoutingInstruction, listAdvertisedMcpServers, } from "./dynamic-catalog.js";
|
|
3
3
|
export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
|
|
4
4
|
export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js";
|
|
5
5
|
export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, type AdmitContextEpochInput, type AdmitContextEpochResult, type ContextEpoch, type ContextSourceSnapshot, } from "./epoch.js";
|
package/dist/context/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, } from "./build.js";
|
|
2
|
-
export { buildDynamicCatalogRoutingInstruction,
|
|
2
|
+
export { buildDynamicCatalogRoutingInstruction, listAdvertisedMcpServers, } from "./dynamic-catalog.js";
|
|
3
3
|
export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
|
|
4
4
|
export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js";
|
|
5
5
|
export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, } from "./epoch.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Epoch-hold live RequestContext overlay lists (
|
|
2
|
+
* Epoch-hold live RequestContext overlay lists (subagents, plugins).
|
|
3
3
|
*
|
|
4
4
|
* Same policy as the tool catalog: first nonempty freeze is UTF-16 by id,
|
|
5
5
|
* equal ids keep frozen bytes (including content), new ids append at the
|
|
@@ -7,12 +7,6 @@
|
|
|
7
7
|
* Run; this only canonicalizes what is advertised.
|
|
8
8
|
*/
|
|
9
9
|
export declare const MAX_OVERLAY_HOLDS = 256;
|
|
10
|
-
export type OverlaySkill = {
|
|
11
|
-
id: string;
|
|
12
|
-
full_path: string;
|
|
13
|
-
content: string;
|
|
14
|
-
description: string;
|
|
15
|
-
};
|
|
16
10
|
export type OverlaySubagent = {
|
|
17
11
|
full_path: string;
|
|
18
12
|
name: string;
|
|
@@ -24,26 +18,11 @@ export type OverlayPlugin = {
|
|
|
24
18
|
line: string;
|
|
25
19
|
};
|
|
26
20
|
export type OverlayHold = {
|
|
27
|
-
skills: OverlaySkill[];
|
|
28
|
-
subagents: OverlaySubagent[];
|
|
29
|
-
plugins: OverlayPlugin[];
|
|
30
|
-
};
|
|
31
|
-
export type OverlayWire = {
|
|
32
|
-
skills: Array<{
|
|
33
|
-
full_path: string;
|
|
34
|
-
content: string;
|
|
35
|
-
description: string;
|
|
36
|
-
}>;
|
|
37
21
|
subagents: OverlaySubagent[];
|
|
38
22
|
plugins: OverlayPlugin[];
|
|
39
23
|
};
|
|
40
24
|
/** Merge live discovery into the conversation overlay epoch. */
|
|
41
|
-
export declare function holdCapabilityOverlay(conversationId: string, live: OverlayHold):
|
|
42
|
-
/**
|
|
43
|
-
* Epoch-held skills with their OpenCode ids. The wire shape drops `id`
|
|
44
|
-
* (`AgentSkill` has no name field), but the host `skill` tool needs it.
|
|
45
|
-
*/
|
|
46
|
-
export declare function getHeldOverlaySkills(conversationId: string): OverlaySkill[];
|
|
25
|
+
export declare function holdCapabilityOverlay(conversationId: string, live: OverlayHold): OverlayHold;
|
|
47
26
|
export declare function clearOverlayHold(conversationId: string): void;
|
|
48
27
|
export declare function transferOverlayHold(previousConversationId: string, nextConversationId: string): void;
|
|
49
28
|
export declare function resetOverlayHoldsForTests(): void;
|
package/dist/context/overlay.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Epoch-hold live RequestContext overlay lists (
|
|
2
|
+
* Epoch-hold live RequestContext overlay lists (subagents, plugins).
|
|
3
3
|
*
|
|
4
4
|
* Same policy as the tool catalog: first nonempty freeze is UTF-16 by id,
|
|
5
5
|
* equal ids keep frozen bytes (including content), new ids append at the
|
|
@@ -29,7 +29,6 @@ function holdList(cached, incoming, idOf) {
|
|
|
29
29
|
}
|
|
30
30
|
function remember(conversationId, hold) {
|
|
31
31
|
const stored = {
|
|
32
|
-
skills: structuredClone(hold.skills),
|
|
33
32
|
subagents: structuredClone(hold.subagents),
|
|
34
33
|
plugins: structuredClone(hold.plugins),
|
|
35
34
|
};
|
|
@@ -43,47 +42,24 @@ function remember(conversationId, hold) {
|
|
|
43
42
|
}
|
|
44
43
|
return stored;
|
|
45
44
|
}
|
|
46
|
-
function skillId(skill) {
|
|
47
|
-
return skill.id || skill.full_path;
|
|
48
|
-
}
|
|
49
|
-
function toWire(hold) {
|
|
50
|
-
return {
|
|
51
|
-
skills: hold.skills.map(({ full_path, content, description }) => ({
|
|
52
|
-
full_path,
|
|
53
|
-
content,
|
|
54
|
-
description,
|
|
55
|
-
})),
|
|
56
|
-
subagents: hold.subagents,
|
|
57
|
-
plugins: hold.plugins,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
45
|
/** Merge live discovery into the conversation overlay epoch. */
|
|
61
46
|
export function holdCapabilityOverlay(conversationId, live) {
|
|
62
47
|
if (!conversationId) {
|
|
63
|
-
return
|
|
64
|
-
skills: inFixedOrder(live.skills, skillId),
|
|
48
|
+
return {
|
|
65
49
|
subagents: inFixedOrder(live.subagents, (agent) => agent.name),
|
|
66
50
|
plugins: inFixedOrder(live.plugins, (plugin) => plugin.id),
|
|
67
|
-
}
|
|
51
|
+
};
|
|
68
52
|
}
|
|
69
53
|
const cached = byConversationId.get(conversationId);
|
|
70
54
|
const next = {
|
|
71
|
-
skills: holdList(cached?.skills, live.skills, skillId),
|
|
72
55
|
subagents: holdList(cached?.subagents, live.subagents, (agent) => agent.name),
|
|
73
56
|
plugins: holdList(cached?.plugins, live.plugins, (plugin) => plugin.id),
|
|
74
57
|
};
|
|
75
|
-
if (next.
|
|
58
|
+
if (next.subagents.length === 0 && next.plugins.length === 0) {
|
|
76
59
|
byConversationId.delete(conversationId);
|
|
77
|
-
return
|
|
60
|
+
return next;
|
|
78
61
|
}
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Epoch-held skills with their OpenCode ids. The wire shape drops `id`
|
|
83
|
-
* (`AgentSkill` has no name field), but the host `skill` tool needs it.
|
|
84
|
-
*/
|
|
85
|
-
export function getHeldOverlaySkills(conversationId) {
|
|
86
|
-
return structuredClone(byConversationId.get(conversationId)?.skills ?? []);
|
|
62
|
+
return remember(conversationId, next);
|
|
87
63
|
}
|
|
88
64
|
export function clearOverlayHold(conversationId) {
|
|
89
65
|
byConversationId.delete(conversationId);
|
package/dist/context/paths.d.ts
CHANGED
|
@@ -21,8 +21,12 @@ export declare function setHostCacheDirOverride(dir: string | undefined): void;
|
|
|
21
21
|
export declare function getHostCacheDirOverride(): string | undefined;
|
|
22
22
|
/** Resolve the native OpenCode cache root when no host bridge is installed. */
|
|
23
23
|
export declare function resolveHostCacheDir(env?: HostPathEnv): string;
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Native OpenCode global config dir: `$XDG_CONFIG_HOME/opencode`, otherwise
|
|
26
|
+
* `~/.config/opencode` — OpenCode's `Global.Path.config` (`xdg-basedir`
|
|
27
|
+
* `xdgConfig`, `packages/core/src/global.ts`), the same in 1.x and 2.0.
|
|
28
|
+
*/
|
|
29
|
+
export declare function opencodeGlobalConfigDir(env?: HostPathEnv): string;
|
|
26
30
|
/**
|
|
27
31
|
* Host global cache dir for Cursor project metadata + model/version caches.
|
|
28
32
|
*
|
|
@@ -57,7 +61,7 @@ export declare function hostGlobalDataDir(env?: HostPathEnv): string;
|
|
|
57
61
|
*/
|
|
58
62
|
export declare function hostPlansDir(_workspaceRoot?: string): string;
|
|
59
63
|
/**
|
|
60
|
-
* Cursor-compatible path slug (`/
|
|
64
|
+
* Cursor-compatible path slug (`/workspace/a/b` → `workspace-a-b`).
|
|
61
65
|
* Used for per-workspace metadata under the host cache.
|
|
62
66
|
*/
|
|
63
67
|
export declare function slugifyWorkspacePath(workspaceRoot: string): string;
|
|
@@ -72,4 +76,3 @@ export declare function slugifyWorkspacePath(workspaceRoot: string): string;
|
|
|
72
76
|
export declare function opencodeProjectDir(workspaceRoot: string): string;
|
|
73
77
|
/** Ensure {@link opencodeProjectDir} exists (mode 0o700) and return it. */
|
|
74
78
|
export declare function ensureOpencodeProjectDir(workspaceRoot: string): string;
|
|
75
|
-
export declare function resolveHomeRelative(p: string): string;
|
package/dist/context/paths.js
CHANGED
|
@@ -68,9 +68,18 @@ export function getHostCacheDirOverride() {
|
|
|
68
68
|
export function resolveHostCacheDir(env = process.env) {
|
|
69
69
|
return bridgeGlobalCacheDir() ?? openCodeGlobalCacheDir(env);
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
function xdgConfigHome(env = process.env) {
|
|
72
|
+
if (env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0)
|
|
73
|
+
return env.XDG_CONFIG_HOME;
|
|
74
|
+
return path.join(resolveHome(env), ".config");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Native OpenCode global config dir: `$XDG_CONFIG_HOME/opencode`, otherwise
|
|
78
|
+
* `~/.config/opencode` — OpenCode's `Global.Path.config` (`xdg-basedir`
|
|
79
|
+
* `xdgConfig`, `packages/core/src/global.ts`), the same in 1.x and 2.0.
|
|
80
|
+
*/
|
|
81
|
+
export function opencodeGlobalConfigDir(env = process.env) {
|
|
82
|
+
return path.join(xdgConfigHome(env), "opencode");
|
|
74
83
|
}
|
|
75
84
|
/**
|
|
76
85
|
* Host global cache dir for Cursor project metadata + model/version caches.
|
|
@@ -116,7 +125,7 @@ export function hostPlansDir(_workspaceRoot) {
|
|
|
116
125
|
return path.join(hostGlobalDataDir(), "plans");
|
|
117
126
|
}
|
|
118
127
|
/**
|
|
119
|
-
* Cursor-compatible path slug (`/
|
|
128
|
+
* Cursor-compatible path slug (`/workspace/a/b` → `workspace-a-b`).
|
|
120
129
|
* Used for per-workspace metadata under the host cache.
|
|
121
130
|
*/
|
|
122
131
|
export function slugifyWorkspacePath(workspaceRoot) {
|
|
@@ -157,8 +166,3 @@ export function ensureOpencodeProjectDir(workspaceRoot) {
|
|
|
157
166
|
`xdg_cache_home=${process.env.XDG_CACHE_HOME ?? "(unset)"}`);
|
|
158
167
|
return dir;
|
|
159
168
|
}
|
|
160
|
-
export function resolveHomeRelative(p) {
|
|
161
|
-
if (p.startsWith("~/"))
|
|
162
|
-
return path.join(homedir(), p.slice(2));
|
|
163
|
-
return p;
|
|
164
|
-
}
|