# Upgrade Guide — v1.2.0 (telegram-history-backfill + test-env GIT_DIR hardening)

<!-- bump: minor -->

## What Changed

Two changes ship together. The headline is the new operator-driven
recovery primitive; the test-env hardening from PR #286 rides along in
the same release.

**1. New: telegram-history-backfill (MTProto recovery primitive).** A
script-tier path to restore `topic-memory.db` after a local-store loss
event. The 2026-05-20 cascade (a Node 22→25 host swap broke
`better-sqlite3`; the recovery procedure restored the module but left
topic-memory's local mirror near-empty) exposed a durable gap: the
Telegram Bot API cannot fetch chat history, so any local mirror loss
was previously permanent. Authenticates as the operator's account via
gramjs (MTProto), iterates forum topics, writes idempotently via the
existing `UNIQUE(message_id, topic_id)` constraint + `INSERT OR IGNORE`.
Three new files under `scripts/`:

- `scripts/lib/topic-memory-importer.mjs` — pure-DB importer with batch
  transactions and INSERT OR IGNORE idempotency.
- `scripts/lib/telegram-historian.mjs` — gramjs wrapper. Auth, topic
  listing, per-topic iteration. **Read-only Telegram posture** — no
  send/edit/delete method is exposed.
- `scripts/telegram-history-backfill.mjs` — CLI entry. Three driving
  modes for auth (interactive stdin, `--phone` + `--code-file` polling
  for SMS, `--password-file` for 2FA). The file-poll mode supports
  autonomous operation paired with instar's Secret Drop web form
  (avoids Telegram's anti-phishing "code was shared in a chat" block).

Session string stored at `~/.local/share/instar-echo-secrets/` mode
0600, outside any git tree.

**2. fix(test-env): close the inherited-GIT_DIR failure class.** Carried
forward from PR #286. When git invokes a hook (`.husky/pre-push` runs
`npm run test:smoke`), it sets `GIT_DIR` / `GIT_WORK_TREE` /
`GIT_INDEX_FILE` / `GIT_OBJECT_DIRECTORY` / `GIT_COMMON_DIR` on the
child env. Those variables override cwd-based repo resolution for
every descendant git process, so test fixtures that did
`git init && git commit` in a tmpdir actually committed into the
parent repo on whichever branch was checked out. That's what landed
`# Test Project` over the project's `README.md` on `main` via PRs #130
and #277 — the README clobber shipped to npm in the v1.1.x series
before PR #285 restored it.

The fix closes the failure class in five interlocking layers:

1. **`tests/vitest-setup.ts` strips the GIT_DIR family** from
   `process.env` before any test file loads. Single-line root-cause fix.
2. **`tests/helpers/git-test-env.ts` exports `sanitizedGitEnv()`** for
   fixtures that pass their own `env` object to `spawnSync` /
   `execFileSync`. Defense-in-depth on top of Layer 1.
3. **`scripts/pre-push-fixture-guard.mjs`** runs in `.husky/pre-push`
   before the smoke tests and refuses pushes whose ahead-of-upstream
   commits carry the historical fixture signature (author email in
   `{test@instar.local, t@t.com, t@e.com, test@test.com}` or subject
   matching `Initial commit` / `seed` / `init` / `Worktree commit N`).
4. **`scripts/check-repo-invariants.mjs`** runs as a dedicated
   `Repo Invariants` CI job and fails the build if `README.md` drops
   below 100 lines or if `file-0.txt` / `seed` appear at repo root.
5. **Removed `file-0.txt` and `seed`** stowaway files from repo root —
   fixture droppings that would have falsely passed Layer 4 on the
   PR adding it.

## Evidence

**Backfill — reproduction prior:** in topic 10873 on 2026-05-20, after
Echo's better-sqlite3 native-module cascade recovery, the agent's local
`topic-memory.db` showed 98 messages across 7 topics where the
operator's Telegram client showed thousands across 100+ topics. The
agent had no path to fetch the missing rows — Bot API has no
history-fetch primitive.

**Backfill — reproduction after:** ran against the lifeline supergroup
(`-1003742343280`) on 2026-05-20T21:00:27Z. Result:
`topics=101 fetched=10308 inserted=10207 skipped=101`. The 101
skipped rows match the pre-existing on-disk count exactly — the
UNIQUE constraint + `INSERT OR IGNORE` correctly de-duplicated the
overlap, no doubles introduced. Topic 10873 went from 43 to 93
messages, restoring the full deployment-lockdown arc.

**Backfill — idempotency:** verified by
`tests/unit/topic-memory-importer.test.ts` (12 cases). Re-running the
same batch produces zero new inserts; partial-overlap re-run inserts
only the new messages; same message_id across different topics is not
a duplicate; FTS index stays clean across re-runs; a regression case
reproduces the exact "post-truncation overlap" shape from the
2026-05-20 incident.

**Test-env GIT_DIR — reproduction prior:** the README clobber on
`main` was reproduced before the fix landed: `gh api
repos/JKHeadley/instar/contents/README.md --jq .size` returned `7`
(the literal text `# Test\n`) prior to PR #285. PR #285 restored the
README; PR #286 closed the underlying class so the same regression
cannot recur. Test-fixture pollution detection was verified end-to-end:
against a tmpdir repo with a commit subject `Initial commit`,
`node scripts/pre-push-fixture-guard.mjs` exits 1 and names the
offending commit. Repo-invariant detection was verified end-to-end:
against a repo where `README.md` has been trimmed to a single line,
`node scripts/check-repo-invariants.mjs` exits 1 and reports the line
count.

## What to Tell Your User

- "I can now restore Telegram message history into my local store if it ever gets wiped again. Tell me 'back up our Telegram history' and I run a one-time auth flow with you (phone number, then a one-time code through a secure form, not through this chat) and walk the supergroup top to bottom. The whole thing is read-only on the Telegram side and the code never passes through our chat, so Telegram does not flag the login."
- "The recovery itself is safe to re-run. If I get interrupted partway through, I just run it again — there is built-in deduplication that means existing messages are skipped, not duplicated."
- "Nothing user-visible from the test-env GIT_DIR fix carried in from PR 286. This release hardens instar's own development pipeline against a class of bug that, while invisible to running agents, had been quietly corrupting our published README. Agents continue to behave identically."

## Summary of New Capabilities

| Capability | How to Use |
|-----------|-----------|
| Telegram history backfill (script tier) | Operator-triggered. One-time auth via `node scripts/telegram-history-backfill.mjs --auth --phone +X...` plus a Secret Drop form for the SMS code, then `--chat <supergroup-id>` to walk every topic. |
| Idempotent topic-memory imports | Built on the existing `UNIQUE(message_id, topic_id)` constraint plus `INSERT OR IGNORE`. Re-running the backfill is a no-op for already-imported rows. |
| Session-string persistence | Stored at `~/.local/share/instar-echo-secrets/telegram-mtproto.session` (mode 0600, outside any git tree). Re-runs after the first auth are non-interactive. |
| GIT_DIR-leak hardening (carried from #286) | Automatic. Test runner strips the GIT_DIR env family before any test loads; pre-push fixture guard refuses pushes carrying fixture-style commits; CI repo-invariants job catches README clobbers and stowaway files. |

## Deferred (Tracked Follow-ups)

- Promotion of the backfill to a full `src/messaging/TelegramHistorian.ts`
  feature with three-tier tests (integration + e2e), an HTTP route on
  the agent server, a CLI subcommand on the `instar` binary, an
  agent-template update so every agent surfaces it, and a migration
  parity entry for existing agents. The script-tier primitive shipping
  now is the proof of concept and the recovery delivery; the durable
  feature is the next PR.
- Bitwarden as the canonical credential source. The credentials live
  in BW already (secure-note item created during the auth handshake),
  but the script reads from the sealed local file at
  `~/.local/share/instar-echo-secrets/telegram-mtproto.json`. A small
  follow-up wires the script to pull from `bw get` when BW is
  unlocked, falling back to the sealed file when not.
- Secret Drop hardening — the bridge that pulls a submitted code out
  of the in-memory store and writes it to the auth script's poll file
  had a parsing bug on first ship that consumed the code without
  extracting it, losing the value. Server-side fix planned: stop
  auto-deleting submissions on first retrieve; rely on the existing
  5-minute timeout for cleanup; add an explicit `?consume=true`
  parameter for callers that want one-shot semantics. Design approved
  by operator 2026-05-20; implementation is the next PR after this
  one lands.
