23 Commits

Author SHA1 Message Date
opencode-sync c14f82830f skills: remove obsolete opencode-config (replaced by opencode-plugins) 2026-10-13 12:40:06 +08:00
opencode-sync a9b4251158 sync: import skills/, plugins/, memory/ and shared files from origin/main 2026-10-13 12:38:53 +08:00
xiaoshuai 33155181a1 Add AGENTS.md instructions and allow tracking it 2026-10-12 21:13:06 +08:00
xiaoshuai d03a9f462c memory: 记下 opencode 配置仓库本身的位置与推送方式
新增 facts/opencode-config-oc-repo.md:~/.config/opencode 是独立仓库
(oc.git / main / SSH),只跟踪三棵树,改动在项目目录里看不到,
必须单独提交。顺带补上 facts 类别的 index.md。

Verified: cd ~/.config/opencode && git status 干净、
git log @{u}..HEAD 为空,push 返回 8dc02b6..2d4df45 之后无新差异。
2026-10-12 18:11:41 +08:00
xiaoshuai 2d4df45acb memory/rules: 用「完全通过测试才提交」替换「改完立即推送」
- 新增 rules/only-commit-fully-tested.md(pinned):端到端跑通才算通过、
  禁止用 mock 冒充验证、已知缺陷不许推、如实写验证范围、不确定就说不确定
- 删除 rules/plugin-push-on-change.md:其「改完立刻 commit+push、不询问」
  与新规则的验证前提冲突
- 同步更新 memory/rules/index.md 的路由表

原规则里仍然有效的两条(改插件后 `opencode service restart`、推送走 SSH)
未写入新文件,需要的话另开一条记。
2026-10-12 18:10:48 +08:00
xiaoshuai cb9db666ba memory: 修注入开销、idf 排序与抽取诊断
注入侧
- context 在每次模型请求都触发,改为按 session + 最新用户消息去重:
  一个 turn 里 n 次工具调用不再重复注入 n 个 memory 块
- 默认注入从 1200 字符 / 内联 4 条正文,降到 600 字符 / 内联 0 条:
  大纲已说明存在什么,正文交给 memory_recall 按需取一次,避免每次
  请求都为同一批正文付费
- halfLifeDays 此前被静默丢弃,注入始终按 14 天半衰期排序,现已接通
- 召回缓存 2s -> 500ms,同一工具链里 remember -> recall 能读到刚写的文件

排序
- idf 原先拿「单文件匹配数」当分子、「总文件数」当分母,每个文件得到
  相同 idf,排序退化为纯词频;改为按文档频率计算的真实 idf

抽取
- 移除 400 段上限:它会静默截断长对话,300 轮只贡献前 ~50 轮
- role 在顶层或 message 下两种返回结构都读,之前只读 .role 得到空转录,
  长度门槛不过,抽取看起来永远不触发
- 去重改按关键词重叠计分,不再用 bigram 重叠:模型改写与人工措辞几乎
  不共享 bigram,真实改写只拿到 0.43;关键词是双方被迫共用的词汇
- 新增 memory_extract_dryrun:对真实模型跑完整抽取链路,不必改
  minTurns 或等长会话
- 新增 memory_inspect:诊断记录按 scope 存入 storage key,并保留历史
  与 fires/injected 计数,去重是否生效可被证伪

其他
- Context 改从 Plugin 命名空间导入,此前类型退化为 unknown
- store 暴露 kind 字段;compileRootTables 内联进 compileRoot
- frontmatter 列表元素统一 String(),index 不再出现数字

验证:由仓库所有者端到端验证(真实模型调用、真实会话);提交者未复跑,
以上为 diff 复核结论。
2026-10-12 18:10:40 +08:00
xiaoshuai 8dc02b6b35 memory: distinguish a failed extraction from an empty one
Probing the extraction path with a real model call showed it returning a
zero-candidate outcome — indistinguishable from "the conversation had
nothing worth keeping". It was actually an auth failure delivered as a JSON
error body rather than a thrown error, which coerceText turned into an empty
string, which parseCandidates turned into an empty array.

extract() now reports `error` and a `rawText` preview instead of collapsing
every failure mode into `{candidates: []}`. An auth error arriving as a
return value rather than a throw is the common case, not the exotic one.

memory_inspect prints the error and the raw response, so this class of
failure now shows up instead of looking like a healthy extractor that
rarely fires.

Verified: thrown error, error object, empty string, and null each produce a
specific message; valid JSON and fenced JSON still parse.
2026-10-09 10:31:06 +08:00
xiaoshuai 92c612b94c memory: fix extraction trigger, dedupe, and add runtime diagnostics
Four defects found by end-to-end testing, not by reading the code.

