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
6 changes: 3 additions & 3 deletions .repository-projection.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"projection": "deixic-code",
"projectionSchemaVersion": 1,
"sourceRepository": "dx-corp/mono",
"sourceSha": "16426c3c3b3dc59f6817c9a2836fd714d17a5533",
"sourceSha": "2283b0b186fcab14fb2da2b58203f59522ccfcc0",
"destinationRepository": "dx-corp/code",
"priorProjectedBase": "5bfbbeb575801b605b42755a5a1154849c56a75a",
"priorProjectedBase": "9ee0b79f0f789db33a877ebc134d67500a04d573",
"definitionDigest": "82936441c776e3e8edb5d215a75007ec9714a233f489d460075d79d5ef5ba32f",
"toolDigest": "c244d99199a7ae3eb8ff644a99462163c23b0bb6a83ef50af01efbdca0b81d04",
"contentDigest": "42be9fd04b572f4a3cf4bb0854bcac5ae498a5d48dc99cf8d129ade8a4ab0536",
"contentDigest": "92cd365a6ecde13575d354a172d727b2d98690a8901e4091f5a39a684ed4029c",
"publicationEligible": true
}
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,42 @@ versioning when releases are cut.
and keep scheduled public runs inert so public publishing stays downstream of
the internal source-of-truth release.

## [0.10.109] - 2026-09-29

### Added

