Releases: KhalilYamber/yammory-system
Release list
v0.8.0
[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 throughaction=auto-tidy, and exits. Nothing appears in any conversation and nothing is added to your context — the drawer shows one status line (pending→running→done · merged N group(s) · batch <id>, orfailedwith a Retry button) and the existing batch block keeps the per-batch rollback button. Granular policysource:tidy-autois 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: falserestores 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, thetidy-runaudit 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 nostartedrow 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 aftertidy.timeoutMs, reporting "timeout" rather than the exit code the kill produces. It resolves its executor instead of guessing a name (a configuredtidy.exec, else the host's own launcher, elsedsh.cmd/dshon PATH). Zero DSH dependency,spawninjectable.Config.tidy—enabled/profile/exec/timeoutMs(the first three are also in the settings card;taskis config-only and overrides the built-in instruction text).execempty means "resolve the executor rather than guess a name" (a configured value wins, then the host's own launcher, thendsh.cmd/dshon 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:
.cmdcannot be spawned directly.node:child_processrefuses batch files (spawn EINVAL) and the default executor isdsh.cmd, so that path went through the shell with the executable and the task text each double-quoted (a single command string pluswindowsVerbatimArguments). A task text containing a double quote is refused loudly; the built-in instruction uses backticks around action names for exactly that reason.detached: truesilently drops the child's output. Measured on Windows: the child exits 0 and the log stays empty; passing the log file's fd tostdio, 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 /Fon Windows, the process group elsewhere) so the shell layer does not leave an orphan behind.- A batch file must be CRLF. An LF
.cmdfails with a baffling'.cmd' is not recognized as an internal or external command, becausecmd.exereads the file in one line..gitattributesnow pins*.cmd/*.batto CRLF, since the repo otherwise leaves line endings to each machine'score.autocrlf. startedandfailedlanding in the same millisecond read as "still running". The state machine checkedrunningbeforefailed; 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 newerstartedrow than anyfailedone, 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 commandlogs and a Retry button nothing could ever satisfy. The executor is now resolved instead of guessed — an explicitly configuredtidy.execwins, then the host's own launcher (the CLI script in the host's ownargv, run by the host's own node: the same DSH, same code, same profile semantics), thendsh.cmd/dsh.exe/dshon PATH — and a full miss fails before spawning anything, naming the three paths tried and the setting that overrides them. The newbootstrapargument rides the array (no-shell) spawn form only and is refused for a.cmd/.batlauncher. - 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
consolidationrow is the receipt) and failed for a round that lands none: when the model answersNOTHINGthere 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 owntidy-run/exitedrow 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.
buildTidyRunStatereported the newest batch in the store whenever the state wasdone, 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'sstartedrow 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
[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 (theusertrack) and Experience (theagenttrack) — 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 carriesfacets,categoriesandprofilefromlib/constants.mjsandservice.listProfiles(), so the client keeps a single source of truth — and stays zero-build (still hand-writtenreact.createElement, still no bundler, no new dependency). Panel read-only discipline unchanged. yammory-experienceskill (source inskills/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 theagenttrack 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 intags(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-observegains 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.
normalizeObservationEntrieshard-codesuser/user-globalandsource: observation, so the observation round cannot write into the experience world even when a caller passestrack/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-stoppingcheck records oneobserve-dueaudit 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 freshobservedrow clears it, so the reminder means "the round did not run". (2) A second scheduled task (\DSH-Memory-Observe, weekly on Sunday) wakes a headlessdshsession 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 missingTSX_TSCONFIG_PATH(tsx resolvestsconfigfrom cwd, and the workspace has none) breaks workspace-internalpathsimports 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/messagewithsource.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 aSCHEDULED_ROUND_MARKER(【无人值守轮】) andextractHumanMessagesdrops that prefix whole, counting it into theinjectedbill 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-tidyaction 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 gradeauto— 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 staysreviewand 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 PLANshowsUSING INDEX).batchReportrebuilds 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-tidyis the write surface a headlessdshsession 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 ownheadlessprofile. Granular write policy (above) is what grants that one path its permission, and two environment traps cost real time on the way (a.cmdwrapper must be ASCII-only where cmd.exe reads it in the console codepage, and--import tsx/esmmust resolve from the checkout directory). Stopping it is either flipping that policy key toask/offor 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
agenttrack 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. TheS4b-6test 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.
#compensateand thedemoteEntriesprimitive 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-autowasoffused to logwritePolicy 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/scopekey > 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.ymlused to run the full gate chain and thennpm publish --provenanceon every tag, so cutting a tag was an irreversible public action taken by a side effect. Publishing is now a separatepublish-npm.ymlgated behindworkflow_dispatch: someone presses the button, and can name the ref (tag, branch or SHA), which is what lets a...
v0.6.0
[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 sharesidebar.footer.action, whose container (.footerActions) is adisplay: flexrow: the row's height is set by its tallest item, and flex's defaultalign-items: stretchonly stretches items that carry no explicit height. Ours was a 42px, full-width row copied fromCordisPanel; the W button isheight: 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 (.panelListuses 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 atposition: fixed; right: 16px; bottom: 56px— the same patch of screendsh-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 bypackages/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'sCordisPanel): a 42px transparent row,border-radius: 12px, hover on--dsw-alias-interactive-bg-hover, the glyph from the officialIconDatabaseOutline16. Wide sidebar = icon plus label; the 56px rail = icon only, since the slot hands its occupant awideprop. The floating button stays as the degraded path: the seat is decided by whether the inject factory actually ran — the host'sslots.injectwaits for an undeclared slot instead of throwing, so atry/catchalone 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. OnefloatingSeatgate 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 fromapplyinstead of frominstallPanel, and the label loses its emoji (🧠 记忆 → 记忆) because the official icon now carries the glyph.panel.enabledkeeps 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)ortag-only hit discountassumes a reader who already knows what those knobs do. Each field whose path has an entry in the newFIELD_NOTESmap 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-expandedfollows the toggle). The button is the officialIconQuestionOutline14from@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 bothpreventDefaultandstopPropagationbecause 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-platformpanel below the hint line.FIELD_NOTESholds all 30 paths with the English and Chinese text and one note text sitting adjacent, which is what makes them reviewable side by side againstFIELD_SPECSabove. 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 officialSwitch(with the field name as its accessible label),Input, aPillgroup for the choice fields (the current value is theactivepill), andButtonfor reset/discard/save (save is the onlyprimary). 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-rowborder-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 officialTag/Buttonatoms 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 samescope.settop-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 anover the linewarning 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 goghost, 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_recallused to rank hits by relevance with recall count and freshness as tie-breakers only; it now scoresrelevance × (1 + heat·heatFactor) × (1 + freshness·freshnessFactor). Heat decays: the factor isrecalls (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 everyfreshnessHalfLifeDays. Two more matching-face changes: a token that hits onlyentry.tagsnow scorestagDiscountinstead of nothing (a body hit still scores1), and a missinglastRecalled/updatedAtscores zero bonus rather than full — treating "no timestamp" as "newest" would have inverted the old ordering. All six values riderecall.weightinginConfig, editable live from the DSH settings card (0.3 / 10 / 14 / 0.2 / 30 / 0.5by 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-writtenclient/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...