Skip to content

manifest: retire the scheduled-task vocabulary - #21

Merged
ualtinok merged 4 commits into
cortexkit:masterfrom
iceteaSA:feat/retire-scheduled-tasks
Aug 15, 2026
Merged

ualtinok merged 4 commits into
cortexkit:masterfrom
iceteaSA:feat/retire-scheduled-tasks

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #14. scheduled_tasks and its supporting types are gone from ModuleManifest, in Rust and TypeScript.

Both halves in one commit, per the reasoning on the issue: a manifest field required by one client and absent from the protocol is the same drift that made scheduler.* readable as a roadmap.

Removed

Rust — crates/subc-protocol/src/manifest.rs: the field, plus ScheduledTask, TaskEligibility, LeaseScope, ModelPolicy, CircuitBreaker, and the fixture that was their only constructor. Ten call sites dropped scheduled_tasks: Vec::new().

TypeScript — clients/subc-client/src/provider.ts: the required scheduled_tasks field, ScheduledTaskInput / TaskEligibilityInput / ModelPolicyInput / CircuitBreakerInput, the LeaseScope type, both construction sites, and normalizeScheduledTask. Five re-exports removed from index.ts.

Swift confirmed unaffected — zero hits under clients/subc-client-swift.

control.rs's manifest() helper is a generic fixture shared by hello-frame parsing, route-liveness, and push-op tests — none of them about scheduled tasks. Its ScheduledTask literal was incidental filler, so the field was dropped and every test kept.

The fixture, called out rather than buried

crates/subc-protocol/tests/golden/module_hello_body.json loses one line. That's the serialized HELLO body carrying the whole manifest, so the field's removal necessarily moves it — a real wire-shape change, not a test artifact.

git diff --stat origin/master -- crates/subc-control/tests/golden/
(empty)

git diff --stat origin/master -- crates/subc-protocol/tests/golden/
 module_hello_body.json | 1 -

subc-control is untouched, per your review criterion.

Compatibility both directions: no deny_unknown_fields anywhere in subc-protocol, so a new daemon parses an old module's HELLO that still sends scheduled_tasks, and vice versa. Only the canonical fixture moves.

Migration cost — I under-priced this on the issue and want it stated correctly

I originally said "one line per Rust module author." That was measured on the Rust side and stated as though it covered the fleet. Corrected:

  • Rust module authors: drop one line at their next build.
  • TypeScript module authors: drop one line, and @cortexkit/subc-client needs a release — removing a required interface field is breaking for anyone typechecking against it.

Verification

cargo test --workspace      31 test binaries, 0 failures
cargo clippy --all-targets  clean
cargo fmt --check           clean
bun test (subc-client)      137 tests, 0 fail, 18 skip

bunx tsc --noEmit fails, and it is pre-existing — error TS2688: Cannot find type definition file for 'node'. node_modules isn't installed in this checkout, so tsc can't resolve anything. I verified this rather than asserting it: the identical error reproduces on a clean worktree of unmodified 06a2e76. bun test resolves types through Bun's own toolchain and passes, which is why the TS suite is meaningful and the tsc run isn't.

Flagging it because a typecheck that structurally cannot run is a gate whose silence means nothing — if CI installs deps, it will typecheck properly there, and that's the run I'd trust over mine.

One process note

My subagent reported a dangling ModelPolicyInput re-export as pre-existing upstream drift. It wasn't — I checked origin/master and the type was defined at provider.ts:153, so the removal was its own, correctly done, and misattributed afterward. Had I forwarded that as filed, this PR would have opened with a phantom bug report against your tree.

I mention it because the same habit is why the fixture claim above is stated as a diff rather than a summary.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Summary by cubic

Retires scheduled-task fields from the manifest across Rust and TypeScript so the daemon no longer exposes unused scheduler vocabulary. Previously, ModuleManifest required scheduled_tasks; now it omits it. The HELLO payload no longer includes scheduled_tasks. Parsing remains backward/forward compatible because the protocol does not deny_unknown_fields.

  • Removes scheduled_tasks and supporting types from subc-protocol; updates call sites in subc-client-rs, subc-core, and subc-mcp.
  • Removes the required field and related types/normalizers from the TypeScript client @cortexkit/subc-client; Swift client is unaffected.
  • Updates the golden module_hello_body.json; subc-control goldens are unchanged.
  • Drops the incidental scheduled-task literal from control.rs’s manifest() test helper; tests remain valid.

