Skip to content

Sync main into drawing - #512

Merged
syzsunshine219 merged 150 commits into
drawingfrom
main
Sep 21, 2026
Merged

syzsunshine219 merged 150 commits into
drawingfrom
main

Conversation

@syzsunshine219

Copy link
Copy Markdown
Collaborator

Summary

Synchronize the current main branch into this maintenance/release branch.

  • Source: main@0074fc0bafcdcdde896f3d3be8d74a541791342c
  • Target: this PR base branch
  • The source branch is used directly as the PR head; no extra sync branch is created.
  • This PR is Draft and remains unmerged.

Validation

  • Source and target refs were checked before opening the PR.
  • Full validation should run in GitHub checks for the target branch.

memory-lee and others added 30 commits September 7, 2026 14:46
Synthetic checkpoint for safe three-way migration onto upstream computer_use; source worktree remains untouched.
Include recording, summarization, workflow extraction, replay scripts, schema, and demo fixtures.
…dinates

The recorder resolved every click by hit-testing the cursor position and then
sleeping 300ms hoping the application had rebuilt its accessibility tree. That
races the renderer, and its own comment admitted Chrome often still returned
stale geometry on the retry.

Keep the focused element continuously up to date from AXObserver notifications
so a click can be attributed immediately; the positional hit test is now only a
fallback for controls that never take focus. Coordinates are no longer emitted.

Event kinds now match the shape Codex/Skysight produces, which the diff engine
and the layered summaries will build on:

  selection.changed  new, and the highest-volume semantic signal
  keyboard.submit    new, a cheap and reliable task-boundary marker
  mouse.drag         new, carrying origin and destination elements
  mouse.context_menu new
  scroll             removed; the AX tree diff carries that state instead

Also add app.secureInput from IsSecureEventInputEnabled() so keystroke text is
suppressed while a password field owns focus, rather than relying only on the
redaction regexes as a second line of defence.

The history JSONL output shape is unchanged so summarize-history and its
fixtures keep working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex's equivalent tool has no replay: it answers questions about what the user
did, and reproducing a behavior is Computer Use's job. Splitting the two the
same way stops a retrieval result from turning into desktop control on its own,
and matches the conclusion that Computer History is searchable evidence rather
than a library of replayable templates.

Drop the `action` enum and the replay branch, add an explicit result limit, and
label every returned field as untrusted observed evidence — the event stream
records whatever appeared on screen, including text written by third parties.

The service keeps prepareReplayUserRequest for the desktop UI; only the
agent-facing surface loses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ools

Always-on capture needs a policy before it needs a daemon. This adds the model
Codex uses: two orthogonal axes rather than one list, so "record everything
except my bank" and "record nothing except my IDE" are both expressible.

  defaultApplicationBehavior  applications matching no app rule, by bundle id
  defaultURLBehavior          websites matching no URL rule, by bare domain

Allow and block rules coexist; a block rule always wins inside its own axis, so
an allow entry can never re-enable something the user excluded. A browser
record with a usable URL must pass both axes, a record without one is judged by
its application alone, and private browsing is excluded unconditionally.

The default is do-not-observe: a fresh install records only what the user has
explicitly allowed.

Adds computer_history_status, _get_settings and _update_settings. Updates
replace the whole document rather than merging, so the update tool says so and
tells the agent to read first — that is what stops it from silently dropping
rules the user never mentioned.

The recorder still takes its allowlist from CLI flags; wiring it to this policy
lands with the resident daemon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every event now carries the state of the focused window, which is only
affordable if consecutive events do not repeat the whole tree. Keep the
previous snapshot per window and emit just the added and removed nodes,
falling back to the full tree when there is nothing to diff against or when
the change is large enough that a diff would not be smaller.

Measured on a live session: the first snapshot is 6775 characters, the next
is a 44-character diff.

Snapshots are throttled to at most one every 400ms and bounded at 400 nodes,
so a busy window cannot turn the recorder into a tree-walking loop. The state
rides through to the history JSONL for the layered summaries to consume.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Observation is now continuous rather than a set of named recordings the user
starts by hand. A recorder runs in the background and its output is sliced into
ten-minute segments, each a directory holding events.jsonl and metadata.json,
with segment ids aligned to the ten-minute grid so they sort and group cleanly.

