@askalf/dario 5.5.25 → 5.5.28

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.
@@ -7,74 +7,14 @@
7
7
  * live cache self-heals when Anthropic ships a new CC version — no user
8
8
  * action required. See src/live-fingerprint.ts for the capture pipeline.
9
9
  */
10
- import { TemplateData } from './live-fingerprint.js';
10
+ import { TemplateData, PLATFORM_ONLY_TOOLS, INTERACTIVE_ONLY_TOOLS, CONFIG_SCOPED_TOOLS } from './live-fingerprint.js';
11
+ export { PLATFORM_ONLY_TOOLS, INTERACTIVE_ONLY_TOOLS, CONFIG_SCOPED_TOOLS };
11
12
  /** The loaded template itself — source, version, capture age, all fields. Startup banners and drift checks read this directly. */
12
13
  export declare const CC_TEMPLATE: TemplateData;
13
- /**
14
- * Tools CC only ships on a specific platform. The bundled template is a
15
- * union capture (any platform the maintainer baked from), so we filter it
16
- * down to the running platform at module load. Real CC on the client side
17
- * only advertises the tools available to its host — forwarding a larger
18
- * set through dario would both leak a fingerprint (Anthropic sees tools
19
- * the client would never actually call) and risk tool_use round-trips
20
- * coming back for a tool the client has no handler for.
21
- *
22
- * PowerShell shipped in CC v2.1.116 on Windows; POSIX CC installs do not
23
- * advertise it. As of CC v2.1.162 the Glob/Grep tools are the same shape:
24
- * Windows CC advertises them, POSIX CC drops them and steers the agent to
25
- * shell `find`/`grep` instead (which PowerShell has no native equivalent
26
- * for). Registering them here filters them to win32 clients AND keeps a
27
- * POSIX auto-bake from dropping them out of the union — the v4.8.28
28
- * regression, where a Linux runner re-baked the bundle down to 28 tools.
29
- * Add new platform-scoped tools here as CC adds them.
30
- */
31
- export declare const PLATFORM_ONLY_TOOLS: Record<string, Set<string>>;
32
14
  /** Keep tool `t` unless its name is listed under a platform other than the current one. */
33
15
  export declare function filterToolsForPlatform<T extends {
34
16
  name: string;
35
17
  }>(tools: T[], platform: string): T[];
36
- /**
37
- * Tools CC only advertises in an INTERACTIVE session. The bake captures CC
38
- * headlessly (`claude --print -p hi`, see live-fingerprint.ts), and CC v2.1.187
39
- * dropped these plan-mode / clarification tools in `--print` mode — so a fresh
40
- * headless capture no longer carries them even though every real interactive CC
41
- * client still advertises them. Like PLATFORM_ONLY_TOOLS, the bundled template
42
- * must stay a SUPERSET: register them here so a headless auto-rebake preserves
43
- * them from the previous bundle instead of dropping them. Dropping them broke
44
- * buildCCRequest's advertise-respects-client contract (the v4.8.93 regression):
45
- * dario advertises only the intersection of the client's declared tools and this
46
- * template, so a missing AskUserQuestion meant dario could not advertise it even
47
- * when a full CC client declared it. Unlike PLATFORM_ONLY_TOOLS these are NOT
48
- * platform-filtered — they stay in CC_TOOL_DEFINITIONS on every host so a client
49
- * that declares them is always honored. Add new interactive-only tools here as
50
- * CC adds them.
51
- */
52
- export declare const INTERACTIVE_ONLY_TOOLS: Set<string>;
53
- /**
54
- * Tools CC advertises only under some runtime configurations. Unlike
55
- * INTERACTIVE_ONLY_TOOLS (absent because the bake captures headlessly), these
56
- * come and go with CC's REMOTE config for the same capture mode: the 2026-08-11
57
- * bake on CC v2.1.232 captured all four headlessly, and the 2026-08-15 bake on
58
- * v2.1.233 captured none of them — while TaskOutput/TaskStop, the rest of the
59
- * task subsystem, stayed put. That is the v4.2.1 drift class (same binary,
60
- * different wire shape via remote configuration), and it is not a signal that
61
- * CC retired the tools.
62
- *
63
- * The bundle must stay a SUPERSET, so the bake preserves these from the previous
64
- * bundle exactly as it does the platform- and interactive-only sets. The cost of
65
- * the two directions is asymmetric, which is what settles it: a stale entry is
66
- * INERT, because buildCCRequest advertises only the intersection of the bundle
67
- * with what the client declared — no client declares it, nothing is advertised.
68
- * Dropping one is NOT inert: CC_NATIVE_NAMES_UNION is derived from this bundle,
69
- * so a client that does declare TaskCreate stops identity-mapping, falls into
70
- * the unmapped round-robin, and is renamed onto a fallback slot with junk args
71
- * (the v4.8.93 regression, caught here by issue-29-tool-translation.mjs).
72
- *
73
- * Add new config-scoped tools here as CC's remote config churns. Removing a name
74
- * is a deliberate act: it means CC genuinely retired the tool, and it should be
75
- * paired with the capture evidence that says so.
76
- */
77
- export declare const CONFIG_SCOPED_TOOLS: Set<string>;
78
18
  /** CC's exact tool definitions for the current platform — filtered from the bundled union. */
