@deepseek-ai/dsh-spill-policy 0.1.2-alpha.5 → 0.1.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/spill/spill-policy/README.md
5
- README.md: 4e7d83e81c865952c14af31cf13b7094e7e5f071
6
- README.zh.md: ef6036478a8d751f9de853df096397e216d4163a
5
+ README.md: 1ccf250bacd0b896e6f140393fd3447de3af1ab0
6
+ README.zh.md: e6c537a1c9bd0c24a09063898638a12bfe84466d
package/README.md CHANGED
@@ -86,11 +86,17 @@ The policy is deliberately narrow: it only decides **when** to spill and compose
86
86
 
87
87
  A `tools/post-execute` waterfall listener (registered with `prepend`, delegating via `next()`) bounds the model-facing result; a `tools/ptc-dispatch-log` listener bounds the durable log copy of each `run_code` sub-call. Both share one replacement helper so the two projections are byte-identical. The post-execute arm skips `read` to avoid a read → spill → read loop; the dispatch-log arm bounds `read` sub-calls because a log copy is not model context.
88
88
 
89
+ <a id="shared-notice-ownership"></a>
90
+ ### Shared notice ownership
91
+
92
+ The browser-safe `@deepseek-ai/dsh-spill-policy/notice` entry owns both `formatSpillNotice(omitted, ref)`, used by the producer, and `hasSpillNotice(text)`, used by presentation consumers. Formatting and recognition share the notice delimiters; omission validation uses the existing `describeOmitted` formatter rather than a second copy of its prose. Recognition accepts a complete final notice after a preview or by itself and preserves the persisted notice spelling. It reads recorded text without rewriting it.
93
+
89
94
  ### Source map
90
95
 
91
96
  | File | Role |
92
97
  |---|---|
93
98
  | [`src/index.ts`](src/index.ts) | Plugin entry: `Config` validation, the two waterfall listeners, the shared replacement helper |
99
+ | [`src/notice.ts`](src/notice.ts) | Browser-safe notice formatting and recognition, published as `./notice` |
94
100
  | [`src/types.ts`](src/types.ts) | `SpillPolicyExec`: the minimal structural view of a tool execution the policy reads for the owning session id |
95
101
  | — | No runtime invariant companion is published; this package exposes no independent event sequence or mutable data relation beyond contracts enforced at its owning seam. |
96
102
 
@@ -111,7 +117,6 @@ Read these pages when the package-level contract is not enough.
111
117
  - [dsh-spill-local](../spill-local/README.md) — the local backend that stores the spilled text.
112
118
  - [dsh-output-retention](../../util/output-retention/README.md) — the preview mechanics (`TextRetainer`) the policy composes.
113
119
  - [Tool output spill decision](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md) — the capability boundary and design rationale.
114
- - [PTC dispatch-log spill decision](../../../.agents/notes/implemented/feature/2026-07-26-ptc-dispatch-log-spill.md) — why the durable log copy is bounded too.
115
120
 
116
121
  -----
117
122
 
@@ -139,6 +144,7 @@ Append-only; newly visible content follows the reusable request prefix and does
139
144
 
140
145
  These limits define when the policy cannot help. They are current package constraints.
141
146
 
147
+ - **Text recognition cannot authenticate output** — a tool can print the same notice text; `hasSpillNotice` identifies a text convention, not proof that the policy saved a result.
142
148
  - **Only final plain-text results are spillable** — mixed-content results, blocked feedback, and `read` pass through; provider truncation or tool-owned retention that happened earlier cannot be recovered here.
143
149
  - **A notice that cannot fit disables replacement for that call** — a tiny cap or long locator leaves the oversized original inline after the backend has already saved an unreferenced spill.
144
150
 
package/README.zh.md CHANGED
@@ -86,11 +86,17 @@ kind: "package-reference"
86
86
 
87
87
  `tools/post-execute` waterfall(瀑布式事件)监听器(以 `prepend` 注册、通过 `next()` 委托)约束面向模型的结果;`tools/ptc-dispatch-log` 监听器约束每个 `run_code` 子调用的持久日志副本。两者共享同一个替换辅助函数,因此两个投影字节一致。post-execute 分支跳过 `read` 以避免 read → spill → read 循环;dispatch-log 分支约束 `read` 子调用,因为日志副本不是模型上下文。
