Skip to content

PureScience v2.21.0

Choose a tag to compare

@github-actions github-actions released this 31 Oct 16:33
· 137 commits to main since this release

Maturity and Known Limitations

We label every capability the way we build it: ✅ shipped and verified on a real machine; 🚧 partly done, with the gap written next to it; 🗺️ planned. Each claim is traceable — a commit, a real-run log, or a record under docs/evidence/.

  • ✅ Shipped and machine-verified: This version made what leaves the app come back with its own receipts, and made a long session readable. The same session exported from the window, the web surface and the CLI produced a byte-identical payload — d36518b53a7d at 24 / 49 / 326 ms — with the whole-package digests differing only in exportedAt, because forging one timestamp to make three numbers agree would forge where the file came from; zip entries now carry a fixed 1980 epoch so the same content and stamp pack to the same bytes. An imported package recomputes its own claims: usage is derived from conversation.json and compared field by field (state: match, turns: 1, with turns that recorded nothing counted apart), tool readings judge the manifest's declaration against the bytes it shipped (absent:no-tool-calls stays a named absence, never "0 tool calls checked"), the verdict is written into the import record and compared there, and a remote import is refused outright (401, local-only-authority) rather than quietly allowed. A settled turn keeps its own 24 px summary row with its own token total, and the three inputs stay apart — a number, "usage not recorded", or no line at all, never a 0 — while the row still folds: on a real window two turns produced groups=2 with expanded: false then true, and four turns produced summaryRows:[24,24,24,24] in an 806 px viewport. That pair of readings took four attempts, and the reason is worth keeping: only one summary row ever appeared, and the cause was the deterministic test agent reusing the same tool-call ids every turn — the app correctly treated them as updates of the first turn's rows — so the product-side fix that shipped is the transcript merging the document's activity list with the live run's instead of letting the live run replace history. The CLI now serves an external MCP client: four read tools watched one audit line per call, then a write (archive_session) that resolves its target read-only, hangs the app's own approval card, and only then acts, refusing by name (not-approved, approval-unavailable, permission-gate-not-wired, already-archived) with the two ledgers paired by the same toolCallId — cancelling leaves the row untouched, approving writes archived and the client reads it back (2 passed, 18.3 s). The home page gained five use-case cards whose every number and quote must appear verbatim in its own evidence file — a reading drifting by a tenth of a second turns that guard red — with a copy control answered by the main process's clipboard bridge rather than the window's denied one. DOCX tables are now drawn from what the document declares instead of what the renderer assumed: the table's six edges read from tblBorders (falling back one level to tblStyle), applied by rendered geometry, a cell's own rules winning, and a table that declares nothing left untouched — a 3 pt green double frame over a 0.5 pt grey grid measured 3.21 px and 0.80 px on a real window. Along the way a real defect came out of a red lane rather than from reading code: the egress proxy's stop() waited forever when a client held its CONNECT tunnel open, which closeAllConnections does not cover. The previous version gave the wait a name and a price, and made the literature library check a retraction, follow citations both ways, and remember how far you read. Seven of the app's slowest requests were attributed from a week of its own log — 141 of 214 error/warn lines were slow IPC — before anything was changed. notebook:state re-parsed whole run documents on every read: on the real 60.1 MB file with 80 runs the hot median went 358.2 ms → 1.6 ms (cold 222.7 → 165.1) after an mtime+size cache that deliberately does not deep-copy, guarded by two tests that make the cache unable to lie. reviewer:get-for-session scanned two tables carrying only primary-key indexes behind an N+1 read and now has both indexes plus one query; notifications:get-snapshot queued its reads behind writes and no longer does; artifacts:read-preview was left alone on purpose, because an event-loop canary sampled at the same moment (261 ms against its 430 ms) accounts for most of it — rotation is reported rather than "fixed". sessions:save-session turned out to be two different slownesses: the 400 ms that dominated the samples was a database query in the post-write capture step (writeMs 3 ms, cpuShare 0), so it is skipped when no artifact version was finalised; the other half is a 60 MB document's write and rename (writeDiskMs 211, writeReplaceMs 176 ms) and is left at the I/O floor. settings:list-skills is the honest non-result: the log says 662 ms median while a full in-process call takes 50.8 ms cold and 11.0 ms hot over 67 real skills, so parallel reads and an mtime+size cache landed (11.0 → 8.1 ms, plus a skill list attribution line past 150 ms) and no target is claimed — without a baseline of the same size, "we improved it" would stand in for "it is fast enough". An event-loop stall used to end at no-cpu-in-process, which is a measurement and not a cause: every record now carries garbage-collection, fs-queue-saturated, cpu-in-process, unexplained or unmeasured, with unexplained naming the milliseconds it ruled out and unmeasured saying an instrument was missing — merging those two is how "nobody was watching" starts reading as "the machine was fine"; the GC observer and the thread-pool canary are started with the app rather than when a stall is noticed, and the 20 ms saturation threshold comes from 57 real canary samples (min 1 / median 24 / p90 180 / max 686 ms). The literature library grew three connected pieces: a retraction check reading four independent Crossref signals instead of only relation['is-retracted-by'] (which hit 0 of 12 sampled withdrawn works, which is why the old checker found nothing), with the winning signal and the check time stored on the reference and the export gate reading only what was already stored; citation discovery in both directions on live data, where one English record reports 83,700 citing works and reads 50 of them while the result says so (incomplete), four Chinese-source records return 47–50 citations each, and language: zh catches only 2 of 200 rows so it is not the criterion; and a three-state reading status (unread / reading / read) that survives a restart, filters the list, and counts what the text cites but you have not finished — where "reading" counts as unfinished, since only read is finished. Previously it made a framework selection stop being silently ignored: two hand-maintained whitelists listed three of four framework ids, so codebuddy survived the settings parser and vanished on the next read, and session persistence dropped it too. and before that a windowless run used to stall where nobody could look, and the review that was supposed to audit it never started. start --no-open has no window to click a permission card in: a real task sat after its first gated tool call with no further event for six minutes. GET /api/v1/permissions and POST /api/v1/permissions/:id/approve (purescience permissions / purescience approve) now read and answer those cards through the same narrow Task view (acp:get-state / acp:respond-permission, 11 → 13 commands — without them the route exists and answers 500), and approving is a named authority, satisfy-approval, which a generic task context cannot exercise: "who approved this" is never abstracted into "automation". Auto-review moved from the renderer to main, because a window was its only trigger — two identical CLI runs, the one with a window open reviewed, the other recorded nothing. Both paths converge through the existing already-reviewed check, so a turn is reviewed once. That first version of the trigger read the session once and so never fired at all — the end_turn event lands before the session is written (~1.8 s later, measured), which left a finished CLI turn unaudited with a clean-looking log; it now waits out the write on a bounded schedule and reports unreadable rather than dropping a turn silently (v2.15.3, measured before/after on the same CLI channel: 0 review rows → 1, verdict pass). Also in this version: a review persists outside any default five-second transaction budget, names its failures in seven families (the raw text kept as evidence), and stops at 300 tool calls per session; a client reset on a tunnel is no longer an uncaughtException; artifact finalisation waits out a durability race with a bounded retry instead of failing the turn; a Session left running by a dead process is closed as interrupted at startup, and a Session that died between "plan approved" and "no longer waiting" comes back idle instead of showing an approval that already happened; a notebook kernel now leads its own process group on POSIX, so teardown is one kill(-pgid) instead of a pid walk that misses whatever forked mid-teardown and cannot see a child launchd adopted (the old path reported reaped: true on that), with the group claim checked against the kernel's real pgid and a named log when it does not match; external-source verification no longer fails every call (an empty response.url fell through ?? into Invalid URL:, and a redirect's three outcomes now have three answers); and the review locator no longer demands a hash the model had to guess — contentHash is optional, pinned to the frozen block, with expected/received named on a mismatch. Measured during the audit that produced this batch, on the 2.15.1 candidate build: a windowless review returning flagged across five checks, 26 real approvals in the same run (PubMed / DOI / Wayback), three live fetch_source fetches (6,988 / 7,018 / 5,403 characters), and that run completed with its artifact written. Re-checked on the build of this commit: the app boots headless in 3 s and GET /api/v1/permissions answers 200 {"data":[]} with no window open — the failure that replaced was a 500 from that same route. The performance conclusion was wrong and the correction is the reading: the 118.9 s save peak and the 203 s projection panel were one process-level freeze, caught by an event-loop stall watchdog that samples from a second thread while the loop is stuck — withdrawn, not optimised around. Not done here: the read-only expression for built-in specialists.

  • ✅ Shipped and machine-verified: Batch removal is one write now: all of the ticked rows go, or none do. The previous version looped the single removal and said what that cost — a crash halfway could leave half the rows gone, and copy pruning decided row by row would depend on list order. This writes the register once and judges pruning against the sources that remain, so two sources sharing bytes clean up once and a surviving source never loses its copy. The change also changes what a refusal means, and the copy had to follow: an atomic batch that fails was not partly applied, so the sentence says the batch was not applied and nothing was removed — and the old "removed 3, 1 refused" string is deleted from all nine dictionaries, because a key describing a failure that can no longer happen is worse than no key. The armed-then-confirm step and page-scoped select-all stay: they guard against a human misclick, which has nothing to do with whether the batch is a loop. Measured: three registered, two ticked, armed, confirmed — 1 passed (7.4s), the unticked row still there. Sources can be removed in batches, without a new channel. One batch is N runs of the single removal, so copy pruning by reference and the verbatim refusal of a rejected row keep working exactly as they always did — a batch cannot become a shortcut around them. Three restraints: the first click only arms the action and a second confirms it, because this is the one control whose effect cannot be undone from the screen; select-all covers only the current page, since ticking rows nobody has looked at would decide on their behalf; and a row the store refuses stays where it is, with the reason shown verbatim. Measured: three registered, two ticked, armed and confirmed — 1 passed (5.4s), and the unticked source is still there. The trade this version made and names: a batch is not atomic, because it is N writes, so a crash halfway could leave some rows gone and some not. An atomic alternative (one write, pruning judged against what remains) was built and not adopted; the choice is recorded in the evidence file. Not done: no undo, paging is still client-side. Molecular dynamics on CPU, and an honest answer about reproducibility. GROMACS 2025.4 from conda-forge minimises crambin and runs 10 ps in seconds: 11 frames, 642 atoms, 303 K measured against a 300 K target, mean potential −2859.6 kJ/mol. The important finding is a negative one — the output is not byte-reproducible, measured at 4 threads, at -nt 1 and fully serial. So the contract differs from inverse folding: the input fingerprint (structure sha256, steps, temperature, seed, force field, engine version) is recomputable anywhere, frames and statistics are what two runs should agree on, and the trajectory digest is the identity of this artifact rather than a promise it can be regenerated. The panel says that next to the digests and the engine payload carries byteIdenticalOutput: false. Not done: vacuum rather than solvent, no trajectory viewer, no RMSD analysis, and binder design is still blocked on a CUDA host. A sweep now leaves a reading behind. Every completed sweep records when it ran and what it found — scanned, kept, removed, still-failed, bytes freed — and that record rides along with the source list the panel already fetches, so no new channel appears. After the fact you can still answer "when did this last run and what did it do"; a cleanup nobody can audit is indistinguishable from one that never happened. Three states stay apart: never swept (no record), swept and found nothing (zeros), and a record that will not parse (treated as no record, because reading a corrupt note as zero would report "nothing was cleaned" while the truth is that we do not know). The source list pages at twenty rows, with the page index clamped so a refresh that shortens the list cannot point past its end. Not done, and said here: bulk removal, a server-side page for very large stores, and sweep history beyond the most recent run. The previous version did inverse folding for real: give it a backbone and it designs sequences, with the fingerprint that lets anyone recompute them. Measured on this machine (CPU, 8 GB): crambin's 46 residues in 0.29 s, two designs scoring 0.8519 and 0.7794 at 0.50 and 0.57 recovery against the input, and the native chain the engine parsed matches the PDB. Two independent runs with the same seed produced byte-identical FASTA — that is what makes a design citable rather than merely plausible, and the result carries the seed, model, temperature and the sha256 of both the structure and the checkpoint so someone without this app can reproduce it. The engine is not bundled (6.7 MB of MIT weights, but ~740 MB of torch): the app declares it, decides availability from whether this machine has it, and lists each missing path instead of saying "not installed". Not done, and named here: no weight download, no environment provisioning, binder design and MD are still open, and designs do not flow back into the structure workbench. The previous version swept the copies nobody references — once per start, and never on a register it could not read. The previous version deleted a stored copy only when its source was removed, which left a gap: a hand-edited register, a restore from backup or a crash between save and delete could orphan a copy that nothing would ever collect. The sweep closes it, with the one guard that matters: the register is read first and an unreadable register aborts the sweep with nothing deleted — an unreadable register looks exactly like "nothing is referenced", and acting on that reading would delete the whole store. Two smaller conservatisms: only digest-shaped names are removed (a file someone dropped in that directory is not this store's copy), and a copy that will not delete is reported by its absence from the pruned list rather than claimed as cleaned. It runs asynchronously at start so a slow disk cannot hold the window, and logs one line when it does something. Still open, and named here: there is no manual trigger, the sweep matches on the digest in the filename rather than re-hashing contents, and there is still no bulk removal or pagination. The previous version added the action the store was missing: removal. A registered source could not leave before, and a URL's stored copy only ever grew. The two facts are handled apart — the row always leaves the register, while the copy is deleted only when no remaining source still names that digest, because two sources can hold the same bytes and deleting the file for one would break the other's search. The response reports which digests were pruned, so a caller never has to guess whether the disk changed. Both the model (a fifth tool, with an unknown id refused by name) and the window (a remove control on each row) go through the same store call and the same refusals. Measured on a real window: register → search → remove, 1 passed (7.6s), the row gone from the list and the panel saying how many copies were pruned. Not done, and said in the same place: no bulk removal, and copy cleanup happens by reference on removal rather than as a periodic sweep — an orphaned copy still sits in the contents directory until that sweep exists. The previous version gave the knowledge store a window of its own. Settings gains a Knowledge panel: the sources this workspace registered with their kind, location and the first bytes of each sha256; a way to register a file, a folder or a URL; and a search that returns each hit's location down to the line, its digest, and how it was matched. The window and the model's tools go through one module and one set of refusals, so what a reader sees cannot drift from what an agent gets — a refused path still says why (source-outside-roots), and three kinds of absence stay apart: a record that cannot be read is never shown as "no sources", stale is marked separately from "could not re-read", and indexed / pending sit beside the status so "nothing matched" and "some sources were not read" are different sentences. Measured on a real window: open the panel, register a real file, search TP53 — 1 passed (6.1s), with both digests on screen (the whole 85 B file and the cited line) equal to what the test process computed from disk, and the citation's :2-2 line range visible. The 26 text keys are written in all nine languages rather than filled with English. The previous version made searching a URL stop meaning re-crawling it. A URL's body is stored at registration under its own sha256, and a search answers from that copy — no network, no waiting, and a slow origin can no longer fail a query. The two paths are deliberately kept apart: the search reads the copy while the citation verifier still re-fetches, so changed content is still stale and an unreachable origin is still the third state unreadable rather than fresh. Measured against a real HTTP server that is really closed after registration: status: ok from the copy, the sha256 equal to an independent hash of the body, the verification unreadable — and while the origin was still alive, not one fetch happened during the query. A registration that predates the copy heals once on the next query, so upgrading does not silently turn existing URL sources into "unreadable"; a source with neither a copy nor a reader remains not-indexed, never "no matches". The previous version gave the knowledge store a surface the model can actually reach. Four tools — knowledge_search, knowledge_register_source, knowledge_list_sources, knowledge_render_wiki — sit on a capability that is granted per session, behind an RPC whitelist with exactly those four methods, revoked when the session ends. Every result carries the sha256 of the exact bytes it was taken from, recomputed on the read side, and a location precise to the line; an empty answer never collapses three different findings into one — ok, no-matches, and not-indexed stay apart, so a source that could not be read is never reported as nothing being there. Retrieval is lexical, and every response says so (mode: lexical, semantic-source-unconfigured), with matchedBy naming per row whether lexical, semantic or both lists produced it — a row both found reads both, never one coincidence dressed as agreement. The four tools hold separate identities in the permission catalog, so a grant can hand out search without handing out registration. Measured on a real window, not by a script asserting itself: the prompt named no tool, only a folder and a question, and the model chose register → search → render by itself; the digest it came back with for the cited line (5c47f7fa…564b) and the one the rendered document carries for the whole file (c60a2637…538d, 85 bytes) each matched an independent recomputation, and two runs of the same document are byte-identical. Also in this version: citations export as BibTeX and RIS and are persisted on the record with their time and session; a session's conversation is stored once, with the three projections rebuilt from the graph on read; chemical structures are recognised only where two mutually independent engines agree; a library id read off a figure is bound to the record actually fetched from the library, never to what a model asserted; and a vector-drawn figure becomes a candidate without a single criterion being relaxed. What this version does not do is said in the same place: a URL source is re-read at every query, so it cannot be searched offline yet — the next piece is a copy taken at registration for search while verification still goes to the network, because a cache that answers the verifier can only ever vouch for itself; and the knowledge store still has no window of its own, so registering and searching currently go through the model's tools only. The previous version made progress a matter of evidence: a session's goal moves only on a reading the app itself resolves and fingerprints — a false fingerprint is refused by name with the record untouched — and the window shows each criterion either carrying that reading or saying unmet. Beside it, a knowledge store fingerprints every source it registers (file, folder or URL; the digest is of the response body, so the same content through a redirect stays one source), marks a result stale when its bytes moved, keeps "could not read" as its own third state, and renders a folder as a byte-deterministic document whose every line cites the file and its current digest. Retrieval is lexical only, and says so on every response. multi-agent orchestration and delegated subagents · Python and R runtimes with audited notebook runs · artifact versions and lineage · reproducibility verification (isolated re-run + byte-exact per-file comparison + environment fingerprint) · the review-to-evidence loop · a nine-language interface · a signed specialist marketplace (24 Chinese-medical packages, 18 skill packs) · 31 connectors (282 tools: gnomAD, Ensembl, UCSC, STRING, PubMed, OpenAlex, IEDB, …) plus ENA run metadata that reports the bytes and MD5 each archive declares instead of downloading it · PDF text, tables, figures and captions with page-level geometry (pdf_tables, pdf_figures) · gene-set enrichment recomputed locally from the integers a service reports, from two services (g:Profiler and Enrichr) · a provider save gate whose runtime availability is kept in sync, with an actionable next step when a provider is unreachable · global search filter sets saved by name and named on the evidence line · NCBI reference sequences with SHA-256, byte count and base count · global search · .science session packages that carry the literature PDFs behind their citations · a citation-style layer (nine built-in styles plus CSL import) · private session bookmarks pinned to an exact artifact version · session forking that carries a count first and a provenance link back · a session information card · and a command line whose doctor reports measured facts while the readiness verdict stays where it belongs. This version fixes what the installed build's own record showed, and adds the first layer of illustration understanding. A read-only audit of the app users are actually running — its own last seven days: 28 sessions, 3,883 completed activities, 126 problem activities — ranked the friction by signature, and the fixes follow that order. An agent that did the work and reported a plan step completed was refused for never having bookkept a start, and a refusal about dependencies never named the one to do first; a completion reported without a start is now legal and still passes the same dependency gate, and the refusal says which step to start with. A whole tool family was silently unattached whenever a server name's hyphen met a framework that folds it to an underscore — the model held the correct name and got No such tool available — and both sides are normalized now. The app could be configured with a proxy and still fail its own 299 MB weight download with a bare fetch failed, because main-process fetches ignored both the app's proxy setting and the operating system's: the download now runs on Chromium's stack, and its failures name the cause and the next step. And a guard that refused a path outside the project described the data root as "the project folder itself", which sent the agent to the wrong place; it now lists the roots it actually enforces. Then illustration understanding, first layer: a figure is classified as chemical structure, protein structure, chart or generic schematic with every vote naming its own evidence, and evidence that is not enough refuses by name instead of letting a generic description impersonate the three classes it must not cover; what a class may derive is stated per class — a chart yields the numbers printed inside its box, each with the geometry of the line it came from, and the rest refuse by name. Deriving lands on the existing provenance surface as a third observation kind carrying an input fingerprint whose recipe is published for recomputation. One real reading contradicted the design and is recorded as such: on this machine's real PDFs every figure candidate was unclassifiable, so the next version widens the evidence instead of loosening the rules. The previous version shipped the assets the parsers will need, and made them arrive verifiable rather than trusted. The manifest that names each local parsing model ships inside the app and is read offline by the app's own validator — field by field, each refusal named (a weight url that is not https is one), because a manifest that reaches users cannot be fixed by a later commit and therefore has to be right before it ships. A model is fetched only when a reader asks for it: streamed to disk, hashed as it arrives, and kept only if those bytes match the published SHA256 — a mismatch or a failure removes the temporary file, because a half-written weight that a later run reads as "cached" is the one thing this path exists to prevent. Three digests do three jobs and are labelled as such: the publisher's md5 from the source record (a cross-check), the sha256 over the bytes we distribute (what the download verifies), and the hosting release's own server-side digest (a third party's, so nobody has to trust our transcription) — all three agree on 298,662,033 bytes, and an independent recomputation on this machine reproduces them. The window lists what the manifest carries — revision, publisher, licence, size, digest — and keeps its download button disabled until the reader ticks that they understand it pulls 299 MB over the internet; the reply is the app's own sentence (downloaded and verified, already cached, or a named refusal), and the list is then re-read, so what is on screen is a fresh reading rather than a guess. A manifest that will not validate names every problem beside its entry and shows no button at all. What this version does not do is said in the same place: fetching a model here does not make anything parse with it yet, because wiring these assets into document and figure parsing is the next piece of work. The previous version turned a method into a product: a recorded notebook run compiles into a callable skill, and a third-party public repository can be read, checked and deployed as a plugin — with the deployment atomic, the execution gated by the protection policy, and nothing written or started before the reader confirms. The previous version widened the read side, and made every new number carry what it is relative to. Pairwise LD arrives with its panel and sample size, an assay's list with the source's own total, a per-submission record with who said it, a miRNA-target table with its header verbatim and its unrecognised columns named, a file listing with declared bytes and no download, a PDB entry's residues in both numbering schemes at once, and one more provenance step that names which later run changed a value and how — under a hash you can recompute. The previous version made a notebook run reviewable before it happens, and named what a run did to your files. A cell is checked against four named rules — fs-delete, fs-overwrite, shell-exec and large-allocation, each carrying its own rule version — and a flagged cell asks on the very card the app already uses, so one decision costs one card: the card lists the rule, its version and the line of code that matched, instead of a sentence saying that something looks risky. Declining is concrete — no kernel is provisioned, no code runs and no run record is written — and what the panel then shows uses that same rule vocabulary in all nine languages (Notebook run declined: deletes files (fs-delete) — no code was executed.), where it used to show an engine class name wrapped in a transport envelope. Every run carries a risk receipt — verdict, rule-set version, code hash and the hits — persisted with the run and discarded wholesale when it fails to parse on read, because half a receipt is worse than none; and a record with no receipt reads as not assessed, never as no risk. Beside it, dependency lineage now works at the alias and in-place-mutation level across runs: each stale-input reason names its kind (name-rewrite or direct-mutation) with the mutation's line and the alias chain it came through, and the content hash a lineage row needs is computed on read, where anyone can recompute it — 1,412 of 1,412 rows on this machine's real data, against 0 before. The in-session file-change review records a real sha256 on both sides of every change: the before side is observed when the run starts, under a bounded sweep (64 MiB of entries and bytes; too-large, not-recorded and unreadable named one by one, never chained from an earlier run's after), so the panel can print a before → after pair you can recompute yourself. Real-machine readings this version: the approval card on a real window (1 passed (11.9s), with NotebookRunDeclinedError: … fs-delete in the app's own log and zero rmtree records after declining), the sentence the panel's own terminal prints (1 passed (8.8s), matching the strip character for character and carrying neither the engine error nor the IPC envelope), and a real 182 MB session workspace whose before and after hashes equal an independent recomputation — with a 6 MB weight file named too-large on both sides, not one byte of it read. Both notebook specs then ran on the certification lane's own machine as well (103 specs: 90 passed, 1 flaky on an unrelated PDF spec that passed on retry, 12 skipped), so the card and that sentence are checked twice, on two machines. The previous version added two working lines. Structures: open a PDB/mmCIF or a predicted model in a 3D view, select a residue range, and what enters the conversation is that selection's own identity plus a fingerprint you can recompute — structure id, chain, residue range, representation and derived sets, never a timestamp and never the name — so "this region" means the same thing tomorrow; a Chinese sentence ("把 145 号半胱氨酸 5 埃以内的残基标出来") becomes a real selection with the parameters it sent shown on screen, and a measurement is either taken on atoms that exist exactly once in that structure or refused by name (a residue-level distance is refused outright, because which atom pair to measure is ambiguous); a selection can be named and coloured, and the name travels in the context block but not in the fingerprint (the same region may be a binding pocket today and an active site tomorrow); exporting the view goes through the approval card — refusing writes nothing at all — and the exported image carries that region's fingerprint and name inside it, while those same text chunks are stripped from anything handed to a model; fetching a structure by PDB or UniProt id is decided by the deny-list / allow-list / protection gates, and the renderer can never name a host or a URL; and prediction-engine outputs land under predictions/<engine>/, where the view labels them with engine, output kind and licence — saying "not an experimental structure" in words. Literature: translate a PDF paragraph by paragraph with resumable checkpoints, so reopening the app never re-translates a finished paragraph — the evidence is that not a single request goes out; read it in source / translation / side-by-side, where both columns share one set of rows so there is nothing to keep in sync; per-page state tells "this page has no text layer" apart from "not translated yet", with four numbers beside it for checking; per-paragraph terminology deviations are named against the app's own MeSH-sourced table and judged only where a translation exists; clicking a translation jumps back to its own line in the source; references are kept verbatim and shown as kept rather than as missing; and the exported side-by-side file carries a recipe, the source hash and both counts in its header, so you can count its pages and paragraphs yourself. Real readings this version: a real 7-page / 39-paragraph PDF (its seventh page has no text layer, and the panel says so), one real translation (deepseek-v4-flash, 20.0 s) that stored its record and surfaced a genuine terminology deviation — the prose came back as 阿司匹林 / 随机对照试验 / 荟萃分析 while the journal name Lancet stayed in Latin — and a prediction-structure load that was refused with no network and no file because nobody could answer the approval card. The previous version asked what examination was done — 20 terms for imaging, endoscopy, pathology and lab procedures (磁共振成像, 内镜检查, 活组织检查, 心电图, 高通量测序 …), each with its English side read from MeSH and the descriptor recorded on the row. 磁共振成像 脑卒中 now goes out as magnetic resonance imaging stroke (PubMed 49,165), all:磁共振成像 as all:magnetic resonance imaging (arXiv 498,230) and 超声心动图 心力衰竭 as echocardiography heart failure (OpenAlex 284,090); before, the examination half of such a question was refused by name with nothing sent. Six rows that had landed under method moved to a new procedure kind — measurement, not study design, and the kind travels to the agent, so the label has to mean what it says — with their English sides and descriptors untouched. All 100 sourced rows still verify against NLM. The previous version asked a mechanism question in Chinese — the term table now carries 33 subject-matter terms (apoptosis, autophagy, ferroptosis, epithelial-mesenchymal transition, exosomes, ubiquitination …), each with its English side read from MeSH and the descriptor recorded on the row. 细胞凋亡 肺癌 now goes out as apoptosis lung cancer (PubMed 31,551) and all:铁死亡 as all:ferroptosis (arXiv 6); before, the mechanism half of such a question was refused by name with nothing sent. The batch's design, rows and readings are the autonomous executor's, replayed here from the recipe its own evidence note lays out after a stand-down — the two read-source cases now share one helper so a new sourced kind cannot land with weaker checks than the ones beside it, and the whole table still verifies 86 of 86 rows against NLM. The previous version made the table's provenance checkable by hand — every sourced row claims its English side was read from a MeSH descriptor, and until now that claim could only be trusted or checked manually. node scripts/verify-mesh-sources.mjs now resolves each descriptor at NLM and compares its preferred label with the term the row actually sends; it reports a row that cannot be fetched differently from a row that disagrees, names entries that are not in the table at all, and exits non-zero on any mismatch. Run over the whole table on the day it landed: 53 of 53 rows matched. It is deliberately not wired into CI — a lane that goes red because a third party is slow teaches people to ignore it. The previous version asked the design half of a Chinese clinical question — the term table now carries 30 study-design, statistics and omics terms (队列研究, 随机对照试验, 荟萃分析, 机器学习 …), each with its English side READ from MeSH rather than translated here: the MeSH descriptor is recorded on the row (mesh:D016449), so anyone can resolve it and check the term. A Chinese question is usually a design word plus a subject, and without these the design half was refused by name — 随机对照试验 阿司匹林 now goes out as randomized controlled trial aspirin (PubMed 9,072) and all:随机对照试验 as all:randomized controlled trial (arXiv 401,679), where before both were refused with nothing sent. The Chinese half is the term as Chinese medical literature writes it, and the evidence note says so per row: only the English side is an authority's. The previous version answered Chinese questions on arXiv — and it earned the rewrite policy the hard way. A single probe came back as HTTP 500 with a valid-looking Atom feed whose totalResults said 1: that 1 was an error entry counting itself, and reading it naively would have reported "one Chinese match" for a query that never ran. The app already detects that feed and raises by name, and this version verified it with a real reading rather than by reading the code. The decisive comparison then settled the policy: all:阿司匹林 returns 0 while all:aspirin returns 42, so arXiv indexes Latin text and refuses unmappable Chinese by name with nothing sent. The rewrite is done IN PLACE — arXiv's query language carries meaning in punctuation, so composing it from space-joined pieces would turn ti:阿司匹林 into ti: aspirin and move the prefix off the term it applies to. The previous version answered Chinese questions on OpenAlex — the app's Chinese term table now rewrites queries for the all-disciplines literature source, and the policy differs from PubMed's because the two sources behave differently, measured rather than assumed: PubMed indexes Latin text (term=阿司匹林 returns zero whether or not the evidence exists) while OpenAlex indexes and answers Chinese (search=阿司匹林 returns 11,867). So OpenAlex gets the rewrite where the table knows a term — the English term of record is far better covered (aspirin 443,778 against 11,867) — and searches what it does not know as typed, naming those terms in the result instead of refusing the query. Only a query made of nothing but function words is refused, by name, without sending anything. The previous version let you check a reading from the window — every reading in an artifact Version's provenance now has a button that re-issues the request it came from and reports what came back: the same bytes, different bytes (both digests are shown), or a refusal that names its reason. Re-issuing is the only thing that can turn a recorded digest into a checked one, because the digest is computed over the response bytes and the bytes are deliberately not stored. The request never carries a URL: the window sends a session and a digest, and the main process looks the reading up among the ones it recorded, so the capability is exactly "re-verify a reading this app took" and cannot be used to fetch an arbitrary URL. A request that was not a read, a URL whose credentials were stripped, and a record that predates the fields a faithful re-issue needs are all refused by name rather than re-issued approximately. The previous version said which run a reading came from — every reading in an artifact Version's provenance can now name the run that recorded it, and the one taken by the run that produced that Version is marked on screen. The id comes from the main process's own session state, never from a caller-supplied argument: run identity is not something a call gets to assert about itself. When no run was executing, the reading carries no id at all rather than a guessed one. And the window promise from the previous version is now kept only where it can be: if the Version's evidence carries no readable creation time, the panel says the readings are the whole session's instead of printing "before this version was written" over a set it never filtered. The previous version made the readings window real — the panel said "what this session read … before this version was written", and the projection listed every reading of the session regardless: a version written at 10:00 showed readings taken at 15:00 under a label that claimed a window. The window is now enforced against the version's own created_at, and readings that fall after it are counted and named on the panel instead of being silently absent — a stamp that will not parse is counted too, never quietly promoted into the list. The certification spec gains a seed on each side of the window and asserts that exactly one reading is listed, so the promise is checked on a real window as well. One correction rides along: the previous body said the panel's real-machine reading had not been taken, and it had — from the certification lane, with its own printed reading. The previous version paid for the readings chain only where it is returned — the journal is one file read per session, and three paths were paying it for answers with nowhere to put it (a reproduction verdict, a code reconstruction, the bulk card listing), so each now says it does not want it; and a session's parallel calls no longer overwrite one another — eight overlapping writes used to leave exactly one reading behind while the dropped count still read zero, a loss that looked like completeness, so writes are serialised per session. A verifier nobody called was removed with the rest of the unconsumed exports, and the shape guard it relied on now runs where the journal is read: a digest nobody could recompute is reported as unreadable rather than shown as a reading. The previous version put that chain on the screen — the artifact-Version provenance panel gains a Readings section: every reading the session recorded, with the service and tool, the request method and URL, the status, the byte count and its sha256; the published recipe it is computed under, read straight from the constant instead of retyped; and how many older readings the cap dropped. The three missing states stay apart — no journal, unreadable, and empty — so an empty list is never drawn where "nothing was read" belongs. The panel asks for the readings on that one read, and the bulk card path still pays no file read per card. **The previous version walked from an artifact Version back to the readings — provenance now lists what was read in that session: the service and tool, the request, the response byte count and its sha256, under a published recipe an independent implementation reproduced byte-for-byte here. The attribution is carried on the value — session plus time window, never per-run causality, because the connector layer knows the session and nothing finer — and a missing journal says not-recorded, an unloaded one says not-loaded, a corrupt one says unreadable, so an empty list is never read as "nothing was read". The GB/T 7714 block for electronic resources also gains the issuing body, which is the shape a Chinese evidence item actually takes ("中华医学会, 2021"); the slot opens only when the record carries a publisher, so a record without one keeps its exact bytes. The previous version named what a call leaves behind — the connector approval card now says when a call creates state on the service rather than only sending a query: the tool that registers your gene list on Enrichr's own servers is marked as such and the card prints that the entry outlives the call and cannot be removed from here. The card used to describe only how long the APPROVAL lasts ("applies to this call only"), which reads as how long the call's effect lasts. The declaring set is pinned by a test in both directions: the twenty-odd POST-only connectors here merely carry queries and are asserted to stay unmarked, so the line keeps its meaning. The previous version answered Chinese questions with evidence — a Chinese term is mapped to the language the sources index before anything is sent, and the rewrite is reported in full: what was recognised, which words were dropped, and the exact query that went out (阿司匹林 returns 80,708 matches and 阿司匹林治疗高血压 4,910, against a count of 0 for the raw Chinese, which the live service answers with an empty result whether or not the evidence exists); Chinese the table cannot map is refused BY NAME with nothing sent, so a zero-match answer can never again be read as 'no evidence'; the connector panel gains zh_medical_terms, which resolves a Chinese term the app has never seen to a citable entity and says which label or alias it matched on without collapsing ranked candidates into one answer; and every connector reading now carries a fingerprint — service, request, response bytes and sha256, under a published recipe, reproduced byte-for-byte here by an independent implementation. The previous version made a stop say only what it did — a remote host that cannot be reached gets a refusal that carries ssh's own sentence and exit status, and its job row stays running instead of being written as cancelled; a stop that does arrive takes the job's whole process group, not just the launcher, and the row reads cancelled on the way back out. A host with neither timeout nor gtimeout — stock macOS — no longer fails the job outright: the workload runs, and the host's stderr carries one named line about the wall-clock limit the app's own budget stands in for. It also adds IEDB immune epitopes, whose every record carries its assay method, MHC restriction, quantitative result and citation, whose empty results name the family that was empty and say this is not an absence of response, and whose truncation flag comes from asking for one row more, because the service reports no total. The previous version made a named environment's packages reachable from the window, and stopped a refusal from claiming an approval that never happened — the Settings Packages dialog adds to and removes from a named environment for real (a distribution installed through it is importable by that environment's own interpreter; one removed through it is gone from the inventory the dialog re-reads), with the app-managed default's inventory compared row by row before and after so a silent retarget cannot pass, and the default itself still additive-only; and a remote run refused by the execution-protection policy now reports protection_refused — saying that nothing was submitted and nobody was asked, and where to change the policy — instead of the old Approval denied, which sent readers after a grant that could not have helped. The previous version inspected what arrives from elsewhere, and let you take back what was merged, delivered or imported — an RO-Crate someone hands you is read in place and checked requirement by requirement, writing nothing and touching no byte of your project, with its payload bytes recomputed too so the checked rule set is the same one the export side applies; an imported skill can be duplicated into a skill of your own that you can edit, while the copy you imported stays as it was imported; the alias a journal merge left behind can be released, with the metrics and references that merge moved left where they are, as the window says before you act; the message centre deletes one notice or clears the whole inbox, stating that your conversations are not deleted and that it cannot be undone; the engine panel reads itself out — seven engines, each with its availability and a named reason, and no download button is offered where no published checksum exists to accept it; and a session replay step can be asked about, the answer assembled locally with the field behind every sentence cited, and the record's silence reported rather than filled in. It also re-read two gaps that had been carried as product defects: the journal-import panel in Settings does render its row-by-row outcome (the earlier reading came from a locator that matched a toolbar button by substring), and the progress detail fields do reach the renderer — both sides of the broadcast read the same message object. The previous version carried the evidence with the package, and gave an interrupted run its own name — a .science session package that lands in a fresh project now writes its citations, review findings, review conclusions and human-pinned evidence into the receiver's own tables, and the certification run compares all four dimensions row by row against the sender's source instead of asserting a hand-written 1 (the receiver is an empty project, so every landed row can only have come from the package); a notebook cell in flight when the app went away is recorded as interrupted with app-terminated — the domain rule has said from the start that this is not a failure, "the code may have been fine", and the write path now agrees, including when the process tree is killed from outside while the app is still shutting down (the crash path consults a shutdown latch, and the shutdown refuses in-flight runs immediately while still letting a queued persistence write finish); a scanned PDF says how many of its pages carry no extractable text rather than only naming the document; a screening batch names the kind of failure it hit so a reader can tell whether to re-run; a failed handoff prints which step to resume from; a script the app truncated is labelled as truncated instead of being shown, and exported to .ipynb, as if it were whole; and pdb_search_by_sequence searches the PDB from a bare sequence, without a UniProt accession. Previously it added a way in to what was already built — a managed artifact preview can open the file in its default application or reveal it in the file manager, with the opener resolving the path through the repository in the main process (anything outside artifact storage is refused by name rather than quietly opened, and a refusal is shown in the handler’s own words); a finished remote job shows what it produced, listing the featured outputs and every file left on the remote host with its own size and reason, saying how many more the payload did not list instead of showing a subset, and printing a failed harvest beside the workdir that is kept so the files can still be fetched by hand; the message-centre badge lights up on its own when a task finishes, without the bell being opened first; and three settings channels that had no handler on the Electron bus — which left the function-model panel saying it could not read its own settings, with every button inert — now answer. Previously it added index coverage you can read — global search reports how much of the corpus the incremental index holds (indexed), how much changed since (pending) and when it was last measured, with an "Index now" step that runs one bounded tick and answers with the reading it produced; on the real window a restart does not move indexed backwards, and deleting the index directory does not turn the corpus into "nothing found" — the next query rebuilds it on disk and the files are still returned (3 hits), while the coverage block keeps reporting the last measured tick instead of a reading taken mid-rebuild. Together with two values for one year printed side by side with their own sources, a batch of interface copy localised in all nine languages, and a measured contrast ratio for text on the brand colour (4.144:1 — below the 4.5 body-text floor, and pure white only reaches 4.330:1, so the gap is recorded rather than papered over). Before that it added importing an environment from an external lock file — every package verified against the md5 the lock publishes, and a single unverifiable entry builds nothing — together with named environments that can be listed, used as a notebook runtime, or removed, with a running kernel's hold on one refused by name rather than reworded. Earlier it added journal entities — an ISSN, or an exact normalised name, resolves a journal; every impact factor and CAS partition carries the year and the source it came from, with Unknown where none was imported (never a 0); two spellings of one journal merge only when you say so, and the old spelling keeps resolving; and metrics import from CSV/TSV with one named result per row. The previous version added a reason for a page that yields no table — the extractor's own measurement, with the counts and floors behind the decision, and a named throw when a shape passes every gate — together with rotation-normalised table reading (text drawn at an angle is measured in its own frame, and the page whose coordinates were rewritten says so) and a named reason on the turn path for a turn that never reached the skill selector, so the trail answers that question instead of staying silent. The version before that added per-run file evidence in three states — read, written, and not captured with a named reason, so a list with no explanation cannot be expressed — together with content-addressed storage that keeps identical bytes once and measures the space it saved off the filesystem (an in-place tidy-up decides by hash, not file size) · explicit states for the official skills catalog, a recomputable quality score on every skill row and an import check that says whether a skill's content still matches what was imported · and a real function-model probe round-trip reporting measured elapsed time and the skill it selected. Earlier it added an execution-protection matrix (four execution surfaces × the filesystem-write and network-allowlist axes, each row naming its gap and the fix path) with unprotected remote runs denied by default · global search scopes split into uploads and generated files, each scope chip carrying what that scan actually covered and showing a dash — never a 0 — for a scope that was not searched · advanced search filters (time window, ordering) that print the request's own condition string on the results, with a re-run check that reports the evidence as unchanged, changed or not found · per-purpose function models whose row names the model in force, the service it comes from and the built-in path used when it is unavailable, plus a real detection round-trip reporting measured elapsed time and token usage and a retrievable trail for every fallback · and a locally recomputable skill trigger-quality score with the missing pieces named, beside a per-reader skill availability matrix whose always-on and globally-disabled entries are shown as locks with their reason · literature and journal records that keep their own history — a replaced PDF stays in view, a reference's notes are writable and survive a restart, and a journal metric corrected by hand keeps the old value beside the new one, each with its own source and year.

  • ✅ Shipped and machine-verified: A sweep now leaves a reading behind. Every completed sweep records when it ran and what it found — scanned, kept, removed, still-failed, bytes freed — and that record rides along with the source list the panel already fetches, so no new channel appears. After the fact you can still answer "when did this last run and what did it do". Three states stay apart: never swept, swept and found nothing, and a record that will not parse. The source list pages at twenty rows with a clamped page index. The previous version did inverse folding for real.

  • 🚧 Partly done: Retrieval is lexical only in this version — no semantic source is wired, so every response carries mode: lexical and names the degradation and every row says lexicalOnly; the semantic step is left for the version that actually has an embedding source rather than pre-wired as dead code. The goals panel is read-only by design (creation and progress belong to the session's own tools, which can name a reading the app resolves). Citations are not yet persisted: what this version ships is the re-checker and the per-row marks. Windows builds are not code-signed — the installer works, but Windows warns about an unknown publisher at install time; we say so here rather than let it surprise you, and signed Windows builds wait for a certificate and a signing identity. CSL import runs a fidelity probe against each document — of eight real published styles, five pass and three are labelled drafts with the missing piece named (their author macros branch on record types we do not model yet). ENA is metadata and reachability only: no FASTQ is downloaded. The shared diff viewer covers artifact comparison and tool-edit diffs; skill updates and session-package record verification are not wired to it yet. Distribution has just started — both READMEs lead with a result the product produced and nine languages now exist — but the download site is not deployed yet and the repository is still young. Two things remain unshipped: the skill-marketplace slice (K1a) is archived, not shipped — the official skill catalog could not be reached from the build machine (the CDN metadata path returned 404, the raw host timed out) and the marketplace protocol root carries specialists only, so no consumer-less code was written and the slice waits for the version that publishes the catalog — and two of the five real-machine criteria for per-purpose models are still uncertified, and the previous version isolated exactly why: the narrow model call is only reached on a bridged (Codex) turn path. A real codex session was established for this version — through the app's own managed install inside a throwaway root (303 MB, nothing on this machine changed) — and a live turn now records for itself that it had no skill-selection bridge (bridge-unavailable, readable in the trail), which is the first of those criteria certified on the turn path. The remaining one — 'the answer on a live turn really comes from the configured vendor model' — is still uncertified, and the previous version isolated why the earlier attempt could not even reach that question: the function-model host handed to the runtime was being dropped at the composition layer, so the configured model was never consulted on a turn and no trail was written. With that fixed, a live turn on the same isolated instance records a real round trip and used-model, and the fallback path that used to claim the model had answered now records call-failed by name — but the party answering in that live reading was a stub endpoint, so 'answered by the vendor model' stays uncertified. Also open and named: the app's own detection accepts only its managed adapter, so a user-installed codex-acp on PATH is not usable in-app yet — a detected-but-unusable adapter reports Codex native executable not found. The two units this narrative previously named as not started are shipped in this version and verified on a real machine: an imported session states where it came from (imported from X / exported at Y / not verified on this machine) and stays read-only, and a data-root move that never finished is re-discovered from its marker on disk after a restart, so it can be completed or discarded without the window that started it. This version also gives a user-added MCP server its whole action surface — its detail page lists the tools the server itself advertises with per-tool permissions, the skip-approvals switch holds, a connection test separates "the server did not answer" from "the server advertises nothing", a signed-in server can be signed out, an unusable one says why on its own row, and the bulk buttons cover these servers too — beside the runtime surface: installing packages into a managed environment, binding and switching a notebook's runtime, a prep overlay whose Cancel works, and a card that says where the app-managed runtime comes from before anything is downloaded.

  • 🗺️ Planned: explicit guidance when a plan is resumed · a wider serverless GPU surface.

Rules we do not relax: no model weight is downloaded without a published SHA256; an imported style or skill without a licence is not installed; a field the record does not carry stays empty and is named — never filled in on its own; code a model reconstructed is never certified as a reproduction; timing conclusions are only drawn from the same corpus, the same driver, measured before and after (a millisecond assertion that wobbles under load is reported as unstable rather than tuned until it passes).



v2.21.0 — 2026-10-31(交付形态:包出去再回来能复算,长会话可读,外部客户端走同一张审批卡)

成熟度自陈:这一版没有新的科学计算能力,做的是交付形态——同一个会话包能从窗口、Web、CLI 三条路出去并且回来时自带复算;
长会话里每个已结束的回合留下自己的摘要行;一个外部 MCP 客户端只能通过应用自己的审批卡写入;首页的用例画廊里
每一个数字与每一句原话都必须在它自己的证据档里逐字存在;DOCX 表格按文档字面写的规则画。凡是给读数的地方都是本机实跑
(真窗口 / 真 HTTP / 真 CLI),凡是不成立的地方写在同一段后面。

一、会话包:三端对称,导入后逐字段复算(D1 / D2)

同一个会话(e2e-session-1)经三条路导出:窗口 sessions:export-package、Web GET /api/v1/sessions/:id/package、
CLI purescience package export。此前后两条根本调不到——契约目录里这两条是 ELECTRON,而 Web 面的 TaskApi.invoke
只触得到 web profile。本版把两条包通道升成 application command + LOCAL,Web/CLI 走同一条链。

路 耗时 字节 整包 SHA-256(前 12) payload 摘要(前 12)
桌面 24 ms 2274 a83ed9856bee d36518b53a7d
Web 49 ms 2273 4261ba974e32 d36518b53a7d
CLI 326 ms 2273 7b34d1935f56 d36518b53a7d
  • payload 摘要三端完全一致:payload = 除 manifest.json 外每个 entry 的 path → sha256,按路径排序后整体再哈希
    ⇒ 会话内容、证据 JSON、文件条目逐字节相同;
  • 整包 SHA 不等,且只差在一个字段:manifest.exportedAt(真实导出时刻)。为了让三个数一致去伪造同一个时间戳,
    等于伪造来源记录,所以不伪造;
  • 导出本身现在可复现:zip entry 固定 1980 纪元(第三方库默认写「现在」),同内容同时间戳两次打包字节相同。
  • 导入(反方向):CLI purescience package import web.science --project <id> --json 退出码 0、322 ms,新会话消息数 2
    与包内 manifest.counts.messages 相等,posture 为 { readOnly: true, executeAllowed: false, verificationLabel: 'source-party' },
    导入记录落 sessions/<projectId>/<sessionId>.import.json(运行守卫读的就是这一份)。
  • 未授权的远程导入被直接拒:经 remote-access 适配器授权后的请求打 POST /api/v1/sessions/package/import → 401,
    导入器一次都没被调用,并留下具名决定 local-only-authority。命令层三种拒绝也各自具名:no-destination /
    404 session-not-found / 422 session-unreadable(不返回空文件)。

D2 复算:导入结果里带 readings,usage 按 conversation.json 逐字段复算后与清单比(本轮真机 state: match,
turns: 1、turnsWithoutUsage 单独计数),toolCalls 判的是声明与包内字节相符(本轮 match + absent:no-tool-calls
——清单说「这次没调工具」,包里也确实没有那份文件,因此不出现「核对过 0 条」这种说法)。两种「一致」在界面与 CLI
上分开渲染;读数同时写进导入记录,用例断言 persistedRecord.readings === imported.readings ⇒ 结论不依赖算它的那个进程。

二、长会话可读(E1 / E2 / E3)

  • 每轮成本(E2):工具组摘要行右侧给这一轮的 token 总量(input + cache + output,量级写法与写作区计量一致)。
    三种输入分开:有 turnUsage → 数字;有 turnUsageUnavailable → 「未记录用量」;该轮没有任何读数 → 整段不出现。
    不写 0,也不写 unknown。

  • 折叠(E1):已结束且不是最新一轮的回合自己折起来(resolveActivityGroupExpansion),最新一轮与正在跑的工具保持展开,
    用户手动开关优先。真机一对读数(两轮):

    [e1-pair] groups=2
      [{expanded:"false", text:"Fetched 2 pages · 2 steps · 3ms · usage not recorded"},
       {expanded:"true",  text:"Fetched 2 pages · 2 steps · 1ms · usage not recorded"}]
    
  • 一对读数为什么取了四次:一直只渲染出一个摘要行。前三次分别怀疑渲染层投影、持久化、store 写入;第四次把探针
    放在每轮结束时读 store,才定因到确定性夹具:toolCallId 里的 process.pid 与 step.index 跨回合不变,第二轮的工具
    调用带着与第一轮完全相同的 id,应用(正确地)把它们当成第一轮那两条的更新 ⇒ 无论跑几轮都只有一轮的活动。
    真实 agent 的 id 每次不同,所以这个夹具从来没暴露过「每个回合各自留档」。修夹具后:after turn 1: 2 → after turn 2: 4,
    盘上 graphActivities=4,live tool-groups: 2。产品侧另有一处真修:转录的活动列表改为「文档 + 本轮」合并,
    不再让本轮把历史整体顶掉(mergeActivityLists)。

  • 排版(E3):正文收窄到 68ch(表格/代码/图保持满列),折叠摘要从卡片降级成 24px 一行。四个带工具调用的真实回合:

    {"viewport":806,"transcriptHeight":572,"perTurn":143,"summaryRows":[24,24,24,24],"groups":4}
    

    同一支探针也打掉了我先前的一个说法:在「只有一个摘要行」的假象下得出的「密度收益比原型小」作废——夹具修好后
    24px 折叠行对长会话是实打实的收益。未做:跨消息画一条连续的「每轮轮脊」,属结构性改动,单独一步。

三、CLI 对外 MCP:读写都走应用自己的审批卡(F1)

  • 第一片(只读):窄命令视图四个工具(list_projects / list_project_files / list_sessions / read_session),
    每次调用留一行 external mcp call 审计。
  • 第二片(写入):archive_session。写路径三步不可换序:先只读解析目标 → 再挂应用自己的审批卡 → 最后才 act;
    绝不走按 profile 自动放行的那条(外部身份一律一张卡)。拒绝码具名:not-approved / approval-unavailable /
    permission-gate-not-wired / already-archived。目标不存在时在问人之前就拒。
  • 两本账配对:permission decision(decision: 'card'、isMcp: true、tool: 'purescience_external/archive_session')
    与 external mcp call(risk: 'write'、decision: declined|approved)的 toolCallId 逐个相同。
  • 真机读数 2 passed (18.3 s):取消 ⇒ not-approved 且数据未动(紧接着 list_sessions 里该行没有 archived);
    批准 ⇒ archived: true + archivedAt,同一客户端再列表可见。
  • 一处与计划的偏离(具名):计划写「归档后 list_sessions 不再列出该会话」,实测目录通道仍返回该行,本版改为
    行上带 archived。让外部客户端看不见自己刚归档的会话,等于让它无法确认写入生效;「默认不列出」是界面视图的过滤,
    不是数据面的语义。
  • 边界:外部客户端作答需要应用发布 web-service.json(无窗口启动总会写);纯窗口启动不发布该文件 ⇒ 那时只能用窗口作答,
    这句写在工具 description 与 CLI.md 里。

四、用例画廊(H1)与 DOCX 表格保真(H2)

  • H1:首页五张卡片,每张的读数、原话、复跑命令都取自本仓已有的真机读数档;守卫测试逐字对着证据档核(把 1 passed (7.8s)
    改成 7.9s 就红),复跑命令里点到的 spec 必须在盘上存在。真机 1 passed (6.8s):点开卡片 → 面板显示读数与原话 →
    复制按钮变「已复制」(窗口的 navigator.clipboard 被拒,走的是主进程剪贴板桥,所以这行意味着桥真的答了)→ 关闭后
    焦点回到开它的那张卡。
  • H2:DOCX 预览把「表格外框」与「格内规则」折成同一套样式——只写 insideH/insideV 的表一条边都不画,外框双线
    会把内部边也画成双线,只写 tblStyle="TableGrid" 的表全部无边。本版从文档字面读每张表的六条边(表自己没写时按
    tblStyle 解析一层),按渲染出来的格子几何判断每条边属于外框还是内部,单元格自己写的边优先,表里一条规则都没写
    就一个像素也不动。对比用例 9 条全绿(真渲染器 + 真 docx 字节);真窗口读数 1 passed (7.0s):3 pt 绿色双线外框配
    0.5 pt 灰色网格,量到 3.21 px / 0.80 px。读不到文档部件时打一条具名 warn 再照常显示——这一段永远不许拖垮预览。

五、沿途修掉的真缺陷(不是顺手)

  • 出口代理 stop() 会永久卡住:客户端不松手时关出口永远不返回,closeAllConnections 也管不到 CONNECT 隧道。这是
    从一条红车道定因出来的真缺陷,修法是先收掉在飞隧道再关服务器。
  • 回合静默看门狗:运行时静默 10 分钟就把这一轮停掉并说明原因,不再让一个回合无声地挂着。
  • 运行时拒绝变成界面上可见的结果:此前拒绝可能只剩沉默。
  • 机检补「后端配置」一项:二进制存在不等于这个后端能用。
  • 探针不得写真实配置根:曾把在用应用切到另一个后端,代价是三次请求被拒、一次挂起 15 分钟。
  • purescience doctor 报发布签名状态:装得上但系统不认发布者时,给一句下一步。

六、明确没做 / 不成立(与上面同权)

  1. 签名与更新四件全部挂起:Developer ID + 公证、Authenticode、原位更新、Homebrew tap —— 用户已决定暂不花钱认证、
    商业化时再谈,本版只做了「如实报签名状态」那一半,不计入本版退出条件。
  2. A 组的发布件读数仍未取(v2.20.0 唯一未满足的退出条件):本机发布件取回 ≈20 KB/s(4 MB range 请求 200 s 未完成)、
    空闲物理页 ≈65 MB,装机与五个真会话在当前机器上不可行。具名挂着,不并入已完成。
  3. 语义检索仍是词法:每个响应带 mode: lexical,每行带 lexicalOnly,不预埋空壳语义端口。
  4. 会话包不含 activities 原文:工具活动在导出时剥成消息上的投影,所以读数只能来自导出方采到的声明,接收端复算的是
    「声明 vs 包内字节」,不是把工具重跑一遍。
  5. 夹具不产生工具调用的那一路是实机;有工具调用的那一路(D2 的行数、H2 的格数)由用例钉住,不靠推断。
  6. DOCX 只解析一层样式继承;渲染格数与文档格数对不上时记 unpairedCells 并不做逐格覆盖(只落表级规则)。
  7. 每轮轮脊未做;E1 的折叠控制只在最新一轮上取到真机读数,settled 组那一对是同一支 spec 的第二轮。

⚠️ macOS signing status: unsigned — the macOS packages in this release carry only an ad-hoc signature (no Apple Developer ID credentials are configured), so Gatekeeper warns on first launch and in-place auto-update is unavailable on macOS. Install the DMG manually (right-click → Open, or xattr -dr com.apple.quarantine "<app>").