1. Extraction never ran, silently. Collecting every event type that
   ctx.event.subscribe actually delivers shows `session.idle` is NOT among
   them — it is defined in the protocol but never emitted on this stream.
   The subscription filtered on it alone, so auto-extraction was a permanent
   no-op with no error. Now debounced off events that do arrive
   (session.step.started / session.tool.called / session.inbox.delivered)
   after `settleMs` of quiet, which is what "idle" meant anyway. Verified:
   the trigger now fires and reports "轮数不足" for a 1-turn session.

2. Dedupe silently stopped rejecting duplicates. It compared against a fixed
   retrieval score, but that score carries idf and scope priors that drift as
   the corpus grows, so an obvious restatement of an existing memory scored
   0.48 against a 0.9 threshold and was written as a rival entry. Now uses
   term coverage of the candidate, which answers "is this already said?" and
   does not move with corpus size. Verified: a 100%-duplicate is rejected, a
   novel pitfall is kept.

3. Harness-injected user turns became the retrieval query. "<system-reminder>"
   and "The server restarted while you were working" are user-role messages,
   so the hook retrieved on them and injected memories chosen by noise. Both
   are filtered out now.

4. The `pinned` flag was absent from the remember tool's input schema, so the
   schema stripped it and pinning was unreachable from the tool surface.
   `kind` also ignored the rules/ category when a stale value was stored.

memory_inspect now reports runtime health for the two paths with no other
observable effect: last injection size and query, and last extraction outcome
with its reason. Without it, "working" and "silently dead" look identical —
which is exactly how defects 1 and 2 survived the first round of testing.
2026-10-09 10:28:34 +08:00
xiaoshuai a64ea2acb3 Add memory plugin: three-tier markdown store with plugin-driven retrieval
A durable memory layer for OpenCode. Structure:

  ROOT.md          total plan: goals, hard rules, directory registry,
                   and a scenario -> file routing table
  <category>/
    index.md       one line per file, COMPILED from frontmatter
    <slug>.md      the memory itself

Two roots, same shape: <config>/memory/ (global) and
<project canonical>/.memory/ (project). Project wins on conflict; global
hard rules stay resident everywhere.

Why a plugin rather than a skill that tells the model to read a folder:
model-driven retrieval is unreliable — the model forgets, reads half the
files, or reads the wrong ones. Markdown is the human-facing format; the
plugin is the engine that parses, scores, and injects.

Measured, not assumed: FTS5's two built-in tokenizers both fail on this
corpus. On SQLite 3.53.4 with "部署脚本用 pnpm 而不是 npm...": unicode61
emits 7 terms for the whole sentence and matches 0 hits for "部署";
trigram needs >= 3 characters and also returns 0 for that two-character
word. So tokenization happens in the plugin (CJK bigrams, latin words)
and no SQL index is used — a thousand memories are fine to scan.

index.md and ROOT's two tables are compiled, never hand-edited: a
hand-maintained index drifts, a generated one cannot. Hit counters live
in .state.json so cross-device sync does not conflict on every recall.

Tools are named recall/remember/forget/inspect rather than
write/read/status: generic names were observed shadowing the harness's
own built-in tools.
2026-10-09 10:17:45 +08:00
xiaoshuai 8ba68d7f9a Untrack opencode.json: its shell path is a per-user Windows absolute path
The "shell" field points at C:\Users\<name>\.config\opencode\pwsh.cmd, so
cloning this repo on Linux or macOS left OpenCode trying to exec a shell that
does not exist there.

The file stays untracked on each machine and keeps the UTF-8 fix working
locally. pwsh.cmd remains tracked: it only calls chcp and pwsh.exe from PATH,
so it is portable.
2026-10-09 00:01:49 +08:00
xiaoshuai 3857c1f051 Remove agent-memory plugin; track UTF-8 shell config and npm manifest
agent-memory is removed: upstream (kurbezz/memory) states Windows is not supported, as the skill relies on absolute symlinks into the store and a Bash CLI. The checked-in plugins/agent-memory.js was a git symlink (mode 120000) to a Linux path, which Windows materialised as a 68-byte text file, failing to load with 'AggregateError: 7 errors building'.

