dsh-hypatia-auto-memory 2.4.2 → 2.4.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/README.md CHANGED
@@ -400,9 +400,12 @@ agent/created ──▶ rules/taboos inject()
400
400
  - **Auto-approve** answers the approval request for the agent's own bash
401
401
  `hypatia` calls, and only those: the executable word must be a trusted
402
402
  basename (after `KEY=value` prefixes) and the command must carry no pipe,
403
- redirect, chain or command substitution *outside quotes* — a JSE argument full
404
- of `|` and `>` inside quotes still qualifies. Everything else goes to the
405
- human. The plugin's own writes never take this path.
403
+ redirect, chain or command substitution *outside single quotes* — a JSE
404
+ argument full of `|` and `>` inside quotes still qualifies. Double quotes do
405
+ **not** make a payload inert: the shell expands `$(…)` and backticks inside
406
+ them, so `hypatia query "$(id)"` goes to the human like any other composed
407
+ command. Everything else goes to the human. The plugin's own writes never
408
+ take this path.
406
409
 
407
410
  Scope, measured rather than assumed: in DSH a bash call raises an approval
408
411
  request **only** when the sandbox denies it and the model retries with
@@ -668,7 +671,7 @@ now only projects plugin Config forms.)
668
671
  | Every write fails right after changing `shelf` | The shelf is not registered or not connected — startup logs `shelf "<name>" is not registered`. Connect it (`hypatia connect <dir> --name <name>`) and save the setting again (or reload the profile), or pick another |
669
672
  | The agent searches `default` while the plugin writes elsewhere | A disk copy of `hypatia-memory` (see below) replaced the bundled skill, or recall is disabled, so nothing told the agent which shelf to use |
670
673
  | Duplicate `msg-*` after weird manual edits | Delete the entry in hypatia and lower `lastLoggedSeq` for that session in the state domain — backfill recreates it once |
671
- | The agent's own `hypatia` write still asks for approval | `autoApprove: false`, the command pipes/redirects/chains outside quotes, or its first word is not one of `binaries` — only plain calls are answered, by design. Reads never reach approval at all, so nothing to fix there |
674
+ | The agent's own `hypatia` write still asks for approval | `autoApprove: false`, the command pipes/redirects/chains outside single quotes (or substitutes `$(…)`/backticks even inside double ones), or its first word is not one of `binaries` — only plain calls are answered, by design. Reads never reach approval at all, so nothing to fix there |
672
675
  | The agent got a memory protocol that tells it to log messages by hand | Another plugin registered `hypatia-memory` first (`dsh-hypatia` still in the profile), or a `hypatia-memory` exists on disk — typically `~/.agents/skills/hypatia-memory`, written by `hypatia skill install --agent codex`. Agent presets load disk skills in a layer nearer than plugins, so it wins in sessions even though the skill center may list this plugin. Remove the copy the startup warning names |
673
676
  | Can't tell which model actually ran | The list is a priority order, so a task's first attempt names the head of `consolidation.models` and a retry names the route it degraded to. Each attempt lands in `~/.dsh/storages/hypatia_auto_memory_diag.json` (`model_calls`) with purpose, provider/model, the `effort` actually sent, outcome, duration and the route's own token split — reading one task's rows top-down is how a fallback is confirmed, and `effort` is how a configured thinking level is confirmed to have been honoured. The usage ledger cannot answer this — it folds agent turns (`assistant/message`) and never sees a plugin's direct `llm.stream`. A `pending` row means the call is still in flight, or the process died mid-call: the row is written before the call and settled after it |
674
677
  | A configured `reasoningEffort` seems to do nothing | The route does not declare that level, so the call drops it rather than risk the core's `UNSUPPORTED_REASONING_EFFORT`. One warning names the route, the value and what the route does declare; `model_calls.effort` then reads empty for those attempts. The card offers exactly the levels the model advertises, so this normally only happens after editing the profile by hand or pointing the route at a different model |
package/README.zh-CN.md CHANGED
@@ -307,8 +307,10 @@ agent/created ──▶ rules/taboos inject()
307
307
 
308
308
  - **Auto-approve** 只回答 Agent 自己的 bash `hypatia` 调用的审批请求,而且仅限这些:
309
309
  可执行词必须是一个可信的 basename(去掉 `KEY=value` 前缀之后),并且命令在
