cursor-opencode-provider 0.7.6 → 0.8.1

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +36 -15
  3. package/SECURITY.md +43 -7
  4. package/dist/agent-url.js +2 -2
  5. package/dist/auth-renewal.d.ts +85 -0
  6. package/dist/auth-renewal.js +284 -0
  7. package/dist/auth.d.ts +38 -22
  8. package/dist/auth.js +79 -96
  9. package/dist/classic-tools.d.ts +15 -0
  10. package/dist/classic-tools.js +45 -0
  11. package/dist/context/build.d.ts +62 -7
  12. package/dist/context/build.js +127 -95
  13. package/dist/context/dynamic-catalog.d.ts +2 -58
  14. package/dist/context/dynamic-catalog.js +16 -161
  15. package/dist/context/epoch.d.ts +6 -4
  16. package/dist/context/epoch.js +22 -10
  17. package/dist/context/frozen.js +42 -15
  18. package/dist/context/index.d.ts +3 -1
  19. package/dist/context/index.js +3 -1
  20. package/dist/context/overlay.d.ts +2 -23
  21. package/dist/context/overlay.js +6 -30
  22. package/dist/context/paths.d.ts +51 -21
  23. package/dist/context/paths.js +78 -27
  24. package/dist/context/rules.d.ts +3 -15
  25. package/dist/context/rules.js +10 -186
  26. package/dist/context/skills-bridge.d.ts +22 -0
  27. package/dist/context/skills-bridge.js +20 -0
  28. package/dist/context/skills.d.ts +31 -8
  29. package/dist/context/skills.js +187 -86
  30. package/dist/debug.d.ts +2 -0
  31. package/dist/debug.js +4 -0
  32. package/dist/errors.d.ts +13 -0
  33. package/dist/errors.js +27 -3
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +119 -15
  36. package/dist/host-plan-file.d.ts +28 -0
  37. package/dist/host-plan-file.js +76 -0
  38. package/dist/image-input.d.ts +18 -0
  39. package/dist/image-input.js +119 -28
  40. package/dist/image-save.d.ts +10 -2
  41. package/dist/image-save.js +35 -23
  42. package/dist/index.d.ts +10 -0
  43. package/dist/language-model.d.ts +86 -25
  44. package/dist/language-model.js +1295 -369
  45. package/dist/model-config.d.ts +9 -0
  46. package/dist/model-config.js +28 -1
  47. package/dist/opencode2/catalog.d.ts +1 -0
  48. package/dist/opencode2/catalog.js +2 -0
  49. package/dist/opencode2/image-save-tool.d.ts +8 -0
  50. package/dist/opencode2/image-save-tool.js +84 -0
  51. package/dist/opencode2/integration.d.ts +30 -7
  52. package/dist/opencode2/integration.js +75 -46
  53. package/dist/opencode2/types.d.ts +15 -0
  54. package/dist/parallel-step.d.ts +65 -0
  55. package/dist/parallel-step.js +102 -0
  56. package/dist/plugin-opencode2.js +58 -56
  57. package/dist/plugin.d.ts +0 -10
  58. package/dist/plugin.js +207 -127
  59. package/dist/pricing-data.d.ts +14 -1
  60. package/dist/pricing-data.js +14 -1
  61. package/dist/protocol/ask-question.d.ts +12 -1
  62. package/dist/protocol/ask-question.js +77 -3
  63. package/dist/protocol/blob-store.js +3 -2
  64. package/dist/protocol/conversation-bind.js +1 -1
  65. package/dist/protocol/conversation-persistence.d.ts +2 -0
  66. package/dist/protocol/conversation-persistence.js +11 -1
  67. package/dist/protocol/conversation-state.d.ts +2 -0
  68. package/dist/protocol/conversation-state.js +19 -0
  69. package/dist/protocol/create-plan.d.ts +47 -25
  70. package/dist/protocol/create-plan.js +35 -24
  71. package/dist/protocol/interactions.d.ts +26 -17
  72. package/dist/protocol/interactions.js +80 -31
  73. package/dist/protocol/messages.js +96 -18
  74. package/dist/protocol/request.d.ts +16 -9
  75. package/dist/protocol/request.js +17 -25
  76. package/dist/protocol/switch-mode.d.ts +66 -27
  77. package/dist/protocol/switch-mode.js +147 -73
  78. package/dist/protocol/token-details.d.ts +2 -0
  79. package/dist/protocol/token-details.js +11 -1
  80. package/dist/protocol/tool-call-bridge.d.ts +4 -0
  81. package/dist/protocol/tool-call-bridge.js +14 -0
  82. package/dist/protocol/tools.d.ts +33 -11
  83. package/dist/protocol/tools.js +165 -103
  84. package/dist/protocol/turn-provenance.d.ts +52 -0
  85. package/dist/protocol/turn-provenance.js +174 -0
  86. package/dist/replay-safety.d.ts +6 -0
  87. package/dist/replay-safety.js +56 -6
  88. package/dist/session.d.ts +44 -2
  89. package/dist/session.js +45 -1
  90. package/dist/shared.d.ts +7 -0
  91. package/dist/shared.js +7 -0
  92. package/dist/todo-store.d.ts +2 -0
  93. package/dist/todo-store.js +19 -0
  94. package/dist/transport/connect.d.ts +2 -0
  95. package/dist/transport/connect.js +23 -6
  96. package/dist/usage.js +23 -6
  97. package/package.json +3 -2
  98. package/dist/context/agents.d.ts +0 -12
  99. package/dist/context/agents.js +0 -136
  100. package/dist/plan-execution-kickoff.d.ts +0 -57
  101. package/dist/plan-execution-kickoff.js +0 -186