The state machine is three-valued, not two:

  running   capturing according to the observation settings
  paused    current segment kept, nothing new written to it
  stopped   recording nothing; completed segments stay searchable

Pause is a first-class state rather than a weaker stop, because "stop watching
while I do something private" must not cost the user the arc they were in the
middle of. The desktop toggle gains a matching third state.

Recording is scoped to the app: the server's stop path finalizes the open
segment, so closing the app ends observation instead of leaving a recorder
running behind the user's back.

Segments carry no title or starting URL — those described a single recording,
and a continuous stream has neither.

Also fixes two defects in the preceding commits: appendEvent destructured its
argument and silently dropped the ax field, and flushKeys still read the
pre-AXObserver application shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Looking back over a working day should not cost a re-read of the whole event
stream. A six-hour summary is now built from the ten-minute summaries covering
its window, so the cost of the wider view scales with the number of summaries
rather than the size of the stream.

That is also why the six-hour file cites the ten-minute files it reused instead
of the segments beneath them: a reader follows one level down, not all the way
down. Applications and context lines are merged without repetition, and a
six-hour file is never folded into another six-hour file.

Segment summaries are now named `<id>-10min-summary.md` so the two layers are
distinguishable on disk and the rollup can find its inputs. The rollup runs
whenever a segment is finalized, which is cheap precisely because it reuses
what is already summarized.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion policy

Making observation resident dropped the hardcoded six-app allowlist that the
old named-recording path passed, and an empty allowlist means "no filter" in
the consumer. The recorder would have observed every application, with no
policy in front of it — the exact combination the settings model exists to
prevent.

The recorder now takes --observation-settings and evaluates the policy per
event, which it has to do itself because the website axis depends on the URL
each event carries. Starting observation is refused outright when the policy
would record nothing, so a fresh install says what is missing instead of
looking like it is recording while producing empty segments.

The rule table is asserted on both sides, in observation-settings.test.ts and
in record-human-history.test.mjs, so the capture path and the agent tools
cannot drift apart on what a policy means.

Also lets the service take an observation settings path, so tests stop writing
the user's real policy file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The timeline was showing another product's records as if they were Memmy's own,
from two directions: Codex's live Skysight directory was read on every snapshot,
and 155 copies of its summaries had been dropped into Memmy's own history
directory during earlier experiments, where nothing distinguished them from a
real capture.

Codex writes `<utc>-<4 random chars>-10min-memory-summary.md`; Memmy names its
segments `<segment id>-10min-summary.md`, with no random component and no
"memory-", so the copies can be recognized by name and left out. They stay on
disk — this hides them, it does not delete anything.

Reading Codex's live directory is now opt-in rather than the default, through a
constructor flag or MEMMY_COMPUTER_HISTORY_CODEX_SYNC=1. The two tests covering
that path opt in explicitly instead of relying on the old default.

On the current machine this takes the timeline from 163 entries to the 8 Memmy
actually captured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reading Codex's Skysight directory made another product's records show up as
Memmy's own. In a real deployment the timeline should come from Memmy's own
capture, so the sync is gone rather than merely defaulted off: the
codex_synced source type, the directory reader, the opt-in flag, and the
sync-state file that existed only to hide synced entries from the timeline.

Trimming codex_synced also simplifies the paths that branched on it — replay
plans, workflow creation and deletion now have one fewer source to reason
about, and deleteHistory no longer has a branch that hides instead of deletes.

isCodexSkysightCopy stays: copies of Codex summaries are still sitting in the
history directory from earlier experiments and must not be shown as captures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The timeline showed a title and a timestamp, and the pane beside it dumped the
raw markdown including its YAML frontmatter. Worse, since observation became
resident there is no user-supplied title, so every entry read "Computer History
<segment id>" above the same templated sentence.

Each finished summary is now narrated by the model: a specific title and two or
three sentences addressed to the user, written from the segment's own evidence.
The call is fire-and-forget after the mechanical summary is already on disk, so
an unreachable model costs the better wording and never the recording, and the
prompt states that the recorded screen content is evidence rather than
instructions.