Migration

  • Rust module authors: remove scheduled_tasks from manifests.
  • TypeScript module authors: upgrade to the next @cortexkit/subc-client and remove scheduled_tasks from manifests; this is a breaking type change.
  • No daemon or config changes required; old/new peers can interoperate on the wire.

Written for commit f4708fb. Summary will update on new commits.

Review in cubic

Deploy ordering (release note)

The daemon must be deployed before any module rebuilt against the new manifest. The old scheduled_tasks field carries no #[serde(default)], so an old daemon refuses a new module's HELLO with missing field scheduled_tasks; a new daemon accepts old modules unchanged. Producer-narrowing, and ck daemon's build_git_sha makes the ordering checkable rather than ritual.

Upstream issue #14, maintainer ruling: scheduled work will never live
in this daemon -- it belongs to a separate service. ModuleManifest.scheduled_tasks
was required, forcing every module author to declare a field for work
the daemon never executes.

Rust (crates/subc-protocol/src/manifest.rs): drop ModuleManifest.scheduled_tasks
and the ScheduledTask/TaskEligibility/LeaseScope/ModelPolicy/CircuitBreaker
types, plus every fixture/constructor reference across subc-client-rs,
subc-core, subc-mcp, and subc-protocol tests. control.rs's manifest()
test helper built a fully-populated ScheduledTask but the surrounding
tests (hello-frame parsing, route liveness, push-op handling) were never
about scheduled tasks -- the literal was incidental filler, so the field
is dropped and the tests kept.

TypeScript (clients/subc-client): drop the mirrored required field,
ScheduledTaskInput/TaskEligibilityInput/ModelPolicyInput/CircuitBreakerInput/
LeaseScope, normalizeScheduledTask, and the stale ModelPolicyInput
re-export in index.ts. Swift confirmed unaffected -- SubcClient never
named this vocabulary.

Regenerated crates/subc-protocol/tests/golden/module_hello_body.json
(the serialized HELLO body carries the whole manifest, so removing the
field necessarily moves it) -- the only golden fixture that changes;
crates/subc-control/tests/golden/ is untouched.

@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.

1 issue found across 16 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/subc-protocol/src/manifest.rs">

<violation number="1" location="crates/subc-protocol/src/manifest.rs:21">
P1: When a new module sends HELLO to an older daemon, registration fails because the older required `scheduled_tasks` field is now omitted. Coordinate this wire-shape change with a protocol/versioned rollout, or continue emitting the field until older daemons are retired; `deny_unknown_fields` does not provide backwards compatibility for missing required fields.</violation>
</file>

You're on the cubic free plan with 11 free PR reviews remaining this month. Upgrade for unlimited reviews.

Re-trigger cubic

pub trust_tier: TrustTier,
pub provides: Vec<ProviderRole>,
pub consumes: Vec<ConsumerRole>,
pub scheduled_tasks: Vec<ScheduledTask>,

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: When a new module sends HELLO to an older daemon, registration fails because the older required scheduled_tasks field is now omitted. Coordinate this wire-shape change with a protocol/versioned rollout, or continue emitting the field until older daemons are retired; deny_unknown_fields does not provide backwards compatibility for missing required fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/subc-protocol/src/manifest.rs, line 21:

<comment>When a new module sends HELLO to an older daemon, registration fails because the older required `scheduled_tasks` field is now omitted. Coordinate this wire-shape change with a protocol/versioned rollout, or continue emitting the field until older daemons are retired; `deny_unknown_fields` does not provide backwards compatibility for missing required fields.</comment>

<file context>
@@ -18,7 +18,6 @@ pub struct ModuleManifest {
     pub provides: Vec<ProviderRole>,
     pub consumes: Vec<ConsumerRole>,
-    pub scheduled_tasks: Vec<ScheduledTask>,
     pub bindings: Bindings,
 }
 
</file context>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Valid, and my compatibility claim in the PR body is wrong. I asserted "compatible both directions" and only tested one of them.

