dsh-local-ai 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.3] - 2026-08-21
9
+
10
+ ### Changed
11
+
12
+ - Upgraded every `@deepseek-ai/dsh-*` peer and dev dependency from `0.1.0-rc.6` to `0.1.0-rc.8` (peer ranges now `>=0.1.0-rc.8 <0.2.0`) for DeepSeek Harness rc8 compatibility; no adapter, routing, or tool API changes were required.
13
+ - Workspace build policy: allowed the `koffi` native build (introduced by the rc8 `dsh-subprocess-local` dependency chain) and repinned `minimumReleaseAgeExclude` to the rc.8 peer family.
14
+ - Synchronized rc.8 baseline references across the five-language READMEs, AGENTS.md, THIRD_PARTY_NOTICES.md, the CI workflow name, and the compat workflow baseline.
15
+ - Merged the 0.1.2 bug-fix line (tool-call id minting and slot-reuse block handling) so this release carries both fixes.
16
+
17
+ ## [0.1.2] - 2026-08-20
18
+
19
+ ### Fixed
20
+
21
+ - Tool calls are emitted with a minted `CallId` instead of an empty one. Ollama's `/api/chat` wire format carries no tool-call id, so `OpenBlock.callId` was never assigned and both the `tool-call-delta` chunks and the closed block ended up as `CallId('')`. Live turns still paired the call with its result positionally, but the persisted `tool/result` carried an empty `source.callId` and the session failed validation on resume (`message must have tool source`).
22
+ - A tool-call array slot reused for a second, unrelated call now opens its own block instead of being diffed as a continuation. Ollama can reuse the same slot across chunks, and the longest-common-prefix diff previously emitted a fragment that reconstructed into invalid JSON while collapsing the two calls into one block.
23
+
8
24
  ## [0.1.1] - 2026-08-19
9
25
 
10
26
  ### Fixed
package/README.es.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Superficie | Estado |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.6` |
27
+ | Harness | DeepSeek Harness `0.1.0-rc.8` |
28
28
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
29
  | Backend | [Ollama](https://ollama.com) (API HTTP local + sonda CLI) |
30
30
  | Modelo | Ruta solo texto (`inputModalities: ['text']`); se admiten llamadas y resultados de herramientas |
@@ -143,7 +143,7 @@ Todos los ajustes son campos `Config` de Schemastery (modificables desde cordis.
143
143
 
144
144
  ## Known limitations
145
145
 
146
- - **Solo rc.6** — desarrollado y probado contra `@deepseek-ai/dsh@0.1.0-rc.6`; se espera que baselines más nuevos funcionen, pero los verifica el workflow mensual de compat.
146
+ - **Solo rc.8** — desarrollado y probado contra `@deepseek-ai/dsh@0.1.0-rc.8`; se espera que baselines más nuevos funcionen, pero los verifica el workflow mensual de compat.
147
147
  - **Ruta solo texto** — el contenido de imagen se rechaza (`UNSUPPORTED_CONTENT`); los modelos locales multimodales aún no están conectados.
148
148
  - **Respaldo a mitad de flujo** — una vez que una ruta local empezó a producir contenido, un fallo posterior se reenvía (no se retira); solo un fallo antes del primer token respalda a la nube.
149
149
 
@@ -151,8 +151,8 @@ Todos los ajustes son campos `Config` de Schemastery (modificables desde cordis.
151
151
 
152
152
  ```sh
153
153
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc: src + tests contra los tipos publicados 0.1.0-rc.6
155
- pnpm run typecheck:ci # tsc estricto contra los tipos publicados rc.6 (skipLibCheck off)
154
+ pnpm run typecheck # tsc: src + tests contra los tipos publicados 0.1.0-rc.8
155
+ pnpm run typecheck:ci # tsc estricto contra los tipos publicados rc.8 (skipLibCheck off)
156
156
  pnpm test # vitest: costuras reales Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess
157
157
  pnpm run test:coverage # puerta de cobertura (90/80/90/90)
158
158
  pnpm run build # bundle tsdown + declaraciones tsc (lib/)
