Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -377,11 +377,17 @@ jobs:
- name: Check command migration manifest
if: needs.changes.outputs.heavy == 'true'
env:
EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
python3 scripts/test_check_command_migration_manifest.py
baseline="${PR_BASE_SHA:-${PUSH_BEFORE_SHA:-}}"
# workflow_dispatch has neither a PR base nor a push-before SHA.
# Exact-head full CI still needs a real previous manifest revision.
if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
baseline="$(git rev-parse HEAD^)"
fi
if [[ -n "${baseline}" && ! "${baseline}" =~ ^0+$ ]]; then
git fetch --no-tags origin "${baseline}"
python3 scripts/check-command-migration-manifest.py --baseline-ref "${baseline}"
Expand Down Expand Up @@ -648,7 +654,11 @@ jobs:
# ubuntu-only, so the required "npm wrapper smoke (ubuntu-latest)"
# context is unaffected. Heavy pull requests execute the Ubuntu smoke
# here; their branches may not be mirrored to CNB.
timeout-minutes: 30
# A cold macOS release build can exceed 30 minutes when the optional
# sccache service becomes unavailable (the official v0.9.12 release and
# Pinvou exact-head verification both hit that path). Keep the smoke
# authoritative instead of letting infrastructure cancel it mid-build.
timeout-minutes: 60
runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }}
strategy:
matrix:
Expand Down
2 changes: 1 addition & 1 deletion crates/app-server/src/daemon_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! daemon over a local socket instead of a TCP port: local multi-client,
//! peer-credential auth, nothing to firewall (CORE-PROTOCOL spec §5). The
//! wire is *identical* to the `--stdio` transport — newline-delimited
//! JSON-RPC 2.0 driven by the same [`crate::run_stdio_loop`] — with exactly
//! JSON-RPC 2.0 driven by the same `crate::run_stdio_loop` — with exactly
//! one addition in front of it: a `daemon/attach` handshake that establishes
//! who this client is and whether it owns the daemon.
//!
Expand Down
80 changes: 48 additions & 32 deletions crates/tui/src/automation_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1410,12 +1410,15 @@ impl AutomationManager {
AutomationRunStatus::Queued | AutomationRunStatus::Running
)
});
let missed_while_offline = now.signed_duration_since(due_at)
> Duration::seconds(AUTOMATION_MISFIRE_GRACE_SECS);

// Recurring Pinvou tasks neither backfill offline slots nor overlap
// an active attempt. Jump directly to the first future slot.
if existing_for_slot || has_active_run || missed_while_offline {
let missed_recurring_slot = !matches!(&schedule, AutomationSchedule::Once { .. })
&& now.signed_duration_since(due_at)
> Duration::seconds(AUTOMATION_MISFIRE_GRACE_SECS);

// Every schedule avoids duplicate and overlapping attempts. Only
// recurring tasks skip slots missed while the scheduler was
// offline: a one-shot has no future slot to advance to, so it must
// remain deliverable until its single run is durably enqueued.
if existing_for_slot || has_active_run || missed_recurring_slot {
self.advance_automation_after_slot(&mut automation, &schedule, now, now)?;
continue;
}
Expand Down Expand Up @@ -3190,14 +3193,20 @@ model = "private-model"
);
}

#[test]
fn once_schedule_fires_once_and_auto_completes() {
#[tokio::test]
async fn forkguard_once_schedule_missed_while_offline_enqueues_exactly_one_run() -> Result<()> {
let tempdir = tempfile::tempdir().expect("tempdir");
let manager = AutomationManager::open(tempdir.path().to_path_buf()).expect("manager");
// Exercise a due one-shot inside the scheduler's offline-misfire
// grace. Older one-shots are intentionally skipped by the Pinvou
// no-backfill contract.
let due_at = Utc::now() - Duration::seconds(10);
let task_manager = TaskManager::start_with_executor(
automation_task_config(tempdir.path().join("tasks")),
std::sync::Arc::new(AutomationNoopExecutor),
)
.await?;
let manager = AutomationManager::open(tempdir.path().join("automations"))?;
// A one-shot missed while the process was offline has no future slot.
// It must still produce its one durable task/run instead of being
// silently advanced to `None` and paused.
let due_at =
Utc::now() - Duration::seconds(AUTOMATION_MISFIRE_GRACE_SECS.saturating_add(30));
let automation = AutomationRecord {
rrule: due_at
.format("FREQ=ONCE;AT=%Y-%m-%dT%H:%M:%S+00:00")
Expand All @@ -3210,28 +3219,35 @@ model = "private-model"
manager
.save_automation(&automation)
.expect("save automation");
let shared: SharedAutomationManager = Arc::new(Mutex::new(manager));

scheduler_tick_shared(&shared, &task_manager).await?;

let due = manager
.collect_due_runs(Utc::now())
.expect("collect due runs");
assert_eq!(due.len(), 1);
let (_automation, run) = &due[0];
assert_eq!(run.scheduled_for, due_at);
let task_id = {
let manager = shared.lock().await;
let runs = manager.list_runs(&automation.id, None)?;
assert_eq!(runs.len(), 1, "the expired one-shot must form one run");
assert_eq!(runs[0].scheduled_for, due_at);
let task_id = runs[0]
.task_id
.clone()
.expect("the run must reference its enqueued task");
let updated = manager.get_automation(&automation.id)?;
assert_eq!(updated.status, AutomationStatus::Paused);
assert_eq!(updated.next_run_at, None);
task_id
};
assert_eq!(task_manager.get_task(&task_id).await?.id, task_id);

manager
.finish_scheduled_run(run, Utc::now())
.expect("finish one-shot run");
let updated = manager
.get_automation(&automation.id)
.expect("updated automation");
assert_eq!(updated.status, AutomationStatus::Paused);
assert_eq!(updated.next_run_at, None);
assert!(
manager
.collect_due_runs(Utc::now() + Duration::hours(1))
.expect("later tick")
.is_empty()
scheduler_tick_shared(&shared, &task_manager).await?;
assert_eq!(
shared.lock().await.list_runs(&automation.id, None)?.len(),
1,
"a completed one-shot slot must not enqueue twice"
);

task_manager.shutdown();
Ok(())
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/src/compaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ duplicating work. Here is the summary produced by the other language model, use
in this summary to assist with your own analysis:";

/// Detection marker for committed compaction-summary text: the stable first
/// sentence of [`SUMMARY_HEADER`]. `engine/context.rs` restores summaries by
/// sentence of `SUMMARY_HEADER`. `engine/context.rs` restores summaries by
/// the same marker on session load.
pub const COMPACTION_SUMMARY_MARKER: &str = "Another language model started to solve this problem";
/// Marker written by pre-v0.9.6 compaction; sessions saved under the old
Expand Down
18 changes: 9 additions & 9 deletions crates/tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ pub struct ProviderCapability {
/// behind" — for example the Kimi Code membership ids, whose limits live in
/// the membership catalog rather than the static model catalogue. Unknown
/// must stay unknown: callers may **not** substitute a placeholder ceiling,
/// and in particular [`crate::route_budget`] does not clamp a requested
/// and in particular `crate::route_budget` does not clamp a requested
/// `max_tokens` against an unknown compatibility cap.
///
/// When `Some`, the value is a documented exact-route maximum or a
Expand Down Expand Up @@ -1536,7 +1536,7 @@ pub(crate) fn legacy_deepseek_alias_effort_for_route(
/// fallback** (#4188).
///
/// Preferred sources are the live Models.dev catalog and the offline bundled
/// snapshot via [`crate::provider_lake`]. Call this directly only for
/// snapshot via `crate::provider_lake`. Call this directly only for
/// Codewhale-only / local providers Models.dev does not represent, or when
/// probing the fallback table in tests. Picker, inventory, and subagent
/// surfaces must go through the provider lake.
Expand Down Expand Up @@ -2314,7 +2314,7 @@ pub struct GoalConfig {
/// Goals are unlimited by default; token/time budgets are telemetry only.
///
/// `None` uses the built-in default
/// ([`crate::goal_loop::DEFAULT_MAX_GOAL_CONTINUATIONS`], currently `0`);
/// (`crate::goal_loop::DEFAULT_MAX_GOAL_CONTINUATIONS`, currently `0`);
/// `0` disables the backstop entirely so only terminal status or user
/// control ends the run.
#[serde(default)]
Expand Down Expand Up @@ -2807,8 +2807,8 @@ pub struct AutoRouterConfig {
#[serde(default)]
pub thinking: Option<String>,
/// Classifier call timeout in seconds. Defaults to
/// [`DEFAULT_AUTO_ROUTER_TIMEOUT_SECS`] (4); `0` means "use the default".
/// Values above [`MAX_AUTO_ROUTER_TIMEOUT_SECS`] (300) are clamped so a
/// `DEFAULT_AUTO_ROUTER_TIMEOUT_SECS` (4); `0` means "use the default".
/// Values above `MAX_AUTO_ROUTER_TIMEOUT_SECS` (300) are clamped so a
/// hung local router cannot stall a turn indefinitely.
#[serde(default)]
pub timeout_secs: Option<u64>,
Expand Down Expand Up @@ -3728,7 +3728,7 @@ impl NetworkPolicyToml {
}
}

/// `[lsp]` table — mirrors [`crate::lsp::LspConfig`]. Documented in
/// `[lsp]` table — mirrors `crate::lsp::LspConfig`. Documented in
/// `config.example.toml`. When omitted, defaults from `LspConfig::default()`
/// apply (enabled, 5 s poll, 20 diagnostics/file, errors only, no overrides).
#[derive(Debug, Clone, Deserialize, Default)]
Expand Down Expand Up @@ -3757,7 +3757,7 @@ pub struct LspConfigToml {
}

impl LspConfigToml {
/// Build a runtime [`crate::lsp::LspConfig`] from the on-disk schema,
/// Build a runtime `crate::lsp::LspConfig` from the on-disk schema,
/// falling back to defaults for any unset fields.
#[must_use]
pub fn into_runtime(self) -> crate::lsp::LspConfig {
Expand Down Expand Up @@ -4757,8 +4757,8 @@ impl Config {
}

/// Classifier call timeout for `[auto.router]` in seconds. Defaults to
/// [`DEFAULT_AUTO_ROUTER_TIMEOUT_SECS`] (4); `0` means "use the default".
/// Values above [`MAX_AUTO_ROUTER_TIMEOUT_SECS`] (300) are clamped so a
/// `DEFAULT_AUTO_ROUTER_TIMEOUT_SECS` (4); `0` means "use the default".
/// Values above `MAX_AUTO_ROUTER_TIMEOUT_SECS` (300) are clamped so a
/// hung local router cannot stall a turn indefinitely.
#[must_use]
pub fn auto_router_timeout_secs(&self) -> u64 {
Expand Down
6 changes: 3 additions & 3 deletions crates/tui/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@ pub struct EngineConfig {
pub translation_enabled: bool,
pub verbosity: Option<String>,
/// Maximum number of assistant steps before stopping. Ordinary interactive
/// hosts use [`UNBOUNDED_MODEL_STEPS`]; explicit test/embed callers may
/// still install a finite boundary.
/// hosts use the finite `DEFAULT_MODEL_STEPS`; configuration and explicit
/// test/embed callers may install a different bounded value.
pub max_steps: u32,
/// Maximum number of concurrently active subagents.
pub max_subagents: usize,
Expand Down Expand Up @@ -493,7 +493,7 @@ pub struct EngineConfig {
/// Cumulative wall-clock budget for one turn (R1). Counted across every
/// model step of the turn, excluding time blocked on a human approval
/// decision. Resolved from `[tui].turn_wall_clock_secs`; always finite —
/// see [`turn_budget::resolve_turn_wall_clock`].
/// see `turn_budget::resolve_turn_wall_clock`.
pub turn_wall_clock: Duration,
/// Per-step cap on accumulated streamed content, in bytes (R1). Resolved
/// from `[tui].stream_max_content_mb`. Pre-R1 this was the hard-coded
Expand Down
4 changes: 2 additions & 2 deletions crates/tui/src/core/engine/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//! - **Never `session.last_tool_catalog`.** That value is one turn stale and
//! stores the pre-activation catalog, so it cannot describe what the *next*
//! request would send. The catalog is rebuilt through
//! [`Engine::build_turn_tool_registry_and_catalog`], which returns the same
//! `Engine::build_turn_tool_registry_and_catalog`, which returns the same
//! typed policy a real turn consumes.
//! - **Never invent a route.** For fixed routes, the host resolves the next
//! turn through the same shared planner production dispatch uses. Auto would
Expand All @@ -21,7 +21,7 @@
//! model, billing, tool budget, or body hash is recycled from the installed
//! route.
//! - **Never resolve by side effect.** The catalog build runs with
//! [`SubAgentWiring::Inert`] and [`McpAccess::PassiveSnapshot`]: no fork
//! `SubAgentWiring::Inert` and `McpAccess::PassiveSnapshot`: no fork
//! snapshot, no spawned drainer, no MCP pool creation, no `connect_all`, no
//! status events. When the connected MCP state is not already exactly what
//! a turn would use, the tool section is reported unavailable rather than
Expand Down
Loading
Loading