@qvac/harness 0.2.6 → 0.2.8

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 (174) hide show
  1. package/.claude/skills/gh-stack/SKILL.md +24 -24
  2. package/.claude/skills/gh-stack/references/commands.md +2 -2
  3. package/.claude/skills/js-house-style/SKILL.md +89 -68
  4. package/.claude/skills/kvcache/SKILL.md +7 -7
  5. package/.claude/skills/pr-create/SKILL.md +58 -15
  6. package/.claude/skills/pr-review/SKILL.md +307 -41
  7. package/CLAUDE.md +3 -0
  8. package/README.md +26 -4
  9. package/dist/harness.bundle +12 -10
  10. package/dist-lib/index.js +58 -58
  11. package/dist-lib/lib/audio-transcode-bare.js +2 -2
  12. package/dist-lib/lib/capabilities/completion.js +30 -30
  13. package/dist-lib/lib/capabilities/download.js +2 -2
  14. package/dist-lib/lib/capabilities/speech-text.js +1 -1
  15. package/dist-lib/lib/capabilities/tool-prompt-log.js +1 -1
  16. package/dist-lib/lib/capabilities/tool-selection.js +2 -2
  17. package/dist-lib/lib/capabilities/tool-turns.js +1 -1
  18. package/dist-lib/lib/capabilities/usage-breakdown.js +1 -1
  19. package/dist-lib/lib/capabilities/voice.js +3 -3
  20. package/dist-lib/lib/connect.js +11 -11
  21. package/dist-lib/lib/context-window.js +1 -1
  22. package/dist-lib/lib/docx/index.d.ts +5 -0
  23. package/dist-lib/lib/docx/index.js +49 -0
  24. package/dist-lib/lib/hardware/capability-probe.js +3 -3
  25. package/dist-lib/lib/hardware/gpu-probe-addon.js +2 -2
  26. package/dist-lib/lib/hardware/gpu-probe-entry.js +1 -1
  27. package/dist-lib/lib/helpers/markup-stream-filter.js +1 -1
  28. package/dist-lib/lib/helpers/run-outcome.js +1 -1
  29. package/dist-lib/lib/helpers/spawn-path.d.ts +1 -1
  30. package/dist-lib/lib/helpers/thinking.js +2 -2
  31. package/dist-lib/lib/helpers/tool-intent.js +1 -1
  32. package/dist-lib/lib/helpers/tool-label.js +3 -3
  33. package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -1
  34. package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -1
  35. package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -1
  36. package/dist-lib/lib/hooks/built-in/egress.js +2 -2
  37. package/dist-lib/lib/hooks/built-in/index.js +3 -3
  38. package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -1
  39. package/dist-lib/lib/hooks/pipeline.js +4 -4
  40. package/dist-lib/lib/hooks/registry.js +1 -1
  41. package/dist-lib/lib/hooks/run.js +1 -1
  42. package/dist-lib/lib/models/model-catalog.js +4 -4
  43. package/dist-lib/lib/models/model-delete.js +1 -1
  44. package/dist-lib/lib/models/model-handle.js +1 -1
  45. package/dist-lib/lib/models/model-list.js +1 -1
  46. package/dist-lib/lib/models/model-pool.js +8 -8
  47. package/dist-lib/lib/models/model-source.js +1 -1
  48. package/dist-lib/lib/models/music-catalog.d.ts +1 -1
  49. package/dist-lib/lib/no-redirect-fetch-bare.js +3 -3
  50. package/dist-lib/lib/no-redirect-fetch-impl.js +1 -1
  51. package/dist-lib/lib/no-redirect-fetch-node.js +2 -2
  52. package/dist-lib/lib/resolution-detector.js +1 -1
  53. package/dist-lib/lib/sdk-loader.js +2 -2
  54. package/dist-lib/lib/sdk-storage.js +1 -1
  55. package/dist-lib/lib/serve.js +10 -10
  56. package/dist-lib/lib/spawn.js +3 -3
  57. package/dist-lib/lib/staging.js +1 -1
  58. package/dist-lib/lib/tools/catalog.js +1 -1
  59. package/dist-lib/lib/tools/create-documents.js +2 -2
  60. package/dist-lib/lib/tools/edit-documents.js +2 -2
  61. package/dist-lib/lib/tools/edit-image.js +2 -2
  62. package/dist-lib/lib/tools/exec/adapters/python/attachments.js +2 -2
  63. package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +1 -1
  64. package/dist-lib/lib/tools/exec/adapters/python/loader.js +1 -1
  65. package/dist-lib/lib/tools/exec/adapters/python/memfs.js +2 -2
  66. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +8 -6
  67. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +3 -3
  68. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +5 -5
  69. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -2
  70. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +4 -4
  71. package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +4 -4
  72. package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.js +1 -1
  73. package/dist-lib/lib/tools/exec/obsidian/cli-process.js +2 -2
  74. package/dist-lib/lib/tools/exec/obsidian/cli.js +4 -4
  75. package/dist-lib/lib/tools/exec/obsidian/command.js +1 -1
  76. package/dist-lib/lib/tools/exec/obsidian/route.js +7 -7
  77. package/dist-lib/lib/tools/exec/obsidian/vault-create.js +2 -2
  78. package/dist-lib/lib/tools/exec/obsidian/vault-fs.js +6 -6
  79. package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +2 -2
  80. package/dist-lib/lib/tools/exec/obsidian/vault-read.js +1 -1
  81. package/dist-lib/lib/tools/exec/obsidian/vault-write.js +2 -2
  82. package/dist-lib/lib/tools/exec/runner.js +1 -1
  83. package/dist-lib/lib/tools/exec.js +13 -13
  84. package/dist-lib/lib/tools/file-glob.js +3 -3
  85. package/dist-lib/lib/tools/fs/local.js +1 -1
  86. package/dist-lib/lib/tools/fs/remote.js +1 -1
  87. package/dist-lib/lib/tools/fs/walk.js +2 -2
  88. package/dist-lib/lib/tools/generate-image.js +4 -4
  89. package/dist-lib/lib/tools/generate-music.js +6 -6
  90. package/dist-lib/lib/tools/grep.js +6 -6
  91. package/dist-lib/lib/tools/http-request/truncate-response.js +1 -1
  92. package/dist-lib/lib/tools/http-request.js +9 -9
  93. package/dist-lib/lib/tools/mcp/utils.js +1 -1
  94. package/dist-lib/lib/tools/mcp-call.js +8 -8
  95. package/dist-lib/lib/tools/operations/builders.d.ts +1 -1
  96. package/dist-lib/lib/tools/operations/builders.js +1 -1
  97. package/dist-lib/lib/tools/operations/compile.js +1 -1
  98. package/dist-lib/lib/tools/operations/manifest.d.ts +16 -16
  99. package/dist-lib/lib/tools/operations/manifest.js +1 -1
  100. package/dist-lib/lib/tools/outbound-guard.js +1 -1
  101. package/dist-lib/lib/tools/rag-search.js +1 -1
  102. package/dist-lib/lib/tools/read-files.js +3 -3
  103. package/dist-lib/lib/tools/skill.js +8 -8
  104. package/dist-lib/lib/tools/skills/auth-resolver.js +2 -2
  105. package/dist-lib/lib/tools/skills/bin-check.js +1 -1
  106. package/dist-lib/lib/tools/skills/bundled-dir.js +2 -2
  107. package/dist-lib/lib/tools/skills/catalog.js +10 -10
  108. package/dist-lib/lib/tools/skills/cli-schema-check.js +1 -1
  109. package/dist-lib/lib/tools/skills/cli-schema-load.js +1 -1
  110. package/dist-lib/lib/tools/skills/cli-schema.js +4 -4
  111. package/dist-lib/lib/tools/skills/credential-check.js +3 -3
  112. package/dist-lib/lib/tools/skills/credential-grants.js +1 -1
  113. package/dist-lib/lib/tools/skills/credential-health.js +2 -2
  114. package/dist-lib/lib/tools/skills/frontmatter-parser.js +1 -1
  115. package/dist-lib/lib/tools/skills/index.js +5 -5
  116. package/dist-lib/lib/tools/skills/mention.js +1 -1
  117. package/dist-lib/lib/tools/skills/overlay-crud.js +1 -1
  118. package/dist-lib/lib/tools/skills/setup-routes.js +1 -1
  119. package/dist-lib/lib/tools/skills/skill-settings.js +1 -1
  120. package/dist-lib/lib/tools/skills/skill-state-constants.d.ts +8 -8
  121. package/dist-lib/lib/tools/skills/skill-state.js +9 -9
  122. package/dist-lib/lib/tools/toolsets.d.ts +1 -1
  123. package/dist-lib/lib/tools/url-guard.js +2 -2
  124. package/dist-lib/lib/tools/weather-lookup/place.d.ts +11 -0
  125. package/dist-lib/lib/tools/weather-lookup/place.js +152 -0
  126. package/dist-lib/lib/tools/weather-lookup/report.d.ts +37 -0
  127. package/dist-lib/lib/tools/weather-lookup/report.js +56 -0
  128. package/dist-lib/lib/tools/weather-lookup.d.ts +8 -0
  129. package/dist-lib/lib/tools/weather-lookup.js +110 -0
  130. package/dist-lib/lib/tools/web-fetch.js +10 -10
  131. package/dist-lib/lib/tools/web-search.js +2 -2
  132. package/dist-lib/lib/transport.js +1 -1
  133. package/dist-lib/lib/validating-lookup.js +1 -1
  134. package/dist-lib/lib/web-fetch/embedded-state.js +1 -1
  135. package/dist-lib/lib/web-fetch/extract.js +1 -1
  136. package/dist-lib/lib/web-fetch/images.js +1 -1
  137. package/dist-lib/lib/web-fetch/video.js +1 -1
  138. package/dist-lib/lib/web-search/engine-registry.js +7 -7
  139. package/dist-lib/lib/web-search/engines/aol.js +3 -3
  140. package/dist-lib/lib/web-search/engines/bing.js +4 -4
  141. package/dist-lib/lib/web-search/engines/brave.js +2 -2
  142. package/dist-lib/lib/web-search/engines/duckduckgo.js +3 -3
  143. package/dist-lib/lib/web-search/engines/google.js +4 -4
  144. package/dist-lib/lib/web-search/engines/wikipedia.js +2 -2
  145. package/dist-lib/lib/web-search/engines/yahoo.js +3 -3
  146. package/dist-lib/lib/web-search/fetch-html.js +1 -1
  147. package/dist-lib/lib/web-search/merge-results.js +1 -1
  148. package/dist-lib/lib/web-search/orchestrator.js +3 -3
  149. package/dist-lib/schema/baseline.js +1 -1
  150. package/dist-lib/schema/build.js +3 -3
  151. package/dist-lib/schema/release-tags.d.ts +2 -1
  152. package/dist-lib/schema/release-tags.js +9 -7
  153. package/docs/decisions/2026-05-29-kvcache-tool-turns.md +75 -0
  154. package/docs/decisions/2026-06-02-kvcache-tool-turns-reenabled.md +127 -0
  155. package/docs/decisions/2026-06-23-android-cpu-llm-defaults.md +126 -0
  156. package/docs/decisions/2026-07-29-assistant-owned-storage-layout.md +88 -0
  157. package/docs/decisions/2026-08-04-sidecar-orphan-reaping.md +79 -0
  158. package/docs/decisions/2026-08-11-pyodide-worker-thread.md +68 -0
  159. package/docs/decisions/2026-08-17-harness-hook-pipeline.md +84 -0
  160. package/docs/decisions/2026-08-17-harness-outbound-egress-guard.md +176 -0
  161. package/docs/decisions/2026-08-20-chat-prompt-prefix-is-stable.md +95 -0
  162. package/docs/decisions/2026-08-20-kvcache-static-placement.md +109 -0
  163. package/docs/decisions/2026-08-21-vendored-python-wheels.md +85 -0
  164. package/docs/decisions/2026-08-26-exec-is-not-gated-by-skills.md +64 -0
  165. package/docs/decisions/2026-08-27-writable-skills-overlay.md +56 -0
  166. package/docs/decisions/2026-09-01-skill-operation-manifests.md +53 -0
  167. package/docs/decisions/2026-09-01-usage-breakdown-is-measured.md +78 -0
  168. package/docs/decisions/2026-09-03-usage-breakdown-reported-by-the-harness.md +136 -0
  169. package/docs/decisions/2026-09-10-skill-wording-is-measured-offline.md +63 -0
  170. package/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md +70 -0
  171. package/docs/decisions/INDEX.md +40 -0
  172. package/docs/decisions/TEMPLATE.md +32 -0
  173. package/package.json +21 -4
  174. package/worker/harness-sidecar.mjs +2 -0
