Skip to content

feat: rename Thingtime plugin and add login command - #560

Open
lopugit wants to merge 12 commits into
developfrom
codex/thingtime-plugin-accounts
Open

feat: rename Thingtime plugin and add login command#560
lopugit wants to merge 12 commits into
developfrom
codex/thingtime-plugin-accounts

Conversation

@lopugit

@lopugit lopugit commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • rename the ChatGPT/Codex plugin package and MCP server to Thingtime
  • add @Thingtime login and @Thingtime list accounts command mappings, backed by the existing OAuth callback and multi-account connection flow
  • make bridge access, refresh, and connection credentials non-expiring by default while retaining server-side revocation

Validation

  • corepack pnpm --dir remix run test:chatgpt
  • corepack pnpm --dir remix exec eslint app/api/utils/chatgpt/plugin.ts app/api/utils/chatgpt/pluginCore.ts app/api/utils/chatgpt/plugin.test.ts
  • corepack pnpm --dir remix run build
  • git diff --check

Manual follow-up

  • In a freshly installed plugin build, issue @Thingtime login, complete the host OAuth callback, add two named accounts, and verify @Thingtime list accounts reports safe metadata for both.

@github-actions github-actions Bot added lopu: mergeable The PR branches can currently be merged without conflicts lopu: overlapping files This PR changes files also changed by another open PR labels Sep 1, 2026
@github-actions
github-actions Bot temporarily deployed to develop-pr-560 September 1, 2026 10:19 Destroyed
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

✅ Develop S3 preview ready

The alias passed the develop bucket CORS preflight and a final live PR/SHA fence.

Generic Vercel Preview deployments use the shared development runtime; this controller adds the stable exact-SHA alias and marker-scoped cleanup.

@github-actions github-actions Bot added lopu: unknown state GitHub is still computing the PR branch state and removed lopu: mergeable The PR branches can currently be merged without conflicts labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Lopu review — infinite-expiry switch is well-supported; one gap fixed in this branch.

I checked each layer of the expiresAt: null change rather than assuming, and the infrastructure genuinely does support it: createSession only defaults when the option is undefined so an explicit null survives; getLiveSession treats null as never-expiring; signJwt already accepts expiresIn: null; and the sessions TTL index carries an explicit comment that null-expiry docs are exempt. infiniteExpiryFilter also matches the idiom already used in apps/grants.ts and apps/namespace.ts. No stale expiresAt: { $gt: now } filter was left behind that would have silently rejected the new sessions.

The gap: revoked bridge sessions could never be reaped

revokeMcpConnection stamps only revokedAt. Before this PR those docs carried a real expiresAt, so the TTL index eventually removed them. With expiresAt: null they became permanently un-reapable — the TTL index skips nulls — so every disconnected ChatGPT connection would accumulate in sessions forever. That's the exact failure mode the index's own comment warns about ("without it expired/revoked sessions … pile up forever"), and patTokens.ts:236 already has the right idiom for it.

Fixed at the single source of truth:

  • auth/sessions.ts — added REVOKED_SESSION_REAP_MS + revokedSessionPatch(); revokeSession now uses an aggregation-pipeline update that fills expiresAt via $ifNull only when absent. A session with a real expiry keeps it, so this is a no-op for browser/logout/account-switcher/SSO callers.
  • chatgpt/plugin.ts — the two bulk updateOne/updateMany disconnect paths use the same patch, not just the early returns.
  • plugin.test.ts — pins the patch shape and the "keeps an existing expiry" property.

This also closes the same latent gap for never-expiring service-account sessions.

Validation: test:chatgpt 26/26 · full test:unit exit 0 · tsc --noEmit clean on both changed files · typecheck ratchet 3/3 (backlog not increased).

Two deliberate tradeoffs worth stating out loud (no change made)

  1. Access tokens no longer carry exp, so a leaked bearer token is valid until its session is explicitly revoked. Revocation still works instantly and this mirrors the existing PAT model — but it does raise the cost of a leak.
  2. Dropping expires_in is legal (RFC 6749 §5.1 marks it RECOMMENDED, not required) and consistent with tokens that never expire. Worth one live check that the ChatGPT/Codex connector doesn't use it to schedule refresh.

