Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
154 commits
Select commit Hold shift + click to select a range
a60b33b
fix: make Loops contact creation non-blocking
shafeeq27edu-ai Aug 19, 2026
b57689d
test: fix AuthService patch target in commoners test
shafeeq27edu-ai Aug 25, 2026
3f3adfd
test: mock LoopsConfig with MagicMock in email tests
shafeeq27edu-ai Aug 25, 2026
e92b722
Merge branch 'main' into fix/async-email-contact
shafeeq27edu-ai Aug 27, 2026
5e8cf96
fix(api): add socket timeout and health check to the durable Redis st…
unfitcoder101 Oct 14, 2026
e6840b1
fix(runner): disable geesefs multipart expiry
maalideekshith Oct 15, 2026
c0cbb8e
fix(runner): keep Daytona sandboxes active during turns
maalideekshith Oct 15, 2026
3fd6b6f
docs(sharing): specs for the 2026-10-26 decisions, needs setup and ch…
mmabrouk Oct 26, 2026
3a27066
feat(api): stop reading workspaces, workspace_members, projects.works…
Oct 28, 2026
04fc199
test(api): pin step 2b (no workspace reads, reruns, counts)
Oct 28, 2026
f57f95e
docs: step 2b upgrade note, design and spec updates
Oct 28, 2026
3809224
fix(api): name project columns instead of a wildcard; exact org detai…
Oct 28, 2026
ddeba91
test(api): seed the watchdog tenant without a workspace row (the ORM …
Oct 28, 2026
d719d80
fix(api): a workspace route segment that names no organization answer…
Oct 28, 2026
67b6da6
docs: the rollback to 0.127.0 deletes stale workspace member rows fir…
Oct 28, 2026
2bdfd4f
docs: the 2b upgrade note version is a placeholder (0.130.0); bookmar…
Oct 29, 2026
1626f28
docs(sharing): M7 spec follows the owner decisions of 2026-10-28
Oct 29, 2026
f23e1c8
docs(sharing): M7 refuses unsupported bindings at run start, not at save
Oct 29, 2026
168a9f2
feat(api): only the executing principal changes what durable work run…
Oct 28, 2026
c3ad538
fix(api): compare durable work inputs exactly and check the locked row
Oct 28, 2026
6a15be1
fix(api): check the evaluation edit rule on the locked run row
Oct 28, 2026
6e6be55
fix(api): simple evaluation edits check the rule on the locked row
Oct 28, 2026
74728a8
fix(api): a simple evaluation rename keeps the stored configuration
Oct 28, 2026
cbb99e1
fix(api): CodeRabbit on the edit rule: omitted data, target shapes, u…
Oct 28, 2026
baac078
feat(api,sdk): connection bindings and personal provider keys (M7)
Oct 28, 2026
9c8afa7
fix(api,sdk): refuse personal bindings that the run path cannot serve…
Oct 28, 2026
485abbf
refactor(api,sdk): one refusal type, no personal MCP contract yet, on…
Oct 28, 2026
572d511
fix(sdk,api): the SDK refuses a personal subscription login; one unsi…
Oct 28, 2026
7a41e18
fix(api,sdk): CodeRabbit on bindings and personal keys
Oct 28, 2026
c9ebcbe
fix(api): check bindings at run start only, never when an agent is saved
Oct 29, 2026
f816b60
feat(api): shared sessions never release a person's own credential (M7)
Oct 28, 2026
88df87c
fix(api): guard reads shares after the lock; child runs and queued se…
Oct 28, 2026
7256cce
fix(api): personal credentials only in a private conversation
Oct 28, 2026
c35633d
fix(api): refuse a run with no conversation before any read; no sessi…
Oct 28, 2026
818e907
test(api): read the mark by column name in the guard integration test
Oct 28, 2026
f2ce19e
fix(api): a child run cannot use a person's own account in this step
Oct 28, 2026
c1f4106
feat(api): each person's own Composio connection (M7)
Oct 28, 2026
74acd88
fix(api): read the Composio user from any connection shape
Oct 28, 2026
a2e2323
fix(api): personal Composio bindings name their own entry; unsigned o…
Oct 28, 2026
c515c9b
refactor(api): the tools path uses the one unsigned-binding refusal
Oct 28, 2026
f8d7d56
feat(api,sdk): each person's own MCP server (M7)
Oct 28, 2026
be0611e
fix(api): listing and deleting personal MCP servers is not blocked by…
Oct 28, 2026
0243aff
fix(api,runner): a personal MCP server relays through the HTTP adapter
Oct 28, 2026
57b404f
feat(api): record which person's own account each turn used (M7)
Oct 28, 2026
0d5f8b0
fix(api): an unusable personal connection is refused before its use i…
Oct 28, 2026
b2e168b
fix(api): give run_credential_uses a nullable created_at per the life…
Oct 28, 2026
5663176
refactor(api): remove SecretMode from the secrets resolver
Oct 28, 2026
42ce712
fix(mobile): space onboarding Connect buttons on phone widths
Oct 30, 2026
f9ce320
fix(runner): do not resend a session/prompt request
Oct 30, 2026
cff0d88
fix(api): bump certifi and litellm in api/uv.lock for security alerts
Oct 31, 2026
4180b1b
fix: map EmbedError subclasses to 400 instead of 500
Christian-Sidak Nov 1, 2026
be389e0
fix(frontend): keep Other write-in box visible after clearing text on…
Christian-Sidak Nov 1, 2026
7351bd2
test(frontend): fix write-in unit tests to use a question with text: …
Christian-Sidak Nov 1, 2026
e1c0385
chore: add demo screenshot for PR description
Christian-Sidak Nov 1, 2026
32898fa
fix(runner): bump zod to 4.6.6 (Dependabot alert #1866)
Nov 1, 2026
6a1689a
fix(frontend): stop the Publish drawer reloading channels while the a…
ashrafchowdury Nov 1, 2026
b809286
fix(frontend): fold the agent's interim messages once the run is over
ashrafchowdury Nov 1, 2026
691e06c
fix(frontend): keep a resuming embed turn open and folded messages be…
ashrafchowdury Nov 1, 2026
50bd89b
fix(frontend): end the embed's answered phase when the gate clears mi…
ashrafchowdury Nov 1, 2026
765413f
fix(api): refuse a skill embed whose slug and id name different workf…
ashrafchowdury Nov 1, 2026
476f731
fix(api): resolve commit-time embed checks like a run, and keep the r…
ashrafchowdury Nov 1, 2026
aab2421
fix(frontend): show skeletons while agent config drawers load
ashrafchowdury Nov 1, 2026
26e9a1d
fix(frontend): keep the instructions drawer skeleton until the editor…
ashrafchowdury Nov 1, 2026
57cc388
chore(frontend): keep the drawer skeleton comments to one line
ashrafchowdury Nov 1, 2026
9d454f3
fix(frontend): switch playground sessions without waiting on the server
ashrafchowdury Nov 1, 2026
b8a50ff
test(chat): a message sent over any parked dock steers instead of que…
ashrafchowdury Nov 1, 2026
7918e63
fix(chat): steer a message sent over any parked dock
ashrafchowdury Nov 1, 2026
ecf972c
fix(frontend): stop skill drawers reporting edits nobody made
ashrafchowdury Nov 1, 2026
5c8c3fd
fix(frontend): close a contained dialog together with its drawer
ashrafchowdury Nov 1, 2026
80392ec
fix(frontend): compare raw editor markdown so an added escape still e…
ashrafchowdury Nov 1, 2026
64e7b28
fix(frontend): load a value set back to an edit the editor already re…
ashrafchowdury Nov 1, 2026
d181bfa
fix(frontend): gate in-app routes from the page so client navigations…
ashrafchowdury Nov 1, 2026
dd901e2
test(api): a lost turn promotes the next queued input
ashrafchowdury Nov 1, 2026
ca80dd0
fix(api): drain the queue when the watchdog settles a lost turn
ashrafchowdury Nov 1, 2026
5013c99
feat(frontend): download a skill as SKILL.md or a .zip
ashrafchowdury Nov 1, 2026
82910b8
fix(frontend): show a card for files the agent's answer links to
ashrafchowdury Nov 1, 2026
e84014a
fix(api): keep a promoted input recoverable when its delivery raises
ashrafchowdury Nov 1, 2026
16cc45f
fix(frontend): download the server's head and drop dot-segment paths
ashrafchowdury Nov 1, 2026
e73fd0d
fix(frontend): keep disable-model-invocation through upload; show dow…
ashrafchowdury Nov 1, 2026
19bdfbc
fix(frontend): match linked file cards to the exact drive file
ashrafchowdury Nov 1, 2026
4b3bd64
test(frontend): cover project switch destination and project-scoped q…
ashrafchowdury Nov 1, 2026
83a206a
fix(frontend): keep the screen on a project switch and key project qu…
ashrafchowdury Nov 1, 2026
c795855
fix(frontend): keep the switch helper at settings/switchContext
ashrafchowdury Nov 1, 2026
dc4b5d0
fix(frontend): restore the mobile import order in two settings tabs
ashrafchowdury Nov 1, 2026
c389788
fix(frontend): drop the automation version tag and match app trigger …
ashrafchowdury Nov 1, 2026
d4711d8
fix(workflows): add docstrings to handle_workflow_exceptions and wrapper
Christian-Sidak Nov 2, 2026
d43baa7
test(frontend): wait for the drawer body in the desktop permissions test
ashrafchowdury Nov 2, 2026
a0f69b8
fix(runner): override transitive undici to 8.12.2 (Dependabot alerts)
Nov 2, 2026
9150b5f
fix(frontend): create template agents without the connect step
ashrafchowdury Nov 2, 2026
356a81d
fix(frontend): retry a failed template pick as that template and keep…
ashrafchowdury Nov 2, 2026
cba667e
fix(frontend): show the config pane skeleton while its chunk loads
ashrafchowdury Nov 2, 2026
613a692
docs(api): update API reference from production OpenAPI spec
github-actions[bot] Nov 2, 2026
cb10d77
v0.128.2
ashrafchowdury Nov 2, 2026
555d6f0
Merge pull request #7939 from Agenta-AI/fix/onboarding-mobile-connect…
mmabrouk Nov 2, 2026
b038959
Merge pull request #7944 from Agenta-AI/automated/update-api-docs
mmabrouk Nov 2, 2026
67d3031
Merge pull request #7973 from Agenta-AI/fix/deps-runner-undici-8.12.2
mmabrouk Nov 2, 2026
793457a
Merge remote-tracking branch 'origin/release/v0.128.2' into fix/geese…
mmabrouk Nov 2, 2026
b345674
Merge pull request #7964 from Agenta-AI/fix/contained-dialog-closes-w…
mmabrouk Nov 2, 2026
4ffcce9
Merge branch 'release/v0.128.2' into fix/daytona-sandbox-autostop
Nov 2, 2026
9a93474
fix(frontend): skip tilde-fenced code when scanning answer links
mmabrouk Nov 2, 2026
cb8cb66
Merge release/v0.128.2 into fix/dependabot-api-certifi-litellm-otel
mmabrouk Nov 2, 2026
26232de
Merge release/v0.128.2 into fix/issue-7896
Nov 2, 2026
334102d
Merge pull request #7958 from Agenta-AI/fix/fold-interim-agent-messages
mmabrouk Nov 2, 2026
c2f0b19
Merge release/v0.128.2 into fix/deps-runner-zod-4.6.6
mmabrouk Nov 2, 2026
e106d53
Merge pull request #7957 from Agenta-AI/fix/publish-drawer-channel-lo…
mmabrouk Nov 2, 2026
b125e9a
Merge release/v0.128.2 into fix/redis-streams-socket-timeout
Nov 2, 2026
0753294
Merge remote-tracking branch 'origin/release/v0.128.2' into pr
mmabrouk Nov 2, 2026
3ab46f5
Merge pull request #7963 from Agenta-AI/fix/skill-drawer-false-dirty
mmabrouk Nov 2, 2026
352fef2
Merge release/v0.128.2 into docs/sharing-specs-2026-10-26
Nov 2, 2026
db3f4be
Merge release/v0.128.2 into feat/m4b-stop-reading-workspaces
Nov 2, 2026
e074044
Merge pull request #7961 from Agenta-AI/fix/instant-session-switch
mmabrouk Nov 2, 2026
1ee6cd4
Merge pull request #7943 from Agenta-AI/fix/runner-no-prompt-resend
mmabrouk Nov 2, 2026
5106480
style(runner): fix comment indentation in the keep-alive runTurn wrapper
Nov 2, 2026
7926065
Merge remote-tracking branch 'origin/release/v0.128.2' into fix/incon…
mmabrouk Nov 2, 2026
de5f83b
fix(api): renumber the step 2b revisions to oss000000062 and ee000000…
Nov 2, 2026
06fb0d5
style(runner): fix indentation in the mount failure detail
Nov 2, 2026
c4cace7
Merge pull request #7960 from Agenta-AI/fix/drawer-loading-skeletons
mmabrouk Nov 2, 2026
24aa784
Merge remote-tracking branch 'origin/release/v0.128.2' into pr6115
Nov 2, 2026
0abbe47
Merge pull request #7968 from Agenta-AI/fix/project-switch-keeps-section
mmabrouk Nov 2, 2026
33095ac
docs(sharing): settle CodeRabbit nits on needs setup and the M6 door …
Nov 2, 2026
d21bd59
fix(api): keep sign-up working when the Loops call fails
Nov 2, 2026
bc69718
Merge pull request #7966 from Agenta-AI/feat/download-skill
mmabrouk Nov 2, 2026
65e039a
fix(api): never report an id as the slug in an inconsistent embed error
mmabrouk Nov 2, 2026
cd8c151
Merge pull request #7970 from Agenta-AI/fix/automation-row-drift-tag
mmabrouk Nov 2, 2026
af02881
Merge release/v0.128.2 into feat/m7-remove-secret-mode
mmabrouk Nov 2, 2026
71c1f75
Merge release/v0.128.2 into fix/template-create-no-connect-gate
mmabrouk Nov 2, 2026
50141f5
fix(frontend): send staged files when a template is picked from the s…
mmabrouk Nov 2, 2026
b01dec3
Merge pull request #7967 from Agenta-AI/fix/zip-file-card-in-transcript
mmabrouk Nov 2, 2026
1d8b6e5
Merge pull request #7962 from Agenta-AI/fix/steer-over-every-dock
mmabrouk Nov 2, 2026
5f204ea
test(frontend): cover the restored write-in clear in OnboardingQuestion
Nov 2, 2026
3b4b63d
fix(runner): keep Daytona sandboxes active on cold and non-pooled tur…
Nov 2, 2026
cf91998
Merge release/v0.128.2 into feat/m4b-stop-reading-workspaces
Nov 2, 2026
3ba0ccf
fix(runner): keep the geesefs stderr tail in the mount failure detail
mmabrouk Nov 2, 2026
84116c6
Merge pull request #6115 from shafeeq27edu-ai/fix/async-email-contact
mmabrouk Nov 2, 2026
9f432f8
Merge pull request #7965 from Agenta-AI/fix/queue-drains-after-lost-turn
mmabrouk Nov 2, 2026
d438c7c
fix(api): keep accumulated stream entries on a read error, drop socke…
Nov 2, 2026
6a0d8a3
Merge release/v0.128.2 into fix/deps-runner-zod-4.6.6
mmabrouk Nov 2, 2026
c1668ec
Merge pull request #7952 from Christian-Sidak/fix/issue-7896
mmabrouk Nov 2, 2026
611c056
Merge pull request #7974 from Agenta-AI/fix/template-create-no-connec…
mmabrouk Nov 2, 2026
c9e6765
Merge pull request #7959 from Agenta-AI/fix/inconsistent-skill-embed
mmabrouk Nov 2, 2026
7932022
Merge pull request #7627 from maalideekshith/fix/daytona-sandbox-auto…
mmabrouk Nov 2, 2026
77f4287
Merge pull request #7604 from maalideekshith/fix/geesefs-multipart-ex…
mmabrouk Nov 2, 2026
c906fcf
Merge pull request #7955 from Christian-Sidak/fix/issue-7898-v2
mmabrouk Nov 2, 2026
98a64c9
Merge pull request #7587 from unfitcoder101/fix/redis-streams-socket-…
mmabrouk Nov 2, 2026
1e787bf
Merge pull request #7956 from Agenta-AI/fix/deps-runner-zod-4.6.6
mmabrouk Nov 2, 2026
82eecba
fix(api): keep miniopy-async 1.21.3 with the certifi security fix
mmabrouk Nov 2, 2026
4a9cf78
Merge pull request #7882 from Agenta-AI/feat/m4b-stop-reading-workspaces
mmabrouk Nov 2, 2026
395a543
Merge pull request #7836 from Agenta-AI/docs/sharing-specs-2026-10-26
mmabrouk Nov 2, 2026
5f75b84
Merge release/v0.128.2 into feat/m7-remove-secret-mode
mmabrouk Nov 2, 2026
e8f7075
fix(api): require miniopy-async 1.21.3 or later in 1.21
mmabrouk Nov 2, 2026
d8bc2f9
Merge pull request #7949 from Agenta-AI/fix/dependabot-api-certifi-li…
mmabrouk Nov 2, 2026
426c0e9
Merge pull request #7893 from Agenta-AI/feat/m7-remove-secret-mode
mmabrouk Nov 2, 2026
d0a1476
chore: stop Entire from pushing session transcripts to origin
Nov 2, 2026
5d3b904
Merge pull request #7988 from Agenta-AI/chore/entire-no-public-sessio…
mmabrouk Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .entire/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"enabled": true,
"telemetry": false,
"strategy_options": {
"push_sessions": false
},
"checkpoints": {
"primary": {
"type": "git-refs"
Expand Down
10 changes: 6 additions & 4 deletions api/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ Project membership and access:
- `check_action_access` delegates to `AccessService.check_request`. Every process that
checks access calls `bootstrap_access()` at startup (the API and both workers do).
- No new code reads `workspace_members` or `project_members` to decide access.
- No code creates or updates `workspaces` or `workspace_members` rows or writes
`projects.workspace_id` (workspace removal step 2). The API answers the organization id
where a workspace id was. Legacy workspace ids still map to their organization through
`db_manager` (`_WORKSPACE_REF_CTE`, `get_workspace`) until step 2b removes those reads.
- No deployed code reads or writes `workspaces`, `workspace_members`,
`projects.workspace_id` or `meters.workspace_id`, and the runtime ORM maps none of them
(workspace removal step 2b; step 3 drops them). The API answers the organization id where
a workspace id was, and a legacy workspace id is an unknown id. Migrations that need
these objects use frozen models, never `oss.src.models.db_models`.
`unit/memberships/test_workspace_storage_unread.py` pins it.
- A hot-path membership read is one SQL statement (`fetch_membership_row`,
`list_membership_rows`). `integration/memberships/test_hot_path_query_counts_postgres.py`
pins the counts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
from sqlalchemy import func, text
from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine

from oss.src.models.db_models import WorkspaceMemberDB as WorkspaceMemberDBE
from oss.src.models.db_models import ProjectDB as ProjectDBE
# Frozen legacy schema: the runtime models no longer map workspaces.
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedWorkspaceMemberDB as WorkspaceMemberDBE,
)
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedProjectDB as ProjectDBE,
)
from oss.databases.postgres.migrations.core.deprecated_models import (
AppDB,
AppVariantDB,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine
from sqlalchemy.future import select

from oss.src.models.db_models import WorkspaceMemberDB as WorkspaceMemberDBE
# Frozen legacy schema: the runtime models no longer map workspaces.
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedWorkspaceMemberDB as WorkspaceMemberDBE,
)
from oss.src.core.environments.dtos import (
EnvironmentCreate,
EnvironmentRevisionCommit,
Expand All @@ -22,7 +25,9 @@
EnvironmentVariantDBE,
)
from oss.src.dbs.postgres.git.dao import GitDAO
from oss.src.models.db_models import ProjectDB as ProjectDBE
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedProjectDB as ProjectDBE,
)

DEFAULT_BATCH_SIZE = 200
DEFAULT_ENVIRONMENTS = ("development", "staging", "production")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@
from sqlalchemy.future import select

from oss.src.models.db_models import (
ProjectDB,
UserDB,
)
from oss.src.models.db_models import (
OrganizationMemberDB,
WorkspaceMemberDB,
ProjectMemberDB,
)

# Frozen legacy schema: the runtime models no longer map workspaces.
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedProjectDB as ProjectDB,
DeprecatedWorkspaceMemberDB as WorkspaceMemberDB,
)


