Skip to content

Releases: KhalilYamber/yammory-system

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 22 Sep 06:52

[0.8.0] - 2026-09-22

Added

  • "Tidy the whole library" now actually runs. The panel button used to register a marker and wait for the next session to notice it, which meant the tidy only happened if you happened to open a conversation afterwards. It now starts a headless session (dsh --profile headless "<task>") at the moment you click: the model in that session reads the tidy plan, merges the literal-duplicate batches through action=auto-tidy, and exits. Nothing appears in any conversation and nothing is added to your context — the drawer shows one status line (pendingrunningdone · merged N group(s) · batch <id>, or failed with a Retry button) and the existing batch block keeps the per-batch rollback button. Granular policy source:tidy-auto is what lets that one path write without a human; every batch still lands behind the same approval gate, carries a batch id, and can be rolled back whole. tidy.enabled: false restores the old queue-only behaviour.
  • Three states are derived, not stored. buildTidyRunState (pure, lib/consolidate.mjs) computes idle / pending / running / done / failed from the tidy marker, the tidy-run audit rows and the batch ledger — no new table, no new column, and no fourth state that could disagree with them. A marker older than the wait window with no started row reads as failed ("nobody picked it up"), not as "never requested", because the marker is evidence that a request exists.
  • lib/spawn.mjs — the background executor controller. Builds the command line (and refuses a profile name, task text or dash-prefixed argument that would not survive the launcher), prunes its own log directory, spawns detached with stdout/stderr into a per-run log file, enforces a single-flight lock so repeated clicks never stack processes, and kills the round after tidy.timeoutMs, reporting "timeout" rather than the exit code the kill produces. It resolves its executor instead of guessing a name (a configured tidy.exec, else the host's own launcher, else dsh.cmd / dsh on PATH). Zero DSH dependency, spawn injectable.
  • Config.tidyenabled / profile / exec / timeoutMs (the first three are also in the settings card; task is config-only and overrides the built-in instruction text). exec empty means "resolve the executor rather than guess a name" (a configured value wins, then the host's own launcher, then dsh.cmd / dsh on PATH); the launcher's own working directory becomes the round's working directory, which is what decides which workspace's memory it can see — a round covers one workspace by design, exactly like the unattended observation round.

Fixed

  • Four Windows-only defects found by running the real thing, not by reading the code. Each one is pinned by a test and by a line in the source explaining why the obvious version does not work:
    • .cmd cannot be spawned directly. node:child_process refuses batch files (spawn EINVAL) and the default executor is dsh.cmd, so that path went through the shell with the executable and the task text each double-quoted (a single command string plus windowsVerbatimArguments). A task text containing a double quote is refused loudly; the built-in instruction uses backticks around action names for exactly that reason.
    • detached: true silently drops the child's output. Measured on Windows: the child exits 0 and the log stays empty; passing the log file's fd to stdio, and piping instead, both lose the bytes. So the round is not detached (the parent is the long-lived host anyway) and logs through pipes; the timeout collects the process tree (taskkill /T /F on Windows, the process group elsewhere) so the shell layer does not leave an orphan behind.
    • A batch file must be CRLF. An LF .cmd fails with a baffling '.cmd' is not recognized as an internal or external command, because cmd.exe reads the file in one line. .gitattributes now pins *.cmd / *.bat to CRLF, since the repo otherwise leaves line endings to each machine's core.autocrlf.
    • started and failed landing in the same millisecond read as "still running". The state machine checked running before failed; a session that spawns and dies instantly (a bad command line, a stub executor) then left the panel spinning forever. Failure now outranks "in progress" — a genuinely running round has a newer started row than any failed one, so nothing else changes.
  • A host that runs DSH from a checkout could never start the round at all. The executor defaulted to the bare name dsh.cmd / dsh, which exists only when the CLI is installed globally; on this machine the host is started as "D:\NodeJS\node.exe" apps\cli\lib\bin.js web --no-open, so eight consecutive clicks produced eight identical '"dsh.cmd"' is not recognized as an internal or external command logs and a Retry button nothing could ever satisfy. The executor is now resolved instead of guessed — an explicitly configured tidy.exec wins, then the host's own launcher (the CLI script in the host's own argv, run by the host's own node: the same DSH, same code, same profile semantics), then dsh.cmd / dsh.exe / dsh on PATH — and a full miss fails before spawning anything, naming the three paths tried and the setting that overrides them. The new bootstrap argument rides the array (no-shell) spawn form only and is refused for a .cmd / .bat launcher.
  • A round that found nothing to merge left the panel spinning forever. "Success writes no row" held for a round that lands a batch (that batch's consolidation row is the receipt) and failed for a round that lands none: when the model answers NOTHING there is no receipt at all, so the panel stayed on "Tidying…" and the button stayed disabled behind it — the very first round after the executor was fixed walked straight into this. A normal exit now writes its own tidy-run / exited row and answers the pending tidy request (tidy-request / cleared (round exited)); the state machine reads that as "this round is over", and a batch receipt still outranks it when there is one.
  • The completion line could hand you a batch from an earlier round. buildTidyRunState reported the newest batch in the store whenever the state was done, without checking that the batch came from this round: a round that merged nothing displayed a stranger's batch id together with a rollback hint for it. Only a batch newer than this round's started row is reported now, and a round with no output says "nothing needed merging".

Changed

  • The red line about background processes got narrower, and the narrowing is documented. The plugin still adds no timer, no long-lived background process and no background model channel — what it may now do is start one user-triggered, one-shot headless session with a real session log, its own audit rows, a batch id and an off switch. The rule is now "no invisible background work", not "no process may ever be started", and AGENTS.md, ARCHITECTURE.md (decision 25) and the five READMEs say so in the same commit.

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 17 Sep 17:22

[0.7.0] - 2026-09-18

Added

  • The panel drawer now shows two worlds, and the memory one is a tree instead of a list. The drawer used to render every entry as one flat list grouped by track/scope, which said nothing about what the plugin actually stores. It now opens on two tabs — Memory (the user track) and Experience (the agent track) — because the two carry different knowledge with different consumption patterns: the profile has to be present every turn, a lesson only when the matching work comes up. The Memory tab renders the seven-facet polygon as a collapsible tree over the fixed facet list (empty facets included, so the architecture is visible rather than inferred), files entries without a facet under Unfiled, and keeps the knowledge-level table as its own collapsed block over the 8 categories × 31 subdomains. Clicking a node reveals its entries; nothing expands by default, which is what makes the first screen a map instead of a wall. The Experience tab buckets by topic tag under the same rule. Both tabs share one filter, one expanded-state memory (module-level, kept while the drawer is closed and reopened) and the existing budget / audit / stats / batch blocks. No new route: the entries response now carries facets, categories and profile from lib/constants.mjs and service.listProfiles(), so the client keeps a single source of truth — and stays zero-build (still hand-written react.createElement, still no bundler, no new dependency). Panel read-only discipline unchanged.
  • yammory-experience skill (source in skills/yammory-experience/) — the write discipline for the second track. One question decides which world a piece of knowledge belongs to: does it need to be present in every turn? If yes it is memory (the questionnaire and observation channels already own that side); if it only matters when a matching task comes up it is experience, and lands on the agent track as environment facts, conventions and lessons. The skill fixes the rest of the convention: one lesson per entry with its symptom and cause, the topic tag first in tags (that is what the panel buckets by and what the warm-up directory lists), A-开发相关 / B-非开发 as the classification tag, and an overdue lesson corrected by adding a new entry and demoting the old one rather than deleting it. Unlike survey / observe / tidy it may fire while working — it reads no history, burns no context and touches no existing entry, and every write still passes the approval gate. yammory-observe gains the matching boundary: observations describe the person, site lessons belong to experience.
  • The observation channel's track is now pinned by a test, not by a convention. normalizeObservationEntries hard-codes user / user-global and source: observation, so the observation round cannot write into the experience world even when a caller passes track / scope; the new case asserts both, which is what turns the split from a habit into a structural fact.
  • The observation channel can now run unattended, and it gets an alarm clock. Two halves, both away from the write path. (1) A read-only agent/turn-stopping check records one observe-due audit row once the last observation is older than seven days (hourly throttle), and the next session's warm-up block ends with a one-line reminder; a fresh observed row clears it, so the reminder means "the round did not run". (2) A second scheduled task (\DSH-Memory-Observe, weekly on Sunday) wakes a headless dsh session in the observed workspace; the model there scans a bounded slice of your own past messages, infers at most three evidence-backed entries and commits them through the same approval gate — allowed by the same granular-policy mechanism that already lets the tidy round write (source:observation: auto / ask / off, fail-closed on a miss). The plugin itself still adds no timer, process or background model channel. The observed workspace is the round's cwd, because gate one reads only sessions whose cwd equals the caller's: one round covers one workspace, and a missing TSX_TSCONFIG_PATH (tsx resolves tsconfig from cwd, and the workspace has none) breaks workspace-internal paths imports before anything else can go wrong.
  • Unattended rounds no longer feed themselves back into the corpus. The kernel records a headless task's positional argument as an ordinary user/message with source.kind === 'user', so a round's own instruction text would have passed gate two as something the user said and could have reappeared as evidence in the next slice. Round task files now open with a SCHEDULED_ROUND_MARKER (【无人值守轮】) and extractHumanMessages drops that prefix whole, counting it into the injected bill rather than hiding it — a narrow exclusion on top of the whitelist, not a replacement for it.
  • Automatic tidy can now run unattended, but only where the mechanics are unambiguous. A new auto-tidy action re-runs five hard gates over the real stored entries (same-bucket / member-count / literal identity once punctuation and whitespace are stripped / similarity / coverage) and refuses anything that does not grade auto — the caller proposes, the core decides. Because the only tier that may land without a human is the one whose members are identical after stripping punctuation and whitespace, a surface-level rewrite always stays review and keeps the interactive route: a one-character flip ("does comply" vs "does not comply") scores 0.9 similarity, above the auto line, so similarity alone must never be the gate. The write pins its own audit source (tidy-auto), which gives a granular write policy (source:tidy-auto: auto / ask / off) a real handle: a background round can be allowed to write while every other write stays behind the approval gate.
  • Every automatic round is one batch, and a batch can be rolled back whole. A batch id is minted per round and carried by the produced entry, the demoted entries and every audit row (entries.batch_id / audit.batch_id, schema v7 → v8, both indexed and used — EXPLAIN QUERY PLAN shows USING INDEX). batchReport rebuilds a whole round from its id alone (source ids, produced ids, source, session, start/end), and /memory restore --batch=<id> rolls a round back in one transaction: the produced entry is demoted, the sources come back, entries of other batches are untouched field by field.
  • The plugin now hands the background executor an entry point, and an off switch. action=auto-tidy is the write surface a headless dsh session calls; the plugin itself still adds no timer, background process or background model channel — the schedule lives in Windows Task Scheduler and the executor is DSH's own headless profile. Granular write policy (above) is what grants that one path its permission, and two environment traps cost real time on the way (a .cmd wrapper must be ASCII-only where cmd.exe reads it in the console codepage, and --import tsx/esm must resolve from the checkout directory). Stopping it is either flipping that policy key to ask / off or disabling the scheduled task — both reversible, neither touching plugin code.

Changed

  • The warm-up block's last line now names the experience topics instead of only counting them. That line exists so the model knows something is waiting on demand — workspace-layer entries and the whole agent track stay out of the frozen block — but a bare count made that knowledge unfindable in practice: nothing hinted that a lesson about, say, WSL path handling existed before the model thought to query for one. The line now appends a directory of the agent track's distinct topic tags (the first non-reserved tag of each entry, deduplicated, sorted, capped at 8) while entry texts still never enter the block, so the frozen prefix stays as small and as stable as before. The S4b-6 test keeps its count assertions and gains one for the directory.

  • The consolidation ledger and the entries now commit together, so the compensating-write machinery is gone. Previously the demotion, the merged entry and the audit rows were written inside one transaction while the summary row and the tidy-queue marker were appended afterwards, with a compensation path to undo the batch when those tail writes failed. Six adversarial review rounds kept finding failure combinations that compensation could not cover (a marker consumed by a round that did not count, a ledger row that claimed a rollback that had not happened, a back-fill that hit an unrelated row). The root cause was an unexamined operational convention — "the new UUID must be generated at insert time" — which forced anything referencing that id outside the transaction. The id is now minted by the protocol layer before the write and threaded into the insert, so entries, demotion rows, the produced row, the summary row and the queue-marker clearing land in ONE commit. #compensate and the demoteEntries primitive were deleted rather than hardened.

  • Denial audit rows now name the policy that actually decided, not the global one. A round rejected because source:tidy-auto was off used to log writePolicy auto (the global value), which made the ledger unable to tell "the policy said off" from "ask with no answerer". The core now resolves the effective policy itself (source:<name> key > track/scope key > global) and every audit label carries it; a live before/after pair exists in the audit ledger of the authoring machine.

  • Pushing a v* tag no longer publishes to npm; the tag only creates the GitHub Release page. release.yml used to run the full gate chain and then npm publish --provenance on every tag, so cutting a tag was an irreversible public action taken by a side effect. Publishing is now a separate publish-npm.yml gated behind workflow_dispatch: someone presses the button, and can name the ref (tag, branch or SHA), which is what lets a...

Read more

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 15 Sep 10:42

[0.6.0] - 2026-09-15

Added

  • The panel entry lines up with the WSL action beside it, and keeps its label. The entry and dsh-wsl-workspace's W button share sidebar.footer.action, whose container (.footerActions) is a display: flex row: the row's height is set by its tallest item, and flex's default align-items: stretch only stretches items that carry no explicit height. Ours was a 42px, full-width row copied from CordisPanel; the W button is height: 28px, so the W sat at the row's top while our icon centred inside 42px — about 7px apart, which reads as "off". The fix shares the vertical geometry only: both buttons are 28px tall, top-aligned, content-centred, flex: none (no stretching); the width follows each button's own content, so the label stays — a 28px pill (icon + 记忆, 13px/500, 6px gap, 10px padding) — and the 56px rail collapses to a 36×36 round icon button, the size the W button also takes there. The 4px leading gap comes from the sidebar's own grouping habit (.panelList uses the same value). The lesson worth keeping: aligning to a sibling means matching one line — height, top edge, centred content — rather than matching its shape.
  • The panel's entry point moved into the sidebar's own foot slot (sidebar.footer.action), so it stops fighting another plugin for the bottom-right corner. The entry used to be a floating button pinned at position: fixed; right: 16px; bottom: 56px — the same patch of screen dsh-tidewatch's peak/valley card occupies, two plugins and one corner, overlapping text. It now registers in the official additive slot (kind: 'list', scope: 'root', declared by packages/client/ui-sidebar), the seat that stacks above Settings in both sidebar widths, and its geometry is copied from that seat's existing occupant (ui-cordis's CordisPanel): a 42px transparent row, border-radius: 12px, hover on --dsw-alias-interactive-bg-hover, the glyph from the official IconDatabaseOutline16. Wide sidebar = icon plus label; the 56px rail = icon only, since the slot hands its occupant a wide prop. The floating button stays as the degraded path: the seat is decided by whether the inject factory actually ran — the host's slots.inject waits for an undeclared slot instead of throwing, so a try/catch alone would have marked an older host as seated and dropped the entry silently — and if the slot is declared only later, retireFloatingSeat() hands over by hiding the floating root it had already mounted. One floatingSeat gate keeps the floating root from being mounted (or re-shown) on the main path at all, including from the settings-page write-back. The entry no longer lives inside the panel root, so the chrome stylesheet is installed from apply instead of from installPanel, and the label loses its emoji (🧠 记忆 → 记忆) because the official icon now carries the glyph. panel.enabled keeps its meaning — show or hide the entry — with the card label, hint and field note reworded to "sidebar memory entry". Making the entry assertable needed one new capability in the test harness: renderSlot(name, props) renders a registered slot's component explicitly, nothing renders slots by default, and the settings card still renders nowhere.
  • Every settings field now carries a question mark, and clicking it explains the field in plain words. A 30-field card whose labels read heat half-life (days) or tag-only hit discount assumes a reader who already knows what those knobs do. Each field whose path has an entry in the new FIELD_NOTES map now renders a small circled question mark between the label text and its trailing badges, and clicking it unfolds a note card under the field while a second click folds it back (aria-expanded follows the toggle). The button is the official IconQuestionOutline14 from @deepseek-ai/dsh-client-ui-primitives, ghost/sm, with the field label as its accessible name (helpLabel), so nothing was hand-drawn and no SVG was copied in. Its click handler calls both preventDefault and stopPropagation because the button sits inside the field's <label>, and a label click would otherwise hand focus to the control; the note card itself is a --dsw-alias-bg-module-platform panel below the hint line. FIELD_NOTES holds all 30 paths with the English and Chinese text and one note text sitting adjacent, which is what makes them reviewable side by side against FIELD_SPECS above. Fields without a note render no question mark at all, so the affordance never lies. The control branches, FIELD_SPECS, CardForm, the parsers and the save path are untouched.
  • The settings card borrows the host's controls too, and its action row moved to where the reading ends. The last round aligned the drawer and the composer switch with ui-primitives; the card was still hand-building its widgets — a native <input type="checkbox">, a native <select>, and border-drawn buttons — so one settings page carried two visual languages. It now uses the official Switch (with the field name as its accessible label), Input, a Pill group for the choice fields (the current value is the active pill), and Button for reset/discard/save (save is the only primary). Three structural fixes ride along: group titles drop to an eyebrow weight (11px, tertiary, letter-spaced) with a hairline divider above each group, so a section no longer reads at the same level as the fields inside it; field rows lose their per-row border-top, which turns a 27-field ladder into readable groups; and the action row moves from above the fields to the bottom of the card, because a Save button you have to scroll back up to find is a button you forget to press. Every field's trailing knobs ( reload, overridden, reset) are now official Tag/Button atoms instead of three unrelated hand-made shapes. The vector-recall switch finally gets its own group and a sentence explaining that it only engages with a provider that declares itself semantic — the built-in hash provider is a placeholder, so the switch falls back to keyword recall. Nothing in the card's data path changed: the same draft/save/validate pipeline, the same field specs, the same scope.set top-field aggregation.
  • The panel drawer's information hierarchy, and the "tidy the whole library" row that used to wrap. The migration to React kept the old flex attribute on the tidy note (flex: 1 1 100%) inside a non-wrapping header row, so the note claimed the whole line, the basis-based shrink squeezed the button from about 76px to 69px, and the label broke into "整理全" / "库". The note now lives under the button in its own footer block (.mem-tidy: button on its own line, note below), and the button asks for its natural width (flex: none + white-space: nowrap). Same pass, four more corrections: group headings stop wearing the drawer-title class (small, tertiary, tighter — the hierarchy is legible again instead of one flat weight); the tidy block moves from mid-drawer to a pinned drawer footer, so the body scrolls and the action stays put; budget rows read as face name (left) + used / limit (right, tabular numerals) + a hairline bar, with an over the line warning Tag and warning-coloured numbers when usage passes the warning line — the wording now matches F3, where the limit warns and never refuses a write; and Esc closes the drawer, because a 460px overlay that covers the page should not be keyboard-trapped. The refresh and close buttons drop the duplicated tooltip (a button that says "Refresh" needs no bubble repeating it) and go ghost, so the header no longer carries three equally heavy buttons. Entry rows lose their per-row card fill in favour of a hairline separator, which is what lets the panel fit more than one thought per screen. Panel copy gains a filter-count line and splits "memory is empty" from "nothing matches the filter", so an empty result stops accusing the store of being empty; the switch's off state drops its strike-through (striking out the word "memory" reads as broken, not as off).
  • Recall now has a weighting formula, and every weight is a setting (spec 3.3, layer B · the zero-dependency half). memory_recall used to rank hits by relevance with recall count and freshness as tie-breakers only; it now scores relevance × (1 + heat·heatFactor) × (1 + freshness·freshnessFactor). Heat decays: the factor is recalls (saturating) × 2^(−days since last recall / heatHalfLifeDays), so a memory has to keep being recalled to keep its heat — without the decay, promoting recall count into the score would wire up a reinforcing loop (more recalls → higher score → more recalls) that eventually lets a few entries monopolise the recall slot. Freshness likewise halves every freshnessHalfLifeDays. Two more matching-face changes: a token that hits only entry.tags now scores tagDiscount instead of nothing (a body hit still scores 1), and a missing lastRecalled / updatedAt scores zero bonus rather than full — treating "no timestamp" as "newest" would have inverted the old ordering. All six values ride recall.weighting in Config, editable live from the DSH settings card (0.3 / 10 / 14 / 0.2 / 30 / 0.5 by default); changing them rebuilds the keyword retriever in place. Zero new dependency, no schema change, no model call.
  • The web panel now borrows the host's own controls and design tokens. The drawer and the composer switch stop shipping private look-alikes: every control comes from @deepseek-ai/dsh-client-ui-primitives (Button, Input, Tag, StateDot, Switch, Tooltip), required at runtime from the host's browser-side platform module table — still zero build, still a hand-written client/client.js, no bundler and no new dependency. Every colour in the plugin's own CSS is now a --dsw-alias-* / --dsw-static-* / --dsw-elevation-* token, so light and dark themes follow automatically; the hardcoded hex values and the `--dsw-a...
Read more