The timeline groups by day and each entry carries its own account. A day that
has a six-hour rollup is headed by it as a collapsible overview rather than
letting it compete with the ten-minute entries. The detail pane renders time,
title, description and applications as a header and drops the frontmatter from
the body.

Entries now carry description, applications and summaryWindow so the timeline
does not have to parse markdown to render itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding description, applications and summaryWindow to the shared markdown
reader put them on workflows too, because the workflow list was built by
spreading the directory entry. The desktop client parses the snapshot with a
strict schema, so every workflow failed validation and the whole page rendered
as a wall of unrecognized_keys errors instead of the timeline.

Build workflows by naming their fields. A workflow is not a summary, and
spreading meant the next field added to the reader would leak the same way.

Nothing caught this: the backend never asserted the snapshot's shape, and the
desktop test mocks the client, so it never runs the schema. Both gaps are now
covered — the workflow test fails if the spread comes back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat: integrate Computer History and Computer Use
…owlist

The default was do-not-observe, which meant a fresh install recorded nothing
until the user named applications one by one. That fails silently in the worst
way available: the UI reads "recording" while nothing is written, and the gap
surfaces days later when the history is asked for and turns out to be empty.

Codex, measured on this machine, recorded 27 distinct applications including
Dock, Finder and System Settings — it observes by default and uses the
blocklist for exceptions. Match that.

What protects the user here was never the direction of this default: it is the
blocklist, pause, the unconditional private-browsing exclusion, secure-input
suppression, no screenshots, local-only storage and the retention window. All
of those hold either way.

The service now writes the policy out on first start, so the recorder parses
one explicit document instead of inferring a policy from a missing file, and
the user has something to edit. A file that exists but does not parse stops the
start rather than falling back to a policy they did not choose.

No default blocklist ships: which applications would go on it is a judgement
better left to the user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moving recordings under `segments/` left the cleanup scanning one level too
high, so it judged the container by its own mtime. That broke retention in both
directions: while recording continued the container stayed fresh and nothing
inside was ever removed, and once recording stopped for long enough the whole
container expired at once, taking every segment with it. In practice it meant
the 48-hour window silently never applied and raw event streams accumulated
without limit.

Judge each segment directory on its own age, never the container, and leave the
open segment alone because it is still being written to. Recordings captured
before segments existed still sit at the top level and are still expired there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ript

The summarizer was a formatter: it emitted one bullet per event under
"Activity timeline" and one numbered step per event under "Reusable operation
experience". A five-minute window produced 280 lines, 175 of them "Typed 1
character(s); text redacted". That is the event stream reformatted, not a
summary, and it is nothing like what it was meant to resemble.

The model now writes the recording summary itself, from the segment's own event
stream compacted into activity arcs: consecutive events are grouped by
application and each run reported once, keeping the semantic labels — a clicked
message, a page title — because those are what let the account say what
happened. Forty keystrokes become one line rather than forty.

Recovering those labels means reading the recorder's enrichment, not just the
top-level attributes: clicks often land on an anonymous container, and the
focused control and nearest labeled descendants and ancestors are where the
name actually is.

What stays is the part that was already right: the durable facts — time range,
applications, event counts, click coverage, approved starting URL. Those are
what remain useful once raw events pass their retention window.

The scroll compression test went with the scroll events themselves, which the
AXObserver rewrite removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ent be pinned

A workflow candidate was generated for every finalized segment. A ten-minute
window boundary has nothing to do with a task boundary, so almost all of them
described a slice of an afternoon rather than anything worth repeating. Steps
are now derived from a segment's event stream when someone actually asks to
repeat something.

That makes the raw stream the thing replay depends on, so retention becomes a
real question rather than a detail. Two answers: within the window, ask and a
workflow is written from the events, and from then on it is permanent and
independent of them. Beyond it, pin the segment beforehand and its events are
exempt.

Because pinning writes a marker inside the segment, age can no longer come from
the directory's mtime — touching it would silently grant another full window.
It comes from the start time the segment recorded for itself.

