You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(embedding): an annotation loop an agent can reproduce, locate, and react to (annotkit-op6)
A host that launches many isolated instances of one binary — an HMR session
per branch, a gallery host, an inspect window per persona — could not say
which world an instance was or where its notes belonged: there is one binary
and N launches, and install() had no environment to read.
- WORLD CONTEXT on every note. An opaque [String: String] the host registers
once and AnnotKit snapshots PER CAPTURE, so toggling dark mode between two
notes files them under two different worlds. It reaches the agent through
the markdown Context line, the JSON store, and annotation_get_pending.
- THE ENVIRONMENT IS THE CONFIGURATION SURFACE. AnnotationEnvironment (pure,
takes the env as an argument) drives install(): ANNOTKIT_NOTES_MD /
ANNOTKIT_NOTES / ANNOTKIT_EVENTS / ANNOTKIT_ROUTE / ANNOTKIT_CONTEXT*,
fanned out through MultiSink. ANNOTKIT_NOTES names the JSON store because
annotkit-mcp already reads that variable for that file — one variable, one
file, both processes wired by the same env dict. Launcher context and the
in-app provider merge with the provider winning: it is the newer
measurement.
- A WATCHABLE STREAM. NotesFileSink writes atomically, which is what stops a
reader seeing half a document and exactly what makes tail -f go silent. So
JSONLEventSink appends beside it, one line per capture/edit/delete. Events
are DERIVED by diffing each flush, not pushed per keystroke, so a woken
reader always finds the note already in the snapshot; the diff compares the
ENCODED record, so a scroll moving pins on screen is not an edit; and lines
go out in one O_APPEND write(2), so a fleet can share one log.
- VALIDATED END TO END. AnnotKitEnvProbe is one embedding host with the human
taken out (same Annotation.install path, capture driven in code).
AgentLoopE2ETests launches two of them concurrently with nothing but an
environment and asserts from outside the processes: context in both
outputs, the MCP bridge serving it, a tail -F watcher woken under a second
after capture, and no cross-writing.
Also fixed: JSONFileSink appended rather than upserted by id, so a second
press of Export put a second copy of every note in the store the agent reads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ANNOTKIT_CONTEXT*` and builds its own sinks, so a launcher configures each
437
+
instance without touching a call site (`docs/embedding.md`). `ANNOTKIT_NOTES` names
438
+
the JSON store rather than the markdown one because `annotkit-mcp` already reads
439
+
that variable for exactly that file: one variable, one file, both halves of the
440
+
product pointed at it by the same env dict. Parsing lives in a pure
441
+
`AnnotationEnvironment` taking the environment as an argument — the process env is
442
+
neither thread-safe to mutate nor reversible between tests.
443
+
444
+
**World context is opaque and snapshotted per note.**`[String: String]`, host
445
+
vocabulary, no schema: the moment AnnotKit knows what a persona is it stops being
446
+
embeddable in the next host. The provider is registered once at install and called
447
+
once per capture, not once per session — appearance, window size and persona all
448
+
change while a session is open, and a note has to record the world it was made in
449
+
rather than the one the app booted into. Launcher context and provider context are
450
+
merged with **the provider winning**, because it is the newer measurement.
451
+
452
+
**The snapshot is unwatchable on purpose, so the stream is separate.**
453
+
`NotesFileSink` writes atomically (rename), which is what stops a reader ever
454
+
seeing half a document — and is exactly what makes `tail -f` go silent, since it
455
+
follows the inode it opened. Rather than give that up, `ANNOTKIT_EVENTS` adds an
456
+
append-only JSONL log beside it. Two rules keep the pair honest:
457
+
458
+
- Events are DERIVED by diffing each flush, not pushed from each capture. Both
459
+
files are written by the same flush, so a watcher woken by a line always finds
460
+
the note already in the snapshot — the stream cannot promise something the
461
+
snapshot has not been given. A note typed but not sent produces no line.
462
+
- The diff compares the ENCODED record, not the struct. The overlay's window-local
463
+
rects move on every scroll, and a note that merely moved on screen must not read
464
+
as edited. What the agent can see changed, or nothing changed.
465
+
466
+
Lines are appended with a single `O_APPEND``write(2)`, so a fleet of instances can
467
+
share one log without tearing each other's lines and each line names its own
468
+
world's snapshot — one `tail -F` covers the fleet. The stream is never a
469
+
substitute for the snapshot: replayed from the beginning it would double-count
470
+
edits and resurrect deletes, and the `process-agentation-notes` skill still reads
471
+
the markdown.
472
+
473
+
`AnnotKitEnvProbe` is one such instance with the human taken out — same
474
+
`Annotation.install` path, capture driven in code — and `AgentLoopE2ETests` runs
475
+
two of them at once to assert reproduce/locate/react/isolate from outside the
476
+
processes.
477
+
427
478
## IP hygiene (carried into the F7 legal gate)
428
479
429
480
- Do not copy original Agentation source (PolyForm Shield 1.0.0, non-compete). Only the `AGENTATION_NOTES.md` file format is reused, reimplemented clean-room.
Copy file name to clipboardExpand all lines: PARITY.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,6 +26,7 @@ row; each asymmetry is closed by code or has a tracked mitigation.
26
26
| Pin CLICK-TO-EDIT in both tools (VRT-u209) | shared: `SelectionGesture.resolve(…pins:)` consults `PinAttentionRule.pressedNote` on a press that did not travel, and the shared catcher in `OverlayView` opens the editor for `.editNote`| same | none — the whole decision is a pure rule over the notes' stored window-local anchors, so both hosts route a tap identically, and iOS gets it from the one catcher it already shares. The TRAVEL gate is the only platform-sensitive part and it is already parameterized (`SelectionGesture.minimumTravel` is 10 on iOS vs 6 on macOS, because a finger rolls several points on a deliberate tap) — so a touch that would have been a sloppy tap still opens the pin's editor rather than being read as a drag. Verification is the same asymmetry as the row above: probe 11c is macOS-only; iOS relies on the shared unit tests |
27
27
| Recall survives a scroll |`KeyablePanel.scrollWheel` measures the translation it applied to the host's `NSScrollView` and feeds `AnnotationSession.translateNotes(by:within:)`| none | ASYMMETRIC BY THE MECHANISM: the correction is affordable on macOS only because the overlay panel already intercepts every wheel event and drives the clip itself, so the exact translation is known at the moment it is applied. iOS does not intercept `UIScrollView`, so nothing there knows a scroll happened; a note's rects stay where they were captured, which is the pre-existing behaviour `AnnotationPins` has always accepted for the pin. The RECEIVING side is shared and unit-tested (`translateNotes` is pure bookkeeping over `pending`), so an iOS host that later observes its own scrollers can adopt it without re-deriving anything |
28
28
| Overlay excluded from element lookup | AX window identifier (`AXIntrospection.overlayWindowIdentifier`) filtered out of every `kAXWindows` read |`PassThroughWindow` TYPE identity filtered out of `IOSElementSource.windows()`| ASYMMETRIC BY NECESSITY — the hosts are different window kinds. macOS's overlay is a separate `NSPanel` matched by the identifier the controller stamps on it; iOS's is a `UIWindow` in the HOST's scene sharing its pid, so no pid/scene filter separates it and a type check (internal to the module) cannot drift the way an identifier convention can. Both filter in the shared window lookup, so snapshot / hit-test / region-anchor / marquee agree; leaving it in would let a marquee bind the user's note to AnnotKit's own UI |
29
+
| Env-configured embedding (destinations, world context, event stream) |`Annotation.install` reads `AnnotationEnvironment` and builds `NotesFileSink`/`JSONFileSink`/`JSONLEventSink`| same | none in the code — the environment parse, the sink fan-out, the per-capture context snapshot and the JSONL diff are all platform-free, and both `install` overloads go through one `makeSession`. ASYMMETRIC VERIFICATION, recorded rather than closed: `AnnotKitEnvProbe` (and the `AgentLoopE2ETests` that launch two of it) mounts a real overlay on an `NSWindow`, so it is macOS-only for the same reason `AnnotKitProbe` is. What that leaves unverified on iOS is only the MOUNT; every file the contract produces is written by shared code covered by `AnnotationEnvironmentTests`, `EventStreamTests` and `SinkTests`|
29
30
| Coordinate space | Cocoa bottom-left to AX top-left flip | UIKit top-left native | iOS needs no flip; shared `ScreenSpace` used only on macOS |
30
31
| Screenshot | ScreenCaptureKit / `cacheDisplay`|`UIGraphicsImageRenderer` + `drawHierarchy`| both capture own hierarchy only; no cross-window or secure overlays |
31
32
| Overlay host | resizing `NSPanel` (toolbar corner idle, full screen annotating) | pass-through `UIWindow`| both interactive; selection via the shared SwiftUI catcher, not a global monitor |
0 commit comments