Now tracked so the setup is reproducible elsewhere: opencode.json and pwsh.cmd configure the UTF-8 shell (chcp 65001) that OpenCode's -NoProfile PowerShell otherwise lacks, and package.json records better-sqlite3, without which the sqlite plugin fails to resolve.
2026-10-08 23:56:06 +08:00
opencode-agent d6cee59bac sqlite: document TUI keybind for generic tool output
The TUI ships with no keybind for session.toggle.generic_tool_output
(the keymap in packages/tui/src/config/keybind.ts sets it to
keybind("none")), so users see collapsed tool cards and no output
at all unless they either open the command palette or rebind the key.

Adds a 'ctrl+t' binding to ~/.config/opencode/tui.json so toggling is
one keystroke away, and documents the palette path + the rebind path
in the README so the user has both. No plugin code change.
2026-10-08 17:46:35 +08:00
opencode-agent e795d24230 sqlite: also emit an ASCII table so the TUI shows columns
The OpenCode TUI renders tool output through a literal <text> node
(packages/tui/.../session/index.tsx, GenericTool), so a GFM table
becomes a wall of pipes. The web/desktop client passes the same output
through the markdown renderer and draws a proper grid, so a GFM table
is the most compact form there.

The previous fix emitted a markdown table and assumed the TUI would
render it. It does not. The fix: ship both. The ASCII table is space-
padded, word-wraps long cells, and stays aligned even when the TUI
shows only its collapsed 3-line preview; the markdown table follows
and is what the web/desktop client renders with a proper grid.

- New rowsToAsciiTable: cellToAscii, truncateForCell, wrapCell,
  visibleWidth, padRight. NULL -> <null>, BLOB -> <BLOB nB>, Date ->
  ISO, BigInt -> string. Cell width capped at 40 chars by default
  (configurable). Single words longer than the cap are hard-wrapped.
- sqlite_query's format=table path now emits the ASCII table first
  and the markdown table second, with a blank line between.
- README explains why the duplication and how to read both.

End-to-end verified via Code Mode (no model in the loop) after
opencode service restart picked up the new transform.
2026-10-08 17:45:44 +08:00
opencode-agent d0f31023bd sqlite: render results as Markdown tables for the TUI
OpenCode's tool content schema (Tool.Content = TextContent | FileContent)
gives a server plugin no metadata channel — the TUI's only view of tool
output is the content string. So: format the result as a Markdown
table, which the TUI's built-in markdown renderer already displays as
a real table.

- sqlite_query gains format = 'table' (default) | 'json' | 'both'.
  'table' is the new default and renders a header line, a separator
  row, and one row per record with pipes/newlines/backslashes escaped.
  'json' keeps the old behaviour for callers that need raw rows.
- Every tool that runs SQL echoes 'SQL: <stmt>  params: [...]' as the
  first line of its output, so the user sees what was executed
  including the bound values.
- Every tool that runs a statement pushes context.progress() at start
  and end, which surfaces in the TUI status bar.
- cellToMarkdown caps each cell at 80 chars to keep wide BLOBs from
  blowing up the TUI column width.

End-to-end verified via Code Mode (no model in the loop):
- format=table renders correctly with edge cases (| in text, \n in
  text, \ in text)
- format=both appends the JSON view below the table
- exec echoes the bound params as a JSON array
- write+read round-trip persists (UPDATE → SELECT sees the new value)
2026-10-08 17:36:44 +08:00
opencode-agent d1aabef771 sqlite: document what survives a process kill
User asked what happens if the window is closed mid-task. Added a
'What happens if the process dies mid-task' section that tabulates
the four exit paths (TUI close, SIGTERM, SIGKILL, power loss) and
empirically confirms that anything past the last fsync'd commit is
on disk. The only ways to lose data are :memory:, an explicit
synchronous=OFF, or a network filesystem. Also corrects the earlier
'leak fd' framing — a dead process leaks no fds, the OS reclaims
them.
2026-10-08 17:22:13 +08:00
opencode-agent f873990e72 sqlite: document lifecycle and concurrency behaviour
User asked what happens if the connection is never closed and whether
concurrent calls are safe. Adds a Lifecycle and concurrency section
to README.md covering:

- not closing only leaks fds; data is durable on commit
- 3-tier concurrency model: same service / multi-CLI same service /
  multi-process, with the actual busy_timeout default (5s) and what
  to do if a longer wait is needed (run PRAGMA busy_timeout via
  sqlite_exec)
- :memory: semantics (gone on close, even within one process)
- NFS/SMB caveat

No code changes; current defaults (busy_timeout 5s, synchronous
FULL, locking normal) are sensible and the existing test sweep
already verified end-to-end.
2026-10-08 17:20:21 +08:00
opencode-agent 9aef200ac9 Add sqlite plugin: open/query/exec/script against .db files
Adds a single-file V2 plugin that exposes eight tools under the
`sqlite` namespace (open, close, list, query, exec, script, tables,
schema) so a model can read and write SQLite databases from a session.