79
19
  export declare const CC_TOOL_DEFINITIONS: {
80
20
  name: string;
@@ -7,32 +7,17 @@
7
7
  * live cache self-heals when Anthropic ships a new CC version — no user
8
8
  * action required. See src/live-fingerprint.ts for the capture pipeline.
9
9
  */
10
- import { loadTemplate, promptVariantsOf, VARIANT_FAMILIES } from './live-fingerprint.js';
10
+ import { loadTemplate, promptVariantsOf, VARIANT_FAMILIES, PLATFORM_ONLY_TOOLS, INTERACTIVE_ONLY_TOOLS, CONFIG_SCOPED_TOOLS, } from './live-fingerprint.js';
11
+ // Re-exported so existing importers (scripts/capture-and-bake.mjs, the template
12
+ // invariant tests) keep their import site. The definitions live in
13
+ // live-fingerprint.ts because loadTemplate must apply the same superset rule to
14
+ // a live capture that the bake applies to the bundle, and cc-template.ts is
15
+ // downstream of that load (#1035).
16
+ export { PLATFORM_ONLY_TOOLS, INTERACTIVE_ONLY_TOOLS, CONFIG_SCOPED_TOOLS };
11
17
  // Load template at module init — prefer live cache, fall back to bundled.
12
18
  const TEMPLATE = loadTemplate({ silent: true });
13
19
  /** The loaded template itself — source, version, capture age, all fields. Startup banners and drift checks read this directly. */
14
20
  export const CC_TEMPLATE = TEMPLATE;
15
- /**
16
- * Tools CC only ships on a specific platform. The bundled template is a
17
- * union capture (any platform the maintainer baked from), so we filter it
18
- * down to the running platform at module load. Real CC on the client side
19
- * only advertises the tools available to its host — forwarding a larger
20
- * set through dario would both leak a fingerprint (Anthropic sees tools
21
- * the client would never actually call) and risk tool_use round-trips
22
- * coming back for a tool the client has no handler for.
23
- *
24
- * PowerShell shipped in CC v2.1.116 on Windows; POSIX CC installs do not
25
- * advertise it. As of CC v2.1.162 the Glob/Grep tools are the same shape:
26
- * Windows CC advertises them, POSIX CC drops them and steers the agent to
27
- * shell `find`/`grep` instead (which PowerShell has no native equivalent
28
- * for). Registering them here filters them to win32 clients AND keeps a
29
- * POSIX auto-bake from dropping them out of the union — the v4.8.28
30
- * regression, where a Linux runner re-baked the bundle down to 28 tools.
31
- * Add new platform-scoped tools here as CC adds them.
32
- */
33
- export const PLATFORM_ONLY_TOOLS = {
34
- win32: new Set(['PowerShell', 'Glob', 'Grep']),
35
- };
36
21
  /** Keep tool `t` unless its name is listed under a platform other than the current one. */
37
22
  export function filterToolsForPlatform(tools, platform) {
38
23
  return tools.filter((tool) => {
@@ -43,57 +28,6 @@ export function filterToolsForPlatform(tools, platform) {
43
28
  return true;
44
29
  });
45
30
  }
46
- /**
47
- * Tools CC only advertises in an INTERACTIVE session. The bake captures CC
48
- * headlessly (`claude --print -p hi`, see live-fingerprint.ts), and CC v2.1.187
49
- * dropped these plan-mode / clarification tools in `--print` mode — so a fresh
50
- * headless capture no longer carries them even though every real interactive CC
51
- * client still advertises them. Like PLATFORM_ONLY_TOOLS, the bundled template
52
- * must stay a SUPERSET: register them here so a headless auto-rebake preserves
53
- * them from the previous bundle instead of dropping them. Dropping them broke
54
- * buildCCRequest's advertise-respects-client contract (the v4.8.93 regression):
55
- * dario advertises only the intersection of the client's declared tools and this
56
- * template, so a missing AskUserQuestion meant dario could not advertise it even
57
- * when a full CC client declared it. Unlike PLATFORM_ONLY_TOOLS these are NOT
58
- * platform-filtered — they stay in CC_TOOL_DEFINITIONS on every host so a client
59
- * that declares them is always honored. Add new interactive-only tools here as
60
- * CC adds them.
61
- */
62
- export const INTERACTIVE_ONLY_TOOLS = new Set([
63
- 'AskUserQuestion',
64
- 'EnterPlanMode',
65
- 'ExitPlanMode',
66
- ]);
67
- /**
68
- * Tools CC advertises only under some runtime configurations. Unlike
69
- * INTERACTIVE_ONLY_TOOLS (absent because the bake captures headlessly), these
70
- * come and go with CC's REMOTE config for the same capture mode: the 2026-08-11
71
- * bake on CC v2.1.232 captured all four headlessly, and the 2026-08-15 bake on
72
- * v2.1.233 captured none of them — while TaskOutput/TaskStop, the rest of the
73
- * task subsystem, stayed put. That is the v4.2.1 drift class (same binary,
74
- * different wire shape via remote configuration), and it is not a signal that
75
- * CC retired the tools.
76
- *
77
- * The bundle must stay a SUPERSET, so the bake preserves these from the previous
78
- * bundle exactly as it does the platform- and interactive-only sets. The cost of
79
- * the two directions is asymmetric, which is what settles it: a stale entry is
80
- * INERT, because buildCCRequest advertises only the intersection of the bundle
81
- * with what the client declared — no client declares it, nothing is advertised.
82
- * Dropping one is NOT inert: CC_NATIVE_NAMES_UNION is derived from this bundle,
83
- * so a client that does declare TaskCreate stops identity-mapping, falls into
84
- * the unmapped round-robin, and is renamed onto a fallback slot with junk args
85
- * (the v4.8.93 regression, caught here by issue-29-tool-translation.mjs).
86
- *
87
- * Add new config-scoped tools here as CC's remote config churns. Removing a name
88
- * is a deliberate act: it means CC genuinely retired the tool, and it should be
89
- * paired with the capture evidence that says so.
90
- */
91
- export const CONFIG_SCOPED_TOOLS = new Set([
92
- 'TaskCreate',
93
- 'TaskGet',
94
- 'TaskList',
95
- 'TaskUpdate',
96
- ]);
97
31
  /** CC's exact tool definitions for the current platform — filtered from the bundled union. */
98
32
  export const CC_TOOL_DEFINITIONS = filterToolsForPlatform(TEMPLATE.tools, process.platform);
99
33
  /** The UNFILTERED bundled union — every tool the bake knows across platforms
@@ -1738,10 +1672,19 @@ export function buildCCRequest(clientBody, billingTag, cacheControl, identity, o
1738
1672
  // appended its assistant reply locally, dario stripped it from the next
1739
1673
  // request, the model regenerated the same reply, dario stripped that, and
1740
1674
  // the loop never terminated (133 POSTs from a single user prompt).
1675
+ //
1676
+ // Restricted to ASSISTANT turns, which is the only shape this loop was
1677
+ // written for (a thinking-only assistant turn emptied by the strip above).
1678
+ // Popping an empty *user* turn is what the loop must not do: it exposes the
1679
+ // assistant turn behind it and produces the very prefill rejection the loop
1680
+ // exists to prevent (dario#1033). An empty user turn is a malformed client
1681
+ // request either way — leaving it in place lets the upstream name it
1682
+ // accurately ("messages.N: content must contain at least one block")
1683
+ // instead of dario converting it into a misleading prefill error.
1741
1684
  while (messages.length > 0) {
1742
1685
  const last = messages[messages.length - 1];
1743
1686
  const contentEmpty = Array.isArray(last.content) && last.content.length === 0;
1744
- if (contentEmpty) {
1687
+ if (contentEmpty && last.role === 'assistant') {
1745
1688
  messages.pop();
1746
1689
  continue;
1747
1690
  }
@@ -234,16 +234,82 @@ export declare function promptVariantsOf(t: TemplateData): Record<string, string
234
234
  * Variants the live template already has win, so a future per-model live
235
235
  * capture supersedes the bake without another change here.
236
236
  */
237
- export declare function withBundledVariants(live: TemplateData): TemplateData;
238
237
  /**
239
- * Load the template synchronously. Prefers the live cache (fresh capture
240
- * from the user's own CC install) and falls back to the bundled snapshot.
238
+ * Tools CC only ships on a specific platform. A capture sees only the host it
239
+ * ran on, so both the bundle and any live capture must be re-unioned against
240
+ * the other platforms' names before use. Filtered back down to the running
241
+ * platform at request time by filterToolsForPlatform().
242
+ *
243
+ * PowerShell shipped in CC v2.1.116 on Windows; POSIX CC installs do not
244
+ * advertise it. As of CC v2.1.162 Glob/Grep are the same shape: Windows CC
245
+ * advertises them, POSIX CC drops them and steers the agent to shell
246
+ * `find`/`grep` instead. Registering them here filters them to win32 clients
247
+ * AND keeps a POSIX capture from dropping them out of the union — the v4.8.28
248
+ * regression, where a Linux runner re-baked the bundle down to 28 tools.
249
+ * Add new platform-scoped tools here as CC adds them.
250
+ */
251
+ export declare const PLATFORM_ONLY_TOOLS: Record<string, Set<string>>;
252
+ /**
253
+ * Tools CC only advertises in an INTERACTIVE session. Captures spawn CC
254
+ * headlessly (`claude --print -p hi`), and CC v2.1.187 dropped these plan-mode /
255
+ * clarification tools in `--print` mode — so a fresh headless capture no longer
256
+ * carries them even though every real interactive CC client still advertises
257
+ * them. Unlike PLATFORM_ONLY_TOOLS these are NOT platform-filtered: they stay in
258
+ * the tool set on every host so a client that declares them is always honored.
259
+ * Add new interactive-only tools here as CC adds them.
260
+ */
261
+ export declare const INTERACTIVE_ONLY_TOOLS: Set<string>;
262
+ /**
263
+ * Tools CC advertises only under some runtime configurations. Unlike
264
+ * INTERACTIVE_ONLY_TOOLS (absent because the capture is headless), these come
265
+ * and go with CC's REMOTE config for the same capture mode: the 2026-08-11 bake
266
+ * on CC v2.1.232 captured all four headlessly, and the 2026-08-15 bake on
267
+ * v2.1.233 captured none of them — while TaskOutput/TaskStop, the rest of the
268
+ * task subsystem, stayed put. That is the v4.2.1 drift class (same binary,
269
+ * different wire shape via remote configuration), not a signal that CC retired
270
+ * the tools.
271
+ *
272
+ * Removing a name here is a deliberate act: it means CC genuinely retired the
273
+ * tool, and it should be paired with the capture evidence that says so.
274
+ */
275
+ export declare const CONFIG_SCOPED_TOOLS: Set<string>;
276
+ /** Why a given tool was preserved — used for logging at bake time. */
277
+ export type PreservedToolReason = 'platform' | 'interactive' | 'config-scoped';
278
+ /**
279
+ * Decide whether `name` must be preserved into a capture taken on `platform`,
280
+ * and say why. The single definition of the superset rule.
281
+ *
282
+ * The cost of the two directions is asymmetric, which is what settles it. A
283
+ * stale entry is INERT: buildCCRequest advertises the intersection of the tool
284
+ * set with what the CLIENT declared, so an entry no client declares is never
285
+ * sent. Dropping one is NOT inert — CC_NATIVE_NAMES_UNION is derived from the
286
+ * loaded template, so a client that does declare the tool stops identity-
287
+ * mapping, falls into the unmapped round-robin, and has its history tool_use
288
+ * blocks renamed onto a fallback slot with junk arguments (the v4.8.93
289
+ * regression).
290
+ */
291
+ export declare function preservedToolReason(name: string, platform: string): PreservedToolReason | null;
292
+ /**
293
+ * Re-union `capture.tools` with any tool `fallback` carries that the capture is
294
+ * required to keep (see preservedToolReason). Returns the merged tool array,
295
+ * CC's alphabetical wire order restored, plus what was preserved and why.
241
296
  *
242
- * This is intentionally sync and fast it runs at module init on every
243
- * dario request handler. The actual capture is async and runs in the
244
- * background via refreshLiveFingerprintAsync(); its results are written
245
- * to the cache file and picked up on the next dario startup.
297
+ * This is the rule the bake has always applied to the previous bundle. It must
298
+ * apply to a LIVE capture too: `loadTemplate` prefers a fresh live cache, the
299
+ * cache refreshes on a 24h TTL, and the capture is headless — so without this,
300
+ * every dario install with CC present degrades its own tool set within a day
301
+ * of running, and CI never sees it because CI has no live cache (#1035).
246
302
  */
303
+ export declare function mergePreservedTools<T extends {
304
+ name: string;
305
+ }>(captureTools: T[], fallbackTools: T[], platform: string): {
306
+ tools: T[];
307
+ preserved: Array<{
308
+ name: string;
309
+ reason: PreservedToolReason;
310
+ }>;
311
+ };
312
+ export declare function withBundledVariants(live: TemplateData): TemplateData;
247
313
  export declare function loadTemplate(_options?: {
248
314
  silent?: boolean;
249
315
  }): TemplateData;
@@ -167,6 +167,114 @@ export function promptVariantsOf(t) {
167
167
  * Variants the live template already has win, so a future per-model live
168
168
  * capture supersedes the bake without another change here.
169
169
  */
170
+ /**
171
+ * Tools CC only ships on a specific platform. A capture sees only the host it
172
+ * ran on, so both the bundle and any live capture must be re-unioned against
173
+ * the other platforms' names before use. Filtered back down to the running
174
+ * platform at request time by filterToolsForPlatform().
175
+ *
176
+ * PowerShell shipped in CC v2.1.116 on Windows; POSIX CC installs do not
177
+ * advertise it. As of CC v2.1.162 Glob/Grep are the same shape: Windows CC
178
+ * advertises them, POSIX CC drops them and steers the agent to shell
179
+ * `find`/`grep` instead. Registering them here filters them to win32 clients
180
+ * AND keeps a POSIX capture from dropping them out of the union — the v4.8.28
181
+ * regression, where a Linux runner re-baked the bundle down to 28 tools.
182
+ * Add new platform-scoped tools here as CC adds them.
183
+ */
184
+ export const PLATFORM_ONLY_TOOLS = {
185
+ win32: new Set(['PowerShell', 'Glob', 'Grep']),
186
+ };
187
+ /**
188
+ * Tools CC only advertises in an INTERACTIVE session. Captures spawn CC
189
+ * headlessly (`claude --print -p hi`), and CC v2.1.187 dropped these plan-mode /
190
+ * clarification tools in `--print` mode — so a fresh headless capture no longer
191
+ * carries them even though every real interactive CC client still advertises
192
+ * them. Unlike PLATFORM_ONLY_TOOLS these are NOT platform-filtered: they stay in
193
+ * the tool set on every host so a client that declares them is always honored.
194
+ * Add new interactive-only tools here as CC adds them.
195
+ */
196
+ export const INTERACTIVE_ONLY_TOOLS = new Set([
197
+ 'AskUserQuestion',
198
+ 'EnterPlanMode',
199
+ 'ExitPlanMode',
200
+ ]);
201
+ /**
202
+ * Tools CC advertises only under some runtime configurations. Unlike
203
+ * INTERACTIVE_ONLY_TOOLS (absent because the capture is headless), these come
204
+ * and go with CC's REMOTE config for the same capture mode: the 2026-08-11 bake
205
+ * on CC v2.1.232 captured all four headlessly, and the 2026-08-15 bake on
206
+ * v2.1.233 captured none of them — while TaskOutput/TaskStop, the rest of the
207
+ * task subsystem, stayed put. That is the v4.2.1 drift class (same binary,
208
+ * different wire shape via remote configuration), not a signal that CC retired
209
+ * the tools.
210
+ *
211
+ * Removing a name here is a deliberate act: it means CC genuinely retired the
212
+ * tool, and it should be paired with the capture evidence that says so.
213
+ */
214
+ export const CONFIG_SCOPED_TOOLS = new Set([
215
+ 'TaskCreate',
216
+ 'TaskGet',
217
+ 'TaskList',
218
+ 'TaskUpdate',
219
+ ]);
220
+ /**
221
+ * Decide whether `name` must be preserved into a capture taken on `platform`,
222
+ * and say why. The single definition of the superset rule.
223
+ *
224
+ * The cost of the two directions is asymmetric, which is what settles it. A
225
+ * stale entry is INERT: buildCCRequest advertises the intersection of the tool
226
+ * set with what the CLIENT declared, so an entry no client declares is never
227
+ * sent. Dropping one is NOT inert — CC_NATIVE_NAMES_UNION is derived from the
228
+ * loaded template, so a client that does declare the tool stops identity-
229
+ * mapping, falls into the unmapped round-robin, and has its history tool_use
230
+ * blocks renamed onto a fallback slot with junk arguments (the v4.8.93
231
+ * regression).
232
+ */
233
+ export function preservedToolReason(name, platform) {
234
+ for (const [plat, names] of Object.entries(PLATFORM_ONLY_TOOLS)) {
235
+ if (names.has(name) && plat !== platform)
236
+ return 'platform';
237
+ }
238
+ if (INTERACTIVE_ONLY_TOOLS.has(name))
239
+ return 'interactive';
240
+ if (CONFIG_SCOPED_TOOLS.has(name))
241
+ return 'config-scoped';
242
+ return null;
243
+ }
244
+ /**
245
+ * Re-union `capture.tools` with any tool `fallback` carries that the capture is
246
+ * required to keep (see preservedToolReason). Returns the merged tool array,
247
+ * CC's alphabetical wire order restored, plus what was preserved and why.
248
+ *
249
+ * This is the rule the bake has always applied to the previous bundle. It must
250
+ * apply to a LIVE capture too: `loadTemplate` prefers a fresh live cache, the
251
+ * cache refreshes on a 24h TTL, and the capture is headless — so without this,
252
+ * every dario install with CC present degrades its own tool set within a day
253
+ * of running, and CI never sees it because CI has no live cache (#1035).
254
+ */
255
+ export function mergePreservedTools(captureTools, fallbackTools, platform) {
256
+ const have = new Set(captureTools.map((t) => t.name));
257
+ const preserved = [];
258
+ const additions = [];
259
+ for (const tool of fallbackTools) {
260
+ if (have.has(tool.name))
261
+ continue;
262
+ const reason = preservedToolReason(tool.name, platform);
263
+ if (!reason)
264
+ continue;
265
+ additions.push(tool);
266
+ preserved.push({ name: tool.name, reason });
267
+ have.add(tool.name);
268
+ }
269
+ if (additions.length === 0)
270
+ return { tools: captureTools, preserved };
271
+ // CC sends tools alphabetically by name — sort after merge so preserved tools
272
+ // insert at their natural position rather than appending at the end.
273
+ return {
274
+ tools: [...captureTools, ...additions].sort((a, b) => a.name.localeCompare(b.name)),
275
+ preserved,
276
+ };
277
+ }
170
278
  export function withBundledVariants(live) {
171
279
  let bundled;
172
280
  try {
@@ -217,12 +325,40 @@ const LIVE_TTL_MS = 24 * 60 * 60 * 1000; // re-extract once a day
217
325
  * background via refreshLiveFingerprintAsync(); its results are written
218
326
  * to the cache file and picked up on the next dario startup.
219
327
  */
328
+ /**
329
+ * Bring a live capture up to the superset the rest of the codebase assumes:
330
+ * merge back any preserved tool the bundle has and the capture lacks, then
331
+ * re-derive `tool_names`.
332
+ *
333
+ * `tool_names === tools.map(t => t.name)` is the contract everywhere —
334
+ * scrubTemplate() sets it from `tools`, and so does the capture path. The bake
335
+ * learned the hard way that a merge which mutates `tools` without re-deriving
336
+ * the list ships an artifact whose two tool lists disagree; do not repeat it
337
+ * here (#1035).
338
+ */
339
+ function withPreservedTools(live, options) {
340
+ let bundled;
341
+ try {
342
+ bundled = loadBundledTemplate({ silent: true });
343
+ }
344
+ catch {
345
+ return live; // bundle unreadable — the capture is still better than throwing
346
+ }
347
+ const { tools, preserved } = mergePreservedTools(live.tools, bundled.tools, process.platform);
348
+ if (preserved.length === 0)
349
+ return live;
350
+ if (!options?.silent) {
351
+ const byReason = preserved.map((p) => `${p.name} (${p.reason})`).join(', ');
352
+ console.log(`[dario] live template: restored ${preserved.length} tool${preserved.length === 1 ? '' : 's'} the capture omitted: ${byReason}`);
353
+ }
354
+ return { ...live, tools, tool_names: tools.map((t) => t.name) };
355
+ }
220
356
  export function loadTemplate(_options) {
221
357
  const cached = readLiveCache();
222
358
  if (cached) {
223
359
  const age = Date.now() - new Date(cached._captured).getTime();
224
360
  if (age < LIVE_TTL_MS) {
225
- return withBundledVariants(cached);
361
+ return withPreservedTools(withBundledVariants(cached), _options);
226
362
  }
227
363
  // Stale cache: prefer whichever of the live cache and the bundled
228
364
  // snapshot was captured more recently — do NOT blindly keep the cache.
@@ -236,7 +372,7 @@ export function loadTemplate(_options) {
236
372
  const bundled = loadBundledTemplate(_options);
237
373
  const cachedAt = new Date(cached._captured).getTime();
238
374
  const bundledAt = new Date(bundled._captured).getTime();
239
- return Number.isFinite(bundledAt) && bundledAt > cachedAt ? bundled : withBundledVariants(cached);
375
+ return Number.isFinite(bundledAt) && bundledAt > cachedAt ? bundled : withPreservedTools(withBundledVariants(cached), _options);
240
376
  }
241
377
  return loadBundledTemplate(_options);
242
378
  }
package/dist/proxy.js CHANGED
@@ -658,6 +658,12 @@ export function sanitizeMessages(body, preserveTags) {
658
658
  if (!messages)
659
659
  return;
660
660
  const patterns = orchestrationPatternsFor(preserveTags);
661
+ // Snapshot the tail turn before scrubbing. If the scrub empties it and the
662
+ // drop below would expose an assistant turn, we put the original content
663
+ // back rather than ship a prefill (dario#1033) — see the guard after the
664
+ // filter for the reasoning.
665
+ const tail = messages.length > 0 ? messages[messages.length - 1] : undefined;
666
+ const tailContentBeforeScrub = tail ? tail.content : undefined;
661
667
  for (const msg of messages) {
662
668
  if (typeof msg.content === 'string') {
663
669
  msg.content = sanitizeContent(msg.content, patterns);
@@ -691,13 +697,48 @@ export function sanitizeMessages(body, preserveTags) {
691
697
  // The message carried nothing for the model, so removing it is the same
692
698
  // decision the block filter already made, applied one level up. String
693
699
  // content scrubbed to '' is the same case in its other shape.
694
- body.messages = messages.filter((m) => {
700
+ const kept = messages.filter((m) => {
695
701
  if (Array.isArray(m.content))
696
702
  return m.content.length > 0;
697
703
  if (typeof m.content === 'string')
698
704
  return m.content !== '';
699
705
  return true;
700
706
  });
707
+ // Invariant: the scrub must never turn a request that ended on a USER turn
708
+ // into one that ends on an ASSISTANT turn. Anthropic reads a trailing
709
+ // assistant turn as a prefill ("continue from this text") and Opus 4.6 under
710
+ // adaptive thinking + the claude-code beta rejects it outright:
711
+ // 400 "This model does not support assistant message prefill.
712
+ // The conversation must end with a user message."
713
+ //
714
+ // CC emits standalone `<system-reminder>` / `<task_metadata>` user turns —
715
+ // notably right after a Task (sub-agent) result is folded back into the
716
+ // parent transcript. Both tags are in ORCHESTRATION_TAG_NAMES, so that turn
717
+ // scrubs to empty, the filter above drops it, and a valid CC request leaves
718
+ // as a prefill (dario#1033).
719
+ //
720
+ // The fix restores the turn's PRE-SCRUB content instead of dropping it. The
721
+ // orchestration tag survives in this one position only, which is the right
722
+ // trade in both directions: for a CC client the tag is CC's own injection,
723
+ // so keeping it is *more* wire-faithful, not less; for a non-CC client a
724
+ // lone tag as the final turn is the actual prompt, and forwarding it beats
725
+ // a hard 400. Every other position still scrubs exactly as before.
726
+ const tailWasDropped = tail !== undefined && kept[kept.length - 1] !== tail;
727
+ const tailHadContent = Array.isArray(tailContentBeforeScrub)
728
+ ? tailContentBeforeScrub.length > 0
729
+ : typeof tailContentBeforeScrub === 'string'
730
+ ? tailContentBeforeScrub !== ''
731
+ : tailContentBeforeScrub != null;
732
+ if (tail !== undefined &&
733
+ tailWasDropped &&
734
+ tail.role === 'user' &&
735
+ tailHadContent &&
736
+ kept.length > 0 &&
737
+ kept[kept.length - 1].role === 'assistant') {
738
+ tail.content = tailContentBeforeScrub;
739
+ kept.push(tail);
740
+ }
741
+ body.messages = kept;
701
742
  }
702
743
  /**
703
744
  * Scrub non-Claude-Code fields and normalize field ordering.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.5.25",
3
+ "version": "5.5.28",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {