Skip to content

feat!: Effect-native core on Effect 4.0.0 and remove the legacy runtime - #211

Open
chenxin-yan wants to merge 2 commits into
docs/effect-native-170-contractfrom
feat/effect-native-core
Open

chenxin-yan wants to merge 2 commits into
docs/effect-native-170-contractfrom
feat/effect-native-core

Conversation

@chenxin-yan

@chenxin-yan chenxin-yan commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Replaces #206, #208 and #209. Stacked on #207 → #205.

Moves Mitome onto the stable Effect 4.0.0 release and the new Effect-native execution core, and deletes the legacy runtime. Pre-release, so there's no compatibility layer.

Delivers #181, #182 and #183, and the SQLite qualification part of #170. The CLI (#184) and TUI (#185) are rebuilt next on this core.

Commits

  1. feat!: run native Sessions, Turns and controlled Tool Steps on Effect 4.0.0
    • Effect 4.0.0 pins and the approved TypeScript 7.0.2 + @effect/tsgo compiler.
    • makeSession/Turn: whole-function Turns that commit staged history once, with owned cleanup and live queries.
    • step/loop/localTools: serial native Tools, fail-closed authorization, incomplete-response handling, generation/dispatch limits and physical request accounting.
    • Provider support for openai, openai-compatible and openai-codex.
    • Qualification fixtures under tools/qualification.
    • The legacy packages don't build at this commit.
  2. refactor!: remove the legacy runtime
    • Deletes @mitome/sdk, @mitome/channels and @mitome/tui, plus the Agent Definition, Extension, Hook, Host and old store code in Core.
    • The CLI keeps only --help/--version.
    • Provider tests are ported to providerModel + LanguageModel.
    • create-mitome scaffolds a native program, and the release fixtures smoke-test the native packed exports.

Merge with "Create a merge commit" so the stack's commit SHAs carry through. Only the merged result builds green; commit 1 alone doesn't.

No local dependency patches

