{
  "schemaVersion": 3,
  "dataset": {
    "version": 3,
    "date": "2026-08-13",
    "group": {
      "id": "developer-infrastructure",
      "name": "Developer Infrastructure"
    },
    "repository": {
      "id": "dagger",
      "repo": "dagger/dagger",
      "name": "Dagger",
      "keywords": [
        "Dagger CI"
      ]
    },
    "context": {
      "repository": "dagger/dagger",
      "url": "https://github.com/dagger/dagger",
      "description": "Automation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud",
      "homepage": "https://dagger.io",
      "language": "Go",
      "topics": [
        "agents",
        "caching",
        "ci-cd",
        "containers",
        "continuous-deployment",
        "continuous-integration",
        "devops",
        "docker",
        "graphql",
        "workflows"
      ],
      "license": "Apache-2.0",
      "defaultBranch": "main",
      "stars": 16155,
      "forks": 910,
      "openIssues": 141,
      "archived": false,
      "collectedAt": "2026-08-13T18:02:08.867432+00:00"
    },
    "news": {
      "repository": "dagger/dagger",
      "collectedAt": "2026-08-13T18:02:08.867432+00:00",
      "latestRelease": {
        "repository": "dagger/dagger",
        "tag": "v0.21.8",
        "title": "v0.21.8",
        "url": "https://github.com/dagger/dagger/releases/tag/v0.21.8",
        "publishedAt": "2026-07-29T17:07:24Z",
        "notes": "## v0.21.8 - 2026-07-29\n\n### Changed\n\n- Changeset diffs are now computed from filesystem metadata instead of full-content comparison, significantly speeding up diff computation for large directories. by @marcosnils in\n  https://github.com/dagger/dagger/pull/13615\n\n### Fixed\n\n- Dockerfile build layer caching so unrelated build-context changes no longer bust the cache: `COPY`-ed directories now get a content-based cache identity, re-keying downstream steps only when copied content\n  actually changes. by @marcosnils in https://github.com/dagger/dagger/pull/13765\n- The `--x-release` CLI re-exec so `_EXPERIMENTAL_DAGGER_RUNNER_HOST` is preserved (with a warning) instead of being stripped, and clarified its startup message to avoid implying it runs from any build. by\n  @tiborvass in https://github.com/dagger/dagger/pull/13752\n- Silent SDK sessions no longer retain frontend telemetry in memory while Cloud and OTLP export remain enabled. `DAGGER_SILENT` is now honored as the equivalent of `--silent`. by @sipsma in\n  https://github.com/dagger/dagger/pull/13762\n\n### What to do next?\n\n- Read the [documentation](https://docs.dagger.io)\n- Join our [Discord server](https://discord.gg/dagger-io)\n- Follow us on [Twitter](https://twitter.com/dagger_io)\n\n",
        "highlights": [
          "v0.21.8 - 2026-07-29",
          "Changed",
          "Changeset diffs are now computed from filesystem metadata instead of full-content comparison, significantly speeding up diff computation for large directories. by @marcosnils in",
          "Fixed",
          "Dockerfile build layer caching so unrelated build-context changes no longer bust the cache: COPY-ed directories now get a content-based cache identity, re-keying downstream steps only when copied content",
          "The --x-release CLI re-exec so EXPERIMENTALDAGGERRUNNERHOST is preserved (with a warning) instead of being stripped, and clarified its startup message to avoid implying it runs from any build. by"
        ],
        "prerelease": false
      },
      "upcoming": [
        {
          "repository": "dagger/dagger",
          "kind": "milestone",
          "title": "vsometime",
          "url": "https://github.com/dagger/dagger/milestone/37",
          "description": "High-priority issues that should be released soon, but are not strict blockers for the immediate next release.",
          "progress": 100,
          "openIssues": 0,
          "closedIssues": 36
        },
        {
          "repository": "dagger/dagger",
          "kind": "milestone",
          "title": "v1.0.1",
          "url": "https://github.com/dagger/dagger/milestone/130",
          "progress": 0,
          "openIssues": 1,
          "closedIssues": 0
        },
        {
          "repository": "dagger/dagger",
          "kind": "milestone",
          "title": "v1.0.0-beta.8",
          "url": "https://github.com/dagger/dagger/milestone/131",
          "progress": 100,
          "openIssues": 0,
          "closedIssues": 9
        },
        {
          "repository": "dagger/dagger",
          "kind": "milestone",
          "title": "v0.21.9",
          "url": "https://github.com/dagger/dagger/milestone/132",
          "description": "",
          "progress": 75,
          "openIssues": 1,
          "closedIssues": 3
        }
      ],
      "communityDiscussions": []
    },
    "runs": [
      {
        "collectedAt": "2026-08-13T12:26:38.318Z",
        "since": "2026-08-12T12:26:38.318Z",
        "observedCount": 23,
        "changedCount": 23
      },
      {
        "collectedAt": "2026-08-13T13:48:00.446149Z",
        "since": "2026-08-12T13:48:00.446149Z",
        "observedCount": 21,
        "changedCount": 21
      },
      {
        "collectedAt": "2026-08-13T16:19:22.035158Z",
        "since": "2026-08-12T16:19:22.035158Z",
        "observedCount": 17,
        "changedCount": 3
      },
      {
        "collectedAt": "2026-08-13T17:43:20.785491Z",
        "since": "2026-08-12T17:43:20.785491Z",
        "observedCount": 18,
        "changedCount": 2
      },
      {
        "collectedAt": "2026-08-13T17:47:07.884300Z",
        "since": "2026-08-12T17:47:07.884300Z",
        "observedCount": 18,
        "changedCount": 0
      },
      {
        "collectedAt": "2026-08-13T18:01:55.420671Z",
        "since": "2026-08-12T18:01:55.420671Z",
        "observedCount": 18,
        "changedCount": 0
      }
    ],
    "signals": [
      {
        "id": "github:dagger/dagger:issue:13626",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "issue",
        "title": "Python SDK ready for 1.0",
        "text": "Close this when the Python SDK is fully ready for 1.0 | Action | Status | PR | Comment | |---|---|---|---| | Module init | ✅ Done | — | We can initialize a module without problem with latest beta version. | | Client init | Missing | — | — | | Codegen for both | — | — | — | | Good default template | 🚧 | https://github.com/dagger/python-sdk/pull/10 | — | | No codegen at runtime | ✅ Done | https://github.com/dagger/dagger/pull/13593 | Merged | | SDK has full test coverage | 🚧 | — | — |",
        "url": "https://github.com/dagger/dagger/issues/13626",
        "createdAt": "2026-07-13T17:36:31Z",
        "updatedAt": "2026-08-12T16:01:13Z",
        "timestamp": "2026-08-12T16:01:13Z",
        "metrics": {
          "reactions": 1,
          "comments": 2
        },
        "labels": [],
        "author": "shykes",
        "state": "open",
        "assignees": [
          "eunomie"
        ],
        "change": "updated"
      },
      {
        "id": "github:dagger/dagger:issue:13627",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "issue",
        "title": "Typescript SDK ready for 1.0",
        "text": "Close this when the TypeScript SDK is fully ready for 1.0 | Action | Status | PR | Comment | |---|---|---|---| | Module init | ✅ Done | — | We can initialize a module without problem with latest beta version. | | Client init | ✅ Done | https://github.com/dagger/typescript-sdk/pull/9 and https://github.com/dagger/dagger/pull/13646 | Codegen is outside the engine | | Codegen for both | ✅ Done | https://github.com/dagger/typescript-sdk/pull/9 and https://github.com/dagger/dagger/pull/13646 | Merged and works | | Good default template | ✅ Done | https://github.com/dagger/typescript-sdk/pull/10 | Merged | | No codegen at runtime | ✅ Done | https://github.com/dagger/dagger/pull/13621 | CI is green, PR is approved, it is mergeable | | SDK has full test coverage | ✅ Done | https://github.com/dagger/typescript-sdk/pull/14, https://github.com/dagger/sdk-sdk/pull/13 | We still need more tests, it's in progress |",
        "url": "https://github.com/dagger/dagger/issues/13627",
        "createdAt": "2026-07-13T17:36:50Z",
        "updatedAt": "2026-08-12T16:01:19Z",
        "timestamp": "2026-08-12T16:01:19Z",
        "metrics": {
          "reactions": 0,
          "comments": 2
        },
        "labels": [],
        "author": "shykes",
        "state": "open",
        "assignees": [
          "TomChv"
        ],
        "change": "updated"
      },
      {
        "id": "github:dagger/dagger:issue:13628",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "issue",
        "title": "Go SDK is ready for 1.0",
        "text": "Close this when the Go SDK is fully ready for 1.0 - Module init - Client init - Codegen for both - Good default template - SDK has full test coverage",
        "url": "https://github.com/dagger/dagger/issues/13628",
        "createdAt": "2026-07-13T17:37:15Z",
        "updatedAt": "2026-08-12T16:01:02Z",
        "timestamp": "2026-08-12T16:01:02Z",
        "metrics": {
          "reactions": 0,
          "comments": 3
        },
        "labels": [],
        "author": "shykes",
        "state": "open",
        "assignees": [
          "grouville"
        ],
        "change": "updated"
      },
      {
        "id": "github:dagger/dagger:issue:13629",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "issue",
        "title": "Dang SDK is ready for 1.0",
        "text": "Close this when the Dang SDK is fully ready for 1.0 - Module init - Client init - Codegen for both - Good default template - SDK has full test coverage",
        "url": "https://github.com/dagger/dagger/issues/13629",
        "createdAt": "2026-07-13T17:37:31Z",
        "updatedAt": "2026-08-12T16:00:52Z",
        "timestamp": "2026-08-12T16:00:52Z",
        "metrics": {
          "reactions": 0,
          "comments": 2
        },
        "labels": [],
        "author": "shykes",
        "state": "open",
        "assignees": [
          "vito"
        ],
        "change": "updated"
      },
      {
        "id": "github:dagger/dagger:issue:13630",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "issue",
        "title": "Java SDK is ready for 1.0",
        "text": "Close this when the Java SDK is fully ready for 1.0 | Action | Status | PR | Comment | |---|---|---|---| | Module init | ✅ Done | — | We can initialize a module without problem with latest beta version. | | Client init | Missing | — | — | | Codegen for both | — | — | — | | Good default template | ✅ Done | https://github.com/dagger/java-sdk/pull/11 | — | | No codegen at runtime | ✅ Done | — | — | | SDK has full test coverage | 🚧 | — | — |",
        "url": "https://github.com/dagger/dagger/issues/13630",
        "createdAt": "2026-07-13T17:37:51Z",
        "updatedAt": "2026-08-12T16:06:09Z",
        "timestamp": "2026-08-12T16:06:09Z",
        "metrics": {
          "reactions": 0,
          "comments": 1
        },
        "labels": [],
        "author": "shykes",
        "state": "open",
        "assignees": [
          "eunomie"
        ],
        "change": "updated"
      },
      {
        "id": "github:dagger/dagger:issue:13631",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "issue",
        "title": "PHP SDK is ready for 1.0",
        "text": "Close this when the PHP SDK is fully ready for 1.0 - Module init - Client init - Codegen for both - Good default template - SDK has full test coverage",
        "url": "https://github.com/dagger/dagger/issues/13631",
        "createdAt": "2026-07-13T17:38:43Z",
        "updatedAt": "2026-08-12T16:01:07Z",
        "timestamp": "2026-08-12T16:01:07Z",
        "metrics": {
          "reactions": 0,
          "comments": 1
        },
        "labels": [],
        "author": "shykes",
        "state": "open",
        "assignees": [
          "tiborvass",
          "eunomie"
        ],
        "change": "updated"
      },
      {
        "id": "github:dagger/dagger:issue:13867",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "issue",
        "title": "Elixir SDK is ready for 1.0",
        "text": "Elixir SDK should be an installable module that passes all checks in sdk-sdk",
        "url": "https://github.com/dagger/dagger/issues/13867",
        "createdAt": "2026-08-10T16:10:35Z",
        "updatedAt": "2026-08-12T22:20:10Z",
        "timestamp": "2026-08-12T22:20:10Z",
        "metrics": {
          "reactions": 0,
          "comments": 2
        },
        "labels": [],
        "author": "kpenfound",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:issue:13883",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "issue",
        "title": "v0.x modules are given a v1.0 view",
        "text": "Related to, but not a duplicate of https://github.com/dagger/dagger/issues/13654 When a module is being developed for the 1.0.0-beta but still specifies an engineVersion of v0.x, there are two breaking error paths described below. The module can get into this broken state without the module author being aware because when using the module directly on 1.0.0-beta, a v0.x module is also served new APIs with the 1.0.0-0 view rather than the correct pre-1.0 view, so if the module uses a new 1.0.0-0 API, it will work on 1.0.0-beta despite the declared engineVersion. ## Users on v0.2x Users of a module described above running a v0.2x engine will fail to load the module because it references APIs that do not exist in v0.2x. The error will complain about the APIs not existing, which likely means nothing to the user. As far as they can tell, it should work on their engine because the declared engineVersion is compatible with theirs. This case has come up with users installing `github.com/dagger/go` on `v0.21.6`. The error was `convert arg ws: node field not found in environment`. This module should have been updated to `engineVersion: 1.0.0-0` when it started using the new APIs ## Users on 1.0.0-beta with dependencies Users of the beta with modules that depend on a module like the one described above will be stuck too. Codegen for the broken module will complete because it is given the 1.0.0-0 view, however when loaded as a dependency of their module it is now on a v0.2x view and the new APIs are no longer available. This case came up with `github.com/dagger/dagger/.dagger/modules/go` used as a dependency throughout the dagger/dagger repo. Its declared engineVersion was v0.21.7 but it uses Workspace.Git",
        "url": "https://github.com/dagger/dagger/issues/13883",
        "createdAt": "2026-08-12T21:03:31Z",
        "updatedAt": "2026-08-12T21:08:55Z",
        "timestamp": "2026-08-12T21:08:55Z",
        "metrics": {
          "reactions": 0,
          "comments": 1
        },
        "labels": [],
        "author": "kpenfound",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:issue:13889",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "issue",
        "title": "🐞 dagger module init fails \"outside changeset root\" when dagger.toml is in a subdirectory (monorepo layout)",
        "text": "### What is the issue? `dagger module init <sdk> <name>` fails when the selected `dagger.toml` lives in a **subdirectory** of the git repo rather than at the git root: ``` Error: sdk module init: failed to call sdk module initModule: workspace path .dagger/modules/hello is outside changeset root common ``` This breaks the common monorepo layout where several project workspaces live in subdirectories under a single git root (e.g. `project-1/dagger.toml`, `common/dagger.toml`). Running `dagger module init` from any of those subdirectories fails. ### Reproduction ```bash mkdir repo && cd repo git init mkdir common touch common/dagger.toml cd common dagger sdk install go dagger module init go hello -y ``` The last command fails with the error above. (Reproduced deterministically on `v1.0.0-beta.9`.) ### Actual vs. expected - **Actual:** `withInitModule` errors with `... is outside changeset root common`; no module is created. - **Expected:** a `hello` module scaffolded under `common/` (e.g. `common/.dagger/modules/hello/`), the same way it works when the config is at the git root. ### Root cause A cwd-vs-root mismatch across the engine ↔ SDK boundary: - The workspace **root** is the git root — `core/workspace/detect.go` walks up to `.git`; a `dagger.toml` does **not** define the root. So with `common/dagger.toml`, root = git root and **cwd = `common`**. - The engine computes the default module path **workspace-root-relative**: `.dagger/modules/<name>`, independent of cwd (`core/schema/workspace_module_init.go`, `relPath = filepath.Join(\".dagger\", \"modules\", args.Name)`), and passes it to the SDK's `initModule(path:)`. - The Go SDK's `initModule` stages files through `polyfill.workspace(ws).fork.withDirectory(path, ...)`. The polyfill (`github.com/dagger/polyfill`, `workspace-fork.dang` → `clientPath`) is **cwd-scoped**: it converts a root-relative path into cwd-local coordinates and **raises when the path is not under cwd**: ``` let relative = workspacePath.trimPrefix(cwd + \"/\") if (relative == workspacePath) { raise \"workspace path \" + workspacePath + \" is outside changeset root \" + cwd } ``` `.dagger/modules/hello` does not start with `common/`, so it raises. When the config is at the git root, `cwd == \".\"` and `clientPath` passes paths through unchanged — which is why it only fails from a subdirectory. Trace (plain progress) showing the boundary: ``` Workspace.withInitModule(name: \"hello\", sdk: \"go\") ├─ Workspace.cwd → \"common\" ├─ GoSdk.initModule(path: \".dagger/modules/hello\") │ └─ PolyfillWorkspaceFork.withDirectory(path: \".dagger/modules/hello\") ERROR │ ! workspace path .dagger/modules/hello is outside changeset root common ``` ### Workarounds (all tested on v1.0.0-beta.9) | Approach | Result | | --- | --- | | `dagger.toml` at the git root, run from root | ✅ works | | Give the subdir its own `.git` (subdir becomes the workspace root) | ✅ works | | `--path common/hello` (path under cwd) | ⚠️ **no error but silently corrupts**: engine config lands at `common/hello/dagger-module.toml` while the SDK's `main.go` lands at `hello/main.go` — a split, non-loadable module | | `-W common` from the git root | ❌ same error (root stays at the git root) | | `--here` | ❌ not a valid flag for `module init` | The `--path`-under-cwd case is arguably the more serious half: it avoids the error but silently produces a broken module, because the engine's config changeset is applied workspace-root-relative while the SDK's source changeset has the cwd prefix stripped. ### Suggested fix Reconcile the two path conventions: the engine emits workspace-root-relative module paths, while the polyfill fork applies caller-cwd-relative ones. Either the engine derives the default path relative to a location the cwd-scoped fork can reach, or engine-driven `initModule` changesets are applied workspace-root-relative. The polyfill notes it is coding around \"today's clients apply returned changesets relative to the caller cwd\", so the reconciliation likely belongs on the engine side. ### Dagger version ``` dagger v1.0.0-beta.9 engine v1.0.0-beta.9+1c6e07b1 go-sdk: github.com/dagger/go-sdk polyfill: github.com/dagger/polyfill@ec3ea84a2351b4beb06ecece951f2e5ef66509ff ```",
        "url": "https://github.com/dagger/dagger/issues/13889",
        "createdAt": "2026-08-13T08:52:40Z",
        "updatedAt": "2026-08-13T08:52:40Z",
        "timestamp": "2026-08-13T08:52:40Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "eunomie",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13754",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "core: pin latest container and Git releases",
        "text": "## Summary Implement #13464's package-manager-style latest-release lifecycle for Git repositories and container images. For Git: - add `GitRepository.latest` - select the greatest stable semantic-version tag, accepting both `v1.2.3` and `1.2.3` - ignore prerelease tags - fall back to the resolved remote HEAD when no stable release tag exists - load remote metadata lazily so frozen pins do not contact the remote For containers: - treat a bare image address such as `alpine` as a latest-release lookup - select the greatest stable semantic-version tag - ignore prerelease tags - fall back to the literal `latest` tag when no stable release tag exists - keep explicit tags such as `alpine:latest` and `alpine:3.22`, plus digested references, on the existing exact-resolution path - list registry tags using the existing authentication, mirror, network, protocol, and TLS settings ## Lockfile behavior The operations intentionally have different lock formats: | Operation | Inputs | Pin value | | --- | --- | --- | | `git.latest` | remote URL | selected symbolic ref and commit as `ref@commit` | | `container.from.latest` | normalized bare repository, platform, and optional transport settings | selected tag and digest as `tag@digest` | | `container.from` | explicit tagged reference, platform, and optional transport settings | resolved digest | All latest-release entries use pin policy. Pinned and frozen resolution reuse the stored selection instead of listing Git refs or container tags again. For example: - `git.latest`: `refs/tags/v0.21.4@0123456789abcdef0123456789abcdef01234567` - `container.from.latest`: `docker.io/library/alpine:3.22.1@sha256:...` - `container.from` for `alpine:latest`: `sha256:...` ## `dagger update` `dagger update` refreshes existing lock entries in place: - `git.latest` reloads the remote refs, selects the latest stable release again, and stores its current commit - `container.from.latest` relists registry tags, selects the latest stable release again, and stores its current digest - exact `container.from` entries resolve the explicit tag again and refresh its digest - container updates preserve optional HTTP/HTTPS and `insecureSkipTLSVerify` lock inputs The update operation does not need the previous pin to resolve successfully; it derives a replacement from the lock entry's operation and inputs. ## Status - [x] Implement latest-release selection and lock lifecycle for Git - [x] Implement latest-release selection and lock lifecycle for containers - [x] Keep explicit container tags and digests on exact `container.from` - [x] Support live, pinned, frozen, and update workflows - [x] Avoid eager Git remote metadata access in frozen mode - [x] Regenerate SDK clients and references for `GitRepository.latest` No container SDK regeneration is needed because the container behavior does not add or change a public GraphQL field. ## How to test Run the focused unit and schema tests: ```console go test ./core ./core/schema ./engine/server/resolver go test ./core/integration -run '^$' ``` Run the lock lifecycle tests against a development engine: ```console dagger api call engine-dev test \\ --pkg ./core/integration \\ --run='TestLockfile/(TestGitLatestLiveCreatesPin|TestGitLatestFrozenUsesPin|TestGitLatestFrozenDoesNotLoadRemoteMetadata|TestUpdateRefreshesExistingGitLatestEntry|TestContainerFromLatestLockLifecycle|TestFromLockfileLiveRefreshesEntry|TestLockUpdateRefreshesExistingEntry)$' ``` For a manual check, save this as `latest.graphql` in a workspace: ```graphql { git(url: \"https://github.com/dagger/dagger.git\") { latest { ref commit } } container { latestRelease: from(address: \"alpine\") { imageRef } explicitLatest: from(address: \"alpine:latest\") { imageRef } } } ``` Then exercise the lifecycle: ```console dagger --lock=live query --doc latest.graphql cat dagger.lock dagger --lock=frozen query --doc latest.graphql dagger update cat dagger.lock ``` The lockfile should contain `git.latest`, `container.from.latest`, and `container.from` entries with the formats described above.",
        "url": "https://github.com/dagger/dagger/pull/13754",
        "timestamp": "2026-08-12T13:11:12Z",
        "metrics": {
          "reactions": 0,
          "comments": 1
        },
        "labels": [],
        "author": "tiborvass",
        "assignees": [],
        "change": "new"
      },
      {
        "id": "github:dagger/dagger:pull_request:13850",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "feat(schema): require declared Workspace! args on module functions",
        "text": "Follows #13845, which made `currentModule.asSDK`'s workspace argument required. This does the same for module functions. ## Problem Workspace arguments on module functions were published as nullable no matter how they were declared, because the engine injects one when the caller leaves it unset (`core/typedef.go`, plus two spots in `core/schema/module.go`). So a function declaring `ws: Workspace!` advertised an optional argument — the same lie `asSDK` told before #13845. The modules already declare the truth. The Go SDK registers a plain `ws *dagger.Workspace` with no `.WithOptional(true)`, unlike a `// +optional` arg next to it: ```go // .dagger/modules/engine-dev/dagger.gen.go WithArg(\"ws\", dag.TypeDef().WithObject(\"Workspace\"), ...). WithArg(\"clientDockerConfig\", dag.TypeDef().WithObject(\"Secret\").WithOptional(true), ...). ``` The engine was overriding that. This stops the override. ## Why the callers had to change The injection can't rescue a required argument. dagql rejects a missing non-null argument in `preselect` (`dagql/objects.go`) *before* it runs the `GetDynamicInput` hook that fills workspace args in. So everything that relied on injection for a required arg now puts it on the selector, resolving it the way `loadWorkspaceArg` does — the workspace an enclosing group bound into the context, else the session's ambient one, and never inherited across a module function boundary. - **generators, checks, up** — all funnel through `ModTreeNode.DagqlValue`, so they're covered in one place, for the root constructor as well as the leaf function. - **scale-out** — needs no query change. It selects `Module.generator(name:)`, which binds no workspace, so it lands on the ambient fallback, which is what it relied on before. - **LLM tools** — `buildObjectMethodSelector` fills a required Workspace from the LLM's bound workspace. - **`dagger call`** — defaults it to `currentWorkspace`. `--ws` is still registered, via a new `workspaceValue` that resolves an address the way `--dir` does, so a caller can aim a function at a different workspace. It just never gets `MarkFlagRequired`, which would otherwise reject the call before the default could be filled in. An argument declared *optional* is unaffected: it stays nullable and keeps its injection. That's why `TestRuntimeDependencyDoesNotInheritWorkspace` is untouched — its fixture declares `// +optional`, so the #13229 isolation behavior and error message are unchanged. ## No codegen Deliberately none. Both edited files are on the module-runtime path, not the core schema: `FunctionSpec` builds the dagql `FieldSpec` for a module's functions at load time, and the `core/schema/module.go` blocks were inside the `functionArg`/`internalFunctionArg` resolvers, mutating the value they return rather than the schema shape. `schema.graphqls`, the SDK clients, the reference docs and the vendored toolchain clients are all unchanged, and module `dagger.gen.go` files would regenerate byte-identical. ## Draft: not yet run Compiles and vets clean for linux, and the integration test typechecks — but nothing here has executed. `core` and `core/integration` can't build natively on darwin (`engine/engineutil` uses linux-only syscalls), so this needs a dev engine run before it's reviewable. The generators/checks/up threading and the `dagger call` default-fill are the paths to exercise first. Changie entry still to add.",
        "url": "https://github.com/dagger/dagger/pull/13850",
        "createdAt": "2026-08-06T14:41:23Z",
        "updatedAt": "2026-08-12T21:27:05Z",
        "timestamp": "2026-08-12T21:27:05Z",
        "metrics": {
          "reactions": 0,
          "comments": 1
        },
        "labels": [],
        "author": "TomChv",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13851",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "feat: add experimental detachable sessions with source callback lifecycle",
        "text": "## What this adds A detachable session is an engine session that keeps running after the client that created it disconnects. Ordinary Dagger sessions end when their main client disconnects. This PR adds an explicit opt-in mode where accepted work can continue, a later CLI can find and inspect the session, and the user terminates it explicitly. This is useful for starting a long build or pipeline from a laptop or short-lived shell without keeping that terminal connected. Two commands accept the new flag: ```console dagger call --detach <function> [args...] dagger api call --detach <function> [args...] ``` A provisional command family manages detachable sessions: ```console dagger sessions list dagger sessions inspect <session-id> dagger sessions attach <session-id> dagger sessions terminate <session-id> ``` For example: ```console $ dagger call --detach build --platform linux/arm64 Session: sess_01j9xk2m4q7c8b3n6s0a9c3fgh Query: qry_01j9xk2p9w2a4c7m5k3p0q1rsv # The initiating CLI has exited. The accepted query continues on the engine. $ dagger sessions list ID STATE QUERY STATUS CREATED ATTACHED sess_01j9xk2m4q7c8b3n6s0a9c3fgh detached qry_01j9xk2p9w2a4c7m5k3p0q1rsv running 2026-08-06T18:12:04Z - $ dagger sessions inspect sess_01j9xk2m4q7c8b3n6s0a9c3fgh $ dagger sessions attach sess_01j9xk2m4q7c8b3n6s0a9c3fgh # Recorded progress is replayed, live progress follows, then the saved result is printed. $ dagger sessions terminate sess_01j9xk2m4q7c8b3n6s0a9c3fgh Terminated sess_01j9xk2m4q7c8b3n6s0a9c3fgh ``` The CLI performs schema discovery and argument conversion before submitting one final GraphQL request. The engine stores the request and returns `202 Accepted` with a query ID. After that acknowledgement, the initiating CLI may disconnect. Exit 0 from `--detach` means the engine accepted responsibility for the query. It does not mean the query succeeded. After creator disconnect: - Engine-only execution and values already copied into engine-owned storage can continue. - A new operation that still needs a creator callback fails individually. It does not terminate the session or unrelated work. - A published source that is no longer available returns a recognizable source-unavailable error without waiting for the initial-publication timeout. - An observer cannot replace or supply creator callbacks. - Source-backed host-to-container tunnels terminate and clean themselves up when their listener stream fails instead of remaining falsely registered as running. A later CLI can inspect or attach to the session. `dagger sessions terminate` cancels remaining work, stops services, releases session resources, and deletes saved result files. The detachable-session P0 and source-client callback designs are included under `hack/designs/`. ## P0 behavior ### Session lifetime and attachments The client generates the detachable session ID before the first request. A detachable session has one engine-owned cancellation root. Main-client disconnect only clears the current attachment. Explicit termination or engine shutdown cancels the session. A detachable session has at most one current attachment. Explicit close and connection health detection can clear it. Both paths compare the attachment ID and an engine generation so a delayed event from an old connection cannot clear a newer attachment. There are three connection roles: - The creator creates the session, performs discovery while attached, publishes its source callbacks, and submits the primary query. - An observer, created by `dagger sessions attach`, follows telemetry and reads the saved result. It does not initialize a generated client, load a workspace or module, or publish creator callbacks. - A control connection implements `list`, `inspect`, and `terminate` without creating a session. The creator retries only HTTP `409 attachment_connection_exists`. An observer retries only HTTP `409 already_attached`, for up to 30 seconds, and never replaces a live attachment. Other protocol responses and transport errors fail immediately. ### Stored query result and telemetry The engine saves the GraphQL response HTTP status, `Content-Type`, and body. The request limit is 16 MiB. The saved result limit is 64 MiB. An oversized result is discarded and reported as `result_discarded`; the session remains alive. Attaching never executes the query again. The detached query uses the creator client's existing telemetry store. Attach replays recorded traces, logs, and metrics through the existing SSE paths, then follows live records. At query completion the engine records a final row for each stream. Replay stops at those rows, even if services write later telemetry. ### Source callback identity and publication Each detachable, non-observer physical source may successfully publish its attachables once. A failed handshake before publication remains retryable. Successful publication records the physical source ID permanently for the lifetime of the detachable session. A later request with the same source ID is rejected before it can replace the original registration. Observer connections publish health only. They do not alter source publication state and are never callback candidates for the creator. This PR does not implement callback rebinding. ### Callback behavior after source loss Initial source publication retains its existing bounded wait so normal startup ordering continues to work. After a source has published successfully and its exact registration is gone: - A blocking lookup returns `source client \"<client-id>\" is unavailable` immediately. - Graceful deregistration makes that state visible immediately. - Abrupt transport loss follows the existing attachable health policy. - `IfAvailable` lookups retain their existing `ok=false` behavior. - Secret and socket candidate selection retains its existing fallback order and aggregate exhaustion error. - Callback failure does not cancel the detachable session or redirect the operation to an observer. Copied and engine-owned values continue without callbacks. This includes completed host snapshots and genuine engine-owned `setSecret` values. Provider-backed URI secrets such as `env://` remain callback-backed. ### Nested client shutdown Nested clients in a detachable session flush workspace locks and telemetry before deregistering and waiting for their exact attachable registration to leave. They close their shutdown channel last. Ordinary-session nested shutdown remains unchanged. ### Host-to-container tunnel lifecycle Host-to-container listener completion is now part of service lifecycle for ordinary and detachable sessions. Listener failure is serialized against service publication. A failed listener either prevents publication or stops and removes the published tunnel. Cleanup cancels blocked sends and dials, closes sibling listeners and late connections, preserves the first cause, releases the tunnel's upstream binding exactly once, and releases tracked resources. Another binding can keep the shared upstream running. ### Existing boundaries Ordinary sessions are unchanged outside the failed-tunnel correction. Without `--detach`, the last main-client disconnect still tears the session down. Nested requests from module code and other nested clients cannot use engine-global `/v1/sessions` control routes. Creator and observer attachment clients cannot use `/shutdown`. Engine shutdown terminates all in-memory detachable sessions through the same terminal cleanup path. Engine startup deletes stale saved-result directories. Sessions do not survive engine restart. Session, query, and attachment IDs use the `sess_`, `qry_`, and `att_` prefixes followed by exactly 26 lowercase base32 characters. The engine validates IDs before registry lookup or filesystem path construction. ## Validation Final reviewed head: `90136f800a9fd6cd1ceaf0a9996f0cbcb0830251`. All engine integration runs below used `engine-dev`, which built and ran the engine from the source worktree. - `go test -count=1 -race ./engine/server ./engine/client ./engine ./engine/clientdb ./internal/cmd/dagger` passed. - The concurrent detachable integration configuration passed 16 tests. Trace: `885dd9b8618590296276698fcefbab49`. - The source-built server lifecycle and protocol suite passed 61 tests with the race detector. Trace: `7e368ab8f4cf3f155d4334a7c984b5e1`. - The source-built client retry, telemetry, and close suite passed 31 tests with the race detector. Trace: `8d3a072cf29372b9dd222a371805cf11`. - The source-built CLI, detach, provisional-command, and existing hidden-command suite passed 32 tests. Trace: `c3a3c44afe1df4a5558024bda41add54`. Source callback and tunnel validation: - `go test -count=1 ./engine ./engine/server ./core ./engine/engineutil` passed. - `go test -count=1 -race ./engine ./engine/server ./core ./engine/engineutil` passed. - Deterministic ordinary and detachable tests cover first-listener failure, later-port failure, completion before publication, completion after publication, exact binding release, listener and monitor exit, service-map cleanup, and tracked-resource release. - The final source-built host-to-container and shared-service selection passed 8 tests. Trace: `8f675a24055c46df4c41ac7a1e5b8ba2`. - The final source-built creator-bound callback and copied-value selection passed 6 tests. Trace: `561c0819611a380dd28cd3759224b14f`. ## Why this PR is still a draft ### Cache ownership for indefinitely live sessions is not solved Cache resources associated with a live detachable session remain held by the existing session ownership model for that session's lifetime. Explicit termination and engine shutdown release session-owned resources. The integration test measured cache growth across three completed but still-live sessions and a return near the warmed baseline after termination. P0 does not define which cache results a session that remains alive indefinitely should keep. That requires a separate ownership and cleanup design before broad use. ### Callback rebinding remains deferred This PR now handles absent sources explicitly and cleans up failed source-backed tunnels. It does not make creator callbacks resumable or replace them with callbacks from an observer. After a published source leaves, new work that requires that source fails instead of being rebound. Creator-backed capabilities that remain unavailable after source loss include: - new host file or directory snapshots and host filesync exports; - URI- or provider-backed secret reads that were not already copied into an engine-owned secret; - new host socket and SSH-agent connections; - default Docker credential callbacks and new Git credential-helper lookups; - host image import, export, or load transfers that have not completed; - lazy local module source, workspace, defaults, or outer environment reads that still reach the creator host; - cache import or export paths that require host transfer callbacks; - terminal, pipe, prompt, stdin, and other interactive callback streams; - new container-to-host reverse-tunnel connections after their source disappears; and - host-to-container tunnel resumption after its listener stream ends. Values copied into engine-owned storage before disconnect continue to work. Failed host-to-container tunnels now clean themselves up, but are not resumed on another client. ### Other explicit P0 limits - There is no public SDK detach API. The new connection fields are internal engine-client fields. - Sessions do not survive engine restart. - `dagger up`, `dagger check`, shell, listen, and interactive input are not supported. - P0 supports one primary query and one current attachment. There is no force attach or concurrent attachment. - There is no result aggregation beyond the one saved primary GraphQL response. - There is no general workflow history, user-provided session name, TTL, or Cloud scale-out. - Network loss before query acknowledgement may leave an empty discoverable session that requires explicit termination. The CLI terminates it automatically when it can prove no query was acknowledged. - Telemetry replay is bounded at query completion. Rows written afterward are not included. - There is no broad long-term result or log database. - The CLI name is provisional. This implementation uses `dagger sessions` because `internal/cmd/dagger/session.go` already defines the hidden `dagger session` command used by SDKs to communicate with the engine. The final public command name remains undecided.",
        "url": "https://github.com/dagger/dagger/pull/13851",
        "createdAt": "2026-08-06T17:40:58Z",
        "updatedAt": "2026-08-12T20:47:04Z",
        "timestamp": "2026-08-12T20:47:04Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "sipsma",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13854",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "workspace: add native polyfill replacements",
        "text": "`dagger/polyfill` currently fills a few gaps that now belong in the engine. This is the additive half of removing it. Tooling modules such as Pytest and Ruff need to find ordinary project roots on disk. `Workspace.findRoots` expresses that policy directly: find roots below the current directory, plus the nearest root above it. `glob` remains available when callers only need file matching. SDKs have a different source of truth. Their managed modules are already registered in `dagger.toml`, so `currentModule.asSDK(workspace: ws).modules` returns the registered modules relevant to the workspace cwd without scanning config files. This also adds `ModuleSource.generate(workspace)`, preserves a literal `engineVersion: \"latest\"` during config edits, and fixes `Workspace.moduleSource` when a module has local dependencies. #### Review The API boundary is the important part: `findRoots` discovers projects from marker files; `asSDK.modules` reads installed SDK registrations; `ModuleSource.generate` composes generated files on a workspace. #### Test ```console go test ./core/schema dagger call engine-dev test --run 'TestWorkspaceFindRoots|TestModuleSourceGenerateWorkspace' ``` The changeset boundary and cwd path behavior are in [#13855](https://github.com/dagger/dagger/pull/13855).",
        "url": "https://github.com/dagger/dagger/pull/13854",
        "createdAt": "2026-08-07T07:45:38Z",
        "updatedAt": "2026-08-13T07:32:10Z",
        "timestamp": "2026-08-13T07:32:10Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "grouville",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13855",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "workspace: make changes cwd-relative and isolate SDK generation",
        "text": "This completes the engine side of removing `dagger/polyfill`. `Workspace.changes()` now has one path contract: after the beta.10 cutover, paths are relative to the workspace cwd. This is true for every workspace, forked or not. Code that intentionally edits the repository root can make that explicit with `workspace.withWorkdir(\".\")`. `Workspace.fork()` only creates a change boundary. The returned workspace sees existing staged files, but `changes()` reports only later edits. The engine forks immediately before invoking an SDK generator, so SDK authors receive one normal workspace and do not need to manage the baseline themselves. `ModuleSource.generate(workspace)` returns the workspace with generated files applied. It no longer stores hidden workspace/CWD provenance or has a second rerooting path: callers get CWD-relative output when they eventually call `Workspace.changes()`. Older modules keep the existing root-relative behavior so current polyfill users do not translate paths twice. #### Review The model is deliberately small: ```text workspace cwd decides where changeset paths start workspace fork decides when change measurement starts module generate returns another workspace ``` #### Test ```console go test ./core/schema dagger call golang lint-all dagger call engine-dev test --run 'TestWorkspace/TestWorkspaceChangesetRooting|TestModuleConfig/TestModuleSourceGenerateWorkspace' dagger call engine-dev test --run 'TestGenerators/TestGeneratorsInstalledInWorkspace' ``` The focused path/generation run passed [here](https://dagger.cloud/dagger/traces/803ce391e7c80c61a2ea8f202e98a096), the four-SDK installed-generator matrix passed [here](https://dagger.cloud/dagger/traces/2b0752905249f4c33389171921cbf42d), and the generator baseline/local-dependency cases passed [here](https://dagger.cloud/dagger/traces/81e10f1148a9a0e34fbdb3ced6142aed). This branch is stacked on [#13854](https://github.com/dagger/dagger/pull/13854) in Git history; GitHub cannot represent that base relationship while both heads live on a fork.",
        "url": "https://github.com/dagger/dagger/pull/13855",
        "createdAt": "2026-08-07T08:22:49Z",
        "updatedAt": "2026-08-13T08:24:56Z",
        "timestamp": "2026-08-13T08:24:56Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "grouville",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13865",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "[backport-0.21] fix(engine): decouple active-clients API from session lifecycle locks",
        "text": "## Summary Backport #13542 to the v0.21 maintenance line for v0.21.9. The Cloud keeper polls `engine { clients }` as part of its composite heartbeat. On v0.21, that enumeration can block behind one session lifecycle lock held across initialization or teardown. Because the keeper does not publish until the query returns, one slow session can suppress all keeper heartbeats, make a live engine unschedulable, and eventually cause suspension. This backport decouples active-client observers from session lifecycle work while preserving the complete synchronization redesign from the source fix. ## Source - Source PR: https://github.com/dagger/dagger/pull/13542 - Source commit: https://github.com/dagger/dagger/commit/0e21927e46c97a7ccb80670d6353d603e95c2885 - Applied with `cherry-pick -x` onto `origin/backport-0.21` at `9f68624bf69c23c01861e298651656ecac780285` - The v0.21 concurrency prerequisite from #13474 is already present as `cd9c80b9da99c0a90126b4a999400543f110c322` ## v0.21 adaptation The main-only `wcprofEnabled` field from #13393 does not exist on v0.21 and is not a dependency, so the conflict-only field and assignment were omitted. This is not a reduced `Clients` unlock. It retains the full semantic unit: - immutable session identity before registry publication - atomic lifecycle state - lifecycle-only mutex - lock-free `Clients`, `activeClientIDs`, and `clientFromIDs` observers - client publication only after successful initialization - removed tombstones and pointer-conditional registry deletion - client DB opening outside `clientMu` - graceful-stop session snapshot - all source liveness/race regressions adapted to v0.21 ## Validation Passed: - `gofmt -w engine/server/server.go engine/server/session.go engine/server/session_test.go` - `go test ./engine/server -run \"^(TestClientsDoesNotBlockWhileSessionLifecycleLocked|TestActiveClientIDsDoesNotBlockWhileSessionLifecycleLocked|TestGetOrInitClientReturnsFastForRemovedTombstone|TestClientFromIDsStateGating|TestSessionLifecycleObserverConcurrency)$\" -count=1` - `go test ./engine/server` - `go test -race ./engine/server` - `git diff --check origin/backport-0.21...HEAD` ## Release note Adds `.changes/unreleased/Fixed-20260810-150651.yaml` for v0.21.9, attributed to source PR #13542. Cloud-side per-operation deadlines remain separate follow-up work and are not changed by this backport.",
        "url": "https://github.com/dagger/dagger/pull/13865",
        "createdAt": "2026-08-10T15:10:20Z",
        "updatedAt": "2026-08-13T12:53:36Z",
        "timestamp": "2026-08-13T12:53:36Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "matipan",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13870",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "fix(check): report the checks a rollup can't run instead of dropping them silently",
        "text": "A `@check` that takes a required argument is silently dropped from `dagger check`. `dagger functions` lists it, `dagger check -l` doesn't, and nothing is said — a check that never ran looks exactly like a check that passed. Reproduced on `main` with a two-check module (`checks-required-arg`): ``` $ dagger functions needs-arg A @check with a required argument, which no rollup can supply. runnable A check a rollup can run. suite Owns checks, but is only reachable by supplying an argument. $ dagger check -l checks-required-arg:runnable # A check a rollup can run. ← and nothing else, no warning ``` After: ``` $ dagger check -l ⚠ skipping check \"checks-required-arg:needs-arg\": requires argument \"name\" ⚠ skipping check \"checks-required-arg:suite:nested\": reachable only through \"checks-required-arg:suite\", which requires argument \"name\" checks-required-arg:runnable # A check a rollup can run. ``` **Cause.** `ModTreeNode.Children()` skipped any function with a required argument with a bare `continue`, so it never became a tree node and could never appear in a rollup — and neither could anything the object it returns owns. That one function feeds all four rollups: checks, generators, services and agents. **Scope.** `@up`, `@generate` and `@agent` reject required arguments at module load, so only `@check` disappears *directly*. All four vanish when an intermediate object-returning function requires an argument (`suite(name: String!): Suite!` hides every leaf below it), which is why the fix and its tests cover all four. **Fix.** The skip is correct — nothing in a bulk run can supply the argument — so what changes is that it is reported. The tree walk hands back the functions it could not enter; each is expanded into the leaves it hides (including behind further required-argument hops), filtered by the same include/exclude/`--skip`/config-ignore rules as the runnable results, and carried on the group as a new `skipped` field. The CLI prints it for `check`, `generate`, `up` and `agent`. Skipped leaves still do **not** appear in the runnable list. **API.** `CheckGroup.skipped`, `GeneratorGroup.skipped`, `UpGroup.skipped`, `AgentGroup.skipped` — `[String!]!`, gated after `v1.0.0-0`, mirroring the neighbouring `GeneratorGroup.loadFailures`. The CLI reads them through raw GraphQL queries, so the generated SDK clients are untouched, exactly like the `loadFailures` change (8d2f31938) did; they'll pick the field up in the next `regen` pass. **Flagging in case CI disagrees** — if a codegen check fails on this, that's the reason and it just needs a regen commit. **Testing.** `dagger api call engine-dev test --pkg ./core/integration --run='^TestChecks$|^TestGenerators$|^TestUp$|^TestAgents$'` → **218 passed, 0 failed** (216 before this change; the 2 new subtests are the include/`--skip` narrowing cases). The new assertions fail on the pre-fix tree with the exact \"no warning\" output above, so they discriminate. `go build ./core/... ./internal/cmd/dagger/...`, `go vet`, `gofmt` clean.",
        "url": "https://github.com/dagger/dagger/pull/13870",
        "timestamp": "2026-08-12T12:41:22Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "eunomie",
        "assignees": [],
        "change": "new"
      },
      {
        "id": "github:dagger/dagger:pull_request:13872",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "fix(elixir): update vulnerable runtime dependencies",
        "text": "## Summary - update `go.opentelemetry.io/otel/sdk` to v1.43.0 - update `google.golang.org/grpc` to v1.82.1 with its required transitive dependencies - refresh the Elixir Go runtime module sums These updates fix the current open Dependabot alerts for `sdk/elixir/runtime/go.mod`.",
        "url": "https://github.com/dagger/dagger/pull/13872",
        "createdAt": "2026-08-11T13:42:25Z",
        "updatedAt": "2026-08-12T22:15:45Z",
        "timestamp": "2026-08-12T22:15:45Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "matipan",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13882",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "feat(workspace): import another workspace's dagger.toml",
        "text": "## What Adds a top-level `import` key to `dagger.toml`: a Git ref to another workspace whose configuration is merged **underneath** the current one. ```toml import = \"github.com/acme/dagger-base@v1.2.0\" # Only the difference from the base: [modules.go.settings] goVersion = \"1.24\" ``` A team can publish one base workspace config — shared tool versions, module refs, settings, environments — and have many downstream repos point at it instead of copying it into each one. Asked for on Discord (vindico: \"I'm currently putting duplicated workspace config in a lot of repos\"; chrisp6229 asked for exactly this, per project type), and scoped to shykes' \"as long as we don't go crazy with the config layering... a basic import feature in dagger.toml\". Design doc and implementation plan: `future/workspace-config-import.md`. ## Merge semantics The importing workspace wins every conflict. Layering, lowest first: imported `dagger.toml` → this repo's `dagger.toml` → user-level overrides → `--env` overlay. Nothing about the existing three layers changes. - **Module entries merge per field**, so a repo can override one setting without repeating the module's ref. That makes `source` optional in the file — an entry may be a pure override of a module the import installs. - **\"Set\" means explicitly present**, not non-zero: `entrypoint = false` and `ignore = []` override an inherited value. Presence comes from the raw TOML, not the parsed struct. - `as-sdk` and `legacy-default-path` are **never inherited** — both describe the imported workspace's own tree. - `[ports.<host>]` replaces the inherited mapping as a unit. - Envs merge by name; an env only the base defines is selectable downstream. The full per-section table is in the design doc. ## The two deliberate limits 1. **One import, no chain.** An `import` in the imported config is an error, not a second layer. Ignoring it would hand the user a config silently missing whatever the base author expected to inherit. 2. **Only remote modules.** Module entries in the imported config whose source is a local path are dropped, with a warning naming them, before the merge — along with their env overlays and any port mapping that forwards to them. Dropping rather than erroring, because a normal repo keeps its own modules under `modules/`, and erroring would make ordinary repos unusable as import targets. Classification of \"local\" is `gitref.FastKindCheck(source, \"\")` plus a directory stat **in the imported tree**, deliberately not `core.ParseRefString`: - an entry's own pin must not be passed, because any non-empty pin makes `FastKindCheck` return `KindGit` while the module loader classifies without it — `source = \"./ci\", pin = \"…\"` would otherwise pass as remote and then resolve locally; - `ParseRefString` falls back to *local* on `gitref.EndpointError`, so a vanity-domain remote would be dropped whenever endpoint discovery is unavailable. Classification must not depend on network reachability. Without the drop, an inherited `source = \"modules/ci\"` resolves against the **importing** workspace's config dir — loading a different module, or failing with a path the user never wrote. The integration test asserts exactly that case, with a same-named directory present downstream. ## Lockfile No new lock entry kind. The import resolves through the ordinary `git(url).head` / `.ref(name)` selectors, so it records the standard `git.ref` / `git.head` entry in `dagger.lock`: `dagger update` and `--lock=live` refresh it, `--lock=frozen` replays the pin and fails when it is missing. Resolution runs after the client's workspace is bound, which is what puts the entry in the right lockfile. ## Reads are effective, writes are local `readWorkspaceConfig` — behind module/env/SDK listings and `currentModule.asSDK` — returns the merged config, as does `configRead`, **including its base path**, which is the plain `dagger workspace config` case and returns raw bytes today. `--load-module <name>` resolves imported modules too. The no-argument output is a standalone snapshot: the `import` key is stripped and named in a `# imported from <ref>` comment, the same way the env-effective view already clears `Env`. Keeping it would make the printed config one that inlines the inherited values *and* imports them again underneath. `dagger workspace config import` still reports the local value. Writes never touch the imported config, and now say so when that would be confusing: unsetting a value the import supplies names the import instead of reporting that nothing is set. `dagger uninstall` on a module that exists only in the import says it cannot be uninstalled here; on a source-less override it removes the override and reports that the module is still provided by the import. Installing over a source-less override fills in its source (and clears the pin that belonged to the inherited ref) rather than reporting a conflict. ## Judgment calls worth a second opinion - **Scalar `import` rather than `[import] source = \"…\"`.** One field, the pin lives in `dagger.lock`, and \"basic import\" was the brief — but it is a one-way door if a second field is ever wanted. - **`ModuleEntry.Source` gains `omitempty`**, so the generated JSON schema no longer marks it required. `ValidateEffectiveConfig` replaces that check on the *effective* config and runs whether or not an import is present — stronger than the schema rule it replaces, but it means a config with a source-less module entry, which loads (uselessly) on main today, now fails. Port mappings are deliberately *not* validated: `dagger workspace config ports.3000.backendService web` writes one key while the serializer writes both, so a partial mapping is a state the CLI itself produces. - **Residual sanitization hole, documented not closed**: an imported source that is `KindUnknown`, absent from the imported tree, and present as a directory in the importing tree is kept and resolves locally. It needs a base config whose own entry is already unloadable plus a colliding downstream directory; closing it means threading a \"resolve remotely only\" hint through `moduleSource`, which is a resolution-pipeline change rather than a config feature. - **No tombstones.** An inherited entry can be overridden but not deleted. - **Lock refresh is not covered by a new integration test**: moving the served base branch under the same URL is not expressible with the git-service fixture, and the entry is the ordinary `git.ref` entry whose refresh `lockfile_test.go` already covers. ## Patches | Patch | Scope | | --- | --- | | `workspace-import-config` | `core/workspace`: the key, presence detection, pure merge, `ValidateEffectiveConfig` | | `workspace-import-resolve` | `core`: shared remote-workspace resolution, imported-config loading and sanitization | | `workspace-import-load` | `engine/server`: merge during workspace load | | `workspace-import-reads` | `core/schema`: effective reads, patch-entry-aware install/uninstall | | `workspace-import-tests` | `core/integration`: multi-workspace fixture over a git service | | `workspace-import-docs` | reference page, regenerated schema, changelog, CLI help | Design doc and plan are the first patches in the series.",
        "url": "https://github.com/dagger/dagger/pull/13882",
        "createdAt": "2026-08-12T15:31:30Z",
        "updatedAt": "2026-08-12T16:09:32Z",
        "timestamp": "2026-08-12T16:09:32Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "eunomie",
        "state": "open",
        "assignees": [],
        "change": "updated"
      },
      {
        "id": "github:dagger/dagger:pull_request:13884",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "[backport-0.21] fix(engine): take session teardown off the client shutdown path",
        "text": "## Summary Backports https://github.com/dagger/dagger/pull/13681 to the v0.21 maintenance line for v0.21.9. When the main client's final connection closes, engine-internal session teardown is now scheduled on a background reaper. This keeps the `/shutdown` response from waiting on unbounded cache and resource reclamation, while the client-critical workspace/service/telemetry drain remains synchronous. This draft also carries the exact proven v0.21 private-GitLab CI-fixture backport from https://github.com/dagger/dagger/pull/13849. It replaces revoked GitLab user PAT usage with project deploy tokens and their required usernames in six integration-test files; it does not change product behavior. The shutdown change addresses shutdown-path teardown scheduling. It does not claim to fix the newly reported startup/egraph contention, and it does not redesign cache release or lifecycle locking. ## Source PR and commits Shutdown-path backport: - Source PR: https://github.com/dagger/dagger/pull/13681 - Exact source commit: https://github.com/dagger/dagger/commit/b3953091561d53b2eb57b1493c116c89551abeb5 - Traceable `cherry-pick -x` backport commit: `a5eb16e5efb93794ba31eb08b84a81f56590874d` - v0.21 test-fixture adaptation: `a75c147aa640348ac3a03b2739e8f60a60ddf56b` - v0.21.9 release-note commit: `ce47b2b336e49e37302d05e6abe8195c8ac6ff52` GitLab CI-fixture backport: - Source PR: https://github.com/dagger/dagger/pull/13849 - Source PR commit: https://github.com/dagger/dagger/commit/0c656b9ca6395fdec1f051d34987bb4f8864196a - Main merge commit: https://github.com/dagger/dagger/commit/66337ba5d3d01ae10a0d7fb97ebe4dec9326e8bc - Proven v0.21 canary backport: `becaa3a33542ccc7e96dbf3f430d887f41958e52` on https://github.com/dagger/dagger/pull/13885 - Exact clean cherry-pick on this branch: `da8c39fb37947ffb4ee304e799b5e09e4c3e3b16` ## Dependency / stacked diff This draft intentionally and temporarily includes and depends on https://github.com/dagger/dagger/pull/13865. PR #13681 relies on the lifecycle-state and removed-tombstone behavior backported there. The branch starts from the exact current #13865 PR head, `35afbde0013e6c40937fedfa67434c801427762f`, but this PR still targets `dagger/dagger:backport-0.21`. It must be rebased or otherwise updated after #13865 merges so that the prerequisite commits disappear from this PR's diff. The stacked diff is expected while #13865 remains open. Since the fixture was propagated here, #13885 merged into `backport-0.21` as `269c47d7708b4623c3fbfdcd2440d19a055dc7e0`, so the target branch now contains the same six-file fixture patch. A clean local three-way merge against that base collapses the identical fixture content: the effective merge delta contains only the #13865 prerequisite and #13681 backport files. The exact fixture cherry-pick remains in this branch's history for provenance. ## v0.21 adaptations Shutdown-path backport: - The production `engine/server/session.go` change cherry-picked cleanly; its patch is identical to the source commit's production patch. - The source regression-test helper relied on a `dagql` import and `wcprofSpanCount` fixture already present on main. v0.21 needs an explicit `dagql` import and has no `wcprofSpanCount` field, so the fixture omits that main-only field. The four regression scenarios and assertions are unchanged. - Added the v0.21.9 unreleased change entry at `.changes/unreleased/Fixed-20260812-212429.yaml`, attributed to source PR #13681. GitLab CI fixture: - Cherry-picked the proven v0.21 commit without conflict or adaptation; the resulting commit has the same stable patch ID as `becaa3a33542ccc7e96dbf3f430d887f41958e52`. - The delta is exactly six existing integration-test files: `cross_session_test.go`, `git_test.go`, `gitcredential_test.go`, `module_config_test.go`, `module_test.go`, and `proxy_test.go`. - No engine/product code, diagnostics, local-worktree workaround, or release note was added by the fixture backport. No teardown, cache-release, or locking behavior was opportunistically redesigned. ## Validation Shutdown-path validation passed locally: - `dagger --progress=plain call engine-dev test --pkg=./engine/server --run='^(TestMainClientLastDisconnectDoesNotBlockOnTeardown|TestSameIDConnectDuringBackgroundTeardownGetsRetryable|TestReapAbandonedWhenMainClientReconnects|TestConcurrentReapsSingleTeardown)$' --race --count=1 --test-verbose` — 4/4 passed (trace `886bbe0b885eadd71e68918a20194ccd`) - `dagger --progress=plain call engine-dev test --pkg=./engine/server --count=1` — 56/56 passed (trace `ac8e829ffe4a031b53a25e5176e4c983`) - `dagger --progress=plain call engine-dev test --pkg=./engine/server --race --count=1` — 56/56 passed (trace `63c38bec3799ed278a72b79ff5305185`) - `dagger --progress=plain check go:lint changelog:generate` — passed (trace `52c8f0caf2bfebc849034933083594a8`) GitLab fixture validation and provenance: - Canary PR #13885 passed all 75 applicable checks on `becaa3a33542ccc7e96dbf3f430d887f41958e52`, including all five formerly failing shards and `TestPrivateGitRepoArgCaching` in real CI. - Fresh CI on this PR's current head passed all 75 applicable checks: 74 Dagger Cloud contexts plus DCO. The five formerly failing shards passed with traces: `test-base` (`2460fc75505f4043c73494f2f9504ba4`), `test-call-and-shell` (`6a40c909805c2ca65b8733650c2daa63`), `test-cli-engine` (`09c7f1aaa19a385f71b973f574042f59`), `test-container` (`4fb6d68d4ea869e4065304ecb7e36cb2`), and `test-modules` (`2eea881d02a2065a40344008d4612f4f`). - Stable patch IDs for `becaa3a33542ccc7e96dbf3f430d887f41958e52` and `da8c39fb37947ffb4ee304e799b5e09e4c3e3b16` match: `c5ece8f8d50919d36bdb6abad1e312d65ae8a2bf`. - The post-cherry-pick delta is exactly six files with 35 insertions and 30 deletions. - `gofmt -d` produced no diff for all six fixture files. - `git diff --check` passed for both the six-file cherry-pick and the complete stacked PR diff. ## Release note The unreleased v0.21.9 `Fixed` entry says that session teardown now runs in the background after the main client disconnects, preventing shutdown responses from timing out while the engine releases session-owned cache results. It attributes the change to source PR #13681. The GitLab fixture update is test-only and does not add a release note. ## Remaining risks / work - Update this branch after #13865 merges to remove the prerequisite from the stacked diff. - Manual playground validation was not run locally.",
        "url": "https://github.com/dagger/dagger/pull/13884",
        "createdAt": "2026-08-12T21:43:59Z",
        "updatedAt": "2026-08-13T02:58:35Z",
        "timestamp": "2026-08-13T02:58:35Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "sipsma",
        "state": "closed",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13885",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "[backport-0.21] fix: hide registry HTTP probe errors",
        "text": "## Summary Backport the scoped telemetry fix that hides normal registry-protocol HTTP probe failures from successful pull and publish progress. Registry HEAD 404s and auth challenges still occur as expected, but they no longer render as scary failed child rows when the enclosing registry operation succeeds. Also backport the shared private-GitLab test fixture update from #13849. GitLab revoked the committed user PATs used by the v0.21 integration tests; the fixture now uses read-only project deploy tokens and forwards their required usernames through each existing authentication path. ## Source provenance Registry progress fix: - Source PR: https://github.com/dagger/dagger/pull/13410 - Exact semantic source commit: https://github.com/dagger/dagger/commit/b9cdb6656036d6efd176fcda8e615332c81cb554 - Merged squash commit for source PR: https://github.com/dagger/dagger/commit/0bb84dde94beb8174d17222f33b782adeb6f8ee9 - Backported with `cherry-pick -x`, preserving Alex Suraci as author. GitLab CI fixture fix: - Source PR: https://github.com/dagger/dagger/pull/13849 - Source PR commit: https://github.com/dagger/dagger/commit/0c656b9ca6395fdec1f051d34987bb4f8864196a - Merged main commit: https://github.com/dagger/dagger/commit/66337ba5d3d01ae10a0d7fb97ebe4dec9326e8bc - Backported as `becaa3a33542ccc7e96dbf3f430d887f41958e52` with `cherry-pick -x`, preserving Guillaume de Rouville as author. ## Root cause and user impact containerd instruments registry HTTP requests through OpenTelemetry. Normal protocol behavior includes failed probes such as HEAD 404 responses for blobs or manifests that are not present yet, plus authentication challenges. The progress frontend forced every failed span visible, even when that span was encapsulated and its parent registry operation completed successfully. As a result, successful `Container.publish` calls could show prominent `remotes.docker.resolver.HTTPRequest` and `HTTP HEAD ERROR` rows, especially in plain progress. The fix encapsulates registry resolve, pull, and push internals and only reveals failed encapsulated children when the enclosing operation itself fails. Genuine registry failures therefore retain their diagnostic HTTP spans. Separately, GitLab automatically revoked the user PATs committed for the disposable private-repository integration fixture. That made five otherwise unrelated CI shards fail wherever they exercised the shared HTTPS fixture. Project deploy tokens require a username/token pair, so changing only the token is insufficient; every fixture consumer must propagate the matching username. ## v0.21 adaptations Registry progress fix: - Cherry-picked the complete scoped semantic fix and its golden regression coverage. - Kept the existing v0.21 golden-fixture hostname; the source commit included unrelated hostname churn in the same fixture. - Excluded the broader transfer-progress row architecture from the remainder of #13410. - No BuildKit, containerd, OpenTelemetry, or other dependency changes. GitLab fixture fix: - Limited to six existing v0.21 integration-test files; no engine or product code changed. - Mapped main's extracted `module_helpers_test.go` change to the equivalent helper in v0.21's `module_test.go`. - Preserved v0.21's existing CLI assertion form in `gitcredential_test.go`. - Omitted main-only workspace/private-dependency coverage and its extracted fixture because those tests and layouts do not exist on v0.21. ## Validation Registry progress fix passed: - `dagger call engine-dev test --pkg='./dagql/idtui' --run='^TestTelemetry/TestGolden/docker-build-fail$'` — 3 tests - `dagger --progress=plain call engine-dev test --pkg='./core/integration' --run='^TestContainer/TestPublish$'` — 2 tests, using the local test registry; expected HEAD 404 probes occurred without HTTP HEAD ERROR progress rows - `dagger call engine-dev test --pkg='./dagql/dagui'` — 22 tests - `dagger call engine-dev test --pkg='./engine/server/resolver'` — 1 test - `dagger --progress=plain -m ./toolchains/go call --source=. lint-module --module=dagql` - `dagger --progress=plain -m ./toolchains/go call --source=. lint-module --module=engine` - `git diff --check upstream/backport-0.21...HEAD` GitLab fixture adaptation passed locally: - `TestGit/TestAuthProviders/GitLab_auth` — direct username/token API path - `TestGitCredential/TestGitCredentialErrors/gitlab_private_module` — host credential-helper/module path - `TestModule/TestCrossSessionSecrets/contenthash_cache_hit_with_secrets` — nested secret path - `TestConfig/TestDaggerGitRefs/Private_GitLab` — six module-reference assertions - `git diff --check` Fresh Dagger Cloud checks on canary head `becaa3a33542ccc7e96dbf3f430d887f41958e52` passed all five shards that previously exposed the revoked GitLab credentials: - `test-split:test-base` - `test-split:test-cli-engine` - `test-split:test-call-and-shell` - `test-split:test-container` - `test-split:test-modules` That run also passed `TestPrivateGitRepoArgCaching`, confirming the earlier linked-worktree `.git` failure was local-state-only. The first fresh `test-split:test-base` run independently hit `TestChangeset/TestWithChangesets/comparison_with_sequential_merge` with `fatal: stash failed`, the exact unrelated snapshot-hardlink/ctime baseline defect documented by main PR #13773. Its isolated supported Dagger Cloud rerun passed in 13m34s (trace `91b9bc8f3a602d14f0e6e8b5627e93c6`). The final PR ledger is 75 successful checks and one intentionally skipped changelog gate. The repository-root lint entrypoint was also attempted for the registry fix. It failed on an unrelated generated import in `docs/versioned_docs/version-0.21.4/getting-started/types/snippets/default-address/go/main.go`; the scoped lints for both changed source trees pass. ## Release note Added a v0.21.9 unreleased `Fixed` entry attributed to source PR #13410: > Registry protocol probes no longer appear as scary errors in progress output when image pulls or publishes succeed, while genuine registry failures remain visible. The GitLab fixture update is test-only and does not add a release note. ## Remaining risks The registry change is a presentation-only policy change scoped to Dagger registry resolve, pull, and push spans. Failed enclosing registry operations still reveal their failed children, and higher verbosity can still expose encapsulated details. The broad progress and transfer-row changes from the rest of #13410 are intentionally not included. The GitLab change depends on the deliberately public, read-only deploy-token fixture remaining valid. Fresh CI on this canary commit verified all five affected shards; propagation to other backport PRs remains a separate lead decision.",
        "url": "https://github.com/dagger/dagger/pull/13885",
        "createdAt": "2026-08-12T21:55:02Z",
        "updatedAt": "2026-08-13T01:54:46Z",
        "timestamp": "2026-08-13T01:54:46Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "sipsma",
        "state": "closed",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13886",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "[backport-0.21] engine: prune DAGQL cache by metadata size",
        "text": "## Summary Backports the complete metadata-aware DagQL cache-pruning behavior from #13846 to v0.21: - tracks a coarse structural metadata estimate (`3072*results + 512*terms + 768*class slots`) - prunes cold persisted roots toward a metadata target independently of disk usage - protects active-session and engine-lifetime/unpruneable closures - schedules structural checks for startup, session completion, graceful shutdown, explicit GC, and the pressure monitor - handles zero-disk metadata pressure, estimator correction, forced sparse-class compaction, and monitor no-progress blocking - adds `gc.dagqlCache.maxEstimatedBytes` / `targetEstimatedBytes`, the `dagger_dagql_cache_metadata_estimated_bytes` metric, bounded aggregate observability, tests, scale benchmarks, docs, and a v0.21.9 change entry This bounds retained metadata growth and may mitigate scale-related symptoms. It does **not** claim to directly fix the reported `egraphMu` / startup contention. The separate bounded manual structural-prune API discussed with Erik is intentionally not included because it was not part of #13846. The source defaults remain unchanged at 4 GiB maximum / 3 GiB target. ## Source PR and commits - Source: #13846 - Exact source series: `615a02dbdaf6ef107f8a1dfa3c45d39624403645^..b19467f2b032bb4c000c5c80bbe21ac062ac03f5` (20 commits; parent `ba839a98077aef4398415ccc805f57fd694c2e4b`) - Metadata-pruning backport series: `f021d54d4c0824061690fa4669fc39d39183d4d1^..0371e77ae8210df2f7583e2fa0ac39b08019f32f`, followed by v0.21 adaptation commit `5479f384e8bff0e2d87d981838528adbe755ef2c` - CI fixture source: #13849; source PR commit `0c656b9ca6395fdec1f051d34987bb4f8864196a`; merged main commit `66337ba5d3d01ae10a0d7fb97ebe4dec9326e8bc` - Proven v0.21 fixture backport: `becaa3a33542ccc7e96dbf3f430d887f41958e52`, validated on #13885 and initially cherry-picked unchanged as `f951484dde70cd431a5e19f84027aa4eaa946367`; after two byte-identical CI head refreshes, the current equivalent tip is `89af13099acc7a46885023b096b25372e32931f4` (tree `631aa606fb061a4464f838559c2569c25bb9eb9b`) All 20 metadata-pruning source commits, including the design-review history, are preserved in order with `cherry-pick -x`; none were squashed or omitted. The additional v0.21 adaptation and fixture-propagation commits are also DCO-signed. The fixture commit preserves Guillaume de Rouville as author and the source traceability footer. ## Prerequisite #13375's GC pressure-monitor machinery is already present on `backport-0.21` as `78fc50e58a4f417825e414463829c1bc7c2053d2`. Its stable patch ID matches source commit `838b7c73ab69fb5bc97cfa1139478ae7d305e4ed`, so this backport relies on the existing v0.21 implementation rather than duplicating it. ## v0.21 adaptations - Preserved v0.21's existing `stateMu` graceful-stop/session lifecycle and namespace-worker initialization while applying the new scheduler/config state. Main-only `core.SetWorkspaceInvalidator`, recursive-read-only-mount context, and a session-test field rename are absent from v0.21 and were not pulled in as unrelated changes. - Kept the cache-pruning reference in its v0.21 location under `skills/cache-expert/references/`. - Adapted restart documentation and the large persistence benchmark from main's schema 17 to v0.21's unchanged schema 16; no persistence schema bump or worker reset was introduced. - Adapted the active zero-disk integration workload from main's `dagger api with-session` spelling to the equivalent v0.21 `dagger run` command. - Added the v0.21.9 unreleased change entry with source attribution `Author: sipsma`, `PR: 13846`. - Added exactly the proven six-file v0.21 GitLab fixture backport from #13885 with no further adaptation. It replaces revoked user PAT usage with read-only project deploy tokens and propagates each required username through the existing integration-test authentication paths; no product code or diagnostics changed. ## Validation run - `./dagql`: 295 tests passed — pruning semantics, trigger/target correction, active/unpruneable protection, no-detail structural mode, compaction, persistence/restart, and estimator accounting. [trace](https://dagger.cloud/dagger/traces/9da3818267833cf8aac933012f30d738) - `./engine/server`: 61 tests passed — defaults/config resolution, no-disk-stat behavior, lifecycle scheduling, pressure monitoring, blocked/no-progress recovery, explicit/session reset behavior. [trace](https://dagger.cloud/dagger/traces/64f555b64551ad8de249de8a057dd38f) - `./engine/config`: compiled successfully (no package tests). [trace](https://dagger.cloud/dagger/traces/de211c18eafdb972b8b8c7b13896815f) - `./cmd/engine`: 7 tests passed. [trace](https://dagger.cloud/dagger/traces/ee7c94816dadfca9f19531a2b045f5f8) - `TestEngine/TestPrometheusMetrics`: 2 tests passed and observed the new metadata gauge. [trace](https://dagger.cloud/dagger/traces/bb2199d5bbf958c11fcee33a7a7fc405) - `TestLocalCache/TestDagqlMetadataGCProtectsActiveZeroDiskResults`: 2 tests passed; the workload remained active through a pressure-monitor interval, produced metadata growth with only 4 KiB disk use, stayed protected while active, then closed and was structurally pruned. [trace](https://dagger.cloud/dagger/traces/7c526fecdbccf5130bf8063c1751bf3d) - All seven one-shot metadata benchmarks passed at their defined 200k/1M scales: allocation-free estimate, metadata-vs-disk snapshots, full prune, four fixture calibrations, correction/churn, sparse forced compaction, and schema-16 import. The 1M import retained schema 16 and peaked at 4,526,727,168 B additional `HeapInuse`; 1M metadata pruning removed all roots in ~14.7 s with ~893.8 MB peak scratch. [trace](https://dagger.cloud/dagger/traces/ca33b9e49899c57cbc83c0237eae9f63) - Root Go `check-tidy` passed. [trace](https://dagger.cloud/dagger/traces/33084456fd29ccfac78180ea42ca88be) - Historical CI on `5479f384e8bff0e2d87d981838528adbe755ef2c` passed DCO and 69 Dagger Cloud contexts; five split suites failed only where GitLab rejected the revoked private-fixture PAT. [base trace](https://dagger.cloud/dagger/traces/6b0504c101392a40e25e6747054c5f18), [call/shell trace](https://dagger.cloud/dagger/traces/5528cad9cd43f8e492f0657fbdf4e7c3), [CLI/engine trace](https://dagger.cloud/dagger/traces/b28a191f2e4baa119ac77dc78f98ce47), [container trace](https://dagger.cloud/dagger/traces/8bb6345aecb7bc216c5c0ad0e4df3aba), [modules trace](https://dagger.cloud/dagger/traces/70c4e0129c4d07d6b8b7f274749186f4). - Canary #13885 proved `becaa3a33542ccc7e96dbf3f430d887f41958e52`: all 75 applicable checks passed, including all five formerly failing shards and `TestPrivateGitRepoArgCaching` in real CI. - The propagated patch is byte-for-byte equivalent by stable patch ID (`c5ece8f8d50919d36bdb6abad1e312d65ae8a2bf`), changed only the same six integration-test files (35 additions, 30 deletions), and passed `git diff --check`. Final head `89af13099acc7a46885023b096b25372e32931f4` passed all 75 applicable statuses (74 Dagger Cloud contexts plus DCO). Initial failures were trace-classified rather than dismissed: Linux/arm64 Wolfi release exited 139 ([failed trace](https://dagger.cloud/dagger/traces/7db17ce3d0d04a45afc83930d7d3bf58), [final pass](https://dagger.cloud/dagger/traces/7c87ea93d8d3b6b9b97ed1a021ce0109)); telemetry saw a runtime-only `13.6% dropped` golden suffix ([failed trace](https://dagger.cloud/dagger/traces/df5d54edea84862b868cfc1570d1aca6), [final pass](https://dagger.cloud/dagger/traces/2d67886db69c386edbd7f32ab7312e38)); and one 441/442 module-runtime run failed during pinned Python 3.13.1 initialization ([failed trace](https://dagger.cloud/dagger/traces/6177b76b28748ff8dbb475970033672b), [final pass](https://dagger.cloud/dagger/traces/1e7802c2531cc335239fdc913dcf5822)). `dagger cloud rerun` could not resolve the cross-fork PR or synthetic merge SHA from this runner, so the two retries used committer-timestamp-only tip refreshes, byte-identical tree checks, and explicit `--force-with-lease`. - During final stewardship, `backport-0.21` advanced from `9f68624bf69c23c01861e298651656ecac780285` to `269c47d7708b4623c3fbfdcd2440d19a055dc7e0` via #13885, which independently absorbed the same six-file fixture patch (stable patch ID `c5ece8f8d50919d36bdb6abad1e312d65ae8a2bf`) alongside its registry-observability fix. Final green synthetic merge `86bdee550d36867afd02cfdd8d44b08ccebaae79` has exact parents base `269c47d7708b4623c3fbfdcd2440d19a055dc7e0` and head `89af13099acc7a46885023b096b25372e32931f4`; the retained traceable head commit merges without conflict or behavioral duplication. Upstream squash commit `49559d42c9c9dc2588dc53f834e15ea572f69ae7` has the identical tree `3821ccc63bdbed5ee821980aa6893b9eea7044be`. - GitHub's `check-for-changelog` job was intentionally skipped because the PR has no `needs/changelog` label; the release entry is present and Dagger Cloud's `changelog:generate` check passed. - Changed Go files are `gofmt`-clean; `git diff --check` passed; all 22 commits have DCO signoff; #13375 prerequisite patch IDs and all 20 `-x` source footers were audited. ## Release note Adds `.changes/unreleased/Fixed-20260812-213129.yaml` for v0.21.9, attributed to source PR #13846. The GitLab fixture propagation is test-only and adds no separate release entry. ## Risk and remaining work This was published and fully validated as a draft because it is a large engine/cache backport. GitHub records `sipsma` as squash-merging it at `2026-08-13T02:58:09Z`, after the final green run. The revoked GitLab fixture credential was addressed by propagating the exact six-file v0.21 patch proven on #13885, and the final combined-head matrix has converged green. The complete integration suite beyond the published split coverage remains unrun. The root full-lint command was attempted but is currently blocked by unrelated generated docs snippets (`default-address/go`) that cannot import their generated internal package. Targeted `go vet` was also attempted; it reports pre-existing context-cancel findings in `dagql/cache.go` (blamed to `aa719a10d52`, before this series) and `engine/server/session_attachables.go` (`74c2889afc6`, untouched here). Those unrelated findings were not changed to keep this backport scoped. The estimator is deliberately coarse and structural rather than RSS-based. A pre-existing oversized schema-16 graph is fully imported before the one-second startup prune, so a temporary upgrade/startup memory peak remains possible. This patch bounds retained metadata after pruning; it does not establish a direct fix for lock contention.",
        "url": "https://github.com/dagger/dagger/pull/13886",
        "createdAt": "2026-08-12T22:44:54Z",
        "updatedAt": "2026-08-13T02:59:40Z",
        "timestamp": "2026-08-13T02:59:40Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "sipsma",
        "state": "closed",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13887",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "engine: support manual metadata pruning",
        "text": "## Summary - add absolute `maxEstimatedBytes` and `targetEstimatedBytes` controls to `Engine.localCache.prune`, including generated CLI and SDK surfaces - run configured structural-memory pruning inline with manual `useDefaultPolicy` requests when automatic GC is enabled, and allow explicit structural pruning when automatic GC is disabled - preserve legacy no-option and disk-only behavior while ensuring metadata-only requests do not run the implicit all-eligible disk-policy stage - add bounded cancellation checks throughout structural snapshotting and planning, including high-fanout dependency and candidate ordering - document the destructive disk fallback, structural disk-reclamation effects, partial-pair validation, and GC-disabled behavior accurately ## Motivation DagQL structural-memory limits previously applied only through automatic or lifecycle GC. A manual local-cache prune applied disk policy immediately but left structural pruning to a later, unrelated background event. Operators also had no explicit structural-memory action when automatic GC was disabled. This change makes a manual request cohesive: combined requests run disk pruning first and structural pruning second under the existing GC serialization lock. Explicit structural controls remain operator actions independent of automatic scheduling. ## Compatibility and user impact - no-option manual prune retains the existing all-releasable disk behavior - disk-only options continue to run only disk pruning - metadata-only options skip the disk-policy stage, though released roots and snapshot GC may still reclaim disk - `useDefaultPolicy` applies configured disk and structural policies only when automatic GC is enabled; with GC disabled it does not resurrect structural policy and retains the existing disk fallback - omitted structural values inherit resolved configured/default limits; explicit values must be positive and the target must be lower than the resolved maximum - structural options are absolute byte integers only - new API arguments and corrected argument documentation are version-gated so the historical base schema remains unchanged A release note, user documentation, GraphQL schema, SDK bindings, and reference artifacts are included. ## Validation - `go test -count=1 ./dagql ./engine/server ./core/schema` - `go test -race -count=1 ./dagql ./engine/server` - `dagger api call engine-dev test --pkg ./core/integration --run='^TestLocalCache/TestLocalCacheManualMetadataPruneCLI$'` - deterministic cancellation, mode-selection, validation-before-mutation, and manual-prune tests repeated up to 100 times - `dagger check golang:lint-all markdown-lint:lint docs:check python-client:lint typescript-client:lint-typescript java-client:lint helm:lint` - `dagger check --generate`: all 24 checks passed on this exact candidate during review; the post-rebase no-op recheck passed 23 checks and the Elixir check aborted in `mix local.hex` with the known VM error `OS monotonic time stepped backwards`",
        "url": "https://github.com/dagger/dagger/pull/13887",
        "createdAt": "2026-08-13T01:43:44Z",
        "updatedAt": "2026-08-13T04:49:58Z",
        "timestamp": "2026-08-13T04:49:58Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "sipsma",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13888",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "dagql: reduce e-graph result-removal contention",
        "text": "## Motivation Removing a result from a wide output equivalence class repeatedly scanned the class's digests and result postings while holding `egraphMu`. Release and metadata-prune work therefore grew superlinearly as the class widened, increasing lock contention for unrelated cache operations. ## Design This change adds three cache-owned derived indexes: - an inverse output-equivalence-class-to-results index; - an exact per-result digest-posting index; and - an exact/broad posting classification for result cleanup. Newly published results record their exact digest postings. Imported results remain conservatively broad because the persistence schema does not contain exact result/term memberships. Broad imported cleanup therefore retains the safe class-posting scan; this change does not claim to remove that scan. The inverse output-class index replaces the separate deterministic class-wide survivor scan with an expiry-aware lookup over associated results. ## Correctness and compatibility Forward and inverse output-class associations are maintained as a pair across publication, merge, compaction, import, expiry, reset, and rollback. Merge and compaction canonicalize roots and rebuild the paired indexes together. Posting insertion and removal use a centralized exact/broad classification, and the survivor predicate preserves expiry behavior. Ownership counts, release cascades and callback order, lookup candidate membership, release/prune visibility, tracing and mutation order, prune cadence, and the `egraphMu` lock domain remain unchanged. Reset clears all three indexes with the e-graph. There is no persistence schema, version, migration, API, or user-visible semantic change. Import reconstructs result/output-equivalence-class associations and marks imported postings broad. Exact result/term associations remain unavailable after import, so the existing conservative class-scan fallback is preserved. No changie entry is included because this is internal derived bookkeeping with no user-facing API or semantic change. ## History and scope The PR contains ten independently reviewable commits across 19 files: the previously reviewed benchmark harness commits, the approved optimization plan, the final two implementation commits, a generator-produced Markdown-whitespace correction, and a behavior-preserving benchmark-harness lint correction. The approved implementation branch begins from the reviewed forced-prune fixture, so the empirical ancestry is intentionally preserved. The ninth commit only replaces ten hard tabs in fenced plan examples with markdownlint-compatible spaces. The tenth removes an unused benchmark-helper return outside the timed region, makes a redirection-only shell no-op explicit, and narrowly documents intentional singleton loop axes without changing their matrices. The implementation delta itself remains nine files; the larger full-PR file count comes from the benchmark harness and plan history rather than additional production scope. ## Validation Commit 8 (`4febbe327d`), which contains the complete production implementation, passed: - `go test ./dagql -count=1` — 1.901s - `go test -race ./dagql -count=1` — 5.427s The same production source tree also passed the post-fetch pre-DCO runs in 1.860s and 4.820s respectively. Commit 9 additionally passed `dagger check markdown-lint:fix`, `dagger check markdown-lint:lint`, and an idempotent `dagger generate --no-apply markdown-lint:fix`. The final head passes `dagger check shellcheck:check`, `dagger check golang:lint-all`, and `go test ./dagql -run '^TestCacheEGraphBenchmark' -count=1`. Both implementation commits were independently validated, the focused functional and race alternations passed, and the final council review converged without an unresolved correctness, semantic, schema, or scope finding. Mutation checks were used during review to prove focused test sensitivity; they are not presented as CI gates. `gofmt`, `git diff --check`, per-commit DCO, and intended-scope checks are clean. ## Paired benchmark evidence The exact baseline was direct implementation parent `0fbfabb33e`, which has the same finalized DagQL harness/fixture as `61dc7a4e` plus the plan only. The measured candidate was `ce068a698` before the signoff-only rewrite. Commit 8, `4febbe327d`, retains that candidate's source tree, `d96baee478b44170e215be176b63b577378385f5`. At the final head, the production implementation remains byte-identical to commit 8. The compiled benchmark harness differs only by removal of a dead, unused return outside the timed region. The runner source differs only by making an existing redirection-only no-op explicit and adding lint comments beside unchanged singleton loops; its diagnostics and behavior are equivalent. The evidence therefore binds behaviorally to the final head; it does not rely on final-tree identity or claim separately built binaries are byte-identical, since embedded VCS revision metadata can differ. - Transient wide release: R256 improved from 46.318 ms to 2.356 ms and R512 from 181.319 ms to 5.379 ms. The timing slope changed from 3.91x to 2.28x, while allocation scaling per doubling changed from 3.94x to 1.99x. - Persisted-fresh prune: R256 improved from 20.256 ms to 5.540 ms and R512 from 156.145 ms to 12.378 ms. The 12.6x R512 ratio and 7.709x baseline timing slope are host-specific, not expected production factors. The stronger structural result is allocation scaling per doubling changing from 3.86x to 1.98x. - Imported release R256 improved from 86.020 ms to 19.765 ms while retaining all 263,683 broad postings. Broad posting cleanup remains; the gain comes from removing the separate class-wide survivor scan. Allocations changed from 1,769,611 to 1,031. - The independent 50,000-result control produced candidate 317.357 ms, baseline 277.558 ms, and the one bounded candidate repeat 270.377 ms. No control regression was demonstrated at this measurement precision; none was excluded. The candidate's own spread was 17.4%, and repeat host load was lower. Identical 100,003 allocs/op, 10,001,440 B/op, within-2% RSS, fixture/posting/lock counters, and opposite setup timing support treating the first candidate point as noise rather than claiming a speedup. Every process exited with status 0, ran one exact sample, reported zero dropped observations, and preserved the fixture, prune, and lock-counter populations. Only the allowed noisy control arm was repeated once. The same-host paired comparison was explicitly authorized because the original Linux VM was unstable. Host-specific timing factors are not presented as production guarantees; the near-linear allocation scaling is the stronger evidence that the quadratic removal term is gone. Raw outputs and SHA-256 manifests are retained internally. ## Rollback and non-goals The two implementation commits are conceptual rollback boundaries: first the output-class inverse index, then exact posting tracking and cleanup. This slice does not add lock sharding, tombstones, asynchronous cleanup, persistence-schema changes, exact imported association persistence, ownership-semantic changes, imported read-path redesign, or broader e-graph optimization.",
        "url": "https://github.com/dagger/dagger/pull/13888",
        "createdAt": "2026-08-13T08:17:53Z",
        "updatedAt": "2026-08-13T09:19:11Z",
        "timestamp": "2026-08-13T09:19:11Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "sipsma",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13890",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "fix: make `dagger module init` work from a subdirectory workspace",
        "text": "# fix: make `dagger module init` work from a subdirectory workspace Fixes #13889. ## The bug With a `dagger.toml` in a subdirectory of the git repo — the monorepo layout, several projects under one root — `dagger module init` fails outright: ```console $ mkdir repo && cd repo && git init && mkdir common && touch common/dagger.toml $ cd common && dagger sdk install go $ dagger module init go hello -y Error: sdk module init: failed to call sdk module initModule: workspace path .dagger/modules/hello is outside changeset root common ``` And the workaround that avoids the error is worse: `--path common/hello` writes the engine's `dagger-module.toml` to `common/hello/` while the SDK's `main.go` lands in `hello/` — a split, non-loadable module, with no error at all. ## Root cause Two path conventions disagree once the caller is not standing at the workspace root, which a subdirectory `dagger.toml` guarantees: workspace root detection walks up to `.git` and a `dagger.toml` does not define the root, so selecting `common/dagger.toml` always means root = git root and `Workspace.cwd = common`. 1. **The engine resolves init paths workspace-root-relative and applies the changeset at the workspace root.** `Workspace.export` writes to `ExportHostPath()`, which is the workspace root. Self-consistent. 2. **An SDK's `initModule` stages files relative to `Workspace.cwd`.** The polyfill's workspace fork (`github.com/dagger/polyfill`, `workspace-fork.dang` → `clientPath`) rebases a root-relative path onto the cwd and raises when the path is not under it, because a changeset returned from an ordinary function call *is* applied at the caller's cwd (`changeset.Export(ctx, \".\")` resolves against the client's OS cwd). The polyfill is right for the second apply base and wrong for the first, and the engine is the layer that both chooses the workspace it shows the SDK and calls `Workspace.export` at the root. So the reconciliation belongs here. This is a founding assumption rather than a regression: the cwd rebasing arrived with the polyfill's root commit (`dagger/sdk-sdk` `732835f`, \"Add SDK development polyfill module\", 2026-05-18, later spun out to `github.com/dagger/polyfill`); the later \"read cwd from Workspace.cwd\" only changed where the cwd came from, not the semantics. Monorepo layouts are simply the first thing to exercise a non-root cwd through init. ## The fix (engine only — no SDK or polyfill change) **Hand the SDK a root-anchored workspace during init.** New `rootAnchoredWorkspace` in `core/schema/workspace_sdk_init.go` returns the workspace with `cwd = \".\"` (built through the `withWorkdir` field so the result stays an attached dagql result, the same way `scopedStagedWorkspace` does it for generators). `initModuleChanges` and `initClientChanges` pass that to the SDK instead of the caller's workspace, so the cwd the SDK sees matches where the changeset actually lands, and the polyfill's rebasing becomes the no-op it already is at the root. Nothing about the generic `changeset.Export(\".\")` path — `dagger generate` included — changes. **Anchor the default module path at the `dagger.toml` being edited.** `.dagger/modules/<name>` was joined against the workspace root. Since `[modules.<name>].source` is resolved against the config directory, a root-anchored default under `common/dagger.toml` would have recorded an entry pointing outside its own project. The default is now `<config dir>/.dagger/modules/<name>` and the recorded source is made config-dir-relative with `filepath.Rel`, the same conversion `dagger install` already performs for a local ref. At the workspace root — every existing workspace — both are unchanged. `--path` keeps its documented workspace-root-relative meaning; only the split it used to produce is fixed. The CLI help and the Go SDK page now say so explicitly — `--path ci` from a subdirectory means `<workspace root>/ci`, not `./ci` — and the generated CLI reference is updated to match. ## Verification New engine integration test `TestGenerators/TestInitFromSubdirectoryWorkspace`, against a fixture SDK whose `initModule`/`initClient` mirror the polyfill's cwd rebasing (so the test reproduces the real failure, not a synthetic one): - `dagger module init` from `common/` puts `dagger-module.toml`, the SDK scaffold and the generator output together under `common/.dagger/modules/newmod/`, records `source = \".dagger/modules/newmod\"` and `path = \"common/.dagger/modules/newmod\"`, and writes nothing at the git root. - `--path common/hello` from `common/` puts both files under `common/hello/` and nothing at `hello/`. - `--path tools/hello` from `common/` — a path *outside* the caller's cwd — puts both files under `tools/`, which is what separates root-anchoring from a cwd-scoped reroot. - `dagger api client init` from `common/` scaffolds at the requested path. Confirmed the test fails on the unfixed engine with the reported `outside changeset root` error, and passes with the fix. The existing root-workspace init tests (`TestModuleInitGeneratesForNewModuleOnly`, `TestAPIClientInitGeneratesForNewClientOnly`) still pass unchanged. ## Noted, not fixed Two adjacent inconsistencies found while tracing this, both out of scope here: - `dagger api client init` cannot take a workspace-root-relative module ref that contains a `.` segment (e.g. `common/.dagger/init-fixture`): `workspace.IsLocalRef` classifies it as remote. Unrelated to cwd; the test fixture sidesteps it by living at a dot-free path. - `workspace.AddMigratedModuleSDK` (used by `dagger setup`) writes `as-sdk.modules[].path` relative to the owning config directory, while every consumer — `sdkOwnersByModulePath`, `removeSDKManagedModuleReference`, and the Go SDK's `modules(ws)` — reads it workspace-root-relative. Identical only when the config is at the root.",
        "url": "https://github.com/dagger/dagger/pull/13890",
        "createdAt": "2026-08-13T14:55:18Z",
        "updatedAt": "2026-08-13T16:14:53Z",
        "timestamp": "2026-08-13T16:14:53Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "eunomie",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13891",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "ci: unpin helm from the retired wolfi helm 3.x package",
        "text": "> [!NOTE] > This unblocks CI repo-wide. `main` and every open PR are currently red. Chainguard moved the wolfi `helm` apk package to the 4.x line, so every `helm~3.18.4` constraint fails to resolve: ``` ERROR: unable to select packages: helm-4-4.2.3-r1: breaks: world[helm~3.18.4] ``` That breaks `ci:bootstrap`, `golang:test-all` (via `e2e/helm`), `helm:lint` and `helm:assert-template`. No repo code is at fault — reproduced with a bare `apk add`. Available wolfi packages are now `helm-4.0.1-r0` (bare `helm`), `helm-3-3.19.2-r2`, `helm-4-4.2.3-r1`. ## Fix - The paths that **package/ship** our chart move to the versioned package so they stay on Helm 3: `helm~3.18.4` → `helm-3~3.19.2` in `.dagger/modules/release/helm.go` and `e2e/helm/helm_test.go`. - `helm:lint` / `helm:assert-template` don't come from those pins — they come from the external `github.com/dagger/helm` module, which hardcodes `apk add \"helm~\" + version`, so 3.x is unselectable there without an upstream change. Its `[modules.helm.settings] version` is set to `4.0.1` (verified it lints/templates our chart fine). Follow-up (not this PR): teach `github.com/dagger/helm` to select the `helm-3` package so the checker can go back to 3.x. ## Verified locally (dev engine) - `helm lint` and `helm assert-template` → PASS (were ERROR). - `e2e/helm`: `TestCustomProbes`, `TestPackageDryRun`, `TestInstallK3S` (all 4 subtests: default/port daemonset, default/port statefulset) → PASS. - `helm-3~3.19.2` install → `helm version` v3.19.2; `helm package .` → `dagger-helm-1.0.0-beta.10.tgz` OK. `docs/versioned_docs/version-0.21.7/modules/helm.mdx` still references `3.18.4` and is left untouched — versioned docs are frozen snapshots.",
        "url": "https://github.com/dagger/dagger/pull/13891",
        "createdAt": "2026-08-13T15:35:09Z",
        "updatedAt": "2026-08-13T16:11:27Z",
        "timestamp": "2026-08-13T16:11:27Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "eunomie",
        "state": "closed",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13892",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "fix(setup): anchor migrated as-sdk module paths at the workspace root",
        "text": "## What `dagger.toml` carries two path conventions that only coincide when the config file sits at the workspace root: - `[modules.<name>].source` resolves against the **config directory** (`ResolveModuleEntrySource`). - `[[modules.<sdk>.as-sdk.modules]].path` is **workspace-root-relative**, everywhere it is read: module→SDK ownership (`sdkOwnersByModulePathFromConfig`), uninstall (`removeSDKManagedModuleReference`), the workspace SDK listing (`workspaceSDKFromEntry`), and `currentModule.asSDK.modules`, which the SDKs themselves consume (including the out-of-repo `dagger/go-sdk`). The migration writer disagreed. `workspaceMigrationInstallDiscoveredModuleSDKs` recorded a discovered local module with `filepath.Rel(owner, projectRoot)` — relative to the config that owns it, not to the workspace root. This patch anchors it at the workspace root via the existing `workspaceMigrationProjectRootRelPath` helper, and states the anchor on the config types (plus the regenerated public JSON schema). ## Is anything broken today? No — and the change is deliberately framed as hardening rather than a user-visible fix. Migration plans every workspace config at `ws.HostPath()`: `PlanMigration` sets `plan.ProjectRoot` to the workspace root when hoisted, and otherwise the project root *is* the workspace root; synthesized parent plans land at the workspace root too. So `owner` always equals the workspace root and the two anchors produce the same string. The mismatch only bites the day migration can plan a config below the workspace root — which the `FIXME(workspace-migrate)` about scanning for legacy child `dagger.json` files points straight at. Fixing the writer rather than the readers is the small, safe side of the mismatch: the readers' contract is depended on by SDKs living in other repos. ## Tests - `core/schema` unit test pinning the anchor for a config planned below the workspace root. It is the only test that fails against the old writer; its comment is explicit that the shape is not reachable through `dagger setup` today. - `core/integration` assertions on the existing subdirectory-toolchains migration: the discovered toolchain is recorded by a workspace-root-relative path, and `dagger uninstall` resolves that path back to its as-sdk entry — end-to-end coverage of the contract in the monorepo layout.",
        "url": "https://github.com/dagger/dagger/pull/13892",
        "createdAt": "2026-08-13T15:44:40Z",
        "updatedAt": "2026-08-13T16:22:51Z",
        "timestamp": "2026-08-13T16:22:51Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "eunomie",
        "state": "open",
        "assignees": []
      },
      {
        "id": "github:dagger/dagger:pull_request:13893",
        "source": "github",
        "group": "developer-infrastructure",
        "project": "dagger/dagger",
        "kind": "pull_request",
        "title": "fix: classify dotted workspace-relative module refs as local",
        "text": "# fix: classify dotted workspace-relative module refs as local ## Problem `dagger api client init <sdk> <client-path> <module-ref>` fails when the module ref is a workspace-root-relative path whose dot segment is not the first one: ```console $ dagger api client init go clients/api common/.dagger/mymod load module source: local path \"common/.dagger/mymod\" does not exist ``` This is the everyday shape in a monorepo: a module that is `.dagger/mymod` from the workspace root is `common/.dagger/mymod` from a subdirectory, and module refs are recorded relative to the workspace root. Prefixing `./` does not help — `resolveWorkspaceClientModuleRef` hands the *cleaned* form to the loader, so the `./` is gone before the ref is classified again. ## Cause `core/workspace.IsLocalRef` recognised a local ref by a leading `.` or `/`, and otherwise by the *absence* of a dot anywhere in the string — a proxy for \"this looks like a hostname\". `common/.dagger/mymod` has neither, so it was classified as a git ref and `resolveClientTargetModule` routed it to the git loader instead of resolving it against the workspace rootfs. The heuristic is shared well beyond client init (config parsing, install, uninstall, SDK init, migration), so it is fixed once, in place. ## Fix `IsLocalRef` moves to its own file and delegates to `core/gitref.FastKindCheck`, which already handles pins, leading `.`/`/`/`..`, and the `http`/`https`/`ssh` schemes, and returns \"unknown\" for the ambiguous dotted case. That case is now resolved by looking for a host only ahead of the first slash — the Go module path convention, where only the first element can be a domain. Two markers keep dot-free hosts remote: a colon (scp-like `git@internal:org/repo.git`, or a `host:port`) and a `/_git/` segment (Azure DevOps Server on-prem, per the matcher in `engine/vcs`). Nothing that was correctly classified local becomes remote: the refs that change are those with a dot-free, colon-free, `_git`-free host and a dot further down the path, which become local. One shape goes the other way, and is a fix too — a scheme-prefixed dot-free host (`ssh://internal/org/repo`) used to read as local because the string had no dot at all; `FastKindCheck` now calls it git, which is what it is. ## Converging the copies The heuristic had been written out four times, and the copies had drifted: | Implementation | Ambiguous-case rule | | | --- | --- | --- | | `core/gitref.FastKindCheck` | returns \"unknown\" by design | the primitive, kept | | `core/workspace.IsLocalRef` | whole-string dot | **fixed here** | | `core/schema.isLocalLegacyModuleRef` | whole-string dot (verbatim copy) | **deleted** | | `daggercmd.workspaceAddressLooksRemote` | whole-string dot | **same bug, fixed here** | | `daggercmd.isObviouslyRemoteWorkspaceRef` | first-segment dot | already correct | `isLocalLegacyModuleRef` drove `dagger update` for legacy `dagger.json` toolchains and blueprints. `workspaceAddressLooksRemote` drove `-W` workspace addresses, where it had the identical bug: a workspace at `services/api.v2` or `common/.dagger/mymod` read as a git ref. `isObviouslyRemoteWorkspaceRef` is worth calling out — it already looked for the dot in the first path segment only, which is exactly the rule `IsLocalRef` now adopts. The convention was already in the tree; this PR just makes it the only one. All four now route through `IsLocalRef`, each keeping only what is genuinely its own: `file://` stripping for a workspace address, and the host stat plus a \"needs a slash\" conservatism for an obviously-remote ref. Untouched on purpose: `auth/registry.go` classifies OCI registry domains under Docker's rules (`localhost`, `:port`), and `engine/vcs` validates hosts inside the VCS matcher. Different questions, deliberately separate. ### Known limitation (pre-existing, unchanged) A local module directory whose *own first path segment* carries the dot — `./mymod.v2` at the workspace root — is still classified as a git ref. The `./` is cleaned away before the ref is classified, and the cleaned form is what gets persisted as `as-sdk.clients.module`, so by the time a client is loaded again the evidence of local-ness is gone. Fixing it means persisting the disambiguating `./` rather than plumbing a kind hint — both `init` and every later load already share the same resolution seam. That is a config-format change, so it is left as a follow-up. Keep such a module under a dot-free first segment, or spell dot-free remote hosts with an explicit `ssh://`/`https://` scheme. Windows-style local paths are equally unchanged: `C:/src/mymod.git` still reads as remote, because the drive letter's colon is indistinguishable from scp-like syntax. It read as remote before this change too, for the dot. ## Tests - `core/workspace/localref_test.go` — table-driven matrix over the classifier: dotted-but-local paths, dot-free-but-remote-looking refs, leading `./`/`../`, absolute paths, `github.com/...` with and without `@version`, scp-style refs (dotted and dot-free hosts), `host:port`, both Azure DevOps shapes, and pins. The dotted-below-first-segment cases fail against the old implementation. - `TestGenerators/TestAPIClientInitDottedModulePath` — `dagger api client init` run from a workspace subdirectory against a module at `common/.dagger/target`, for both the bare and `./`-prefixed ref, asserting the scaffold, the generated client and the recorded config ref. Without the fix both subtests fail with the exact error above. - `internal/cmd/dagger` — `TestWorkspaceAddressLooksRemote` gains the dotted workspace-address cases, and `TestIsObviouslyRemoteWorkspaceRef` is new, pinning the \"needs a slash\" conservatism that keeps a lone `my.dir` local.",
        "url": "https://github.com/dagger/dagger/pull/13893",
        "createdAt": "2026-08-13T16:22:02Z",
        "updatedAt": "2026-08-13T16:48:12Z",
        "timestamp": "2026-08-13T16:48:12Z",
        "metrics": {
          "reactions": 0,
          "comments": 0
        },
        "labels": [],
        "author": "eunomie",
        "state": "open",
        "assignees": []
      }
    ],
    "events": [
      {
        "id": "event:55ae1d76cc13c4442e8f",
        "signalId": "github:dagger/dagger:pull_request:13865",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13865",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "[backport-0.21] fix(engine): decouple active-clients API from session lifecycle locks",
          "text": "## Summary Backport #13542 to the v0.21 maintenance line for v0.21.9. The Cloud keeper polls `engine { clients }` as part of its composite heartbeat. On v0.21, that enumeration can block behind one session lifecycle lock held across initialization or teardown. Because the keeper does not publish until the query returns, one slow session can suppress all keeper heartbeats, make a live engine unschedulable, and eventually cause suspension. This backport decouples active-client observers from session lifecycle work while preserving the complete synchronization redesign from the source fix. ## Source - Source PR: https://github.com/dagger/dagger/pull/13542 - Source commit: https://github.com/dagger/dagger/commit/0e21927e46c97a7ccb80670d6353d603e95c2885 - Applied with `cherry-pick -x` onto `origin/backport-0.21` at `9f68624bf69c23c01861e298651656ecac780285` - The v0.21 concurrency prerequisite from #13474 is already present as `cd9c80b9da99c0a90126b4a999400543f110c322` ## v0.21 adaptation The main-only `wcprofEnabled` field from #13393 does not exist on v0.21 and is not a dependency, so the conflict-only field and assignment were omitted. This is not a reduced `Clients` unlock. It retains the full semantic unit: - immutable session identity before registry publication - atomic lifecycle state - lifecycle-only mutex - lock-free `Clients`, `activeClientIDs`, and `clientFromIDs` observers - client publication only after successful initialization - removed tombstones and pointer-conditional registry deletion - client DB opening outside `clientMu` - graceful-stop session snapshot - all source liveness/race regressions adapted to v0.21 ## Validation Passed: - `gofmt -w engine/server/server.go engine/server/session.go engine/server/session_test.go` - `go test ./engine/server -run \"^(TestClientsDoesNotBlockWhileSessionLifecycleLocked|TestActiveClientIDsDoesNotBlockWhileSessionLifecycleLocked|TestGetOrInitClientReturnsFastForRemovedTombstone|TestClientFromIDsStateGating|TestSessionLifecycleObserverConcurrency)$\" -count=1` - `go test ./engine/server` - `go test -race ./engine/server` - `git diff --check origin/backport-0.21...HEAD` ## Release note Adds `.changes/unreleased/Fixed-20260810-150651.yaml` for v0.21.9, attributed to source PR #13542. Cloud-side per-operation deadlines remain separate follow-up work and are not changed by this backport.",
          "url": "https://github.com/dagger/dagger/pull/13865",
          "createdAt": "2026-08-10T15:10:20Z",
          "updatedAt": "2026-08-13T12:53:36Z",
          "timestamp": "2026-08-13T12:53:36Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "matipan",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:3e04daa346125c5c924c",
        "signalId": "github:dagger/dagger:pull_request:13888",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13888",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "dagql: reduce e-graph result-removal contention",
          "text": "## Motivation Removing a result from a wide output equivalence class repeatedly scanned the class's digests and result postings while holding `egraphMu`. Release and metadata-prune work therefore grew superlinearly as the class widened, increasing lock contention for unrelated cache operations. ## Design This change adds three cache-owned derived indexes: - an inverse output-equivalence-class-to-results index; - an exact per-result digest-posting index; and - an exact/broad posting classification for result cleanup. Newly published results record their exact digest postings. Imported results remain conservatively broad because the persistence schema does not contain exact result/term memberships. Broad imported cleanup therefore retains the safe class-posting scan; this change does not claim to remove that scan. The inverse output-class index replaces the separate deterministic class-wide survivor scan with an expiry-aware lookup over associated results. ## Correctness and compatibility Forward and inverse output-class associations are maintained as a pair across publication, merge, compaction, import, expiry, reset, and rollback. Merge and compaction canonicalize roots and rebuild the paired indexes together. Posting insertion and removal use a centralized exact/broad classification, and the survivor predicate preserves expiry behavior. Ownership counts, release cascades and callback order, lookup candidate membership, release/prune visibility, tracing and mutation order, prune cadence, and the `egraphMu` lock domain remain unchanged. Reset clears all three indexes with the e-graph. There is no persistence schema, version, migration, API, or user-visible semantic change. Import reconstructs result/output-equivalence-class associations and marks imported postings broad. Exact result/term associations remain unavailable after import, so the existing conservative class-scan fallback is preserved. No changie entry is included because this is internal derived bookkeeping with no user-facing API or semantic change. ## History and scope The PR contains ten independently reviewable commits across 19 files: the previously reviewed benchmark harness commits, the approved optimization plan, the final two implementation commits, a generator-produced Markdown-whitespace correction, and a behavior-preserving benchmark-harness lint correction. The approved implementation branch begins from the reviewed forced-prune fixture, so the empirical ancestry is intentionally preserved. The ninth commit only replaces ten hard tabs in fenced plan examples with markdownlint-compatible spaces. The tenth removes an unused benchmark-helper return outside the timed region, makes a redirection-only shell no-op explicit, and narrowly documents intentional singleton loop axes without changing their matrices. The implementation delta itself remains nine files; the larger full-PR file count comes from the benchmark harness and plan history rather than additional production scope. ## Validation Commit 8 (`4febbe327d`), which contains the complete production implementation, passed: - `go test ./dagql -count=1` — 1.901s - `go test -race ./dagql -count=1` — 5.427s The same production source tree also passed the post-fetch pre-DCO runs in 1.860s and 4.820s respectively. Commit 9 additionally passed `dagger check markdown-lint:fix`, `dagger check markdown-lint:lint`, and an idempotent `dagger generate --no-apply markdown-lint:fix`. The final head passes `dagger check shellcheck:check`, `dagger check golang:lint-all`, and `go test ./dagql -run '^TestCacheEGraphBenchmark' -count=1`. Both implementation commits were independently validated, the focused functional and race alternations passed, and the final council review converged without an unresolved correctness, semantic, schema, or scope finding. Mutation checks were used during review to prove focused test sensitivity; they are not presented as CI gates. `gofmt`, `git diff --check`, per-commit DCO, and intended-scope checks are clean. ## Paired benchmark evidence The exact baseline was direct implementation parent `0fbfabb33e`, which has the same finalized DagQL harness/fixture as `61dc7a4e` plus the plan only. The measured candidate was `ce068a698` before the signoff-only rewrite. Commit 8, `4febbe327d`, retains that candidate's source tree, `d96baee478b44170e215be176b63b577378385f5`. At the final head, the production implementation remains byte-identical to commit 8. The compiled benchmark harness differs only by removal of a dead, unused return outside the timed region. The runner source differs only by making an existing redirection-only no-op explicit and adding lint comments beside unchanged singleton loops; its diagnostics and behavior are equivalent. The evidence therefore binds behaviorally to the final head; it does not rely on final-tree identity or claim separately built binaries are byte-identical, since embedded VCS revision metadata can differ. - Transient wide release: R256 improved from 46.318 ms to 2.356 ms and R512 from 181.319 ms to 5.379 ms. The timing slope changed from 3.91x to 2.28x, while allocation scaling per doubling changed from 3.94x to 1.99x. - Persisted-fresh prune: R256 improved from 20.256 ms to 5.540 ms and R512 from 156.145 ms to 12.378 ms. The 12.6x R512 ratio and 7.709x baseline timing slope are host-specific, not expected production factors. The stronger structural result is allocation scaling per doubling changing from 3.86x to 1.98x. - Imported release R256 improved from 86.020 ms to 19.765 ms while retaining all 263,683 broad postings. Broad posting cleanup remains; the gain comes from removing the separate class-wide survivor scan. Allocations changed from 1,769,611 to 1,031. - The independent 50,000-result control produced candidate 317.357 ms, baseline 277.558 ms, and the one bounded candidate repeat 270.377 ms. No control regression was demonstrated at this measurement precision; none was excluded. The candidate's own spread was 17.4%, and repeat host load was lower. Identical 100,003 allocs/op, 10,001,440 B/op, within-2% RSS, fixture/posting/lock counters, and opposite setup timing support treating the first candidate point as noise rather than claiming a speedup. Every process exited with status 0, ran one exact sample, reported zero dropped observations, and preserved the fixture, prune, and lock-counter populations. Only the allowed noisy control arm was repeated once. The same-host paired comparison was explicitly authorized because the original Linux VM was unstable. Host-specific timing factors are not presented as production guarantees; the near-linear allocation scaling is the stronger evidence that the quadratic removal term is gone. Raw outputs and SHA-256 manifests are retained internally. ## Rollback and non-goals The two implementation commits are conceptual rollback boundaries: first the output-class inverse index, then exact posting tracking and cleanup. This slice does not add lock sharding, tombstones, asynchronous cleanup, persistence-schema changes, exact imported association persistence, ownership-semantic changes, imported read-path redesign, or broader e-graph optimization.",
          "url": "https://github.com/dagger/dagger/pull/13888",
          "createdAt": "2026-08-13T08:17:53Z",
          "updatedAt": "2026-08-13T09:19:11Z",
          "timestamp": "2026-08-13T09:19:11Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "sipsma",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:01653038091f7fe46c57",
        "signalId": "github:dagger/dagger:issue:13889",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:issue:13889",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "issue",
          "title": "🐞 dagger module init fails \"outside changeset root\" when dagger.toml is in a subdirectory (monorepo layout)",
          "text": "### What is the issue? `dagger module init <sdk> <name>` fails when the selected `dagger.toml` lives in a **subdirectory** of the git repo rather than at the git root: ``` Error: sdk module init: failed to call sdk module initModule: workspace path .dagger/modules/hello is outside changeset root common ``` This breaks the common monorepo layout where several project workspaces live in subdirectories under a single git root (e.g. `project-1/dagger.toml`, `common/dagger.toml`). Running `dagger module init` from any of those subdirectories fails. ### Reproduction ```bash mkdir repo && cd repo git init mkdir common touch common/dagger.toml cd common dagger sdk install go dagger module init go hello -y ``` The last command fails with the error above. (Reproduced deterministically on `v1.0.0-beta.9`.) ### Actual vs. expected - **Actual:** `withInitModule` errors with `... is outside changeset root common`; no module is created. - **Expected:** a `hello` module scaffolded under `common/` (e.g. `common/.dagger/modules/hello/`), the same way it works when the config is at the git root. ### Root cause A cwd-vs-root mismatch across the engine ↔ SDK boundary: - The workspace **root** is the git root — `core/workspace/detect.go` walks up to `.git`; a `dagger.toml` does **not** define the root. So with `common/dagger.toml`, root = git root and **cwd = `common`**. - The engine computes the default module path **workspace-root-relative**: `.dagger/modules/<name>`, independent of cwd (`core/schema/workspace_module_init.go`, `relPath = filepath.Join(\".dagger\", \"modules\", args.Name)`), and passes it to the SDK's `initModule(path:)`. - The Go SDK's `initModule` stages files through `polyfill.workspace(ws).fork.withDirectory(path, ...)`. The polyfill (`github.com/dagger/polyfill`, `workspace-fork.dang` → `clientPath`) is **cwd-scoped**: it converts a root-relative path into cwd-local coordinates and **raises when the path is not under cwd**: ``` let relative = workspacePath.trimPrefix(cwd + \"/\") if (relative == workspacePath) { raise \"workspace path \" + workspacePath + \" is outside changeset root \" + cwd } ``` `.dagger/modules/hello` does not start with `common/`, so it raises. When the config is at the git root, `cwd == \".\"` and `clientPath` passes paths through unchanged — which is why it only fails from a subdirectory. Trace (plain progress) showing the boundary: ``` Workspace.withInitModule(name: \"hello\", sdk: \"go\") ├─ Workspace.cwd → \"common\" ├─ GoSdk.initModule(path: \".dagger/modules/hello\") │ └─ PolyfillWorkspaceFork.withDirectory(path: \".dagger/modules/hello\") ERROR │ ! workspace path .dagger/modules/hello is outside changeset root common ``` ### Workarounds (all tested on v1.0.0-beta.9) | Approach | Result | | --- | --- | | `dagger.toml` at the git root, run from root | ✅ works | | Give the subdir its own `.git` (subdir becomes the workspace root) | ✅ works | | `--path common/hello` (path under cwd) | ⚠️ **no error but silently corrupts**: engine config lands at `common/hello/dagger-module.toml` while the SDK's `main.go` lands at `hello/main.go` — a split, non-loadable module | | `-W common` from the git root | ❌ same error (root stays at the git root) | | `--here` | ❌ not a valid flag for `module init` | The `--path`-under-cwd case is arguably the more serious half: it avoids the error but silently produces a broken module, because the engine's config changeset is applied workspace-root-relative while the SDK's source changeset has the cwd prefix stripped. ### Suggested fix Reconcile the two path conventions: the engine emits workspace-root-relative module paths, while the polyfill fork applies caller-cwd-relative ones. Either the engine derives the default path relative to a location the cwd-scoped fork can reach, or engine-driven `initModule` changesets are applied workspace-root-relative. The polyfill notes it is coding around \"today's clients apply returned changesets relative to the caller cwd\", so the reconciliation likely belongs on the engine side. ### Dagger version ``` dagger v1.0.0-beta.9 engine v1.0.0-beta.9+1c6e07b1 go-sdk: github.com/dagger/go-sdk polyfill: github.com/dagger/polyfill@ec3ea84a2351b4beb06ecece951f2e5ef66509ff ```",
          "url": "https://github.com/dagger/dagger/issues/13889",
          "createdAt": "2026-08-13T08:52:40Z",
          "updatedAt": "2026-08-13T08:52:40Z",
          "timestamp": "2026-08-13T08:52:40Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "eunomie",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:d2f8dd920cb4fd5ed583",
        "signalId": "github:dagger/dagger:pull_request:13855",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13855",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "workspace: make changes cwd-relative and isolate SDK generation",
          "text": "This completes the engine side of removing `dagger/polyfill`. `Workspace.changes()` now has one path contract: after the beta.10 cutover, paths are relative to the workspace cwd. This is true for every workspace, forked or not. Code that intentionally edits the repository root can make that explicit with `workspace.withWorkdir(\".\")`. `Workspace.fork()` only creates a change boundary. The returned workspace sees existing staged files, but `changes()` reports only later edits. The engine forks immediately before invoking an SDK generator, so SDK authors receive one normal workspace and do not need to manage the baseline themselves. `ModuleSource.generate(workspace)` returns the workspace with generated files applied. It no longer stores hidden workspace/CWD provenance or has a second rerooting path: callers get CWD-relative output when they eventually call `Workspace.changes()`. Older modules keep the existing root-relative behavior so current polyfill users do not translate paths twice. #### Review The model is deliberately small: ```text workspace cwd decides where changeset paths start workspace fork decides when change measurement starts module generate returns another workspace ``` #### Test ```console go test ./core/schema dagger call golang lint-all dagger call engine-dev test --run 'TestWorkspace/TestWorkspaceChangesetRooting|TestModuleConfig/TestModuleSourceGenerateWorkspace' dagger call engine-dev test --run 'TestGenerators/TestGeneratorsInstalledInWorkspace' ``` The focused path/generation run passed [here](https://dagger.cloud/dagger/traces/803ce391e7c80c61a2ea8f202e98a096), the four-SDK installed-generator matrix passed [here](https://dagger.cloud/dagger/traces/2b0752905249f4c33389171921cbf42d), and the generator baseline/local-dependency cases passed [here](https://dagger.cloud/dagger/traces/81e10f1148a9a0e34fbdb3ced6142aed). This branch is stacked on [#13854](https://github.com/dagger/dagger/pull/13854) in Git history; GitHub cannot represent that base relationship while both heads live on a fork.",
          "url": "https://github.com/dagger/dagger/pull/13855",
          "createdAt": "2026-08-07T08:22:49Z",
          "updatedAt": "2026-08-13T08:24:56Z",
          "timestamp": "2026-08-13T08:24:56Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "grouville",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:60f4700377445f8d0de3",
        "signalId": "github:dagger/dagger:pull_request:13854",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13854",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "workspace: add native polyfill replacements",
          "text": "`dagger/polyfill` currently fills a few gaps that now belong in the engine. This is the additive half of removing it. Tooling modules such as Pytest and Ruff need to find ordinary project roots on disk. `Workspace.findRoots` expresses that policy directly: find roots below the current directory, plus the nearest root above it. `glob` remains available when callers only need file matching. SDKs have a different source of truth. Their managed modules are already registered in `dagger.toml`, so `currentModule.asSDK(workspace: ws).modules` returns the registered modules relevant to the workspace cwd without scanning config files. This also adds `ModuleSource.generate(workspace)`, preserves a literal `engineVersion: \"latest\"` during config edits, and fixes `Workspace.moduleSource` when a module has local dependencies. #### Review The API boundary is the important part: `findRoots` discovers projects from marker files; `asSDK.modules` reads installed SDK registrations; `ModuleSource.generate` composes generated files on a workspace. #### Test ```console go test ./core/schema dagger call engine-dev test --run 'TestWorkspaceFindRoots|TestModuleSourceGenerateWorkspace' ``` The changeset boundary and cwd path behavior are in [#13855](https://github.com/dagger/dagger/pull/13855).",
          "url": "https://github.com/dagger/dagger/pull/13854",
          "createdAt": "2026-08-07T07:45:38Z",
          "updatedAt": "2026-08-13T07:32:10Z",
          "timestamp": "2026-08-13T07:32:10Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "grouville",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:cbc10660267d8f84e063",
        "signalId": "github:dagger/dagger:pull_request:13887",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13887",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "engine: support manual metadata pruning",
          "text": "## Summary - add absolute `maxEstimatedBytes` and `targetEstimatedBytes` controls to `Engine.localCache.prune`, including generated CLI and SDK surfaces - run configured structural-memory pruning inline with manual `useDefaultPolicy` requests when automatic GC is enabled, and allow explicit structural pruning when automatic GC is disabled - preserve legacy no-option and disk-only behavior while ensuring metadata-only requests do not run the implicit all-eligible disk-policy stage - add bounded cancellation checks throughout structural snapshotting and planning, including high-fanout dependency and candidate ordering - document the destructive disk fallback, structural disk-reclamation effects, partial-pair validation, and GC-disabled behavior accurately ## Motivation DagQL structural-memory limits previously applied only through automatic or lifecycle GC. A manual local-cache prune applied disk policy immediately but left structural pruning to a later, unrelated background event. Operators also had no explicit structural-memory action when automatic GC was disabled. This change makes a manual request cohesive: combined requests run disk pruning first and structural pruning second under the existing GC serialization lock. Explicit structural controls remain operator actions independent of automatic scheduling. ## Compatibility and user impact - no-option manual prune retains the existing all-releasable disk behavior - disk-only options continue to run only disk pruning - metadata-only options skip the disk-policy stage, though released roots and snapshot GC may still reclaim disk - `useDefaultPolicy` applies configured disk and structural policies only when automatic GC is enabled; with GC disabled it does not resurrect structural policy and retains the existing disk fallback - omitted structural values inherit resolved configured/default limits; explicit values must be positive and the target must be lower than the resolved maximum - structural options are absolute byte integers only - new API arguments and corrected argument documentation are version-gated so the historical base schema remains unchanged A release note, user documentation, GraphQL schema, SDK bindings, and reference artifacts are included. ## Validation - `go test -count=1 ./dagql ./engine/server ./core/schema` - `go test -race -count=1 ./dagql ./engine/server` - `dagger api call engine-dev test --pkg ./core/integration --run='^TestLocalCache/TestLocalCacheManualMetadataPruneCLI$'` - deterministic cancellation, mode-selection, validation-before-mutation, and manual-prune tests repeated up to 100 times - `dagger check golang:lint-all markdown-lint:lint docs:check python-client:lint typescript-client:lint-typescript java-client:lint helm:lint` - `dagger check --generate`: all 24 checks passed on this exact candidate during review; the post-rebase no-op recheck passed 23 checks and the Elixir check aborted in `mix local.hex` with the known VM error `OS monotonic time stepped backwards`",
          "url": "https://github.com/dagger/dagger/pull/13887",
          "createdAt": "2026-08-13T01:43:44Z",
          "updatedAt": "2026-08-13T04:49:58Z",
          "timestamp": "2026-08-13T04:49:58Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "sipsma",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:2ee39b0300eb30f58abb",
        "signalId": "github:dagger/dagger:pull_request:13884",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13884",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "[backport-0.21] fix(engine): take session teardown off the client shutdown path",
          "text": "## Summary Backports https://github.com/dagger/dagger/pull/13681 to the v0.21 maintenance line for v0.21.9. When the main client's final connection closes, engine-internal session teardown is now scheduled on a background reaper. This keeps the `/shutdown` response from waiting on unbounded cache and resource reclamation, while the client-critical workspace/service/telemetry drain remains synchronous. This draft also carries the exact proven v0.21 private-GitLab CI-fixture backport from https://github.com/dagger/dagger/pull/13849. It replaces revoked GitLab user PAT usage with project deploy tokens and their required usernames in six integration-test files; it does not change product behavior. The shutdown change addresses shutdown-path teardown scheduling. It does not claim to fix the newly reported startup/egraph contention, and it does not redesign cache release or lifecycle locking. ## Source PR and commits Shutdown-path backport: - Source PR: https://github.com/dagger/dagger/pull/13681 - Exact source commit: https://github.com/dagger/dagger/commit/b3953091561d53b2eb57b1493c116c89551abeb5 - Traceable `cherry-pick -x` backport commit: `a5eb16e5efb93794ba31eb08b84a81f56590874d` - v0.21 test-fixture adaptation: `a75c147aa640348ac3a03b2739e8f60a60ddf56b` - v0.21.9 release-note commit: `ce47b2b336e49e37302d05e6abe8195c8ac6ff52` GitLab CI-fixture backport: - Source PR: https://github.com/dagger/dagger/pull/13849 - Source PR commit: https://github.com/dagger/dagger/commit/0c656b9ca6395fdec1f051d34987bb4f8864196a - Main merge commit: https://github.com/dagger/dagger/commit/66337ba5d3d01ae10a0d7fb97ebe4dec9326e8bc - Proven v0.21 canary backport: `becaa3a33542ccc7e96dbf3f430d887f41958e52` on https://github.com/dagger/dagger/pull/13885 - Exact clean cherry-pick on this branch: `da8c39fb37947ffb4ee304e799b5e09e4c3e3b16` ## Dependency / stacked diff This draft intentionally and temporarily includes and depends on https://github.com/dagger/dagger/pull/13865. PR #13681 relies on the lifecycle-state and removed-tombstone behavior backported there. The branch starts from the exact current #13865 PR head, `35afbde0013e6c40937fedfa67434c801427762f`, but this PR still targets `dagger/dagger:backport-0.21`. It must be rebased or otherwise updated after #13865 merges so that the prerequisite commits disappear from this PR's diff. The stacked diff is expected while #13865 remains open. Since the fixture was propagated here, #13885 merged into `backport-0.21` as `269c47d7708b4623c3fbfdcd2440d19a055dc7e0`, so the target branch now contains the same six-file fixture patch. A clean local three-way merge against that base collapses the identical fixture content: the effective merge delta contains only the #13865 prerequisite and #13681 backport files. The exact fixture cherry-pick remains in this branch's history for provenance. ## v0.21 adaptations Shutdown-path backport: - The production `engine/server/session.go` change cherry-picked cleanly; its patch is identical to the source commit's production patch. - The source regression-test helper relied on a `dagql` import and `wcprofSpanCount` fixture already present on main. v0.21 needs an explicit `dagql` import and has no `wcprofSpanCount` field, so the fixture omits that main-only field. The four regression scenarios and assertions are unchanged. - Added the v0.21.9 unreleased change entry at `.changes/unreleased/Fixed-20260812-212429.yaml`, attributed to source PR #13681. GitLab CI fixture: - Cherry-picked the proven v0.21 commit without conflict or adaptation; the resulting commit has the same stable patch ID as `becaa3a33542ccc7e96dbf3f430d887f41958e52`. - The delta is exactly six existing integration-test files: `cross_session_test.go`, `git_test.go`, `gitcredential_test.go`, `module_config_test.go`, `module_test.go`, and `proxy_test.go`. - No engine/product code, diagnostics, local-worktree workaround, or release note was added by the fixture backport. No teardown, cache-release, or locking behavior was opportunistically redesigned. ## Validation Shutdown-path validation passed locally: - `dagger --progress=plain call engine-dev test --pkg=./engine/server --run='^(TestMainClientLastDisconnectDoesNotBlockOnTeardown|TestSameIDConnectDuringBackgroundTeardownGetsRetryable|TestReapAbandonedWhenMainClientReconnects|TestConcurrentReapsSingleTeardown)$' --race --count=1 --test-verbose` — 4/4 passed (trace `886bbe0b885eadd71e68918a20194ccd`) - `dagger --progress=plain call engine-dev test --pkg=./engine/server --count=1` — 56/56 passed (trace `ac8e829ffe4a031b53a25e5176e4c983`) - `dagger --progress=plain call engine-dev test --pkg=./engine/server --race --count=1` — 56/56 passed (trace `63c38bec3799ed278a72b79ff5305185`) - `dagger --progress=plain check go:lint changelog:generate` — passed (trace `52c8f0caf2bfebc849034933083594a8`) GitLab fixture validation and provenance: - Canary PR #13885 passed all 75 applicable checks on `becaa3a33542ccc7e96dbf3f430d887f41958e52`, including all five formerly failing shards and `TestPrivateGitRepoArgCaching` in real CI. - Fresh CI on this PR's current head passed all 75 applicable checks: 74 Dagger Cloud contexts plus DCO. The five formerly failing shards passed with traces: `test-base` (`2460fc75505f4043c73494f2f9504ba4`), `test-call-and-shell` (`6a40c909805c2ca65b8733650c2daa63`), `test-cli-engine` (`09c7f1aaa19a385f71b973f574042f59`), `test-container` (`4fb6d68d4ea869e4065304ecb7e36cb2`), and `test-modules` (`2eea881d02a2065a40344008d4612f4f`). - Stable patch IDs for `becaa3a33542ccc7e96dbf3f430d887f41958e52` and `da8c39fb37947ffb4ee304e799b5e09e4c3e3b16` match: `c5ece8f8d50919d36bdb6abad1e312d65ae8a2bf`. - The post-cherry-pick delta is exactly six files with 35 insertions and 30 deletions. - `gofmt -d` produced no diff for all six fixture files. - `git diff --check` passed for both the six-file cherry-pick and the complete stacked PR diff. ## Release note The unreleased v0.21.9 `Fixed` entry says that session teardown now runs in the background after the main client disconnects, preventing shutdown responses from timing out while the engine releases session-owned cache results. It attributes the change to source PR #13681. The GitLab fixture update is test-only and does not add a release note. ## Remaining risks / work - Update this branch after #13865 merges to remove the prerequisite from the stacked diff. - Manual playground validation was not run locally.",
          "url": "https://github.com/dagger/dagger/pull/13884",
          "createdAt": "2026-08-12T21:43:59Z",
          "updatedAt": "2026-08-13T02:58:35Z",
          "timestamp": "2026-08-13T02:58:35Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "sipsma",
          "state": "closed",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:b67299ed21837d0e898a",
        "signalId": "github:dagger/dagger:pull_request:13886",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13886",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "[backport-0.21] engine: prune DAGQL cache by metadata size",
          "text": "## Summary Backports the complete metadata-aware DagQL cache-pruning behavior from #13846 to v0.21: - tracks a coarse structural metadata estimate (`3072*results + 512*terms + 768*class slots`) - prunes cold persisted roots toward a metadata target independently of disk usage - protects active-session and engine-lifetime/unpruneable closures - schedules structural checks for startup, session completion, graceful shutdown, explicit GC, and the pressure monitor - handles zero-disk metadata pressure, estimator correction, forced sparse-class compaction, and monitor no-progress blocking - adds `gc.dagqlCache.maxEstimatedBytes` / `targetEstimatedBytes`, the `dagger_dagql_cache_metadata_estimated_bytes` metric, bounded aggregate observability, tests, scale benchmarks, docs, and a v0.21.9 change entry This bounds retained metadata growth and may mitigate scale-related symptoms. It does **not** claim to directly fix the reported `egraphMu` / startup contention. The separate bounded manual structural-prune API discussed with Erik is intentionally not included because it was not part of #13846. The source defaults remain unchanged at 4 GiB maximum / 3 GiB target. ## Source PR and commits - Source: #13846 - Exact source series: `615a02dbdaf6ef107f8a1dfa3c45d39624403645^..b19467f2b032bb4c000c5c80bbe21ac062ac03f5` (20 commits; parent `ba839a98077aef4398415ccc805f57fd694c2e4b`) - Metadata-pruning backport series: `f021d54d4c0824061690fa4669fc39d39183d4d1^..0371e77ae8210df2f7583e2fa0ac39b08019f32f`, followed by v0.21 adaptation commit `5479f384e8bff0e2d87d981838528adbe755ef2c` - CI fixture source: #13849; source PR commit `0c656b9ca6395fdec1f051d34987bb4f8864196a`; merged main commit `66337ba5d3d01ae10a0d7fb97ebe4dec9326e8bc` - Proven v0.21 fixture backport: `becaa3a33542ccc7e96dbf3f430d887f41958e52`, validated on #13885 and initially cherry-picked unchanged as `f951484dde70cd431a5e19f84027aa4eaa946367`; after two byte-identical CI head refreshes, the current equivalent tip is `89af13099acc7a46885023b096b25372e32931f4` (tree `631aa606fb061a4464f838559c2569c25bb9eb9b`) All 20 metadata-pruning source commits, including the design-review history, are preserved in order with `cherry-pick -x`; none were squashed or omitted. The additional v0.21 adaptation and fixture-propagation commits are also DCO-signed. The fixture commit preserves Guillaume de Rouville as author and the source traceability footer. ## Prerequisite #13375's GC pressure-monitor machinery is already present on `backport-0.21` as `78fc50e58a4f417825e414463829c1bc7c2053d2`. Its stable patch ID matches source commit `838b7c73ab69fb5bc97cfa1139478ae7d305e4ed`, so this backport relies on the existing v0.21 implementation rather than duplicating it. ## v0.21 adaptations - Preserved v0.21's existing `stateMu` graceful-stop/session lifecycle and namespace-worker initialization while applying the new scheduler/config state. Main-only `core.SetWorkspaceInvalidator`, recursive-read-only-mount context, and a session-test field rename are absent from v0.21 and were not pulled in as unrelated changes. - Kept the cache-pruning reference in its v0.21 location under `skills/cache-expert/references/`. - Adapted restart documentation and the large persistence benchmark from main's schema 17 to v0.21's unchanged schema 16; no persistence schema bump or worker reset was introduced. - Adapted the active zero-disk integration workload from main's `dagger api with-session` spelling to the equivalent v0.21 `dagger run` command. - Added the v0.21.9 unreleased change entry with source attribution `Author: sipsma`, `PR: 13846`. - Added exactly the proven six-file v0.21 GitLab fixture backport from #13885 with no further adaptation. It replaces revoked user PAT usage with read-only project deploy tokens and propagates each required username through the existing integration-test authentication paths; no product code or diagnostics changed. ## Validation run - `./dagql`: 295 tests passed — pruning semantics, trigger/target correction, active/unpruneable protection, no-detail structural mode, compaction, persistence/restart, and estimator accounting. [trace](https://dagger.cloud/dagger/traces/9da3818267833cf8aac933012f30d738) - `./engine/server`: 61 tests passed — defaults/config resolution, no-disk-stat behavior, lifecycle scheduling, pressure monitoring, blocked/no-progress recovery, explicit/session reset behavior. [trace](https://dagger.cloud/dagger/traces/64f555b64551ad8de249de8a057dd38f) - `./engine/config`: compiled successfully (no package tests). [trace](https://dagger.cloud/dagger/traces/de211c18eafdb972b8b8c7b13896815f) - `./cmd/engine`: 7 tests passed. [trace](https://dagger.cloud/dagger/traces/ee7c94816dadfca9f19531a2b045f5f8) - `TestEngine/TestPrometheusMetrics`: 2 tests passed and observed the new metadata gauge. [trace](https://dagger.cloud/dagger/traces/bb2199d5bbf958c11fcee33a7a7fc405) - `TestLocalCache/TestDagqlMetadataGCProtectsActiveZeroDiskResults`: 2 tests passed; the workload remained active through a pressure-monitor interval, produced metadata growth with only 4 KiB disk use, stayed protected while active, then closed and was structurally pruned. [trace](https://dagger.cloud/dagger/traces/7c526fecdbccf5130bf8063c1751bf3d) - All seven one-shot metadata benchmarks passed at their defined 200k/1M scales: allocation-free estimate, metadata-vs-disk snapshots, full prune, four fixture calibrations, correction/churn, sparse forced compaction, and schema-16 import. The 1M import retained schema 16 and peaked at 4,526,727,168 B additional `HeapInuse`; 1M metadata pruning removed all roots in ~14.7 s with ~893.8 MB peak scratch. [trace](https://dagger.cloud/dagger/traces/ca33b9e49899c57cbc83c0237eae9f63) - Root Go `check-tidy` passed. [trace](https://dagger.cloud/dagger/traces/33084456fd29ccfac78180ea42ca88be) - Historical CI on `5479f384e8bff0e2d87d981838528adbe755ef2c` passed DCO and 69 Dagger Cloud contexts; five split suites failed only where GitLab rejected the revoked private-fixture PAT. [base trace](https://dagger.cloud/dagger/traces/6b0504c101392a40e25e6747054c5f18), [call/shell trace](https://dagger.cloud/dagger/traces/5528cad9cd43f8e492f0657fbdf4e7c3), [CLI/engine trace](https://dagger.cloud/dagger/traces/b28a191f2e4baa119ac77dc78f98ce47), [container trace](https://dagger.cloud/dagger/traces/8bb6345aecb7bc216c5c0ad0e4df3aba), [modules trace](https://dagger.cloud/dagger/traces/70c4e0129c4d07d6b8b7f274749186f4). - Canary #13885 proved `becaa3a33542ccc7e96dbf3f430d887f41958e52`: all 75 applicable checks passed, including all five formerly failing shards and `TestPrivateGitRepoArgCaching` in real CI. - The propagated patch is byte-for-byte equivalent by stable patch ID (`c5ece8f8d50919d36bdb6abad1e312d65ae8a2bf`), changed only the same six integration-test files (35 additions, 30 deletions), and passed `git diff --check`. Final head `89af13099acc7a46885023b096b25372e32931f4` passed all 75 applicable statuses (74 Dagger Cloud contexts plus DCO). Initial failures were trace-classified rather than dismissed: Linux/arm64 Wolfi release exited 139 ([failed trace](https://dagger.cloud/dagger/traces/7db17ce3d0d04a45afc83930d7d3bf58), [final pass](https://dagger.cloud/dagger/traces/7c87ea93d8d3b6b9b97ed1a021ce0109)); telemetry saw a runtime-only `13.6% dropped` golden suffix ([failed trace](https://dagger.cloud/dagger/traces/df5d54edea84862b868cfc1570d1aca6), [final pass](https://dagger.cloud/dagger/traces/2d67886db69c386edbd7f32ab7312e38)); and one 441/442 module-runtime run failed during pinned Python 3.13.1 initialization ([failed trace](https://dagger.cloud/dagger/traces/6177b76b28748ff8dbb475970033672b), [final pass](https://dagger.cloud/dagger/traces/1e7802c2531cc335239fdc913dcf5822)). `dagger cloud rerun` could not resolve the cross-fork PR or synthetic merge SHA from this runner, so the two retries used committer-timestamp-only tip refreshes, byte-identical tree checks, and explicit `--force-with-lease`. - During final stewardship, `backport-0.21` advanced from `9f68624bf69c23c01861e298651656ecac780285` to `269c47d7708b4623c3fbfdcd2440d19a055dc7e0` via #13885, which independently absorbed the same six-file fixture patch (stable patch ID `c5ece8f8d50919d36bdb6abad1e312d65ae8a2bf`) alongside its registry-observability fix. Final green synthetic merge `86bdee550d36867afd02cfdd8d44b08ccebaae79` has exact parents base `269c47d7708b4623c3fbfdcd2440d19a055dc7e0` and head `89af13099acc7a46885023b096b25372e32931f4`; the retained traceable head commit merges without conflict or behavioral duplication. Upstream squash commit `49559d42c9c9dc2588dc53f834e15ea572f69ae7` has the identical tree `3821ccc63bdbed5ee821980aa6893b9eea7044be`. - GitHub's `check-for-changelog` job was intentionally skipped because the PR has no `needs/changelog` label; the release entry is present and Dagger Cloud's `changelog:generate` check passed. - Changed Go files are `gofmt`-clean; `git diff --check` passed; all 22 commits have DCO signoff; #13375 prerequisite patch IDs and all 20 `-x` source footers were audited. ## Release note Adds `.changes/unreleased/Fixed-20260812-213129.yaml` for v0.21.9, attributed to source PR #13846. The GitLab fixture propagation is test-only and adds no separate release entry. ## Risk and remaining work This was published and fully validated as a draft because it is a large engine/cache backport. GitHub records `sipsma` as squash-merging it at `2026-08-13T02:58:09Z`, after the final green run. The revoked GitLab fixture credential was addressed by propagating the exact six-file v0.21 patch proven on #13885, and the final combined-head matrix has converged green. The complete integration suite beyond the published split coverage remains unrun. The root full-lint command was attempted but is currently blocked by unrelated generated docs snippets (`default-address/go`) that cannot import their generated internal package. Targeted `go vet` was also attempted; it reports pre-existing context-cancel findings in `dagql/cache.go` (blamed to `aa719a10d52`, before this series) and `engine/server/session_attachables.go` (`74c2889afc6`, untouched here). Those unrelated findings were not changed to keep this backport scoped. The estimator is deliberately coarse and structural rather than RSS-based. A pre-existing oversized schema-16 graph is fully imported before the one-second startup prune, so a temporary upgrade/startup memory peak remains possible. This patch bounds retained metadata after pruning; it does not establish a direct fix for lock contention.",
          "url": "https://github.com/dagger/dagger/pull/13886",
          "createdAt": "2026-08-12T22:44:54Z",
          "updatedAt": "2026-08-13T02:59:40Z",
          "timestamp": "2026-08-13T02:59:40Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "sipsma",
          "state": "closed",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:f9faff46b710ff923d85",
        "signalId": "github:dagger/dagger:pull_request:13885",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13885",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "[backport-0.21] fix: hide registry HTTP probe errors",
          "text": "## Summary Backport the scoped telemetry fix that hides normal registry-protocol HTTP probe failures from successful pull and publish progress. Registry HEAD 404s and auth challenges still occur as expected, but they no longer render as scary failed child rows when the enclosing registry operation succeeds. Also backport the shared private-GitLab test fixture update from #13849. GitLab revoked the committed user PATs used by the v0.21 integration tests; the fixture now uses read-only project deploy tokens and forwards their required usernames through each existing authentication path. ## Source provenance Registry progress fix: - Source PR: https://github.com/dagger/dagger/pull/13410 - Exact semantic source commit: https://github.com/dagger/dagger/commit/b9cdb6656036d6efd176fcda8e615332c81cb554 - Merged squash commit for source PR: https://github.com/dagger/dagger/commit/0bb84dde94beb8174d17222f33b782adeb6f8ee9 - Backported with `cherry-pick -x`, preserving Alex Suraci as author. GitLab CI fixture fix: - Source PR: https://github.com/dagger/dagger/pull/13849 - Source PR commit: https://github.com/dagger/dagger/commit/0c656b9ca6395fdec1f051d34987bb4f8864196a - Merged main commit: https://github.com/dagger/dagger/commit/66337ba5d3d01ae10a0d7fb97ebe4dec9326e8bc - Backported as `becaa3a33542ccc7e96dbf3f430d887f41958e52` with `cherry-pick -x`, preserving Guillaume de Rouville as author. ## Root cause and user impact containerd instruments registry HTTP requests through OpenTelemetry. Normal protocol behavior includes failed probes such as HEAD 404 responses for blobs or manifests that are not present yet, plus authentication challenges. The progress frontend forced every failed span visible, even when that span was encapsulated and its parent registry operation completed successfully. As a result, successful `Container.publish` calls could show prominent `remotes.docker.resolver.HTTPRequest` and `HTTP HEAD ERROR` rows, especially in plain progress. The fix encapsulates registry resolve, pull, and push internals and only reveals failed encapsulated children when the enclosing operation itself fails. Genuine registry failures therefore retain their diagnostic HTTP spans. Separately, GitLab automatically revoked the user PATs committed for the disposable private-repository integration fixture. That made five otherwise unrelated CI shards fail wherever they exercised the shared HTTPS fixture. Project deploy tokens require a username/token pair, so changing only the token is insufficient; every fixture consumer must propagate the matching username. ## v0.21 adaptations Registry progress fix: - Cherry-picked the complete scoped semantic fix and its golden regression coverage. - Kept the existing v0.21 golden-fixture hostname; the source commit included unrelated hostname churn in the same fixture. - Excluded the broader transfer-progress row architecture from the remainder of #13410. - No BuildKit, containerd, OpenTelemetry, or other dependency changes. GitLab fixture fix: - Limited to six existing v0.21 integration-test files; no engine or product code changed. - Mapped main's extracted `module_helpers_test.go` change to the equivalent helper in v0.21's `module_test.go`. - Preserved v0.21's existing CLI assertion form in `gitcredential_test.go`. - Omitted main-only workspace/private-dependency coverage and its extracted fixture because those tests and layouts do not exist on v0.21. ## Validation Registry progress fix passed: - `dagger call engine-dev test --pkg='./dagql/idtui' --run='^TestTelemetry/TestGolden/docker-build-fail$'` — 3 tests - `dagger --progress=plain call engine-dev test --pkg='./core/integration' --run='^TestContainer/TestPublish$'` — 2 tests, using the local test registry; expected HEAD 404 probes occurred without HTTP HEAD ERROR progress rows - `dagger call engine-dev test --pkg='./dagql/dagui'` — 22 tests - `dagger call engine-dev test --pkg='./engine/server/resolver'` — 1 test - `dagger --progress=plain -m ./toolchains/go call --source=. lint-module --module=dagql` - `dagger --progress=plain -m ./toolchains/go call --source=. lint-module --module=engine` - `git diff --check upstream/backport-0.21...HEAD` GitLab fixture adaptation passed locally: - `TestGit/TestAuthProviders/GitLab_auth` — direct username/token API path - `TestGitCredential/TestGitCredentialErrors/gitlab_private_module` — host credential-helper/module path - `TestModule/TestCrossSessionSecrets/contenthash_cache_hit_with_secrets` — nested secret path - `TestConfig/TestDaggerGitRefs/Private_GitLab` — six module-reference assertions - `git diff --check` Fresh Dagger Cloud checks on canary head `becaa3a33542ccc7e96dbf3f430d887f41958e52` passed all five shards that previously exposed the revoked GitLab credentials: - `test-split:test-base` - `test-split:test-cli-engine` - `test-split:test-call-and-shell` - `test-split:test-container` - `test-split:test-modules` That run also passed `TestPrivateGitRepoArgCaching`, confirming the earlier linked-worktree `.git` failure was local-state-only. The first fresh `test-split:test-base` run independently hit `TestChangeset/TestWithChangesets/comparison_with_sequential_merge` with `fatal: stash failed`, the exact unrelated snapshot-hardlink/ctime baseline defect documented by main PR #13773. Its isolated supported Dagger Cloud rerun passed in 13m34s (trace `91b9bc8f3a602d14f0e6e8b5627e93c6`). The final PR ledger is 75 successful checks and one intentionally skipped changelog gate. The repository-root lint entrypoint was also attempted for the registry fix. It failed on an unrelated generated import in `docs/versioned_docs/version-0.21.4/getting-started/types/snippets/default-address/go/main.go`; the scoped lints for both changed source trees pass. ## Release note Added a v0.21.9 unreleased `Fixed` entry attributed to source PR #13410: > Registry protocol probes no longer appear as scary errors in progress output when image pulls or publishes succeed, while genuine registry failures remain visible. The GitLab fixture update is test-only and does not add a release note. ## Remaining risks The registry change is a presentation-only policy change scoped to Dagger registry resolve, pull, and push spans. Failed enclosing registry operations still reveal their failed children, and higher verbosity can still expose encapsulated details. The broad progress and transfer-row changes from the rest of #13410 are intentionally not included. The GitLab change depends on the deliberately public, read-only deploy-token fixture remaining valid. Fresh CI on this canary commit verified all five affected shards; propagation to other backport PRs remains a separate lead decision.",
          "url": "https://github.com/dagger/dagger/pull/13885",
          "createdAt": "2026-08-12T21:55:02Z",
          "updatedAt": "2026-08-13T01:54:46Z",
          "timestamp": "2026-08-13T01:54:46Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "sipsma",
          "state": "closed",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:a457752b481827485127",
        "signalId": "github:dagger/dagger:issue:13867",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:issue:13867",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "issue",
          "title": "Elixir SDK is ready for 1.0",
          "text": "Elixir SDK should be an installable module that passes all checks in sdk-sdk",
          "url": "https://github.com/dagger/dagger/issues/13867",
          "createdAt": "2026-08-10T16:10:35Z",
          "updatedAt": "2026-08-12T22:20:10Z",
          "timestamp": "2026-08-12T22:20:10Z",
          "metrics": {
            "reactions": 0,
            "comments": 2
          },
          "labels": [],
          "author": "kpenfound",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:907591d68979018964f8",
        "signalId": "github:dagger/dagger:pull_request:13872",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13872",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "fix(elixir): update vulnerable runtime dependencies",
          "text": "## Summary - update `go.opentelemetry.io/otel/sdk` to v1.43.0 - update `google.golang.org/grpc` to v1.82.1 with its required transitive dependencies - refresh the Elixir Go runtime module sums These updates fix the current open Dependabot alerts for `sdk/elixir/runtime/go.mod`.",
          "url": "https://github.com/dagger/dagger/pull/13872",
          "createdAt": "2026-08-11T13:42:25Z",
          "updatedAt": "2026-08-12T22:15:45Z",
          "timestamp": "2026-08-12T22:15:45Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "matipan",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:737f01be6c6b9438753c",
        "signalId": "github:dagger/dagger:pull_request:13850",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13850",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "feat(schema): require declared Workspace! args on module functions",
          "text": "Follows #13845, which made `currentModule.asSDK`'s workspace argument required. This does the same for module functions. ## Problem Workspace arguments on module functions were published as nullable no matter how they were declared, because the engine injects one when the caller leaves it unset (`core/typedef.go`, plus two spots in `core/schema/module.go`). So a function declaring `ws: Workspace!` advertised an optional argument — the same lie `asSDK` told before #13845. The modules already declare the truth. The Go SDK registers a plain `ws *dagger.Workspace` with no `.WithOptional(true)`, unlike a `// +optional` arg next to it: ```go // .dagger/modules/engine-dev/dagger.gen.go WithArg(\"ws\", dag.TypeDef().WithObject(\"Workspace\"), ...). WithArg(\"clientDockerConfig\", dag.TypeDef().WithObject(\"Secret\").WithOptional(true), ...). ``` The engine was overriding that. This stops the override. ## Why the callers had to change The injection can't rescue a required argument. dagql rejects a missing non-null argument in `preselect` (`dagql/objects.go`) *before* it runs the `GetDynamicInput` hook that fills workspace args in. So everything that relied on injection for a required arg now puts it on the selector, resolving it the way `loadWorkspaceArg` does — the workspace an enclosing group bound into the context, else the session's ambient one, and never inherited across a module function boundary. - **generators, checks, up** — all funnel through `ModTreeNode.DagqlValue`, so they're covered in one place, for the root constructor as well as the leaf function. - **scale-out** — needs no query change. It selects `Module.generator(name:)`, which binds no workspace, so it lands on the ambient fallback, which is what it relied on before. - **LLM tools** — `buildObjectMethodSelector` fills a required Workspace from the LLM's bound workspace. - **`dagger call`** — defaults it to `currentWorkspace`. `--ws` is still registered, via a new `workspaceValue` that resolves an address the way `--dir` does, so a caller can aim a function at a different workspace. It just never gets `MarkFlagRequired`, which would otherwise reject the call before the default could be filled in. An argument declared *optional* is unaffected: it stays nullable and keeps its injection. That's why `TestRuntimeDependencyDoesNotInheritWorkspace` is untouched — its fixture declares `// +optional`, so the #13229 isolation behavior and error message are unchanged. ## No codegen Deliberately none. Both edited files are on the module-runtime path, not the core schema: `FunctionSpec` builds the dagql `FieldSpec` for a module's functions at load time, and the `core/schema/module.go` blocks were inside the `functionArg`/`internalFunctionArg` resolvers, mutating the value they return rather than the schema shape. `schema.graphqls`, the SDK clients, the reference docs and the vendored toolchain clients are all unchanged, and module `dagger.gen.go` files would regenerate byte-identical. ## Draft: not yet run Compiles and vets clean for linux, and the integration test typechecks — but nothing here has executed. `core` and `core/integration` can't build natively on darwin (`engine/engineutil` uses linux-only syscalls), so this needs a dev engine run before it's reviewable. The generators/checks/up threading and the `dagger call` default-fill are the paths to exercise first. Changie entry still to add.",
          "url": "https://github.com/dagger/dagger/pull/13850",
          "createdAt": "2026-08-06T14:41:23Z",
          "updatedAt": "2026-08-12T21:27:05Z",
          "timestamp": "2026-08-12T21:27:05Z",
          "metrics": {
            "reactions": 0,
            "comments": 1
          },
          "labels": [],
          "author": "TomChv",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:aec7900349a1732f9410",
        "signalId": "github:dagger/dagger:issue:13883",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:issue:13883",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "issue",
          "title": "v0.x modules are given a v1.0 view",
          "text": "Related to, but not a duplicate of https://github.com/dagger/dagger/issues/13654 When a module is being developed for the 1.0.0-beta but still specifies an engineVersion of v0.x, there are two breaking error paths described below. The module can get into this broken state without the module author being aware because when using the module directly on 1.0.0-beta, a v0.x module is also served new APIs with the 1.0.0-0 view rather than the correct pre-1.0 view, so if the module uses a new 1.0.0-0 API, it will work on 1.0.0-beta despite the declared engineVersion. ## Users on v0.2x Users of a module described above running a v0.2x engine will fail to load the module because it references APIs that do not exist in v0.2x. The error will complain about the APIs not existing, which likely means nothing to the user. As far as they can tell, it should work on their engine because the declared engineVersion is compatible with theirs. This case has come up with users installing `github.com/dagger/go` on `v0.21.6`. The error was `convert arg ws: node field not found in environment`. This module should have been updated to `engineVersion: 1.0.0-0` when it started using the new APIs ## Users on 1.0.0-beta with dependencies Users of the beta with modules that depend on a module like the one described above will be stuck too. Codegen for the broken module will complete because it is given the 1.0.0-0 view, however when loaded as a dependency of their module it is now on a v0.2x view and the new APIs are no longer available. This case came up with `github.com/dagger/dagger/.dagger/modules/go` used as a dependency throughout the dagger/dagger repo. Its declared engineVersion was v0.21.7 but it uses Workspace.Git",
          "url": "https://github.com/dagger/dagger/issues/13883",
          "createdAt": "2026-08-12T21:03:31Z",
          "updatedAt": "2026-08-12T21:08:55Z",
          "timestamp": "2026-08-12T21:08:55Z",
          "metrics": {
            "reactions": 0,
            "comments": 1
          },
          "labels": [],
          "author": "kpenfound",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:8399c46edd90198bc758",
        "signalId": "github:dagger/dagger:pull_request:13851",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13851",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "feat: add experimental detachable sessions with source callback lifecycle",
          "text": "## What this adds A detachable session is an engine session that keeps running after the client that created it disconnects. Ordinary Dagger sessions end when their main client disconnects. This PR adds an explicit opt-in mode where accepted work can continue, a later CLI can find and inspect the session, and the user terminates it explicitly. This is useful for starting a long build or pipeline from a laptop or short-lived shell without keeping that terminal connected. Two commands accept the new flag: ```console dagger call --detach <function> [args...] dagger api call --detach <function> [args...] ``` A provisional command family manages detachable sessions: ```console dagger sessions list dagger sessions inspect <session-id> dagger sessions attach <session-id> dagger sessions terminate <session-id> ``` For example: ```console $ dagger call --detach build --platform linux/arm64 Session: sess_01j9xk2m4q7c8b3n6s0a9c3fgh Query: qry_01j9xk2p9w2a4c7m5k3p0q1rsv # The initiating CLI has exited. The accepted query continues on the engine. $ dagger sessions list ID STATE QUERY STATUS CREATED ATTACHED sess_01j9xk2m4q7c8b3n6s0a9c3fgh detached qry_01j9xk2p9w2a4c7m5k3p0q1rsv running 2026-08-06T18:12:04Z - $ dagger sessions inspect sess_01j9xk2m4q7c8b3n6s0a9c3fgh $ dagger sessions attach sess_01j9xk2m4q7c8b3n6s0a9c3fgh # Recorded progress is replayed, live progress follows, then the saved result is printed. $ dagger sessions terminate sess_01j9xk2m4q7c8b3n6s0a9c3fgh Terminated sess_01j9xk2m4q7c8b3n6s0a9c3fgh ``` The CLI performs schema discovery and argument conversion before submitting one final GraphQL request. The engine stores the request and returns `202 Accepted` with a query ID. After that acknowledgement, the initiating CLI may disconnect. Exit 0 from `--detach` means the engine accepted responsibility for the query. It does not mean the query succeeded. After creator disconnect: - Engine-only execution and values already copied into engine-owned storage can continue. - A new operation that still needs a creator callback fails individually. It does not terminate the session or unrelated work. - A published source that is no longer available returns a recognizable source-unavailable error without waiting for the initial-publication timeout. - An observer cannot replace or supply creator callbacks. - Source-backed host-to-container tunnels terminate and clean themselves up when their listener stream fails instead of remaining falsely registered as running. A later CLI can inspect or attach to the session. `dagger sessions terminate` cancels remaining work, stops services, releases session resources, and deletes saved result files. The detachable-session P0 and source-client callback designs are included under `hack/designs/`. ## P0 behavior ### Session lifetime and attachments The client generates the detachable session ID before the first request. A detachable session has one engine-owned cancellation root. Main-client disconnect only clears the current attachment. Explicit termination or engine shutdown cancels the session. A detachable session has at most one current attachment. Explicit close and connection health detection can clear it. Both paths compare the attachment ID and an engine generation so a delayed event from an old connection cannot clear a newer attachment. There are three connection roles: - The creator creates the session, performs discovery while attached, publishes its source callbacks, and submits the primary query. - An observer, created by `dagger sessions attach`, follows telemetry and reads the saved result. It does not initialize a generated client, load a workspace or module, or publish creator callbacks. - A control connection implements `list`, `inspect`, and `terminate` without creating a session. The creator retries only HTTP `409 attachment_connection_exists`. An observer retries only HTTP `409 already_attached`, for up to 30 seconds, and never replaces a live attachment. Other protocol responses and transport errors fail immediately. ### Stored query result and telemetry The engine saves the GraphQL response HTTP status, `Content-Type`, and body. The request limit is 16 MiB. The saved result limit is 64 MiB. An oversized result is discarded and reported as `result_discarded`; the session remains alive. Attaching never executes the query again. The detached query uses the creator client's existing telemetry store. Attach replays recorded traces, logs, and metrics through the existing SSE paths, then follows live records. At query completion the engine records a final row for each stream. Replay stops at those rows, even if services write later telemetry. ### Source callback identity and publication Each detachable, non-observer physical source may successfully publish its attachables once. A failed handshake before publication remains retryable. Successful publication records the physical source ID permanently for the lifetime of the detachable session. A later request with the same source ID is rejected before it can replace the original registration. Observer connections publish health only. They do not alter source publication state and are never callback candidates for the creator. This PR does not implement callback rebinding. ### Callback behavior after source loss Initial source publication retains its existing bounded wait so normal startup ordering continues to work. After a source has published successfully and its exact registration is gone: - A blocking lookup returns `source client \"<client-id>\" is unavailable` immediately. - Graceful deregistration makes that state visible immediately. - Abrupt transport loss follows the existing attachable health policy. - `IfAvailable` lookups retain their existing `ok=false` behavior. - Secret and socket candidate selection retains its existing fallback order and aggregate exhaustion error. - Callback failure does not cancel the detachable session or redirect the operation to an observer. Copied and engine-owned values continue without callbacks. This includes completed host snapshots and genuine engine-owned `setSecret` values. Provider-backed URI secrets such as `env://` remain callback-backed. ### Nested client shutdown Nested clients in a detachable session flush workspace locks and telemetry before deregistering and waiting for their exact attachable registration to leave. They close their shutdown channel last. Ordinary-session nested shutdown remains unchanged. ### Host-to-container tunnel lifecycle Host-to-container listener completion is now part of service lifecycle for ordinary and detachable sessions. Listener failure is serialized against service publication. A failed listener either prevents publication or stops and removes the published tunnel. Cleanup cancels blocked sends and dials, closes sibling listeners and late connections, preserves the first cause, releases the tunnel's upstream binding exactly once, and releases tracked resources. Another binding can keep the shared upstream running. ### Existing boundaries Ordinary sessions are unchanged outside the failed-tunnel correction. Without `--detach`, the last main-client disconnect still tears the session down. Nested requests from module code and other nested clients cannot use engine-global `/v1/sessions` control routes. Creator and observer attachment clients cannot use `/shutdown`. Engine shutdown terminates all in-memory detachable sessions through the same terminal cleanup path. Engine startup deletes stale saved-result directories. Sessions do not survive engine restart. Session, query, and attachment IDs use the `sess_`, `qry_`, and `att_` prefixes followed by exactly 26 lowercase base32 characters. The engine validates IDs before registry lookup or filesystem path construction. ## Validation Final reviewed head: `90136f800a9fd6cd1ceaf0a9996f0cbcb0830251`. All engine integration runs below used `engine-dev`, which built and ran the engine from the source worktree. - `go test -count=1 -race ./engine/server ./engine/client ./engine ./engine/clientdb ./internal/cmd/dagger` passed. - The concurrent detachable integration configuration passed 16 tests. Trace: `885dd9b8618590296276698fcefbab49`. - The source-built server lifecycle and protocol suite passed 61 tests with the race detector. Trace: `7e368ab8f4cf3f155d4334a7c984b5e1`. - The source-built client retry, telemetry, and close suite passed 31 tests with the race detector. Trace: `8d3a072cf29372b9dd222a371805cf11`. - The source-built CLI, detach, provisional-command, and existing hidden-command suite passed 32 tests. Trace: `c3a3c44afe1df4a5558024bda41add54`. Source callback and tunnel validation: - `go test -count=1 ./engine ./engine/server ./core ./engine/engineutil` passed. - `go test -count=1 -race ./engine ./engine/server ./core ./engine/engineutil` passed. - Deterministic ordinary and detachable tests cover first-listener failure, later-port failure, completion before publication, completion after publication, exact binding release, listener and monitor exit, service-map cleanup, and tracked-resource release. - The final source-built host-to-container and shared-service selection passed 8 tests. Trace: `8f675a24055c46df4c41ac7a1e5b8ba2`. - The final source-built creator-bound callback and copied-value selection passed 6 tests. Trace: `561c0819611a380dd28cd3759224b14f`. ## Why this PR is still a draft ### Cache ownership for indefinitely live sessions is not solved Cache resources associated with a live detachable session remain held by the existing session ownership model for that session's lifetime. Explicit termination and engine shutdown release session-owned resources. The integration test measured cache growth across three completed but still-live sessions and a return near the warmed baseline after termination. P0 does not define which cache results a session that remains alive indefinitely should keep. That requires a separate ownership and cleanup design before broad use. ### Callback rebinding remains deferred This PR now handles absent sources explicitly and cleans up failed source-backed tunnels. It does not make creator callbacks resumable or replace them with callbacks from an observer. After a published source leaves, new work that requires that source fails instead of being rebound. Creator-backed capabilities that remain unavailable after source loss include: - new host file or directory snapshots and host filesync exports; - URI- or provider-backed secret reads that were not already copied into an engine-owned secret; - new host socket and SSH-agent connections; - default Docker credential callbacks and new Git credential-helper lookups; - host image import, export, or load transfers that have not completed; - lazy local module source, workspace, defaults, or outer environment reads that still reach the creator host; - cache import or export paths that require host transfer callbacks; - terminal, pipe, prompt, stdin, and other interactive callback streams; - new container-to-host reverse-tunnel connections after their source disappears; and - host-to-container tunnel resumption after its listener stream ends. Values copied into engine-owned storage before disconnect continue to work. Failed host-to-container tunnels now clean themselves up, but are not resumed on another client. ### Other explicit P0 limits - There is no public SDK detach API. The new connection fields are internal engine-client fields. - Sessions do not survive engine restart. - `dagger up`, `dagger check`, shell, listen, and interactive input are not supported. - P0 supports one primary query and one current attachment. There is no force attach or concurrent attachment. - There is no result aggregation beyond the one saved primary GraphQL response. - There is no general workflow history, user-provided session name, TTL, or Cloud scale-out. - Network loss before query acknowledgement may leave an empty discoverable session that requires explicit termination. The CLI terminates it automatically when it can prove no query was acknowledged. - Telemetry replay is bounded at query completion. Rows written afterward are not included. - There is no broad long-term result or log database. - The CLI name is provisional. This implementation uses `dagger sessions` because `internal/cmd/dagger/session.go` already defines the hidden `dagger session` command used by SDKs to communicate with the engine. The final public command name remains undecided.",
          "url": "https://github.com/dagger/dagger/pull/13851",
          "createdAt": "2026-08-06T17:40:58Z",
          "updatedAt": "2026-08-12T20:47:04Z",
          "timestamp": "2026-08-12T20:47:04Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "sipsma",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:6c5e573492a4b578417d",
        "signalId": "github:dagger/dagger:pull_request:13882",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13882",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "feat(workspace): import another workspace's dagger.toml",
          "text": "## What Adds a top-level `import` key to `dagger.toml`: a Git ref to another workspace whose configuration is merged **underneath** the current one. ```toml import = \"github.com/acme/dagger-base@v1.2.0\" # Only the difference from the base: [modules.go.settings] goVersion = \"1.24\" ``` A team can publish one base workspace config — shared tool versions, module refs, settings, environments — and have many downstream repos point at it instead of copying it into each one. Asked for on Discord (vindico: \"I'm currently putting duplicated workspace config in a lot of repos\"; chrisp6229 asked for exactly this, per project type), and scoped to shykes' \"as long as we don't go crazy with the config layering... a basic import feature in dagger.toml\". Design doc and implementation plan: `future/workspace-config-import.md`. ## Merge semantics The importing workspace wins every conflict. Layering, lowest first: imported `dagger.toml` → this repo's `dagger.toml` → user-level overrides → `--env` overlay. Nothing about the existing three layers changes. - **Module entries merge per field**, so a repo can override one setting without repeating the module's ref. That makes `source` optional in the file — an entry may be a pure override of a module the import installs. - **\"Set\" means explicitly present**, not non-zero: `entrypoint = false` and `ignore = []` override an inherited value. Presence comes from the raw TOML, not the parsed struct. - `as-sdk` and `legacy-default-path` are **never inherited** — both describe the imported workspace's own tree. - `[ports.<host>]` replaces the inherited mapping as a unit. - Envs merge by name; an env only the base defines is selectable downstream. The full per-section table is in the design doc. ## The two deliberate limits 1. **One import, no chain.** An `import` in the imported config is an error, not a second layer. Ignoring it would hand the user a config silently missing whatever the base author expected to inherit. 2. **Only remote modules.** Module entries in the imported config whose source is a local path are dropped, with a warning naming them, before the merge — along with their env overlays and any port mapping that forwards to them. Dropping rather than erroring, because a normal repo keeps its own modules under `modules/`, and erroring would make ordinary repos unusable as import targets. Classification of \"local\" is `gitref.FastKindCheck(source, \"\")` plus a directory stat **in the imported tree**, deliberately not `core.ParseRefString`: - an entry's own pin must not be passed, because any non-empty pin makes `FastKindCheck` return `KindGit` while the module loader classifies without it — `source = \"./ci\", pin = \"…\"` would otherwise pass as remote and then resolve locally; - `ParseRefString` falls back to *local* on `gitref.EndpointError`, so a vanity-domain remote would be dropped whenever endpoint discovery is unavailable. Classification must not depend on network reachability. Without the drop, an inherited `source = \"modules/ci\"` resolves against the **importing** workspace's config dir — loading a different module, or failing with a path the user never wrote. The integration test asserts exactly that case, with a same-named directory present downstream. ## Lockfile No new lock entry kind. The import resolves through the ordinary `git(url).head` / `.ref(name)` selectors, so it records the standard `git.ref` / `git.head` entry in `dagger.lock`: `dagger update` and `--lock=live` refresh it, `--lock=frozen` replays the pin and fails when it is missing. Resolution runs after the client's workspace is bound, which is what puts the entry in the right lockfile. ## Reads are effective, writes are local `readWorkspaceConfig` — behind module/env/SDK listings and `currentModule.asSDK` — returns the merged config, as does `configRead`, **including its base path**, which is the plain `dagger workspace config` case and returns raw bytes today. `--load-module <name>` resolves imported modules too. The no-argument output is a standalone snapshot: the `import` key is stripped and named in a `# imported from <ref>` comment, the same way the env-effective view already clears `Env`. Keeping it would make the printed config one that inlines the inherited values *and* imports them again underneath. `dagger workspace config import` still reports the local value. Writes never touch the imported config, and now say so when that would be confusing: unsetting a value the import supplies names the import instead of reporting that nothing is set. `dagger uninstall` on a module that exists only in the import says it cannot be uninstalled here; on a source-less override it removes the override and reports that the module is still provided by the import. Installing over a source-less override fills in its source (and clears the pin that belonged to the inherited ref) rather than reporting a conflict. ## Judgment calls worth a second opinion - **Scalar `import` rather than `[import] source = \"…\"`.** One field, the pin lives in `dagger.lock`, and \"basic import\" was the brief — but it is a one-way door if a second field is ever wanted. - **`ModuleEntry.Source` gains `omitempty`**, so the generated JSON schema no longer marks it required. `ValidateEffectiveConfig` replaces that check on the *effective* config and runs whether or not an import is present — stronger than the schema rule it replaces, but it means a config with a source-less module entry, which loads (uselessly) on main today, now fails. Port mappings are deliberately *not* validated: `dagger workspace config ports.3000.backendService web` writes one key while the serializer writes both, so a partial mapping is a state the CLI itself produces. - **Residual sanitization hole, documented not closed**: an imported source that is `KindUnknown`, absent from the imported tree, and present as a directory in the importing tree is kept and resolves locally. It needs a base config whose own entry is already unloadable plus a colliding downstream directory; closing it means threading a \"resolve remotely only\" hint through `moduleSource`, which is a resolution-pipeline change rather than a config feature. - **No tombstones.** An inherited entry can be overridden but not deleted. - **Lock refresh is not covered by a new integration test**: moving the served base branch under the same URL is not expressible with the git-service fixture, and the entry is the ordinary `git.ref` entry whose refresh `lockfile_test.go` already covers. ## Patches | Patch | Scope | | --- | --- | | `workspace-import-config` | `core/workspace`: the key, presence detection, pure merge, `ValidateEffectiveConfig` | | `workspace-import-resolve` | `core`: shared remote-workspace resolution, imported-config loading and sanitization | | `workspace-import-load` | `engine/server`: merge during workspace load | | `workspace-import-reads` | `core/schema`: effective reads, patch-entry-aware install/uninstall | | `workspace-import-tests` | `core/integration`: multi-workspace fixture over a git service | | `workspace-import-docs` | reference page, regenerated schema, changelog, CLI help | Design doc and plan are the first patches in the series.",
          "url": "https://github.com/dagger/dagger/pull/13882",
          "createdAt": "2026-08-12T15:31:30Z",
          "updatedAt": "2026-08-12T16:09:32Z",
          "timestamp": "2026-08-12T16:09:32Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "eunomie",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      },
      {
        "id": "event:621f3d8ebdee61cfa94a",
        "signalId": "github:dagger/dagger:issue:13630",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:issue:13630",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "issue",
          "title": "Java SDK is ready for 1.0",
          "text": "Close this when the Java SDK is fully ready for 1.0 | Action | Status | PR | Comment | |---|---|---|---| | Module init | ✅ Done | — | We can initialize a module without problem with latest beta version. | | Client init | Missing | — | — | | Codegen for both | — | — | — | | Good default template | ✅ Done | https://github.com/dagger/java-sdk/pull/11 | — | | No codegen at runtime | ✅ Done | — | — | | SDK has full test coverage | 🚧 | — | — |",
          "url": "https://github.com/dagger/dagger/issues/13630",
          "createdAt": "2026-07-13T17:37:51Z",
          "updatedAt": "2026-08-12T16:06:09Z",
          "timestamp": "2026-08-12T16:06:09Z",
          "metrics": {
            "reactions": 0,
            "comments": 1
          },
          "labels": [],
          "author": "shykes",
          "state": "open",
          "assignees": [
            "eunomie"
          ],
          "change": "updated"
        }
      },
      {
        "id": "event:c26ef901a04183d2b7b9",
        "signalId": "github:dagger/dagger:issue:13627",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:issue:13627",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "issue",
          "title": "Typescript SDK ready for 1.0",
          "text": "Close this when the TypeScript SDK is fully ready for 1.0 | Action | Status | PR | Comment | |---|---|---|---| | Module init | ✅ Done | — | We can initialize a module without problem with latest beta version. | | Client init | ✅ Done | https://github.com/dagger/typescript-sdk/pull/9 and https://github.com/dagger/dagger/pull/13646 | Codegen is outside the engine | | Codegen for both | ✅ Done | https://github.com/dagger/typescript-sdk/pull/9 and https://github.com/dagger/dagger/pull/13646 | Merged and works | | Good default template | ✅ Done | https://github.com/dagger/typescript-sdk/pull/10 | Merged | | No codegen at runtime | ✅ Done | https://github.com/dagger/dagger/pull/13621 | CI is green, PR is approved, it is mergeable | | SDK has full test coverage | ✅ Done | https://github.com/dagger/typescript-sdk/pull/14, https://github.com/dagger/sdk-sdk/pull/13 | We still need more tests, it's in progress |",
          "url": "https://github.com/dagger/dagger/issues/13627",
          "createdAt": "2026-07-13T17:36:50Z",
          "updatedAt": "2026-08-12T16:01:19Z",
          "timestamp": "2026-08-12T16:01:19Z",
          "metrics": {
            "reactions": 0,
            "comments": 2
          },
          "labels": [],
          "author": "shykes",
          "state": "open",
          "assignees": [
            "TomChv"
          ],
          "change": "updated"
        }
      },
      {
        "id": "event:b30f80d2fe7a5e26d05d",
        "signalId": "github:dagger/dagger:issue:13626",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:issue:13626",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "issue",
          "title": "Python SDK ready for 1.0",
          "text": "Close this when the Python SDK is fully ready for 1.0 | Action | Status | PR | Comment | |---|---|---|---| | Module init | ✅ Done | — | We can initialize a module without problem with latest beta version. | | Client init | Missing | — | — | | Codegen for both | — | — | — | | Good default template | 🚧 | https://github.com/dagger/python-sdk/pull/10 | — | | No codegen at runtime | ✅ Done | https://github.com/dagger/dagger/pull/13593 | Merged | | SDK has full test coverage | 🚧 | — | — |",
          "url": "https://github.com/dagger/dagger/issues/13626",
          "createdAt": "2026-07-13T17:36:31Z",
          "updatedAt": "2026-08-12T16:01:13Z",
          "timestamp": "2026-08-12T16:01:13Z",
          "metrics": {
            "reactions": 1,
            "comments": 2
          },
          "labels": [],
          "author": "shykes",
          "state": "open",
          "assignees": [
            "eunomie"
          ],
          "change": "updated"
        }
      },
      {
        "id": "event:38017a7765092cb8af08",
        "signalId": "github:dagger/dagger:issue:13631",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:issue:13631",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "issue",
          "title": "PHP SDK is ready for 1.0",
          "text": "Close this when the PHP SDK is fully ready for 1.0 - Module init - Client init - Codegen for both - Good default template - SDK has full test coverage",
          "url": "https://github.com/dagger/dagger/issues/13631",
          "createdAt": "2026-07-13T17:38:43Z",
          "updatedAt": "2026-08-12T16:01:07Z",
          "timestamp": "2026-08-12T16:01:07Z",
          "metrics": {
            "reactions": 0,
            "comments": 1
          },
          "labels": [],
          "author": "shykes",
          "state": "open",
          "assignees": [
            "tiborvass",
            "eunomie"
          ],
          "change": "updated"
        }
      },
      {
        "id": "event:0cf0ea195de1fa3a900a",
        "signalId": "github:dagger/dagger:issue:13628",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:issue:13628",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "issue",
          "title": "Go SDK is ready for 1.0",
          "text": "Close this when the Go SDK is fully ready for 1.0 - Module init - Client init - Codegen for both - Good default template - SDK has full test coverage",
          "url": "https://github.com/dagger/dagger/issues/13628",
          "createdAt": "2026-07-13T17:37:15Z",
          "updatedAt": "2026-08-12T16:01:02Z",
          "timestamp": "2026-08-12T16:01:02Z",
          "metrics": {
            "reactions": 0,
            "comments": 3
          },
          "labels": [],
          "author": "shykes",
          "state": "open",
          "assignees": [
            "grouville"
          ],
          "change": "updated"
        }
      },
      {
        "id": "event:90d892ed368f99ac627c",
        "signalId": "github:dagger/dagger:issue:13629",
        "event": "changed",
        "observedAt": "2026-08-13T13:48:00.446149Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:issue:13629",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "issue",
          "title": "Dang SDK is ready for 1.0",
          "text": "Close this when the Dang SDK is fully ready for 1.0 - Module init - Client init - Codegen for both - Good default template - SDK has full test coverage",
          "url": "https://github.com/dagger/dagger/issues/13629",
          "createdAt": "2026-07-13T17:37:31Z",
          "updatedAt": "2026-08-12T16:00:52Z",
          "timestamp": "2026-08-12T16:00:52Z",
          "metrics": {
            "reactions": 0,
            "comments": 2
          },
          "labels": [],
          "author": "shykes",
          "state": "open",
          "assignees": [
            "vito"
          ],
          "change": "updated"
        }
      },
      {
        "id": "event:a83d9cd224ad48e73733",
        "signalId": "github:dagger/dagger:pull_request:13892",
        "event": "discovered",
        "observedAt": "2026-08-13T16:19:22.035158Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13892",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "fix(setup): anchor migrated as-sdk module paths at the workspace root",
          "text": "## What `dagger.toml` carries two path conventions that only coincide when the config file sits at the workspace root: - `[modules.<name>].source` resolves against the **config directory** (`ResolveModuleEntrySource`). - `[[modules.<sdk>.as-sdk.modules]].path` is **workspace-root-relative**, everywhere it is read: module→SDK ownership (`sdkOwnersByModulePathFromConfig`), uninstall (`removeSDKManagedModuleReference`), the workspace SDK listing (`workspaceSDKFromEntry`), and `currentModule.asSDK.modules`, which the SDKs themselves consume (including the out-of-repo `dagger/go-sdk`). The migration writer disagreed. `workspaceMigrationInstallDiscoveredModuleSDKs` recorded a discovered local module with `filepath.Rel(owner, projectRoot)` — relative to the config that owns it, not to the workspace root. This patch anchors it at the workspace root via the existing `workspaceMigrationProjectRootRelPath` helper, and states the anchor on the config types (plus the regenerated public JSON schema). ## Is anything broken today? No — and the change is deliberately framed as hardening rather than a user-visible fix. Migration plans every workspace config at `ws.HostPath()`: `PlanMigration` sets `plan.ProjectRoot` to the workspace root when hoisted, and otherwise the project root *is* the workspace root; synthesized parent plans land at the workspace root too. So `owner` always equals the workspace root and the two anchors produce the same string. The mismatch only bites the day migration can plan a config below the workspace root — which the `FIXME(workspace-migrate)` about scanning for legacy child `dagger.json` files points straight at. Fixing the writer rather than the readers is the small, safe side of the mismatch: the readers' contract is depended on by SDKs living in other repos. ## Tests - `core/schema` unit test pinning the anchor for a config planned below the workspace root. It is the only test that fails against the old writer; its comment is explicit that the shape is not reachable through `dagger setup` today. - `core/integration` assertions on the existing subdirectory-toolchains migration: the discovered toolchain is recorded by a workspace-root-relative path, and `dagger uninstall` resolves that path back to its as-sdk entry — end-to-end coverage of the contract in the monorepo layout.",
          "url": "https://github.com/dagger/dagger/pull/13892",
          "createdAt": "2026-08-13T15:44:40Z",
          "updatedAt": "2026-08-13T16:17:10Z",
          "timestamp": "2026-08-13T16:17:10Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "eunomie",
          "state": "open",
          "assignees": [],
          "change": "new"
        }
      },
      {
        "id": "event:85e9c2068de2908cf915",
        "signalId": "github:dagger/dagger:pull_request:13890",
        "event": "discovered",
        "observedAt": "2026-08-13T16:19:22.035158Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13890",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "fix: make `dagger module init` work from a subdirectory workspace",
          "text": "# fix: make `dagger module init` work from a subdirectory workspace Fixes #13889. ## The bug With a `dagger.toml` in a subdirectory of the git repo — the monorepo layout, several projects under one root — `dagger module init` fails outright: ```console $ mkdir repo && cd repo && git init && mkdir common && touch common/dagger.toml $ cd common && dagger sdk install go $ dagger module init go hello -y Error: sdk module init: failed to call sdk module initModule: workspace path .dagger/modules/hello is outside changeset root common ``` And the workaround that avoids the error is worse: `--path common/hello` writes the engine's `dagger-module.toml` to `common/hello/` while the SDK's `main.go` lands in `hello/` — a split, non-loadable module, with no error at all. ## Root cause Two path conventions disagree once the caller is not standing at the workspace root, which a subdirectory `dagger.toml` guarantees: workspace root detection walks up to `.git` and a `dagger.toml` does not define the root, so selecting `common/dagger.toml` always means root = git root and `Workspace.cwd = common`. 1. **The engine resolves init paths workspace-root-relative and applies the changeset at the workspace root.** `Workspace.export` writes to `ExportHostPath()`, which is the workspace root. Self-consistent. 2. **An SDK's `initModule` stages files relative to `Workspace.cwd`.** The polyfill's workspace fork (`github.com/dagger/polyfill`, `workspace-fork.dang` → `clientPath`) rebases a root-relative path onto the cwd and raises when the path is not under it, because a changeset returned from an ordinary function call *is* applied at the caller's cwd (`changeset.Export(ctx, \".\")` resolves against the client's OS cwd). The polyfill is right for the second apply base and wrong for the first, and the engine is the layer that both chooses the workspace it shows the SDK and calls `Workspace.export` at the root. So the reconciliation belongs here. This is a founding assumption rather than a regression: the cwd rebasing arrived with the polyfill's root commit (`dagger/sdk-sdk` `732835f`, \"Add SDK development polyfill module\", 2026-05-18, later spun out to `github.com/dagger/polyfill`); the later \"read cwd from Workspace.cwd\" only changed where the cwd came from, not the semantics. Monorepo layouts are simply the first thing to exercise a non-root cwd through init. ## The fix (engine only — no SDK or polyfill change) **Hand the SDK a root-anchored workspace during init.** New `rootAnchoredWorkspace` in `core/schema/workspace_sdk_init.go` returns the workspace with `cwd = \".\"` (built through the `withWorkdir` field so the result stays an attached dagql result, the same way `scopedStagedWorkspace` does it for generators). `initModuleChanges` and `initClientChanges` pass that to the SDK instead of the caller's workspace, so the cwd the SDK sees matches where the changeset actually lands, and the polyfill's rebasing becomes the no-op it already is at the root. Nothing about the generic `changeset.Export(\".\")` path — `dagger generate` included — changes. **Anchor the default module path at the `dagger.toml` being edited.** `.dagger/modules/<name>` was joined against the workspace root. Since `[modules.<name>].source` is resolved against the config directory, a root-anchored default under `common/dagger.toml` would have recorded an entry pointing outside its own project. The default is now `<config dir>/.dagger/modules/<name>` and the recorded source is made config-dir-relative with `filepath.Rel`, the same conversion `dagger install` already performs for a local ref. At the workspace root — every existing workspace — both are unchanged. `--path` keeps its documented workspace-root-relative meaning; only the split it used to produce is fixed. The CLI help and the Go SDK page now say so explicitly — `--path ci` from a subdirectory means `<workspace root>/ci`, not `./ci` — and the generated CLI reference is updated to match. ## Verification New engine integration test `TestGenerators/TestInitFromSubdirectoryWorkspace`, against a fixture SDK whose `initModule`/`initClient` mirror the polyfill's cwd rebasing (so the test reproduces the real failure, not a synthetic one): - `dagger module init` from `common/` puts `dagger-module.toml`, the SDK scaffold and the generator output together under `common/.dagger/modules/newmod/`, records `source = \".dagger/modules/newmod\"` and `path = \"common/.dagger/modules/newmod\"`, and writes nothing at the git root. - `--path common/hello` from `common/` puts both files under `common/hello/` and nothing at `hello/`. - `--path tools/hello` from `common/` — a path *outside* the caller's cwd — puts both files under `tools/`, which is what separates root-anchoring from a cwd-scoped reroot. - `dagger api client init` from `common/` scaffolds at the requested path. Confirmed the test fails on the unfixed engine with the reported `outside changeset root` error, and passes with the fix. The existing root-workspace init tests (`TestModuleInitGeneratesForNewModuleOnly`, `TestAPIClientInitGeneratesForNewClientOnly`) still pass unchanged. ## Noted, not fixed Two adjacent inconsistencies found while tracing this, both out of scope here: - `dagger api client init` cannot take a workspace-root-relative module ref that contains a `.` segment (e.g. `common/.dagger/init-fixture`): `workspace.IsLocalRef` classifies it as remote. Unrelated to cwd; the test fixture sidesteps it by living at a dot-free path. - `workspace.AddMigratedModuleSDK` (used by `dagger setup`) writes `as-sdk.modules[].path` relative to the owning config directory, while every consumer — `sdkOwnersByModulePath`, `removeSDKManagedModuleReference`, and the Go SDK's `modules(ws)` — reads it workspace-root-relative. Identical only when the config is at the root.",
          "url": "https://github.com/dagger/dagger/pull/13890",
          "createdAt": "2026-08-13T14:55:18Z",
          "updatedAt": "2026-08-13T16:14:53Z",
          "timestamp": "2026-08-13T16:14:53Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "eunomie",
          "state": "open",
          "assignees": [],
          "change": "new"
        }
      },
      {
        "id": "event:df514da6a7f2288fd370",
        "signalId": "github:dagger/dagger:pull_request:13891",
        "event": "discovered",
        "observedAt": "2026-08-13T16:19:22.035158Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13891",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "ci: unpin helm from the retired wolfi helm 3.x package",
          "text": "> [!NOTE] > This unblocks CI repo-wide. `main` and every open PR are currently red. Chainguard moved the wolfi `helm` apk package to the 4.x line, so every `helm~3.18.4` constraint fails to resolve: ``` ERROR: unable to select packages: helm-4-4.2.3-r1: breaks: world[helm~3.18.4] ``` That breaks `ci:bootstrap`, `golang:test-all` (via `e2e/helm`), `helm:lint` and `helm:assert-template`. No repo code is at fault — reproduced with a bare `apk add`. Available wolfi packages are now `helm-4.0.1-r0` (bare `helm`), `helm-3-3.19.2-r2`, `helm-4-4.2.3-r1`. ## Fix - The paths that **package/ship** our chart move to the versioned package so they stay on Helm 3: `helm~3.18.4` → `helm-3~3.19.2` in `.dagger/modules/release/helm.go` and `e2e/helm/helm_test.go`. - `helm:lint` / `helm:assert-template` don't come from those pins — they come from the external `github.com/dagger/helm` module, which hardcodes `apk add \"helm~\" + version`, so 3.x is unselectable there without an upstream change. Its `[modules.helm.settings] version` is set to `4.0.1` (verified it lints/templates our chart fine). Follow-up (not this PR): teach `github.com/dagger/helm` to select the `helm-3` package so the checker can go back to 3.x. ## Verified locally (dev engine) - `helm lint` and `helm assert-template` → PASS (were ERROR). - `e2e/helm`: `TestCustomProbes`, `TestPackageDryRun`, `TestInstallK3S` (all 4 subtests: default/port daemonset, default/port statefulset) → PASS. - `helm-3~3.19.2` install → `helm version` v3.19.2; `helm package .` → `dagger-helm-1.0.0-beta.10.tgz` OK. `docs/versioned_docs/version-0.21.7/modules/helm.mdx` still references `3.18.4` and is left untouched — versioned docs are frozen snapshots.",
          "url": "https://github.com/dagger/dagger/pull/13891",
          "createdAt": "2026-08-13T15:35:09Z",
          "updatedAt": "2026-08-13T16:11:27Z",
          "timestamp": "2026-08-13T16:11:27Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "eunomie",
          "state": "closed",
          "assignees": [],
          "change": "new"
        }
      },
      {
        "id": "event:f7100c42085dfe4bb2c3",
        "signalId": "github:dagger/dagger:pull_request:13893",
        "event": "discovered",
        "observedAt": "2026-08-13T17:43:20.785491Z",
        "changedFields": [],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13893",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "fix: classify dotted workspace-relative module refs as local",
          "text": "# fix: classify dotted workspace-relative module refs as local ## Problem `dagger api client init <sdk> <client-path> <module-ref>` fails when the module ref is a workspace-root-relative path whose dot segment is not the first one: ```console $ dagger api client init go clients/api common/.dagger/mymod load module source: local path \"common/.dagger/mymod\" does not exist ``` This is the everyday shape in a monorepo: a module that is `.dagger/mymod` from the workspace root is `common/.dagger/mymod` from a subdirectory, and module refs are recorded relative to the workspace root. Prefixing `./` does not help — `resolveWorkspaceClientModuleRef` hands the *cleaned* form to the loader, so the `./` is gone before the ref is classified again. ## Cause `core/workspace.IsLocalRef` recognised a local ref by a leading `.` or `/`, and otherwise by the *absence* of a dot anywhere in the string — a proxy for \"this looks like a hostname\". `common/.dagger/mymod` has neither, so it was classified as a git ref and `resolveClientTargetModule` routed it to the git loader instead of resolving it against the workspace rootfs. The heuristic is shared well beyond client init (config parsing, install, uninstall, SDK init, migration), so it is fixed once, in place. ## Fix `IsLocalRef` moves to its own file and delegates to `core/gitref.FastKindCheck`, which already handles pins, leading `.`/`/`/`..`, and the `http`/`https`/`ssh` schemes, and returns \"unknown\" for the ambiguous dotted case. That case is now resolved by looking for a host only ahead of the first slash — the Go module path convention, where only the first element can be a domain. Two markers keep dot-free hosts remote: a colon (scp-like `git@internal:org/repo.git`, or a `host:port`) and a `/_git/` segment (Azure DevOps Server on-prem, per the matcher in `engine/vcs`). Nothing that was correctly classified local becomes remote: the refs that change are those with a dot-free, colon-free, `_git`-free host and a dot further down the path, which become local. One shape goes the other way, and is a fix too — a scheme-prefixed dot-free host (`ssh://internal/org/repo`) used to read as local because the string had no dot at all; `FastKindCheck` now calls it git, which is what it is. ## Converging the copies The heuristic had been written out four times, and the copies had drifted: | Implementation | Ambiguous-case rule | | | --- | --- | --- | | `core/gitref.FastKindCheck` | returns \"unknown\" by design | the primitive, kept | | `core/workspace.IsLocalRef` | whole-string dot | **fixed here** | | `core/schema.isLocalLegacyModuleRef` | whole-string dot (verbatim copy) | **deleted** | | `daggercmd.workspaceAddressLooksRemote` | whole-string dot | **same bug, fixed here** | | `daggercmd.isObviouslyRemoteWorkspaceRef` | first-segment dot | already correct | `isLocalLegacyModuleRef` drove `dagger update` for legacy `dagger.json` toolchains and blueprints. `workspaceAddressLooksRemote` drove `-W` workspace addresses, where it had the identical bug: a workspace at `services/api.v2` or `common/.dagger/mymod` read as a git ref. `isObviouslyRemoteWorkspaceRef` is worth calling out — it already looked for the dot in the first path segment only, which is exactly the rule `IsLocalRef` now adopts. The convention was already in the tree; this PR just makes it the only one. All four now route through `IsLocalRef`, each keeping only what is genuinely its own: `file://` stripping for a workspace address, and the host stat plus a \"needs a slash\" conservatism for an obviously-remote ref. Untouched on purpose: `auth/registry.go` classifies OCI registry domains under Docker's rules (`localhost`, `:port`), and `engine/vcs` validates hosts inside the VCS matcher. Different questions, deliberately separate. ### Known limitation (pre-existing, unchanged) A local module directory whose *own first path segment* carries the dot — `./mymod.v2` at the workspace root — is still classified as a git ref. The `./` is cleaned away before the ref is classified, and the cleaned form is what gets persisted as `as-sdk.clients.module`, so by the time a client is loaded again the evidence of local-ness is gone. Fixing it means persisting the disambiguating `./` rather than plumbing a kind hint — both `init` and every later load already share the same resolution seam. That is a config-format change, so it is left as a follow-up. Keep such a module under a dot-free first segment, or spell dot-free remote hosts with an explicit `ssh://`/`https://` scheme. Windows-style local paths are equally unchanged: `C:/src/mymod.git` still reads as remote, because the drive letter's colon is indistinguishable from scp-like syntax. It read as remote before this change too, for the dot. ## Tests - `core/workspace/localref_test.go` — table-driven matrix over the classifier: dotted-but-local paths, dot-free-but-remote-looking refs, leading `./`/`../`, absolute paths, `github.com/...` with and without `@version`, scp-style refs (dotted and dot-free hosts), `host:port`, both Azure DevOps shapes, and pins. The dotted-below-first-segment cases fail against the old implementation. - `TestGenerators/TestAPIClientInitDottedModulePath` — `dagger api client init` run from a workspace subdirectory against a module at `common/.dagger/target`, for both the bare and `./`-prefixed ref, asserting the scaffold, the generated client and the recorded config ref. Without the fix both subtests fail with the exact error above. - `internal/cmd/dagger` — `TestWorkspaceAddressLooksRemote` gains the dotted workspace-address cases, and `TestIsObviouslyRemoteWorkspaceRef` is new, pinning the \"needs a slash\" conservatism that keeps a lone `my.dir` local.",
          "url": "https://github.com/dagger/dagger/pull/13893",
          "createdAt": "2026-08-13T16:22:02Z",
          "updatedAt": "2026-08-13T16:48:12Z",
          "timestamp": "2026-08-13T16:48:12Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "eunomie",
          "state": "open",
          "assignees": [],
          "change": "new"
        }
      },
      {
        "id": "event:c30e12611aa32a4012bb",
        "signalId": "github:dagger/dagger:pull_request:13892",
        "event": "changed",
        "observedAt": "2026-08-13T17:43:20.785491Z",
        "changedFields": [
          "updatedAt"
        ],
        "signal": {
          "id": "github:dagger/dagger:pull_request:13892",
          "source": "github",
          "group": "developer-infrastructure",
          "project": "dagger/dagger",
          "kind": "pull_request",
          "title": "fix(setup): anchor migrated as-sdk module paths at the workspace root",
          "text": "## What `dagger.toml` carries two path conventions that only coincide when the config file sits at the workspace root: - `[modules.<name>].source` resolves against the **config directory** (`ResolveModuleEntrySource`). - `[[modules.<sdk>.as-sdk.modules]].path` is **workspace-root-relative**, everywhere it is read: module→SDK ownership (`sdkOwnersByModulePathFromConfig`), uninstall (`removeSDKManagedModuleReference`), the workspace SDK listing (`workspaceSDKFromEntry`), and `currentModule.asSDK.modules`, which the SDKs themselves consume (including the out-of-repo `dagger/go-sdk`). The migration writer disagreed. `workspaceMigrationInstallDiscoveredModuleSDKs` recorded a discovered local module with `filepath.Rel(owner, projectRoot)` — relative to the config that owns it, not to the workspace root. This patch anchors it at the workspace root via the existing `workspaceMigrationProjectRootRelPath` helper, and states the anchor on the config types (plus the regenerated public JSON schema). ## Is anything broken today? No — and the change is deliberately framed as hardening rather than a user-visible fix. Migration plans every workspace config at `ws.HostPath()`: `PlanMigration` sets `plan.ProjectRoot` to the workspace root when hoisted, and otherwise the project root *is* the workspace root; synthesized parent plans land at the workspace root too. So `owner` always equals the workspace root and the two anchors produce the same string. The mismatch only bites the day migration can plan a config below the workspace root — which the `FIXME(workspace-migrate)` about scanning for legacy child `dagger.json` files points straight at. Fixing the writer rather than the readers is the small, safe side of the mismatch: the readers' contract is depended on by SDKs living in other repos. ## Tests - `core/schema` unit test pinning the anchor for a config planned below the workspace root. It is the only test that fails against the old writer; its comment is explicit that the shape is not reachable through `dagger setup` today. - `core/integration` assertions on the existing subdirectory-toolchains migration: the discovered toolchain is recorded by a workspace-root-relative path, and `dagger uninstall` resolves that path back to its as-sdk entry — end-to-end coverage of the contract in the monorepo layout.",
          "url": "https://github.com/dagger/dagger/pull/13892",
          "createdAt": "2026-08-13T15:44:40Z",
          "updatedAt": "2026-08-13T16:22:51Z",
          "timestamp": "2026-08-13T16:22:51Z",
          "metrics": {
            "reactions": 0,
            "comments": 0
          },
          "labels": [],
          "author": "eunomie",
          "state": "open",
          "assignees": [],
          "change": "updated"
        }
      }
    ]
  }
}