package/README.hi.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | सतह | स्थिति |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.6` |
27
+ | Harness | DeepSeek Harness `0.1.0-rc.8` |
28
28
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
29
  | Backend | [Ollama](https://ollama.com) (स्थानीय HTTP API + CLI जाँच) |
30
30
  | Model | केवल-पाठ रूट (`inputModalities: ['text']`); टूल कॉल व परिणाम समर्थित हैं |
@@ -143,7 +143,7 @@ dsh --profile web --dump-config | grep -A2 'id: dsh-local-ai'
143
143
 
144
144
  ## Known limitations
145
145
 
146
- - **केवल rc.6** — `@deepseek-ai/dsh@0.1.0-rc.6` के विरुद्ध विकसित व परीक्षित; नए harness बेसलाइन काम करने की अपेक्षा है, पर मासिक compat workflow उन्हें सत्यापित करता है।
146
+ - **केवल rc.8** — `@deepseek-ai/dsh@0.1.0-rc.8` के विरुद्ध विकसित व परीक्षित; नए harness बेसलाइन काम करने की अपेक्षा है, पर मासिक compat workflow उन्हें सत्यापित करता है।
147
147
  - **केवल-पाठ रूट** — छवि सामग्री अस्वीकृत होती है (`UNSUPPORTED_CONTENT`); मल्टीमॉडल स्थानीय मॉडल अभी जुड़े नहीं हैं।
148
148
  - **मध्य-स्ट्रीम वापसी** — एक बार स्थानीय रूट सामग्री बनाना शुरू कर दे, तो बाद की विफलता आगे भेजी जाती है (वापस नहीं ली जाती); केवल पहले टोकन से पहले की विफलता क्लाउड पर लौटती है।
149
149
 
@@ -151,8 +151,8 @@ dsh --profile web --dump-config | grep -A2 'id: dsh-local-ai'
151
151
 
152
152
  ```sh
153
153
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc: src + परीक्षण, प्रकाशित 0.1.0-rc.6 प्रकारों के विरुद्ध
155
- pnpm run typecheck:ci # सख्त tsc, प्रकाशित rc.6 प्रकारों के विरुद्ध (skipLibCheck बंद)
154
+ pnpm run typecheck # tsc: src + परीक्षण, प्रकाशित 0.1.0-rc.8 प्रकारों के विरुद्ध
155
+ pnpm run typecheck:ci # सख्त tsc, प्रकाशित rc.8 प्रकारों के विरुद्ध (skipLibCheck बंद)
156
156
  pnpm test # vitest: वास्तविक Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess सीम
157
157
  pnpm run test:coverage # कवरेज द्वार (90/80/90/90)
158
158
  pnpm run build # tsdown बंडल + tsc घोषणाएँ (lib/)
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Surface | Status |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.6` |
27
+ | Harness | DeepSeek Harness `0.1.0-rc.8` |
28
28
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
29
  | Backend | [Ollama](https://ollama.com) (local HTTP API + CLI probe) |
30
30
  | Model | Text-only route (`inputModalities: ['text']`); tool calls and tool results are supported |
@@ -143,7 +143,7 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). An id
143
143
 
144
144
  ## Known limitations
145
145
 
146
- - **rc.6 only** — developed and tested against `@deepseek-ai/dsh@0.1.0-rc.6`; newer harness baselines are expected to work but are verified by the monthly compat workflow.
146
+ - **rc.8 only** — developed and tested against `@deepseek-ai/dsh@0.1.0-rc.8`; newer harness baselines are expected to work but are verified by the monthly compat workflow.
147
147
  - **Text-only route** — image content is rejected (`UNSUPPORTED_CONTENT`); multimodal local models are not wired up yet.
148
148
  - **Mid-stream fallback** — once a local route has started producing content, a later failure is forwarded (not retracted); only a failure before the first token falls back to the cloud.
149
149
 
@@ -151,8 +151,8 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). An id
151
151
 
152
152
  ```sh
153
153
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc: src + tests against the published 0.1.0-rc.6 types
155
- pnpm run typecheck:ci # strict tsc against published rc.6 types (skipLibCheck off)
154
+ pnpm run typecheck # tsc: src + tests against the published 0.1.0-rc.8 types
155
+ pnpm run typecheck:ci # strict tsc against published rc.8 types (skipLibCheck off)
156
156
  pnpm test # vitest: real Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess seams
157
157
  pnpm run test:coverage # coverage gate (90/80/90/90)
158
158
  pnpm run build # tsdown bundle + tsc declarations (lib/)
