@lazyingart/agent-web 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +438 -0
  3. package/docs/architecture.md +503 -0
  4. package/package.json +43 -0
  5. package/src/aginti-adapter.js +602 -0
  6. package/src/chat-context.js +1020 -0
  7. package/src/chat-migrations.js +947 -0
  8. package/src/chat-store.js +3308 -0
  9. package/src/cli.js +134 -0
  10. package/src/cloud-server.js +2043 -0
  11. package/src/contracts.js +103 -0
  12. package/src/deterministic-context-summarizer.js +254 -0
  13. package/src/direct-chat-capability-limits.js +66 -0
  14. package/src/direct-chat-contract.js +3 -0
  15. package/src/errors.js +50 -0
  16. package/src/http-contract.js +592 -0
  17. package/src/index.js +88 -0
  18. package/src/localllm-connector.js +667 -0
  19. package/src/migrations.js +231 -0
  20. package/src/operator-health.js +184 -0
  21. package/src/password-verifier.js +131 -0
  22. package/src/service-config.js +547 -0
  23. package/src/service.js +408 -0
  24. package/src/sqlite-health.js +83 -0
  25. package/src/storage-path.js +130 -0
  26. package/src/store.js +914 -0
  27. package/src/validation.js +181 -0
  28. package/src/vision-attachment.js +404 -0
  29. package/src/web/aginti-client.js +552 -0
  30. package/src/web/aginti-protocol.js +1146 -0
  31. package/src/web/asset-map.js +462 -0
  32. package/src/web/browser-app.js +6491 -0
  33. package/src/web/cloud-session-client.js +427 -0
  34. package/src/web/direct-chat-client.js +1482 -0
  35. package/src/web/index.js +10 -0
  36. package/src/web/presentation-state.js +107 -0
  37. package/src/web/pwa-assets.js +854 -0
  38. package/src/web/pwa-update-handoff-store.js +179 -0
  39. package/src/web/safe-rendering.js +836 -0
  40. package/src/web/vision-image-client.js +546 -0
  41. package/src/web/vision-image-sanitizer.js +168 -0
  42. package/src/web/web-release.js +28 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LazyingArt contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,438 @@