@@ -2,15 +2,14 @@ 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 MCP-shaped RequestContext ads, then GetDynamicTools / CallDynamicTool
6
- * (exec `mcp_args`). This module builds the shared routing guidance and the
7
- * OpenCode-shaped Mid-Conversation skill-catalog update used on both hosts.
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
8
  *
9
- * OpenCode itself does **not** per-turn nudge matched skill ids. It:
10
- * - Puts name/description guidance in the frozen baseline (`SystemPrompt.skills`
11
- * / `SkillGuidance` / `SkillInstructions`)
12
- * - Emits a Mid-Conversation update only when the available-skills list changes
13
- * - OpenCode 2: skips re-invoke when `<skill_content>` is already in the turn
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>`).
14
13
  *
15
14
  * OpenCode 2 still needs `exposeDirectMcpTools` so MCP tools leave Code Mode
16
15
  * and enter the AI SDK catalog; without that step they never reach this
@@ -19,12 +18,6 @@ import { resolveToolServerIdentity } from "../protocol/tools.js";
19
18
  const MAX_MCP_SERVERS_IN_GUIDANCE = 8;
20
19
  /** Synthetic server builtins and unknown tools are advertised under (`toolsToMcpDescriptors`). */
21
20
  const DEFAULT_TOOL_SERVER = "opencode";
22
- /**
23
- * Skill ids already admitted to the model for a conversation (baseline RequestContext
24
- * freeze, or the last Mid-Conversation catalog update). First admission is silent —
25
- * OpenCode puts that list in the system baseline, not a per-turn reminder.
26
- */
27
- const admittedSkillIdsByConversation = new Map();
28
21
  function listWithOverflow(items, limit) {
29
22
  const shown = items.slice(0, limit).map((item) => `\`${item}\``).join(", ");
30
23
  const hidden = items.length - limit;
@@ -48,42 +41,11 @@ export function listAdvertisedMcpServers(toolNames, knownMcpServers = []) {
48
41
  }
49
42
  return [...servers];
50
43
  }
51
- /** Skill id OpenCode's `skill` tool expects: the discovered id, else `…/skills/<id>/SKILL.md`. */
52
- export function skillNameFromAgentSkill(skill) {
53
- const id = typeof skill.id === "string" ? skill.id.trim() : "";
54
- if (id)
55
- return id;
56
- const fullPath = typeof skill.full_path === "string" ? skill.full_path.trim() : "";
57
- const parts = fullPath.split(/[\\/]+/).filter(Boolean);
58
- if (parts.length < 2 || parts.at(-1).toLowerCase() !== "skill.md")
59
- return undefined;
60
- return parts.at(-2);
61
- }
62
- function skillIds(skills) {
63
- const ids = [];
64
- const seen = new Set();
65
- for (const skill of skills) {
66
- const name = skillNameFromAgentSkill(skill);
67
- if (!name || seen.has(name))
68
- continue;
69
- seen.add(name);
70
- ids.push(name);
71
- }
72
- return [...ids].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
73
- }
74
- function sameIdList(left, right) {
75
- if (left.length !== right.length)
76
- return false;
77
- for (let i = 0; i < left.length; i++)
78
- if (left[i] !== right[i])
79
- return false;
80
- return true;
81
- }
82
44
  /**
83
45
  * Shared system-guidance line: name advertised dynamic-catalog tools and prefer
84
46
  * them over Grep/Shell when skills or project rules apply. Concrete skill ids
85
- * stay out of the frozen baseline (they live in RequestContext `agent_skills`
86
- * and in Mid-Conversation updates when the catalog changes — same split as
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
87
49
  * OpenCode's SkillGuidance / SkillInstructions).
88
50
  */