package/README.pt.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Superfície | Status |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.6` |
27
+ | Harness | DeepSeek Harness `0.1.0-rc.8` |
28
28
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
29
  | Backend | [Ollama](https://ollama.com) (API HTTP local + sonda CLI) |
30
30
  | Modelo | Rota somente texto (`inputModalities: ['text']`); chamadas e resultados de ferramentas são suportados |
@@ -143,7 +143,7 @@ Todos os ajustes são campos `Config` de Schemastery (modificáveis pelo cordis.
143
143
 
144
144
  ## Known limitations
145
145
 
146
- - **Somente rc.6** — desenvolvido e testado contra `@deepseek-ai/dsh@0.1.0-rc.6`; espera-se que baselines mais novos funcionem, mas são verificados pelo workflow mensal de compat.
146
+ - **Somente rc.8** — desenvolvido e testado contra `@deepseek-ai/dsh@0.1.0-rc.8`; espera-se que baselines mais novos funcionem, mas são verificados pelo workflow mensal de compat.
147
147
  - **Rota somente texto** — conteúdo de imagem é rejeitado (`UNSUPPORTED_CONTENT`); modelos locais multimodais ainda não estão conectados.
148
148
  - **Fallback no meio do fluxo** — uma vez que uma rota local começou a produzir conteúdo, uma falha posterior é reencaminhada (não retirada); apenas uma falha antes do primeiro token faz fallback para a nuvem.
149
149
 
@@ -151,8 +151,8 @@ Todos os ajustes são campos `Config` de Schemastery (modificáveis pelo cordis.
151
151
 
152
152
  ```sh
153
153
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc: src + testes contra os tipos publicados 0.1.0-rc.6
155
- pnpm run typecheck:ci # tsc estrito contra os tipos publicados rc.6 (skipLibCheck off)
154
+ pnpm run typecheck # tsc: src + testes contra os tipos publicados 0.1.0-rc.8
155
+ pnpm run typecheck:ci # tsc estrito contra os tipos publicados rc.8 (skipLibCheck off)
156
156
  pnpm test # vitest: costuras reais Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess
157
157
  pnpm run test:coverage # porta de cobertura (90/80/90/90)
158
158
  pnpm run build # bundle tsdown + declarações tsc (lib/)
package/README.zh.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | 项目 | 状态 |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.6` |
27
+ | Harness | DeepSeek Harness `0.1.0-rc.8` |
28
28
  | Node | `^22.19.0 \|\| >=24.0.0` |
29
29
  | 后端 | [Ollama](https://ollama.com)(本地 HTTP API + CLI 探测) |
30
30
  | 模型 | 纯文本路由(`inputModalities: ['text']`);支持工具调用与工具结果 |
@@ -143,7 +143,7 @@ dsh --profile web --dump-config | grep -A2 'id: dsh-local-ai'
143
143
 
144
144
  ## Known limitations
145
145
 
146
- - **仅 rc.6** —— 针对 `@deepseek-ai/dsh@0.1.0-rc.6` 开发与测试;更新版本的 harness 基线预期可用,但由每月 compat workflow 验证。
146
+ - **仅 rc.8** —— 针对 `@deepseek-ai/dsh@0.1.0-rc.8` 开发与测试;更新版本的 harness 基线预期可用,但由每月 compat workflow 验证。
147
147
  - **纯文本路由** —— 图片内容会被拒绝(`UNSUPPORTED_CONTENT`);多模态本地模型尚未接入。
148
148
  - **中途失败不回退** —— 本地路由一旦开始产出内容,之后的失败会透传(无法撤回);只有首个 token 前的失败才回退云端。
149
149
 
@@ -151,8 +151,8 @@ dsh --profile web --dump-config | grep -A2 'id: dsh-local-ai'
151
151
 
152
152
  ```sh
153
153
  pnpm install # node ^22.19 || >=24
154
- pnpm run typecheck # tsc:src + 测试,对发布版 0.1.0-rc.6 类型
155
- pnpm run typecheck:ci # 严格 tsc,对发布版 rc.6 类型(关闭 skipLibCheck)
154
+ pnpm run typecheck # tsc:src + 测试,对发布版 0.1.0-rc.8 类型
155
+ pnpm run typecheck:ci # 严格 tsc,对发布版 rc.8 类型(关闭 skipLibCheck)
156
156
  pnpm test # vitest:真实 Context/LlmRuntime/ToolRuntime/CommandRuntime/subprocess 机制
157
157
  pnpm run test:coverage # 覆盖率门禁(90/80/90/90)
158
158
  pnpm run build # tsdown 打包 + tsc 声明(lib/)