310
- **引号之外**不能有管道、重定向、串联或命令替换——一个引号内满是 `|` 和 `>` 的 JSE
311
- 参数仍然合格。其余都交给人类。插件自己的写入从不走这条路。
310
+ **单引号之外**不能有管道、重定向、串联或命令替换——一个引号内满是 `|` 和 `>` 的 JSE
311
+ 参数仍然合格。双引号**不**能当作"已成字面量":shell 会在双引号内展开 `$(…)` 和
312
+ 反引号,所以 `hypatia query "$(id)"` 和任何带组合的命令一样交给人类。其余都交给
313
+ 人类。插件自己的写入从不走这条路。
312
314
 
313
315
  Scope 是实测而非想当然:在 DSH 里,一个 bash 调用**只有**在沙箱拒绝、且模型带
314
316
  `sandbox_permissions` 重试时才会发起审批请求。检索(`query`、`search`、`list`、
@@ -522,7 +524,7 @@ shelf 清单本来就不是配置,而设置域现在只投影插件的 Config
522
524
  | 改了 `shelf` 之后每次写入都失败 | shelf 没注册或没连接——启动时记录 `shelf "<name>" is not registered`。连接它(`hypatia connect <dir> --name <name>`)后重新保存设置(或重载 profile),或换一个 |
523
525
  | Agent 在 `default` 里搜,而插件写在别处 | 一份磁盘上的 `hypatia-memory`(见下)替换了随包技能,或 recall 被禁用,所以没有东西告诉 Agent 用哪个 shelf |
524
526
  | 手动乱改后出现重复 `msg-*` | 在 hypatia 里删掉该条目,并在 state domain 里把该会话的 `lastLoggedSeq` 调低——回填会重新创建它一次 |
525
- | Agent 自己的 `hypatia` 写入仍要审批 | `autoApprove: false`、命令在引号外有管道/重定向/串联、或首词不是 `binaries` 之一——按设计只回答纯调用。读取根本不会走到审批,所以那里没有要修的 |
527
+ | Agent 自己的 `hypatia` 写入仍要审批 | `autoApprove: false`、命令在单引号外有管道/重定向/串联(双引号内的 `$(…)`/反引号也算)、或首词不是 `binaries` 之一——按设计只回答纯调用。读取根本不会走到审批,所以那里没有要修的 |
526
528
  | Agent 拿到了一份教它手动记录消息的记忆协议 | 另一个插件先注册了 `hypatia-memory`(profile 里还有 `dsh-hypatia`),或磁盘上有 `hypatia-memory`——通常是 `hypatia skill install --agent codex` 写的 `~/.agents/skills/hypatia-memory`。Agent 预设会在比插件更近的一层加载磁盘技能,所以它在会话里胜出,尽管技能中心可能列出本插件。删掉启动告警指出的那份拷贝 |
527
529
  | 不知道实际用了哪个模型 | 列表是优先级顺序:一个任务的第一次尝试就是 `consolidation.models` 的队首,重试则是它降级到的那条。每次尝试都落在 `~/.dsh/storages/hypatia_auto_memory_diag.json`(`model_calls`),带 purpose、provider/model、**实际发出的 `effort`**、outcome、耗时以及路由自己上报的 token 拆分——把同一个任务的几行按序读下来,就能确认备选确实被用上了,而 `effort` 是确认配置的思考档位真的生效了的依据。usage ledger 答不了这个问题——它只折算 Agent 的 turn(`assistant/message`),看不见插件直连的 `llm.stream`。`pending` 行表示调用仍在进行中,或进程在调用中途死了:记录在调用前先落盘、调用结束后回填 |
528
530
  | 配了 `reasoningEffort` 却像没生效 | 该路由没有声明这个档位,于是调用把它丢掉了,以免撞上核心的 `UNSUPPORTED_REASONING_EFFORT`。会有一条警告点名路由、值和该路由实际声明的档位,此后这些尝试的 `model_calls.effort` 是空的。卡片只提供模型声明的档位,所以这通常只发生在手改 profile、或把这条路由换成了别的模型之后 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hypatia-auto-memory",
3
- "version": "2.4.2",
3
+ "version": "2.4.3",
4
4
  "description": "Event-driven Hypatia memory for DSH: automatic conversation logging via session events, queued background consolidation (span summaries + work-unit extraction) with a dedicated model route, and recall injection — zero per-call model work in the main session.",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -8,10 +8,13 @@
8
8
  * prompt every single time and, in practice, stops the agent from retrieving at
9
9
  * all.
10
10
  *
11
- * The matcher is narrow on purpose: a bash call whose executable word is a
12
- * trusted basename AND that contains no shell composition outside quotes. A
13
- * command that pipes, redirects, chains or substitutes is a command that can do
14
- * something other than talk to hypatia, so it goes to the human.
11
+ * The matcher is narrow on purpose: a shell call whose executable word is a
12
+ * trusted basename AND that contains no shell composition outside single
13
+ * quotes. A command that pipes, redirects, chains or substitutes is a command
14
+ * that can do something other than talk to hypatia, so it goes to the human.
15
+ * Only single quotes are trusted to make a payload literal: `$(…)` and
16
+ * backticks are expanded inside double quotes, so a double-quoted payload is
17
+ * still a payload that runs.
15
18
  *
16
19
  * Ported from `dsh-hypatia`'s auto-approve (same repository, MIT), which this
17
20
  * plugin replaces — see README.
@@ -21,6 +24,11 @@
21
24
 
22
25
  /**
23
26
  * True when the command's executable word (after env assignments) is trusted.
27
+ *
28
+ * Windows shapes are accepted too — an optional `.exe` suffix and backslash
29
+ * separators, as the pwsh tool produces them on a Windows host
30
+ * (`C:\Users\me\bin\hypatia.exe`, `hypatia.exe`).
31
+ *
24
32
  * @param {string} command
25
33
  * @param {string[]} binaries - trusted basenames, e.g. `['hypatia']`.
26
34
  */
@@ -34,15 +42,30 @@ export function invokesTrustedBinary(command, binaries) {
34
42
  }
35
43
  const word = /^([^\s"']+)/.exec(rest)?.[1]
36
44
  if (word === undefined) return false
37
- return binaries.some((binary) => word === binary || word.endsWith(`/${binary}`))
45
+ // The suffix is tried on both separators and on the bare word, so `hypatia.exe`
46
+ // and `C:/bin/hypatia.exe` match without a second regex.
47
+ return binaries.some((binary) =>
48
+ word === binary
49
+ || word === `${binary}.exe`
50
+ || word.endsWith(`/${binary}`)
51
+ || word.endsWith(`/${binary}.exe`)
52
+ || word.endsWith(`\\${binary}`)
53
+ || word.endsWith(`\\${binary}.exe`))
38
54
  }
39
55
 
40
56
  /**
41
- * True when the command contains shell composition OUTSIDE quotes.
57
+ * True when the command contains shell composition outside SINGLE quotes.
42
58
  *
43
59
  * Quote tracking matters: a JSE query argument routinely contains `|` or `>`
44
60
  * inside a quoted string (`'["$knowledge",[["$gt","x",1]]]'`), and treating
45
61
  * those as composition would send every structured query to the human.
62
+ *
63
+ * Double quotes are different, and that difference is the whole point: a shell
64
+ * expands `$(…)` and backticks inside them, so `hypatia query "$(id)"` still
65
+ * runs something else and must reach the human. Only single quotes make a
66
+ * payload literal. Everything else (`|`, `>`, `&&`, `;`) stays non-composition
67
+ * inside double quotes, which is what keeps `--data="a | b"` prompt-free.
68
+ *
46
69
  * @param {string} command
47
70
  */
48
71
  export function hasShellComposition(command) {
@@ -67,7 +90,13 @@ export function hasShellComposition(command) {
67
90
  double = double === false
68
91
  continue
69
92
  }
70
- if (single || double) continue
93
+ if (single) continue
94
+ if (double) {
95
+ // Substitution survives double quoting; nothing else does.
96
+ if (char === '`') return true
97
+ if (char === '$' && command[index + 1] === '(') return true
98
+ continue
99
+ }
71
100
  if (char === '&' || char === ';' || char === '|' || char === '<' || char === '>' || char === '`' || char === '\n') {
72
101
  return true
73
102
  }
@@ -78,11 +107,17 @@ export function hasShellComposition(command) {
78
107
 
79
108
  /**
80
109
  * True when this tool execution is a plain call to a trusted binary.
110
+ *
111
+ * `pwsh` and `powershell` qualify because that is how a Windows host runs its
112
+ * shell tool; the command string gets the same conservative scan, so widening
113
+ * this gate changes only which tool name may ask, not what may pass.
114
+ *
81
115
  * @param {{name?: string, arguments?: {command?: unknown}}} exec
82
116
  * @param {string[]} binaries
83
117
  */
84
118
  export function isPureTrustedCall(exec, binaries) {
85
- if (exec?.name !== 'bash') return false
119
+ const name = exec?.name
120
+ if (name !== 'bash' && name !== 'pwsh' && name !== 'powershell') return false
86
121
  const command = exec.arguments?.command
87
122
  if (typeof command !== 'string') return false
88
123
  return invokesTrustedBinary(command, binaries) && hasShellComposition(command) === false