@@ -0,0 +1,37 @@
1
+ import { type ResolvedPlace } from './place.ts';
2
+ interface Translated {
3
+ readonly value: string;
4
+ }
5
+ interface CurrentCondition {
6
+ readonly temp_C?: string;
7
+ readonly temp_F?: string;
8
+ readonly FeelsLikeC?: string;
9
+ readonly humidity?: string;
10
+ readonly windspeedKmph?: string;
11
+ readonly weatherDesc?: ReadonlyArray<Translated>;
12
+ }
13
+ interface HourlyEntry {
14
+ readonly time?: string;
15
+ readonly weatherDesc?: ReadonlyArray<Translated>;
16
+ }
17
+ interface DailyForecast {
18
+ readonly date?: string;
19
+ readonly maxtempC?: string;
20
+ readonly mintempC?: string;
21
+ readonly maxtempF?: string;
22
+ readonly mintempF?: string;
23
+ readonly hourly?: ReadonlyArray<HourlyEntry>;
24
+ }
25
+ interface NearestArea {
26
+ readonly areaName?: ReadonlyArray<Translated>;
27
+ readonly region?: ReadonlyArray<Translated>;
28
+ readonly country?: ReadonlyArray<Translated>;
29
+ }
30
+ export interface WeatherJson {
31
+ readonly nearest_area?: ReadonlyArray<NearestArea>;
32
+ readonly current_condition?: ReadonlyArray<CurrentCondition>;
33
+ readonly weather?: ReadonlyArray<DailyForecast>;
34
+ }
35
+ export declare function placeOf(json: WeatherJson): ResolvedPlace | null;
36
+ export declare function renderReport(json: WeatherJson, place: ResolvedPlace, asked: string, verified: boolean): string;
37
+ export {};
@@ -0,0 +1,56 @@
1
+ import { describePlace } from './place.js';
2
+ const MAX_DAYS = 3;
3
+ const DAY_LABELS = ['today', 'tomorrow'];
4
+ const RANGE = '\u2013';
5
+ const NOON = 4;
6
+ function firstValue(values) {
7
+ return values?.[0]?.value?.trim() ?? '';
8
+ }
9
+ export function placeOf(json) {
10
+ const area = json.nearest_area?.[0];
11
+ if (area === undefined)
12
+ return null;
13
+ const place = {
14
+ area: firstValue(area.areaName),
15
+ region: firstValue(area.region),
16
+ country: firstValue(area.country)
17
+ };
18
+ return place.area || place.region || place.country ? place : null;
19
+ }
20
+ export function renderReport(json, place, asked, verified) {
21
+ const lines = [headline(place, asked, verified)];
22
+ const now = json.current_condition?.[0];
23
+ if (now !== undefined)
24
+ lines.push(currentLine(now));
25
+ const days = json.weather?.slice(0, MAX_DAYS) ?? [];
26
+ for (const [index, day] of days.entries())
27
+ lines.push(dailyLine(day, index));
28
+ return lines.join('\n');
29
+ }
30
+ function headline(place, asked, verified) {
31
+ const resolved = describePlace(place);
32
+ if (verified)
33
+ return resolved;
34
+ return `${resolved} (not "${asked}" — this is where it resolved)`;
35
+ }
36
+ function currentLine(now) {
37
+ const parts = [`Now: ${now.temp_C ?? '?'}°C / ${now.temp_F ?? '?'}°F`];
38
+ const desc = firstValue(now.weatherDesc);
39
+ if (desc)
40
+ parts.push(desc);
41
+ if (now.FeelsLikeC !== undefined)
42
+ parts.push(`feels like ${now.FeelsLikeC}°C`);
43
+ if (now.humidity !== undefined)
44
+ parts.push(`humidity ${now.humidity}%`);
45
+ if (now.windspeedKmph !== undefined)
46
+ parts.push(`wind ${now.windspeedKmph} km/h`);
47
+ return parts.join(', ');
48
+ }
49
+ function dailyLine(day, index) {
50
+ const label = DAY_LABELS[index];
51
+ const date = `${day.date ?? '?'}${label === undefined ? '' : ` (${label})`}`;
52
+ const celsius = `${day.mintempC ?? '?'}${RANGE}${day.maxtempC ?? '?'}°C`;
53
+ const fahrenheit = `${day.mintempF ?? '?'}${RANGE}${day.maxtempF ?? '?'}°F`;
54
+ const desc = firstValue(day.hourly?.[NOON]?.weatherDesc);
55
+ return `${date}: ${celsius} / ${fahrenheit}${desc ? `, ${desc}` : ''}`;
56
+ }
@@ -0,0 +1,8 @@
1
+ import type { ToolDefinition } from '../capabilities/completion.ts';
2
+ import type { FetchFn } from './http-request.ts';
3
+ import { type HostResolver } from './url-guard.ts';
4
+ export interface WeatherLookupToolOptions {
5
+ fetch?: FetchFn;
6
+ resolveHost?: HostResolver;
7
+ }
8
+ export declare function createWeatherLookupTool({ fetch: fetchImpl, resolveHost }?: WeatherLookupToolOptions): ToolDefinition;
@@ -0,0 +1,110 @@
1
+ import defaultFetch from '#no-redirect-fetch';
2
+ import { AbortController } from 'bare-abort-controller';
3
+ import { lookupHost } from '../dns-lookup.js';
4
+ import { isRedirectStatus } from './http-request/utils.js';
5
+ import { resolveOutboundUrl } from './url-guard.js';
6
+ import { checkQualifier, cityMatches, describePlace, qualifierOf } from './weather-lookup/place.js';
7
+ import { placeOf, renderReport } from './weather-lookup/report.js';
8
+ const WEATHER_HOST = 'wttr.in';
9
+ const LOOKUP_TIMEOUT_MS = 15_000;
10
+ const MAX_BODY_BYTES = 400_000;
11
+ export function createWeatherLookupTool({ fetch: fetchImpl, resolveHost = lookupHost } = {}) {
12
+ return {
13
+ name: 'weather_lookup',
14
+ toolset: 'http-request',
15
+ skillGated: true,
16
+ autoApprove: true,
17
+ description: 'Look up the current weather and a three-day forecast for a place, via wttr.in. ' +
18
+ 'Returns the place the service resolved on its first line, then the conditions now and ' +
19
+ 'one line per day. Use it for any question about weather, temperature or forecast.',
20
+ parameters: {
21
+ type: 'object',
22
+ properties: {
23
+ location: {
24
+ type: 'string',
25
+ description: 'The place exactly as the user wrote it, including any country or state: ' +
26
+ '"Nassau, Bahamas", "Paris, Texas", "London"'
27
+ }
28
+ },
29
+ required: ['location']
30
+ },
31
+ effect: () => 'read',
32
+ resource: () => ({ kind: 'host', label: WEATHER_HOST }),
33
+ async execute(args, ctx) {
34
+ const raw = args.location;
35
+ if (typeof raw !== 'string' || raw.trim().length === 0) {
36
+ return { error: 'location must be a non-empty string' };
37
+ }
38
+ const location = raw.trim();
39
+ if (ctx?.signal?.aborted)
40
+ return { error: 'weather_lookup cancelled' };
41
+ const url = new URL(`https://${WEATHER_HOST}/${encodeURIComponent(location)}?format=j1`);
42
+ const fetch = fetchImpl ?? defaultFetch;
43
+ const controller = new AbortController();
44
+ const timeout = setTimeout(() => controller.abort(undefined), LOOKUP_TIMEOUT_MS);
45
+ const onCallerAbort = () => controller.abort(undefined);
46
+ ctx?.signal?.addEventListener('abort', onCallerAbort);
47
+ try {
48
+ const check = await resolveOutboundUrl(url, resolveHost, ctx?.signal);
49
+ if (check.blocked)
50
+ return { error: check.blocked };
51
+ const response = await fetch(url, {
52
+ method: 'GET',
53
+ headers: { Accept: 'application/json' },
54
+ signal: controller.signal,
55
+ redirect: 'manual',
56
+ validatedAddresses: check.addresses,
57
+ maxBytes: MAX_BODY_BYTES
58
+ });
59
+ if (isRedirectStatus(response.status)) {
60
+ return { error: `${WEATHER_HOST} redirected the lookup for "${location}"` };
61
+ }
62
+ const body = await response.text();
63
+ if (/location not found/i.test(body))
64
+ return { content: notFound(location) };
65
+ if (response.status !== 200) {
66
+ return { error: unavailable(`it answered HTTP ${response.status}`) };
67
+ }
68
+ let json;
69
+ try {
70
+ json = JSON.parse(body);
71
+ }
72
+ catch {
73
+ return { error: unavailable('it did not return a forecast') };
74
+ }
75
+ const place = placeOf(json);
76
+ if (place === null)
77
+ return { content: notFound(location) };
78
+ const qualifier = qualifierOf(location);
79
+ const verdict = qualifier === null ? null : checkQualifier(qualifier, place);
80
+ const named = cityMatches(location, place);
81
+ if (verdict === 'contradiction' && !named) {
82
+ return {
83
+ content: `No such place: "${location}". ${WEATHER_HOST} resolved it to ` +
84
+ `${describePlace(place)}, which is somewhere else.`
85
+ };
86
+ }
87
+ const verified = qualifier === null ? named : verdict === 'match';
88
+ return { content: renderReport(json, place, location, verified) };
89
+ }
90
+ catch (err) {
91
+ if (ctx?.signal?.aborted)
92
+ return { error: 'weather_lookup cancelled' };
93
+ if (controller.signal.aborted) {
94
+ return { error: `weather_lookup timed out after ${LOOKUP_TIMEOUT_MS} ms` };
95
+ }
96
+ return { error: unavailable(err instanceof Error ? err.message : String(err)) };
97
+ }
98
+ finally {
99
+ clearTimeout(timeout);
100
+ ctx?.signal?.removeEventListener('abort', onCallerAbort);
101
+ }
102
+ }
103
+ };
104
+ }
105
+ function unavailable(reason) {
106
+ return `The weather service could not be reached: ${reason}.`;
107
+ }
108
+ function notFound(location) {
109
+ return `No such place: "${location}". ${WEATHER_HOST} does not know it.`;
110
+ }
@@ -1,15 +1,15 @@
1
1
  import defaultFetch from '#no-redirect-fetch';