BATCH_SIZE = 100
DEMOS = "AGENTA_DEMOS"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from sqlalchemy.future import select
from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine

from oss.src.models.db_models import ProjectDB as ProjectDBE
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedProjectDB as ProjectDBE,
)
from oss.databases.postgres.migrations.core.deprecated_models import (
AppDB,
AppVariantDB,
Expand All @@ -36,7 +38,11 @@
EnvironmentRevisionData,
)
from oss.src.core.shared.dtos import Reference
from oss.src.models.db_models import WorkspaceMemberDB as WorkspaceMemberDBE

# Frozen legacy schema: the runtime models no longer map workspaces.
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedWorkspaceMemberDB as WorkspaceMemberDBE,
)


# Define constants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
from sqlalchemy import func
from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine

from oss.src.models.db_models import WorkspaceMemberDB as WorkspaceMemberDBE
from oss.src.models.db_models import ProjectDB as ProjectDBE
# Frozen legacy schema: the runtime models no longer map workspaces.
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedWorkspaceMemberDB as WorkspaceMemberDBE,
)
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedProjectDB as ProjectDBE,
)
from oss.databases.postgres.migrations.core.deprecated_models import EvaluatorConfigDB
from oss.src.dbs.postgres.workflows.dbes import (
WorkflowArtifactDBE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
from sqlalchemy import func
from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine

from oss.src.models.db_models import WorkspaceMemberDB as WorkspaceMemberDBE
from oss.src.models.db_models import ProjectDB as ProjectDBE
# Frozen legacy schema: the runtime models no longer map workspaces.
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedWorkspaceMemberDB as WorkspaceMemberDBE,
)
from ee.databases.postgres.migrations.core.deprecated_models import (
DeprecatedProjectDB as ProjectDBE,
)
from oss.src.dbs.postgres.testcases.dbes import (
TestcaseBlobDBE,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""fold the meter rows that still carry a workspace (rerun of ee0000000007)

Step 2b of workspace removal (design.md D7, meter-scope spec "Re-keying keeps every
count"). During the step 2 rollout, pods of the release before it (step 1) still
counted per-user `TRACES_RETRIEVED` under ids with a workspace part, after
ee0000000007 had moved every such row. Step-2 code never read those rows as current
and bumped them by their stored id. From this release on, code no longer reads
`meters.workspace_id`, so this revision folds those rows into their step-2 ids first.

It runs ee0000000007's `run` unchanged: the same refusals before any write, the same
`meters:report` lock for the whole run, the same keyset walk in batches of 1000, one
statement per batch that adds `value` and `synced` into the row with the new id, each
batch in its own transaction with `lock_timeout = 2s` and `statement_timeout = 60s`.
With no row to fold, it takes no lock and needs no Redis.

Idempotent: a rerun finds no row with a workspace. One-way like ee0000000007:
`downgrade()` does nothing; restore the pre-release dump of `meters` to undo it. Data
only, so the migration gate reports no finding.

Revision ID: ee0000000009
Revises: ee0000000008
Create Date: 2026-10-28 00:00:00.000000
"""

import importlib.util
from pathlib import Path
from typing import Sequence, Union

from alembic import op
from sqlalchemy import text

revision: str = "ee0000000009"
down_revision: Union[str, None] = "ee0000000008"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

_STEP_2_REVISION = "ee0000000007_rekey_meters_without_workspace.py"


def _step_2_revision():
"""ee0000000007, loaded from its file (revision files are not a package)."""

path = Path(__file__).with_name(_STEP_2_REVISION)
spec = importlib.util.spec_from_file_location("_ee0000000007", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def upgrade() -> None:
with op.get_context().autocommit_block():
connection = op.get_bind()
connection.execute(text("SET lock_timeout = '2s'"))
connection.execute(text("SET statement_timeout = '60s'"))
try:
_step_2_revision().run(connection)
finally:
connection.execute(text("RESET lock_timeout"))
connection.execute(text("RESET statement_timeout"))


def downgrade() -> None:
"""One-way data change, as ee0000000007. Restore the pre-release dump of `meters`
to undo it."""
3 changes: 0 additions & 3 deletions api/ee/src/dbs/postgres/meters/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,6 @@ async def fetch(
)
):
stmt = stmt.filter_by(organization_id=scope.organization_id)
# A row with a workspace is legacy (a step-1 pod wrote it during the
# rollout): never the current row; the next fold merges it.
stmt = stmt.filter(MeterDBE.workspace_id.is_(None))
stmt = stmt.filter_by(project_id=scope.project_id)
stmt = stmt.filter_by(user_id=scope.user_id)

Expand Down
2 changes: 1 addition & 1 deletion api/ee/src/dbs/postgres/meters/dbes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class MeterDBE(Base, MeterDBA):
),
# Org-rollup scan path used by /billing/usage (filter on org only) and
# entitlements soft-check fallback (filter on the full prefix).
# Finer scope dimensions (workspace/project/user) are sparse and always
# Finer scope dimensions (project/user) are sparse and always
# filtered alongside `organization_id`, so PG handles them in-memory
# against the rows this index narrows down.
Index(
Expand Down
155 changes: 155 additions & 0 deletions api/ee/tests/pytest/integration/meters/test_meters_refold_postgres.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""Revision `ee0000000009` (step 2b fold of meter rows that still carry a workspace).

ee0000000007 moved every row with a workspace. During the step 2 rollout, step-1 pods
could still write a per-user `TRACES_RETRIEVED` row under its old id (a straggler).
ee0000000009 reruns the same walk, so the straggler is added into the re-keyed row and
no row keeps a workspace before step 2b code stops reading the column.

Each case seeds its own organization and limits the walk to it.
"""

import functools
import importlib.util
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4

import pytest

from oss.src.dbs.postgres.shared.engine import get_transactions_engine
from oss.tests.pytest.utils.membership_seed import (
delete_seeded_organization,
seed_organization,
)
from ee.tests.pytest.integration.meters.test_meters_rekey_postgres import (
_FakeRedis,
_id,
_insert,
_meters,
)


pytestmark = pytest.mark.integration

_VERSIONS = (
Path(__file__).resolve().parents[4]
/ "databases/postgres/migrations/core_ee/versions"
)


@functools.lru_cache(maxsize=1)
def _revision():
path = _VERSIONS / "ee0000000009_refold_meters_without_workspace.py"
spec = importlib.util.spec_from_file_location("_revision_ee009", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def _run(organization_ids):
step_2 = _revision()._step_2_revision()

async def run():
engine = get_transactions_engine()._engine
async with engine.connect() as connection:
connection = await connection.execution_options(
isolation_level="AUTOCOMMIT"
)
return await connection.run_sync(
lambda sync: step_2.run(
sync,
organization_ids=organization_ids,
lock=step_2.ReportLock(_FakeRedis()),
)
)

return run()


@pytest.fixture
async def org():
seeded = await seed_organization(members={"m": "editor"})
yield SimpleNamespace(
id=seeded.organization_id,
ws=seeded.workspace_ids[0],
prj=seeded.project_ids[0],
usr=seeded.user_ids["m"],
)
await delete_seeded_organization(seeded)


def test_ee009_follows_ee008_and_reuses_ee007_run():
revision = _revision()
assert (revision.revision, revision.down_revision) == (
"ee0000000009",
"ee0000000008",
)
step_2 = revision._step_2_revision()
assert step_2.revision == "ee0000000007"
assert callable(step_2.run)


async def test_ee009_folds_a_straggler_into_the_rekeyed_row(org):
# ee0000000007 already moved the row: it has the step-2 id and no workspace.
await _insert(org=org.id, wrk=None, prj=org.prj, usr=org.usr, value=40, synced=30)
# A step-1 pod counted three more reads under the old id after the move.
await _insert(org=org.id, wrk=org.ws, prj=org.prj, usr=org.usr, value=3, synced=1)
counts = await _run([org.id])
rows = await _meters(org.id)
new_id = _id(org=org.id, prj=org.prj, usr=org.usr)
assert list(rows) == [new_id]
assert (rows[new_id]["value"], rows[new_id]["synced"]) == (43, 31)
assert rows[new_id]["workspace_id"] is None
assert counts["rows"] == 1


async def test_ee009_leaves_no_row_with_a_workspace_and_is_idempotent(org):
await _insert(org=org.id, wrk=None, prj=org.prj, usr=org.usr, value=4, synced=4)
await _insert(org=org.id, wrk=uuid4(), prj=org.prj, usr=org.usr, value=2, synced=0)
await _run([org.id])
after_first = await _meters(org.id)
assert all(row["workspace_id"] is None for row in after_first.values())
counts = await _run([org.id])
assert counts == {"batches": 0, "rows": 0, "inserted_or_folded": 0}
assert await _meters(org.id) == after_first


async def test_ee009_without_stragglers_changes_nothing(org):
await _insert(org=org.id, wrk=None, prj=org.prj, usr=org.usr, value=9, synced=5)
before = await _meters(org.id)
counts = await _run([org.id])
assert counts["rows"] == 0
assert await _meters(org.id) == before


def test_ee009_upgrade_runs_ee007_in_an_autocommit_block(monkeypatch):
revision = _revision()
calls = []

class _StepTwo:
@staticmethod
def run(bind, **kwargs):
calls.append(kwargs)

class _Context:
def autocommit_block(self):
import contextlib

return contextlib.nullcontext()

statements = []

class _Bind:
def execute(self, statement):
statements.append(str(statement))

monkeypatch.setattr(revision, "_step_2_revision", lambda: _StepTwo)
monkeypatch.setattr(revision.op, "get_context", lambda: _Context())
monkeypatch.setattr(revision.op, "get_bind", lambda: _Bind())
revision.upgrade()
assert calls == [{}]
assert statements[:2] == [
"SET lock_timeout = '2s'",
"SET statement_timeout = '60s'",
]
assert statements[-2:] == ["RESET lock_timeout", "RESET statement_timeout"]
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ async def _cascade(uid):
calls.append(("cascade", uid))
return [ORG_A, ORG_B]

async def _batch(*, org_ids, workspace_ids, project_ids, user_ids):
async def _batch(*, org_ids, project_ids, user_ids):
calls.append(("batch", list(user_ids)))

monkeypatch.setattr(m, "is_ee", lambda: True)
Expand Down
Loading
Loading