89
51
  export function buildDynamicCatalogRoutingInstruction(options) {
@@ -102,127 +64,20 @@ export function buildDynamicCatalogRoutingInstruction(options) {
102
64
  `- OpenCode host tools that are not in Cursor's native top-level list (including ${extras.join(" and ")}) ` +
103
65
  "are reached through GetDynamicTools / CallDynamicTool (or the host's equivalent dynamic catalog). " +
104
66
  "When a skill matches or project rules name an MCP server, discover and call those tools that way " +
105
- "before Grep/Shell fallbacks. Do not narrate that they are unavailable.",
67
+ "before Grep/Shell fallbacks. Do not narrate that they are unavailable. " +
68
+ // Cursor's server answers lookups and calls in its own `cursor` namespace
69
+ // (built-in tools such as GenerateImage) and rejects any other name there.
70
+ `Host tools are in namespace \`${DEFAULT_TOOL_SERVER}\`` +
71
+ (mcpServers.length > 0 ? " and MCP tools in their server's namespace" : "") +
72
+ "; namespace `cursor` holds only Cursor's built-in tools.",
106
73
  ];
107
74
  if (hasSkill) {
108
75
  // Mirror OpenCode 1 SystemPrompt.skills / OC2 SkillInstructions.render.
109
76
  lines.push("- Skills provide specialized instructions and workflows for specific tasks. " +
110
77
  "Use the `skill` tool to load a skill when a task matches its description " +
111
- "(RequestContext `agent_skills` lists names and descriptions). " +
78
+ "(the host system prompt and `skill` tool carry names and descriptions). " +
112
79
  "A skill that is already present in the conversation as a `<skill_content>` block " +
113
80
  "does not need to be invoked again.");
114
81
  }
115
82
  return lines.join("\n");
116
83
  }