2
2
  import { AbortController } from 'bare-abort-controller';
3
- import { lookupHost } from "../dns-lookup.js";
4
- import { AbortedError } from "../helpers/race-abort.js";
5
- import { extractEmbeddedState } from "../web-fetch/embedded-state.js";
6
- import { extractPageText, looksLikeHtml } from "../web-fetch/extract.js";
7
- import { imageFileName, pageImageUrls } from "../web-fetch/images.js";
8
- import { pageVideoRef } from "../web-fetch/video.js";
9
- import { MAX_HTML_CHARS } from "../web-search/fetch-html.js";
10
- import { BROWSER_UA } from "../web-search/user-agents.js";
11
- import { isRedirectStatus } from "./http-request/utils.js";
12
- import { resolveOutboundUrl } from "./url-guard.js";
3
+ import { lookupHost } from '../dns-lookup.js';
4
+ import { AbortedError } from '../helpers/race-abort.js';
5
+ import { extractEmbeddedState } from '../web-fetch/embedded-state.js';
6
+ import { extractPageText, looksLikeHtml } from '../web-fetch/extract.js';
7
+ import { imageFileName, pageImageUrls } from '../web-fetch/images.js';
8
+ import { pageVideoRef } from '../web-fetch/video.js';
9
+ import { MAX_HTML_CHARS } from '../web-search/fetch-html.js';
10
+ import { BROWSER_UA } from '../web-search/user-agents.js';
11
+ import { isRedirectStatus } from './http-request/utils.js';
12
+ import { resolveOutboundUrl } from './url-guard.js';
13
13
  const SMALL_CONTEXT_TOKENS = 12_288;
