Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
073c512
fix: sanitize Gemini schemas and MCP notifications (fixes #754)
1jehuang Aug 3, 2026
336bf4d
fix(usage): show Anthropic model-scoped limits
1jehuang Aug 3, 2026
83860a1
style(usage): format model-scoped limits
1jehuang Aug 3, 2026
76ec0ae
fix(desktop2): hide completed todo marker glyph
1jehuang Aug 3, 2026
88a19f3
release: v0.67.1
1jehuang Aug 3, 2026
7d71370
feat(sdk): bundle platform jcode runtimes
1jehuang Aug 3, 2026
62b1d37
feat: add fast macOS release path
1jehuang Aug 3, 2026
6567e07
fix(tui): render latex in white
1jehuang Aug 3, 2026
d06b769
fix(tui): normalize dvipng math colors
1jehuang Aug 3, 2026
68300b8
feat(schema): replace per-provider deny-lists with a dialect system
1jehuang Aug 3, 2026
f03d2c7
fix(test): repair four build and test gates that were failing on master
1jehuang Aug 3, 2026
5e1804c
fix(schema): Antigravity's non-Gemini routes still speak the Gemini p…
1jehuang Aug 3, 2026
2151cee
chore: lock jcode-schema-dialect dependency
1jehuang Aug 3, 2026
e31ebaa
feat(gemini): self-heal a tool-schema rejection on the native route too
1jehuang Aug 3, 2026
1e29ed7
fix macOS notification click session routing
1jehuang Aug 4, 2026
4581aec
fix(schema): learn every keyword a 400 names, not just the first
1jehuang Aug 4, 2026
373b576
fix(openai): stop claiming strict for a schema OpenAI rejects (fixes …
1jehuang Aug 4, 2026
e1e47ad
fix(openai): fail strict closed for the remaining #711 constructs (fi…
1jehuang Aug 4, 2026
7872b45
fix(anthropic): reroute exhausted Fable quota to Opus
1jehuang Aug 4, 2026
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
31 changes: 31 additions & 0 deletions .github/workflows/publish-typescript-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ name: Publish TypeScript SDK

on:
workflow_dispatch:
inputs:
jcode_release_tag:
description: Released jcode tag whose binaries should be bundled
required: true

permissions:
contents: read
Expand Down Expand Up @@ -41,5 +45,32 @@ jobs:
- name: Validate SDK
run: npm run check

- name: Download released jcode runtimes
working-directory: ${{ github.workspace }}
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p sdk-runtime-assets
gh release download "${{ inputs.jcode_release_tag }}" --dir sdk-runtime-assets \
--pattern 'jcode-linux-x86_64.tar.gz' \
--pattern 'jcode-linux-aarch64.tar.gz' \
--pattern 'jcode-macos-x86_64.tar.gz' \
--pattern 'jcode-macos-aarch64.tar.gz' \
--pattern 'jcode-windows-x86_64.tar.gz' \
--pattern 'jcode-windows-aarch64.tar.gz'

- name: Prepare platform packages
working-directory: ${{ github.workspace }}
run: bash scripts/prepare_sdk_runtime_packages.sh sdk-runtime-assets

# These must exist before the main package is published. npm selects only
# the package matching the consumer's os/cpu from optionalDependencies.
- name: Publish platform runtime packages
working-directory: ${{ github.workspace }}
run: |
for package in sdk/npm/*; do
npm publish "$package" --access public --provenance
done

- name: Publish with npm provenance
run: npm publish --access public --provenance
19 changes: 18 additions & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jcode"
version = "0.67.0"
version = "0.67.1"
description = "Possibly the greatest coding agent ever built — blazing-fast TUI, multi-model, swarm coordination, 30+ tools"
edition = "2024"
autobins = false
Expand Down Expand Up @@ -49,6 +49,7 @@ members = [
"crates/jcode-provider-metadata",
"crates/jcode-provider-env",
"crates/jcode-provider-core",
"crates/jcode-schema-dialect",
"crates/jcode-provider-bedrock",
"crates/jcode-provider-anthropic",
"crates/jcode-provider-antigravity",
Expand Down
4 changes: 4 additions & 0 deletions changelog/index.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"entries": [
{
"version": "0.67.1",
"date": "2026-08-03"
},
{
"version": "0.67.0",
"date": "2026-08-03"
Expand Down
13 changes: 13 additions & 0 deletions changelog/v0.67.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "0.67.1",
"date": "2026-08-03",
"title": "Provider reliability fixes",
"improvements": [
"Anthropic usage now shows model-specific weekly limits alongside account-wide windows"
],
"fixes": [
"Gemini tool schemas are sanitized for provider compatibility",
"MCP notifications no longer cause request-handling failures",
"Completed desktop todo items render without a duplicate marker glyph"
]
}
1 change: 1 addition & 0 deletions crates/jcode-app-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jcode-agent-runtime = { path = "../jcode-agent-runtime" }
jcode-ambient-types = { path = "../jcode-ambient-types" }
jcode-notify-email = { path = "../jcode-notify-email" }
jcode-provider-core = { path = "../jcode-provider-core" }
jcode-schema-dialect = { path = "../jcode-schema-dialect" }
# NOTE: jcode-app-core does NOT depend on any jcode-tui-* crate. They were
# unused dead dependency edges here (the TUI declares them itself). Removing
# them stops a jcode-tui-* edit from cascading a recompile through app-core.
Expand Down
14 changes: 14 additions & 0 deletions crates/jcode-app-core/src/server/client_lifecycle_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ async fn cancel_without_local_task_still_signals_session_control() {
soft_interrupt_queue,
stop_signal.clone(),
);
// The point of this path is a turn this connection does not own (attach
// after reload, server-initiated turn). Without a registered active turn
// the cancel is a deliberate no-op, because arming the signal with nothing
// running only kills the *next* message.
let _active_turn = crate::turn_cancel_registry::register_active_turn(
"session_detached_cancel",
InterruptSignal::new(),
);
let (client_event_tx, mut client_event_rx) = mpsc::unbounded_channel::<ServerEvent>();
let swarm_members = Arc::new(RwLock::new(HashMap::new()));
let swarms_by_id = Arc::new(RwLock::new(HashMap::new()));
Expand Down Expand Up @@ -348,6 +356,12 @@ async fn deferred_cancel_reset_does_not_erase_newer_cancel() {
Arc::clone(&soft_interrupt_queue),
stop_signal.clone(),
);
// A turn owned by another connection is what makes this the signalling
// path rather than the idle no-op; see the sibling test.
let _active_turn = crate::turn_cancel_registry::register_active_turn(
"session_detached_cancel_race",
InterruptSignal::new(),
);
let (client_event_tx, _client_event_rx) = mpsc::unbounded_channel::<ServerEvent>();
let swarm_members = Arc::new(RwLock::new(HashMap::new()));
let swarms_by_id = Arc::new(RwLock::new(HashMap::new()));
Expand Down
140 changes: 140 additions & 0 deletions crates/jcode-app-core/src/tool/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1444,3 +1444,143 @@ async fn test_single_output_ceiling_is_absolute_not_only_proportional() {
);
}
}

/// Every built-in tool, normalized for every provider dialect, must be
/// sendable.
///
/// This is the guard the recurring schema-outage class never had. #446, #495,
/// #543, #655, #687, #713 and #754 were each discovered by a user whose
/// provider had gone down, then fixed by appending one keyword to one
/// provider's deny-list. Nothing checked the *other* providers for the same
/// construct, which is exactly how #754 hit Gemini through Antigravity months
/// after the same class was fixed for OpenAI.
///
/// Running the real registry through every registered dialect turns "some
/// provider is about to break" into a failing test on the commit that
/// introduces it.
#[tokio::test]
async fn tool_schemas_are_sendable_to_every_provider_dialect() {
let provider: Arc<dyn Provider> = Arc::new(MockProvider);
let registry = Registry::new(provider).await;
let defs = registry.definitions(None).await;
assert!(!defs.is_empty(), "the sweep must not pass vacuously");

let mut failures = Vec::new();
// Not per-dialect: no provider *rejects* a property that declares no type,
// but OpenAI refuses `strict` for the whole catalog over one (#713), so a
// built-in tool acquiring one would silently cost every OpenAI-route agent
// its structured-output guarantees.
for def in &defs {
for error in jcode_schema_dialect::untyped_properties(&def.input_schema) {
failures.push(format!("tool `{}` {error}", def.name));
}
}
for spec in jcode_schema_dialect::registry::ALL {
for def in &defs {
let normalized = jcode_schema_dialect::dialect::apply(&def.input_schema, spec);
for error in
jcode_schema_dialect::must_not_contain_unsupported_constructs(&normalized, spec)
{
failures.push(format!("[{}] tool `{}` {error}", spec.id, def.name));
}
// Over-stripping is the hazard an allow-list introduces: a dialect
// that forgot to list `description` would produce requests that
// succeed while silently deleting every tool's prompt text.
for error in jcode_schema_dialect::must_preserve_meaning(&def.input_schema, &normalized)
{
failures.push(format!(
"[{}] tool `{}` lost meaning: {error}",
spec.id, def.name
));
}
}
}

assert!(
failures.is_empty(),
"tool schemas are not sendable to every provider:\n{}",
failures.join("\n")
);
}

/// The sweep above must fail when a tool really does carry a construct a
/// provider rejects, otherwise it is decorative. Feeds the exact
/// `@playwright/mcp` schema from #754 through the same checker to prove the
/// detection works end to end.
#[test]
fn the_dialect_sweep_catches_the_issue_754_schema() {
let hostile = serde_json::json!({
"type": "object",
"properties": {
"data": {
"type": "object",
"additionalProperties": { "type": "string" },
"propertyNames": { "type": "string" }
}
}
});

let unnormalized = jcode_schema_dialect::must_not_contain_unsupported_constructs(
&hostile,
&jcode_schema_dialect::registry::GEMINI,
);
assert!(
unnormalized.iter().any(|e| e.message.contains("propertyNames")),
"the checker must flag the raw schema, got {unnormalized:?}"
);

let normalized = jcode_schema_dialect::dialect::apply(
&hostile,
&jcode_schema_dialect::registry::GEMINI,
);
assert!(
jcode_schema_dialect::must_not_contain_unsupported_constructs(
&normalized,
&jcode_schema_dialect::registry::GEMINI,
)
.is_empty(),
"and must pass once normalized"
);
}

/// Failing strict eligibility closed for #711/#713 must not quietly cost jcode's
/// own tools their strict mode, since that would drop the structured-output
/// guarantees on every OpenAI-route tool call with nothing to notice.
///
/// The four tools listed below were already non-strict before that change, for
/// reasons unrelated to it (`batch` declares `additionalProperties: true` so its
/// sub-call payloads stay open-world; the others carry open maps or untyped
/// action payloads). Pinning the exact set is what makes this a regression
/// detector: a fifth name appearing means a stricter rule went too far, and a
/// name disappearing means a tool became strict-eligible and the list is stale.
#[tokio::test]
async fn only_the_known_open_world_tools_are_ineligible_for_openai_strict_mode() {
/// Built-ins that legitimately cannot be strict. Verified against master
/// before the #711/#713 eligibility changes, so this is pre-existing.
const KNOWN_OPEN_WORLD_TOOLS: &[&str] = &["batch", "browser", "initiative", "swarm"];

let provider: Arc<dyn Provider> = Arc::new(MockProvider);
let registry = Registry::new(provider).await;
let defs = registry.definitions(None).await;
assert!(!defs.is_empty(), "the sweep must not pass vacuously");

let mut ineligible: Vec<String> = Vec::new();
for def in &defs {
let compatible =
jcode_provider_core::openai_schema::openai_compatible_schema(&def.input_schema);
if !jcode_provider_core::openai_schema::schema_supports_strict(&compatible) {
ineligible.push(def.name.clone());
}
}
ineligible.sort();

let expected: Vec<String> = KNOWN_OPEN_WORLD_TOOLS
.iter()
.map(ToString::to_string)
.collect();
assert_eq!(
ineligible, expected,
"the set of strict-ineligible built-in tools changed; a new name means an \
eligibility rule is too aggressive, a missing name means this list is stale"
);
}
12 changes: 6 additions & 6 deletions crates/jcode-app-core/src/tool/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ impl Tool for TodoTool {
"understands_user_intent": {
"type": "string",
"enum": ["uncertain", "partial", "clear", "complete"],
"description": "How well you understand what the user actually wants. Report uncertain or partial when guessing at intent."
"description": "How well you understand what the user wants. Report uncertain or partial when guessing."
}
}
},
Expand Down Expand Up @@ -716,16 +716,16 @@ impl Tool for TodoTool {
"autonomy": {
"type": "string",
"enum": ["requested_only", "necessary_followthrough", "proactive", "stewardship"],
"description": "How far beyond the literal request the work extended. Assess honestly from completed work and consequential adjacent follow-through."
"description": "How far beyond the literal request the work went. Assess from what was completed."
},
"iteration_maturity": {
"type": "string",
"enum": ["not_started", "exploring", "improving", "plateau_unproven", "outcome_reached", "constraints_exhausted", "plateau_confirmed", "budget_exhausted"],
"description": "How far the feedback loop has actually been exercised and the evidence-based reason, if any, that further iteration should stop. Assess the current state honestly rather than predicting a future result."
"description": "How far the feedback loop was actually exercised, and any evidence-based reason to stop iterating."
},
"stopping_evidence": {
"type": "string",
"description": "Concrete evidence supporting the reported iteration maturity when a stopping claim is made. Name relevant attempts, observations, remaining hypotheses, or an actual constraint or budget."
"description": "Evidence for the reported iteration_maturity: attempts, observations, or a real budget limit."
}
}
}
Expand Down Expand Up @@ -918,8 +918,8 @@ mod tests {
.get("description")
.and_then(Value::as_str)
.expect("alignment score should describe representation coverage");
assert!(alignment_description.contains("what the user actually wants"));
assert!(alignment_description.contains("when guessing at intent"));
assert!(alignment_description.contains("what the user wants"));
assert!(alignment_description.contains("when guessing"));
// The detailed calibration rubric moved out of the always-on schema
// into deferred turn-finish continuation messages, which are paid only
// when the completed turn needs another quality pass.
Expand Down
2 changes: 1 addition & 1 deletion crates/jcode-base/src/mcp/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ impl McpClient {
}

// Send initialized notification
let notif = JsonRpcRequest::new(0, "notifications/initialized", None);
let notif = JsonRpcNotification::new("notifications/initialized", None);
let msg = serde_json::to_string(&notif)? + "\n";
self.handle.writer_tx.send(msg).await?;

Expand Down
Loading
Loading