Commit Graph

593 Commits

Author SHA1 Message Date
Nicolò Boschi c61c4e7d7d release(coding-agents): v0.5.1 2026-09-01 15:54:30 +02:00
Nicolò Boschi 0eb818d0ab fix(coding-agents): the runtime auto-update never fired — registry 406 on the /latest media type (#3997)
latestVersion asked /<pkg>/latest with accept: application/vnd.npm.install-v1+json, which the registry only serves for the packument — it answers 406 there. !r.ok returned "", indistinguishable from "no newer version", so the runtime silently never updated. Drop the header. The fetch stub now answers 406 to that media type like the real registry, which makes four existing tests fail on the 0.5.0 code.
2026-09-01 15:53:00 +02:00
Nicolò Boschi 208b8729b6 release(coding-agents): v0.5.0 2026-09-01 15:13:25 +02:00
Nicolò Boschi 71a9a7008f feat(coding-agents): first-class pi support, sharing one extension adapter and installer with Prime Agent (#3993)
Adds pi (@earendil-works/pi-coding-agent) as a harness and extracts createPiExtension(harness),
the extension adapter pi and its fork Prime Agent share. Supersedes #3775.

The `pi` key is removed from package.json: both hosts read that same key, so it could only ever
name one bundle, and the host it did not name loaded the other's and reported the wrong harness.
`hindsight-coding-agents install pi|prime-agent` is now the only route for both.

Also makes the companion skill self-update for every host that installs one — the paths now live
once in core/skill-dirs.ts, which both the installer and skill-sync read.

Co-authored-by: Sebastian Otaegui <feniix@gmail.com>
2026-09-01 15:11:06 +02:00
Nicolò Boschi 833d400f9b chore(coding-agents): sync generated files after per_source (#3872) (#3994)
#3872 merged while `verify-generated-files` was still running, and that job
failed ~40s after the merge. Two generation steps had not been run:

- prettier reformats the widened `ObservationScopes` union onto one member
  per line (the `prettier-int-coding-agents` task in scripts/hooks/lint.sh).
- `./scripts/generate-docs-skill.sh` emits a third copy of the coding-agents
  page under skills/hindsight-docs/references/, which needs the `per_source`
  section like the other two.

CI only runs on `pull_request`, so this drift is invisible on main but fails
`verify-generated-files` for the next PR that opens against it.

No behaviour change: formatting plus a regenerated doc.

Claude-Session: https://claude.ai/code/session_011n8KQc8sCe4cJJCd8Cv69n
2026-09-01 14:37:07 +02:00
Shaun Eccles-Smith 247e9b3138 feat(coding-agents): add per_source observation scoping (#3872)
* feat(coding-agents): add per_source observation scoping

On a repo worked by a coding agent, commit diffs and session transcripts
make different kinds of claim. A diff records what the code does; a
transcript records what someone intended, argued for, or discarded. Under
the `shared` default both consolidate into one undifferentiated belief
set, so an idea floated in chat and never implemented is indistinguishable
from a belief derived from the commits, and "what does the codebase
actually do" cannot be answered from commit-derived knowledge alone.

This cannot be fixed by configuration. The server treats an explicit
scope list as unconditional: `_resolve_obs_tags_list` and
`_resolve_write_scopes` in the consolidator both return the parsed list
verbatim, without filtering it against the memory's own tags. A configured
`[[], ["source:git"], ["source:chat"]]` therefore writes EVERY document
into all three scopes, and the `source:git` scope fills with observations
built from chat transcripts. Only a per-document decision separates them.

`per_source` is resolved client-side, per document, in the new exported
`resolveRetainScopes`, and never reaches the server. It expands to the
global scope plus one per distinct `source:` tag the document carries,
sorted — a document with two source tags (the commit-message seed keeps
`source:git` alongside `source:git-log` so the cold-repo check still sees
it) gets a scope for each, which needs no arbitrary tie-break and does not
depend on the order the caller assembled its tags in.

Reading only `source:` is what keeps this safe. `per_tag` splits on the
right axis but also on every other one: it would reinstate the per-agent
`harness:` fork that #3564 and #3575 removed, and any volatile tag such as
a session id from `retainTags` would become its own scope — the
fragmentation bug itself.

The empty scope is always emitted first and unchanged, so the merged view
matches `shared` exactly and the untagged observations that knowledge
pages read (`tags_match: "all"`, per #3664) are unaffected.

The cost is honest: one extra consolidation pass per document. That is the
price of the axis, and it is why this is opt-in —
`DEFAULT_OBSERVATION_SCOPES` remains `shared` and no existing value
changes meaning.

Closes #3871

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdYchB9yKUWw8oa16RWQu9

* fix(coding-agents): not every source tag names a kind of claim

Running per_source on real repositories showed two of the five source tags are
provenance labels rather than axes, and each produced a scope worth less than it
cost.

source:git-log is a bookkeeping alias. git.ts tags the commit-message seed with
it AND source:git, so emitting a scope for each gave two near-identical belief
sets — 307 observations against 302 on one repo — and doubled the consolidation
for that pair. It is the same claim as source:git: what the commits say. Mapped
onto it rather than dropped, because that seed is where a cold repo's entire
commit history arrives.

source:survey-baseline marks the "researching…" status document, whose retain
strategy is meant to extract nothing. It still yielded a scope holding exactly
one observation — a belief set that exists only to be noise. Excluded.

Emitting a scope per DISTINCT source tag, sorted, is still right: taking the
first made the result depend on the order git.ts assembled its tags, which a
test pins. It is the vocabulary that carries two non-semantic entries, not the
rule.

* Revert "fix(coding-agents): not every source tag names a kind of claim"

This reverts 0786de8. The evidence behind it was an artefact.

The claim was that source:git and source:git-log produced near-identical belief
sets (307 observations against 302). They did — but only because at the time of
measurement the bank held exactly ONE git document, the commit-message seed,
which carries both tags. The per-commit diff backfill under gitIngest: "full"
had not run yet, so one document was feeding both scopes.

Once it does, the two diverge and are genuinely different questions:
source:git-log is fed only by the seed — what the commit MESSAGES say — while
source:git also collects every per-commit diff. Intent against implementation.

The reasoning was wrong at a deeper level too. Overlapping content across scopes
is not duplication to be engineered away: consolidation already distills and
deduplicates WITHIN each scope, and a fact that legitimately answers two
questions belonging to both is the design working. Excluding tags to avoid
overlap misreads what a scope is for.

source:survey-baseline goes back for the same reason. Its scope holding a single
observation suggests the marker document's retain strategy is extracting facts
it is meant to suppress — which is worth fixing where it happens, not papering
over in the scope resolver.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-09-01 14:28:09 +02:00
Nicolò Boschi c249d43796 coding-agents: support opencode 2, whose plugin API shares nothing with v1's (#3985)
opencode v2 (`@opencode-ai/cli@beta`, binary `opencode2`) installs alongside v1
and rewrote the plugin contract end to end: a v1 plugin is a function returning a
bag of named hooks, a v2 plugin is `{id, setup(ctx)}` where ctx hands out
per-domain registration. Handing either host the other's export loads a plugin
that registers nothing and reports no error, so v2 needs its own adapter — mapped
onto the same harness-agnostic RuntimeCore, which keeps recall, injection, the
native hindsight_* tools, the cold seed and write-back identical to every other
harness:

  v1 chat.message                       -> ctx.session.hook("prompt")
  v1 experimental.chat.system.transform -> ctx.session.hook("context")
  v1 tool: {...}                        -> ctx.tool.transform(d => d.add(...))
  v1 event (session.idle)               -> ctx.event.subscribe()
  v1 client.session.messages()          -> ctx.session.context({sessionID})

Three host behaviours shaped the result, each verified against a live
opencode2 0.0.0-beta-18743:

ONE CONFIG ENTRY SERVES BOTH CLIS. v1 and v2 read the same
~/.config/opencode/opencode.json, and v1 REJECTS the whole file when it sees v2's
`plugins` key ("Configuration is invalid ... Unrecognized key: plugins") — so a
second entry is not an option. What makes one entry work is that they resolve a
plugin DIRECTORY differently: v1 follows package.json `main` (dist/index.js), v2
ignores `main` and loads <dir>/index.js. The package therefore ships a root
index.js re-exporting the v2 entry, and the path already in a user's `plugin`
array drives the right plugin under each host, with no config change.

THE EVENT STREAM IS GLOBAL; THE SESSION HOOKS ARE NOT. v2's background service
hosts every open project at once. `ctx.event.subscribe()` delivers session.idle
for sessions in OTHER projects, with no location on the envelope to tell them
apart, while prompt/context fire only for the location that loaded the instance.
Acting on those ids would fetch another project's transcript and retain it into
this project's bank, so write-back is gated on sessions our own prompt hook
admitted. This is a bank-isolation invariant, and it has a regression test.

TOOLS NEED options.codemode: false. Without it a tool is reachable only through
v2's `execute` code-execution tool, and a model calling it by name — which the
skill and the injected preamble both tell it to do — gets "Unknown tool:
hindsight_...".

Two capabilities are deliberately absent. The codebase survey gets no opencode2
recipe: v2 plugins cannot define an agent (agent.transform has no `add`) and a
config-file agent is not a sandbox either, since the user's own global
permissions are appended after an agent's rules — the survey reads untrusted repo
files, so without a guaranteed read-only boundary it falls back to another
installed agent's CLI, as it already does for kilo/cline/cursor/dcode. And there
is no toast: v2 plugins can observe tui.toast.show but not publish one, so the
seed banner is logged. v1's mid-session cadence write-back is also gone, because
v2 makes it redundant rather than because it was missed — session.execution.succeeded
fires after every assistant turn, so the idle path already runs once per turn
from the authoritative post-reply transcript.

Verified end to end against a real opencode2 and server: plugin loads reporting
harness opencode2, per-repo bank derived, cold seed and survey ran, the injected
block reached the model (it quoted it back verbatim), hindsight_ingest_document
was called by name, and session idle produced retain_ok. Also verified the staged
published-package shape loads under opencode2, and that opencode v1 still loads
dist/index.js and reports harness "opencode" with the new root index.js present.

Closes #3795

Claude-Session: https://claude.ai/code/session_01WbaYxouCERUv9djo3GnnA2
2026-09-01 14:08:34 +02:00
Altay 173ed6a93c fix(coding-agents): annotate MCP tool safety (#3818) 2026-09-01 12:06:34 +02:00
MasterST1337 5ceca53e93 fix(coding-agents): survey prompt says Glob, not Read, for the directory layout (#3796)
The cold-repo survey spawns a headless `claude -p --model haiku
--allowedTools Read Glob Grep` session. SURVEY_PROMPT asks the model to
understand "the directory layout" but never says which tool shows it, and
haiku reaches for `Read(<repoRoot>)` — a bare directory path, which errors
with EISDIR before the survey has read anything.

Measured on one machine's OTel export, 2026-08-23..25: 10 distinct survey
sessions failed this way on first tool call, across kids-yt-factory,
hindsight, sitebrain.eu, PlanView, Meridian and four .traycer worktrees —
i.e. every repo the survey ran cold against, plus a fresh worktree each
time (a new worktree is a new bank, so the survey re-runs).

Glob was already in --allowedTools; the model was simply never told to
prefer it here. This is a prompt-level counter-instruction, not a
structural guarantee — the robust fix is at the tool-wiring layer (deny
Read on a directory, or give the survey an ls-shaped tool), which this
does not attempt.
2026-09-01 12:00:52 +02:00
Nicolò Boschi c23e98856b fix(coding-agents): stop a failed git probe from forking a worktree into its own bank (#3981)
A transient failure of the single `git rev-parse --git-common-dir` probe silently
changed a repository's bank identity: `getProjectRootFromGit` mapped every failure
to `null`, indistinguishable from "not a git repository", so `gitProjectName` took
the basename fallback and a linked worktree was retained into a brand-new bank —
permanently, with no log line, no diag event and no retry. One repository in the
wild accumulated eight stray `coding-agent::<repo>-wtN` banks this way.

Both triggers were load-dependent and silent: the 1000 ms timeout elapsing under
machine load, and `execFileSync` failing to spawn at all (EAGAIN under process
pressure).

Read the repository layout instead of spawning git (core/git-layout.ts). `.git` is
either the git directory or a one-line pointer to it, and a linked worktree's git
directory names its repository in `commondir` — so the answer is a handful of fs
reads against a format git itself guarantees, with no subprocess, no timeout and
no spawn to fail. A pure-JS git library was considered and rejected: isomorphic-git
models objects and refs, not worktree discovery, so it answers a different question
at a much larger cost in a package bundled into every hook process.

On top of that, the fix the issue asks for:

- the probe distinguishes "not a repository" from "could not tell", and only the
  former reaches the basename fallback;
- transient errors retry with backoff before the probe gives up;
- a failed probe never guesses: resolution throws BankResolutionError and every
  entrypoint goes through `deriveBankIdOrSkip`, so the lifecycle hooks skip the
  session (recoverable) rather than scatter it (not);
- the skip logs a warn and a `bank_unresolved` diag event.

Also rejects a common dir that no longer exists, so a pruned worktree can no longer
be named after its own dangling `.git/worktrees/<name>` — the same wrong id by a
different route.

Tests cover real repos/worktrees/bare hubs (including resolution with PATH emptied,
proving nothing spawns), the retry and failure classification, the refusal to guess
plus its diag event, and a family guard asserting no module outside core/bank.ts
calls the throwing form directly.

Fixes #3950

Claude-Session: https://claude.ai/code/session_011n8KQc8sCe4cJJCd8Cv69n
2026-09-01 11:25:55 +02:00
Nicolò Boschi 1aef228f7b feat(coding-agents): add qwen-code as a hook harness (#3979)
* feat(coding-agents): add qwen-code as a hook harness

Qwen Code speaks Claude Code's hook protocol field for field -- same stdin
envelope (session_id / transcript_path / cwd / hook_event_name), same
hookSpecificOutput.additionalContext + systemMessage output, same exit
semantics, and a settings.json shape byte-for-byte what cmdHook() already
emits. So HOOK_HARNESSES["qwen-code"] is claude-code's spec with three deltas,
none of which is visible from the diff:

1. TIMEOUTS ARE MILLISECONDS. Qwen passes a command hook's `timeout` straight
   to setTimeout ("Timeout in milliseconds, default 60000" -- its own bundled
   docs/features/hooks.md). Every other harness here is seconds, so the
   installed values are 30000/30000/60000. Writing 30/60 would register 30ms
   hooks, and that misconfiguration LOOKS fine in testing: Qwen spawns without
   detached:true and kills only the direct child, so the orphaned work still
   completes. HookHarnessSpec therefore declares `timeoutUnit`, and the
   lifecycle tests normalise through it -- changing 30_000 to 30, or dropping
   the unit, now fails a test instead of shipping dead hooks. The prompt budget
   must also clear core/hook.ts's HOOK_REFLECT_CAP_MS (25_000).

2. parse reads `submitted_prompt`, not `prompt`. UserPromptSubmit also fires on
   tool-result continuations, where `prompt` holds model-bound tool output;
   keying on it would recall ~20x per user turn against tool results.
   `submitted_prompt` is attached only when the turn is both the first and a
   genuine userQuery. Accepted cost: it is the interactive TUI's projection, so
   headless (`qwen -p`), serve, SDK and ACP sessions seed and retain but never
   recall. The E2E declares injectsIntoModel: false for exactly that reason --
   a different reason from grok-build's passive hook.

3. type:"user" is NOT a user turn. `provenance` is the discriminator: across a
   22-transcript corpus, synthetic records (notification 285, cron 21,
   goal_runtime 1) outnumber real_user ones (69) by 4.4:1. transcript-qwen.ts
   gates on it, rejecting a present-but-malformed value outright rather than
   falling through to a subtype heuristic, and requiring a positive subagent
   marker (agentId/isSidechain) before accepting an absent one -- subagent
   transcripts use a third envelope with neither provenance nor subtype.

Qwen also echoes injected context back into its own transcript, wrapped in
<qwen:user-prompt-submit-context> with the inner tags HTML-escaped, so the
shared stripInjectedMemory (which matches raw tags) cannot see it. The reader
removes it using the two forms of pairing evidence Qwen's contract defines:
systemPayload.hookContext present -> use systemPayload.displayText, the host's
own pre-hook projection, with no tag matching at all; otherwise a COMPLETE
tagged context in the FINAL part after at least one other part. It deliberately
does NOT match the tag as a substring -- the contract is explicit that the tag
is "a provenance marker, not ... a general trust boundary" and that consumers
"must not infer that arbitrary tag-like user text is hook provenance", so a
prompt merely quoting the tag is preserved intact.

Also here:
- registry.test.ts gains the REVERSE parity check. The existing one is
  directional (installer -> registry), so a harness present in the registry but
  missing from INSTALLERS passes it while `install <name>` returns "unknown
  harness". That is exactly how this change was briefly broken.
- readQwenTranscript catches lazy-read faults. readJsonlTail guards
  statSync/openSync, but the generator reads at iteration, and runRetainHook
  calls the reader outside buildRetain's catch -- so a directory passed as
  transcript_path (which passes both guards, then throws EISDIR on the first
  readSync) rejected the whole Stop hook. NOTE: the underlying gap is in the
  shared jsonl.ts and affects every harness; it deserves its own fix rather
  than riding along here.
- Logo asset from homarr-labs/dashboard-icons (Apache-2.0, svg/qwen.svg).

Tests: 675 pass. Note HINDSIGHT_BANK_ID must be unset in the environment, or
config.test.ts's "missing files yield defaults" fails on the leaked value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WkVVSuv7j1FwTtVpkzzmWR

* chore(coding-agents): regenerate docs artifacts and apply prettier

Address the two CI-blocking review items on #3769:

- Regenerate the two artifacts derived from the README's new Qwen Code
  section: hindsight-docs/docs-integrations/coding-agents.md (via
  sync-coding-agents-doc.mjs) and the docs-skill copy (via
  generate-docs-skill.sh).
- Run prettier over the package with 3.7.4 -- the version CI's root
  `npm ci` pins -- covering the four files the review names, plus
  package.json, where it restores the literal ellipsis in `description`
  that the branch had accidentally left as a unicode escape sequence.

Verified: sync-coding-agents-doc.mjs --check, build-skill.mjs --check,
and prettier --check all pass; 675 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qx1ByUMo3SJhXP7MBxEQ1g

* fix(coding-agents): close the qwen-code harness parity gaps

Rebased onto main (the branch was 98 commits behind and conflicted with the
dcode harness) and closed the sites a new hook harness must also appear in,
found by sweeping every place an existing harness is registered:

- `SKILL_DIRS` (core/skill-sync.ts) did not map qwen-code, so the skill the
  installer copies into ~/.qwen/skills would have been installed once and then
  never refreshed by `npm update -g`. Verified ~/.qwen/skills is Qwen's real
  user-level skills root (Storage.getUserSkillsDirs, SKILL_PROVIDER_CONFIG_DIRS
  = [".qwen", ".agents"]). Added a family-wide guard asserting SKILL_DIRS covers
  every `installSkill` call site, with the five pre-existing gaps (copilot-cli,
  grok-build, cline-cli, dsh, prime-agent) listed explicitly so a new harness
  cannot silently join them.
- The docs site had no qwen-code icon, so the README's own logo would 404, and
  the harness was absent from both coding-agent logo rosters.
- package-lock.json did not carry the three new bins.

Two comments asserted things that are not true of qwen-code 0.22.3 and are
corrected against its source: the hook runner spawns `detached` and calls
`terminateHookProcessTree` on timeout, so a milliseconds/seconds mix-up loses
the retain outright rather than orphaning it; and headless `qwen -p` DOES carry
`submitted_prompt` — the E2E is retention-only because the stub model echoes
only the first 20 000 characters of the request, which Qwen's system prompt
alone exceeds.

Verified end to end against a local API: installed the published tarball into a
container, ran `qwen -p`, and confirmed session_start / reflect_ok / pages_ok /
retain_ok, a document tagged `harness:qwen-code`, and zero injected-memory
leakage in the retained text.

Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN

---------

Co-authored-by: Mallory M <mallorymiller1984@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:16:02 +02:00
Nicolò Boschi 0c2f9e7c2f fix(coding-agents): only ever ADD to a bank's config, never overwrite it (#3966)
Closes #3927.

`configureBank` runs on every session start, and it re-sent the plugin's five
retain strategies and its `knowledge` entity-label group as whole values. The
server stores each of those as ONE config value, so an import replaces the map
outright: a strategy the user had defined was deleted, their edits to the
plugin's own strategies (mission, extraction mode, chunk size) were reverted,
and `retain_default_strategy` could be left naming a strategy that no longer
existed — silently, once per session, on whatever bank the plugin was pointed
at. Pointed at a global bank shared with non-coding work, the plugin took the
bank over.

This is the third fix for one shape. #1270 covered OpenClaw's missions, #2492
this plugin's; each protected only the fields that had just been noticed, and
everything else kept being stamped back. So the rule is now the whole surface
rather than a list: read the bank's current OVERRIDES and write only where the
bank is silent. `codingBankManifest()` returns the template fields still
missing, or nothing at all — a settled bank now makes no import call.

The container fields merge per entry, which keeps the reason the re-apply
exists: a strategy ADDED by a newer release still reaches an existing bank.
What it gives up is deliberate — a release that REWORDS an existing strategy or
label does not reach a bank that already has it; clearing that override takes
the current default back on the next pass.

`manageBankConfig: false` keeps the plugin out of the bank's configuration
entirely, for a bank whose owner shapes it themselves. That bank should then
define the strategies this plugin retains under, and the cost is documented as
the silent one it is: the server does not reject a retain naming a strategy the
bank lacks — `apply_strategy` logs a warning and extracts with the bank's own
config, so a commit diff, a session transcript and a survey marker would all
get the same generic treatment. (The claim that such a retain is *rejected* was
inherited from #3352's rationale and is wrong; corrected here.)

Knowledge pages are seeded either way: they are not bank configuration, and
`pageTriggerType` already governs what they cost.
2026-09-01 11:06:35 +02:00
Nicolò Boschi 4fa110dd59 coding-agents: keep the installed runtime current by itself (#3965)
`install` stages this package into ~/.hindsight/coding-agents and points every wired
agent's hooks at that copy. Nothing ever refreshed it: the only update path was the
user remembering to re-run `install`, so a machine could sit several versions behind
indefinitely — a fix only reached people who happened to re-install. Found on a
machine running 0.4.2 while 0.4.3 had been published for days, with no signal
anywhere that an update existed.

Once a day, at session start, ask the registry for the published version and — when
it is newer — spawn a detached updater. The current session keeps running the code
it already loaded; the next one starts on the new build.

`update` is a new installer command: `install`'s staging half and nothing else. It
replaces the staged runtime (a path stable across versions, so every wired agent
picks the new code up on its next spawn) and writes to NO host config. That
separation is what makes it safe to run unattended — an `install` would need a
harness list, and choosing one on the user's behalf would rewire agents they never
asked us to touch. The cost is bounded and documented: a release introducing a NEW
hook entry point is staged but not referenced until a manual `install`.

Only ever replaces a runtime it can prove npx downloaded. `stageRuntime` records the
directory it copied from, and a copy staged from `npm i -g`, from a project
dependency, or from a local checkout is left to whoever manages that source:
re-staging behind npm's back would leave `npm ls -g` naming a version that is no
longer what runs, and re-staging over a checkout would replace a developer's own
build mid-session. A missing marker means no — it is written on every install from
this version on, and a machine has to re-install once to get this code at all, so a
runtime old enough to lack it is too old to be running the check. Failing closed
costs one manual install; failing open costs somebody their working tree.

Concurrency is real here, not hypothetical: this is a plugin for machines that run
five agents at once, and a 24h stamp does not serialise anything — several sessions
starting in the same second all read "due" before any has written it. Two concurrent
stageRuntime runs are `rmSync(dist)` then `cpSync`, where one process deletes the
directory the other is half way through writing, leaving a runtime with missing
entry points and every hook broken. A lock claimed before the registry call makes a
burst produce ONE request and one updater; same shape as deepen.ts's per-bank lock,
with the holder's pid deciding liveness so a crash cannot wedge the window, and the
stored pid is the detached CHILD's since the copy outlives the session.

Other guards, each with a test: `npx` must be on PATH (without it there is nothing
to spawn, so the check is skipped rather than burning a request and failing a spawn
asynchronously); a prerelease never supersedes the release of the same version; an
unreadable staged version never guesses; the survey's own headless session is
excluded; and both ownership refusals stamp the check so each states its reason at
most once a day.

`autoUpdate: false` (or HINDSIGHT_AUTO_UPDATE=false) pins the installed version,
settable globally, per harness or per bank. `disabled` stops it too — an inert
plugin should stay inert, and a network call plus a background npm install is not
inert.

Wired at BOTH session-start paths — `runSessionStartHook` for the hook harnesses and
`RuntimeCore.seedIfCold` for the persistent-plugin hosts — plus a family-wide guard
test that enumerates session starts structurally rather than from a hand-maintained
list, so a third host cannot land without an update check. Session-start
housekeeping has gone missing on the plugin hosts before (#3524), and the harness
that forgets is by definition the one whose test nobody wrote.

Known window, documented in the module doc: staging replaces dist/ wholesale, so a
hook spawning during the copy can fail to load. Running processes are unaffected,
the window is milliseconds once a day, and the cost is one turn without memory.
Serialising against it would need a lock every hook takes on every turn — a worse
trade than the window it closes.

Also hoists survey.ts's `binExists` to util.ts as `binOnPath`.

Claude-Session: https://claude.ai/code/session_011KDT484YujNcBxHzbfzNbk
2026-09-01 10:45:47 +02:00
Nicolò Boschi 4388e0f95b feat(coding-agents): add native DeepAgents Dcode integration (#3887)
Registers `dcode` (LangChain's deepagents-code) as a native Agent Plugin: root `plugin.json`
contributing the shared skill, the Hooks V2 SessionStart/UserPromptSubmit/Stop lifecycle, and the
`hindsight_*` MCP server, installed through Dcode's own marketplace/plugin manager.

Includes fixes found by running it against deepagents-code 0.1.65:

- Decode `last_assistant_message`. Dcode's transcript lags the Stop event, so that field is
  load-bearing, but it is computed as `str(content)` — a Python repr whenever the provider returns
  content blocks. It was retaining ~1.8KB of encrypted reasoning payload as the assistant turn, and
  never comparing equal to the transcript's clean text, so an already-flushed reply was appended
  again every turn. Guarded family-wide: any harness surfacing the field must declare a decoder.
- Annotate the six read-only MCP tools with `readOnlyHint`. Dcode rejects unannotated MCP calls in
  headless mode, which made recall and the knowledge-page tools unusable under `dcode -n`.
- Parity: local history import (attributed via `dcode threads list --json`, since the transcripts
  carry no cwd), a Docker E2E adapter and image, the harness icon on the docs site as well as the
  control plane, and a clean uninstall that also retires the marketplace it registered.
- Document that Dcode cannot host the codebase survey: `hindsight_ingest_document` writes, so its
  headless runtime gates it by design; it falls back to another agent's CLI like eight other
  harnesses.

Supersedes #3887.

Co-authored-by: Paritoshdagar <paritoshdagar@gmail.com>

Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN
2026-09-01 10:13:24 +02:00
Nicolò Boschi d7d137fa1e fix(daemon): drop the idle timeout that could kill an in-flight request (#3930)
* fix(daemon): drop the idle timeout that could kill an in-flight request

The daemon's idle checker measured idleness as "time since the last request
*started*" (IdleTimeoutMiddleware stamped last_activity on entry, never on
completion), so a retain, reflect or consolidation that outlived the configured
timeout was SIGTERM'd mid-flight — the client saw a connection error and the
work was lost. Raising the timeout only lowered the odds; 0 (the default
everywhere but the legacy cursor hook package) was the only safe value.

Rather than teach the middleware to count in-flight requests, remove the
feature: a shared local daemon that quietly exits under a long operation is not
worth the resource reclamation it buys. The daemon now runs until it is
stopped.

- Delete IdleTimeoutMiddleware, the idle-checker thread and DEFAULT_IDLE_TIMEOUT.
- Keep `--idle-timeout` parseable — hindsight-embed and every coding-agent
  integration still pass it — but ignore it, printing a note when it is
  non-zero. The integration packages therefore need no change and no release.
- Drop it from the current docs, the SDK/integration READMEs and the generated
  docs skill. The coding-agents README keeps the row marked deprecated because
  docs-freshness.test.ts requires every readable RawConfig field to be
  documented; versioned docs, blog posts and changelogs are left as history.

Closes #3903

Claude-Session: https://claude.ai/code/session_012gXUp1i7YrWmLJVUYki53g

* review: drop the now-dead logger and refresh the comments the removal invalidated

- daemon.py: the module logger only served the deleted idle checker.
- coding-agents daemon.ts / runtime.ts / runtime.test.ts and openclaw: comments,
  a debug line and the plugin-schema description still described an auto-exit
  that can no longer happen. The config fields stay (inert) as agreed.

Claude-Session: https://claude.ai/code/session_012gXUp1i7YrWmLJVUYki53g
2026-08-31 15:59:31 +02:00
Mike Evans cd2e5407aa fix(coding-agents): stop opencode's install from replacing a JSONC config (#3843)
opencode loads its global config from either `~/.config/opencode/opencode.json` or `opencode.jsonc`. The installer knew only the first name and read it with `readJson`, whose strict `JSON.parse` rejects the comments and trailing commas a hand-maintained config carries and whose `catch` returns `{}`. A user whose config is `opencode.jsonc` got a second config file created beside their real one; had it been named `.json`, the `{}` fallback would have been written back carrying only our `plugin` key, taking their providers, MCP servers and agent overrides with it.

Three layers change:

- `opencodeConfigPath` probes `opencode.jsonc` then `opencode.json`, edits whichever exists, and creates `opencode.json` only when neither does — the same shape as `kiloConfigPath`.
- The adapter parses with `parseJsonc` and aborts with a `SKIPPED` log on a config it cannot read, rather than falling back to `{}`.
- `writeJsonc` sets or deletes one top-level key through `jsonc-parser`, so text it did not touch survives byte for byte, and a CRLF file stays CRLF. Parsing was only half the problem: a config that read correctly still came back stripped, because `writeJson` round-trips through `JSON.stringify`.

kilo had the JSONC-aware read and the same lossy write, and its test asserted the loss; it now writes through `writeJsonc` too, with a parity guard sweeping both JSONC hosts through a real install and uninstall.

`jsonc-parser` goes in tsup's `noExternal`: `installer.js` is staged to `~/.hindsight/coding-agents` as dist + skill + package.json and never `node_modules`, so an external import there cannot resolve and re-running `install` from the staged copy would exit with `ERR_MODULE_NOT_FOUND`.

Comments inside the `plugin` array are still lost, since that value is re-emitted; everything outside it is kept.
2026-08-31 14:54:49 +02:00
Evo 1977d5804b fix(coding-agents): stop unsafe gitlog cleanup (#3879)
The git-log deepen path enumerated bank-wide `source:git-log` documents and deleted every returned non-canonical one as stale. The document listing endpoint's inclusive `all` tag mode can return untagged documents, so that sweep deleted unrelated documents — and because deletion cascades to facts, a routine coding-agents sync could remove unrelated memories from a shared bank.

Remove the cleanup entirely: git-log sync now owns only its canonical document and never deletes other document IDs. Internal multi-tag strategy probes use `all_strict`, while `listDocumentIds()` keeps its existing public `all` default for external callers. A git-log snapshot counts as current only when the current-HEAD query contains this repository's canonical document; otherwise the sync performs an idempotent upsert.

Canonical IDs remain `gitlog:<repoName>`, so same-named repositories and forks can still share one ID. This change prevents cross-document deletion; renamespacing safely needs a separate ownership/migration design.

Fixes #3877.
2026-08-31 14:50:43 +02:00
Sanderhoff-alt 3b51887619 fix(openclaw): add non-interactive flags to plugin install in smoke test (#3917)
Recent versions of the openclaw CLI require explicit confirmation for
installing plugins from non-ClawHub local archives and for accepting
declared plugin capabilities. In non-interactive CI environments, the
unprompted command aborts with an install cancellation error.

Pass `--force --accept-capabilities` to `openclaw plugins install` and
`--force` to `openclaw plugins uninstall` in `smoke-test.sh` so the
smoke test completes cleanly without interactive prompts.
2026-08-31 11:51:48 +02:00
Nicolò Boschi 3a4a100075 release(coding-agents): v0.4.3
Claude-Session: https://claude.ai/code/session_01AHvpDBNP3CXfVL88WnazmC
2026-08-26 14:52:42 +02:00
Nicolò Boschi e04d02ee62 fix(coding-agents): don't let one stray file abort the Claude import, and give Prime Agent the companion skill (#3771, #3772) (#3812)
* fix(coding-agents): skip a stray file instead of aborting the import, and give Prime Agent the skill

`importLocalHistory` documents that it never throws, but the Claude reader did:
`jsonlFiles` gated on `existsSync`, which is TRUE for a regular file sitting where
a project directory was expected, and the `readdirSync` behind it threw ENOTDIR
straight out of `--import-conversations` — one junk entry killed the whole run
instead of costing that entry. A `listDir` helper now owns "there is nothing here
to list", which also closes the same hole one level up on `~/.claude/projects`
itself. (#3771)

Prime Agent documents the same skill-discovery roots the other skills-capable
hosts use but never received the packaged companion skill, so it only ever picked
one up by accident from the shared `~/.agents` root. It now installs into its own
`~/.prime/agent/skills`: `uninstallSkill` removes a fixed directory name, so
writing to the shared root would make `uninstall prime-agent` delete Codex's and
dsh's copy. (#3772)

Closes #3771
Closes #3772

Claude-Session: https://claude.ai/code/session_01AHvpDBNP3CXfVL88WnazmC

* docs(coding-agents): regenerate the docs skill mirror for the Prime Agent skill line

Claude-Session: https://claude.ai/code/session_01AHvpDBNP3CXfVL88WnazmC
2026-08-26 14:50:52 +02:00
Nicolò Boschi ea9b2fecfb release(coding-agents): v0.4.2 2026-08-24 14:26:58 +02:00
Altay 3b2438356e fix(coding-agents): check knowledge pages before reflecting (#3702)
* fix(coding-agents): check knowledge pages before reflecting

* docs(coding-agents): align pages-first guidance

* docs(coding-agents): regenerate the docs-skill reference from the README

The README edit reached skill/SKILL.md and the docs page but not
skills/hindsight-docs/references/sdks/integrations/coding-agents.md, which
is generated from the README too (by scripts/generate-docs-skill.sh). It
still described autoReflect=false as "the tool guide instead tells the
agent to call hindsight_reflect", contradicting the behaviour this branch
introduces, and verify-generated-files failed on the drift.

All four surfaces now carry the same wording.

Claude-Session: https://claude.ai/code/session_01HVZ94d143NPSsZjbnvwqba

* refactor(coding-agents): name the new-goal trigger for what it now does

Code-review follow-ups, no behaviour change:

- REFLECT_ON_GOALS -> PAGES_FIRST_ON_GOALS. The constant is file-local and
  its old name described the pre-change behaviour (reflect first), which is
  exactly the guidance this branch reverses.
- ToolGuideOpts.reflectOnNewGoals kept its name (two call sites depend on
  it) but its doc comment now records what changed and why, per the
  keep-comments-current rule.
- Explain why the negative assertion deliberately omits the `s` flag: it
  guards the old single-line wording, and with `s` it would span newlines
  and match the legitimate hindsight_reflect / FIRST STOP lines below.

Claude-Session: https://claude.ai/code/session_01HVZ94d143NPSsZjbnvwqba

---------

Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-08-24 14:23:30 +02:00
pstanton237 b448202bd3 fix(coding-agents): find Claude sessions for underscore paths (#3732)
* fix(coding-agents): find Claude sessions for underscore paths

* fix(coding-agents): encode every non-alphanumeric in the Claude project dir

Claude Code does not special-case underscores: it replaces EVERY
non-alphanumeric character in the absolute path with `-`. Encoding only
`/`, `.` and `_` still missed whole repositories, most commonly ones with
a space in the path (`~/Documents/My Projects/...`), which reported
"no past sessions found on disk" while the transcripts sat on disk under
their real name.

Verified against Claude Code 2.1.241 by running it in two directories:

    .../hs_under_test   -> ...-hs-under-test
    .../hs+odd@repo v2  -> ...-hs-odd-repo-v2

Case is preserved and runs are not collapsed, so the encoding is a 1:1
character substitution.

Widening the class cannot misattribute a session: the directory name only
narrows the candidate set, and each transcript must still record a `cwd`
inside the repository before it is imported. A regression test covers that
collision directly.

Claude-Session: https://claude.ai/code/session_01HVZ94d143NPSsZjbnvwqba

---------

Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-08-24 13:26:45 +02:00
Sanderhoff-alt 1e45834788 fix(integrations): resolve bare-hub project names (#3741)
Resolve hidden bare repository directories to their hub project name across
coding-agents, OpenCode, Claude Code, and OMO.
Use stable standalone bare names; Claude Code and OMO may migrate existing
users from old parent-directory bank IDs to the corrected names.
Old memories remain in the previous bank because bank IDs are not migrated.
Handle optional Git mock arguments for strict TypeScript checks.
Add mock and real-Git regression coverage, including bare-hub opt-in and
directory-bank mapping inheritance.
2026-08-24 13:15:48 +02:00
Sanderhoff-alt 5bda953a2d fix(coding-agents): tolerate hidden page triggers (#3742)
Treat an omitted page trigger as unknown rather than drift.
Avoid trigger-only PATCH requests to older servers that reject the field.
Continue reconciling source-query changes and explicit trigger drift.
Document the unknown-trigger behavior and add regression coverage.
2026-08-24 13:15:24 +02:00
Sanderhoff-alt 3f14672ea0 fix(windows): hide daemon subprocess windows (#3743)
Add windowsHide to daemon probes, detached starters, and
hindsight-all commands. Cover the behavior with regression tests for
both packages.
2026-08-24 11:57:41 +02:00
Parafee41 0429060be9 fix(coding-agents): hide Windows child processes (#3736) 2026-08-24 11:55:29 +02:00
Sanderhoff-alt 1cbd77d2b8 docs(coding-agents): add Devin CLI to harness list in README intro (#3721) (#3738) 2026-08-24 11:47:50 +02:00
Nicolò Boschi c30a3aa8da release(openclaw): v0.11.1 2026-08-24 11:10:33 +02:00
Nicolò Boschi d97b560e2c fix(openclaw): probe append capability in local-daemon mode (#3686) (#3706)
`detectAppendCapability` was wired into the four external-API code paths only
(#1102: "wired into all 4 checkExternalApiHealth call sites"); local-daemon mode
has no such call site, so it was never probed. `supportsUpdateModeAppend` stayed
`false` there forever, retain fell back to per-turn document ids sequenced from
an in-memory counter, and every host restart replayed `…:turn:000001` onto the
previous cycle's document — which retain's default `update_mode: 'replace'`
deletes before reprocessing. A production deployment with months of daily
traffic on one session was left with 29 documents.

The daemon is a Hindsight API like any other. Treat it as one:

- probe `<daemon>/version` in all three local-daemon paths — initial start, the
  healthy-daemon re-check, and reinit — so append mode engages and document ids
  become session-stable
- `getActiveApiEndpoint()` resolves "the API we are talking to" (external URL or
  spawned daemon); `refreshQueueOperationIdCapability` uses it, so local-daemon
  retains finally carry `operation_id` instead of being hard-gated on
  `usingExternalApi` and replaying non-idempotently
- initialize the retain queue in both modes: a daemon that is still booting or
  has crashed is as unreachable as a remote API, and a failed retain was simply
  dropped
- stamp a per-process boot token into fallback ids (`…:turn:<boot>:000001`).
  The counter is in memory *and* FIFO-capped at MAX_TRACKED_SESSIONS, so ids
  could be recycled without any restart; this holds even when a probe fails
  transiently and the fallback re-engages.

Tests: new local-daemon-parity.test.ts drives the real `service.start()` against
a mocked daemon — probes /version and enables append, keeps the fallback when
the daemon reports `store_document_text: false`, picks up a queue file from a
previous session, and mints a fresh boot token per process. Verified red on the
pre-fix code.
2026-08-24 11:07:39 +02:00
Nicolò Boschi 1b74e3efa8 docs(coding-agents): generate the companion skill from the README (#3735) (#3753)
The skill and the README documented the same configuration in two hand-
maintained files, and they drifted in both directions.

The skill never named `bankIdTemplate`, `dynamicBankId`, `optInOnly`,
`optInPaths`, `retainTags`, `retainMetadata`, `resolveWorktrees`,
`maxParallelRetains` or any daemon setting, and it stated outright that
the package reads "no environment variables" — there are 35
(core/config.ts ENV_KEYS). That is the half #3735 reported. The README
had lost the other half: `autoReflect` and `surveyRefreshCommits` were
documented only in the skill, and neither file mentioned that
`HINDSIGHT_CONFIG` relocates the config file, that the layering chain
ends at `banks.<resolvedBankId>`, or that `optInPaths` takes a
comma-separated env value like its sibling `retainTags`.

The README is now the single source. scripts/build-skill.mjs copies the
regions it marks with `skill:begin` / `skill:end` into skill/SKILL.md,
normalising heading levels per region; only the agent-facing half — which
tools to call, crediting memory, correcting a stale memory — stays
separate, in skill-src/preamble.md. The docs page was already generated
from the README; its generator now strips the markers, which MDX cannot
parse.

The skill's operative debugging knowledge (sync-status readiness,
resetting a bank, the survey-baseline/gitlog marker documents, the
session_start/deepen_started triage rule) moved into the README, so it
reaches users and the docs site too.

src/docs-freshness.test.ts fails when the skill is stale, when a field of
`RawConfig` is readable from a config file but named nowhere in the
README — the check that makes this class of drift impossible to
reintroduce — and when a documented claim about the env layer stops
holding (every key really is `HINDSIGHT_<FIELD_IN_CAPS>`; the four
map-valued settings really are file-only). It also unit-tests the marker
parser. prepublishOnly regenerates the skill so a publish cannot ship a
stale copy.
2026-08-24 11:06:28 +02:00
Nicolò Boschi c5e3feed4c release(coding-agents): v0.4.1 2026-08-20 10:51:28 +02:00
Nicolò Boschi 170a078578 fix(coding-agents): let knowledge pages see the observations they ask for, and stop tagging initiative markers with a page id (#3664)
* fix(coding-agents): let knowledge pages see the observations they ask for, and stop tagging initiative markers with a page id (#3641)

Two tag mistakes, one root: the plugin used `tags` for things tags cannot do.

1. Pages could never retrieve an observation. Every seeded page is tag-scoped to
   one `knowledge:<tier>` label, and the server defaults a tagged model to
   `all_strict`, which EXCLUDES untagged memories. Since #3564 this plugin
   retains with `observation_scopes: "shared"`, which consolidates into the
   single empty scope — so every observation in these banks is untagged. Pages
   asked for the `observation` fact type and could not match a single row of it,
   synthesizing from raw world/experience facts alone.

   The trigger now states `tags_match: "all"`: AND over the page's tier tag, but
   including untagged rows. A `knowledge:decision` fact still cannot reach the
   Component map (it is tagged and lacks that page's tag), while the untagged
   shared observations reach every page, where the `source_query` selects among
   them. `seedPages()` re-sends the trigger when the live page reports a
   different `tags_match`, so previously seeded banks are repaired too — the
   source query is unchanged on those banks, so it could not be the drift signal.

2. `relatedPageId:<pageId>` was never a tag. The tag vocabulary here is fixed and
   low-cardinality on purpose (matching is exact set-ops, no wildcards); one new
   tag value per initiative isolates nothing — the concrete page and the overview
   both filter on `knowledge:feature-work` — and lands on every fact extracted
   from the marker. It existed only to carry an id into the synthesis prompt.

   The id now rides on `metadata.relatedPageId` and, because reflect strips
   `metadata` from its search results while keeping `context`, on a
   `[[page:<id>]]` link in the marker's retain context. That is the channel the
   overview page actually reads back, so its cross-links keep working.

Per-initiative pages remain scoped by their `source_query`, not by tags; giving
them real tag isolation needs work facts to carry the initiative, which is a
separate change.

* test(coding-agents): guard the single observation scope over the whole source tree

Forwarding `observationScopes` from every entrypoint (already guarded) is
worthless if a write path skips the method that actually sends it. `retain()` is
the only place `observation_scopes` reaches the wire, so a second `/memories`
POST anywhere would silently consolidate under the server's `combined` default
and split one repo's beliefs per tag combination again (#3564) — while writing
perfectly good memories, so no existing test would fail.

Asserts over the source tree, the way daemon.test.ts guards `ensureDaemon`: no
module addresses the memories endpoint but the client, and that one call site
sits inside retain() with the scoping on the item it posts. Both halves verified
to fail when violated.

* refactor(coding-agents): type the page PATCH body, and cover the server that reports no trigger

Review follow-ups on this branch:
- the PATCH payload was a `Record<string, unknown>`; its keys are known, so state them.
- no test covered the case most banks are actually in today — a server older than
  #3572 reports no trigger on the tree, so the policy is unknowable and gets
  re-sent. It must be trigger-ONLY, or every deepen run rebuilds all five pages.
2026-08-20 10:35:51 +02:00
Nicolò Boschi f626cc8907 release(openclaw): v0.11.0 2026-08-19 21:55:00 +00:00
Evo 3d7f7930bb fix(openclaw): make queued retains idempotent (#3143)
* fix(openclaw): make queued retains idempotent

* chore(docs): sync generated OpenAPI reference

* style(openclaw): apply repository formatter

* fix(openclaw): keep the idempotency fix off the retain hot path

The operation-id work was gating live retains on the capability probe, which
cost more than the duplicates it prevents:

- every external-mode retain awaited a fresh `/version` round trip before
  sending, putting an extra request in front of every turn. The answer changes
  at most once per server restart, so the probe now runs only while the
  capability is still unknown.
- an unknown capability refused the *first* send — deferring it into the queue,
  or dropping it outright when no queue existed. Nothing is stored server-side
  on a first attempt, so there is nothing to duplicate: it now sends without the
  wire field, exactly as before this change. The id is still allocated and
  persisted with the request, so the replay — the only path that can duplicate —
  stays idempotent once the capability is known.
- a queue that failed to initialise threw out of service start. Losing the queue
  degrades to pre-queue behaviour; it should not take the plugin down.
- the flush no longer probes when the queue is empty, and warns instead of
  debug-logging when it holds a replay back, so a permanently unreachable
  `/version` cannot silently grow the queue forever.

Reworks the integration test around the corrected contract: a lost
acknowledgement replays under the id its first attempt already carried; an
unreachable `/version` still lets the first attempt through but holds the
replay; a known capability costs no further probes; and a stopped generation
does not resume against a restarted client.

---------

Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-08-19 23:54:35 +02:00
chethanuk e62ce26ef2 fix(openclaw): strip operator-configured display-name prefix from recall and retain (#3120)
* fix(openclaw): add opt-in display-name prefix stripping

Some channels prepend a human display name to the user text ("Alice: today
weather?"). stripRuntimeEnvelope only removed opaque routing ids
(om_/ou_/oc_), so the name survived into both the recall query and the
retained transcript, where it gets extracted as a fact.

Stripping is fixed once inside stripRuntimeEnvelope, so every call site
benefits -- recall (extractRecallQuery, composeRecallQuery) and retain
(prepareRetentionTranscript, extractStructuredBlocks) alike.

A generic `Word:` heuristic is deliberately not attempted: index.test.ts
pins that "计划: 今天修 retain 污染" must survive untouched, and no payload
field carries a display name to match against. The pattern is therefore
operator-supplied via senderPrefixPattern. Unset (the default) is
byte-identical to the previous behaviour; an invalid regex is ignored
rather than thrown.

* chore(skills): regenerate docs-skill openapi after #3109

CI's verify-generated-files job runs generate-docs-skill.sh and fails on any
diff. #3109 added last_write_at without regenerating this file, so the job is
currently red on main and on every open PR. Mechanical regen only.

* docs(openclaw): document senderPrefixPattern, and explain the module-global

The README and plugin manifest carried the new setting but the docs page's
option list did not, so the only people who hit #3070 (Feishu operators)
would not find the knob. Regenerated the docs-skill mirror to match.

Also replaces a stray marker in the comment above the compiled pattern with
what a reader actually needs: the global is safe because the host holds one
hindsight-openclaw config, and that is the assumption to revisit if a single
process ever serves more than one.

---------

Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-08-19 23:48:34 +02:00
Nicolò Boschi 99f408fc4a release(coding-agents): v0.4.0 2026-08-19 14:20:49 +00:00
Nicolò Boschi 1e889bd1c1 fix(coding-agents): tolerate empty tool discovery (#3626)
Co-authored-by: Altay <altay@hey.com>
2026-08-19 16:20:14 +02:00
xiaoxiao 0eee31b63c fix(openclaw): inject [doc:<document_id>] provenance into auto-recall memories (#3583)
* fix(openclaw): inject [doc:<document_id>] provenance into auto-recall memories

formatMemories previously dropped document_id before injection, so recalled
memories carried no source-session marker and another session/channel
memories could be mistaken for current context. Append [doc:<document_id>]
to each memory bullet when present; observations (no document_id) stay
unmarked and keep mentioned_at. Signature unchanged - no config, types, or
call-site changes.

Fixes #3582

* fix(openclaw): also inject the occurred window into auto-recall memories

`formatMemories` dropped `occurred_start`/`occurred_end` along with
`document_id`. `mentioned_at` only says when a fact was stated, so with the
event window gone the agent cannot order past events against each other —
"visited Paris" and "deployed the indexer" both read as whatever date they
happened to be mentioned on.

Each bound can be absent independently, so the wording is per-case
(`[occurred: X]`, `[occurred: X → Y]`, `[occurred from: X]`,
`[occurred until: Y]`) rather than a half-empty range the model has to
interpret.

---------

Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-08-19 16:13:50 +02:00
Max Marino 1d9a6a381c feat(llm): add GitHub Copilot subscription provider (#3597)
Adds a `github-copilot` LLM provider backed by the official GitHub Copilot SDK, using the signed-in Copilot entitlement with no `HINDSIGHT_API_LLM_API_KEY`.

Verified end-to-end against a live Copilot subscription: retain, consolidation and reflect all run on the provider.

Review fixes on top of the original submission:
- default to `gpt-5.6-terra`; the submitted `gpt-5.6-sol` is not in the model list the runtime serves, so every call failed and the provider was unusable out of the box
- make a rejected request configuration terminal instead of a runtime failure; an unavailable model was invalidating the shared runtime and respawning `copilot --headless` once per attempt (5 spawns at max_retries=3, ~12 at the default of 10)
- let a token-authenticated host run without `~/.copilot`, so the documented COPILOT_GITHUB_TOKEN / GH_TOKEN / GITHUB_TOKEN path works in containers and CI
- regenerate skills/hindsight-docs (the provider list also feeds the generated faq.md)

Co-authored-by: Max Marino <dudujuju828@users.noreply.github.com>
2026-08-19 15:48:00 +02:00
Nicolò Boschi 069ed96f35 fix(coding-agents): inherit opt-in across worktrees (#3471) (#3627)
* fix(coding-agents): inherit opt-in across worktrees

* fix(coding-agents): resolve removed worktree paths

* fix(coding-agents): retain opt-in after worktree removal

* fix(coding-agents): resolve worktree approval through one cascade

Review follow-up on the worktree opt-in inheritance.

`lookupDirectories` walked its own copy of the candidate cascade that
`gitProjectName` already had, and the two disagreed: bank identity
consulted the session root, approval and mapping did not. A Codex
session in a removed worktree therefore fell off its mapped bank, while
the same session under Claude Code kept it via CLAUDE_PROJECT_DIR — the
one harness that exports a project root.

Hoist the cascade into `mainWorktreeRoot` and resolve both through it.
`deriveBankId` already takes the session root and now passes it down
instead of dropping it on the mapPathToBank path.

* docs(coding-agents): sync the generated docs page with the README

---------

Co-authored-by: Altay <altay@hey.com>
2026-08-19 15:40:35 +02:00
Nicolò Boschi 7f7d2a0a38 chore(deps): close every open medium/high Dependabot alert (#3624)
* chore(deps): sweep open medium/high Dependabot alerts across all lockfiles

* chore(deps): bump @hey-api/openapi-ts to 0.97.3 and regenerate the TS client

* docs(scripts): note that the Deno client patch anchor tracks the generator version
2026-08-19 15:21:45 +02:00
Nicolò Boschi 30943cba13 fix(coding-agents): recapture an initiative when the plan changes (#3616) (#3617)
hindsight_capture_initiative was described as "call it ONCE, EARLY" —
before any code is written. Real feature work keeps moving after that
point, and nothing told the agent to capture again, so the only memory
carrying `relatedPageId:<pageId>` was the opening plan. Mid-work pivots
never reached the initiative page, and that stale marker stayed its most
specifically tagged source.

The update path already exists: `relates_to_page_id` skips the page
create and accrues another marker onto the existing page. It was just
described as "attach to an existing initiative", which agents don't read
as "the plan changed".

Keep the early-capture trigger and add the recapture one across all
three agent-facing surfaces, so they cannot drift:

- the MCP tool description (knowledge-tools.ts)
- TOOL_GUIDE, injected at SessionStart and on the roster refresh
- skill/SKILL.md

The marker verb for a recapture also moves from "Enhancement to an
existing initiative" to "Update to an existing initiative" — that string
is what the page synthesis reads, and a scope pivot is not an addition.

Regression tests on all three surfaces assert the recapture trigger is
present and "ONCE, EARLY" is gone.
2026-08-19 14:43:31 +02:00
Nicolò Boschi cbbbb5c876 fix(coding-agents): name the calling harness in every MCP registration (#3612)
* fix(coding-agents): name the calling harness in every MCP registration

Claude Code and Codex launch the same dist/mcp-server.js, and only
HINDSIGHT_MCP_HARNESS tells that server which one is calling. Four
installers never set it — codex, cursor-cli, copilot-cli and grok-build —
so their sessions fell back to "claude-code" for both the retain stamp
and bank derivation: a Codex hindsight_ingest_document landed tagged
harness:claude-code, indistinguishable from Claude Code's own writes.

Every registration now names its own harness (claude-code included, so it
no longer depends on the fallback). Codex's install also REPLACES an
existing [mcp_servers.hindsight] block instead of skipping when one is
present — appending only when absent made it install-once-only, so the
harness-less block could never be repaired by re-running the installer,
which is the upgrade path for anyone already hitting this.

The guard sweeps INSTALLERS and greps whatever the install actually wrote
for mcp-server.js: the harness that forgets is by construction the one
nobody wrote a test for.

Fixes #3603

* fix(coding-agents): require HINDSIGHT_MCP_HARNESS instead of guessing claude-code

The fallback read as a safe convenience and was not. Every host launches
the same mcp-server.js, so a registration that named no harness was
silently served as Claude Code — which is what made #3603 invisible: the
mis-stamped documents looked exactly like Claude Code's own. A wrong
harness corrupts stored data (the harness:<id> stamp and the bank the
session resolves); refusing to start is recoverable by re-running the
installer, and the error says so.

That turned up a second registration site the installer sweep could not
see: the codebase survey builds INLINE MCP recipes for its claude-code
and codex spawns (core/survey.ts), and neither named a harness — so a
Codex-run survey already filed its findings as harness:claude-code in
Claude Code's bank, and would now fail to start at all. Both recipes name
their agent.

Guarded by a source sweep modelled on the daemon-parity test: a module
that points at mcp-server.js is registering it and must assign the
variable. It matches an assignment rather than a mention on purpose — a
bare-name search is satisfied by the comments this fix added next to each
registration, which is exactly how survey.ts would have slipped through
again.
2026-08-19 14:08:54 +02:00
Nicolò Boschi 188eaa3dc7 fix(coding-agents): honor retainSessions in the hook harnesses (#3596) (#3607)
* fix(coding-agents): honor retainSessions in the hook harnesses (#3596)

`retainSessions` was parsed, defaulted, env-mapped and accepted as a known
config key, but only `RuntimeCore` (opencode, Kilo, Cline, Prime Agent, dsh)
ever read it. The shared Stop-hook flow behind every hook harness — claude-code,
codex, cursor-cli, copilot-cli, devin-cli, grok-build, antigravity-cli — checked
`disabled` twice and `retainSessions` never, so `retainSessions: false` (global,
per-harness or in a `banks.<id>` section) wrote the transcript back anyway. The
comment claimed this was deliberate while the docs sold the flag as a general
write-back opt-out, including a per-bank example.

Gate the write-back in `runRetainHook`, after `applyBankConfig` so a bank
section can flip it either way, and before `ensureDaemon` — a session that
writes nothing has no reason to bring a server up. A `retain_disabled` diag
record replaces the `retain_ok` that used to appear, so the opt-out is
verifiable in the diagnostic log.

`deepen`'s conversation-history import is the same door one session later: it
reads the harness's own history files and files them as `chat:<id>`. Honoring
the flag in only one of the two places would have left the opt-out cosmetic, so
it skips the import too. Git ingest, seeding, knowledge pages, recall and the
memory tools are all untouched — that separation is what distinguishes this flag
from the `disabled` kill switch.

Tests: four end-to-end `runRetainHook` cases (default writes; global false
writes nothing and builds no client; a bank override opts one repo out; a bank
override re-enables under a global opt-out), two of which fail against the
pre-fix code. Plus a family-wide structural guard in the shape of
`daemon.test.ts`'s "every harness entrypoint reaches a daemon": every module
calling `retainLiveSession` or `ingestChats` must consult the flag. The path
that forgot is by definition the one with no test, so the guard is asserted over
the whole family rather than per-harness.

* chore(docs): re-sync the coding-agents page after the README reflow
2026-08-19 11:51:25 +02:00
Nicolò Boschi 39de3974ea fix(coding-agents): keep a long-lived host's credential live, and say which one it used (#3600) (#3606)
* fix(coding-agents): keep a long-lived host's credential live, and say which one it used (#3600)

`HindsightClient` copied `apiToken` at construction and never re-read it, so a
host that outlives its credential — dsh, Cline, Kilo, Prime Agent, opencode,
the MCP server — kept signing with a key the operator had already replaced.
Enabling auth or rotating the key mid-session 401'd every call until the whole
host restarted, while `hindsight_diagnose` re-read the file and reported the
situation as healthy. The one-shot hook binaries were immune, which is why the
same machine showed working hooks alongside dead in-session tools.

The credential is now resolved through a provider on a 401 and the request
replayed once, but only if the re-resolved token actually CHANGED — a genuinely
wrong key still surfaces as one 401 rather than doubling every failing request.
The happy path never touches the filesystem.

All three fetch paths go through one signing helper. `reflect` and the drain
poll fetched directly, so a recovery wired into `req()` alone would have left
them failing forever.

Both #3600 and the two drifts below come from the same shape: five hosts each
carried their own copy of loadConfig -> deriveBankId -> applyBankConfig ->
new HindsightClient. So the fix is one shared builder (core/host-client.ts)
rather than a sixth line pasted into each. Hoisting it fixes two settings that
had already gone missing that way:

- dsh and Prime Agent never passed `maxParallelRetains`, so both silently
  ignored it and always used the default 10.
- dsh never passed the directory to `applyBankConfig`, so `optInOnly` was not
  enforced there at all: an unapproved repo still got a bank.

`hindsight_diagnose` now reports the credential IN USE next to the one on disk
(booleans only, never the value), resolved through the same pipeline the host
used — including a per-bank `banks.<id>.apiToken`, which a bare loadConfig()
comparison would have reported as a permanent false mismatch. Without this the
drift stays invisible to the one tool whose purpose is to explain it.

A 401 also now says whether a credential was even sent. The server answers
identically for "no key" and "wrong key"; only the client knows which it was.

Behaviour change worth naming: `disabled: true` now wins uniformly. It already
did for every host except the MCP server, which applied the `banks.<id>`
section first and so could be re-enabled per bank; `optInOnly`/`optInPaths` is
the supported way to run memory in only some projects. Resolution also stops
before bank derivation when disabled, since that shells out to git and the
disabled path exists to be a zero-overhead baseline.

Reported with a verified local patch and a full root-cause analysis by
@allenliang2022 in #3600; this implements that approach.

* docs(coding-agents): say when a config change takes effect, and that the token is the exception

Nothing in the README or the skill said when an edit to
~/.hindsight/coding-agent.json actually applies — and the answer differs per
host: a hook harness re-reads the file on every invocation and picks a change
up on the next prompt, a persistent plugin holds it for the life of the agent
process, and the MCP server for the session.

That gap got worse, not better, with the credential fix: the apiToken row now
says it is picked up without a restart, which reads as "config is live" unless
the rule it is an exception to is written down somewhere.
2026-08-19 11:40:41 +02:00
Nicolò Boschi e11a59ff64 fix(coding-agents): timestamp the aggregated git-log document (#3602) (#3605)
`ingestGitLog` retained the commit-message history with no `timestamp`, so retain
stamped "now", the extraction prompt got `Event Date: Unknown`, and every fact
extracted from those messages landed with a null occurred_start/occurred_end —
invisible to temporal search and neutral for recency scoring.

Anchor the document on the newest commit it actually contains (`git log -n 1
--no-merges --format=%aI`, matching gitLogText's traversal, so a merge HEAD does
not misdate it). Null on an empty repo/non-repo, in which case the timestamp is
omitted as before.
2026-08-19 11:01:22 +02:00
Nicolò Boschi 0de91b8b73 fix(coding-agents): let hindsight_reflect wait as long as it is configured to (#3590) (#3592)
The `hindsight_reflect` MCP tool aborted every call at a hardcoded 120s, no
matter what `reflectTimeoutMs` was set to: the handler passed no `timeoutMs`,
so `HindsightClient.reflect()` fell back to its own 120s default. On a
populated bank, `budget: "high"` synthesis routinely runs longer than that —
the identical direct API call succeeded — so the tool was unusable and the
config field was dead.

Both paths that build the tools dropped the setting, not just the one filed:
`selectTools()` (MCP server) and `RuntimeCore.toolSpecs()` (the persistent
plugin harnesses — opencode, Kilo, Cline, dsh, Prime Agent).

The tool's window is now its own knob, `reflectToolTimeoutMs`, defaulting to
330s — above the server's own reflect wall timeout (300s), so the server
decides when to give up rather than an arbitrary client deadline. It inherits
an explicitly raised `reflectTimeoutMs` (the field users already reach for),
but a short one never lowers it: that value bounds an automatic hook which
must fit the host's 25s window, not a call the agent is waiting on.

`reflectBudget` makes the hardcoded `budget: "high"` configurable too, for
large banks where high-budget synthesis exceeds the server's wall timeout.

To stop this recurring, `reflect()`'s `timeoutMs` is now required — the right
deadline differs by an order of magnitude between the hook and the tool, so
there is no sensible default to fall back to silently.
2026-08-19 10:47:31 +02:00
Nicolò Boschi 6692e38c80 fix(api): paginate the bank list (#3586)
* fix(api): paginate the bank list

GET /v1/default/banks returned every bank in the system: no limit, no
offset, and a query with no LIMIT clause. Beyond the unbounded payload, the
per-bank work — config resolution and a live store count for banks whose
memories live outside SQL — ran for every bank rather than the ones being
shown.

The endpoint now takes limit/offset (defaults 100/0, matching
list_documents) plus a `q` substring filter on bank id and name, and
returns total/limit/offset alongside `banks`. Paging happens after
filter_bank_list rather than in SQL: that extension hook can drop any bank,
so a SQL page would hand back short pages and a total counting banks the
caller can't see.

Consumers page instead of taking the first 100: the control-plane bank
selector scrolls infinitely and searches server-side, the CLI walks every
page, and the Zapier bank dropdown became canPaginate.

* fix(api): bound the bank-list probes and keep the selected bank's name

Follow-ups from reviewing the pagination change:
- the control-plane health probe and the Zapier credential test only need to
  know the endpoint answers, so they ask for limit=1 instead of a default page
- the header showed the raw bank id whenever the selected bank sat past the
  first page, so its name is fetched directly
- limit/offset are clamped in the engine: the page is a Python slice, and the
  MCP tool takes both straight from a model with no HTTP-layer validation

* docs(mcp): document list_banks query/limit/offset

* fix(control-plane): make the bank selector actually page and report empty searches

Verified against a 130-bank instance: the infinite scroll never fired. The
observer effect read listRef.current/sentinelRef.current on the commit that
flips the popover open, but Radix mounts the content in a portal afterwards, so
both refs were null and nothing re-ran the effect — the selector sat on its
first 50 banks forever. Tracking the nodes as state through callback refs
re-runs the effect when they attach; paging now walks offset 0/50/100 and stops
at the total.

An empty result also read "No memory banks yet." after a search that simply
matched nothing, so searches get their own message.

* feat(control-plane): smooth the bank selector as pages land and searches narrow

The list is paged and searched server-side now, so rows appear and vanish in
batches — every page landed as a hard 50-row pop, and a search that narrowed to
one bank snapped the popover shut from 300px.

- rows fade and lift in, staggered within their page and capped so the tail of a
  50-row page doesn't crawl; only rows that actually mount animate, so appending
  page 2 leaves page 1 still
- the list height follows cmdk's --cmdk-list-height, easing down to the filtered
  set instead of jumping
- the previous results hold their place and dim while the next set is in flight,
  rather than blanking on every keystroke

The animations are defined in globals.css next to the existing logo keyframes:
tailwindcss-animate is a Tailwind v3 plugin declared in tailwind.config.ts, but
this app runs Tailwind v4 with the CSS-first config, so `animate-in` and friends
compile to nothing here.

* refactor(control-plane): tidy the bank row className and import
2026-08-18 17:27:27 +02:00
Nicolò Boschi 27e4b188d7 fix(coding-agents): consolidate one set of observations per bank (#3575)
Every document this integration writes carries provenance tags (`source:chat`,
`harness:<id>`, `knowledge:<kind>`, anything from `retainTags`). Consolidation's
default `combined` scoping groups observations by a memory's WHOLE tag set, so
those tags become a consolidation boundary: work one repo with two agents and
the `harness:<id>` tag alone yields two parallel sets of beliefs that never
merge, each blind to the other, at double the consolidation cost (#3564).

Retain with `observation_scopes: "shared"` instead — one global scope per bank,
which is what a bank already is: one project's memory. The tags stay on the
facts, so recall filtering, the documents-list filter and each document's agent
logo are unaffected.

New `observationScopes` config field (default `"shared"`) sets it, per bank via
`banks.<id>` like any behavioral field, or `HINDSIGHT_OBSERVATION_SCOPES` for
the scalar modes. `"combined"` restores the previous behaviour.
2026-08-18 12:01:50 +02:00