117
- function skillEntriesXml(skills, argKey) {
118
- const lines = ["<available_skills>"];
119
- for (const skill of skills) {
120
- const name = skillNameFromAgentSkill(skill);
121
- if (!name)
122
- continue;
123
- const description = typeof skill.description === "string" ? skill.description : "";
124
- lines.push(" <skill>");
125
- if (argKey === "id")
126
- lines.push(` <id>${name}</id>`);
127
- lines.push(` <name>${name}</name>`);
128
- if (description)
129
- lines.push(` <description>${description}</description>`);
130
- lines.push(" </skill>");
131
- }
132
- lines.push("</available_skills>");
133
- return lines;
134
- }
135
- /**
136
- * OpenCode-shaped Mid-Conversation skill update.
137
- *
138
- * Pure: compare `previousSkillIds` (null = not yet admitted this conversation)
139
- * to the current catalog. First admission returns undefined — baseline
140
- * RequestContext + routing instruction already carry the list. Later turns
141
- * only speak when the id set changes (agent switch / discovery growth), matching
142
- * `SkillGuidance.update` / `SkillInstructions.update`.
143
- */
144
- export function buildSkillCatalogChangeReminder(options) {
145
- const nextSkillIds = skillIds(options.skills);
146
- if (!options.hasSkillTool) {
147
- return { text: undefined, nextSkillIds: options.previousSkillIds ? [...options.previousSkillIds] : [] };
148
- }
149
- if (options.previousSkillIds === null) {
150
- return { text: undefined, nextSkillIds };
151
- }
152
- if (sameIdList(options.previousSkillIds, nextSkillIds)) {
153
- return { text: undefined, nextSkillIds };
154
- }
155
- const argKey = options.skillArgKey === "id" ? "id" : "name";
156
- const callHint = argKey === "id"
157
- ? "Call `skill` with `{ \"id\": \"<skill-id>\" }` (host schema key is `id`)."
158
- : "Call `skill` with `{ \"name\": \"<skill-id>\" }` (host schema key is `name`).";
159
- const body = nextSkillIds.length === 0
160
- ? ["Skill guidance is no longer available. Do not use any previously listed skill."]
161
- : [
162
- "The available skills have changed. This list supersedes the previous available skills list.",
163
- "Skills provide specialized instructions and workflows for specific tasks.",
164
- "Use the skill tool to load a skill when a task matches its description.",
165
- "The user may also invoke a skill directly. When that happens, its instructions appear in the conversation as a <skill_content> block, the same shape the skill tool returns. A skill that is already present this way does not need to be invoked again.",
166
- `OpenCode skills are invoked with the host \`skill\` tool through GetDynamicTools / CallDynamicTool (MCP server \`${DEFAULT_TOOL_SERVER}\`, tool \`skill\`), not by Grep/Read of SKILL.md. ${callHint}`,
167
- ...skillEntriesXml(options.skills, argKey),
168
- ];
169
- return {
170
- text: `<system_reminder>\n${body.join("\n")}\n</system_reminder>`,
171
- nextSkillIds,
172
- };
173
- }
174
- /**
175
- * Admit the current skill catalog for a conversation and return a Mid-Conversation
176
- * reminder only when OpenCode would (catalog changed after baseline admission).
177
- */
178
- export function takeSkillCatalogChangeReminder(conversationId, options) {
179
- const previous = conversationId
180
- ? (admittedSkillIdsByConversation.get(conversationId) ?? null)
181
- : null;
182
- const { text, nextSkillIds } = buildSkillCatalogChangeReminder({
183
- ...options,
184
- previousSkillIds: previous,
185
- });
186
- if (!conversationId)
187
- return text;
188
- if (!options.hasSkillTool && previous === null && nextSkillIds.length === 0) {
189
- return text;
190
- }
191
- admittedSkillIdsByConversation.delete(conversationId);
192
- admittedSkillIdsByConversation.set(conversationId, nextSkillIds);
193
- while (admittedSkillIdsByConversation.size > 256) {
194
- const oldest = admittedSkillIdsByConversation.keys().next().value;
195
- if (!oldest)
196
- break;
197
- admittedSkillIdsByConversation.delete(oldest);
198
- }
199
- return text;
200
- }
201
- export function clearSkillCatalogAdmission(conversationId) {
202
- admittedSkillIdsByConversation.delete(conversationId);
203
- }
204
- export function transferSkillCatalogAdmission(previousConversationId, nextConversationId) {
205
- if (!previousConversationId || !nextConversationId)
206
- return;
207
- const prior = admittedSkillIdsByConversation.get(previousConversationId);
208
- admittedSkillIdsByConversation.delete(previousConversationId);
209
- admittedSkillIdsByConversation.delete(nextConversationId);
210
- if (prior)
211
- admittedSkillIdsByConversation.set(nextConversationId, prior);
212
- }
213
- export function resetSkillCatalogAdmissionsForTests() {
214
- admittedSkillIdsByConversation.clear();
215
- }
216
- /**
217
- * @deprecated Prefer `buildSkillCatalogChangeReminder` / `takeSkillCatalogChangeReminder`.
218
- * Kept as a thin alias for callers that still expect the old name; always returns
219
- * undefined for userText-based matching (OpenCode never did per-turn id nudges).
220
- */
221
- export function buildSkillCatalogNudge(options) {
222
- return buildSkillCatalogChangeReminder({
223
- hasSkillTool: options.hasSkillTool,
224
- skills: options.skills,
225
- previousSkillIds: options.previousSkillIds ?? null,
226
- skillArgKey: options.skillArgKey,
227
- }).text;
228
- }
@@ -7,10 +7,10 @@ export type ContextSourceSnapshot = {
7
7
  };