88
88
 
89
+ <a id="shared-notice-ownership"></a>
90
+ ### 共享通知的所有权
91
+
92
+ 浏览器安全入口 `@deepseek-ai/dsh-spill-policy/notice` 同时负责生产方使用的 `formatSpillNotice(omitted, ref)` 和展示消费方使用的 `hasSpillNotice(text)`。格式化与识别共用通知分隔符;省略信息通过现有的 `describeOmitted` 格式化函数校验,而非复制一套文案。识别支持预览之后或单独出现的完整末尾通知,并保留持久化通知的原有拼写。它只读取已记录的文本,不改写文本。
93
+
89
94
  ### 源码地图
90
95
 
91
96
  | 文件 | 职责 |
92
97
  |---|---|
93
98
  | [`src/index.ts`](src/index.ts) | 插件入口:`Config` 校验、两个 waterfall 监听器、共享替换辅助函数 |
99
+ | [`src/notice.ts`](src/notice.ts) | 浏览器安全的通知格式化与识别,以 `./notice` 发布 |
94
100
  | [`src/types.ts`](src/types.ts) | `SpillPolicyExec`:策略读取所属会话 id 所需的最小结构化工具执行视图 |
95
101
  | — | 不发布运行时不变式伴生入口;约定在 seam 处强制执行。 |
96
102
 
@@ -111,7 +117,6 @@ kind: "package-reference"
111
117
  - [dsh-spill-local](../spill-local/README.zh.md)——保存 spill 文本的本地后端。
112
118
  - [dsh-output-retention](../../util/output-retention/README.zh.md)——策略组合的预览机制(`TextRetainer`)。
113
119
  - [工具输出 spill 决策](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md)——能力边界与设计依据。
114
- - [PTC dispatch-log spill 决策](../../../.agents/notes/implemented/feature/2026-07-26-ptc-dispatch-log-spill.zh.md)——为何持久日志副本同样设界。
115
120
 
116
121
  -----
117
122
 
@@ -139,6 +144,7 @@ kind: "package-reference"
139
144
 
140
145
  这些限制说明策略在哪些情况下无法提供帮助。它们是当前的包约束。
141
146
 
147
+ - **文本识别无法认证输出来源**——工具也能打印相同的通知文本;`hasSpillNotice` 识别的是文本约定,不能证明策略保存过结果。
142
148
  - **只能对最终纯文本结果执行 spill**——混合内容结果、阻止反馈与 `read` 会原样通过;此前已经发生的提供方截断或工具自有保留无法在此恢复。
143
149
  - **通知无法容纳时会禁用该次调用的替换**——上限极小或定位信息很长时,后端已经保存了无引用的 spill,但过大的原始结果仍留在内联位置。
144
150
 
package/lib/index.js CHANGED
@@ -1,5 +1,27 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { TextRetainer, describeOmitted } from "@deepseek-ai/dsh-output-retention";
3
+ //#region lib/types/notice.js
4
+ /** Browser-safe formatting and recognition of persisted spill-policy notices. */
5
+ const OPEN = "(";
6
+ const CLOSE = ")";
7
+ const LOCATION = " Full formatted result stored at: ";
8
+ const GUIDANCE_SEPARATOR = ". ";
9
+ const EXACT_OMISSION = describeOmitted({
10
+ kind: "exact",
11
+ count: 0
12
+ }, "bytes");
13
+ const COUNT_OFFSET = EXACT_OMISSION.indexOf("0");
14
+ EXACT_OMISSION.slice(COUNT_OFFSET + 1);
15
+ /**
16
+ * Format the notice appended to a retained preview, preserving its persisted spelling.
17
+ * @param omitted - bytes omitted by the retention policy.
18
+ * @param ref - saved text locator and retrieval guidance.
19
+ * @returns the complete notice without a leading preview separator.
20
+ */
21
+ function formatSpillNotice(omitted, ref) {
22
+ return `${OPEN}${describeOmitted(omitted, "bytes")}${LOCATION}${ref.locator}${GUIDANCE_SEPARATOR}${ref.retrievalHint}${CLOSE}`;
23
+ }
24
+ //#endregion
3
25
  //#region lib/types/index.js