Reproduced the failing direction rather than reasoning about it further:

#[derive(serde::Deserialize)]
struct OldManifest { module_id: String, scheduled_tasks: Vec<String> }

serde_json::from_str::<OldManifest>(r#"{"module_id":"aft"}"#)
// → Error: missing field `scheduled_tasks` at line 1 column 19

ModuleManifest at master carries no #[serde(default)] on that field, so it is genuinely required on deserialize. deny_unknown_fields governs extra keys and says nothing about missing ones. I conflated the two.

The directions are not symmetric:

old daemon new daemon
old module sends the field parses parses — extra key ignored
new module omits it FAILS — missing required field parses

The safe direction is old-module→new-daemon, which is the one I checked and the one my deny_unknown_fields reasoning actually covers. The unsafe one is new-module→old-daemon — exactly the deployment order that happens in practice, since clients upgrade independently of the daemon.

This box demonstrates the hazard: my daemon ran a build 623 commits stale until this afternoon while modules were rebuilt against a much newer tree. A skew window measured in days.

Options, and the call is yours

Cubic's suggestion — keep emitting the field — defeats the purpose, since authors would still have to declare it.

I also can't fix this from the new side. The old daemon is the one that needs the key present, and no change to a new module makes an already-shipped binary tolerate its absence. So:

  1. Land as-is with a hard ordering constraint — daemon upgrades before any module rebuilt against the new manifest. ck daemon's build_git_sha makes that checkable rather than hoped-for, and it's a one-time cost on a fleet you control.
  2. Add #[serde(default)] to the field on master first, ship it, then excise in a later release. The intermediate daemon tolerates both shapes and the ordering constraint disappears.

Option 2 is the correct protocol move and costs one release cycle. Option 1 is defensible only because every consumer is in your fleet.

If you want option 2, this PR should probably become the #[serde(default)] commit and the excision waits behind it. Say which and I'll reshape it.

Worth naming the general shape, since it bit me twice today: I checked the direction that confirmed my conclusion and stopped. Same error as my MASKED verdict on #11 — one explanation enumerated, the other never asked about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The parent comment still applies: your reproduction confirms that a new module omitting required scheduled_tasks cannot register with an old daemon, while the reverse direction remains safe. I’d choose option 2—ship a #[serde(default)] compatibility release before excising the field—because the demonstrated multi-day skew makes daemon-first ordering fragile. deny_unknown_fields only addresses extra keys, and continuing to emit the field would not achieve the intended removal.

Thanks for the feedback! I've saved this as a new learning.

@ualtinok

Copy link
Copy Markdown
Contributor

Reviewed in full — the removal is complete on both halves (Rust types gone, TS interface gone, every constructor site swept, the phase1_integration and catalog_update fixtures updated), and the golden change is exactly the one you pre-flagged on #14: module_hello_body.json loses the field because it serializes the whole manifest, subc-control vectors untouched. Finding-not-fixup handled the way the criterion intended.

Two gates before merge, one yours and one mine to run:

1. Version bump, in this PR. subc-protocol is at 0.10.0 and this is a wire-visible code change; CI enforces that any crates/subc-* code change carries a Cargo.toml version bump (scripts/fleet/check-wire-crate-versions.sh). You could not have seen that check — it rides the Rust matrix, which fork PRs do not trigger. Please bump subc-protocol to 0.11.0 (and the TS package's minor). 0.11.0 rather than 0.10.1 deliberately: see the deploy-ordering note below — a manifest-shape change is exactly what the minor is for.

2. Deploy-ordering fact, for the record rather than for you to fix: the old scheduled_tasks field has no #[serde(default)], so an old daemon refuses a new module's HELLO (missing required field), while a new daemon accepts old modules fine (serde tolerates the unknown field on parse). That is producer-narrowing: daemon deploys first, modules rebuild after. Our fleet handles this by ritual, but the constraint belongs in the PR description so the release notes carry it — one sentence is enough, and it is the difference between a clean cutover and a module that cannot register with a confident-looking error.

Push the bump and I'll run your head through the internal twin for the full matrix (same flow as #10/#18), then merge on green.

The scheduled-task excision changes what a module serializes in HELLO, and
seven repos path-depend on subc-protocol without being able to see that the
code moved (a path dependency records a bare version with no source and no
checksum, so a --locked build over there cannot tell). The minor is what
carries a manifest-shape change per the maintainer's note on the PR.

Three in-workspace dependents pin "0.10", which is a caret requirement that
excludes 0.11 -- subc-control, subc-client-rs, and subc-transport all fail to
resolve until they take the new minor. Bumped alongside, verified by a cargo
check that reproduced the resolution failure first.

@cortexkit/subc-client 0.5.0 -> 0.6.0: removing a required interface field is
breaking for anyone typechecking against it.
check-wire-crate-versions.sh flags subc-core because this branch edits lines
under src/. All three edits are unreachable from a library consumer's build:
the control.rs change is inside #[cfg(test)] mod tests, bench_harness.rs is
#[cfg(feature = "bench-harness")] and off by default, and fake-aft-stub is a
src/bin auto-binary rather than part of the lib.

Taking the bump the check asks for rather than arguing past it. Patch, not
minor: nothing a consumer compiles by default moved, and ck-subc's own output
is byte-unaffected.
The lock records the workspace crates' own versions, so it moves with the
bump. Committed separately because the test run that regenerated it landed
after the version commit -- a --locked build would have failed on the skew.
@iceteaSA

Copy link
Copy Markdown
Collaborator Author

Both gates addressed, pushed through f872077. Ready for the twin.

Version bump — four crates, not one

subc-protocol 0.10.0 → 0.11.0 as you asked. Three in-workspace dependents pin version = "0.10", which is a caret requirement that excludes 0.11, so subc-control, subc-client-rs, and subc-transport all had to take the new minor. I reproduced the failure before editing them rather than assuming the cascade:

error: failed to select a version for the requirement `subc-protocol = "^0.10"`
candidate versions found which didn't match: 0.11.0
required by package `subc-control v0.1.1`

@cortexkit/subc-client 0.5.0 → 0.6.0.

Cargo.lock needed its own commit. The lock records workspace crates' own versions, and the test run that regenerated it landed after my version commit — a --locked build would have failed on the skew. Verified cargo check --workspace --locked passes now.

The check also flagged subc-core, and I took the bump rather than arguing past it

Ran check-wire-crate-versions.sh locally once you named it. It flags subc-core too, because this branch edits lines under src/. All three edits are unreachable from a library consumer's build:

  • control.rs — inside #[cfg(test)] mod tests (module opens at :2757)
  • bench_harness.rs — #[cfg(feature = "bench-harness")], off by default
  • fake-aft-stub.rs — a src/bin auto-binary, not part of the lib

So nothing a consumer compiles by default moved, and ck-subc's own output is byte-unaffected. Bumped 0.3.0 → 0.3.1 anyway — patch rather than minor, since the check's escape hatch is "confirm the change is doc-only" and mine is test-only, which isn't the same claim. Gate is green: 6 cross-repo crates examined against origin/master, none changed without a version bump.

Observation for you, not a request: the check's header says it "asks whether the OUTPUT can move, not whether a file did," and exempts doc-only on exactly that principle. #[cfg(test)] and off-by-default-feature code can't move a consumer's output either, by the same argument. Whether that's worth teaching the check is your call — there's a real cost to it, since a cfg(test) detector that's wrong in the permissive direction is worse than a check that occasionally asks for a harmless patch bump. I'd probably leave it. Flagging because the next contributor will hit the same thing and may reach for the doc-only hatch instead of bumping.

Deploy-ordering note

Added to the PR description as you asked, one sentence in the migration section. For the record here too: the old field has no #[serde(default)], so an old daemon refuses a new module's HELLO with missing field scheduled_tasks — daemon deploys first, modules rebuild after. New daemon accepts old modules fine.

Worth noting your build_git_sha work makes that ordering checkable rather than ritual — a module can compare against the daemon it's talking to instead of trusting deploy discipline.

Gates

cargo test --workspace          31 binaries, 0 failures
cargo fmt --check               clean
cargo check --workspace --locked  clean
check-wire-crate-versions.sh    6 examined, 0 violations

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.

ModuleManifest still requires scheduled_tasks for work core will never execute — what's the disposition?

2 participants