8
8
  export type ContextEpoch = {
9
9
  conversationId: string;
10
- /** Exact system text seeded at epoch start. Empty when recovered from checkpoint. */
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 original baseline bytes. */
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 (restart past a checkpoint, original bytes unknown): never freeze
46
- * live host text as a new baseline and never send a seed systemPrompt.
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;
@@ -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 (Cursor checkpoint Runs cannot rewrite systemPrompt)
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
- * Cursor constraint: systemPrompt is only sent on seed Runs. Checkpointed
11
- * Runs admit updates exclusively via the live user message.
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 (restart past a checkpoint, original bytes unknown): never freeze
84
- * live host text as a new baseline and never send a seed systemPrompt.
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: Cursor already holds the baseline in the
94
- // checkpoint. Record the live snapshot without reseeding systemPrompt.
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
- const midConversationMessage = combineMessages(oneShots);
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 };
@@ -1,7 +1,6 @@
1
- import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, } from "./build.js";
1
+ import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, resolveSkillLocations, withSystemInstructions, } from "./build.js";
2
2
  import { clearContextEpoch, endContextEpoch, resetContextEpochsForTests } from "./epoch.js";
3
3
  import { clearOverlayHold, resetOverlayHoldsForTests, transferOverlayHold, } from "./overlay.js";
4
- import { clearSkillCatalogAdmission, resetSkillCatalogAdmissionsForTests, transferSkillCatalogAdmission, } from "./dynamic-catalog.js";
5
4
  import { trace } from "../debug.js";
6
5
  import { encodeMessage } from "../protocol/messages.js";
7
6
  /**
@@ -11,10 +10,10 @@ import { encodeMessage } from "../protocol/messages.js";
11
10
  * base plus live plugin/tool overlays. Rebuilding volatile git/layout data on
12
11
  * every Run shifts the prompt prefix and tanks prompt-cache hits.
13
12
  *
14
- * Skills, subagents, plugin metadata, and tool/MCP capabilities are rediscovered
15
- * each Run, then epoch-held (equal ids keep frozen bytes; new ids append) and
16
- * overlaid on that base. If the encoded overlay bytes did not change, the exact
17
- * 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.
18
17
  */
19
18
  const byConversationId = new Map();
20
19
  const materializedByConversationId = new Map();
@@ -45,7 +44,6 @@ function remember(conversationId, context) {
45
44
  byConversationId.delete(oldest);
46
45
  materializedByConversationId.delete(oldest);
47
46
  clearOverlayHold(oldest);
48
- clearSkillCatalogAdmission(oldest);
49
47
  }
50
48
  }
51
49
  /** Frozen stable RequestContext base for this conversation, if any. */
@@ -69,7 +67,6 @@ export function clearFrozenRequestContext(conversationId) {
69
67
  byConversationId.delete(conversationId);
70
68
  materializedByConversationId.delete(conversationId);
71
69
  clearOverlayHold(conversationId);
72
- clearSkillCatalogAdmission(conversationId);
73
70
  clearContextEpoch(conversationId);
74
71
  }