4
26
  /**
5
27
  * The spill-policy PLUGIN: a `tools/post-execute` result transformer that keeps
@@ -77,10 +99,6 @@ function preview(text, budget) {
77
99
  omitted: kept.omittedBytes
78
100
  };
79
101
  }
80
- /** The spill-notice line for a given omission + saved reference (no preview, no leading blank line). */
81
- function spillNotice(omitted, ref) {
82
- return `(${describeOmitted(omitted, "bytes")} Full formatted result stored at: ${ref.locator}. ${ref.retrievalHint})`;
83
- }
84
102
  function apply(ctx, config) {
85
103
  const maxInlineBytes = config.maxInlineBytes;
86
104
  if (maxInlineBytes === void 0) return;
@@ -106,6 +124,7 @@ function apply(ctx, config) {
106
124
  const save = {
107
125
  owner: { sessionId },
108
126
  source: {
127
+ kind: "tool",
109
128
  toolName,
110
129
  callId,
111
130
  label
@@ -120,12 +139,12 @@ function apply(ctx, config) {
120
139
  ctx.logger.warn(`spill-policy: saveText failed for ${toolName}: ${String(error)}; keeping the inline content`);
121
140
  return;
122
141
  }
123
- const reserve = Buffer.byteLength(spillNotice({
142
+ const reserve = Buffer.byteLength(formatSpillNotice({
124
143
  kind: "exact",
125
144
  count: totalBytes
126
145
  }, ref), "utf8") + 2;
127
146
  const { text: previewText, omitted } = preview(text, Math.max(0, cap - reserve));
128
- const notice = spillNotice(omitted, ref);
147
+ const notice = formatSpillNotice(omitted, ref);
129
148
  const replacedText = previewText.length > 0 ? `${previewText}\n\n${notice}` : notice;
130
149
  if (Buffer.byteLength(replacedText, "utf8") > cap) {
131
150
  ctx.logger.warn(`spill-policy: spill notice for ${toolName} exceeds maxInlineBytes; keeping the inline content`);
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The spill-policy PLUGIN: a `tools/post-execute` result transformer that keeps
3
+ * oversized plain-text tool results out of the model's context. When a final
4
+ * result's UTF-8 size exceeds `maxInlineBytes`, it saves the FULL text to a
5
+ * session-scoped spill artifact (`ctx.spillStore`) and replaces the
6
+ * model-facing result with a bounded head/tail preview plus the backend's
7
+ * locator and retrieval guidance.
8
+ *
9
+ * It registers NO service and owns NO storage or preview mechanics: preview is
10
+ * `@deepseek-ai/dsh-output-retention` (`TextRetainer`), storage is `ctx.spillStore`.
11
+ * The policy only decides WHEN to spill and composes the notice.
12
+ *
13
+ * A second arm applies the SAME cap to the durable log: the
14
+ * `tools/ptc-dispatch-log` waterfall bounds the `tool/code-dispatch` event's
15
+ * copy of an oversized `run_code` sub-call result (the program's value is
16
+ * untouched; UIs and replay read the full text through the spill artifact).
17
+ *
18
+ * ## Deliberately narrow
19
+ *
20
+ * - Omitted `maxInlineBytes` ⇒ the plugin registers nothing (a true no-op).
21
+ * - Plain-text results only: a result carrying any non-text block is left
22
+ * untouched (the policy knows only the final formatted text, not tool
23
+ * internals).
24
+ * - Nested composite calls skip the MODEL-facing arm; their durable log copy
25
+ * is bounded by the dispatch-log arm instead.
26
+ * - Accepted value replacements pass through for registry revalidation and
27
+ * rendering; this presentation policy cannot also replace content in the
28
+ * same mutually exclusive decision.
29
+ * - `read` is skipped by the model-facing arm to avoid a
30
+ * `read → spill → read again` loop; the dispatch-log arm bounds `read`
31
+ * sub-calls too (a log copy is not model context, and `read` is precisely
32
+ * the tool that produces huge logs).
33
+ * - Best-effort: no session owner, no `ctx.spillStore` backend, or a save
34
+ * failure ⇒ log and return the original result. A spill failure must NEVER
35
+ * turn a successful tool call into an `isError` or hide the inline result.
36
+ *
37
+ * It COMPOSES with other post-execute listeners: its prepended listener
38
+ * delegates via `next()` and bounds the resulting content projection, so
39
+ * tool-owned asynchronous projection runs before generic bounding, a hook that
40
+ * replaced content still has its replacement bounded, and value replacements
41
+ * and `block` decisions pass through unchanged.
42
+ *
43
+ * @module @deepseek-ai/dsh-spill-policy
44
+ */
45
+ import z from '@deepseek-ai/schemastery';
46
+ import { TextRetainer } from '@deepseek-ai/dsh-output-retention';
47
+ import { formatSpillNotice } from "./notice.js";
48
+ /** Cordis plugin name used by loader diagnostics. */
49
+ export const name = 'spill-policy';
50
+ /** Require the tool registry (its `tools/post-execute` waterfall is the extension point we transform). */
51
+ export const inject = ['tools'];
52
+ export const Config = z.object({
53
+ maxInlineBytes: z.number(),
54
+ });
55
+ /** All-text content flattened to one UTF-8 string, or `undefined` if any block is non-text. */
56
+ function flattenPlainText(content) {
57
+ let text = '';
58
+ for (const block of content) {
59
+ if (block.type !== 'text')
60
+ return undefined;
61
+ text += block.text;
62
+ }
63
+ return text;
64
+ }
65
+ /** The owning session id, or `undefined` for a call with no agent (a direct/test call). */
66
+ function ownerSessionId(exec) {
67
+ return exec.agent?.session.header.id;
68
+ }
69
+ /** Build the bounded head/tail preview for `text`, splitting `budget` bytes across the two ends. */
70
+ function preview(text, budget) {
71
+ const headBytes = Math.ceil(budget / 2);
72
+ const tailBytes = Math.floor(budget / 2);
73
+ const retainer = new TextRetainer({ kind: 'headTail', headBytes, tailBytes });
74
+ retainer.push(text);
75
+ const kept = retainer.finish();
76
+ return { text: kept.text, omitted: kept.omittedBytes };
77
+ }
78
+ export function apply(ctx, config) {
79
+ const maxInlineBytes = config.maxInlineBytes;
80
+ // Omitted ⇒ no automatic spill policy: register nothing at all.
81
+ if (maxInlineBytes === undefined)
82
+ return;
83
+ // Validate at LOAD, not per call: a negative/fractional cap would reach
84
+ // TextRetainer's assertBudget and throw, turning every oversized-result call
85
+ // into an isError. A bad config must fail the deployment, not the tool.
86
+ if (!Number.isInteger(maxInlineBytes) || maxInlineBytes < 0) {
87
+ throw new Error(`spill-policy: maxInlineBytes must be a non-negative integer (got ${maxInlineBytes})`);
88
+ }
89
+ // Narrowed once for the nested arms (closure narrowing does not survive awaits).
90
+ const cap = maxInlineBytes;
91
+ /**
92
+ * Spill `text` and build the bounded replacement (preview + notice), or
93
+ * return `undefined` when the policy must keep the original (no session
94
+ * owner, no backend, storage failure, or no within-cap replacement).
95
+ * Shared verbatim by the model-facing post-execute arm and the durable
96
+ * dispatch-log arm so both produce byte-identical projections.
97
+ */
98
+ async function spillReplacement(text, totalBytes, sessionId, toolName, callId, label) {
99
+ if (sessionId === undefined) {
100
+ ctx.logger.warn(`spill-policy: no session owner for ${toolName} ${label}; keeping the inline content`);
101
+ return undefined;
102
+ }
103
+ const spillStore = ctx.get('spillStore');
104
+ if (!spillStore) {
105
+ ctx.logger.warn('spill-policy: no ctx.spillStore backend loaded; keeping the inline content');
106
+ return undefined;
107
+ }
108
+ const save = {
109
+ owner: { sessionId },
110
+ source: { kind: 'tool', toolName, callId, label },
111
+ suggestedName: `${toolName}.txt`,
112
+ content: text,
113
+ };
114
+ let ref;
115
+ try {
116
+ ref = await spillStore.saveText(save);
117
+ }
118
+ catch (error) {
119
+ // Best-effort: a storage failure (permissions, ENOSPC, backend down) must
120
+ // never fail the call or hide the content — keep the original inline.
121
+ ctx.logger.warn(`spill-policy: saveText failed for ${toolName}: ${String(error)}; keeping the inline content`);
122
+ return undefined;
123
+ }
124
+ // Reserve the notice's byte cost INSIDE maxInlineBytes so the replacement
125
+ // (preview + blank line + notice) never exceeds the documented cap — a naive
126
+ // preview that spent the whole budget then appended the notice could be
127
+ // larger than the cap, and for a marginally-over result even larger than the
128
+ // original. The reservation uses a notice priced at the worst-case omission
129
+ // count (the full byte total): its digit count bounds the real count's, so
130
+ // the reserved size is a safe upper bound and the final notice is never
131
+ // longer than what we reserved. `\n\n` is the 2-byte join.
132
+ const reserve = Buffer.byteLength(formatSpillNotice({ kind: 'exact', count: totalBytes }, ref), 'utf8') + 2;
133
+ const previewBudget = Math.max(0, cap - reserve);
134
+ const { text: previewText, omitted } = preview(text, previewBudget);
135
+ const notice = formatSpillNotice(omitted, ref);
136
+ const replacedText = previewText.length > 0 ? `${previewText}\n\n${notice}` : notice;
137
+ // Invariant: the policy NEVER emits a replacement larger than the cap. When
138
+ // the notice alone exceeds maxInlineBytes (a tiny cap or a long spill root),
139
+ // there is no within-cap replacement, so keep the inline content — spilling
140
+ // would break the advertised cap. (A within-cap replacement is always
141
+ // smaller than the original, which is > cap by the entry condition, so this
142
+ // one check subsumes "not smaller than the original" too. The spill file
143
+ // already written is a harmless orphan; cleanup is deferred.)
144
+ if (Buffer.byteLength(replacedText, 'utf8') > cap) {
145
+ ctx.logger.warn(`spill-policy: spill notice for ${toolName} exceeds maxInlineBytes; keeping the inline content`);
146
+ return undefined;
147
+ }
148
+ return replacedText;
149
+ }
150
+ ctx.on('tools/post-execute', async (exec, result, next) => {
151
+ // Delegate first so a downstream listener (e.g. a hook) settles the result;
152
+ // we bound whatever it accepted. A block passes through — spill only shapes
153
+ // accepted plain-text results, never corrective feedback.
154
+ const decision = await next();
155
+ // Skip `read` to avoid a read → spill → read again loop.
156
+ if (decision.kind !== 'accept' || Object.hasOwn(decision, 'value')
157
+ || exec.parent !== undefined || exec.name === 'read')
158
+ return decision;
159
+ const content = decision.content ?? result.content;
160
+ const text = flattenPlainText(content);
161
+ if (text === undefined)
162
+ return decision;
163
+ const totalBytes = Buffer.byteLength(text, 'utf8');
164
+ if (totalBytes <= maxInlineBytes)
165
+ return decision;
166
+ const replacedText = await spillReplacement(text, totalBytes, ownerSessionId(exec), exec.name, exec.callId, 'result');
167
+ if (replacedText === undefined)
168
+ return decision;
169
+ const replaced = [{ type: 'text', text: replacedText }];
170
+ return { kind: 'accept', content: replaced, ...decision.additionalContexts ? { additionalContexts: decision.additionalContexts } : {} };
171
+ }, { prepend: true });
172
+ // The durable-log arm: bound the `tool/code-dispatch` event's copy of an
173
+ // oversized sub-call result the same way the model-facing arm bounds an
174
+ // outer result. The program's returned value is untouched (it already
175
+ // crossed the worker boundary whole); only the session log's copy shrinks
176
+ // to preview + locator, so replay and UIs read the full text through the
177
+ // spill artifact exactly as they do for spilled native results.
178
+ ctx.on('tools/ptc-dispatch-log', async (dispatch, next) => {
179
+ const content = await next();
180
+ // `read` sub-calls spill too: the log copy is not model context, so the
181
+ // read → spill → read-again loop the post-execute arm avoids cannot
182
+ // happen here, and read is precisely the tool that produces huge logs.
183
+ const text = flattenPlainText(content);
184
+ if (text === undefined)
185
+ return content;
186
+ const totalBytes = Buffer.byteLength(text, 'utf8');
187
+ if (totalBytes <= maxInlineBytes)
188
+ return content;
189
+ const replacedText = await spillReplacement(text, totalBytes, ownerSessionId(dispatch.exec), dispatch.name, dispatch.subCallId, 'dispatch');
190
+ if (replacedText === undefined)
191
+ return content;
192
+ return [{ type: 'text', text: replacedText }];
193
+ }, { prepend: true });
194
+ }
195
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,18 @@
1
+ /** Browser-safe formatting and recognition of persisted spill-policy notices. */
2
+ import { type Omitted } from '@deepseek-ai/dsh-output-retention';
3
+ import type { SpillRef } from '@deepseek-ai/dsh-spill';
4
+ /**
5
+ * Format the notice appended to a retained preview, preserving its persisted spelling.
6
+ * @param omitted - bytes omitted by the retention policy.
7
+ * @param ref - saved text locator and retrieval guidance.
8
+ * @returns the complete notice without a leading preview separator.
9
+ */
10
+ export declare function formatSpillNotice(omitted: Omitted, ref: Pick<SpillRef, 'locator' | 'retrievalHint'>): string;
11
+ /**
12
+ * Recognize a final spill-policy notice in persisted text, including notice-only output.
13
+ * This identifies the text convention, not authenticated provenance of tool output.
14
+ * @param text - complete recorded text result.
15
+ * @returns whether a complete notice occupies the end of the result.
16
+ */
17
+ export declare function hasSpillNotice(text: string): boolean;
18
+ //# sourceMappingURL=notice.d.ts.map
@@ -0,0 +1,51 @@
1
+ /** Browser-safe formatting and recognition of persisted spill-policy notices. */
2
+ import { describeOmitted } from '@deepseek-ai/dsh-output-retention';
3
+ const OPEN = '(';
4
+ const CLOSE = ')';
5
+ const LOCATION = ' Full formatted result stored at: ';
6
+ const GUIDANCE_SEPARATOR = '. ';
7
+ const SEPARATOR = '\n\n';
8
+ const EXACT_OMISSION = describeOmitted({ kind: 'exact', count: 0 }, 'bytes');
9
+ const COUNT_OFFSET = EXACT_OMISSION.indexOf('0');
10
+ const COUNT_SUFFIX = EXACT_OMISSION.slice(COUNT_OFFSET + 1);
11
+ /**
12
+ * Format the notice appended to a retained preview, preserving its persisted spelling.
13
+ * @param omitted - bytes omitted by the retention policy.
14
+ * @param ref - saved text locator and retrieval guidance.
15
+ * @returns the complete notice without a leading preview separator.
16
+ */
17
+ export function formatSpillNotice(omitted, ref) {
18
+ return `${OPEN}${describeOmitted(omitted, 'bytes')}${LOCATION}${ref.locator}${GUIDANCE_SEPARATOR}${ref.retrievalHint}${CLOSE}`;
19
+ }
20
+ function isOmission(text) {
21
+ if (text === describeOmitted({ kind: 'none' }, 'bytes')
22
+ || text === describeOmitted({ kind: 'unknown' }, 'bytes'))
23
+ return true;
24
+ const count = Number(text.slice(COUNT_OFFSET, text.length - COUNT_SUFFIX.length));
25
+ return Number.isSafeInteger(count) && count >= 0
26
+ && text === describeOmitted({ kind: 'exact', count }, 'bytes');
27
+ }
28
+ /**
29
+ * Recognize a final spill-policy notice in persisted text, including notice-only output.
30
+ * This identifies the text convention, not authenticated provenance of tool output.
31
+ * @param text - complete recorded text result.
32
+ * @returns whether a complete notice occupies the end of the result.
33
+ */
34
+ export function hasSpillNotice(text) {
35
+ if (!text.endsWith(CLOSE))
36
+ return false;
37
+ let start = 0;
38
+ while (true) {
39
+ const next = text.indexOf(`${SEPARATOR}${OPEN}`, start);
40
+ const candidate = text.slice(start, next < 0 ? -CLOSE.length : next);
41
+ const location = candidate.indexOf(LOCATION, OPEN.length);
42
+ if (candidate.startsWith(OPEN) && location >= 0
43
+ && isOmission(candidate.slice(OPEN.length, location))) {
44
+ return text.indexOf(GUIDANCE_SEPARATOR, start + location + LOCATION.length) >= 0;
45
+ }
46
+ if (next < 0)
47
+ return false;
48
+ start = next + SEPARATOR.length;
49
+ }
50
+ }
51
+ //# sourceMappingURL=notice.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Vocabulary for the spill-policy plugin: the minimal structural view of a tool
3
+ * execution the policy needs to derive the owning session for a spill artifact.
4
+ *
5
+ * `@deepseek-ai/dsh-tools`' `ToolExecution` satisfies this shape, so the policy
6
+ * reads `exec` straight through without importing `dsh-tools` or `dsh-agent`.
7
+ * Only the session HEADER id is read — the same identity every other subsystem
8
+ * keys off (see `dsh-tool-bash`'s owner derivation).
9
+ *
10
+ * @module @deepseek-ai/dsh-spill-policy/types
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-spill-policy",
3
3
  "description": "Tool-result spill policy for the DeepSeek Harness — replaces oversized plain-text tool results with a retained preview plus a spill-file path (no service API)",
4
- "version": "0.1.2-alpha.5",
4
+ "version": "0.1.3-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -18,33 +18,38 @@
18
18
  "types": "./lib/types/index.d.ts",
19
19
  "default": "./lib/index.js"
20
20
  },
21
+ "./notice": {
22
+ "types": "./lib/types/notice.d.ts",
23
+ "default": "./lib/types/notice.js"
24
+ },
21
25
  "./src/*": "./src/*",
22
26
  "./package.json": "./package.json"
23
27
  },
24
28
  "files": [
25
29
  "lib/index.js",
30
+ "lib/types/**/*.js",
26
31
  "lib/types/**/*.d.ts"
27
32
  ],
28
33
  "license": "MIT",
29
34
  "peerDependencies": {
30
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.5",
31
- "@deepseek-ai/dsh-output-retention": "^0.1.2-alpha.5",
32
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
33
- "@deepseek-ai/dsh-spill": "^0.1.2-alpha.5",
34
- "@deepseek-ai/dsh-tools": "^0.1.2-alpha.5",
35
+ "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
36
+ "@deepseek-ai/dsh-output-retention": "^0.1.3-alpha.2",
37
+ "@deepseek-ai/dsh-spill": "^0.1.3-alpha.2",
38
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
39
+ "@deepseek-ai/dsh-tools": "^0.1.3-alpha.2",
35
40
  "@deepseek-ai/cordis": "^4.0.2"
36
41
  },
37
42
  "dependencies": {
38
43
  "@deepseek-ai/schemastery": "^3.18.2"
39
44
  },
40
45
  "devDependencies": {
41
- "@deepseek-ai/dsh-agent": "^0.1.2-alpha.5",
42
- "@deepseek-ai/dsh-code-runtime-worker-thread": "^0.1.2-alpha.5",
43
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.5",
44
- "@deepseek-ai/dsh-output-retention": "^0.1.2-alpha.5",
45
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
46
- "@deepseek-ai/dsh-spill": "^0.1.2-alpha.5",
47
- "@deepseek-ai/dsh-tools": "^0.1.2-alpha.5",
46
+ "@deepseek-ai/dsh-agent": "^0.1.3-alpha.2",
47
+ "@deepseek-ai/dsh-code-runtime-worker-thread": "^0.1.3-alpha.2",
48
+ "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
49
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
50
+ "@deepseek-ai/dsh-spill": "^0.1.3-alpha.2",
51
+ "@deepseek-ai/dsh-output-retention": "^0.1.3-alpha.2",
52
+ "@deepseek-ai/dsh-tools": "^0.1.3-alpha.2",
48
53
  "@deepseek-ai/cordis": "^4.0.2"
49
54
  }
50
55
  }