14
14
  const SMALL_CONTEXT_CHARS = 6_000;
15
15
  const LARGE_CONTEXT_CHARS = 16_000;
@@ -1,6 +1,6 @@
1
1
  import defaultFetch from '#fetch';
2
- import { formatResultsForPrompt } from "../web-search/format-results.js";
3
- import { searchWeb } from "../web-search/orchestrator.js";
2
+ import { formatResultsForPrompt } from '../web-search/format-results.js';
3
+ import { searchWeb } from '../web-search/orchestrator.js';
4
4
  const nodeFetch = async (url, init) => {
5
5
  const res = await defaultFetch(url, init);
6
6
  return { ok: res.ok, status: res.status, text: () => res.text() };
@@ -1,6 +1,6 @@
1
1
  import c from 'compact-encoding';
2
2
  import { Duplex } from 'streamx';
3
- export { settleOnClose } from "./rpc-session.js";
3
+ export { settleOnClose } from './rpc-session.js';
4
4
  export function clientSessions(pipe) {
5
5
  let current = session(pipe);
6
6
  pipe.on('data', (chunk) => current.push(chunk));
@@ -1,4 +1,4 @@
1
- import { isPrivateOrBlocked } from "./tools/private-ip.js";
1
+ import { isPrivateOrBlocked } from './tools/private-ip.js';
2
2
  export class BlockedAddressError extends Error {
3
3
  code = 'EBLOCKEDADDR';
4
4
  constructor(hostname, address) {
@@ -1,4 +1,4 @@
1
- import { decodeHtmlEntities } from "../web-search/html-helpers.js";
1
+ import { decodeHtmlEntities } from '../web-search/html-helpers.js';
2
2
  const LDJSON_SCRIPT = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
3
3
  const JSON_SCRIPT = /<script[^>]*type=["']application\/json["'][^>]*>([\s\S]*?)<\/script>/gi;
4
4
  const INLINE_SCRIPT = /<script\b(?![^>]*\bsrc=)(?![^>]*type=["']application\/(?:ld\+)?json["'])[^>]*>([\s\S]*?)<\/script>/gi;
@@ -1,4 +1,4 @@
1
- import { decodeHtmlEntities, stripHtmlTags } from "../web-search/html-helpers.js";
1
+ import { decodeHtmlEntities, stripHtmlTags } from '../web-search/html-helpers.js';
2
2
  const NOISE_BLOCKS = [
3
3
  'script',
4
4
  'style',
@@ -1,4 +1,4 @@
1
- import { decodeHtmlEntities } from "../web-search/html-helpers.js";
1
+ import { decodeHtmlEntities } from '../web-search/html-helpers.js';
2
2
  const META_TAG = /<meta\s[^>]*>/gi;
3
3
  const IMAGE_ATTR = /(?:property|name)=["'](?:og:image(?::secure_url|:url)?|twitter:image)["']/i;
4
4
  const META_CONTENT = /content=["']([^"']+)["']/i;
@@ -1,4 +1,4 @@
1
- import { decodeHtmlEntities } from "../web-search/html-helpers.js";
1
+ import { decodeHtmlEntities } from '../web-search/html-helpers.js';
2
2
  const META_TAG = /<meta\s[^>]*>/gi;
3
3
  const VIDEO_ATTR = /(?:property|name)=["']og:video(?::secure_url|:url)?["']/i;
4
4
  const META_CONTENT = /content=["']([^"']+)["']/i;
@@ -1,10 +1,10 @@
1
- import { searchAol } from "./engines/aol.js";
2
- import { searchBing } from "./engines/bing.js";
3
- import { searchBrave } from "./engines/brave.js";
4
- import { searchDuckDuckGo } from "./engines/duckduckgo.js";
5
- import { searchGoogle } from "./engines/google.js";
6
- import { searchWikipedia } from "./engines/wikipedia.js";
7
- import { searchYahoo } from "./engines/yahoo.js";
1
+ import { searchAol } from './engines/aol.js';
2
+ import { searchBing } from './engines/bing.js';
3
+ import { searchBrave } from './engines/brave.js';
4
+ import { searchDuckDuckGo } from './engines/duckduckgo.js';
5
+ import { searchGoogle } from './engines/google.js';
6
+ import { searchWikipedia } from './engines/wikipedia.js';
7
+ import { searchYahoo } from './engines/yahoo.js';
8
8
  export const DEFAULT_MAX_RESULTS = 8;
9
9
  export const ENGINES = [
10
10
  { name: 'duckduckgo', fn: searchDuckDuckGo },
@@ -1,6 +1,6 @@
1
- import { fetchHtml } from "../fetch-html.js";
2
- import { clean } from "../html-helpers.js";
3
- import { urlMatchesHost } from "../url-helpers.js";
1
+ import { fetchHtml } from '../fetch-html.js';
2
+ import { clean } from '../html-helpers.js';
3
+ import { urlMatchesHost } from '../url-helpers.js';
4
4
  function decodeRuUrl(encoded) {
5
5
  try {
6
6
  const href = decodeURIComponent(encoded);
@@ -1,7 +1,7 @@
1
- import { fetchHtml } from "../fetch-html.js";
2
- import { clean } from "../html-helpers.js";
3
- import { urlMatchesHost } from "../url-helpers.js";
4
- import { SAFARI_UA } from "../user-agents.js";
1
+ import { fetchHtml } from '../fetch-html.js';
2
+ import { clean } from '../html-helpers.js';
3
+ import { urlMatchesHost } from '../url-helpers.js';
4
+ import { SAFARI_UA } from '../user-agents.js';
5
5
  function isOrganicBingUrl(url) {
6
6
  return url.startsWith('http') && !urlMatchesHost(url, 'bing.com') && !url.includes('msockid=');
7
7
  }
@@ -1,5 +1,5 @@
1
- import { fetchHtml } from "../fetch-html.js";
2
- import { clean } from "../html-helpers.js";
1
+ import { fetchHtml } from '../fetch-html.js';
2
+ import { clean } from '../html-helpers.js';
3
3
  function parseSnippetBlock(inner) {
4
4
  const urlMatch = inner.match(/<a[^>]*href="(https?:\/\/(?!imgs\.search\.brave)[^"]*)"[^>]*>/i);
5
5
  if (!urlMatch) {
@@ -1,6 +1,6 @@
1
- import { fetchHtml } from "../fetch-html.js";
2
- import { clean } from "../html-helpers.js";
3
- import { urlMatchesHost } from "../url-helpers.js";
1
+ import { fetchHtml } from '../fetch-html.js';
2
+ import { clean } from '../html-helpers.js';
3
+ import { urlMatchesHost } from '../url-helpers.js';
4
4
  function extractDdgUrl(raw) {
5
5
  if (raw.includes('uddg=')) {
6
6
  const m = raw.match(/uddg=([^&]+)/);
@@ -1,7 +1,7 @@
1
- import { fetchHtml } from "../fetch-html.js";
2
- import { clean } from "../html-helpers.js";
3
- import { urlMatchesHost } from "../url-helpers.js";
4
- import { GOOGLE_GSA_UA } from "../user-agents.js";
1
+ import { fetchHtml } from '../fetch-html.js';
2
+ import { clean } from '../html-helpers.js';
3
+ import { urlMatchesHost } from '../url-helpers.js';
4
+ import { GOOGLE_GSA_UA } from '../user-agents.js';
5
5
  function resolveGoogleUrl(rawUrl) {
6
6
  if (rawUrl.startsWith('/url?q=')) {
7
7
  return decodeURIComponent(rawUrl.slice(7).split('&')[0] || '');
@@ -1,5 +1,5 @@
1
- import { fetchJson } from "../fetch-html.js";
2
- import { clean } from "../html-helpers.js";
1
+ import { fetchJson } from '../fetch-html.js';
2
+ import { clean } from '../html-helpers.js';
3
3
  function extractSearchItems(data) {
4
4
  if (typeof data !== 'object' || data === null) {
5
5
  return [];
@@ -1,6 +1,6 @@
1
- import { fetchHtml } from "../fetch-html.js";
2
- import { clean } from "../html-helpers.js";
3
- import { urlMatchesHost } from "../url-helpers.js";
1
+ import { fetchHtml } from '../fetch-html.js';
2
+ import { clean } from '../html-helpers.js';
3
+ import { urlMatchesHost } from '../url-helpers.js';
4
4
  function decodeRuUrl(encoded) {
5
5
  try {
6
6
  const href = decodeURIComponent(encoded);
@@ -1,5 +1,5 @@
1
1
  import { AbortController } from 'bare-abort-controller';
2
- import { BROWSER_UA } from "./user-agents.js";
2
+ import { BROWSER_UA } from './user-agents.js';
3
3
  export const ENGINE_TIMEOUT_MS = 8000;
4
4
  export const MAX_HTML_CHARS = 2_000_000;
5
5
  export class TimeoutError extends Error {
@@ -1,4 +1,4 @@
1
- import { normalizeUrl } from "./url-normalize.js";
1
+ import { normalizeUrl } from './url-normalize.js';
2
2
  const WEAK_RESULT_PENALTY = 0.2;
3
3
  function scoreFor(r) {
4
4
  const base = 1 / r.position;
@@ -1,6 +1,6 @@
1
- import { DEFAULT_MAX_RESULTS, ENGINES } from "./engine-registry.js";
2
- import { TimeoutError } from "./fetch-html.js";
3
- import { mergeResults } from "./merge-results.js";
1
+ import { DEFAULT_MAX_RESULTS, ENGINES } from './engine-registry.js';
2
+ import { TimeoutError } from './fetch-html.js';
3
+ import { mergeResults } from './merge-results.js';
4
4
  export function classifyEngineError(reason) {
5
5
  return reason instanceof TimeoutError ? 'timeout' : 'error';
6
6
  }
@@ -2,7 +2,7 @@ import fs from 'bare-fs';
2
2
  import path from 'bare-path';
3
3
  import process from 'bare-process';
4
4
  import { spawnSync } from 'bare-subprocess';
5
- import { previousReleaseTags } from "./release-tags.js";
5
+ import { previousReleaseTags } from './release-tags.js';
6
6
  export function rewindSpec(root, dirs, { carries, keep }) {
7
7
  if (process.env.SCHEMA_NO_BASELINE)
8
8
  return [];
@@ -3,9 +3,9 @@ import { fileURLToPath } from 'bare-url';
3
3
  import HRPCBuilder from 'hrpc';
4
4
  import Hyperschema from 'hyperschema';
5
5
  import HyperschemaTS from 'hyperschema-ts';
6
- import { resetHrpcIds, rewindSpec, unstageSpec } from "./baseline.js";
7
- import { writeBind, writeClient } from "./generate.js";
8
- import { registerHarnessApi, registerHarnessTypes } from "./harness.js";
6
+ import { resetHrpcIds, rewindSpec, unstageSpec } from './baseline.js';
7
+ import { writeBind, writeClient } from './generate.js';
8
+ import { registerHarnessApi, registerHarnessTypes } from './harness.js';
9
9
  const dir = path.dirname(fileURLToPath(import.meta.url));
10
10
  const ROOT = path.join(dir, '..');
11
11
  const SPEC_DIR = path.join(dir, '..', 'spec');
@@ -1,4 +1,5 @@
1
- export declare function currentReleaseLine(root: string): string;
1
+ import { Version } from 'bare-semver';
2
+ export declare function currentReleaseVersion(root: string): Version;
2
3
  export declare function previousReleaseTags(root: string, { count, carries }: {
3
4
  count?: number;
4
5
  carries: string;
@@ -2,24 +2,26 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { spawnSync } from 'child_process';
4
4
  import { Version } from 'bare-semver';
5
- export function currentReleaseLine(root) {
5
+ export function currentReleaseVersion(root) {
6
6
  const file = path.join(root, 'package.json');
7
7
  const { version } = JSON.parse(fs.readFileSync(file, 'utf8'));
8
- const parsed = /^(\d+)\.(\d+)/.exec(version);
9
- if (!parsed)
10
- throw new Error(`no release line in package version ${version}`);
11
- return `${parsed[1]}.${parsed[2]}`;
8
+ try {
9
+ return Version.parse(version);
10
+ }
11
+ catch {
12
+ throw new Error(`no release version in package version ${version}`);
13
+ }
12
14
  }
13
15
  const TAG_PREFIX = 'v';
14
16
  export function previousReleaseTags(root, { count = 3, carries }) {
15
- const current = currentReleaseLine(root);
17
+ const current = currentReleaseVersion(root);
16
18
  const listed = spawnSync('git', ['tag', '-l', `${TAG_PREFIX}*`], { cwd: root });
17
19
  if (listed.status !== 0)
18
20
  throw new Error(`git tag -l failed: ${listed.stderr ?? ''}`);
19
21
  const newestPerLine = new Map();
20
22
  for (const line of (listed.stdout?.toString() ?? '').split('\n')) {
21
23
  const parsed = parseTag(line.trim());
22
- if (!parsed || parsed.line === current)
24
+ if (!parsed || Version.compare(parsed.version, current) > 0)
23
25
  continue;
24
26
  const winner = newestPerLine.get(parsed.line);
25
27
  if (!winner || Version.compare(parsed.version, winner.version) > 0) {
@@ -0,0 +1,75 @@
1
+ ---
2
+ id: ADR-2026-05-29-kvcache-tool-turns
3
+ status: superseded
4
+ introduced_by: PR-228
5
+ supersedes: []
6
+ superseded_by: [ADR-2026-06-02-kvcache-tool-turns-reenabled]
7
+ ---
8
+
9
+ # KV-cache session is disabled for tool-using completions
10
+
11
+ > **Superseded** by
12
+ > [ADR-2026-06-02-kvcache-tool-turns-reenabled](./2026-06-02-kvcache-tool-turns-reenabled.md):
13
+ > the cache is re-enabled for tool turns now that models load with
14
+ > `toolsMode: "dynamic"` (the SDK's DYNAMIC path is coherent, including across
15
+ > reload). Kept for history.
16
+
17
+ ## Decision
18
+
19
+ The `@qvac/sdk` session prompt cache (`completion({ kvCache })`) is passed
20
+ **only for tool-free completions** — `kvCache` is set only when
21
+ `tools.length === 0`. Agentic / tool-using turns always run without the session
22
+ cache.
23
+
24
+ ## Context
25
+
26
+ The SDK session cache has two paths (see
27
+ `@qvac/sdk → server/bare/plugins/llamacpp-completion/ops/completion-stream.ts` in the SDK):
28
+
29
+ - **STATIC** (no `tools`): on a cache hit it sends `history.slice(savedCount)`
30
+ and self-heals a stale boundary by re-sending the full history. Robust against
31
+ our reconstructed history.
32
+ - **DYNAMIC** (`tools` present): on a cache hit it sends only a role-based
33
+ _fragment_ of the history and trusts the SDK's own tool-chain flow to have
34
+ anchored the rest of the cache.
35
+
36
+ Our agentic loop builds (and on reload reconstructs from a persisted transcript)
37
+ the full history its own way. That does not satisfy the DYNAMIC path's
38
+ assumptions, so on a tool-cache hit the model receives an incoherent prompt and
39
+ emits `<tool_call>` markup as plain text instead of a parseable tool call —
40
+ silently breaking tool calling (no parse, no approval, no execution). Tools are
41
+ text-parsed (`toolCalling: "textParse"`), so a corrupted prompt degrades to a
42
+ plain assistant message rather than erroring.
43
+
44
+ ## Alternatives Considered
45
+
46
+ - **Always-on per-chat `kvCache`** (the state that introduced the regression):
47
+ breaks tool calling as above.
48
+ - **Auto-cache (`kvCache: true`)**: tool-call turns are never cached, and a
49
+ verbatim `cacheableAssistantContent` echo is required for a hit —
50
+ incompatible with our thinking-stripped reconstruction.
51
+ - **Conform to the SDK's tool-chain contract** (collapse consecutive messages +
52
+ `deleteCache` on divergence, like the legacy desktop worker): viable but a
53
+ separate project, tracked in `.ai/cursor/plans/kvcache-tool-turn-reuse.md`.
54
+
55
+ ## Consequences
56
+
57
+ - Tool turns re-prefill the full history each call (no session-cache reuse).
58
+ Tool calling stays correct; this is the prior known-good behavior.
59
+ - Tool-free turns (plain chat / title / summary) still benefit from the STATIC
60
+ cache path.
61
+ - Regression guards are GPU-free and mock-based:
62
+ `qvac-app → core/src/services/message/agentic-completion.test.ts` (argument-contract:
63
+ never send `kvCache` with `tools`) and the spec-fake
64
+ `qvac-app → core/src/services/message/__fixtures__/fake-completion-sdk.ts` (reproduces the
65
+ cache-hit tool-parse failure).
66
+ - Revisit if the SDK adds a "send full history, you slice it" mode for tools, or
67
+ when the conformance project lands.
68
+
69
+ ## When Agents Should Read This
70
+
71
+ Read this before changing how `kvCache` is passed to `sdk.completion`, the
72
+ agentic loop's history construction, or transcript reconstruction —
73
+ `qvac-app → core/src/services/message/agentic-completion.ts`,
74
+ `qvac-app → core/src/services/message/agentic-loop.ts`,
75
+ `qvac-app → core/src/services/common/transcript.ts`.