- Dispatch a Dex-approved call without parking for its own resume (#11373). <!-- maestro-release-note:fc4185e21ce8 -->

### Changed

- Add bounded gateway command load proof (#11386). <!-- maestro-release-note:4256d0e220f9 -->
- Channel-edge-slack: correlate every Slack event log line (event_id, retry_num, trace_id) (#11385). <!-- maestro-release-note:1a393b12eff5 -->
- Platform-api: add dex_turn_submit_seconds and phase histograms (#11379). <!-- maestro-release-note:6a48a876c2c3 -->
- Model-gateway: drop unbounded labels from provider latency histogram, extend buckets to 120s (#11378). <!-- maestro-release-note:209ed1efd319 -->
- Tool-executor: latency histograms for computer.start and tool calls (#11375). <!-- maestro-release-note:5e5647fd9492 -->
- Stabilize the parallel cold-provision timing check (#11393). <!-- maestro-release-note:01525800399d -->
- Platform-worker: continue the Slack trace through reply projection and delivery; slack_delivery_* logs (#11389). <!-- maestro-release-note:cb75862159a2 -->

### Fixed

- Clear two deny-level clippy lints on main (#11392). <!-- maestro-release-note:4a03d59956fa -->
- Park dex approval for human-held API keys (#11390). <!-- maestro-release-note:e38b98413144 -->
- Declare tools the history calls but the request does not offer (#11388). <!-- maestro-release-note:46d862d3e289 -->
- Route public projection off saturated PR runners (#11387). <!-- maestro-release-note:3296e7bcc5ff -->
- Repair dex-loop build and provider SSE framing (#11383). <!-- maestro-release-note:9090527a6ab7 -->
- Resident-process PUT counts as activity (#11384). <!-- maestro-release-note:11824636cccb -->
- Record sandbox audit binding production applier (#11382). <!-- maestro-release-note:6a60d2b1e2ad -->
- Execute Dex gateway commands on placement workers (#11381). <!-- maestro-release-note:25b18bbb0df4 -->
- Remove stash-conflict marker that left deixic_operating_threads_tests.rs unparseable (#11380). <!-- maestro-release-note:f6d426f86031 -->
- Preflight public mirror App access before build (#11374). <!-- maestro-release-note:d855a193f499 -->
- Own the sandbox idle policy; sandboxwich TTL is a safety net (#11376). <!-- maestro-release-note:bf679739c311 -->
- Grant delivery worker the ack-obligation table (#11377). <!-- maestro-release-note:19ed9d6d21ca -->
- Unblock Maestro release validation (#11397). <!-- maestro-release-note:0bf28330bba1 -->
- Stop saturated workers scanning queued jobs (#11396). <!-- maestro-release-note:30f2a14ae377 -->
- Don't surface React Query CancelledError as a thread load error (#11395). <!-- maestro-release-note:2c239e3178bf -->
- Log why a Gemini stream failed and the shape of a rejected history (#11394). <!-- maestro-release-note:14818c237b97 -->
- Close the learning-outbox test left open by a stash marker (#11391). <!-- maestro-release-note:9153bad05a2a -->

## [0.10.108] - 2026-09-29

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@evalops/deixic-code",
"description": "Deixic Code — native Rust coding agent, CLI, TUI, and runtime gateway",
"version": "0.10.108",
"version": "0.10.109",
"private": false,
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/maestro-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "maestro"
version = "0.10.108"
version = "0.10.109"
edition = "2021"
license = "MIT"
description = "Canonical native Rust CLI for Deixic Code"
Expand Down
59 changes: 59 additions & 0 deletions vendor/dex-loop/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
[package]
name = "dex-loop"
version = "0.1.0"
edition = "2024"
license = "UNLICENSED"
publish = false
rust-version = "1.95"

[dependencies]
futures-util = "0.3"
hex = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
thiserror = "2.0"
# Only the timer, to race the model stream against `budget.wall` in
# `Engine::model_step` and each tool call against its deadline: the crate
# still does no I/O of its own.
tokio = { version = "1.48", features = ["time"] }
tokio-util = "0.7"

[dev-dependencies]
proptest = "1.9"
rand = "0.8"
tokio = { version = "1.48", features = ["macros", "rt", "sync", "test-util", "time"] }

[lints.rust]
non_ascii_idents = "deny"
unsafe_code = "forbid"
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }
unused_lifetimes = "warn"

[lints.clippy]
await_holding_lock = "deny"
dbg_macro = "deny"
disallowed_methods = "deny"
empty_drop = "deny"
exit = "deny"
filetype_is_file = "deny"
fn_to_numeric_cast_any = "deny"
lossy_float_literal = "deny"
mem_forget = "deny"
mutex_atomic = "deny"
rc_buffer = "deny"
rest_pat_in_fully_bound_structs = "deny"
string_add = "deny"
string_lit_as_bytes = "deny"
todo = "deny"
unchecked_time_subtraction = "deny"
undocumented_unsafe_blocks = "deny"
verbose_file_reads = "deny"

# Trim debug info in local dev/test builds: full `debug = true` is the
# default and was measured at ~49% of artifact bytes. Line tables keep
# backtraces and panic locations useful. CI overrides both profiles to 0
# via CARGO_PROFILE_DEV_DEBUG / CARGO_PROFILE_TEST_DEBUG, which take
# precedence over these manifest values.

[workspace]
70 changes: 70 additions & 0 deletions vendor/dex-loop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# dex-loop

The one Dex agent loop. Web, Slack, Teams and every later surface run the same
engine over the same event log; a surface only writes ingress events and
renders the stream.

## The loop

```text
host appends UserMessage { principal, text }
ctx = rehydrate(thread, log) // warm or after a crash: same path
loop {
read control events (steer, interrupt, approval decisions, answers)
StepStarted -> stream the model -> text to the log as it arrives
ModelStepCompleted { text, calls } // commit point, before any policy
no calls? Final, return Done
per call, in the model's order:
policy (under the call's principal) -> approval? park, return Parked
read-only: join the parallel wave // results enter history in call order
mutation: Effects claim -> ToolStarted -> run -> record -> ToolFinished
}
```

`Engine::run(&mut ctx, &cancel)` returns `Done`, `Parked(approval)`,
`Asked(call)`, `Interrupted`, or `Failed`. After an approval or answer the
host appends the decision and calls `run` again; the pending calls come from
the log, not from memory.

## Ports

| Port | Job |
| --- | --- |
| `Log` | Append events (fenced by the thread lease), coalesce text into `TextDelta` rows, read control events after a cursor. |
| `Model` | Stream one response for the history and the offered tool schemas. |
| `Tools` | Catalog, `search`, `policy` (governance, grants, guardrails, guardian) and `run`. |
| `Effects` | The durable ledger for mutations: claim before dispatch, record after. A mutation is dispatched at most once per call id; an unknown outcome is never replayed. |
| `Sanitizer` | Customer-safe text: replaces tool names and internal names before any delta is written. `Lexicon` is the built-in implementation. |
| `Compactor` | Optional. `Threshold` summarizes old history when it grows past a size. |

The engine offers `tools.search` plus core tools on every step; tools that
search matches are exposed from the next step and recorded as `ToolsExposed`.

## Events

Ingress (hosts write): `UserMessage`, `Steer`, `Interrupt`, `ApprovalDecided`,
`Answer`, `ToolProgress`. Each ingress event carries its principal.

Engine: `StepStarted`, `TextDelta`, `Usage`, `ModelStepCompleted`,
`ModelAttemptAbandoned`, `ToolStarted`, `ToolsExposed`, `ToolFinished`
(`succeeded | failed | running | unknown`), `ApprovalRequested`, `Question`,
`Compaction`, `Final`, `Error`, `Interrupted`.

Interrupt cancels the model stream and running reads; a mutation that has
started completes, and the turn stops before the next effect.

## What this crate will never contain

- Surface logic: no Slack, Teams, web or renderer code, and no per-surface
behavior in the loop.
- Coding concepts: no coding task kinds, validators or workflows. Coding is
tools run through this same loop.
- Provider or service clients: no HTTP, SQL, model SDKs or `tokio::spawn`.
Hosts implement the ports.

## Checks

```bash
cargo test --manifest-path rust/Cargo.toml -p dex-loop
cargo clippy --manifest-path rust/Cargo.toml -p dex-loop --all-targets -- -D warnings
```
123 changes: 123 additions & 0 deletions vendor/dex-loop/src/budget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! Per-turn limits.

use std::fmt;
use std::time::Duration;

use crate::event::Usage;

/// Limits for one turn. The engine checks them before every model call.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Budget {
/// Model calls per turn that may offer tools. Once a turn has made this
/// many calls the engine makes one more, with no tools offered, so the
/// model writes its answer from what it has. If that call still asks for
/// a tool, the turn fails with `BudgetExhausted`.
pub max_steps: u32,
/// Input plus output tokens per turn.
pub max_tokens: u64,
pub max_cost_micros: u64,
/// Time spent inside one `Engine::run` call. Time parked on an approval
/// or a question does not count.
pub wall: Duration,
}

impl Default for Budget {
/// 60 steps and 25 minutes. Token and cost caps come from org policy, so
/// the default leaves them open.
fn default() -> Self {
Self {
max_steps: 60,
max_tokens: u64::MAX,
max_cost_micros: u64::MAX,
wall: Duration::from_secs(25 * 60),
}
}
}

/// The limit a turn ran out of.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BudgetAxis {
Steps,
Tokens,
Cost,
Wall,
}

impl fmt::Display for BudgetAxis {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
BudgetAxis::Steps => "steps",
BudgetAxis::Tokens => "tokens",
BudgetAxis::Cost => "cost",
BudgetAxis::Wall => "wall",
})
}
}

impl Budget {
/// Whether the next model call is the answer-only call: `steps` calls
/// have already been made and the tool-offering allowance is spent.
pub fn answer_only(&self, steps: u32) -> bool {
steps >= self.max_steps
}

/// The first exhausted axis, if any. `steps` counts model calls made so
/// far. Steps are exhausted only after the answer-only call
/// (`max_steps + 1`), which `answer_only` allows for. The other axes are
/// exhausted as soon as they are reached, answer-only call included.
pub fn exhausted(&self, steps: u32, usage: Usage, elapsed: Duration) -> Option<BudgetAxis> {
if steps > self.max_steps {
Some(BudgetAxis::Steps)
} else if usage.tokens() >= self.max_tokens {
Some(BudgetAxis::Tokens)
} else if usage.cost_micros >= self.max_cost_micros {
Some(BudgetAxis::Cost)
} else if elapsed >= self.wall {
Some(BudgetAxis::Wall)
} else {
None
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn each_axis_is_reported() {
let budget = Budget {
max_steps: 2,
max_tokens: 100,
max_cost_micros: 50,
wall: Duration::from_secs(1),
};
let usage = |tokens, cost| Usage {
input_tokens: tokens,
output_tokens: 0,
cost_micros: cost,
};
let short = Duration::from_millis(1);
assert_eq!(budget.exhausted(1, usage(10, 1), short), None);
// The answer-only call after `max_steps` is still allowed.
assert_eq!(budget.exhausted(2, usage(10, 1), short), None);
assert!(!budget.answer_only(1));
assert!(budget.answer_only(2));
assert_eq!(
budget.exhausted(3, usage(10, 1), short),
Some(BudgetAxis::Steps)
);
assert_eq!(
budget.exhausted(1, usage(100, 1), short),
Some(BudgetAxis::Tokens)
);
assert_eq!(
budget.exhausted(1, usage(10, 50), short),
Some(BudgetAxis::Cost)
);
assert_eq!(
budget.exhausted(1, usage(10, 1), Duration::from_secs(1)),
Some(BudgetAxis::Wall)
);
}
}
Loading
Loading