- OpenCode discovers it automatically from ~/.config/opencode/plugins/.
- New runtime dependency: better-sqlite3 (installed in this repo's
  node_modules so the host's module resolution picks it up).
- README documents install, copy-to-another-machine, options, and
  troubleshooting; SETUP.md is a copy-paste checklist.
- Schema/options were trimmed down while authoring: the initial draft
  registered a no-op SQLite function on every open as a 'future hook';
  removed, it was dead code.
2026-10-08 17:03:48 +08:00
xiaoshuai 3d72d7821a Add agent-memory skill and plugin
Install the three-level file-based agent memory (kurbezz/memory):
skills land in ~/.agents/skills/agent-memory via 'npx skills add',
and plugins/agent-memory.js is the installer-created symlink into it.
Pure markdown store under ~/.agent-memory, no vector DB and no API key.
2026-10-08 15:43:16 +08:00
xiaoshuai d6e29cbbad searxng-search: record that no domestic-only engine set works here
Asked for a domestic-only default, so every general-web index SearXNG ships
for China was added to keep_only, the container was restarted, and each was
measured over 5 runs per query. None of them is usable. The default stays
bing,yandex.

  engine          en hits  zh hits  outcome
  360search         5/5      5/5   reliable, 0% relevant
  quark             0/5      0/5   rate-limited dead after two good responses
  baidu              -        0   CAPTCHA, 1h IP suspension
  sogou              -        0   upstream AttributeError
  baidu kaifa        -      0/10   code index: "opencode v2" matches OpenCV2
  bilibili           -        -   video titles, not a web index
  chinaso news       -        -   falls through to the default engine set

quark was the one that looked like the fix. On its first two requests it
returned the exact article being asked about as result #1 on both the Chinese
and the English query -- better than anything else measured -- and then answered
0 results on 5 consecutive runs of each. Had it not been run 5 times, it would
have looked like the obvious upgrade.

baidu kaifa is worth remembering as a category error: it is a code search
index, so it matches "opencode v2" against OpenCV2 and returns CSDN blog posts
at 0% relevance. It is reachable and does not CAPTCHA, which makes it look
healthy in /config while being useless.

The foreign engines that were reachable (brave, duckduckgo, google, qwant,
wikipedia) were added at the same time and all returned 0, consistent with the
network timeouts already recorded in settings.yml. mojeek and startpage are not
present in this SearXNG release at all, so the earlier note calling them
"unreachable" was imprecise.

settings.yml was restored from a backup and diffed byte-identical before this
commit; the probe engines never left the instance.

cn.bing.com is the only dependable endpoint and is a China-hosted Bing, so bing
counts as the domestic option. Its one real limitation stands: it ignores the
site: operator, so site-restricted search is unavailable through this instance.

Also fixes a duplicated `bing` row left in the README engine table by the
previous commit.
2026-10-08 13:44:46 +08:00
xiaoshuai aa70b381bf searxng-search: default engines bing,yandex instead of bing,360search
The default was chosen by a single Chinese test query and generalized from
it. Measuring both a Chinese and an English technical query shows it was
inverted on both counts. Results mentioning "opencode", out of returned:

                       zh "OpenCode V2 和 V1 区别"   en "opencode v2 migrate from v1 plugin api"
  bing                          10/10                     missed the doc entirely
  360search                      0/5                      4, unrelated (sohu/toutiao)
  yandex                        14/15                     15, migration doc ranked #1
  naver                           0/9                     15, migration doc ranked #1

yandex had been excluded on the claim that it "has almost no Chinese index".
Measured, it has the best Chinese relevance of any engine here, and on the
English query the V1->V2 migration doc is its first result -- the exact
document that the old default could not find at all.

360search had been the default. It is the worst engine on both query types.
It splits the query on "V1"/"V2" and answers with Android and CANN
version-difference articles, which produced the 10-of-15 noise in the first
round of testing.

naver is left out rather than added: strongest on English, 0/9 on Chinese, so
including it would trade one kind of noise for another. It is documented as
worth passing explicitly for English lookups.

The comment above the constant now carries the measurement table, so the
default can be re-chosen from data instead of re-derived from a hunch.

Not changed, and worth knowing: cn.bing.com ignores the site: operator.
engines=bing&q=site:opencode.ai+v2+migration returns 0 results from
opencode.ai, while the same query without site: returns 3. Site-restricted
search is not available through this instance.

Unreproduced: one run of the Chinese query returned entirely unrelated
results from bing (Doubao, Zhipin, Zhihu) while four other runs with the same
parameters returned 10/10 relevant. No parameter difference was found between
the good and bad runs. Treated as transient upstream behaviour, not a plugin
bug, and not documented as a rule.

README engine table updated to the measured numbers.
2026-10-08 13:35:57 +08:00
xiaoshuai 9e10925023 searxng-search: document the plugin and how the instance was deployed
Two docs next to the plugin: README.md for the tool, SEARXNG-SETUP.md for the
Docker deployment behind it. Both are written from measurements against the
running instance, not from a guide.

The setup doc exists because the deployment decisions were only recorded as
comments inside settings.yml, which means they are unfindable unless you
already know to look there. It also records the parts that are not guessable:
why the port is 39217, why limiter is off, why the UI locale is zh-Hans-CN
rather than zh-CN, and which engine is broken how.

Corrections found while writing it:

- The rebuild steps pointed at searxng/searxng-docker, which is superseded. That
  repository now holds only a deprecation notice and its docker-compose.yml raw
  URL 404s. The templates moved to searxng/master/container/. Verified both
  local files are byte-identical to the current templates, so the deployment
  was always the current one -- only the documented URL was wrong.
- The plugin README claimed the entrypoint comes from package.json. This
  directory has no package.json; the entrypoint falls back to index.ts. Restarted
  the service with both .md files in place to confirm they do not affect
  resolution.

Documents the two SearXNG behaviours that fail silently rather than loudly,
both verified by reading the engine label on each result:

- An unknown engine name is ignored and the default engine set is searched
  instead, with no error. engines=mojeek returns 10 bing results and looks like
  a success, as does a typo. The only reliable signal is whether the requested
  engine appears in results[].engine.
- /config is not a way to check this. It reported disabled: false for every
  engine while baidu and sogou were in fact being dropped, because it reflects
  upstream defaults and not the local disabled: true overrides.

Also records that ctx.options is always {} for a directory-discovered plugin,
so the four options in PluginConfig cannot be supplied without moving the
plugin out of plugins/ and registering it in opencode.jsonc. Verified by
writing JSON.stringify(ctx.options) to a file from inside setup().

No secret_key value is in either file; settings.yml stays outside this
repository under ~/Docker.
2026-10-08 13:29:02 +08:00
xiaoshuai 13d353e96d skills: require every skills/ and plugins/ change to be pushed
Editing the skill, or a plugin under plugins/, is a change to the
~/.config/opencode repository. That had no rule attached to it, so work
landed locally and the remote silently drifted.

Add "Push every change" near the top of SKILL.md, where it is read before
the reference material rather than buried at the end, plus a fifth item in
"Verifying before you answer" so the push is part of the final check rather
than a separate task that gets skipped.

The section records the three constraints that are not guessable:

- Push over SSH. origin is git@git.xsinfo.vip:xiaoshuai/oc.git; an HTTPS push
  prompts for a password and there is no TTY, so it fails outright.
- Never git init inside skills/ or plugins/. OpenCode discovers every immediate
  subdirectory of plugins/ as a plugin package, so a .git there is loaded as
  plugins/.git/index and fails on each startup. The repository belongs at the
  config root, one level above both scan roots.
- Do not git add -f past .gitignore. opencode.jsonc, cli.json, service.json,
  package.json and node_modules/ are machine-local by design.
2026-10-08 13:17:48 +08:00
xiaoshuai 15a1bbcee0 Version OpenCode skills and plugins
Repository sits at the OpenCode config root rather than inside skills/ or
plugins/. OpenCode scans exactly those two subdirectories, and a .git
directory inside either one is picked up as a plugin and fails to load:

  loading plugin id=.../plugins/.git entrypoint=.../plugins/.git/index
  failed to load plugin ... AggregateError: 2 errors building .../plugins/.git/index

The watcher.ignore config option does not help: it only suppresses filesystem
update events, not plugin discovery, and the "-" disable prefix only applies to
plugins listed in opencode.json(c), not auto-discovered ones.

.gitignore admits only skills/ and plugins/ (plus itself). Personal config
(opencode.jsonc, cli.json, service.json) and node_modules stay local.

skills/opencode-plugins  V2-only plugin authoring reference and 7 templates,
                         transcribed from the V2 docs with the URL on every file
plugins/searxng-search   searxng_search tool over a self-hosted SearXNG JSON API
2026-10-08 13:12:10 +08:00