diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index e02690ce0..0dbcde4a0 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -25189,10 +25189,10 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "409": description: "The chosen target cannot take the copy, and nothing was stored: a managed profile - (`backup.target.managed_profile`), an account pending deletion (`backup.target.pending_deletion`), an - account that already holds records (`backup.target.holds_records`, `meta.models` naming them), a username - already taken (`backup.target.username_taken`), or a file whose own account is still here - (`backup.owner.exists`)." + (`backup.target.managed_profile`), an account pending deletion (`backup.target.pending_deletion`), an admin + account (`backup.target.admin_account`), an account that already holds records + (`backup.target.holds_records`, `meta.models` naming them), a username already taken + (`backup.target.username_taken`), or a file whose own account is still here (`backup.owner.exists`)." content: application/json: schema: @@ -26190,17 +26190,19 @@ paths: - in: query name: values schema: - description: Comma-separated value series to include, as `MeasurementType` names, `MOOD`, or a lab marker as + description: "Comma-separated value series to include, as `MeasurementType` names, `MOOD`, or a lab marker as `lab:` (see `availableLabs`). A name given twice counts once, so the list is bounded by the - names that exist; an unknown measurement name answers 422, and so do more than 24 lab markers (ids, unlike - names, are not a closed set). A lab marker that is not in the record (deleted, or another record's) is - left out of `labSeries` without an error, so a stored choice never breaks the page. + names that exist; an unknown measurement name or a malformed marker key answers 422. At most 24 lab + markers are drawn (ids, unlike names, are not a closed set): the first 24 are kept and any further ones + left out without an error. A lab marker that is not in the record (deleted, or another record's) is + likewise left out of `labSeries`, so a stored choice never breaks the page." type: string - description: Comma-separated value series to include, as `MeasurementType` names, `MOOD`, or a lab marker as + description: "Comma-separated value series to include, as `MeasurementType` names, `MOOD`, or a lab marker as `lab:` (see `availableLabs`). A name given twice counts once, so the list is bounded by the - names that exist; an unknown measurement name answers 422, and so do more than 24 lab markers (ids, unlike - names, are not a closed set). A lab marker that is not in the record (deleted, or another record's) is left - out of `labSeries` without an error, so a stored choice never breaks the page. + names that exist; an unknown measurement name or a malformed marker key answers 422. At most 24 lab markers + are drawn (ids, unlike names, are not a closed set): the first 24 are kept and any further ones left out + without an error. A lab marker that is not in the record (deleted, or another record's) is likewise left out + of `labSeries`, so a stored choice never breaks the page." responses: "200": description: The timeline for the requested zoom and window. @@ -38714,36 +38716,37 @@ components: auth.reproof.too_weak, auth.scope.insufficient, auth.session.insecure_transport, auth.session.unavailable, auth.stepup.required, auth.token.expired, auth.token.invalid; backup — backup.foreign_reference, backup.key.missing, backup.owner.exists, backup.owner.missing, - backup.payload.undecryptable, backup.restore.active, backup.section.missing, - backup.target.holds_records, backup.target.managed_profile, backup.target.not_found, - backup.target.pending_deletion, backup.target.unconfirmed, backup.target.username_taken, - backup.upload.too_large; biomarkers — biomarkers.unit.locked; body-site — body-site.invalid; coach — - coach.budget.exceeded, coach.conversation.invalidTitle, coach.conversation.notFound, - coach.fenced.attachmentConflict, coach.fenced.attachmentLimit, coach.fenced.attachmentNotFound, - coach.fenced.attachmentNotIndexed, coach.fenced.attachmentRequired, coach.fenced.attachmentUnavailable, - coach.fenced.invalid, coach.request.invalid; coach-prefs — coach-prefs.body.invalid_json, - coach-prefs.body.invalid_shape; consent — consent.ai.confirmRequired, consent.ai.required; cycle — - cycle.bulk.invalid, cycle.bulk.too_large, cycle.calendar.invalid, cycle.calendar.range, - cycle.cycles.invalid, cycle.day-log.conflict, cycle.day-log.invalid, cycle.disabled, - cycle.period.invalid, cycle.symptom.custom.invalid, cycle.symptom.custom.limit; cycle-prefs — - cycle-prefs.body.invalid_json, cycle-prefs.invalid; disable-coach — disable-coach.body.invalid_json; - data_wipe — data_wipe.copy_failed; dawarich — dawarich.credentials_rejected, - dawarich.home_place_unknown, dawarich.invalid, dawarich.not_connected, dawarich.rate_limited, - dawarich.unreachable; day — day.invalid; demo — demo.readOnly; documents — - documents.chat.conversationNotFound, documents.chat.invalid, documents.inbound.alreadyConfirmed, - documents.inbound.alreadyPartlyConfirmed, documents.inbound.budgetExceeded, - documents.inbound.decryptFailed, documents.inbound.encounterNotFound, documents.inbound.episodeNotFound, - documents.inbound.extractFailed, documents.inbound.factNotFound, documents.inbound.factNotPending, - documents.inbound.factTypeMismatch, documents.inbound.fileTooLarge, documents.inbound.fileType, - documents.inbound.invalidMetadata, documents.inbound.invalidQuery, documents.inbound.invalidUpdate, - documents.inbound.localOcrDisabled, documents.inbound.notDeleted, documents.inbound.notFound, - documents.inbound.notIndexed, documents.inbound.outputLimit, documents.inbound.pdfNeedsAnthropic, - documents.inbound.providerUnsupported, documents.inbound.quotaExceeded, documents.inbound.rateLimited, - documents.inbound.restoreDuplicate, documents.inbound.restoreGone, documents.inbound.sourceAliasLimit, - documents.inbound.uploadBusy, documents.inbound.uploadTimeout, documents.inbound.vaccinationNotFound, - documents.sources.authRefused, documents.sources.badResponse, documents.sources.browserOnly, - documents.sources.invalidAddress, documents.sources.linkTargetNotFound, documents.sources.notConnected, - documents.sources.notFound, documents.sources.organizationRequired, documents.sources.originNotAllowed, + backup.payload.undecryptable, backup.restore.active, backup.restore.rows_exist_elsewhere, + backup.section.missing, backup.target.admin_account, backup.target.holds_records, + backup.target.managed_profile, backup.target.not_found, backup.target.pending_deletion, + backup.target.unconfirmed, backup.target.username_taken, backup.upload.too_large; biomarkers — + biomarkers.unit.locked; body-site — body-site.invalid; coach — coach.budget.exceeded, + coach.conversation.invalidTitle, coach.conversation.notFound, coach.fenced.attachmentConflict, + coach.fenced.attachmentLimit, coach.fenced.attachmentNotFound, coach.fenced.attachmentNotIndexed, + coach.fenced.attachmentRequired, coach.fenced.attachmentUnavailable, coach.fenced.invalid, + coach.request.invalid; coach-prefs — coach-prefs.body.invalid_json, coach-prefs.body.invalid_shape; + consent — consent.ai.confirmRequired, consent.ai.required; cycle — cycle.bulk.invalid, + cycle.bulk.too_large, cycle.calendar.invalid, cycle.calendar.range, cycle.cycles.invalid, + cycle.day-log.conflict, cycle.day-log.invalid, cycle.disabled, cycle.period.invalid, + cycle.symptom.custom.invalid, cycle.symptom.custom.limit; cycle-prefs — cycle-prefs.body.invalid_json, + cycle-prefs.invalid; disable-coach — disable-coach.body.invalid_json; data_wipe — data_wipe.copy_failed; + dawarich — dawarich.credentials_rejected, dawarich.home_place_unknown, dawarich.invalid, + dawarich.not_connected, dawarich.rate_limited, dawarich.unreachable; day — day.invalid; demo — + demo.readOnly; documents — documents.chat.conversationNotFound, documents.chat.invalid, + documents.inbound.alreadyConfirmed, documents.inbound.alreadyPartlyConfirmed, + documents.inbound.budgetExceeded, documents.inbound.decryptFailed, documents.inbound.encounterNotFound, + documents.inbound.episodeNotFound, documents.inbound.extractFailed, documents.inbound.factNotFound, + documents.inbound.factNotPending, documents.inbound.factTypeMismatch, documents.inbound.fileTooLarge, + documents.inbound.fileType, documents.inbound.invalidMetadata, documents.inbound.invalidQuery, + documents.inbound.invalidUpdate, documents.inbound.localOcrDisabled, documents.inbound.notDeleted, + documents.inbound.notFound, documents.inbound.notIndexed, documents.inbound.outputLimit, + documents.inbound.pdfNeedsAnthropic, documents.inbound.providerUnsupported, + documents.inbound.quotaExceeded, documents.inbound.rateLimited, documents.inbound.restoreDuplicate, + documents.inbound.restoreGone, documents.inbound.sourceAliasLimit, documents.inbound.uploadBusy, + documents.inbound.uploadTimeout, documents.inbound.vaccinationNotFound, documents.sources.authRefused, + documents.sources.badResponse, documents.sources.browserOnly, documents.sources.invalidAddress, + documents.sources.linkTargetNotFound, documents.sources.notConnected, documents.sources.notFound, + documents.sources.organizationRequired, documents.sources.originNotAllowed, documents.sources.permissionMissing, documents.sources.rateLimited, documents.sources.redirected, documents.sources.tokenRequired, documents.sources.unavailable, documents.sources.unreachable, documents.sources.versionTooOld; encounter — encounter.invalid, encounter.link.invalid, @@ -90610,9 +90613,11 @@ components: type: string description: "Stable reason: `backup_not_found`, `backup_changed`, `backup.payload.undecryptable`, `schema_invalid`, `incompatible_schema_version`, `owner_mismatch`, `owner_not_found`, `backup.section.missing`, - `document_ciphertext_missing`, `backup.key.missing`, `backup.foreign_reference`, `time_budget`, - `transaction_failed`, `interrupted`, `not_started`, `enqueue_failed`, `failed_after_commit`, `unexpected`. - Every code but `failed_after_commit` means the account was not changed." + `document_ciphertext_missing`, `backup.key.missing`, `backup.foreign_reference`, + `backup.restore.rows_exist_elsewhere` (a copy put into a chosen account whose records another account + already holds, because the same file was restored there before), `time_budget`, `transaction_failed`, + `interrupted`, `not_started`, `enqueue_failed`, `failed_after_commit`, `unexpected`. Every code but + `failed_after_commit` means the account was not changed." message: type: string sections: diff --git a/docs/codex-protocol-spec.md b/docs/codex-protocol-spec.md index 0baf43877..ba81bf20f 100644 --- a/docs/codex-protocol-spec.md +++ b/docs/codex-protocol-spec.md @@ -692,7 +692,7 @@ The chain is overridable via the env var `CODEX_MODEL_FALLBACK_CHAIN` (comma-sep | `401` after refresh, `403`, `429`, `5xx` | propagate (don't walk) | | `200` with SSE `response.failed.error.code === "invalid_prompt"` | propagate (request shape is wrong, slug is fine) | -**Positive cache** — when a slug is observed working (HTTP 200 with at least one `response.completed` event), record `(slug, timestamp)` for that sign-in. On its next request, jump straight to the cached slug; the model list is read only when there is none or it stops working. TTL: 1 hour. Scope: per stored credential (`user:` for a person's connection, `central` for the operator-shared one), process-local, no DB write, at most 1000 sign-ins held. Each entry carries a digest of the ChatGPT account id it was learned under, and a credential reconnected to another account starts empty. Two sign-ins on different plans no longer overwrite each other's working slug. +**Positive cache** — when a slug is observed working (HTTP 200 with at least one `response.completed` event), record `(slug, timestamp)` for that sign-in. On its next request, jump straight to the cached slug; the model list is read only when there is none or it stops working. TTL: 1 hour. Scope: per stored credential (`user:` for a person's connection, `central` for the operator-shared one), process-local, no DB write, at most 1000 sign-ins held. Each entry carries a digest of the ChatGPT account id it was learned under, and a client reads it only under that account, so a credential reconnected to another account starts empty: its first request tries no slug, skips no refusal and trusts no list the old account left, in any process. The vision gate holds no account id; the connect and disconnect routes rebind or drop the entry, so in the process that handled the change it answers from the new sign-in at once, and elsewhere from that process's first request on it. Two sign-ins on different plans no longer overwrite each other's working slug. **Cache invalidation**: on a 400/404 walk the refused slug leaves the working slot and goes to the back of that sign-in's line for six hours. The next-best slug is promoted into the cache only after it succeeds. diff --git a/docs/ops/backup-restore.md b/docs/ops/backup-restore.md index 2ab5215f3..1f8d5dd98 100644 --- a/docs/ops/backup-restore.md +++ b/docs/ops/backup-restore.md @@ -103,18 +103,35 @@ exists, and the console then asks where the records should go: password or email, as an ordinary account with the user role, and nobody can sign into it yet. Before handing it over, set its first password with **Reset password** in Admin → Users; the person can change it, add an email - and enrol a passkey from their settings. + and enrol a passkey from their settings. The account is made before the + file is read, with an `admin.user.create` audit row (`outcome: "created"`), + and removed again when the file is refused, with an `admin.user.delete` row + (`reason: "backup_upload_refused"`). When that row says `outcome: "failed"`, + the removal did not happen and the account is left over, holding nothing: + delete it in Admin → Users. - **An existing account**, for someone who has signed up again. Pick the account and type its username to confirm. The account must hold no records yet: a restore replaces an account's records and does not merge them, so an account with even one record of any kind a copy carries is refused, with the - kinds named. Managed profiles and accounts pending deletion are not offered. + kinds named. What signing up and the setup questions leave behind does not + count (the setup answers and the cycle settings): the restore replaces both + with the copy's. A reading or medication entered on setup's last screen is a + record, and the account is refused until it is deleted, because the restore + would delete it too. Managed profiles, accounts pending deletion and admin + accounts are not offered: an admin account is the operator's, its settings + are read under the operator's wider AI endpoint policy, and its role reaches + every account, so another person's record does not go into it. The file is then uploaded a second time, and its owner is changed to the chosen account as it is read. Nothing else in the file changes. The stored copy is -then that account's copy in every respect: its preview, its download and its -restore are the ordinary ones, and **Restore** on its row brings everything -back under the chosen account. The erased account's id is not stored, not in +then that account's copy in every respect but one: its preview, its download +and its restore are the ordinary ones, and **Restore** on its row brings +everything back under the chosen account. The one difference is that it is +stored as `RETARGETED_UPLOAD_