Skip to content

Promote staging to production: doc content storage, connectors extractors, SecretStr credentials, security bumps - #189

Merged
qiuethan merged 96 commits into
mainfrom
staging
Aug 9, 2026
Merged

Promote staging to production: doc content storage, connectors extractors, SecretStr credentials, security bumps#189
qiuethan merged 96 commits into
mainfrom
staging

Conversation

@qiuethan

@qiuethan qiuethan commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Promotes staging (d86a55e) to production. 96 commits, Aug 1 → Aug 9, since prod's current b445d1d7.

What ships

Features

Fixes

Dependencies — 8 Dependabot bumps, all security-motivated: undici 6.28.0, @fastify/static 10.1.2, fast-uri 3.1.5, brace-expansion 5.0.9, find-my-way 9.7.0, cryptography 50.0.0, pyasn1 0.6.4, pypdf 6.15.0

Housekeeping — ruff format enforced in CI for packages/auth, services/meeting, services/documentation-system (#184#186); per-service doc sets and agent guides (#187); SECURITY-REVIEW-2026-07-13.md removed (#188); docs/DEPLOYMENT-HISTORY.md refreshed to cover connectors and this promotion (#191)

Migrations

Two, both on documentation-system, run via preDeployCommand:

revision change risk
005_doc_content creates doc_content (new 1:1 table, FK cascade on doc delete) additive, clean downgrade()
006_enable_google_content_fetch sets content_fetch_enabled = true for gdocs, gsheets, gslides, gdrive 4-row data update, reversible

Neither rewrites nor drops existing data. Both have already applied cleanly against staging's database. No other service has new revisions — #190 and #191 add none.

Known: connectors is not provisioned

Migration 006 enables Google content fetching, which depends on the connectors service — and there is no connectors service in Railway in either environment. documentation-system correspondingly lacks CONNECTORS_BASE_URL, CONNECTORS_API_KEY, and GOOGLE_CREDENTIALS_JSON in both staging and production.

This degrades rather than breaks. Per services/documentation-system/.env.example, CONNECTORS_API_KEY is a soft dependency: a missing/default value logs a startup warning and boots anyway, and Google-source fetches are recorded as per-doc ingest warnings rather than breaking the catalog. Staging has run in exactly this configuration since Aug 1.

Expect ingest warning noise on Google sources until connectors is provisioned.

Pre-flight already done

  • Prod meeting config wired. Railway was ignoring services/meeting/railway.json for that service (railwayConfigFile: null), so it took its port from the Dockerfile CMD — which fix: correct hardcoded ports in meeting and documentation-system Dockerfiles #179 changes. Left alone, this promotion would have taken prod's meeting service down with a green deploy and no healthcheck to catch it. Both environments now read railway.json: --port ${PORT}, healthcheckPath: /health, overlapSeconds: 0, numReplicas: 1.
  • Service URLs templated. All *_BASE_URL variables in both environments converted from hardcoded literals to http://${{service.RAILWAY_PRIVATE_DOMAIN}}:${{service.PORT}}, matching docs/RAILWAY-DEPLOYMENT.md. Each renders byte-identical to the literal it replaced, so no behavior change — but port changes now propagate instead of silently pointing at a dead port.

Verification

  • All 96 commits CI-green.
  • SecretStr call sites traced. All six services' verify_production_secrets unwrap via .get_secret_value() before comparing — the silent failure mode (a SecretStr never compares equal to a str, so a dev-default check would stop firing) does not occur. All 13 non-config use sites unwrap correctly. platform_auth/secret_guard.py backstops with a named TypeError.
  • Last 5 commits verified formatting-only by AST comparison — 44 of 47 changed Python files parse to identical ASTs. The 3 that differ are test files: two import reorderings, one removal of genuinely unused imports. Test and assertion counts unchanged.
  • Staging healthy on 3f00593: all six services SUCCESS, 1/1 replicas, meeting /health 200. Two commits have landed since that check — b0ea4e2 (fix(meeting): salvage a recording when the WebSocket drops #190, meeting) and d86a55e (docs: refresh the deployment record before the staging → main promotion #191, docs only). Re-confirm staging's meeting deploy on d86a55e before merging, since fix(meeting): salvage a recording when the WebSocket drops #190 changes its start command.

Watch on deploy

documentation-system — the only service with migrations, and the only one whose deploy can fail. It fails loudly rather than silently.

🤖 Generated with Claude Code

qiuethan and others added 30 commits August 1, 2026 00:31
…nd hashing

Adds get_doc_content_meta (both adapters, same visibility gate and join as
get_doc_content) so content_hash/fetched_at are readable without private
adapter access. Centralizes MAX_CONTENT_CHARS and sha256 hashing in
src/content.py so ingest, refetch, and the upcoming source connectors share
one truncation and hashing rule instead of duplicating it.
test_memory_store.py resolved generate_key/parse_prefix/verify_key from
llm's src.api.hashing, which only worked because the shared workspace venv
happens to expose services/llm/src as a top-level src package. CI's
auth-lib-test job installs packages/auth in isolation (uv sync --extra dev
from packages/auth), where llm's src is not on the path, so this would
have failed in CI. It also violated platform_auth's own pure-leaf
contract from the test side.

Use platform_auth's envelope-parametrized generate_key/parse_prefix
directly with a neutral test envelope instead of llm's llm_-bound
wrapper.
Makes GoogleSource real: parse_file_id -> Drive metadata lookup ->
MIME-keyed extractor dispatch. Extraction is a strategy (Extractor
protocol + EXTRACTORS dict) rather than a single export table, so
Task 6 can swap in a native Docs API extractor without touching the
fetch path or the Slides/Sheets fallback. DriveExportExtractor covers
all editor types plus plain media download for uploaded text files.
Registers gdocs/gsheets/gslides/gdrive in the source registry.
Review Finding 1: _build_services built a hard-coded drive/docs dict,
so a future extractor needing a new API (e.g. slides) would KeyError
and require editing google.py, breaking the "one new file plus one
registry entry" property. Add a services tuple to the Extractor
protocol alongside scopes, declare it on DriveExportExtractor, add
required_services() mirroring required_scopes(), and have
_build_services build only the clients required_services() names via
a name-to-API-version map (unmapped names raise SourceNotConfigured).
docs is no longer built eagerly; with only DriveExportExtractor
registered, required_services() is just drive.

Review Finding 2: move the imports the brief had appended mid-file to
the top of google.py to clear ruff E402, and run ruff format on it
and on fetch.py (pre-existing, unrelated drift) so ruff check and
ruff format --check are both clean. No behavior change.
…ache google clients

Closes out the final review pass on this branch before PR: documents
CONNECTORS_BASE_URL/CONNECTORS_API_KEY everywhere an operator needs them
(env.example, README, deploy runbooks) and the required connectors-first
deploy order; closes a FetchError-mapping gap where a non-JSON or
non-object 200 body from connectors could 500 an ingest instead of
degrading to a warning; threads REQUEST_TIMEOUT_S through to the Google
HTTP transport; memoizes GoogleSource's built API clients (lock-guarded)
and shares one instance across all four Google source ids instead of
rebuilding credentials/clients on every request; normalizes empty
connectors content to None like WebFetcher already does; and raises
connectors' transport-guard max_content_chars above documentation-system's
own clamp so its truncation warning can actually fire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… validation

Critical: memoized httplib2 transports were shared across threads, risking
one concurrent /fetch's response bytes landing on another's connection.
Now only the service-account credentials are memoized; the AuthorizedHttp
transport and discovery clients are rebuilt fresh inside fetch().

Also: stop ConnectorsFetcher from pre-truncating content so clamp_content
remains the single authority and its truncation warning can actually fire;
validate the decoded connectors response body against a pydantic model so a
wrong-typed content/title field becomes a FetchError instead of escaping as
a raw TypeError/ValidationError; and correct DEPLOYMENT.md/README.md, which
overstated that a connectors outage never fails refetch (it returns 502 by
design, unlike ingest which degrades to a warning).
…ing convention tests

Six convention divergences from the connectors-service audit: 8004 collided
with meeting, the root README never registered the service, config.py used a
bare raise instead of the shared accumulator pattern, .env.example dropped
the shared tier/mint-hint header, and tests/test_openapi.py /
tests/test_audit_log.py (the two convention tests every sibling service has)
were missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
connectors_api_key was added to verify_production_secrets()'s hard-fail
list by false analogy to directory_api_key. team-tracking is a hard
dependency (every doc's owner and access decision resolves against it);
connectors is soft (backs only 4 of 8 sources, and ingest_doc already
degrades a connectors FetchError into a per-doc warning rather than
failing the doc). It also contradicted connectors' own config, which
deliberately excludes GOOGLE_CREDENTIALS_JSON from its hard-fail list
for the identical reason: degraded, not broken.

Remove connectors_api_key from the insecure list; log a WARNING instead
naming CONNECTORS_API_KEY and the consequence (Google-source fetches
will fail and show up as per-doc ingest warnings). Confirmed the warning
actually reaches the logs despite this service having no logging config:
unlike the meeting service's INFO-level bug (fixed in d7f0c93), WARNING
records still escape via logging.lastResort even with zero handlers on
the chain -- verified against uvicorn's real LOGGING_CONFIG in a fresh
interpreter.

Updated the docstring and every doc (.env.example, README, DEPLOYMENT.md,
RAILWAY-DEPLOYMENT.md) that claimed a missing CONNECTORS_API_KEY blocks
boot; deploy-order advice softened from required to recommended.

186 passed, 29 skipped (was 185/29) in documentation-system; connectors
60 passed, packages/auth 32 passed, services/llm 56 passed -- all
unaffected suites unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extract generic table rendering from docs.py to a shared markdown module
so that Slides extractor can reuse it. Fixes two defects from docs review:
pipes in cells are now escaped to prevent column separator breakage, and
ragged rows are padded to the widest row rather than truncated to header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…te hermetic

google_credentials_json was a plain str, so any repr/diff/traceback of
Settings could print the raw base64-encoded service-account private key.
That is exactly what happened when a failing assertion diff dumped a
developer real key to the terminal and CI log. Switch it to SecretStr so
no future code path can leak it, updating the one use site (registry.py)
to call get_secret_value() at the boundary before handing GoogleSource a
plain str.

That failure was only visible because the test suite silently read the
developer local .env (env_file=".env" in Settings.model_config), so a
teammate local config could turn the suite red for reasons unrelated to
their change. Add an autouse conftest fixture that neutralizes env_file
before any Settings are constructed, making the suite hermetic, and add a
regression test proving the secret never appears in repr/str output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…le placeholder

Real decks are often built from free text boxes rather than the layout's
title field, so shape.placeholder can be absent on every slide (confirmed
against a real 61-slide deck). Placeholder detection stays the primary path;
when none is found, the first non-empty text shape in reading order is
promoted to the title instead, excluded from the body by element identity.
Apostrophes and cell-reference-lookalike names (Q1, A1) are ambiguous or
invalid unquoted in A1 notation; bare spaces were never the problem.
Quoting is harmless for names that already worked, so quote every title
sent to batchGet while keeping the raw title in the '## <title>' heading.
dependabot Bot and others added 24 commits August 7, 2026 17:45
Bumps [undici](https://github.com/nodejs/undici) from 6.27.0 to 6.28.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](nodejs/undici@v6.27.0...v6.28.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.28.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
…erfiles

Both Dockerfiles carried the port of the service they were forked from:

  meeting              EXPOSE/--port 8003  (verification's port)   -> 8004
  documentation-system EXPOSE/--port 8000  (team-tracking's port)  -> 8001

Production is unaffected: every railway.json sets
`startCommand: sh -c 'uvicorn ... --port ${PORT}'`, which overrides the
Dockerfile CMD, and EXPOSE is documentation-only in Docker.

The impact is on anyone running the image directly. `docker run -p 8004:8004`
against the meeting image silently serves nothing, because the app is
listening on 8003 inside the container. It also meant meeting and
verification claimed the same internal port, as did documentation-system and
team-tracking, so a naive `-p 8003:8003` could land on the wrong service with
no error.

CI's docker-build smoke test only checks `import src.api.app`, so it never
exercised the CMD and could not have caught this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`context: ..` from services/<svc>/ resolves to services/, not the repo root,
so neither compose file could build:

  services/pyproject.toml          MISSING  (root manifest is at the repo root)
  services/uv.lock                 MISSING
  services/packages/               MISSING
  services/services/llm/Dockerfile MISSING  (dockerfile: is relative to context)

It fails on the dockerfile lookup, before any COPY runs.

The Dockerfiles themselves are correct — they are written for a repo-root
context, which is exactly what Railway uses (dockerfilePath:
services/<svc>/Dockerfile, built from the root). Only the compose files were
wrong, so this never affected a deploy.

Only llm and connectors have a `build:` section; team-tracking,
documentation-system and verification use compose solely to stand up Postgres,
which is why this went unnoticed.

Verified with `docker compose config`: context now resolves to the repo root
instead of services/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chore(deps): bump pyasn1 from 0.6.3 to 0.6.4
…-bot/fast-uri-3.1.5

chore(deps): bump fast-uri from 3.1.3 to 3.1.5 in /discord-bot
…-bot/undici-6.28.0

chore(deps): bump undici from 6.27.0 to 6.28.0 in /discord-bot
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.14.2 to 6.15.0.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](py-pdf/pypdf@6.14.2...6.15.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.15.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@fastify/static](https://github.com/fastify/fastify-static) from 9.1.3 to 10.1.2.
- [Release notes](https://github.com/fastify/fastify-static/releases)
- [Commits](fastify/fastify-static@v9.1.3...v10.1.2)

---
updated-dependencies:
- dependency-name: "@fastify/static"
  dependency-version: 10.1.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [find-my-way](https://github.com/delvedor/find-my-way) from 9.6.0 to 9.7.0.
- [Release notes](https://github.com/delvedor/find-my-way/releases)
- [Commits](delvedor/find-my-way@v9.6.0...v9.7.0)

---
updated-dependencies:
- dependency-name: find-my-way
  dependency-version: 9.7.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
fix: correct hardcoded ports in meeting and documentation-system Dockerfiles
fix: point llm and connectors compose build context at the repo root
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.7 to 5.0.9.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v5.0.7...v5.0.9)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [cryptography](https://github.com/pyca/cryptography) from 49.0.0 to 50.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](pyca/cryptography@49.0.0...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
chore(deps): bump cryptography from 49.0.0 to 50.0.0
…-bot/brace-expansion-5.0.9

chore(deps): bump brace-expansion from 5.0.7 to 5.0.9 in /discord-bot
chore(deps): bump pypdf from 6.14.2 to 6.15.0
…-bot/fastify/static-10.1.2

chore(deps): bump @fastify/static from 9.1.3 to 10.1.2 in /discord-bot
…-bot/find-my-way-9.7.0

chore(deps): bump find-my-way from 9.6.0 to 9.7.0 in /discord-bot
`auth-lib-test` ran `ruff check` but not `ruff format --check`. Unlike the
`meeting` and `documentation-system` deferrals, this one had no explanatory
comment in ci.yml — it looks like an omission rather than a decision, since
every other gap in that file is documented.

Ran `ruff format .` (ruff 0.15.20, the version pinned in uv.lock, so the
output matches what CI would produce) and added the missing step.

4 files changed, all cosmetic: blank lines around a nested function, and
wrapping a json.dumps call. No logic touched.

  ruff check .          All checks passed!
  ruff format --check . 13 files already formatted

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#185)

Clears the deferral ci.yml has carried since the service shipped: 12 files
under services/meeting were unformatted, so adding the step would have landed
meeting-test red.

Ran `ruff format .` (ruff 0.15.20, the version pinned in uv.lock, so the
output matches CI), removed the deferral comment, and added the step.

Formatting only -- no logic, signatures, or behavior changed. The changes are
line wrapping, trailing commas, and blank lines; sessions.py and pdf.py carry
most of it.

  ruff check .          All checks passed!
  ruff format --check . 40 files already formatted

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#186)

Clears the last CI lint gap. ci.yml deferred both ruff steps here because the
service had pre-existing lint findings and a large formatter diff.

Lint (9 findings, all in tests/, none in src/):
  - 3x F401 unused fastapi imports in test_authz.py            (autofixed)
  - 1x F811 duplicate InMemoryStorageAdapter import            (autofixed)
  - 4x E402 module-level imports part-way down two test files

The E402s were the residue of test blocks appended to the end of the file
along with a second import stanza -- no skip guard, no sys.path manipulation,
nothing conditional. Fixed by merging those imports into the top-of-file block
and deleting the duplicates, rather than silencing with noqa. The moved names
(UUID, uuid4, Actor, DENY, SEE_ALL) were only used below their old position,
so hoisting them is safe.

Format: 31 files, ruff 0.15.20 (the version pinned in uv.lock, so output
matches CI).

  ruff check .          All checks passed!
  ruff format --check . 65 files already formatted

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…187)

* docs: add agent guides, complete the per-service doc sets, fix drift

Three things, one theme: make the repo's documentation complete and true.

1. Agent layer
   - AGENTS.md: the invariants an agent must not violate, workflow rules, and
     the repo-specific gotchas that otherwise cost an hour (the uv --project
     CLI collision, the 5434 port clash, /swagger vs /docs).
   - CLAUDE.md is a pointer to it, so there is one source of truth rather than
     two files that drift.
   - .gitignore: /.claude was ignored wholesale, so project skills, commands
     and settings could never be committed. Now re-includes skills/, commands/,
     agents/ and settings.json while still ignoring settings.local.json and
     worktrees/. Verified with git check-ignore.

2. Per-service docs
   Every service now has the same four-doc set as team-tracking and
   documentation-system (API / ARCHITECTURE / CONTRIBUTING / DEPLOYMENT):
   connectors, llm, meeting, verification. Plus CONTRIBUTING for discord-bot
   and packages/auth. Written from the source, not templated -- they capture
   things no README said, e.g. why connectors memoizes credentials but rebuilds
   the httplib2 transport per fetch, and the O(n^2) cost bug that shaped
   meeting's session design.

3. Drift fixed
   - connectors was missing from docs/ARCHITECTURE.md and docs/DEVELOPMENT.md
     entirely; both still said "five backend services".
   - "meeting -> llm is the only service-to-service dependency" was wrong twice
     over: documentation-system -> connectors is a second one, and the bot
     calls llm directly on the @-mention path.
   - documentation-system's migration head was recorded as 004; it is 006.
   - CI job count was 9 in three places; it is 10.
   - Stray </content> and </invoke> generation artifacts were committed in four
     markdown files.
   - Deleted every hardcoded test count. Five of six were wrong, one by more
     than 2x. They cannot survive, so they are gone rather than corrected.

Also adds PR and issue templates. The issue templates are classic Markdown
rather than YAML forms on purpose: blocked-ready-automation.yml extracts issue
numbers only from the line matching /blocked by/, and a YAML form renders the
heading and its value on separate lines, which would silently break it. All
three templates were checked against that regex for the untouched, filled-in
and line-deleted cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: fix contradictions and stale CI claims found in review

Review of #187 came back RED. Fixes:

Wrong:
- DEPLOYMENT-HISTORY said "10 jobs" then listed 9 -- connectors-test was
  missing from the list. The count was corrected in this PR without adding
  the bullet, so the page contradicted itself.
- "Only four of these are required status checks" was false. Branch protection
  on staging and main requires ALL TEN. Verified against the GitHub API. A
  contributor would have read this and assumed a red llm-test doesn't block.
- docker-build smoke-tests six APIs, not five.
- connectors CONTRIBUTING said meeting's format check was "still deferred"
  while meeting CONTRIBUTING said it was cleared -- two files in this PR
  disagreeing. Both are moot now that #184/#185/#186 have merged.
- packages/auth CONTRIBUTING said auth-lib-test runs only ruff check.
- llm DEPLOYMENT inverted the dependency arrow: it is
  documentation-system -> connectors, not the reverse. Direction drives deploy
  order, which is what that section is about.

Stale:
- AGENTS.md claimed "every Python CI job" gates both ruff steps. python-test
  runs no ruff at all (team-tracking's linting is in python-lint), so the
  correct scope is every Python *service*.
- documentation-system's own CONTRIBUTING and README still showed ruff check
  only; #186 added both gates.

Nits: connectors API.md was missing a third warning producer and two 422
detail strings; meeting API.md gave [MM:SS] as the only transcript timestamp
shape (it is [HH:MM:SS] past an hour, reachable at the 4h cap); the --project
warning cited src.cli:main for connectors/llm, which actually use
src.mint_key:main -- the src package collision is the real hazard, not the
entry point name; AGENTS.md zone list omitted `root`; README called it two
issue templates when there are three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Deletes the 2026-07-13 security review and the three references to it
(AGENTS.md's "Where to look" table, docs/DEVELOPMENT.md's "Where to go next",
and the repo-layout tree in README.md) so no link is left dangling.

The document remains in git history at 223e806 if it is ever needed.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Two production meetings were lost in one morning. Both ended the same way:
the audio WebSocket closed cleanly mid-recording (17m24s and 8m03s in, so
not a fixed timeout), and the transcript -- already fully assembled in the
service's memory -- was thrown away.

That loss was by design, and the design was wrong. The WS disconnect handler
ran discard() immediately, deregistering the session, so the bot's follow-up
POST /stop would have 404'd even if it had tried one. It didn't try: onClose
went straight to teardown, which posts "no minutes will be posted".

Both halves are needed, so both are here:

- Server: a disconnect now HOLDS the session for DISCONNECT_GRACE_S (default
  60s) instead of destroying it, and marks end-of-audio (a closed socket can
  deliver no more frames, so /stop's barrier passes immediately rather than
  burning its 5s drain). discard() runs only if the window expires unclaimed.
- Bot: onClose/onError run the normal finalize (POST /stop -> post the PDF)
  instead of announcing a dead meeting. The channel is only told the minutes
  were lost if that finalize itself fails.

Without the server hold the bot's /stop would 404; without the bot salvage
the held session would just expire. Neither is useful alone.

Also log the WebSocket close code on both sides. Discarding it is what made
these two failures undiagnosable: no error surfaced on either side of the
wire, so there was nothing to distinguish a proxy drop from a keepalive
timeout from a client bug.

Raise uvicorn's ws keepalive from its 20s/20s defaults to 60s/60s. A client
briefly slow to answer a ping got its socket closed mid-recording, which is a
leading suspect for the close cause -- untested, hence the wider margin
rather than a claimed fix.

Reconnecting to a held session is deliberately NOT supported: a second WS
connect for a live session_id still closes 1008. A held session is claimable
by /stop, not resumable.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to Misty / staging August 8, 2026 16:35 Inactive
…on (#191)

`docs/DEPLOYMENT-HISTORY.md` was never updated when `connectors` landed on
2026-08-01, so the file that explains how the platform is deployed described a
five-service platform. Everything here is a doc fix; no code changes.

- DEPLOYMENT-HISTORY: add `connectors` to the shape diagram, the service count
  (six → seven Railway entities), the no-database rationale (two → three), and
  the `CONSUMER_KEYS` handoff list. Correct "`meeting` is staging-only / is the
  newest service" — `connectors` is newer and also staging-only — and say
  plainly in Current state that promoting ships their code but does not
  provision them.
- DEPLOYMENT-HISTORY: add release-log entries for connectors (2026-08-01) and
  for this train (2026-08-08) — the meeting disconnect salvage, the widened WS
  keepalive, the SecretStr rollout, ruff-format enforcement, and the Dockerfile
  port / compose context fixes. The log had stopped at 2026-07-26.
- RAILWAY-DEPLOYMENT: "all four CI checks are required" → ten; the file
  contradicted DEPLOYMENT-HISTORY, which already said ten.
- RAILWAY-DEPLOYMENT + services/meeting/docs/DEPLOYMENT: document `meeting`'s
  `--ws-ping-interval 60 --ws-ping-timeout 60`. It shipped in #190 undocumented,
  and it is the one service whose `startCommand` is not the plain template — an
  operator recreating it from the runbook would have silently dropped the fix.
- AGENTS.md: "see the table below" pointed at a CI table that doesn't exist in
  the file; link to ci.yml and the DEPLOYMENT-HISTORY section instead.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to Misty / staging August 9, 2026 18:25 Inactive
@qiuethan
qiuethan merged commit f692b0b into main Aug 9, 2026
28 checks passed
qiuethan added a commit that referenced this pull request Aug 14, 2026
The deployment record described a production environment that has not
existed since 2026-07-27. Three claims were wrong:

- `meeting` was listed as staging-only and absent from the production
  column of the topology diagram. It has had a production service since
  2026-07-27, and `/record` has been functional there since.
- `connectors` was listed as "provisioned on staging only". It has no
  Railway service in *either* environment — the project has six, and
  neither environment sets CONNECTORS_BASE_URL, CONNECTORS_API_KEY, or
  GOOGLE_CREDENTIALS_JSON.
- The service count read "seven shared services", counting a Railway
  entity that does not exist.

Read before the 2026-08-09 promotion, this would have suggested /record
was about to reach production for the first time, and that connectors
was running in staging when nothing was.

Historical release-log entries keep their original text and gain dated
corrections — they were accurate when written. "Current state" and the
topology diagram are rewritten to describe what is actually deployed.

Also adds the 2026-08-09 promotion (PR #189, merge f692b0b) to the
release log, including the reason the #179 Dockerfile port changes were
inert on Railway and how to prove a service reads its railway.json.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant