Proto commits in anyproto/anytype-heart

These commits are when the Protocol Buffers files have changed: (only the last 100 relevant commits are shown)

Commit:c6b9267
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7556: membership V2 freshness, ordering and bounded calls The desktop membership page could spin forever after a wake: the startup GetProducts/GetStatus failed on a stale connection, the error dropped cached data, and later successful background polls sent no event because nothing changed. core/payments: - Per-resource V2 state (status, products): persisted provenance and lastSuccessAt, per-start verification, and an (epoch, revision) that identifies exactly one payload across responses and events. - One commit path: outcome ordering (fetchSeq after the limiter; successes and failures ordered separately), field-scoped cache writes before events, revision bump on any data difference, owed recovery events after STALE/NONE answers, in-memory apply when persistence fails. - NoCache calls answer FRESH, STALE (cached data on transient errors) or NONE. 15s budget starts before the status limiter; cache access is ctx-bounded. - Transient-error classifier (deadlines, lost connections incl. drpc's EOF->Canceled, yamux/QUIC/handshake/socket errors); caller cancellation is not an outcome. - Refresh controller: non-blocking force admission for all callers; manual refresh limited in heart (1 admission/30s, 3 min window that never shortens a purchase window, >=10s spacing, sleep-aware deadlines). New MembershipV2GetStatus.forceRefreshSec. - Limits updates no longer run on the response path. - debugstat provider for payment attempts, outcomes and forced polls. core/payments.go: V2 handlers keep the response envelope on errors, no panic when not logged in, explicit NONE. core/files/filesync: non-blocking RequestNodeUsageUpdate trigger; UpdateNodeUsage removed. proto: MembershipV2 FetchState (freshness, lastSuccessfulFetchAt, lastRefreshError, revision) on V2 responses and update events. go.mod: any-sync -> GO-7556 branch head (anyproto/any-sync#801).

The documentation is generated from this commit.

Commit:4e083fc
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7368 Add client pubsub: middleware protocol over any-sync ephemeral pub/sub Expose the any-sync space-scoped pub/sub engine to app clients: - PubsubPublish/PubsubSubscribe/PubsubUnsubscribe RPCs with wildcard topic patterns and subId-keyed subscriptions - Event.Pubsub.Message broadcast with verified sender identity - payloads encrypted with the space's current ACL read key (same key as object changes), decrypted+verified before events are emitted - LAN peers served via the shared DRPC server; publishes go to the responsible node plus LAN peers - docs/pubsub: design doc and client developer guide (topics, cadence, closing messages)

Commit:54401a8
Author:Roman Khafizianov

GO-7542 Report JSON API listen/bind status via event and RPC response Previously, a JSON API bind failure (e.g. "address already in use") was swallowed into a log line only -- no RPC response or event ever reported it. startServer() returned before the detached ListenAndServe goroutine had even attempted to bind, so AccountSelect/AccountCreate/ AccountChangeJsonApiAddr all reported success regardless of outcome. Add a shared Event.Account.JsonApiStatus{success, listenAddr, error}, broadcast on every bind attempt (initial select/create and every ReassignAddress) and also returned directly as AccountChangeJsonApiAddr's new `status` response field, so the caller gets it synchronously while every other open session learns it via the event. net.Listen now runs synchronously so the outcome is knowable at all, which surfaced a real data race: the Serve goroutine read s.httpSrv live instead of a captured value, so a fast reassignment could leave the old listen address reachable, silently routed through the new server's handler. Fixed alongside a socket leak on a constructor panic path and two pre-existing unsynchronized field accesses on apiService, all found by a 3-lens codex review (correctness/concurrency, protocol design, testing) and confirmed with go test -race. See docs/JsonApiListenFailureReporting.md for the full design.

Commit:d94821a
Author:Roman Khafizianov

GO-7383 Add type property RPCs that keep lists, links and views consistent Adding a property to a type took the desktop client three calls — ObjectCreateRelation, BlockDataviewRelationSet, ObjectListSetDetails — and none of them put the property in the type's views. The client rendered it in view settings as a disabled row, and it only became a real column the next time someone opened that screen. On delete the client dropped the dataview's relationLinks but left the views, and the stale column vanished silently the next time view settings opened. Android takes a third route and Swift a fourth: three clients, three ways of editing one list, none of them maintaining the views. Consistency was the client's job, done lazily and differently in each direction. ObjectTypePropertyAdd and ObjectTypePropertyRemove do the whole job in one apply on the type: the property joins the recommended list its section names and leaves the other three, the dataview gains its RelationLink, and every view gains the column at the visibility the request asked for. Remove is the mirror — it reuses the prune that already existed and reports the views it left alone because they group, sort or filter by the property, since dropping a column out from under an arrangement would leave a board grouped by something nobody can see. Add resolves a key against a live relation and refuses one nothing carries, rather than deriving an id for a property that does not exist. An empty key mints from name and format through the path ObjectCreateRelation uses. Everything refusable is refused before anything is created, because a minted relation is a separate object the type's apply cannot roll back. The file section is not offered. recommendedFileRelations is derived by FillRecommendedRelations from a fixed set of file-metadata keys, on the four file types only, and systemobjectreviser recomputes it wholesale — so a value written there is overwritten without notice the next time those types are revised. The wire enum reserves the number rather than offering it, Add refuses it in both directions, and Remove refuses a property sitting in it. ObjectTypeRelationAdd and ObjectTypeRelationRemove are removed. They wrote recommendedRelations and nothing else, and resolved keys through DeriveObjectID, which computes an id without checking that any relation carries it — so a misspelled key silently wrote a dangling reference into the list. Nothing called them: zero call sites in heart, anytype-ts, anytype-swift and anytype-kotlin, which never even built a wrapper. ObjectTypeRecommendedRelationsSet and ObjectTypeRecommendedFeaturedRelationsSet are deprecated rather than removed, because Android calls the first from three screens. They replace the whole list and touch neither relationLinks nor the views, so every caller must do its own read-modify-write — which Android does against a cached copy of the type.

Commit:4910adf
Author:Roman Khafizianov
Committer:GitHub

GO-7395 Local API hardening and file download changes (#3272)

Commit:9264e5d
Author:Roman Khafizianov

GO-7349 Merge latest develop into go-7349-import-llm

Commit:8119a12
Author:Roman Khafizianov

GO-7383 Use one-shot cross-space full-text search Route v1 and v2 global search through ObjectCrossSpaceSearch with grant scoping, full-text queries, lookahead pagination, and partial-result metadata. Simplify endpoint titles and regenerate API documentation.

Commit:e05618b
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7383 Expose API grants and enable compact view IDs Return the persisted approval when issuing API keys and indicate when the spaces list excludes live spaces outside the grant. Pin AnyBlock's compact widget/view IDs and use its object label plan for view listings. Refresh protocol and API documentation, including the Markdown API introduction, and cover the response and ID contracts with regression tests.

Commit:cb5b4e3
Author:Roman Khafizianov

GO-7395 Merge develop and update AnyBlock integration Merge origin/develop at 95e3cf947, including the AnyBlock exporter and its pinned protocol release. Adapt collection documents to collection_items, preserve API type addressing and grant-aware session revocation alongside synchronous RPC account IDs, and regenerate the merged protobuf definitions. Keep explicit Heart authentication and provenance schema extensions covered by compatibility checks. Correct stale chat-count and fragile space-test expectations. Validation: go test -p 4 -json ./... passes across all 238 tested packages; git diff --cached --check passes.

Commit:e768fb0
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7383 Merge develop into AnyBlock exporter

Commit:5fd547c
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7383 Pin AnyBlock and complete export reporting Pin github.com/anyproto/any-block to 12807aa201e3 and remove the local replacement. Carry export reports through RPCs and notifications, preserve source paths, classify non-blocking notes, and include chat objects. Update remote-file metadata integration, export naming, cancellation handling, generated bindings, and regression coverage.

Commit:2e614f9
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7395 Reserve the pairing-code field the renamed events dropped LinkApprovalRequest and LinkApprovalHide start at field 2 because field 1 was 'string challenge' on the LinkChallenge pair they replaced. The number was freed without being reserved, while requestedGrant's 4 was reserved — so the one field that carried the 4-digit code was the one left reusable. Oneof numbers 204 and 205 are deliberately reused, so a desktop build predating the rename decodes these messages with the old descriptor. Today it reads an empty code. Add any string at field 1 later and every such client renders it to the user AS the code to type — which is the exact confusion the rename was chosen to prevent (a distinct name so no client can subscribe to the old code-carrying shape by accident). Verified with protoc: before this, a new field at = 1 compiled silently; now it fails with 'uses reserved number 1', the same way = 4 already did. Found by the 4-lens review.

Commit:2254bd3
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7395 Pairing: the approval carries the grant, apps request only a permission NewChallenge.requestedGrant is gone (field reserved): an app cannot know space ids before pairing, so a space-listed request was unimplementable by the clients it existed for. Apps now declare only requestedPerm — a prompt pre-fill, never a ceiling. ApproveChallenge.Request gains the user's grant: required on JsonAPI approvals (a key with access to nothing is a dead key), forbidden on Limited, validated inside the session service BEFORE any state change so a rejected grant leaves the prompt answerable. The challenge record stores the approved grant beside the code — the solver has no vector to supply or widen it — and solve persists it verbatim. Grant refusals map to BAD_INPUT. Pending TTL grows 60s -> 180s: picking spaces is a real interaction.

Commit:628a38d
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7395 Wallet: allSpaces grants, schema version 2 AppGrant and AppLinkGrant gain a dynamic allSpaces flag (covers spaces created after the approval, GitHub all-repositories semantics). Validation requires exactly one of allSpaces and a non-empty space list, so "all" can never be spelled as "empty" — the empty list keeps denying everything. The grant schema version bumps to 2 for diagnosability: a version-1 binary reading an allSpaces grant fails closed either way, but now names the real problem instead of reading as a corrupt key. The envelope version stays 2. Proto()/AppLinkGrantFromProto carry the flag both ways; a dropped flag would narrow to nothing, fail-closed but a lie.

Commit:7586836
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7395 Approve local-link pairing before minting the code Local-link pairing minted the 4-digit code the moment a challenge was requested and broadcast it to every session, so an unapproved request always had a secret to guess and the per-run counters were the only DoS defence. Rework the flow to approve-then-mint: - StartNewChallenge registers a pending request and mints nothing. The new LinkApprovalRequest event names the caller (process path, browser origin) and carries no code. - AccountLocalLinkApproveChallenge is the user's decision. It is the only place a code is minted and returns it to the approving session alone, never on the event bus. Addressed by (processPath, origin) — the caller the prompt displayed; one pending prompt per caller keeps it unambiguous. - SolveChallenge gates on approved state; a solve against a pending challenge does not count against the failure budget. - Denials are remembered for the app run; pending prompts expire after 60s, approved codes 5 min after approval (SweepExpired). The requesting client's API (/v1/auth/challenges, /v1/auth/api_keys) is unchanged; only the desktop client gains the approval step. Headless deployments use AccountLocalLinkCreateApp, which mints a key with no challenge, so no auto-approve backdoor is added. Security: AccountLocalLinkApproveChallenge stays out of noAuthMethods and limitedScopeMethods so authorization admits full scope alone, and it rejects any caller carrying an Origin header. Both pinned by tests. Incorporates the origin-attribution groundwork it depends on: ClientInfo.origin from the browser's Origin header, process resolution for JSON API callers, and per-caller challenge budgets. Docs: docs/LocalLinkPairingApproval.md (design), docs/LocalLinkApprovalDesktopGuide.md (client integration).

Commit:0430772
Author:Roman Khafizianov

GO-7471 Merge develop: reconcile the start-cancel protocol with the account lease develop landed GO-7477's account directory process lease, which threads its acquire/release through the same three start entry points this branch rewrote, and GO-7492, whose spacecore wiring sits beside this branch's observer lookups. The substantive conflict was the start-cancel protocol. This branch replaced appAccountStartInProcessCancel with beginStart/cancelStart, which supersedes an earlier start, closes the app of a cancelled one, covers the lock wait rather than just the StartNewApp call, and reports whether it cancelled anything — the last of which AccountStop needs to honour RemoveData. That protocol is kept; the lease keeps its named returns, because its deferred cleanup mutates err to release on a failed start. spacecore Init and space's test fixtures take both sides.

Commit:3c7b082
Author:Roman Khafizianov

GO-7471 Correct two client-contract claims that were not true Both the guide and the proto promised phases were monotone apart from the WaitingForNetwork overlay. They are not: the phase is derived from the run's current condition rather than latched, so it moves back when that condition regresses. Losing the last node connection during the account fetch goes FetchingAccount -> Connecting, verified against the tracker. The behaviour is right — we really are connecting again — so the claim is what needed fixing. A client that trusted it and dropped backward transitions would stick on a phase the run had already left. The minimal-client pseudocode dismissed the UI on `done`, but `done` covers both terminals, so a Failed run (AccountDeleted, IncompatibleVersion, …) hid the one message the user needed. Test Failed first, and render stalled spaces as a determinate stall with a retry rather than as progress.

Commit:7b24be7
Author:Roman Khafizianov

GO-7471 Bound the settle, and say which of the two things went wrong Finished required every tracked space to reach Loaded or Error and the completeness gate to open. Both can fail to happen on a healthy app, and the surface then showed progress that could never resolve: - A warm start with no connectivity loads every space from disk but never sees a responsible tech-space diff, so the gate stays shut for a run that is, locally, entirely done. - A space whose load result never arrives holds the whole run open. The captured field case is 79 of 80 loaded with the last space in Pulling, attempt 0, no error. These are different situations and deserve different answers, so the settle bound gives them different ones. When the run has gone quiet with the local view set complete: if every space settled, Finished fires with viewsConfirmed false — ready, but no promise that nothing else is out there. If a space never reported, it becomes Stalled and the run stays open, because "79 of 80, one stalled" is a determinate answer a client can offer a retry for, and an open run still reports the space if it loads later. Claiming Finished there would claim the account is recovered when it demonstrably is not. Any fold that changed something restarts the bound, so a slow but progressing sync never trips it, and a stalled space that finally lands transitions normally and finishes the run.

Commit:130add6
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7383 Use external AnyBlock v2 exporter Routes Export_AnyBlockV2 through the portable bundle composer and path plan in github.com/anyproto/any-block: plan every path single-threaded before anything is written, emit through width-bounded queue tasks, and finish with index.json and properties.json re-read through the package that wrote them. The bundle has five kind directories. Neither a relation option nor the property that owns it is written as a document: a property is not an object a person opens, so everything it means — format, name, vocabulary, and whether it is hidden or removed — travels on its dictionary entry, and a property nothing references is not carried at all (any-block SPEC §2f, §15 #21, §15 #23). The composer states what a bundle could not carry rather than hiding it, and Stats is the only channel it has — an omitted document leaves nothing behind to compare, so a dropped select vocabulary is invisible everywhere else. Discarding Stats made that promise true of the composer and false of the export, so the three loss-bearing fields are logged: the vocabularies the used-only rule dropped, the referenced properties nothing can define, and the vocabularies the dictionary cannot state at all.

Commit:29bdbe2
Author:Roman Khafizianov

GO-7471 Make AccountRecoveryState total The RPC returned ACCOUNT_IS_NOT_RUNNING before AccountSelect and in the race between the RPC and startNewApp reaching Begin — an ordering a client cannot express. Tracker.Snapshot is now total: before the first Begin it is the idle snapshot (empty runId, the new explicit Phase.NotStarted rather than a zero value that would read as LookingForPeers), and a closed or terminal run keeps reporting itself until the next Begin, so Fail-after-Close is unchanged. The handler never errors; the ACCOUNT_IS_NOT_RUNNING code stays in the proto for wire compatibility and is unreachable. The client guide replaces its ordering table with the contract: call the RPC whenever you like; an empty runId means no run; a subscribing client never needs it except mid-run attach or an id gap.

Commit:d572a2c
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7471 Report the LAN space exchange and derive a local-peers headline Connecting to a LAN peer says nothing about whether it holds the account: the recovery stream showed PeerConnected for a LAN peer while P2PStatusUpdate reported no devices, because that surface reads the peer store, which the space exchange populates after the connection. The tracker now observes peerstore (the line after the exchange on both the outbound and inbound paths) and reports the answer as a fact — PeerSpaceExchange{peerId, exchanged, hasAccountSpace, sharedSpaceCount} — rather than a verdict: until GO-7492 a cold device exchanges zero tokens and every LAN peer answers "nothing shared", so a field named "account not found" would be wrong exactly where it matters. The fold derives LocalPeersState over every LAN peer: the negative verdict surfaces only once every connected peer has answered and none holds the account, and one positive answer flips it at once. The client guide says not to surface "not found on this device" copy until GO-7492 ships.

Commit:c8d4cee
Author:Roman Khafizianov

GO-7471 Fold space discovery, load results and finalization into the tracker A space is finalized when its space controller publishes its load result: spaceloader forwards startLoad (optimistic on-disk fast path => Loaded at once, the loader never publishes Loading for it) and onLoad (Ok => Loaded, Missing => Error, a cancelled build => no transition, its own deletion verdict as a bool). Discovery keys off the tech-space SpaceView subscription only — a space pushed by a LAN peer produces no pull events — and a view deleted while recovering emits Removed and leaves the run. Regular-space pull progress folds to Pulling. Finished needs the account ready, every tracked space Loaded or Error, and the SpaceView-completeness gate: the tech space's diff against a responsible node (treesyncer's SyncAll, consumed for the tech space only) with every missing id resolved by an arriving view or a later diff, or two consecutive diffs that resolve nothing — then with viewsConfirmed=false. After Done every producer is a no-op, so the snapshot is frozen and the replay property holds end to end over a full cold recovery. Claude-Session: https://claude.ai/code/session_01DmWXGJpgSYDjfbyekn6TWN

Commit:d6a3280
Author:Roman Khafizianov

GO-7471 Fold the account fetch into the recovery tracker The tracker learns the tech space id from space.Init, before Run pulls it, so the tech-space SpacePull is labelled as the account fetch while it happens: PullEventWaiting opens a round (AccountFetchStarted, and a second Waiting is the only sign the 15s bounded load gave way to the unbounded retry), Attempt names the peer, a failed Result classifies in the account context (deleted space => AccountDeleted, missing space => AccountNotFound). AccountReady follows every close(techSpaceReady) — load, create, and the create-for-old-accounts fallback — exactly once, and enters the tech space into the space log as Loaded. spacecore injects the tracker as commonspace.Deps.PullObserver in loadSpace, covering every space type. core/application joins the exported recovery.ErrAccountNotFound into a start error that carries space.ErrSpaceNotExists, so classification stays in one place without core/recovery importing the space tree. Claude-Session: https://claude.ai/code/session_01DmWXGJpgSYDjfbyekn6TWN

Commit:979bf88
Author:Roman Khafizianov

GO-7471 Fold the peer connection lifecycle into the recovery tracker net/peerobservermux fills the single peerobserver slot with a fan-out; the recovery tracker registers with it at Init and folds DialStarted, Connected, DialFailed and Closed by counting open connections per peer (clamped at zero, never a latched bool), with kind and node types from nodeconf. WaitingForNetwork needs zero open connections, a dial failure observed while nothing was open, and ten seconds of that outage — an idle-TTL close alone never triggers it — or the device reporting offline. The first incompatible-version dial latches that class. LAN discovery possibility and LAN peers are forwarded from localdiscovery's hook and from spacecore.PeerDiscovered, which already owns the single SetNotifier slot. Mode gains a never-emitted ModeUnknown = 0 so an unset field cannot read as a cold recovery; PeerDiscovered and PeerDisconnected carry kind and node types so a replayed log matches the snapshot exactly. Claude-Session: https://claude.ai/code/session_01DmWXGJpgSYDjfbyekn6TWN

Commit:8b3bcae
Author:Roman Khafizianov

GO-7471 Add Event.Account.Recovery and Rpc.Account.RecoveryState protos Wire surface for the account start-up status stream: the Update event (monotonic ids, runId, typed payloads), the folded Snapshot, and the RecoveryState RPC. The handler returns ACCOUNT_IS_NOT_RUNNING until the core/recovery fold lands. The nested error message is ErrorInfo rather than Error: protobuf enum values share the enclosing scope, and SpaceState.Error already takes that name. Claude-Session: https://claude.ai/code/session_01DmWXGJpgSYDjfbyekn6TWN

Commit:3268123
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7383 Add API v2 object and editing services

Commit:a6fbdb8
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7383 Use external AnyBlock v2 exporter

Commit:2915c2f
Author:Roman Khafizianov

GO-7477 Add account directory process lease

Commit:c80d87b
Author:Roman Khafizianov

GO-7349 comments: finish the sweep, including the wire docs The rest of the citations, rewritten by hand rather than by pattern: where a section number stood in for a concept the concept is named (the crawl-resume class, the frozen core, the drift rule, the array form for table cells), and where it was the subject of the sentence the sentence now states the rule. The proto comments went with them, so docs/proto.md is regenerated: AIParams no longer points clients at a design note that this branch deleted, and Import.Statistic explains the no-blended-percentage rule instead of citing the paragraph that argued it. Two spec references survive on purpose — SPEC.md and ImportV2Design.md by name, without a paragraph. Those documents exist; their numbering does not survive an edit.

Commit:346eea6
Author:Roman Khafizianov

GO-7349 comments: say the thing, not the section number Nearly 460 comment lines cited a document paragraph — mostly of the design notes this branch just consolidated, so the citations pointed at files that no longer exist. Where the section number was the whole parenthetical it goes; where it stood in for a concept the concept is named instead (the progress contract, the frozen manifest core, the crawl-resume class); where a comment leaned on a doc for its meaning it now says the rule itself. References to living documents stay, minus the paragraph number: a reader can find ImportV2Design.md, and a section number would go stale the next time that file is edited.

Commit:c9c973f
Author:Roman Khafizianov

GO-7349 Merge develop into the import branch Brings in 62 develop commits (up to 7e8f5535a). Four generated protobuf files conflicted and were regenerated with `make protos` rather than hand-merged; the .proto sources merged cleanly and both sides' additions survive (AI.ListModels beside ObjectDeletionAudit, Import.Statistic = 147 beside objectCleanupSuggestion = 146). The one semantic conflict was MakeDataviewContent, where develop landed its own fix for the GO-5969 column-visibility regression (660a8ce6d) while this branch fixed the same bug differently. Git combined both texts silently, which left develop's comment describing behaviour the merged code no longer had. Resolved in develop's favour: explicitly passed relLinks are the visible set, ot alone is a fallback column source whose columns stay hidden. That covers the case that mattered here — a type's own view passes both, so its properties are visible columns — and keeps the decision develop just made authoritative. The branch's own addition, ReconcileTypeDataviewColumns, is orthogonal and stays: it repairs views built before the fix and views built while a property's relation object was still being written, which the import's concurrent workers produce. One reconciler test built its fixture with ot alone and so assumed the dropped semantics; it now passes both, mirroring what objecttype.go actually calls. Verified still load-bearing by mutation.

Commit:49b6fcb
Author:Roman Khafizianov

GO-7349 importv2: AI.ListModels validates a provider and lists its models Adds anytype.Rpc.AI.ListModels: one RPC that both validates a ProviderConfig (base URL + token) and returns its model catalog — a successful list already proves the endpoint is reachable and the token works, so there is no separate validate RPC. Error codes reuse the existing AI vocabulary (ENDPOINT_NOT_REACHABLE, AUTH_REQUIRED, RATE_LIMIT_EXCEEDED, MODEL_NOT_FOUND, BAD_INPUT). llmclient gains: - ListModels(ctx, Config) — calls GET /v1/models through the existing go-openai client/transport, mapped through the same error sentinels CompleteJSON uses. - FromProtoForListing — a sibling to FromProto for the case a listing call has no model yet (FromProto's ok=false-on-empty-model contract is left untouched; its only caller, the importv2 planner, relies on it meaning "feature off"). - FilterChatModels — for OPENAI only, narrows the catalog to chat-completion models via a documented id deny-list. Verified GET /v1/models carries no capability/modality field for any of OpenAI/ollama/LM Studio/llama.cpp (see the doc comment on nonChatModelIDPatterns for the evidence), so an allow-list would silently drop future models; OLLAMA/LMSTUDIO/LLAMACPP catalogs are returned unfiltered since those only ever list what the user chose to host. core/ai.go wires AIListModels: FromProtoForListing -> ListModels -> FilterChatModels when provider is OPENAI. Regenerated pb/*.pb.go, pb/service, clientlibrary/service and docs/proto.md via make protos.

Commit:118a9a4
Author:Roman Khafizianov

GO-7383 Export.Format gains AnyBlockJSON (proto only) The native exporter has had no way to be ASKED for: a client calls ObjectListExport with a model.Export.Format, and the enum stopped at GRAPH_JSON = 5. This adds AnyBlockJSON = 6 and regenerates. Additive by construction — every existing value keeps its number, so a client that has never heard of the format is unaffected, and one that sends 6 against an older middleware gets the unknown-format path rather than a silently different export. Generation was validated before the edit rather than after: `make setup-protoc-go` builds the fork the repo pins (go.mod replaces gogo/protobuf with anyproto/protobuf), and a dry `make protos-go` against an unmodified tree produced ZERO changed files — so the local toolchain reproduces the committed output exactly and nothing in this diff is generator drift. The generated diff is accordingly the enum and nothing else: 521 lines of gzipped FileDescriptorProto (11242 → 11250 bytes), 14 enum-map lines re-aligned because "AnyBlockJSON" is longer than "GRAPH_JSON", and the five lines of the constant itself. Routing is deliberately NOT in this commit: nothing yet maps the new value onto the exporter, so requesting it today behaves exactly as an unknown format did. That keeps the regenerated protobuf reviewable on its own.

Commit:0b0144f
Author:Roman Khafizianov

GO-7453 ChatSearch: optional message creator filter Rpc.Chat.Search.Request gains repeated creators (empty = all authors), honored in every scope: - FT path: Must-nested term clauses on the raw-indexed Author field (exact, case-sensitive identity match; boost 0) — scoping at the query keeps the candidate budget on the requested authors' messages instead of post-filtering them out of a shared page. Empty creator strings are ignored (a raw empty term would compile to a match-nothing clause). Hydration backstops the clause for stale FT docs. - Browse path (empty fullText): chatrepository.GetLastMessagesByCreators filters in the store query, newest selected, ascending like GetLastMessages (queryMessages reverses for chat-display order).

Commit:2b8863a
Author:Roman Khafizianov

GO-7449 GO-7452 Default browse sort: lastModifiedDate desc when no sorts and no text Clients' browse requests (filters+limit only, observed from desktop) were getting an arbitrary id-ordered prefix per space: without a sort the per-space candidates are whatever natural order yields first and the merge falls back to the id tiebreak. Mirror chat search's browse default: empty sorts + empty fullText now sort by lastModifiedDate desc — served by the existing per-space lastModifiedDate index, so the per-space candidates become the newest offset+limit objects and the merged page is the true vault-wide recents list. Also dedupe an accidentally twice-inserted test and document in the proto that an unlimited request materializes every space in full.

Commit:447b55c
Author:Roman Khafizianov

GO-7449 GO-7452 Apply 3-lens review fixes: per-space FT scoping, merge-order fidelity, determinism Review round (3 independent lenses, findings measured against real tantivy): - CRITICAL: perSpaceQuery never set SpaceId, so every space ran the same global fulltext search, resolved the global candidate list against its own store and dropped everyone else's hits — spaces whose matches don't rank in the global candidate page silently returned nothing (measured: 5 of 6 spaces absent), each starving space escalating to the 2000-doc hard cap (measured 1.4s vs 7ms for 15 spaces). Now scoped per store. Paired fix: the ftsearch space clause gets boost 0 — at boost 1 its IDF added a per-space additive bias (larger for smaller spaces) that made _final_score incomparable across spaces; within one space it was a constant, so per-space ranking is unchanged. - Merge-order fidelity: the merge now reproduces exactly the order each store cut its candidates with — database.InjectDefaultOrder (implicit score-first order of fulltext queries) + exported database.NewSetOrder (includeTime resolution, custom orders) — instead of bare NewKeyOrder over the raw request sorts. Measured bugs fixed: fulltext+sorts pages duplicated and permanently hid records (top-K property violated), date sorts collapsed to day granularity and grouped by space. - Option/object order maps resolve across all queried stores via a union wrapper instead of one random representative space (foreign options produced empty sort keys and clumped). - Determinism: the store snapshot is sorted (map iteration order leaked into every tie) and the merge comparator ends on the object id — offset paging returned duplicates in ~15% of runs before. - allStoresLoaded now also turns false when a space's store failed and was skipped — it is the client's only completeness signal. - Tech space and marketplace are excluded (parity with the fulltext iteration and the cross-space subscription; spaceView/participant objects leaked into results). - ctx plumbed through (cancelable between spaces), negative paging clamped (offset==-limit used to disable the per-space bound), filters cloned per space (nested-filter injection mutated the shared slice). Regression tests: FT candidate survival across a noisy space, same-day date interleaving, repeat-call determinism + no-duplicate paging, tech space exclusion, negative-paging clamp.

Commit:455507f
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7449 GO-7452 One-shot cross-space ObjectSearch without subscription New ObjectCrossSpaceSearch RPC: query the spaces whose object stores are loaded at the moment of the call, with no subscription machinery and no waiting for the store warm-up (stores load sequentially on app start). The response reports allStoresLoaded so the caller knows when records cover only a partial space set — retry later for the complete view, or use ObjectCrossSpaceSearchSubscribe which streams later-loading spaces. objectstore.QueryCrossSpaceNoWait: snapshots the loaded stores (warm-up flag read before the snapshot so true always means a complete set), skips failing spaces instead of aborting, bounds per-space candidates by offset+limit and applies paging after the merge. Merged order: requested sorts (formats resolved via one store — bundled relations are identical across spaces), else the injected _final_score for fulltext queries.

Commit:f173ef8
Author:Roman Khafizianov

GO-7449 Browse mode: empty fullText returns the latest messages in scope Empty query = the search screen's entry state: newest messages of every chat in scope, served from the message stores instead of FT (no indexing lag — a just-sent message appears immediately). Chats are enumerated from the object index (chatDerived|discussion, not deleted; cross-space query for the vault scope); each contributes its newest offset+limit messages by orderId, merged and sorted — default CREATED_AT desc, since scores don't exist without a query. Failing chats are skipped in multi-chat scopes, same as search.

Commit:da53491
Author:Roman Khafizianov

GO-7449 Apply 3-lens review fixes: store-backed liveness gate, error isolation Review round (3 independent lenses) findings applied: - F1 (high): the preview registry covers only chatDerived, but message docs are FT-indexed for discussion (object chats) too — the space scope silently dropped object-chat hits. The gate is now isLiveChat: the per-space object index is queried for layout chatDerived|discussion and not-deleted, which also removes the Search dependency on the registry entirely. - F2 (major): one failing chat (space mid-deletion, broken store) aborted the whole multi-chat search and could resurrect a deleted space's crdt.db via a stale registry entry — multi-chat scopes now log and skip the chat; the single-chat scope keeps the hard error. - F3 (major): chatRegistrySnapshot serialized search behind the s.lock hold that Run keeps across the cross-space subscription bootstrap — gone with the registry. - SpaceId attribution: hits now carry the doc's stored SpaceID field (DocumentMatch.SpaceId), so results are attributable in every scope, including chatId-set/spaceId-empty. - ftLimit capped at 2000 (ftCandidatesHardLimit parity), bare error return wrapped, proto comment aligned with actual scope semantics (ORDER_ID note moved to sorts), ForceFTIndex comment no longer oversells freshness. Regression tests: discussion-layout inclusion, non-chat layout exclusion, broken-chat survival, stored-space attribution. Spec updated accordingly.

Commit:32baa8c
Author:Roman Khafizianov

GO-7449 Add spaceId to chat search results, document search scopes model.Search.Message.Result gains spaceId so hits are attributable when the search spans multiple chats or spaces. Chat.Search.Request comments document the new scope semantics: empty chatId = all chats in spaceId, empty spaceId = all spaces.

Commit:20f452d
Author:Roman Khafizianov

GO-7383 Provenance records the RAW app name, compared exactly (spec §5 revised) The stamped and compared value becomes the raw session AppName; the proto field is renamed to say what it holds — pb.Change.integrationName / ChangeNoSnapshot.integrationName, wire number 10 unchanged. The slug normalization is dropped at the root: it was many-to-one (F2, executed — a key paired as "Claude/Desktop" archived "Claude Desktop"'s output while the consent dialog showed two different strings) and lossy (F3, executed — Cyrillic/CJK/emoji names slugged to "", leaving those keys' objects permanently unprovenanced). The tolerance normalization bought (re-pair as "claude desktop" still matched) is deliberately given up: for an authorization comparison, tolerance is a liability. IntegrationKeyFromAppName is DELETED, not parked: with the raw name on the change it has no caller, and attribution's integration object will hash the raw name for its unique key (display comes from the name detail), so the slug is not its future helper either — keeping it would be a rival spelling authority. What remains in core/domain (integrationname.go): the ctx carrier, renamed CtxWithIntegrationName/IntegrationNameFromCtx, and the new bound. The bound: no length limit existed anywhere on AppName, and the raw name now rides every creating change and debug exports — CreateApp and the challenge flow (effective name: requested, else process name) reject names over domain.MaxIntegrationNameLen (128 bytes); reject, never truncate. The §11.7 empty-name guard is now sufficient as issued: no non-empty name can lose its record. Regression pins, each verified to fail under a simulated revert: middleware installs slug-hostile names raw ("Claude/Desktop", привет, \U0001f642); DELETE refuses the F2 pair "Claude Desktop" vs "Claude/Desktop" naming both; the F3 shape (both sides привет) archives; issuance refuses 129 bytes and mints 128. Spec: APIV2_OBJECT_DELETE.md §5 rewritten (the slug argument replaced by the revision record), §8/§10/§11/§12/§13/§14/§15 made consistent; APIV2.md §8.42 rule paragraph updated. protoc regen moved only pb/changes.pb.go + docs/proto.md.

Commit:a2a4191
Author:Roman Khafizianov

GO-7383 Provenance records the RAW app name, compared exactly (spec §5 revised) The stamped and compared value becomes the raw session AppName; the proto field is renamed to say what it holds — pb.Change.integrationName / ChangeNoSnapshot.integrationName, wire number 10 unchanged. The slug normalization is dropped at the root: it was many-to-one (F2, executed — a key paired as "Claude/Desktop" archived "Claude Desktop"'s output while the consent dialog showed two different strings) and lossy (F3, executed — Cyrillic/CJK/emoji names slugged to "", leaving those keys' objects permanently unprovenanced). The tolerance normalization bought (re-pair as "claude desktop" still matched) is deliberately given up: for an authorization comparison, tolerance is a liability. IntegrationKeyFromAppName is DELETED, not parked: with the raw name on the change it has no caller, and attribution's integration object will hash the raw name for its unique key (display comes from the name detail), so the slug is not its future helper either — keeping it would be a rival spelling authority. What remains in core/domain (integrationname.go): the ctx carrier, renamed CtxWithIntegrationName/IntegrationNameFromCtx, and the new bound. The bound: no length limit existed anywhere on AppName, and the raw name now rides every creating change and debug exports — CreateApp and the challenge flow (effective name: requested, else process name) reject names over domain.MaxIntegrationNameLen (128 bytes); reject, never truncate. The §11.7 empty-name guard is now sufficient as issued: no non-empty name can lose its record. Regression pins, each verified to fail under a simulated revert: middleware installs slug-hostile names raw ("Claude/Desktop", привет, \U0001f642); DELETE refuses the F2 pair "Claude Desktop" vs "Claude/Desktop" naming both; the F3 shape (both sides привет) archives; issuance refuses 129 bytes and mints 128. Spec: APIV2_OBJECT_DELETE.md §5 rewritten (the slug argument replaced by the revision record), §8/§10/§11/§12/§13/§14/§15 made consistent; APIV2.md §8.42 rule paragraph updated. protoc regen moved only pb/changes.pb.go + docs/proto.md.

Commit:b4920dd
Author:Roman Khafizianov

GO-7383 pb.Change/ChangeNoSnapshot: integrationKey = 10, copied on the no-snapshot read path The change-level provenance field the object-DELETE rule reads and integration attribution later shares (APIV2_OBJECT_DELETE.md §11.1): a normalized slug of the paired app's name, stamped by heart from the session. Both messages take wire number 10 — they share numbers by design (content 3, fileKeys 6, timestamp 7, version 8, changeType 9; 1/2/5 are historical and stay unused). The ChangeNoSnapshot→Change conversion copies the field so a value written at creation is not silently dropped on every subsequent read.

Commit:b810b2b
Author:Roman Khafizianov

GO-7383 pb.Change/ChangeNoSnapshot: integrationKey = 10, copied on the no-snapshot read path The change-level provenance field the object-DELETE rule reads and integration attribution later shares (APIV2_OBJECT_DELETE.md §11.1): a normalized slug of the paired app's name, stamped by heart from the session. Both messages take wire number 10 — they share numbers by design (content 3, fileKeys 6, timestamp 7, version 8, changeType 9; 1/2/5 are historical and stay unused). The ChangeNoSnapshot→Change conversion copies the field so a value written at creation is not silently dropped on every subsequent read.

Commit:d55c570
Author:Roman Khafizianov

GO-7349 importv2: the §15 pull surface — poll a run that has no engine ObjectImportRunStatus / ObjectImportRunList (spec §15.5), plus the full Event.Import.Statistic wire contract (§15.2: separate page/file counters, the three-state running/throttled/retrying model, cancel semantics, safeToClose — and deliberately NO blended overall-percentage field: fetching is pacer-bound at ~1.5 items/s, creating runs at persist speed, any blended bar crawls then leaps). - dormant runs — a crashed process's dir awaiting the sweep, a suspended run — are served from manifest + ledger ALONE, §15.4's right-hand column: counters via the same resume.Load reading the restart uses, totals from a snapshot-free spool census, cancelEffect/safeToClose from the sticky materialize marker, live issue counts from the durable ledger. This is what makes polling by importId restart-proof: the id is the runId, minted at beginRun, stable across incarnations. - live runs are served from the running run's own store handle (a live-status registry keyed by runId, registered at beginRun / resumeRun and settled on every lifecycle path) — never by second-opening a live db under its writer. - enumeration is a sibling RPC, not an empty-id overload (§15.5); a sick dir degrades to a logged skip, never fails the listing. - proto regen round-trips clean: a no-op make protos-go/protos-server produced zero diff before the change, so the generated-file diff is exactly the two RPCs and the event message. Field 147 (146 and below taken, 144 left for its neighbor gap). Scope, stated in the spec (§15.7 as-built note): the PUSH producer — the coalescing emitter, pacer/retry hooks in the Notion client, converter-side ANALYZING/currentItem, bytes, rate/ETA — is the §15 remainder. The event message ships now so the contract cannot move under it; until the producer lands, the in-memory-only fields are served zero/empty and nothing emits on the stream.

Commit:58b80d9
Author:Roman Khafizianov

GO-7433 Add ObjectDeletionAudit RPC Lists what was removed from a space, newest first, with who created and who removed each one. Two sources, merged into one chronological list. Deleted objects come from the space settings tree, the space-level log every deletion is recorded in. Each of its changes is signed, so it carries the deleter's identity and the deletion time, and storage keeps those changes forever since tree reduction only prunes the in-memory tree. The walk starts at the root rather than resuming from the last change seen: a settings change synced from another device attaches under its own parent, which can sit behind wherever the previous walk stopped, and iterating from there would step over it forever. The mark is the tree's heads, checked from one head-storage lookup, so an unchanged tree costs nothing and never builds the tree at all. Types, properties, relation options and templates come from their own trees. These are never actually deleted — deleteDerivedObject sets isUninstalled and leaves the tree intact — so they never reach the settings tree. That leaves the evidence in better shape, not worse: isUninstalled is an ordinary synced detail, so setting it produced a signed change whose identity and timestamp are exactly who and when, and the object keeps its name, which no destroyed object can. The last change touching the relation wins, in either direction: tracking only the trues would pin whoever uninstalled it before the most recent reinstall. isUninstalled discriminates the two kinds in the response. Only one is reversible, and presenting them identically misstates what happened. Client implementation plan: anytype-ts/docs/plans/2026-08-09-deletion-audit-desktop.md

Commit:f15d6e8
Author:Roman Khafizianov

GO-7383 API v2: scoped API keys — auth hygiene, the grant, and enforcement An API key can now be granted {spaces x read|readwrite}. No existing key breaks: enforcement is /v2-only, so /v1 behaves exactly as it ships today, and legacy keys are grandfathered and signalled rather than refused. P0, the pre-existing holes that made scoping meaningless until fixed: - H1 SolveChallenge minted the pairing session with the zero-valued named return (Limited) instead of challenge.scope, so a gRPC-paired JsonAPI client got a live Limited gRPC session in any space. - H2 the HTTP gate never consulted scope; a clipper key opened the whole JSON API. The gate now refuses non-JsonAPI/Full keys, on /v2 only. - H3 CreateApp persisted any scope, Full included; now rejected. - H4 revocation closed one session per app hash and missed tokens derived via WalletCreateSession(token:). It now sweeps the full token set through a reverse index, and ReadAppLink re-derives AppHash (it is json:"-", so every HTTP-minted session had been filed under "" and survived revoke). - H5 ExpireAt was stored and never enforced; now checked at mint and per request, evicting the cached entry. P1, the grant: - AppLinkGrant lives inside the sealed, HMAC-bound, signed envelope, at file version 2 so an older binary refuses a scoped key rather than honoring it unscoped. Validation is one persist-time gate, re-run at read. - New JSON-API keys mint as anytype_<52 base32>_<8 hex crc32>. The body alphabet excludes base64's + / = so the key survives \b-anchored scanner rules, query strings, double-click and shell quoting. Parsing strips the prefix and hashes the same raw bytes, so every existing key resolves. Format is never an authorization input; the grant is the sole authority. - Enforcement: apiv2.ensureSpaceGrant on the /v2 group over an explicit 46-entry route registry (verb x global class), fail-closed on anything unregistered; the tech space denied unless granted; granted keys refused on /v1; global search and space listing intersect their INPUT set so a non-granted space cannot leak through a per-space warning; a service-layer backstop carries both the space and verb halves at every write entry. - GET /v2/auth/whoami derives from the same grant record the gate reads — one derivation, so the mirror cannot drift from the gate. grant.scoped is an explicit boolean: spaces:null for a legacy key would be a fail-open trap. WWW-Authenticate accompanies auth failures. - Legacy keys get Anytype-Key-Status/Anytype-Notice and a Link rel="deprecation", never RFC 9745 Deprecation/Sunset: those require a Date and scope the signal to the resource, which on /v1 would assert the opposite of the grandfathering promise. - docs/secret-scanning/ ships detection-only gitleaks and TruffleHog rules on the published range pattern. Grants are issuable and editable over the gRPC app-link surface (CreateApp, the new UpdateApp, and a requestedGrant honored at SolveChallenge); the desktop picker rides the separate challenge rework.

Commit:219d754
Author:Roman Khafizianov

GO-7383 API v2: scoped API keys — auth hygiene, the grant, and enforcement An API key can now be granted {spaces x read|readwrite}. No existing key breaks: enforcement is /v2-only, so /v1 behaves exactly as it ships today, and legacy keys are grandfathered and signalled rather than refused. P0, the pre-existing holes that made scoping meaningless until fixed: - H1 SolveChallenge minted the pairing session with the zero-valued named return (Limited) instead of challenge.scope, so a gRPC-paired JsonAPI client got a live Limited gRPC session in any space. - H2 the HTTP gate never consulted scope; a clipper key opened the whole JSON API. The gate now refuses non-JsonAPI/Full keys, on /v2 only. - H3 CreateApp persisted any scope, Full included; now rejected. - H4 revocation closed one session per app hash and missed tokens derived via WalletCreateSession(token:). It now sweeps the full token set through a reverse index, and ReadAppLink re-derives AppHash (it is json:"-", so every HTTP-minted session had been filed under "" and survived revoke). - H5 ExpireAt was stored and never enforced; now checked at mint and per request, evicting the cached entry. P1, the grant: - AppLinkGrant lives inside the sealed, HMAC-bound, signed envelope, at file version 2 so an older binary refuses a scoped key rather than honoring it unscoped. Validation is one persist-time gate, re-run at read. - New JSON-API keys mint as anytype_<52 base32>_<8 hex crc32>. The body alphabet excludes base64's + / = so the key survives \b-anchored scanner rules, query strings, double-click and shell quoting. Parsing strips the prefix and hashes the same raw bytes, so every existing key resolves. Format is never an authorization input; the grant is the sole authority. - Enforcement: apiv2.ensureSpaceGrant on the /v2 group over an explicit 46-entry route registry (verb x global class), fail-closed on anything unregistered; the tech space denied unless granted; granted keys refused on /v1; global search and space listing intersect their INPUT set so a non-granted space cannot leak through a per-space warning; a service-layer backstop carries both the space and verb halves at every write entry. - GET /v2/auth/whoami derives from the same grant record the gate reads — one derivation, so the mirror cannot drift from the gate. grant.scoped is an explicit boolean: spaces:null for a legacy key would be a fail-open trap. WWW-Authenticate accompanies auth failures. - Legacy keys get Anytype-Key-Status/Anytype-Notice and a Link rel="deprecation", never RFC 9745 Deprecation/Sunset: those require a Date and scope the signal to the resource, which on /v1 would assert the opposite of the grandfathering promise. - docs/secret-scanning/ ships detection-only gitleaks and TruffleHog rules on the published range pattern. Grants are issuable and editable over the gRPC app-link surface (CreateApp, the new UpdateApp, and a requestedGrant honored at SolveChallenge); the desktop picker rides the separate challenge rework.

Commit:e6fadf0
Author:Roman Khafizianov

GO-7349 importv2 llm: AIParams on the import request (proto) Optional BYOK enrichment config: AIParams submessage (field 16) wrapping the existing Rpc.AI.ProviderConfig plus the includeContentSamples flag (docs/ImportV2LLM.md §9). Absent = feature off.

Commit:6d4f18b
Author:Roman Khafizianov

GO-7349 Merge remote-tracking branch 'origin/go-7383-anyblockjson' into go-7349-import-llm # Conflicts: # core/indexer/fulltext.go # core/indexer/fulltext_test.go # go.mod # pb/events.pb.go

Commit:c572323
Author:Roman Khafizianov

GO-7395 Approve local-link pairing before minting the code Local-link pairing minted the 4-digit code the moment a challenge was requested and broadcast it to every session, so an unapproved request always had a secret to guess and the per-run counters were the only DoS defence. Rework the flow to approve-then-mint: - StartNewChallenge registers a pending request and mints nothing. The new LinkApprovalRequest event names the caller (process path, browser origin) and carries no code. - AccountLocalLinkApproveChallenge is the user's decision. It is the only place a code is minted and returns it to the approving session alone, never on the event bus. Addressed by (processPath, origin) — the caller the prompt displayed; one pending prompt per caller keeps it unambiguous. - SolveChallenge gates on approved state; a solve against a pending challenge does not count against the failure budget. - Denials are remembered for the app run; pending prompts expire after 60s, approved codes 5 min after approval (SweepExpired). The requesting client's API (/v1/auth/challenges, /v1/auth/api_keys) is unchanged; only the desktop client gains the approval step. Headless deployments use AccountLocalLinkCreateApp, which mints a key with no challenge, so no auto-approve backdoor is added. Security: AccountLocalLinkApproveChallenge stays out of noAuthMethods and limitedScopeMethods so authorization admits full scope alone, and it rejects any caller carrying an Origin header. Both pinned by tests. Incorporates the origin-attribution groundwork it depends on: ClientInfo.origin from the browser's Origin header, process resolution for JSON API callers, and per-caller challenge budgets. Docs: docs/LocalLinkPairingApproval.md (design), docs/LocalLinkApprovalDesktopGuide.md (client integration).

Commit:46e015d
Author:Roman Khafizianov

GO-7379 Merge develop; regenerate protos Only the generated pb/commands.pb.go conflicted (pb/protos/commands.proto merged cleanly); resolved by re-running make protos-go over the merged sources. networkId field intact.

Commit:d4a4092
Author:Roman Khafizianov

GO-7222 Share an anyoneCanJoin invite only as a reader's link Nobody approves a join made through an anyoneCanJoin link: whoever holds it is in the space, with the permissions the invite carries. Read access is as much as a member is trusted to give away, so an invite that grants more than that stays in the owner's account. domain.ShareableWithinSpace is the rule, and the three ways to arrive at a shared invite that grants too much all go through it: generating one, publishing one the owner holds, and raising the permissions of one that is already shared. Each is refused with INVITE_NOT_SHAREABLE.

Commit:bc61498
Author:Roman Khafizianov

GO-7222 Publish an owner-held invite instead of replacing it InviteGenerate for an invite of the type the space already has now decides on the sharing mode without touching the acl. Asking for shareWithinSpace on an invite the owner holds publishes it: the same cid and key move out of the owner's space view and into the workspace, no acl record is replaced and no new file is stored, so the link the owner already handed out keeps working. The other way round is refused with INVITE_ALREADY_SHARED. The workspace's change history has already given that invite's cid and key to every member of the space, and clearing the details would not take it back -- only revoking the invite does, and that is the client's call to make.

Commit:78141f0
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7222 Keep space invites in the owner's account unless shareWithinSpace Rpc.Space.InviteGenerate gets a shareWithinSpace flag. Without it the invite's cid and key go to the owner's space view, which only their own devices sync, and the workspace -- which every member of the space syncs -- is left with nothing but the spaceInviteHeldByOwner marker. Members read the marker through InviteGetCurrent as heldByOwner with an empty cid and key: they learn an invite exists and that the owner is the one to ask for the link. With the flag set, the invite goes to the workspace as before and any member can share it. Generate and Revoke always write both objects, so switching a space between the two can never leave the previous invite readable where it used to live.

Commit:a13b4fa
Author:Roman Khafizianov

GO-7379 Add networkId to DeviceNetworkStateSet for instant same-type switch recovery The type enum alone cannot express 'same type, different network' (Wi-Fi->Wi-Fi SSID switch, cellular PDP re-attach, VPN reconnect), so those OS callbacks were deduped away and recovery waited on the 5s interface poll or transport keepalives. Clients now pass the OS path identity (iOS: NWPath interfaces/gateways digest; Android: Network#getNetworkHandle()) and an identity change triggers the connectivity recovery pipeline immediately. Backward compatible: empty networkId keeps type-only semantics; the identity only counts as changed when known both before and after, so older clients see no behavior change.

Commit:92f61c4
Author:Roman Khafizianov

GO-7368 Add client pubsub: middleware protocol over any-sync ephemeral pub/sub Expose the any-sync space-scoped pub/sub engine to app clients: - PubsubPublish/PubsubSubscribe/PubsubUnsubscribe RPCs with wildcard topic patterns and subId-keyed subscriptions - Event.Pubsub.Message broadcast with verified sender identity - payloads encrypted with the space's current ACL read key (same key as object changes), decrypted+verified before events are emitted - LAN peers served via the shared DRPC server; publishes go to the responsible node plus LAN peers - docs/pubsub: design doc and client developer guide (topics, cadence, closing messages)

Commit:ce9ca6d
Author:Roman Khafizianov

GO-7349 importv2: import report page + API surfacing (§16.1, decision §13.7) The issue ledger now reaches users instead of dying in Result.Issues. A run that produced warning-or-worse issues persists one report page in the space through the normal pool: a summary table (severity x code x count) and a toggle per group with a line per issue, mention-linked to the created object when its source key resolves (marks carry source keys so the resolver rewrites them like any other ref). The page is the primary surface - clients render a discard button on it; it is claimed before finalize so the root collection lists it. Report failures degrade to warnings and can never abort a finished import. Info-only runs stay clean (flavour/type-suggestion diagnostics ride along, never cause a report). API (sanctioned 2026-07-11): NotificationImport and EventImportFinish gain reportObjectId + issuesCount. The adapter also emits one structured end-of-run log line (counts by severity/code) so Sentry/Graylog events name the failing code and object class. Parity test excludes the v2-only report page.

Commit:9b3e643
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7323 Add ObjectCleanupSuggestions and ObjectCleanupSuggestionIgnore protos

Commit:490f764
Author:Roman Khafizianov

GO-7323 Rename OrphansDetected event to CleanupSuggestion Phase 1 is unmerged, so the protocol has never shipped. The popup event and the new space-wide cleanup list are the same concept; unify the client-facing vocabulary on 'cleanup suggestion'. Internal Go keeps the precise term 'orphan'. Field number 146 and the Trigger enum are unchanged.

Commit:996b348
Author:Roman Khafizianov

GO-7323 Merge branch 'develop' of github.com:anyproto/anytype-heart into go-7323-cascade-deletion-orphan-events # Conflicts: # pb/commands.pb.go

Commit:0146559
Author:Roman Khafizianov

GO-7362 debug: make DebugRunProfiler trace opt-in, drop account_select_trace A timed DebugRunProfiler run now captures only CPU + heap(start/end) + goroutines(start/end) by default. The runtime execution trace is the heaviest artifact in both archive size and runtime overhead, so it is now opt-in via a new includeTrace flag on RpcDebugRunProfilerRequest. The in-flight account_select_trace is removed from the profiler bundle entirely, so the login flight recorder is left untouched by the profiler. RunProfiler is split into a testable runTimedProfiler(profilesDir, ...) inner method so the capture logic can be exercised without the global initialparams singleton; a new test asserts the trace gating.

Commit:ad586ff
Author:Roman Khafizianov

GO-7323 Add OrphansDetected event and skipCascade request field

Commit:92cc65a
Author:Roman Khafizianov

GO-7292 Merge origin/develop into go-7292-client-driven-lazy-multispace-loading Resolve the chatsubscription warm-up conflict by adopting develop's gated warm-up (GO-7302): warm the chat tree only for full subscriptions (!OnlyLastMessage), skipping vault previews. Restored the objectGetter dependency, Init wiring, and cache/zap imports that the branch had dropped. Regenerated pb/commands.pb.go via `make protos`.

Commit:66f0ce4
Author:Roman Khafizianov

GO-7311 Support text ranges for first/last block in BlockCopy/BlockCut Add optional selectedTextRangeLastBlock to Rpc.Block.Copy.Request and Rpc.Block.Cut.Request. When set and the request contains multiple blocks, selectedTextRange is applied to the first block and selectedTextRangeLastBlock to the last block, so a selection can start and end in the middle of text blocks. Blocks in between are copied/cut whole; on cut, partially selected blocks keep the unselected part of their text. When the new field is not set, behavior is unchanged.

Commit:07cca8a
Author:Roman Khafizianov

GO-7292 Merge origin/develop into go-7292-client-driven-lazy-multispace-loading

Commit:33c2cb4
Author:Sergey Cherepanov

GO-7300 Push limit/offset to anystore in objects list API The GET /spaces/:id/objects API built an unbounded ObjectSearch request and applied limit/offset in memory, so any-store fetched and sorted the entire filtered result set just to return one page. Add a needTotal flag and total field to the ObjectSearch RPC. Implement spaceindex.QueryAndCount, which compiles the filters once and returns both the limited page and the full count (via coll.Find(filter).Count) without materializing or sorting the whole set. ListObjects now requests limit+1 to derive hasMore and reads the exact total from the response. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Commit:cb10981
Author:Roman Khafizianov
Committer:Roman Khafizianov

GO-7292 Add preferredSpaceId + AccountPreloadRemainingSpaces proto

Commit:0277214
Author:Roman Khafizianov

GO-6473 Add Admin to ParticipantPermissions enum

Commit:d30e876
Author:Anthony Akentiev
Committer:GitHub

GO-7233 V2CartUpdate: IsLifetime option (#3125)

Commit:6ff8f79
Author:Sergey

GO-7214 Merge branch 'develop' of github.com:anyproto/anytype-heart into go-7214-quote-block

Commit:329e0db
Author:Roman Khafizianov

GO-7143 jsonInfo + KindGoroutines + trim DB path + rename LONG_RPC Proto (Event.Debug.ProfileCreated): - reasonDesc -> jsonInfo. The middleware always JSON-encodes extras (debugreporter.Reporter.Report marshals map[string]any), so the new name tells downstream consumers they can parse it directly and forward structured context to Sentry. - Expand comments: `reason` is NOT the DebugRunProfiler Reason enum; values come from the internal call sites (MEMORY_GROWTH, LONG_RPC, DB_CORRUPTION, ...). KindGoroutines: a lighter artifact for cases where goroutine stacks are the signal and a heap snapshot would be noise. debugsnapshot.Save learns a variadic options parameter (WithoutHeap). The long-RPC interceptor switches from KindHeap to KindGoroutines — the blocked stack is the actionable bit, and skipping heap.pb.gz cuts per-incident artifact cost noticeably. Rename LONG_METHOD -> LONG_RPC. Method was ambiguous (Go method? class method?); LONG_RPC matches the casing of the other internal reasons and makes clear we are talking about gRPC calls. Privacy: DB_CORRUPTION reports now send "parentDir/file.db" instead of the full absolute path — enough to identify which database was corrupted without disclosing the user's home directory or account layout through Sentry.

Commit:974d306
Author:Roman Khafizianov

GO-7143 Rename reasonDesc->jsonInfo, add KindGoroutines, strip db paths Proto changes (Event.Debug.ProfileCreated): - reasonDesc -> jsonInfo. The middleware always JSON-encodes its extras (see debugreporter.Reporter.Report which marshals map[string]any), so the new name tells downstream consumers they can parse it directly and forward structured context to Sentry. - Expanded comments to spell out that `reason` is NOT the DebugRunProfiler enum — values come from the internal call sites (MEMORY_GROWTH, LONG_METHOD, DB_CORRUPTION, ...). KindGoroutines: a lighter artifact for cases where goroutine stacks are the signal and heap data is noise. debugsnapshot.Save learns a variadic options parameter (WithoutHeap). The long-method interceptor switches from KindHeap to KindGoroutines — the blocked stack is the actionable bit, and skipping heap.pb.gz cuts per-incident artifact cost noticeably. Privacy: DB_CORRUPTION reports now send "parentDir/file.db" instead of the full absolute path — enough to identify which database was corrupted without disclosing the user's home directory or full account layout to Sentry.

Commit:f8ce064
Author:Roman Khafizianov

GO-7143 Emit Event.Debug.ProfileCreated for server-initiated profiles Add a new oneof entry on Event.Message — Debug.ProfileCreated (field 145) — carrying reason, reasonDesc, path and a full bool. It signals that the middleware wrote a profile archive on its own accord, so clients can surface it or queue the archive for upload without polling common/profiles/. DebugRunProfiler responses already carry the path, so those aren't re-announced. Producers: - core/debug/profiler (desktop): after a successful memory-growth snapshot, broadcast via the event sender injected at Init. Mobile builds remain a no-op via the existing build-tag split. - metrics.saveLongMethodTrace: gains a SetProfileCreatedHook callback (atomic.Value, nil-by-default) that is invoked after each successful long_method_*.txt.gz write. The profiler component registers the hook in Init so both producers go through one broadcaster, and metrics stays free of a direct dependency on core/event. Proto regen covers events.pb.go and the generated docs.

Commit:f846755
Author:Sergey

GO-7214 Split quote block into editor and message variants Replace MessageBlockQuote with two distinct variants: - MessageBlockEditorQuote for quoting a block from an editor object (blockId + content) - MessageBlockMessageQuote for quoting a chat message (messageId + participantId + content) Embed MessageBlockText as the shared content shape so quotes carry style, marks, checked, and lang for free. Extract marshalTextContent / unmarshalTextContent / validateTextContent helpers and reuse them from the text block and both quote variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Commit:167848c
Author:Roman Khafizianov

GO-7143 Add startup/shutdown logs, report options, buffered-log flush on close Lifecycle: - Log "starting app" with middleware and full app version in StartNewApp. - Log "closing app: initiated" and "closing app: finished" (with elapsed ms) in application.Service.stop(), and call log.Sync() at the tail so the shutdown line actually drains the buffered zap sink before exit. - Promote "anytype-app" and "anytype-core-account" to INFO in the default log levels so these lifecycle markers emit. DebugExportReport: - Add bool full to RpcDebugExportReport.Request; when false (default) include only the 2 newest log files by mtime, when true include all. - Preserve each source file's modification time in the zip via zip.FileHeader.Modified instead of using the archive creation time. - Rename the output archive to anytype-report-<version>-<YYYYMMDD-HHMMSS>-*.zip for easier triage. Logging buffer: - Use the same 256KB buffer / 30s flush interval on all platforms; mobile only keeps smaller rotated log files for disk reasons.

Commit:bc1a2fd
Author:Roman Khafizianov

GO-7143 Merge remote-tracking branch 'origin/develop' into go-7143-improve-loggingtracingreporting

Commit:a6ecdae
Author:Anthony Akentiev
Committer:GitHub

GO-7118 membership: new option - Lifetime (#3103)

Commit:ae7fd86
Author:Sergey

GO-7214 Add quote block type for chat messages Add MessageBlockQuote variant to ChatMessage.MessageBlock oneof with blockId and text fields. Wire marshal/unmarshal in the anyenc layer and require both fields non-empty on validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Commit:fb681c2
Author:Sergey
Committer:GitHub

Merge pull request #3092 from anyproto/go-6962-personal-favorites GO-6962 Personal favorites

Commit:93feeb2
Author:Sergey

GO-7168 Merge branch 'develop' of github.com:anyproto/anytype-heart into go-7168-notification-preferences-unread-tracking-for-object

Commit:72f38bc
Author:Sergey

GO-7166 Split message count out of chat state Move messageCount from ChatState to a dedicated field on ChatGetMessages/ChatSubscribeLastMessages responses and emit a ChatUpdateMessageCount event when the count changes. Add CountMessages to the chat repository. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Commit:3bb5fa4
Author:Sergey

GO-6962 Merge branch 'develop' of github.com:anyproto/anytype-heart into go-6962-personal-favorites

Commit:461a0c1
Author:Sergey

GO-7166 Add message count to chat state Track total non-deleted message count in ChatState, populated on load and incremented via ChatStateUpdate events on new messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Commit:0392fba
Author:Sergey

GO-7168 Add notificationSubscribers relation to chat object Store the subscriber set as a participant-id list on the details doc via AddToSet/Pull so the format is compatible with a list<string> relation. Auto-subscribe the creator on ObjectAddDiscussion. Expose ChatAddNotificationSubscriber / ChatRemoveNotificationSubscriber RPCs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Commit:9da7884
Author:Sergey

GO-6962 Add personal widgets virtual object backed by tech-space store Personal favorites widgets are now stored in a single per-account CRDT store object in the tech space and projected into each space as a virtual SmartBlock under the hardcoded id `_personalWidgets_<spaceId>`. The tech-space store (SmartBlockTypeTechSpaceObject) holds all widget entries for all spaces. The per-space virtual widget (SmartBlockTypeTechSpaceVirtualObject) is a StaticSource smartblock that loads its entries from the store via a mediator Service, and on every Apply does a deterministic snapshot-sync of the block tree back into the store (Create / Delete / Update with AfterId — no linked-list repair, no MoveWidget). Wiring highlights: - techspace.Run now derives the personal favorites store on both create and load paths. - NewStore receives its update callback at construction time; the store dispatches observer fan-out on a goroutine so local pushes don't re-enter the caller's smartblock lock. - Handler.pending is mutex-guarded; BeforeModify/Delete look up the doc's spaceId so observer dispatch can filter by space. - idresolver.ResolveSpaceID short-circuits '_personalWidgets_*' by parsing the id directly — no dependency on the bindId index, which isn't populated on a clean cache. - smartblock.Apply's empty-tree guard now also exempts SmartBlockTypeTechSpaceObject, matching AccountObject. - Store values go through anyenc (arena.NewString / NewNumberInt) to avoid the "unexpected tail" parse failure caused by passing raw Go strings through storestate.Builder.Modify. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Commit:0bd3c3f
Author:Roman Khafizianov

GO-7152 Merge branch 'develop' of ssh://github.com/anyproto/anytype-heart into go-7152-context-aware-gc-all-objects # Conflicts: # pb/commands.pb.go # util/builtinobjects/data/get_started.zip

Commit:dffc943
Author:Roman Khafizianov

GO-7152 Emit ObjectAutoRestore event when GC auto-restores objects Mirror the ObjectAutoArchive event for the unarchive direction: emit EventObjectAutoRestore from restoreObjectsOnUnarchive and CheckObjectsOnLinksRestored so callers can update their UI accordingly.

Commit:13a5715
Author:kirillston

GO-6915 Merge develop

Commit:d428afd
Author:Roman Khafizianov

GO-7152 Emit ObjectAutoArchive session event when GC archives files When the GC archives file objects as a side effect of archiving/editing, accumulate an Object.AutoArchive event in the session context so the originating client can show a toast. Key changes: - Add Object.AutoArchive proto event (field 142) and regenerate - Add sctx session.Context param to all three ObjectGC interface methods and thread it from every call site (API handlers, smartblock.Apply, chats service, delete, fileobject service, objectcreator) - Add accumulateAutoArchiveEvent helper in filegc that merges IDs into a single deduplicated event per session, so nested cascades produce exactly one event - Make triggerFileGCOnArchive synchronous so archive RPCs capture events before getResponseEvent is called - Add ResponseEvent event field to SetIsArchived and ListSetIsArchived responses; wire getResponseEvent in both RPC handlers - In smartblock.Apply use session.NewChildContext for the async GC goroutine and push accumulated events via SendToSession after it finishes, avoiding the page.Lock -> archive.Lock deadlock

Commit:334f9e3
Author:kirillston

GO-6915 Template placeholders stage 1

Commit:8977186
Author:kirillston

GO-6915 Merge develop

Commit:07bd9bf
Author:kirillston

GO-6946 Space Members Add List RPC

Commit:ad15851
Author:Roman Khafizianov

GO-7143 IOS-5994 Add DebugExportReport, DebugCleanupReport RPCs; export FreeOSMemory to clib - Rename DebugExportLog → DebugExportReport; add lastModifiedTs to response (unix ts of newest source file — pass to DebugCleanupReport after upload) - Add DebugCleanupReport(ts) RPC: removes log and profile files with modTime < ts - DebugRunProfiler: document durationInSeconds, replace string reason with typed enum (USER_REQUEST, MEMORY_PRESSURE_WARN/CRITICAL, THERMAL_SERIOUS/CRITICAL), add reasonDesc string for free-form context - Export FreeOSMemory() from clib (calls debug.FreeOSMemory — GC + release to OS) for iOS/Android to call on critical memory pressure signals

Commit:1f48935
Author:kirillston

GO-6946 Add permissions parameter

Commit:c73597c
Author:kirillston

GO-6946 Merge dev

Commit:cafc566
Author:kirillston

GO-3455 New err code on BlockPaste

Commit:cc8c340
Author:kirillston
Committer:kirillston

GO-6915 Add Delete cmd

Commit:aa1a2c0
Author:kirillston
Committer:kirillston

GO-6915 Update data format

Commit:9ff4d7c
Author:kirillston
Committer:kirillston

GO-6915 Move rpc to template namespace