@github-actions github-actions Bot added lopu: mergeable The PR branches can currently be merged without conflicts and removed lopu: unknown state GitHub is still computing the PR branch state labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Lopu repository review

Lopu reviewed this PR against develop as Thingtime's principal PR and repository manager. Using Claude Opus 5.

Lopu found no justified local change to publish from this review pass.

Lopu review — PR #560 · codex/thingtime-plugin-accountsdevelop

Compared 79d2436c against develop @ 4387af92. 6 files / ~116 lines of real source.

Check state

gh pr checks 560: 24 pass / 57 skipping / 0 fail, MERGEABLE / CLEAN. Nothing failing.
CodeQL snapshot for this head is empty; 560.json stays [].

The title no longer describes the branch

The PR is titled "feat: rename Thingtime plugin and add login command", and d22bd404
(feat(chatgpt): add Thingtime login command) is indeed on the branch. But that work is already on
develop
: I diffed remix/app/api/utils/chatgpt/ between the base and the head and the only
remaining delta is plugin.ts (+26/−5) and plugin.test.ts (+39/−1) — the session-reaping fix, not
the login command.

What actually remains to merge is a session-lifecycle fix, and it is a good one. The title and
body should be updated before merge so the merge commit and CHANGELOG describe what landed.

What the remaining change does

getLiveSession checks revokedAt before expiry, so revoking is the authoritative kill switch. But
the sessions TTL index (col('sessions').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }))
skips expiresAt: null, and the never-expiring purposes — service accounts, PATs, the ChatGPT
MCP bridge — carry exactly that. A plain $set: { revokedAt } therefore stranded each revoked row
in Mongo forever. revokedSessionPipeline stamps a 30-day reap date only when there isn't one.

Findings

No defects. The three things I checked:

  1. No security regression from the new expiresAt. A revoked never-expiring session now has
    expiresAt = now + 30d instead of null — so any query filtering on liveness without checking
    revokedAt would newly match it. I read every such call site:
    liveAppSessionsFilter (grants.ts:24) and all four infiniteExpiryFilter uses in plugin.ts
    (688, 708, 761, 770) all carry revokedAt: null. And infiniteExpiryFilter matched
    expiresAt: null before the change anyway, so the match set is unchanged either way.
  2. The pipeline form is the right API, and hiding the $set body is the right fence.
    $ifNull only resolves inside an aggregation pipeline; handed to a plain updateOne the driver
    stores the literal { $ifNull: [...] } sub-document into expiresAt — no error, no type
    complaint, and the field is no longer a Date, so the TTL index skips it and the row is stranded
    exactly as before. Keeping revokedSessionSet unexported makes that regression unreachable rather
    than merely discouraged. The test asserts rest.length === 0, which is what pins it.
  3. Pipeline-update semantics are correct for every caller. findOneAndUpdate with a pipeline is
    supported (MongoDB 4.2+); aggregation $set with the dotted path 'meta.consumedAt' preserves
    sibling fields in the embedded document rather than replacing meta; and revoked in
    revokeMcpConnection is a plain array reused across two calls, which is safe.

The stage ordering in consumedSessionPipeline (reap first, meta.consumedAt second) is asserted in
the test, which is worth having — the reverse order would let the reap stage drop the marker.

Scope note

The comment on grants.ts is honest that the sibling purpose: 'app' sweeps (appTokens.ts,
apps.ts, appLifecycleCore.ts) still use a plain $set, that they are safe today for the
issueAppToken reason, and that unifying them is a deliberate follow-up. I agree with drawing the
line there — widening the blast radius of a session-revocation change beyond what the diagnosis
covers is how a small correct fix becomes a large uncertain one. Worth a follow-up issue so the
asymmetry does not become folklore.

Changes made

None.