Effect and @effect/* are used exactly as published at 4.0.0. There is no patches/ directory and no patchedDependencies. Earlier drafts patched three Effect modules; those patches were removed by owner decision, and Mitome accepts upstream behavior:

  • When a Tool handler fails with a typed error and its cleanup crashes or is interrupted, Effect's Toolkit keeps only the typed error. A return-mode Tool's failure is not part of the Step's error type.
  • @effect/ai-openai does not validate the Responses status, unfinished item statuses or computer_call items. An incomplete response without a reason, or one stopped by max_output_tokens, is not reported as incomplete.
  • The OpenAI and OpenAI-compatible bindings re-encode Tool parameters through the application's parameter schema. The Codex provider is Mitome's own code and keeps its stricter checks.

Tests that only covered the patched behavior were removed (4 core, 51 provider cases). See the dated note in docs/research/native-baseline-qualification.md.

Verification (local, Bun 1.4.0 / Node 26.7.0)

  • bun run build -- --force, bun run check -- --force and bun run test -- --force all exit 0:
    • core 88 tests, providers 381, cli 2, create-mitome 11, scripts 7.
    • The release tarball/install fixtures pass, including the packed native smoke.
  • tools/qualification checks pass: native baseline, and SQLite on Node and Bun.

Not covered: the scaffold's runtime node index.ts needs credentials, so only its typecheck is gated. The CLI binary release-artifact path wasn't exercised locally.

Review in cubic

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c04a855

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@mitome/providers Minor
@mitome/core Minor
create-mitome Minor
@mitome/cli Minor
@mitome/cli-darwin-arm64 Minor
@mitome/cli-darwin-x64 Minor
@mitome/cli-linux-arm64 Minor
@mitome/cli-linux-arm64-musl Minor
@mitome/cli-linux-x64 Minor
@mitome/cli-linux-x64-musl Minor
@mitome/cli-win32-arm64 Minor
@mitome/cli-win32-x64 Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

21 issues found across 204 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/docs/content/docs/contributing.mdx">

<violation number="1" location="apps/docs/content/docs/contributing.mdx:11">
P3: The `Manual TUI check` section at the end of this file still instructs contributors to run the TUI in a real terminal before each release, but this PR deletes `packages/tui` entirely, so that check cannot be performed. Remove the section and its heading together with the TUI references this change strips out.</violation>
</file>

<file name="scripts/dev-cli.ts">

<violation number="1" location="scripts/dev-cli.ts:9">
P2: The documented `bun run dev:cli init` bootstrap no longer works because this wrapper forwards `init` to the help/version-only CLI, which rejects it. Update ADR-0030 and remove the obsolete sandbox-bootstrap contract, or preserve an explicit bootstrap path.</violation>
</file>

<file name="packages/providers/src/openai-codex/request.ts">

<violation number="1" location="packages/providers/src/openai-codex/request.ts:91">
P1: The optional default Tool choice is now rejected for Codex. Treat `undefined` as the native `"auto"` default, otherwise callers that omit `toolChoice` cannot generate at all.</violation>
</file>

<file name="packages/create-mitome/src/template.ts">

<violation number="1" location="packages/create-mitome/src/template.ts:112">
P2: The Codex scaffold cannot authenticate by following this README: `login` is only a function requiring `configDirectory`, `input`, and `output`, and the generated project provides no caller or auth command. Add a runnable login step or generate an authentication helper before directing users to `node index.ts`.</violation>
</file>

<file name="patches/@effect%2Fai-openai-compat@4.0.0.patch">

<violation number="1" location="patches/@effect%2Fai-openai-compat@4.0.0.patch:9">
P3: This patch changes the generated `dist/OpenAiLanguageModel.js` line layout without updating its source map. Debugger stack locations after this hunk can therefore resolve to the wrong TypeScript lines; regenerate and include `OpenAiLanguageModel.js.map` with the patch.</violation>
</file>

<file name="packages/core/src/provider.ts">

<violation number="1" location="packages/core/src/provider.ts:150">
P2: `providerModel` returns every service produced by the provider layer, exposing provider internals despite its contract to expose only the model and accounting. Return a fresh context containing only those two services.</violation>
</file>

<file name="tools/qualification/sqlite-transactions/checks.ts">

<violation number="1" location="tools/qualification/sqlite-transactions/checks.ts:96">
P2: The peer and temporary directory are created before the cleanup guard, so a module-resolution or identity-check failure leaks the temporary SQLite directory. Move setup and execution under a `try/finally` immediately after creating the peer.</violation>

<violation number="2" location="tools/qualification/sqlite-transactions/checks.ts:213">
P2: `Effect.scoped(sql.reserve)` releases the connection before the injected method and later transactions run, so this test depends on the pool reusing the same connection. Keep the reservation scope around the entire synthetic check to guarantee that `withTransaction` uses `conn`.</violation>
</file>

<file name="docs/research/session-turn-contract-proof.md">

<violation number="1" location="docs/research/session-turn-contract-proof.md:103">
P3: This paragraph says the current pins are rc.118, while this checkout uses Effect and the SQLite adapters at 4.0.0; the plan also names rc.117 patches. Update the version references to match the artifacts qualified by this PR.</violation>

<violation number="2" location="docs/research/session-turn-contract-proof.md:105">
P2: This new section contradicts retained statements in the same file and leaves the document self-inconsistent. It says the owner approved a temporary SQLite patch and that root devDependencies were pinned, but the final "Selected decisions and next proof" section still states "repository dependencies remain unchanged. No backend/schema selection, repository dependency change, fix contribution, local workaround, or production migration is authorized," and the ownership-revalidation section (line ~62) still states "repository dependencies remain rc.108." Update those stale statements to match the newly documented dependency/patch state.</violation>
</file>

<file name="packages/core/src/session/step.ts">

<violation number="1" location="packages/core/src/session/step.ts:397">
P1: The authorization boundary does not fail closed for malformed callback results: only an exact `"deny"` blocks policy, while invalid truthy consent or authority values are accepted. Validate policy results against `"allow" | "ask" | "deny"` and require consent/authority results to equal `true` before dispatch.</violation>
</file>

<file name="packages/providers/src/openai-codex/sse.ts">

<violation number="1" location="packages/providers/src/openai-codex/sse.ts:244">
P2: This locator check rejects an item when the stream switches from an item-id locator to an output-index locator, even though both identify the same item. Adopt the index when it is absent from `byIndex`, and only reject it when it already belongs to another item; otherwise valid mixed-key streams fail with `InvalidOutputError`.</violation>
</file>

<file name=".changeset/effect-4-pin.md">

<violation number="1" location=".changeset/effect-4-pin.md:4">
P3: `create-mitome` is marked `patch` while `@mitome/core` and `@mitome/providers` are `minor`, but all three are in one `fixed` group in `.changeset/config.json`, so changesets cuts them at the same version: create-mitome will be released at the group's minor (0.1.0), not 0.0.1, and its changelog for this changeset will show a bump that never happens. Mark it `minor` to match the group, or drop the `patch` entry here since the group bump governs it.</violation>
</file>

<file name="packages/core/src/session/session.ts">

<violation number="1" location="packages/core/src/session/session.ts:308">
P2: Snapshot the validated execution bounds when `make` creates the Session. Reading the caller-owned `limits` object here lets later mutations raise or lower limits for live Sessions, contrary to the documented per-Session policy.</violation>
</file>

<file name="docs/research/native-baseline-qualification.md">

<violation number="1" location="docs/research/native-baseline-qualification.md:3">
P2: The "current status" line says the catalog and SQLite adapters are pinned to `4.0.0-rc.118`, but the merged result pins stable `4.0.0`: root `package.json` catalog/devDependencies (`effect`, `@effect/ai-openai`, `@effect/ai-openai-compat`, `@effect/platform-bun`, `@effect/vitest`, `@effect/sql-sqlite-node`, `@effect/sql-sqlite-bun`), `patchedDependencies`, and `patches/` all use the `@4.0.0` variants (`effect@4.0.0.patch`, `@effect/ai-openai@4.0.0.patch`, `@effect/ai-openai-compat@4.0.0.patch`); the fixture header also says "installed Effect 4.0.0 (with the retained patches/effect@4.0.0.patch)". Update the status to the 4.0.0 qualification and demote the "Installed rc.118 upgrade" section to dated history so this record matches the repository and the PR's own change.</violation>

<violation number="2" location="docs/research/native-baseline-qualification.md:261">
P3: This reproduction only works on the author's workstation: the `/home/cyan/.../run-v2.sh` runner is not in the repository. Check in the runner and its inputs or provide a checkout-relative reproduction.</violation>
</file>

<file name="packages/core/test/session/step-outcomes.test.ts">

<violation number="1" location="packages/core/test/session/step-outcomes.test.ts:138">
P3: The third script entry `[text("z")]` is never consumed: this test calls `step` only twice (one with a policy, one without), and each `step` performs exactly one generation (`admitted` calls `model.generateText` once), so only the first two generations run. The never-dispatched text generation is dead test data; remove it, or add the third `step` if the intent was to assert that a generation with no Tool Calls records nothing.</violation>
</file>

<file name="packages/providers/test/openai-codex/codex.test.ts">

<violation number="1" location="packages/providers/test/openai-codex/codex.test.ts:191">
P3: The rewritten end-to-end test drops the `response.output_item.added`/`.done` events with an unknown `future_item` type that the old test injected mid-stream. That was the only remaining coverage that a future/unknown Codex item type is silently skipped without corrupting the part stream, and `sse.test.ts` never feeds an unknown item type through `output_item.added`/`.done` (it only uses message/function_call/reasoning items). Since the parser was rebuilt in this PR and Codex is a best-effort unofficial API, keep the unknown-item events in this SSE fixture so the skip path stays covered end to end.</violation>
</file>

<file name="packages/core/test/session/step-lifetime.test.ts">

<violation number="1" location="packages/core/test/session/step-lifetime.test.ts:136">
P3: This assertion treats any Exit failure as success, but an interrupted parked fiber is also an `Exit.failure`, so the test would still pass if the fiber was torn down by scope teardown instead of dying on staging into the already-ended Turn. The point of the test is that the late response cannot stage into the closed Turn, so assert that exact die, as the first test block does, with `defect(outcome.exit)`.</violation>
</file>

<file name="packages/core/test/session/step.test.ts">

<violation number="1" location="packages/core/test/session/step.test.ts:821">
P3: These busy-wait loops spin `Effect.yieldNow` until `write:a:start` appears; if a regression stops the handler from ever starting, the test spins until the runner timeout instead of failing on the actual mismatch (gly letter. Same pattern at line 948). Bound the wait so a regression fails fast with the real assertion error.</violation>
</file>

<file name="docs/plans/effect-native-library.md">

<violation number="1" location="docs/plans/effect-native-library.md:659">
P3: This PR pins and patches the stable Effect `4.0.0` releases (changeset `effect-4-pin.md` moves off `4.0.0-rc.108`; bun.lock resolves `@effect/ai-openai@4.0.0` and `@effect/ai-openai-compat@4.0.0`, and the patch artifacts are named `patches/*@4.0.0.patch`), but the added plan text still identifies the local patches as patches of `4.0.0-rc.117` packages (also in the new `#183` paragraph: 'A development-only patch of `@effect/ai-openai@4.0.0-rc.117`' and 'patches, of `effect@4.0.0-rc.117` and `@effect/ai-openai-compat@4.0.0-rc.117`'). Align the version references in the added text with the pinned 4.0.0 packages so the release-blocking claim points at the packages that actually carry the patches; the sql adapters are pinned `4.0.0` as well and no SQLite adapter patch file exists under `patches/`.</violation>
</file>

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Re-trigger cubic

): "auto" | "none" | undefined => {
if (choice === "auto" || choice === "none") return choice;
if (Predicate.isObject(choice) && "oneOf" in choice && choice.mode !== "required") return "auto";
return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The optional default Tool choice is now rejected for Codex. Treat undefined as the native "auto" default, otherwise callers that omit toolChoice cannot generate at all.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/providers/src/openai-codex/request.ts, line 91:

<comment>The optional default Tool choice is now rejected for Codex. Treat `undefined` as the native `"auto"` default, otherwise callers that omit `toolChoice` cannot generate at all.</comment>

<file context>
@@ -61,10 +61,41 @@ const contentFor = (
+): "auto" | "none" | undefined => {
+  if (choice === "auto" || choice === "none") return choice;
+  if (Predicate.isObject(choice) && "oneOf" in choice && choice.mode !== "required") return "auto";
+  return undefined;
+};
+
</file context>
Suggested change
return undefined;
return choice === undefined ? "auto" : undefined;

: withServices(Effect.suspend(() => policy(request))),
"deny",
);
if (decision === "deny") return yield* denial("Tool call was denied by policy");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The authorization boundary does not fail closed for malformed callback results: only an exact "deny" blocks policy, while invalid truthy consent or authority values are accepted. Validate policy results against "allow" | "ask" | "deny" and require consent/authority results to equal true before dispatch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/session/step.ts, line 397:

<comment>The authorization boundary does not fail closed for malformed callback results: only an exact `"deny"` blocks policy, while invalid truthy consent or authority values are accepted. Validate policy results against `"allow" | "ask" | "deny"` and require consent/authority results to equal `true` before dispatch.</comment>

<file context>
@@ -0,0 +1,618 @@
+            : withServices(Effect.suspend(() => policy(request))),
+          "deny",
+        );
+        if (decision === "deny") return yield* denial("Tool call was denied by policy");
+        if (decision === "ask") {
+          const granted =
</file context>

Comment thread scripts/dev-cli.ts
"--output-logs=errors-only",
],
const child = Bun.spawn(
["bun", join(root, "packages", "cli", "src", "index.ts"), ...process.argv.slice(2)],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The documented bun run dev:cli init bootstrap no longer works because this wrapper forwards init to the help/version-only CLI, which rejects it. Update ADR-0030 and remove the obsolete sandbox-bootstrap contract, or preserve an explicit bootstrap path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/dev-cli.ts, line 9:

<comment>The documented `bun run dev:cli init` bootstrap no longer works because this wrapper forwards `init` to the help/version-only CLI, which rejects it. Update ADR-0030 and remove the obsolete sandbox-bootstrap contract, or preserve an explicit bootstrap path.</comment>

<file context>
@@ -1,101 +1,18 @@
-    "--output-logs=errors-only",
-  ],
+const child = Bun.spawn(
+  ["bun", join(root, "packages", "cli", "src", "index.ts"), ...process.argv.slice(2)],
   {
-    cwd: root,
</file context>

const credential =
provider === "openai"
? "Set `OPENAI_API_KEY`."
: "Store a ChatGPT Codex Credential with the `login` export of `@mitome/providers/openai-codex`.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Codex scaffold cannot authenticate by following this README: login is only a function requiring configDirectory, input, and output, and the generated project provides no caller or auth command. Add a runnable login step or generate an authentication helper before directing users to node index.ts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/create-mitome/src/template.ts, line 112:

<comment>The Codex scaffold cannot authenticate by following this README: `login` is only a function requiring `configDirectory`, `input`, and `output`, and the generated project provides no caller or auth command. Add a runnable login step or generate an authentication helper before directing users to `node index.ts`.</comment>

<file context>
@@ -94,42 +105,21 @@ const tsconfigSource = `${JSON.stringify(
+  const credential =
+    provider === "openai"
+      ? "Set `OPENAI_API_KEY`."
+      : "Store a ChatGPT Codex Credential with the `login` export of `@mitome/providers/openai-codex`.";
+  return `# Mitome Agent\n\n## Next steps\n\n${credential}\n\n\`\`\`sh\nnpm install\nnode index.ts\n\`\`\`\n`;
 };
</file context>

detail: `Provider "${provider.id}" does not account for requests of Model "${modelId}"`,
});
}
return Context.add(context, ModelRequestAccounting, accounting.value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: providerModel returns every service produced by the provider layer, exposing provider internals despite its contract to expose only the model and accounting. Return a fresh context containing only those two services.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/provider.ts, line 150:

<comment>`providerModel` returns every service produced by the provider layer, exposing provider internals despite its contract to expose only the model and accounting. Return a fresh context containing only those two services.</comment>

<file context>
@@ -69,19 +91,65 @@ export const makeProvider = <const Id extends string, const ModelIds extends Rea
+          detail: `Provider "${provider.id}" does not account for requests of Model "${modelId}"`,
+        });
+      }
+      return Context.add(context, ModelRequestAccounting, accounting.value);
+    }),
+  );
</file context>
Suggested change
return Context.add(context, ModelRequestAccounting, accounting.value);
return Context.add(
Context.add(Context.empty(), LanguageModel.LanguageModel, model),
ModelRequestAccounting,
accounting.value,
);

usage: { inputTokens: {}, outputTokens: {} },
},
],
[text("z")],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The third script entry [text("z")] is never consumed: this test calls step only twice (one with a policy, one without), and each step performs exactly one generation (admitted calls model.generateText once), so only the first two generations run. The never-dispatched text generation is dead test data; remove it, or add the third step if the intent was to assert that a generation with no Tool Calls records nothing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/test/session/step-outcomes.test.ts, line 138:

<comment>The third script entry `[text("z")]` is never consumed: this test calls `step` only twice (one with a policy, one without), and each `step` performs exactly one generation (`admitted` calls `model.generateText` once), so only the first two generations run. The never-dispatched text generation is dead test data; remove it, or add the third `step` if the intent was to assert that a generation with no Tool Calls records nothing.</comment>

<file context>
@@ -0,0 +1,192 @@
+                          usage: { inputTokens: {}, outputTokens: {} },
+                        },
+                      ],
+                      [text("z")],
+                    ],
+                    [],
</file context>

@@ -195,48 +188,40 @@ describe("Codex SSE", () => {
},
});
try {
const events: Array<unknown> = [];
const parts: Array<AiResponse.AnyPart> = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The rewritten end-to-end test drops the response.output_item.added/.done events with an unknown future_item type that the old test injected mid-stream. That was the only remaining coverage that a future/unknown Codex item type is silently skipped without corrupting the part stream, and sse.test.ts never feeds an unknown item type through output_item.added/.done (it only uses message/function_call/reasoning items). Since the parser was rebuilt in this PR and Codex is a best-effort unofficial API, keep the unknown-item events in this SSE fixture so the skip path stays covered end to end.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/providers/test/openai-codex/codex.test.ts, line 191:

<comment>The rewritten end-to-end test drops the `response.output_item.added`/`.done` events with an unknown `future_item` type that the old test injected mid-stream. That was the only remaining coverage that a future/unknown Codex item type is silently skipped without corrupting the part stream, and `sse.test.ts` never feeds an unknown item type through `output_item.added`/`.done` (it only uses message/function_call/reasoning items). Since the parser was rebuilt in this PR and Codex is a best-effort unofficial API, keep the unknown-item events in this SSE fixture so the skip path stays covered end to end.</comment>

<file context>
@@ -195,48 +188,40 @@ describe("Codex SSE", () => {
     });
     try {
-      const events: Array<unknown> = [];
+      const parts: Array<AiResponse.AnyPart> = [];
       const { promise: output, resolve: firstOutput } = Promise.withResolvers<void>();
-      const turn = Effect.runPromise(
</file context>

}),
);
// The late response cannot stage into the ended Turn, but its request stays counted.
expect(Exit.isFailure(outcome.exit)).toBe(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This assertion treats any Exit failure as success, but an interrupted parked fiber is also an Exit.failure, so the test would still pass if the fiber was torn down by scope teardown instead of dying on staging into the already-ended Turn. The point of the test is that the late response cannot stage into the closed Turn, so assert that exact die, as the first test block does, with defect(outcome.exit).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/test/session/step-lifetime.test.ts, line 136:

<comment>This assertion treats any Exit failure as success, but an interrupted parked fiber is also an `Exit.failure`, so the test would still pass if the fiber was torn down by scope teardown instead of dying on staging into the already-ended Turn. The point of the test is that the late response cannot stage into the closed Turn, so assert that exact die, as the first test block does, with `defect(outcome.exit)`.</comment>

<file context>
@@ -0,0 +1,318 @@
+        }),
+      );
+      // The late response cannot stage into the ended Turn, but its request stays counted.
+      expect(Exit.isFailure(outcome.exit)).toBe(true);
+      expect(outcome.usage).toEqual({ generations: 1, dispatches: 0, physicalRequests: 1 });
+      expect(outcome.history).toEqual([]);
</file context>
Suggested change
expect(Exit.isFailure(outcome.exit)).toBe(true);
expect(defect(outcome.exit)).toBe(true);

turnOf(program, scriptedModel([[call("a", "write"), call("b", "read")]], log)),
);
yield* Effect.yieldNow;
while (!log.includes("write:a:start")) yield* Effect.yieldNow;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: These busy-wait loops spin Effect.yieldNow until write:a:start appears; if a regression stops the handler from ever starting, the test spins until the runner timeout instead of failing on the actual mismatch (gly letter. Same pattern at line 948). Bound the wait so a regression fails fast with the real assertion error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/test/session/step.test.ts, line 821:

<comment>These busy-wait loops spin `Effect.yieldNow` until `write:a:start` appears; if a regression stops the handler from ever starting, the test spins until the runner timeout instead of failing on the actual mismatch (gly letter. Same pattern at line 948). Bound the wait so a regression fails fast with the real assertion error.</comment>

<file context>
@@ -0,0 +1,984 @@
+          turnOf(program, scriptedModel([[call("a", "write"), call("b", "read")]], log)),
+        );
+        yield* Effect.yieldNow;
+        while (!log.includes("write:a:start")) yield* Effect.yieldNow;
+        expect(log).toEqual(["generate:0", "write:a:start"]);
+        yield* Fiber.interrupt(fiber);
</file context>

The first isolated Session/Turn experiment demonstrated exact unary result/error/requirement typing, process-local staging and bounded passive observation, but exposed Session-close/in-flight-save and child/resource lifetime gaps. Parent reruns reproduced 15 checks, four expected compiler failures, a publication-before-save mutant failure, and the explicit post-release-commit counterexample. See [proof evidence](../research/session-turn-contract-proof.md). These are not native AI, Host, restart, or power-loss guarantees.

The user selected Turn-owned invocation scope and acceptance of non-resumable custom programs with pause/reconciliation after crash. The separate v2 proof now supplies a real Turn Scope and Session-owned admitted execution; parent reruns pass 14 inherited checks, seven ownership checks, two boundary controls and the intended negative/mutation tests. Independent review accepts the bounded proof with explicit sequential/protected shutdown ordering and limited cancelled-caller diagnostic claims. A stuck save still blocks shutdown. The full bounded ownership proof now passes on isolated rc.117 using native scoped cleanup and ordinary join, without the obsolete compensations; parent reruns and independent review agree. The cancellation-intersection test confirms that the execution retains body/cleanup causes while the cancelled caller receives only interruption. These bounded checks do not settle durable outcomes or all lifetime misuse. SQLite is a researched candidate, not a selected backend; rc.108 transaction failures have now been independently reproduced on real SQLite. Parent-verified published rc.117 packages fix the tested BEGIN, Cause-loss and observer regressions, but still leak the active connection after failed COMMIT: the desired cleanup test remains red. A supported upstream resolution is required before transaction integration (see proof evidence). No local workaround or fix contribution is approved. No contract or implementation issue is made ready merely because the scratch checks pass.
The user selected Turn-owned invocation scope and acceptance of non-resumable custom programs with pause/reconciliation after crash. The separate v2 proof now supplies a real Turn Scope and Session-owned admitted execution; parent reruns pass 14 inherited checks, seven ownership checks, two boundary controls and the intended negative/mutation tests. Independent review accepts the bounded proof with explicit sequential/protected shutdown ordering and limited cancelled-caller diagnostic claims. A stuck save still blocks shutdown. The full bounded ownership proof now passes on isolated rc.117 using native scoped cleanup and ordinary join, without the obsolete compensations; parent reruns and independent review agree. The cancellation-intersection test confirms that the execution retains body/cleanup causes while the cancelled caller receives only interruption. These bounded checks do not settle durable outcomes or all lifetime misuse. rc.108 transaction failures have been independently reproduced on real SQLite. Published rc.117 packages fix the tested BEGIN, Cause-loss and observer regressions, but still leak the active connection after failed COMMIT. The owner has since selected SQLite as the durable-backend direction and approved the qualified minimal upstream fix as a development-only patch of the matching rc.117 packages, for repository development and backend tests only ([development-only integration](../research/session-turn-contract-proof.md#development-only-sqlite-integration)). Portable library delivery and release stay blocked until a fixed upstream release is qualified; schema, recovery policy and limits remain open. No contract or implementation issue is made ready merely because the scratch checks pass.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This PR pins and patches the stable Effect 4.0.0 releases (changeset effect-4-pin.md moves off 4.0.0-rc.108; bun.lock resolves @effect/ai-openai@4.0.0 and @effect/ai-openai-compat@4.0.0, and the patch artifacts are named patches/*@4.0.0.patch), but the added plan text still identifies the local patches as patches of 4.0.0-rc.117 packages (also in the new #183 paragraph: 'A development-only patch of @effect/ai-openai@4.0.0-rc.117' and 'patches, of effect@4.0.0-rc.117 and @effect/ai-openai-compat@4.0.0-rc.117'). Align the version references in the added text with the pinned 4.0.0 packages so the release-blocking claim points at the packages that actually carry the patches; the sql adapters are pinned 4.0.0 as well and no SQLite adapter patch file exists under patches/.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/plans/effect-native-library.md, line 659:

<comment>This PR pins and patches the stable Effect `4.0.0` releases (changeset `effect-4-pin.md` moves off `4.0.0-rc.108`; bun.lock resolves `@effect/ai-openai@4.0.0` and `@effect/ai-openai-compat@4.0.0`, and the patch artifacts are named `patches/*@4.0.0.patch`), but the added plan text still identifies the local patches as patches of `4.0.0-rc.117` packages (also in the new `#183` paragraph: 'A development-only patch of `@effect/ai-openai@4.0.0-rc.117`' and 'patches, of `effect@4.0.0-rc.117` and `@effect/ai-openai-compat@4.0.0-rc.117`'). Align the version references in the added text with the pinned 4.0.0 packages so the release-blocking claim points at the packages that actually carry the patches; the sql adapters are pinned `4.0.0` as well and no SQLite adapter patch file exists under `patches/`.</comment>

<file context>
@@ -652,7 +656,7 @@ No gate is waived by a successful internal TUI/HTTP demo. Public native examples
 The first isolated Session/Turn experiment demonstrated exact unary result/error/requirement typing, process-local staging and bounded passive observation, but exposed Session-close/in-flight-save and child/resource lifetime gaps. Parent reruns reproduced 15 checks, four expected compiler failures, a publication-before-save mutant failure, and the explicit post-release-commit counterexample. See [proof evidence](../research/session-turn-contract-proof.md). These are not native AI, Host, restart, or power-loss guarantees.
 
-The user selected Turn-owned invocation scope and acceptance of non-resumable custom programs with pause/reconciliation after crash. The separate v2 proof now supplies a real Turn Scope and Session-owned admitted execution; parent reruns pass 14 inherited checks, seven ownership checks, two boundary controls and the intended negative/mutation tests. Independent review accepts the bounded proof with explicit sequential/protected shutdown ordering and limited cancelled-caller diagnostic claims. A stuck save still blocks shutdown. The full bounded ownership proof now passes on isolated rc.117 using native scoped cleanup and ordinary join, without the obsolete compensations; parent reruns and independent review agree. The cancellation-intersection test confirms that the execution retains body/cleanup causes while the cancelled caller receives only interruption. These bounded checks do not settle durable outcomes or all lifetime misuse. SQLite is a researched candidate, not a selected backend; rc.108 transaction failures have now been independently reproduced on real SQLite. Parent-verified published rc.117 packages fix the tested BEGIN, Cause-loss and observer regressions, but still leak the active connection after failed COMMIT: the desired cleanup test remains red. A supported upstream resolution is required before transaction integration (see proof evidence). No local workaround or fix contribution is approved. No contract or implementation issue is made ready merely because the scratch checks pass.
+The user selected Turn-owned invocation scope and acceptance of non-resumable custom programs with pause/reconciliation after crash. The separate v2 proof now supplies a real Turn Scope and Session-owned admitted execution; parent reruns pass 14 inherited checks, seven ownership checks, two boundary controls and the intended negative/mutation tests. Independent review accepts the bounded proof with explicit sequential/protected shutdown ordering and limited cancelled-caller diagnostic claims. A stuck save still blocks shutdown. The full bounded ownership proof now passes on isolated rc.117 using native scoped cleanup and ordinary join, without the obsolete compensations; parent reruns and independent review agree. The cancellation-intersection test confirms that the execution retains body/cleanup causes while the cancelled caller receives only interruption. These bounded checks do not settle durable outcomes or all lifetime misuse. rc.108 transaction failures have been independently reproduced on real SQLite. Published rc.117 packages fix the tested BEGIN, Cause-loss and observer regressions, but still leak the active connection after failed COMMIT. The owner has since selected SQLite as the durable-backend direction and approved the qualified minimal upstream fix as a development-only patch of the matching rc.117 packages, for repository development and backend tests only ([development-only integration](../research/session-turn-contract-proof.md#development-only-sqlite-integration)). Portable library delivery and release stay blocked until a fixed upstream release is qualified; schema, recovery policy and limits remain open. No contract or implementation issue is made ready merely because the scratch checks pass.
 
 ## Pi reference-alignment review
</file context>
Suggested change
The user selected Turn-owned invocation scope and acceptance of non-resumable custom programs with pause/reconciliation after crash. The separate v2 proof now supplies a real Turn Scope and Session-owned admitted execution; parent reruns pass 14 inherited checks, seven ownership checks, two boundary controls and the intended negative/mutation tests. Independent review accepts the bounded proof with explicit sequential/protected shutdown ordering and limited cancelled-caller diagnostic claims. A stuck save still blocks shutdown. The full bounded ownership proof now passes on isolated rc.117 using native scoped cleanup and ordinary join, without the obsolete compensations; parent reruns and independent review agree. The cancellation-intersection test confirms that the execution retains body/cleanup causes while the cancelled caller receives only interruption. These bounded checks do not settle durable outcomes or all lifetime misuse. rc.108 transaction failures have been independently reproduced on real SQLite. Published rc.117 packages fix the tested BEGIN, Cause-loss and observer regressions, but still leak the active connection after failed COMMIT. The owner has since selected SQLite as the durable-backend direction and approved the qualified minimal upstream fix as a development-only patch of the matching rc.117 packages, for repository development and backend tests only ([development-only integration](../research/session-turn-contract-proof.md#development-only-sqlite-integration)). Portable library delivery and release stay blocked until a fixed upstream release is qualified; schema, recovery policy and limits remain open. No contract or implementation issue is made ready merely because the scratch checks pass.
The owner has since selected SQLite as the durable-backend direction and approved the qualified minimal upstream fix as a development-only patch of the pinned 4.0.0 packages, for repository development and backend tests only

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Two reproduced issues need fixes before merging: a special-name Tool can bypass authorization and dispatch limits, and scoped work started during Turn cleanup can outlive commit and Session infrastructure release.

Reviewed changes: Reviewed the Effect-native replacement and its integration boundaries, focusing on executable behavior and retained tests rather than exhaustively rereading deleted legacy tests because the clean cutover is intentional.

  • Session ownership: Fresh scoped Sessions, whole-function Turns, save-before-publication, fencing, receipts and passive observation.
  • Controlled execution: Serialized Steps, native Tool registration and codecs, policy/consent/authority checks, incomplete results and request/dispatch accounting.
  • Provider integration: OpenAI and compatible bindings, Codex SSE reconciliation, redirect handling and the three development dependency patches.
  • Cutover and qualification: Legacy package removal, help/version-only CLI, native scaffolding, release fixture changes and native/SQLite qualification.

Verification passed: package typechecks and 537 tests across Core, Providers, CLI and scaffolding; native qualification; SQLite qualification on Bun 1.4.0 and Node 24.19.0. Separate full-core probes on the patched Effect 4.0.0 installation reproduced both findings below. Release tarball fixtures, cross-platform release binaries and live credentialed Provider calls were not rerun; the explicitly documented publication block for development patches remains necessary.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6-astra | 𝕏

Comment on lines +146 to +150
const output = {} as GatedHandlers<Tools>;
// SAFETY: the own keys of `handlers` were just checked to be exactly the Toolkit's Tool names.
for (const name of Object.keys(handlers) as Array<keyof Toolkit.HandlersFrom<Tools>>) {
const handler = handlers[name];
output[name] = (params, handlerContext) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve special Tool names as own gated-handler properties

For a valid Tool named __proto__, this assignment changes output's prototype instead of creating an own handler binding, so native toHandlers omits the gate. If a same-ID native handler is ambient, controlled dispatch falls back to it and bypasses policy, consent, authority and dispatch reservation; I reproduced a Complete result with policy deny, authority false, a dispatch limit of 0, and neither authorization callback invoked.

Technical details
# Prototype-safe gated handler registration

## Affected sites
- packages/core/src/session/step.ts:146-150 constructs the wrapper map with ordinary property assignment.
- packages/core/src/session/step.ts:421-424 provides the resulting handler context; an omitted binding leaves an ambient same-ID handler available.
- Native Effect 4.0.0 Toolkit.make supports this name using prototype-safe own-property assignment; Toolkit.toHandlers uses Object.entries, and Toolkit.handle resolves the handler by Tool id from Context.

## Reproduction
- Create Toolkit.make(Tool.make("__proto__", { parameters: Schema.Struct({}), success: Schema.String })).
- Register localTools with a computed-property handler map: { ["__proto__"]: localHandler }.
- Provide kit.toLayer({ ["__proto__"]: ambientHandler }) around the controlled Step.
- Return a native Model call named "__proto__" with params {}.
- Set policy to return "deny", authority to return false, and Session limits to { generations: 1, dispatches: 0 }.
- Actual on patched Effect 4.0.0: ambientHandler runs, localHandler/policy/authority do not; Step is Complete and usage.dispatches is 0. Without the ambient handler, the missing binding instead fails dispatch.

## Required outcome
- Every accepted Tool name must have its own gated binding; native ambient handlers must not replace it.
- Use a prototype-safe map or own-property definition and add a special-name regression with an ambient handler and denying authorization.

## Source
- https://github.com/Effect-TS/effect/blob/effect%404.0.0/packages/effect/src/ai/Toolkit.ts
- https://github.com/chenxin-yan/mitome/blob/f15d696dd10b10ea0cd6820c401b72d51683d849/packages/core/src/session/step.ts#L421-L424

Comment on lines +341 to +345
const result = yield* Effect.gen(function* () {
const cleanup = yield* Effect.forkChild(owned);
return yield* restoreProgram(Fiber.join(cleanup)).pipe(
Effect.ensuring(Fiber.interrupt(cleanup)),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Drain scoped children started during Turn cleanup

A Turn finalizer that calls Effect.forkScoped(..., { startImmediately: true }) uses its captured Turn Scope after that Scope has been marked closed, so Effect interrupts the new daemon fiber without awaiting it. Joining cleanup therefore does not cover that child's held cleanup: I reproduced Session.run returning and publishing history, followed by Session infrastructure release, while the child was still running.

Technical details
# Scoped cleanup work escapes the Turn completion barrier

## Affected sites
- packages/core/src/session/session.ts:329-345 relies on joining the cleanup fiber to settle all cleanup-created children.
- packages/core/test/session/session.test.ts:258-295 and 339-395 cover finalizer-created forkChild children; forkScoped is covered only when started in the program body.

## Reproduction
Allocate Deferred<void> values cleaning and gate outside the Turn, stage one message, then register:

```ts
yield* Effect.addFinalizer(() =>
  Effect.forkScoped(
    Effect.never.pipe(
      Effect.ensuring(
        Deferred.succeed(cleaning, undefined).pipe(
          Effect.andThen(Deferred.await(gate)),
        ),
      ),
    ),
    { startImmediately: true },
  ).pipe(Effect.asVoid),
);
```

Keep the returned child handle when probing its state. Actual on patched Effect 4.0.0: session.run completes; history contains the staged message; child.pollUnsafe() is undefined. Closing the allocating Session Scope releases infrastructure before gate is opened. Opening gate finally drains the child.

## Cause
Effect.addFinalizer restores the registration Context, including the Turn Scope. Native Scope.close marks that Scope Closed before invoking finalizers. Native forkScoped delegates to forkIn, which creates a daemon and, for a closed Scope, only calls interruptUnsafe; it does not join the child. That child is neither a native child of the cleanup fiber nor registered with the Session execution Scope.

## Required outcome
- Supported Turn-owned scoped work started during cleanup must settle before save, commit, invocation completion and Session dependency release.
- Add a held-cleanup regression covering forkScoped during a finalizer, not just forkChild.
- This does not require changing native reverse finalizer order or propagating unjoined child failures; those intentional contracts can stay unchanged.

## Source
- https://github.com/Effect-TS/effect/blob/effect%404.0.0/packages/effect/src/internal/effect.ts#L5624-L5665
- https://github.com/chenxin-yan/mitome/blob/f15d696dd10b10ea0cd6820c401b72d51683d849/packages/core/src/session/session.ts#L325-L348

… 4.0.0

Adopt the stable Effect 4.0.0 release and the approved TypeScript
7.0.2 + @effect/tsgo compiler, and add the Effect-native execution core.

- Native baseline (#181): pin effect and every @effect/* package to
  4.0.0, migrate to the stable module paths, and add compile-time and
  runtime qualification fixtures for native Tool/Toolkit behavior.
- Sessions and whole-function Turns (#182): ordinary Effect programs
  run through makeSession and Turn, with staged history committed once,
  owned cleanup and live queries.
- SQLite transaction qualification for the Node and Bun adapters (#170).
- Controlled Tool Steps (#183): step and loop with serial native Tools,
  fail-closed authorization, incomplete-response handling, generation
  and dispatch limits, and physical request accounting across the
  openai, openai-compatible and openai-codex providers.

The legacy Promise SDK and Extension runtime no longer build against
this core; the next commit removes them.
Delete the Promise SDK, the Agent Definition/Extension/Hook framework,
the old Host model, the old Transcript and Route stores, the HTTP
channel host and the old TUI, so the repository builds on the native
Session/Turn/Step core alone. Pre-release: no compatibility layer.

- Remove @mitome/sdk, @mitome/channels and @mitome/tui.
- Core exports only the native Session, Turn, Step and Provider surface.
- The CLI keeps only --help/--version until native application loading
  lands (#184); the TUI is rebuilt in #185.
- Port the provider tests from the removed engine to providerModel with
  native LanguageModel generation.
- create-mitome scaffolds a native Effect program.
- Release fixtures smoke-test the native packed exports.
- Move the models.dev filter used by hint generation into scripts.
@chenxin-yan
chenxin-yan force-pushed the feat/effect-native-core branch from f15d696 to c04a855 Compare October 6, 2026 23:17
@pullfrog

pullfrog Bot commented Oct 6, 2026

Copy link
Copy Markdown

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using gpt-6-astra | 𝕏

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant