Skip to content

fix(serve): start scheduler at startup instead of first ACP connection - #10766

Merged
lifeizhou-ap merged 2 commits into
aaif-goose:mainfrom
ccgauvin94:fix/serve-eager-scheduler
Aug 3, 2026
Merged

fix(serve): start scheduler at startup instead of first ACP connection#10766
lifeizhou-ap merged 2 commits into
aaif-goose:mainfrom
ccgauvin94:fix/serve-eager-scheduler

Conversation

@ccgauvin94

Copy link
Copy Markdown
Contributor

Fixes #10765

Initializes the scheduler in handle_serve_command right after arg/auth validation instead of waiting for the first client. If init fails it warns and falls back to the current lazy path (the OnceCell stays empty, so the next create_agent retries like before).

Tested with an every-minute schedule under an isolated GOOSE_PATH_ROOT and nothing ever connecting: 0 fires before, fires every minute after. Also added a small test for init/idempotence.

Shouldn't fight with #10738: start_scheduler just drives scheduler(), which returns None when the kill switch is set, so eager init becomes a no-op for harnesses that opt out. And this doesn't change the multi-process story, every Desktop window's goosed already starts its scheduler the moment the window connects.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65cc58c869

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

})
.collect::<Result<Vec<_>>>()?;
let secret_key = env_secret.unwrap_or_else(generate_serve_secret_key);
if let Err(error) = server.start_scheduler().await {

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 Badge Refresh schedules added before the first client connects

Because this eagerly initializes the server's scheduler from the current schedule.json, a goose serve process that starts with no schedules will keep an empty in-memory job set. If a user then runs goose schedule add in another process before the first ACP client connects, create_agent will reuse this already-initialized scheduler instead of loading the newly persisted job, so the schedule will not be registered for automatic execution until some later operation happens to call list_scheduled_jobs() and sync from storage.

Useful? React with 👍 / 👎.

@ccgauvin94 ccgauvin94 Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The stale window existed before this PR too for anything added after the first client connected, but eager init did make it wider. Will investigate...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, should be resolved by create_agent: scheduler.list_scheduled_jobs().await - runs sync_fron_storage() which should register anything new.

@ccgauvin94
ccgauvin94 marked this pull request as draft July 28, 2026 16:14
@ccgauvin94
ccgauvin94 force-pushed the fix/serve-eager-scheduler branch from 9eeea72 to 97c6c52 Compare July 28, 2026 16:22
@ccgauvin94
ccgauvin94 marked this pull request as ready for review July 28, 2026 17:00

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97c6c524cd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

})
.collect::<Result<Vec<_>>>()?;
let secret_key = env_secret.unwrap_or_else(generate_serve_secret_key);
if let Err(error) = server.start_scheduler().await {

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 Badge Keep headless scheduler in sync after startup

With this eager initialization, a goose serve process that starts before any jobs are created has a live scheduler whose job set is populated only from the schedule.json contents seen at construction. The new storage sync runs only from create_agent, so if a user runs goose schedule add ... in another process and no ACP client connects—the headless mode this change targets—the persisted job is never registered and will not fire until some later client/list path happens to sync storage.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is a regression. From what I can tell, before the change a serve that never got a client had no scheduler at all, so external jobs never fired. Now, jobs present at startup fire, and any client that connects picks up later additions. To solve this there would need to be a periodic re-sync which I think is out of scope for this change.

A headless `goose serve` builds its scheduler lazily, on the first
create_agent call, so nothing runs scheduled jobs until a client
connects. Nobody notices in Desktop because opening the app connects,
but a server left running on its own just never fires anything.

The lazy OnceCell looks like migration residue rather than a decision:
the old REST goosed started its scheduler at boot, and stdio `goose acp`
still creates its agent immediately at startup.

Start it in handle_serve_command and warn instead of failing if it does
not come up, since create_agent will retry on the first connection
anyway. With aaif-goose#10781 this is a no-op unless --enable-scheduler is passed.
Starting the scheduler at startup means it loads whatever was on disk at
that moment. A job added later by another process, `goose schedule add`
or the Desktop, is not in the running server's registry until something
happens to re-read storage.

list_scheduled_jobs syncs from storage and the sync is diff based, so
calling it on connect is cheap and idempotent.
@ccgauvin94
ccgauvin94 force-pushed the fix/serve-eager-scheduler branch from 97c6c52 to 97d0115 Compare July 29, 2026 13:29

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97d0115771

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

})
.collect::<Result<Vec<_>>>()?;
let secret_key = env_secret.unwrap_or_else(generate_serve_secret_key);
if let Err(error) = server.start_scheduler().await {

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 Badge Honor schedule removals after headless startup

When goose serve --enable-scheduler starts with jobs already in schedule.json, this eagerly registers them in the in-process scheduler, but there is no headless refresh after that; the only new storage refresh is create_agent calling list_scheduled_jobs(), which requires an ACP client. If another process later runs goose schedule remove (or a Desktop process pauses/updates the same persisted job) while no client connects, the serve process keeps firing the stale in-memory cron entry. Unlike the prior add-after-start case, this job was loaded by the new eager startup path, so the PR can keep executing work the user has since removed or disabled.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The firing task checks the in-memory map only (current_jobs_arc.lock(), !j.paused) and never re-reads schedule.json. So a live scheduler already goes stale on external removals today. I could fix this but it expands the scope and I'd like to get this merged, the more I add, the more scrutiny we will need.

@ccgauvin94

Copy link
Copy Markdown
Contributor Author

Re-based for #10781

@alexhancock alexhancock self-assigned this Jul 30, 2026
@alexhancock

Copy link
Copy Markdown
Collaborator

cc @lifeizhou-ap who I think may have done something else related to the same objective, but I cannot find the commit at the moment. I want to clarify before we go ahead with this change.

@ccgauvin94-SUN

Copy link
Copy Markdown

cc @lifeizhou-ap who I think may have done something else related to the same objective, but I cannot find the commit at the moment. I want to clarify before we go ahead with this change.

Sounds good, thanks for looking it over.

@lifeizhou-ap lifeizhou-ap self-assigned this Aug 3, 2026

@lifeizhou-ap lifeizhou-ap left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The use case is really a valid use case. Thanks for the fix

@lifeizhou-ap
lifeizhou-ap added this pull request to the merge queue Aug 3, 2026
Merged via the queue into aaif-goose:main with commit 5ab0e6d Aug 3, 2026
25 checks passed
@ccgauvin94
ccgauvin94 deleted the fix/serve-eager-scheduler branch August 3, 2026 02:17
michaelneale added a commit that referenced this pull request Aug 4, 2026
* origin/main: (42 commits)
  chore (codex-acp): migrate to @agentclientprotocol/codex-acp (#10923)
  style: add dark mode inline code styling for message bubbles (#10861)
  docs: clarify macOS sandbox feature was removed in post-v1.25.0 cleanup (#10900)
  fall back to static model list only for non-models payloads (#10189)
  Remove CLI project support (#10838)
  fix(anthropic): add claude-sonnet-5 and claude-fable-5 to known models list (#10865)
  fix: stdio extensions silently skipped when name missing or env: used in config (#10773)
  feat(desktop): show session metadata on sidebar chat hover (#10663)
  fix(serve): start scheduler at startup instead of first ACP connection (#10766)
  fix: respect disabled builtin extensions (developer) at session start (#10223)
  fix: parse PyPI requirements for OSV checks (#10510)
  fix(telegram): contain voice file extensions (#10456)
  Revert "feat(security): chunk command-classifier input with overlapping windows" (#10416) (#10870)
  docs: update Discord invite (#10863)
  fix(security): preserve denied tool request precedence (#10612)
  fix(hints): contain subdirectory hint discovery (#10545)
  chore(deps): bump pem from 3.0.6 to 4.0.0 (#10853)
  chore(deps): bump base64 from 0.22.1 to 0.23.0 (#10851)
  chore(deps): bump jsonwebtoken from 10.4.0 to 11.0.0 (#10850)
  chore(deps): bump astral-sh/setup-uv from 8.3.2 to 9.0.0 (#10847)
  ...
alexhancock added a commit that referenced this pull request Aug 4, 2026
* origin/main: (58 commits)
  Handle MCP tool list change notifications
  fix: restore final_output_tool when agent is recreated after LRU eviction (#10793)
  feat(ui): bring back make sidebar resizable with drag handle and persisted width (#10795)
  docs: document auto-injected GOOSE_SHELL flags (#10759)
  feat: surface output-token-limit info (#10831)
  fix(desktop): avoid O(n²) cloning during session load (#10665)
  feat: add interactive menu for single select elicitations (#10327)
  fix(acp): prefix child stderr log target so it passes the default goose=info filter (#10901)
  chore (codex-acp): migrate to @agentclientprotocol/codex-acp (#10923)
  style: add dark mode inline code styling for message bubbles (#10861)
  docs: clarify macOS sandbox feature was removed in post-v1.25.0 cleanup (#10900)
  fall back to static model list only for non-models payloads (#10189)
  Remove CLI project support (#10838)
  fix(anthropic): add claude-sonnet-5 and claude-fable-5 to known models list (#10865)
  fix: stdio extensions silently skipped when name missing or env: used in config (#10773)
  feat(desktop): show session metadata on sidebar chat hover (#10663)
  fix(serve): start scheduler at startup instead of first ACP connection (#10766)
  fix: respect disabled builtin extensions (developer) at session start (#10223)
  fix: parse PyPI requirements for OSV checks (#10510)
  fix(telegram): contain voice file extensions (#10456)
  ...

# Conflicts:
#	Cargo.lock
#	crates/goose/src/agents/extension_manager.rs
#	crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything@2026.1.14
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.

Scheduled jobs never run in a headless goose serve until a client connects

4 participants