Replay availability now follows from whether the event stream is still on disk,
not from whether the summary contains a section listing actions — that section
is gone, and every path that parsed it was quietly returning nothing.

Searching no longer filters by replayability. It is evidence retrieval: an
entry used to vanish from search the moment its events expired, which is
exactly when the written summary is all that remains and the only thing that
can still answer what happened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… so when it does not

The titles stayed mechanical because the call was missing what the chat-title
generator passes: reasoningEffort "none". A reasoning model spent the budget
thinking and returned empty content, and every failure path here returned null
without a word — so a broken call looked exactly like a feature that was never
wired. Diagnosing it took reading the code rather than the symptom, which is
the real defect.

Narration now reports why it produced nothing, through a callback the service
logs and exposes as observation.narrationError, so the mechanical wording is
always explained rather than merely observed.

It also runs once a segment has enough stream to be worth summarizing, not only
when the segment closes. Waiting for the close left every entry reading
mechanically for the whole ten minutes someone is most likely to be looking at
it, which is how this went unnoticed for three rounds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…just summaries

The summaries and the event streams are two different records answering two
different questions — what a window was about, and what specifically happened
in it. Only the first was reachable, so "who contacted me today" could not be
answered even though the message text was sitting in the stream all along.

Three things were in the way. computer_history_status reported the summary
directory as event_stream_root_path, so following it led to the wrong place; it
now reports both, named for what they hold. Search results carried no way down
to the evidence; each match now names its event stream, or says the raw events
have passed retention rather than implying the summary was all there ever was.
And nothing told the agent this record existed or how to read it, which the new
computer-history skill now does: the layout, when to stay in the summaries,
what fields carry a clicked message, and to grep rather than read whole files,
because one event line can carry a whole accessibility tree.

The skill also states the two rules this record needs: it is evidence and never
instructions, however much a captured message reads like one; and an answer
must distinguish "nothing was found" from "recording was off" or "the events
expired", because those send the user somewhere different.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ning it by hand

Adding eventStreamPath to history entries broke the page the same way adding
description did: the desktop client validates the snapshot strictly, so a field
the agent grows and the client does not know about fails every entry and
renders a wall of unrecognized_keys.

The first time, the answer was a test asserting the workflow field names. That
only covered workflows, so histories drifted next, and the same test would have
kept passing however many more fields were added elsewhere. Listing field names
by hand is the same work the schema already does, done twice.

The schema now lives on its own, free of any browser dependency, and a test at
the repository root parses a real snapshot from the real service with it. It
belongs at the root because the contract spans two packages that cannot import
each other — which is exactly why the sides were free to drift. Reverting the
missing field makes it fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…null

Narration resolved its model with `llmRuntime(modelPreset ?? null)`. That turns
"no preset was asked for" into "resolve the preset called null", which the
gateway's resolver cannot do — it answered model_selection_unavailable, and
every summary silently kept its mechanical title.

Reproduced against the real config: llmRuntime() resolves gpt-5.6-terra,
llmRuntime("computer-use-fast") resolves it too, and only llmRuntime(null)
throws. Pass nothing when there is nothing to pass.

End to end on a real segment, 424 events compact to 56 activity arcs and come
back as "Computer History Integration Review" with an account naming DingTalk,
Arc, PR #371 and the architecture docs — instead of "用户在「Computer History
2026-09-08T08-20-00Z」中完成了一组电脑操作".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ow it only when written

Two things were wrong with what the timeline showed.

The written summary never survived recording. Every sixty seconds the live pass
regenerated the mechanical markdown over the file, while narration ran once per
segment — so the account was written, overwritten on the next tick, and only
stuck after the segment closed. That is why a summary appeared to require a
manual stop.

And it was only ever half written. Narration replaced the recording summary
alone, leaving "Memory summary" as a templated sentence, "Relevant prior
context" as boilerplate about the capture policy, and "Important non-obvious
context" holding event counts, screen resolution and file paths — machine
bookkeeping under a heading promising insight about the user.

The model now writes the body: what the window was for, how it relates to the
windows before it, the specifics worth keeping once the raw events expire, and
the account of what happened. It is given the two preceding summaries, without
which any claim about what came before would be invention. The mechanical pass
is reduced to frontmatter and citations, and the machine facts are gone.