Validation run

  • gh pr checks 560 / gh pr view 560.
  • Traced the TTL index definition (collections.ts:1156) and its documented expiresAt: null
    exemption, and read getLiveSession to confirm revokedAt precedes the expiry check.
  • Audited every liveAppSessionsFilter / infiniteExpiryFilter call site for revokedAt: null.
  • Confirmed the chatgpt/ delta versus develop is only the two files above — i.e. the titled
    feature has already landed.
  • The test:chatgpt suite (app/api/utils/chatgpt/*.test.ts) needs the app's full dep tree, so I
    did not run it locally; CI's Build + typecheck ratchet + unit tests job runs it green on this head.

View Lopu workflow run

@github-actions
github-actions Bot temporarily deployed to develop-pr-560 September 1, 2026 10:52 Destroyed
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🦄 Lopu review — one revoke path was missed

Good, focused change. Reviewing this against the current develop rather than the merge
base matters here: the plugin rename, login_thingtime, and the expiresAt: null switch
have all landed independently, so the real content of this PR is just
revokedSessionPatch + its two call sites.

Which makes the gap easier to see — the highest-frequency revoke on the bridge does not
use the new patch
:

exchangeRefreshTokenGrant burns one refresh session on every rotation, and
createMcpRefreshGrant mints those with expiresAt: null:

const consumed = await (await getSessionsCollection()).findOneAndUpdate(
  refreshFilter,
  { $set: { revokedAt: now, 'meta.consumedAt': now } },   // ← no reap date
  { returnDocument: 'before' }
);

The sessions TTL index (mongodb/collections.ts:880) skips non-Date expiresAt — as
that file's own comment says — so every rotation permanently strands one dead document.
Before the infinite-expiry switch these carried a 180-day expiry and aged out on their
own. This is exactly what revokedSessionPatch exists to prevent.

Applied in this PR's worktree:

  • extracted consumedSessionPatch(now) next to infiniteExpiryFilter — the same
    two-stage pipeline the disconnect path already uses ($ifNull preserves a real expiry,
    so it is also correct for the short-TTL grants)
  • exchangeRefreshTokenGrant now uses it
  • added a regression test asserting the pipeline shape — without a named helper the call
    site could not be covered at all, which is why the existing revokedSessionPatch test
    did not catch this

The authorization-code consumption path a few lines above is not affected: those
sessions carry a real 5-minute expiry.

app/api/utils/chatgpt/*.test.ts 27/27 · lint clean · tsc 154 errors with zero in
chatgpt/ or auth/sessions (matches the documented pre-existing baseline) · ratchet 3/3.

Follow-up, out of scope here: passwordResets.ts:53 revokes all of a user's live
sessions with no purpose filter and no reap date, so it strands never-expiring bridge
sessions the same way. Same for appTokens.ts:72. Worth a sweep now that the helper exists.

@github-actions github-actions Bot added lopu: conflicting GitHub reports merge conflicts for the current PR snapshot lopu: queued The current PR snapshot is waiting in Lopu's PR-management queue and removed lopu: mergeable The PR branches can currently be merged without conflicts labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu live PR update

Status: ↪️ Resolver finished; a newer conflict remains

Current phase: The next detector event will own the current branch state

Estimated completion: Done — no further active-work ETA.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-01 13:33 UTC (UTC+00:00) 2026-09-01 06:33 PDT (UTC-07:00) 2026-09-01 23:33 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 13
Repository Conflicting 7
Repository Out-of-date with target 2
Repository GitHub state unknown 0
Repository Part of an open stack 0
Repository Touch files changed by another open PR 36
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 1
This resolver batch Currently resolving 0
This resolver batch Waiting 0
This resolver batch Finished 1

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 3 changed files are also touched by #57, #68, #92, #93, #101, #105, #116, #122, #130, #131, #132, #133, +8 more.

Exact branch pair: developcodex/thingtime-plugin-accounts.

Timeline

  • 11:26 UTC — Detected conflicts between develop and codex/thingtime-plugin-accounts; assigning the exact snapshot to the resolver queue.
  • 11:28 UTC — Still safely queued behind earlier admitted Lopu work; no duplicate resolver was spawned.
  • 11:38 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 11:49 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 11:59 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 12:10 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 12:20 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 12:30 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 12:41 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 12:51 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 13:01 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 13:12 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 13:22 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 13:32 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 13:33 UTC — The worker published its verified result, but GitHub reports the latest PR state as conflicting again; Lopu will rediscover it automatically.

Technical run details — optional; this comment is the human-facing source of truth.

@github-actions github-actions Bot removed the lopu: queued The current PR snapshot is waiting in Lopu's PR-management queue label Sep 1, 2026
@github-actions
github-actions Bot temporarily deployed to develop-pr-560 September 1, 2026 11:27 Destroyed
@github-actions github-actions Bot added the lopu: queued The current PR snapshot is waiting in Lopu's PR-management queue label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected merge conflicts

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 12:04 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-01 11:44 UTC (UTC+00:00) 2026-09-01 04:44 PDT (UTC-07:00) 2026-09-01 21:44 AEST (UTC+10:00)
Estimated finish 2026-09-01 12:04 UTC (UTC+00:00) 2026-09-01 05:04 PDT (UTC-07:00) 2026-09-01 22:04 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 40
Repository Conflicting 29
Repository Out-of-date with target 1
Repository GitHub state unknown 0
Repository Part of an open stack 0
Repository Touch files changed by another open PR 36
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 28
This resolver batch Currently resolving 0
This resolver batch Waiting 28
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 3 changed files are also touched by #57, #68, #92, #93, #101, #105, #116, #122, #130, #131, #132, #133, +8 more.

Exact branch pair: developcodex/thingtime-plugin-accounts.

Timeline

  • 11:44 UTC — Detected conflicts between develop and codex/thingtime-plugin-accounts; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected merge conflicts

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 12:58 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-01 12:38 UTC (UTC+00:00) 2026-09-01 05:38 PDT (UTC-07:00) 2026-09-01 22:38 AEST (UTC+10:00)
Estimated finish 2026-09-01 12:58 UTC (UTC+00:00) 2026-09-01 05:58 PDT (UTC-07:00) 2026-09-01 22:58 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 12
Repository Conflicting 8
Repository Out-of-date with target 2
Repository GitHub state unknown 0
Repository Part of an open stack 0
Repository Touch files changed by another open PR 7
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 7
This resolver batch Currently resolving 0
This resolver batch Waiting 7
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 2 changed files are also touched by #295, #485, #499, #564.

Exact branch pair: developcodex/thingtime-plugin-accounts.

Timeline

  • 12:38 UTC — Detected conflicts between develop and codex/thingtime-plugin-accounts; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu live PR update

Status: ✅ Lopu finished — this PR is mergeable

Current phase: GitHub verified the published branch result

Estimated completion: Done — no further active-work ETA.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-01 19:29 UTC (UTC+00:00) 2026-09-01 12:29 PDT (UTC-07:00) 2026-09-02 05:29 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 16
Repository Conflicting 1
Repository Out-of-date with target 2
Repository GitHub state unknown 0
Repository Part of an open stack 0
Repository Touch files changed by another open PR 8
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 9
This resolver batch Currently resolving 1
This resolver batch Waiting 0
This resolver batch Finished 8

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 2 changed files are also touched by #295, #485, #499, #564.

Exact branch pair: developcodex/thingtime-plugin-accounts.

Timeline

  • 12:46 UTC — Detected conflicts between develop and codex/thingtime-plugin-accounts; assigning the exact snapshot to the resolver queue.
  • 15:16 UTC — Still safely queued behind earlier admitted Lopu work; no duplicate resolver was spawned.
  • 15:27 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 15:38 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 15:48 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 15:59 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 16:10 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 16:21 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 16:31 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 16:42 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 16:53 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 17:04 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 17:15 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 17:25 UTC — 10-minute check-in: still working — Waiting in Lopu's serialized PR-management lane.
  • 17:32 UTC — Using repository context to resolve the conflict semantics.
  • 17:36 UTC — Rebuilding Graphify structure and semantic context.
  • 17:44 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 17:54 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 18:05 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 18:16 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 18:26 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 18:37 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 18:47 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 18:57 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 19:08 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 19:18 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.
  • 19:29 UTC — The resolver worker completed successfully and GitHub now reports this PR as mergeable.

Technical run details — optional; this comment is the human-facing source of truth.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌿 Lopu — I watched MongoDB actually delete the row

Head 79d2436c against develop @ 4387af92. All checks green, trusted CodeQL snapshot empty, dispositions left []. No correctness, security, or compatibility defect found.

Every earlier pass on this PR proved the field becomes a Date. None proved the consequence. So this pass ran the TTL monitor to completion against mongo:7, driving the real revokedSessionPipeline / consumedSessionPipeline imports with the exact index from collections.ts:1156:

revoked never-expiring session, reap date passed deleted by the TTL monitor
control — same row revoked with the old plain $set still there, expiresAt still null

The leak is real and this closes it. That is the whole claim, now observed rather than inferred.

Two properties I checked because nobody had

modifiedCount survives the pipeline conversion. This is not academic: revokeGrant hands result.modifiedCount straight back over the public API as revoked: N. Aggregation-pipeline updateMany reported matchedCount: 3, modifiedCount: 3 — no silent API regression.

Reaping is fail-closed. I deleted a consumed refresh row and replayed the exact findOneAndUpdate: nullinvalid_grant. Absence is indistinguishable from revoked, in the safe direction. Deleting the evidence never resurrects the credential.

The way this could have gone quietly wrong

Stamping a future expiresAt on a revoked row is dangerous if anything treats "not expired" as "live". I checked all seven readers that use $or: [{ expiresAt: null }, { expiresAt: { $gt: now } }]:

adminSnapshot.ts:99 · deviceAuth.ts:159 · appStorageManagement.ts:265 · namespace.ts:190 · namespace.ts:228 · browse.ts:157 · grants.ts:28

All seven also pin revokedAt: null. No reader can be fooled by the reap date. Clean.

One genuine behaviour change — and why I left it alone

Moving from an update operator to a pipeline changes what happens when meta is not a document:

pipeline    → silently REPLACES meta with { consumedAt: … }
plain $set  → throws "Cannot create field 'consumedAt' in element {meta: "legacy-string"}"

Measured, not guessed. But it is unreachable: createSession always writes meta: options.meta ?? {}, nothing in the codebase assigns a non-document meta, and migrations.ts:1205 normalizes meta: doc.meta || {}. A $type guard would be churn defending an impossible state, so I recorded it instead of writing it.

On the shape-only tests

The two new tests assert pipeline shape rather than Mongo behaviour, and I think that is the right ceiling here rather than a gap worth closing: no node --test suite in this repo has a Mongo harness, and the live API suite only touches the unauthenticated guards on these routes (auth-logout-anonymous, admin-apps-revoke-guarded). Adding that infrastructure is a much bigger conversation than this PR.

Worth saying that the shape tests do defend the exact regression the comments fear most — assert.equal(rest.length, 0) fails the moment someone unwraps the pipeline back into a bare $set body, which Mongo would store as a literal { $ifNull: … } sub-document and strand the row all over again.

Changed nothing, on purpose

The three sibling purpose: 'app' sweeps are provably safe (issueAppToken always stamps a real expiry), and appLifecycleCore runs inside a transaction against an injected collection double — converting it for uniformity carries more risk than the inconsistency does. Same call on the constant duplication (REVOKED_SESSION_REAP_MS vs THIRTY_DAYS_MS vs REVOKED_PAT_REAP_MS): retention window and default lifetime are separate knobs, and collapsing them couples two unrelated policies.

Validation

test:chatgpt 28/28 · test:schemas 109/109 · test:migrations 36/36 · eslint on all six changed files 0 errors · Mongo 7 verification 15/15.

Honestly: my first test:chatgpt run failed. That was me, not the PR — I installed with --ignore-scripts and skipped bcrypt's native binding, so the file died at module load. node scripts/ensure-bcrypt-binding.js, then 28/28. The one ESLint warning (registry.ts:4338, no-script-url) is pre-existing, identical on base, and is itself an XSS guard.

Still outstanding, and it is not a code problem

The title and body still describe the plugin rename and @Thingtime login, which are already in develop. Flagged an hour ago and unchanged — I am not re-arguing it, just noting it survives. The metadata is the only thing left between this branch and merge.

Ship it.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu review — title mismatch, and a good session-lifecycle fix underneath

Same pattern as #554: the plugin rename / login command is already on
develop, so the net diff against the merge base contains none of it. What
remains is an unrelated — and worthwhile — fix. Worth retitling so a reviewer
finds what the title promises.

On the fix itself: the leak is real and the design is careful. The
sessions TTL index is { expiresAt: 1 }, { expireAfterSeconds: 0 } (confirmed
in collections.ts), and TTL skips documents whose indexed field is not a Date
— so every revoked never-expiring session (service accounts, PATs, the ChatGPT
MCP bridge) was stranded permanently. Refresh rotation makes that unbounded: one
orphaned document per refresh.

The best decision here is what you refused to export. Keeping
revokedSessionSet module-private and exporting only the pipeline form means
the silent-regression path is unreachable rather than merely discouraged —
handed to a plain updateOne, $ifNull is stored as a literal sub-document
with no error and no type complaint, the field stops being a Date, and the row
is stranded exactly as before. That is the difference between a fix and a fix
that quietly comes undone.

Behaviour-safety check. My main concern was the side effect: a revoked
never-expiring session now carries expiresAt: now + 30d instead of null, so
any query filtering on expiry without revokedAt would change results. I
swept every sessions query in app/api/utils/:

apps/grants.ts · apps/browse.ts · apps/namespace.ts ×2 ·
apps/appStorageManagement.ts · apps/desktopOAuth.ts ·
devices/deviceAuth.ts · devices/devices.ts ×2 ·
chatgpt/plugin.ts infiniteExpiryFilter (all 4 call sites)

All 13 pair the expiry predicate with revokedAt: null, so a revoked row can
never resurface through the new value. getLiveSession also checks revokedAt
before expiry, so your comment about revocation staying the authoritative kill
switch is accurate.

consumedSessionPipeline composes correctly — reap stage first, and dotted
meta.consumedAt in an aggregation $set merges into the embedded document
rather than replacing it, so sibling meta fields survive. findOneAndUpdate
with returnDocument: 'before' accepts a pipeline, so the rotation call site is
fine.

Flagging your own follow-up so it does not get lost: the sibling
purpose: 'app' sweeps in appTokens.ts, apps.ts and appLifecycleCore.ts
still use a plain $set. Safe today for the issueAppToken reason you give —
worth an issue so "make the rule uniform" survives this PR.

No changes requested.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Lopu review — no defects found. One housekeeping item before merge.

The fix is correct and the premise is real

I verified the TTL claim rather than assuming it: mongodb/collections.ts:1156 is col('sessions').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }), and its own comment already notes that expiresAt: null docs are exempt because TTL skips non-dates. So a plain $set: { revokedAt } on a never-expiring session really did strand it in Mongo forever.

Keeping revokedSessionSet module-private is the load-bearing decision here, and the comment explaining it is exactly right: $ifNull only evaluates inside an aggregation pipeline, so handing that object to a plain updateOne would store the literal { $ifNull: [...] } sub-document — no error, no type complaint, but no longer a Date, so TTL skips it and the leak is silently back. Making that unreachable from outside the module beats documenting it.

I checked the "safe for the same issueAppToken reason" claim

The grants.ts comment says the sibling purpose: 'app' sweeps still use a plain $set and are safe today. That holds — I audited all five remaining sites rather than taking it:

Site Why it's safe
appTokens.ts:72 issueAppToken always sets expiresAt = now + APP_TOKEN_TTL_MS — a real Date
apps.ts:381 same; every purpose: 'app' row carries that expiry
appLifecycleCore.ts:65 same
desktopOAuth.ts:98 filter requires expiresAt: { $gt: now }, so the row always has a Date
plugin.ts:636 (exchangeAuthorizationCodeGrant) same — 5-minute code, filter requires expiresAt: { $gt: now }

The last two look like misses inside files this PR is already editing, but they aren't — both filters guarantee a real expiry. No gap found. Scoping and the honest follow-up note are both accurate.

Also correct: consumedSessionPipeline puts the reap stage first so meta.consumedAt can't be dropped by it (and the test pins the stage order), and aggregation $set with a dotted path merges rather than replacing meta, matching the previous plain-update semantics.

🟡 The description no longer matches the diff

The body describes three things — rename the plugin package/MCP server, add @Thingtime login / list accounts, and make bridge credentials non-expiring. GitHub's file list shows none of that remains in the delta; develop already absorbed it through the two merge commits on this branch. What's left is exactly the safety follow-up to the third bullet — the non-expiring switch is what created the unreapable-row hazard this now fixes.

Coherent work, but a reviewer reading the description will go looking for changes that aren't there, and the merge commit will describe the wrong thing. Worth retitling to the session-reaping fix and trimming the body. I haven't edited it — that's yours to word.

Validation caveat

The review worktree has no node_modules, so test:chatgpt and tsc --noEmit were not run. The two added tests are pure assertions over the pipeline shape and read correctly, but they're unverified by execution in this pass — let CI confirm.

Posted by Lopu, Thingtime's PR manager.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Lopu · the title no longer describes the branch

Small but worth fixing before merge: this PR is titled "feat: rename Thingtime plugin and add login command", but the head at 79d2436 contains none of that. What is here is a session-revocation TTL fixrevokedSessionPipeline, stamping a reap date on never-expiring sessions so the sessions TTL index can actually clear them. Worth retitling so the changelog and git log read true.

On the change itself: correct, and well-scoped. The bug is real — the TTL index is { expiresAt: 1 }, { expireAfterSeconds: 0 } and TTL indexes skip documents whose field is not a date, so a plain $set: { revokedAt } on a service account / PAT / MCP-bridge session revoked it correctly and then stranded the document forever.

The design detail I want to highlight is keeping revokedSessionSet module-private. That is not stylistic. $ifNull only evaluates inside an aggregation pipeline; handed to a plain updateOne, the driver stores the literal { $ifNull: [...] } sub-document into expiresAt — no error, no type complaint, but the field is no longer a Date, the TTL index skips it, and the row is stranded exactly as before. Making the unwrapped form unreachable turns a silent regression into a compile error, and there is a test asserting rest.length === 0 specifically to hold that.

I verified every site the PR deliberately leaves on a plain $set, rather than taking the comments on trust:

site filter verdict
chatgpt/plugin.ts:636 (auth-code grant) expiresAt: { $gt: now } safe — only matches a real future Date
apps/desktopOAuth.ts:98 expiresAt: { $gt: now } safe — same shape
apps/appTokens.ts:72, apps/apps.ts:381, apps/appLifecycleCore.ts:65 purpose: 'app', no expiry constraint safe today — issueAppToken unconditionally stamps expiresAt (appTokens.ts:53)
apps/grants.ts:97 liveAppSessionsFilter admits expiresAt: null hardened here, correctly, as a guard

The scoping is accurate and the comments are honest about which sites are "safe for a reason that could change" versus "fixed". exchangeRefreshTokenGrant — which uses infiniteExpiryFilter and genuinely can match a null-expiry row, once per refresh — is the one that mattered, and it is fixed.

No changes needed from me.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected merge conflicts

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 10:20 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 10:00 UTC (UTC+00:00) 2026-09-03 03:00 PDT (UTC-07:00) 2026-09-03 20:00 AEST (UTC+10:00)
Estimated finish 2026-09-03 10:20 UTC (UTC+00:00) 2026-09-03 03:20 PDT (UTC-07:00) 2026-09-03 20:20 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 15
Repository Conflicting 1
Repository Out-of-date with target 4
Repository GitHub state unknown 2
Repository Part of an open stack 0
Repository Touch files changed by another open PR 9
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 1
This resolver batch Currently resolving 0
This resolver batch Waiting 1
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 1 changed file is also touched by #295, #578, #592.

Exact branch pair: developcodex/thingtime-plugin-accounts.

Timeline

  • 10:00 UTC — Detected conflicts between develop and codex/thingtime-plugin-accounts; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected merge conflicts

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 10:38 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 10:18 UTC (UTC+00:00) 2026-09-03 03:18 PDT (UTC-07:00) 2026-09-03 20:18 AEST (UTC+10:00)
Estimated finish 2026-09-03 10:38 UTC (UTC+00:00) 2026-09-03 03:38 PDT (UTC-07:00) 2026-09-03 20:38 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 17
Repository Conflicting 1
Repository Out-of-date with target 6
Repository GitHub state unknown 1
Repository Part of an open stack 0
Repository Touch files changed by another open PR 10
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 7
This resolver batch Currently resolving 0
This resolver batch Waiting 7
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 1 changed file is also touched by #295, #578, #592.

Exact branch pair: developcodex/thingtime-plugin-accounts.

Timeline

  • 10:18 UTC — Detected conflicts between develop and codex/thingtime-plugin-accounts; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Branch status still computing. The base branch moved, and GitHub had not finished recomputing whether this PR conflicts or is behind after the detector waited 500s — so no branch update was started this round. The next push or the twice-hourly scheduled sweep (minutes :02/:32) re-checks automatically.

Posted by the conflict detector at 10:18 UTC, 2026-09-03; this notice is edited in place on re-checks.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected merge conflicts

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 11:07 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 10:47 UTC (UTC+00:00) 2026-09-03 03:47 PDT (UTC-07:00) 2026-09-03 20:47 AEST (UTC+10:00)
Estimated finish 2026-09-03 11:07 UTC (UTC+00:00) 2026-09-03 04:07 PDT (UTC-07:00) 2026-09-03 21:07 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 17
Repository Conflicting 5
Repository Out-of-date with target 5
Repository GitHub state unknown 2
Repository Part of an open stack 0
Repository Touch files changed by another open PR 9
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 10
This resolver batch Currently resolving 0
This resolver batch Waiting 10
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 1 changed file is also touched by #295, #578.

Exact branch pair: developcodex/thingtime-plugin-accounts.

Timeline

  • 10:47 UTC — Detected conflicts between develop and codex/thingtime-plugin-accounts; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected merge conflicts

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 12:12 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 11:52 UTC (UTC+00:00) 2026-09-03 04:52 PDT (UTC-07:00) 2026-09-03 21:52 AEST (UTC+10:00)
Estimated finish 2026-09-03 12:12 UTC (UTC+00:00) 2026-09-03 05:12 PDT (UTC-07:00) 2026-09-03 22:12 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 16
Repository Conflicting 4
Repository Out-of-date with target 5
Repository GitHub state unknown 1
Repository Part of an open stack 0
Repository Touch files changed by another open PR 9
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 9
This resolver batch Currently resolving 0
This resolver batch Waiting 9
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 1 changed file is also touched by #295, #578.

Exact branch pair: developcodex/thingtime-plugin-accounts.

Timeline

  • 11:52 UTC — Detected conflicts between develop and codex/thingtime-plugin-accounts; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected merge conflicts

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 13:08 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 12:48 UTC (UTC+00:00) 2026-09-03 05:48 PDT (UTC-07:00) 2026-09-03 22:48 AEST (UTC+10:00)
Estimated finish 2026-09-03 13:08 UTC (UTC+00:00) 2026-09-03 06:08 PDT (UTC-07:00) 2026-09-03 23:08 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 17
Repository Conflicting 3
Repository Out-of-date with target 5
Repository GitHub state unknown 1
Repository Part of an open stack 0
Repository Touch files changed by another open PR 9
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 10
This resolver batch Currently resolving 0
This resolver batch Waiting 10
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 1 changed file is also touched by #295, #578.

Exact branch pair: developcodex/thingtime-plugin-accounts.

Timeline

  • 12:48 UTC — Detected conflicts between develop and codex/thingtime-plugin-accounts; assigning the exact snapshot to the resolver queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lopu: conflicting GitHub reports merge conflicts for the current PR snapshot lopu: overlapping files This PR changes files also changed by another open PR lopu: queued The current PR snapshot is waiting in Lopu's PR-management queue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant