mirror of
https://github.com/vectorize-io/hindsight.git
synced 2026-09-14 19:31:49 +08:00
c61c4e7d7d4296d9fa594bc2054419564d538672
593 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c61c4e7d7d | release(coding-agents): v0.5.1 | ||
|
|
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. |
||
|
|
208b8729b6 | release(coding-agents): v0.5.0 | ||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
173ed6a93c | fix(coding-agents): annotate MCP tool safety (#3818) | ||
|
|
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. |
||
|
|
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 |
||
|
|
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>
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
3a4a100075 |
release(coding-agents): v0.4.3
Claude-Session: https://claude.ai/code/session_01AHvpDBNP3CXfVL88WnazmC |
||
|
|
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 |
||
|
|
ea9b2fecfb | release(coding-agents): v0.4.2 | ||
|
|
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> |
||
|
|
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>
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
0429060be9 | fix(coding-agents): hide Windows child processes (#3736) | ||
|
|
1cbd77d2b8 | docs(coding-agents): add Devin CLI to harness list in README intro (#3721) (#3738) | ||
|
|
c30a3aa8da | release(openclaw): v0.11.1 | ||
|
|
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. |
||
|
|
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. |
||
|
|
c5e3feed4c | release(coding-agents): v0.4.1 | ||
|
|
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. |
||
|
|
f626cc8907 | release(openclaw): v0.11.0 | ||
|
|
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> |
||
|
|
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>
|
||
|
|
99f408fc4a | release(coding-agents): v0.4.0 | ||
|
|
1e889bd1c1 |
fix(coding-agents): tolerate empty tool discovery (#3626)
Co-authored-by: Altay <altay@hey.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
