@dsh-cc/advisor-watchdog 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.i18n.yaml +6 -0
- package/README.md +36 -0
- package/README.zh.md +38 -0
- package/lib/advise.d.ts +46 -0
- package/lib/advise.d.ts.map +1 -0
- package/lib/advise.js +71 -0
- package/lib/advise.js.map +1 -0
- package/lib/delta.d.ts +80 -0
- package/lib/delta.d.ts.map +1 -0
- package/lib/delta.js +140 -0
- package/lib/delta.js.map +1 -0
- package/lib/guard.d.ts +66 -0
- package/lib/guard.d.ts.map +1 -0
- package/lib/guard.js +113 -0
- package/lib/guard.js.map +1 -0
- package/lib/index.d.ts +40 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +45 -0
- package/lib/index.js.map +1 -0
- package/lib/journal.d.ts +41 -0
- package/lib/journal.d.ts.map +1 -0
- package/lib/journal.js +32 -0
- package/lib/journal.js.map +1 -0
- package/lib/quarantine.d.ts +16 -0
- package/lib/quarantine.d.ts.map +1 -0
- package/lib/quarantine.js +23 -0
- package/lib/quarantine.js.map +1 -0
- package/lib/settings.d.ts +62 -0
- package/lib/settings.d.ts.map +1 -0
- package/lib/settings.js +164 -0
- package/lib/settings.js.map +1 -0
- package/lib/wiring.d.ts +35 -0
- package/lib/wiring.d.ts.map +1 -0
- package/lib/wiring.js +283 -0
- package/lib/wiring.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm check:readme --write
|
|
5
|
+
README.md: dab52f2bf8ac3d0df036fd9a4b6a416790fdd121
|
|
6
|
+
README.zh.md: 59d1ff422b97e5916a41453f3119bfb3a25e11dc
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @dsh-cc/advisor-watchdog
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Advisor watchdog: an opt-in **second model** that reviews every completed turn. A passive read-only `llm/stream` listener keeps, per session, the newest qualifying conversation request's message array; at each `agent/turn-stopping`, the plugin extracts the window since its per-session cursor (≈ the last completed turn), renders it as `[role] content` lines (tool-call blocks as `[assistant tool_use <name>] <args>`), and fire-and-forget asks a cheap lane (default alias `haiku`) via `@dsh-cc/side-query`'s one-shot `runSideQuery` — with `onUnrouted: 'skip'` (a hard no-inherit rule: an unconfigured alias can never silently run on the main route) and `rejectToolCalls: true` (no tool loop can exist). The reply is parsed against a strict JSON contract (`nit | concern | blocker`, ≤ 16 notes, ≤ 500 chars, empty allowed) and surviving notes are delivered by one resolve-time `agent.inject()` with source kind `advisor` — never mid-tool-batch, never waking an idle session. **Default off** (`cc-advisor.enabled: false`).
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
Plain plugin (no Service, no isolate key) registering two listeners:
|
|
10
|
+
|
|
11
|
+
- **`llm/stream`** (read-only, observe-and-passthrough, `{ global: true, prepend: true }` — the cache-health seam): keeps the newest qualifying request's message array per session. A request qualifies iff the loop stamped `sessionId` and no `purpose` is set — so the advisor's own hand-built one-shot calls never enter the snapshots (self-observation excluded by construction), and compaction/session-title auxiliary lanes are skipped. Zero settings/IO work: one array reference, `next()` immediately.
|
|
12
|
+
- **`agent/turn-stopping`** (the trigger; synchronous up to capture, never throwing): gates — settings `enabled` (raw dual-half read), the `subagents` gate unless top-level, session-disabled, inFlight (a stop during flight captures nothing and leaves the cursor alone, so the window accumulates into the next stop) — then the cursor protocol over the snapshot: first observation (init) and rewritten history (compaction/rewind reset) are skipped without a run; the candidate window drops every injected source kind (the advisor's own output is invisible to itself, so a re-opened advisory tail turn never spawns anything); an empty-after-filter window or a window without a genuine user message advances the cursor and skips. On review, the cursor advances immediately (eligibility, not completion), the run is spawned detached, and the turn counter increments after capture; at resolve time notes are dropped as stale unless `turnCounter - capturedTurn <= 1`.
|
|
13
|
+
|
|
14
|
+
Emission guard (ported from oh-my-pi, plan Appendix A), in fixed order: severity filter → normalized exact-set denylist (37 verbatim omp phrases; "Stop." matches, a genuine blocker mentioning "Stop:" does not) → quarantine scan against `@dsh-cc/permission-rules`' `DEFAULT_DANGEROUS_PATTERNS` → flat dedupe LRU (4096 fingerprints) → immune window (fresh `concern` notes suppressed for `immune-turns` after a delivered concern/blocker) → per-run budget (2 non-blockers, blockers exempt). A session cap (24 delivered notes) silences the advisor for the session.
|
|
15
|
+
|
|
16
|
+
Journals one JSON line per attempted run to `$DSH_HOME/advisor/<sessionId>.jsonl` (fields: ts, turn, alias, model, inheritedRoute, ok, reason, durationMs, deltaMessages, deltaBytes, notesIn, notesOut, drops, `usage: null` — token metering is N/A until `SideQueryResult` surfaces usage, plan §7). Dogfood plan + jq scoreboard: [`docs/dogfood/advisor-watchdog.md`](../../../docs/dogfood/advisor-watchdog.md).
|
|
17
|
+
|
|
18
|
+
Cross-package obligation: the `advisor` injected kind is added to the denylists in `@dsh-cc/turn-rules` (matcher) and `@dsh-cc/memory` (recall) — the advisor can never feed on its own or other plugins' injected text, and nothing downstream feeds on advisories.
|
|
19
|
+
|
|
20
|
+
## Settings (user layer only)
|
|
21
|
+
|
|
22
|
+
Key `cc-advisor` in the **user-layer** `settings.json` (the harness-home file). Project scope is **never read** — invisible, not refused.
|
|
23
|
+
|
|
24
|
+
| Key | Default | Meaning |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `enabled` | `false` | Master flag. Ship-dark. |
|
|
27
|
+
| `alias` | `"haiku"` | Cheap-lane alias resolved through `ccModelRoutes`. |
|
|
28
|
+
| `budget` | `2` | Max non-blocker notes per run (1–8); blockers exempt. |
|
|
29
|
+
| `immune-turns` | `3` | Turns after a delivered concern/blocker during which fresh concerns are suppressed. |
|
|
30
|
+
| `session-cap` | `24` | Delivered notes per session; reaching it disables the advisor for the session. |
|
|
31
|
+
| `severities` | all three | Which severities survive the first filter. |
|
|
32
|
+
| `subagents` | `"off"` | Global subagent gate (settings-level only, §4.7): `off` reviews top-level sessions only; `on` reviews subagent sessions with the session alias; an alias string reviews them with that alias. |
|
|
33
|
+
|
|
34
|
+
## Shape
|
|
35
|
+
|
|
36
|
+
Plain cordis plugin (no Service, no isolate key). Mounted by `packages/preset/cc` in the cc-services group at the group tail, after turn-rules — turn-rules' prompt matcher must see the un-advised prompt (advisor text is denylisted from the matcher's candidate either way). All failure modes fail soft: a listener can never block a step, throw into a waterfall, or wake an idle driver.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @dsh-cc/advisor-watchdog
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
(中文说明,与英文版实质等价。)
|
|
6
|
+
|
|
7
|
+
Advisor watchdog:可选开启的**第二模型**,审查每一个已完成的轮次。一个只读被动的 `llm/stream` 监听器按会话保存最新的符合条件的对话请求消息数组;在每个 `agent/turn-stopping`,插件提取自其每会话游标以来的窗口(≈ 上一个已完成轮次),渲染为 `[role] content` 行(tool-call 块渲染为 `[assistant tool_use <name>] <args>`),并通过 `@dsh-cc/side-query` 的一次性 `runSideQuery` 以发后即忘的方式询问廉价通道(默认别名 `haiku`)——携带 `onUnrouted: 'skip'`(硬性禁继承规则:未配置的别名绝不可能静默跑在主路由上)和 `rejectToolCalls: true`(不可能存在工具循环)。回复按严格 JSON 契约解析(`nit | concern | blocker`,≤ 16 条,≤ 500 字符,允许为空),幸存的注记在解析时经一次 `agent.inject()` 投递、来源标记 `advisor`——绝不插入工具批次中段,绝不唤醒空闲会话。**默认关闭**(`cc-advisor.enabled: false`)。
|
|
8
|
+
|
|
9
|
+
## 工作方式
|
|
10
|
+
|
|
11
|
+
普通插件(无 Service、无 isolate key),注册两个监听器:
|
|
12
|
+
|
|
13
|
+
- **`llm/stream`**(只读、观察直通、`{ global: true, prepend: true }`——cache-health 同一接缝):按会话保存最新的符合条件的请求消息数组。请求符合条件当且仅当循环已盖 `sessionId` 戳且未设 `purpose`——因此 advisor 自己手工构建的一次性调用从不进入快照(自我观察在构造上被排除),压缩/会话标题等辅助通道也被跳过。零设置/IO 开销:仅持一个数组引用,立即 `next()`。
|
|
14
|
+
- **`agent/turn-stopping`**(触发器;捕获前全程同步、从不抛出):闸门——设置 `enabled`(原始双半读取)、非顶层会话受 `subagents` 闸门约束、会话已禁用、inFlight(运行期间的轮次停止既不捕获也不推进游标,窗口累积到下一次停止)——随后对快照执行游标协议:首次观察(init)与被重写的历史(压缩/回退 reset)直接跳过不计费;候选窗口剔除所有注入来源类型(advisor 自己的输出对自身不可见,因此被重开的 advisory 尾轮不会触发任何新调用);过滤后为空或无真实用户消息的窗口推进游标并跳过。命中审查时游标立即推进(标记资格而非完成),分离地派发运行,并在捕获之后递增轮次计数器;解析时仅当 `turnCounter - capturedTurn <= 1` 才投递,否则按过期丢弃。
|
|
15
|
+
|
|
16
|
+
发射守卫(自 oh-my-pi 移植,计划附录 A),按固定顺序:严重度过滤 → 归一化精确集合拒绝列表(37 条 omp 原文短语;"Stop." 匹配,而真正提及 "Stop:" 的 blocker 不匹配)→ 以 `@dsh-cc/permission-rules` 的 `DEFAULT_DANGEROUS_PATTERNS` 做隔离扫描 → 扁平去重 LRU(4096 个指纹)→ 免疫窗口(投递过 concern/blocker 之后的 `immune-turns` 个轮次内,新的 `concern` 被抑制)→ 每次运行预算(2 条非 blocker,blocker 豁免)。会话总量上限(24 条已投递注记)到达后对该会话静默。
|
|
17
|
+
|
|
18
|
+
每次尝试的运行向 `$DSH_HOME/advisor/<sessionId>.jsonl` 追加一行 JSON(字段:ts、turn、alias、model、inheritedRoute、ok、reason、durationMs、deltaMessages、deltaBytes、notesIn、notesOut、drops、`usage: null`——在 `SideQueryResult` 暴露用量之前不计量成本,计划 §7)。Dogfood 计划与 jq 记分板:[`docs/dogfood/advisor-watchdog.md`](../../../docs/dogfood/advisor-watchdog.md)。
|
|
19
|
+
|
|
20
|
+
跨包义务:注入类型 `advisor` 已加入 `@dsh-cc/turn-rules`(matcher)与 `@dsh-cc/memory`(recall)的拒绝列表——advisor 永不以自己或其他插件的注入文本为食,下游也永不以 advisory 构建查询。
|
|
21
|
+
|
|
22
|
+
## 配置(仅用户层)
|
|
23
|
+
|
|
24
|
+
用户层 `settings.json`(harness-home 文件)中的 `cc-advisor` 键。**永不读取**项目作用域——结构上不可见,并非"被拒绝"。
|
|
25
|
+
|
|
26
|
+
| 键 | 默认值 | 含义 |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `enabled` | `false` | 总开关。默认黑暗。 |
|
|
29
|
+
| `alias` | `"haiku"` | 经 `ccModelRoutes` 解析的廉价通道别名。 |
|
|
30
|
+
| `budget` | `2` | 每次运行的非 blocker 注记上限(1–8);blocker 豁免。 |
|
|
31
|
+
| `immune-turns` | `3` | 投递过 concern/blocker 后的新 concern 抑制轮数。 |
|
|
32
|
+
| `session-cap` | `24` | 每会话已投递注记上限;到达即对该会话禁用。 |
|
|
33
|
+
| `severities` | 三者全开 | 通过第一道过滤的严重级别。 |
|
|
34
|
+
| `subagents` | `"off"` | 子代理全局闸门(仅设置层,§4.7):`off` 仅审查顶层会话;`on` 以会话别名审查子代理会话;别名字符串则以该别名审查。 |
|
|
35
|
+
|
|
36
|
+
## 形态
|
|
37
|
+
|
|
38
|
+
普通 cordis 插件(无 Service、无 isolate key)。由 `packages/preset/cc` 挂载在 cc-services 组尾、turn-rules 之后——turn-rules 的提示匹配器必须看到未被建议的提示(无论怎样,advisor 文本都被排除在匹配候选之外)。所有故障均向软侧失效:监听器绝不能阻塞步骤、向瀑布抛错或唤醒空闲驱动。
|
package/lib/advise.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cheap-lane envelope and output contract (plan
|
|
3
|
+
* docs/plans/2026-09-23-advisor-watchdog.md §4.2/§4.3): one-shot
|
|
4
|
+
* `runSideQuery` with `onUnrouted: 'skip'` (THE no-inherit switch) and
|
|
5
|
+
* `rejectToolCalls: true` (one-shot — no tool loop can exist), plus the
|
|
6
|
+
* deterministic zod parse of the note list.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
12
|
+
import { runSideQuery } from '@dsh-cc/side-query';
|
|
13
|
+
import type { Severity } from './settings.ts';
|
|
14
|
+
import type { AdvisorNote } from './guard.ts';
|
|
15
|
+
/** The advisor system prompt (§4.3): role, input format, taxonomy, empty contract. */
|
|
16
|
+
export declare const ADVISOR_SYSTEM_PROMPT: string;
|
|
17
|
+
/** Parse failure result (§4.3 step 3): deliver nothing. */
|
|
18
|
+
export type ParseOutcome = {
|
|
19
|
+
ok: true;
|
|
20
|
+
notes: AdvisorNote[];
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Deterministic parse (§4.3): strip at most one surrounding fence, then
|
|
26
|
+
* validate against the zod schema. Any failure ⇒ malformed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseAdvisorNotes(raw: string): ParseOutcome;
|
|
29
|
+
/** Options for the one-shot advisor call (§4.2 exact shape). */
|
|
30
|
+
export interface AdviseOptions {
|
|
31
|
+
agent: Agent;
|
|
32
|
+
alias: string;
|
|
33
|
+
renderedDelta: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run the one-shot advisor side query. Never throws (runSideQuery collapses
|
|
37
|
+
* every failure). No caller signal: disposal is preset remount.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runAdvisor(ctx: Context, opts: {
|
|
40
|
+
agent: Agent;
|
|
41
|
+
alias: string;
|
|
42
|
+
renderedDelta: string;
|
|
43
|
+
}): ReturnType<typeof runSideQuery>;
|
|
44
|
+
/** Re-export for the severity type consumer. */
|
|
45
|
+
export type { Severity };
|
|
46
|
+
//# sourceMappingURL=advise.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advise.d.ts","sourceRoot":"","sources":["../src/advise.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,sFAAsF;AACtF,eAAO,MAAM,qBAAqB,QAStB,CAAA;AAUZ,2DAA2D;AAC3D,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,WAAW,EAAE,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAA;AAEjB;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAiB3D;AAED,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,KAAK,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,CAAA;CACtB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAC3D,UAAU,CAAC,OAAO,YAAY,CAAC,CAWjC;AAED,gDAAgD;AAChD,YAAY,EAAE,QAAQ,EAAE,CAAA"}
|
package/lib/advise.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cheap-lane envelope and output contract (plan
|
|
3
|
+
* docs/plans/2026-09-23-advisor-watchdog.md §4.2/§4.3): one-shot
|
|
4
|
+
* `runSideQuery` with `onUnrouted: 'skip'` (THE no-inherit switch) and
|
|
5
|
+
* `rejectToolCalls: true` (one-shot — no tool loop can exist), plus the
|
|
6
|
+
* deterministic zod parse of the note list.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { runSideQuery } from '@dsh-cc/side-query';
|
|
12
|
+
/** The advisor system prompt (§4.3): role, input format, taxonomy, empty contract. */
|
|
13
|
+
export const ADVISOR_SYSTEM_PROMPT = [
|
|
14
|
+
'You are a read-only reviewer of one completed turn of a coding session.',
|
|
15
|
+
'Input: transcript delta lines of the form `[role] content`, oldest first.',
|
|
16
|
+
'Emit notes at exactly one of three severities:',
|
|
17
|
+
'- "nit": minor or stylistic.',
|
|
18
|
+
'- "concern": a likely problem worth one mention.',
|
|
19
|
+
'- "blocker": user-visible harm in progress, e.g. ignoring an explicit instruction or a destructive command about to run.',
|
|
20
|
+
'Output ONLY raw JSON: {"notes": [{"severity": "nit|concern|blocker", "text": "<= 500 chars"}]}, at most 16 notes.',
|
|
21
|
+
'When nothing is worth saying, answer {"notes": []} — empty is the common case.',
|
|
22
|
+
].join('\n');
|
|
23
|
+
/** The exact output contract (§4.3 step 2). */
|
|
24
|
+
const NotesSchema = z.object({
|
|
25
|
+
notes: z.array(z.object({
|
|
26
|
+
severity: z.enum(['nit', 'concern', 'blocker']),
|
|
27
|
+
text: z.string().min(1).max(500),
|
|
28
|
+
})).max(16),
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Deterministic parse (§4.3): strip at most one surrounding fence, then
|
|
32
|
+
* validate against the zod schema. Any failure ⇒ malformed.
|
|
33
|
+
*/
|
|
34
|
+
export function parseAdvisorNotes(raw) {
|
|
35
|
+
let text = raw.trim();
|
|
36
|
+
if (text.startsWith('```')) {
|
|
37
|
+
// Strip at most one surrounding fence (with optional language tag). The
|
|
38
|
+
// closing fence is optional — an assembler may deliver only the opener.
|
|
39
|
+
const firstNewline = text.indexOf('\n');
|
|
40
|
+
if (firstNewline === -1)
|
|
41
|
+
return { ok: false };
|
|
42
|
+
text = text.slice(firstNewline + 1);
|
|
43
|
+
const closing = text.lastIndexOf('```');
|
|
44
|
+
if (closing !== -1)
|
|
45
|
+
text = text.slice(0, closing).trim();
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const parsed = NotesSchema.parse(JSON.parse(text));
|
|
49
|
+
return { ok: true, notes: parsed.notes };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { ok: false };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run the one-shot advisor side query. Never throws (runSideQuery collapses
|
|
57
|
+
* every failure). No caller signal: disposal is preset remount.
|
|
58
|
+
*/
|
|
59
|
+
export function runAdvisor(ctx, opts) {
|
|
60
|
+
return runSideQuery(ctx, {
|
|
61
|
+
agent: opts.agent,
|
|
62
|
+
alias: opts.alias,
|
|
63
|
+
system: ADVISOR_SYSTEM_PROMPT,
|
|
64
|
+
prompt: opts.renderedDelta,
|
|
65
|
+
maxTokens: 512,
|
|
66
|
+
timeoutMs: 10_000,
|
|
67
|
+
onUnrouted: 'skip',
|
|
68
|
+
rejectToolCalls: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=advise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advise.js","sourceRoot":"","sources":["../src/advise.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAIjD,sFAAsF;AACtF,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,yEAAyE;IACzE,2EAA2E;IAC3E,gDAAgD;IAChD,8BAA8B;IAC9B,kDAAkD;IAClD,0HAA0H;IAC1H,mHAAmH;IACnH,gFAAgF;CACjF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEZ,+CAA+C;AAC/C,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QACtB,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;QAC/C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;KACjC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;CACZ,CAAC,CAAA;AAOF;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,IAAI,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IACrB,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,wEAAwE;QACxE,wEAAwE;QACxE,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACvC,IAAI,YAAY,KAAK,CAAC,CAAC;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAA;QAC7C,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAA;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;QACvC,IAAI,OAAO,KAAK,CAAC,CAAC;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAA;IAC1D,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;QAClD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAA;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAA;IACtB,CAAC;AACH,CAAC;AASD;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,GAAY,EACZ,IAA4D;IAE5D,OAAO,YAAY,CAAC,GAAG,EAAE;QACvB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,MAAM,EAAE,qBAAqB;QAC7B,MAAM,EAAE,IAAI,CAAC,aAAa;QAC1B,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,MAAM;QACjB,UAAU,EAAE,MAAM;QAClB,eAAe,EAAE,IAAI;KACtB,CAAC,CAAA;AACJ,CAAC"}
|
package/lib/delta.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure delta extraction (plan docs/plans/2026-09-23-advisor-watchdog.md
|
|
3
|
+
* §4.1): snapshot-window decision over the passive `llm/stream` snapshot,
|
|
4
|
+
* injected-kind filtering, cursor state, and the newest-tailed ≤ 32 KiB
|
|
5
|
+
* render. No I/O, no state.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/** The advisory-notes render cap (§4.1). */
|
|
10
|
+
export declare const MAX_DELTA_BYTES = 32768;
|
|
11
|
+
/** Tool-call JSON args render cap (§4.1 render rule). */
|
|
12
|
+
export declare const MAX_TOOL_ARGS_BYTES = 2000;
|
|
13
|
+
/**
|
|
14
|
+
* Injected source kinds whose messages NEVER enter a review delta (§4.1
|
|
15
|
+
* step 3). Mirrors the recall.ts / turn-rules matcher denylists rather than
|
|
16
|
+
* importing them (no shared export exists across those packages).
|
|
17
|
+
* KNOW YOUR INJECTOR: any new injected message source kind must be added to
|
|
18
|
+
* ALL THREE copies (recall.ts, turn-rules/matcher.ts, here) or a
|
|
19
|
+
* self-feeding phantom loop re-opens.
|
|
20
|
+
*/
|
|
21
|
+
export declare const INJECTED_SOURCE_DENYLIST: readonly string[];
|
|
22
|
+
/** This plugin's own injected source kind (MessageSourceMap augmentation in wiring.ts). */
|
|
23
|
+
export declare const ADVISOR_SOURCE_KIND = "advisor";
|
|
24
|
+
/** Cursor over the reviewed prefix of the snapshot messages. */
|
|
25
|
+
export interface Cursor {
|
|
26
|
+
/** Number of snapshot messages already reviewed (or deliberately skipped). */
|
|
27
|
+
count: number;
|
|
28
|
+
/** `JSON.stringify` of the message at `count - 1` (compaction anchor). */
|
|
29
|
+
tail: string;
|
|
30
|
+
}
|
|
31
|
+
/** Structural content-block subset of a `llm/stream` request message. */
|
|
32
|
+
export interface DeltaBlock {
|
|
33
|
+
type: string;
|
|
34
|
+
text?: string;
|
|
35
|
+
/** `tool-call` blocks: the invoked tool name. */
|
|
36
|
+
name?: string;
|
|
37
|
+
/** `tool-call` blocks: raw JSON arguments string. */
|
|
38
|
+
arguments?: string;
|
|
39
|
+
/** `tool-result` blocks: nested content. */
|
|
40
|
+
content?: readonly DeltaBlock[];
|
|
41
|
+
}
|
|
42
|
+
/** Structural message subset read off the `llm/stream` request snapshot. */
|
|
43
|
+
export interface DeltaMessage {
|
|
44
|
+
role?: string;
|
|
45
|
+
content: readonly DeltaBlock[];
|
|
46
|
+
source?: {
|
|
47
|
+
kind?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The per-trigger snapshot review decision (§4.1 steps 3-4). */
|
|
51
|
+
export type ReviewWindow = {
|
|
52
|
+
action: 'init';
|
|
53
|
+
} | {
|
|
54
|
+
action: 'reset';
|
|
55
|
+
} | {
|
|
56
|
+
action: 'skip';
|
|
57
|
+
reason: 'empty-window' | 'no-genuine-user';
|
|
58
|
+
} | {
|
|
59
|
+
action: 'review';
|
|
60
|
+
window: readonly DeltaMessage[];
|
|
61
|
+
};
|
|
62
|
+
/** True when the message is genuine user input (source-less OR kind 'user') with ≥1 non-empty text block. */
|
|
63
|
+
export declare function isGenuineUser(message: DeltaMessage): boolean;
|
|
64
|
+
/** The cursor for a full snapshot (first observation / reset / advance). */
|
|
65
|
+
export declare function fullCursor(messages: readonly DeltaMessage[]): Cursor;
|
|
66
|
+
/**
|
|
67
|
+
* The snapshot-window decision (§4.1 steps 3-4), in order:
|
|
68
|
+
* init → reset-and-skip → filter → empty-window skip → no-genuine-user skip
|
|
69
|
+
* → review. The caller advances the cursor to `fullCursor(snapshot)` on
|
|
70
|
+
* `init`, `reset`, and `skip` — and immediately (before the async call) on
|
|
71
|
+
* `review` — so a failed cheap-lane call loses its window rather than
|
|
72
|
+
* double-billing the next one.
|
|
73
|
+
*/
|
|
74
|
+
export declare function reviewWindow(snapshot: readonly DeltaMessage[], cursor: Cursor | undefined): ReviewWindow;
|
|
75
|
+
/**
|
|
76
|
+
* Render the delta newest-tailed (§4.1): over the byte cap, drop from the
|
|
77
|
+
* OLDEST end and prepend `[truncated N older bytes]`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function renderDelta(window: readonly DeltaMessage[]): string;
|
|
80
|
+
//# sourceMappingURL=delta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delta.d.ts","sourceRoot":"","sources":["../src/delta.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,4CAA4C;AAC5C,eAAO,MAAM,eAAe,QAAS,CAAA;AAErC,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,OAAQ,CAAA;AAExC;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAOrD,CAAA;AAED,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAE5C,gEAAgE;AAChE,MAAM,WAAW,MAAM;IACrB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAA;CACb;AAED,yEAAyE;AACzE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;CAChC;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;IAC9B,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3B;AAED,iEAAiE;AACjE,MAAM,MAAM,YAAY,GACpB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACnB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,GAAG,iBAAiB,CAAA;CAAE,GAC9D;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;CAAE,CAAA;AAEzD,6GAA6G;AAC7G,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAG5D;AAOD,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAKpE;AAOD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,YAAY,CAcd;AA2CD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAcnE"}
|
package/lib/delta.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure delta extraction (plan docs/plans/2026-09-23-advisor-watchdog.md
|
|
3
|
+
* §4.1): snapshot-window decision over the passive `llm/stream` snapshot,
|
|
4
|
+
* injected-kind filtering, cursor state, and the newest-tailed ≤ 32 KiB
|
|
5
|
+
* render. No I/O, no state.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/** The advisory-notes render cap (§4.1). */
|
|
10
|
+
export const MAX_DELTA_BYTES = 32_768;
|
|
11
|
+
/** Tool-call JSON args render cap (§4.1 render rule). */
|
|
12
|
+
export const MAX_TOOL_ARGS_BYTES = 2_000;
|
|
13
|
+
/**
|
|
14
|
+
* Injected source kinds whose messages NEVER enter a review delta (§4.1
|
|
15
|
+
* step 3). Mirrors the recall.ts / turn-rules matcher denylists rather than
|
|
16
|
+
* importing them (no shared export exists across those packages).
|
|
17
|
+
* KNOW YOUR INJECTOR: any new injected message source kind must be added to
|
|
18
|
+
* ALL THREE copies (recall.ts, turn-rules/matcher.ts, here) or a
|
|
19
|
+
* self-feeding phantom loop re-opens.
|
|
20
|
+
*/
|
|
21
|
+
export const INJECTED_SOURCE_DENYLIST = [
|
|
22
|
+
'memory',
|
|
23
|
+
'cc-subagent-children',
|
|
24
|
+
'cc-workflow-completion',
|
|
25
|
+
'turn-rules',
|
|
26
|
+
'plugin',
|
|
27
|
+
'advisor',
|
|
28
|
+
];
|
|
29
|
+
/** This plugin's own injected source kind (MessageSourceMap augmentation in wiring.ts). */
|
|
30
|
+
export const ADVISOR_SOURCE_KIND = 'advisor';
|
|
31
|
+
/** True when the message is genuine user input (source-less OR kind 'user') with ≥1 non-empty text block. */
|
|
32
|
+
export function isGenuineUser(message) {
|
|
33
|
+
if (message.source !== undefined && message.source.kind !== 'user')
|
|
34
|
+
return false;
|
|
35
|
+
return message.content.some(block => block.type === 'text' && (block.text ?? '').length > 0);
|
|
36
|
+
}
|
|
37
|
+
/** Injected kinds are invisible to the advisor (§4.1 step 3). */
|
|
38
|
+
function isInjected(message) {
|
|
39
|
+
return message.source !== undefined && INJECTED_SOURCE_DENYLIST.includes(message.source.kind ?? '');
|
|
40
|
+
}
|
|
41
|
+
/** The cursor for a full snapshot (first observation / reset / advance). */
|
|
42
|
+
export function fullCursor(messages) {
|
|
43
|
+
return {
|
|
44
|
+
count: messages.length,
|
|
45
|
+
tail: messages.length > 0 ? JSON.stringify(messages[messages.length - 1]) : '',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The tail anchor for the message at `count - 1`, or '' when count is 0. */
|
|
49
|
+
function tailAt(messages, count) {
|
|
50
|
+
return count > 0 ? JSON.stringify(messages[count - 1] ?? '') : '';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The snapshot-window decision (§4.1 steps 3-4), in order:
|
|
54
|
+
* init → reset-and-skip → filter → empty-window skip → no-genuine-user skip
|
|
55
|
+
* → review. The caller advances the cursor to `fullCursor(snapshot)` on
|
|
56
|
+
* `init`, `reset`, and `skip` — and immediately (before the async call) on
|
|
57
|
+
* `review` — so a failed cheap-lane call loses its window rather than
|
|
58
|
+
* double-billing the next one.
|
|
59
|
+
*/
|
|
60
|
+
export function reviewWindow(snapshot, cursor) {
|
|
61
|
+
// First observation: cold/resumed history is never review-billed.
|
|
62
|
+
if (cursor === undefined)
|
|
63
|
+
return { action: 'init' };
|
|
64
|
+
// History rewritten (compaction/rewind) → reset+skip.
|
|
65
|
+
if (snapshot.length < cursor.count || tailAt(snapshot, cursor.count) !== cursor.tail) {
|
|
66
|
+
return { action: 'reset' };
|
|
67
|
+
}
|
|
68
|
+
// Candidate window, filtered to drop injected kinds.
|
|
69
|
+
const window = snapshot.slice(cursor.count).filter(message => !isInjected(message));
|
|
70
|
+
// Empty window after filtering ⇒ advance cursor, skip.
|
|
71
|
+
if (window.length === 0)
|
|
72
|
+
return { action: 'skip', reason: 'empty-window' };
|
|
73
|
+
// No genuine user message ⇒ advance cursor, skip (spend guard + wake-loop break).
|
|
74
|
+
if (!window.some(isGenuineUser))
|
|
75
|
+
return { action: 'skip', reason: 'no-genuine-user' };
|
|
76
|
+
return { action: 'review', window };
|
|
77
|
+
}
|
|
78
|
+
/** Args blob truncated at 2000 bytes (§4.1 render rule). */
|
|
79
|
+
function truncateArgs(args) {
|
|
80
|
+
return Buffer.byteLength(args, 'utf8') > MAX_TOOL_ARGS_BYTES
|
|
81
|
+
? `${Buffer.from(args, 'utf8').subarray(0, MAX_TOOL_ARGS_BYTES).toString('utf8')}…`
|
|
82
|
+
: args;
|
|
83
|
+
}
|
|
84
|
+
/** Render one content block (§4.1 render rule). */
|
|
85
|
+
function renderBlock(block) {
|
|
86
|
+
if (block.type === 'tool-call') {
|
|
87
|
+
return `[assistant tool_use ${block.name ?? 'unknown'}] ${truncateArgs(block.arguments ?? '')}`;
|
|
88
|
+
}
|
|
89
|
+
if (block.type === 'tool-result') {
|
|
90
|
+
return (block.content ?? [])
|
|
91
|
+
.filter(nested => nested.type === 'text')
|
|
92
|
+
.map(nested => nested.text ?? '')
|
|
93
|
+
.join('\n');
|
|
94
|
+
}
|
|
95
|
+
return block.text ?? '';
|
|
96
|
+
}
|
|
97
|
+
/** `[role] content` lines for one message: text blocks joined, tool blocks rendered. */
|
|
98
|
+
function renderMessage(message) {
|
|
99
|
+
const role = message.role ?? 'user';
|
|
100
|
+
const textParts = [];
|
|
101
|
+
const toolLines = [];
|
|
102
|
+
for (const block of message.content) {
|
|
103
|
+
if (block.type === 'tool-call' || block.type === 'tool-result') {
|
|
104
|
+
const line = renderBlock(block);
|
|
105
|
+
if (line.length > 0)
|
|
106
|
+
toolLines.push(line);
|
|
107
|
+
}
|
|
108
|
+
else if (block.type === 'text') {
|
|
109
|
+
textParts.push(block.text ?? '');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const lines = [];
|
|
113
|
+
const text = textParts.join('\n');
|
|
114
|
+
if (text.length > 0)
|
|
115
|
+
lines.push(`[${role}] ${text}`);
|
|
116
|
+
lines.push(...toolLines);
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Render the delta newest-tailed (§4.1): over the byte cap, drop from the
|
|
121
|
+
* OLDEST end and prepend `[truncated N older bytes]`.
|
|
122
|
+
*/
|
|
123
|
+
export function renderDelta(window) {
|
|
124
|
+
const lines = window.flatMap(renderMessage).filter(line => line.length > 0);
|
|
125
|
+
const total = Buffer.byteLength(lines.join('\n'), 'utf8');
|
|
126
|
+
if (total <= MAX_DELTA_BYTES)
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
let truncatedBytes = 0;
|
|
129
|
+
let start = 0;
|
|
130
|
+
// Drop whole oldest lines until the remainder fits under the cap.
|
|
131
|
+
while (start < lines.length) {
|
|
132
|
+
truncatedBytes += Buffer.byteLength(lines[start] ?? '', 'utf8') + 1;
|
|
133
|
+
start += 1;
|
|
134
|
+
if (total - truncatedBytes <= MAX_DELTA_BYTES)
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
const marker = `[truncated ${truncatedBytes} older bytes]`;
|
|
138
|
+
return [marker, ...lines.slice(start)].join('\n');
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=delta.js.map
|