@@ -13,7 +13,7 @@ published tarball; these are install-time dependencies:
13
13
  | [typescript](https://github.com/microsoft/TypeScript) | `^5.9.0` | Apache-2.0 | Build-time declaration emission (`lib/types/`) |
14
14
  | [@deepseek-ai/cordis](https://www.npmjs.com/package/@deepseek-ai/cordis) | `^4.0.1` (peer) | See package | The plugin runtime |
15
15
  | [@deepseek-ai/schemastery](https://www.npmjs.com/package/@deepseek-ai/schemastery) | `^3.18.0` (peer) | See package | Configuration schema |
16
- | `@deepseek-ai/dsh-*` peers | `0.1.0-rc.6` (peer) | See packages | Official harness seams (`dsh-llm`, `dsh-tools`, `dsh-subprocess`, `dsh-commands`, `dsh-timeout`) |
16
+ | `@deepseek-ai/dsh-*` peers | `0.1.0-rc.8` (peer) | See packages | Official harness seams (`dsh-llm`, `dsh-tools`, `dsh-subprocess`, `dsh-commands`, `dsh-timeout`) |
17
17
 
18
18
  At runtime the plugin only talks to the Ollama endpoint you configure (over
19
19
  its HTTP API and, for the process-liveness probe, its CLI); it performs no
package/lib/index.js CHANGED
@@ -590,6 +590,18 @@ function closeBlock(block) {
590
590
  }
591
591
  }
592
592
  /**
593
+ * Mint a session-unique tool-call id for a freshly opened block. Ollama's
594
+ * wire format carries no tool-call id, so one must be synthesized: without it
595
+ * the emitted chunks and the persisted tool result all carry an empty CallId,
596
+ * and the session fails validation on resume. The block index keeps
597
+ * same-millisecond parallel calls apart.
598
+ * @param block - the freshly opened tool-call block.
599
+ * @returns the minted id.
600
+ */
601
+ function mintCallId(block) {
602
+ return `ollama-${Date.now().toString(36)}-${block.index}-${Math.random().toString(36).slice(2, 8)}`;
603
+ }
604
+ /**
593
605
  * Compute the append delta from a cumulative JSON string, so the harness's
594
606
  * delta-concatenating assembler reconstructs the full arguments. Ollama grows
595
607
  * the arguments object monotonically, so the delta is everything past the
@@ -605,6 +617,36 @@ function argumentsDelta(previous, next) {
605
617
  return next.slice(index);
606
618
  }
607
619
  /**
620
+ * Whether the previous cumulative arguments object is a key/value subset of
621
+ * the next one, i.e. one call grew monotonically. While one call streams,
622
+ * Ollama only adds keys; when it reuses a slot for a new call, existing keys
623
+ * reappear with different values, so the subset check fails and the slot must
624
+ * open a fresh block. Non-object arguments (or either side failing to parse,
625
+ * which cannot happen for JSON.stringify output but keeps the guard total)
626
+ * fall back to treating the chunk as a continuation.
627
+ * @param previous - the previously seen cumulative JSON (or `''`).
628
+ * @param next - the new cumulative JSON.
629
+ * @returns `true` when `next` extends `previous` monotonically.
630
+ */
631
+ function isMonotonicExtension(previous, next) {
632
+ if (previous === "") return true;
633
+ let prevObj;
634
+ let nextObj;
635
+ try {
636
+ prevObj = JSON.parse(previous);
637
+ nextObj = JSON.parse(next);
638
+ } catch {
639
+ return true;
640
+ }
641
+ if (typeof prevObj !== "object" || prevObj === null || Array.isArray(prevObj)) return true;
642
+ if (typeof nextObj !== "object" || nextObj === null || Array.isArray(nextObj)) return true;
643
+ for (const [key, value] of Object.entries(prevObj)) {
644
+ if (!(key in nextObj)) return false;
645
+ if (JSON.stringify(nextObj[key]) !== JSON.stringify(value)) return false;
646
+ }
647
+ return true;
648
+ }
649
+ /**
608
650
  * Consume Ollama NDJSON lines and yield StreamChunks. Text and reasoning deltas
609
651
  * stream as they arrive; tool-call deltas are diffed from the cumulative wire
610
652
  * arguments; `block-end`, `usage`, and `finish` are deferred to the `done`
@@ -680,6 +722,7 @@ async function* translate(lines) {
680
722
  let block = toolBlocks.get(callIndex);
681
723
  if (!block) {
682
724
  block = open("tool-call");
725
+ block.callId = mintCallId(block);
683
726
  toolBlocks.set(callIndex, block);
684
727
  toolArguments.set(callIndex, "");
685
728
  yield {
@@ -692,7 +735,19 @@ async function* translate(lines) {
692
735
  const cumulative = JSON.stringify(call?.function?.arguments ?? {});
693
736
  const previous = toolArguments.get(callIndex) ?? "";
694
737
  if (cumulative !== previous) {
695
- const fragment = argumentsDelta(previous, cumulative);
738
+ if (!isMonotonicExtension(previous, cumulative)) {
739
+ block = open("tool-call");
740
+ block.callId = mintCallId(block);
741
+ if (call?.function?.name !== void 0) block.name = call.function.name;
742
+ toolBlocks.set(callIndex, block);
743
+ toolArguments.set(callIndex, "");
744
+ yield {
745
+ type: "block-start",
746
+ index: block.index,
747
+ blockType: "tool-call"
748
+ };
749
+ }
750
+ const fragment = argumentsDelta(toolArguments.get(callIndex) ?? "", cumulative);
696
751
  toolArguments.set(callIndex, cumulative);
697
752
  block.text += fragment;
698
753
  yield {
@@ -737,7 +792,7 @@ async function* translate(lines) {
737
792
  throw new LlmError("Ollama NDJSON stream ended without a done chunk", "STREAM_CLOSED");
738
793
  }
739
794
  //#endregion
740
- //#region \0@oxc-project+runtime@0.144.0/helpers/esm/usingCtx.js
795
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/usingCtx.js
741
796
  function _usingCtx() {
742
797
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
743
798
  var n = Error();
@@ -1101,7 +1156,7 @@ async function checkHealth(baseURL, fetchImpl, subprocess, requestTimeoutMs, gra
1101
1156
  * The `dsh-local-ai` plugin version. The release script bumps this string
1102
1157
  * alongside `package.json`; `test/version.spec.ts` trips when the two drift.
1103
1158
  */
1104
- const VERSION = "0.1.1";
1159
+ const VERSION = "0.1.3";
1105
1160
  //#endregion
1106
1161
  //#region src/index.ts
1107
1162
  const name = "local-ai";
@@ -44,6 +44,19 @@ export declare function mapFinishReason(reason: string): FinishReason;
44
44
  * @returns the fragment to append.
45
45
  */
46
46
  export declare function argumentsDelta(previous: string, next: string): string;
47
+ /**
48
+ * Whether the previous cumulative arguments object is a key/value subset of
49
+ * the next one, i.e. one call grew monotonically. While one call streams,
50
+ * Ollama only adds keys; when it reuses a slot for a new call, existing keys
51
+ * reappear with different values, so the subset check fails and the slot must
52
+ * open a fresh block. Non-object arguments (or either side failing to parse,
53
+ * which cannot happen for JSON.stringify output but keeps the guard total)
54
+ * fall back to treating the chunk as a continuation.
55
+ * @param previous - the previously seen cumulative JSON (or `''`).
56
+ * @param next - the new cumulative JSON.
57
+ * @returns `true` when `next` extends `previous` monotonically.
58
+ */
59
+ export declare function isMonotonicExtension(previous: string, next: string): boolean;
47
60
  /**
48
61
  * Consume Ollama NDJSON lines and yield StreamChunks. Text and reasoning deltas
49
62
  * stream as they arrive; tool-call deltas are diffed from the cumulative wire
@@ -1 +1 @@
1
- {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/translate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAgB,YAAY,EAAE,WAAW,EAAc,MAAM,sBAAsB,CAAA;AAE/F,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,KAAK,CAAC;YAAE,QAAQ,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;gBAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;aAAE,CAAA;SAAE,CAAC,CAAA;KAC1F,CAAA;IACD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAYD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAW5D;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED;;;;;;;;GAQG;AACH,wBAAuB,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC,WAAW,CAAC,CA4G1F"}
1
+ {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/translate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAgB,YAAY,EAAE,WAAW,EAAc,MAAM,sBAAsB,CAAA;AAE/F,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,KAAK,CAAC;YAAE,QAAQ,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;gBAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;aAAE,CAAA;SAAE,CAAC,CAAA;KAC1F,CAAA;IACD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAYD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAW5D;AA6BD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAiB5E;AAED;;;;;;;;GAQG;AACH,wBAAuB,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC,WAAW,CAAC,CA0H1F"}
@@ -38,6 +38,18 @@ function closeBlock(block) {
38
38
  };
39
39
  }
40
40
  }
41
+ /**
42
+ * Mint a session-unique tool-call id for a freshly opened block. Ollama's
43
+ * wire format carries no tool-call id, so one must be synthesized: without it
44
+ * the emitted chunks and the persisted tool result all carry an empty CallId,
45
+ * and the session fails validation on resume. The block index keeps
46
+ * same-millisecond parallel calls apart.
47
+ * @param block - the freshly opened tool-call block.
48
+ * @returns the minted id.
49
+ */
50
+ function mintCallId(block) {
51
+ return `ollama-${Date.now().toString(36)}-${block.index}-${Math.random().toString(36).slice(2, 8)}`;
52
+ }
41
53
  /**
42
54
  * Compute the append delta from a cumulative JSON string, so the harness's
43
55
  * delta-concatenating assembler reconstructs the full arguments. Ollama grows
@@ -55,6 +67,42 @@ export function argumentsDelta(previous, next) {
55
67
  index += 1;
56
68
  return next.slice(index);
57
69
  }
70
+ /**
71
+ * Whether the previous cumulative arguments object is a key/value subset of
72
+ * the next one, i.e. one call grew monotonically. While one call streams,
73
+ * Ollama only adds keys; when it reuses a slot for a new call, existing keys
74
+ * reappear with different values, so the subset check fails and the slot must
75
+ * open a fresh block. Non-object arguments (or either side failing to parse,
76
+ * which cannot happen for JSON.stringify output but keeps the guard total)
77
+ * fall back to treating the chunk as a continuation.
78
+ * @param previous - the previously seen cumulative JSON (or `''`).
79
+ * @param next - the new cumulative JSON.
80
+ * @returns `true` when `next` extends `previous` monotonically.
81
+ */
82
+ export function isMonotonicExtension(previous, next) {
83
+ if (previous === '')
84
+ return true;
85
+ let prevObj;
86
+ let nextObj;
87
+ try {
88
+ prevObj = JSON.parse(previous);
89
+ nextObj = JSON.parse(next);
90
+ }
91
+ catch {
92
+ return true;
93
+ }
94
+ if (typeof prevObj !== 'object' || prevObj === null || Array.isArray(prevObj))
95
+ return true;
96
+ if (typeof nextObj !== 'object' || nextObj === null || Array.isArray(nextObj))
97
+ return true;
98
+ for (const [key, value] of Object.entries(prevObj)) {
99
+ if (!(key in nextObj))
100
+ return false;
101
+ if (JSON.stringify(nextObj[key]) !== JSON.stringify(value))
102
+ return false;
103
+ }
104
+ return true;
105
+ }
58
106
  /**
59
107
  * Consume Ollama NDJSON lines and yield StreamChunks. Text and reasoning deltas
60
108
  * stream as they arrive; tool-call deltas are diffed from the cumulative wire
@@ -115,6 +163,7 @@ export async function* translate(lines) {
115
163
  let block = toolBlocks.get(callIndex);
116
164
  if (!block) {
117
165
  block = open('tool-call');
166
+ block.callId = mintCallId(block);
118
167
  toolBlocks.set(callIndex, block);
119
168
  toolArguments.set(callIndex, '');
120
169
  yield { type: 'block-start', index: block.index, blockType: 'tool-call' };
@@ -125,7 +174,21 @@ export async function* translate(lines) {
125
174
  const cumulative = JSON.stringify(call?.function?.arguments ?? {});
126
175
  const previous = toolArguments.get(callIndex) ?? '';
127
176
  if (cumulative !== previous) {
128
- const fragment = argumentsDelta(previous, cumulative);
177
+ if (!isMonotonicExtension(previous, cumulative)) {
178
+ // The cumulative arguments no longer extend the ones this slot has
179
+ // seen: Ollama reused the array slot for a second, unrelated call.
180
+ // Diffing against the old call would emit a fragment that
181
+ // reconstructs into invalid JSON and collapse the two calls into
182
+ // one block, so open a fresh block for the new call instead.
183
+ block = open('tool-call');
184
+ block.callId = mintCallId(block);
185
+ if (call?.function?.name !== undefined)
186
+ block.name = call.function.name;
187
+ toolBlocks.set(callIndex, block);
188
+ toolArguments.set(callIndex, '');
189
+ yield { type: 'block-start', index: block.index, blockType: 'tool-call' };
190
+ }
191
+ const fragment = argumentsDelta(toolArguments.get(callIndex) ?? '', cumulative);
129
192
  toolArguments.set(callIndex, cumulative);
130
193
  block.text += fragment;
131
194
  yield {
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/translate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/translate.ts"],"names":[],"mappings":"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"}
@@ -2,5 +2,5 @@
2
2
  * The `dsh-local-ai` plugin version. The release script bumps this string
3
3
  * alongside `package.json`; `test/version.spec.ts` trips when the two drift.
4
4
  */
5
- export declare const VERSION = "0.1.1";
5
+ export declare const VERSION = "0.1.3";
6
6
  //# sourceMappingURL=version.d.ts.map
@@ -2,5 +2,5 @@
2
2
  * The `dsh-local-ai` plugin version. The release script bumps this string
3
3
  * alongside `package.json`; `test/version.spec.ts` trips when the two drift.
4
4
  */
5
- export const VERSION = '0.1.1';
5
+ export const VERSION = '0.1.3';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-local-ai",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local-model (Ollama) integration for DeepSeek Harness: discover, pull, remove, and inspect local models, route requests to them by task type or keyword with automatic fallback to the cloud, and get a one-shot status overview via /ollama.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,7 +64,7 @@
64
64
  ],
65
65
  "compatibility": {
66
66
  "dshVersions": [
67
- "0.1.0-rc.6"
67
+ "0.1.0-rc.8"
68
68
  ]
69
69
  },
70
70
  "capability": {
@@ -99,11 +99,11 @@
99
99
  "packageManager": "pnpm@11.7.0",
100
100
  "peerDependencies": {
101
101
  "@deepseek-ai/cordis": "^4.0.1",
102
- "@deepseek-ai/dsh-commands": "0.1.0-rc.6",
103
- "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
104
- "@deepseek-ai/dsh-subprocess": "0.1.0-rc.6",
105
- "@deepseek-ai/dsh-timeout": "0.1.0-rc.6",
106
- "@deepseek-ai/dsh-tools": "0.1.0-rc.6",
102
+ "@deepseek-ai/dsh-commands": ">=0.1.0-rc.8 <0.2.0",
103
+ "@deepseek-ai/dsh-llm": ">=0.1.0-rc.8 <0.2.0",
104
+ "@deepseek-ai/dsh-subprocess": ">=0.1.0-rc.8 <0.2.0",
105
+ "@deepseek-ai/dsh-timeout": ">=0.1.0-rc.8 <0.2.0",
106
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.2.0",
107
107
  "@deepseek-ai/schemastery": "^3.18.0"
108
108
  },
109
109
  "dependencies": {
@@ -114,14 +114,14 @@
114
114
  "@deepseek-ai/cordis": "^4.0.1",
115
115
  "@deepseek-ai/cordis-plugin-include": "1.0.6",
116
116
  "@deepseek-ai/cordis-plugin-loader": "1.0.2",
117
- "@deepseek-ai/dsh-commands": "0.1.0-rc.6",
118
- "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
119
- "@deepseek-ai/dsh-session": "0.1.0-rc.6",
120
- "@deepseek-ai/dsh-subprocess": "0.1.0-rc.6",
121
- "@deepseek-ai/dsh-subprocess-local": "0.1.0-rc.6",
122
- "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6",
123
- "@deepseek-ai/dsh-timeout": "0.1.0-rc.6",
124
- "@deepseek-ai/dsh-tools": "0.1.0-rc.6",
117
+ "@deepseek-ai/dsh-commands": "0.1.0-rc.8",
118
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
119
+ "@deepseek-ai/dsh-session": "0.1.0-rc.8",
120
+ "@deepseek-ai/dsh-subprocess": "0.1.0-rc.8",
121
+ "@deepseek-ai/dsh-subprocess-local": "0.1.0-rc.8",
122
+ "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.8",
123
+ "@deepseek-ai/dsh-timeout": "0.1.0-rc.8",
124
+ "@deepseek-ai/dsh-tools": "0.1.0-rc.8",
125
125
  "@deepseek-ai/schemastery": "^3.18.0",
126
126
  "@types/node": "^22.19.0",
127
127
  "@vitest/coverage-v8": "^3.2.0",
package/src/translate.ts CHANGED
@@ -69,6 +69,19 @@ function closeBlock(block: OpenBlock): ContentBlock {
69
69
  }
70
70
  }
71
71
 
72
+ /**
73
+ * Mint a session-unique tool-call id for a freshly opened block. Ollama's
74
+ * wire format carries no tool-call id, so one must be synthesized: without it
75
+ * the emitted chunks and the persisted tool result all carry an empty CallId,
76
+ * and the session fails validation on resume. The block index keeps
77
+ * same-millisecond parallel calls apart.
78
+ * @param block - the freshly opened tool-call block.
79
+ * @returns the minted id.
80
+ */
81
+ function mintCallId(block: OpenBlock): string {
82
+ return `ollama-${Date.now().toString(36)}-${block.index}-${Math.random().toString(36).slice(2, 8)}`
83
+ }
84
+
72
85
  /**
73
86
  * Compute the append delta from a cumulative JSON string, so the harness's
74
87
  * delta-concatenating assembler reconstructs the full arguments. Ollama grows
@@ -85,6 +98,37 @@ export function argumentsDelta(previous: string, next: string): string {
85
98
  return next.slice(index)
86
99
  }
87
100
 
101
+ /**
102
+ * Whether the previous cumulative arguments object is a key/value subset of
103
+ * the next one, i.e. one call grew monotonically. While one call streams,
104
+ * Ollama only adds keys; when it reuses a slot for a new call, existing keys
105
+ * reappear with different values, so the subset check fails and the slot must
106
+ * open a fresh block. Non-object arguments (or either side failing to parse,
107
+ * which cannot happen for JSON.stringify output but keeps the guard total)
108
+ * fall back to treating the chunk as a continuation.
109
+ * @param previous - the previously seen cumulative JSON (or `''`).
110
+ * @param next - the new cumulative JSON.
111
+ * @returns `true` when `next` extends `previous` monotonically.
112
+ */
113
+ export function isMonotonicExtension(previous: string, next: string): boolean {
114
+ if (previous === '') return true
115
+ let prevObj: unknown
116
+ let nextObj: unknown
117
+ try {
118
+ prevObj = JSON.parse(previous)
119
+ nextObj = JSON.parse(next)
120
+ } catch {
121
+ return true
122
+ }
123
+ if (typeof prevObj !== 'object' || prevObj === null || Array.isArray(prevObj)) return true
124
+ if (typeof nextObj !== 'object' || nextObj === null || Array.isArray(nextObj)) return true
125
+ for (const [key, value] of Object.entries(prevObj)) {
126
+ if (!(key in nextObj)) return false
127
+ if (JSON.stringify((nextObj as Record<string, unknown>)[key]) !== JSON.stringify(value)) return false
128
+ }
129
+ return true
130
+ }
131
+
88
132
  /**
89
133
  * Consume Ollama NDJSON lines and yield StreamChunks. Text and reasoning deltas
90
134
  * stream as they arrive; tool-call deltas are diffed from the cumulative wire
@@ -150,6 +194,7 @@ export async function* translate(lines: AsyncIterable<string>): AsyncGenerator<S
150
194
  let block = toolBlocks.get(callIndex)
151
195
  if (!block) {
152
196
  block = open('tool-call')
197
+ block.callId = mintCallId(block)
153
198
  toolBlocks.set(callIndex, block)
154
199
  toolArguments.set(callIndex, '')
155
200
  yield { type: 'block-start', index: block.index, blockType: 'tool-call' }
@@ -160,7 +205,20 @@ export async function* translate(lines: AsyncIterable<string>): AsyncGenerator<S
160
205
  const cumulative = JSON.stringify(call?.function?.arguments ?? {})
161
206
  const previous = toolArguments.get(callIndex) ?? ''
162
207
  if (cumulative !== previous) {
163
- const fragment = argumentsDelta(previous, cumulative)
208
+ if (!isMonotonicExtension(previous, cumulative)) {
209
+ // The cumulative arguments no longer extend the ones this slot has
210
+ // seen: Ollama reused the array slot for a second, unrelated call.
211
+ // Diffing against the old call would emit a fragment that
212
+ // reconstructs into invalid JSON and collapse the two calls into
213
+ // one block, so open a fresh block for the new call instead.
214
+ block = open('tool-call')
215
+ block.callId = mintCallId(block)
216
+ if (call?.function?.name !== undefined) block.name = call.function.name
217
+ toolBlocks.set(callIndex, block)
218
+ toolArguments.set(callIndex, '')
219
+ yield { type: 'block-start', index: block.index, blockType: 'tool-call' }
220
+ }
221
+ const fragment = argumentsDelta(toolArguments.get(callIndex) ?? '', cumulative)
164
222
  toolArguments.set(callIndex, cumulative)
165
223
  block.text += fragment
166
224
  yield {
package/src/version.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * The `dsh-local-ai` plugin version. The release script bumps this string
3
3
  * alongside `package.json`; `test/version.spec.ts` trips when the two drift.
4
4
  */
5
- export const VERSION = '0.1.1'
5
+ export const VERSION = '0.1.3'