75
72
  /**
@@ -87,12 +84,11 @@ export function transferFrozenRequestContext(previousConversationId, nextConvers
87
84
  const base = byConversationId.get(previousConversationId);
88
85
  const materialized = materializedByConversationId.get(previousConversationId);
89
86
  // System Context epoch does not transfer — compaction starts a fresh baseline.
90
- // Overlay hold does transfer: same workspace, same advertised skill/agent/plugin
87
+ // Overlay hold does transfer: same workspace, same advertised agent/plugin
91
88
  // bytes, so the comparison seed can still match. clearFrozenRequestContext
92
89
  // also drops epoch state for each id.
93
90
  endContextEpoch(previousConversationId, nextConversationId);
94
91
  transferOverlayHold(previousConversationId, nextConversationId);
95
- transferSkillCatalogAdmission(previousConversationId, nextConversationId);
96
92
  byConversationId.delete(previousConversationId);
97
93
  materializedByConversationId.delete(previousConversationId);
98
94
  byConversationId.delete(nextConversationId);
@@ -114,7 +110,6 @@ export function resetFrozenRequestContextsForTests() {
114
110
  materializedByConversationId.clear();
115
111
  buildsByConversationId.clear();
116
112
  resetOverlayHoldsForTests();
117
- resetSkillCatalogAdmissionsForTests();
118
113
  resetContextEpochsForTests();
119
114
  }
120
115
  function sameBytes(a, b) {
@@ -125,6 +120,23 @@ function sameBytes(a, b) {
125
120
  return false;
126
121
  return true;
127
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
+ }
128
140
  function rememberMaterialized(conversationId, context) {
129
141
  const bytes = encodeMessage("RequestContext", context);
130
142
  const previous = materializedByConversationId.get(conversationId);
@@ -148,12 +160,27 @@ export async function getOrBuildRequestContext(conversationId, input, opts) {
148
160
  if (opts?.refresh && conversationId)
149
161
  clearOverlayHold(conversationId);
150
162
  if (!opts?.refresh && conversationId) {
151
- const base = getFrozenRequestContext(conversationId);
163
+ let base = getFrozenRequestContext(conversationId);
152
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
+ }
153
176
  const dynamic = await buildDynamicRequestContext(scoped);
154
- const materialized = rememberMaterialized(conversationId, materializeRequestContext(base, dynamic));
177
+ const skillLocations = await resolveSkillLocations(scoped, scoped.workspaceRoot);
178
+ const materialized = rememberMaterialized(conversationId, materializeRequestContext(base, dynamic, {
179
+ tools: scoped.tools,
180
+ ...skillLocations,
181
+ }));
155
182
  trace(`request_context: materialized conversationId=${conversationId} ` +
156
- `tools=${Array.isArray(materialized.context.tools) ? materialized.context.tools.length : 0} ` +
183
+ `tools=${advertisedMetaToolCount(materialized.context)} ` +
157
184
  `reused=${materialized.reused}`);
158
185
  return materialized;
159
186
  }
@@ -187,7 +214,7 @@ export async function getOrBuildRequestContext(conversationId, input, opts) {
187
214
  ? rememberMaterialized(conversationId, context)
188
215
  : { context: freezeSnapshot(structuredClone(context)), reused: false };
189
216
  trace(`request_context: built+frozen conversationId=${conversationId || "(none)"} ` +
190
- `tools=${Array.isArray(materialized.context.tools) ? materialized.context.tools.length : 0} ` +
217
+ `tools=${advertisedMetaToolCount(materialized.context)} ` +
191
218
  `refresh=${!!opts?.refresh}`);
192
219
  return { context: materialized.context, reused: false };
193
220
  }
@@ -1,6 +1,8 @@
1
1
  export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, type BuildRequestContextInput, } from "./build.js";
2
- export { buildDynamicCatalogRoutingInstruction, buildSkillCatalogChangeReminder, buildSkillCatalogNudge, takeSkillCatalogChangeReminder, listAdvertisedMcpServers, skillNameFromAgentSkill, type AgentSkillLike, } from "./dynamic-catalog.js";
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";
6
6
  export { HOST_PATH_BRIDGE, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, hostGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
7
+ export { HOST_SKILLS_BRIDGE, setHostSkillsBridgeForTests, type HostSkill, type OpenCodeSkillsBridge, } from "./skills-bridge.js";
8
+ export { agentSkillsForCursor, hostSkillFiles, rememberHostSkillFiles, resetHostSkillFilesForTests, skillToolAdvertised, } from "./skills.js";
@@ -1,6 +1,8 @@
1
1
  export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, } from "./build.js";
2
- export { buildDynamicCatalogRoutingInstruction, buildSkillCatalogChangeReminder, buildSkillCatalogNudge, takeSkillCatalogChangeReminder, listAdvertisedMcpServers, skillNameFromAgentSkill, } from "./dynamic-catalog.js";
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";
6
6
  export { HOST_PATH_BRIDGE, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, hostGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
7
+ export { HOST_SKILLS_BRIDGE, setHostSkillsBridgeForTests, } from "./skills-bridge.js";
8
+ export { agentSkillsForCursor, hostSkillFiles, rememberHostSkillFiles, resetHostSkillFilesForTests, skillToolAdvertised, } from "./skills.js";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Epoch-hold live RequestContext overlay lists (skills, subagents, plugins).
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): OverlayWire;
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;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Epoch-hold live RequestContext overlay lists (skills, subagents, plugins).
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 toWire({
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.skills.length === 0 && next.subagents.length === 0 && next.plugins.length === 0) {
58
+ if (next.subagents.length === 0 && next.plugins.length === 0) {
76
59
  byConversationId.delete(conversationId);
77
- return toWire(next);
60
+ return next;
78
61
  }
79
- return toWire(remember(conversationId, next));
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);