An entry appears in the timeline only once written, so the placeholder is never
what a reader meets. Imported and demo entries, which no model writes, are
unaffected.

Verified end to end on a real segment: four model-written sections, the prior
context genuinely continuing the earlier window, and no bookkeeping left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page was a list beside a markdown pane, and the pane showed the file:
frontmatter bookkeeping, a templated "Memory summary", citations. Only four
fields were ever meant for a reader — title, description, applications and the
account itself — so the page is now a single column of those, and the markdown
body no longer reaches it.

Three things were wrong underneath that, each of which made history appear to
lose entries.

A summary was dated by its file. Nothing writes `captured_at`, so `createdAt`
fell through to mtime, and a summary is rewritten every time the model catches
up with it. A ten-minute window from 10:10 displayed as 10:28, walked forward
on every regeneration, and sorted rollups into the middle of the segments they
cover. The window an id already names is the honest answer.

Finalizing a segment published less than was already published. The mechanical
pass overwrote the account with its placeholder and narration replaced it a few
seconds later — an entry that blinked out and returned, or never returned when
the model was unreachable. The regeneration now happens beside the standing
summary and is swapped in only once written.

And a rollup stood next to the segments it summarizes, saying the same six
hours twice. A rollup now appears only once its window has closed, at which
point it stands in for what it covers; a window whose rollup never ran keeps
its segments rather than losing them.

Applications are drawn as their real icons, read through NSWorkspace rather
than out of the bundle, because a growing share of applications ship their icon
in an asset catalog with no .icns to find.

Also: every string moved into the message catalogs, which this file had been
violating since before this change, and the stylesheet it replaced is gone —
including the two `font: inherit` declarations that were failing the prototype
alignment test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(computer-history): make the timeline a feed of what the model wrote, and catch up with main
Computer History sat among the memory views — overview, tasks, skills — but it
is not a view of what the agent remembered. It is a capture the user switches
on, closer to the cross-agent source it now sits beside.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…der src/tools

Computer History was spread over four places — a service under
entrypoints/frontend-bridge, its policy and summaries under
core/agent-runtime, its agent tools beside unrelated tools, and the scripts
that do the recording and summarizing in a workflows/ folder at the repository
root. It now lives in src/tools/computer-history/mac, with Computer Use in
src/tools/computer-use and an empty computer-history/win held open for the
Windows port.

The move was not only cosmetic. electron-builder ships dist/, and workflows/
was never in it, so the recorder and summarizer did not exist in an installed
app: the service found them by walking up to a repository checkout that a
packaged Memmy does not have. The scripts are TypeScript now and compile into
dist/ beside the service; the Swift helpers and the replay script are copied
there by the build; and everything is located relative to the module rather
than to a checkout.

Along the way:

- The summarizer and the workflow-candidate extractor are called in process
  instead of spawning node every minute. The recorder is still a process, since
  it runs for the length of a segment.
- The recorder entry point is injectable. The observation tests started the
  real one — which taps the keyboard and mouse — and never stopped it, so each
  run left recorders listening to the machine after the suite finished. They
  now use a stand-in and shut every service down.
- The three script tests had never run: nothing wired them into a test runner.
  They run under vitest now, calling the modules rather than spawning them.
- The demo fixtures and the documents beside them are removed, and with them
  the three features that only read those fixtures — installing the demo
  History, generating its canned workflow, and the CUA smoke test — none of
  which the UI still offered.
- Five values the summarizer computed and never used are gone.

`.*` in .gitignore would drop the placeholder that keeps win/ in the tree, so
that one file is whitelisted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jiachengzhen and others added 24 commits September 20, 2026 11:46
…-release-v1.1.6

Backport native npm Computer Use and macOS permission onboarding to v1.1.6
…-pr439-isolated

fix: cover PR494 computer use contracts and tests
…1.1.7

codex/sync main into release v1.1.7
@syzsunshine219
syzsunshine219 marked this pull request as ready for review September 21, 2026 11:33
@syzsunshine219
syzsunshine219 merged commit 100890c into drawing Sep 21, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants