Commit graph

76 commits

Author SHA1 Message Date
Natalie
336f41c905 fix(deploy): macsync edge writes a conf.d snippet, not the whole Caddyfile
Some checks are pending
Swift Build & Test / swift build + test (push) Waiting to run
ct.prod is a SHARED DMZ (Prospector's apps.ftw.pw + macsync). The old edge
script overwrote /etc/caddy/Caddyfile wholesale, so it and Prospector's deploy
clobbered each other (an outage: a Prospector deploy dropped the macsync site
and repointed DNS). Now each service owns one /etc/caddy/conf.d/<svc>.caddy and
the main Caddyfile just `import conf.d/*.caddy`. deploy-edge.sh idempotently adds
the import, removes any legacy inline macsync block, writes conf.d/macsync.caddy,
validates, and hot-reloads — never touching other sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:14 -04:00
Natalie
af2514c098 docs(server): drop removed face-worker/imajin mention from storage comment
Some checks failed
Swift Build & Test / swift build + test (push) Waiting to run
Server Typecheck & Test / bun typecheck + test (push) Failing after 5m11s
Follow-up to the dead-pipeline removal — the StorageAdapter doc comment still
named the deleted consumers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 06:37:17 -04:00
Natalie
bc00a9e0a1 fix(server): green typecheck — outbox generics, scheduler tuples, ioredis dedupe
Some checks failed
Swift Build & Test / swift build + test (push) Waiting to run
Server Typecheck & Test / bun typecheck + test (push) Has been cancelled
- outbox/repo.ts: pgRunReturning generic must extend QueryResultRow; the
  `| undefined` belongs on the return type, not the type argument.
- outbox/scheduler.ts: annotate hot/pacedDelaySec as [number, number] so the
  frozen default satisfies SchedulerConfig's readonly tuple fields.
- config: add WORKER_CONCURRENCY (used by shared/queues); default 4.
- pin ioredis to 5.10.1 via overrides so it dedupes with bullmq's bundled copy
  (root resolved 5.11.1 vs bullmq's 5.10.1 -> two installs -> type clash).

`bun run typecheck` now passes (exit 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 06:36:26 -04:00
Natalie
5bc635572d refactor(server): remove dead imajin/iphoto-processing pipeline
The imajin image-AI + iphoto thumbnail/classify/face pipeline was half-built and
abandoned: its workers were never started, the admin backfill route was never
mounted, and the code referenced config keys (IMAJIN_*_URL) and exports
(PhotoMediaKind, mediaKindFromMime, setAlbumCoverPhoto) plus photo columns
(media_kind, mime_type, processing_status) that don't exist. Nothing wired
imports any of it. Delete the whole self-contained cluster (9 typecheck errors)
per zero-tech-debt; the live sync surfaces and iphoto/service are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 06:36:26 -04:00
Natalie
e52ed1b44f feat(deploy): codify the ct.prod DMZ edge + add deploy/ops runbook
Some checks failed
Swift Build & Test / swift build + test (push) Waiting to run
Server Typecheck & Test / bun typecheck + test (push) Failing after 5m48s
deploy-edge.sh reproducibly configures macsync's public edge on ct.prod (Caddy
-> macsync 10.20.0.5:3201 over the VPC), so a ct.prod rebuild restores it (it was
hand-configured during cut-over). docs/DEPLOY.md documents the two-box DMZ/internal
topology, one-command deploys, rebuild recovery, secrets model, security posture,
and how to run the tests. Verified: edge returns 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 06:25:49 -04:00
Natalie
e325d1e79e fix(server/test): scope integration harness to macsync schema (was stale icloud)
The db-harness rewrote migration SQL by replacing `icloud.` with the per-run test
schema, but the schema was renamed to `macsync.` long ago — so with a DB the
integration suites would have hit the real `macsync` schema instead of an
isolated `macsync_test_*` one. Rewrite `macsync.` and rename the test schema
prefix accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 06:25:49 -04:00
Natalie
dfa7eb519d fix(inotes): build US/RS separators with character id, not backslash-u escapes
Some checks are pending
Swift Build & Test / swift build + test (push) Waiting to run
THE actual root cause of notes never syncing (masked for several rebuilds by
os_log <private> redaction, now surfaced as AppleScript error -2741): the
fetchAllNotes script joined fields with backslash-u-001F / backslash-u-001E
separators, but AppleScript has no backslash-u escape (only \n \t \" \\), so the
script never compiled. Produce the 0x1F/0x1E separators (which Self.parse splits
on) via "character id 31" / "character id 30".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:00:16 -04:00
Natalie
7af883a066 fix(inotes): run Notes AppleScript on a thread with a live run loop
Some checks are pending
Swift Build & Test / swift build + test (push) Waiting to run
The detached thread had no run loop, so AESendMessage(kAEWaitReply) for the long
~25s notes fetch never received its reply (errored). Marshal scripts onto one
long-lived thread that owns a continuously-running CFRunLoop: in-process (TCC
attributes the event to MacSync, grant honored), real run loop (reply pumped),
off-main (no agent freeze). Also log the AppleScript error with .public privacy
so failures are visible instead of os_log's <private> redaction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:02:12 -04:00
Natalie
acebcdc37e deploy(server): rewrite deploy-server.sh as a rebuild-safe one-command deploy
Some checks are pending
Swift Build & Test / swift build + test (push) Waiting to run
Captures the working DO-native deployment so a terraform rebuild (which wipes
the manual install) is recovered with one command: installs runtime (bun/redis/
caddy), syncs code, pushes secrets OVER SSH (never in cloud-init user-data — that
is metadata-readable, per the gpu.sh finding), wires the systemd unit + Caddy TLS
edge, verifies health. Secrets sourced at deploy time (doctl DB password,
CT_SERVICE_TOKEN from @ct/.env.local, Spaces keys from vault) — none hardcoded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:31:56 -04:00
Natalie
92871203e5 chore(inotes): log the AppleScript error detail on fetchAllNotes failure
Surface the underlying NSAppleScript errorMessage instead of a bare
"script failed", to diagnose the Notes read failure mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:15:58 -04:00
Natalie
4997a6ccb4 fix(inotes): run the Notes NSAppleScript on a dedicated thread, not main
Some checks failed
Swift Build & Test / swift build + test (push) Waiting to run
Server Typecheck & Test / bun typecheck + test (push) Failing after 1m36s
A ~25s `tell application "Notes"` read on the main actor blocks the run loop the
app and sync coordinator need (the read silently stalled — no fetch, no error).
Run it on a fresh thread via a continuation: NSAppleScript.executeAndReturnError
is synchronous and handles its own Apple-event reply, so it keeps the in-process
TCC attribution (the grant applies) without blocking the main thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:08:24 -04:00
Natalie
8b01246e2e fix(server): require operator token to register a device
Single-user deployment on a public TLS edge — only the operator (who holds the
service token, attached as a Bearer on every client request) should onboard a
device. Drop the auth exemption on /client/devices/register so anonymous callers
get 401 instead of a working token; /client/devices/:id/status stays open since
it is polled before the device token is issued.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:31:04 -04:00
Natalie
d03b9e3046 fix(inotes): read Notes via in-process NSAppleScript so the TCC grant applies
osascript runs out-of-process, so TCC attributes the Apple event to osascript
rather than MacSync — every Notes read was denied even after the user granted
MacSync → Notes Automation (the script works fine from Terminal). Send the
event in-process via NSAppleScript on the main actor (tell-application events
need a live run loop for their reply); the grant is then honored and notes
sync. The read is infrequent (600s cycle) and brief enough for a menu agent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 03:54:02 -04:00
Natalie
8597406898 fix(inotes): probe Notes with a data event so the Automation prompt fires
`tell application "Notes" to get name` resolves the app's bundle name WITHOUT
sending an Apple event, so it never triggers the TCC Automation prompt — Notes
stayed ungranted and unsynced while Messages (granted via a real data event)
worked. Probe with `count notes`, a real automation event, in both the inotes
authorization cycle (auto, no menu click) and the tray "Grant Automation" item
(now data-reading per app: chats/accounts/notes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 03:41:59 -04:00
Natalie
90014f5dd3 fix(client): probe each app separately when priming Automation TCC
A single AppleScript with tell-blocks for Messages, Mail, and Notes halts at
the first un-authorized app (errAEEventNotPermitted), so the later apps never
receive an Apple event and never register in the Automation pane — leaving
Notes/Mail ungranted (and unsynced) while only Messages appeared. Probe each
target in its own NSAppleScript execution so each registers and surfaces its
own first-run prompt independently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:53:27 -04:00
Natalie
02c78db2de docs(manifest): correct backend droplet IP + log command
Some checks are pending
Swift Build & Test / swift build + test (push) Waiting to run
The backend droplet is 165.227.96.183 (DO lilith-store-backend, nyc3, wg
10.9.0.5), not the stale 209.38.51.98. Logs go to /var/log/mac-sync-server.log
(the droplet journald is volatile), so the logs command tails the file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:23:00 -04:00
Natalie
f9cf50e695 fix(server): unbuffered logging + reject operator token on contact sync
Some checks failed
Swift Build & Test / swift build + test (push) Waiting to run
Server Typecheck & Test / bun typecheck + test (push) Failing after 5m9s
- logger: emit straight to fd 1/2 (unbuffered). The buffered process.std*
  streams block-buffer to a pipe under systemd, so low-volume logs never
  flushed and were invisible.
- /client/imessage/contacts: return 401 (like /sync/batch) when the caller
  presents the operator/service token instead of a device token, instead of
  500ing on a null deviceId downstream.
- systemd unit: reflect the working deploy (root + /root/.bun, Redis
  dependency, file logging since the droplet journald is volatile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:47:18 -04:00
Natalie
f6ce05d864 feat(server): declare queue/photo deps + REDIS_URL, switch to bun lockfile
Some checks are pending
Server Typecheck & Test / bun typecheck + test (push) Waiting to run
Swift Build & Test / swift build + test (push) Waiting to run
The server source imports bullmq/ioredis/sharp/exifr but they were never
declared (the stale pnpm-lock pinned tarballs to the dead black.lan registry).
Declare them, add REDIS_URL to the config schema (default local Redis) since
the queue connection already reads it, and replace the unusable pnpm-lock with
a bun.lock resolved against npmjs. Import graph now evaluates cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:03:08 -04:00
Natalie
e84b878cea feat(server): vendor createPool in-repo, drop dead @lilith/quinn-db-pg
Some checks are pending
Server Typecheck & Test / bun typecheck + test (push) Waiting to run
Swift Build & Test / swift build + test (push) Waiting to run
The @lilith/quinn-db-pg@1.0.1-dev.* snapshot was only ever published to the
retired black.lan Verdaccio and resolves nowhere now (not on the DO forge, no
local cache, no source). Replace the single `createPool` import with a faithful
in-repo pg.Pool factory (service-name -> QUINN_<SERVICE>_DB_URL) and add `pg`
as a direct dependency (was transitive via the dead package).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:49:03 -04:00
Natalie
144571d042 chore(macsync): update self-path to @ct/@applications after reorg
Some checks are pending
Swift Build & Test / swift build + test (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:41:59 -04:00
Natalie
4ea358035a chore(mac-sync): manifest + deploy + bunfig updates
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:35:13 -04:00
Natalie
ad8e126dd1 docs(mac-sync): outbox/read architecture, handoffs, module docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:35:13 -04:00
Natalie
3c3c9e7dfa feat(shared): LocalWebServer outbox/read routes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:35:13 -04:00
Natalie
5347a8d7e3 feat(mcp): outbox/read client methods + tools + README
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:35:13 -04:00
Natalie
1de0ccdfd6 feat(server): wire outbox + read features into app + my surface
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:35:13 -04:00
Natalie
9f7c9f4533 feat(read): conversation-read entity + read feature + my-surface endpoint & tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:35:13 -04:00
Natalie
8d821c8c97 feat(outbox): outbox entity, service, sweep + my-surface endpoint & tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:35:13 -04:00
Natalie
53c900b3ee chore(infra): add .infra.yaml (convention:infra_manifest) for infra-net reconcile
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:10:18 -04:00
Natalie
52e641c9a5 fix(deploy): use official rclone for mount (brew rclone lacks FUSE on macOS)
Homebrew's rclone is compiled without 'mount' support on macOS. Resolve a
mount-capable binary ($HOME/bin/rclone, official rclone.org build) and fail
fast with install guidance if none is found. brew rclone still serves plain
transfers via spaces-env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:18:03 -04:00
Natalie
576496ca3e feat(deploy): video-projects FUSE mount over DO Spaces
Generalize the photos-originals rclone-mount pattern to a video-projects
prefix so the video studio (and imajin ETL, per storage-portability-plan
§2.3) can read/write multi-GB project sources/renders as local files while
only hot data stays resident on plum (bounded VFS LRU cache). Lets a
small-disk laptop work with large footage without filling APFS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:10:13 -04:00
Natalie
46d350d8ce feat(send-queue): autoqueue toggle + message provenance
autoqueue: send_rate_config gains auto_queue (default true). When on, the
pending endpoint holds over-cap sends to drip out (burst-friendly); when
off, the cap is disabled and queued sends release immediately. Threaded
through getSendRateConfig/setSendRateConfig and GET/PUT /admin/send-rate-limit.

provenance: send_queue gains authored_by + dispatched_by (who composed the
text vs what triggered the send), a fixed vocabulary (user, claude-prospector,
claude-messenger, autoresponder, scheduled-worker, unknown) validated at the
enqueue boundary and recorded on insert. Nullable for legacy rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 21:28:36 -04:00
Natalie
242d7cd1a8 feat(send-queue): burst-friendly outbound send-rate cap (default 10/5min)
The /client/imessage/send-queue/pending endpoint released up to 50 queued
sends per poll, so an enqueued burst all fired at once. Add a configurable
release cap: the endpoint now returns at most (maxSends − sent-in-window)
queued items, so a burst queues and drips out at the configured rate.

- macsync.send_rate_config single-row table, default max_sends=10,
  window_seconds=300 (10 per 5 min).
- entities/send-queue repo: getSendRateConfig / setSendRateConfig /
  countSentWithin.
- Admin control: GET/PUT /admin/send-rate-limit (service-token auth) so the
  cap is adjustable at runtime (wired to MCP via quinn.api separately).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:35:18 -04:00
Natalie
ab44591b8a fix(imessage): stop blob sync starving the periodic read cycle
The iMessage read cycle is driven by BaseSyncManager's 30s timer →
syncNow(), which is gated by 'guard !isSyncing'. performSync awaited
blobSyncManager.syncBlobs() inline, and that blob pass infinite-loops
when the upload backend is failing: /attachments/missing has no cursor,
so a full page of perpetually-failing uploads is re-fetched and re-failed
forever, the loop only breaking on a < pageSize page. performSync never
returned → isSyncing stuck true → every 30s read tick swallowed. Net
effect: messages only synced on app launch, drifting hours behind between
restarts (send-queue timers are independent, so they kept polling — the
tell that the timer fired but syncNow was gated).

Two fixes:
- Decouple the blob pass: fire it detached + in-flight-guarded instead of
  awaiting it on the read cycle, so a slow/failing blob backend can never
  hold isSyncing.
- Bound the blob loop: stop a pass after any full page that produced zero
  successful uploads (the same missing set would be re-fetched), instead
  of spinning forever.

Verified: read cycle now fires every ~30s on the live process without a
restart; blob pass logs 'stopping pass' and returns; store lag ~7s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:37:40 -04:00
Natalie
464bbbd48d refactor(imessage): remove redundant contact-summary enrichment
The contact-summary sweep generated a 3-field digest (mostRecently /
overallSummary / recap) per iMessage contact via the model-boss chat
endpoint. It's redundant with the prospector, which already classifies
1271 prospects with tier + archetype + score + status — strictly richer
per-person intel for the contacts that matter. It was also the path that
wedged the server against the decommissioned model-boss host (2026-06-23).

Remove the generation path entirely: the per-sync sweep in
ingestContacts, the contact-summary feature module + its test, and the
now-orphaned chatJson client in shared/model-boss.ts (contact-summary was
its only caller). The connection circuit breaker stays — the
embedding-worker still calls the same coordinator and needs the same
wedge protection.

Kept the read-side data layer (summary_data column, summaryData field,
updateContactSummary, the /my/contacts surface field) dormant as the
landing spot if summaries are ever repopulated offline via batch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:02:10 -04:00
Natalie
1ebbd8e872 fix(imessage): circuit-break model-boss calls so a dead coordinator can't wedge ingest
The contact-summary sweep and embedding-worker call model-boss (GPU
coordinator) sequentially. When its host is offline every call paid the
full TCP-connect timeout (~3s) before failing; a sweep over ~1700
contacts serialised thousands of slow failures and stalled the whole
server — message ingest froze for hours while the listener stayed up
(observed 2026-06-23, coordinator host decommissioned).

Add a connectivity circuit breaker in shared/model-boss.ts: after 3
consecutive connection failures it opens for a 60s cooldown and fails
fast (no fetch), auto-probing once afterwards to recover. The
contact-summary sweep now bails the moment the breaker is open instead
of queueing doomed per-contact work. HTTP error responses still count as
reachable — the breaker tracks connectivity, not request success.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:30:15 -04:00
Natalie
cae15ae9f1 fix(@applications/@mac-sync): 🐛 update lan instead of local in all configs
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-10 03:12:06 -07:00
Natalie
06a8ace642 feat(imessage): improve incremental sync with date-based watermarking
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-31 18:43:38 -06:00
Natalie
982cee2982 feat(apps): add incremental sync overlap for recent messages
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-31 18:36:01 -06:00
Natalie
0bffc51524 feat(contacts): enable contact render polling opt-in
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-22 13:34:08 -07:00
Natalie
42b9229a9f feat(@applications/@mac-sync): add async actor-based iMessage sender with timeout guard
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-22 01:56:05 -07:00
Natalie
9373b14ab4 fix(@mac-sync): 🐛 add keychain search list cleanup on sign failure
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-21 22:03:45 -07:00
Natalie
48217173a4 feat(imessage): add fallback sms retry logic
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-21 21:06:28 -07:00
Natalie
5290e1de2f feat(imessage): improve iMessage service detection and error handling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-21 20:24:45 -07:00
Natalie
b104ee1b12 fix(@mac-sync): 🐛 add debug flag for send-queue tracing
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-21 19:39:09 -07:00
Natalie
08c638532c feat(sync): add trace logging hooks
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-21 19:04:28 -07:00
Natalie
e5cad45ec3 feat(@applications/mac-sync): add timeout handling for osascript send
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-21 18:43:35 -07:00
Natalie
c2c7870915 feat(photos): improve search weighting and type safety
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-19 00:04:30 -07:00
Natalie
9a9040cad5 feat(@applications/mac-sync): add message metadata fields
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-18 23:58:09 -07:00
Natalie
b95d6c40af fix(@applications/mac-sync): 🐛 update mac sync send queue queries and db table names
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-18 21:12:47 -07:00
Natalie
a8a5654e97 fix(client): 🐛 change put to post for attachment upload endpoint
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-18 01:19:51 -07:00