1
+ # LazyingAgentWeb
2
+
3
+ `@lazyingart/agent-web` is the standalone cloud PWA and HTTP/BFF for
4
+ `llm.lazying.art`. It provides a usable browser chat surface while keeping
5
+ AgInTi, LocalLLM, and LazyEdge independently replaceable.
6
+
7
+ > **Deployment status (v0.1.29 candidate; v0.1.27 production):** production is promoted
8
+ > independently from repository commits, with immutable acceptance receipts and
9
+ > a verified rollback release. The current deployment enables AgInTi Agent only
10
+ > through the accepted native AgInTi capability proof; if that proof is absent
11
+ > or disabled, the BFF fails closed and Agent is unavailable. Direct Chat stays
12
+ > separate and does not enable or authorize Agent. Current accepted Agent
13
+ > requests can execute one exact fenced `python` block without model planning,
14
+ > retain public failure reasons across reload, and follow exact idempotent Resume
15
+ > successor runs with an optional corrected prompt. The accepted production release makes
16
+ > completed Agent conversations truly multi-turn: later prompts resume the exact
17
+ > terminal predecessor, retry an uncertain mutation once with the same idempotency
18
+ > key, and keep any rejected prompt editable without an optimistic duplicate.
19
+ > Direct Chat remembers the user's non-private workspace-mode preference across
20
+ > a full reload, while
21
+ > confirmed sign-out fences stale history reads. The accepted v0.1.23 release also
22
+ > keeps failed-predecessor and corrected-success messages in chronological
23
+ > order when their verified output and plot artifacts are restored after reload.
24
+ > The baseline accepted artifacts remain declarative plot, table, and Markdown
25
+ > schemas. Responsive plot artifacts in v0.1.25 use the available message width,
26
+ > retain readable axes on DPR3 phones, wrap legends, and cannot be reflowed into
27
+ > the sidebar column by privacy masking. The accepted v0.1.27 release also makes an
28
+ > already-restored Agent-thread selection idempotent: clicking it cannot launch
29
+ > a redundant ledger replay that detaches and rebuilds messages or figures, while
30
+ > an invalid replay or unconfirmed mutation remains explicitly reopenable for
31
+ > authoritative recovery. A brand-new conversation whose creation response is
32
+ > lost retains its exact idempotency ticket for the next Send instead of
33
+ > deadlocking or minting a duplicate. Numeric plot axes choose the shortest
34
+ > distinguishing visible tick labels while retaining exact
35
+ > values in accessible plot metadata. The v0.1.29 candidate also preserves an
36
+ > unsent Agent follow-up across a release-fenced full reload: the encrypted
37
+ > handoff retains its mode and owned thread, replays that thread after reload,
38
+ > derives the authoritative terminal predecessor, and only then enables the
39
+ > restored draft. The next Send therefore resumes the same Agent run instead of
40
+ > silently changing to Direct Chat. v0.1.24 preserves the v0.1.23
41
+ > backward-compatible, default-disabled grounded-search extension: only an exact
42
+ > AgInTi capability may reveal the explicit Search
43
+ > controls, bind `{mode: web|papers|both, limit: 1..20}` to one Agent input, and
44
+ > admit bounded HTTPS-only `sources` artifacts. It also adds an exact-origin,
45
+ > session-and-CSRF-bound fallback for iOS/PWA requests that omit all or part of
46
+ > otherwise-valid Fetch Metadata,
47
+ > retained mobile CSRF state, release-pinned requests with encrypted draft/image
48
+ > refresh handoff, resume-time session validation, and secret-free rejection telemetry.
49
+ > The browser has no direct LocalLLM search route.
50
+ > Voice messages and general artifact uploads remain unavailable. The next
51
+ > capability-gated artifact extension adds local-only PDF/TeX downloads: AgInTi
52
+ > retains the bytes, while the cloud BFF only streams an authenticated bounded
53
+ > response and never stores or caches it.
54
+
55
+ The ownership model, Chat/Agent data planes, recovery semantics, artifact
56
+ boundary, and replaceable-node contract are specified in
57
+ [docs/architecture.md](docs/architecture.md).
58
+
59
+ ## Implemented surface
60
+
61
+ - A bright-by-default installable PWA with persistent theme and workspace-mode preferences,
62
+ browser-password-manager integration, durable thread restoration, resumable
63
+ streaming, explicit cancellation, optional one-to-four-image Direct Chat input,
64
+ Markdown, KaTeX math, and safe declarative plot/table/Markdown rendering,
65
+ plus capability-gated text-only source cards that never fetch automatically
66
+ and safe Open/Download controls for AgInTi-owned PDF/TeX files.
67
+ - A root-only Node HTTP/BFF with exact routes, host/origin/fetch-metadata/CSRF
68
+ enforcement, opaque remembered sessions, bounded request/stream/job
69
+ admission, owner-safe response projections, and graceful job draining.
70
+ - `CloudIndexStore` for cloud accounts, digest-only browser sessions,
71
+ presentation-only AgInTi thread indexes, delivery cursors, and closed
72
+ idempotency receipts.
73
+ - `DirectChatStore` for cloud-owned Direct Chat threads and hash-linked message
74
+ ledgers, atomic user-message/generation start, durable fenced dispatch leases,
75
+ exact-once assistant finalization, replayable deltas, cancellation, bounded
76
+ retention, compaction snapshots, private immutable vision attachments, and
77
+ receipt-authorized Direct Chat thread deletion.
78
+ - `DirectChatContextCoordinator` for bounded LocalLLM context assembly and
79
+ provenance-bound chat compaction. The standalone service uses a deterministic
80
+ local summarizer that performs no model or network call, so compaction cannot
81
+ bypass the single-inference admission fence. Summaries are explicitly
82
+ labeled untrusted conversation data and never gain system, developer,
83
+ policy, tool, or Agent authority.
84
+ - `createLocalLlmConnector()` for a fixed set of LocalLLM model aliases over an
85
+ exact authenticated `127.0.0.1` OpenAI-compatible `/v1` endpoint. It validates
86
+ models and SSE frames, bounds input/output, rejects redirects and partial
87
+ redispatch, sends canonical images only through the fixed `localllm-vision`
88
+ alias, and has no hosted-provider fallback.
89
+ - A fail-closed AgInTi BFF transport and cloud-owned stateless adapter. The
90
+ browser can call only the frozen public protocol; the server derives
91
+ identity/session context, validates exact requests and responses, and sends
92
+ only `x-aginti-principal-id`, `x-aginti-browser-session-id`, and standard
93
+ `Idempotency-Key` authority to AgInTi. LazyEdge remains an opaque transport;
94
+ Agent state and decisions never move into this package. Search-bearing run
95
+ inputs receive a fresh server-side capability preflight, so a disabled or
96
+ legacy AgInTi never receives the extension field. The exact `file` capability
97
+ negotiates creation only; a verified file event and receipt remain durable
98
+ read authority across a read-compatible rollback. File bytes use a separate
99
+ authenticated release-bound GET/HEAD BFF route that converts one validated
100
+ browser range into structured AgInTi input, streams with backpressure, and
101
+ never forwards browser credentials or headers to the local service.
102
+
103
+ ## Component boundaries
104
+
105
+ | Component | Owns | Must not own |
106
+ | --- | --- | --- |
107
+ | LazyingAgentWeb | Cloud accounts and browser sessions; Direct Chat history, deltas, and chat-only context; AgInTi presentation pointers and delivery cursors; PWA and safe rendering | Agent plans, Agent messages/context/compaction, tools, execution, artifact authority, inference implementation, tunnels |
108
+ | AgInTi | Authoritative Agent threads, runs, plans, context, compaction, tools, event ledger, cancellation, and artifacts | Cloud login/session policy, LocalLLM inference implementation, LazyEdge transport |
109
+ | LocalLLM | Text, coding, embedding, and vision inference | Chat persistence, Agent orchestration, cloud accounts, edge transport |
110
+ | LazyEdge | Authenticated, replaceable transport between the cloud BFF and exact enrolled local services | Chat or Agent semantics, model behavior, cloud presentation state |
111
+
112
+ Direct Chat is deliberately cloud-owned because LocalLLM inference is
113
+ stateless. Agent mode is only another frontend for AgInTi: the cloud database
114
+ must not contain AgInTi messages, summaries, plans, tool calls/results,
115
+ commands, workspace paths, runtime policy, artifact bodies, or sandbox state.
116
+ Removing a cloud Agent index cannot delete its authoritative AgInTi thread.
117
+
118
+ ## PWA releases and latest-version reload
119
+
120
+ `createStandaloneAssetMap()` builds and brands a complete release map. Its
121
+ immutable release ID is derived from the full shell content and pinned build
122
+ inputs, and every JavaScript module, CSS file, KaTeX module, and icon is placed
123
+ under `/assets/r/<release>/`. The server accepts only that verified branded map;
124
+ it cannot be paired with a caller-invented release ID.
125
+
126
+ The update authority is the stable `/sw.js` route, served with `no-store`,
127
+ `no-cache`, and `must-revalidate`. The browser registers it with
128
+ `updateViaCache: "none"`. Controlled pages check at startup and on bounded
129
+ foreground/online transitions, while a fresh uncontrolled install skips a
130
+ redundant immediate update race. A complete successor shell is verified by
131
+ exact URL, MIME type, security headers, byte length, and SHA-256 before it can
132
+ wait for activation.
133
+ The page proves a waiting worker's release over a one-shot message channel and
134
+ suppresses the banner only when it exactly matches the loaded shell. The UI
135
+ offers **Update** and **Later** for a verified successor, conservatively falling
136
+ back to the same offer for an unresponsive legacy worker; Update reloads once
137
+ only after the new worker controls the tab. Activation retains the current and
138
+ immediately previous verified shell. An offline or failed update leaves the
139
+ current app usable.
140
+
141
+ If Update is confirmed while a definitively unsent Direct Chat composer is
142
+ eligible, or while an exact Agent composer is idle or follows an authoritative
143
+ terminal run, the page stores one bounded AES-GCM ciphertext in a dedicated
144
+ IndexedDB store. The v0.1.29 inner payload schema v3 preserves the exact mode,
145
+ owned Agent thread when present, explicit Search settings or No Search, draft,
146
+ and bounded images. Its random key exists only in the replacement navigation
147
+ fragment, which browsers never send in HTTP requests. The fragment deliberately
148
+ remains while authenticated take and decryption are pending; only a bounded
149
+ recovery retry or verified release hop reattaches it, and successful recovery
150
+ then scrubs it. The exact account, scope, source/target releases, expiry, digest,
151
+ and canonical image contract are revalidated; restoration never dispatches the
152
+ draft. Expired, malformed, and excess orphan records are pruned. Passwords,
153
+ active sends or generations, nonterminal Agent work, and ambiguous mutations
154
+ remain reload blockers.
155
+
156
+ If a release fence is discovered while same-account sign-in is only restoring
157
+ an already server-owned Agent or Direct Chat conversation, v3 may carry an
158
+ empty exact thread selection as read-recovery metadata. It never carries or
159
+ replays a mutation ticket. A durable ciphertext can also cross a signed-out
160
+ release fence through a key-authenticated opaque hop; account verification
161
+ still happens only in the successor before any plaintext is shown.
162
+
163
+ A v0.1.27 handoff has an inner schema-v2 payload that cannot prove its original
164
+ conversation mode or Search choice. v0.1.29 therefore keeps that recovered
165
+ composer read-only until the user explicitly chooses its destination and then
166
+ confirms **Search** or **No Search**; it never guesses and never auto-sends.
167
+
168
+ The handoff accepts at most four canonical images and 16 MiB in aggregate. Any
169
+ larger, active, ambiguous, malformed, expired, or ownership-mismatched draft is
170
+ ineligible; the current page stays open rather than discarding it.
171
+
172
+ Only immutable public shell assets enter Cache Storage. Login/session, Direct
173
+ Chat, Agent, SSE, artifact, and upload traffic always bypasses it. Production
174
+ must stage the entire immutable namespace before atomically switching the root
175
+ HTML and stable worker response.
176
+
177
+ Authenticated history and original attachment bytes remain authoritative in
178
+ the cloud SQLite stores. The PWA may keep only a disposable, bounded per-tab
179
+ Blob LRU for viewport-near attachment previews; it is never written to browser
180
+ storage and is purged across authentication, account, and release boundaries.
181
+ Historical rendered previews have a separate four-image / 64 MiB estimated
182
+ decoded-pixel limit. Eviction revokes the object URL and leaves a tap-to-reload
183
+ placeholder, so scrolling through a long image thread cannot retain every
184
+ decoded surface. Up to four staged or just-sent composer images are transient
185
+ rather than part of this history cache and are revoked at their existing send, view, and
186
+ authentication boundaries.
187
+ The server similarly caches only successful integrity-audit state, bounded by
188
+ thread and invalidated by every local write or SQLite `data_version` change.
189
+ It does not duplicate or relax validation of private message data.
190
+
191
+ ## Public package entry point
192
+
193
+ The package root exports the implemented server and storage primitives plus the
194
+ browser/PWA protocol:
195
+
196
+ ```js
197
+ import {
198
+ CloudIndexStore,
199
+ DirectChatContextCoordinator,
200
+ DirectChatStore,
201
+ createAgintiAgentAdapter,
202
+ createCloudServer,
203
+ createLocalLlmConnector,
204
+ createStandaloneAssetMap,
205
+ failClosedCapabilities
206
+ } from '@lazyingart/agent-web';
207
+ ```
208
+
209
+ The three runtime stores/coordinators are intentionally injected into
210
+ `createCloudServer()` rather than hidden behind globals. The LocalLLM connector
211
+ also receives its transport credential through a server-side provider; neither
212
+ that credential nor an AgInTi/LazyEdge credential is sent to the browser or
213
+ stored in this repository.
214
+
215
+ ## Standalone service configuration
216
+
217
+ `lazying-agent-web serve` reads one owner-only JSON configuration and separate
218
+ owner-only `LoadCredential` files. A secret-free shape is:
219
+
220
+ ```json
221
+ {
222
+ "schema": "lazying-agent-service/v1",
223
+ "listen": { "host": "127.0.0.1", "port": 18543 },
224
+ "publicOrigin": "https://llm.lazying.art",
225
+ "account": {
226
+ "username": "lachlanchen",
227
+ "principalId": "principal_account_one",
228
+ "displayName": "Lachlan"
229
+ },
230
+ "state": {
231
+ "cloudIndexDatabase": "/var/lib/lazying-agent-web/cloud/index.sqlite",
232
+ "directChatDatabase": "/var/lib/lazying-agent-web/chat/chat.sqlite"
233
+ },
234
+ "pwa": {
235
+ "versionLabel": "release",
236
+ "title": "LazyingArt Agent",
237
+ "name": "LazyingArt Agent",
238
+ "shortName": "Lazying Agent"
239
+ },
240
+ "localLlm": {
241
+ "baseUrl": "http://127.0.0.1:18008/v1",
242
+ "allowedModelAliases": ["localllm-deep", "localllm-vision"],
243
+ "defaultModelAlias": "localllm-deep",
244
+ "vision": { "enabled": false }
245
+ },
246
+ "aginti": {
247
+ "enabled": true,
248
+ "baseUrl": "http://127.0.0.1:18009"
249
+ },
250
+ "credentials": {
251
+ "passwordHash": "login-password-hash",
252
+ "localLlmToken": "localllm-token",
253
+ "agintiToken": "aginti-token"
254
+ }
255
+ }
256
+ ```
257
+
258
+ The filenames identify distinct credential files; raw password verifiers and
259
+ bearer values never appear in the JSON. Credential directories and files may
260
+ be systemd `LoadCredential` material owned by root (including its read-only
261
+ root-group delivery modes under `/run/credentials/<unit>/`) or owner-only
262
+ files owned by the service account;
263
+ symlinks, hard links, world access, and any non-owner write access remain
264
+ rejected. The preferred fixed-parameter scrypt verifier uses a 64-byte
265
+ derived key, while the canonical 32-byte verifier used by the current v0.2
266
+ login is accepted for a password-preserving migration. Set `aginti` to
267
+ `{ "enabled": false }`
268
+ and omit `credentials.agintiToken` when the Agent transport is intentionally
269
+ absent. Configuring the transport does not claim Agent readiness: capability
270
+ discovery stays fail-closed until AgInTi itself proves its native API, policy,
271
+ sandbox, and current resource admission.
272
+
273
+ Before switching an edge proxy to a candidate build, derive its exact,
274
+ secret-free static allowlist from that same installed package and config:
275
+
276
+ ```sh
277
+ lazying-agent-web edge-routes --config /etc/lazying-agent-web/service.json
278
+ ```
279
+
280
+ The JSON contract contains the candidate content-bound `releaseId`, the exact
281
+ `GET`/`HEAD` paths, and the query-bearing request targets used by the service
282
+ worker. Stage and validate the proxy from this output, switch the proxy and app
283
+ as one release, then verify every request target before retiring the previous
284
+ allowlist. Unknown or foreign release assets must continue to return 404.
285
+
286
+ Operators can inspect the deployed release and its decoupled runtime state
287
+ without adding a public HTTP endpoint:
288
+
289
+ ```sh
290
+ lazying-agent-web health --config /etc/lazying-agent-web/service.json
291
+ ```
292
+
293
+ The command opens `CloudIndexStore` and `DirectChatStore` state read-only and
294
+ reports them independently. LocalLLM and configured AgInTi probes are bounded;
295
+ their errors collapse to fixed reason codes. Paths, origins, account identity,
296
+ credentials, and raw upstream responses are never emitted. LazyEdge is always
297
+ reported as `not_probed` with `healthClaim: false`; use LazyEdge doctor for
298
+ transport health. A degraded or unavailable report exits nonzero. This is an
299
+ operator diagnostic, not a public liveness endpoint: `/health` and
300
+ `/api/health` remain default-deny 404s, and dependency health does not gate the
301
+ static shell.
302
+
303
+ Direct Chat vision remains fail-closed when `localLlm.vision` is absent or
304
+ disabled. Enabling it requires the fixed `localllm-vision` alias while keeping a
305
+ different default text alias. The PWA accepts one to four JPEG, PNG, HEIC, or
306
+ HEIF still images plus a non-empty prompt and accepts each source file up to
307
+ 24 MiB. HEIC/HEIF support uses only a feature-detected native browser decoder;
308
+ AVIF, sequences, conflicting brands, and malformed ISO-BMFF framing fail closed.
309
+ The browser redraws and downscales every accepted source sequentially through a
310
+ canvas to remove source metadata, and only canonical JPEG/PNG bytes can cross
311
+ the wire or enter storage. Slow native decoding exposes a visible and accessible
312
+ `Preparing images…` state; timeout, cancellation, session changes, and PWA
313
+ controller changes fence late decoder completions. It enforces 4 MiB per
314
+ canonical image and 16 MiB per message. A separate preview is bounded to 512
315
+ pixels and 512 KiB so a visible
316
+ mobile gallery never retains the full upload surfaces.
317
+ The server independently validates MIME, framing, dimensions, metadata absence,
318
+ digest, ordering, unique attachment IDs, count, and aggregate bytes before
319
+ committing the user message, every private image, and the generation atomically.
320
+
321
+ A minimal in-process storage-only probe is:
322
+
323
+ ```js
324
+ import { CloudIndexStore, createCapabilityContract } from '@lazyingart/agent-web';
325
+
326
+ const store = new CloudIndexStore({
327
+ databasePath: '/srv/lazying-agent-web/private/index.sqlite'
328
+ });
329
+
330
+ console.log(createCapabilityContract());
331
+ console.log(store.healthCheck()); // DirectChatStore exposes the same safe shape.
332
+ store.close();
333
+ ```
334
+
335
+ ## Runtime
336
+
337
+ - Node.js 22.21.0 or newer, with the built-in `node:sqlite` module.
338
+ - Exact runtime dependencies `es-module-lexer@2.3.1` and `katex@0.16.47`.
339
+ - A local filesystem with normal POSIX ownership and locking semantics for the
340
+ private SQLite directories.
341
+
342
+ `node:sqlite` emits an experimental warning on the tested Node 22.21.0 runtime.
343
+ No native SQLite addon or browser CDN is used. The pinned parser verifies the
344
+ complete immutable module graph, and the pinned KaTeX module provides local
345
+ math rendering under the release namespace.
346
+
347
+ ## Storage and HTTP safety
348
+
349
+ Both SQLite stores require absolute on-disk paths. They create state directories
350
+ with mode `0700` and databases with mode `0600`, and reject symlinks, foreign
351
+ owners/application IDs, insecure permissions, hard-linked database files,
352
+ future schemas, migration checksum drift, integrity failures, and foreign-key
353
+ corruption. SQLite uses `DELETE` journaling, `FULL` synchronous writes, foreign
354
+ keys, `trusted_schema=OFF`, disabled extension loading, a bounded busy timeout,
355
+ and `BEGIN IMMEDIATE` mutations.
356
+
357
+ Raw browser session and CSRF tokens are never stored; `CloudIndexStore` retains
358
+ only SHA-256 digests. An HTTP adapter derives every `accountId` from the verified
359
+ browser session. Browser payloads and public JSON/SSE projections never choose
360
+ or expose that owner identifier.
361
+
362
+ Browser-session admission first removes expired rows. At the per-account cap,
363
+ a successful new login atomically rotates only that account's oldest-issued
364
+ session, with a deterministic digest tie-break; token collisions fail before
365
+ eviction. This keeps sign-in available without deleting another account's
366
+ session or temporarily exceeding the cap.
367
+
368
+ Idempotency rows are bounded closed receipts rather than arbitrary response
369
+ caches. Direct Chat starts a user message and its pending generation in one
370
+ transaction. A durable owner digest plus monotonic fence prevents two cloud
371
+ workers from dispatching the same generation concurrently; a stale worker
372
+ cannot append or finalize after losing its lease.
373
+
374
+ Direct Chat deletion is a distinct exact mutation:
375
+ `POST /api/chat/threads/delete` requires the authenticated browser session,
376
+ CSRF proof, a caller-generated idempotency key, and the exact current
377
+ revision/hash cursor. The store refuses an active generation, a stale cursor,
378
+ or a trailing user message whose send acceptance is unresolved. On success it
379
+ atomically writes a content-free schema-v5 deletion receipt containing only
380
+ identity, cursor metadata, and digests before removing the thread and its
381
+ private descendants. The receipt is immutable, supports an exact retry without
382
+ retaining the raw key or deleted content, and permanently retires that
383
+ account/thread identifier. This route deletes only cloud-owned
384
+ Direct Chat state; deleting an Agent presentation index or an authoritative
385
+ AgInTi thread remains a separate contract.
386
+
387
+ Canonical attachment bytes are durable only in the owner-private Direct Chat
388
+ database. The message ledger and browser API expose a size/dimension/MIME/SHA-256
389
+ descriptor, never the bytes or base64. Authenticated previews are `no-store`
390
+ responses, and image data never enters Cache Storage, localStorage, or
391
+ sessionStorage. IndexedDB is used only for the encrypted, expiring, one-shot
392
+ confirmed-update handoff described above; it is not chat history or a retry
393
+ queue. Base64 exists only transiently in the browser's exact in-memory retry
394
+ ticket and the bounded browser-to-BFF and BFF-to-LocalLLM request bodies. The
395
+ browser serializes a prepared image request once before entering the network
396
+ ambiguity boundary, reuses those exact bytes only when a status probe proves
397
+ the generation absent, and releases the raw images and serialized ticket as
398
+ soon as the server accepts the durable turn.
399
+
400
+ Ordered attachment responses use an explicit message-list schema request. A
401
+ previous PWA that omits it receives the first descriptor in the legacy singular
402
+ shape, so a stale open tab keeps its text/history protocol valid until the
403
+ content-versioned PWA refresh takes control.
404
+
405
+ Schema v5 adds the durable authority receipts required for safe Direct Chat
406
+ thread deletion. It is now the common Direct Chat schema whether vision is
407
+ enabled or disabled, so the ordered v4 attachment tables are materialized on
408
+ upgrade while image use remains fail-closed at the application boundary.
409
+ Existing v3 single-image rows still migrate to ordered position zero. A v5-aware
410
+ build running with vision disabled can serve authenticated previews and exact
411
+ retries of previously committed image turns, but refuses new image turns and
412
+ follow-ups that would reuse stored images.
413
+
414
+ A pre-v5 binary cannot reopen the migrated database. Before activation, block
415
+ every dynamic API, stop the service, verify sidecar-free SQLite `DELETE`
416
+ journal state, take an offline private database backup, and preflight a copy of
417
+ that backup with the candidate release. The snapshot may be restored only
418
+ while all dynamic APIs remain blocked and before any v5 write authority or the
419
+ v5 deletion API is activated. After that boundary, preserve the live v5
420
+ database and use only a v5-aware rollback release; restoring the older snapshot
421
+ could discard accepted messages or deletion authority.
422
+
423
+ The production server is designed to bind on loopback behind Caddy. It trusts
424
+ the configured public authority/client-address headers only from that local
425
+ proxy boundary. It does not terminate public TLS, manage a LazyEdge tunnel, or
426
+ launch LocalLLM/AgInTi/sandboxes itself.
427
+
428
+ ## Checks
429
+
430
+ Run the offline checks with:
431
+
432
+ ```sh
433
+ npm run check
434
+ npm test
435
+ ```
436
+
437
+ These checks do not deploy the package or exercise the blocked live
438
+ Docker/model acceptance gate.